Public/Test-NSPConditionalAccessTravelOverlap.ps1
|
function Test-NSPConditionalAccessTravelOverlap { <# .SYNOPSIS Read-only: finds users in more than one active travel group. .DESCRIPTION Travel policies are separate blocks and stack: a user in Group-Travel-FR and in a DE+JP trip group is blocked in France by the trip policy and in Germany and Japan by the France policy, so they can sign in from none of them. Each traveler should be in one travel group at a time; combine the countries into one trip instead. Reads the transitive user members of every active entry's groups (synced and _azure). Returns one row per user in two or more entries; no rows means no overlap. A group that cannot be read throws, so a partial answer is never reported as clean. .EXAMPLE Test-NSPConditionalAccessTravelOverlap -TenantProfile $profilePath | Format-Table -Wrap #> [CmdletBinding()] param( [Parameter(Mandatory)]$TenantProfile, [string]$ConfigRoot ) if ($TenantProfile -is [string]) { $TenantProfile = Get-Content -LiteralPath $TenantProfile -Raw | ConvertFrom-Json } function Get-Value { param($Object, [string]$Key) Get-NSPConditionalAccessMapValue $Object $Key } $active = @(Get-NSPConditionalAccessTravelEntry -TenantProfile $TenantProfile | Where-Object { $_.Status -eq 'Active' -and $_.Ids.Count }) if ($active.Count -lt 2) { return } Import-NSPM365Graph $context = Assert-NSPConditionalAccessSession -ConfigRoot $ConfigRoot if ($context.TenantId -ne (Get-Value $TenantProfile 'TenantId')) { throw "The Graph session is for tenant $($context.TenantId), not the profile's tenant." } $membership = @{} $users = @{} $position = 0 foreach ($entry in $active) { $position++ Write-NSPConditionalAccessProgress -Activity Overlap -Status "Members of travel entry $($entry.Key)" -Step $position -Of $active.Count foreach ($groupId in $entry.Ids) { foreach ($member in Invoke-NSPGraphCollection -Uri "groups/$groupId/transitiveMembers/microsoft.graph.user?`$select=id,userPrincipalName,displayName") { $userId = Get-Value $member 'id' $users[$userId] = $member if (-not $membership.ContainsKey($userId)) { $membership[$userId] = [System.Collections.Generic.List[object]]::new() } if (-not @($membership[$userId] | Where-Object Key -eq $entry.Key).Count) { $membership[$userId].Add($entry) } } } } Write-NSPConditionalAccessProgress -Activity Overlap -Completed foreach ($userId in @($membership.Keys | Sort-Object)) { $entries = @($membership[$userId]) if ($entries.Count -lt 2) { continue } [pscustomobject]@{ UserId = $userId UserPrincipalName = Get-Value $users[$userId] 'userPrincipalName' DisplayName = Get-Value $users[$userId] 'displayName' Entries = ($entries | ForEach-Object Key) -join ', ' Message = "In $($entries.Count) travel groups, whose blocks stack: $(($entries | ForEach-Object { "$($_.Key) allows $($_.Countries -join '+')" }) -join '; '). Signing in works from none of those countries; keep one group per traveler." } } } |