function Get-ADTargetServer { <# .SYNOPSIS Resolves the appropriate Active Directory domain controller for a given identity. .DESCRIPTION Queries the Global Catalog to locate which domain an AD object belongs to, then discovers and returns a suitable domain controller for that domain. Supports forest root and all child domains without hardcoded server references. This is a reusable helper function consumed by other module functions such as Get-QAUser and any future functions requiring domain-aware DC resolution. .PARAMETER Identity The identity to look up. Accepts SamAccountName, UPN, email address, display name, DistinguishedName, or CanonicalName. .PARAMETER GlobalCatalog Optional. The Global Catalog server (port 3268) to use for initial forest-wide lookup. If omitted, the function auto-discovers a GC in the current site. .PARAMETER PreferredSite Optional. AD site name to prefer when discovering a domain controller. Useful in multi-site environments to avoid cross-site DC resolution. .OUTPUTS PSCustomObject with properties: DomainController, Domain, SamAccountName, DistinguishedName .EXAMPLE Get-ADTargetServer -Identity "jdoe" .EXAMPLE Get-ADTargetServer -Identity "john.doe@yourcompany.com" .EXAMPLE Get-ADTargetServer -Identity "jdoe" -PreferredSite "SiteA" .EXAMPLE Get-ADTargetServer -Identity <distinguishedName> .EXAMPLE Get-ADTargetServer -Identity <samaccountname> .NOTES Module : MyModule Author : Infrastructure Team Version : 1.0.0 Requires : ActiveDirectory module #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string]$Identity, [Parameter(Mandatory = $false)] [string]$GlobalCatalog, [Parameter(Mandatory = $false)] [string]$PreferredSite ) process { # ── GC Resolution ───────────────────────────────────────────────────── # Auto-discover a GC in the current forest if none is explicitly provided. if (-not $GlobalCatalog) { try { Write-Verbose "No GlobalCatalog specified — discovering one in the current forest." $discoveredGC = Get-ADDomainController -Discover -Service GlobalCatalog -ErrorAction Stop | Select-Object -ExpandProperty HostName | Select-Object -First 1 $GlobalCatalog = '{0}:3268' -f $discoveredGC Write-Verbose "Using auto-discovered Global Catalog: $GlobalCatalog" } catch { throw "Failed to discover a Global Catalog server. Provide one explicitly via -GlobalCatalog. Error: $_" } } else { if ($GlobalCatalog -notmatch ':\d+$') { $GlobalCatalog = '{0}:3268' -f $GlobalCatalog Write-Verbose "Appended GC port — using: $GlobalCatalog" } } # ── Step 1: Forest-wide identity resolution via Global Catalog ──────── # The GC holds a partial replica of every domain in the forest, making it # the correct and only reliable place to search across all domains. Write-Verbose "Searching Global Catalog '$GlobalCatalog' for identity '$Identity'." $mailWildcard = "$Identity@*" try { $gcResult = Get-ADUser -Filter { (SamAccountName -eq $Identity) -or (UserPrincipalName -eq $Identity) -or (EmailAddress -eq $Identity) -or (Mail -like $mailWildcard) -or (Name -eq $Identity) -or (ObjectGUID -eq $Identity) -or (DistinguishedName -eq $Identity) } -Properties UserPrincipalName, SamAccountName, DistinguishedName, CanonicalName ` -Server $GlobalCatalog ` -ErrorAction Stop } catch { throw "Global Catalog query failed against '$GlobalCatalog' for '$Identity'. Error: $_" } if (-not $gcResult) { Write-Warning "Identity '$Identity' was not found in the forest via Global Catalog '$GlobalCatalog'." return $null } if ($gcResult.Count -gt 1) { $matchList = ($gcResult | ForEach-Object { $_.UserPrincipalName }) -join ', ' throw "Ambiguous identity '$Identity' matched multiple objects: $matchList. Provide a more specific identity." } # ── Step 2: Extract the domain from the DistinguishedName ───────────── # Example DN: CN=John Doe,OU=Users,DC=child,DC=root,DC=local # Extracted: child.root.local $dn = $gcResult.DistinguishedName $domainDNS = ($dn -split ',' | Where-Object { $_ -match '^DC=' } | ForEach-Object { ($_ -split '=')[1] } ) -join '.' Write-Verbose "Resolved domain '$domainDNS' from DN: $dn" if (-not $domainDNS) { throw "Could not derive a domain DNS name from DistinguishedName: '$dn'" } # ── Step 3: Discover a writable DC in the resolved domain ───────────── # -Writable ensures we never land on a Read-Only DC (RODC). try { $discoverParams = @{ DomainName = $domainDNS Writable = $true ErrorAction = 'Stop' } if ($PreferredSite) { Write-Verbose "Preferring AD site '$PreferredSite' for DC discovery in domain '$domainDNS'." $discoverParams['SiteName'] = $PreferredSite } $targetDC = Get-ADDomainController -Discover @discoverParams | Select-Object -ExpandProperty HostName | Select-Object -First 1 } catch { if ($PreferredSite) { Write-Warning "No DC found in site '$PreferredSite' for domain '$domainDNS'. Falling back to any available DC." try { $targetDC = Get-ADDomainController -Discover -DomainName $domainDNS -Writable -ErrorAction Stop | Select-Object -ExpandProperty HostName | Select-Object -First 1 } catch { throw "Failed to discover any writable DC for domain '$domainDNS'. Error: $_" } } else { throw "Failed to discover a writable DC for domain '$domainDNS'. Error: $_" } } Write-Verbose "Resolved domain controller: $targetDC (Domain: $domainDNS)" # ── Output ──────────────────────────────────────────────────────────── [PSCustomObject]@{ DomainController = $targetDC Domain = $domainDNS SamAccountName = $gcResult.SamAccountName DistinguishedName = $dn } } }
How to Use
Dot-source the file or include it in your module, then call it with any supported identity format. The function resolves across all domains in the forest — no domain parameter needed.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| -Identity | [string] | Required | SamAccountName, UPN, email address, display name, DistinguishedName, or ObjectGUID. |
| -GlobalCatalog | [string] | Optional | GC server for forest-wide lookup. Port 3268 is appended automatically. If omitted, a GC is auto-discovered in the current site. |
| -PreferredSite | [string] | Optional | AD site name to prefer for DC discovery. Falls back to any available writable DC if no DC exists in the specified site. |
Usage Examples
# Resolve by SamAccountName Get-ADTargetServer -Identity "jdoe" # Resolve by UPN Get-ADTargetServer -Identity "john.doe@root.local" # Resolve by email address Get-ADTargetServer -Identity "j.doe@yourcompany.com" # Resolve with site preference (multi-site environment) Get-ADTargetServer -Identity "jdoe" -PreferredSite "London-HQ" # Supply a specific GC — useful when auto-discovery is unreliable Get-ADTargetServer -Identity "jdoe" -GlobalCatalog "gc01.root.local" # Use the resolved DC in a downstream AD command $target = Get-ADTargetServer -Identity "jdoe" Get-ADUser -Identity $target.SamAccountName -Server $target.DomainController -Properties * # Pipeline — resolve multiple identities "jdoe", "asmith", "bwilson" | Get-ADTargetServer
Sample Output
DomainController : dc02.child.root.local Domain : child.root.local SamAccountName : jdoe DistinguishedName : CN=John Doe,OU=Users,DC=child,DC=root,DC=local
Production Notes
GC Port Reachability
Port 3268 must be reachable from the host running the script. In environments with strict firewall segmentation between sites, verify before relying on auto-discovery: Test-NetConnection <gc-hostname> -Port 3268
RODC Avoidance
The function explicitly requests a writable DC (-Writable) at every discovery step. This prevents routing write operations to Read-Only Domain Controllers in branch office deployments. Ensure at least one writable DC is reachable per domain in the forest.
Ambiguous Identity Handling
If the supplied identity matches more than one AD object — common with display name lookups — the function throws with all matching UPNs listed. Always use SamAccountName, UPN, or email in automated pipelines to avoid ambiguous matches.
Module Integration Pattern
Designed as a helper consumed by other module functions. In QAModule v1.7, every AD write operation calls Get-ADTargetServer first to resolve the correct DC — this eliminates cross-domain write failures caused by hardcoded or assumed DC references.