Public/Start-NSPEntraUserPairing.ps1
|
function Start-NSPEntraUserPairing { <# .SYNOPSIS Walks an operator through a queue, one cloud user at a time. Changes nothing in a directory. .DESCRIPTION Shows each undecided, needs-review or deferred pair, with the options to choose a candidate, search AD (with -Server), skip, flag or defer. A selection is recorded immediately, without another confirmation. The queue is saved after every decision, so stopping loses nothing. Q stops; run it again to carry on. Diagnostic pairs such as already-linked accounts are not offered. A loaded queue has to go through Sync-NSPEntraPairingQueue first. -WhatIf walks the review without saving. Returns the saved path and the count of each decision. .EXAMPLE Start-NSPEntraUserPairing -Queue $queue -Server 'dc01.contoso.example' -Forest #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)]$Queue, [string]$Path, [string]$Server, [switch]$Forest, [switch]$IncludeDecided ) if ($Queue.needsRevalidation) { throw 'This queue was loaded and has not been revalidated. Run Sync-NSPEntraPairingQueue first.' } Import-NSPToolkitModule -Name 'NSP.Console' -MinimumVersion '0.1.1' if (-not $Path -and $PSCmdlet.ShouldProcess('review queue', 'Save')) { $Path = Export-NSPEntraPairingQueue -Queue $Queue } $save = { if ($PSCmdlet.ShouldProcess('review queue', 'Save')) { Export-NSPEntraPairingQueue -Queue $Queue -Path $Path | Out-Null } } $selectable = @('Suggested', 'Ambiguous', 'OneToMany', 'NotFound') $rank = @{ 'needs-review' = 0; undecided = 1; deferred = 2 } # Sort-Object is not stable under 5.1, so the UPN keeps the order the same on both hosts. $work = @(@($Queue.pairs) | Where-Object { $_.status -in $selectable -and ($IncludeDecided -or $_.decision -in @('undecided', 'needs-review', 'deferred')) } | Sort-Object @{ Expression = { if ($rank.ContainsKey([string]$_.decision)) { $rank[[string]$_.decision] } else { 3 } } }, cloudUpnAtReview) $position = 0 $finalReview = $false $completed = $false :review while ($true) { if ($finalReview -or $position -ge $work.Count) { $all = @($Queue.pairs | Where-Object { $_.status -in $selectable -and $_.executionState -eq 'pending' } | Sort-Object cloudUpnAtReview) Clear-NSPConsole Write-NSPConsoleRule -Title 'Final review - choose any user to change the decision' $finalChoices = @($all | ForEach-Object { [pscustomobject]@{ Label = "$($_.cloudUpnAtReview) -> $($_.adUserPrincipalNameAtReview) [$($_.decision)] $($_.roleReview)"; Value = "edit:$($_.cloudObjectId)" } }) + @([pscustomobject]@{ Key = 'X'; Label = 'Finish and save review'; Value = 'finish' }) $answer = Read-NSPMenu -Prompt 'Review every decision before finishing. This saves choices only.' -Choices $finalChoices -CancelLabel 'Save and stop for now' if ($answer -eq 'finish') { $completed = $true; break review } if ($null -eq $answer) { break review } $id = ([string]$answer).Substring('edit:'.Length) $work = $all $position = 0 while ($position -lt $work.Count -and $work[$position].cloudObjectId -ne $id) { $position++ } if ($position -ge $work.Count) { continue } $finalReview = $true } $pair = $work[$position] while ($true) { Show-NSPEntraPairReview -Pair $pair -Position ($position + 1) -Total $work.Count $choices = [System.Collections.Generic.List[object]]::new() foreach ($candidate in @($pair.candidates)) { $disabled = if ($candidate.enabled) { '' } else { ', DISABLED' } $choices.Add([pscustomobject]@{ Label = "$($candidate.domain)\$($candidate.samAccountName) $($candidate.userPrincipalName) [$($candidate.confidence)$disabled]" Value = "select:$($candidate.adObjectGuid)" Help = @($candidate.evidence) -join ', ' }) } if ($Server) { $choices.Add([pscustomobject]@{ Key = 'S'; Label = 'Search AD'; Value = 'search' }) } if ($position -gt 0) { $choices.Add([pscustomobject]@{ Key = 'B'; Label = 'Back to previous user'; Value = 'back' }) } $choices.Add([pscustomobject]@{ Key = 'R'; Label = 'Review all decisions'; Value = 'review-all' }) $choices.Add([pscustomobject]@{ Key = 'K'; Label = 'Skip'; Value = 'skipped' }) $choices.Add([pscustomobject]@{ Key = 'F'; Label = 'Flag for someone to check'; Value = 'flagged' }) $choices.Add([pscustomobject]@{ Key = 'D'; Label = 'Defer'; Value = 'deferred' }) $answer = Read-NSPMenu -Prompt 'What should happen to this user?' -Choices $choices.ToArray() -CancelLabel 'Stop for now' if ($null -eq $answer) { break review } if ($answer -eq 'back') { if ($position -gt 0) { $position-- }; $finalReview = $false; continue review } if ($answer -eq 'review-all') { $finalReview = $true; continue review } try { if ($answer -in @('skipped', 'deferred')) { Add-NSPEntraPairDecision -Queue $Queue -CloudObjectId $pair.cloudObjectId -Decision $answer | Out-Null } elseif ($answer -eq 'flagged') { $note = Read-NSPOptional -Prompt 'What should they check?' Add-NSPEntraPairDecision -Queue $Queue -CloudObjectId $pair.cloudObjectId -Decision flagged -Note $note | Out-Null } else { $decision = @{ Queue = $Queue; CloudObjectId = $pair.cloudObjectId; Decision = 'selected' } if ($answer -eq 'search') { $term = Read-NSPNonEmpty -Prompt 'Search sAMAccountName, UPN, name, mail or proxy address (* is a wildcard)' -AllowBack if (Test-NSPBackSignal $term) { continue } $found = @(Find-NSPEntraPairADUser -Term $term -Server $Server -Forest:$Forest) if (-not $found.Count) { Write-NSPConsoleLine -Message 'No AD user matched.' -Role Warning continue } $picks = @($found | ForEach-Object { [pscustomobject]@{ Label = "$($_.DomainDNSRoot)\$($_.SamAccountName) $($_.UserPrincipalName) $($_.DisplayName)"; Value = $_ } }) $picked = Read-NSPChoice -Choices $picks -Prompt 'Which AD user?' if ($null -eq $picked) { continue } $decision.ADUser = $picked } else { $guid = ([string]$answer).Substring('select:'.Length) $decision.AdObjectGuid = $guid } Add-NSPEntraPairDecision @decision | Out-Null } } catch [System.Management.Automation.RuntimeException] { # A refused selection, such as an AD user already taken. The operator chooses again. Write-NSPConsoleLine -Message $_.Exception.Message -Role Error continue } & $save $position++ continue review } } & $save $counts = [ordered]@{} foreach ($group in @(@($Queue.pairs) | Group-Object decision)) { $counts[$group.Name] = $group.Count } [pscustomobject]@{ Path = $Path; Decisions = [pscustomobject]$counts; ReviewCompleted = $completed } } |