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
38 changes: 38 additions & 0 deletions docs/reference/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,41 @@ Recommended provider documentation pattern:

- List supported capabilities in the provider documentation.
- If a capability is only partially supported (e.g., limited attribute set), document constraints explicitly.


---

## ContextResolvers: read-only capabilities and predefined Context paths

Workflows may declare a `ContextResolvers` section to populate `Request.Context.*` at planning time using read-only provider capabilities. Only the capabilities listed below are permitted in `ContextResolvers`.

Each capability writes to a **predefined, fixed path** under `Request.Context`. This path is not user-configurable, which prevents accidental overwrites and ensures a consistent context shape across all workflows.

| Capability | Predefined `Request.Context` path | Required `With` keys |
|---|---|---|
| `IdLE.Entitlement.List` | `Request.Context.Identity.Entitlements` | `IdentityKey` (string) |
| `IdLE.Identity.Read` | `Request.Context.Identity.Profile` | `IdentityKey` (string) |

### Example

```powershell
ContextResolvers = @(
@{
Capability = 'IdLE.Entitlement.List'
Provider = 'Identity' # optional; auto-selected if omitted
With = @{ IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' }
# Writes to Request.Context.Identity.Entitlements (predefined, not configurable)
}
@{
Capability = 'IdLE.Identity.Read'
With = @{ IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' }
# Writes to Request.Context.Identity.Profile (predefined, not configurable)
}
)
```

Steps can then reference the resolved data in their `Condition`:

```powershell
Condition = @{ Exists = 'Request.Context.Identity.Entitlements' }
```
59 changes: 59 additions & 0 deletions examples/workflows/mock/joiner-with-context-resolvers.psd1
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
@{
# Joiner workflow demonstrating ContextResolvers to enrich Request.Context at planning time.
#
# ContextResolvers run BEFORE step conditions are evaluated. They use read-only provider
# capabilities to fetch data and write it under Request.Context.*.
#
# Each capability writes to a predefined path (no user-configurable 'To'):
# IdLE.Entitlement.List -> Request.Context.Identity.Entitlements
# IdLE.Identity.Read -> Request.Context.Identity.Profile

Name = 'Joiner - ContextResolvers Demo'
LifecycleEvent = 'Joiner'

# Planning-time resolvers: run before condition evaluation.
# Each capability has a predefined output path under Request.Context.*
ContextResolvers = @(
@{
# Fetch current entitlements for the identity being onboarded.
# 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'
}
}
)

Steps = @(
@{
# Always runs - ensures the base group membership.
Name = 'EnsureBaseGroup'
Type = 'IdLE.Step.EnsureEntitlement'
With = @{
IdentityKey = 'user1'
Entitlement = @{ Kind = 'Group'; Id = 'all-employees' }
State = 'Present'
Provider = 'Identity'
}
}

@{
# Runs only when entitlements were successfully pre-resolved by the ContextResolver.
# References the predefined context path for IdLE.Entitlement.List.
Name = 'EmitContextAvailable'
Type = 'IdLE.Step.EmitEvent'
Condition = @{
Exists = 'Request.Context.Identity.Entitlements'
}
With = @{
Message = 'Entitlement context was pre-resolved by ContextResolvers and is available for planning.'
}
}
)
}
81 changes: 81 additions & 0 deletions src/IdLE.Core/Private/Get-IdleReadOnlyCapabilities.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
Set-StrictMode -Version Latest

function Get-IdleReadOnlyCapabilities {
<#
.SYNOPSIS
Returns the allow-list of read-only capabilities usable in ContextResolvers.

.DESCRIPTION
ContextResolvers may only invoke capabilities from this allow-list.
This enforces the read-only guarantee at planning time: resolvers cannot
trigger mutations or side effects via the planning pipeline.

Only capabilities that are safe to invoke at planning time (no side effects,
deterministic, serializable output) should be added to this list.

Each capability in this list has a predefined output path in Request.Context
(see Get-IdleCapabilityContextPath).

.OUTPUTS
String[]

.EXAMPLE
$allowed = Get-IdleReadOnlyCapabilities
# Returns: @('IdLE.Entitlement.List', 'IdLE.Identity.Read')
#>
[CmdletBinding()]
[OutputType([string[]])]
param()

return @(
'IdLE.Entitlement.List'
Comment thread
blindzero marked this conversation as resolved.
'IdLE.Identity.Read'
)
}

function Get-IdleCapabilityContextPath {
<#
.SYNOPSIS
Returns the predefined Request.Context sub-path for a read-only capability.

.DESCRIPTION
Each read-only capability allowed in ContextResolvers writes its result to a fixed,
predefined path under Request.Context. This prevents user-configurable overwrites
and ensures consistent context shape across workflows.

The path is relative to Request.Context (e.g., 'Identity.Entitlements' maps to
Request.Context.Identity.Entitlements).

.PARAMETER Capability
The capability identifier (must be in the read-only allow-list).

.OUTPUTS
String

.EXAMPLE
Get-IdleCapabilityContextPath -Capability 'IdLE.Entitlement.List'
# Returns: 'Identity.Entitlements'

.EXAMPLE
Get-IdleCapabilityContextPath -Capability 'IdLE.Identity.Read'
# Returns: 'Identity.Profile'
#>
[CmdletBinding()]
[OutputType([string])]
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $Capability
)

switch ($Capability) {
'IdLE.Entitlement.List' { return 'Identity.Entitlements' }
'IdLE.Identity.Read' { return 'Identity.Profile' }
default {
throw [System.ArgumentException]::new(
"No predefined context path defined for capability '$Capability'.",
'Capability'
)
}
}
}
Loading