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
6 changes: 4 additions & 2 deletions docs/reference/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,10 @@ Each capability writes to a **predefined, fixed path** under `Request.Context`.
ContextResolvers = @(
@{
Capability = 'IdLE.Entitlement.List'
Provider = 'Identity' # optional; auto-selected if omitted
With = @{ IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' }
With = @{
IdentityKey = '{{Request.IdentityKeys.EmployeeId}}'
Provider = 'Identity' # optional; auto-selected if omitted
}
# Writes to Request.Context.Identity.Entitlements (predefined, not configurable)
}
@{
Expand Down
19 changes: 12 additions & 7 deletions docs/use/workflows/context-resolver.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ A resolver entry is defined at workflow root level:
ContextResolvers = @(
@{
Capability = 'IdLE.Identity.Read'
Provider = 'Identity' # optional
With = @{
IdentityKey = '{{Request.IdentityKeys.EmployeeId}}'
IdentityKey = '{{Request.IdentityKeys.EmployeeId}}'
Provider = 'Identity' # optional; auto-selected if omitted
AuthSessionName = 'Tier0' # optional; requires AuthSessionBroker in Providers
}
}

Expand Down Expand Up @@ -83,12 +84,16 @@ A resolver entry is defined at workflow root level:
- `Capability` (required)
A permitted read-only capability.

- `Provider` (optional)
Provider alias. If omitted, IdLE selects a provider advertising the capability.

- `With` (required)
- `With` (hashtable, optional — required in practice, as capabilities need at least `IdentityKey`)
Inputs required by the capability. Template substitution is supported.

| `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. |

Output paths are predefined and cannot be changed.

---
Expand Down Expand Up @@ -156,7 +161,7 @@ Condition = @{ Exists = 'Request.Context.Identity.Entitlements' }

### Ambiguous provider

- If multiple providers advertise a capability, specify `Provider` explicitly.
- If multiple providers advertise a capability, specify `With.Provider` explicitly.

### Context value missing

Expand Down
7 changes: 3 additions & 4 deletions examples/workflows/mock/joiner-with-context-resolvers.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@
# Writes to Request.Context.Identity.Entitlements (predefined).
Capability = 'IdLE.Entitlement.List'

# The provider alias that supports IdLE.Entitlement.List.
# If omitted, the first provider advertising the capability is used.
Provider = 'Identity'

# Resolver inputs.
With = @{
IdentityKey = 'user1'
# Provider alias that supports IdLE.Entitlement.List.
# If omitted, the provider is auto-selected when exactly one match exists.
Provider = 'Identity'
}
}
)
Expand Down
12 changes: 6 additions & 6 deletions src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ function Invoke-IdleContextResolvers {
- Only capabilities in the read-only allow-list (Get-IdleReadOnlyCapabilities) may be used.
- Each capability writes to a fixed, predefined path under Request.Context.
The output path is not user-configurable.
- Provider is selected by alias when 'Provider' is specified; otherwise the first
provider (sorted by alias) that advertises the capability is used; ambiguity when
multiple providers match causes a fail-fast error.
- Provider is selected by alias when 'With.Provider' is specified. When 'With.Provider'
is omitted, auto-selection only succeeds if exactly one provider advertises the
capability; zero matches or multiple matches both cause a fail-fast error.
- Auth sessions are supported via With.AuthSessionName / With.AuthSessionOptions,
using the AuthSessionBroker in Providers (same pattern as step execution).

Expand Down Expand Up @@ -93,8 +93,8 @@ function Invoke-IdleContextResolvers {
$with = Resolve-IdleWorkflowTemplates -Value $with -Request $Request -StepName $resolverPath

# --- Provider selection ---
$providerAlias = if ($resolver.Contains('Provider') -and -not [string]::IsNullOrWhiteSpace([string]$resolver.Provider)) {
[string]$resolver.Provider
$providerAlias = if ($with -is [System.Collections.IDictionary] -and $with.Contains('Provider') -and -not [string]::IsNullOrWhiteSpace([string]$with.Provider)) {
[string]$with.Provider
}
else {
$null
Expand Down Expand Up @@ -244,7 +244,7 @@ function Select-IdleResolverProviderAlias {
if ($matchingAliases.Count -gt 1) {
$aliasList = $matchingAliases -join ', '
throw [System.ArgumentException]::new(
"${ResolverPath}: Multiple providers advertise capability '$Capability': $aliasList. Specify 'Provider' in the resolver to disambiguate.",
"${ResolverPath}: Multiple providers advertise capability '$Capability': $aliasList. Specify 'With.Provider' in the resolver to disambiguate.",
'Providers'
)
}
Expand Down
26 changes: 18 additions & 8 deletions src/IdLE.Core/Private/Test-IdleWorkflowSchema.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ function Test-IdleWorkflowSchema {
}
else {
# 'To' is not user-configurable; each capability has a predefined output path.
$allowedResolverKeys = @('Capability', 'Provider', 'With')
$allowedResolverKeys = @('Capability', 'With')

$i = 0
foreach ($resolver in $Workflow.ContextResolvers) {
Expand All @@ -261,13 +261,23 @@ function Test-IdleWorkflowSchema {
}

# 'With' is optional but must be a hashtable if present.
if ($resolver.ContainsKey('With') -and $null -ne $resolver.With -and $resolver.With -isnot [hashtable]) {
$errors.Add("'$resolverPath.With' must be a hashtable (resolver input parameters).")
}

# 'Provider' is optional but must be a non-empty string if present.
if ($resolver.ContainsKey('Provider') -and $null -ne $resolver.Provider -and [string]::IsNullOrWhiteSpace([string]$resolver.Provider)) {
$errors.Add("'$resolverPath.Provider' must not be an empty string.")
if ($resolver.ContainsKey('With') -and $null -ne $resolver.With) {
if ($resolver.With -isnot [hashtable]) {
$errors.Add("'$resolverPath.With' must be a hashtable (resolver input parameters).")
}
else {
$with = $resolver.With

# 'With.Provider' is optional but must be a non-empty string if present.
if ($with.ContainsKey('Provider') -and $null -ne $with.Provider -and [string]::IsNullOrWhiteSpace([string]$with.Provider)) {
$errors.Add("'$resolverPath.With.Provider' must not be an empty string.")
}

# 'With.AuthSessionOptions' must be a hashtable if present.
if ($with.ContainsKey('AuthSessionOptions') -and $null -ne $with.AuthSessionOptions -and $with.AuthSessionOptions -isnot [hashtable]) {
$errors.Add("'$resolverPath.With.AuthSessionOptions' must be a hashtable.")
}
}
}

$i++
Expand Down
86 changes: 86 additions & 0 deletions tests/Core/Test-IdleWorkflowSchema.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,89 @@ Describe 'Workflow schema validation - Condition/Precondition DSL parity' {
}
}
}

Describe 'Workflow schema validation - ContextResolvers' {
InModuleScope 'IdLE.Core' {
It 'rejects root-level Provider key in a resolver entry (must use With.Provider)' {
$workflow = @{
Name = 'Root Provider Rejected'
LifecycleEvent = 'Joiner'
ContextResolvers = @(
@{
Capability = 'IdLE.Entitlement.List'
Provider = 'Identity'
With = @{ IdentityKey = 'user1' }
}
)
Steps = @(
@{ Name = 'Step1'; Type = 'IdLE.Step.Noop' }
)
}

$errors = Test-IdleWorkflowSchema -Workflow $workflow
@($errors | Where-Object { $_ -like "*Unknown key*Provider*" }).Count | Should -BeGreaterThan 0
}

It 'rejects With.Provider as empty string' {
$workflow = @{
Name = 'With.Provider Empty'
LifecycleEvent = 'Joiner'
ContextResolvers = @(
@{
Capability = 'IdLE.Entitlement.List'
With = @{ IdentityKey = 'user1'; Provider = '' }
}
)
Steps = @(
@{ Name = 'Step1'; Type = 'IdLE.Step.Noop' }
)
}

$errors = Test-IdleWorkflowSchema -Workflow $workflow
@($errors | Where-Object { $_ -like "*With.Provider*must not be an empty string*" }).Count | Should -BeGreaterThan 0
}

It 'rejects With.AuthSessionOptions as a non-hashtable' {
$workflow = @{
Name = 'With.AuthSessionOptions Invalid'
LifecycleEvent = 'Joiner'
ContextResolvers = @(
@{
Capability = 'IdLE.Entitlement.List'
With = @{ IdentityKey = 'user1'; AuthSessionName = 'Tier0'; AuthSessionOptions = 'not-a-hashtable' }
}
)
Steps = @(
@{ Name = 'Step1'; Type = 'IdLE.Step.Noop' }
)
}

$errors = Test-IdleWorkflowSchema -Workflow $workflow
@($errors | Where-Object { $_ -like "*With.AuthSessionOptions*must be a hashtable*" }).Count | Should -BeGreaterThan 0
}

It 'accepts a valid resolver with With.Provider and With.AuthSessionOptions' {
$workflow = @{
Name = 'Valid Resolver With.Provider'
LifecycleEvent = 'Joiner'
ContextResolvers = @(
@{
Capability = 'IdLE.Entitlement.List'
With = @{
IdentityKey = 'user1'
Provider = 'Identity'
AuthSessionName = 'Tier0'
AuthSessionOptions = @{ Role = 'Tier0' }
}
}
)
Steps = @(
@{ Name = 'Step1'; Type = 'IdLE.Step.Noop' }
)
}

$errors = Test-IdleWorkflowSchema -Workflow $workflow
@($errors).Count | Should -Be 0
}
}
}
6 changes: 4 additions & 2 deletions tests/fixtures/workflows/resolver-condition.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
ContextResolvers = @(
@{
Capability = 'IdLE.Entitlement.List'
Provider = 'Identity'
With = @{ IdentityKey = 'user1' }
With = @{
IdentityKey = 'user1'
Provider = 'Identity'
}
}
)
Steps = @(
Expand Down
6 changes: 4 additions & 2 deletions tests/fixtures/workflows/resolver-context-type-conflict.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
ContextResolvers = @(
@{
Capability = 'IdLE.Entitlement.List'
Provider = 'Identity'
With = @{ IdentityKey = 'user1' }
With = @{
IdentityKey = 'user1'
Provider = 'Identity'
}
}
)
Steps = @(
Expand Down
6 changes: 4 additions & 2 deletions tests/fixtures/workflows/resolver-empty-entitlements.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
ContextResolvers = @(
@{
Capability = 'IdLE.Entitlement.List'
Provider = 'Identity'
With = @{ IdentityKey = 'user2' }
With = @{
IdentityKey = 'user2'
Provider = 'Identity'
}
}
)
Steps = @(
Expand Down
6 changes: 4 additions & 2 deletions tests/fixtures/workflows/resolver-identity-read.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
ContextResolvers = @(
@{
Capability = 'IdLE.Identity.Read'
Provider = 'Identity'
With = @{ IdentityKey = 'user1' }
With = @{
IdentityKey = 'user1'
Provider = 'Identity'
}
}
)
Steps = @(
Expand Down
6 changes: 4 additions & 2 deletions tests/fixtures/workflows/resolver-snapshot.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
ContextResolvers = @(
@{
Capability = 'IdLE.Entitlement.List'
Provider = 'Identity'
With = @{ IdentityKey = 'snap-user' }
With = @{
IdentityKey = 'snap-user'
Provider = 'Identity'
}
}
)
Steps = @(
Expand Down
6 changes: 4 additions & 2 deletions tests/fixtures/workflows/resolver-template.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
ContextResolvers = @(
@{
Capability = 'IdLE.Entitlement.List'
Provider = 'Identity'
With = @{ IdentityKey = '{{Request.IdentityKeys.Id}}' }
With = @{
IdentityKey = '{{Request.IdentityKeys.Id}}'
Provider = 'Identity'
}
}
)
Steps = @(
Expand Down
6 changes: 3 additions & 3 deletions tests/fixtures/workflows/resolver-with-auth-session.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
ContextResolvers = @(
@{
Capability = 'IdLE.Entitlement.List'
Provider = 'Identity'
With = @{
IdentityKey = 'user1'
AuthSessionName = 'TestSession'
IdentityKey = 'user1'
Provider = 'Identity'
AuthSessionName = 'TestSession'
}
}
)
Expand Down