diff --git a/docs/use/walkthrough/05-providers-authentication.md b/docs/use/walkthrough/05-providers-authentication.md index 97e96ae8..0ff7abd7 100644 --- a/docs/use/walkthrough/05-providers-authentication.md +++ b/docs/use/walkthrough/05-providers-authentication.md @@ -146,6 +146,16 @@ $broker = New-IdleAuthSession -SessionMap @{ } ``` +:::info Framework-Reserved Keys + +The execution framework automatically injects `CorrelationId` and `Actor` into auth session options during execution. These keys have special handling: + +- **AuthSessionName-only patterns** (e.g., `@{ AuthSessionName = 'AD' }`): Framework keys are ignored during matching, allowing simple patterns to work regardless of injected metadata +- **Multi-key patterns** (e.g., `@{ AuthSessionName = 'AD'; Actor = 'ops-user' }`): Framework keys participate in matching, enabling advanced actor-based routing + +**Recommendation**: Use user-defined routing keys (like `Role`, `Environment`, `Tier`) instead of `Actor` or `CorrelationId` to avoid confusion, as framework values change per execution and are not under user control. +::: + To make the broker available at runtime, add it to the provider registry under the key `AuthSessionBroker`: ```powershell diff --git a/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 b/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 index ba6c0a7b..8db50910 100644 --- a/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 +++ b/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 @@ -180,6 +180,31 @@ function New-IdleAuthSessionBroker { 'SessionMap' ) } + + # Validate that patterns don't use framework-reserved keys inappropriately. + # CorrelationId and Actor are automatically injected by the execution context. + # Using them in patterns can lead to unexpected behavior: + # - For AuthSessionName-only patterns, they are ignored (to fix the reported bug) + # - For multi-key patterns, they CAN be matched, but values are framework-controlled + # We warn about this to help users avoid confusion. + $frameworkKeys = @('CorrelationId', 'Actor') + $hasFrameworkKeys = $false + foreach ($fwKey in $frameworkKeys) { + if ($pattern.ContainsKey($fwKey)) { + $hasFrameworkKeys = $true + break + } + } + + if ($hasFrameworkKeys) { + # Issue a warning for patterns using framework-controlled keys. + # These CAN match (in multi-key patterns), but values are controlled by the framework. + # Users should be aware this may not work as expected since CorrelationId/Actor + # are automatically set per execution, not per workflow definition. + $patternDesc = ($pattern.Keys | ForEach-Object { "$_=$($pattern[$_])" }) -join ', ' + Write-Warning "SessionMap pattern { $patternDesc } includes framework-controlled keys (CorrelationId, Actor). These keys are automatically set by the execution context and may not match user expectations. Consider using user-defined routing keys instead." + } + # Create a readable pattern description for error messages $patternDesc = ($pattern.Keys | ForEach-Object { "$_=$($pattern[$_])" }) -join ', ' $context = "SessionMap entry { $patternDesc }" @@ -248,6 +273,11 @@ function New-IdleAuthSessionBroker { $authSessionNameMatches = @() $legacyMatches = @() + # Framework metadata keys that are automatically injected by the execution context. + # These keys should be excluded when matching AuthSessionName-only patterns, + # but CAN be used in multi-key patterns for advanced routing scenarios. + $frameworkMetadataKeys = @('CorrelationId', 'Actor') + foreach ($entry in $this.SessionMap.GetEnumerator()) { $pattern = $entry.Key @@ -260,15 +290,28 @@ function New-IdleAuthSessionBroker { # If pattern has ONLY AuthSessionName (no other keys) if ($pattern.Keys.Count -eq 1) { - # Only match if Options is null or empty - if ($null -eq $Options -or $Options.Count -eq 0) { + # For AuthSessionName-only patterns, ignore framework metadata in Options. + # This allows simple patterns like @{ AuthSessionName = 'Entra' } to match + # even when the framework injects CorrelationId and Actor. + $hasUserOptions = $false + if ($null -ne $Options -and $Options.Count -gt 0) { + foreach ($key in $Options.Keys) { + if ($key -notin $frameworkMetadataKeys) { + $hasUserOptions = $true + break + } + } + } + + if (-not $hasUserOptions) { $authSessionNameMatches += $entry } continue } - # Pattern has additional keys beyond AuthSessionName - # All other keys in pattern must match Options (if Options provided) + # Pattern has additional keys beyond AuthSessionName. + # Match ALL keys in pattern against Options (including Actor/CorrelationId if present). + # This supports advanced routing: @{ AuthSessionName = 'AD'; Actor = 'ops-user' } $matches = $true foreach ($key in $pattern.Keys) { if ($key -eq 'AuthSessionName') { diff --git a/tests/Core/New-IdleAuthSession.Tests.ps1 b/tests/Core/New-IdleAuthSession.Tests.ps1 index 06620ca5..4aa79aa5 100644 --- a/tests/Core/New-IdleAuthSession.Tests.ps1 +++ b/tests/Core/New-IdleAuthSession.Tests.ps1 @@ -245,6 +245,112 @@ Describe 'New-IdleAuthSession' { } } + Context 'Framework metadata handling' { + It 'ignores CorrelationId when matching AuthSessionName-only pattern' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'Entra' } = @{ AuthSessionType = 'OAuth'; Credential = $testToken } + } + + # Framework adds CorrelationId and Actor to Options + $session = $broker.AcquireAuthSession('Entra', @{ CorrelationId = 'test-corr-id'; Actor = 'test-actor' }) + + $session | Should -Not -BeNullOrEmpty + $session | Should -Be 'mock-oauth-token-12345' + } + + It 'ignores Actor when matching AuthSessionName-only pattern' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Credential = $testCred } + } + + $session = $broker.AcquireAuthSession('AD', @{ Actor = 'admin-user' }) + + $session | Should -Not -BeNullOrEmpty + $session | Should -BeOfType [PSCredential] + $session.UserName | Should -Be 'TestUser' + } + + It 'ignores both CorrelationId and Actor when matching AuthSessionName-only pattern' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Credential = $testToken } + } + + $session = $broker.AcquireAuthSession('EXO', @{ CorrelationId = [guid]::NewGuid().ToString(); Actor = 'system' }) + + $session | Should -Not -BeNullOrEmpty + $session | Should -Be 'mock-oauth-token-12345' + } + + It 'matches AuthSessionName with additional user options despite framework metadata' { + $password1 = ConvertTo-SecureString 'Password1!' -AsPlainText -Force + $cred1 = New-Object System.Management.Automation.PSCredential('ADAdm', $password1) + + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD'; Role = 'Admin' } = $cred1 + } -AuthSessionType 'Credential' + + # Framework adds metadata, user provides Role + $session = $broker.AcquireAuthSession('AD', @{ Role = 'Admin'; CorrelationId = 'test-id'; Actor = 'user' }) + + $session | Should -Not -BeNullOrEmpty + $session.UserName | Should -Be 'ADAdm' + } + + It 'does not match when user provides non-matching options even with framework metadata' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Credential = $testCred } + } + + # User provides Role which is not in pattern, so should not match despite framework metadata + { $broker.AcquireAuthSession('AD', @{ Role = 'Admin'; CorrelationId = 'test' }) } | + Should -Throw '*No matching auth session found*' + } + + It 'allows Actor-based routing in multi-key patterns' { + $password1 = ConvertTo-SecureString 'OpsPassword!' -AsPlainText -Force + $opsCred = New-Object System.Management.Automation.PSCredential('ops-user', $password1) + + $password2 = ConvertTo-SecureString 'AdminPassword!' -AsPlainText -Force + $adminCred = New-Object System.Management.Automation.PSCredential('admin-user', $password2) + + # Suppress warnings for this test since we're intentionally using framework keys + $WarningPreference = 'SilentlyContinue' + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD'; Actor = 'ops-user' } = $opsCred + @{ AuthSessionName = 'AD'; Actor = 'admin-user' } = $adminCred + } -AuthSessionType 'Credential' + $WarningPreference = 'Continue' + + # Match ops-user + $session = $broker.AcquireAuthSession('AD', @{ Actor = 'ops-user'; CorrelationId = 'test' }) + $session.UserName | Should -Be 'ops-user' + + # Match admin-user + $session2 = $broker.AcquireAuthSession('AD', @{ Actor = 'admin-user'; CorrelationId = 'test2' }) + $session2.UserName | Should -Be 'admin-user' + } + + It 'issues warning when patterns include framework keys' { + $warnings = @() + $null = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD'; Actor = 'test' } = @{ AuthSessionType = 'Credential'; Credential = $testCred } + } -WarningVariable warnings -WarningAction SilentlyContinue + + $warnings.Count | Should -BeGreaterThan 0 + $warnings[0] | Should -Match 'framework-controlled keys' + } + + It 'issues warning when multi-key pattern with non-framework keys also includes framework keys' { + $warnings = @() + $null = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD'; Actor = 'test'; Role = 'Admin' } = @{ AuthSessionType = 'Credential'; Credential = $testCred } + } -WarningVariable warnings -WarningAction SilentlyContinue + + $warnings.Count | Should -BeGreaterThan 0 + $warnings[0] | Should -Match 'framework-controlled keys' + } + } + Context 'Module export' { It 'is available as exported command from IdLE module' { $command = Get-Command -Name New-IdleAuthSession -ErrorAction SilentlyContinue