### Efficient way to get AD user membership recursively with PowerShell

The other day, one customer asked for a solution to get full user membership in Active Directory for audit purposes. The solution should retrieve not only direct group membership, but indirect (through group nesting) too. Although, the question is plain and simple, solution is very interesting from various perspectives.

At first, let illustrate a sample user and group membership diagram:

Quick diagram observation suggests us that we have a directed graph (it is not a tree), where users and groups are vertexes and membership relations are directed edges. Arrows identify relationship direction.

Our graph contains two users, User1 and User2 and eight groups: G1G8. In a given case, User1 is direct member of groups G1, G2 and G3, User2 is direct member of G8 only. Group G1 is member of G4, G2 is direct member of G4 and G5 and so on. For description purposes I labeled all edges. This should be clear.

# The algorithm

Our initial algorithm would be as follows:

1. Initialize array to store current user group membership and assign to $UserGroups. 2. Get direct groups for user. Assign them to$currentGroups;
3. Loop over $currentGroups and assign each item to$currentGroup;
4. Add $currentGroup to the$UserGroups variable;
5. Retrieve direct groups for $currentGroup and assign them to$currentGroups and repeat steps 3-5.
6. End loop.

Quick algorithm observation suggests that we will deal with recursive loop.

# Potential issues

While it looks pretty legitimate, we may encounter the following issues (and we definitely will with the current diagram):

1) We will have a lot of duplicates. For example, G4 group will be listed twice, because we have two paths from User1 to G4: User1 → e2 → e5 → G4 and User1 → e3 → e6 → G4. The same thing is for G7. We have two paths through: User1 → e1 → e10 –> G7 and User1 → e2 → e4 → e7 → e8 → G7.

Of course, nothing prevents as to use Select-Object –Unique to get unique entries, but this wouldn’t be very efficient. Especially when there are a lot of duplicates.

2) Our logic may never end, because we have one closed walk: when starting from G2 vertex we will return to it through: G2 → e4 → e7 → e8 → e9 → G2. If we do not take additional steps, the code will enter into an infinity loop and eventually will fail with stack overflow exception.

To avoid both potential issues, we have to keep a separate array where we would store visited vertexes (groups). And during each group processing we will check whether the current vertex was already visited and skip if we did. By doing this we will implement a very basic spanning tree algorithm where we convert our graph into tree, so from each Group vertex only single path will exist down to a User vertex. At this point we do not care about which path is shortest, because edges have zero cost. An updated algorithm would look as follows:

1. Initialize array to store current user group membership and assign to $UserGroups. 2. Get direct groups for user. Assign them to$currentGroups;
3. Loop over $currentGroups and assign each item to$currentGroup;
4. If the $UserGroups contains$currentUser, skip this entry and return to step 3.
5. Add $currentGroup to the$UserGroups variable;
6. Retrieve direct groups for $currentGroup and assign them to$currentGroups and repeat steps 3-5.
7. End loop.

# Performance tuning

Step 4 of the updated algorithm suggests multiple searches in $UserGroups array. For small arrays (when user is a member of only few groups) it is acceptable to use linear search that will result in $O(n)$ (where n is a total number of user groups) complexity in each iteration and for entire lookup it will take up to $O(\frac{n^2}{2})$. For larger organizations when user is a member of a large number of groups, linear search will quickly become ineffective. To reduce lookup costs,$UserGroups array will be initialized as a hashtable which has $O(1)$ complexity for each iteration and will be up to $O(n)$ for entire lookup.

