internal/functions/resolve/Resolve-User.ps1
|
<#
.SYNOPSIS Resolves one or more users (id / GUID, UPN, displayName, special tokens) with caching and optional object expansion. .DESCRIPTION Modernized resolver implementing array-based bulk resolution. GUID inputs are centralized through Resolve-DirectoryObject (single getByIds batches) instead of per-user GET or $batch requests, reducing round-trips and leveraging shared caches. Non-GUID inputs (displayName / UPN) still use filtered /users queries via Graph $batch for efficiency. Special tokens supported verbatim: All, None, GuestsOrExternalUsers. Behavior: - Accepts [string[]] $InputReference; returns ordered array when multiple inputs, scalar when single (backward compatible). - -Expand returns detail object { id, displayName, userPrincipalName }. - -DisplayName returns only displayName (fast path with DirectoryObject batch for GUID inputs). - Caches id, displayName, userPrincipalName under each of those keys for fast subsequent lookups. Limitations / Notes: - displayName -> id lookup still requires filtered endpoint; /directoryObjects/getByIds cannot perform name search. - For GUIDs we always retrieve detail once (even if only id requested) to warm cache for potential later displayName requests. #> function Resolve-User { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string[]] $InputReference, [switch] $DontFailIfNotExisting, [switch] $SearchInDesiredConfiguration, [switch] $Expand, [switch] $UserPrincipalName, [System.Management.Automation.PSCmdlet] $Cmdlet = $PSCmdlet ) begin { if ($InputReference.Count -gt 1) { $InputReference = $InputReference | ForEach-Object { Resolve-String -Text $_ } } else { $InputReference = Resolve-String -Text $InputReference[0] } if (-not $script:userDetailCache) { $script:userDetailCache = @{} } } process { # Array handling wrapper if ($InputReference -is [array] -and $InputReference.Count -gt 1) { # Early exit if everything already cached (excluding special values) if (Test-TmfInputsCached -CacheName 'userDetailCache' -Inputs $InputReference -SkipValues @('All', 'None', 'GuestsOrExternalUsers')) { $results = foreach ($i in $InputReference) { if ($i -in @('All', 'None', 'GuestsOrExternalUsers')) { $i } elseif ($script:userDetailCache.ContainsKey($i)) { if ($Expand) { $script:userDetailCache[$i] } elseif ($UserPrincipalName) { $script:userDetailCache[$i].userPrincipalName } else { $script:userDetailCache[$i].id } } else { $i } } return , $results } $prefetch = { param($all) $needDetail = $true # Always fetch detail for GUIDs to warm cache # GUID inputs centralized via Resolve-DirectoryObject $guidIds = $all | Where-Object { $_ -match $script:guidRegex } | Select-Object -Unique if ($guidIds.Count -gt 0) { try { $resolvedObjs = Resolve-DirectoryObject -InputReference $guidIds -Types user -ReturnObjects -DontFailIfNotExisting if ($resolvedObjs) { foreach ($obj in $resolvedObjs) { if ($obj -is [string]) { continue } # unresolved echoed $detail = [pscustomobject]@{ id = $obj.id; displayName = $obj.displayName; userPrincipalName = $obj.userPrincipalName } Add-TmfCacheEntries -CacheName 'userDetailCache' -Objects @($detail) -KeyProperties id, displayName, userPrincipalName } } } catch { Write-PSFMessage -Level Verbose -Message ("Resolve-DirectoryObject bulk user prefetch failed: {0}" -f $_.Exception.Message) -Tag 'prefetch', 'user', 'directoryObject' -ErrorRecord $_ } } # Non-GUID inputs (displayName / UPN) still use $batch filtered queries $requests = @(); $idMap = @{}; $ridCounter = 0 $toResolve = $all | Where-Object { -not $script:userDetailCache.ContainsKey($_) -and ($_ -notmatch $script:guidRegex) -and ($_ -notin @('All', 'None', 'GuestsOrExternalUsers')) } if ($toResolve.Count -gt 0) { $displayNames = $toResolve | Where-Object { $_ -notmatch $script:upnRegex } $upns = $toResolve | Where-Object { $_ -match $script:upnRegex } foreach ($dn in $displayNames) { $ridCounter++; $rid = "d$ridCounter"; $escaped = $dn -replace "'", "''" $requests += @{ id = $rid; method = 'GET'; url = "/users?`$filter=displayName eq '$escaped'&`$select=id,displayName,userPrincipalName" } $idMap[$rid] = $dn } foreach ($u in $upns) { $ridCounter++; $rid = "u$ridCounter"; $escaped = $u -replace "'", "''" $requests += @{ id = $rid; method = 'GET'; url = "/users?`$filter=userPrincipalName eq '$escaped'&`$select=id,displayName,userPrincipalName" } $idMap[$rid] = $u } } for ($i = 0; $i -lt $requests.Count; $i += 20) { $chunk = $requests[$i..([Math]::Min($i + 19, $requests.Count - 1))] try { $body = @{ requests = $chunk } | ConvertTo-Json -Depth 6 $response = Invoke-MgGraphRequest -Method POST -Uri ("$script:graphBaseUrl/`$batch") -Body $body -ContentType 'application/json' if ($response -and $response.responses) { foreach ($r in $response.responses) { if ($r.status -ge 200 -and $r.status -lt 300 -and $r.body) { $origInput = if ($idMap.ContainsKey($r.id)) { $idMap[$r.id] } else { $null } $match = if ($r.body.value) { $r.body.value | Select-Object -First 1 } else { $r.body } if ($match) { $detail = [pscustomobject]@{ id = $match.id; displayName = $match.displayName; userPrincipalName = $match.userPrincipalName } Add-TmfCacheEntries -CacheName 'userDetailCache' -Objects @($detail) -KeyProperties id, displayName, userPrincipalName if ($origInput -and -not $script:userDetailCache.ContainsKey($origInput)) { $script:userDetailCache[$origInput] = $detail } } } } } } catch { Write-PSFMessage -Level Warning -Message ("User batch prefetch failed chunk starting index {0}: {1}" -f $i, $_.Exception.Message) -Tag 'batch', 'failed' -ErrorRecord $_ } } } $single = { param($one) if ($script:userDetailCache.ContainsKey($one)) { if ($Expand) { return $script:userDetailCache[$one] } elseif ($UserPrincipalName) { return ($script:userDetailCache[$one].userPrincipalName) } else { return $script:userDetailCache[$one].id } } return (Resolve-User -InputReference $one -DontFailIfNotExisting -Expand:$Expand -UserPrincipalName:$UserPrincipalName -SearchInDesiredConfiguration:$SearchInDesiredConfiguration -Cmdlet $Cmdlet) } return Invoke-TmfArrayResolution -Inputs $InputReference -Prefetch $prefetch -ResolveSingle $single } try { # Fast path cache checks if (-not $Expand -and -not $UserPrincipalName -and $script:userDetailCache.ContainsKey($InputReference)) { return $script:userDetailCache[$InputReference].id } if (-not $Expand -and $UserPrincipalName -and $script:userDetailCache.ContainsKey($InputReference)) { return ($script:userDetailCache[$InputReference].userPrincipalName) } if ($InputReference -in @('None', 'All', 'GuestsOrExternalUsers')) { if ($Expand) { return [pscustomobject]@{ id = $InputReference; displayName = $InputReference; userPrincipalName = $InputReference } } return $InputReference } $fullObj = $null; $resolvedId = $null; $needDetail = $Expand -or $UserPrincipalName if ($InputReference -match $script:guidRegex) { if ($script:userDetailCache.ContainsKey($InputReference)) { if ($Expand) { return $script:userDetailCache[$InputReference] } elseif ($UserPrincipalName) { return ($script:userDetailCache[$InputReference].userPrincipalName) } else { return $script:userDetailCache[$InputReference].id } } try { $resolved = Resolve-DirectoryObject -InputReference $InputReference -Types user -ReturnObjects -DontFailIfNotExisting if ($resolved -and $resolved -isnot [string]) { $fullObj = [pscustomobject]@{ id = $resolved.id; displayName = $resolved.displayName; userPrincipalName = $resolved.userPrincipalName } Add-TmfCacheEntries -CacheName 'userDetailCache' -Objects @($fullObj) -KeyProperties id, displayName, userPrincipalName $resolvedId = $fullObj.id } elseif (-not $resolvedId) { if ($DontFailIfNotExisting) { return $InputReference } else { throw "Cannot find user $InputReference" } } } catch { if ($DontFailIfNotExisting) { Write-PSFMessage -Level Warning -Message ("Cannot resolve User (GUID) '{0}': {1}" -f $InputReference, $_.Exception.Message) -Tag failed -ErrorRecord $_; return $InputReference } else { throw } } } elseif ($InputReference -match $script:upnRegex -or $InputReference -match "#EXT#") { try { $fullObj = (Invoke-MgGraphRequest -Method GET -Uri ("$script:graphBaseUrl/users?`$filter=userPrincipalName eq '{0}'&`$select=id,displayName,userPrincipalName" -f [System.Web.HttpUtility]::UrlEncode($InputReference))).value | Select-Object -First 1; $resolvedId = $fullObj.id } catch { if ($DontFailIfNotExisting) { Write-PSFMessage -Level Warning -Message ("Cannot resolve User (UPN) '{0}': {1}" -f $InputReference, $_.Exception.Message) -Tag failed -ErrorRecord $_; return $InputReference } else { throw } } } else { try { $fullObj = (Invoke-MgGraphRequest -Method GET -Uri ("$script:graphBaseUrl/users?`$filter=displayName eq '{0}'&`$select=id,displayName,userPrincipalName" -f $InputReference)).value | Select-Object -First 1; $resolvedId = $fullObj.id } catch { if ($DontFailIfNotExisting) { Write-PSFMessage -Level Warning -Message ("Cannot resolve User (displayName) '{0}': {1}" -f $InputReference, $_.Exception.Message) -Tag failed -ErrorRecord $_; return $InputReference } else { throw } } } if (-not $resolvedId -and $SearchInDesiredConfiguration) { if ($InputReference -in $script:desiredConfiguration['users'].displayName) { $resolvedId = $InputReference } } if (-not $resolvedId) { if ($DontFailIfNotExisting) { return $InputReference } else { throw "Cannot find user $InputReference" } } if (-not $Expand) { if ($UserPrincipalName) { return ($fullObj.userPrincipalName) }; return $resolvedId } if (-not $fullObj) { $fullObj = [pscustomobject]@{ id = $resolvedId; displayName = $null; userPrincipalName = $null } } $detail = [pscustomobject]@{ id = $fullObj.id; displayName = $fullObj.displayName; userPrincipalName = $fullObj.userPrincipalName } foreach ($key in @($detail.id, $detail.displayName, $detail.userPrincipalName)) { if ($key -and -not $script:userDetailCache.ContainsKey($key)) { $script:userDetailCache[$key] = $detail } } return $detail } catch { if ($DontFailIfNotExisting) { Write-PSFMessage -Level Warning -Message ("Cannot resolve User '{0}': {1}" -f $InputReference, $_.Exception.Message) -Tag failed -ErrorRecord $_; return $InputReference } else { Write-PSFMessage -Level Warning -Message ("Cannot resolve User '{0}': {1}" -f $InputReference, $_.Exception.Message) -Tag failed -ErrorRecord $_; $Cmdlet.ThrowTerminatingError($_) } } } } |