Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions docs/reference/providers/provider-ad.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<ProviderAlias>.<AuthSessionKey>.Identity.Profile`
Engine-defined View: `Request.Context.Views.Identity.Profile`
Type: `PSCustomObject` (`PSTypeName = 'IdLE.Identity'`)

Top-level properties:
Expand All @@ -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.<ProviderAlias>.<AuthSessionKey>.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.<ProviderAlias>.<AuthSessionKey>.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:
Expand All @@ -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

Expand Down
12 changes: 9 additions & 3 deletions docs/reference/providers/provider-entraID.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<ProviderAlias>.<AuthSessionKey>.Identity.Profile`
Engine-defined View: `Request.Context.Views.Identity.Profile`
Type: `PSCustomObject` (`PSTypeName = 'IdLE.Identity'`)

Top-level properties:
Expand All @@ -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.<ProviderAlias>.<AuthSessionKey>.Identity.Profile.Attributes.DisplayName`), **not** `Request.Context.Views.Identity.Profile.DisplayName` (or `Request.Context.Providers.<ProviderAlias>.<AuthSessionKey>.Identity.Profile.DisplayName`).

### Capability: `IdLE.Entitlement.List`

Writes to: `Request.Context.Identity.Entitlements`
Writes to scoped path: `Request.Context.Providers.<ProviderAlias>.<AuthSessionKey>.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:
Expand All @@ -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

Expand Down
12 changes: 9 additions & 3 deletions docs/reference/providers/provider-mock.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<ProviderAlias>.<AuthSessionKey>.Identity.Profile`
Engine-defined View: `Request.Context.Views.Identity.Profile`
Type: `PSCustomObject` (`PSTypeName = 'IdLE.Identity'`)

Top-level properties:
Expand All @@ -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.<ProviderAlias>.<AuthSessionKey>.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.<ProviderAlias>.<AuthSessionKey>.Identity.Entitlements`
Engine-defined View: `Request.Context.Views.Identity.Entitlements`
Type: `object[]` (array of `PSCustomObject`, `PSTypeName = 'IdLE.Entitlement'`)

Each element is normalized via `ConvertToEntitlement`:
Expand All @@ -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

Expand Down
45 changes: 24 additions & 21 deletions docs/use/workflows/conditions.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
title: Conditions
sidebar_label: Conditions
Expand Down Expand Up @@ -119,16 +119,16 @@
- 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

Expand All @@ -139,10 +139,10 @@
- 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'
}
}
Expand All @@ -157,18 +157,18 @@
- 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-*'
}
}
Expand All @@ -185,18 +185,18 @@
- 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-*'
}
}
Expand All @@ -216,9 +216,12 @@

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.<ProviderAlias>.<AuthSessionKey>.Identity.Entitlements.Id` (where `<AuthSessionKey>` is the auth session key; `Default` is used when no `With.AuthSessionName` is specified).

**Example**:
```powershell
Expand All @@ -230,7 +233,7 @@
# 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-*'
}
}
Expand Down Expand Up @@ -299,7 +302,7 @@
```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'
}
}
Expand All @@ -310,7 +313,7 @@
```powershell
Condition = @{
NotLike = @{
Path = 'Request.Context.Identity.Entitlements.Id'
Path = 'Request.Context.Views.Identity.Entitlements.Id'
Pattern = 'CN=HR-*'
}
}
Expand All @@ -321,7 +324,7 @@
```powershell
Condition = @{
Like = @{
Path = 'Request.Context.Identity.Profile.DisplayName'
Path = 'Request.Context.Views.Identity.Profile.Attributes.DisplayName'
Pattern = '* (Contractor)'
}
}
Expand All @@ -335,7 +338,7 @@
@{ 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'
}
}
Expand Down
2 changes: 1 addition & 1 deletion docs/use/workflows/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
111 changes: 111 additions & 0 deletions tests/Core/New-IdlePlan.ContextResolvers.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Loading
Loading