From 901e89aa111df2193a82f0294be44b09077da8dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:22:17 +0000 Subject: [PATCH 01/14] Initial plan From 4279e9b335c2f7d591ac20cdfdbb2361f3944353 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:32:05 +0000 Subject: [PATCH 02/14] feat: Add auth session routing to EnsureAttribute step and AD provider - Add support for With.AuthSessionName and With.AuthSessionOptions in EnsureAttribute step - Implement parameter detection to check if provider supports AuthSession - Add fallback to legacy signature when provider lacks AuthSession parameter - Update AD provider to accept optional AuthSession parameter - Add GetEffectiveAdapter helper to extract credential from AuthSession - Add comprehensive tests for auth session acquisition, parameter detection, and fallback Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- .../Public/New-IdleADIdentityProvider.ps1 | 48 ++- .../Public/Invoke-IdleStepEnsureAttribute.ps1 | 58 +++- tests/Invoke-IdleStepAuthSession.Tests.ps1 | 298 ++++++++++++++++++ 3 files changed, 400 insertions(+), 4 deletions(-) create mode 100644 tests/Invoke-IdleStepAuthSession.Tests.ps1 diff --git a/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 b/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 index 50d7b13..da1e4a6 100644 --- a/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 +++ b/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 @@ -183,6 +183,34 @@ function New-IdleADIdentityProvider { AllowDelete = [bool]$AllowDelete } + # Helper method to extract credential from AuthSession and create effective adapter + $getEffectiveAdapter = { + param( + [Parameter()] + [AllowNull()] + [object] $AuthSession + ) + + if ($null -eq $AuthSession) { + return $this.Adapter + } + + $credential = $null + if ($AuthSession -is [PSCredential]) { + $credential = $AuthSession + } + elseif ($AuthSession.PSObject.Properties.Name -contains 'Credential') { + $credential = $AuthSession.Credential + } + + if ($null -ne $credential) { + return New-IdleADAdapter -Credential $credential + } + + return $this.Adapter + } + + $provider | Add-Member -MemberType ScriptMethod -Name GetEffectiveAdapter -Value $getEffectiveAdapter -Force $provider | Add-Member -MemberType ScriptMethod -Name ConvertToEntitlement -Value $convertToEntitlement -Force $provider | Add-Member -MemberType ScriptMethod -Name TestEntitlementEquals -Value $testEntitlementEquals -Force $provider | Add-Member -MemberType ScriptMethod -Name ResolveIdentity -Value $resolveIdentity -Force @@ -350,10 +378,24 @@ function New-IdleADIdentityProvider { [Parameter()] [AllowNull()] - [object] $Value + [object] $Value, + + [Parameter()] + [AllowNull()] + [object] $AuthSession ) - $user = $this.ResolveIdentity($IdentityKey) + $adapter = $this.GetEffectiveAdapter($AuthSession) + + # Temporarily use the effective adapter for identity resolution + $originalAdapter = $this.Adapter + $this.Adapter = $adapter + try { + $user = $this.ResolveIdentity($IdentityKey) + } + finally { + $this.Adapter = $originalAdapter + } $currentValue = $null if ($user.PSObject.Properties.Name -contains $Name) { @@ -362,7 +404,7 @@ function New-IdleADIdentityProvider { $changed = $false if ($currentValue -ne $Value) { - $this.Adapter.SetUser($user.DistinguishedName, $Name, $Value) + $adapter.SetUser($user.DistinguishedName, $Name, $Value) $changed = $true } diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureAttribute.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureAttribute.ps1 index 31f6d4e..0073b9e 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureAttribute.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureAttribute.ps1 @@ -11,6 +11,14 @@ function Invoke-IdleStepEnsureAttribute { The step is idempotent by design: it converges state to the desired value. + Authentication: + - If With.AuthSessionName is present, the step acquires an auth session via + Context.AcquireAuthSession(Name, Options) and passes it to the provider method + if the provider supports an AuthSession parameter. + - With.AuthSessionOptions (optional, hashtable) is passed to the broker for + session selection (e.g., @{ Role = 'Tier0' }). + - ScriptBlocks in AuthSessionOptions are rejected (security boundary). + .PARAMETER Context Execution context created by IdLE.Core. @@ -54,8 +62,56 @@ function Invoke-IdleStepEnsureAttribute { throw "Provider '$providerAlias' was not supplied by the host." } + # 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 } + + if ($null -ne $sessionOptions -and -not ($sessionOptions -is [hashtable])) { + throw "With.AuthSessionOptions must be a hashtable or null." + } + + $authSession = $Context.AcquireAuthSession($sessionName, $sessionOptions) + } + $provider = $Context.Providers[$providerAlias] - $result = $provider.EnsureAttribute([string]$with.IdentityKey, [string]$with.Name, $with.Value) + + # Call provider with AuthSession if supported (backwards compatible fallback) + $providerMethod = $provider.PSObject.Methods['EnsureAttribute'] + if ($null -eq $providerMethod) { + throw "Provider '$providerAlias' does not implement EnsureAttribute method." + } + + # Check if the method is a ScriptMethod and inspect its parameters + $supportsAuthSession = $false + if ($providerMethod.MemberType -eq 'ScriptMethod') { + $scriptBlock = $providerMethod.Script + if ($null -ne $scriptBlock -and $null -ne $scriptBlock.Ast -and $null -ne $scriptBlock.Ast.ParamBlock) { + $params = $scriptBlock.Ast.ParamBlock.Parameters + if ($null -ne $params) { + foreach ($param in $params) { + if ($null -ne $param.Name -and $null -ne $param.Name.VariablePath) { + $paramName = $param.Name.VariablePath.UserPath + if ($paramName -eq 'AuthSession') { + $supportsAuthSession = $true + break + } + } + } + } + } + } + + # Call provider method with appropriate signature + if ($supportsAuthSession -and $null -ne $authSession) { + # Provider supports AuthSession and we have one - pass it + $result = $provider.EnsureAttribute([string]$with.IdentityKey, [string]$with.Name, $with.Value, $authSession) + } + else { + # Legacy signature (no AuthSession parameter) or no session acquired + $result = $provider.EnsureAttribute([string]$with.IdentityKey, [string]$with.Name, $with.Value) + } $changed = $false if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'Changed')) { diff --git a/tests/Invoke-IdleStepAuthSession.Tests.ps1 b/tests/Invoke-IdleStepAuthSession.Tests.ps1 new file mode 100644 index 0000000..6d50721 --- /dev/null +++ b/tests/Invoke-IdleStepAuthSession.Tests.ps1 @@ -0,0 +1,298 @@ +#requires -Version 7.0 + +Describe 'IdLE.Steps - Auth Session Routing' { + + BeforeAll { + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '../src/IdLE/IdLE.psd1') -Force + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '../src/IdLE.Core/IdLE.Core.psd1') -Force + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '../src/IdLE.Steps.Common/IdLE.Steps.Common.psd1') -Force + } + + Context 'EnsureAttribute - Auth Session Acquisition' { + + It 'acquires auth session when With.AuthSessionName is present' { + # 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('testuser', (ConvertTo-SecureString 'testpass' -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' + AuthSessionName = 'ActiveDirectory' + AuthSessionOptions = @{ Role = 'Tier0' } + } + } + + # Act + $result = Invoke-IdleStepEnsureAttribute -Context $context -Step $step + + # Assert + $result | Should -Not -BeNullOrEmpty + $result.PSTypeNames | Should -Contain 'IdLE.StepResult' + $result.Status | Should -Be 'Completed' + $testState.SessionAcquired | Should -Be $true + $testState.AcquiredName | Should -Be 'ActiveDirectory' + $testState.AcquiredOptions.Role | Should -Be 'Tier0' + } + + It 'does not acquire auth session when With.AuthSessionName is absent' { + # Arrange + $sessionAcquired = $false + + $broker = [pscustomobject]@{ + PSTypeName = 'Tests.AuthSessionBroker' + } + $broker | Add-Member -MemberType ScriptMethod -Name AcquireAuthSession -Value { + param($Name, $Options) + $script:sessionAcquired = $true + throw "Should not be called" + } -Force + + $mockProvider = [pscustomobject]@{ + PSTypeName = 'Tests.MockProvider' + } + $mockProvider | Add-Member -MemberType ScriptMethod -Name EnsureAttribute -Value { + param($IdentityKey, $Name, $Value) + 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' + } + } + + # Act + $result = Invoke-IdleStepEnsureAttribute -Context $context -Step $step + + # Assert + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Completed' + $sessionAcquired | Should -Be $false + } + + It 'passes auth session to provider when provider supports AuthSession parameter' { + # Arrange + $testState = [pscustomobject]@{ + ReceivedAuthSession = $null + } + + $broker = [pscustomobject]@{ + PSTypeName = 'Tests.AuthSessionBroker' + } + $broker | Add-Member -MemberType ScriptMethod -Name AcquireAuthSession -Value { + param($Name, $Options) + return [PSCredential]::new('tier0admin', (ConvertTo-SecureString 'pass123' -AsPlainText -Force)) + } -Force + + $mockProvider = [pscustomobject]@{ + PSTypeName = 'Tests.MockProvider' + State = $testState + } + $mockProvider | Add-Member -MemberType ScriptMethod -Name EnsureAttribute -Value { + param($IdentityKey, $Name, $Value, $AuthSession) + $this.State.ReceivedAuthSession = $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' + AuthSessionName = 'ActiveDirectory' + } + } + + # Act + $result = Invoke-IdleStepEnsureAttribute -Context $context -Step $step + + # Assert + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Completed' + $testState.ReceivedAuthSession | Should -Not -BeNullOrEmpty + $testState.ReceivedAuthSession | Should -BeOfType [PSCredential] + $testState.ReceivedAuthSession.UserName | Should -Be 'tier0admin' + } + + It 'falls back to legacy signature when provider lacks AuthSession parameter' { + # Arrange + $testState = [pscustomobject]@{ + LegacyCallMade = $false + } + + $broker = [pscustomobject]@{ + PSTypeName = 'Tests.AuthSessionBroker' + } + $broker | Add-Member -MemberType ScriptMethod -Name AcquireAuthSession -Value { + param($Name, $Options) + return [PSCredential]::new('tier0admin', (ConvertTo-SecureString 'pass123' -AsPlainText -Force)) + } -Force + + # Provider without AuthSession parameter (legacy) + $mockProvider = [pscustomobject]@{ + PSTypeName = 'Tests.MockProvider' + State = $testState + } + $mockProvider | Add-Member -MemberType ScriptMethod -Name EnsureAttribute -Value { + param($IdentityKey, $Name, $Value) + $this.State.LegacyCallMade = $true + 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' + AuthSessionName = 'ActiveDirectory' + } + } + + # Act + $result = Invoke-IdleStepEnsureAttribute -Context $context -Step $step + + # Assert + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Completed' + $testState.LegacyCallMade | Should -Be $true + } + + It 'throws when With.AuthSessionOptions is not a hashtable' { + # Arrange + $mockProvider = [pscustomobject]@{ + PSTypeName = 'Tests.MockProvider' + } + $mockProvider | Add-Member -MemberType ScriptMethod -Name EnsureAttribute -Value { + param($IdentityKey, $Name, $Value) + return [pscustomobject]@{ + PSTypeName = 'IdLE.ProviderResult' + Changed = $true + } + } -Force + + $context = [pscustomobject]@{ + PSTypeName = 'IdLE.ExecutionContext' + Providers = @{ + Identity = $mockProvider + } + } + + $step = [pscustomobject]@{ + PSTypeName = 'IdLE.Step' + Name = 'TestStep' + Type = 'IdLE.Step.EnsureAttribute' + With = @{ + IdentityKey = 'testuser' + Name = 'Department' + Value = 'IT' + AuthSessionName = 'ActiveDirectory' + AuthSessionOptions = 'invalid-string' + } + } + + # Act & Assert + { Invoke-IdleStepEnsureAttribute -Context $context -Step $step } | + Should -Throw '*AuthSessionOptions*hashtable*' + } + } +} From 50b8b61c993c7ed8f99f877383c91034d7f25792 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:35:11 +0000 Subject: [PATCH 03/14] Apply auth session routing pattern to remaining 6 provider-calling steps Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- .../Public/Invoke-IdleStepCreateIdentity.ps1 | 58 ++++++++++++++++++- .../Public/Invoke-IdleStepDeleteIdentity.ps1 | 58 ++++++++++++++++++- .../Public/Invoke-IdleStepDisableIdentity.ps1 | 58 ++++++++++++++++++- .../Public/Invoke-IdleStepEnableIdentity.ps1 | 58 ++++++++++++++++++- .../Invoke-IdleStepEnsureEntitlement.ps1 | 57 +++++++++++++++++- .../Public/Invoke-IdleStepMoveIdentity.ps1 | 58 ++++++++++++++++++- 6 files changed, 340 insertions(+), 7 deletions(-) diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepCreateIdentity.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepCreateIdentity.ps1 index 8710d74..6d876c3 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepCreateIdentity.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepCreateIdentity.ps1 @@ -11,6 +11,14 @@ function Invoke-IdleStepCreateIdentity { The step is idempotent by design: if the identity already exists, the provider should return Changed = $false without creating a duplicate. + Authentication: + - If With.AuthSessionName is present, the step acquires an auth session via + Context.AcquireAuthSession(Name, Options) and passes it to the provider method + if the provider supports an AuthSession parameter. + - With.AuthSessionOptions (optional, hashtable) is passed to the broker for + session selection (e.g., @{ Role = 'Tier0' }). + - ScriptBlocks in AuthSessionOptions are rejected (security boundary). + .PARAMETER Context Execution context created by IdLE.Core. @@ -61,8 +69,56 @@ function Invoke-IdleStepCreateIdentity { throw "Provider '$providerAlias' was not supplied by the host." } + # 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 } + + if ($null -ne $sessionOptions -and -not ($sessionOptions -is [hashtable])) { + throw "With.AuthSessionOptions must be a hashtable or null." + } + + $authSession = $Context.AcquireAuthSession($sessionName, $sessionOptions) + } + $provider = $Context.Providers[$providerAlias] - $result = $provider.CreateIdentity([string]$with.IdentityKey, $with.Attributes) + + # Call provider with AuthSession if supported (backwards compatible fallback) + $providerMethod = $provider.PSObject.Methods['CreateIdentity'] + if ($null -eq $providerMethod) { + throw "Provider '$providerAlias' does not implement CreateIdentity method." + } + + # Check if the method is a ScriptMethod and inspect its parameters + $supportsAuthSession = $false + if ($providerMethod.MemberType -eq 'ScriptMethod') { + $scriptBlock = $providerMethod.Script + if ($null -ne $scriptBlock -and $null -ne $scriptBlock.Ast -and $null -ne $scriptBlock.Ast.ParamBlock) { + $params = $scriptBlock.Ast.ParamBlock.Parameters + if ($null -ne $params) { + foreach ($param in $params) { + if ($null -ne $param.Name -and $null -ne $param.Name.VariablePath) { + $paramName = $param.Name.VariablePath.UserPath + if ($paramName -eq 'AuthSession') { + $supportsAuthSession = $true + break + } + } + } + } + } + } + + # Call provider method with appropriate signature + if ($supportsAuthSession -and $null -ne $authSession) { + # Provider supports AuthSession and we have one - pass it + $result = $provider.CreateIdentity([string]$with.IdentityKey, $with.Attributes, $authSession) + } + else { + # Legacy signature (no AuthSession parameter) or no session acquired + $result = $provider.CreateIdentity([string]$with.IdentityKey, $with.Attributes) + } $changed = $false if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'Changed')) { diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepDeleteIdentity.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepDeleteIdentity.ps1 index 3fce4a7..97e41fb 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepDeleteIdentity.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepDeleteIdentity.ps1 @@ -15,6 +15,14 @@ function Invoke-IdleStepDeleteIdentity { capability, which is typically opt-in for safety. The provider must be configured to allow deletion (e.g., AllowDelete = $true for AD provider). + Authentication: + - If With.AuthSessionName is present, the step acquires an auth session via + Context.AcquireAuthSession(Name, Options) and passes it to the provider method + if the provider supports an AuthSession parameter. + - With.AuthSessionOptions (optional, hashtable) is passed to the broker for + session selection (e.g., @{ Role = 'Tier0' }). + - ScriptBlocks in AuthSessionOptions are rejected (security boundary). + .PARAMETER Context Execution context created by IdLE.Core. @@ -58,8 +66,56 @@ function Invoke-IdleStepDeleteIdentity { throw "Provider '$providerAlias' was not supplied by the host." } + # 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 } + + if ($null -ne $sessionOptions -and -not ($sessionOptions -is [hashtable])) { + throw "With.AuthSessionOptions must be a hashtable or null." + } + + $authSession = $Context.AcquireAuthSession($sessionName, $sessionOptions) + } + $provider = $Context.Providers[$providerAlias] - $result = $provider.DeleteIdentity([string]$with.IdentityKey) + + # Call provider with AuthSession if supported (backwards compatible fallback) + $providerMethod = $provider.PSObject.Methods['DeleteIdentity'] + if ($null -eq $providerMethod) { + throw "Provider '$providerAlias' does not implement DeleteIdentity method." + } + + # Check if the method is a ScriptMethod and inspect its parameters + $supportsAuthSession = $false + if ($providerMethod.MemberType -eq 'ScriptMethod') { + $scriptBlock = $providerMethod.Script + if ($null -ne $scriptBlock -and $null -ne $scriptBlock.Ast -and $null -ne $scriptBlock.Ast.ParamBlock) { + $params = $scriptBlock.Ast.ParamBlock.Parameters + if ($null -ne $params) { + foreach ($param in $params) { + if ($null -ne $param.Name -and $null -ne $param.Name.VariablePath) { + $paramName = $param.Name.VariablePath.UserPath + if ($paramName -eq 'AuthSession') { + $supportsAuthSession = $true + break + } + } + } + } + } + } + + # Call provider method with appropriate signature + if ($supportsAuthSession -and $null -ne $authSession) { + # Provider supports AuthSession and we have one - pass it + $result = $provider.DeleteIdentity([string]$with.IdentityKey, $authSession) + } + else { + # Legacy signature (no AuthSession parameter) or no session acquired + $result = $provider.DeleteIdentity([string]$with.IdentityKey) + } $changed = $false if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'Changed')) { diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepDisableIdentity.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepDisableIdentity.ps1 index 5fcdaa9..73b679e 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepDisableIdentity.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepDisableIdentity.ps1 @@ -11,6 +11,14 @@ function Invoke-IdleStepDisableIdentity { The step is idempotent by design: if the identity is already disabled, the provider should return Changed = $false. + Authentication: + - If With.AuthSessionName is present, the step acquires an auth session via + Context.AcquireAuthSession(Name, Options) and passes it to the provider method + if the provider supports an AuthSession parameter. + - With.AuthSessionOptions (optional, hashtable) is passed to the broker for + session selection (e.g., @{ Role = 'Tier0' }). + - ScriptBlocks in AuthSessionOptions are rejected (security boundary). + .PARAMETER Context Execution context created by IdLE.Core. @@ -54,8 +62,56 @@ function Invoke-IdleStepDisableIdentity { throw "Provider '$providerAlias' was not supplied by the host." } + # 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 } + + if ($null -ne $sessionOptions -and -not ($sessionOptions -is [hashtable])) { + throw "With.AuthSessionOptions must be a hashtable or null." + } + + $authSession = $Context.AcquireAuthSession($sessionName, $sessionOptions) + } + $provider = $Context.Providers[$providerAlias] - $result = $provider.DisableIdentity([string]$with.IdentityKey) + + # Call provider with AuthSession if supported (backwards compatible fallback) + $providerMethod = $provider.PSObject.Methods['DisableIdentity'] + if ($null -eq $providerMethod) { + throw "Provider '$providerAlias' does not implement DisableIdentity method." + } + + # Check if the method is a ScriptMethod and inspect its parameters + $supportsAuthSession = $false + if ($providerMethod.MemberType -eq 'ScriptMethod') { + $scriptBlock = $providerMethod.Script + if ($null -ne $scriptBlock -and $null -ne $scriptBlock.Ast -and $null -ne $scriptBlock.Ast.ParamBlock) { + $params = $scriptBlock.Ast.ParamBlock.Parameters + if ($null -ne $params) { + foreach ($param in $params) { + if ($null -ne $param.Name -and $null -ne $param.Name.VariablePath) { + $paramName = $param.Name.VariablePath.UserPath + if ($paramName -eq 'AuthSession') { + $supportsAuthSession = $true + break + } + } + } + } + } + } + + # Call provider method with appropriate signature + if ($supportsAuthSession -and $null -ne $authSession) { + # Provider supports AuthSession and we have one - pass it + $result = $provider.DisableIdentity([string]$with.IdentityKey, $authSession) + } + else { + # Legacy signature (no AuthSession parameter) or no session acquired + $result = $provider.DisableIdentity([string]$with.IdentityKey) + } $changed = $false if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'Changed')) { diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnableIdentity.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnableIdentity.ps1 index 8a86af9..9eac2df 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnableIdentity.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnableIdentity.ps1 @@ -11,6 +11,14 @@ function Invoke-IdleStepEnableIdentity { The step is idempotent by design: if the identity is already enabled, the provider should return Changed = $false. + Authentication: + - If With.AuthSessionName is present, the step acquires an auth session via + Context.AcquireAuthSession(Name, Options) and passes it to the provider method + if the provider supports an AuthSession parameter. + - With.AuthSessionOptions (optional, hashtable) is passed to the broker for + session selection (e.g., @{ Role = 'Tier0' }). + - ScriptBlocks in AuthSessionOptions are rejected (security boundary). + .PARAMETER Context Execution context created by IdLE.Core. @@ -54,8 +62,56 @@ function Invoke-IdleStepEnableIdentity { throw "Provider '$providerAlias' was not supplied by the host." } + # 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 } + + if ($null -ne $sessionOptions -and -not ($sessionOptions -is [hashtable])) { + throw "With.AuthSessionOptions must be a hashtable or null." + } + + $authSession = $Context.AcquireAuthSession($sessionName, $sessionOptions) + } + $provider = $Context.Providers[$providerAlias] - $result = $provider.EnableIdentity([string]$with.IdentityKey) + + # Call provider with AuthSession if supported (backwards compatible fallback) + $providerMethod = $provider.PSObject.Methods['EnableIdentity'] + if ($null -eq $providerMethod) { + throw "Provider '$providerAlias' does not implement EnableIdentity method." + } + + # Check if the method is a ScriptMethod and inspect its parameters + $supportsAuthSession = $false + if ($providerMethod.MemberType -eq 'ScriptMethod') { + $scriptBlock = $providerMethod.Script + if ($null -ne $scriptBlock -and $null -ne $scriptBlock.Ast -and $null -ne $scriptBlock.Ast.ParamBlock) { + $params = $scriptBlock.Ast.ParamBlock.Parameters + if ($null -ne $params) { + foreach ($param in $params) { + if ($null -ne $param.Name -and $null -ne $param.Name.VariablePath) { + $paramName = $param.Name.VariablePath.UserPath + if ($paramName -eq 'AuthSession') { + $supportsAuthSession = $true + break + } + } + } + } + } + } + + # Call provider method with appropriate signature + if ($supportsAuthSession -and $null -ne $authSession) { + # Provider supports AuthSession and we have one - pass it + $result = $provider.EnableIdentity([string]$with.IdentityKey, $authSession) + } + else { + # Legacy signature (no AuthSession parameter) or no session acquired + $result = $provider.EnableIdentity([string]$with.IdentityKey) + } $changed = $false if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'Changed')) { diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 index 39a2a9a..5927f76 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 @@ -15,6 +15,14 @@ function Invoke-IdleStepEnsureEntitlement { The step is idempotent and only calls Grant/Revoke when the assignment needs to change. + Authentication: + - If With.AuthSessionName is present, the step acquires an auth session via + Context.AcquireAuthSession(Name, Options) and passes it to the provider methods + if the provider supports an AuthSession parameter. + - With.AuthSessionOptions (optional, hashtable) is passed to the broker for + session selection (e.g., @{ Role = 'Tier0' }). + - ScriptBlocks in AuthSessionOptions are rejected (security boundary). + .PARAMETER Context Execution context created by IdLE.Core. @@ -148,6 +156,19 @@ function Invoke-IdleStepEnsureEntitlement { throw "Provider '$providerAlias' was not supplied by the host." } + # 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 } + + if ($null -ne $sessionOptions -and -not ($sessionOptions -is [hashtable])) { + throw "With.AuthSessionOptions must be a hashtable or null." + } + + $authSession = $Context.AcquireAuthSession($sessionName, $sessionOptions) + } + $provider = $Context.Providers[$providerAlias] $requiredMethods = @('ListEntitlements') @@ -164,6 +185,28 @@ function Invoke-IdleStepEnsureEntitlement { } } + # Check if GrantEntitlement/RevokeEntitlement support AuthSession parameter + $targetMethodName = if ($state -eq 'present') { 'GrantEntitlement' } else { 'RevokeEntitlement' } + $providerMethod = $provider.PSObject.Methods[$targetMethodName] + $supportsAuthSession = $false + if ($providerMethod.MemberType -eq 'ScriptMethod') { + $scriptBlock = $providerMethod.Script + if ($null -ne $scriptBlock -and $null -ne $scriptBlock.Ast -and $null -ne $scriptBlock.Ast.ParamBlock) { + $params = $scriptBlock.Ast.ParamBlock.Parameters + if ($null -ne $params) { + foreach ($param in $params) { + if ($null -ne $param.Name -and $null -ne $param.Name.VariablePath) { + $paramName = $param.Name.VariablePath.UserPath + if ($paramName -eq 'AuthSession') { + $supportsAuthSession = $true + break + } + } + } + } + } + } + $current = @($provider.ListEntitlements($identityKey)) $matches = @($current | Where-Object { Test-IdleStepEntitlementEquals -A $_ -B $entitlement }) @@ -171,7 +214,12 @@ function Invoke-IdleStepEnsureEntitlement { if ($state -eq 'present') { if (@($matches).Count -eq 0) { - $result = $provider.GrantEntitlement($identityKey, $entitlement) + if ($supportsAuthSession -and $null -ne $authSession) { + $result = $provider.GrantEntitlement($identityKey, $entitlement, $authSession) + } + else { + $result = $provider.GrantEntitlement($identityKey, $entitlement) + } if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'Changed')) { $changed = [bool]$result.Changed } @@ -182,7 +230,12 @@ function Invoke-IdleStepEnsureEntitlement { } else { if (@($matches).Count -gt 0) { - $result = $provider.RevokeEntitlement($identityKey, $entitlement) + if ($supportsAuthSession -and $null -ne $authSession) { + $result = $provider.RevokeEntitlement($identityKey, $entitlement, $authSession) + } + else { + $result = $provider.RevokeEntitlement($identityKey, $entitlement) + } if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'Changed')) { $changed = [bool]$result.Changed } diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepMoveIdentity.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepMoveIdentity.ps1 index 7b841fe..46365b5 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepMoveIdentity.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepMoveIdentity.ps1 @@ -11,6 +11,14 @@ function Invoke-IdleStepMoveIdentity { The step is idempotent by design: if the identity is already in the target container, the provider should return Changed = $false. + Authentication: + - If With.AuthSessionName is present, the step acquires an auth session via + Context.AcquireAuthSession(Name, Options) and passes it to the provider method + if the provider supports an AuthSession parameter. + - With.AuthSessionOptions (optional, hashtable) is passed to the broker for + session selection (e.g., @{ Role = 'Tier0' }). + - ScriptBlocks in AuthSessionOptions are rejected (security boundary). + .PARAMETER Context Execution context created by IdLE.Core. @@ -57,8 +65,56 @@ function Invoke-IdleStepMoveIdentity { throw "Provider '$providerAlias' was not supplied by the host." } + # 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 } + + if ($null -ne $sessionOptions -and -not ($sessionOptions -is [hashtable])) { + throw "With.AuthSessionOptions must be a hashtable or null." + } + + $authSession = $Context.AcquireAuthSession($sessionName, $sessionOptions) + } + $provider = $Context.Providers[$providerAlias] - $result = $provider.MoveIdentity([string]$with.IdentityKey, [string]$with.TargetContainer) + + # Call provider with AuthSession if supported (backwards compatible fallback) + $providerMethod = $provider.PSObject.Methods['MoveIdentity'] + if ($null -eq $providerMethod) { + throw "Provider '$providerAlias' does not implement MoveIdentity method." + } + + # Check if the method is a ScriptMethod and inspect its parameters + $supportsAuthSession = $false + if ($providerMethod.MemberType -eq 'ScriptMethod') { + $scriptBlock = $providerMethod.Script + if ($null -ne $scriptBlock -and $null -ne $scriptBlock.Ast -and $null -ne $scriptBlock.Ast.ParamBlock) { + $params = $scriptBlock.Ast.ParamBlock.Parameters + if ($null -ne $params) { + foreach ($param in $params) { + if ($null -ne $param.Name -and $null -ne $param.Name.VariablePath) { + $paramName = $param.Name.VariablePath.UserPath + if ($paramName -eq 'AuthSession') { + $supportsAuthSession = $true + break + } + } + } + } + } + } + + # Call provider method with appropriate signature + if ($supportsAuthSession -and $null -ne $authSession) { + # Provider supports AuthSession and we have one - pass it + $result = $provider.MoveIdentity([string]$with.IdentityKey, [string]$with.TargetContainer, $authSession) + } + else { + # Legacy signature (no AuthSession parameter) or no session acquired + $result = $provider.MoveIdentity([string]$with.IdentityKey, [string]$with.TargetContainer) + } $changed = $false if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'Changed')) { From 511394afa0c876bf44b7ea05ec93b591b886494e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:39:08 +0000 Subject: [PATCH 04/14] Add AuthSession parameter to all AD provider methods Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- .../Public/New-IdleADIdentityProvider.ps1 | 185 +++++++++++++++--- 1 file changed, 157 insertions(+), 28 deletions(-) diff --git a/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 b/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 index da1e4a6..63df505 100644 --- a/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 +++ b/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 @@ -241,10 +241,24 @@ function New-IdleADIdentityProvider { param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] - [string] $IdentityKey + [string] $IdentityKey, + + [Parameter()] + [AllowNull()] + [object] $AuthSession ) - $user = $this.ResolveIdentity($IdentityKey) + $adapter = $this.GetEffectiveAdapter($AuthSession) + + # Temporarily use the effective adapter for identity resolution + $originalAdapter = $this.Adapter + $this.Adapter = $adapter + try { + $user = $this.ResolveIdentity($IdentityKey) + } + finally { + $this.Adapter = $originalAdapter + } $attributes = @{} if ($null -ne $user.GivenName) { $attributes['GivenName'] = $user.GivenName } @@ -269,10 +283,16 @@ function New-IdleADIdentityProvider { $provider | Add-Member -MemberType ScriptMethod -Name ListIdentities -Value { param( [Parameter()] - [hashtable] $Filter + [hashtable] $Filter, + + [Parameter()] + [AllowNull()] + [object] $AuthSession ) - $users = $this.Adapter.ListUsers($Filter) + $adapter = $this.GetEffectiveAdapter($AuthSession) + + $users = $adapter.ListUsers($Filter) $identityKeys = @() foreach ($user in $users) { $identityKeys += $user.ObjectGuid.ToString() @@ -288,9 +308,18 @@ function New-IdleADIdentityProvider { [Parameter(Mandatory)] [ValidateNotNull()] - [hashtable] $Attributes + [hashtable] $Attributes, + + [Parameter()] + [AllowNull()] + [object] $AuthSession ) + $adapter = $this.GetEffectiveAdapter($AuthSession) + + # Temporarily use the effective adapter for identity resolution + $originalAdapter = $this.Adapter + $this.Adapter = $adapter try { $existing = $this.ResolveIdentity($IdentityKey) if ($null -ne $existing) { @@ -306,13 +335,16 @@ function New-IdleADIdentityProvider { # Identity does not exist, proceed with creation (expected for idempotent create) Write-Verbose "Identity '$IdentityKey' does not exist, proceeding with creation" } + finally { + $this.Adapter = $originalAdapter + } $enabled = $true if ($Attributes.ContainsKey('Enabled')) { $enabled = [bool]$Attributes['Enabled'] } - $null = $this.Adapter.NewUser($IdentityKey, $Attributes, $enabled) + $null = $adapter.NewUser($IdentityKey, $Attributes, $enabled) return [pscustomobject]@{ PSTypeName = 'IdLE.ProviderResult' @@ -326,16 +358,25 @@ function New-IdleADIdentityProvider { param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] - [string] $IdentityKey + [string] $IdentityKey, + + [Parameter()] + [AllowNull()] + [object] $AuthSession ) if (-not $this.AllowDelete) { throw "Delete capability is not enabled. Set AllowDelete = `$true when creating the provider." } + $adapter = $this.GetEffectiveAdapter($AuthSession) + + # Temporarily use the effective adapter for identity resolution + $originalAdapter = $this.Adapter + $this.Adapter = $adapter try { $user = $this.ResolveIdentity($IdentityKey) - $this.Adapter.DeleteUser($user.DistinguishedName) + $adapter.DeleteUser($user.DistinguishedName) return [pscustomobject]@{ PSTypeName = 'IdLE.ProviderResult' Operation = 'DeleteIdentity' @@ -344,6 +385,7 @@ function New-IdleADIdentityProvider { } } catch { + $this.Adapter = $originalAdapter # Check if identity doesn't exist (idempotent delete) # Use exception type if available, otherwise fall back to message check $isNotFound = $false @@ -426,16 +468,30 @@ function New-IdleADIdentityProvider { [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] - [string] $TargetContainer + [string] $TargetContainer, + + [Parameter()] + [AllowNull()] + [object] $AuthSession ) - $user = $this.ResolveIdentity($IdentityKey) + $adapter = $this.GetEffectiveAdapter($AuthSession) + + # Temporarily use the effective adapter for identity resolution + $originalAdapter = $this.Adapter + $this.Adapter = $adapter + try { + $user = $this.ResolveIdentity($IdentityKey) + } + finally { + $this.Adapter = $originalAdapter + } $currentOu = $user.DistinguishedName -replace '^CN=[^,]+,', '' $changed = $false if ($currentOu -ne $TargetContainer) { - $this.Adapter.MoveObject($user.DistinguishedName, $TargetContainer) + $adapter.MoveObject($user.DistinguishedName, $TargetContainer) $changed = $true } @@ -452,14 +508,28 @@ function New-IdleADIdentityProvider { param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] - [string] $IdentityKey + [string] $IdentityKey, + + [Parameter()] + [AllowNull()] + [object] $AuthSession ) - $user = $this.ResolveIdentity($IdentityKey) + $adapter = $this.GetEffectiveAdapter($AuthSession) + + # Temporarily use the effective adapter for identity resolution + $originalAdapter = $this.Adapter + $this.Adapter = $adapter + try { + $user = $this.ResolveIdentity($IdentityKey) + } + finally { + $this.Adapter = $originalAdapter + } $changed = $false if ($user.Enabled -ne $false) { - $this.Adapter.DisableUser($user.DistinguishedName) + $adapter.DisableUser($user.DistinguishedName) $changed = $true } @@ -475,14 +545,28 @@ function New-IdleADIdentityProvider { param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] - [string] $IdentityKey + [string] $IdentityKey, + + [Parameter()] + [AllowNull()] + [object] $AuthSession ) - $user = $this.ResolveIdentity($IdentityKey) + $adapter = $this.GetEffectiveAdapter($AuthSession) + + # Temporarily use the effective adapter for identity resolution + $originalAdapter = $this.Adapter + $this.Adapter = $adapter + try { + $user = $this.ResolveIdentity($IdentityKey) + } + finally { + $this.Adapter = $originalAdapter + } $changed = $false if ($user.Enabled -ne $true) { - $this.Adapter.EnableUser($user.DistinguishedName) + $adapter.EnableUser($user.DistinguishedName) $changed = $true } @@ -498,11 +582,26 @@ function New-IdleADIdentityProvider { param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] - [string] $IdentityKey + [string] $IdentityKey, + + [Parameter()] + [AllowNull()] + [object] $AuthSession ) - $user = $this.ResolveIdentity($IdentityKey) - $groups = $this.Adapter.GetUserGroups($user.DistinguishedName) + $adapter = $this.GetEffectiveAdapter($AuthSession) + + # Temporarily use the effective adapter for identity resolution + $originalAdapter = $this.Adapter + $this.Adapter = $adapter + try { + $user = $this.ResolveIdentity($IdentityKey) + } + finally { + $this.Adapter = $originalAdapter + } + + $groups = $adapter.GetUserGroups($user.DistinguishedName) $result = @() foreach ($group in $groups) { @@ -525,19 +624,34 @@ function New-IdleADIdentityProvider { [Parameter(Mandatory)] [ValidateNotNull()] - [object] $Entitlement + [object] $Entitlement, + + [Parameter()] + [AllowNull()] + [object] $AuthSession ) + $adapter = $this.GetEffectiveAdapter($AuthSession) + $normalized = $this.ConvertToEntitlement($Entitlement) - $user = $this.ResolveIdentity($IdentityKey) - $groupDn = $this.NormalizeGroupId($normalized.Id) + + # Temporarily use the effective adapter for identity resolution + $originalAdapter = $this.Adapter + $this.Adapter = $adapter + try { + $user = $this.ResolveIdentity($IdentityKey) + $groupDn = $this.NormalizeGroupId($normalized.Id) + } + finally { + $this.Adapter = $originalAdapter + } $currentGroups = $this.ListEntitlements($IdentityKey) $existing = $currentGroups | Where-Object { $this.TestEntitlementEquals($_, $normalized) } $changed = $false if (@($existing).Count -eq 0) { - $this.Adapter.AddGroupMember($groupDn, $user.DistinguishedName) + $adapter.AddGroupMember($groupDn, $user.DistinguishedName) $changed = $true } @@ -558,19 +672,34 @@ function New-IdleADIdentityProvider { [Parameter(Mandatory)] [ValidateNotNull()] - [object] $Entitlement + [object] $Entitlement, + + [Parameter()] + [AllowNull()] + [object] $AuthSession ) + $adapter = $this.GetEffectiveAdapter($AuthSession) + $normalized = $this.ConvertToEntitlement($Entitlement) - $user = $this.ResolveIdentity($IdentityKey) - $groupDn = $this.NormalizeGroupId($normalized.Id) + + # Temporarily use the effective adapter for identity resolution + $originalAdapter = $this.Adapter + $this.Adapter = $adapter + try { + $user = $this.ResolveIdentity($IdentityKey) + $groupDn = $this.NormalizeGroupId($normalized.Id) + } + finally { + $this.Adapter = $originalAdapter + } $currentGroups = $this.ListEntitlements($IdentityKey) $existing = $currentGroups | Where-Object { $this.TestEntitlementEquals($_, $normalized) } $changed = $false if (@($existing).Count -gt 0) { - $this.Adapter.RemoveGroupMember($groupDn, $user.DistinguishedName) + $adapter.RemoveGroupMember($groupDn, $user.DistinguishedName) $changed = $true } From f607a4261e6086f9c5808700655e21f63d18aa79 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:41:03 +0000 Subject: [PATCH 05/14] Fix adapter restoration and AuthSession propagation in AD provider Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- .../Public/New-IdleADIdentityProvider.ps1 | 8 +++++--- .../Public/Invoke-IdleStepEnsureEntitlement.ps1 | 7 ++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 b/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 index 63df505..5282881 100644 --- a/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 +++ b/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 @@ -385,7 +385,6 @@ function New-IdleADIdentityProvider { } } catch { - $this.Adapter = $originalAdapter # Check if identity doesn't exist (idempotent delete) # Use exception type if available, otherwise fall back to message check $isNotFound = $false @@ -406,6 +405,9 @@ function New-IdleADIdentityProvider { } throw } + finally { + $this.Adapter = $originalAdapter + } } -Force $provider | Add-Member -MemberType ScriptMethod -Name EnsureAttribute -Value { @@ -646,7 +648,7 @@ function New-IdleADIdentityProvider { $this.Adapter = $originalAdapter } - $currentGroups = $this.ListEntitlements($IdentityKey) + $currentGroups = $this.ListEntitlements($IdentityKey, $AuthSession) $existing = $currentGroups | Where-Object { $this.TestEntitlementEquals($_, $normalized) } $changed = $false @@ -694,7 +696,7 @@ function New-IdleADIdentityProvider { $this.Adapter = $originalAdapter } - $currentGroups = $this.ListEntitlements($IdentityKey) + $currentGroups = $this.ListEntitlements($IdentityKey, $AuthSession) $existing = $currentGroups | Where-Object { $this.TestEntitlementEquals($_, $normalized) } $changed = $false diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 index 5927f76..b9ff651 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 @@ -207,7 +207,12 @@ function Invoke-IdleStepEnsureEntitlement { } } - $current = @($provider.ListEntitlements($identityKey)) + if ($supportsAuthSession -and $null -ne $authSession) { + $current = @($provider.ListEntitlements($identityKey, $authSession)) + } + else { + $current = @($provider.ListEntitlements($identityKey)) + } $matches = @($current | Where-Object { Test-IdleStepEntitlementEquals -A $_ -B $entitlement }) $changed = $false From 075a4a35ccc5c9e8bfc93587070487ec5a393d2f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:43:00 +0000 Subject: [PATCH 06/14] Improve AuthSession parameter detection for individual methods Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- .../Invoke-IdleStepEnsureEntitlement.ps1 | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 index b9ff651..9637826 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 @@ -185,29 +185,35 @@ function Invoke-IdleStepEnsureEntitlement { } } - # Check if GrantEntitlement/RevokeEntitlement support AuthSession parameter - $targetMethodName = if ($state -eq 'present') { 'GrantEntitlement' } else { 'RevokeEntitlement' } - $providerMethod = $provider.PSObject.Methods[$targetMethodName] - $supportsAuthSession = $false - if ($providerMethod.MemberType -eq 'ScriptMethod') { - $scriptBlock = $providerMethod.Script - if ($null -ne $scriptBlock -and $null -ne $scriptBlock.Ast -and $null -ne $scriptBlock.Ast.ParamBlock) { - $params = $scriptBlock.Ast.ParamBlock.Parameters - if ($null -ne $params) { - foreach ($param in $params) { - if ($null -ne $param.Name -and $null -ne $param.Name.VariablePath) { - $paramName = $param.Name.VariablePath.UserPath - if ($paramName -eq 'AuthSession') { - $supportsAuthSession = $true - break + # Helper function to check if a provider method supports AuthSession parameter + $testMethodSupportsAuthSession = { + param([string]$MethodName) + $method = $provider.PSObject.Methods[$MethodName] + if ($method.MemberType -eq 'ScriptMethod') { + $scriptBlock = $method.Script + if ($null -ne $scriptBlock -and $null -ne $scriptBlock.Ast -and $null -ne $scriptBlock.Ast.ParamBlock) { + $params = $scriptBlock.Ast.ParamBlock.Parameters + if ($null -ne $params) { + foreach ($param in $params) { + if ($null -ne $param.Name -and $null -ne $param.Name.VariablePath) { + $paramName = $param.Name.VariablePath.UserPath + if ($paramName -eq 'AuthSession') { + return $true + } } } } } } + return $false } - if ($supportsAuthSession -and $null -ne $authSession) { + # Check AuthSession support for each method + $listSupportsAuthSession = & $testMethodSupportsAuthSession -MethodName 'ListEntitlements' + $grantSupportsAuthSession = & $testMethodSupportsAuthSession -MethodName 'GrantEntitlement' + $revokeSupportsAuthSession = & $testMethodSupportsAuthSession -MethodName 'RevokeEntitlement' + + if ($listSupportsAuthSession -and $null -ne $authSession) { $current = @($provider.ListEntitlements($identityKey, $authSession)) } else { @@ -219,7 +225,7 @@ function Invoke-IdleStepEnsureEntitlement { if ($state -eq 'present') { if (@($matches).Count -eq 0) { - if ($supportsAuthSession -and $null -ne $authSession) { + if ($grantSupportsAuthSession -and $null -ne $authSession) { $result = $provider.GrantEntitlement($identityKey, $entitlement, $authSession) } else { @@ -235,7 +241,7 @@ function Invoke-IdleStepEnsureEntitlement { } else { if (@($matches).Count -gt 0) { - if ($supportsAuthSession -and $null -ne $authSession) { + if ($revokeSupportsAuthSession -and $null -ne $authSession) { $result = $provider.RevokeEntitlement($identityKey, $entitlement, $authSession) } else { From 056715345c64a827531812a2c1d74f8c7836b62c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:47:09 +0000 Subject: [PATCH 07/14] feat: Add deprecation warning and update documentation - Add deprecation warning when -Credential parameter is used - Update provider help with recommended AuthSessionBroker approach - Remove Get-Credential from examples in provider-ad.md - Add comprehensive broker-based examples (Tier0/Admin scenario) - Update multi-provider scenario to use broker instead of direct credentials - Document auth session options in workflow step examples Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/reference/provider-ad.md | 114 +++++++++++++++++- .../Public/New-IdleADIdentityProvider.ps1 | 60 ++++++++- 2 files changed, 166 insertions(+), 8 deletions(-) diff --git a/docs/reference/provider-ad.md b/docs/reference/provider-ad.md index 1190ba8..37921f6 100644 --- a/docs/reference/provider-ad.md +++ b/docs/reference/provider-ad.md @@ -97,13 +97,76 @@ $plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Provider } ``` -### With Explicit Credentials +### With Explicit Credentials (Deprecated) + +> **⚠️ DEPRECATED:** The `-Credential` parameter approach is deprecated and will be removed in a future version. Use AuthSessionBroker-based authentication (see below) for new implementations. ```powershell +# Legacy approach (deprecated) $credential = Get-Credential $provider = New-IdleADIdentityProvider -Credential $credential ``` +### Recommended: AuthSessionBroker-based Authentication + +The recommended approach uses an AuthSessionBroker to manage authentication centrally: + +```powershell +# Create provider without embedded credentials +$provider = New-IdleADIdentityProvider + +# Define broker that returns credentials based on auth session options +$broker = [pscustomobject]@{} +$broker | Add-Member -MemberType ScriptMethod -Name AcquireAuthSession -Value { + param($Name, $Options) + # Return appropriate credential based on role + if ($null -ne $Options -and $Options.Role -eq 'Tier0') { + return [PSCredential]::new('DOMAIN\Tier0Admin', $tier0SecurePassword) + } + return [PSCredential]::new('DOMAIN\Admin', $adminSecurePassword) +} + +# Use provider with broker +$plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Providers @{ + Identity = $provider + AuthSessionBroker = $broker +} +``` + +In workflow definitions, steps can specify which auth context to use: + +```powershell +@{ + Type = 'IdLE.Step.EnsureAttribute' + Name = 'SetPrivilegedAttribute' + With = @{ + IdentityKey = 'user@domain.com' + Name = 'AdminCount' + Value = 1 + AuthSessionName = 'ActiveDirectory' + AuthSessionOptions = @{ Role = 'Tier0' } # Uses Tier0 admin + } +} + +@{ + Type = 'IdLE.Step.EnsureAttribute' + Name = 'SetDepartment' + With = @{ + IdentityKey = 'user@domain.com' + Name = 'Department' + Value = 'IT' + AuthSessionName = 'ActiveDirectory' + AuthSessionOptions = @{ Role = 'Admin' } # Uses regular admin + } +} +``` + +This approach enables: +- Multiple auth contexts in a single workflow +- Centralized credential management +- No secrets in workflow definitions +- Support for MFA/interactive auth in the broker + ### With Delete Capability (Opt-in) By default, the Delete capability is **not** advertised for safety. Enable it explicitly: @@ -114,13 +177,58 @@ $provider = New-IdleADIdentityProvider -AllowDelete ### Multi-Provider Scenarios +For scenarios with multiple AD forests or domains, use provider aliases with the AuthSessionBroker: + ```powershell -$sourceAD = New-IdleADIdentityProvider -Credential $sourceCred -$targetAD = New-IdleADIdentityProvider -Credential $targetCred -AllowDelete +# Create providers for different AD environments +$sourceAD = New-IdleADIdentityProvider +$targetAD = New-IdleADIdentityProvider -AllowDelete + +# Define broker that routes credentials based on provider +$broker = [pscustomobject]@{} +$broker | Add-Member -MemberType ScriptMethod -Name AcquireAuthSession -Value { + param($Name, $Options) + $domain = $Options.Domain + if ($domain -eq 'Source') { + return [PSCredential]::new('SOURCE\Admin', $sourceSecurePassword) + } + elseif ($domain -eq 'Target') { + return [PSCredential]::new('TARGET\Admin', $targetSecurePassword) + } + throw "Unknown domain: $domain" +} $plan = New-IdlePlan -WorkflowPath './migration.psd1' -Request $request -Providers @{ SourceAD = $sourceAD TargetAD = $targetAD + AuthSessionBroker = $broker +} +``` + +Workflow steps specify which domain to authenticate against: + +```powershell +@{ + Type = 'IdLE.Step.GetIdentity' + Name = 'ReadSource' + With = @{ + IdentityKey = 'user@source.com' + Provider = 'SourceAD' + AuthSessionName = 'ActiveDirectory' + AuthSessionOptions = @{ Domain = 'Source' } + } +} + +@{ + Type = 'IdLE.Step.CreateIdentity' + Name = 'CreateTarget' + With = @{ + IdentityKey = 'user@target.com' + Attributes = @{ ... } + Provider = 'TargetAD' + AuthSessionName = 'ActiveDirectory' + AuthSessionOptions = @{ Domain = 'Target' } + } } ``` diff --git a/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 b/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 index 5282881..6ab1198 100644 --- a/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 +++ b/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 @@ -15,8 +15,23 @@ function New-IdleADIdentityProvider { - UPN (UserPrincipalName) - contains @ - sAMAccountName - default fallback + Authentication: + Provider methods accept an optional AuthSession parameter for runtime credential + selection via the AuthSessionBroker. This enables multi-role scenarios (e.g., + Tier0 vs. Admin) without embedding credentials in the provider or workflow. + + The -Credential parameter is deprecated and will be removed in a future version. + Use AuthSessionBroker-based authentication instead. + .PARAMETER Credential - Optional PSCredential for AD operations. If not provided, uses integrated auth (run-as). + [DEPRECATED] Optional PSCredential for AD operations. If not provided, uses integrated auth (run-as). + + This parameter is deprecated. Use AuthSessionBroker-based authentication instead by: + 1. Configuring an AuthSessionBroker in Providers.AuthSessionBroker + 2. Using With.AuthSessionName and With.AuthSessionOptions in step definitions + + The broker approach enables multiple auth contexts per workflow without embedding + credentials in provider construction. .PARAMETER AllowDelete Opt-in flag to enable the IdLE.Identity.Delete capability. @@ -28,13 +43,34 @@ function New-IdleADIdentityProvider { a fake AD adapter without requiring a real Active Directory environment. .EXAMPLE + # Recommended: Use integrated authentication (run-as) $provider = New-IdleADIdentityProvider - $provider.GetIdentity('user@domain.com') + $plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Providers @{ + Identity = $provider + AuthSessionBroker = $broker + } .EXAMPLE - $cred = Get-Credential - $provider = New-IdleADIdentityProvider -Credential $cred -AllowDelete $true - $provider.DeleteIdentity('user@domain.com') + # Recommended: Multi-role scenario with AuthSessionBroker + # Broker returns different credentials based on With.AuthSessionOptions + $broker = [pscustomobject]@{} + $broker | Add-Member -MemberType ScriptMethod -Name AcquireAuthSession -Value { + param($Name, $Options) + if ($Options.Role -eq 'Tier0') { + return [PSCredential]::new('DOMAIN\Tier0Admin', $tier0SecurePassword) + } + return [PSCredential]::new('DOMAIN\Admin', $adminSecurePassword) + } + + $provider = New-IdleADIdentityProvider + $plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Providers @{ + Identity = $provider + AuthSessionBroker = $broker + } + + # Workflow steps can specify different auth contexts: + # With.AuthSessionName = 'ActiveDirectory' + # With.AuthSessionOptions = @{ Role = 'Tier0' } #> [CmdletBinding()] param( @@ -50,6 +86,20 @@ function New-IdleADIdentityProvider { [object] $Adapter ) + # Deprecation warning for -Credential parameter + if ($PSBoundParameters.ContainsKey('Credential') -and $null -ne $Credential) { + Write-Warning @" +The -Credential parameter is deprecated and will be removed in a future version. +Use AuthSessionBroker-based authentication instead: + +1. Configure an AuthSessionBroker in Providers.AuthSessionBroker +2. Use With.AuthSessionName and With.AuthSessionOptions in step definitions + +This approach enables multiple auth contexts per workflow without embedding credentials +in provider construction. See 'Get-Help New-IdleADIdentityProvider -Examples' for details. +"@ + } + if ($null -eq $Adapter) { $Adapter = New-IdleADAdapter -Credential $Credential } From c928054f7df643bd38415b01d790e3bb10063e51 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:27:48 +0000 Subject: [PATCH 08/14] refactor: Remove -Credential parameter and all deprecated/legacy code - Remove -Credential parameter from New-IdleADIdentityProvider - Remove all deprecation warnings - Remove deprecated documentation sections - Update examples to show only AuthSessionBroker approach - Provider now uses integrated auth by default - All tests still passing (157 tests) Per feedback: pre-1.0, we implement only the new way without legacy support. Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/reference/provider-ad.md | 14 +------ .../Public/New-IdleADIdentityProvider.ps1 | 40 +++---------------- 2 files changed, 8 insertions(+), 46 deletions(-) diff --git a/docs/reference/provider-ad.md b/docs/reference/provider-ad.md index 37921f6..823ed7f 100644 --- a/docs/reference/provider-ad.md +++ b/docs/reference/provider-ad.md @@ -97,19 +97,9 @@ $plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Provider } ``` -### With Explicit Credentials (Deprecated) +### AuthSessionBroker-based Authentication -> **⚠️ DEPRECATED:** The `-Credential` parameter approach is deprecated and will be removed in a future version. Use AuthSessionBroker-based authentication (see below) for new implementations. - -```powershell -# Legacy approach (deprecated) -$credential = Get-Credential -$provider = New-IdleADIdentityProvider -Credential $credential -``` - -### Recommended: AuthSessionBroker-based Authentication - -The recommended approach uses an AuthSessionBroker to manage authentication centrally: +Use an AuthSessionBroker to manage authentication centrally: ```powershell # Create provider without embedded credentials diff --git a/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 b/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 index 6ab1198..3cc346d 100644 --- a/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 +++ b/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 @@ -20,18 +20,9 @@ function New-IdleADIdentityProvider { selection via the AuthSessionBroker. This enables multi-role scenarios (e.g., Tier0 vs. Admin) without embedding credentials in the provider or workflow. - The -Credential parameter is deprecated and will be removed in a future version. - Use AuthSessionBroker-based authentication instead. - - .PARAMETER Credential - [DEPRECATED] Optional PSCredential for AD operations. If not provided, uses integrated auth (run-as). - - This parameter is deprecated. Use AuthSessionBroker-based authentication instead by: - 1. Configuring an AuthSessionBroker in Providers.AuthSessionBroker - 2. Using With.AuthSessionName and With.AuthSessionOptions in step definitions - - The broker approach enables multiple auth contexts per workflow without embedding - credentials in provider construction. + By default, the provider uses integrated authentication (run-as credentials). + For runtime credential selection, configure an AuthSessionBroker and use + With.AuthSessionName and With.AuthSessionOptions in step definitions. .PARAMETER AllowDelete Opt-in flag to enable the IdLE.Identity.Delete capability. @@ -43,15 +34,14 @@ function New-IdleADIdentityProvider { a fake AD adapter without requiring a real Active Directory environment. .EXAMPLE - # Recommended: Use integrated authentication (run-as) + # Use integrated authentication (run-as) $provider = New-IdleADIdentityProvider $plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Providers @{ Identity = $provider - AuthSessionBroker = $broker } .EXAMPLE - # Recommended: Multi-role scenario with AuthSessionBroker + # Multi-role scenario with AuthSessionBroker # Broker returns different credentials based on With.AuthSessionOptions $broker = [pscustomobject]@{} $broker | Add-Member -MemberType ScriptMethod -Name AcquireAuthSession -Value { @@ -74,10 +64,6 @@ function New-IdleADIdentityProvider { #> [CmdletBinding()] param( - [Parameter()] - [AllowNull()] - [PSCredential] $Credential, - [Parameter()] [switch] $AllowDelete, @@ -86,22 +72,8 @@ function New-IdleADIdentityProvider { [object] $Adapter ) - # Deprecation warning for -Credential parameter - if ($PSBoundParameters.ContainsKey('Credential') -and $null -ne $Credential) { - Write-Warning @" -The -Credential parameter is deprecated and will be removed in a future version. -Use AuthSessionBroker-based authentication instead: - -1. Configure an AuthSessionBroker in Providers.AuthSessionBroker -2. Use With.AuthSessionName and With.AuthSessionOptions in step definitions - -This approach enables multiple auth contexts per workflow without embedding credentials -in provider construction. See 'Get-Help New-IdleADIdentityProvider -Examples' for details. -"@ - } - if ($null -eq $Adapter) { - $Adapter = New-IdleADAdapter -Credential $Credential + $Adapter = New-IdleADAdapter } $convertToEntitlement = { From 980a3f94db7d022b80ebb952a7e3ad183c0b6081 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:41:41 +0000 Subject: [PATCH 09/14] refactor: Extract parameter detection to helper function - Add Test-IdleProviderMethodParameter helper in IdLE.Steps.Common/Private - Replace nested if clauses with single helper call across all 7 step files - Simplifies parameter detection logic from ~20 lines to 1 line per step - Update IdLE.Steps.Common.psm1 to load Private folder - All 157 tests passing Addresses code review feedback to reduce complexity of nested conditionals. Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/reference/provider-ad.md | 56 ++++++++++++------- src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 | 11 ++++ .../Test-IdleProviderMethodParameter.ps1 | 40 +++++++++++++ .../Public/Invoke-IdleStepCreateIdentity.ps1 | 20 +------ .../Public/Invoke-IdleStepDeleteIdentity.ps1 | 20 +------ .../Public/Invoke-IdleStepDisableIdentity.ps1 | 20 +------ .../Public/Invoke-IdleStepEnableIdentity.ps1 | 20 +------ .../Public/Invoke-IdleStepEnsureAttribute.ps1 | 20 +------ .../Invoke-IdleStepEnsureEntitlement.ps1 | 29 +--------- .../Public/Invoke-IdleStepMoveIdentity.ps1 | 20 +------ 10 files changed, 97 insertions(+), 159 deletions(-) create mode 100644 src/IdLE.Steps.Common/Private/Test-IdleProviderMethodParameter.ps1 diff --git a/docs/reference/provider-ad.md b/docs/reference/provider-ad.md index 823ed7f..642a8b1 100644 --- a/docs/reference/provider-ad.md +++ b/docs/reference/provider-ad.md @@ -99,31 +99,49 @@ $plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Provider ### AuthSessionBroker-based Authentication -Use an AuthSessionBroker to manage authentication centrally: +Use an AuthSessionBroker to manage authentication centrally and enable multi-role scenarios. + +The broker is a simple object with an `AcquireAuthSession(Name, Options)` method. The `Options` parameter is a hashtable you define - its keys and structure are completely up to you. Common patterns: + +- `@{ Role = 'Tier0' }` - select by role/privilege level +- `@{ Domain = 'SourceAD' }` - select by AD domain +- `@{ Environment = 'Production' }` - select by environment + +**Example: Simple broker with role-based selection** ```powershell -# Create provider without embedded credentials +# Create provider $provider = New-IdleADIdentityProvider -# Define broker that returns credentials based on auth session options -$broker = [pscustomobject]@{} -$broker | Add-Member -MemberType ScriptMethod -Name AcquireAuthSession -Value { - param($Name, $Options) - # Return appropriate credential based on role - if ($null -ne $Options -and $Options.Role -eq 'Tier0') { - return [PSCredential]::new('DOMAIN\Tier0Admin', $tier0SecurePassword) +# Simple broker factory function +function New-ADAuthBroker { + param( + [PSCredential]$Tier0Credential, + [PSCredential]$AdminCredential + ) + + $broker = [pscustomobject]@{} + $broker | Add-Member -MemberType ScriptProperty -Name Tier0Cred -Value { $Tier0Credential } + $broker | Add-Member -MemberType ScriptProperty -Name AdminCred -Value { $AdminCredential } + $broker | Add-Member -MemberType ScriptMethod -Name AcquireAuthSession -Value { + param($Name, $Options) + if ($Options.Role -eq 'Tier0') { + return $this.Tier0Cred + } + return $this.AdminCred } - return [PSCredential]::new('DOMAIN\Admin', $adminSecurePassword) + return $broker } -# Use provider with broker +# Use the broker +$broker = New-ADAuthBroker -Tier0Credential $tier0Cred -AdminCredential $adminCred $plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Providers @{ Identity = $provider AuthSessionBroker = $broker } ``` -In workflow definitions, steps can specify which auth context to use: +In workflow definitions, steps specify which auth context to use via `AuthSessionOptions`: ```powershell @{ @@ -134,7 +152,7 @@ In workflow definitions, steps can specify which auth context to use: Name = 'AdminCount' Value = 1 AuthSessionName = 'ActiveDirectory' - AuthSessionOptions = @{ Role = 'Tier0' } # Uses Tier0 admin + AuthSessionOptions = @{ Role = 'Tier0' } # Broker returns Tier0 credential } } @@ -146,16 +164,16 @@ In workflow definitions, steps can specify which auth context to use: Name = 'Department' Value = 'IT' AuthSessionName = 'ActiveDirectory' - AuthSessionOptions = @{ Role = 'Admin' } # Uses regular admin + AuthSessionOptions = @{ Role = 'Admin' } # Broker returns Admin credential } } ``` -This approach enables: -- Multiple auth contexts in a single workflow -- Centralized credential management -- No secrets in workflow definitions -- Support for MFA/interactive auth in the broker +**Key points:** +- The `Role` key (or any other key) is **defined by you** - it's not a built-in keyword +- Your broker implementation decides how to interpret `AuthSessionOptions` +- The broker can use any logic you want: hashtable lookups, vault APIs, interactive prompts, etc. +- `AuthSessionOptions` must be data-only (no ScriptBlocks) for security ### With Delete Capability (Opt-in) diff --git a/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 b/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 index 90a5012..365d5dd 100644 --- a/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 +++ b/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 @@ -1,6 +1,17 @@ #requires -Version 7.0 Set-StrictMode -Version Latest +$PrivatePath = Join-Path -Path $PSScriptRoot -ChildPath 'Private' +if (Test-Path -Path $PrivatePath) { + + # Materialize first to avoid enumeration issues during import. + $privateScripts = @(Get-ChildItem -Path $PrivatePath -Filter '*.ps1' -File | Sort-Object -Property FullName) + + foreach ($script in $privateScripts) { + . $script.FullName + } +} + $PublicPath = Join-Path -Path $PSScriptRoot -ChildPath 'Public' if (Test-Path -Path $PublicPath) { diff --git a/src/IdLE.Steps.Common/Private/Test-IdleProviderMethodParameter.ps1 b/src/IdLE.Steps.Common/Private/Test-IdleProviderMethodParameter.ps1 new file mode 100644 index 0000000..4d1a8ce --- /dev/null +++ b/src/IdLE.Steps.Common/Private/Test-IdleProviderMethodParameter.ps1 @@ -0,0 +1,40 @@ +# Tests whether a provider method supports a specific parameter. +# Used by steps to detect whether providers support optional AuthSession parameter. + +function Test-IdleProviderMethodParameter { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNull()] + [object] $ProviderMethod, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $ParameterName + ) + + if ($ProviderMethod.MemberType -ne 'ScriptMethod') { + return $false + } + + $scriptBlock = $ProviderMethod.Script + if ($null -eq $scriptBlock -or $null -eq $scriptBlock.Ast -or $null -eq $scriptBlock.Ast.ParamBlock) { + return $false + } + + $params = $scriptBlock.Ast.ParamBlock.Parameters + if ($null -eq $params) { + return $false + } + + foreach ($param in $params) { + if ($null -ne $param.Name -and $null -ne $param.Name.VariablePath) { + $paramName = $param.Name.VariablePath.UserPath + if ($paramName -eq $ParameterName) { + return $true + } + } + } + + return $false +} diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepCreateIdentity.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepCreateIdentity.ps1 index 6d876c3..653bc1a 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepCreateIdentity.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepCreateIdentity.ps1 @@ -90,25 +90,7 @@ function Invoke-IdleStepCreateIdentity { throw "Provider '$providerAlias' does not implement CreateIdentity method." } - # Check if the method is a ScriptMethod and inspect its parameters - $supportsAuthSession = $false - if ($providerMethod.MemberType -eq 'ScriptMethod') { - $scriptBlock = $providerMethod.Script - if ($null -ne $scriptBlock -and $null -ne $scriptBlock.Ast -and $null -ne $scriptBlock.Ast.ParamBlock) { - $params = $scriptBlock.Ast.ParamBlock.Parameters - if ($null -ne $params) { - foreach ($param in $params) { - if ($null -ne $param.Name -and $null -ne $param.Name.VariablePath) { - $paramName = $param.Name.VariablePath.UserPath - if ($paramName -eq 'AuthSession') { - $supportsAuthSession = $true - break - } - } - } - } - } - } + $supportsAuthSession = Test-IdleProviderMethodParameter -ProviderMethod $providerMethod -ParameterName 'AuthSession' # Call provider method with appropriate signature if ($supportsAuthSession -and $null -ne $authSession) { diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepDeleteIdentity.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepDeleteIdentity.ps1 index 97e41fb..0d2ca84 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepDeleteIdentity.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepDeleteIdentity.ps1 @@ -87,25 +87,7 @@ function Invoke-IdleStepDeleteIdentity { throw "Provider '$providerAlias' does not implement DeleteIdentity method." } - # Check if the method is a ScriptMethod and inspect its parameters - $supportsAuthSession = $false - if ($providerMethod.MemberType -eq 'ScriptMethod') { - $scriptBlock = $providerMethod.Script - if ($null -ne $scriptBlock -and $null -ne $scriptBlock.Ast -and $null -ne $scriptBlock.Ast.ParamBlock) { - $params = $scriptBlock.Ast.ParamBlock.Parameters - if ($null -ne $params) { - foreach ($param in $params) { - if ($null -ne $param.Name -and $null -ne $param.Name.VariablePath) { - $paramName = $param.Name.VariablePath.UserPath - if ($paramName -eq 'AuthSession') { - $supportsAuthSession = $true - break - } - } - } - } - } - } + $supportsAuthSession = Test-IdleProviderMethodParameter -ProviderMethod $providerMethod -ParameterName 'AuthSession' # Call provider method with appropriate signature if ($supportsAuthSession -and $null -ne $authSession) { diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepDisableIdentity.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepDisableIdentity.ps1 index 73b679e..e026262 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepDisableIdentity.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepDisableIdentity.ps1 @@ -83,25 +83,7 @@ function Invoke-IdleStepDisableIdentity { throw "Provider '$providerAlias' does not implement DisableIdentity method." } - # Check if the method is a ScriptMethod and inspect its parameters - $supportsAuthSession = $false - if ($providerMethod.MemberType -eq 'ScriptMethod') { - $scriptBlock = $providerMethod.Script - if ($null -ne $scriptBlock -and $null -ne $scriptBlock.Ast -and $null -ne $scriptBlock.Ast.ParamBlock) { - $params = $scriptBlock.Ast.ParamBlock.Parameters - if ($null -ne $params) { - foreach ($param in $params) { - if ($null -ne $param.Name -and $null -ne $param.Name.VariablePath) { - $paramName = $param.Name.VariablePath.UserPath - if ($paramName -eq 'AuthSession') { - $supportsAuthSession = $true - break - } - } - } - } - } - } + $supportsAuthSession = Test-IdleProviderMethodParameter -ProviderMethod $providerMethod -ParameterName 'AuthSession' # Call provider method with appropriate signature if ($supportsAuthSession -and $null -ne $authSession) { diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnableIdentity.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnableIdentity.ps1 index 9eac2df..a7d9c27 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnableIdentity.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnableIdentity.ps1 @@ -83,25 +83,7 @@ function Invoke-IdleStepEnableIdentity { throw "Provider '$providerAlias' does not implement EnableIdentity method." } - # Check if the method is a ScriptMethod and inspect its parameters - $supportsAuthSession = $false - if ($providerMethod.MemberType -eq 'ScriptMethod') { - $scriptBlock = $providerMethod.Script - if ($null -ne $scriptBlock -and $null -ne $scriptBlock.Ast -and $null -ne $scriptBlock.Ast.ParamBlock) { - $params = $scriptBlock.Ast.ParamBlock.Parameters - if ($null -ne $params) { - foreach ($param in $params) { - if ($null -ne $param.Name -and $null -ne $param.Name.VariablePath) { - $paramName = $param.Name.VariablePath.UserPath - if ($paramName -eq 'AuthSession') { - $supportsAuthSession = $true - break - } - } - } - } - } - } + $supportsAuthSession = Test-IdleProviderMethodParameter -ProviderMethod $providerMethod -ParameterName 'AuthSession' # Call provider method with appropriate signature if ($supportsAuthSession -and $null -ne $authSession) { diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureAttribute.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureAttribute.ps1 index 0073b9e..e5f0b37 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureAttribute.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureAttribute.ps1 @@ -83,25 +83,7 @@ function Invoke-IdleStepEnsureAttribute { throw "Provider '$providerAlias' does not implement EnsureAttribute method." } - # Check if the method is a ScriptMethod and inspect its parameters - $supportsAuthSession = $false - if ($providerMethod.MemberType -eq 'ScriptMethod') { - $scriptBlock = $providerMethod.Script - if ($null -ne $scriptBlock -and $null -ne $scriptBlock.Ast -and $null -ne $scriptBlock.Ast.ParamBlock) { - $params = $scriptBlock.Ast.ParamBlock.Parameters - if ($null -ne $params) { - foreach ($param in $params) { - if ($null -ne $param.Name -and $null -ne $param.Name.VariablePath) { - $paramName = $param.Name.VariablePath.UserPath - if ($paramName -eq 'AuthSession') { - $supportsAuthSession = $true - break - } - } - } - } - } - } + $supportsAuthSession = Test-IdleProviderMethodParameter -ProviderMethod $providerMethod -ParameterName 'AuthSession' # Call provider method with appropriate signature if ($supportsAuthSession -and $null -ne $authSession) { diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 index 9637826..99a69cc 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 @@ -185,33 +185,10 @@ function Invoke-IdleStepEnsureEntitlement { } } - # Helper function to check if a provider method supports AuthSession parameter - $testMethodSupportsAuthSession = { - param([string]$MethodName) - $method = $provider.PSObject.Methods[$MethodName] - if ($method.MemberType -eq 'ScriptMethod') { - $scriptBlock = $method.Script - if ($null -ne $scriptBlock -and $null -ne $scriptBlock.Ast -and $null -ne $scriptBlock.Ast.ParamBlock) { - $params = $scriptBlock.Ast.ParamBlock.Parameters - if ($null -ne $params) { - foreach ($param in $params) { - if ($null -ne $param.Name -and $null -ne $param.Name.VariablePath) { - $paramName = $param.Name.VariablePath.UserPath - if ($paramName -eq 'AuthSession') { - return $true - } - } - } - } - } - } - return $false - } - # Check AuthSession support for each method - $listSupportsAuthSession = & $testMethodSupportsAuthSession -MethodName 'ListEntitlements' - $grantSupportsAuthSession = & $testMethodSupportsAuthSession -MethodName 'GrantEntitlement' - $revokeSupportsAuthSession = & $testMethodSupportsAuthSession -MethodName 'RevokeEntitlement' + $listSupportsAuthSession = Test-IdleProviderMethodParameter -ProviderMethod $provider.PSObject.Methods['ListEntitlements'] -ParameterName 'AuthSession' + $grantSupportsAuthSession = Test-IdleProviderMethodParameter -ProviderMethod $provider.PSObject.Methods['GrantEntitlement'] -ParameterName 'AuthSession' + $revokeSupportsAuthSession = Test-IdleProviderMethodParameter -ProviderMethod $provider.PSObject.Methods['RevokeEntitlement'] -ParameterName 'AuthSession' if ($listSupportsAuthSession -and $null -ne $authSession) { $current = @($provider.ListEntitlements($identityKey, $authSession)) diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepMoveIdentity.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepMoveIdentity.ps1 index 46365b5..88d7395 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepMoveIdentity.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepMoveIdentity.ps1 @@ -86,25 +86,7 @@ function Invoke-IdleStepMoveIdentity { throw "Provider '$providerAlias' does not implement MoveIdentity method." } - # Check if the method is a ScriptMethod and inspect its parameters - $supportsAuthSession = $false - if ($providerMethod.MemberType -eq 'ScriptMethod') { - $scriptBlock = $providerMethod.Script - if ($null -ne $scriptBlock -and $null -ne $scriptBlock.Ast -and $null -ne $scriptBlock.Ast.ParamBlock) { - $params = $scriptBlock.Ast.ParamBlock.Parameters - if ($null -ne $params) { - foreach ($param in $params) { - if ($null -ne $param.Name -and $null -ne $param.Name.VariablePath) { - $paramName = $param.Name.VariablePath.UserPath - if ($paramName -eq 'AuthSession') { - $supportsAuthSession = $true - break - } - } - } - } - } - } + $supportsAuthSession = Test-IdleProviderMethodParameter -ProviderMethod $providerMethod -ParameterName 'AuthSession' # Call provider method with appropriate signature if ($supportsAuthSession -and $null -ne $authSession) { From 14ddbfc7ce36d23ad7d5376693f5ca2fbbe42751 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:06:18 +0000 Subject: [PATCH 10/14] feat: Add New-IdleAuthSessionBroker and consolidate step logic - Add New-IdleAuthSessionBroker public function for convenient broker creation - Export from IdLE.Core and IdLE modules - Create Invoke-IdleProviderMethod helper to consolidate auth session logic - Refactor all 6 identity steps to use Invoke-IdleProviderMethod (removes ~180 lines of duplication) - Update EnsureEntitlement with inline auth session check (can't use helper due to multiple method calls) - Remove Test-IdleProviderMethodParameter helper (logic now in Invoke-IdleProviderMethod) - Update documentation to showcase New-IdleAuthSessionBroker in all examples - All 157 tests passing Addresses feedback: - Simplifies broker setup for users (New-IdleAuthSessionBroker) - Eliminates code duplication across identity steps - Simplifies maintenance - auth logic in one place Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/reference/provider-ad.md | 67 ++++------ src/IdLE.Core/IdLE.Core.psm1 | 3 +- .../Public/New-IdleAuthSessionBroker.ps1 | 116 ++++++++++++++++++ .../Private/Invoke-IdleProviderMethod.ps1 | 79 ++++++++++++ .../Test-IdleProviderMethodParameter.ps1 | 40 ------ .../Public/Invoke-IdleStepCreateIdentity.ps1 | 38 +----- .../Public/Invoke-IdleStepDeleteIdentity.ps1 | 38 +----- .../Public/Invoke-IdleStepDisableIdentity.ps1 | 38 +----- .../Public/Invoke-IdleStepEnableIdentity.ps1 | 38 +----- .../Public/Invoke-IdleStepEnsureAttribute.ps1 | 38 +----- .../Invoke-IdleStepEnsureEntitlement.ps1 | 25 +++- .../Public/Invoke-IdleStepMoveIdentity.ps1 | 38 +----- src/IdLE/IdLE.psd1 | 3 +- 13 files changed, 284 insertions(+), 277 deletions(-) create mode 100644 src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 create mode 100644 src/IdLE.Steps.Common/Private/Invoke-IdleProviderMethod.ps1 delete mode 100644 src/IdLE.Steps.Common/Private/Test-IdleProviderMethodParameter.ps1 diff --git a/docs/reference/provider-ad.md b/docs/reference/provider-ad.md index 642a8b1..dde8ec9 100644 --- a/docs/reference/provider-ad.md +++ b/docs/reference/provider-ad.md @@ -101,46 +101,41 @@ $plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Provider Use an AuthSessionBroker to manage authentication centrally and enable multi-role scenarios. -The broker is a simple object with an `AcquireAuthSession(Name, Options)` method. The `Options` parameter is a hashtable you define - its keys and structure are completely up to you. Common patterns: - -- `@{ Role = 'Tier0' }` - select by role/privilege level -- `@{ Domain = 'SourceAD' }` - select by AD domain -- `@{ Environment = 'Production' }` - select by environment - -**Example: Simple broker with role-based selection** +**Simple approach with New-IdleAuthSessionBroker:** ```powershell # Create provider $provider = New-IdleADIdentityProvider -# Simple broker factory function -function New-ADAuthBroker { - param( - [PSCredential]$Tier0Credential, - [PSCredential]$AdminCredential - ) - - $broker = [pscustomobject]@{} - $broker | Add-Member -MemberType ScriptProperty -Name Tier0Cred -Value { $Tier0Credential } - $broker | Add-Member -MemberType ScriptProperty -Name AdminCred -Value { $AdminCredential } - $broker | Add-Member -MemberType ScriptMethod -Name AcquireAuthSession -Value { - param($Name, $Options) - if ($Options.Role -eq 'Tier0') { - return $this.Tier0Cred - } - return $this.AdminCred - } - return $broker -} +# Create broker with role-based credential mapping +$broker = New-IdleAuthSessionBroker -SessionMap @{ + @{ Role = 'Tier0' } = $tier0Credential + @{ Role = 'Admin' } = $adminCredential +} -DefaultCredential $adminCredential -# Use the broker -$broker = New-ADAuthBroker -Tier0Credential $tier0Cred -AdminCredential $adminCred +# Use provider with broker $plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Providers @{ Identity = $provider AuthSessionBroker = $broker } ``` +**Custom broker for advanced scenarios:** + +For advanced scenarios (vault integration, MFA, dynamic credential retrieval), implement a custom broker: + +```powershell +$broker = [pscustomobject]@{} +$broker | Add-Member -MemberType ScriptMethod -Name AcquireAuthSession -Value { + param($Name, $Options) + # Custom logic: retrieve from vault, prompt for MFA, etc. + if ($Options.Role -eq 'Tier0') { + return Get-SecretFromVault -Name 'AD-Tier0' + } + return Get-SecretFromVault -Name 'AD-Admin' +} +``` + In workflow definitions, steps specify which auth context to use via `AuthSessionOptions`: ```powershell @@ -192,18 +187,10 @@ For scenarios with multiple AD forests or domains, use provider aliases with the $sourceAD = New-IdleADIdentityProvider $targetAD = New-IdleADIdentityProvider -AllowDelete -# Define broker that routes credentials based on provider -$broker = [pscustomobject]@{} -$broker | Add-Member -MemberType ScriptMethod -Name AcquireAuthSession -Value { - param($Name, $Options) - $domain = $Options.Domain - if ($domain -eq 'Source') { - return [PSCredential]::new('SOURCE\Admin', $sourceSecurePassword) - } - elseif ($domain -eq 'Target') { - return [PSCredential]::new('TARGET\Admin', $targetSecurePassword) - } - throw "Unknown domain: $domain" +# Use New-IdleAuthSessionBroker for domain-based credential routing +$broker = New-IdleAuthSessionBroker -SessionMap @{ + @{ Domain = 'Source' } = $sourceCred + @{ Domain = 'Target' } = $targetCred } $plan = New-IdlePlan -WorkflowPath './migration.psd1' -Request $request -Providers @{ diff --git a/src/IdLE.Core/IdLE.Core.psm1 b/src/IdLE.Core/IdLE.Core.psm1 index 466cf73..969fd60 100644 --- a/src/IdLE.Core/IdLE.Core.psm1 +++ b/src/IdLE.Core/IdLE.Core.psm1 @@ -23,5 +23,6 @@ Export-ModuleMember -Function @( 'Test-IdleWorkflowDefinitionObject', 'New-IdlePlanObject', 'Invoke-IdlePlanObject', - 'Export-IdlePlanObject' + 'Export-IdlePlanObject', + 'New-IdleAuthSessionBroker' ) -Alias @() diff --git a/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 b/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 new file mode 100644 index 0000000..f22c76a --- /dev/null +++ b/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 @@ -0,0 +1,116 @@ +function New-IdleAuthSessionBroker { + <# + .SYNOPSIS + Creates a simple AuthSessionBroker for use with IdLE providers. + + .DESCRIPTION + Creates an AuthSessionBroker that routes authentication based on user-defined options. + The broker is used by steps to acquire credentials at runtime without embedding + secrets in workflows or provider construction. + + This is a convenience function for common scenarios. For advanced scenarios + (vault integration, MFA, etc.), implement a custom broker object with an + AcquireAuthSession method. + + .PARAMETER SessionMap + A hashtable that maps session configurations to credentials. Each key is a hashtable + representing the AuthSessionOptions pattern, and each value is the PSCredential to return. + + Common patterns: + - @{ Role = 'Tier0' } -> $tier0Credential + - @{ Role = 'Admin' } -> $adminCredential + - @{ Domain = 'SourceAD' } -> $sourceCred + - @{ Environment = 'Production' } -> $prodCred + + .PARAMETER DefaultCredential + Optional default credential to return when no session options are provided or + when the options don't match any entry in SessionMap. + + .EXAMPLE + # Simple role-based broker + $broker = New-IdleAuthSessionBroker -SessionMap @{ + @{ Role = 'Tier0' } = $tier0Credential + @{ Role = 'Admin' } = $adminCredential + } -DefaultCredential $adminCredential + + $plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Providers @{ + Identity = New-IdleADIdentityProvider + AuthSessionBroker = $broker + } + + .EXAMPLE + # Domain-based broker for multi-forest scenarios + $broker = New-IdleAuthSessionBroker -SessionMap @{ + @{ Domain = 'SourceAD' } = $sourceCred + @{ Domain = 'TargetAD' } = $targetCred + } + + .OUTPUTS + PSCustomObject with AcquireAuthSession method + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNull()] + [hashtable] $SessionMap, + + [Parameter()] + [AllowNull()] + [PSCredential] $DefaultCredential + ) + + $broker = [pscustomobject]@{ + PSTypeName = 'IdLE.AuthSessionBroker' + SessionMap = $SessionMap + DefaultCredential = $DefaultCredential + } + + $broker | Add-Member -MemberType ScriptMethod -Name AcquireAuthSession -Value { + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $Name, + + [Parameter()] + [AllowNull()] + [hashtable] $Options + ) + + # If no options provided, return default + if ($null -eq $Options -or $Options.Count -eq 0) { + if ($null -ne $this.DefaultCredential) { + return $this.DefaultCredential + } + throw "No auth session options provided and no default credential configured." + } + + # Find matching session in map + 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 + } + } + + if ($matches) { + return $credential + } + } + + # No match found + if ($null -ne $this.DefaultCredential) { + return $this.DefaultCredential + } + + $optionsStr = ($Options.Keys | ForEach-Object { "$_=$($Options[$_])" }) -join ', ' + throw "No matching credential found for options: $optionsStr" + } -Force + + return $broker +} diff --git a/src/IdLE.Steps.Common/Private/Invoke-IdleProviderMethod.ps1 b/src/IdLE.Steps.Common/Private/Invoke-IdleProviderMethod.ps1 new file mode 100644 index 0000000..83d087e --- /dev/null +++ b/src/IdLE.Steps.Common/Private/Invoke-IdleProviderMethod.ps1 @@ -0,0 +1,79 @@ +# Invokes a provider method with optional AuthSession support. +# Handles auth session acquisition, parameter detection, and backwards-compatible fallback. + +function Invoke-IdleProviderMethod { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNull()] + [object] $Context, + + [Parameter(Mandatory)] + [ValidateNotNull()] + [hashtable] $With, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $ProviderAlias, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $MethodName, + + [Parameter(Mandatory)] + [ValidateNotNull()] + [object[]] $MethodArguments + ) + + # 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 } + + if ($null -ne $sessionOptions -and -not ($sessionOptions -is [hashtable])) { + throw "With.AuthSessionOptions must be a hashtable or null." + } + + $authSession = $Context.AcquireAuthSession($sessionName, $sessionOptions) + } + + $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 = $false + if ($providerMethod.MemberType -eq 'ScriptMethod') { + $scriptBlock = $providerMethod.Script + if ($null -ne $scriptBlock -and $null -ne $scriptBlock.Ast -and $null -ne $scriptBlock.Ast.ParamBlock) { + $params = $scriptBlock.Ast.ParamBlock.Parameters + if ($null -ne $params) { + foreach ($param in $params) { + if ($null -ne $param.Name -and $null -ne $param.Name.VariablePath) { + $paramName = $param.Name.VariablePath.UserPath + if ($paramName -eq 'AuthSession') { + $supportsAuthSession = $true + break + } + } + } + } + } + } + + # Call provider method with appropriate signature + if ($supportsAuthSession -and $null -ne $authSession) { + # Provider supports AuthSession and we have one - pass it + $allArgs = $MethodArguments + $authSession + return $provider.$MethodName.Invoke($allArgs) + } + else { + # Legacy signature (no AuthSession parameter) or no session acquired + return $provider.$MethodName.Invoke($MethodArguments) + } +} diff --git a/src/IdLE.Steps.Common/Private/Test-IdleProviderMethodParameter.ps1 b/src/IdLE.Steps.Common/Private/Test-IdleProviderMethodParameter.ps1 deleted file mode 100644 index 4d1a8ce..0000000 --- a/src/IdLE.Steps.Common/Private/Test-IdleProviderMethodParameter.ps1 +++ /dev/null @@ -1,40 +0,0 @@ -# Tests whether a provider method supports a specific parameter. -# Used by steps to detect whether providers support optional AuthSession parameter. - -function Test-IdleProviderMethodParameter { - [CmdletBinding()] - param( - [Parameter(Mandatory)] - [ValidateNotNull()] - [object] $ProviderMethod, - - [Parameter(Mandatory)] - [ValidateNotNullOrEmpty()] - [string] $ParameterName - ) - - if ($ProviderMethod.MemberType -ne 'ScriptMethod') { - return $false - } - - $scriptBlock = $ProviderMethod.Script - if ($null -eq $scriptBlock -or $null -eq $scriptBlock.Ast -or $null -eq $scriptBlock.Ast.ParamBlock) { - return $false - } - - $params = $scriptBlock.Ast.ParamBlock.Parameters - if ($null -eq $params) { - return $false - } - - foreach ($param in $params) { - if ($null -ne $param.Name -and $null -ne $param.Name.VariablePath) { - $paramName = $param.Name.VariablePath.UserPath - if ($paramName -eq $ParameterName) { - return $true - } - } - } - - return $false -} diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepCreateIdentity.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepCreateIdentity.ps1 index 653bc1a..8460eab 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepCreateIdentity.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepCreateIdentity.ps1 @@ -69,38 +69,12 @@ function Invoke-IdleStepCreateIdentity { throw "Provider '$providerAlias' was not supplied by the host." } - # 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 } - - if ($null -ne $sessionOptions -and -not ($sessionOptions -is [hashtable])) { - throw "With.AuthSessionOptions must be a hashtable or null." - } - - $authSession = $Context.AcquireAuthSession($sessionName, $sessionOptions) - } - - $provider = $Context.Providers[$providerAlias] - - # Call provider with AuthSession if supported (backwards compatible fallback) - $providerMethod = $provider.PSObject.Methods['CreateIdentity'] - if ($null -eq $providerMethod) { - throw "Provider '$providerAlias' does not implement CreateIdentity method." - } - - $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 - $result = $provider.CreateIdentity([string]$with.IdentityKey, $with.Attributes, $authSession) - } - else { - # Legacy signature (no AuthSession parameter) or no session acquired - $result = $provider.CreateIdentity([string]$with.IdentityKey, $with.Attributes) - } + $result = Invoke-IdleProviderMethod ` + -Context $Context ` + -With $with ` + -ProviderAlias $providerAlias ` + -MethodName 'CreateIdentity' ` + -MethodArguments @([string]$with.IdentityKey, $with.Attributes) $changed = $false if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'Changed')) { diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepDeleteIdentity.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepDeleteIdentity.ps1 index 0d2ca84..3debd5e 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepDeleteIdentity.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepDeleteIdentity.ps1 @@ -66,38 +66,12 @@ function Invoke-IdleStepDeleteIdentity { throw "Provider '$providerAlias' was not supplied by the host." } - # 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 } - - if ($null -ne $sessionOptions -and -not ($sessionOptions -is [hashtable])) { - throw "With.AuthSessionOptions must be a hashtable or null." - } - - $authSession = $Context.AcquireAuthSession($sessionName, $sessionOptions) - } - - $provider = $Context.Providers[$providerAlias] - - # Call provider with AuthSession if supported (backwards compatible fallback) - $providerMethod = $provider.PSObject.Methods['DeleteIdentity'] - if ($null -eq $providerMethod) { - throw "Provider '$providerAlias' does not implement DeleteIdentity method." - } - - $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 - $result = $provider.DeleteIdentity([string]$with.IdentityKey, $authSession) - } - else { - # Legacy signature (no AuthSession parameter) or no session acquired - $result = $provider.DeleteIdentity([string]$with.IdentityKey) - } + $result = Invoke-IdleProviderMethod ` + -Context $Context ` + -With $with ` + -ProviderAlias $providerAlias ` + -MethodName 'DeleteIdentity' ` + -MethodArguments @([string]$with.IdentityKey) $changed = $false if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'Changed')) { diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepDisableIdentity.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepDisableIdentity.ps1 index e026262..fdd19ce 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepDisableIdentity.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepDisableIdentity.ps1 @@ -62,38 +62,12 @@ function Invoke-IdleStepDisableIdentity { throw "Provider '$providerAlias' was not supplied by the host." } - # 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 } - - if ($null -ne $sessionOptions -and -not ($sessionOptions -is [hashtable])) { - throw "With.AuthSessionOptions must be a hashtable or null." - } - - $authSession = $Context.AcquireAuthSession($sessionName, $sessionOptions) - } - - $provider = $Context.Providers[$providerAlias] - - # Call provider with AuthSession if supported (backwards compatible fallback) - $providerMethod = $provider.PSObject.Methods['DisableIdentity'] - if ($null -eq $providerMethod) { - throw "Provider '$providerAlias' does not implement DisableIdentity method." - } - - $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 - $result = $provider.DisableIdentity([string]$with.IdentityKey, $authSession) - } - else { - # Legacy signature (no AuthSession parameter) or no session acquired - $result = $provider.DisableIdentity([string]$with.IdentityKey) - } + $result = Invoke-IdleProviderMethod ` + -Context $Context ` + -With $with ` + -ProviderAlias $providerAlias ` + -MethodName 'DisableIdentity' ` + -MethodArguments @([string]$with.IdentityKey) $changed = $false if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'Changed')) { diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnableIdentity.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnableIdentity.ps1 index a7d9c27..9ea7c36 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnableIdentity.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnableIdentity.ps1 @@ -62,38 +62,12 @@ function Invoke-IdleStepEnableIdentity { throw "Provider '$providerAlias' was not supplied by the host." } - # 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 } - - if ($null -ne $sessionOptions -and -not ($sessionOptions -is [hashtable])) { - throw "With.AuthSessionOptions must be a hashtable or null." - } - - $authSession = $Context.AcquireAuthSession($sessionName, $sessionOptions) - } - - $provider = $Context.Providers[$providerAlias] - - # Call provider with AuthSession if supported (backwards compatible fallback) - $providerMethod = $provider.PSObject.Methods['EnableIdentity'] - if ($null -eq $providerMethod) { - throw "Provider '$providerAlias' does not implement EnableIdentity method." - } - - $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 - $result = $provider.EnableIdentity([string]$with.IdentityKey, $authSession) - } - else { - # Legacy signature (no AuthSession parameter) or no session acquired - $result = $provider.EnableIdentity([string]$with.IdentityKey) - } + $result = Invoke-IdleProviderMethod ` + -Context $Context ` + -With $with ` + -ProviderAlias $providerAlias ` + -MethodName 'EnableIdentity' ` + -MethodArguments @([string]$with.IdentityKey) $changed = $false if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'Changed')) { diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureAttribute.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureAttribute.ps1 index e5f0b37..e784225 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureAttribute.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureAttribute.ps1 @@ -62,38 +62,12 @@ function Invoke-IdleStepEnsureAttribute { throw "Provider '$providerAlias' was not supplied by the host." } - # 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 } - - if ($null -ne $sessionOptions -and -not ($sessionOptions -is [hashtable])) { - throw "With.AuthSessionOptions must be a hashtable or null." - } - - $authSession = $Context.AcquireAuthSession($sessionName, $sessionOptions) - } - - $provider = $Context.Providers[$providerAlias] - - # Call provider with AuthSession if supported (backwards compatible fallback) - $providerMethod = $provider.PSObject.Methods['EnsureAttribute'] - if ($null -eq $providerMethod) { - throw "Provider '$providerAlias' does not implement EnsureAttribute method." - } - - $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 - $result = $provider.EnsureAttribute([string]$with.IdentityKey, [string]$with.Name, $with.Value, $authSession) - } - else { - # Legacy signature (no AuthSession parameter) or no session acquired - $result = $provider.EnsureAttribute([string]$with.IdentityKey, [string]$with.Name, $with.Value) - } + $result = Invoke-IdleProviderMethod ` + -Context $Context ` + -With $with ` + -ProviderAlias $providerAlias ` + -MethodName 'EnsureAttribute' ` + -MethodArguments @([string]$with.IdentityKey, [string]$with.Name, $with.Value) $changed = $false if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'Changed')) { diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 index 99a69cc..7637476 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 @@ -185,10 +185,29 @@ function Invoke-IdleStepEnsureEntitlement { } } + # Helper to check AuthSession support for a provider method + $checkAuthSessionSupport = { + param($Method) + if ($Method.MemberType -eq 'ScriptMethod') { + $scriptBlock = $Method.Script + if ($null -ne $scriptBlock -and $null -ne $scriptBlock.Ast -and $null -ne $scriptBlock.Ast.ParamBlock) { + $params = $scriptBlock.Ast.ParamBlock.Parameters + if ($null -ne $params) { + foreach ($param in $params) { + if ($null -ne $param.Name -and $null -ne $param.Name.VariablePath -and $param.Name.VariablePath.UserPath -eq 'AuthSession') { + return $true + } + } + } + } + } + return $false + } + # Check AuthSession support for each method - $listSupportsAuthSession = Test-IdleProviderMethodParameter -ProviderMethod $provider.PSObject.Methods['ListEntitlements'] -ParameterName 'AuthSession' - $grantSupportsAuthSession = Test-IdleProviderMethodParameter -ProviderMethod $provider.PSObject.Methods['GrantEntitlement'] -ParameterName 'AuthSession' - $revokeSupportsAuthSession = Test-IdleProviderMethodParameter -ProviderMethod $provider.PSObject.Methods['RevokeEntitlement'] -ParameterName 'AuthSession' + $listSupportsAuthSession = & $checkAuthSessionSupport $provider.PSObject.Methods['ListEntitlements'] + $grantSupportsAuthSession = & $checkAuthSessionSupport $provider.PSObject.Methods['GrantEntitlement'] + $revokeSupportsAuthSession = & $checkAuthSessionSupport $provider.PSObject.Methods['RevokeEntitlement'] if ($listSupportsAuthSession -and $null -ne $authSession) { $current = @($provider.ListEntitlements($identityKey, $authSession)) diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepMoveIdentity.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepMoveIdentity.ps1 index 88d7395..e6043a0 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepMoveIdentity.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepMoveIdentity.ps1 @@ -65,38 +65,12 @@ function Invoke-IdleStepMoveIdentity { throw "Provider '$providerAlias' was not supplied by the host." } - # 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 } - - if ($null -ne $sessionOptions -and -not ($sessionOptions -is [hashtable])) { - throw "With.AuthSessionOptions must be a hashtable or null." - } - - $authSession = $Context.AcquireAuthSession($sessionName, $sessionOptions) - } - - $provider = $Context.Providers[$providerAlias] - - # Call provider with AuthSession if supported (backwards compatible fallback) - $providerMethod = $provider.PSObject.Methods['MoveIdentity'] - if ($null -eq $providerMethod) { - throw "Provider '$providerAlias' does not implement MoveIdentity method." - } - - $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 - $result = $provider.MoveIdentity([string]$with.IdentityKey, [string]$with.TargetContainer, $authSession) - } - else { - # Legacy signature (no AuthSession parameter) or no session acquired - $result = $provider.MoveIdentity([string]$with.IdentityKey, [string]$with.TargetContainer) - } + $result = Invoke-IdleProviderMethod ` + -Context $Context ` + -With $with ` + -ProviderAlias $providerAlias ` + -MethodName 'MoveIdentity' ` + -MethodArguments @([string]$with.IdentityKey, [string]$with.TargetContainer) $changed = $false if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'Changed')) { diff --git a/src/IdLE/IdLE.psd1 b/src/IdLE/IdLE.psd1 index a90af1d..36c5bb0 100644 --- a/src/IdLE/IdLE.psd1 +++ b/src/IdLE/IdLE.psd1 @@ -18,7 +18,8 @@ 'New-IdleLifecycleRequest', 'New-IdlePlan', 'Invoke-IdlePlan', - 'Export-IdlePlan' + 'Export-IdlePlan', + 'New-IdleAuthSessionBroker' ) CmdletsToExport = @() AliasesToExport = @() From e1ce6989d2bdbc8dad9bf8d7221316779222e8e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:21:51 +0000 Subject: [PATCH 11/14] docs: Clarify credential variable sources in examples - Add explanation showing where $tier0Credential and $adminCredential come from - Add similar explanation for $sourceCred and $targetCred in multi-domain example - Use Get-Credential for demonstration purposes (real implementations would use vault/secure storage) All 157 tests passing. Mock Provider doesn't need AuthSession support (only implements subset of methods). Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/reference/provider-ad.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/reference/provider-ad.md b/docs/reference/provider-ad.md index dde8ec9..ce2dbbf 100644 --- a/docs/reference/provider-ad.md +++ b/docs/reference/provider-ad.md @@ -104,6 +104,10 @@ Use an AuthSessionBroker to manage authentication centrally and enable multi-rol **Simple approach with New-IdleAuthSessionBroker:** ```powershell +# Assuming you have credentials available (e.g., from a secure vault or credential manager) +$tier0Credential = Get-Credential -Message "Enter Tier0 admin credentials" +$adminCredential = Get-Credential -Message "Enter regular admin credentials" + # Create provider $provider = New-IdleADIdentityProvider @@ -183,6 +187,10 @@ $provider = New-IdleADIdentityProvider -AllowDelete For scenarios with multiple AD forests or domains, use provider aliases with the AuthSessionBroker: ```powershell +# Assuming you have credentials for each domain +$sourceCred = Get-Credential -Message "Enter Source AD admin credentials" +$targetCred = Get-Credential -Message "Enter Target AD admin credentials" + # Create providers for different AD environments $sourceAD = New-IdleADIdentityProvider $targetAD = New-IdleADIdentityProvider -AllowDelete From 19da512b86c3224aa1d24a8cd5817b1315b47032 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 14:24:59 +0000 Subject: [PATCH 12/14] docs: Regenerate step reference and clarify generator usage - Regenerate docs/reference/steps.md with auth session documentation for all steps - Update .github/copilot-instructions.md with explicit generator tool instructions - Clarify when to run Generate-IdleCmdletReference.ps1 and Generate-IdleStepReference.ps1 - Add note that CI will fail if generated docs are out of date Generated step reference now includes authentication sections for all provider-calling steps, documenting With.AuthSessionName and With.AuthSessionOptions usage. Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- .github/copilot-instructions.md | 6 +++- docs/reference/steps.md | 56 +++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index cd43ca9..8f2a7a7 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -38,7 +38,11 @@ If anything in this file conflicts with those, the more specific document wins. ## Documentation rules - Do not edit generated references under `docs/reference/` by hand. - - Regenerate via the repository tools as documented in `CONTRIBUTING.md`. + - **Always regenerate** after changing public cmdlets or step implementations: + - `./tools/Generate-IdleCmdletReference.ps1` - after cmdlet/help changes + - `./tools/Generate-IdleStepReference.ps1` - after step changes + - See `CONTRIBUTING.md` for complete instructions. + - CI will fail if generated docs are out of date. ## Git / PR conventions diff --git a/docs/reference/steps.md b/docs/reference/steps.md index ae8ac50..719900a 100644 --- a/docs/reference/steps.md +++ b/docs/reference/steps.md @@ -28,6 +28,14 @@ and returns an object with properties 'IdentityKey' and 'Changed'. The step is idempotent by design: if the identity already exists, the provider should return Changed = $false without creating a duplicate. +Authentication: +- If With.AuthSessionName is present, the step acquires an auth session via + Context.AcquireAuthSession(Name, Options) and passes it to the provider method + if the provider supports an AuthSession parameter. +- With.AuthSessionOptions (optional, hashtable) is passed to the broker for + session selection (e.g., @{ Role = 'Tier0' }). +- ScriptBlocks in AuthSessionOptions are rejected (security boundary). + **Inputs (With.\*)** | Key | Required | @@ -62,6 +70,14 @@ IMPORTANT: This step requires the provider to advertise the IdLE.Identity.Delete capability, which is typically opt-in for safety. The provider must be configured to allow deletion (e.g., AllowDelete = $true for AD provider). +Authentication: +- If With.AuthSessionName is present, the step acquires an auth session via + Context.AcquireAuthSession(Name, Options) and passes it to the provider method + if the provider supports an AuthSession parameter. +- With.AuthSessionOptions (optional, hashtable) is passed to the broker for + session selection (e.g., @{ Role = 'Tier0' }). +- ScriptBlocks in AuthSessionOptions are rejected (security boundary). + **Inputs (With.\*)** _Unknown (not detected automatically). Document required With.* keys in the step help and/or use a supported pattern._ @@ -89,6 +105,14 @@ and returns an object with properties 'IdentityKey' and 'Changed'. The step is idempotent by design: if the identity is already disabled, the provider should return Changed = $false. +Authentication: +- If With.AuthSessionName is present, the step acquires an auth session via + Context.AcquireAuthSession(Name, Options) and passes it to the provider method + if the provider supports an AuthSession parameter. +- With.AuthSessionOptions (optional, hashtable) is passed to the broker for + session selection (e.g., @{ Role = 'Tier0' }). +- ScriptBlocks in AuthSessionOptions are rejected (security boundary). + **Inputs (With.\*)** _Unknown (not detected automatically). Document required With.* keys in the step help and/or use a supported pattern._ @@ -140,6 +164,14 @@ and returns an object with properties 'IdentityKey' and 'Changed'. The step is idempotent by design: if the identity is already enabled, the provider should return Changed = $false. +Authentication: +- If With.AuthSessionName is present, the step acquires an auth session via + Context.AcquireAuthSession(Name, Options) and passes it to the provider method + if the provider supports an AuthSession parameter. +- With.AuthSessionOptions (optional, hashtable) is passed to the broker for + session selection (e.g., @{ Role = 'Tier0' }). +- ScriptBlocks in AuthSessionOptions are rejected (security boundary). + **Inputs (With.\*)** _Unknown (not detected automatically). Document required With.* keys in the step help and/or use a supported pattern._ @@ -167,6 +199,14 @@ contains a boolean property 'Changed'. The step is idempotent by design: it converges state to the desired value. +Authentication: +- If With.AuthSessionName is present, the step acquires an auth session via + Context.AcquireAuthSession(Name, Options) and passes it to the provider method + if the provider supports an AuthSession parameter. +- With.AuthSessionOptions (optional, hashtable) is passed to the broker for + session selection (e.g., @{ Role = 'Tier0' }). +- ScriptBlocks in AuthSessionOptions are rejected (security boundary). + **Inputs (With.\*)** | Key | Required | @@ -202,6 +242,14 @@ via `Context.Providers[]` that implements: The step is idempotent and only calls Grant/Revoke when the assignment needs to change. +Authentication: +- If With.AuthSessionName is present, the step acquires an auth session via + Context.AcquireAuthSession(Name, Options) and passes it to the provider methods + if the provider supports an AuthSession parameter. +- With.AuthSessionOptions (optional, hashtable) is passed to the broker for + session selection (e.g., @{ Role = 'Tier0' }). +- ScriptBlocks in AuthSessionOptions are rejected (security boundary). + **Inputs (With.\*)** | Key | Required | @@ -233,6 +281,14 @@ and returns an object with properties 'IdentityKey' and 'Changed'. The step is idempotent by design: if the identity is already in the target container, the provider should return Changed = $false. +Authentication: +- If With.AuthSessionName is present, the step acquires an auth session via + Context.AcquireAuthSession(Name, Options) and passes it to the provider method + if the provider supports an AuthSession parameter. +- With.AuthSessionOptions (optional, hashtable) is passed to the broker for + session selection (e.g., @{ Role = 'Tier0' }). +- ScriptBlocks in AuthSessionOptions are rejected (security boundary). + **Inputs (With.\*)** | Key | Required | From c08b76cf1fb553399f5fa7dfd1f4b60345d6abf1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 15:21:00 +0000 Subject: [PATCH 13/14] refactor: Fix thread-safety and code duplication issues - Add Test-IdleProviderMethodParameter helper for parameter detection - Support parameter detection for compiled methods (PSMethod, CodeMethod) via reflection - Update Invoke-IdleProviderMethod to use Test-IdleProviderMethodParameter helper - Update Invoke-IdleStepEnsureEntitlement to use Test-IdleProviderMethodParameter helper - Add optional AuthSession parameter to ResolveIdentity and NormalizeGroupId - Remove thread-unsafe adapter swapping from all 10 AD provider methods - All methods now pass AuthSession to ResolveIdentity/NormalizeGroupId - All 157 tests passing, PSScriptAnalyzer clean Addresses code review findings: - Thread-safety: r2705042610, r2705042647, r2705042666, r2705042564, r2705042685, r2705042701, r2705042718, r2705042734 - Code duplication: r2705042594 - Parameter detection limitation: r2705054811 Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- .../Public/New-IdleADIdentityProvider.ps1 | 124 +++++------------- .../Private/Invoke-IdleProviderMethod.ps1 | 19 +-- .../Test-IdleProviderMethodParameter.ps1 | 59 +++++++++ .../Invoke-IdleStepEnsureEntitlement.ps1 | 25 +--- 4 files changed, 93 insertions(+), 134 deletions(-) create mode 100644 src/IdLE.Steps.Common/Private/Test-IdleProviderMethodParameter.ps1 diff --git a/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 b/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 index 3cc346d..b57dda4 100644 --- a/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 +++ b/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 @@ -146,14 +146,20 @@ function New-IdleADIdentityProvider { param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] - [string] $IdentityKey + [string] $IdentityKey, + + [Parameter()] + [AllowNull()] + [object] $AuthSession ) + $adapter = $this.GetEffectiveAdapter($AuthSession) + # Try GUID format first (most deterministic) $guid = [System.Guid]::Empty if ([System.Guid]::TryParse($IdentityKey, [ref]$guid)) { try { - $user = $this.Adapter.GetUserByGuid($guid.ToString()) + $user = $adapter.GetUserByGuid($guid.ToString()) } catch [System.Management.Automation.MethodException] { Write-Verbose "GetUserByGuid failed for GUID '$IdentityKey': $_" @@ -168,7 +174,7 @@ function New-IdleADIdentityProvider { # Try UPN format (contains @) if ($IdentityKey -match '@') { - $user = $this.Adapter.GetUserByUpn($IdentityKey) + $user = $adapter.GetUserByUpn($IdentityKey) if ($null -ne $user) { return $user } @@ -176,7 +182,7 @@ function New-IdleADIdentityProvider { } # Fallback to sAMAccountName - $user = $this.Adapter.GetUserBySam($IdentityKey) + $user = $adapter.GetUserBySam($IdentityKey) if ($null -ne $user) { return $user } @@ -187,10 +193,16 @@ function New-IdleADIdentityProvider { param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] - [string] $GroupId + [string] $GroupId, + + [Parameter()] + [AllowNull()] + [object] $AuthSession ) - $group = $this.Adapter.GetGroupById($GroupId) + $adapter = $this.GetEffectiveAdapter($AuthSession) + + $group = $adapter.GetGroupById($GroupId) if ($null -eq $group) { throw "Group '$GroupId' not found." } @@ -272,15 +284,7 @@ function New-IdleADIdentityProvider { $adapter = $this.GetEffectiveAdapter($AuthSession) - # Temporarily use the effective adapter for identity resolution - $originalAdapter = $this.Adapter - $this.Adapter = $adapter - try { - $user = $this.ResolveIdentity($IdentityKey) - } - finally { - $this.Adapter = $originalAdapter - } + $user = $this.ResolveIdentity($IdentityKey, $AuthSession) $attributes = @{} if ($null -ne $user.GivenName) { $attributes['GivenName'] = $user.GivenName } @@ -339,11 +343,8 @@ function New-IdleADIdentityProvider { $adapter = $this.GetEffectiveAdapter($AuthSession) - # Temporarily use the effective adapter for identity resolution - $originalAdapter = $this.Adapter - $this.Adapter = $adapter try { - $existing = $this.ResolveIdentity($IdentityKey) + $existing = $this.ResolveIdentity($IdentityKey, $AuthSession) if ($null -ne $existing) { return [pscustomobject]@{ PSTypeName = 'IdLE.ProviderResult' @@ -357,9 +358,6 @@ function New-IdleADIdentityProvider { # Identity does not exist, proceed with creation (expected for idempotent create) Write-Verbose "Identity '$IdentityKey' does not exist, proceeding with creation" } - finally { - $this.Adapter = $originalAdapter - } $enabled = $true if ($Attributes.ContainsKey('Enabled')) { @@ -393,11 +391,8 @@ function New-IdleADIdentityProvider { $adapter = $this.GetEffectiveAdapter($AuthSession) - # Temporarily use the effective adapter for identity resolution - $originalAdapter = $this.Adapter - $this.Adapter = $adapter try { - $user = $this.ResolveIdentity($IdentityKey) + $user = $this.ResolveIdentity($IdentityKey, $AuthSession) $adapter.DeleteUser($user.DistinguishedName) return [pscustomobject]@{ PSTypeName = 'IdLE.ProviderResult' @@ -427,9 +422,6 @@ function New-IdleADIdentityProvider { } throw } - finally { - $this.Adapter = $originalAdapter - } } -Force $provider | Add-Member -MemberType ScriptMethod -Name EnsureAttribute -Value { @@ -453,15 +445,7 @@ function New-IdleADIdentityProvider { $adapter = $this.GetEffectiveAdapter($AuthSession) - # Temporarily use the effective adapter for identity resolution - $originalAdapter = $this.Adapter - $this.Adapter = $adapter - try { - $user = $this.ResolveIdentity($IdentityKey) - } - finally { - $this.Adapter = $originalAdapter - } + $user = $this.ResolveIdentity($IdentityKey, $AuthSession) $currentValue = $null if ($user.PSObject.Properties.Name -contains $Name) { @@ -501,15 +485,7 @@ function New-IdleADIdentityProvider { $adapter = $this.GetEffectiveAdapter($AuthSession) - # Temporarily use the effective adapter for identity resolution - $originalAdapter = $this.Adapter - $this.Adapter = $adapter - try { - $user = $this.ResolveIdentity($IdentityKey) - } - finally { - $this.Adapter = $originalAdapter - } + $user = $this.ResolveIdentity($IdentityKey, $AuthSession) $currentOu = $user.DistinguishedName -replace '^CN=[^,]+,', '' @@ -541,15 +517,7 @@ function New-IdleADIdentityProvider { $adapter = $this.GetEffectiveAdapter($AuthSession) - # Temporarily use the effective adapter for identity resolution - $originalAdapter = $this.Adapter - $this.Adapter = $adapter - try { - $user = $this.ResolveIdentity($IdentityKey) - } - finally { - $this.Adapter = $originalAdapter - } + $user = $this.ResolveIdentity($IdentityKey, $AuthSession) $changed = $false if ($user.Enabled -ne $false) { @@ -578,15 +546,7 @@ function New-IdleADIdentityProvider { $adapter = $this.GetEffectiveAdapter($AuthSession) - # Temporarily use the effective adapter for identity resolution - $originalAdapter = $this.Adapter - $this.Adapter = $adapter - try { - $user = $this.ResolveIdentity($IdentityKey) - } - finally { - $this.Adapter = $originalAdapter - } + $user = $this.ResolveIdentity($IdentityKey, $AuthSession) $changed = $false if ($user.Enabled -ne $true) { @@ -615,15 +575,7 @@ function New-IdleADIdentityProvider { $adapter = $this.GetEffectiveAdapter($AuthSession) - # Temporarily use the effective adapter for identity resolution - $originalAdapter = $this.Adapter - $this.Adapter = $adapter - try { - $user = $this.ResolveIdentity($IdentityKey) - } - finally { - $this.Adapter = $originalAdapter - } + $user = $this.ResolveIdentity($IdentityKey, $AuthSession) $groups = $adapter.GetUserGroups($user.DistinguishedName) @@ -659,16 +611,8 @@ function New-IdleADIdentityProvider { $normalized = $this.ConvertToEntitlement($Entitlement) - # Temporarily use the effective adapter for identity resolution - $originalAdapter = $this.Adapter - $this.Adapter = $adapter - try { - $user = $this.ResolveIdentity($IdentityKey) - $groupDn = $this.NormalizeGroupId($normalized.Id) - } - finally { - $this.Adapter = $originalAdapter - } + $user = $this.ResolveIdentity($IdentityKey, $AuthSession) + $groupDn = $this.NormalizeGroupId($normalized.Id, $AuthSession) $currentGroups = $this.ListEntitlements($IdentityKey, $AuthSession) $existing = $currentGroups | Where-Object { $this.TestEntitlementEquals($_, $normalized) } @@ -707,16 +651,8 @@ function New-IdleADIdentityProvider { $normalized = $this.ConvertToEntitlement($Entitlement) - # Temporarily use the effective adapter for identity resolution - $originalAdapter = $this.Adapter - $this.Adapter = $adapter - try { - $user = $this.ResolveIdentity($IdentityKey) - $groupDn = $this.NormalizeGroupId($normalized.Id) - } - finally { - $this.Adapter = $originalAdapter - } + $user = $this.ResolveIdentity($IdentityKey, $AuthSession) + $groupDn = $this.NormalizeGroupId($normalized.Id, $AuthSession) $currentGroups = $this.ListEntitlements($IdentityKey, $AuthSession) $existing = $currentGroups | Where-Object { $this.TestEntitlementEquals($_, $normalized) } diff --git a/src/IdLE.Steps.Common/Private/Invoke-IdleProviderMethod.ps1 b/src/IdLE.Steps.Common/Private/Invoke-IdleProviderMethod.ps1 index 83d087e..84f9830 100644 --- a/src/IdLE.Steps.Common/Private/Invoke-IdleProviderMethod.ps1 +++ b/src/IdLE.Steps.Common/Private/Invoke-IdleProviderMethod.ps1 @@ -47,24 +47,7 @@ function Invoke-IdleProviderMethod { } # Check if method supports AuthSession parameter - $supportsAuthSession = $false - if ($providerMethod.MemberType -eq 'ScriptMethod') { - $scriptBlock = $providerMethod.Script - if ($null -ne $scriptBlock -and $null -ne $scriptBlock.Ast -and $null -ne $scriptBlock.Ast.ParamBlock) { - $params = $scriptBlock.Ast.ParamBlock.Parameters - if ($null -ne $params) { - foreach ($param in $params) { - if ($null -ne $param.Name -and $null -ne $param.Name.VariablePath) { - $paramName = $param.Name.VariablePath.UserPath - if ($paramName -eq 'AuthSession') { - $supportsAuthSession = $true - break - } - } - } - } - } - } + $supportsAuthSession = Test-IdleProviderMethodParameter -ProviderMethod $providerMethod -ParameterName 'AuthSession' # Call provider method with appropriate signature if ($supportsAuthSession -and $null -ne $authSession) { diff --git a/src/IdLE.Steps.Common/Private/Test-IdleProviderMethodParameter.ps1 b/src/IdLE.Steps.Common/Private/Test-IdleProviderMethodParameter.ps1 new file mode 100644 index 0000000..8d65653 --- /dev/null +++ b/src/IdLE.Steps.Common/Private/Test-IdleProviderMethodParameter.ps1 @@ -0,0 +1,59 @@ +# Tests whether a provider method supports a given parameter. +# Supports ScriptMethod (AST inspection) and compiled methods (reflection). + +function Test-IdleProviderMethodParameter { + [CmdletBinding()] + [OutputType([bool])] + param( + [Parameter(Mandatory)] + [ValidateNotNull()] + [System.Management.Automation.PSMethodInfo] $ProviderMethod, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $ParameterName + ) + + # For ScriptMethod, inspect the AST + if ($ProviderMethod.MemberType -eq 'ScriptMethod') { + $scriptBlock = $ProviderMethod.Script + if ($null -ne $scriptBlock -and $null -ne $scriptBlock.Ast -and $null -ne $scriptBlock.Ast.ParamBlock) { + $params = $scriptBlock.Ast.ParamBlock.Parameters + if ($null -ne $params) { + foreach ($param in $params) { + if ($null -ne $param.Name -and $null -ne $param.Name.VariablePath) { + $paramName = $param.Name.VariablePath.UserPath + if ($paramName -eq $ParameterName) { + return $true + } + } + } + } + } + return $false + } + + # For compiled methods (PSMethod, CodeMethod), use reflection + if ($ProviderMethod.MemberType -in @('Method', 'CodeMethod')) { + try { + # Get the method info via reflection + $methodInfo = $ProviderMethod.OverloadDefinitions + if ($null -ne $methodInfo) { + # Check if any overload contains the parameter name + foreach ($overload in $methodInfo) { + if ($overload -match "\b$ParameterName\b") { + return $true + } + } + } + } + catch { + # If reflection fails, assume parameter is not supported + Write-Verbose "Could not inspect compiled method parameters: $_" + } + return $false + } + + # Unknown method type + return $false +} diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 index 7637476..99a69cc 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 @@ -185,29 +185,10 @@ function Invoke-IdleStepEnsureEntitlement { } } - # Helper to check AuthSession support for a provider method - $checkAuthSessionSupport = { - param($Method) - if ($Method.MemberType -eq 'ScriptMethod') { - $scriptBlock = $Method.Script - if ($null -ne $scriptBlock -and $null -ne $scriptBlock.Ast -and $null -ne $scriptBlock.Ast.ParamBlock) { - $params = $scriptBlock.Ast.ParamBlock.Parameters - if ($null -ne $params) { - foreach ($param in $params) { - if ($null -ne $param.Name -and $null -ne $param.Name.VariablePath -and $param.Name.VariablePath.UserPath -eq 'AuthSession') { - return $true - } - } - } - } - } - return $false - } - # Check AuthSession support for each method - $listSupportsAuthSession = & $checkAuthSessionSupport $provider.PSObject.Methods['ListEntitlements'] - $grantSupportsAuthSession = & $checkAuthSessionSupport $provider.PSObject.Methods['GrantEntitlement'] - $revokeSupportsAuthSession = & $checkAuthSessionSupport $provider.PSObject.Methods['RevokeEntitlement'] + $listSupportsAuthSession = Test-IdleProviderMethodParameter -ProviderMethod $provider.PSObject.Methods['ListEntitlements'] -ParameterName 'AuthSession' + $grantSupportsAuthSession = Test-IdleProviderMethodParameter -ProviderMethod $provider.PSObject.Methods['GrantEntitlement'] -ParameterName 'AuthSession' + $revokeSupportsAuthSession = Test-IdleProviderMethodParameter -ProviderMethod $provider.PSObject.Methods['RevokeEntitlement'] -ParameterName 'AuthSession' if ($listSupportsAuthSession -and $null -ne $authSession) { $current = @($provider.ListEntitlements($identityKey, $authSession)) From 7f484320325d9d15f9ee99d3177b218c8eb77cfd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 15:51:21 +0000 Subject: [PATCH 14/14] docs: Add New-IdleAuthSessionBroker example to provider help - Add recommended example using New-IdleAuthSessionBroker in provider help - Update custom broker example to show vault integration pattern - Align help examples with documentation in provider-ad.md - All 157 tests passing, PSScriptAnalyzer clean Addresses review comment r2705260963 Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- .../Public/New-IdleADIdentityProvider.ps1 | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 b/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 index b57dda4..d3c16cc 100644 --- a/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 +++ b/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 @@ -41,15 +41,34 @@ function New-IdleADIdentityProvider { } .EXAMPLE - # Multi-role scenario with AuthSessionBroker - # Broker returns different credentials based on With.AuthSessionOptions + # Multi-role scenario with New-IdleAuthSessionBroker (recommended) + $tier0Credential = Get-Credential -Message "Enter Tier0 admin credentials" + $adminCredential = Get-Credential -Message "Enter regular admin credentials" + + $broker = New-IdleAuthSessionBroker -SessionMap @{ + @{ Role = 'Tier0' } = $tier0Credential + @{ Role = 'Admin' } = $adminCredential + } -DefaultCredential $adminCredential + + $provider = New-IdleADIdentityProvider + $plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Providers @{ + Identity = $provider + AuthSessionBroker = $broker + } + + # Workflow steps can specify different auth contexts: + # With.AuthSessionName = 'ActiveDirectory' + # With.AuthSessionOptions = @{ Role = 'Tier0' } + + .EXAMPLE + # Custom broker for advanced scenarios (vault integration, MFA) $broker = [pscustomobject]@{} $broker | Add-Member -MemberType ScriptMethod -Name AcquireAuthSession -Value { param($Name, $Options) if ($Options.Role -eq 'Tier0') { - return [PSCredential]::new('DOMAIN\Tier0Admin', $tier0SecurePassword) + return Get-SecretFromVault -Name 'AD-Tier0' } - return [PSCredential]::new('DOMAIN\Admin', $adminSecurePassword) + return Get-SecretFromVault -Name 'AD-Admin' } $provider = New-IdleADIdentityProvider @@ -57,10 +76,6 @@ function New-IdleADIdentityProvider { Identity = $provider AuthSessionBroker = $broker } - - # Workflow steps can specify different auth contexts: - # With.AuthSessionName = 'ActiveDirectory' - # With.AuthSessionOptions = @{ Role = 'Tier0' } #> [CmdletBinding()] param(