From d6c64e4075de86cfd36c36a9fadaa65430f2e579 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 13:04:09 +0000 Subject: [PATCH 1/5] Initial plan From dd223abe777486e00f20a33b4d4dd6083de25222 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 13:10:48 +0000 Subject: [PATCH 2/5] Implement AuthSessionName-based routing and make SessionMap optional - Made SessionMap optional in New-IdleAuthSessionBroker/New-IdleAuthSession - Added validation: require DefaultAuthSession when SessionMap is empty/null - Implemented AuthSessionName-based routing in AcquireAuthSession method - Support AuthSessionName key in SessionMap keys (e.g., @{ AuthSessionName = 'AD'; Role = 'ADAdm' }) - Prioritize AuthSessionName matches over legacy Options-only matches - Added ambiguity detection for multiple AuthSessionName-only matches - Updated Invoke-IdleProviderMethod to acquire default session even without AuthSessionName - Moved AuthSessionOptions validation to occur before broker check - Added comprehensive tests for new routing behavior - All existing tests pass Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- .../Public/Invoke-IdleProviderMethod.ps1 | 41 ++++- .../Public/New-IdleAuthSessionBroker.ps1 | 152 +++++++++++++---- src/IdLE/Public/New-IdleAuthSession.ps1 | 9 +- tests/Core/New-IdleAuthSession.Tests.ps1 | 154 ++++++++++++++++++ .../Invoke-IdleStepAuthSession.Tests.ps1 | 66 ++++++++ 5 files changed, 377 insertions(+), 45 deletions(-) diff --git a/src/IdLE.Core/Public/Invoke-IdleProviderMethod.ps1 b/src/IdLE.Core/Public/Invoke-IdleProviderMethod.ps1 index e750241a..ec3a10bf 100644 --- a/src/IdLE.Core/Public/Invoke-IdleProviderMethod.ps1 +++ b/src/IdLE.Core/Public/Invoke-IdleProviderMethod.ps1 @@ -11,7 +11,9 @@ function Invoke-IdleProviderMethod { provider methods with proper authentication handling. Key features: - - Acquires auth sessions via Context.AcquireAuthSession when With.AuthSessionName is present + - Acquires auth sessions via Context.AcquireAuthSession when AuthSessionBroker is available + - If With.AuthSessionName is present, uses it for session routing + - If With.AuthSessionName is absent but broker exists, attempts to acquire default session - Detects whether provider methods support AuthSession parameter (backwards compatible) - Passes AuthSession to provider methods that support it - Validates provider existence and method implementation @@ -70,15 +72,40 @@ function Invoke-IdleProviderMethod { # Auth session acquisition (optional, data-only) $authSession = $null - if ($With.ContainsKey('AuthSessionName')) { - $sessionName = [string]$With.AuthSessionName - $sessionOptions = if ($With.ContainsKey('AuthSessionOptions')) { $With.AuthSessionOptions } else { $null } - + + # Validate AuthSessionOptions early (regardless of broker availability) + if ($With.ContainsKey('AuthSessionOptions')) { + $sessionOptions = $With.AuthSessionOptions if ($null -ne $sessionOptions -and -not ($sessionOptions -is [hashtable])) { throw "With.AuthSessionOptions must be a hashtable or null." } - - $authSession = $Context.AcquireAuthSession($sessionName, $sessionOptions) + } + + # Check if an AuthSessionBroker is available + $brokerAvailable = $Context.PSObject.Properties.Name -contains 'Providers' -and + $null -ne $Context.Providers -and + $Context.Providers.ContainsKey('AuthSessionBroker') + + if ($brokerAvailable) { + # If AuthSessionName is provided, use it for routing + if ($With.ContainsKey('AuthSessionName')) { + $sessionName = [string]$With.AuthSessionName + $sessionOptions = if ($With.ContainsKey('AuthSessionOptions')) { $With.AuthSessionOptions } else { $null } + + $authSession = $Context.AcquireAuthSession($sessionName, $sessionOptions) + } + else { + # No AuthSessionName provided - try to acquire default session + # Use a placeholder name and no options to trigger default session logic + try { + $authSession = $Context.AcquireAuthSession('__default__', $null) + } + catch { + # If acquiring default fails, continue without auth session + # This preserves backward compatibility for providers that don't require auth + $authSession = $null + } + } } $provider = $Context.Providers[$ProviderAlias] diff --git a/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 b/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 index 5462595f..ab29c583 100644 --- a/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 +++ b/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 @@ -13,16 +13,17 @@ function New-IdleAuthSessionBroker { AcquireAuthSession method. .PARAMETER SessionMap - A hashtable that maps session configurations to auth sessions. Each key is a hashtable + Optional hashtable that maps session configurations to auth sessions. Each key is a hashtable representing the AuthSessionOptions pattern, and each value is the auth session to return. The value can be a PSCredential, token string, session object, or any object appropriate for the AuthSessionType. - Common patterns: - - @{ Role = 'Tier0' } -> $tier0Credential (for Credential type) - - @{ Role = 'Admin' } -> $adminToken (for OAuth type) - - @{ Server = 'Server01' } -> $remoteSession (for PSRemoting type) - - @{ Environment = 'Production' } -> $prodCred + Keys can include AuthSessionName for name-based routing: + - @{ AuthSessionName = 'AD'; Role = 'ADAdm' } -> $admAD (AuthSessionName + Role routing) + - @{ AuthSessionName = 'EXO' } -> $exoToken (AuthSessionName-only routing) + - @{ Role = 'Tier0' } -> $tier0Credential (Options-only routing, legacy support) + + SessionMap is optional if DefaultAuthSession is provided. .PARAMETER DefaultAuthSession Optional default auth session to return when no session options are provided or @@ -39,17 +40,21 @@ function New-IdleAuthSessionBroker { - 'Credential': Credential-based authentication (e.g., Active Directory, mock providers) .EXAMPLE - # Simple role-based broker with Credential session type - $broker = New-IdleAuthSessionBroker -SessionMap @{ - @{ Role = 'Tier0' } = $tier0Credential - @{ Role = 'Admin' } = $adminCredential - } -DefaultAuthSession $adminCredential -AuthSessionType 'Credential' + # Simple single-credential broker (no SessionMap required) + $broker = New-IdleAuthSessionBroker -DefaultAuthSession $admCred -AuthSessionType 'Credential' $plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Providers @{ Identity = New-IdleADIdentityProvider AuthSessionBroker = $broker } + .EXAMPLE + # AuthSessionName-based routing with roles + $broker = New-IdleAuthSessionBroker -SessionMap @{ + @{ AuthSessionName = 'AD'; Role = 'ADAdm' } = $tier0Credential + @{ AuthSessionName = 'EXO'; Role = 'EXOAdm' } = $exoToken + } -DefaultAuthSession $adminCredential -AuthSessionType 'Credential' + .EXAMPLE # OAuth broker with token strings $broker = New-IdleAuthSessionBroker -SessionMap @{ @@ -74,8 +79,9 @@ function New-IdleAuthSessionBroker { #> [CmdletBinding()] param( - [Parameter(Mandatory)] - [ValidateNotNull()] + [Parameter()] + [AllowNull()] + [AllowEmptyCollection()] [hashtable] $SessionMap, [Parameter()] @@ -87,6 +93,11 @@ function New-IdleAuthSessionBroker { [string] $AuthSessionType ) + # Validate: If SessionMap is empty/null, DefaultAuthSession must be provided + if (($null -eq $SessionMap -or $SessionMap.Count -eq 0) -and $null -eq $DefaultAuthSession) { + throw "SessionMap is empty or null. DefaultAuthSession must be provided when SessionMap is not used." + } + $broker = [pscustomobject]@{ PSTypeName = 'IdLE.AuthSessionBroker' SessionMap = $SessionMap @@ -105,10 +116,6 @@ function New-IdleAuthSessionBroker { [hashtable] $Options ) - # $Name is part of the broker contract but not used in this simple implementation - # This broker routes based on Options only; custom brokers may use Name for additional routing - $null = $Name - # TODO: Implement type-specific validation rules for AuthSessionType # Current implementation allows all options for all session types # Future enhancements may add: @@ -116,40 +123,115 @@ function New-IdleAuthSessionBroker { # - PSRemoting: Validate remote session state, connectivity # - Credential: Validate credential format, domain membership - # If no options provided, return default - if ($null -eq $Options -or $Options.Count -eq 0) { + # If SessionMap is null or empty, return default + if ($null -eq $this.SessionMap -or $this.SessionMap.Count -eq 0) { if ($null -ne $this.DefaultAuthSession) { return $this.DefaultAuthSession } - throw "No auth session options provided and no default auth session configured." + throw "No SessionMap configured and no default auth session available." } - # Find matching session in map + # Matching logic: + # 1. If Name provided: try to match entries with AuthSessionName key + # 2. If Options provided: match all key/value pairs + # 3. Fall back to DefaultAuthSession + # 4. Fail with clear error + + $authSessionNameMatches = @() + $legacyMatches = @() + foreach ($entry in $this.SessionMap.GetEnumerator()) { $pattern = $entry.Key - $credential = $entry.Value - - # Check if all keys in pattern match Options - $matches = $true - foreach ($key in $pattern.Keys) { - if (-not $Options.ContainsKey($key) -or $Options[$key] -ne $pattern[$key]) { - $matches = $false - break + + # Check if pattern includes AuthSessionName + if ($pattern.ContainsKey('AuthSessionName')) { + # AuthSessionName must match + if ($pattern.AuthSessionName -ne $Name) { + continue + } + + # If pattern has ONLY AuthSessionName (no other keys), it's a match + if ($pattern.Keys.Count -eq 1) { + $authSessionNameMatches += $entry + continue + } + + # Pattern has additional keys beyond AuthSessionName + # All other keys in pattern must match Options (if Options provided) + $matches = $true + foreach ($key in $pattern.Keys) { + if ($key -eq 'AuthSessionName') { + continue # Already checked + } + + # If Options is null or doesn't contain the key, no match + if ($null -eq $Options -or -not $Options.ContainsKey($key) -or $Options[$key] -ne $pattern[$key]) { + $matches = $false + break + } + } + + if ($matches) { + $authSessionNameMatches += $entry } } - - if ($matches) { - return $credential + else { + # Legacy: pattern without AuthSessionName - match based on Options only + if ($null -eq $Options -or $Options.Count -eq 0) { + continue # No options to match + } + + $matches = $true + foreach ($key in $pattern.Keys) { + if (-not $Options.ContainsKey($key) -or $Options[$key] -ne $pattern[$key]) { + $matches = $false + break + } + } + + if ($matches) { + $legacyMatches += $entry + } } } - # No match found + # Prioritize AuthSessionName-based matches over legacy matches + $matchingEntries = @() + if (@($authSessionNameMatches).Count -gt 0) { + $matchingEntries = @($authSessionNameMatches) + } else { + $matchingEntries = @($legacyMatches) + } + + # Return first match if exactly one found + if ($matchingEntries.Count -eq 1) { + return $matchingEntries[0].Value + } + + # If multiple matches, this is ambiguous - fail with clear error + if ($matchingEntries.Count -gt 1) { + $matchDetails = ($matchingEntries | ForEach-Object { + $currentEntry = $_ + $keyStr = ($currentEntry.Key.Keys | ForEach-Object { "$_=$($currentEntry.Key[$_])" }) -join ', ' + "{ $keyStr }" + }) -join '; ' + throw "Ambiguous auth session match for Name='$Name'. Multiple entries matched: $matchDetails. Provide AuthSessionOptions to disambiguate." + } + + # No match found - fall back to default if ($null -ne $this.DefaultAuthSession) { return $this.DefaultAuthSession } - $optionsStr = ($Options.Keys | ForEach-Object { "$_=$($Options[$_])" }) -join ', ' - throw "No matching auth session found for options: $optionsStr" + # No match and no default + $nameStr = "Name='$Name'" + $optionsPart = if ($null -ne $Options -and $Options.Count -gt 0) { + $optsStr = ($Options.Keys | ForEach-Object { "$_=$($Options[$_])" }) -join ', ' + ", Options={ $optsStr }" + } else { + "" + } + throw "No matching auth session found for $nameStr$optionsPart and no default auth session configured." } -Force return $broker diff --git a/src/IdLE/Public/New-IdleAuthSession.ps1 b/src/IdLE/Public/New-IdleAuthSession.ps1 index 434b8ee2..5f5b3fd8 100644 --- a/src/IdLE/Public/New-IdleAuthSession.ps1 +++ b/src/IdLE/Public/New-IdleAuthSession.ps1 @@ -46,8 +46,9 @@ function New-IdleAuthSession { #> [CmdletBinding()] param( - [Parameter(Mandatory)] - [ValidateNotNull()] + [Parameter()] + [AllowNull()] + [AllowEmptyCollection()] [hashtable] $SessionMap, [Parameter()] @@ -61,9 +62,11 @@ function New-IdleAuthSession { # Delegate to IdLE.Core implementation. $params = @{ - SessionMap = $SessionMap AuthSessionType = $AuthSessionType } + if ($PSBoundParameters.ContainsKey('SessionMap')) { + $params['SessionMap'] = $SessionMap + } if ($PSBoundParameters.ContainsKey('DefaultAuthSession')) { $params['DefaultAuthSession'] = $DefaultAuthSession } diff --git a/tests/Core/New-IdleAuthSession.Tests.ps1 b/tests/Core/New-IdleAuthSession.Tests.ps1 index 064d04c9..1518768b 100644 --- a/tests/Core/New-IdleAuthSession.Tests.ps1 +++ b/tests/Core/New-IdleAuthSession.Tests.ps1 @@ -166,4 +166,158 @@ Describe 'New-IdleAuthSession' { $session | Should -Not -BeNullOrEmpty } } + + Context 'Optional SessionMap' { + It 'creates broker with only DefaultAuthSession (no SessionMap)' { + $broker = New-IdleAuthSession -DefaultAuthSession $testCred -AuthSessionType 'Credential' + + $broker | Should -Not -BeNullOrEmpty + $broker.DefaultAuthSession | Should -Not -BeNullOrEmpty + $broker.SessionMap | Should -BeNullOrEmpty + } + + It 'returns DefaultAuthSession when SessionMap is null' { + $broker = New-IdleAuthSession -DefaultAuthSession $testCred -AuthSessionType 'Credential' + + $session = $broker.AcquireAuthSession('AnyName', $null) + + $session | Should -Not -BeNullOrEmpty + $session.UserName | Should -Be 'TestUser' + } + + It 'returns DefaultAuthSession when SessionMap is empty' { + $broker = New-IdleAuthSession -SessionMap @{} -DefaultAuthSession $testCred -AuthSessionType 'Credential' + + $session = $broker.AcquireAuthSession('AnyName', $null) + + $session | Should -Not -BeNullOrEmpty + $session.UserName | Should -Be 'TestUser' + } + + It 'throws when SessionMap is null and DefaultAuthSession is not provided' { + { + New-IdleAuthSession -SessionMap $null -AuthSessionType 'Credential' + } | Should -Throw '*DefaultAuthSession must be provided*' + } + + It 'throws when SessionMap is empty and DefaultAuthSession is not provided' { + { + New-IdleAuthSession -SessionMap @{} -AuthSessionType 'Credential' + } | Should -Throw '*DefaultAuthSession must be provided*' + } + } + + Context 'AuthSessionName-based routing' { + BeforeEach { + $password1 = ConvertTo-SecureString 'Password1!' -AsPlainText -Force + $cred1 = New-Object System.Management.Automation.PSCredential('ADAdm', $password1) + + $password2 = ConvertTo-SecureString 'Password2!' -AsPlainText -Force + $cred2 = New-Object System.Management.Automation.PSCredential('EXOAdm', $password2) + + $password3 = ConvertTo-SecureString 'Password3!' -AsPlainText -Force + $cred3 = New-Object System.Management.Automation.PSCredential('ADRead', $password3) + } + + It 'matches AuthSessionName without options' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = $cred1 + @{ AuthSessionName = 'EXO' } = $cred2 + } -AuthSessionType 'Credential' + + $session = $broker.AcquireAuthSession('AD', $null) + + $session | Should -Not -BeNullOrEmpty + $session.UserName | Should -Be 'ADAdm' + } + + It 'matches AuthSessionName with matching options' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD'; Role = 'ADAdm' } = $cred1 + @{ AuthSessionName = 'AD'; Role = 'ADRead' } = $cred3 + } -AuthSessionType 'Credential' + + $session = $broker.AcquireAuthSession('AD', @{ Role = 'ADRead' }) + + $session | Should -Not -BeNullOrEmpty + $session.UserName | Should -Be 'ADRead' + } + + It 'falls back to default when AuthSessionName does not match' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = $cred1 + } -DefaultAuthSession $testCred -AuthSessionType 'Credential' + + $session = $broker.AcquireAuthSession('EXO', $null) + + $session | Should -Not -BeNullOrEmpty + $session.UserName | Should -Be 'TestUser' + } + + It 'throws when AuthSessionName matches multiple entries (ambiguous)' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = $cred1 + @{ AuthSessionName = 'AD' } = $cred3 + } -AuthSessionType 'Credential' + + { $broker.AcquireAuthSession('AD', $null) } | + Should -Throw '*Ambiguous*' + } + + It 'prefers AuthSessionName match over legacy Options-only match' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ Role = 'Admin' } = $testCred + @{ AuthSessionName = 'AD'; Role = 'Admin' } = $cred1 + } -AuthSessionType 'Credential' + + $session = $broker.AcquireAuthSession('AD', @{ Role = 'Admin' }) + + $session | Should -Not -BeNullOrEmpty + $session.UserName | Should -Be 'ADAdm' + } + + It 'supports legacy Options-only routing when AuthSessionName is not in pattern' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ Role = 'Tier0' } = $cred1 + @{ Role = 'Admin' } = $cred2 + } -AuthSessionType 'Credential' + + $session = $broker.AcquireAuthSession('AnyName', @{ Role = 'Admin' }) + + $session | Should -Not -BeNullOrEmpty + $session.UserName | Should -Be 'EXOAdm' + } + + It 'throws when AuthSessionName does not match and no default provided' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = $cred1 + } -AuthSessionType 'Credential' + + { $broker.AcquireAuthSession('EXO', $null) } | + Should -Throw '*No matching auth session found*' + } + + It 'matches complex pattern: AuthSessionName + multiple options' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD'; Role = 'Admin'; Environment = 'Prod' } = $cred1 + } -AuthSessionType 'Credential' + + $session = $broker.AcquireAuthSession('AD', @{ Role = 'Admin'; Environment = 'Prod' }) + + $session | Should -Not -BeNullOrEmpty + $session.UserName | Should -Be 'ADAdm' + } + + It 'does not match when partial options provided' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD'; Role = 'Admin'; Environment = 'Prod' } = $cred1 + } -DefaultAuthSession $testCred -AuthSessionType 'Credential' + + # Only providing Role, not Environment - should fall back to default + $session = $broker.AcquireAuthSession('AD', @{ Role = 'Admin' }) + + $session | Should -Not -BeNullOrEmpty + $session.UserName | Should -Be 'TestUser' + } + } } diff --git a/tests/Steps/Invoke-IdleStepAuthSession.Tests.ps1 b/tests/Steps/Invoke-IdleStepAuthSession.Tests.ps1 index 0640054b..85a1db23 100644 --- a/tests/Steps/Invoke-IdleStepAuthSession.Tests.ps1 +++ b/tests/Steps/Invoke-IdleStepAuthSession.Tests.ps1 @@ -294,5 +294,71 @@ Describe 'IdLE.Steps - Auth Session Routing' { { Invoke-IdleStepEnsureAttribute -Context $context -Step $step } | Should -Throw '*AuthSessionOptions*hashtable*' } + + It 'acquires default auth session when AuthSessionName is absent but broker exists' { + # Arrange + $testState = [pscustomobject]@{ + SessionAcquired = $false + AcquiredName = $null + AcquiredOptions = $null + } + + $broker = [pscustomobject]@{ + PSTypeName = 'Tests.AuthSessionBroker' + State = $testState + } + $broker | Add-Member -MemberType ScriptMethod -Name AcquireAuthSession -Value { + param($Name, $Options) + $this.State.SessionAcquired = $true + $this.State.AcquiredName = $Name + $this.State.AcquiredOptions = $Options + return [PSCredential]::new('defaultuser', (ConvertTo-SecureString 'defaultpass' -AsPlainText -Force)) + } -Force + + $mockProvider = [pscustomobject]@{ + PSTypeName = 'Tests.MockProvider' + } + $mockProvider | Add-Member -MemberType ScriptMethod -Name EnsureAttribute -Value { + param($IdentityKey, $Name, $Value, $AuthSession) + return [pscustomobject]@{ + PSTypeName = 'IdLE.ProviderResult' + Changed = $true + } + } -Force + + $context = [pscustomobject]@{ + PSTypeName = 'IdLE.ExecutionContext' + Providers = @{ + Identity = $mockProvider + AuthSessionBroker = $broker + } + } + $context | Add-Member -MemberType ScriptMethod -Name AcquireAuthSession -Value { + param($Name, $Options) + return $this.Providers.AuthSessionBroker.AcquireAuthSession($Name, $Options) + } -Force + + $step = [pscustomobject]@{ + PSTypeName = 'IdLE.Step' + Name = 'TestStep' + Type = 'IdLE.Step.EnsureAttribute' + With = @{ + IdentityKey = 'testuser' + Name = 'Department' + Value = 'IT' + # No AuthSessionName - should still try to acquire default session + } + } + + # Act + $result = Invoke-IdleStepEnsureAttribute -Context $context -Step $step + + # Assert + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Completed' + $testState.SessionAcquired | Should -Be $true + $testState.AcquiredName | Should -Be '__default__' + $testState.AcquiredOptions | Should -BeNullOrEmpty + } } } From 203db12f5cb182257450b4924c56f90dc76d5b72 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 13:34:18 +0000 Subject: [PATCH 3/5] Add Server/Domain/Environment examples and clarify DefaultAuthSession requirement - Re-added Server, Domain, and Environment examples to SessionMap documentation - These patterns are used by providers (PSRemoting, multi-forest AD, environment routing) - Clarified that at least one of SessionMap or DefaultAuthSession must be provided - All tests passing (30 tests, 0 failures) Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 b/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 index ab29c583..091e81e1 100644 --- a/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 +++ b/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 @@ -22,6 +22,9 @@ function New-IdleAuthSessionBroker { - @{ AuthSessionName = 'AD'; Role = 'ADAdm' } -> $admAD (AuthSessionName + Role routing) - @{ AuthSessionName = 'EXO' } -> $exoToken (AuthSessionName-only routing) - @{ Role = 'Tier0' } -> $tier0Credential (Options-only routing, legacy support) + - @{ Server = 'AADConnect01' } -> $remoteSession (for PSRemoting scenarios) + - @{ Domain = 'SourceAD' } -> $sourceCred (for multi-forest scenarios) + - @{ Environment = 'Production' } -> $prodCred (for environment-specific routing) SessionMap is optional if DefaultAuthSession is provided. @@ -30,6 +33,8 @@ function New-IdleAuthSessionBroker { when the options don't match any entry in SessionMap. Can be a PSCredential, token string, session object, or any object appropriate for the AuthSessionType. + At least one of SessionMap or DefaultAuthSession must be provided. + .PARAMETER AuthSessionType Specifies the type of authentication session. This determines validation rules, lifecycle management, and telemetry behavior. @@ -74,6 +79,13 @@ function New-IdleAuthSessionBroker { @{ Server = 'AADConnect01' } = $remoteSessionCred } -AuthSessionType 'PSRemoting' + .EXAMPLE + # Environment-based routing for multi-environment scenarios + $broker = New-IdleAuthSessionBroker -SessionMap @{ + @{ Environment = 'Production' } = $prodCred + @{ Environment = 'Test' } = $testCred + } -DefaultAuthSession $devCred -AuthSessionType 'Credential' + .OUTPUTS PSCustomObject with AcquireAuthSession method #> From 213cc1ae2b8c7947dc8cb2fb142567290037c4bb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 14:56:31 +0000 Subject: [PATCH 4/5] Fix critical auth session acquisition issues - Fix example in New-IdleAuthSessionBroker to avoid mixing token with Credential type - Move provider method support check before auth acquisition to avoid unnecessary broker calls - Replace '__default__' magic string with empty string for default session requests - Add fail-fast when AuthSessionName is set but Context lacks AcquireAuthSession method - Fix Name-only pattern matching to only match when Options is null/empty (prevents ambiguity) - Improve error handling: rethrow broker errors when provider method requires AuthSession - Add test for fail-fast behavior when AuthSessionName is set without broker - All 381 tests passing Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- .../Public/Invoke-IdleProviderMethod.ps1 | 51 +++++++++++-------- .../Public/New-IdleAuthSessionBroker.ps1 | 19 +++++-- .../Invoke-IdleStepAuthSession.Tests.ps1 | 40 ++++++++++++++- 3 files changed, 84 insertions(+), 26 deletions(-) diff --git a/src/IdLE.Core/Public/Invoke-IdleProviderMethod.ps1 b/src/IdLE.Core/Public/Invoke-IdleProviderMethod.ps1 index ec3a10bf..11540f7c 100644 --- a/src/IdLE.Core/Public/Invoke-IdleProviderMethod.ps1 +++ b/src/IdLE.Core/Public/Invoke-IdleProviderMethod.ps1 @@ -81,12 +81,29 @@ function Invoke-IdleProviderMethod { } } - # Check if an AuthSessionBroker is available - $brokerAvailable = $Context.PSObject.Properties.Name -contains 'Providers' -and - $null -ne $Context.Providers -and - $Context.Providers.ContainsKey('AuthSessionBroker') + $provider = $Context.Providers[$ProviderAlias] + + # Check if provider method exists + $providerMethod = $provider.PSObject.Methods[$MethodName] + if ($null -eq $providerMethod) { + throw "Provider '$ProviderAlias' does not implement $MethodName method." + } + + # Check if method supports AuthSession parameter + $supportsAuthSession = Test-IdleProviderMethodParameter -ProviderMethod $providerMethod -ParameterName 'AuthSession' + + # Check if context can acquire auth sessions + $canAcquireAuth = $Context.PSObject.Methods.Name -contains 'AcquireAuthSession' + + # Only acquire auth session if provider method can use it or if explicitly requested + $shouldAcquireAuth = $With.ContainsKey('AuthSessionName') -or ($supportsAuthSession -and $canAcquireAuth) - if ($brokerAvailable) { + if ($shouldAcquireAuth) { + if (-not $canAcquireAuth) { + # AuthSessionName was explicitly set but context cannot acquire sessions + throw "With.AuthSessionName is set to '$($With.AuthSessionName)' but Context does not have an AcquireAuthSession method." + } + # If AuthSessionName is provided, use it for routing if ($With.ContainsKey('AuthSessionName')) { $sessionName = [string]$With.AuthSessionName @@ -95,30 +112,22 @@ function Invoke-IdleProviderMethod { $authSession = $Context.AcquireAuthSession($sessionName, $sessionOptions) } else { - # No AuthSessionName provided - try to acquire default session - # Use a placeholder name and no options to trigger default session logic + # No AuthSessionName provided but provider supports auth - try to acquire default session + # Use empty string to signal default request without conflicting with user session names try { - $authSession = $Context.AcquireAuthSession('__default__', $null) + $authSession = $Context.AcquireAuthSession('', $null) } catch { - # If acquiring default fails, continue without auth session - # This preserves backward compatibility for providers that don't require auth + # If provider method supports AuthSession and default acquisition fails, rethrow + if ($supportsAuthSession) { + throw + } + # Otherwise, continue without auth session for backward compatibility $authSession = $null } } } - $provider = $Context.Providers[$ProviderAlias] - - # Check if provider method exists - $providerMethod = $provider.PSObject.Methods[$MethodName] - if ($null -eq $providerMethod) { - throw "Provider '$ProviderAlias' does not implement $MethodName method." - } - - # Check if method supports AuthSession parameter - $supportsAuthSession = Test-IdleProviderMethodParameter -ProviderMethod $providerMethod -ParameterName 'AuthSession' - # Call provider method with appropriate signature if ($supportsAuthSession -and $null -ne $authSession) { # Provider supports AuthSession and we have one - pass it diff --git a/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 b/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 index 091e81e1..09410fdf 100644 --- a/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 +++ b/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 @@ -57,7 +57,7 @@ function New-IdleAuthSessionBroker { # AuthSessionName-based routing with roles $broker = New-IdleAuthSessionBroker -SessionMap @{ @{ AuthSessionName = 'AD'; Role = 'ADAdm' } = $tier0Credential - @{ AuthSessionName = 'EXO'; Role = 'EXOAdm' } = $exoToken + @{ AuthSessionName = 'AD'; Role = 'ADRead' } = $readOnlyCredential } -DefaultAuthSession $adminCredential -AuthSessionType 'Credential' .EXAMPLE @@ -120,7 +120,7 @@ function New-IdleAuthSessionBroker { $broker | Add-Member -MemberType ScriptMethod -Name AcquireAuthSession -Value { param( [Parameter(Mandatory)] - [ValidateNotNullOrEmpty()] + [AllowEmptyString()] [string] $Name, [Parameter()] @@ -135,6 +135,14 @@ function New-IdleAuthSessionBroker { # - PSRemoting: Validate remote session state, connectivity # - Credential: Validate credential format, domain membership + # Empty string signals default session request + if ([string]::IsNullOrEmpty($Name)) { + if ($null -ne $this.DefaultAuthSession) { + return $this.DefaultAuthSession + } + throw "No default auth session configured." + } + # If SessionMap is null or empty, return default if ($null -eq $this.SessionMap -or $this.SessionMap.Count -eq 0) { if ($null -ne $this.DefaultAuthSession) { @@ -162,9 +170,12 @@ function New-IdleAuthSessionBroker { continue } - # If pattern has ONLY AuthSessionName (no other keys), it's a match + # If pattern has ONLY AuthSessionName (no other keys) if ($pattern.Keys.Count -eq 1) { - $authSessionNameMatches += $entry + # Only match if Options is null or empty + if ($null -eq $Options -or $Options.Count -eq 0) { + $authSessionNameMatches += $entry + } continue } diff --git a/tests/Steps/Invoke-IdleStepAuthSession.Tests.ps1 b/tests/Steps/Invoke-IdleStepAuthSession.Tests.ps1 index 85a1db23..69d488be 100644 --- a/tests/Steps/Invoke-IdleStepAuthSession.Tests.ps1 +++ b/tests/Steps/Invoke-IdleStepAuthSession.Tests.ps1 @@ -357,8 +357,46 @@ Describe 'IdLE.Steps - Auth Session Routing' { $result | Should -Not -BeNullOrEmpty $result.Status | Should -Be 'Completed' $testState.SessionAcquired | Should -Be $true - $testState.AcquiredName | Should -Be '__default__' + $testState.AcquiredName | Should -Be '' $testState.AcquiredOptions | Should -BeNullOrEmpty } + + It 'throws when AuthSessionName is set but no broker is available' { + # Arrange + $mockProvider = [pscustomobject]@{ + PSTypeName = 'Tests.MockProvider' + } + $mockProvider | Add-Member -MemberType ScriptMethod -Name EnsureAttribute -Value { + param($IdentityKey, $Name, $Value, $AuthSession) + return [pscustomobject]@{ + PSTypeName = 'IdLE.ProviderResult' + Changed = $true + } + } -Force + + $context = [pscustomobject]@{ + PSTypeName = 'IdLE.ExecutionContext' + Providers = @{ + Identity = $mockProvider + # No AuthSessionBroker + } + } + + $step = [pscustomobject]@{ + PSTypeName = 'IdLE.Step' + Name = 'TestStep' + Type = 'IdLE.Step.EnsureAttribute' + With = @{ + IdentityKey = 'testuser' + Name = 'Department' + Value = 'IT' + AuthSessionName = 'ActiveDirectory' # Explicitly set but no broker + } + } + + # Act & Assert + { Invoke-IdleStepEnsureAttribute -Context $context -Step $step } | + Should -Throw '*AuthSessionName*AcquireAuthSession*' + } } } From e8253050cc3b0424d9c1cd8e571a9564c2dfe616 Mon Sep 17 00:00:00 2001 From: Matthias Fleschuetz <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> Date: Sun, 8 Feb 2026 16:35:09 +0100 Subject: [PATCH 5/5] update cmdlet reference --- docs/reference/cmdlets/New-IdleAuthSession.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/cmdlets/New-IdleAuthSession.md b/docs/reference/cmdlets/New-IdleAuthSession.md index dbcadfa8..f331e309 100644 --- a/docs/reference/cmdlets/New-IdleAuthSession.md +++ b/docs/reference/cmdlets/New-IdleAuthSession.md @@ -13,7 +13,7 @@ Creates a simple AuthSessionBroker for use with IdLE providers. ## SYNTAX ``` -New-IdleAuthSession [-SessionMap] <Hashtable> [[-DefaultAuthSession] <Object>] [-AuthSessionType] <String> +New-IdleAuthSession [[-SessionMap] <Hashtable>] [[-DefaultAuthSession] <Object>] [-AuthSessionType] <String> [-ProgressAction <ActionPreference>] [<CommonParameters>] ``` @@ -43,7 +43,7 @@ Type: Hashtable Parameter Sets: (All) Aliases: -Required: True +Required: False Position: 1 Default value: None Accept pipeline input: False