Created
March 1, 2019 12:14
-
-
Save evetsleep/b70c85e54e33a892cc38af492094dba3 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function PageMembers { | |
[CmdletBinding()]Param( | |
[Parameter(ValueFromPipeline)] | |
[System.DirectoryServices.SearchResult]$SearchResult | |
) | |
process { | |
<# | |
When getting a search result back for a group object you will likely get | |
either a member property OR a member;range=0-1499. If you get the later | |
then you need to page out the memberships by asking AD for more pages. For | |
example, if the first page is 0-999, then the next page would be 1000-1999 (if | |
our increment was 999). After every page you will get a member;range=#-# result, | |
however when you've reached the last page you'll get a result that looks like this: | |
mamber;range=#-* | |
The * means we're done and there are no more pages required. | |
#> | |
if ($SearchResult.properties.PropertyNames -match 'member;range=0-\d+') { | |
Write-Verbose -Message ('Large group: {0}' -f $SearchResult.properties.distinguishedname[0]) | |
# Initialize starting point as 0 and use $done to track if we have more to page out. | |
$from = 0 | |
$done = $false | |
$groupDN = $SearchResult.Properties.distinguishedname[0] | |
$groupEntry = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$GroupDN") | |
while ($done -eq $false) { | |
trap {$done = $true; continue} | |
$to = $from + 999 | |
$memberFilter = 'member;range={0}-{1}' -f $from,$to | |
Write-Verbose (' {0} Query range: {1}' -f $SearchResult.properties.distinguishedname[0],$memberFilter) | |
# Connect to and query for the group, but return a paged entry for member. | |
$ds = New-Object System.DirectoryServices.DirectorySearcher($groupEntry,"(objectClass=*)",$memberFilter,'Base') | |
$getMore = $ds.FindOne() | |
# Determine what the member property was that was returned. | |
$memberProp = $getMore.properties.PropertyNames | Where-Object { $PSItem -match '^member;range=\d+.*' } | |
Write-Verbose (' {0} Result range: {1}' -f $SearchResult.properties.distinguishedname[0],$memberProp) | |
# If we're done we need to set the DONE flag | |
if ($memberProp -match '^member;range=\d+-\*$') { $done = $true } | |
# Process the paged out members and write them out. | |
$getMore.properties.$memberProp | ForEach-Object { Write-Output $PSItem } | |
# Add 1000 to our current starting point. | |
$from += 1000 | |
} | |
} | |
elseif ($SearchResult.properties.PropertyNames -match '^member$') { | |
# If we got just a member property back then no paging required. | |
$SearchResult.properties.member | |
} | |
else { | |
Write-Verbose -Message ('No members: {0}' -f $SearchResult.properties.distinguishedname[0]) | |
} | |
} | |
} | |
function Get-ListMember { | |
<# | |
.SYNOPSIS | |
Returns the membership of a group in DN form. | |
.DESCRIPTION | |
Returns the membership of a group. If recursion is necessary just pass in the -Recursion parameter. When recursively parsing a groups members, depending on the size of the group, the performance could be quite slow since each member needs to be examined in AD to determine if the member is also a group. If you have a large and slow request you can pre-cache a list of DN's which are users or groups so that the processing is faster. This is in the form of the -Precache and DNCache parameters. If you specify -Precache, then the function will create a hash table of ALL users\groups on its own. If you specify -DNCache, you can build your own cache which must be in the form of: | |
KEY: DN | |
Value: USER OR GROUP | |
.PARAMETER Group | |
The name of the group to examine. If a regular name is entered we first look it up in AD and get the distinguished name and then examine the membership. If a DN is provided we go straight to connecting to that DN and processing it. | |
.PARAMETER Recursive | |
Tells the function to expand all group members which are groups as well. | |
.PARAMETER Precache | |
The function will pre-cache the DN:ObjectClass of all users\groups in AD. Depending on your network connection this may take a minute or two and is not always necessary depending on the size of the group(s) you're handling. | |
.PARAMETER DNCache | |
You can, if you want, pre-supply a pre-build hash table of DN:objectClass values. This is useful if you're going to be looking up more than one group recursively. An example of pre-building a cache would be: | |
$DNCache = @{} | |
Get-ADObject -Server "<GlobalCatalog>:3268" -Filter "objectClass -eq 'user' -or objectClass -eq 'group'" -Properties distinguishedname,objectClass | ForEach-Object { | |
$DNCache.Add($PSItem.distinguishedname,$PSItem.objectClass) | |
} | |
Then you could pass this to Get-ListMember to speed recursion up. | |
.PARAMETER memberHash | |
Do not populate this. This is used internally when performing recursive searches\expansions. | |
.PARAMETER groupHash | |
Do not populate this. This is used internally when performing recursive searches\expansions. | |
.EXAMPLE | |
(Get-ListMember -Group bigGroup).count | |
5171 | |
Here is a straight group listing (in this case a large group that requires paging). | |
.EXAMPLE | |
(Get-ListMember -Group smallGroup).count | |
10 | |
PS C:\> (Get-ListMember -Group smallGroup -Recursive).count | |
110 | |
You can see with the first query it looks like there are only 10 members, but that is because some of them are groups. If we recursively go through them we instead can see that there are 110 unique members of the group. | |
.EXAMPLE | |
$DNCache = @{} | |
PS C:\> Get-ADObject -Server "<GlobalCatalog>:3268" -Filter "objectClass -eq 'user' -or objectClass -eq 'group'" -Properties distinguishedname,objectClass | ForEach-Object { $DNCache.Add($PSItem.distinguishedname,$PSItem.objectClass) } | |
PS C:\> $lotsOfGroups | Foreach-Object { $members = Get-ListMember -Group $PSItem -Recursive -DNCache $DNCache; [PSCustomObject]@{Name=$PSItem;MemberCount=$members.count} } | |
We have a large list of groups and we want to get a count of all the members. Because some of the groups are nested we need to do a recursive expansion to get all of the members. The DNCache is pre-populated so we can more easily tell, without having to query for each member, what is a user and what is a group. Without the DNCache parameter, each time we see a new member we need to query AD to determine if it is a group (so we can expand that too). This will slow down processing in a very noticeable way if a pre-populated DN cache is not used. | |
.INPUTS | |
String | |
.OUTPUTS | |
String | |
#> | |
[CmdletBinding()]Param( | |
[Parameter(Mandatory)] | |
[String]$Group, | |
[Parameter()] | |
[Switch]$Recursive, | |
[Parameter()] | |
[Switch]$Precache, | |
[Parameter()] | |
$DNCache, | |
[Parameter()] | |
$memberHash, | |
[Parameter()] | |
$groupHash | |
) | |
try { | |
$GC = '{0}:3268' -f (Get-ADDomainController -Discover -Service GlobalCatalog -ErrorAction STOP | Select-Object -ExpandProperty hostname) | |
} | |
catch { | |
Write-Error -ErrorAction STOP -Message ('Failed to discover a global catalog: {0}' -f $PSItem.exception.message) | |
} | |
# Parse out an full DN for a group to be processed. | |
if ($Group -match '^CN=.*') { | |
$GroupDN = $Group | |
} | |
else { | |
try { | |
$GroupDN = Get-ADGroup -Filter "name -eq '$Group'" -Server $GC -ErrorAction STOP | Select-Object -ExpandProperty distinguishedname | |
if ( ($GroupDN | Measure-Object).Count -gt 1 ) { | |
Write-Error -ErrorAction STOP -Message ('Too many matches found for {0}: {1}. Please specify a DN instead.' -f $Group,$GroupDN.Count) | |
} | |
} | |
catch { | |
Write-Error -ErrorAction STOP -Message $PSItem.exception.message | |
} | |
} | |
# Used to keep track of the members we've already seen. If we see more that one we ignore all those that follow. | |
if (-not $memberHash) { | |
$memberHash = @{} | |
} | |
# Used to keep track of groups we've already processed. This prevents us from having a nested loop that lasts forever. | |
if (-not $groupHash) { | |
$groupHash = @{} | |
} | |
# Give us the ability to pre-load all user\group DN's to speed things along. For large processes this will go a long way | |
# to improving speed. | |
if ($Precache -and -not $DNCache) { | |
$DNCache = @{} | |
Get-ADObject -Server $GC -Filter "objectClass -eq 'user' -or objectClass -eq 'group'" -Properties distinguishedname,objectClass | ForEach-Object { | |
$DNCache.Add($PSItem.distinguishedname,$PSItem.objectClass) | |
} | |
Write-Verbose ('DN Cache loaded: {0}' -f $DNCache.Keys.Count) | |
} | |
# Start with our initial group and load it in. | |
Write-Verbose ('Processing: {0}' -f $GroupDN) | |
if ($groupHash.ContainsKey($GroupDN) -eq $false ) { | |
$groupHash.Add($groupDN,0) | |
} | |
# Create a DS object and query for members. | |
$groupEntry = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$GroupDN") | |
$groupSearcher = New-Object System.DirectoryServices.DirectorySearcher($groupEntry,"(objectClass=*)",@('distinguishedname','member'),'Base') | |
$groupObject = $groupSearcher.FindOne() | |
# If we're going to be recursive we need to do special things, otherwise we just page out the memberships. | |
if ($Recursive) { | |
$groupMemberList = New-Object System.Collections.Generic.List[String] | |
# Get the current membership (one level, no recursion) of the group. | |
$groupObject | PageMembers | ForEach-Object { $groupMemberList.Add($PSItem) } | |
foreach ($member in $groupMemberList) { | |
# If we've already seen either the user or group we don't need to process them again. | |
if ($memberHash.ContainsKey($member) -eq $true -or $groupHash.ContainsKey($member)) {continue} | |
# If we are using a cache of DN's we will want to check the cache before going to AD for | |
# looking up an object to determine its type. | |
try { | |
if ($DNCache) { | |
if ($DNCache.ContainsKey($member) ) { | |
switch ($DNCache[$member]) { | |
'group' { | |
Write-Verbose ('Cache hit(group): {0}' -f $member) | |
Get-ListMember -Group $member -memberHash $memberHash -groupHash $groupHash -DNCache $DNCache -Recursive | |
} | |
'user' { | |
Write-Verbose ('Cache hit(user): {0}' -f $member) | |
$memberHash.Add($member,0) | |
Write-Output $member | |
} | |
default { | |
[ADSI]$child = "LDAP://$member" | |
Write-Verbose ('Non-Cache hit({0}): {1}' -f $child.SchemaClassName,$member) | |
if ($child.SchemaClassName -eq 'group') { | |
Get-ListMember -Group $member -memberHash $memberHash -groupHash $groupHash -DNCache $DNCache -Recursive | |
} | |
else { | |
$memberHash.Add($member,0) | |
Write-Output $member | |
} | |
} | |
} | |
} | |
else { | |
# If we didn't see the DN in the cache we need to manually inspect it. | |
[ADSI]$child = "LDAP://$member" | |
Write-Verbose ('Non-Cache hit({0}): {1}' -f $child.SchemaClassName,$member) | |
if ($child.SchemaClassName -eq 'group') { | |
Get-ListMember -Group $member -memberHash $memberHash -groupHash $groupHash -DNCache $DNCache -Recursive | |
} | |
else { | |
$memberHash.Add($member,0) | |
Write-Output $member | |
} | |
} | |
} | |
else { | |
# If we haven't pre-loaded a cache we need to determine if a member is a group or not. | |
[ADSI]$child = "LDAP://$member" | |
if ($child.SchemaClassName -eq 'group') { | |
Get-ListMember -Group $member -memberHash $memberHash -groupHash $groupHash -Recursive | |
} | |
else { | |
$memberHash.Add($member,0) | |
Write-Output $member | |
} | |
} | |
} | |
catch { | |
Write-Error -ErrorAction STOP -Message ('Error processing {0}: {1}' -f $member,$PSItem.exception.message) | |
} | |
} | |
} | |
else { | |
# We add the members to the hash just for counting purposes, then spit out the DN. | |
$groupObject | PageMembers | ForEach-Object { $memberHash.Add($PSItem,0); $PSItem } | |
} | |
Write-Verbose ('Groups: {0}; Members: {1}' -f $groupHash.Keys.Count,$memberHash.Keys.Count) | |
} | |
Export-ModuleMember -Function Get-ListMember |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment