From bb6bea41041f700c4ee45735daf4b650015eb50a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:32:41 +0000 Subject: [PATCH 01/16] Initial plan From 588ae5094864b7c79057449d4819209adbb6322b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:36:48 +0000 Subject: [PATCH 02/16] Flatten Identity.Profile attributes for direct template access - Modified Invoke-IdleContextResolvers to flatten identity attributes - Added ConvertTo-IdleFlattenedIdentity helper function - Attributes are now accessible as Identity.Profile.DisplayName instead of Identity.Profile.Attributes.DisplayName - Preserved Attributes hashtable for backwards compatibility - Added comprehensive tests for flattened structure and template substitution Co-authored-by: ntt-matthias-fleschuetz <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> --- .../Private/Invoke-IdleContextResolvers.ps1 | 83 +++++++++++++- .../New-IdlePlan.ContextResolvers.Tests.ps1 | 104 ++++++++++++++++++ 2 files changed, 184 insertions(+), 3 deletions(-) diff --git a/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 b/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 index 00cadea8..4cbf851c 100644 --- a/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 +++ b/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 @@ -337,10 +337,18 @@ function Invoke-IdleResolverCapabilityDispatch { } $supportsAuthSession = Test-IdleProviderMethodParameter -ProviderMethod $method -ParameterName 'AuthSession' - if ($supportsAuthSession -and $null -ne $AuthSession) { - return $provider.GetIdentity($identityKey, $AuthSession) + $identity = if ($supportsAuthSession -and $null -ne $AuthSession) { + $provider.GetIdentity($identityKey, $AuthSession) + } + else { + $provider.GetIdentity($identityKey) } - return $provider.GetIdentity($identityKey) + + # Flatten the identity object by merging Attributes into the top level. + # This allows users to access Request.Context.Identity.Profile.DisplayName + # instead of Request.Context.Identity.Profile.Attributes.DisplayName. + # The Attributes hashtable is still preserved as a property for backwards compatibility. + return ConvertTo-IdleFlattenedIdentity -Identity $identity } default { @@ -404,3 +412,72 @@ function Set-IdleContextValue { $current[$segments[-1]] = $Value } + +function ConvertTo-IdleFlattenedIdentity { + <# + .SYNOPSIS + Flattens an identity object by promoting Attributes to top-level properties. + + .DESCRIPTION + Takes an identity object returned by a provider (with IdentityKey, Enabled, Attributes) + and creates a new object where: + - IdentityKey and Enabled are preserved at the top level + - All properties from the Attributes hashtable are promoted to top-level properties + - The original Attributes hashtable is preserved for backwards compatibility + + This allows users to access Request.Context.Identity.Profile.DisplayName + instead of Request.Context.Identity.Profile.Attributes.DisplayName. + + .PARAMETER Identity + The identity object returned by a provider's GetIdentity method. + + .OUTPUTS + PSCustomObject with flattened attributes. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [AllowNull()] + [object] $Identity + ) + + if ($null -eq $Identity) { + return $null + } + + # Extract core properties + $identityKey = $null + $enabled = $null + $attributes = $null + + if ($Identity -is [System.Collections.IDictionary]) { + $identityKey = $Identity['IdentityKey'] + $enabled = $Identity['Enabled'] + $attributes = $Identity['Attributes'] + } + else { + $props = $Identity.PSObject.Properties + if ($props.Name -contains 'IdentityKey') { $identityKey = $Identity.IdentityKey } + if ($props.Name -contains 'Enabled') { $enabled = $Identity.Enabled } + if ($props.Name -contains 'Attributes') { $attributes = $Identity.Attributes } + } + + # Build flattened object + $flattened = [ordered]@{ + IdentityKey = $identityKey + Enabled = $enabled + Attributes = $attributes + } + + # Promote all attribute keys to top level + if ($null -ne $attributes -and $attributes -is [System.Collections.IDictionary]) { + foreach ($key in $attributes.Keys) { + # Only add if not already present (core properties take precedence) + if (-not $flattened.Contains($key)) { + $flattened[$key] = $attributes[$key] + } + } + } + + return [pscustomobject]$flattened +} diff --git a/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 b/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 index 0552b1fa..7e9dede9 100644 --- a/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 +++ b/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 @@ -124,6 +124,51 @@ Describe 'New-IdlePlan - ContextResolvers' { $plan.Request.Context.Identity.Profile | Should -Not -BeNullOrEmpty $plan.Request.Context.Identity.Profile.IdentityKey | Should -Be 'user1' } + + It 'IdLE.Identity.Read resolver flattens Attributes to top-level properties' { + $wfPath = Join-Path $script:FixturesPath 'resolver-identity-read.psd1' + + $req = New-IdleTestRequest -LifecycleEvent 'Joiner' -IdentityKeys @{ Id = 'user1' } + + $provider = New-IdleMockIdentityProvider -InitialStore @{ + 'user1' = @{ + IdentityKey = 'user1' + Enabled = $true + Attributes = @{ + DisplayName = 'User One' + Department = 'IT' + EmailAddress = 'user1@example.com' + UserPrincipalName = 'user1@example.com' + } + Entitlements = @() + } + } + + $providers = @{ + Identity = $provider + StepRegistry = @{ 'IdLE.Step.EmitEvent' = 'Invoke-IdleContextResolverTestNoopStep' } + } + + $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers + + $plan | Should -Not -BeNullOrEmpty + $profile = $plan.Request.Context.Identity.Profile + + # Core properties should be present + $profile.IdentityKey | Should -Be 'user1' + $profile.Enabled | Should -Be $true + + # Attributes hashtable should be preserved for backwards compatibility + $profile.Attributes | Should -Not -BeNullOrEmpty + $profile.Attributes | Should -BeOfType [hashtable] + $profile.Attributes.DisplayName | Should -Be 'User One' + + # Attributes should be flattened to top level for direct access + $profile.DisplayName | Should -Be 'User One' + $profile.Department | Should -Be 'IT' + $profile.EmailAddress | Should -Be 'user1@example.com' + $profile.UserPrincipalName | Should -Be 'user1@example.com' + } } Context 'To is not a supported key (output path is predefined per capability)' { @@ -280,6 +325,65 @@ Describe 'New-IdlePlan - ContextResolvers' { $entitlements.Count | Should -Be 1 $entitlements[0].Id | Should -Be 'tmpl-grp' } + + It 'resolves templates using flattened Identity.Profile attributes' { + # Create a workflow with a step that uses template substitution with Identity.Profile attributes + $wfContent = @' +@{ + Name = 'Identity Profile Template Test' + LifecycleEvent = 'Joiner' + ContextResolvers = @( + @{ + Capability = 'IdLE.Identity.Read' + With = @{ + IdentityKey = '{{Request.IdentityKeys.Id}}' + Provider = 'Identity' + } + } + ) + Steps = @( + @{ + Name = 'TestStep' + Type = 'IdLE.Step.EmitEvent' + With = @{ + Message = 'User: {{Request.Context.Identity.Profile.DisplayName}}, Email: {{Request.Context.Identity.Profile.EmailAddress}}' + Department = '{{Request.Context.Identity.Profile.Department}}' + } + } + ) +} +'@ + $tempWfPath = Join-Path $TestDrive 'wf-identity-profile-template.psd1' + Set-Content -Path $tempWfPath -Value $wfContent + + $req = New-IdleTestRequest -LifecycleEvent 'Joiner' -IdentityKeys @{ Id = 'user1' } + + $provider = New-IdleMockIdentityProvider -InitialStore @{ + 'user1' = @{ + IdentityKey = 'user1' + Enabled = $true + Attributes = @{ + DisplayName = 'John Doe' + EmailAddress = 'john.doe@example.com' + Department = 'Engineering' + } + Entitlements = @() + } + } + + $providers = @{ + Identity = $provider + StepRegistry = @{ 'IdLE.Step.EmitEvent' = 'Invoke-IdleContextResolverTestNoopStep' } + } + + $plan = New-IdlePlan -WorkflowPath $tempWfPath -Request $req -Providers $providers + + $plan | Should -Not -BeNullOrEmpty + $plan.Steps[0].Status | Should -Be 'Planned' + # Verify templates were resolved using flattened attributes + $plan.Steps[0].With.Message | Should -Be 'User: John Doe, Email: john.doe@example.com' + $plan.Steps[0].With.Department | Should -Be 'Engineering' + } } Context 'Auth session threading' { From e7cd3c573e9c8b38d42b8d1f106352d3ae1a9736 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:40:53 +0000 Subject: [PATCH 03/16] Add edge case tests and verbose warning for attribute conflicts - Added test for null Attributes handling - Added test for empty Attributes hashtable - Added test for conflicting attribute names (core properties take precedence) - Added Write-Verbose warning when attributes conflict with core property names - All 20 ContextResolver tests pass Co-authored-by: ntt-matthias-fleschuetz <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> --- .../Private/Invoke-IdleContextResolvers.ps1 | 5 + .../New-IdlePlan.ContextResolvers.Tests.ps1 | 108 ++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 b/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 index 4cbf851c..06b1bdfd 100644 --- a/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 +++ b/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 @@ -476,6 +476,11 @@ function ConvertTo-IdleFlattenedIdentity { if (-not $flattened.Contains($key)) { $flattened[$key] = $attributes[$key] } + else { + # Warn if an attribute key conflicts with a core property name + # This helps users understand why an attribute might not be accessible at top level + Write-Verbose "Identity attribute '$key' conflicts with a core property name and will not be promoted to top level. Access via Attributes.$key instead." + } } } diff --git a/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 b/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 index 7e9dede9..9fe5a39c 100644 --- a/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 +++ b/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 @@ -169,6 +169,114 @@ Describe 'New-IdlePlan - ContextResolvers' { $profile.EmailAddress | Should -Be 'user1@example.com' $profile.UserPrincipalName | Should -Be 'user1@example.com' } + + It 'IdLE.Identity.Read resolver handles null Attributes gracefully' { + $wfPath = Join-Path $script:FixturesPath 'resolver-identity-read.psd1' + + $req = New-IdleTestRequest -LifecycleEvent 'Joiner' -IdentityKeys @{ Id = 'user1' } + + $provider = New-IdleMockIdentityProvider -InitialStore @{ + 'user1' = @{ + IdentityKey = 'user1' + Enabled = $true + Attributes = $null + Entitlements = @() + } + } + + $providers = @{ + Identity = $provider + StepRegistry = @{ 'IdLE.Step.EmitEvent' = 'Invoke-IdleContextResolverTestNoopStep' } + } + + $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers + + $plan | Should -Not -BeNullOrEmpty + $profile = $plan.Request.Context.Identity.Profile + + # Core properties should be present + $profile.IdentityKey | Should -Be 'user1' + $profile.Enabled | Should -Be $true + + # Attributes should be null (not an empty hashtable) + $profile.Attributes | Should -BeNullOrEmpty + } + + It 'IdLE.Identity.Read resolver handles empty Attributes hashtable' { + $wfPath = Join-Path $script:FixturesPath 'resolver-identity-read.psd1' + + $req = New-IdleTestRequest -LifecycleEvent 'Joiner' -IdentityKeys @{ Id = 'user1' } + + $provider = New-IdleMockIdentityProvider -InitialStore @{ + 'user1' = @{ + IdentityKey = 'user1' + Enabled = $true + Attributes = @{} + Entitlements = @() + } + } + + $providers = @{ + Identity = $provider + StepRegistry = @{ 'IdLE.Step.EmitEvent' = 'Invoke-IdleContextResolverTestNoopStep' } + } + + $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers + + $plan | Should -Not -BeNullOrEmpty + $profile = $plan.Request.Context.Identity.Profile + + # Core properties should be present + $profile.IdentityKey | Should -Be 'user1' + $profile.Enabled | Should -Be $true + + # Attributes should be an empty hashtable (not null) + $profile.Attributes | Should -BeOfType [hashtable] + $profile.Attributes.Count | Should -Be 0 + } + + It 'IdLE.Identity.Read resolver does not overwrite core properties with conflicting attributes' { + $wfPath = Join-Path $script:FixturesPath 'resolver-identity-read.psd1' + + $req = New-IdleTestRequest -LifecycleEvent 'Joiner' -IdentityKeys @{ Id = 'user1' } + + $provider = New-IdleMockIdentityProvider -InitialStore @{ + 'user1' = @{ + IdentityKey = 'user1' + Enabled = $true + Attributes = @{ + IdentityKey = 'conflicting-value' # This conflicts with core property + Enabled = $false # This also conflicts + DisplayName = 'User One' + } + Entitlements = @() + } + } + + $providers = @{ + Identity = $provider + StepRegistry = @{ 'IdLE.Step.EmitEvent' = 'Invoke-IdleContextResolverTestNoopStep' } + } + + $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers + + $plan | Should -Not -BeNullOrEmpty + $profile = $plan.Request.Context.Identity.Profile + + # Core IdentityKey should NOT be overwritten by conflicting attribute + $profile.IdentityKey | Should -Be 'user1' + + # Core Enabled should NOT be overwritten by conflicting attribute + $profile.Enabled | Should -Be $true + + # DisplayName should be flattened (no conflict) + $profile.DisplayName | Should -Be 'User One' + + # Conflicting attributes should still be accessible via Attributes hashtable + $profile.Attributes.IdentityKey | Should -Be 'conflicting-value' + $profile.Attributes.Enabled | Should -Be $false + $profile.Attributes.DisplayName | Should -Be 'User One' + } } Context 'To is not a supported key (output path is predefined per capability)' { From 6e6b5f329182874bb97066c7f63811490704fa38 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:47:33 +0000 Subject: [PATCH 04/16] Improve documentation for reserved property names in flattening - Added comment documenting reserved properties (IdentityKey, Enabled, Attributes) - Clarified behavior when attribute names conflict with core properties - Enhanced function description to explain conflict resolution Co-authored-by: ntt-matthias-fleschuetz <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> --- .../Private/Invoke-IdleContextResolvers.ps1 | 8 +- testResults.xml | 81 +++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 testResults.xml diff --git a/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 b/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 index 06b1bdfd..fd2eeecb 100644 --- a/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 +++ b/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 @@ -425,6 +425,10 @@ function ConvertTo-IdleFlattenedIdentity { - All properties from the Attributes hashtable are promoted to top-level properties - The original Attributes hashtable is preserved for backwards compatibility + Reserved property names (IdentityKey, Enabled, Attributes) will not be overwritten + if they appear as keys in the Attributes hashtable. If a conflict occurs, a verbose + warning is emitted and the attribute remains accessible only via Attributes.PropertyName. + This allows users to access Request.Context.Identity.Profile.DisplayName instead of Request.Context.Identity.Profile.Attributes.DisplayName. @@ -469,7 +473,9 @@ function ConvertTo-IdleFlattenedIdentity { Attributes = $attributes } - # Promote all attribute keys to top level + # Promote all attribute keys to top level. + # Reserved property names (IdentityKey, Enabled, Attributes) will not be overwritten + # if they appear as keys in the Attributes hashtable. if ($null -ne $attributes -and $attributes -is [System.Collections.IDictionary]) { foreach ($key in $attributes.Keys) { # Only add if not already present (core properties take precedence) diff --git a/testResults.xml b/testResults.xml new file mode 100644 index 00000000..ababb8f2 --- /dev/null +++ b/testResults.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 7e6b18c9a5f9d78ecff562fb5f40bf9043f90b73 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:20:12 +0000 Subject: [PATCH 05/16] Remove testResults.xml and update .gitignore - Remove accidentally committed testResults.xml file - Add testResults.xml (with capital R) to .gitignore - Add *.trx pattern to catch all test result files Co-authored-by: ntt-matthias-fleschuetz <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> --- .gitignore | 2 ++ testResults.xml | 81 ------------------------------------------------- 2 files changed, 2 insertions(+), 81 deletions(-) delete mode 100644 testResults.xml diff --git a/.gitignore b/.gitignore index 8e1bd534..4c4c0dfc 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,8 @@ coverage.* test-results.* *.lcov testresults.xml +testResults.xml +*.trx docs-audit.json # Packages diff --git a/testResults.xml b/testResults.xml deleted file mode 100644 index ababb8f2..00000000 --- a/testResults.xml +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From cb0cc8ed612fc3e6b5c41d6173326ed4f637483a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Flesch=C3=BCtz?= <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:42:43 +0100 Subject: [PATCH 06/16] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 b/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 index 9fe5a39c..2e6db68e 100644 --- a/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 +++ b/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 @@ -199,7 +199,8 @@ Describe 'New-IdlePlan - ContextResolvers' { $profile.Enabled | Should -Be $true # Attributes should be null (not an empty hashtable) - $profile.Attributes | Should -BeNullOrEmpty + $profile | Should -HaveProperty 'Attributes' + $profile.Attributes | Should -Be $null } It 'IdLE.Identity.Read resolver handles empty Attributes hashtable' { From e9199e907942abcae166ac7181f4d098f73d1199 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:50:31 +0000 Subject: [PATCH 07/16] Preserve PSTypeName when flattening and copying identity objects - Updated ConvertTo-IdleFlattenedIdentity to clone original object and preserve all properties including PSTypeName - Updated Copy-IdleDataObject to preserve PSTypeName by inserting into TypeNames collection - Fixed test to use PSObject.Properties.Name instead of unsupported Should -HaveProperty - All 344 core tests pass Co-authored-by: ntt-matthias-fleschuetz <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> --- src/IdLE.Core/Private/Copy-IdleDataObject.ps1 | 15 +++- .../Private/Invoke-IdleContextResolvers.ps1 | 69 ++++++++++++------- .../New-IdlePlan.ContextResolvers.Tests.ps1 | 5 +- 3 files changed, 63 insertions(+), 26 deletions(-) diff --git a/src/IdLE.Core/Private/Copy-IdleDataObject.ps1 b/src/IdLE.Core/Private/Copy-IdleDataObject.ps1 index 804fa1b3..2336c828 100644 --- a/src/IdLE.Core/Private/Copy-IdleDataObject.ps1 +++ b/src/IdLE.Core/Private/Copy-IdleDataObject.ps1 @@ -50,10 +50,23 @@ function Copy-IdleDataObject { $props = @($Value.PSObject.Properties | Where-Object MemberType -in @('NoteProperty', 'Property')) if ($null -ne $props -and @($props).Count -gt 0) { $o = [ordered]@{} + foreach ($p in $props) { $o[$p.Name] = Copy-IdleDataObject -Value $p.Value } - return [pscustomobject]$o + + $result = [pscustomobject]$o + + # Preserve PSTypeName(s) from the original object by inserting into TypeNames collection + $typeNames = @($Value.PSObject.TypeNames | Where-Object { + $_ -ne 'System.Management.Automation.PSCustomObject' -and $_ -ne 'System.Object' + }) + if ($typeNames.Count -gt 0) { + # Insert the primary type name at position 0 (before PSCustomObject) + $result.PSObject.TypeNames.Insert(0, $typeNames[0]) + } + + return $result } return $Value diff --git a/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 b/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 index fd2eeecb..9c1806a9 100644 --- a/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 +++ b/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 @@ -449,46 +449,67 @@ function ConvertTo-IdleFlattenedIdentity { return $null } - # Extract core properties - $identityKey = $null - $enabled = $null - $attributes = $null - + # Clone the original identity object to preserve all existing properties and PSTypeName + $cloned = [ordered]@{} + + # Capture PSTypeName(s) from the original object if it's a PSCustomObject + $typeNames = @() + if ($Identity -isnot [System.Collections.IDictionary]) { + foreach ($typeName in $Identity.PSObject.TypeNames) { + if ($typeName -ne 'System.Management.Automation.PSCustomObject' -and $typeName -ne 'System.Object') { + $typeNames += $typeName + } + } + } + + # Copy all properties from the original object if ($Identity -is [System.Collections.IDictionary]) { - $identityKey = $Identity['IdentityKey'] - $enabled = $Identity['Enabled'] - $attributes = $Identity['Attributes'] + foreach ($key in $Identity.Keys) { + if ($key -ne 'PSTypeName') { + $cloned[$key] = $Identity[$key] + } + } + # Also check for PSTypeName in the hashtable + if ($Identity.ContainsKey('PSTypeName')) { + $typeNames += $Identity['PSTypeName'] + } } else { - $props = $Identity.PSObject.Properties - if ($props.Name -contains 'IdentityKey') { $identityKey = $Identity.IdentityKey } - if ($props.Name -contains 'Enabled') { $enabled = $Identity.Enabled } - if ($props.Name -contains 'Attributes') { $attributes = $Identity.Attributes } + foreach ($prop in $Identity.PSObject.Properties) { + $cloned[$prop.Name] = $prop.Value + } } - - # Build flattened object - $flattened = [ordered]@{ - IdentityKey = $identityKey - Enabled = $enabled - Attributes = $attributes + + # Add PSTypeName to the cloned hashtable if we captured any + if ($typeNames.Count -gt 0) { + $cloned['PSTypeName'] = $typeNames[0] # Primary type name } + + # Convert to PSCustomObject + $flattened = [pscustomobject]$cloned # Promote all attribute keys to top level. # Reserved property names (IdentityKey, Enabled, Attributes) will not be overwritten # if they appear as keys in the Attributes hashtable. + $attributes = $null + if ($flattened.PSObject.Properties.Name -contains 'Attributes') { + $attributes = $flattened.Attributes + } + if ($null -ne $attributes -and $attributes -is [System.Collections.IDictionary]) { + $reservedNames = @('IdentityKey', 'Enabled', 'Attributes') foreach ($key in $attributes.Keys) { - # Only add if not already present (core properties take precedence) - if (-not $flattened.Contains($key)) { - $flattened[$key] = $attributes[$key] + # Only add if not already present (existing properties take precedence) + if ($flattened.PSObject.Properties.Name -notcontains $key) { + $flattened | Add-Member -MemberType NoteProperty -Name $key -Value $attributes[$key] -Force } - else { - # Warn if an attribute key conflicts with a core property name + elseif ($reservedNames -contains $key) { + # Warn if an attribute key conflicts with a reserved property name # This helps users understand why an attribute might not be accessible at top level Write-Verbose "Identity attribute '$key' conflicts with a core property name and will not be promoted to top level. Access via Attributes.$key instead." } } } - return [pscustomobject]$flattened + return $flattened } diff --git a/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 b/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 index 2e6db68e..7b70557d 100644 --- a/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 +++ b/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 @@ -168,6 +168,9 @@ Describe 'New-IdlePlan - ContextResolvers' { $profile.Department | Should -Be 'IT' $profile.EmailAddress | Should -Be 'user1@example.com' $profile.UserPrincipalName | Should -Be 'user1@example.com' + + # PSTypeName should be preserved from the original identity object + $profile.PSObject.TypeNames | Should -Contain 'IdLE.Identity' } It 'IdLE.Identity.Read resolver handles null Attributes gracefully' { @@ -199,7 +202,7 @@ Describe 'New-IdlePlan - ContextResolvers' { $profile.Enabled | Should -Be $true # Attributes should be null (not an empty hashtable) - $profile | Should -HaveProperty 'Attributes' + $profile.PSObject.Properties.Name | Should -Contain 'Attributes' $profile.Attributes | Should -Be $null } From 026b580e2d8922876ea0247c5f2155a47653de9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:45:45 +0000 Subject: [PATCH 08/16] Update context-resolver documentation for attribute flattening - Added comprehensive "Identity Profile Attribute Flattening" section to context-resolver.md - Documented direct access pattern, backwards compatibility, and reserved property names - Updated capabilities.md with note about automatic flattening behavior - Updated all provider docs (AD, EntraID, Mock) with flattening notes and cross-references - Documents PSTypeName preservation and conflict resolution behavior Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/reference/capabilities.md | 2 + docs/reference/providers/provider-ad.md | 2 + docs/reference/providers/provider-entraID.md | 2 + docs/reference/providers/provider-mock.md | 2 + docs/use/workflows/context-resolver.md | 63 ++++++++++++++++++++ 5 files changed, 71 insertions(+) diff --git a/docs/reference/capabilities.md b/docs/reference/capabilities.md index 976c7f1e..ea86666b 100644 --- a/docs/reference/capabilities.md +++ b/docs/reference/capabilities.md @@ -145,6 +145,8 @@ Each capability writes to a **predefined, fixed path** under `Request.Context`. > **Note**: `IdLE.Entitlement.List` writes an array of entitlement objects, each with properties: `Kind` (string), `Id` (string), and optionally `DisplayName` (string). To reference entitlement Ids in Conditions, use `Request.Context.Identity.Entitlements.Id`. See [Conditions - Member-Access Enumeration](../use/workflows/conditions.md#member-access-enumeration). +> **Note**: `IdLE.Identity.Read` automatically flattens identity attributes to the top level of `Request.Context.Identity.Profile`. You can access attributes directly (e.g., `Request.Context.Identity.Profile.DisplayName`) instead of via the nested path (e.g., `Request.Context.Identity.Profile.Attributes.DisplayName`). The `Attributes` hashtable is preserved for backwards compatibility. See [Context Resolvers - Identity Profile Attribute Flattening](../use/workflows/context-resolver.md#identity-profile-attribute-flattening) for details. + ### Example ```powershell diff --git a/docs/reference/providers/provider-ad.md b/docs/reference/providers/provider-ad.md index 0fdfef4f..4121447a 100644 --- a/docs/reference/providers/provider-ad.md +++ b/docs/reference/providers/provider-ad.md @@ -104,6 +104,8 @@ Top-level properties: | `Enabled` | `bool` | Derived from AD user `Enabled`. | | `Attributes` | `hashtable` | Key/value bag; keys are strings; values are typically `string`. | +> **Note**: Identity attributes are automatically flattened to the top level of `Request.Context.Identity.Profile`. You can access attributes directly (e.g., `Profile.DisplayName`) instead of via the nested path (`Profile.Attributes.DisplayName`). See [Context Resolvers - Identity Profile Attribute Flattening](../../use/workflows/context-resolver.md#identity-profile-attribute-flattening). + `Attributes` keys populated by this provider (when present on the AD user object): | Attribute key | Type | diff --git a/docs/reference/providers/provider-entraID.md b/docs/reference/providers/provider-entraID.md index da8a984e..410c3524 100644 --- a/docs/reference/providers/provider-entraID.md +++ b/docs/reference/providers/provider-entraID.md @@ -104,6 +104,8 @@ Top-level properties: | `Enabled` | `bool` | Derived from Entra user `accountEnabled`. | | `Attributes` | `hashtable` | Key/value bag; keys are strings; values are typically `string`. | +> **Note**: Identity attributes are automatically flattened to the top level of `Request.Context.Identity.Profile`. You can access attributes directly (e.g., `Profile.DisplayName`) instead of via the nested path (`Profile.Attributes.DisplayName`). See [Context Resolvers - Identity Profile Attribute Flattening](../../use/workflows/context-resolver.md#identity-profile-attribute-flattening). + `Attributes` keys populated by this provider (when present on the user object): | Attribute key | Type | Source (Graph field) | diff --git a/docs/reference/providers/provider-mock.md b/docs/reference/providers/provider-mock.md index 92df852c..ebe70667 100644 --- a/docs/reference/providers/provider-mock.md +++ b/docs/reference/providers/provider-mock.md @@ -79,6 +79,8 @@ Top-level properties: | `Enabled` | `bool` | Stored boolean value (defaults to `$true` when created on demand). | | `Attributes` | `hashtable` | Free-form key/value bag stored in the mock provider store. | +> **Note**: Identity attributes are automatically flattened to the top level of `Request.Context.Identity.Profile`. You can access attributes directly (e.g., `Profile.DisplayName`) instead of via the nested path (`Profile.Attributes.DisplayName`). See [Context Resolvers - Identity Profile Attribute Flattening](../../use/workflows/context-resolver.md#identity-profile-attribute-flattening). + Mock-specific behavior: - Missing identities are created **on-demand** on first `GetIdentity` call (planning-time resolvers may therefore “create” a record in the in-memory store). - `Attributes` is whatever your tests/demos put into the store (commonly `string` values). diff --git a/docs/use/workflows/context-resolver.md b/docs/use/workflows/context-resolver.md index aa020be8..ec5d3499 100644 --- a/docs/use/workflows/context-resolver.md +++ b/docs/use/workflows/context-resolver.md @@ -98,6 +98,69 @@ Output paths are predefined and cannot be changed. --- +## Identity Profile Attribute Flattening + +When using `IdLE.Identity.Read`, the identity object returned by the provider contains an `Attributes` hashtable with properties like `DisplayName`, `EmailAddress`, `Department`, etc. + +**IdLE automatically flattens these attributes** to the top level of `Request.Context.Identity.Profile` for convenient access in templates and conditions. + +### Direct Access Pattern + +You can access identity attributes directly without nested `.Attributes.` path: + +```powershell +# ✅ Direct access (recommended) +'{{Request.Context.Identity.Profile.DisplayName}}' +'{{Request.Context.Identity.Profile.EmailAddress}}' +'{{Request.Context.Identity.Profile.Department}}' +``` + +### Backwards Compatibility + +The original `Attributes` hashtable is preserved, so legacy workflows continue to work: + +```powershell +# ✅ Also works (backwards compatible) +'{{Request.Context.Identity.Profile.Attributes.DisplayName}}' +``` + +### Structure Example + +After resolution, the profile object contains: + +```powershell +Request.Context.Identity.Profile = @{ + PSTypeName = 'IdLE.Identity' # Preserved from provider + IdentityKey = 'user123' # Core property + Enabled = $true # Core property + Attributes = @{ ... } # Original hashtable (backwards compat) + DisplayName = 'Jane Doe' # Flattened from Attributes + EmailAddress = 'jane.doe@example.com' # Flattened from Attributes + Department = 'Engineering' # Flattened from Attributes + # ... all other attribute keys promoted to top level +} +``` + +### Reserved Property Names + +The following property names are **reserved** and will not be overwritten by attribute keys: + +- `IdentityKey` +- `Enabled` +- `Attributes` +- `PSTypeName` (internal type metadata) + +If an attribute key conflicts with a reserved name, a verbose warning is emitted and the attribute remains accessible only via `Attributes.PropertyName`: + +```powershell +# If provider returns Attributes = @{ IdentityKey = 'some-value' } +# ⚠️ Warning emitted, attribute not promoted +# ✅ Access via: Request.Context.Identity.Profile.Attributes.IdentityKey +# ❌ Request.Context.Identity.Profile.IdentityKey returns the actual identity key +``` + +--- + ## Common Patterns ### Resolve once, use everywhere From bd2ae4665ea4f2ab54367fa7aa934a8470ce8aaf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:56:56 +0000 Subject: [PATCH 09/16] Add provider selection and authentication section to context-resolver docs - Added comprehensive "Provider Selection and Authentication" section - Documented auto-selection vs explicit provider specification - Explained AuthSessionBroker, AuthSessionName, and AuthSessionOptions usage - Listed provider-specific identity attributes with cross-references - Centralized provider/auth documentation in context-resolver.md as requested Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/use/workflows/context-resolver.md | 128 +++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/docs/use/workflows/context-resolver.md b/docs/use/workflows/context-resolver.md index ec5d3499..f1a46ce8 100644 --- a/docs/use/workflows/context-resolver.md +++ b/docs/use/workflows/context-resolver.md @@ -98,6 +98,134 @@ Output paths are predefined and cannot be changed. --- +## Provider Selection and Authentication + +### Provider Selection + +Context Resolvers use providers to access external systems for reading identity and entitlement data. + +**Auto-selection (recommended for single provider scenarios):** + +If you have only one provider that advertises the capability, you can omit the `Provider` parameter: + +```powershell +ContextResolvers = @( + @{ + Capability = 'IdLE.Identity.Read' + With = @{ + IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' + # Provider is omitted - IdLE auto-selects the matching provider + } + } +) +``` + +**Explicit provider selection (required for multiple providers):** + +If you have multiple providers that advertise the same capability (e.g., multiple AD forests, AD + Entra ID), you must specify which provider to use: + +```powershell +ContextResolvers = @( + @{ + Capability = 'IdLE.Identity.Read' + With = @{ + IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' + Provider = 'PrimaryAD' # explicit selection required + } + } +) +``` + +If multiple providers match and no explicit `Provider` is specified, planning fails with an ambiguity error. + +### Authentication Sessions + +Providers may require authentication to access external systems. IdLE uses **AuthSessionBroker** to manage authentication sessions. + +**Basic usage (no authentication required):** + +Some providers (like Mock) don't require authentication: + +```powershell +ContextResolvers = @( + @{ + Capability = 'IdLE.Identity.Read' + With = @{ + IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' + Provider = 'Mock' + } + } +) +``` + +**Using named auth sessions:** + +For providers that require authentication, specify an `AuthSessionName` that references a session managed by your `AuthSessionBroker`: + +```powershell +ContextResolvers = @( + @{ + Capability = 'IdLE.Identity.Read' + With = @{ + IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' + Provider = 'PrimaryAD' + AuthSessionName = 'Tier0' # Named session from AuthSessionBroker + } + } +) +``` + +The `AuthSessionBroker` must be configured in your `Providers` parameter when calling `New-IdlePlan`. The broker is responsible for: +- Managing credential/token lifecycle +- Acquiring and caching authentication sessions +- Providing sessions to providers and steps on demand + +**Advanced: AuthSessionOptions** + +Some `AuthSessionBroker` implementations accept additional options via `AuthSessionOptions`: + +```powershell +ContextResolvers = @( + @{ + Capability = 'IdLE.Identity.Read' + With = @{ + IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' + Provider = 'EntraID' + AuthSessionName = 'GraphAPI' + AuthSessionOptions = @{ + Scopes = @('User.Read.All', 'Group.Read.All') + } + } + } +) +``` + +> **Security note**: `AuthSessionOptions` must be data-only (hashtables, strings, numbers, booleans). ScriptBlocks and executable objects are rejected. + +### Provider-Specific Identity Attributes + +Different providers populate different attributes in the `Identity.Profile.Attributes` hashtable. After flattening, these become top-level properties. + +**Active Directory (AD) provider** populates: +- `GivenName`, `Surname`, `DisplayName` +- `Department`, `Title`, `Description` +- `EmailAddress`, `UserPrincipalName`, `sAMAccountName`, `DistinguishedName` + +**Entra ID (EntraID) provider** populates: +- `GivenName`, `Surname`, `DisplayName` +- `UserPrincipalName`, `Mail` +- `Department`, `JobTitle`, `OfficeLocation`, `CompanyName` + +**Mock provider** populates: +- Any attributes you configure in your test/demo scenarios + +For complete provider-specific attribute lists, see the individual provider documentation: +- [Active Directory Provider](../../reference/providers/provider-ad.md#capability-idleidentityread) +- [Entra ID Provider](../../reference/providers/provider-entraID.md#capability-idleidentityread) +- [Mock Provider](../../reference/providers/provider-mock.md#capability-idleidentityread) + +--- + ## Identity Profile Attribute Flattening When using `IdLE.Identity.Read`, the identity object returned by the provider contains an `Attributes` hashtable with properties like `DisplayName`, `EmailAddress`, `Department`, etc. From 560194ec0bec021b0dda03351ca98551979a7949 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 20:12:33 +0000 Subject: [PATCH 10/16] Remove backward compatibility for Attributes hashtable and add precedence documentation - Removed preservation of Attributes hashtable after flattening (no backward compatibility) - Updated ConvertTo-IdleFlattenedIdentity to always remove Attributes property - Updated reserved property names list (removed 'Attributes' from reserved list) - Updated documentation to remove backward compatibility references - Added comprehensive "Multiple Resolvers and Precedence" section documenting: - Sequential execution order of resolvers - How later resolvers overwrite earlier ones for same context path - Using multiple providers with different auth sessions - Best practices for avoiding conflicts - Updated all tests to verify Attributes is removed after flattening - All 20 ContextResolver tests passing Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/use/workflows/context-resolver.md | 129 +++++++++++++++--- .../Private/Invoke-IdleContextResolvers.ps1 | 28 ++-- .../New-IdlePlan.ContextResolvers.Tests.ps1 | 24 ++-- 3 files changed, 133 insertions(+), 48 deletions(-) diff --git a/docs/use/workflows/context-resolver.md b/docs/use/workflows/context-resolver.md index f1a46ce8..c22e6f56 100644 --- a/docs/use/workflows/context-resolver.md +++ b/docs/use/workflows/context-resolver.md @@ -234,24 +234,15 @@ When using `IdLE.Identity.Read`, the identity object returned by the provider co ### Direct Access Pattern -You can access identity attributes directly without nested `.Attributes.` path: +You can access identity attributes directly at the top level: ```powershell -# ✅ Direct access (recommended) +# ✅ Direct access '{{Request.Context.Identity.Profile.DisplayName}}' '{{Request.Context.Identity.Profile.EmailAddress}}' '{{Request.Context.Identity.Profile.Department}}' ``` -### Backwards Compatibility - -The original `Attributes` hashtable is preserved, so legacy workflows continue to work: - -```powershell -# ✅ Also works (backwards compatible) -'{{Request.Context.Identity.Profile.Attributes.DisplayName}}' -``` - ### Structure Example After resolution, the profile object contains: @@ -261,11 +252,10 @@ Request.Context.Identity.Profile = @{ PSTypeName = 'IdLE.Identity' # Preserved from provider IdentityKey = 'user123' # Core property Enabled = $true # Core property - Attributes = @{ ... } # Original hashtable (backwards compat) - DisplayName = 'Jane Doe' # Flattened from Attributes - EmailAddress = 'jane.doe@example.com' # Flattened from Attributes - Department = 'Engineering' # Flattened from Attributes - # ... all other attribute keys promoted to top level + DisplayName = 'Jane Doe' # Flattened from provider Attributes + EmailAddress = 'jane.doe@example.com' # Flattened from provider Attributes + Department = 'Engineering' # Flattened from provider Attributes + # ... all other attributes promoted to top level } ``` @@ -275,18 +265,113 @@ The following property names are **reserved** and will not be overwritten by att - `IdentityKey` - `Enabled` -- `Attributes` - `PSTypeName` (internal type metadata) -If an attribute key conflicts with a reserved name, a verbose warning is emitted and the attribute remains accessible only via `Attributes.PropertyName`: +If an attribute key conflicts with a reserved name, a verbose warning is emitted during planning, and the conflicting attribute is skipped. + +--- + +## Multiple Resolvers and Precedence + +### Execution Order + +Context Resolvers are executed **sequentially in the order they are declared** in the workflow's `ContextResolvers` array: + +```powershell +ContextResolvers = @( + @{ # Executed first + Capability = 'IdLE.Identity.Read' + With = @{ + IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' + Provider = 'PrimaryAD' + } + } + @{ # Executed second + Capability = 'IdLE.Entitlement.List' + With = @{ + IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' + Provider = 'PrimaryAD' + } + } +) +``` + +### Precedence for Overlapping Data + +If multiple resolvers write to the **same context path**, later resolvers **overwrite** earlier ones: + +```powershell +ContextResolvers = @( + @{ + Capability = 'IdLE.Identity.Read' + With = @{ + IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' + Provider = 'PrimaryAD' # Reads from Primary AD + } + } + @{ + Capability = 'IdLE.Identity.Read' + With = @{ + IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' + Provider = 'EntraID' # Overwrites Identity.Profile with Entra ID data + } + } +) +``` + +> **Important**: Both resolvers write to `Request.Context.Identity.Profile`. The **second resolver wins** and its data becomes the final `Identity.Profile` value. + +### Using Multiple Providers with Different Auth Sessions + +You can configure multiple resolvers that use different providers and authentication sessions for different systems: ```powershell -# If provider returns Attributes = @{ IdentityKey = 'some-value' } -# ⚠️ Warning emitted, attribute not promoted -# ✅ Access via: Request.Context.Identity.Profile.Attributes.IdentityKey -# ❌ Request.Context.Identity.Profile.IdentityKey returns the actual identity key +ContextResolvers = @( + @{ + Capability = 'IdLE.Identity.Read' + With = @{ + IdentityKey = '{{Request.IdentityKeys.sAMAccountName}}' + Provider = 'PrimaryAD' + AuthSessionName = 'Tier0-AD' # On-premises AD auth session + } + } + @{ + Capability = 'IdLE.Entitlement.List' + With = @{ + IdentityKey = '{{Request.IdentityKeys.UserPrincipalName}}' + Provider = 'EntraID' + AuthSessionName = 'GraphAPI' # Cloud auth session + } + } +) ``` +Each resolver independently: +- Selects its own `Provider` +- Uses its own `AuthSessionName` (if authentication is required) +- Can pass provider-specific options via `AuthSessionOptions` + +### Avoiding Conflicts + +To avoid unintended overwrites when using multiple providers: + +1. **Use different capabilities** that write to different context paths: + - `IdLE.Identity.Read` → `Request.Context.Identity.Profile` + - `IdLE.Entitlement.List` → `Request.Context.Identity.Entitlements` + +2. **Declare resolvers in intentional order** if you need the later resolver to win: + ```powershell + # Get basic profile from AD first + @{ Capability = 'IdLE.Identity.Read'; With = @{ Provider = 'AD' } } + + # Overwrite with cloud-enriched profile if needed + @{ Capability = 'IdLE.Identity.Read'; With = @{ Provider = 'EntraID' } } + ``` + +3. **Use unique identity keys** appropriate for each provider: + - AD providers often use `sAMAccountName` or `DistinguishedName` + - Entra ID providers use `UserPrincipalName` or `ObjectId` + --- ## Common Patterns diff --git a/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 b/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 index 9c1806a9..ee8231cf 100644 --- a/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 +++ b/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 @@ -344,10 +344,10 @@ function Invoke-IdleResolverCapabilityDispatch { $provider.GetIdentity($identityKey) } - # Flatten the identity object by merging Attributes into the top level. + # Flatten the identity object by promoting Attributes to the top level. # This allows users to access Request.Context.Identity.Profile.DisplayName - # instead of Request.Context.Identity.Profile.Attributes.DisplayName. - # The Attributes hashtable is still preserved as a property for backwards compatibility. + # directly instead of Request.Context.Identity.Profile.Attributes.DisplayName. + # The Attributes hashtable is removed after flattening. return ConvertTo-IdleFlattenedIdentity -Identity $identity } @@ -423,14 +423,14 @@ function ConvertTo-IdleFlattenedIdentity { and creates a new object where: - IdentityKey and Enabled are preserved at the top level - All properties from the Attributes hashtable are promoted to top-level properties - - The original Attributes hashtable is preserved for backwards compatibility + - The original Attributes hashtable is removed after flattening - Reserved property names (IdentityKey, Enabled, Attributes) will not be overwritten + Reserved property names (IdentityKey, Enabled) will not be overwritten if they appear as keys in the Attributes hashtable. If a conflict occurs, a verbose - warning is emitted and the attribute remains accessible only via Attributes.PropertyName. + warning is emitted and the conflicting attribute is skipped. This allows users to access Request.Context.Identity.Profile.DisplayName - instead of Request.Context.Identity.Profile.Attributes.DisplayName. + directly at the top level. .PARAMETER Identity The identity object returned by a provider's GetIdentity method. @@ -489,7 +489,7 @@ function ConvertTo-IdleFlattenedIdentity { $flattened = [pscustomobject]$cloned # Promote all attribute keys to top level. - # Reserved property names (IdentityKey, Enabled, Attributes) will not be overwritten + # Reserved property names (IdentityKey, Enabled) will not be overwritten # if they appear as keys in the Attributes hashtable. $attributes = $null if ($flattened.PSObject.Properties.Name -contains 'Attributes') { @@ -497,7 +497,7 @@ function ConvertTo-IdleFlattenedIdentity { } if ($null -ne $attributes -and $attributes -is [System.Collections.IDictionary]) { - $reservedNames = @('IdentityKey', 'Enabled', 'Attributes') + $reservedNames = @('IdentityKey', 'Enabled') foreach ($key in $attributes.Keys) { # Only add if not already present (existing properties take precedence) if ($flattened.PSObject.Properties.Name -notcontains $key) { @@ -505,11 +505,17 @@ function ConvertTo-IdleFlattenedIdentity { } elseif ($reservedNames -contains $key) { # Warn if an attribute key conflicts with a reserved property name - # This helps users understand why an attribute might not be accessible at top level - Write-Verbose "Identity attribute '$key' conflicts with a core property name and will not be promoted to top level. Access via Attributes.$key instead." + # This helps users understand why an attribute was skipped + Write-Verbose "Identity attribute '$key' conflicts with a core property name and will be skipped during flattening." } } } + + # Always remove the Attributes property after flattening (no backward compatibility) + # This applies whether Attributes was null, empty, or had content + if ($flattened.PSObject.Properties.Name -contains 'Attributes') { + $flattened.PSObject.Properties.Remove('Attributes') + } return $flattened } diff --git a/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 b/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 index 7b70557d..70b8d86f 100644 --- a/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 +++ b/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 @@ -158,17 +158,15 @@ Describe 'New-IdlePlan - ContextResolvers' { $profile.IdentityKey | Should -Be 'user1' $profile.Enabled | Should -Be $true - # Attributes hashtable should be preserved for backwards compatibility - $profile.Attributes | Should -Not -BeNullOrEmpty - $profile.Attributes | Should -BeOfType [hashtable] - $profile.Attributes.DisplayName | Should -Be 'User One' - # Attributes should be flattened to top level for direct access $profile.DisplayName | Should -Be 'User One' $profile.Department | Should -Be 'IT' $profile.EmailAddress | Should -Be 'user1@example.com' $profile.UserPrincipalName | Should -Be 'user1@example.com' + # Attributes hashtable should be removed after flattening + $profile.PSObject.Properties.Name | Should -Not -Contain 'Attributes' + # PSTypeName should be preserved from the original identity object $profile.PSObject.TypeNames | Should -Contain 'IdLE.Identity' } @@ -201,9 +199,8 @@ Describe 'New-IdlePlan - ContextResolvers' { $profile.IdentityKey | Should -Be 'user1' $profile.Enabled | Should -Be $true - # Attributes should be null (not an empty hashtable) - $profile.PSObject.Properties.Name | Should -Contain 'Attributes' - $profile.Attributes | Should -Be $null + # Attributes should be removed (was null, so after flattening there's no Attributes property) + $profile.PSObject.Properties.Name | Should -Not -Contain 'Attributes' } It 'IdLE.Identity.Read resolver handles empty Attributes hashtable' { @@ -234,9 +231,8 @@ Describe 'New-IdlePlan - ContextResolvers' { $profile.IdentityKey | Should -Be 'user1' $profile.Enabled | Should -Be $true - # Attributes should be an empty hashtable (not null) - $profile.Attributes | Should -BeOfType [hashtable] - $profile.Attributes.Count | Should -Be 0 + # Attributes should be removed after flattening (was empty, so nothing to flatten) + $profile.PSObject.Properties.Name | Should -Not -Contain 'Attributes' } It 'IdLE.Identity.Read resolver does not overwrite core properties with conflicting attributes' { @@ -276,10 +272,8 @@ Describe 'New-IdlePlan - ContextResolvers' { # DisplayName should be flattened (no conflict) $profile.DisplayName | Should -Be 'User One' - # Conflicting attributes should still be accessible via Attributes hashtable - $profile.Attributes.IdentityKey | Should -Be 'conflicting-value' - $profile.Attributes.Enabled | Should -Be $false - $profile.Attributes.DisplayName | Should -Be 'User One' + # Attributes hashtable should be removed after flattening + $profile.PSObject.Properties.Name | Should -Not -Contain 'Attributes' } } From 75450944a61c468d7dc87f485b4cdbbd35eeeaeb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 20:13:36 +0000 Subject: [PATCH 11/16] Update provider documentation to clarify no backward compatibility for Attributes - Updated AD, EntraID, and Mock provider docs - Changed "You can access" to "You must access" (direct access only) - Explicitly state that Attributes hashtable is removed after flattening - Removed reference to nested path as an alternative Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/reference/providers/provider-ad.md | 2 +- docs/reference/providers/provider-entraID.md | 2 +- docs/reference/providers/provider-mock.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/reference/providers/provider-ad.md b/docs/reference/providers/provider-ad.md index 4121447a..e4950efa 100644 --- a/docs/reference/providers/provider-ad.md +++ b/docs/reference/providers/provider-ad.md @@ -104,7 +104,7 @@ Top-level properties: | `Enabled` | `bool` | Derived from AD user `Enabled`. | | `Attributes` | `hashtable` | Key/value bag; keys are strings; values are typically `string`. | -> **Note**: Identity attributes are automatically flattened to the top level of `Request.Context.Identity.Profile`. You can access attributes directly (e.g., `Profile.DisplayName`) instead of via the nested path (`Profile.Attributes.DisplayName`). See [Context Resolvers - Identity Profile Attribute Flattening](../../use/workflows/context-resolver.md#identity-profile-attribute-flattening). +> **Note**: Identity attributes are automatically flattened to the top level of `Request.Context.Identity.Profile`. You must access attributes directly (e.g., `Profile.DisplayName`). The `Attributes` hashtable is removed after flattening. See [Context Resolvers - Identity Profile Attribute Flattening](../../use/workflows/context-resolver.md#identity-profile-attribute-flattening). `Attributes` keys populated by this provider (when present on the AD user object): diff --git a/docs/reference/providers/provider-entraID.md b/docs/reference/providers/provider-entraID.md index 410c3524..d2549d85 100644 --- a/docs/reference/providers/provider-entraID.md +++ b/docs/reference/providers/provider-entraID.md @@ -104,7 +104,7 @@ Top-level properties: | `Enabled` | `bool` | Derived from Entra user `accountEnabled`. | | `Attributes` | `hashtable` | Key/value bag; keys are strings; values are typically `string`. | -> **Note**: Identity attributes are automatically flattened to the top level of `Request.Context.Identity.Profile`. You can access attributes directly (e.g., `Profile.DisplayName`) instead of via the nested path (`Profile.Attributes.DisplayName`). See [Context Resolvers - Identity Profile Attribute Flattening](../../use/workflows/context-resolver.md#identity-profile-attribute-flattening). +> **Note**: Identity attributes are automatically flattened to the top level of `Request.Context.Identity.Profile`. You must access attributes directly (e.g., `Profile.DisplayName`). The `Attributes` hashtable is removed after flattening. See [Context Resolvers - Identity Profile Attribute Flattening](../../use/workflows/context-resolver.md#identity-profile-attribute-flattening). `Attributes` keys populated by this provider (when present on the user object): diff --git a/docs/reference/providers/provider-mock.md b/docs/reference/providers/provider-mock.md index ebe70667..10ce5d44 100644 --- a/docs/reference/providers/provider-mock.md +++ b/docs/reference/providers/provider-mock.md @@ -79,7 +79,7 @@ Top-level properties: | `Enabled` | `bool` | Stored boolean value (defaults to `$true` when created on demand). | | `Attributes` | `hashtable` | Free-form key/value bag stored in the mock provider store. | -> **Note**: Identity attributes are automatically flattened to the top level of `Request.Context.Identity.Profile`. You can access attributes directly (e.g., `Profile.DisplayName`) instead of via the nested path (`Profile.Attributes.DisplayName`). See [Context Resolvers - Identity Profile Attribute Flattening](../../use/workflows/context-resolver.md#identity-profile-attribute-flattening). +> **Note**: Identity attributes are automatically flattened to the top level of `Request.Context.Identity.Profile`. You must access attributes directly (e.g., `Profile.DisplayName`). The `Attributes` hashtable is removed after flattening. See [Context Resolvers - Identity Profile Attribute Flattening](../../use/workflows/context-resolver.md#identity-profile-attribute-flattening). Mock-specific behavior: - Missing identities are created **on-demand** on first `GetIdentity` call (planning-time resolvers may therefore “create” a record in the in-memory store). From 8a8040d9b07e92e275135e1b661cc0bb273d40bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 20:14:18 +0000 Subject: [PATCH 12/16] Improve documentation clarity for reserved property names - Separated core reserved properties (IdentityKey, Enabled) from internal metadata (PSTypeName) - Added concrete example showing what happens with conflicting attribute names - Clarified that conflicting attribute values are lost during flattening - Made it explicit which properties prevent attribute promotion Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/use/workflows/context-resolver.md | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/docs/use/workflows/context-resolver.md b/docs/use/workflows/context-resolver.md index c22e6f56..1360d990 100644 --- a/docs/use/workflows/context-resolver.md +++ b/docs/use/workflows/context-resolver.md @@ -261,13 +261,24 @@ Request.Context.Identity.Profile = @{ ### Reserved Property Names -The following property names are **reserved** and will not be overwritten by attribute keys: +The following core property names are **reserved** and will not be overwritten by attribute keys during flattening: -- `IdentityKey` -- `Enabled` -- `PSTypeName` (internal type metadata) +- `IdentityKey` - The identity key used by the workflow +- `Enabled` - The identity enabled status -If an attribute key conflicts with a reserved name, a verbose warning is emitted during planning, and the conflicting attribute is skipped. +The following is preserved as internal type metadata: + +- `PSTypeName` - Type name metadata (e.g., 'IdLE.Identity') + +If an attribute key conflicts with a reserved core property name, a verbose warning is emitted during planning, and the conflicting attribute is skipped: + +```powershell +# Example: Provider returns Attributes = @{ IdentityKey = 'conflicting-value'; Enabled = $false } +# ⚠️ Verbose warnings emitted for both IdentityKey and Enabled +# ✅ Profile.IdentityKey returns the actual identity key (core property wins) +# ✅ Profile.Enabled returns the actual enabled status (core property wins) +# ❌ Conflicting attribute values are lost during flattening +``` --- From c78359c77759043f17b27078e14feb6e3a508cc9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:00:55 +0000 Subject: [PATCH 13/16] Simplify context-resolver documentation with comprehensive inline example - Replaced multiple similar examples with one comprehensive example featuring inline comments - Consolidated provider selection and authentication sections - Simplified identity profile flattening explanation - Condensed multiple resolvers and precedence section - Merged troubleshooting and common patterns - Reduced documentation from 509 to 315 lines (38% reduction) - All 20 ContextResolver tests passing - Awaiting clarification on per-provider/per-authsession context path feature request Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/use/workflows/context-resolver.md | 459 +++++++------------------ 1 file changed, 133 insertions(+), 326 deletions(-) diff --git a/docs/use/workflows/context-resolver.md b/docs/use/workflows/context-resolver.md index 1360d990..faf2b584 100644 --- a/docs/use/workflows/context-resolver.md +++ b/docs/use/workflows/context-resolver.md @@ -28,73 +28,97 @@ to rely on data that was resolved once during planning. --- -## Full Example - -A resolver entry is defined at workflow root level: - +## Complete Example with Inline Comments ```powershell @{ - Name = 'Joiner - Context Resolver Demo' - LifecycleEvent = 'Joiner' + Name = 'Offboarding - Context Resolver Example' + LifecycleEvent = 'Leaver' + # ContextResolvers populate Request.Context.* during planning + # They execute sequentially in declaration order BEFORE step conditions are evaluated ContextResolvers = @( + + # Resolver 1: Read identity profile from Active Directory @{ - Capability = 'IdLE.Identity.Read' + Capability = 'IdLE.Identity.Read' # REQUIRED - Must be from allow-list With = @{ - IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' - Provider = 'Identity' # optional; auto-selected if omitted - AuthSessionName = 'Tier0' # optional; requires AuthSessionBroker in Providers + IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' # REQUIRED - Template substitution supported + Provider = 'PrimaryAD' # OPTIONAL - Auto-selected if only one provider matches + AuthSessionName = 'Tier0-AD' # OPTIONAL - Named auth session from AuthSessionBroker + # AuthSessionOptions = @{ Scopes = @('...') } # OPTIONAL - Provider-specific auth options (must be data-only) } + # Output: Request.Context.Identity.Profile (fixed path, cannot be changed) + # After flattening: Profile.DisplayName, Profile.EmailAddress, etc. are accessible directly } + # Resolver 2: List entitlements for the identity @{ Capability = 'IdLE.Entitlement.List' With = @{ IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' + Provider = 'PrimaryAD' # OPTIONAL + # AuthSessionName can be different per resolver if needed } + # Output: Request.Context.Identity.Entitlements (fixed path) } ) Steps = @( - + # Step conditions can reference resolved context data @{ - Name = 'Disable only if identity exists' + Name = 'Disable account only if identity exists in AD' Type = 'IdLE.Step.DisableIdentity' - Condition = @{ - Exists = 'Request.Context.Identity.Profile' + Exists = 'Request.Context.Identity.Profile' # Check if identity was found } } + # Template substitution can use flattened attributes @{ - Name = 'Emit audit event' + Name = 'Send notification email' Type = 'IdLE.Step.EmitEvent' - With = @{ - Message = 'Disabled identity {{Request.Context.Identity.Profile.DisplayName}}' + # Direct access to flattened attributes (no .Attributes. needed) + Message = 'Disabled account for {{Request.Context.Identity.Profile.DisplayName}} ({{Request.Context.Identity.Profile.EmailAddress}})' + } + } + + # Preconditions can also reference context (evaluated at execution time) + @{ + Name = 'Revoke admin entitlements' + Type = 'IdLE.Step.PruneEntitlements' + Precondition = @{ + Exists = 'Request.Context.Identity.Entitlements' # Ensure entitlements were resolved } } ) } ``` -### Keys +### Resolver Configuration Keys + +| Key | Type | Required | Description | +|-----|------|----------|-------------| +| `Capability` | `string` | **Yes** | Read-only capability from allow-list: `IdLE.Identity.Read`, `IdLE.Entitlement.List` | +| `With` | `hashtable` | **Yes**¹ | Inputs required by the capability. Template substitution supported. | +| `With.IdentityKey` | `string` | **Yes** | Identity key for lookup. Required by both capabilities. | +| `With.Provider` | `string` | No | Provider alias. Auto-selected if omitted and only one provider matches. Required if multiple providers advertise the capability. | +| `With.AuthSessionName` | `string` | No | Named auth session to acquire via `AuthSessionBroker`. | +| `With.AuthSessionOptions` | `hashtable` | No | Provider-specific auth options. Must be data-only (no ScriptBlocks). | -- `Capability` (required) - A permitted read-only capability. +¹ Technically optional, but required in practice for all current capabilities. -- `With` (hashtable, optional — required in practice, as capabilities need at least `IdentityKey`) - Inputs required by the capability. Template substitution is supported. +### Output Paths (Predefined) - | `With` key | Type | Required | Description | - |---|---|---|---| - | `IdentityKey` | `string` | Per capability | Required by `IdLE.Identity.Read` and `IdLE.Entitlement.List`. | - | `Provider` | `string` | No | Provider alias. If omitted, IdLE auto-selects a provider advertising the capability. Ambiguity (multiple providers matching) is a fail-fast error. | - | `AuthSessionName` | `string` | No | Named auth session to acquire via `AuthSessionBroker`. Requires an `AuthSessionBroker` entry in `Providers`. | - | `AuthSessionOptions` | `hashtable` | No | Options passed to `AuthSessionBroker.AcquireAuthSession`. Must be a hashtable. ScriptBlocks are rejected. | +Each capability writes to a fixed path under `Request.Context`: -Output paths are predefined and cannot be changed. +| Capability | Output Path | Description | +|------------|-------------|-------------| +| `IdLE.Identity.Read` | `Identity.Profile` | Identity object with attributes flattened to top level | +| `IdLE.Entitlement.List` | `Identity.Entitlements` | Array of entitlement objects | + +> **Important**: Output paths cannot be customized. If multiple resolvers use the same capability, later resolvers overwrite earlier ones (last-writer-wins). --- @@ -102,404 +126,187 @@ Output paths are predefined and cannot be changed. ### Provider Selection -Context Resolvers use providers to access external systems for reading identity and entitlement data. - -**Auto-selection (recommended for single provider scenarios):** - -If you have only one provider that advertises the capability, you can omit the `Provider` parameter: - -```powershell -ContextResolvers = @( - @{ - Capability = 'IdLE.Identity.Read' - With = @{ - IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' - # Provider is omitted - IdLE auto-selects the matching provider - } - } -) -``` - -**Explicit provider selection (required for multiple providers):** - -If you have multiple providers that advertise the same capability (e.g., multiple AD forests, AD + Entra ID), you must specify which provider to use: +**Auto-selection:** If only one provider advertises the capability, omit `Provider`: ```powershell -ContextResolvers = @( - @{ - Capability = 'IdLE.Identity.Read' - With = @{ - IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' - Provider = 'PrimaryAD' # explicit selection required - } - } -) +With = @{ + IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' + # Provider omitted - auto-selected +} ``` -If multiple providers match and no explicit `Provider` is specified, planning fails with an ambiguity error. - -### Authentication Sessions - -Providers may require authentication to access external systems. IdLE uses **AuthSessionBroker** to manage authentication sessions. - -**Basic usage (no authentication required):** - -Some providers (like Mock) don't require authentication: +**Explicit selection:** Required when multiple providers match: ```powershell -ContextResolvers = @( - @{ - Capability = 'IdLE.Identity.Read' - With = @{ - IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' - Provider = 'Mock' - } - } -) +With = @{ + IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' + Provider = 'PrimaryAD' # Disambiguates between PrimaryAD and EntraID +} ``` -**Using named auth sessions:** +### Authentication -For providers that require authentication, specify an `AuthSessionName` that references a session managed by your `AuthSessionBroker`: +Some providers require authentication via `AuthSessionBroker`: ```powershell -ContextResolvers = @( - @{ - Capability = 'IdLE.Identity.Read' - With = @{ - IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' - Provider = 'PrimaryAD' - AuthSessionName = 'Tier0' # Named session from AuthSessionBroker - } - } -) +With = @{ + IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' + Provider = 'PrimaryAD' + AuthSessionName = 'Tier0' # Named session from AuthSessionBroker +} ``` -The `AuthSessionBroker` must be configured in your `Providers` parameter when calling `New-IdlePlan`. The broker is responsible for: -- Managing credential/token lifecycle -- Acquiring and caching authentication sessions -- Providing sessions to providers and steps on demand - -**Advanced: AuthSessionOptions** - -Some `AuthSessionBroker` implementations accept additional options via `AuthSessionOptions`: +Advanced auth options (provider-specific): ```powershell -ContextResolvers = @( - @{ - Capability = 'IdLE.Identity.Read' - With = @{ - IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' - Provider = 'EntraID' - AuthSessionName = 'GraphAPI' - AuthSessionOptions = @{ - Scopes = @('User.Read.All', 'Group.Read.All') - } - } - } -) +AuthSessionOptions = @{ + Scopes = @('User.Read.All', 'Group.Read.All') +} ``` -> **Security note**: `AuthSessionOptions` must be data-only (hashtables, strings, numbers, booleans). ScriptBlocks and executable objects are rejected. - -### Provider-Specific Identity Attributes +> **Security**: `AuthSessionOptions` must be data-only (no ScriptBlocks). -Different providers populate different attributes in the `Identity.Profile.Attributes` hashtable. After flattening, these become top-level properties. +### Provider-Specific Attributes -**Active Directory (AD) provider** populates: -- `GivenName`, `Surname`, `DisplayName` -- `Department`, `Title`, `Description` -- `EmailAddress`, `UserPrincipalName`, `sAMAccountName`, `DistinguishedName` +Different providers populate different attributes. After flattening, attributes become top-level properties: -**Entra ID (EntraID) provider** populates: -- `GivenName`, `Surname`, `DisplayName` -- `UserPrincipalName`, `Mail` -- `Department`, `JobTitle`, `OfficeLocation`, `CompanyName` +- **AD**: `GivenName`, `Surname`, `DisplayName`, `Department`, `Title`, `EmailAddress`, `UserPrincipalName`, `sAMAccountName`, `DistinguishedName` +- **Entra ID**: `GivenName`, `Surname`, `DisplayName`, `UserPrincipalName`, `Mail`, `Department`, `JobTitle`, `OfficeLocation` +- **Mock**: Configurable test attributes -**Mock provider** populates: -- Any attributes you configure in your test/demo scenarios - -For complete provider-specific attribute lists, see the individual provider documentation: -- [Active Directory Provider](../../reference/providers/provider-ad.md#capability-idleidentityread) -- [Entra ID Provider](../../reference/providers/provider-entraID.md#capability-idleidentityread) -- [Mock Provider](../../reference/providers/provider-mock.md#capability-idleidentityread) +See provider docs for complete lists: [AD](../../reference/providers/provider-ad.md#capability-idleidentityread), [Entra ID](../../reference/providers/provider-entraID.md#capability-idleidentityread), [Mock](../../reference/providers/provider-mock.md#capability-idleidentityread) --- ## Identity Profile Attribute Flattening -When using `IdLE.Identity.Read`, the identity object returned by the provider contains an `Attributes` hashtable with properties like `DisplayName`, `EmailAddress`, `Department`, etc. - -**IdLE automatically flattens these attributes** to the top level of `Request.Context.Identity.Profile` for convenient access in templates and conditions. - -### Direct Access Pattern - -You can access identity attributes directly at the top level: +Provider identity objects contain an `Attributes` hashtable. **IdLE automatically flattens these to top-level properties** for direct access: ```powershell -# ✅ Direct access +# ✅ Direct access (attributes flattened to top level) '{{Request.Context.Identity.Profile.DisplayName}}' '{{Request.Context.Identity.Profile.EmailAddress}}' -'{{Request.Context.Identity.Profile.Department}}' -``` -### Structure Example +# ❌ Nested access no longer supported (Attributes removed after flattening) +'{{Request.Context.Identity.Profile.Attributes.DisplayName}}' +``` -After resolution, the profile object contains: +**Flattened structure:** ```powershell Request.Context.Identity.Profile = @{ - PSTypeName = 'IdLE.Identity' # Preserved from provider - IdentityKey = 'user123' # Core property - Enabled = $true # Core property - DisplayName = 'Jane Doe' # Flattened from provider Attributes - EmailAddress = 'jane.doe@example.com' # Flattened from provider Attributes - Department = 'Engineering' # Flattened from provider Attributes - # ... all other attributes promoted to top level + PSTypeName = 'IdLE.Identity' # Preserved from provider + IdentityKey = 'user123' # Core property + Enabled = $true # Core property + DisplayName = 'Jane Doe' # Flattened from Attributes + EmailAddress = 'jane@example.com' # Flattened from Attributes + # ... other attributes as top-level properties } ``` -### Reserved Property Names - -The following core property names are **reserved** and will not be overwritten by attribute keys during flattening: - -- `IdentityKey` - The identity key used by the workflow -- `Enabled` - The identity enabled status - -The following is preserved as internal type metadata: - -- `PSTypeName` - Type name metadata (e.g., 'IdLE.Identity') - -If an attribute key conflicts with a reserved core property name, a verbose warning is emitted during planning, and the conflicting attribute is skipped: - -```powershell -# Example: Provider returns Attributes = @{ IdentityKey = 'conflicting-value'; Enabled = $false } -# ⚠️ Verbose warnings emitted for both IdentityKey and Enabled -# ✅ Profile.IdentityKey returns the actual identity key (core property wins) -# ✅ Profile.Enabled returns the actual enabled status (core property wins) -# ❌ Conflicting attribute values are lost during flattening -``` +**Reserved names:** `IdentityKey` and `Enabled` cannot be overwritten by attributes. Conflicts trigger verbose warnings and the attribute is skipped. --- ## Multiple Resolvers and Precedence -### Execution Order - -Context Resolvers are executed **sequentially in the order they are declared** in the workflow's `ContextResolvers` array: +Resolvers execute **sequentially in declaration order**. If multiple resolvers write to the same path, **later ones overwrite earlier ones** (last-writer-wins): ```powershell ContextResolvers = @( - @{ # Executed first - Capability = 'IdLE.Identity.Read' - With = @{ - IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' - Provider = 'PrimaryAD' - } - } - @{ # Executed second - Capability = 'IdLE.Entitlement.List' - With = @{ - IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' - Provider = 'PrimaryAD' - } - } + @{ Capability = 'IdLE.Identity.Read'; With = @{ Provider = 'PrimaryAD' } } # Executes first + @{ Capability = 'IdLE.Identity.Read'; With = @{ Provider = 'EntraID' } } # Overwrites Profile with EntraID data ) +# Result: Request.Context.Identity.Profile contains EntraID data only ``` -### Precedence for Overlapping Data - -If multiple resolvers write to the **same context path**, later resolvers **overwrite** earlier ones: +**Using different providers per resolver:** ```powershell ContextResolvers = @( @{ Capability = 'IdLE.Identity.Read' - With = @{ - IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' - Provider = 'PrimaryAD' # Reads from Primary AD - } - } - @{ - Capability = 'IdLE.Identity.Read' - With = @{ - IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' - Provider = 'EntraID' # Overwrites Identity.Profile with Entra ID data - } - } -) -``` - -> **Important**: Both resolvers write to `Request.Context.Identity.Profile`. The **second resolver wins** and its data becomes the final `Identity.Profile` value. - -### Using Multiple Providers with Different Auth Sessions - -You can configure multiple resolvers that use different providers and authentication sessions for different systems: - -```powershell -ContextResolvers = @( - @{ - Capability = 'IdLE.Identity.Read' With = @{ IdentityKey = '{{Request.IdentityKeys.sAMAccountName}}' Provider = 'PrimaryAD' - AuthSessionName = 'Tier0-AD' # On-premises AD auth session + AuthSessionName = 'Tier0-AD' # On-premises AD auth } } @{ - Capability = 'IdLE.Entitlement.List' + Capability = 'IdLE.Entitlement.List' With = @{ IdentityKey = '{{Request.IdentityKeys.UserPrincipalName}}' Provider = 'EntraID' - AuthSessionName = 'GraphAPI' # Cloud auth session + AuthSessionName = 'GraphAPI' # Cloud auth (different session) } } ) +# Result: Profile from AD, Entitlements from EntraID (no conflicts - different paths) ``` -Each resolver independently: -- Selects its own `Provider` -- Uses its own `AuthSessionName` (if authentication is required) -- Can pass provider-specific options via `AuthSessionOptions` - -### Avoiding Conflicts - -To avoid unintended overwrites when using multiple providers: - -1. **Use different capabilities** that write to different context paths: - - `IdLE.Identity.Read` → `Request.Context.Identity.Profile` - - `IdLE.Entitlement.List` → `Request.Context.Identity.Entitlements` - -2. **Declare resolvers in intentional order** if you need the later resolver to win: - ```powershell - # Get basic profile from AD first - @{ Capability = 'IdLE.Identity.Read'; With = @{ Provider = 'AD' } } - - # Overwrite with cloud-enriched profile if needed - @{ Capability = 'IdLE.Identity.Read'; With = @{ Provider = 'EntraID' } } - ``` - -3. **Use unique identity keys** appropriate for each provider: - - AD providers often use `sAMAccountName` or `DistinguishedName` - - Entra ID providers use `UserPrincipalName` or `ObjectId` +**Best practices:** +- Use different capabilities to avoid overwrites (`IdLE.Identity.Read` → `Identity.Profile`, `IdLE.Entitlement.List` → `Identity.Entitlements`) +- If intentional overwrite is needed, declare resolvers in the desired order +- Use appropriate identity keys for each provider (AD: `sAMAccountName`, Entra ID: `UserPrincipalName`) --- -## Common Patterns - -### Resolve once, use everywhere - -Resolve identity or entitlements once and reuse the result in: +## Common Patterns and Troubleshooting -- Conditions -- Preconditions -- Templates +### Resolve Once, Use Everywhere -Example: +Resolve identity/entitlements once during planning, then reuse in conditions, preconditions, and templates: ```powershell +# In step condition Condition = @{ Exists = 'Request.Context.Identity.Profile' } -DisplayName = '{{Request.Context.Identity.Profile.DisplayName}}' +# In template +Message = '{{Request.Context.Identity.Profile.DisplayName}} offboarded' ``` -### Guard destructive steps +### Guard Destructive Operations -Only perform destructive actions if identity exists: +Only perform actions if identity exists: ```powershell -Condition = @{ - Exists = 'Request.Context.Identity.Profile' -} +Condition = @{ Exists = 'Request.Context.Identity.Profile' } ``` -### Entitlement snapshot usage +### Troubleshooting -Resolve entitlements once: +| Problem | Solution | +|---------|----------| +| Resolver not executed | Ensure `ContextResolvers` is at workflow root level | +| Capability not permitted | Only `IdLE.Identity.Read` and `IdLE.Entitlement.List` are allowed | +| Ambiguous provider | Specify `With.Provider` explicitly when multiple providers match | +| Context value missing | Verify `With` parameters and template placeholders resolve correctly | +| Type conflict in context | Cannot overwrite existing context path with incompatible type | -```powershell -ContextResolvers = @( - @{ - Capability = 'IdLE.Entitlement.List' - With = @{ IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' } - } -) -``` +### Inspecting Resolved Context -Then guard on availability: - -```powershell -Condition = @{ Exists = 'Request.Context.Identity.Entitlements' } -``` - ---- - -## Troubleshooting - -### Resolver not executed - -- Ensure `ContextResolvers` is defined at workflow root. -- Verify correct property name (`ContextResolvers`). - -### Capability not permitted - -- Only allowlisted read-only capabilities can be used. -- Validation happens during plan build. - -### Ambiguous provider - -- If multiple providers advertise a capability, specify `With.Provider` explicitly. - -### Context value missing - -- Verify required `With` parameters. -- Ensure template placeholders resolve correctly. - -### Type conflict in context path - -- A resolver cannot overwrite an existing path with incompatible type. - -### Inspecting resolved context data - -When working with complex objects (like entitlements), you may need to inspect the structure to determine the correct path syntax for Conditions or to understand what properties are available. - -**Method 1: Inspect the plan object after planning** +View resolved context after planning: ```powershell $plan = New-IdlePlan -WorkflowPath ./workflow.psd1 -Request $req -Providers $providers -# View the entire context structure +# View entire context $plan.Request.Context | ConvertTo-Json -Depth 5 -# View specific resolved data -$plan.Request.Context.Identity.Entitlements | ConvertTo-Json -Depth 2 -``` - -**Method 2: Use Format-Table for quick inspection** +# View specific data +$plan.Request.Context.Identity.Profile +$plan.Request.Context.Identity.Entitlements | Format-Table -```powershell -# After planning, inspect entitlements structure -$plan.Request.Context.Identity.Entitlements | Format-Table -AutoSize -``` - -**Method 3: Access individual properties** - -```powershell -# Check if entitlements are objects with properties +# Inspect object structure $plan.Request.Context.Identity.Entitlements[0] | Get-Member -$plan.Request.Context.Identity.Entitlements[0].Id -$plan.Request.Context.Identity.Entitlements[0].DisplayName ``` -**Using discovered structure in Conditions** - -Once you know the structure (e.g., entitlements are objects with `Kind`, `Id`, `DisplayName`), use member-access enumeration in your condition paths: +Use discovered structure in conditions: ```powershell -# Extract Id values from all entitlement objects Condition = @{ NotContains = @{ - Path = 'Request.Context.Identity.Entitlements.Id' + Path = 'Request.Context.Identity.Entitlements.Id' # Member-access enumeration Value = 'CN=BreakGlass-Users,OU=Groups,DC=example,DC=com' } } From bab54484b5ef5c1a3884ba299071899168ac3d8d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:34:51 +0000 Subject: [PATCH 14/16] test(core): remove attribute flattening and align tests with scoped context model - Remove ConvertTo-IdleFlattenedIdentity function - attributes kept nested under Profile.Attributes - Update Invoke-IdleContextResolvers to return identity as-is without flattening - Update New-IdlePlan.ContextResolvers.Tests.ps1 to use scoped paths: Providers...Identity.Profile.Attributes.* - Update template test to use full scoped path with nested Attributes - All 40 ContextResolver tests now passing - Partial update to Test-IdleCondition.Tests.ps1 paths (8 tests still failing - need to fix mock context structures) Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- .../Private/Invoke-IdleContextResolvers.ps1 | 116 +----------------- .../New-IdlePlan.ContextResolvers.Tests.ps1 | 61 ++++----- tests/Core/Test-IdleCondition.Tests.ps1 | 59 ++++++--- 3 files changed, 78 insertions(+), 158 deletions(-) diff --git a/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 b/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 index 2e8d3a92..388883ee 100644 --- a/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 +++ b/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 @@ -753,11 +753,9 @@ function Invoke-IdleResolverCapabilityDispatch { $provider.GetIdentity($identityKey) } - # Flatten the identity object by promoting Attributes to the top level. - # This allows users to access Request.Context.Identity.Profile.DisplayName - # directly instead of Request.Context.Identity.Profile.Attributes.DisplayName. - # The Attributes hashtable is removed after flattening. - return ConvertTo-IdleFlattenedIdentity -Identity $identity + # Return the identity object as-is with nested Attributes. + # Users access attributes via Request.Context.Providers...Identity.Profile.Attributes. + return $identity } default { @@ -822,109 +820,7 @@ function Set-IdleContextValue { $current[$segments[-1]] = $Value } -function ConvertTo-IdleFlattenedIdentity { - <# - .SYNOPSIS - Flattens an identity object by promoting Attributes to top-level properties. - - .DESCRIPTION - Takes an identity object returned by a provider (with IdentityKey, Enabled, Attributes) - and creates a new object where: - - IdentityKey and Enabled are preserved at the top level - - All properties from the Attributes hashtable are promoted to top-level properties - - The original Attributes hashtable is removed after flattening - - Reserved property names (IdentityKey, Enabled) will not be overwritten - if they appear as keys in the Attributes hashtable. If a conflict occurs, a verbose - warning is emitted and the conflicting attribute is skipped. +# ConvertTo-IdleFlattenedIdentity function removed - attributes are kept nested under Profile.Attributes +# The scoped context model uses: Request.Context.Providers...Identity.Profile.Attributes. +# NOT flattened to: Request.Context.Providers...Identity.Profile. - This allows users to access Request.Context.Identity.Profile.DisplayName - directly at the top level. - - .PARAMETER Identity - The identity object returned by a provider's GetIdentity method. - - .OUTPUTS - PSCustomObject with flattened attributes. - #> - [CmdletBinding()] - param( - [Parameter(Mandatory)] - [AllowNull()] - [object] $Identity - ) - - if ($null -eq $Identity) { - return $null - } - - # Clone the original identity object to preserve all existing properties and PSTypeName - $cloned = [ordered]@{} - - # Capture PSTypeName(s) from the original object if it's a PSCustomObject - $typeNames = @() - if ($Identity -isnot [System.Collections.IDictionary]) { - foreach ($typeName in $Identity.PSObject.TypeNames) { - if ($typeName -ne 'System.Management.Automation.PSCustomObject' -and $typeName -ne 'System.Object') { - $typeNames += $typeName - } - } - } - - # Copy all properties from the original object - if ($Identity -is [System.Collections.IDictionary]) { - foreach ($key in $Identity.Keys) { - if ($key -ne 'PSTypeName') { - $cloned[$key] = $Identity[$key] - } - } - # Also check for PSTypeName in the hashtable - if ($Identity.ContainsKey('PSTypeName')) { - $typeNames += $Identity['PSTypeName'] - } - } - else { - foreach ($prop in $Identity.PSObject.Properties) { - $cloned[$prop.Name] = $prop.Value - } - } - - # Add PSTypeName to the cloned hashtable if we captured any - if ($typeNames.Count -gt 0) { - $cloned['PSTypeName'] = $typeNames[0] # Primary type name - } - - # Convert to PSCustomObject - $flattened = [pscustomobject]$cloned - - # Promote all attribute keys to top level. - # Reserved property names (IdentityKey, Enabled) will not be overwritten - # if they appear as keys in the Attributes hashtable. - $attributes = $null - if ($flattened.PSObject.Properties.Name -contains 'Attributes') { - $attributes = $flattened.Attributes - } - - if ($null -ne $attributes -and $attributes -is [System.Collections.IDictionary]) { - $reservedNames = @('IdentityKey', 'Enabled') - foreach ($key in $attributes.Keys) { - # Only add if not already present (existing properties take precedence) - if ($flattened.PSObject.Properties.Name -notcontains $key) { - $flattened | Add-Member -MemberType NoteProperty -Name $key -Value $attributes[$key] -Force - } - elseif ($reservedNames -contains $key) { - # Warn if an attribute key conflicts with a reserved property name - # This helps users understand why an attribute was skipped - Write-Verbose "Identity attribute '$key' conflicts with a core property name and will be skipped during flattening." - } - } - } - - # Always remove the Attributes property after flattening (no backward compatibility) - # This applies whether Attributes was null, empty, or had content - if ($flattened.PSObject.Properties.Name -contains 'Attributes') { - $flattened.PSObject.Properties.Remove('Attributes') - } - - return $flattened -} diff --git a/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 b/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 index 96ceee75..33b8e5c6 100644 --- a/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 +++ b/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 @@ -157,20 +157,19 @@ Describe 'New-IdlePlan - ContextResolvers' { $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers $plan | Should -Not -BeNullOrEmpty - $profile = $plan.Request.Context.Identity.Profile + # Profile is written to scoped path: Providers...Identity.Profile + $profile = $plan.Request.Context.Providers.Identity.Default.Identity.Profile # Core properties should be present $profile.IdentityKey | Should -Be 'user1' $profile.Enabled | Should -Be $true - # Attributes should be flattened to top level for direct access - $profile.DisplayName | Should -Be 'User One' - $profile.Department | Should -Be 'IT' - $profile.EmailAddress | Should -Be 'user1@example.com' - $profile.UserPrincipalName | Should -Be 'user1@example.com' - - # Attributes hashtable should be removed after flattening - $profile.PSObject.Properties.Name | Should -Not -Contain 'Attributes' + # Attributes should be nested under Profile.Attributes (not flattened) + $profile.Attributes | Should -Not -BeNullOrEmpty + $profile.Attributes.DisplayName | Should -Be 'User One' + $profile.Attributes.Department | Should -Be 'IT' + $profile.Attributes.EmailAddress | Should -Be 'user1@example.com' + $profile.Attributes.UserPrincipalName | Should -Be 'user1@example.com' # PSTypeName should be preserved from the original identity object $profile.PSObject.TypeNames | Should -Contain 'IdLE.Identity' @@ -198,14 +197,15 @@ Describe 'New-IdlePlan - ContextResolvers' { $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers $plan | Should -Not -BeNullOrEmpty - $profile = $plan.Request.Context.Identity.Profile + # Profile is written to scoped path: Providers...Identity.Profile + $profile = $plan.Request.Context.Providers.Identity.Default.Identity.Profile # Core properties should be present $profile.IdentityKey | Should -Be 'user1' $profile.Enabled | Should -Be $true - # Attributes should be removed (was null, so after flattening there's no Attributes property) - $profile.PSObject.Properties.Name | Should -Not -Contain 'Attributes' + # Attributes should remain null (not flattened, kept as-is) + $profile.Attributes | Should -BeNullOrEmpty } It 'IdLE.Identity.Read resolver handles empty Attributes hashtable' { @@ -230,14 +230,18 @@ Describe 'New-IdlePlan - ContextResolvers' { $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers $plan | Should -Not -BeNullOrEmpty - $profile = $plan.Request.Context.Identity.Profile + # Profile is written to scoped path: Providers...Identity.Profile + $profile = $plan.Request.Context.Providers.Identity.Default.Identity.Profile # Core properties should be present $profile.IdentityKey | Should -Be 'user1' $profile.Enabled | Should -Be $true - # Attributes should be removed after flattening (was empty, so nothing to flatten) - $profile.PSObject.Properties.Name | Should -Not -Contain 'Attributes' + # Attributes should be empty hashtable (not flattened, kept as-is) + $profile.PSObject.Properties.Name | Should -Contain 'Attributes' + if ($null -ne $profile.Attributes) { + $profile.Attributes.Count | Should -Be 0 + } } It 'IdLE.Identity.Read resolver does not overwrite core properties with conflicting attributes' { @@ -266,19 +270,20 @@ Describe 'New-IdlePlan - ContextResolvers' { $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers $plan | Should -Not -BeNullOrEmpty - $profile = $plan.Request.Context.Identity.Profile + # Profile is written to scoped path: Providers...Identity.Profile + $profile = $plan.Request.Context.Providers.Identity.Default.Identity.Profile - # Core IdentityKey should NOT be overwritten by conflicting attribute + # Core IdentityKey should NOT be overwritten by attributes (attributes stay nested) $profile.IdentityKey | Should -Be 'user1' - # Core Enabled should NOT be overwritten by conflicting attribute + # Core Enabled should NOT be overwritten by attributes (attributes stay nested) $profile.Enabled | Should -Be $true - # DisplayName should be flattened (no conflict) - $profile.DisplayName | Should -Be 'User One' - - # Attributes hashtable should be removed after flattening - $profile.PSObject.Properties.Name | Should -Not -Contain 'Attributes' + # Attributes should remain nested with all keys intact (no flattening) + $profile.Attributes | Should -Not -BeNullOrEmpty + $profile.Attributes.IdentityKey | Should -Be 'conflicting-value' + $profile.Attributes.Enabled | Should -Be $false + $profile.Attributes.DisplayName | Should -Be 'User One' } } @@ -450,8 +455,8 @@ Describe 'New-IdlePlan - ContextResolvers' { $entitlements[0].Id | Should -Be 'tmpl-grp' } - It 'resolves templates using flattened Identity.Profile attributes' { - # Create a workflow with a step that uses template substitution with Identity.Profile attributes + It 'resolves templates using nested Identity.Profile.Attributes paths' { + # Create a workflow with a step that uses template substitution with Identity.Profile.Attributes $wfContent = @' @{ Name = 'Identity Profile Template Test' @@ -470,8 +475,8 @@ Describe 'New-IdlePlan - ContextResolvers' { Name = 'TestStep' Type = 'IdLE.Step.EmitEvent' With = @{ - Message = 'User: {{Request.Context.Identity.Profile.DisplayName}}, Email: {{Request.Context.Identity.Profile.EmailAddress}}' - Department = '{{Request.Context.Identity.Profile.Department}}' + Message = 'User: {{Request.Context.Providers.Identity.Default.Identity.Profile.Attributes.DisplayName}}, Email: {{Request.Context.Providers.Identity.Default.Identity.Profile.Attributes.EmailAddress}}' + Department = '{{Request.Context.Providers.Identity.Default.Identity.Profile.Attributes.Department}}' } } ) @@ -504,7 +509,7 @@ Describe 'New-IdlePlan - ContextResolvers' { $plan | Should -Not -BeNullOrEmpty $plan.Steps[0].Status | Should -Be 'Planned' - # Verify templates were resolved using flattened attributes + # Verify templates were resolved using nested Attributes paths $plan.Steps[0].With.Message | Should -Be 'User: John Doe, Email: john.doe@example.com' $plan.Steps[0].With.Department | Should -Be 'Engineering' } diff --git a/tests/Core/Test-IdleCondition.Tests.ps1 b/tests/Core/Test-IdleCondition.Tests.ps1 index 09acc3bf..69592cf8 100644 --- a/tests/Core/Test-IdleCondition.Tests.ps1 +++ b/tests/Core/Test-IdleCondition.Tests.ps1 @@ -123,7 +123,7 @@ Describe 'Condition DSL (schema + evaluator)' { It 'accepts Contains operator with Path + Value' { $condition = @{ Contains = @{ - Path = 'Request.Context.Identity.Entitlements' + Path = 'Request.Context.Views.Identity.Entitlements' Value = 'CN=Group,OU=Groups,DC=example,DC=com' } } @@ -146,7 +146,7 @@ Describe 'Condition DSL (schema + evaluator)' { It 'rejects Contains with missing Value' { $condition = @{ Contains = @{ - Path = 'Request.Context.Identity.Entitlements' + Path = 'Request.Context.Views.Identity.Entitlements' } } @@ -157,7 +157,7 @@ Describe 'Condition DSL (schema + evaluator)' { It 'accepts NotContains operator with Path + Value' { $condition = @{ NotContains = @{ - Path = 'Request.Context.Identity.Entitlements' + Path = 'Request.Context.Views.Identity.Entitlements' Value = 'CN=Group,OU=Groups,DC=example,DC=com' } } @@ -169,7 +169,7 @@ Describe 'Condition DSL (schema + evaluator)' { It 'accepts Like operator with Path + Pattern' { $condition = @{ Like = @{ - Path = 'Request.Context.Identity.Profile.DisplayName' + Path = 'Request.Context.Views.Identity.Profile.Attributes.DisplayName' Pattern = '* (Contractor)' } } @@ -181,7 +181,7 @@ Describe 'Condition DSL (schema + evaluator)' { It 'rejects Like with missing Pattern' { $condition = @{ Like = @{ - Path = 'Request.Context.Identity.Profile.DisplayName' + Path = 'Request.Context.Views.Identity.Profile.Attributes.DisplayName' } } @@ -192,7 +192,7 @@ Describe 'Condition DSL (schema + evaluator)' { It 'accepts NotLike operator with Path + Pattern' { $condition = @{ NotLike = @{ - Path = 'Request.Context.Identity.Entitlements' + Path = 'Request.Context.Views.Identity.Entitlements' Pattern = 'CN=HR-*' } } @@ -376,6 +376,7 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [pscustomobject]@{ + Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Entitlements = @( [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com'; DisplayName = 'Users' } @@ -389,7 +390,7 @@ Describe 'Condition DSL (schema + evaluator)' { $condition = @{ Contains = @{ - Path = 'Request.Context.Identity.Entitlements.Id' + Path = 'Request.Context.Views.Identity.Entitlements.Id' Value = 'CN=BreakGlass-Users,OU=Groups,DC=example,DC=com' } } @@ -401,6 +402,7 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [pscustomobject]@{ + Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Entitlements = @( [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com'; DisplayName = 'Users' } @@ -413,7 +415,7 @@ Describe 'Condition DSL (schema + evaluator)' { $condition = @{ Contains = @{ - Path = 'Request.Context.Identity.Entitlements.Id' + Path = 'Request.Context.Views.Identity.Entitlements.Id' Value = 'CN=BreakGlass-Users,OU=Groups,DC=example,DC=com' } } @@ -425,6 +427,7 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [pscustomobject]@{ + Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Name = 'John Doe' } @@ -446,6 +449,7 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [pscustomobject]@{ + Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Metadata = @{ Department = 'Engineering' @@ -470,6 +474,7 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [pscustomobject]@{ + Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Metadata = @{ Department = 'Engineering' @@ -494,6 +499,7 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [pscustomobject]@{ + Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Metadata = @{ Department = 'Engineering' @@ -518,6 +524,7 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [pscustomobject]@{ + Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Metadata = @{ Department = 'Engineering' @@ -542,6 +549,7 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [pscustomobject]@{ + Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Entitlements = @( [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com'; DisplayName = 'Users' } @@ -554,7 +562,7 @@ Describe 'Condition DSL (schema + evaluator)' { $condition = @{ NotContains = @{ - Path = 'Request.Context.Identity.Entitlements.Id' + Path = 'Request.Context.Views.Identity.Entitlements.Id' Value = 'CN=BreakGlass-Users,OU=Groups,DC=example,DC=com' } } @@ -566,6 +574,7 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [pscustomobject]@{ + Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Entitlements = @( [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com'; DisplayName = 'Users' } @@ -578,7 +587,7 @@ Describe 'Condition DSL (schema + evaluator)' { $condition = @{ NotContains = @{ - Path = 'Request.Context.Identity.Entitlements.Id' + Path = 'Request.Context.Views.Identity.Entitlements.Id' Value = 'CN=BreakGlass-Users,OU=Groups,DC=example,DC=com' } } @@ -590,6 +599,7 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [pscustomobject]@{ + Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Profile = [pscustomobject]@{ DisplayName = 'John Doe (Contractor)' @@ -601,7 +611,7 @@ Describe 'Condition DSL (schema + evaluator)' { $condition = @{ Like = @{ - Path = 'Request.Context.Identity.Profile.DisplayName' + Path = 'Request.Context.Views.Identity.Profile.Attributes.DisplayName' Pattern = '* (Contractor)' } } @@ -613,6 +623,7 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [pscustomobject]@{ + Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Profile = [pscustomobject]@{ DisplayName = 'John Doe' @@ -624,7 +635,7 @@ Describe 'Condition DSL (schema + evaluator)' { $condition = @{ Like = @{ - Path = 'Request.Context.Identity.Profile.DisplayName' + Path = 'Request.Context.Views.Identity.Profile.Attributes.DisplayName' Pattern = '* (Contractor)' } } @@ -636,6 +647,7 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [pscustomobject]@{ + Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Entitlements = @( [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com'; DisplayName = 'Users' } @@ -649,7 +661,7 @@ Describe 'Condition DSL (schema + evaluator)' { $condition = @{ Like = @{ - Path = 'Request.Context.Identity.Entitlements.Id' + Path = 'Request.Context.Views.Identity.Entitlements.Id' Pattern = 'CN=HR-*' } } @@ -661,6 +673,7 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [pscustomobject]@{ + Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Entitlements = @( [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com'; DisplayName = 'Users' } @@ -673,7 +686,7 @@ Describe 'Condition DSL (schema + evaluator)' { $condition = @{ Like = @{ - Path = 'Request.Context.Identity.Entitlements.Id' + Path = 'Request.Context.Views.Identity.Entitlements.Id' Pattern = 'CN=HR-*' } } @@ -685,6 +698,7 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [pscustomobject]@{ + Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Profile = [pscustomobject]@{ DisplayName = 'John Doe' @@ -696,7 +710,7 @@ Describe 'Condition DSL (schema + evaluator)' { $condition = @{ NotLike = @{ - Path = 'Request.Context.Identity.Profile.DisplayName' + Path = 'Request.Context.Views.Identity.Profile.Attributes.DisplayName' Pattern = '* (Contractor)' } } @@ -708,6 +722,7 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [pscustomobject]@{ + Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Profile = [pscustomobject]@{ DisplayName = 'John Doe (Contractor)' @@ -719,7 +734,7 @@ Describe 'Condition DSL (schema + evaluator)' { $condition = @{ NotLike = @{ - Path = 'Request.Context.Identity.Profile.DisplayName' + Path = 'Request.Context.Views.Identity.Profile.Attributes.DisplayName' Pattern = '* (Contractor)' } } @@ -731,6 +746,7 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [pscustomobject]@{ + Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Entitlements = @( [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com'; DisplayName = 'Users' } @@ -743,7 +759,7 @@ Describe 'Condition DSL (schema + evaluator)' { $condition = @{ NotLike = @{ - Path = 'Request.Context.Identity.Entitlements.Id' + Path = 'Request.Context.Views.Identity.Entitlements.Id' Pattern = 'CN=HR-*' } } @@ -755,6 +771,7 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [pscustomobject]@{ + Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Entitlements = @( [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com'; DisplayName = 'Users' } @@ -767,7 +784,7 @@ Describe 'Condition DSL (schema + evaluator)' { $condition = @{ NotLike = @{ - Path = 'Request.Context.Identity.Entitlements.Id' + Path = 'Request.Context.Views.Identity.Entitlements.Id' Pattern = 'CN=HR-*' } } @@ -779,6 +796,7 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [pscustomobject]@{ + Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Entitlements = @( [pscustomobject]@{ Kind = 'Group'; Id = 'CN=admins,OU=Groups,DC=example,DC=com'; DisplayName = 'Admins' } @@ -791,7 +809,7 @@ Describe 'Condition DSL (schema + evaluator)' { $condition = @{ Contains = @{ - Path = 'Request.Context.Identity.Entitlements.Id' + Path = 'Request.Context.Views.Identity.Entitlements.Id' Value = 'CN=USERS,OU=Groups,DC=example,DC=com' } } @@ -803,6 +821,7 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [pscustomobject]@{ + Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Profile = [pscustomobject]@{ DisplayName = 'john doe (contractor)' @@ -814,7 +833,7 @@ Describe 'Condition DSL (schema + evaluator)' { $condition = @{ Like = @{ - Path = 'Request.Context.Identity.Profile.DisplayName' + Path = 'Request.Context.Views.Identity.Profile.Attributes.DisplayName' Pattern = '* (CONTRACTOR)' } } From aa520a48b101f3cc9bf32aefdd2aa85bdaa5493c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:43:58 +0000 Subject: [PATCH 15/16] test(core): fix Test-IdleCondition mock context structures for Views model - Added missing closing braces for Views wrappers in mock contexts - Fixed indentation of Identity objects nested under Views - Updated condition paths to use Request.Context.Views.Identity.* instead of flat paths - Simplified Profile paths by removing .Attributes where mock contexts don't have it - All 45 Condition DSL tests now passing - All 366 core tests passing Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- 0 | 1 + tests/Core/Test-IdleCondition.Tests.ps1 | 81 +++++++++++++++---------- 2 files changed, 51 insertions(+), 31 deletions(-) create mode 100644 0 diff --git a/0 b/0 new file mode 100644 index 00000000..0ca95142 --- /dev/null +++ b/0 @@ -0,0 +1 @@ +True diff --git a/tests/Core/Test-IdleCondition.Tests.ps1 b/tests/Core/Test-IdleCondition.Tests.ps1 index 69592cf8..6b11eb66 100644 --- a/tests/Core/Test-IdleCondition.Tests.ps1 +++ b/tests/Core/Test-IdleCondition.Tests.ps1 @@ -169,7 +169,7 @@ Describe 'Condition DSL (schema + evaluator)' { It 'accepts Like operator with Path + Pattern' { $condition = @{ Like = @{ - Path = 'Request.Context.Views.Identity.Profile.Attributes.DisplayName' + Path = 'Request.Context.Views.Identity.Profile.DisplayName' Pattern = '* (Contractor)' } } @@ -181,7 +181,7 @@ Describe 'Condition DSL (schema + evaluator)' { It 'rejects Like with missing Pattern' { $condition = @{ Like = @{ - Path = 'Request.Context.Views.Identity.Profile.Attributes.DisplayName' + Path = 'Request.Context.Views.Identity.Profile.DisplayName' } } @@ -377,13 +377,14 @@ Describe 'Condition DSL (schema + evaluator)' { Request = [pscustomobject]@{ Context = [pscustomobject]@{ Views = [pscustomobject]@{ - Identity = [pscustomobject]@{ + Identity = [pscustomobject]@{ Entitlements = @( [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com'; DisplayName = 'Users' } [pscustomobject]@{ Kind = 'Group'; Id = 'CN=BreakGlass-Users,OU=Groups,DC=example,DC=com'; DisplayName = 'BreakGlass Users' } [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Admins,OU=Groups,DC=example,DC=com'; DisplayName = 'Admins' } ) } + } } } } @@ -403,12 +404,13 @@ Describe 'Condition DSL (schema + evaluator)' { Request = [pscustomobject]@{ Context = [pscustomobject]@{ Views = [pscustomobject]@{ - Identity = [pscustomobject]@{ + Identity = [pscustomobject]@{ Entitlements = @( [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com'; DisplayName = 'Users' } [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Admins,OU=Groups,DC=example,DC=com'; DisplayName = 'Admins' } ) } + } } } } @@ -428,16 +430,17 @@ Describe 'Condition DSL (schema + evaluator)' { Request = [pscustomobject]@{ Context = [pscustomobject]@{ Views = [pscustomobject]@{ - Identity = [pscustomobject]@{ + Identity = [pscustomobject]@{ Name = 'John Doe' } + } } } } $condition = @{ Contains = @{ - Path = 'Request.Context.Identity.Name' + Path = 'Request.Context.Views.Identity.Name' Value = 'John' } } @@ -450,19 +453,20 @@ Describe 'Condition DSL (schema + evaluator)' { Request = [pscustomobject]@{ Context = [pscustomobject]@{ Views = [pscustomobject]@{ - Identity = [pscustomobject]@{ + Identity = [pscustomobject]@{ Metadata = @{ Department = 'Engineering' Location = 'Seattle' } } + } } } } $condition = @{ Contains = @{ - Path = 'Request.Context.Identity.Metadata' + Path = 'Request.Context.Views.Identity.Metadata' Value = 'Engineering' } } @@ -475,19 +479,20 @@ Describe 'Condition DSL (schema + evaluator)' { Request = [pscustomobject]@{ Context = [pscustomobject]@{ Views = [pscustomobject]@{ - Identity = [pscustomobject]@{ + Identity = [pscustomobject]@{ Metadata = @{ Department = 'Engineering' Location = 'Seattle' } } + } } } } $condition = @{ NotContains = @{ - Path = 'Request.Context.Identity.Metadata' + Path = 'Request.Context.Views.Identity.Metadata' Value = 'HR' } } @@ -500,19 +505,20 @@ Describe 'Condition DSL (schema + evaluator)' { Request = [pscustomobject]@{ Context = [pscustomobject]@{ Views = [pscustomobject]@{ - Identity = [pscustomobject]@{ + Identity = [pscustomobject]@{ Metadata = @{ Department = 'Engineering' Location = 'Seattle' } } + } } } } $condition = @{ Like = @{ - Path = 'Request.Context.Identity.Metadata' + Path = 'Request.Context.Views.Identity.Metadata' Pattern = 'Eng*' } } @@ -525,19 +531,20 @@ Describe 'Condition DSL (schema + evaluator)' { Request = [pscustomobject]@{ Context = [pscustomobject]@{ Views = [pscustomobject]@{ - Identity = [pscustomobject]@{ + Identity = [pscustomobject]@{ Metadata = @{ Department = 'Engineering' Location = 'Seattle' } } + } } } } $condition = @{ NotLike = @{ - Path = 'Request.Context.Identity.Metadata' + Path = 'Request.Context.Views.Identity.Metadata' Pattern = 'HR*' } } @@ -550,12 +557,13 @@ Describe 'Condition DSL (schema + evaluator)' { Request = [pscustomobject]@{ Context = [pscustomobject]@{ Views = [pscustomobject]@{ - Identity = [pscustomobject]@{ + Identity = [pscustomobject]@{ Entitlements = @( [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com'; DisplayName = 'Users' } [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Admins,OU=Groups,DC=example,DC=com'; DisplayName = 'Admins' } ) } + } } } } @@ -575,12 +583,13 @@ Describe 'Condition DSL (schema + evaluator)' { Request = [pscustomobject]@{ Context = [pscustomobject]@{ Views = [pscustomobject]@{ - Identity = [pscustomobject]@{ + Identity = [pscustomobject]@{ Entitlements = @( [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com'; DisplayName = 'Users' } [pscustomobject]@{ Kind = 'Group'; Id = 'CN=BreakGlass-Users,OU=Groups,DC=example,DC=com'; DisplayName = 'BreakGlass Users' } ) } + } } } } @@ -600,18 +609,19 @@ Describe 'Condition DSL (schema + evaluator)' { Request = [pscustomobject]@{ Context = [pscustomobject]@{ Views = [pscustomobject]@{ - Identity = [pscustomobject]@{ + Identity = [pscustomobject]@{ Profile = [pscustomobject]@{ DisplayName = 'John Doe (Contractor)' } } + } } } } $condition = @{ Like = @{ - Path = 'Request.Context.Views.Identity.Profile.Attributes.DisplayName' + Path = 'Request.Context.Views.Identity.Profile.DisplayName' Pattern = '* (Contractor)' } } @@ -624,18 +634,19 @@ Describe 'Condition DSL (schema + evaluator)' { Request = [pscustomobject]@{ Context = [pscustomobject]@{ Views = [pscustomobject]@{ - Identity = [pscustomobject]@{ + Identity = [pscustomobject]@{ Profile = [pscustomobject]@{ DisplayName = 'John Doe' } } + } } } } $condition = @{ Like = @{ - Path = 'Request.Context.Views.Identity.Profile.Attributes.DisplayName' + Path = 'Request.Context.Views.Identity.Profile.DisplayName' Pattern = '* (Contractor)' } } @@ -648,13 +659,14 @@ Describe 'Condition DSL (schema + evaluator)' { Request = [pscustomobject]@{ Context = [pscustomobject]@{ Views = [pscustomobject]@{ - Identity = [pscustomobject]@{ + Identity = [pscustomobject]@{ Entitlements = @( [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com'; DisplayName = 'Users' } [pscustomobject]@{ Kind = 'Group'; Id = 'CN=HR-Employees,OU=Groups,DC=example,DC=com'; DisplayName = 'HR Employees' } [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Admins,OU=Groups,DC=example,DC=com'; DisplayName = 'Admins' } ) } + } } } } @@ -674,12 +686,13 @@ Describe 'Condition DSL (schema + evaluator)' { Request = [pscustomobject]@{ Context = [pscustomobject]@{ Views = [pscustomobject]@{ - Identity = [pscustomobject]@{ + Identity = [pscustomobject]@{ Entitlements = @( [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com'; DisplayName = 'Users' } [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Admins,OU=Groups,DC=example,DC=com'; DisplayName = 'Admins' } ) } + } } } } @@ -699,18 +712,19 @@ Describe 'Condition DSL (schema + evaluator)' { Request = [pscustomobject]@{ Context = [pscustomobject]@{ Views = [pscustomobject]@{ - Identity = [pscustomobject]@{ + Identity = [pscustomobject]@{ Profile = [pscustomobject]@{ DisplayName = 'John Doe' } } + } } } } $condition = @{ NotLike = @{ - Path = 'Request.Context.Views.Identity.Profile.Attributes.DisplayName' + Path = 'Request.Context.Views.Identity.Profile.DisplayName' Pattern = '* (Contractor)' } } @@ -723,18 +737,19 @@ Describe 'Condition DSL (schema + evaluator)' { Request = [pscustomobject]@{ Context = [pscustomobject]@{ Views = [pscustomobject]@{ - Identity = [pscustomobject]@{ + Identity = [pscustomobject]@{ Profile = [pscustomobject]@{ DisplayName = 'John Doe (Contractor)' } } + } } } } $condition = @{ NotLike = @{ - Path = 'Request.Context.Views.Identity.Profile.Attributes.DisplayName' + Path = 'Request.Context.Views.Identity.Profile.DisplayName' Pattern = '* (Contractor)' } } @@ -747,12 +762,13 @@ Describe 'Condition DSL (schema + evaluator)' { Request = [pscustomobject]@{ Context = [pscustomobject]@{ Views = [pscustomobject]@{ - Identity = [pscustomobject]@{ + Identity = [pscustomobject]@{ Entitlements = @( [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com'; DisplayName = 'Users' } [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Admins,OU=Groups,DC=example,DC=com'; DisplayName = 'Admins' } ) } + } } } } @@ -772,12 +788,13 @@ Describe 'Condition DSL (schema + evaluator)' { Request = [pscustomobject]@{ Context = [pscustomobject]@{ Views = [pscustomobject]@{ - Identity = [pscustomobject]@{ + Identity = [pscustomobject]@{ Entitlements = @( [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com'; DisplayName = 'Users' } [pscustomobject]@{ Kind = 'Group'; Id = 'CN=HR-Employees,OU=Groups,DC=example,DC=com'; DisplayName = 'HR Employees' } ) } + } } } } @@ -797,12 +814,13 @@ Describe 'Condition DSL (schema + evaluator)' { Request = [pscustomobject]@{ Context = [pscustomobject]@{ Views = [pscustomobject]@{ - Identity = [pscustomobject]@{ + Identity = [pscustomobject]@{ Entitlements = @( [pscustomobject]@{ Kind = 'Group'; Id = 'CN=admins,OU=Groups,DC=example,DC=com'; DisplayName = 'Admins' } [pscustomobject]@{ Kind = 'Group'; Id = 'CN=users,OU=Groups,DC=example,DC=com'; DisplayName = 'Users' } ) } + } } } } @@ -822,18 +840,19 @@ Describe 'Condition DSL (schema + evaluator)' { Request = [pscustomobject]@{ Context = [pscustomobject]@{ Views = [pscustomobject]@{ - Identity = [pscustomobject]@{ + Identity = [pscustomobject]@{ Profile = [pscustomobject]@{ DisplayName = 'john doe (contractor)' } } + } } } } $condition = @{ Like = @{ - Path = 'Request.Context.Views.Identity.Profile.Attributes.DisplayName' + Path = 'Request.Context.Views.Identity.Profile.DisplayName' Pattern = '* (CONTRACTOR)' } } From a5994c13020e024937f421d521383eb47c10d517 Mon Sep 17 00:00:00 2001 From: Matthias Fleschuetz <13959569+blindzero@users.noreply.github.com> Date: Fri, 13 Mar 2026 20:13:32 +0100 Subject: [PATCH 16/16] removed stale file --- 0 | 1 - 1 file changed, 1 deletion(-) delete mode 100644 0 diff --git a/0 b/0 deleted file mode 100644 index 0ca95142..00000000 --- a/0 +++ /dev/null @@ -1 +0,0 @@ -True