Hey, Scripting Guy! How Can I Generate a List of All Groups of Which a User Is a Member?

Hey, Scripting Guy! How Can I Generate a List of All Groups of Which a User Is a Member?

  • Comments 18
  • Likes

Bookmark and Share

Hey, Scripting Guy! Question

Hey, Scripting Guy! I need to get a list of all the groups of which a user is a member. I do not mean the groups of which the user is a direct member, but I need to know all of the groups. You know that groups can be members of other groups, and other groups can be member of still further groups. There is no way using the GUI to identify all of the indirect group memberships. The reason I am asking for this is because I recently found a user that had domain administrator rights! But when I looked on the Member Of tab, the Domain Admins group was not listed. When I went to the Domain Admins group, the user was not listed there either. What I need is a sort of "Resultant Set of Groups" tool.

-- DM

Hey, Scripting Guy! Answer

Hello DM,

Microsoft Scripting Guy Ed Wilson here. I really wish I had a fireplace in my study. I have one in the living room, but it is downstairs. I could, of course, take my laptop downstairs, snuggle up in front of the fireplace with my leather moose footstool, and write my article, but I do not like typing for extended periods of time on the laptop keyboard. I do have the Microsoft Desktop 7000 keyboard I take with me when I travel, but I guess overall, I am more comfortable in my study—I just wish I had a fireplace. Oh, well. It is cold and raining outside, which makes it a great day for fireplaces and hot tea. I am drinking a nice cup of Dragon Well (Longjing) tea, and munching on English walnuts with a small chunk of artisan cheese. Cream is on the Zune, and things are humming along nicely.

DM, you are absolutely correct about needing a "Resultant Set of Groups" tool. Before the introduction of the .NET Framework 3.5, if you wanted to figure out all of the group memberships for a user, you had to do the following:

1.     Identify the groups to which the user belonged.

2.     Find the groups that were members of that group.

3.     Determine if the group was a member of other groups.

4.     Find each of the other groups, and see if the user was a member of those groups.

No matter which scripting language was used to perform the query, the process was a bit convoluted and confusing. In the end, one was never certain if the script or the results were correct. Other than manual inspection, there was not a good way to test the script for logic errors. The Member Of tab from a user named bob is seen in the following image. This tab shows membership in two groups:

Image of user bob as member of two groups


As you will see later, the user bob is a member of more than two groups. But there appears to be no way to find out this information, other than to do a lot of clicking.

All of that has changed with .NET Framework 3.5 because of the introduction of a new class. The Get-UsersGroupMemberShips.ps1 script which I wrote for you illustrates using the new UserPrincipal .NET Framework class from the System.DirectoryServices.AccountManagement namespace. The complete Get-UsersGroupMemberShips.ps1 script is seen here.

Get-UsersGroupMemberShips.ps1

Function New-Underline($Text)
{
  "`n$Text`n$(`"-`" * $Text.length)"
} #end New-UnderLine

Function Test-DotNetFrameWork35
{
 Test-path -path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5'
} #end Test-DotNetFrameWork35

Function Get-UserPrincipal($cName, $cContainer, $userName)
{
 $dsam = "System.DirectoryServices.AccountManagement"
 $rtn = [reflection.assembly]::LoadWithPartialName($dsam)
 $cType = "domain" #context type
 $iType = "SamAccountName"
 $dsamUserPrincipal = "$dsam.userPrincipal" -as [type]
 $principalContext = new-object "$dsam.PrincipalContext"($cType,$cName,$cContainer)
 $dsamUserPrincipal::FindByIdentity($principalContext,$iType,$userName)
} # end Get-UserPrincipal

# *** Entry Point to Script ***

If(-not(Test-DotNetFrameWork35)) { “Requires .NET Framework 3.5” ; exit }

[string]$userName = "bob"
[string]$cName = "NWTraders"
[string]$cContainer = "dc=nwtraders,dc=com"

$userPrincipal = Get-UserPrincipal -userName $userName -cName $cName -cContainer $cContainer

New-UnderLine -Text "Direct Group MemberShip:"
$userPrincipal.getGroups() | foreach-object { $_.name }

New-UnderLine -Text "Indirect Group Membership:"
$userPrincipal.GetAuthorizationGroups()  | foreach-object { $_.name }

 

The first thing the Get-UsersGroupMemberShips.ps1 script does is create the New-UnderLine function. This function is used to provide separation between the output heading and the displayed groups. The New-Underline function accepts a single parameter named –text. The first thing the function does is print a blank line (by using the `n character). The text is then displayed followed by another blank line. The cool thing is way the "-" character is multiplied by the length of the input text. The length property is always available on strings. The use of character multiplication is illustrated here:

PS C:\> $s = "abcde"

PS C:\> $s.Length

5

PS C:\> "-" * $s.Length

-----

PS C:\>

The complete New-UnderLine function is seen here.

Function New-Underline($Text)

{

  "`n$Text`n$(`"-`" * $Text.length)"

} #end New-UnderLine

Because the script will not work unless the .NET Framework 3.5 is installed on the computer, it makes sense to test for this requirement. If the .NET Framework 3.5 does not exist, the script will exit. To perform the test, the Test-DotNetFrameworks35 function is used. In the function the Test-Path cmdlet is used to look for a registry key that is created when .NET Framework 3.5 is installed. The Test-DotNetFramework35 function is seen here:

Function Test-DotNetFrameWork35

{

 Test-path -path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5'

} #end Test-DotNetFrameWork35

The key to the script is the Get-UserPrincipal function. To use the userPrincipal and the principalContext .NET Framework classes, the assembly that contains those classes must be first be loaded. In Windows PowerShell 2.0 you can use the Add-Type cmdlet, but because Windows PowerShell 2.0 has not yet officially shipped and because you may still be working with Windows PowerShell 1.0 computers, the Get-UserPrincipal function uses the "old fashioned" way to load the assembly by using a static method from the [reflection.assembly] .NET Framework class. The name of the namespace is a bit long, and it is stored in a variable to make the code easier to read. This also saves the trouble of repeating the namespace name several times in the script. This section of the script is seen here:

Function Get-UserPrincipal($cName, $cContainer, $userName)

{

 $dsam = "System.DirectoryServices.AccountManagement"

 $rtn = [reflection.assembly]::LoadWithPartialName($dsam)

Two more variables are created. The first variable holds the context type for the query, and the second variable is used to hold the account type. The –as [type] construction is used to create the instance of the userPrincipal .NET Framework class. This is shown here:

$cType = "domain" #context type

$iType = "SamAccountName"

$dsamUserPrincipal = "$dsam.userPrincipal" -as [type]

It is time to create a new principalContext class. To do this, the principalContext constructor that accepts three arguments is used. The first parameter for the constructor is the context type; the second and third parameters are strings. The ContextType is an enumeration value from the system.directoryServices.AccountManagement namespace. The New-Object cmdlet is used to create the new instance of the class, and the returned PrincipalContext object is stored in the $principalContext variable. This line of the script is shown here:

$principalContext = new-object "$dsam.PrincipalContext"($cType,$cName,$cContainer)

The last thing to do in the Get-UserPrincipal function is to call the FindByIdentity static method from the userprincipal object that was created earlier. The script uses the version (overload) of the FindByIdentity static method that accepts three parameters. The three parameters are a principalContext object that is stored in the $principalContext variable, an IdentityType enumeration value that is stored in the $iType variable, and a string that represents the identity of the user principal object. The user principal object that is retrieved by the FindByIdentity method is returned to the section of the script that calls the Get-UserPrincipal function. The FindByIdentity method call is shown here:

$dsamUserPrincipal::FindByIdentity($principalContext,$iType,$userName)

The complete Get-UserPrincipal function is seen here:

Function Get-UserPrincipal($cName, $cContainer, $userName)

{

 $dsam = "System.DirectoryServices.AccountManagement"

 $rtn = [reflection.assembly]::LoadWithPartialName($dsam)

 $cType = "domain" #context type

 $iType = "SamAccountName"

 $dsamUserPrincipal = "$dsam.userPrincipal" -as [type]

 $principalContext = new-object "$dsam.PrincipalContext"($cType,$cName,$cContainer)

 $dsamUserPrincipal::FindByIdentity($principalContext,$iType,$userName)

} # end Get-UserPrincipal

The entry point to the script creates several variables. The variables are the user name whose groups will be listed, the NetBIOS name of the domain, and the LDAP version of the domain name. These variables are seen here:

[string]$userName = "bob"

[string]$cName = "NWTraders"

[string]$cContainer = "dc=nwtraders,dc=com"

The Get-UserPrincipal function is called and the three newly created variables are passed to it. The returned userPrincipal object is stored in the $userPrincipal variable. This is shown here:

$userPrincipal = Get-UserPrincipal -userName $userName -cName $cName -cContainer $cContainer

The group membership report is created by calling the getGroups method from the userPrincipal object and the getAuthorizationGroups method from the same object. This section of the script is shown here:

New-UnderLine -Text "Direct Group MemberShip:"

$userPrincipal.getGroups() | foreach-object { $_.name }

 

New-UnderLine -Text "Indirect Group Membership:"

$userPrincipal.GetAuthorizationGroups()  | foreach-object { $_.name }

When the Get-UsersGroupMemberShips.ps1 script is run, the results shown in the following image are seen:

Image of results from running the script


Well, DM, this wraps up searching for users’ indirect group memberships. This also wraps up Searching Active Directory Week. Tomorrow we open the scripter@microsoft.com inbox, and look at several questions that don’t require long answers.

If you want to know exactly what we will be covering tomorrow, follow us on Twitter or Facebook. If you have any questions, send e-mail to us at scripter@microsoft.com or post them on the Official Scripting Guys Forum. See you tomorrow. Until then, peace. 

Ed Wilson and Craig Liebendorfer, Scripting Guys

 

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • Hello!,
    Will this work out-of the box from a Windows 8 machine (no .NET 3.5)?

  • #updated for Windows 8 and modern PowerShell

    Function Get-UserPrincipal{
    Param(
    [string]$SamAccountName=$env:username,
    [string]$UserDomain=$env:userDomain,
    [string]$Container=[System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain().GetDirectoryEntry().Distinguishedname
    )
    $ctx = New-Object System.DirectoryServices.AccountManagement.PrincipalContext('domain',$UserDomain,$Container)
    [System.DirectoryServices.AccountManagement.UserPrincipal]::FindByIdentity($ctx,'SamAccountName',$SamAccountName)
    }

    $principal=Get-UserPrincipal

    $principal.getGroups() | ForEach-Object{ $_.name }
    $principal.GetAuthorizationGroups()|foreach-object { $_.name }

  • Try again - updated for Windows 8 and modern PowerShell.

    Function Get-UserPrincipal{
    Param(
    [string]$SamAccountName=$env:username,
    [string]$UserDomain=$env:userDomain,
    [string]$Container=[System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain().GetDirectoryEntry().Distinguishedname
    )
    $ctx = New-Object System.DirectoryServices.AccountManagement.PrincipalContext('domain',$UserDomain,$Container)
    [System.DirectoryServices.AccountManagement.UserPrincipal]::FindByIdentity($ctx,'SamAccountName',$SamAccountName)
    }

    $principal=Get-UserPrincipal

    $principal.getGroups() | ForEach-Object{ $_.name }
    $principal.GetAuthorizationGroups()|foreach-object { $_.name }

  • It is clear that the comments on this site continue to be broken.

  • thanks