Public/Compare-EntraUserGroups.ps1
|
function Compare-EntraUserGroups { <# .SYNOPSIS Read-only side-by-side comparison of the direct group memberships of two Entra ID users. .DESCRIPTION Reads the direct group memberships of both users and returns one record per group with Membership = 'Source only', 'Target only' or 'Both'. Groups that could not simply be copied (dynamic, on-prem synced, Exchange-managed) carry an explanatory note. Nothing is changed. .PARAMETER Source UPN of the first user (the "template" side). .PARAMETER Target UPN of the second user. .EXAMPLE Compare-EntraUserGroups -Source template@contoso.com -Target new@contoso.com #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string]$Source, [Parameter(Mandatory, Position = 1)] [string]$Target ) $ErrorActionPreference = 'Stop' Connect-EgmGraph $srcUser = Get-MgUser -UserId $Source $tgtUser = Get-MgUser -UserId $Target $srcGroups = @(Get-MgUserMemberOf -UserId $srcUser.Id -All | ConvertTo-EgmGroupInfo) $tgtGroups = @(Get-MgUserMemberOf -UserId $tgtUser.Id -All | ConvertTo-EgmGroupInfo) $srcIds = [System.Collections.Generic.HashSet[string]]::new() foreach ($g in $srcGroups) { [void]$srcIds.Add($g.Id) } $tgtIds = [System.Collections.Generic.HashSet[string]]::new() foreach ($g in $tgtGroups) { [void]$tgtIds.Add($g.Id) } Write-Host "" Write-Host "Source: $($srcUser.UserPrincipalName) | Target: $($tgtUser.UserPrincipalName)" -ForegroundColor Cyan function Get-Note([object]$GroupInfo) { if ($GroupInfo.IsDynamic) { 'dynamic group - cannot be copied' } elseif ($GroupInfo.OnPremSynced) { 'synced from on-prem AD - manage in local AD' } elseif ($GroupInfo.Route -eq 'Exchange') { 'managed via Exchange Online' } else { '' } } $records = [System.Collections.Generic.List[pscustomobject]]::new() foreach ($g in $srcGroups) { $records.Add([pscustomobject]@{ Group = $g.Name Type = $g.Kind Membership = if ($tgtIds.Contains($g.Id)) { 'Both' } else { 'Source only' } Note = Get-Note -GroupInfo $g }) } foreach ($g in $tgtGroups) { if (-not $srcIds.Contains($g.Id)) { $records.Add([pscustomobject]@{ Group = $g.Name Type = $g.Kind Membership = 'Target only' Note = Get-Note -GroupInfo $g }) } } $order = @{ 'Source only' = 0; 'Target only' = 1; 'Both' = 2 } $records | Sort-Object { $order[$_.Membership] }, Group } |