Sunday, January 22, 2017

How to enlists AD user group memberships recursively including all parent groups and using standard capabilities of Active Directory module for Windows Powershell

Recently, one of my customers has asked me to write a simple solution, which would assist him with recursive retrieval of group memberships of every AD and local built-in groups in his domain and local machine.

Thus the solution should have listed every group to which his account directly belonged to as well as all parent groups chained to it. In addition, the solution should have retrieved and show the owner of each group.

Typical use cases

Use case 1. Put user1 is a direct member of group1. group1 is a direct member of group 2, group2 is a direct member of group3. So the full chain reads as user1 > group1 > group2 > group3. The logic should return group1, group2, group3 for user1.

Use case 2. Put user1 is a direct member of group1. group1 is a direct member of group 2. group2 also includes group3 as a member, however, group3 does not contain user1. So the full chain reads as user1 > group1 > group2. And group 3 > group 2. In this case, the logic should return only group1 and group2 for user1.

Powershell-based solution

The complete source code of the solution is given below.

The solution runs in Powershell version 2.0 or higher. It employs the standard cmdlets of Powershell module "ActiveDirectory" for Windows. Details on a quick installation of this module to various editions of Windows will be given in the next chapter.

 

 

 

 

 

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
param(
  [string]$userName = [System.Environment]::UserName    # Valid formats: ID, domain\ID, ID@domain, ID@domain.com
  ,[bool]$useExtendedOutput = $false
)

######################################################## FUNCTIONS #########################################################
function Get-ADGroupRecursiveParentChain() {
param(
  [string]$distinguishedName                # Sample value: CN=TestGroupGlobal,CN=Users,DC=vanilla,DC=cgi,DC=com
  ,[array]$allGroups
)
  $group = Get-ADGroup -Identity $distinguishedName -Property MemberOf,ManagedBy
  if( !$? ) {
    throw
  }
  $tmp = ($allGroups |? {$_.distinguishedName -ieq $group.distinguishedName})
  if( $tmp -eq $null -or $tmp.Length -eq 0 ) {
    $allGroups += $group
  }
  $group.MemberOf | % {
    $nestedGroup = Get-ADGroup -Identity $_ -Property MemberOf,ManagedBy
    if( !$? ) {
      throw
    } 
    $tmp2 = ($allGroups |? {$_.distinguishedName -ieq $nestedGroup.distinguishedName})
    if( $tmp2 -eq $null -or $tmp2.Length -eq 0 ) {
      $allGroups += $nestedGroup
    }
    $allGroups = Get-ADGroupRecursiveParentChain -distinguishedName $nestedGroup.distinguishedName -allGroups $allGroups
  }
  
  return $allGroups
}
####################################################### //FUNCTIONS ########################################################

####################################################### EXECUTION ##########################################################
# Step 0. Perform validations to confirm the standard Windows Active Directory Powershell module has been installed.
try {
  Import-Module ActiveDirectory
  Get-ADDomain | out-null
} catch{
  write-host "The required Windows Powershell module 'ActiveDirectory' is not installed on this machine." `
    -ForegroundColor Magenta
  write-host "Use the following link and/or commands to install it and then execute this script again." `
    -ForegroundColor Magenta
  write-host
  write-host "https://4sysops.com/archives/how-to-install-the-powershell-active-directory-module"
  write-host
  write-host "Import-Module ActiveDirectory"
  write-host
  $message = "Alternatively, you can open a remote Powershell session to your server, which has" + `
    " 'ActiveDirectory' module installed. Refer to instructions in the file readme.txt in the same directory for details."
  write-host $message -ForegroundColor Magenta
  write-host
  return
}

# Step 1. Parse value of $userName to extract username and domain into separate parts
$accountName = $null
$domain = $null
$match = [regex]::Match($userName, "([^\\]+)\\(.+)")
if( $match.Success ) {
  $domain = $match.Groups[1].Value
  $accountName = $match.Groups[2].Value
} else {
  $match = [regex]::Match($userName, "([^@]+)@(.+)")
  if( $match.Success ) {
    $accountName = $match.Groups[1].Value
    $domain = $match.Groups[2].Value
  } else {
    $accountName = $userName
  }
}

# Step 2. Extract first level groups where the user is a direct member of.
$user = $null
$explicitGroups = $null
if( $domain ) {
  $user = Get-ADUser -Identity $accountName -Server $domain

  if( !$? ) {
    # Error, user not found.
    return
  }
  $explicitGroups = Get-ADPrincipalGroupMembership -Identity $accountName -Server $domain
  if( !$? ) {
    # Error, explicit groups not found.
    return
  }
} else {
  $user = Get-ADUser -Identity $accountName
  if( !$? ) {
    # Error, user not found.
    return
  }
  $explicitGroups = Get-ADPrincipalGroupMembership -Identity $accountName
  if( !$? ) {
    # Error, explicit groups not found.
    return
  }
}

# Step 3. Recursively extract groups of deeper levels where the first level groups are either direct or nested members of.
# Example 1. Put user1 is a direct member of group1. group1 is a direct member of group 2, group2 is a direct member of group3.
# So the full chain reads as user1 > group1 > group2 > group3. --> The logic should return group1, group2, group3 for user1.
# Example 2. Put user1 is a direct member of group1. group1 is a direct member of group 2. group2 also includes group3 as a member. 
# group3 does not contain user1
# So the full chain reads as user1 > group1 > group2. group 3 > group 2 --> The logic should return only group1 and group2 for user1.
$allGroups = @()
$explicitGroups | % {
  $allGroups = Get-ADGroupRecursiveParentChain -distinguishedName $_.distinguishedName -allGroups $allGroups
  if( !$? ) {
    # Error, nested groups not found.
    return
  }
}

# Step 4. Show the current user
write-host
$message = "UserID: " + $user.SamAccountName + " | Distinguished name: " + $user.distinguishedName
write-host $message

# Step 5. Enlist all groups where the user is either a direct member of or an indirect member resolved via nested group memberships.
# Example of simple-to-read view
if( $useExtendedOutput ) {
  $allGroups | sort -Property name `
    | ft -wrap `
    @{n="Group short name";e={$_.name}},
    @{n="Group distinguished name";e={$_.distinguishedName + [System.Environment]::NewLine}},
    @{n="Owner";e={(Get-ADObject -Identity $_ -Property nTSecurityDescriptor `
      | select -ExpandProperty nTSecurityDescriptor).Owner}},
    @{n="Manager";e={if($_.ManagedBy){(Get-ADObject -Identity $_.ManagedBy).name}}}

  # Example of more generic view
  #$allGroups | sort -Property name `
  #  | ft -wrap `
  #    Name,
  #    #DistinguishedName,
  #    @{n="Owner";e={if($_){(Get-ADObject -Identity $_ -Property nTSecurityDescriptor | select -ExpandProperty nTSecurityDescriptor).Owner}}},
  #    ManagedBy

  write-host "In total: $($allGroups.Count) groups"
  write-host
  
} else {
  $allGroups | sort -Property name `
    | ft -auto -wrap `
    @{n="Group short name";e={$_.name}},
    @{n="Owner";e={(Get-ADObject -Identity $_ -Property nTSecurityDescriptor `
      | select -ExpandProperty nTSecurityDescriptor).Owner}},
    @{n="Manager";e={if($_.ManagedBy){(Get-ADObject -Identity $_.ManagedBy).name}}}

  # Example of more generic view
  #$allGroups | sort -Property name `
  #  | ft -auto -wrap `
  #    Name,
  #    @{n="Owner";e={if($_){(Get-ADObject -Identity $_ -Property nTSecurityDescriptor | select -ExpandProperty nTSecurityDescriptor).Owner}}},
  #    ManagedBy

  write-host "Use an extra parameter -useExtendedOutput `$true to see the extended information"
  write-host
}

How to run this script on various Windows versions.

1. Copy the script given above and save it to some file, for example, c:\tmp\EnlistUserGroupMembershipsRecursively.ps1. The script has been tested in Powershell 2.0 and higher. In order to run the script successfully, you need to install and enable the standard Active Directory module for Windows Powershell that allows you to work with Active Directory objects using Powershell cmdlets.

- Detailed instructions how to do it can be found here: https://4sysops.com/archives/how-to-install-the-powershell-active-directory-module - There are chapters that describe module's installation on particular operating systems like Windows 7 / 8, Windows 2008 / 2012 servers. - After you have installed Active Directory Powershell module, remember to execute the following command: Import-Module ActiveDirectory

2. If you may not install anything on your local machine, you can use the alternative option to connect remotely to a server, which already has Active Directory module Powershell installed and also has connection to your Active Directory Domain Controller. - First of all, you need to enable Powershell remoting by executing the following command on your server and confirming operations. It should create correct Windows Firewall openings to grant access for remote WinRM operations. Enable-PSRemoting - More details can be found here: https://blogs.technet.microsoft.com/heyscriptingguy/2012/12/30/understanding-powershell-remote-management/ - Next, you need to enter your remote Powershell session on the server from your local machine. Execute the following commands. Enter-PSSession -ComputerName –credential domain\administrator Import-Module activedirectory Set-Location c:\ - More details can be found here: https://blogs.technet.microsoft.com/heyscriptingguy/2011/10/04/use-powershell-active-directory-cmdlets-without-installing-any-software/

3. After completion of step 1 above (or alternative step 2), execute the script EnlistUserGroupsRecursively.ps1 with optional parameter -userName, for example:

EnlistUserGroupsRecursively.ps1 # This call uses domain name of the current user. OR EnlistUserGroupsRecursively.ps1 -userName ID # This works for the current domain and uses the default DC. OR EnlistUserGroupsRecursively.ps1 -userName domain\ID OR EnlistUserGroupsRecursively.ps1 -userName ID@domain.com

4. Active directory groups do not have a preset special entity like "Owner". There is only an Owner of a securable AD object (AD group). In addition, each AD group may also have a Manager set via "Managed By" tab of Group Properties. In practice, sometimes namely this Manager is considered as an Owner. - An example of where to set a Group Manager: Open Start > Administrative Tools > Active Directory Users and Computers > Your Domain > Users > Double click on a desired group > Properties > Managed By

A sample output of this script is shown in the screenshot below.