Managing Active Directory Groups with ADSI and PowerShell

PowerShell Text Purple hero
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.
 

 
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.

032717 1617 ManagingADG8
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.

032717 1617 ManagingADG9
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.