Another question is about steps 6 when we will retrieve parent groups for $currentGroup variable. Depending on a group membership complexity, we may encounter multiple identical (redundant) queries to domain controller. For single user lookups it is acceptable to issue separate queries to retrieve group object with memberOf attribute, because they all will be unique (due to step 4). However, when we do such lookup for a large number of users (or even for all users in Active Directory domain), we will get a lot of identical and redundant queries. There are two approaches that may solve this puzzle: 1. Introduce another array to cache retrieved from Active Directory group objects. And during group object retrieval check if the current group is already cached and use cache information, otherwise issue a query to domain controller. This solution is effective when client has reliable connection to domain controller, but may fail if the connection is poor. 2. Issue a single request to domain controller and retrieve *all* groups from Active Directory and use it as a local cache. This solution is effective from performance perspective when client is connected to domain controller over poor/unreliable connection, because only single query is issued. Additionally, it is effective when many users are processed. In other cases it may not be effective from memory consumption perspective. Another downside, domain controller will get increased workload to process the query. For group cache I would recommend to use hashtable as well, because the code will extensively lookup at cache and efficient lookup will positively impact overall script performance. I’m not going to favor any of these solutions, you can select either at your choice. Though, in the code I’m going to use first approach. # Writing solution in PowerShell In the solution, we will use PowerShell and Active Directory PowerShell module which is shipped with ADDS remote server administration tools (RSAT). Make sure if it is installed on your system. And here is a code with relevant comments: function Get-UserGroupMembershipRecursive { [CmdletBinding()] param( [Parameter(Mandatory =$true, ValueFromPipeline = $true)] [String[]]$UserName
)
begin {
# introduce two lookup hashtables. First will contain cached AD groups,
# second will contain user groups. We will reuse it for each user.
# format: Key = group distinguished name, Value = ADGroup object
$ADGroupCache = @{}$UserGroups = @{}
# define recursive function to recursively process groups.
function __findPath ([string]$currentGroup) { Write-Verbose "Processing group:$currentGroup"
# we must do processing only if the group is not already processed.
# otherwise we will get an infinity loop
if (!$UserGroups.ContainsKey($currentGroup)) {
# retrieve group object, either, from cache (if is already cached)
# or from Active Directory
$groupObject = if ($ADGroupCache.ContainsKey($currentGroup)) { Write-Verbose "Found group in cache:$currentGroup"
$ADGroupCache[$currentGroup]
} else {
Write-Verbose "Group: $currentGroup is not presented in cache. Retrieve and cache."$g = Get-ADGroup -Identity $currentGroup -Property "MemberOf" # immediately add group to local cache:$ADGroupCache.Add($g.DistinguishedName,$g)
$g } # add current group to user groups$UserGroups.Add($currentGroup,$groupObject)
Write-Verbose "Member of: $currentGroup" foreach ($p in $groupObject.MemberOf) { __findPath$p
}
} else {Write-Verbose "Closed walk or duplicate on '$currentGroup'. Skipping."} } } process { foreach ($user in $UserName) { Write-Verbose "==========$user =========="
# clear group membership prior to each user processing
$UserObject = Get-ADUser -Identity$user -Property "MemberOf"
$UserObject.MemberOf | ForEach-Object {__findPath$_}
New-Object psobject -Property @{
UserName = $UserObject.Name; MemberOf =$UserGroups.Values | % {$_}; # groups are added in no particular order }$UserGroups.Clear()
}
}
}

And example output with verbose tracing where we can track code logic:

PS C:\> $report = Get-UserGroupMembershipRecursive user1, user2 -Verbose VERBOSE: ========== user1 ========== VERBOSE: Processing group: CN=G3,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Group: CN=G3,OU=StubOU,DC=sysadmins,DC=lv is not presented in cache. Retrieve and cache. VERBOSE: Member of: CN=G3,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Processing group: CN=G6,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Group: CN=G6,OU=StubOU,DC=sysadmins,DC=lv is not presented in cache. Retrieve and cache. VERBOSE: Member of: CN=G6,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Processing group: CN=G7,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Group: CN=G7,OU=StubOU,DC=sysadmins,DC=lv is not presented in cache. Retrieve and cache. VERBOSE: Member of: CN=G7,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Processing group: CN=G2,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Group: CN=G2,OU=StubOU,DC=sysadmins,DC=lv is not presented in cache. Retrieve and cache. VERBOSE: Member of: CN=G2,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Processing group: CN=G5,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Group: CN=G5,OU=StubOU,DC=sysadmins,DC=lv is not presented in cache. Retrieve and cache. VERBOSE: Member of: CN=G5,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Processing group: CN=G6,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Closed walk or duplicate on 'CN=G6,OU=StubOU,DC=sysadmins,DC=lv'. Skipping. VERBOSE: Processing group: CN=G4,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Group: CN=G4,OU=StubOU,DC=sysadmins,DC=lv is not presented in cache. Retrieve and cache. VERBOSE: Member of: CN=G4,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Processing group: CN=G7,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Closed walk or duplicate on 'CN=G7,OU=StubOU,DC=sysadmins,DC=lv'. Skipping. VERBOSE: Processing group: CN=G2,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Closed walk or duplicate on 'CN=G2,OU=StubOU,DC=sysadmins,DC=lv'. Skipping. VERBOSE: Processing group: CN=G1,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Group: CN=G1,OU=StubOU,DC=sysadmins,DC=lv is not presented in cache. Retrieve and cache. VERBOSE: Member of: CN=G1,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Processing group: CN=G4,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Closed walk or duplicate on 'CN=G4,OU=StubOU,DC=sysadmins,DC=lv'. Skipping. VERBOSE: ========== user2 ========== VERBOSE: Processing group: CN=G8,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Group: CN=G8,OU=StubOU,DC=sysadmins,DC=lv is not presented in cache. Retrieve and cache. VERBOSE: Member of: CN=G8,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Processing group: CN=G5,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Found group in cache: CN=G5,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Member of: CN=G5,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Processing group: CN=G6,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Found group in cache: CN=G6,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Member of: CN=G6,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Processing group: CN=G7,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Found group in cache: CN=G7,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Member of: CN=G7,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Processing group: CN=G2,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Found group in cache: CN=G2,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Member of: CN=G2,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Processing group: CN=G5,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Closed walk or duplicate on 'CN=G5,OU=StubOU,DC=sysadmins,DC=lv'. Skipping. VERBOSE: Processing group: CN=G4,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Found group in cache: CN=G4,OU=StubOU,DC=sysadmins,DC=lv VERBOSE: Member of: CN=G4,OU=StubOU,DC=sysadmins,DC=lv PS C:\>$report

