Public/Get-NSPEntraPairingAddressPlan.ps1
|
function Get-NSPEntraPairingAddressPlan { <# .SYNOPSIS Previews AD address preparation for the selected pairs using NSP.ActiveDirectory. .DESCRIPTION Requires a complete forest inventory for collision checks. Returns the AD specialist's before/after plans. Does not apply changes or assume a missing forest UPN suffix exists. After any Set-NSPADUserAddress -Apply attempt, mark the queue needsRevalidation, retain results, read fresh inventories and sync the queue before continuing to hard-match preflight. Compares selected cloud users by stable ID, proposes additive SMTP/SIP/X500 preservation, and exposes every cloud proxy for review. SPO/reserved routing values stay cloud-managed by default; other protocols require an explicit decision. No addresses are synthesized. .PARAMETER AddressPolicy Optional AD policy controls: SAM rename, creation of empty mail, independent UPN/mail/ primary updates, old-alias preservation/conversion, alias-domain and X400/X500 removals. Pairing creates empty mail from the chosen primary by default; explicitly disable if needed. .PARAMETER CloudPrimaryConflictAction Block by default when cloud primary differs. UsePlannedAD explicitly selects the reviewed AD primary while keeping the cloud address as a secondary unless removed by policy. .PARAMETER CloudProxyAddressType Protocols eligible for literal cloud-to-AD additions. Defaults to SMTP, SIP and X500. Other protocols remain visible; opaque service values require service-specific review. .EXAMPLE $plans = Get-NSPEntraPairingAddressPlan -Queue $queue -Inventory $inventory -OldDomain old.example -NewDomain new.example #> [CmdletBinding()] param([Parameter(Mandatory)]$Queue, [Parameter(Mandatory)]$Inventory, [Parameter(Mandatory)][string]$OldDomain, [Parameter(Mandatory)][string]$NewDomain, [ValidateSet('ReplaceSuffix','SamAccountName','FLast','First.Last','LastF')][string]$Format = 'ReplaceSuffix', [hashtable]$AddressPolicy = @{}, [ValidateSet('Block','UsePlannedAD')][string]$CloudPrimaryConflictAction = 'Block', [ValidatePattern('^[A-Za-z][A-Za-z0-9]*$')][string[]]$CloudProxyAddressType = @('SMTP','SIP','X500')) if ($Queue.needsRevalidation) { throw 'Revalidate the queue before preparing addresses.' } if ([string]$Inventory.TenantId -ne [string]$Queue.tenantId) { throw 'Inventory tenant differs from the queue.' } if (-not $Inventory.ForestWide) { throw 'Address preparation requires a complete forest inventory (-Forest).' } if ($Inventory.ForestName -and [string]$Inventory.ForestName -ine [string]$Queue.forest) { throw 'Inventory forest differs from the queue.' } Import-NSPToolkitModule -Name 'NSP.ActiveDirectory' -MinimumVersion '0.1.13' if (-not (Get-Command Get-NSPADUserAddressPlan -ErrorAction SilentlyContinue)) { throw 'The AD address-planning API is not available. Use the AD specialist delivery when ready.' } $ids = @($Queue.pairs | Where-Object decision -eq 'selected' | ForEach-Object { [guid]$_.adObjectGuid }) if (-not $ids.Count) { return } $allowed = @('ChangeSamAccountName','CreateMailIfEmpty','UpdateUPN','UpdateMail','UpdatePrimarySmtp','PreserveOldAddress','ConvertAliasDomain','RemoveAliasDomain','RemoveAddressType') foreach ($key in $AddressPolicy.Keys) { if ($key -notin $allowed) { throw "Unsupported address policy: $key" } } foreach ($key in @($AddressPolicy.Keys | Where-Object { $_ -notin @('RemoveAliasDomain','RemoveAddressType') })) { if ($AddressPolicy[$key] -isnot [bool] -and $AddressPolicy[$key] -isnot [Management.Automation.SwitchParameter]) { throw "Address policy $key must be a Boolean, not text." } } $additions = @{}; $reviews = @{}; $cloudByAD = @{}; $selectedCloudIds = @{} $cloudClaims = @{} foreach ($cloudUser in $Inventory.CloudUsers) { $values = @($cloudUser.proxyAddresses) foreach ($name in @('userPrincipalName','mail')) { if ($cloudUser.$name) { $values += 'smtp:' + $cloudUser.$name } } foreach ($value in $values) { $claim = Get-NSPEntraProxyClaim $value if (-not $claim) { continue } if (-not $cloudClaims.ContainsKey($claim)) { $cloudClaims[$claim] = @{} } $cloudClaims[$claim][[string]$cloudUser.id] = $true } } foreach ($pair in @($Queue.pairs | Where-Object decision -eq 'selected')) { $id = ([guid]$pair.adObjectGuid).ToString('D') if ($cloudByAD.ContainsKey($id)) { throw 'Multiple selected cloud users reference one AD GUID.' } if (-not $pair.cloudObjectId -or $selectedCloudIds.ContainsKey([string]$pair.cloudObjectId)) { throw 'Multiple selected AD users reference one cloud identity, or the cloud identity is missing.' } $selectedCloudIds[[string]$pair.cloudObjectId] = $true $ad = @($Inventory.ADUsers | Where-Object { [guid]$_.ObjectGUID -eq [guid]$id }) $cloud = @($Inventory.CloudUsers | Where-Object { [string]$_.id -ceq [string]$pair.cloudObjectId }) if ($ad.Count -ne 1 -or $cloud.Count -ne 1 -or [string]$ad[0].ObjectSID -cne [string]$pair.adSid) { throw 'Selected AD/cloud identities no longer resolve uniquely in the inventory. Refresh and revalidate.' } $cloudByAD[$id] = $cloud[0] $reviews[$id] = @(Get-NSPEntraPairingProxyReview -CloudUser $cloud[0] -ADUser $ad[0] -CopyType $CloudProxyAddressType) $values = @($reviews[$id] | Where-Object Disposition -eq 'AddToAD' | ForEach-Object { $_.Address }) if ($values.Count) { $additions[$id] = [string[]]$values } } $options = @{User=@($Inventory.ADUsers);Identity=$ids;OldDomain=$OldDomain;NewDomain=$NewDomain;Format=$Format;CreateMailIfEmpty=$true} foreach ($key in $AddressPolicy.Keys) { $options[$key] = $AddressPolicy[$key] } if ($additions.Count) { if (-not (Get-Command Get-NSPADUserAddressPlan).Parameters.ContainsKey('AdditionalProxyAddresses')) { throw 'The AD planner cannot preserve cloud-only proxy addresses yet. Update the AD capability before applying; no addresses were changed.' } $options.AdditionalProxyAddresses = $additions } foreach ($plan in @(Get-NSPADUserAddressPlan @options)) { $id = ([guid]$plan.ObjectGUID).ToString('D'); $cloud = $cloudByAD[$id] foreach ($proxy in $reviews[$id]) { if ($proxy.Disposition -in @('AddToAD','PresentInAD') -and @($plan.After.ProxyAddresses) -inotcontains $proxy.Address) { $proxy.Disposition = 'ExcludedByPolicy' $proxy.Reason = 'Excluded by the explicitly selected alias/type policy; the cloud value remains visible for review.' } } $reasons = [Collections.Generic.List[string]]::new() foreach ($reason in $plan.Reasons) { $reasons.Add($reason) } if (@($reviews[$id] | Where-Object Disposition -eq 'NeedsReview').Count) { $reasons.Add('Cloud proxy addresses need a preservation decision; review every protocol before applying.') } $primary = @($cloud.proxyAddresses | Where-Object { $_ -cmatch '^SMTP:' }) $planned = @($plan.After.ProxyAddresses | Where-Object { $_ -cmatch '^SMTP:' }) if ($primary.Count -gt 1 -or ($CloudPrimaryConflictAction -eq 'Block' -and $primary.Count -eq 1 -and ($planned.Count -ne 1 -or $primary[0] -ine $planned[0]))) { $reasons.Add('Cloud primary SMTP differs from the proposed AD primary. Resolve the address policy explicitly; no primary is silently demoted.') } $claims = @($plan.After.ProxyAddresses) foreach ($name in @('UserPrincipalName','Mail')) { if ($plan.After.$name) { $claims += 'smtp:' + $plan.After.$name } } foreach ($value in $claims) { $claim = Get-NSPEntraProxyClaim $value if ($claim -and $cloudClaims.ContainsKey($claim) -and @($cloudClaims[$claim].Keys | Where-Object { $_ -cne [string]$cloud.id }).Count) { $reasons.Add('A proposed address is claimed by a different cloud user in the inventory.'); break } } # Keep the AD writer's actual Before/After and provenance. Extra review fields never # replace Before with cloud values or bypass the writer's fresh-AD regeneration. $plan | Add-Member -NotePropertyName CloudBefore -NotePropertyValue ([pscustomobject]@{ObjectId=[string]$cloud.id;UserPrincipalName=[string]$cloud.userPrincipalName;Mail=[string]$cloud.mail;ProxyAddresses=[string[]]@($cloud.proxyAddresses)}) $plan | Add-Member -NotePropertyName ProxyAddressReview -NotePropertyValue @($reviews[$id]) $plan | Add-Member -NotePropertyName CloudPrimaryConflictAction -NotePropertyValue $CloudPrimaryConflictAction $plan.Reasons = [string[]]@($reasons); $plan.Blocked = $reasons.Count -gt 0 $plan } } |