Solved

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

Posted on 2011-09-28
4
2,188 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
[X]
Welcome to Experts Exchange

Add your voice to the tech community where 5M+ people just like you are talking about what matters.

  • Help others & share knowledge
  • Earn cash & points
  • Learn & ask questions
  • 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

Office 365 Training for Admins - 7 Day Trial

Learn how to provision tenants, synchronize on-premise Active Directory, implement Single Sign-On, customize Office deployment, and protect your organization with eDiscovery and DLP policies.  Only from Platform Scholar.

Question has a verified solution.

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

Uncontrolled local administrators groups within any organization pose a huge security risk. Because these groups are locally managed it becomes difficult to audit and maintain them.
Group policies can be applied selectively to specific devices with the help of groups. Utilising this, it is possible to phase-in group policies, over a period of time, by randomly adding non-members user or computers at a set interval, to a group f…
This tutorial will walk an individual through the process of configuring their Windows Server 2012 domain controller to synchronize its time with a trusted, external resource. Use Google, Bing, or other preferred search engine to locate trusted NTP …
Attackers love to prey on accounts that have privileges. Reducing privileged accounts and protecting privileged accounts therefore is paramount. Users, groups, and service accounts need to be protected to help protect the entire Active Directory …
Suggested Courses

615 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