Private/AD/Get-ADUsersToSetEmployeeID.ps1
|
<# .SYNOPSIS Match unlinked source users to existing AD accounts by username and name. .DESCRIPTION For each source user without an ADCurrentUserID, looks for an AD account whose SamAccountName equals the source Username and whose GivenName/Surname also match. A match is returned so the source record can be linked to (and its EmployeeID set on) that existing account — letting previously unlinked or deprovisioned accounts be reconciled and, if inactive, deactivated. A username that matches a different name is logged as an error and skipped — unless that exact mismatch was approved via Approve-IDBridgeNameMismatch (persisted in <DataRoot>\ApprovedNameMismatches.csv), in which case it links; an approval whose recorded account/name no longer matches the current AD account is logged as a warning and skipped. A match for a user who is inactive or not AD-provisioned whose account is already disabled is still returned, but logged at Trace ("nothing to reconcile") instead of Info: nothing writes the link for it (updates cover active users, the deactivate step enabled accounts), so it recurs every run. .PARAMETER UserList The source records (the in-progress user list). .PARAMETER CurrentADUsers All current AD users (from Get-TargetDataAD .Users). .OUTPUTS [hashtable] personID -> @{ ID (ObjectGUID); Groups; EnabledStatus; User }. .EXAMPLE $matches = Get-ADUsersToSetEmployeeID -UserList $sourceData -CurrentADUsers $adData.Users .NOTES Created by: Sam Cattanach Modified: 2026-10-09 #> function Get-ADUsersToSetEmployeeID { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] $UserList, [Parameter(Mandatory = $true)] $CurrentADUsers ) #Set Users that need EmployeeID set in AD #If no user exists with the employee ID, try username #Username has to pair with the first name and last name - unless the mismatch was #explicitly approved via Approve-IDBridgeNameMismatch $itemUpdateList = @{} $approvedMismatches = Get-IDBridgeApprovedNameMismatches foreach ($item in $UserList | Where-Object {-not $_.ADCurrentUserID}) { if ($item.personID -notin $CurrentADUsers.employeeID){ #One outcome line per user, logged where the decision is made, so the discovery #and its result read together instead of in separate phases if ($item.username -in $CurrentADUsers.SamAccountName) { $ADUser = $null $ADUser = ($CurrentADUsers | Where-Object {$_.SamAccountName -eq $item.username}) $link = $false $matchedBy = $null if ($ADUser.Surname -eq $item.NameLast -and $ADUser.GivenName -eq $item.NameFirst) { $matchedBy = "by username+name" $link = $true } else { #Name differs - honor a recorded approval only while both sides still match #what was approved; a drifted account name means re-approval is required $approval = $approvedMismatches["AD|$($item.personID)"] if ($approval -and $approval.Account -eq $item.username -and $approval.DirectoryName -eq ($ADUser.GivenName + " " + $ADUser.Surname)) { $matchedBy = "by username with name mismatch approved on $($approval.ApprovedDate)" $link = $true } elseif ($approval) { Write-Log -Message ("AD: Username " + $item.username + " for " + $item.personID + " has an approval from " + $approval.ApprovedDate + " but the account no longer matches it (approved: " + $approval.Account + " / " + $approval.DirectoryName + ", current: " + $item.username + " / " + $ADUser.GivenName + " " + $ADUser.Surname + ") - not linked; re-approve with Approve-IDBridgeNameMismatch.") -Level Warn } else { Write-Log -Message ("AD: Username " + $item.username + " for " + $item.personID + " is already taken by " + $ADUser.GivenName + " " + $ADUser.Surname + " in AD but source name is " + $item.NameFirst + " " + $item.NameLast + " - not linked.") -Level Error } } if ($link) { #Not active in AD and the account is already disabled: neither the update step (active users) #nor the deactivate step (enabled accounts) writes the link, so it re-matches every run - Trace, not Info if (($item.IDBActive -eq $false -or $item.ProvisionAD -eq $false) -and $ADUser.Enabled -eq $false) { Write-Log -Message ("AD: No user with EmployeeID: $($item.personID) - matched existing $($ADUser.UserPrincipalName) $matchedBy, but the source user is inactive or not AD-provisioned and the account is already disabled - nothing to reconcile.") -Level Trace } else { Write-Log -Message ("AD: No user with EmployeeID: $($item.personID) - matched existing $($ADUser.UserPrincipalName) $matchedBy; will link EmployeeID.") } $itemUpdateList[$item.personID] = [PSCustomObject]@{ ID = $ADUser.ObjectGUID #CurrentGroups is the AD.groupsExcluded-filtered name list built by #Get-TargetDataAD - a fresh MemberOf query would bypass the exclusions Groups = $ADUser.CurrentGroups EnabledStatus = $ADUser.Enabled User = $ADUser } } } elseif ($item.IDBActive -eq $false) { Write-Log -Message ("AD: No user found with EmployeeID: $($item.personID). Source user is inactive and has no AD account - nothing to reconcile.") -Level Trace } else { Write-Log -Message ("AD: No user found with EmployeeID: $($item.personID) - no username match; treated as new.") -Level Trace } } } return $itemUpdateList } |