Active Directory|PowerShell

Managing Active Directory Groups with ADSI and PowerShell

Ready for some more PowerShell and ADSI fun? In the last article, I showed you how to create an Active Directory (AD) user account with ADSI and PowerShell. Of course, you probably want to put that user into a group or two. In fact, you might even like to manage groups with PowerShell. Let’s see how much we can cover today. As is usual with any series I do, I am assuming that you are caught up on the previous articles.

 

 

Sponsored Content

What is “Inside Microsoft Teams”?

“Inside Microsoft Teams” is a webcast series, now in Season 4 for IT pros hosted by Microsoft Product Manager, Stephen Rose. Stephen & his guests comprised of customers, partners, and real-world experts share best practices of planning, deploying, adopting, managing, and securing Teams. You can watch any episode at your convenience, find resources, blogs, reviews of accessories certified for Teams, bonus clips, and information regarding upcoming live broadcasts. Our next episode, “Polaris Inc., and Microsoft Teams- Reinventing how we work and play” will be airing on Oct. 28th from 10-11am PST.

We will begin with a new user account.

[ADSI]$ken = "LDAP://CN=Ken Dew,OU=IT,OU=Departments,OU=Employees,DC=Globomantics,DC=local"

The MemberOf property, which will show groups that Ken belongs to is empty. The automatic Domain Users group is never shown.

Empty MemberOf property (Image Credit: Jeff Hicks)
Empty MemberOf Property (Image Credit: Jeff Hicks)

In AD, group membership is stored as a link in the group object. I want to add Ken to the Chicago IT group. I will need to get that object with ADSI.

[ADSI]$group = "LDAP://CN=Chicago IT,OU=Groups,OU=Employees,DC=Globomantics,DC=local"

The Member property shows the distinguishedname of each member.

Group members (Image Credit: Jeff Hicks)
Group Members (Image Credit: Jeff Hicks)

To add Ken, I will invoke the Add() method on the group object and pass in the user’s AD path.

$group.Add($ken.ADSPath)

Refreshing the caches on the objects and re-checking membership shows that it was successful.

Adding a group member with PowerShell (Image Credit: Jeff Hicks)
Adding a Group Member with PowerShell (Image Credit: Jeff Hicks)

Removing a member is just about the same. The difference is to simply use the Remove() method.

$group.remove($ken.ADSPath)

These methods are immediate and do not require running SetInfo().

I removed Ken because I wanted to show you an ADSI alternative. Using LDAP is nice but you need to know the exact path to the object. I will cover searching AD in another article. You can also use the WinNT moniker, which treats objects in a flatter fashion. The concepts and commands are similar.

[ADSI]$group = "WinNT://globomantics/Chicago IT,group"
$group.Add("WinNT://globomantics/kdew,user")

Verifying the group membership with WinNT is a bit trickier.
$group.Invoke("Members").Foreach({$_.Gettype().InvokeMember("ADSPath","GetProperty",$null,$_,$null,$null)})

Group Members with WinNT (Image Credit: Jeff Hicks)
Group Members with WinNT (Image Credit: Jeff Hicks)

If you are building a PowerShell tool, there is nothing preventing you from using both LDAP and WinNT in the same command. Use whatever you find easiest.

Managing group membership is pretty straightforward from either the user or group perspective. One common task is to recursively get a group’s members. I have one such group.

Listing group members with ADSI (Image Credit: Jeff Hicks)
Listing Group Members with ADSI (Image Credit: Jeff Hicks)

I could get some basic information like this:

$group.member | foreach {
[ADSI]$m = "LDAP://$_"
$props = 'ADSPath','DistinguishedName','Name','Description','SchemaClassname'
$h = [ordered]@{}
foreach ($item in $props) {
if ($m.$item -is [System.Collections.CollectionBase]) {
$h.Add($item,$m.$item.value)
}
else {
$h.Add($item,$m.$item)
}

}
[pscustomobject]$h
} | format-list

Enumerating group members with PowerShell (Image Credit: Jeff Hicks)
Enumerating Group Members with PowerShell (Image Credit: Jeff Hicks)

Clearly, I need to do the same thing for each nested group. The easiest approach is to create a function that calls itself.

Function Get-GroupMember {
[cmdletbinding()]
Param([string]$ADSPath)

[ADSI]$group = $ADSPath

$group.member | foreach {
[ADSI]$child = "LDAP://$_"
if ($child.SchemaClassName -eq "group") {
Get-GroupMember $child.ADSPath
}
else {
$child | Select ADSPath,SchemaClassname
}
}

}

The end result is a list of all non-group accounts because you could have a group with users or computers.

Enumerating nested group membership (Image Credit: Jeff Hicks)
Enumerating Nested Group Membership (Image Credit: Jeff Hicks)

Before we go, I suppose we should look at how to create a group.

Like a single-user, you first need the parent container or OU. Then, you can create a new group object that specifies the canonical name.

[ADSI]$OU = "LDAP://OU=Groups,OU=Employees,DC=globomantics,DC=local"
$new = $ou.create("group","CN=ProjectX")
$new.put("sAMAccountName", "ProjectX")
$new.setinfo()
$new.refreshcache()

By default, this will create a global security group.

A New Group (Image Credit: Jeff Hicks)

