Recently I needed to reduce the licenses we were consuming with a particular cloud service. I was told that a couple of Active Directory groups served as the service’s ACL. All the accounts in question were disabled but the disabled status didn’t synchronize to the cloud service provider. I needed to remove all disabled user accounts from the ACLs in our AD. My first thought was, “I love it when I get the easy ones”. That should always be a sign that something isn’t going to be easy, LOL.

I immediately opened PowerShell, imported the AD Module and ran a recursive Get-ADGroupMemeber query on the two groups I was told about. I added all the returned objects to an array and then populated that array with the disabled user account’s SamAccountNames. I made another array and added the groups/sub-groups to it. Finally, I used nested foreach loops to remove each user from each of my two AD groups.


Import-Module ActiveDirectory

$allusers = @()
$group1 = Get-ADGroupMember -Identity "Name of Group 1" -Recursive
$group2 = Get-ADGroupMember -Identity "Name of Group 2" -Recursive
$allusers += $group1.SamAccountName
$allusers += $group2.SamAccountName

$users2remove = $allusers | % {Get-ADUser $_ |Select samaccountname, DistinguishedName, enabled|where enabled -ne "true"}

$removes = @()
$removes += $users2remove.samaccountname

$subgroups = @()
$subgroups1 = Get-ADGroupMember -Identity "Name of Parent Group1" |Where {$_.ObjectClass -eq "Group"}
$subgroups2 = Get-ADGroupMember -Identity "Name of Parent Group2"| Where {$_.ObjectClass -eq "Group"}

Foreach ($group in $subgroups) {
     Foreach ($remove in $removes) {
     Remove-ADGroupMember -Identity $group -Members $remove -Confirm:$false
     Write-Host "Disabled Account $remove removed from $group"
     }
}

I ran my script, watched the output, marveled at my own genius, and sent my boss a list of the users I had removed.  An hour or two later my manager pinged me to say that when he looks in the application’s portal he sees that most of the disabled users are gone but there are a couple hundred that still show up as having a license. Hmmmmm.

I reviewed my logic and found the problem. I didn’t do a recursive search on the sub-groups. I opened ADUC and looked, sure enough, there were a ton of nested sub-groups under the two I was told about and some of the users appeared to be in multiple groups. So even though I had removed them from the top-level they were still consuming a license through their membership in a nested group.

“Easy enough to fix”, my internal dialog told me. I just added the -recursive parameter to my sub-group searches and ran my script again.  Well, that didn’t work. After some trial and error, I was able to determine that the recursive parameter of the cmdlet does find all of the user objects no matter how deep they are. However, it only returns the first layer of group objects. Now what?

I needed my code to say, “if a group is returned run the search again” in a loop until no more groups were returned. I was disappointed that none of my Internet searches turned up working code. I ended up writing the function below. If you have landed on this page from similar circumstances, I hope it helps you out of your jam.


## Get-ADNestedGroups
## Author: Kevin Trent, https://whatdouknow.com, kevin-trent@hotmail.com
## Distribute freely but please leave author's information attached

function Get-ADNestedGroups {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[string]$Group
)

## Find all parent group objects
$members = Get-ADGroupMember -Identity $Group

## Array to hold each child group
$ChildGroups = @()

## Foreach loop to find all objects in child groups
## If any objects are groups we call the function again
## Will loop until no more groups are found
## Displays each group name on screen and adds to childgroups array

     Foreach ($member in $members){
          If ($member.objectClass -eq "Group"){
          $ChildGroups += $member.name|where {$_ -ne $null}
          Write-Host $member.name|where {$_ -ne $null}
          Get-ADNestedGroups -Group $member
          }
      }
#Outputs the contents of the childgroups array to text file
$ChildGroups|out-file $env:userprofile\documents\SubGroups.txt
}

P.S.

If you are trying to run the first script I started my story with; replace the subgroup arrary and variables with the function. Then change the subgroups variable in the nested foreach to childgroups.

Advertisements