Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/use/walkthrough/05-providers-authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 47 additions & 4 deletions src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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 }"
Expand Down Expand Up @@ -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

Expand All @@ -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') {
Expand Down
106 changes: 106 additions & 0 deletions tests/Core/New-IdleAuthSession.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading