If you read the verbose commands in the script, they will explain a bit of what is being done.
Begin
{
Write-Verbose "ENTER - BEGIN BLOCK"
Write-Verbose "Create User, Group and GroupList variables"
[System.Collections.ArrayList]$User = @()
[System.Collections.ArrayList]$Group = @()
[System.Collections.ArrayList]$GroupList = @()
Write-Verbose "Collect AD Domains"
$DomainList = (Get-ADForest).domains | Get-ADDomain | Select-Object DistinguishedName, DNSRoot | Sort-Object
Write-Verbose "EXIT - BEGIN BLOCK"
}
I use an [ArrayList] instead of a [Array] for the User, Group and GroupList, because you cannot remove an object from an array because it is a type with a fixed size. An array can increase in size but never decrease. An [ArrayList] on the other hand does have the remove method in its type.
The $DomainList variable is used to obtain the DisinguishedName and DNSRoot properties for each of the domains in the organization. The properties are used later to determine what domain the group is located in. This is important for when the group i.e. Administrators has Enterprise Admins nested from the parent domain, refer to lines 129 - 135.
The process block is quite extensive and handles all of the logic, I’m going to break this down into two pieces.
The first part I will discuss is the foreach loop created on line 100. I use the Get-ADGroupMember command to identify the members of the parent ad group based on what was provided for the Identity parameter. With each of the members, the switch statement identifies if the ObjectClass of the member is Group or not.
Lines 101 - 122.
Process
{
Write-Verbose "ENTER - PROCESS BLOCK"
Write-Verbose "ENTER - Foreach - $Identity"
Foreach ($Member in (Get-ADGroupMember -Identity $Identity)){
switch ($Member.ObjectClass) {
Group {
Write-Verbose "Nested AD Group Identified: $($Member.Name)"
If ($Member.DistinguishedName -notin $Group.DistinguishedName) {
$Group.Add($Member) | Out-Null
IF ($ListGroups) {
$GroupList.Add($Member) | Out-Null
}
}
Else {
Write-Verbose "$($Member.Name) is already identified, skipping to mitigate duplicate entry"
}
}
default{
If ($Member.DistinguishedName -notin $User.DistinguishedName) {
$User.Add($Member) | Out-Null
}
Else {
Write-Verbose "$($Member.Name) is already identified, skipping to mitigate duplicate entry"
}
}
}
}
Write-Verbose "EXIT - Foreach - $Identity"
To identify the Objectclass we can do this with an IF Statement or a Switch as seen below.
If ($member.ObjectClass -eq 'Group'){
If (Member.DistinguishedName -NotIn$Group.DistinguishedName){
$Group.Add($member) | Out-Null
}
}
Else{
If ($Member.DistinguishedName -notin $User.DistinguishedName){
$User.Add($Member) | Out-Null
}
}
The If statement is a viable option; however, the switch statement allows the same and increases the legibility of the code.
As each member is iterated through and the object is added either to the [ArrayList]$Group or [ArrayList]$User. If the -ListGroups parameter is used, groups are added to the [ArrayList]$GroupList.
The second piece of the Process block deals with the desired recursing of the nested groups to find additional nested groups. This is where the “MAGIC” happens. By using the Do-While loop, while [ArrayList] $Group -gt 0, resolves the issue of how many levels of nested groups can be drilled down. The answer is pretty much until the computer/server runs out of RAM.
As previously stated in a prior post, we identify the domain via a ForEach loop with $DomainList. The If statement checks the first group object distinguished name and uses the EndsWith() methods to check wether $Domain.DistinguishedName is at the end of $Group[0].DistinguishedName. If this is true, the $Domain.DNSRoot property is assigned to the Server parameter. DNSRoot is used because the -Server parameter for Get-ADGroupMember does not support DistinguishedName.
Lines 129 - 136
foreach ($Domain in $DomainList){
If ($Group[0].DistinguishedName.EndsWith($Domain.DistinguishedName)) {
$GetADGroup = @{
Identity = $Group[0].DistinguishedName
Server = $Domain.DNSRoot
}
}
}
The $GetADGroup variable is a hashtable and I use a method in PowerShell known as Splatting.
<p style=“padding-left: 40px;”>“Splatting is a method of passing a collection of parameter values to a command as a unit. PowerShell associates each value in the collection with a command parameter. Splatted parameter values are stored in named splatting variables, which look like standard variables, but begin with an At symbol (@) instead of a dollar sign ($)” - Microsoft About Splatting</p>
I digress, back to the script.
During the Do-While loop, the goal is only to look at one group at a time, this is accomplished by only indexing the first entry in [ArrayList] $Group. This is accomplished with [0] to the right of $Group. By using the distinguished name, I remove almost all ambiguity from the query, but this doesn’t work if the group is in multiple domains i.e. Domain Admins. This issue is resolved by comparing the end of the distinguished name matches the distinguished name of the AD domain.
At this point, the logic is a rinse and repeat as previously done in lines 100 - 123 and continues to repeat while the $Group -gt 0. Once the group has been processed, it is removed from the [Arraylist]$Group with the remove method. The default behavior is to provide all of the users, but if you use -ListGroups instead this will bypass all Object Classes, not equal to Group.
Woohoo! Finally done with that explanation. That is a lot of information to digest.