Skip to main content

This PowerShell script retrieves the group membership of an AD object and outputs the result just like a regular cmdlet, which means the result can be further manipulated such as exporting it to a CSV file or using it as a part of another PowerShell script.

<#
.NOTES
    Name: Get-GroupMembership.ps1
    Author: Daniel Sheehan
    Requires: PowerShell v2 or higher, the Active Directory module, and the
    Exchange Management Shell on the computer running the script. Also the
    account running this script needs to have read rights in AD and Exchange in
    order to query both.
    Version History:
    1.0 - 3/14/2014 - Initial Release.
    1.1 - 3/25/2014 - Added Domain\UserName Identity input support, recursive
    group membership lookup support, and updated comments.
    1.2 - 3/26/2014 - Added support for UPN, email address, and
    DistinguishedName as the Identity input. Also and added support for objects
    passed through the pipeline to the script.
    1.3 - 3/28/2014 - Add error handling and reporting if multiple objects were
    found. For example duplciate UserNames in separate domains.
.SYNOPSIS
    Retrieves all Groups or just Distribution Groups that a User/Group/Contact
    object is directly and optionally indirectly a member of.
.DESCRIPTION
    This script selects a GC in the local AD site and then queries it for all of
    the Groups an object is a member of. By default all Groups are returned, but
    optionally only Distribution Groups can be returned. It can also optionally
    recursively search Groups the object is indirectly a member of. The
    resulting Groups are then output just like a regular cmdlet, where they can
    be further processed such as exporting them to a CSV.
.PARAMETER Identity
    This mandatory value designates the object to search for in AD. It can be in
    the format of Domain\UserName, UserName, UPN, email address, or
    DistinguishedName. In the context of this script UserName applies to the
    "SamAccountName" of User and Group objects.
.PARAMETER SearchPath
    This optional value designates the base LDAP path to start the object search
    in. If no value is defined, the entire forest is searched.
.PARAMETER Recursive
    This optional switch tells the script to perform additional recursive Group
    membership queries of the object. For example if the object is in GroupA,
    and GroupA is nested in GroupB, then GroupB will only be included in the
    output with GroupA if this switch is used.
.PARAMETER DistributionGroupsOnly
    This optional switch tells the script to only return Distribution Groups in
    the output, otherwise all Groups are returned. When used in conjunction with
    the Recursive switch, a Group is only processed for recursion if it is
    mail-enabled.
    Consider the following scenario where GroupA is nested in GroupB, Group B is
    nested in GroupC, GroupC is nested in GroupD, and all Groups except GroupC
    are mail-enabled. Only GroupA and GroupB will be included in the output
    because the recursion stops at and excludes GroupC from the output because
    GroupC is not mail-enabled.
.EXAMPLE
    [PS] C:\>.\Get-GroupMembership johndoe
    The entire forest is searched for the object with the UserName "johndoe",
    and only the direct Group memberships are output.
.EXAMPLE
    [PS] C:\>.\Get-GroupMembership -Identity Company\johndoe
    -SearchPath "DC=subdomain,DC=company,DC=com"
    The LDAP subtree search starts at the subdomain named "subdomain" for the
    object with the Domain "Company" and the UserName "johndoe", and only the
    object's direct Group memberships are output.
.EXAMPLE
    [PS] C:\>.\Get-GroupMembership -Identity johndoe@company.com -Recursive
    -DistributionGroupsOnly
    The entire forest is searched for the object with the UPN or email address
    of "johndoe@company.com", and both the direct and indirect Distribution Group
    memberships are output.
.EXAMPLE
    [PS] C:\>Get-User johndoe | .\Get-GroupMembership -Recursive
    The entire forest is searched for the DN of the Get-User object "johndoe",
    and both the direct and indirect Group memberships are output.
#>

Param (
    # Read in the mandatory -Identity value from the command line or object pipeline. Even if -Identity isn't used on the
    #   command line, the first value after the script name is used for this variable.
    [Parameter(Mandatory=$True,Position=0,ValueFromPipeline=$True)]
    $Identity,
    # Read in the LDAP -SearchPath value if one is optionally provided. Otherwise default to the value of "ForestRoot" which
    #   tells the script to start the search at the root of the AD Forest.
    [Parameter(Mandatory=$False)]
    [String]$SearchPath = "ForestRoot",
    # Check if the Recursive switch is optionally provided. The switch value is only True if the switch is provided, otherwise
    #   it is False.
    [Parameter(Mandatory=$False)]
    [Switch]$Recursive,
    # Check if the DistributionGroupsOnly switch is optionally provided. The switch value is only True if the switch is provided,
    #   otherwise it is False.
    [Parameter(Mandatory=$False)]
    [Switch]$DistributionGroupsOnly
)

# Check to see if the ActiveDirectory Module is not currently loaded.
If (!(Get-Module "ActiveDirectory")) {
    # It's not loaded so load it.
    Import-Module "ActiveDirectory"
    # Check to make sure there wasn't an error loading the module.
    If (!$?) {
        # There was an error loading the ActiveDirectory module, so report that and exit out of the script.
        Write-Host -ForegroundColor Red "There was an error loading the Active Directory module. Exiting..."
        BREAK
    }
}

# Check to see if the AD scope setting is not set to the entire forest.
If ((Get-ADServerSettings).ViewEntireForest -notlike "True") {
    # It's not so set the scope to the entire forest.
    Set-ADServerSettings -ViewEntireForest $True | Out-Null
}

# Verify the Identity variable is not blank, which could only happen if a blank value was passed into the script from the pipeline.
If (!$Identity) {
    # It's blank so report it and exit out of the script.
    Write-Host -ForegroundColor Red "No valid Identity was passed to this script. Exiting."
    BREAK
# It's not blank so check to see if the Identity variable is not a string passed in from the command line, such as an object passed
#   through a pipeline.
} ElseIf ($Identity -isnot [String]) {
    # It not a string so extract the DistinguishedName string from the object, and check to make sure the extraction is successful.
    If (!([String]$Identity = $Identity.DistinguishedName)) {
        Write-Host -ForegroundColor Red "The object passed to this script does not have a DistinguishedName value. Exiting."
        BREAK
    }
}

# Check to see if the SearchPath is still set to the default string of "ForestRoot".
If ($SearchPath -match "ForestRoot") {
    # It is, so go grab the AD Forest Root Domain name and format it for the LDAP SearchPath query.
    $SearchPath = "DC=" + ((Get-ADForest).RootDomain).Replace(".",",DC=")
}

# Find a GC in the local site and extract it's FQDN.
[String]$GCServer = (Get-ADDomainController -Discover -Service "GlobalCatalog").HostName
# Establish the LDAP path to the a GC in the local site using the defined search path.
$SearchRoot = New-Object System.DirectoryServices.DirectoryEntry("LDAP://" + $GCServer + ":3268/" + $SearchPath)
# Establish the LDAP connection to AD.
$ADSearch = New-Object System.DirectoryServices.DirectorySearcher
# Use the LDAP path as the root search path in AD.
$ADSearch.SearchRoot = $SearchRoot
# Configure the AD search to search subtress frrom the root search path.
$ADSearch.SearchScope = "Subtree"
# Configure the AD search to only use 1000 pages.
$ADSearch.PageSize = 1000
# Check to see if the Identity variable contains as "\" character, which means it contains a domain and user name.
If ($Identity -like "*\*") {
    # It does so grab the user's UPN and so it can be used as a part of a UserPrincipalName query.
    If ($UserUPN = (Get-User $Identity -ErrorAction:SilentlyContinue).UserPrincipalName) {
        # The UPN retrieval was succesfful so format the filter string to use it.
        $ADSearch.Filter = "(userPrincipalName=$UserUPN)"
    } Else {
        # The UPN retrieval wasn't succesful, so write that to the screen and exit out of the script.
        Write-Host -ForegroundColor Red "The Domain\UserName of $Identity was not found. Please check your syntax and try again."
        BREAK
    }
# Check to see if the Identity variable contains a "@" character, which means it is a UPN or email address.
} ElseIf ($Identity -like "*@*") {
    # It does so format the filter to query for either the UPN or the email address.
    $ADSearch.Filter = "(|(UserPrincipalName=$Identity)(proxyAddresses=smtp:$Identity))"
# Check to see if the Identity variable starts with the characters CN=, which means it is DistinguishedName.
} ElseIf ($Identity -like "CN=*") {
    # It does so format the filter to query for the DistinguishedName.
    $ADSearch.Filter = "(distinguishedName=$Identity)"
} Else {
    # It doesn't match any of the other checks so assume so the Identity is a UserName, and format the filter to be a samAccountName query.
    $ADSearch.Filter = "(samAccountName=$Identity)"
}
# Specify which attributes to retrieve for the object.
$Attributes = @("DisplayName","UserPrincipalName","DistinguishedName","MemberOf")
# Configure the AD search to extract the attributes defined above as properties.
$ADSearch.PropertiesToLoad.Addrange($Attributes)

# The GetGroupMemberOf function is only used as a part of the Recursive group lookup below.
Function GetGroupMemberOf {
    # Grab the passed through DN string of the group being checked.
    Param (
        [Parameter(Mandatory=$True, ValueFromPipeline=$True)]
        [String]$CheckGroup
    )
    # Extract the MemberOf attribute of the group being checked.
    $GroupMemberOf = (Get-ADGroup $CheckGroup -Server ($GCServer +":3268") -Properties MemberOf).MemberOf
    # If there were any groups listed in MemberOf attribute, loop through them and check them individually.
    ForEach ($GroupDN in $GroupMemberOf) {
        # Search the global ObjectGroups variable to see if the group we are checking for is not listed. Otherwise if it is listed we already processed it and can skip it.
        If (!($ObjectGroups -like $GroupDN)) {
            # The group isn't already listed in the ObjectGroups variable, so make sure the DistributionGroupsOnly isn't set to true while the group isn't email enabled.
            If (!(($DistributionGroupsOnly) -and ((Get-ADGroup $GroupDN -Server ($GCServer +":3268") -Properties Mail).Mail -eq $Null))) {
                # The group passed the check, so add it to the global ObjectGroups variable for output.
                $Script:ObjectGroups += $GroupDN
                # Since the group was added to the list, it too has to be sent through the Function to see if it is nested inside of other groups.
                GetGroupMemberOf $GroupDN
            }
        }
    }
}

# Execute the object search in AD and gather the number of objects returned.
$ADObject = $ADSearch.FindAll()
$ADObjectCount = $ADObject.Count

# Check to make sure at least one object is found.
If ($ADObjectCount -eq 1) {
    # Only one object was found so capture each group the object is a member of into the Array variable named ObjectGroups. [0] is used to specify
    #   the first object in an array of objects (even though there is 1 object in the array)
    [Array]$ObjectGroups = $ADObject[0].Properties.memberof
    # Check to see if the Recursive switch was specified.
    If ($Recursive) {
        # It was so loop through each group the object is a member of.
        ForEach ($ObjectGroup in $ObjectGroups) {
            # Check the group to see if it is a member of another group by calling the Function above.
            GetGroupMemberOf $ObjectGroup
        }
    }
    # Check to see if the DistributionGroupsOnly switch was specified.
    If ($DistributionGroupsOnly) {
        # It was so output the collected groups using Get-DistriutionGroup. SilentlyContinue helps suppress errors for any non-mail-enabled
        #   groups returned as a part of the initial AD query.
        $ObjectGroups | Get-DistributionGroup -ErrorAction:SilentlyContinue
    } Else {
        # It wasn't so output the collected groups using Get-Group. Get-Group is used versus Get-ADGroup because it returns more information than
        #   and provides an output more consistent with Get-DistributionGroup.
        $ObjectGroups | Get-Group
    }
# There wasn't just one object, so check to make sure there aren't multiple objects.
} ElseIf ($ADObjectCount -gt 1) {
    # There were multiple objects found, so report an error to the screen.
    Write-Host -ForegroundColor Red "More than one object was found with the provided Identity. Review them and provide a unqiue attribute to query:"
    # Create an Array to hold the multiple objects.
    $MultipleObjects = @()
    # Loop through object in the ADObject collection.
    $ADObject.GetEnumerator() | Foreach-Object {
        # Extract the 3 object values as strings.
        [String]$DisplayName = $_.Properties.displayname.GetEnumerator()
        [String]$UserPrincipalName = $_.Properties.userprincipalname.GetEnumerator()
        [String]$DistinguishedName = $_.Properties.distinguishedname.GetEnumerator()
        # Add all 3 values as properties of a PowerShell object named SingleObject.
        $SingleObject = New-Object PSObject -Property @{
            DisplayName = $DisplayName
            UserPrincipalName = $UserPrincipalName
            DistinguishedName = $DistinguishedName
        }
        # Add the SingleObject to the Array named MultipleObjects.
        $MultipleObjects += $SingleObject
    }
    # Report the multiple objects found as an autosized table to the screen, sorting by the order by the DisplayName attribute.
    $MultipleObjects | Sort DisplayName | FT -AutoSize
} Else {
    # There no objects found so report that to the screen and finish the script.
    Write-Host -ForegroundColor Yellow "The Identity `"$Identity`" wasn't found on the GC $GCServer."
    Write-Host "Please check Identity information and try again and make sure you are using one of the following Identity formats:"
    Write-Host "1. UserName                - Example: johndoe"
    Write-Host "2. Domain\UserName         - Example: Company\johndoe"
    Write-Host "3. UserPrincipalName (UPN) - Example: johndoe@company.com"
    Write-Host "4. Email Address           - Example: johndoe@mail.company.com"
    Write-Host "5. DistinguishedName       - Example: `"CN=Doe\, John,OU=Users,DC=company,DC=com`""
}