Private/Convert/Resolve-Principal.ps1
|
function Resolve-Principal { <# .SYNOPSIS Resolves an IdentityReference to a complete principal object and caches it in the PrincipalStore. .DESCRIPTION Takes a System.Security.Principal.IdentityReference object (either NTAccount or SecurityIdentifier) and resolves it to a complete principal object via LDAP query. The resolved principal is cached in the module-level PrincipalStore for fast subsequent lookups, and a DirectoryEntry is returned. On domain-joined computers, uses the built-in Translate() method. On non-domain joined computers, performs an LDAP query using provided credentials to resolve the SID to an NTAccount. Supports forest-wide searches using Global Catalog when RootDSE is provided, enabling resolution of principals from child domains and trusted domains within the forest. .PARAMETER IdentityReference The IdentityReference object to convert. Can be either NTAccount or SecurityIdentifier. .PARAMETER Credential PSCredential for authenticating to Active Directory. Required for LDAP queries. .PARAMETER RootDSE A DirectoryEntry object for the RootDSE. Used to determine the domain context for LDAP queries. If not specified, attempts to query without specific domain context. .INPUTS System.Security.Principal.IdentityReference Accepts IdentityReference objects (NTAccount or SecurityIdentifier) via the pipeline. .OUTPUTS System.DirectoryServices.DirectoryEntry Returns the DirectoryEntry object for the principal with the specified SID. .EXAMPLE $sid = [System.Security.Principal.SecurityIdentifier]::new('S-1-5-21-...') $sid | Resolve-Principal -Credential $cred -RootDSE $rootDSE Resolves a SID to a DirectoryEntry and caches the principal. .EXAMPLE $ntAccount = [System.Security.Principal.NTAccount]::new('DOMAIN\User') $ntAccount | Resolve-Principal -Credential $cred -RootDSE $rootDSE Resolves an NTAccount to a DirectoryEntry and caches the principal. .EXAMPLE $ace.IdentityReference | Resolve-Principal -Credential $cred -RootDSE $rootDSE Resolves IdentityReferences from an ACL to DirectoryEntry objects and caches them. .NOTES Requires Credential and RootDSE parameters for LDAP queries. Uses Global Catalog for forest-wide searches to support child domain resolution. #> [CmdletBinding()] [OutputType([System.DirectoryServices.DirectoryEntry])] param( [Parameter(Mandatory, ValueFromPipeline)] [System.Security.Principal.IdentityReference] $IdentityReference ) begin { # Initialize Principal Store if it doesn't exist # Store for IdentityReference → Full principal object with all properties if (-not $script:PrincipalStore) { $script:PrincipalStore = @{} } } process { # Convert IdentityReference to SID for store key $sidKey = $IdentityReference | Convert-IdentityReferenceToSid if (-not $sidKey) { Write-Warning "Could not convert IdentityReference to SID: $($IdentityReference.Value)" return $null } $sidString = $sidKey.Value # Try to get NTAccount name for the SID (for well-known principals) $ntAccountName = if ($IdentityReference -is [System.Security.Principal.NTAccount]) { # Already have the friendly name $IdentityReference.Value } elseif ($IdentityReference -is [System.Security.Principal.SecurityIdentifier]) { # Try to translate SID to NTAccount to get friendly name try { $ntAccount = $IdentityReference.Translate([System.Security.Principal.NTAccount]) $ntAccount.Value } catch { Write-Verbose "Could not translate SID '$sidString' to NTAccount. $_" $null } } else { $null } # Check store first - use SID as store key if ($script:PrincipalStore.ContainsKey($sidString)) { $storedPrincipal = $script:PrincipalStore[$sidString] Write-Verbose "Store HIT: Found stored principal for SID '$sidString': $($storedPrincipal.distinguishedName)" # Create fresh DirectoryEntry from stored DN $objectPath = "LDAP://$script:Server/$($storedPrincipal.distinguishedName)" $objectEntry = New-AuthenticatedDirectoryEntry -Path $objectPath return $objectEntry } Write-Verbose "Store MISS: No stored DN found for SID '$sidString', performing LDAP lookup" try { # First try Global Catalog search for forest-wide lookup Write-Verbose "Attempting Global Catalog search for SID '$sidString'" $gcSearcher = New-GCSearcher -Filter "(objectSid=$sidString)" -PropertiesToLoad @('distinguishedName', 'objectSid', 'sAMAccountName', 'objectClass', 'displayName', 'memberOf', 'userPrincipalName') if ($gcSearcher) { try { $gcResult = $gcSearcher.FindOne() if ($gcResult) { $distinguishedName = $gcResult.Properties['distinguishedName'][0] Write-Verbose "Found SID in GC at: $distinguishedName" # Build complete principal object for store $principalObject = [LS2Principal]::new($gcResult, $script:Server, $sidKey, $ntAccountName) # Store the complete principal object using SID as key $script:PrincipalStore[$sidString] = $principalObject Write-Verbose "Stored principal object for SID '$sidString': $distinguishedName (objectClass: $($principalObject.objectClass))" # Return DirectoryEntry for the found object $objectPath = "LDAP://$script:Server/$distinguishedName" $objectEntry = New-AuthenticatedDirectoryEntry -Path $objectPath Write-Verbose "Resolved SID '$sidString' to '$distinguishedName' via Global Catalog" return $objectEntry } } catch { Write-Verbose "Global Catalog search failed, falling back to domain search: $_" } finally { if ($gcSearcher) { $gcSearcher.Dispose() } } } # Fallback to direct LDAP search in default domain Write-Verbose "Attempting direct LDAP search for SID '$sidString'" $domainDN = if ($script:RootDSE) { $script:RootDSE.defaultNamingContext.Value } else { $null } if (-not $domainDN) { Write-Warning "Could not determine domain DN for SID resolution." return $null } # Create LDAP searcher with credentials $searcher = New-LDAPSearcher -DomainDN $domainDN -Filter "(objectSid=$sidString)" -PropertiesToLoad @('distinguishedName', 'objectSid', 'sAMAccountName', 'objectClass', 'displayName', 'memberOf', 'userPrincipalName') $result = $searcher.FindOne() if ($result) { $distinguishedName = $result.Properties['distinguishedName'][0] # Build complete principal object for store $principalObject = [LS2Principal]::new($result, $script:Server, $sidKey, $ntAccountName) # Store the complete principal object using SID as key $script:PrincipalStore[$sidString] = $principalObject Write-Verbose "Stored principal object for SID '$sidString': $distinguishedName (objectClass: $($principalObject.objectClass))" # Return DirectoryEntry for the found object $objectPath = "LDAP://$script:Server/$distinguishedName" $objectEntry = New-AuthenticatedDirectoryEntry -Path $objectPath Write-Verbose "Resolved SID '$sidString' to '$distinguishedName' via LDAP" return $objectEntry } else { Write-Warning "Could not find SID '$sidString' in Active Directory via LDAP query." # For well-known SIDs that don't exist in AD, create a minimal store entry # This includes BUILTIN groups and other system principals if ($ntAccountName) { Write-Verbose "Creating store entry for well-known SID '$sidString' with name '$ntAccountName'" $principalObject = [LS2Principal]::new($sidString, $ntAccountName) # Store the principal object $script:PrincipalStore[$sidString] = $principalObject Write-Verbose "Stored well-known principal for SID '$sidString': $ntAccountName" } return $null } } catch { Write-Warning "LDAP query failed for SID '$sidString': $_" return $null } finally { if ($searcher) { $searcher.Dispose() } } } } |