Public/Core/Approve-IDBridgeNameMismatch.ps1
|
<# .SYNOPSIS Interactively review and approve source/directory name mismatches so the accounts can be linked. .DESCRIPTION Onboarding tool, run by hand — never called by the pipeline. When EmployeeID linking finds a source user's username already taken by an account with a different name, the pipeline logs an error and skips it. This cmdlet gathers the same source and directory data the pipeline would (Initialize-IDBridge, source plugins, AD/Google target data, dedupe, overrides), finds every such mismatch, and walks them one at a time in the console showing both names side by side. Approving records the decision to <DataRoot>\ApprovedNameMismatches.csv — nothing is written to AD or Google here. On the next Invoke-IDBridge run the linking functions honor the approval and link the account, and the normal update pass then sets the EmployeeID and renames the account to the source (SIS) name under all the usual safety gates (ReadOnly, ChangeThreshold). Each approval is saved as it is made, so quitting mid-review loses nothing. AD and Google are approved independently (one row per directory), and an approval is honored only while the account's username and directory name still match what was approved — a drifted account must be re-approved (it shows up here again). .PARAMETER RootPath Base dir for Config/Logs/Exports/Plugins/Data/Vault. Defaults to C:\IDBridge. .PARAMETER SkipAD Skip Active Directory mismatches for this review. .PARAMETER SkipGoogle Skip Google Workspace mismatches for this review. .OUTPUTS [pscustomobject] @{ Reviewed; Approved; Skipped; FilePath }. .EXAMPLE Approve-IDBridgeNameMismatch .EXAMPLE Approve-IDBridgeNameMismatch -RootPath 'C:\IDBridge' -SkipGoogle .NOTES Created by: Sam Cattanach Modified: 2026-08-19 #> function Approve-IDBridgeNameMismatch { [CmdletBinding()] param ( [string]$RootPath = "C:\IDBridge", [switch]$SkipAD, [switch]$SkipGoogle ) #region Import Configuration try { Initialize-IDBridge -RootPath $RootPath } catch { Throw } try { $IDConfig = Get-IDBridgeConfig } catch { Throw } #endregion Import Configuration #region Apply Runtime Overrides if ($SkipAD) { $IDConfig.AD.enabled = $false } if ($SkipGoogle) { $IDConfig.Google.enabled = $false } if ($IDConfig.AD.enabled -ne $true -and $IDConfig.Google.enabled -ne $true) { Write-Log -Message "Approve: Both AD and Google are disabled - nothing to review." -Level Error Throw "Approve: Both AD and Google are disabled - nothing to review." } #TestRun caps each source plugin at 10 records - the mismatch set would be incomplete if ($IDConfig.Debug.testRun -eq $true) { Write-Log -Message "Approve: Debug.testRun is enabled - source data is capped at 10 records per plugin, so this review may miss mismatches." -Level Warn } #endregion Apply Runtime Overrides #region Google Auth # Gated on GoogleToken.Enabled only (mirrors Invoke-IDBridge): -SkipGoogle reviews # still need headers for Sheets source plugins. if ($IDConfig.GoogleToken.Enabled -eq $true) { try { Connect-IDBridgeGoogle } catch { Throw } } else { Write-Log -Message "Google API integration is disabled. Google-related functions will be skipped." -Level Trace } #endregion Google Auth #region Gather Source & Directory Data Write-Log -Message "Approve: Gathering source and directory data for name-mismatch review." try { $plugins = Invoke-SourcePlugins $sourceData = $plugins.SourceData $overrideData = $plugins.OverrideData } catch { Throw } if ($IDConfig.Google.enabled -eq $true) { try { $googleData = Get-TargetDataGoogle -ErrorAction Stop } catch { Throw } } if ($IDConfig.AD.enabled -eq $true) { try { $adData = Get-TargetDataAD -ErrorAction Stop } catch { Throw } } if ($IDConfig.AD.enabled -eq $true) { $sourceData = Add-TargetDataAD -SourceData $sourceData -ADData $adData } if ($IDConfig.Google.enabled -eq $true) { $sourceData = Add-TargetDataGoogle -SourceData $sourceData -GoogleData $googleData } $sourceData = Remove-IDBridgeDuplicateID -SourceData $sourceData $sourceData = Merge-IDBridgeOverrideData -SourceData $sourceData -OverrideData $overrideData #endregion Gather Source & Directory Data #region Find Mismatches #Same detection as the linking functions: unlinked source user whose username is taken by an #account with a different name. A mismatch with a still-valid approval is excluded; one whose #approval went stale (account renamed since) is included so it can be re-approved. $approvedMismatches = Get-IDBridgeApprovedNameMismatches $mismatches = @() if ($IDConfig.AD.enabled -eq $true) { foreach ($item in $sourceData | Where-Object {-not $_.ADCurrentUserID}) { if ($item.personID -notin $adData.Users.employeeID -and $item.username -in $adData.Users.SamAccountName) { $ADUser = ($adData.Users | Where-Object {$_.SamAccountName -eq $item.username}) if ($ADUser.Surname -ne $item.NameLast -or $ADUser.GivenName -ne $item.NameFirst) { $directoryName = $ADUser.GivenName + " " + $ADUser.Surname $approval = $approvedMismatches["AD|$($item.personID)"] if (-not ($approval -and $approval.Account -eq $item.username -and $approval.DirectoryName -eq $directoryName)) { $mismatches += [PSCustomObject]@{ Directory = 'AD' PersonID = $item.personID Account = $item.username Display = $ADUser.UserPrincipalName SourceName = $item.NameFirst + " " + $item.NameLast DirectoryName = $directoryName } } } } } } if ($IDConfig.Google.enabled -eq $true) { foreach ($item in $sourceData | Where-Object {-not $_.GoogleCurrentUserID}) { if ($item.UPN -in $googleData.Users.primaryEmail) { $googleUser = ($googleData.Users | Where-Object {$_.primaryEmail -eq $item.UPN}) if ($googleUser.Name.familyName -ne $item.NameLast -or $googleUser.Name.givenName -ne $item.NameFirst) { $directoryName = $googleUser.Name.givenName + " " + $googleUser.Name.familyName $approval = $approvedMismatches["Google|$($item.personID)"] if (-not ($approval -and $approval.Account -eq $item.UPN -and $approval.DirectoryName -eq $directoryName)) { $mismatches += [PSCustomObject]@{ Directory = 'Google' PersonID = $item.personID Account = $item.UPN Display = $googleUser.primaryEmail SourceName = $item.NameFirst + " " + $item.NameLast DirectoryName = $directoryName } } } } } } #endregion Find Mismatches #region Review $approvalFilePath = "$($IDConfig.Paths.DataRoot)\ApprovedNameMismatches.csv" if ($mismatches.Count -eq 0) { Write-Log -Message "Approve: No unapproved name mismatches found - nothing to review." Write-Host "No unapproved name mismatches found - nothing to review." -ForegroundColor Green return [PSCustomObject]@{ Reviewed = 0 Approved = 0 Skipped = 0 FilePath = $approvalFilePath } } Write-Log -Message "Approve: $($mismatches.Count) name mismatch(es) to review." #Existing rows are kept in memory so each approval rewrites the full file; a re-approval #replaces the old row for that Directory+PersonID instead of duplicating it $approvalRows = @() if (Test-Path $approvalFilePath) { $approvalRows = @(Import-Csv -Path $approvalFilePath) } Write-Host "" Write-Host "$($mismatches.Count) name mismatch(es) to review." -ForegroundColor Cyan Write-Host "Approving records the decision to $approvalFilePath - no directory writes happen here." Write-Host "On the next Invoke-IDBridge run an approved account is linked and RENAMED to the source (SIS) name." -ForegroundColor Yellow Write-Host "" $approvedCount = 0 $skippedCount = 0 $reviewedCount = 0 foreach ($mismatch in $mismatches) { $reviewedCount++ Write-Host ("[{0}/{1}] {2} PersonID {3} ({4})" -f $reviewedCount, $mismatches.Count, $mismatch.Directory, $mismatch.PersonID, $mismatch.Display) -ForegroundColor Cyan Write-Host (" Source (SIS) name: " + $mismatch.SourceName) Write-Host (" {0} name:{1} {2}" -f $mismatch.Directory, (" " * (12 - $mismatch.Directory.Length)), $mismatch.DirectoryName) $answer = $null while ($answer -notin @('A', 'S', 'Q')) { $answer = (Read-Host "Approve link? [A]pprove / [S]kip / [Q]uit").Trim().ToUpper() } if ($answer -eq 'Q') { $reviewedCount-- Write-Host "Quitting review - decisions already made are saved." -ForegroundColor Yellow break } if ($answer -eq 'A') { #Replace any prior (stale) approval for this Directory+PersonID, then save #immediately so quitting mid-review loses nothing $approvalRows = @($approvalRows | Where-Object { -not ($_.Directory -eq $mismatch.Directory -and $_.PersonID -eq $mismatch.PersonID) }) $approvalRows += [PSCustomObject]@{ PersonID = $mismatch.PersonID Directory = $mismatch.Directory Account = $mismatch.Account SourceName = $mismatch.SourceName DirectoryName = $mismatch.DirectoryName ApprovedDate = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') } if (-not (Test-Path "$($IDConfig.Paths.DataRoot)")) { New-Item -Path "$($IDConfig.Paths.DataRoot)" -ItemType Directory -Force | Out-Null } $approvalRows | Export-Csv -Path $approvalFilePath -NoTypeInformation -Force Write-Log -Message ("Approve: $($mismatch.Directory): Name mismatch approved for $($mismatch.PersonID) ($($mismatch.Account)) - source name " + $mismatch.SourceName + ", directory name " + $mismatch.DirectoryName + ".") Write-Host " Approved." -ForegroundColor Green $approvedCount++ } else { Write-Log -Message ("Approve: $($mismatch.Directory): Name mismatch skipped for $($mismatch.PersonID) ($($mismatch.Account)).") -Level Trace Write-Host " Skipped." -ForegroundColor Yellow $skippedCount++ } Write-Host "" } $remainingCount = $mismatches.Count - $reviewedCount Write-Log -Message "Approve: Review finished - $approvedCount approved, $skippedCount skipped, $remainingCount not reviewed." Write-Host ("Review finished: {0} approved, {1} skipped, {2} not reviewed." -f $approvedCount, $skippedCount, $remainingCount) -ForegroundColor Cyan if ($approvedCount -gt 0) { Write-Host "Approved accounts will be linked on the next Invoke-IDBridge run." } #endregion Review return [PSCustomObject]@{ Reviewed = $reviewedCount Approved = $approvedCount Skipped = $skippedCount FilePath = $approvalFilePath } } |