Last Update: Sep 04, 2024 | Published: Apr 10, 2017
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.
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.
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.
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)})
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.
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
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.
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.
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.
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.
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."} } }
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.
I will change this to a universal security group with my function and verify.
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.