Private/DomainLookupHelpers.ps1

# LDAP and DC-locator access for Get-NSPADDomainInfo / Get-NSPADComputer. No AD module, no AD Web
# Services: the DC locator (DsGetDcName, the same lookup Windows logon uses) and plain LDAP. Tests
# mock these two functions.

function Find-NSPADDomainController {
    # A responsive DC: -Server is checked and returned as the DC's own name; otherwise the locator
    # picks one for this computer's domain, forcing rediscovery so a DC that went bad since the
    # last lookup isn't reused from cache.
    param([string]$Server)
    if ($Server) {
        $context = [System.DirectoryServices.ActiveDirectory.DirectoryContext]::new('DirectoryServer', $Server)
        return [System.DirectoryServices.ActiveDirectory.DomainController]::GetDomainController($context).Name
    }
    $domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain()
    $domain.FindDomainController([System.DirectoryServices.ActiveDirectory.LocatorOptions]::ForceRediscovery).Name
}

function Read-NSPADRootDse {
    # The DC's RootDSE naming contexts. RootDSE is read as an entry, not searched: a
    # DirectorySearcher rooted at RootDSE fails ("the provider does not support searching").
    param([Parameter(Mandatory)][string]$Server)
    $rootDse = [ADSI]"LDAP://$Server/RootDSE"
    @{
        defaultnamingcontext       = [string]$rootDse.defaultNamingContext[0]
        configurationnamingcontext = [string]$rootDse.configurationNamingContext[0]
    }
}

function Read-NSPADLdap {
    # One LDAP search against one DC. Returns one hashtable per result: lower-case property name ->
    # first value (byte arrays such as objectSid stay byte arrays), or every value for the names in
    # -MultiValued (an array). -Scope Base reads one object (constructed attributes such as
    # tokenGroups only come back from a Base read).
    param(
        [Parameter(Mandatory)][string]$Server,
        [Parameter(Mandatory)][string]$SearchBase,
        [string]$Filter = '(objectClass=*)',
        [Parameter(Mandatory)][string[]]$Property,
        [ValidateSet('Base', 'OneLevel', 'Subtree')][string]$Scope = 'Subtree',
        [string[]]$MultiValued = @()
    )
    $root = [ADSI]"LDAP://$Server/$($SearchBase -replace '/', '\/')"
    $searcher = New-Object System.DirectoryServices.DirectorySearcher($root, $Filter)
    $searcher.SearchScope = $Scope
    foreach ($name in $Property) { [void]$searcher.PropertiesToLoad.Add($name) }
    $multi = @($MultiValued | ForEach-Object { $_.ToLowerInvariant() })
    $results = $searcher.FindAll()
    try {
        foreach ($result in $results) {
            $item = @{}
            foreach ($name in $result.Properties.PropertyNames) {
                $key = $name.ToLowerInvariant()
                $item[$key] = if ($multi -contains $key) { , @($result.Properties[$name]) } else { $result.Properties[$name][0] }
            }
            $item
        }
    } finally { $results.Dispose() }
}