- Export and import to and from Excel with the PowerShell module ImportExcel - Thu, Apr 21 2022
- Getting started with the PSReadLine module for PowerShell - Thu, Feb 24 2022
- SecretsManagement module for PowerShell: Save passwords in PowerShell - Tue, Dec 22 2020
The problem with nested groups
Active Directory supports the concept of "nesting" groups inside one another. For example, consider two groups: GroupA and GroupB. GroupB can be a member of GroupA. If I assign GroupA write permissions to Folder1, then the members of GroupB also have write access to Folder1. Nesting groups inside each other can be a powerful way to assign access dynamically. There is no limit to the amount of nesting in Active Directory. For example, this scenario is valid (but not recommended):
- GroupE is a member of GroupD
- GroupD is a member of GroupC
- GroupC is a member of GroupB
- GroupB is a member of GroupA
That scenario can be challenging to unwind and report on. A circular reference can occur if an administrator is not careful. This occurs when groups are nested inside each other, creating an endless loop.
- GroupC is a member of GroupB
- GroupB is a member of GroupA
- GroupA is a member of GroupC
Get-ADGroupMember basics
Microsoft created the Get-ADGroupMember cmdlet to return lists of group members. For everyday tasks, it works as advertised. If I want to see the members of a group called ParentGroup05, the syntax is straightforward.
PS C:\> Get-ADGroupMember ParentGroup05 | select Name, objectclass
The results show that there are three group members and a nested group named NestedGroup05.
The cmdlet also supports recursive lookups, which return users from all nested groups. Here, we can see that it shows six users: three in the parent group and three in the nested group.
PS C:\> Get-ADGroupMember ParentGroup05 -recursive | select Name, objectclass
But if you look closely, we have a potential problem. The cmdlet so far has shown the number of users OR a list of users and groups. If we perform a recursive search, it returns the data in one format (a flat list of users without group membership info). If we perform a non-recursive search, it returns the data in another format (a list of parent group users and a list of nested groups).
- What if I need to know how many users are in each nested group?
- What if I need to know which members belong to which group?
Get-ADGroup
In the examples above, there were criteria that could be useful: the presence of nested groups, the number of users in a nested group, and the concept of recursion or finding groups within groups. Get-ADGroupMember has trouble returning this information. However, Get-ADGroup can return the information we're looking for.
You may wonder how it is possible that Get-ADGroup can return group member information. Remember that when looking at group information in Active Directory Users and Computers, you can see MemberOf information. This means that there is a relationship between the group and the group members that Active Directory tracks. If we look at the same information with PowerShell, we can see that Get-ADGroup knows about the group member information.
Why is this important? There are two important aspects here that make this significant. We said earlier that Get-ADGroupMember isn't returning the data we want, so we need to look elsewhere. Get-ADGroup offers an alternate way to get the data we are looking for, and we can use PowerShell to access the data. If it's available via PowerShell, then we should be able to grab the data and format it as we wish.
Get-NestedGroup
Get-ADGroup can access the required information, but to do so requires using LDAP filters. If you have any experience using LDAP filters, then you know that their syntax can be challenging for users to understand. We can access most of the information required using one line of code:
Get-ADGroup -LDAPFilter "(&(objectCategory=group)(memberof=$($ADGrp.DistinguishedName)))"
This line instructs Active Directory to return only the groups that are members of $ADGrp. I saved the output to a variable and then the rest of the information was available by grabbing various properties. From there, I built a function that allows me to pass in a group (or multiple groups) to query and then format the results as I wish.
Here's what the output of my function Get-NestedGroup looks like when I query a group called ParentGroup05. The group has one nested group inside it, which has three members.
Get-NestedGroup ParentGroup05
I designed the tool to look for nested group members two levels deep. That means that if we search for nested groups in ParentGroup01, then it will return any nested groups and then check those nested groups for nested groups.
Get-NestedGroup ParentGroup01 | Format-Table
The group ParentGroup01 was searched and returned two groups: NestedGroup01 and NestedGroup02.
The tool then checked those two groups and found one more nested group named LargeGroup3000. Each group that is found also displays the number of group members and some basic group information.
The Get-NestedGroup tool can be used with a Server parameter. This gives the ability to query results against a specific DC (or a dc in a different domain). If no server is input, the tool finds a DC in the same site that the query is run from. This guarantees that searches remain fast.
The tool can query multiple parent groups at a time like this:
Get-NestedGroup Parentgroup01, Parentgroup02
I prefer the Format-Table output for more complex lookups, but the default view can be useful.
One thing I haven't mentioned yet is how fast this tool is! It returns results in about 15 milliseconds. If a similar recursive group lookup is done with Get-ADGroupMember, the results take anywhere from 5 to 20 seconds to complete.
I built this tool to help me get around the limits of Get-ADGroupMember when working with large groups. I needed a solution to pull out the nested groups from parent groups and give me the relevant information about those nested groups. Keep an eye out for my sister tool for grabbing the nested user information from parent groups.
The latest version of this tool is always available in my PowerShell GitHub Repo.
Subscribe to 4sysops newsletter!
function Get-NestedGroup { <# .SYNOPSIS Gets a list of nested groups inside an Active Directory group .DESCRIPTION Gets a list of nested groups inside an Active Directory group using LDAPFilter. Checks for two levels of nested groups from the parent group. .PARAMETER Group The name of an Active Directory group .PARAMETER Server The name of Domain controller to use for query. Valid entries are a server name or servername:3268 for a Global Catalog query. .EXAMPLE PS C:\> get-nestedgroup "Server Admins" ParentGroup : Server Admins NestedGroup : NYC Server Admins NestedGroupMemberCount : 8 ObjectClass : group ObjectPath : contoso.com/Groups/NYC Server Admins DistinguishedName : CN=NYC Server Admins,OU=Groups,DC=contoso,DC=com Returns the nested groups that are inside the group named "Server Admins". NOTE: NestedGroupMemberCount is the number of objects (aka members) inside the nested group. In this example, "NYC Server Admins" contains 8 objects. This number IS NOT the number of nested groups inside NYC Server Admins. .EXAMPLE PS C:\> $selectprops = "ParentGroup","NestedGroup","NestedGroupMemberCount" PS C:\> Get-NestedGroup "Exchange Recipient Administrators" | Select-Object $selectprops | format-table ParentGroup NestedGroup NestedGroupMemberCount ----------- ----------- ---------------------- Exchange Recipient Administrators Exchange Organization Administrators 5 Exchange Recipient Administrators Global Service Desk 117 Exchange Recipient Administrators Mail Admins 1 Returns the nested groups in a table format. Uses a variable to specify the parameters for Select-Object .EXAMPLE PS C:\> Get-NestGroup $NYCGrps | Format-Table There are no nested groups inside NYC-Desktops There are no nested groups inside NYC-Servers There are no nested groups inside NYC-Laptops There are no nested groups inside NYC-Admins There are no nested groups inside NYC-HelpDesk Checks the six groups saved in the variable $NYCGrps for nested groups. In this example, none of six groups have any nested groups. .INPUTS Inputs (if any) .OUTPUTS Output (if any) .NOTES AUTHOR: Mike Kanakos VERSION: 1.0.4 DateCreated: 2020-04-15 DateUpdated: 2019-07-28 #> [CmdletBinding()] param ( [Parameter(ValueFromPipelineByPropertyName, Mandatory = $True)] [String[]]$Group, [Parameter()] [String]$Server = (Get-ADReplicationsite | Get-ADDomainController -SiteName $_.name -Discover -ErrorAction SilentlyContinue).name ) begin { } process { foreach ($item in $Group) { $ADGrp = Get-ADGroup -Identity $item -Server $Server $QueryResult = Get-ADGroup -LDAPFilter "(&(objectCategory=group)(memberof=$($ADGrp.DistinguishedName)))" -Properties canonicalname -Server $Server if ( $null -ne $QueryResult) { foreach ($grp in $QueryResult) { $GrpLookup = Get-ADGroup -Identity "$($Grp.DistinguishedName)" -Properties Members, CanonicalName -Server $Server $NestedGroupInfo = [PSCustomObject]@{ 'ParentGroup' = $item 'NestedGroup' = $Grp.Name 'NestedGroupMemberCount' = $GrpLookup.Members.count 'ObjectClass' = $Grp.ObjectClass 'ObjectPath' = $GrpLookup.CanonicalName 'DistinguishedName' = $GrpLookup.DistinguishedName } #end PSCustomObject $NestedGroupInfo } #end of foreach inside if statement } else { Write-Information "There are no nested groups inside $item" -InformationAction Continue } #end if/else # checking for groups of nested groups foreach ($NestedGrp in $QueryResult) { $NestedADGrp = Get-ADGroup -Identity $NestedGrp -Server $Server $NestedQueryResult = Get-ADGroup -LDAPFilter "(&(objectCategory=group)(memberof=$($NestedADGrp.DistinguishedName)))" -Properties canonicalname -Server $Server If ($null -ne $NestedQueryResult) { foreach ($SubGrp in $NestedQueryResult) { $SubGrpLookup = Get-ADGroup -Identity "$($SubGrp.DistinguishedName)" -Properties Members, CanonicalName -Server $Server } $SubNestedGroupInfo = [PSCustomObject]@{ 'ParentGroup' = $NestedADGrp.Name 'NestedGroup' = $SubGrp.Name 'NestedGroupMemberCount' = $SubGrpLookup.Members.count 'ObjectClass' = $SubGrp.ObjectClass 'ObjectPath' = $SubGrpLookup.CanonicalName 'DistinguishedName' = $SubGrpLookup.DistinguishedName } #end PSCustomObject $SubNestedGroupInfo } } } #end parent foreach } #end process block end {} }#end function
I believe there may be a slight error in your code. I entered an issue in GitHub for you to look at if you like.
Hi Mike,
I saw the github issue you opened. Thank you for reaching out. I am going to paste my response I left on github here as well.
so let's go through the question together…. for those unfamilar with the issue Mike raised. you can look here:
https://github.com/compwiz32/PowerShell/issues/7
I am sorry the line numbers dont line up, but i copied lines 110-125 here….
this IF loop is only used if there is a nested group found in the earlier lookup…
… and then it goes back through the foreach if there are more nestedgroups in $nestedqueryresult
if I move object inside the foreach, then the next time the loop runs, it will overwrite the first result (if there are more than one).
PowerShell keeps track of the pscustomobject for me. When I come through the second time, it adds to the existing results on output. It took me a long time to understand that logic, but that's Powershell stepping in and doing the work for me … the next run of the loop ADDS to $SubNestedGroupInfo – it doesn’t overwrite it.
does that make sense?
Hi Mike,
I love the script. I have a question and i hope this isnt too difficult. I would like it to also spit out the indivitual members of the nested groups.
i.e.
If i run
$selectprops = “ParentGroup”,”NestedGroup”,”NestedGroupMemberCount”
Get-NestedGroup “Administrators” | Select-Object $selectprops | format-table
i would like to get ParentGroup, Nested Group, NestedGroupMemberCount, and Nestedgroup members.
Is this possible?