internal/functions/resolve/Resolve-DirectoryObject.ps1
|
function Resolve-DirectoryObject { <# .SYNOPSIS Resolves one or more Azure AD / Entra directory object IDs (GUID inputs) with caching & optional detail retrieval. .DESCRIPTION Central bulk resolver used by higher-level Resolve-* functions for GUID inputs. Supports array input preserving order. Populates two script caches: - $script:directoryObjectCache : id -> id (fast existence / id mapping) - $script:directoryObjectDetailCache : id -> detail PSCustomObject { id, displayName, mailNickname, userPrincipalName } When -ReturnObjects is specified, detail objects are returned (using getByIds first, falling back to single GET). Non-GUID inputs are currently not resolved (future enhancement placeholder) and echoed / cause failure depending on -DontFailIfNotExisting. Implements chunked POST /directoryObjects/getByIds (<=100 ids per request). Uses provided -Types filter when supplied. Limitations: - /directoryObjects/getByIds cannot resolve displayName -> id; callers must use resource-specific resolvers for that scenario. - Only a subset of properties is cached; expand if future scenarios need more. # TODO: Add Pester tests (CI-002, CI-005, POW-FUNC-028) for: (a) array ordering, (b) mixed cached + uncached, (c) ReturnObjects vs ids, (d) DontFailIfNotExisting behavior. #> [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [string[]] $InputReference, [string[]] $Types, [switch] $ReturnObjects, [switch] $DontFailIfNotExisting, [System.Management.Automation.PSCmdlet] $Cmdlet = $PSCmdlet ) begin { if (-not $script:directoryObjectCache) { $script:directoryObjectCache = @{} } if (-not $script:directoryObjectDetailCache) { $script:directoryObjectDetailCache = @{} } if ($InputReference) { $InputReference = $InputReference | ForEach-Object { Resolve-String -Text $_ } } } process { if (-not $InputReference) { return } if ($InputReference.Count -gt 1) { $original = $InputReference $guidInputs = @(); $results = New-Object System.Collections.Generic.List[object] foreach ($token in $original) { if ($script:directoryObjectCache.ContainsKey($token)) { # Already have at least id; supply detail if requested and available if ($ReturnObjects -and $script:directoryObjectDetailCache.ContainsKey($token)) { $results.Add($script:directoryObjectDetailCache[$token]) } else { $results.Add($script:directoryObjectCache[$token]) } } elseif ($token -match $script:guidRegex) { $guidInputs += $token } else { if ($DontFailIfNotExisting) { Write-PSFMessage -Level Warning -Message ("DirectoryObject non-GUID reference '{0}' not resolved (not supported by getByIds)." -f $token) -Tag 'resolve', 'directoryObject', 'nonguid'; $results.Add($token) } else { $results.Add($token) } } } # Fetch uncached GUIDs (detail always fetched if ReturnObjects to satisfy property needs) $toFetch = $guidInputs | Where-Object { -not $script:directoryObjectCache.ContainsKey($_) -or ($ReturnObjects -and -not $script:directoryObjectDetailCache.ContainsKey($_)) } | Select-Object -Unique for ($i = 0; $i -lt $toFetch.Count; $i += 100) { $chunk = $toFetch[$i..([Math]::Min($i + 99, $toFetch.Count - 1))] try { $body = @{ ids = $chunk }; if ($Types) { $body.types = $Types } $resp = Invoke-MgGraphRequest -Method POST -Uri ("$script:graphBaseUrl/directoryObjects/getByIds") -Body ($body | ConvertTo-Json) -ContentType 'application/json' if ($resp.value) { foreach ($obj in $resp.value) { if (-not $obj.id) { continue } if (-not $script:directoryObjectCache.ContainsKey($obj.id)) { $script:directoryObjectCache[$obj.id] = $obj.id } # Always capture detail object (enables later ReturnObjects without another network call) if (-not $script:directoryObjectDetailCache.ContainsKey($obj.id)) { $detail = [pscustomobject]@{ id = $obj.id; displayName = $obj.displayName; mailNickname = $obj.mailNickname; userPrincipalName = $obj.userPrincipalName } $script:directoryObjectDetailCache[$obj.id] = $detail } } } } catch { Write-PSFMessage -Level Warning -Message ("Bulk directoryObjects getByIds failed starting {0}: {1}" -f $chunk[0], $_.Exception.Message) -Tag 'bulk', 'directoryObject', 'failed' -ErrorRecord $_ } finally { } } # Finalize preserving original order $final = foreach ($token in $original) { if ($script:directoryObjectCache.ContainsKey($token)) { if ($ReturnObjects -and $script:directoryObjectDetailCache.ContainsKey($token)) { $script:directoryObjectDetailCache[$token] } else { $script:directoryObjectCache[$token] } } elseif ($token -match $script:guidRegex) { # Fallback single GET (unexpected miss) try { $idObj = Invoke-MgGraphRequest -Method GET -Uri ("$script:graphBaseUrl/directoryObjects/{0}" -f $token) if ($idObj.id) { $script:directoryObjectCache[$token] = $idObj.id if (-not $script:directoryObjectDetailCache.ContainsKey($idObj.id)) { $script:directoryObjectDetailCache[$idObj.id] = [pscustomobject]@{ id = $idObj.id; displayName = $idObj.displayName; mailNickname = $idObj.mailNickname; userPrincipalName = $idObj.userPrincipalName } } if ($ReturnObjects) { $script:directoryObjectDetailCache[$idObj.id] } else { $idObj.id } } elseif ($DontFailIfNotExisting) { Write-PSFMessage -Level Warning -Message ("Cannot find directoryObject {0}" -f $token) -Tag 'resolve', 'directoryObject', 'missing'; $token } else { $Cmdlet.ThrowTerminatingError((New-Object System.Management.Automation.ErrorRecord ([System.Exception]::new("Cannot find directoryObject $token")), 'DirectoryObjectNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $token)) } } catch { if ($DontFailIfNotExisting) { Write-PSFMessage -Level Warning -Message ("Cannot resolve DirectoryObject '{0}': {1}" -f $token, $_.Exception.Message) -Tag 'failed', 'directoryObject' -ErrorRecord $_; $token } else { $Cmdlet.ThrowTerminatingError($_) } } } else { if ($DontFailIfNotExisting) { $token } else { Write-PSFMessage -Level Warning -Message ("Cannot resolve non-GUID DirectoryObject reference '{0}'." -f $token) -Tag 'failed', 'directoryObject'; $Cmdlet.ThrowTerminatingError([System.Exception]::new("Cannot find directoryObject $token")) } } } return , $final } # Single input path $single = $InputReference[0] if ($script:directoryObjectCache.ContainsKey($single)) { if ($ReturnObjects -and $script:directoryObjectDetailCache.ContainsKey($single)) { return $script:directoryObjectDetailCache[$single] } return $script:directoryObjectCache[$single] } if ($single -notmatch $script:guidRegex) { if ($DontFailIfNotExisting) { Write-PSFMessage -Level Warning -Message ("DirectoryObject non-GUID reference '{0}' not resolved." -f $single) -Tag 'resolve', 'directoryObject', 'nonguid'; return $single } $Cmdlet.ThrowTerminatingError((New-Object System.Management.Automation.ErrorRecord ([System.Exception]::new("Cannot find directoryObject $single")), 'DirectoryObjectNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $single)) } # Single lookup via getByIds to allow future type filtering consistency $body = @{ ids = @($single) } if ($Types) { $body.types = $Types } $resp1 = $null try { foreach ($item in $InputReference) { Write-PSFMessage -Level Verbose -Message $Item } $resp1 = Invoke-MgGraphRequest -Method POST -Uri ("$script:graphBaseUrl/directoryObjects/getByIds") -Body ($body | ConvertTo-Json) -ContentType 'application/json' } catch { Write-PSFMessage -Level Warning -Message ("getByIds single lookup failed for '{0}': {1}" -f $single, $_.Exception.Message) -Tag 'failed', 'directoryObject', 'single' -ErrorRecord $_ if ($DontFailIfNotExisting) { return $single } else { $Cmdlet.ThrowTerminatingError($_) } } $directoryObject = $resp1.value | Select-Object -First 1 if (-not $directoryObject) { if ($DontFailIfNotExisting) { return $single } $Cmdlet.ThrowTerminatingError((New-Object System.Management.Automation.ErrorRecord ([System.Exception]::new("Cannot find directoryObject $single")), 'DirectoryObjectNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $single)) } if (-not $script:directoryObjectCache.ContainsKey($directoryObject.id)) { $script:directoryObjectCache[$directoryObject.id] = $directoryObject.id } if (-not $script:directoryObjectDetailCache.ContainsKey($directoryObject.id)) { $script:directoryObjectDetailCache[$directoryObject.id] = [pscustomobject]@{ id = $directoryObject.id; displayName = $directoryObject.displayName; mailNickname = $directoryObject.mailNickname; userPrincipalName = $directoryObject.userPrincipalName } } if ($ReturnObjects) { return $script:directoryObjectDetailCache[$directoryObject.id] } else { return $directoryObject.id } } } |