Go Premium for a chance to win a PS4. Enter to Win

x
?
Solved

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

Posted on 2011-09-28
4
Medium Priority
?
2,277 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

Featured Post

What is SQL Server and how does it work?

The purpose of this paper is to provide you background on SQL Server. It’s your self-study guide for learning fundamentals. It includes both the history of SQL and its technical basics. Concepts and definitions will form the solid foundation of your future DBA expertise.

Question has a verified solution.

If you are experiencing a similar issue, please ask a related question

There are times when we need to generate a report on the inbox rules, where users have set up forwarding externally in their mailbox. In this article, I will be sharing a script I wrote to generate the report in CSV format.
High user turnover can cause old/redundant user data to consume valuable space. UserResourceCleanup was developed to address this by automatically deleting user folders when the user account is deleted.
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…
There are cases when e.g. an IT administrator wants to have full access and view into selected mailboxes on Exchange server, directly from his own email account in Outlook or Outlook Web Access. This proves useful when for example administrator want…

971 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