razumny
razumny

Reputation: 127

Powershell: Find all nested groups

Using get-adgroupmember to find the immediate members of a group is well-known. Likewise, adding the -recursive parameter is a well-known way to find all users that are members of a group. Is there a way to find all groups that are members of a group?

Background: One of my customers have had a nasty habit of using nested groups for access management. The worst case sees six levels of nested groups, each level having a significant number of groups as members.

Upvotes: 1

Views: 4260

Answers (2)

Santiago Squarzon
Santiago Squarzon

Reputation: 59820

Another take to Theo's helpful answer using a Stack<T> instead of recursion.

Note, this function will output unique objects, i.e.: if a user was a member of more than one nested group, said user will be only outputted once (same applies for any ObjectClass).

using namespace System.Collections.Generic
using namespace Microsoft.ActiveDirectory.Management

function Get-ADGroupMemberRecursive {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('DistinguishedName')]
        [string] $Identity,

        [Parameter()]
        [string[]] $Properties,

        [Parameter()]
        [string] $Server
    )

    begin {
        class IdentityHelper {
            hidden [hashtable] $domainmap = @{}
            hidden [hashtable] $cache = @{}

            [void] Add([object] $object) {
                $this.cache.Add($object.distinguishedName, $object)
            }

            [object] Get([string] $distinguishedName, [hashtable] $properties) {
                if ($this.cache.ContainsKey($distinguishedName)) {
                    return $this.cache[$distinguishedName]
                }

                $obj = Get-ADObject $distinguishedName @properties
                $this.Add($obj)
                return $obj
            }

            [object] TryGet([string] $distinguishedName, [hashtable] $properties) {
                $domain = $distinguishedName -replace '^.+?(?=DC=)'

                if (-not $this.domainmap.ContainsKey($domain)) {
                    $this.domainmap[$domain] = [adsi]::new('LDAP://' + $domain).InvokeGet('dc')
                }

                $obj = Get-ADObject $distinguishedName @properties -Server $this.domainmap[$domain]
                $this.Add($obj)
                return $obj
            }
        }

        $adObjParams = @{ Properties = @('Member') }

        if ($PSBoundParameters.ContainsKey('Properties')) {
            $adObjParams['Properties'] += $Properties | Sort-Object -Unique
        }

        if ($PSBoundParameters.ContainsKey('Server')) {
            $adObjParams['Server'] = $Server
        }

        $hash = [HashSet[guid]]::new()
        $stack = [Stack[ADObject]]::new()
        $helper = [IdentityHelper]::new()
    }

    process {
        try {
            $group = Get-ADGroup $Identity @adObjParams
        }
        catch {
            $PSCmdlet.WriteError($_)
            return
        }

        $stack.Push($group)
        while ($stack.Count) {
            $group = $stack.Pop()

            foreach ($member in $group.member) {
                try {
                    $obj = $helper.Get($member, $adObjParams)
                }
                catch {
                    # member might be in a different domain
                    $e = $_

                    try {
                        # attempt to query the identity in this domain
                        $helper.TryGet($member, $adObjParams)
                    }
                    catch {
                        # if failed, fallback
                        $PSCmdlet.WriteError($e)
                        continue
                    }
                }

                $obj

                if ($obj.objectClass -eq 'group' -and $hash.Add($obj.objectGuid)) {
                    $stack.Push($obj)
                }
            }
        }

        $hash.Clear()
    }
}

Usage is fairly straight forward:

# positional binding
Get-ADGroupMemberRecursive groupNameHere

# or through pipeline
Get-ADGroup groupNameHere | Get-ADGroupMemberRecursive

Upvotes: 2

Theo
Theo

Reputation: 61028

Revised answer

Unfortunately, using Get-ADGroupMember together with switch -Recursive will not return members that are groups. As the docs state:

If the Recursive parameter is specified, the cmdlet gets all members in the hierarchy of the group that do not contain child objects.

To get an array of nested group objects within a certain parent group, you will need a recursive function like below (untested):

function Get-NestedADGroup {
    Param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [Alias('Group')]
        # any of Name, DistinguishedName, objectGUID, objectSid, sAMAccountName
        [string]$Identity,

        # optional parameters
        [string[]]$Properties = @(),
        [string]$Server = $null,

        [Parameter(Mandatory = $false)]
        # This allows you to pass in a username as a string and have an interactive prompt for the password
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty
    )

    # Get-ADGroup by default returns properties DistinguishedName, GroupCategory, GroupScope, Name, ObjectClass, 
    # ObjectGUID, SamAccountName, SID. This function always adds property Members to that.
    # (PowerShell property name: Members; LDAP attribute name: member)
    $params = @{
        Identity   = $Identity
        Properties = @($Properties) + 'Members' | Sort-Object -Unique  
    }  
    if (![string]::IsNullOrWhiteSpace($Server)) { $params['Server'] = $Server }
    if (($Credential) -and $Credential -ne [System.Management.Automation.PSCredential]::Empty) {
        $params['Credential'] = $Credential
    }

    # try and get the group object from AD
    $adGroup = Get-ADGroup @params -ErrorAction SilentlyContinue
    if ($adGroup) {
        if (-not $script:groupsHash.ContainsKey($adGroup.DistinguishedName)) {
            # output this group object
            $adGroup
            # add the DistinguishedName of this group to the hash to prevent circular references
            $script:groupsHash[$adGroup.DistinguishedName] = $true
            # loop through the groups Members collection (all DistinguishedNames)
            foreach ($member in $adGroup.Members){
                $params['Identity'] = $member  # reuse the params Hashtable
                $params.Remove('Properties')   # remove Properties from the Hashtable for Get-ADObject
                $memberObject = Get-ADObject @params -ErrorAction SilentlyContinue
                if (($memberObject) -and $memberObject.objectClass -eq 'group') {
                    # recurse to get the nested groups
                    $params['Properties'] = $Properties  # add the Properties to the Hashtable again
                    Get-NestedADGroup @params
                }
            }
        }
    }
    else {
        Write-Warning "Group '$($Identity)' could not be found.."
    }
}

# create a Hashtable to avoid circular nested groups
$groupsHash = @{}

# call the function
$result = Get-NestedADGroup -Group 'TheGroupToInspect'

# output just the names
$result.Name

Upvotes: 2

Related Questions