Public/Resolve-NSPConditionalAccessPolicy.ps1
|
function Resolve-NSPConditionalAccessPolicy { <# .SYNOPSIS Builds the Graph request body for a policy definition from a tenant profile. Runs offline. .DESCRIPTION Makes no Graph calls and no writes. The definition is validated first; any error finding stops resolution. Every {Kind:Name} reference must resolve to at least one object ID or resolution throws, so an assignment or exclusion is never silently dropped. The body is in the Graph REST JSON shape (camelCase) for POST/PATCH identity/conditionalAccess/policies. The display name is '<Id> <Name>'. The state comes from the definition's DefaultState unless -State overrides it (reportOnly maps to enabledForReportingButNotEnforced). -TenantProfile binds breakglass, groups, the home country, and named locations. Its PolicyTargets section adds users and groups to this policy's includes and excludes, on top of the definition's own (Add-NSPConditionalAccessPolicyTarget). -Directory supplies lookups that are not tenant configuration, currently Applications (display name to appId). .EXAMPLE $tenantProfile = Get-Content .\Config\Local\TenantProfiles\contoso.json -Raw | ConvertFrom-Json Get-NSPConditionalAccessDefinition -Id 'NSP-BASE-MFA-01' | Resolve-NSPConditionalAccessPolicy -TenantProfile $tenantProfile -Directory $directory #> [CmdletBinding()] [OutputType([System.Collections.Specialized.OrderedDictionary])] param( [Parameter(Mandatory, ValueFromPipeline)][System.Collections.IDictionary]$Definition, [Parameter(Mandatory)]$TenantProfile, $Directory, [ValidateSet('reportOnly', 'disabled', 'enabled')][string]$State ) begin { $schema = Get-NSPConditionalAccessDefinitionSchema } process { $errors = @(Test-NSPConditionalAccessDefinition -Definition $Definition | Where-Object Severity -eq 'Error') if ($errors.Count) { throw "Definition $($Definition['Id']) is invalid: $(($errors | ForEach-Object { "[$($_.Rule)] $($_.Message)" }) -join ' ')" } $conditions = [ordered]@{} foreach ($spec in $schema.Lists) { $container = if ($spec.Section) { $Definition[$spec.Section] } else { $Definition } if ($container -isnot [System.Collections.IDictionary] -or -not $container.Contains($spec.Key)) { continue } $field = if ($spec.Section) { "$($spec.Section).$($spec.Key)" } else { $spec.Key } $resolved = [System.Collections.Generic.List[string]]::new() foreach ($item in @($container[$spec.Key] | Where-Object { $null -ne $_ })) { $values = if (-not $spec.Contains('Values') -and $item -cmatch $schema.ReferencePattern) { Resolve-NSPConditionalAccessReference -Kind $Matches['kind'] -Name $Matches['name'] ` -TenantProfile $TenantProfile -Directory $Directory -Context "$($Definition['Id']) $field" } else { $item } foreach ($value in @($values | Where-Object { $_ })) { if (-not $resolved.Contains($value)) { $resolved.Add($value) } } } if ($resolved.Count -eq 0) { continue } # Assigned in each branch: an if-expression would unroll a one-item list. if ($spec.Shape -eq 'Csv') { $value = $resolved.ToArray() -join ',' } elseif ($spec.Shape -eq 'GuestTypes') { $value = [ordered]@{ guestOrExternalUserTypes = $resolved.ToArray() -join ',' externalTenants = [ordered]@{ '@odata.type' = '#microsoft.graph.conditionalAccessAllExternalTenants'; membershipKind = 'all' } } } else { $value = $resolved.ToArray() } if ($spec.Target.Count -eq 1) { $conditions[$spec.Target[0]] = $value } else { if (-not $conditions.Contains($spec.Target[0])) { $conditions[$spec.Target[0]] = [ordered]@{} } $conditions[$spec.Target[0]][$spec.Target[1]] = $value } } # Users and groups the operator added to this policy (Add-NSPConditionalAccessPolicyTarget). $added = Get-NSPConditionalAccessMapValue (Get-NSPConditionalAccessMapValue $TenantProfile 'PolicyTargets') ([string]$Definition['Id']) foreach ($field in 'IncludeUsers', 'IncludeGroups', 'ExcludeUsers', 'ExcludeGroups') { $ids = @(Get-NSPConditionalAccessMapValue $added $field | ForEach-Object { Get-NSPConditionalAccessMapValue $_ 'Id' } | Where-Object { $_ }) if (-not $ids.Count) { continue } if (-not $conditions.Contains('users')) { $conditions['users'] = [ordered]@{} } $users = $conditions['users'] if ($field -like 'Include*' -and @($users['includeUsers']) -contains 'All') { throw "$($Definition['Id']): the tenant profile adds $field, but the policy already includes every user. Remove the addition." } $key = $field.Substring(0, 1).ToLowerInvariant() + $field.Substring(1) $merged = [System.Collections.Generic.List[string]]::new() foreach ($value in @(@($users[$key]) + $ids | Where-Object { $_ })) { if (-not $merged.Contains([string]$value)) { $merged.Add([string]$value) } } $users[$key] = $merged.ToArray() } $stateName = if ($State) { $State } else { $Definition['DefaultState'] } $body = [ordered]@{ displayName = "$($Definition['Id']) $($Definition['Name'])" state = $schema.States[$stateName] conditions = $conditions } if ($Definition['Grant']) { $body['grantControls'] = [ordered]@{ operator = $Definition['Grant']['Operator'] builtInControls = @($Definition['Grant']['BuiltInControls'] | Where-Object { $_ }) } $strength = $Definition['Grant']['AuthenticationStrength'] if ($strength -and $strength -cmatch $schema.ReferencePattern) { $strengthId = Resolve-NSPConditionalAccessReference -Kind $Matches['kind'] -Name $Matches['name'] ` -TenantProfile $TenantProfile -Directory $Directory -Context "$($Definition['Id']) Grant.AuthenticationStrength" $body['grantControls']['authenticationStrength'] = [ordered]@{ id = [string]$strengthId } } } $session = $Definition['Session'] $frequency = if ($session) { $session['SignInFrequency'] } else { $null } $persistent = if ($session) { $session['PersistentBrowser'] } else { $null } if ($frequency -or $persistent) { $body['sessionControls'] = [ordered]@{} } if ($frequency -and $frequency['EveryTime']) { $body['sessionControls']['signInFrequency'] = [ordered]@{ frequencyInterval = 'everyTime' authenticationType = 'primaryAndSecondaryAuthentication' isEnabled = $true } } elseif ($frequency) { $body['sessionControls']['signInFrequency'] = [ordered]@{ value = [int]$frequency['Value'] type = $frequency['Type'] isEnabled = $true } } if ($persistent) { $body['sessionControls']['persistentBrowser'] = [ordered]@{ mode = $persistent; isEnabled = $true } } $body } } |