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