If you want to modify the type and/or scope, you will have to use the GroupType value. You will also have to do some bitwise operations. We will need some values:

New-Variable ADS_GROUP_TYPE_SECURITY_ENABLED 0x80000000 -option constant

With this, I can test if the group is a security group or not.

Testing for a Security Group (Image Credit: Jeff Hicks)

The GroupType value alone is not very meaningful. To change this to a distribution group, calculate a new value with -bxor operator for GroupType.

$v = $new.groupType.value -bxor $ADS_GROUP_TYPE_SECURITY_ENABLED
$new.put("grouptype",$v)
$new.Setinfo()

Re-testing confirms the change.

Changing group to a distribution list (Image Credit: Jeff Hicks)
Changing Group to a Distribution List (Image Credit: Jeff Hicks)

I will re-run the same commands to toggle it back to a security group.

Likewise, the group scope can be determined with a -band operation. I wrote a simple function to get the current scope.

Function Get-GroupScope {
[cmdletbinding()]
Param([object]$Value)

New-Variable ADS_GROUP_GLOBAL -Value 2 -Option Constant
New-Variable ADS_GROUP_DOMAINLOCAL -Value 4 -Option Constant
New-Variable ADS_GROUP_UNIVERSAL -Value 8 -Option Constant

Write-Verbose "Evaluating $value"
Switch ($value) {
{($_ -band $ADS_GROUP_DOMAINLOCAL) -as [boolean] } { "DomainLocal" }
{($_ -band $ADS_GROUP_GLOBAL) -as [boolean] } { "Global"}
{($_ -band $ADS_GROUP_UNIVERSAL) -as [boolean] } { "Universal"}Default { Write-Warning "Unable to determine group scope."}
}
}

Getting group scope (Image Credit: Jeff Hicks)
Getting Group Scope (Image Credit: Jeff Hicks)

To modify the group scope, you can assign one of the constants as the new grouptype. You also need to indicate whether the group is security enabled or not. The tricky part is that you cannot change both the scope and type with the same command. If you do, you will most likely get an error about the server refusing the request. This function should simplify the process.

Function Set-GroupScope {
[cmdletbinding()]
Param(
[Parameter(Position = 0,Mandatory,ValueFromPipeline)]
[ValidateNotNullorEmpty()]
#an ADSI Group object
[System.DirectoryServices.DirectoryEntry]$Group,
[ValidateSet("DomainLocal","Global","Universal")]
[string]$Scope = "Global",
[switch]$AsDistribution
)

Begin {
  New-Variable ADS_GROUP_GLOBAL -Value 2 -Option Constant
  New-Variable ADS_GROUP_DOMAINLOCAL -Value 4 -Option Constant
  New-Variable ADS_GROUP_UNIVERSAL -Value 8 -Option Constant
  New-Variable ADS_GROUP_TYPE_SECURITY_ENABLED 0x80000000 -option constant
} #begin

Process {

  Write-Verbose "Setting group scope for $($group.name)"

  $value = $group.groupType.value
  Write-Verbose "Evaluating $value"

  Switch ($Scope) {
    #create a temporary value
    "DomainLocal" {
        $tmp = $ADS_GROUP_DOMAINLOCAL
    }
    "Global" {
        $tmp = $ADS_GROUP_GLOBAL
    }
    "Universal" {
        $tmp = $ADS_GROUP_UNIVERSAL
    }
  }

  Write-Verbose "Setting value to $tmp"
  $group.put("grouptype",$tmp)
  $group.SetInfo()

  #Add Security setting unless specified AsDistribution
  if ($AsDistribution) {
     #no other change needed
     Write-Verbose "Group is now a distribution list"
  }
  else {
     Write-Verbose "Re-enabling as a security group"
     $group.RefreshCache()
     $group.put("grouptype",($group.groupType.value -bxor $ADS_GROUP_TYPE_SECURITY_ENABLED))
     $group.SetInfo()
  }
} #process

End {
   #not used
}

}

To use this function, you will need an ADSI object for the group. I have been using $new. Right now, this is a global security group.

Getting current group values (Image Credit: Jeff Hicks)
Getting Current Group Values (Image Credit: Jeff Hicks)

I will change this to a universal security group with my function and verify.

Modifying the group scope with PowerShell (Image Credit: Jeff Hicks)
Modifying the Group Scope with PowerShell (Image Credit: Jeff Hicks)

As with all of the code I provide here, do not use in a production setting until you have safely tested it. You will want to understand how it works. I have one more concept to cover with ADSI and PowerShell. We will look at that next time.

Related Topics:

BECOME A PETRI MEMBER:

Don't have a login but want to join the conversation? Sign up for a Petri Account

Register
Comments (0)

Leave a Reply

External Sharing and Guest User Access in Microsoft 365 and Teams

This eBook will dive into policy considerations you need to make when creating and managing guest user access to your Teams network, as well as the different layers of guest access and the common challenges that accompany a more complicated Microsoft 365 infrastructure.

You will learn:

  • Who should be allowed to be invited as a guest?
  • What type of guests should be able to access files in SharePoint and OneDrive?
  • How should guests be offboarded?
  • How should you determine who has access to sensitive information in your environment?

Sponsored by: