Solved

Use Powershell to Fetch Nested Group Memberships in AD (groups only)

Posted on 2011-09-28
4
2,049 Views
Last Modified: 2012-05-12
Hello,

Challenge: convert AD mail-enabled groups from global to universal type. I need to know the group structure before I start converting groups because you cannot convert a global member group to universal when it's member of a global parent group. The conversion has to be top-down in order to work.

I came across the attached code (don't remember the site to give proper credit to the author, sorry), which does almost what I'm trying to do with a couple of slight modifications:

1) The function takes one group and crawls its structure
2) The return value is numeric instead of a list of groups (list of group DNs would be ideal)

Here's the AD groups

GroupA        GroupB              GroupC
    |                   |                         |
    GroupD        GroupE               GroupF
                             |
                             GroupG


The output should look something similar to this (list of DNs):

Level 1  Level 2   Level3  ...
GroupA  GroupD  
GroupB  GroupE  GroupG
GroupC  GroupF

I'd like to modify the attached code to 1) crawl all mail-enabled groups in AD and 2) display results in a list format with the levels (or heights of the groups) so that I can convert each level (top-down).
Param (
    [Parameter(Mandatory=$true,
        Position=0,
        ValueFromPipeline=$true,
        HelpMessage="DN or ObjectGUID of the AD Group."
    )]
    [string]$groupIdentity,
    [switch]$showTree
    )
#Validate Quest PSSnapin is loaded
Add-PSSnapin -Name Quest.ActiveRoles.ADManagement -ErrorAction SilentlyContinue
$global:numberOfRecursiveGroupMemberships = 0
$lastGroupAtALevelFlags = @() 

function Get-GroupNesting ([string] $identity, [int] $level, [hashtable] $groupsVisitedBeforeThisOne, [bool] $lastGroupOfTheLevel)
{
    $group = $null
    $group = Get-QADGroup -Identity $identity -SizeLimit 0
    if($lastGroupAtALevelFlags.Count -le $level)
    {
        $lastGroupAtALevelFlags = $lastGroupAtALevelFlags + 0
    }
    if($group -ne $null)
    {
        if($showTree)
        {
            for($i = 0; $i -lt $level - 1; $i++)
            {
                if($lastGroupAtALevelFlags[$i] -ne 0)
                {
                    Write-Host -ForegroundColor Blue -NoNewline "  "
                }
                else
                {
                    Write-Host -ForegroundColor Blue -NoNewline "?  "
                }
            }
            if($level -ne 0)
            {
                if($lastGroupOfTheLevel)
                {
                    Write-Host -ForegroundColor Blue -NoNewline "?€??"
                }
                else
                {
                    Write-Host -ForegroundColor Blue -NoNewline "?¥??"
                }
            }
            Write-Host -ForegroundColor Blue $group.Name
        }
        $groupsVisitedBeforeThisOne.Add($group.DN,$null)
        $global:numberOfRecursiveGroupMemberships ++
        $groupMemberShipCount = $group.memberOf.Count
        if ($groupMemberShipCount -gt 0)
        {
            $maxMemberGroupLevel = 0
            $count = 0
            foreach($groupDN in $group.memberOf)
            {
                $count++
                $lastGroupOfThisLevel = $false
                if($count -eq $groupMemberShipCount){$lastGroupOfThisLevel = $true; $lastGroupAtALevelFlags[$level] = 1}
                if(-not $groupsVisitedBeforeThisOne.Contains($groupDN)) #prevent cyclic dependancies
                {
                    $memberGroupLevel = Get-GroupNesting -Identity $groupDN -Level $($level+1) -GroupsVisitedBeforeThisOne $groupsVisitedBeforeThisOne -lastGroupOfTheLevel $lastGroupOfThisLevel
                    if ($memberGroupLevel -gt $maxMemberGroupLevel){$maxMemberGroupLevel = $memberGroupLevel}
                }
            }
            $level = $maxMemberGroupLevel
        }
        else #we've reached the top level group, return it's height
        {
            return $level
        }
        return $level
    }
}
$global:numberOfRecursiveGroupMemberships = 0
$groupObj = Get-QADGroup -Identity $groupIdentity -SizeLimit 0
if($groupObj)
{
    [int]$maxNestingLevel = Get-GroupNesting -Identity $groupIdentity -Level 0 -GroupsVisitedBeforeThisOne @{} -lastGroupOfTheLevel $false
	Add-Member -InputObject $groupObj -MemberType NoteProperty  -Name MaxNestingLevel -Value $maxNestingLevel -Force
    Add-Member -InputObject $groupObj -MemberType NoteProperty  -Name NestedGroupMembershipCount -Value $($global:numberOfRecursiveGroupMemberships - 1) -Force
	$groupObj | Select-Object Name,DN,MaxNestingLevel,NestedGroupMembershipCount | Format-List
}

Open in new window

0
Comment
Question by:bndit
  • 3
4 Comments
 
LVL 35

Expert Comment

by:YZlat
ID: 36719918
0
 
LVL 2

Author Comment

by:bndit
ID: 36818376
@YZlat
Thanks for the link. However, the script I'm trying to use uses the Quest cmlets, which is not a big deal I can do the conversion. I ran into a problem when I copied and pasted the script...not sure what I'm missing.
error.png
0
 
LVL 2

Accepted Solution

by:
bndit earned 0 total points
ID: 37149371
I found *exactly* what I was looking for.  Here's the article that explains the methodology and the script. The author of the script is Stefan Kowalewski and he wrote an awesome script!! Hope this helps someone like it helped me.

http://www.windowsitpro.com/article/windows-powershell/powershell-script-lists-group-hierarchies-in-any-ldap-directory
function GetMembers($getMembersEntry)
{
	# set the scope to base for this search operation
    $baseScope = [System.DirectoryServices.Protocols.SearchScope]"Base"

	# create a search request for this attribute scoped query for groups
	# the search filter is being reused since this code returns groups as well
	$getMembersSearchRequest = New-Object `
	  System.DirectoryServices.Protocols.SearchRequest(
	    $getMembersEntry.DistinguishedName, 
		  $searchFilter, $baseScope, $attributes)
	
	# create AsqRequestControl object and specify the attribute to query
	# in this case, the member attribute of the root group specified by the 
	# $getMembersEntry.DistinguishedName
	$getMemberAsqRequest = New-Object `
	  System.DirectoryServices.Protocols.AsqRequestControl("member")
	
	# add the AsqRequestControl object to search request 
	# directory control collection
	[Void]$getMembersSearchRequest.Controls.Add($getMemberAsqRequest)
	
	# get search reponse object
	[System.DirectoryServices.Protocols.SearchResponse]$getMemberSearchResponse `
	  = $connection.SendRequest($getMembersSearchRequest)
	
	if ($getMemberSearchResponse.ResultCode -eq "success")
	{
		if ($searchResponse.Controls.Length -eq 1 `
		  -and $getMemberSearchResponse.Controls[0] -is 
		    [System.DirectoryServices.Protocols.AsqResponseControl])
		{
			# cast the directory control into an AsqResponseControl object
			[System.DirectoryServices.Protocols.AsqResponseControl] `
			  $getMemberAsqResponse = `
			    [System.DirectoryServices.Protocols.AsqResponseControl] `
				  $getMemberSearchResponse.Controls[0]

			# assign entries to new variable
			$memberEntries = $getMemberSearchResponse.Entries
			
			# check member entries count
			# if count is greater than zero, get members for that group
			# if count is zero,  output members
			if ($memberEntries.Count -gt 0)
			{
				# iterate through the returned LDAP objects to build 
				# the Groups list
				foreach ($childEntry in $memberEntries)
				{
					# add entry onto stack
					$groupStack.Push($childEntry.Attributes["cn"][0].ToString())
					
					GetMembers($childEntry)
					
					# remove entry from stack
					[Void]$groupStack.Pop()
				}
			}
			else
			{
				# invert stack for output
				# create temp stack so we don't affect the group stack
				$tempStack = $groupStack.Clone()
				
				# create our output stack
				$outputStack = New-Object System.Collections.Stack
		
				# invert stack
				while($tempStack.Count -ne 0)
				{
					$outputStack.Push($tempStack.Pop())
				}
				
				# create output sting
				[string]$stackOutputter = "" 
				
				# populate output string
				while ($outputStack -ne 0)
				{
					$stackOutputter += $outputStack.Pop() + ", "
				}
				
				# output contents, truncate the last two
				# characters to get rid of the comma and space
				Write-Host $stackOutputter.Remove($stackOutputter.Length - 2, 2)
			}
		}
		else
		{
			echo ""
			Write-Host "The server cannot return ASQ results" `
			  -ForegroundColor red
			echo ""
			exit
		}
	}
	else
	{
		echo ""
		Write-Host "The following result code was returned 
		  by the directory server: " `
		    $getMemberSearchResponse.ResultCode ". Error message:" `
			  + $getMemberSearchResponse.ErrorMessage -ForegroundColor red
		echo ""
		exit
	}
}

# declare and initialize a variable that references the 
# System.DirectoryServices.Protocols assembly
$directoryServicesProtocolsCore = 
	New-Object System.Reflection.AssemblyName(
		"System.DirectoryServices.Protocols, Version=2.0.0.0, 
		Culture=Neutral, PublicKeyToken=b03f5f7f11d50a3a")
# then load this assembly
[Void][Reflection.Assembly]::Load($directoryServicesProtocolsCore)

# check command line parameters
if ($args.Length -ne 2)
{
	echo ""
	Write-Host "Specify the domain name and the distinguished name of an OU
	  or container (CN) to evaluate" -ForegroundColor yellow
	Write-Host "example: GetGroupRelationships amer.corp.eds.com 
	  ou=Groups,dc=amer,dc=corp,dc=eds,dc=com" -ForegroundColor yellow
	echo ""
	exit
}

# set domain name and distinguished name based on passed in parameters
[string]$domainName = $args[0]
[string]$distinguishedName = $args[1]

# set variables
# stack to hold groups
[System.Collections.Stack]$groupStack = New-Object System.Collections.Stack
# search filter for all searches being performed
[string]$searchFilter = "(&(objectClass=group)(objectCategory=group))"
# search attribute for all searches being performed
[string[]]$attributes = "cn"

# set search scope level
$oneLevelScope = [System.DirectoryServices.Protocols.SearchScope]"OneLevel"

# variables for establishing a connection to the directory
$identifier = New-Object `
	System.DirectoryServices.Protocols.LdapDirectoryIdentifier($domainName)
$connection = New-Object `
	System.DirectoryServices.Protocols.LdapConnection($identifier)

# create search request object
$searchRequest = New-Object `
	System.DirectoryServices.Protocols.SearchRequest(
	  $distinguishedName, $searchFilter, $oneLevelScope, $attributes)

# add the sort request control object to sort on common name (cn)
$sortRequest = New-Object `
	System.DirectoryServices.Protocols.SortRequestControl("cn", $false)

# add the sort request object to the search request directory control collection
[Void]$searchRequest.Controls.Add($sortRequest)

# registering this exception before sending the request to an LDAP 
# server in order to gracefully handle the error and present 
# meaningful information to the reader
trap [System.DirectoryServices.Protocols.DirectoryOperationException]
{
	# unremark the next two lines to get more information about the
	# trapped error.
	# Write-Error $("Trapped: " + $_.Exception.GetType().FullName);
	# Write-Error $("Error: " + $_.Exception.Message);
	Write-Host "Verify that the distinguished name to the container you entered
	  is correct and enclosed in quotes" -ForegroundColor red
	continue;
}

# get search response
$searchResponse = $connection.SendRequest($searchRequest)

# process the server response containing all groups in an OU
if ($searchResponse.ResultCode -eq "success")
{
	if ($searchResponse.Entries.Count -gt 0)
	{
		if ($searchResponse.Controls.Length -eq 1 `
		  -and $searchResponse.Controls[0] -is `
		    [System.DirectoryServices.Protocols.SortResponseControl])
		{
			# iterate through the returned LDAP objects to build the Group list
			foreach ($entry in $searchResponse.Entries)
			{
				# add entry to stack
				$groupStack.push($entry.Attributes["cn"][0].ToString())
				
				GetMembers($entry)
				
				# remove the entry from stack
				[Void]$groupStack.Pop()
			}
		}
		else
		{
			echo ""
			Write-Host "The expected search response was not returned" `
			  -ForegroundColor red
			echo ""
			exit
		}
	}
}

Open in new window

0
 
LVL 2

Author Closing Comment

by:bndit
ID: 37169547
Found the solution on my own.
0

Join & Write a Comment

This is a PowerShell web interface I use to manage some task as a network administrator. Clicking an action button on the left frame will display a form in the middle frame to input some data in textboxes, process this data in PowerShell and display…
Create and license users in Office 365 in bulk based on a CSV file. A step-by-step guide with PowerShell script examples.
This tutorial will walk an individual through the process of transferring the five major, necessary Active Directory Roles, commonly referred to as the FSMO roles from a Windows Server 2008 domain controller to a Windows Server 2012 domain controlle…
This tutorial will walk an individual through the process of transferring the five major, necessary Active Directory Roles, commonly referred to as the FSMO roles to another domain controller. Log onto the new domain controller with a user account t…

747 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question

Need Help in Real-Time?

Connect with top rated Experts

13 Experts available now in Live!

Get 1:1 Help Now