diff --git a/docs/reference/providers/provider-ad.md b/docs/reference/providers/provider-ad.md index 0fdfef4f..801fa2dd 100644 --- a/docs/reference/providers/provider-ad.md +++ b/docs/reference/providers/provider-ad.md @@ -92,7 +92,8 @@ This provider supports Context Resolvers for the allowlisted, read-only capabili ### Capability: `IdLE.Identity.Read` -Writes to: `Request.Context.Identity.Profile` +Writes to scoped path: `Request.Context.Providers...Identity.Profile` +Engine-defined View: `Request.Context.Views.Identity.Profile` Type: `PSCustomObject` (`PSTypeName = 'IdLE.Identity'`) Top-level properties: @@ -119,9 +120,12 @@ Top-level properties: | `sAMAccountName` | `string` | | `DistinguishedName` | `string` | +> **Attribute access**: Profile attributes are nested under the `Attributes` key. Use `Request.Context.Views.Identity.Profile.Attributes.DisplayName` in Conditions (or the scoped `Request.Context.Providers...Identity.Profile.Attributes.DisplayName`), **not** `Request.Context.Views.Identity.Profile.DisplayName`. + ### Capability: `IdLE.Entitlement.List` -Writes to: `Request.Context.Identity.Entitlements` +Writes to scoped path: `Request.Context.Providers...Identity.Entitlements` +Engine-defined View: `Request.Context.Views.Identity.Entitlements` Type: `object[]` (array of `PSCustomObject`, `PSTypeName = 'IdLE.Entitlement'`) Each element represents one AD group membership: @@ -135,7 +139,9 @@ Each element represents one AD group membership: Notes: - The output paths are fixed by the engine and cannot be changed. -- Use these values in **Conditions**, **Preconditions**, and **Templates** (resolved during planning). +- Each entry is automatically annotated with `SourceProvider` and `SourceAuthSessionName` metadata. +- Use the global View (`Request.Context.Views.Identity.Entitlements`) in **Conditions** when you don't need to filter by provider. Use the scoped path when you need results from a specific provider only. +- See [Context Resolvers](../../use/workflows/context-resolver.md) for the full path reference. ## Configuration diff --git a/docs/reference/providers/provider-entraID.md b/docs/reference/providers/provider-entraID.md index da8a984e..0a0a176f 100644 --- a/docs/reference/providers/provider-entraID.md +++ b/docs/reference/providers/provider-entraID.md @@ -92,7 +92,8 @@ This provider supports Context Resolvers for the allowlisted, read-only capabili ### Capability: `IdLE.Identity.Read` -Writes to: `Request.Context.Identity.Profile` +Writes to scoped path: `Request.Context.Providers...Identity.Profile` +Engine-defined View: `Request.Context.Views.Identity.Profile` Type: `PSCustomObject` (`PSTypeName = 'IdLE.Identity'`) Top-level properties: @@ -118,9 +119,12 @@ Top-level properties: | `OfficeLocation` | `string` | `officeLocation` | | `CompanyName` | `string` | `companyName` | +> **Attribute access**: Profile attributes are nested under the `Attributes` key. In Conditions, use `Request.Context.Views.Identity.Profile.Attributes.DisplayName` (or the scoped `Request.Context.Providers...Identity.Profile.Attributes.DisplayName`), **not** `Request.Context.Views.Identity.Profile.DisplayName` (or `Request.Context.Providers...Identity.Profile.DisplayName`). + ### Capability: `IdLE.Entitlement.List` -Writes to: `Request.Context.Identity.Entitlements` +Writes to scoped path: `Request.Context.Providers...Identity.Entitlements` +Engine-defined View: `Request.Context.Views.Identity.Entitlements` Type: `object[]` (array of `PSCustomObject`, `PSTypeName = 'IdLE.Entitlement'`) Each element represents one Entra ID group membership: @@ -135,7 +139,9 @@ Each element represents one Entra ID group membership: Notes: - The output paths are fixed by the engine and cannot be changed. -- Use these values in **Conditions**, **Preconditions**, and **Templates** (resolved during planning). +- Each entry is automatically annotated with `SourceProvider` and `SourceAuthSessionName` metadata. +- Use the global View (`Request.Context.Views.Identity.Entitlements`) in **Conditions** when you don't need to filter by provider. Use the scoped path when you need results from a specific provider only. +- See [Context Resolvers](../../use/workflows/context-resolver.md) for the full path reference. ## Configuration diff --git a/docs/reference/providers/provider-mock.md b/docs/reference/providers/provider-mock.md index 92df852c..937d6eab 100644 --- a/docs/reference/providers/provider-mock.md +++ b/docs/reference/providers/provider-mock.md @@ -67,7 +67,8 @@ This provider supports Context Resolvers for the allowlisted, read-only capabili ### Capability: `IdLE.Identity.Read` -Writes to: `Request.Context.Identity.Profile` +Writes to scoped path: `Request.Context.Providers...Identity.Profile` +Engine-defined View: `Request.Context.Views.Identity.Profile` Type: `PSCustomObject` (`PSTypeName = 'IdLE.Identity'`) Top-level properties: @@ -83,9 +84,12 @@ 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). +> **Attribute access**: Profile attributes are nested under the `Attributes` key. In Conditions, use the full path including `Attributes`, for example: `Request.Context.Views.Identity.Profile.Attributes.DisplayName` (or the scoped `Request.Context.Providers...Identity.Profile.Attributes.DisplayName`), **not** `Request.Context.Views.Identity.Profile.DisplayName`. + ### Capability: `IdLE.Entitlement.List` -Writes to: `Request.Context.Identity.Entitlements` +Writes to scoped path: `Request.Context.Providers...Identity.Entitlements` +Engine-defined View: `Request.Context.Views.Identity.Entitlements` Type: `object[]` (array of `PSCustomObject`, `PSTypeName = 'IdLE.Entitlement'`) Each element is normalized via `ConvertToEntitlement`: @@ -99,7 +103,9 @@ Each element is normalized via `ConvertToEntitlement`: Notes: - The output paths are fixed by the engine and cannot be changed. -- Use these values in **Conditions**, **Preconditions**, and **Templates** (resolved during planning). +- Each entry is automatically annotated with `SourceProvider` and `SourceAuthSessionName` metadata. +- Use the global View (`Request.Context.Views.Identity.Entitlements`) in **Conditions** when you don't need to filter by provider. Use the scoped path when you need results from a specific provider only. +- See [Context Resolvers](../../use/workflows/context-resolver.md) for the full path reference. ## Configuration diff --git a/docs/use/workflows/conditions.md b/docs/use/workflows/conditions.md index aec6fa60..b5d31b46 100644 --- a/docs/use/workflows/conditions.md +++ b/docs/use/workflows/conditions.md @@ -119,16 +119,16 @@ This section is the authoritative DSL reference. - Throws an error if `Path` resolves to a scalar ```powershell -# Check if a specific group DN is in the entitlements +# Check if a specific group DN is in the entitlements (using the global View populated by ContextResolvers) @{ Contains = @{ - Path = 'Request.Context.Identity.Entitlements.Id' + Path = 'Request.Context.Views.Identity.Entitlements.Id' Value = 'CN=BreakGlass-Users,OU=Groups,DC=example,DC=com' } } ``` -> **Note**: When `Request.Context.Identity.Entitlements` contains objects (e.g., `@{ Kind = 'Group'; Id = '...'; DisplayName = '...' }`), use `.Id` or `.DisplayName` to extract the property values: `Entitlements.Id` returns an array of all Id values. +> **Note**: When `Request.Context.Views.Identity.Entitlements` contains objects (e.g., `@{ Kind = 'Group'; Id = '...'; DisplayName = '...' }`), use `.Id` or `.DisplayName` to extract the property values: `Entitlements.Id` returns an array of all Id values. #### NotContains @@ -139,10 +139,10 @@ This section is the authoritative DSL reference. - Throws an error if `Path` resolves to a scalar ```powershell -# Prevent execution if identity has a specific group +# Prevent execution if identity has a specific group (using the global View populated by ContextResolvers) @{ NotContains = @{ - Path = 'Request.Context.Identity.Entitlements.Id' + Path = 'Request.Context.Views.Identity.Entitlements.Id' Value = 'CN=BreakGlass-Users,OU=Groups,DC=example,DC=com' } } @@ -157,18 +157,18 @@ This section is the authoritative DSL reference. - Uses PowerShell's `-like` operator (supports `*` and `?` wildcards) ```powershell -# Scalar example: check if DisplayName contains "Contractor" +# Scalar example: check if DisplayName matches a pattern (attributes are nested under Attributes key) @{ Like = @{ - Path = 'Request.Context.Identity.Profile.DisplayName' + Path = 'Request.Context.Views.Identity.Profile.Attributes.DisplayName' Pattern = '* (Contractor)' } } -# List example: check if any entitlement Id matches the pattern +# List example: check if any entitlement Id matches the pattern (using the global View) @{ Like = @{ - Path = 'Request.Context.Identity.Entitlements.Id' + Path = 'Request.Context.Views.Identity.Entitlements.Id' Pattern = 'CN=HR-*' } } @@ -185,18 +185,18 @@ This section is the authoritative DSL reference. - Uses PowerShell's `-notlike` operator (supports `*` and `?` wildcards) ```powershell -# Scalar example +# Scalar example (attributes are nested under Attributes key) @{ NotLike = @{ - Path = 'Request.Context.Identity.Profile.DisplayName' + Path = 'Request.Context.Views.Identity.Profile.Attributes.DisplayName' Pattern = '* (Contractor)' } } -# List example: ensure no HR groups in entitlements +# List example: ensure no HR groups in entitlements (using the global View) @{ NotLike = @{ - Path = 'Request.Context.Identity.Entitlements.Id' + Path = 'Request.Context.Views.Identity.Entitlements.Id' Pattern = 'CN=HR-*' } } @@ -216,9 +216,12 @@ This section is the authoritative DSL reference. When a `Path` points to a list of objects, you can access properties of those objects using dot notation: -- `Request.Context.Identity.Entitlements` → returns array of entitlement objects -- `Request.Context.Identity.Entitlements.Id` → returns array of all `Id` values -- `Request.Context.Identity.Entitlements.DisplayName` → returns array of all `DisplayName` values +- `Request.Context.Views.Identity.Entitlements` → returns array of entitlement objects +- `Request.Context.Views.Identity.Entitlements.Id` → returns array of all `Id` values +- `Request.Context.Views.Identity.Entitlements.DisplayName` → returns array of all `DisplayName` values + +> **Note**: These paths reference the **global View** populated by a `ContextResolvers` entry with `IdLE.Entitlement.List`. See [Context Resolvers](./context-resolver.md) for details. +> For provider-specific entitlements, use the scoped path: `Request.Context.Providers...Identity.Entitlements.Id` (where `` is the auth session key; `Default` is used when no `With.AuthSessionName` is specified). **Example**: ```powershell @@ -230,7 +233,7 @@ When a `Path` points to a list of objects, you can access properties of those ob # Check if any entitlement Id matches a pattern @{ Like = @{ - Path = 'Request.Context.Identity.Entitlements.Id' + Path = 'Request.Context.Views.Identity.Entitlements.Id' Pattern = 'CN=HR-*' } } @@ -299,7 +302,7 @@ Condition = @{ ```powershell 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' } } @@ -310,7 +313,7 @@ Condition = @{ ```powershell Condition = @{ NotLike = @{ - Path = 'Request.Context.Identity.Entitlements.Id' + Path = 'Request.Context.Views.Identity.Entitlements.Id' Pattern = 'CN=HR-*' } } @@ -321,7 +324,7 @@ Condition = @{ ```powershell Condition = @{ Like = @{ - Path = 'Request.Context.Identity.Profile.DisplayName' + Path = 'Request.Context.Views.Identity.Profile.Attributes.DisplayName' Pattern = '* (Contractor)' } } @@ -335,7 +338,7 @@ Condition = @{ @{ Equals = @{ Path = 'Plan.LifecycleEvent'; Value = 'Leaver' } } @{ NotContains = @{ - Path = 'Request.Context.Identity.Entitlements.Id' + Path = 'Request.Context.Views.Identity.Entitlements.Id' Value = 'CN=Protected-Accounts,OU=Groups,DC=example,DC=com' } } diff --git a/docs/use/workflows/templates.md b/docs/use/workflows/templates.md index 02a8daa0..b4c8c8ba 100644 --- a/docs/use/workflows/templates.md +++ b/docs/use/workflows/templates.md @@ -14,7 +14,7 @@ No ScriptBlocks or dynamic PowerShell expressions are supported. ## What is Template Substitution? -Template substitution resolves values from: `Request.*`g +Template substitution resolves values from: `Request.*` It replaces template placeholders with actual values before execution. diff --git a/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 b/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 index 3966f3c8..f7568be7 100644 --- a/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 +++ b/tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1 @@ -1061,6 +1061,117 @@ Describe 'New-IdlePlan - ContextResolvers' { } } + Context 'Profile attribute access via conditions (post-#259 model)' { + It 'condition using Views profile attribute path (Attributes.Department) marks step Planned when attribute matches' { + $wfPath = Join-Path $script:FixturesPath 'resolver-profile-attribute-condition.psd1' + + $req = New-IdleTestRequest -LifecycleEvent 'Joiner' -IdentityKeys @{ Id = 'user1' } + + $provider = New-IdleMockIdentityProvider -InitialStore @{ + 'user1' = @{ + IdentityKey = 'user1' + Enabled = $true + Attributes = @{ Department = 'Contractors' } + Entitlements = @() + } + } + + $providers = @{ + Identity = $provider + StepRegistry = @{ 'IdLE.Step.EmitEvent' = 'Invoke-IdleContextResolverTestNoopStep' } + } + + $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers + + $plan | Should -Not -BeNullOrEmpty + + # Verify attributes are nested under Attributes key in the resolver output + $profile = $plan.Request.Context.Providers.Identity.Default.Identity.Profile + $profile | Should -Not -BeNullOrEmpty + $profile.Attributes | Should -Not -BeNullOrEmpty + $profile.Attributes.Department | Should -Be 'Contractors' + + # View also populated and has nested attributes + $viewProfile = $plan.Request.Context.Views.Identity.Profile + $viewProfile | Should -Not -BeNullOrEmpty + $viewProfile.Attributes.Department | Should -Be 'Contractors' + + # ContractorStep: condition matches because Department attribute matches the 'Contractors' pattern + $contractorStep = $plan.Steps | Where-Object { $_.Name -eq 'ContractorStep' } + $contractorStep | Should -Not -BeNullOrEmpty + $contractorStep.Status | Should -Be 'Planned' + + # ScopedProfileStep: condition checks that Attributes exists on the scoped path + $scopedStep = $plan.Steps | Where-Object { $_.Name -eq 'ScopedProfileStep' } + $scopedStep | Should -Not -BeNullOrEmpty + $scopedStep.Status | Should -Be 'Planned' + } + + It 'condition using Views profile attribute path marks step NotApplicable when attribute does not match' { + $wfPath = Join-Path $script:FixturesPath 'resolver-profile-attribute-condition.psd1' + + $req = New-IdleTestRequest -LifecycleEvent 'Joiner' -IdentityKeys @{ Id = 'user1' } + + $provider = New-IdleMockIdentityProvider -InitialStore @{ + 'user1' = @{ + IdentityKey = 'user1' + Enabled = $true + # Department does not match the 'Contractors' pattern + Attributes = @{ Department = 'Engineering' } + Entitlements = @() + } + } + + $providers = @{ + Identity = $provider + StepRegistry = @{ 'IdLE.Step.EmitEvent' = 'Invoke-IdleContextResolverTestNoopStep' } + } + + $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers + + $plan | Should -Not -BeNullOrEmpty + + # ContractorStep: condition does not match — Department 'Engineering' does not match the 'Contractors' pattern + $contractorStep = $plan.Steps | Where-Object { $_.Name -eq 'ContractorStep' } + $contractorStep | Should -Not -BeNullOrEmpty + $contractorStep.Status | Should -Be 'NotApplicable' + } + + It 'profile attributes are nested under Attributes key, not promoted to top-level' { + $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 = 'John Doe'; Department = 'IT' } + Entitlements = @() + } + } + + $providers = @{ + Identity = $provider + StepRegistry = @{ 'IdLE.Step.EmitEvent' = 'Invoke-IdleContextResolverTestNoopStep' } + } + + $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers + + $profile = $plan.Request.Context.Providers.Identity.Default.Identity.Profile + + # Attributes are under the Attributes hashtable, not promoted to top-level + $profile.Attributes | Should -Not -BeNullOrEmpty + $profile.Attributes.DisplayName | Should -Be 'John Doe' + $profile.Attributes.Department | Should -Be 'IT' + + # Attributes are NOT promoted to the top level of the profile object + # (DisplayName is not a direct property of the profile PSCustomObject) + $profile.PSObject.Properties.Name | Should -Not -Contain 'DisplayName' + $profile.PSObject.Properties.Name | Should -Not -Contain 'Department' + } + } + Context 'Request.Context.Current alias (execution-time preconditions)' { It 'Current resolves to the step provider/auth scoped context during precondition evaluation' { $wfPath = Join-Path $script:FixturesPath 'resolver-current-precondition.psd1' diff --git a/tests/Core/Test-IdleCondition.Tests.ps1 b/tests/Core/Test-IdleCondition.Tests.ps1 index 09acc3bf..1aaa1a07 100644 --- a/tests/Core/Test-IdleCondition.Tests.ps1 +++ b/tests/Core/Test-IdleCondition.Tests.ps1 @@ -376,12 +376,14 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [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' } - ) + Views = [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' } + ) + } } } } @@ -389,7 +391,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,11 +403,13 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [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' } - ) + Views = [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' } + ) + } } } } @@ -413,7 +417,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' } } @@ -542,11 +546,13 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [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' } - ) + Views = [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' } + ) + } } } } @@ -554,7 +560,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,11 +572,13 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [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' } - ) + Views = [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' } + ) + } } } } @@ -578,7 +586,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,9 +598,11 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [pscustomobject]@{ - Identity = [pscustomobject]@{ - Profile = [pscustomobject]@{ - DisplayName = 'John Doe (Contractor)' + Views = [pscustomobject]@{ + Identity = [pscustomobject]@{ + Profile = [pscustomobject]@{ + Attributes = @{ 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,9 +623,11 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [pscustomobject]@{ - Identity = [pscustomobject]@{ - Profile = [pscustomobject]@{ - DisplayName = 'John Doe' + Views = [pscustomobject]@{ + Identity = [pscustomobject]@{ + Profile = [pscustomobject]@{ + Attributes = @{ DisplayName = 'John Doe' } + } } } } @@ -624,7 +636,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,12 +648,14 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [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' } - ) + Views = [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' } + ) + } } } } @@ -649,7 +663,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,11 +675,13 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [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' } - ) + Views = [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' } + ) + } } } } @@ -673,7 +689,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,9 +701,11 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [pscustomobject]@{ - Identity = [pscustomobject]@{ - Profile = [pscustomobject]@{ - DisplayName = 'John Doe' + Views = [pscustomobject]@{ + Identity = [pscustomobject]@{ + Profile = [pscustomobject]@{ + Attributes = @{ DisplayName = 'John Doe' } + } } } } @@ -696,7 +714,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,9 +726,11 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [pscustomobject]@{ - Identity = [pscustomobject]@{ - Profile = [pscustomobject]@{ - DisplayName = 'John Doe (Contractor)' + Views = [pscustomobject]@{ + Identity = [pscustomobject]@{ + Profile = [pscustomobject]@{ + Attributes = @{ DisplayName = 'John Doe (Contractor)' } + } } } } @@ -719,7 +739,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,11 +751,13 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [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' } - ) + Views = [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' } + ) + } } } } @@ -743,7 +765,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,11 +777,13 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [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' } - ) + Views = [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' } + ) + } } } } @@ -767,7 +791,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,11 +803,13 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [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' } - ) + Views = [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' } + ) + } } } } @@ -791,7 +817,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,9 +829,11 @@ Describe 'Condition DSL (schema + evaluator)' { $context = [pscustomobject]@{ Request = [pscustomobject]@{ Context = [pscustomobject]@{ - Identity = [pscustomobject]@{ - Profile = [pscustomobject]@{ - DisplayName = 'john doe (contractor)' + Views = [pscustomobject]@{ + Identity = [pscustomobject]@{ + Profile = [pscustomobject]@{ + Attributes = @{ DisplayName = 'john doe (contractor)' } + } } } } @@ -814,7 +842,7 @@ Describe 'Condition DSL (schema + evaluator)' { $condition = @{ Like = @{ - Path = 'Request.Context.Identity.Profile.DisplayName' + Path = 'Request.Context.Views.Identity.Profile.Attributes.DisplayName' Pattern = '* (CONTRACTOR)' } } diff --git a/tests/fixtures/workflows/resolver-profile-attribute-condition.psd1 b/tests/fixtures/workflows/resolver-profile-attribute-condition.psd1 new file mode 100644 index 00000000..77781767 --- /dev/null +++ b/tests/fixtures/workflows/resolver-profile-attribute-condition.psd1 @@ -0,0 +1,35 @@ +@{ + Name = 'Resolver Profile Attribute Condition Test' + LifecycleEvent = 'Joiner' + ContextResolvers = @( + @{ + Capability = 'IdLE.Identity.Read' + With = @{ + IdentityKey = 'user1' + Provider = 'Identity' + } + } + ) + Steps = @( + @{ + Name = 'ContractorStep' + Type = 'IdLE.Step.EmitEvent' + # Profile attributes are nested under Attributes key. + # Use Views path for the aggregated view across all providers. + Condition = @{ + Like = @{ + Path = 'Request.Context.Views.Identity.Profile.Attributes.Department' + Pattern = 'Contractors' + } + } + } + @{ + Name = 'ScopedProfileStep' + Type = 'IdLE.Step.EmitEvent' + # Scoped path: check attribute from the specific provider/session. + Condition = @{ + Exists = 'Request.Context.Providers.Identity.Default.Identity.Profile.Attributes' + } + } + ) +}