--------                                                    --------

PS C:\> $report[0].MemberOf.name G5 G2 G7 G3 G4 G1 G6 PS C:\>$report[1].MemberOf.name
G8
G2
G6
G4
G7
G5
PS C:\>

We can confirm that user membership information correctly reflect our diagram. In addition, we see that second user lookup actively uses AD group cache and only one query was issued to domain controller.

HTH

Michael Graham 26.10.2016 05:38 (GMT+3) Efficient way to get AD user membership recursively with PowerShell

Very well thought out script. I appreciate that you posted it. I had recently created a recursive script myself, and was looking to increase it's performance. My previous script is here: https://github.com/mgraham-cracker/ADSearch if you would like to take a look at some features I built into it.

I went ahead and quickly created a modified version of your script to test a key feature I needed, an inheritance path field. It is often good for me to know how a person is in a group, not just that they are in a group. I put the code below if you were interested.

Thanks again,

--- Code Below ---

function Get-UserGroupMembershipRecursive {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline =$true)]
[String[]]$UserName ) begin { # introduce two lookup hashtables. First will contain cached AD groups, # second will contain user groups. We will reuse it for each user. # format: Key = group distinguished name, Value = ADGroup object$ADGroupCache = @{}
$UserGroups = @{}$OutObject = @()
# define recursive function to recursively process groups.
function __findPath ([string]$currentGroup, [string]$comment) {
Write-Verbose "Processing group: $currentGroup" # we must do processing only if the group is not already processed. # otherwise we will get an infinity loop if (!$UserGroups.ContainsKey($currentGroup)) { # retrieve group object, either, from cache (if is already cached) # or from Active Directory$groupObject = if ($ADGroupCache.ContainsKey($currentGroup)) {
Write-Verbose "Found group in cache: $currentGroup"$ADGroupCache[$currentGroup].Psobject.Copy() } else { Write-Verbose "Group:$currentGroup is not presented in cache. Retrieve and cache."
$g = Get-ADGroup -Identity$currentGroup -Property objectclass,sid,whenchanged,whencreated,samaccountname,displayname,enabled,distinguishedname,memberof
# immediately add group to local cache:
$ADGroupCache.Add($g.DistinguishedName, $g)$g
}

$c =$comment + "->" + $groupObject.SamAccountName$UserGroups.Add($c,$groupObject)

Write-Verbose "Membership Path:  $c" foreach ($p in $groupObject.MemberOf) { __findPath$p $c } } else {Write-Verbose "Closed walk or duplicate on '$currentGroup'. Skipping."}
}
}
process {
foreach ($user in$UserName) {
Write-Verbose "========== $user ==========" # clear group membership prior to each user processing$UserObject = Get-ADUser -Identity $user -Property objectclass,sid,whenchanged,whencreated,samaccountname,displayname,enabled,distinguishedname,memberof$UserObject.MemberOf | ForEach-Object {__findPath $_$UserObject.SamAccountName}
}
foreach($g in$UserGroups.GetEnumerator())
{
$OutObject += [pscustomobject]@{ ObjectClass =$g.value.ObjectClass;
UserName = $UserObject.SamAccountName; InheritancePath =$g.key;
MemberOf = $g.value.SamAccountName; WhenChanged =$g.value.WhenChanged;
WhenCreated = $g.value.WhenCreated; } }$OutObject | Sort-Object -Property UserName,InheritancePath,MemberOf | Out-Gridview
$UserGroups.Clear() } } Earl Hyde 24.05.2017 18:53 (GMT+3) Efficient way to get AD user membership recursively with PowerShell I love the work both of you did on this and don't presume to comment on the efficiency. I just added a couple of stylistic modificiations. GroupScope & GroupCategory were added which are advantageous to have in the output and date outputs were formatted as ragged date columns are hard to read. function Get-UserGroupMembershipRecursive { [CmdletBinding()] param( [Parameter(Mandatory =$true, ValueFromPipeline = $true)] [String[]]$UserName
)
begin {
# introduce two lookup hashtables. First will contain cached AD groups,
# second will contain user groups. We will reuse it for each user.
# format: Key = group distinguished name, Value = ADGroup object
$ADGroupCache = @{}$UserGroups = @{}
$OutObject = @() # define recursive function to recursively process groups. function __findPath ([string]$currentGroup, [string]$comment) { Write-Verbose "Processing group:$currentGroup"
# we must do processing only if the group is not already processed.
# otherwise we will get an infinity loop
if (!$UserGroups.ContainsKey($currentGroup)) {
# retrieve group object, either, from cache (if is already cached)
# or from Active Directory
$groupObject = if ($ADGroupCache.ContainsKey($currentGroup)) { Write-Verbose "Found group in cache:$currentGroup"
$ADGroupCache[$currentGroup].Psobject.Copy()
} else {
Write-Verbose "Group: $currentGroup is not presented in cache. Retrieve and cache."$g = Get-ADGroup -Identity $currentGroup -Property objectclass,sid,whenchanged,whencreated,samaccountname,displayname,enabled,distinguishedname,memberof,groupscope,groupcategory # immediately add group to local cache:$ADGroupCache.Add($g.DistinguishedName,$g)
$g }$c = $comment + "->" +$groupObject.SamAccountName

$UserGroups.Add($c, $groupObject) Write-Verbose "Membership Path:$c"
foreach ($p in$groupObject.MemberOf) {
__findPath $p$c
}
} else { Write-Verbose "Closed walk or duplicate on '$currentGroup'. Skipping." } } } process {$enus = 'en-US' -as [Globalization.CultureInfo]
foreach ($user in$UserName) {
Write-Verbose "========== $user ==========" # clear group membership prior to each user processing$UserObject = Get-ADUser -Identity $user -Property objectclass,sid,whenchanged,whencreated,samaccountname,displayname,enabled,distinguishedname,memberof$UserObject.MemberOf | ForEach-Object {__findPath $_$UserObject.SamAccountName}
}
foreach($g in$UserGroups.GetEnumerator())
{
$OutObject += [pscustomobject]@{ ObjectClass =$g.value.ObjectClass;
UserName = $UserObject.SamAccountName; InheritancePath =$g.key;
MemberOf = $g.value.SamAccountName; GroupScope =$g.value.GroupScope;
GroupCategory = $g.value.GroupCategory; WhenCreated2 =$g.value.WhenCreated.ToString("MM/dd/yyyy hh:mm tt", $enus); WhenChanged =$g.value.WhenChanged.ToString("MM/dd/yyyy hh:mm tt", $enus); } }$OutObject | Sort-Object -Property UserName,InheritancePath,MemberOf | Out-Gridview
$UserGroups.Clear() } } Tobias 03.08.2017 09:19 (GMT+3) Efficient way to get AD user membership recursively with PowerShell Thank you all for your scripts and findings. The company I'm working for uses hundreds of startup scripts that have been partially created per user or for smaller user groups. This now needs to be consolidated and brought to a "per team" approach, so I was asked to change the startup script behaviour and put together new scripts that makes sure users only get mappings as they're supposed to. So for the me question really is: Is AD user John Doe member (direct or indirect) of the Group TEST_Nested_Group_I_am_well_hidden or not at all. A simply Yes or No would then do the trick, so thanks @Michael Graham, I have used your code as I'm able to filter out the groups within the GridView display, and if it appears in the list the user somehow belongs to that list or group and I therefore know that the mapping is correct or not. This has been a great help! Best regards Tobias Anonymous 01.10.2017 18:16 (GMT+3) Efficient way to get AD user membership recursively with PowerShell I checked all your script and they looks good but there is one problem I noticed. It doesn't show the "Domain Users" groups. Which is default group that every new user have. udo 22.11.2017 14:48 (GMT+3) Efficient way to get AD user membership recursively with PowerShell Nice script, but because the "Domain Users" Group is skipped the results are not correct. In my case severak groups are not in the ouput because those groups have just the "Domain Users" group as a member. JohnH 01.12.2017 23:50 (GMT+3) Efficient way to get AD user membership recursively with PowerShell Thanks also for the script, it was helpful ... I'm finishing a script that goes the other way Group to users with the pathing (so Get-ADGroupMember -recursive not good enough). As for the Domain Users issue: the ".MemberOf" method is limited because it does not return the User's Primary Group...if you were to change the Primary Group, that new Primary Group would then be missing methinks. You can Instead use: Get-ADPrincipalGroupMembership (BUT its a little bit slower you'll notice). Here is the most recent Script in the thread (from Earl Hyde) with the changes (two places)...basically replacing .MemberOf with Get-ADPrincipalGroupMembership cmdlet. I just plopped it in...might want to make it a bit more elegent. function Get-UserGroupMembershipRecursive { [CmdletBinding()] param( [Parameter(Mandatory =$true, ValueFromPipeline = $true)] [String[]]$UserName
)
begin {
# introduce two lookup hashtables. First will contain cached AD groups,
# second will contain user groups. We will reuse it for each user.
# format: Key = group distinguished name, Value = ADGroup object
$ADGroupCache = @{}$UserGroups = @{}
$OutObject = @() # define recursive function to recursively process groups. function __findPath ([string]$currentGroup, [string]$comment) { Write-Verbose "Processing group:$currentGroup"
# we must do processing only if the group is not already processed.
# otherwise we will get an infinity loop
if (!$UserGroups.ContainsKey($currentGroup)) {
# retrieve group object, either, from cache (if is already cached)
# or from Active Directory
$groupObject = if ($ADGroupCache.ContainsKey($currentGroup)) { Write-Verbose "Found group in cache:$currentGroup"
$ADGroupCache[$currentGroup].Psobject.Copy()
} else {
Write-Verbose "Group: $currentGroup is not presented in cache. Retrieve and cache."$g = Get-ADGroup -Identity $currentGroup -Property objectclass,sid,whenchanged,whencreated,samaccountname,displayname,enabled,distinguishedname,memberof,groupscope,groupcategory # immediately add group to local cache:$ADGroupCache.Add($g.DistinguishedName,$g)
$g }$c = $comment + "->" +$groupObject.SamAccountName

$UserGroups.Add($c, $groupObject) Write-Verbose "Membership Path:$c"
foreach ($p in (Get-ADPrincipalGroupMembership$groupObject.SamAccountName)) {
__findPath $p$c
}
} else { Write-Verbose "Closed walk or duplicate on '$currentGroup'. Skipping." } } } process {$enus = 'en-US' -as [Globalization.CultureInfo]
foreach ($user in$UserName) {
Write-Verbose "========== $user ==========" # clear group membership prior to each user processing$UserObject = Get-ADUser -Identity $user -Property objectclass,sid,whenchanged,whencreated,samaccountname,displayname,enabled,distinguishedname,memberof (Get-ADPrincipalGroupMembership$UserObject.SamAccountName) | ForEach-Object {__findPath $_$UserObject.SamAccountName}
}
foreach($g in$UserGroups.GetEnumerator())
{
$OutObject += [pscustomobject]@{ ObjectClass =$g.value.ObjectClass;
UserName = $UserObject.SamAccountName; InheritancePath =$g.key;
MemberOf = $g.value.SamAccountName; GroupScope =$g.value.GroupScope;
GroupCategory = $g.value.GroupCategory; WhenCreated2 =$g.value.WhenCreated.ToString("MM/dd/yyyy hh:mm tt", $enus); WhenChanged =$g.value.WhenChanged.ToString("MM/dd/yyyy hh:mm tt", $enus); } }$OutObject | Sort-Object -Property UserName,InheritancePath,MemberOf | Out-Gridview
$UserGroups.Clear() } } Udo 03.12.2017 12:38 (GMT+3) Efficient way to get AD user membership recursively with PowerShell Hi John, your script and all above, except Vadims original script will hang in a loop if the AD groups are in a loop as Vadims illustrated in the diagram. e.g. Membership Path: User1->G3->G7->G2->G5->G6->G7->G2->G5->G6->G7 and so on and on. This comes from$UserGroups.Add($c,$groupObject). If you change it into $UserGroups.Add($
currentGroup, $groupObject) it will work even if you have a loop in the AD groups. 03.12.2017 13:08 (GMT+3) Efficient way to get AD user membership recursively with PowerShell > if you were to change the Primary Group, that new Primary Group would then be missing methinks it does, however I'm assuming that default primary group value is used. It is extremely rare case when you need to change primary group and in most cases it should be "Domain Users". Udo 03.12.2017 13:44 (GMT+3) Efficient way to get AD user membership recursively with PowerShell you can also process the primary group however it is called if you change:$UserObject = Get-ADUser -Identity $user -Property objectclass,sid,whenchanged,whencreated,samaccountname,displayname,enabled,distinguishedname,memberof$UserObject.MemberOf | ForEach-Object {__findPath $_$UserObject.SamAccountName}

into

$UserObject = Get-ADUser -Identity$user -Property objectclass,sid,whenchanged,whencreated,samaccountname,displayname,enabled,distinguishedname,memberof,PrimaryGroup
$UserObject.MemberOf | ForEach-Object {__findPath$_ $UserObject.SamAccountName}$UserObject.PrimaryGroup | ForEach-Object {__findPath $_$UserObject.SamAccountName}

erlwes 29.12.2017 14:44 (GMT+3) Efficient way to get AD user membership recursively with PowerShell

@Tobias

If you only need a certaint level of detail, and the script will be running in end-user context, you can get the groupmemberships from current Windows session like so:

whoami /groups /fo csv | ConvertFrom-Csv

Regards,
Erlend.

Kasper Katzmann 10.01.2018 14:52 (GMT+3) Efficient way to get AD user membership recursively with PowerShell

This is so usefull - thumbs up :)

Is there a way to get the results from all users in an OU?

Something like $Users = @(Get-ADUser -filter * -SearchBase "OU=Users,OU=$UserName,OU=UserAccounts,OU=SITCustomers,DC=PROD,DC=SITAD,DC=DK" | select sAMAccountName)

Can't seem to get it right when I try...

Kasper Katzmann 11.01.2018 15:23 (GMT+3) Efficient way to get AD user membership recursively with PowerShell

I figured out how to make it multiuser aware.
By adding user or a customer OU, the scope (User or CU (CustomerUnit)) and how to output (Out-GridView or Csv).

Example: Get-GroupMemberShips -Customer Contoso -Scope CU -Output Csv
This will create a Csv-file named C:\temp\GroupMemberShips - Contoso - 11-01-2018 14.23.csv

------ SCRIPT BEGIN ------

Function Get-GroupMemberShips
{
<#
.EXAMPLE
Get-SITGroupMemberShips -Customer [samAccountName] -Scope [User/Customer OU] -Output [Out-GridView/Csv]

ObjectClass  |  UserName  |  InheritancePath       |  MemberOf  |  WhenChanged       |  WhenCreated
group        |  JDOE      |  JDOE->GROUP1->GROUP2  |  GROUP2    |  26-05-2017 18:31  |  21-02-2017 15:13

#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline =$true, Position = 0)]
[String[]]$Customer, [Parameter(Mandatory =$true, ValueFromPipeline = $true, Position = 2)] [ValidateSet("Csv","Out-GridView")] [String[]]$Output,
[Parameter(Mandatory = $true, ValueFromPipeline =$true, Position = 1)]
[ValidateSet("User","CU")]
[String[]]$Scope="User" )$ErrorActionPreference = "SilentlyContinue"

function Get-UserGroupMembershipRecursive {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline =$true)]
[String[]]$UserName )$ErrorActionPreference = "SilentlyContinue"

begin {
# introduce two lookup hashtables. First will contain cached AD groups,
# second will contain user groups. We will reuse it for each user.
# format: Key = group distinguished name, Value = ADGroup object
$ADGroupCache = @{}$UserGroups = @{}
$OutObject = @() # define recursive function to recursively process groups. function __findPath ([string]$currentGroup, [string]$comment) { Write-Verbose "Processing group:$currentGroup"
# we must do processing only if the group is not already processed.
# otherwise we will get an infinity loop
if (!$UserGroups.ContainsKey($currentGroup)) {
# retrieve group object, either, from cache (if is already cached)
# or from Active Directory
$groupObject = if ($ADGroupCache.ContainsKey($currentGroup)) { Write-Verbose "Found group in cache:$currentGroup"
$ADGroupCache[$currentGroup].Psobject.Copy()
} else {
Write-Verbose "Group: $currentGroup is not presented in cache. Retrieve and cache."$g = Get-ADGroup -Identity $currentGroup -Property objectclass,sid,whenchanged,whencreated,samaccountname,displayname,enabled,distinguishedname,memberof # immediately add group to local cache:$ADGroupCache.Add($g.DistinguishedName,$g)
$g }$c = $comment + "->" +$groupObject.SamAccountName

$UserGroups.Add($c, $groupObject) Write-Verbose "Membership Path:$c"
foreach ($p in$groupObject.MemberOf) {
__findPath $p$c
}
} else {Write-Verbose "Closed walk or duplicate on '$currentGroup'. Skipping."} } } process { foreach ($user in $UserName) { Write-Verbose "==========$user =========="
# clear group membership prior to each user processing
$UserObject = Get-ADUser -Identity$user -Property objectclass,sid,whenchanged,whencreated,samaccountname,displayname,enabled,distinguishedname,memberof
$UserObject.MemberOf | ForEach-Object {__findPath$_ $UserObject.SamAccountName} } foreach($g in $UserGroups.GetEnumerator()) {$OutObject += [pscustomobject]@{
ObjectClass = $g.value.ObjectClass; UserName =$UserObject.SamAccountName;
InheritancePath = $g.key; MemberOf =$g.value.SamAccountName;
WhenChanged = $g.value.WhenChanged; WhenCreated =$g.value.WhenCreated;
}
}
$OutObject | Sort-Object -Property UserName,InheritancePath,MemberOf #| Out-Gridview$UserGroups.Clear()
}
}

If($Scope -eq "CU") {$Users = Get-ADUser -filter * -SearchBase "OU=Users,OU=$Customer,OU=Accounts,OU=OURCustomers,DC=PRODUCTION,DC=OURAD,DC=DK" | select sAMAccountName If($Output -eq "Csv")
{
$Date = Get-Date -Format d$Time = Get-Date -Format t
$Timestamp = "$Date $Time"$Timestamp = $Timestamp -replace(":",".")$CsvPath      = "c:\temp\GroupMemberships - $Customer -$Timestamp.csv"
}

foreach($user in$users)
{
$usr =$user.SamAccountName
if($Output -eq "Csv") { Get-UserGroupMembershipRecursive$usr | Export-Csv "c:\temp\GroupMemberships - $Customer -$Timestamp.csv" -Delimiter ";" -NoTypeInformation -Encoding Unicode -Append
}
Else
{
$thisUser = Get-UserGroupMembershipRecursive$usr
$allUsers =$allUsers + $thisUser } }$allUsers | Out-GridView
}
Else
{
Get-UserGroupMembershipRecursive $Customer } } ------ SCRIPT END ------ Lukas 09.05.2018 12:17 (GMT+3) Efficient way to get AD user membership recursively with PowerShell Built-in solution for presented problem: Run CMD or Powershell -> ntdsutil "group member eval" "run <DOMAIN> <samAccountName>" :) Best, Lukas DarkLite1 18.07.2018 16:33 (GMT+3) Efficient way to get AD user membership recursively with PowerShell Fantastic script guys! Can't improve it unless the following changes I applied: • Changed to the .ForEach({}) method instead of piping. (Since the newer versions of PowerShell support this method, it's way faster, same for .Where({})) • Added the PrimaryGroup as suggested by @Udo • Removed some Verbose text to speed things up even more. Function Get-ADUserGroupMembershipRecursiveHC { [CmdletBinding()] Param ( [Parameter(Mandatory, ValueFromPipeline)] [String[]]$SamAccountName
)

Begin {
$ADGroupCache = @{}$UserGroups = @{}

Function Get-ADGroupPath ([String]$CurrentGroup) { if (-not$UserGroups.ContainsKey($CurrentGroup)) {$GroupObject = if ($ADGroupCache.ContainsKey($CurrentGroup)) {
$ADGroupCache[$CurrentGroup]
}
else {
$Group = Get-ADGroup -Identity$CurrentGroup -Property MemberOf
$ADGroupCache.Add($Group.DistinguishedName, $Group)$Group
}

$UserGroups.Add($CurrentGroup, $GroupObject) @($GroupObject.MemberOf).ForEach({Get-ADGroupPath $_}) } else { Write-Verbose "Group '$CurrentGroup' already registered."
}
}
}

Process {
Try {
foreach ($S in$SamAccountName) {
$User = Get-ADUser -Identity$S -Property MemberOf, PrimaryGroup
@($User.MemberOf).ForEach({Get-ADGroupPath -CurrentGroup$_})
@($User.PrimaryGroup).ForEach({Get-ADGroupPath -CurrentGroup$_})

[PSCustomObject]@{
UserName = $User.Name MemberOf = @($UserGroups.Values).ForEach({$_}) }$UserGroups.Clear()
}
}
Catch {
throw "Failed retrieving group membership recursively for '$SamAccountName':$_"
}
}
}

Marcel 02.12.2019 10:02 (GMT+3) Efficient way to get AD user membership recursively with PowerShell

Hi.

I've added this line to allow for multidomain support.

$Server = ($currentGroup.split(',') | ? { $_.split('=')[0] -eq "DC" } | % {$_.split('=')[1] }) -join '.'
$g = Get-ADGroup -Identity$currentGroup -Property "MemberOf" -Server $Server It takes the DC= parts of the$currentGroup and uses it as server in the Get-ADGroup cmdlet.

Aaron 21.07.2020 20:20 (GMT+3) Efficient way to get AD user membership recursively with PowerShell

i know this is late to the party, but users are not the only thing that can be in groups...

particularly adusers, adcomputers and adserviceaccounts are all useful to enumerate in this way(which can all be contained in an adobject with a filter)

Drew 10.08.2020 11:23 (GMT+3) Efficient way to get AD user membership recursively with PowerShell

I agree with aaron, appreciate this user script. is there one similar floating about for computers?

09.09.2020 10:59 (GMT+3) Efficient way to get AD user membership recursively with PowerShell

very nice presentation and good DFS implementation

