
While working on a Citrix Cloud migration project in a multi-forest environment, there was a need to migrate existing AD group members that were used on-prem to Azure AD groups.
To do this, I needed to report of current group membership for all Citrix-published apps. There was also a requirement to know the parent group of each user since there were many randomly nested groups from the local domain and other domains from trusted forests that were not necessarily needed.
The challenge was that many of those groups had foreign principals (and possibley orphened) in other domains/forests as well as other nested groups:
Parent Group | User/Group | Domain |
Group A | User1 | XYZ.com |
Group A | User2 | ForeginDomain.com |
Group A | Group B | XYZ.com |
Group B | User3 | XYZ.com |
Group B | User4 | ForeginDomain.com |
You would think Get-ADgroupMember with a -recursive switch should cater for such, however, the command only returns one or more principal objects that represent users, computers, or groups that are members of the specified group. It doesn’t return members of other trusted forests. It also will cause the cmdlet to return an unspecified error if any foreign principal member of the queried group is orphaned and no longer exists in their original domain.
The way around this is to use Get-ADGroup Cmdlet and look at the Member property.This propery lists the distiguishedname of all member of an AD group includeing foregin security principals.
1 2 3 4 5 6 |
#Get details of groups queried by the function to find their members $GroupProperties = Get-ADGroup -Identity $($GroupEntry) -Properties * -Server $Server -ErrorAction SilentlyContinue #Get the "members" property of the groups and pipe it into Get-ADObject $NestedGroupsAndUsers = $GroupProperties.Members | Get-ADObject -Properties * -Server $Server -ErrorAction SilentlyContinue |
Knowing this, we can pipe this data into Get-ADObject and get the Object SID. We can then use the SecurityIdentifier .Net class with the Translate Method to get the NTAccount Name from the SID.
1 2 3 |
$ObjectNameFromSID = ([System.Security.Principal.SecurityIdentifier]$row.name).Translate([System.Security.Principal.NTAccount]) |
From here we go and get the actual object from its original domain using an LDAPQuery. To do this, we workout the domain name using the NetBiosName
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
Try { ##Find the DNSRoot name (Domain full name) based on the current user/group being processed e.g. domain\user If ($ObjectNameFromSID) { $CurrentUserDomainFullName = $DomainsTable.where({$_.NetBIOSName -like ($ObjectNameFromSID.value.split("\")[0])}).DNSRoot } } Catch { Write-Error "Could not find a matching domain for $($ObjectNameFromSID) from the list of provided domains: $($DomainsHashTable.NetBIOSName) .Full error: $($Error[0])" #Clean up variables $LDAPQuery = $CurrentUserDomainFullName = $ObjectNameFromSID = $null #skip just this iteration, but continue loop Continue NestedRows } Try { #Call the domain found above and retrieve the users data $LDAPQuery = [ADSI]"LDAP://$($CurrentUserDomainFullName)/<SID=$($row.name)>" } Catch { Write-Error 'Error querying AD via LDAP: [ADSI]"LDAP://$($CurrentUserDomainFullName)/<SID=$($row.name)>"'+ "Full error: $($error[0])" #Clean up variables $LDAPQuery = $CurrentUserDomainFullName = $ObjectNameFromSID = $null #skip just this iteration, but continue loop Continue NestedRows } |
If the object is not found in its original domain, a PSObject is created with a set of properties and added to the results array
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
If ($null -eq $ObjectNameFromSID) { $NewPSObj = New-Object -TypeName PSobject $NewPSObj | Add-Member -MemberType NoteProperty -Name "ParentGroup" -Value "$($GroupEntry)" -Force $NewPSObj | Add-Member -MemberType NoteProperty -Name "Status" -Value "Couldn't find in target domain" -Force $NewPSObj | Add-Member -MemberType NoteProperty -Name "Name" -Value "$($row.name)" -Force $NewPSObj | Add-Member -MemberType NoteProperty -Name "DisplayName" -Value "$($row.DisplayName)" -Force $AllMemberObjects += $NewPSObj $NewPSObj = $null #Clean up variables $LDAPQuery = $CurrentUserDomainFullName = $ObjectNameFromSID = $null #skip just this iteration, but continue loop Continue } |
If the object is found and it was for a user object, we use LDAP to get the object properties from the original domain.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#If the current retrieved object is a user, grab their information if ($LDAPQuery.SchemaClassName -eq "user") { $NewPSObj = New-Object -TypeName PSobject $NewPSObj = Get-ADObject -Identity $($LDAPQuery.distinguishedName) -Server $CurrentUserDomainFullName -Properties * $NewPSObj | Add-Member -MemberType NoteProperty -Name "ParentGroup" -Value "$($GroupEntry)" -Force $NewPSObj | Add-Member -MemberType NoteProperty -Name "ObjectDomain" -Value "$($CurrentUserDomainFullName)" -Force $AllMemberObjects += $NewPSObj $NewPSObj = $null #Clean up variables $LDAPQuery = $CurrentUserDomainFullName = $ObjectNameFromSID = $null } |
If the found object was a group, the function calls itself for the said group recursively
1 2 3 4 5 6 |
#If the current retrieved object is a group, recursivly call the function again for that group elseif ($LDAPQuery.SchemaClassName -eq "group") { Get-ForeignAndLocalMembers -Groups "$($LDAPQuery.sAMAccountName)" -Server $($CurrentUserDomainFullName) -Domains $Domains } |
Finally, if the object was not a foreign security pricipal i.e. local to the domain of its parent group, we use Get-ADObject as normal to fetch the data
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
else { #If the object (group or user) is local to the current domain, query it using get-adobject $NewPSObj = New-Object -TypeName PSobject $NewPSObj = Get-ADObject -Identity $row.distinguishedName -Server $Server -Properties * #Add member property to display the parent group and domain name of the current object $NewPSObj | Add-Member -MemberType NoteProperty -Name "ParentGroup" -Value "$($GroupEntry)" -Force $NewPSObj | Add-Member -MemberType NoteProperty -Name "ObjectDomain" -Value "$($Server)" -Force #pipe the new PS object containing the AD object and the added ParentGroup property into the results array $AllMemberObjects += $NewPSObj $NewPSObj = $null #Clean up variables $LDAPQuery = $CurrentUserDomainFullName = $ObjectNameFromSID = $null } |
The function can be downloaded from my Github Repo. Examples of usage can be found in the function help.
Happy scripting!