Private/ConvertTo-NSPEntraSourceAnchorResult.ps1
|
function ConvertTo-NSPEntraSourceAnchorResult { <# .SYNOPSIS Works out the source-anchor strategy from Entra Connect's rules, connectors and global setting. .DESCRIPTION Offline: it only reads what it is given, so tests can feed it sanitized rule objects. Provenance is Verified only when every AD connector's enabled inbound user rules flow one recognized strategy and the global setting, when readable, does not contradict it. Scoped rules are not resolved per object, so rules that disagree stay Unverified rather than being settled by precedence. Anything unrecognized is Unverified: a wrong guess here would write a wrong immutable ID. Contact rules are left out: their objects are never paired, and Entra Connect keeps objectGUID for them whatever the user anchor is. Only source type contact is left out, so a source type nobody has seen still counts. #> [CmdletBinding()] param( [AllowEmptyCollection()][object[]]$Rule = @(), [AllowEmptyCollection()][object[]]$Connector = @(), [AllowEmptyCollection()][object[]]$GlobalParameter = @(), [string]$ComputerName, [string]$ModuleVersion ) $reasons = [System.Collections.Generic.List[string]]::new() $notes = [System.Collections.Generic.List[string]]::new() $adConnectors = @{} foreach ($item in $Connector) { if ([string]$item.ConnectorTypeName -eq 'AD') { $adConnectors[[string]$item.Identifier] = [string]$item.Name } } if (-not $adConnectors.Count) { $reasons.Add('No AD connector was readable.') } $contactRules = 0 $evidence = foreach ($item in $Rule) { if ($item.Disabled -or [string]$item.Direction -ne 'Inbound' -or [string]$item.TargetObjectType -ne 'person') { continue } $connectorId = [string]$item.Connector if (-not $adConnectors.ContainsKey($connectorId)) { continue } if ([string]$item.SourceObjectType -eq 'contact') { $contactRules++; continue } foreach ($mapping in @($item.AttributeFlowMappings)) { if ($null -eq $mapping -or [string]$mapping.Destination -notin @('sourceAnchorBinary', 'sourceAnchor')) { continue } $strategy = Get-NSPEntraAnchorFlowStrategy -Mapping $mapping if ($strategy -eq 'Derived') { continue } [pscustomobject]@{ ConnectorName = $adConnectors[$connectorId] RuleName = [string]$item.Name Precedence = $item.Precedence SourceObjectType = [string]$item.SourceObjectType Destination = [string]$mapping.Destination FlowType = [string]$mapping.FlowType SourceAttribute = @($mapping.Source | Where-Object { $_ } | ForEach-Object { [string]$_ }) Expression = [string]$mapping.Expression Strategy = $strategy } } } $evidence = @($evidence) if ($contactRules) { $notes.Add("$contactRules contact rule(s) left out: contacts are not paired.") } if (@($evidence | Where-Object { $_.Strategy -ne 'Unknown' -and $_.Expression -match 'msExchRecipientTypeDetails' }).Count) { $notes.Add('These rules give no anchor to a user with msExchRecipientTypeDetails 2. The inventory cannot see that attribute, and a write for such a user is not expected to link.') } $perConnector = foreach ($id in $adConnectors.Keys) { $name = $adConnectors[$id] $strategies = @($evidence | Where-Object ConnectorName -eq $name | ForEach-Object { $_.Strategy } | Sort-Object -Unique) if ($strategies.Count -eq 0) { $reasons.Add("Connector '$name': no enabled inbound rule flows the source anchor.") } elseif ($strategies.Count -gt 1) { $reasons.Add("Connector '$name': rules flow different anchors ($($strategies -join ', ')). Review precedence and scope in the Synchronization Rules Editor.") } elseif ($strategies[0] -eq 'Unknown') { $reasons.Add("Connector '$name': the anchor flow is not a recognized form. Review it in the Synchronization Rules Editor.") } [pscustomobject]@{ ConnectorName = $name; Strategy = if ($strategies.Count -eq 1) { $strategies[0] } else { 'Unknown' } } } $perConnector = @($perConnector) $distinct = @($perConnector | ForEach-Object { $_.Strategy } | Sort-Object -Unique) $strategy = if ($distinct.Count -eq 1) { $distinct[0] } else { 'Unknown' } if ($distinct.Count -gt 1) { $reasons.Add("Connectors use different anchors: $($distinct -join ', ').") } $setting = @($GlobalParameter | Where-Object { [string]$_.Name -eq 'Microsoft.SynchronizationOption.AnchorAttribute' })[0] $globalValue = if ($setting) { [string]$setting.Value } else { $null } if (-not $globalValue) { $notes.Add('The global anchor setting was not readable; the rules alone were used.') } elseif ($strategy -ne 'Unknown') { $expected = if ($strategy -eq 'ObjectGuid') { 'objectGUID' } else { 'mS-DS-ConsistencyGuid' } if ($globalValue -ne $expected) { $reasons.Add("The global setting names $globalValue, but the rules flow $strategy.") } } [pscustomobject]@{ PSTypeName = 'NSP.M365.UserPairing.SourceAnchor' Provenance = if ($reasons.Count) { 'Unverified' } else { 'Verified' } Strategy = $strategy Reasons = @($reasons) Notes = @($notes) GlobalSetting = $globalValue Connectors = $perConnector Evidence = $evidence ComputerName = $ComputerName ModuleVersion = $ModuleVersion CollectedAtUtc = [datetime]::UtcNow.ToString('o') } } |