Private/Controls/Resolve-CAGrantControl.ps1
function Resolve-CAGrantControl { <# .SYNOPSIS Evaluates the grant controls for a Conditional Access policy that applies to a sign-in scenario. .DESCRIPTION This function evaluates the grant controls of a Conditional Access policy to determine if access is blocked, granted, or conditionally granted based on the policy's requirements and the provided user and device contexts. The evaluation follows Microsoft's implementation order with block priority: 1. First check for block controls which immediately result in blocked access 2. Process authentication strength if specified 3. Evaluate remaining controls with unified AND/OR logic .PARAMETER Policy The Conditional Access policy to evaluate. .PARAMETER UserContext The user context for the sign-in scenario. .PARAMETER DeviceContext The device context for the sign-in scenario. .EXAMPLE Resolve-CAGrantControl -Policy $policy -UserContext $UserContext -DeviceContext $DeviceContext #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [object]$Policy, [Parameter(Mandatory = $true)] [object]$UserContext, [Parameter(Mandatory = $true)] [object]$DeviceContext ) # If no grant controls specified, access is granted if (-not $Policy.GrantControls -or -not $Policy.GrantControls.BuiltInControls) { Write-Verbose "No grant controls specified, access is granted" return @{ AccessResult = "Granted" Reason = "No grant controls specified" SatisfiedControls = @() RequiredControls = @() } } $controls = $Policy.GrantControls.BuiltInControls $operator = $Policy.GrantControls._Operator # AND or OR # Always check block first - immediate exit if block is specified if ($controls -contains "block") { Write-Verbose "Block control is specified, access is blocked" return @{ AccessResult = "Blocked" Reason = "Block control specified" SatisfiedControls = @() RequiredControls = @() } } # Handle authentication strength as a special case with priority if ($Policy.GrantControls.AuthenticationStrength) { $authStrengthResult = Test-AuthenticationStrength -AuthStrength $Policy.GrantControls.AuthenticationStrength -UserContext $UserContext if (-not $authStrengthResult.Satisfied) { Write-Verbose "Authentication strength requirement not met: $($authStrengthResult.Reason)" return @{ AccessResult = "ConditionallyGranted" Reason = "Authentication strength requirement not met" SatisfiedControls = @() RequiredControls = @("Authentication Strength: $($authStrengthResult.RequiredStrength)") AuthStrengthDetails = $authStrengthResult } } Write-Verbose "Authentication strength requirement satisfied: $($authStrengthResult.Reason)" # If no other controls specified, access is granted if (-not $controls -or $controls.Count -eq 0) { return @{ AccessResult = "Granted" Reason = "Authentication strength requirement satisfied" SatisfiedControls = @("Authentication Strength: $($authStrengthResult.RequiredStrength)") RequiredControls = @() AuthStrengthDetails = $authStrengthResult } } } # Process all other controls $satisfiedControls = @() $requiredControls = @() # Process each control type foreach ($control in $controls) { $controlResult = Test-GrantControl -Control $control -UserContext $UserContext -DeviceContext $DeviceContext if ($controlResult.Satisfied) { $satisfiedControls += $controlResult.DisplayName } else { $requiredControls += $controlResult.DisplayName } # Debug verbose output to help troubleshoot Write-Verbose "Control: $control - Display Name: $($controlResult.DisplayName) - Satisfied: $($controlResult.Satisfied)" } # Determine access result based on operator with unified logic $accessGranted = if ($operator -eq "OR") { # At least one control must be satisfied for OR $satisfiedControls.Count -gt 0 } else { # All controls must be satisfied for AND (default) $requiredControls.Count -eq 0 } if ($accessGranted) { Write-Verbose "Access granted. All required controls satisfied." return @{ AccessResult = "Granted" Reason = "All required controls satisfied" SatisfiedControls = $satisfiedControls RequiredControls = @() } } else { # For conditional access, make sure we have at least one default control if none specified if ($requiredControls.Count -eq 0) { $requiredControls = @("mfa") # Default to MFA as required control if nothing else specified Write-Verbose "No specific required controls found, defaulting to MFA requirement" } Write-Verbose "Access conditionally granted. Required controls: $($requiredControls -join ', ')" return @{ AccessResult = "ConditionallyGranted" Reason = if ($operator -eq "OR") { "At least one control must be satisfied" } else { "All controls must be satisfied" } SatisfiedControls = $satisfiedControls RequiredControls = $requiredControls } } } function Test-GrantControl { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [Parameter(Mandatory = $true)] [string]$Control, [Parameter(Mandatory = $true)] [object]$UserContext, [Parameter(Mandatory = $true)] [object]$DeviceContext ) Write-Verbose "Testing grant control: $Control" $controlMap = @{ "mfa" = @{ DisplayName = "mfa" TestProperty = { param($u, $d) $u.MfaAuthenticated } } "compliantDevice" = @{ DisplayName = "compliantDevice" TestProperty = { param($u, $d) $d.Compliance } } "domainJoinedDevice" = @{ DisplayName = "domainJoinedDevice" TestProperty = { param($u, $d) $d.JoinType -eq "Hybrid" } } "approvedApplication" = @{ DisplayName = "approvedApplication" TestProperty = { param($u, $d) $d.ApprovedApplication } } "compliantApplication" = @{ DisplayName = "appProtectionPolicy" TestProperty = { param($u, $d) $d.AppProtectionPolicy } } "passwordChange" = @{ DisplayName = "passwordChange" TestProperty = { param($u, $d) $false } # For simulation, always require password change if specified } "terms" = @{ DisplayName = "terms" TestProperty = { param($u, $d) $false } # For simulation, always require terms if specified } } # Get control details from the map $controlDetails = $controlMap[$Control] # Default behavior for custom controls and unknown controls if (-not $controlDetails) { return @{ Satisfied = $false DisplayName = $Control } } # Test if the control is satisfied $satisfied = & $controlDetails.TestProperty $UserContext $DeviceContext return @{ Satisfied = $satisfied DisplayName = $controlDetails.DisplayName } } function Test-AuthenticationStrength { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [Parameter(Mandatory = $true)] [object]$AuthStrength, [Parameter(Mandatory = $true)] [object]$UserContext ) # This is a stub for now - will be implemented in Phase 2, Task 2.1 # Currently always returns not satisfied to simulate the requirement return @{ Satisfied = $false RequiredStrength = $AuthStrength.displayName } } |