I encountered the need as well because of RBAC and external trusts.
I developped as well a powershell function but based on a BFS and set parameters to take into account the scope search forest, domain, domain trusts forest trusts or explicit domains. I used the.net classes so no need for the RSAT and activedirectory module. I shared the function on my github for anyone who might have some interest as well

Stanvy 19.02.2021 22:04 (GMT+3) Efficient way to get AD user membership recursively with PowerShell

I’m incredibly surprised to see how such a trivial task leaded to so complicated solutions! In order to get all the groups that object (user, group, contact, computer, ou, foreignSecurityPrincipal - just regardless its’ class) is member of (excluding primary group) the only thing you need is a single LDAP request:

Where "CN=Object Name,OU=OU Name,DC=domain,DC=com" is "distinguishedName" of the object of your interest.

criffo 05.04.2021 17:30 (GMT+3) Efficient way to get AD user membership recursively with PowerShell

Hi Stanvy,

Very practical but still; it is a comparaison until last parent to control if user is a nested member of a group or serie of groups by DN property

Is there a way to get the tree of an adboject memberships; starting form the object and get results from other forests as well ?

Actually I did that using the ldapsearcher class but I admit it is quite a long script (avaimabme opn github Criffo : getadobjectmemberof custom)

Actually we needed for users review and memberhips based on a reference user even if we could use the get=adprincipal ... we needed to find any multile memeberships by group depencies in cas of example cross nodes netsing

KryptykHermit 22.04.2021 20:08 (GMT+3) Efficient way to get AD user membership recursively with PowerShell

I had to go through this exercise this morning, thinking "someone HAS to have something like this out there already!?".  If anyone is interested, here's my take on the process.  I'm a canonicalname kinda guy, but feel free to adjust as you see fit.  Properties are CASE Sensative!

<code>

function Get-GroupMembersRecursive {

param(

[string]$GroupName ) ####################################################### # FUNCTIONS ####################################################### function ConvertDNtoCN ($DN) {

foreach ($item in ($DN.replace('\,','~').split(","))) {

switch ($item.TrimStart().Substring(0,2)) { 'CN' {$CN = '/' + $item.Replace("CN=","")} 'OU' {$OU += ,$item.Replace("OU=","");$OU += '/'}

'DC' {$DC +=$item.Replace("DC=","");$DC += '.'} } }$CanonicalName = $DC.Substring(0,$DC.length - 1)

for ($i =$OU.count;$i -ge 0;$i -- ){$CanonicalName +=$OU[$i]} if ($DN.Substring(0,2) -eq 'CN' ) {

$CanonicalName +=$CN.Replace('~','\,')

}

Return $CanonicalName } ####################################################### # Create an ADSI Searcher$searcher = [adsisearcher]''

# Append a filter for the main group name

$searcher.Filter = "name=$groupname"

# Search for the group

$distinguishedName =$searcher.FindOne().Properties.distinguishedname

# create a filter with the DN of the main group

[string]$ldapFilter = "(memberOf:1.2.840.113556.1.4.1941:=$($distinguishedName))" # update the searcher$searcher.Filter = $ldapFilter # Query the users$searcher.FindAll().Properties | ForEach-Object {

[pscustomobject]@{

Name          = (-join $_.cn) Class =$(if ($_.objectclass -match 'group') { 'Group' } else { 'User' }) CanonicalName = (ConvertDNtoCN -DN$_.distinguishedname)

FirstName     = (-join $_.givenname) LastName = (-join$_.sn)

}

} | Sort-Object -Property 'Class', 'Name' |

Format-Table -AutoSize

}

</code>

vit 27.04.2021 13:56 (GMT+3) Efficient way to get AD user membership recursively with PowerShell

Get-ADGroup $groupname | Get-ADGroupMember -Recursive FromMotherRussiaWithLove 17.09.2021 00:51 (GMT+3) Efficient way to get AD user membership recursively with PowerShell AD has the tokenGroups calculated property: https://docs.microsoft.com/en-us/windows/win32/adschema/a-tokengroups smthg like that:$username = "vasya"

$sids = (Get-ADObject (get-aduser$username).DistinguishedName -Properties tokenGroups).tokenGroups

$domsid = (Get-ADDomain).DomainSID foreach ($sid in $sids) { if ($sid.AccountDomainSid -notlike $domsid) { continue #here going to global catalog :) } get-adgroup$sid.value -Properties name | select name
}

FromMotherRussiaWithLove 17.09.2021 01:05 (GMT+3) Efficient way to get AD user membership recursively with PowerShell

* tokenGroups property returns only security group's SIDs, distrib lists are not included

FromMotherRussiaWithLove 17.09.2021 21:34 (GMT+3) Efficient way to get AD user membership recursively with PowerShell

Hehey!

Just found yet more useful property; msds-memberOfTransitive

Contains DNs of all groups (security and distrib)