diff --git a/docs/reference/providers/_provider-name_template.md b/docs/reference/providers/_provider-name_template.md index b6620357..a2780118 100644 --- a/docs/reference/providers/_provider-name_template.md +++ b/docs/reference/providers/_provider-name_template.md @@ -117,6 +117,37 @@ $result = Invoke-IdlePlan -Plan $plan -Providers $providers --- + +## Context Resolvers + +> Context Resolvers populate **`Request.Context.*` during planning** using **read-only** provider capabilities. +> Workflow authors can then reference the resolved values in **Conditions**, **Preconditions**, and **Templates**. + +### Supported Context Resolver capabilities + +> Document, **per supported read-only capability**, what your provider writes into `Request.Context.*`. +> - If your provider supports none of the allowlisted capabilities, state that explicitly. +> - Keep this section **reference-style**: focus on *paths*, *shapes*, and *types* (including nested properties). +> - Output paths are **predefined** by the engine and **cannot be changed** by workflow authors. + +#### Capability: `IdLE.Capability.Path` + +Writes to: `Request.Context.Target` +Type: `OutputType` (`PSTypeName = 'IdLE.Identity'`) + +Top-level properties: + +| Property | Type | Notes | +| --- | --- | --- | +| `PSTypeName` | `string` | Always `IdLE.Identity`. | +| `Property1` | `property1-type` | property1 description | +| `PropertyN` | `propertyN-type` | propertyN description | +| `PropertyX` | `hashtable` | optional in case of Key/value bag; keys are strings; values are provider-defined (commonly `string`). | + +`PropertyX` contents: +- List the attributes you populate and their types. +- Only document what workflow authors can *rely on* (stable contract, not incidental Graph/AD fields). + ## Configuration ### Provider creation diff --git a/docs/reference/providers/provider-ad.md b/docs/reference/providers/provider-ad.md index 7e42da22..0fdfef4f 100644 --- a/docs/reference/providers/provider-ad.md +++ b/docs/reference/providers/provider-ad.md @@ -86,6 +86,57 @@ The AD provider supports the common identity lifecycle and entitlement operation | `IdLE.Step.RemoveEntitlement` | Remove managed groups | Prefer explicit allow-lists / managed lists | | `IdLE.Step.DeleteIdentity` | Delete user | **Opt-in** via `-AllowDelete` (see Configuration) | +## Context Resolvers + +This provider supports Context Resolvers for the allowlisted, read-only capabilities below. + +### Capability: `IdLE.Identity.Read` + +Writes to: `Request.Context.Identity.Profile` +Type: `PSCustomObject` (`PSTypeName = 'IdLE.Identity'`) + +Top-level properties: + +| Property | Type | Notes | +| --- | --- | --- | +| `PSTypeName` | `string` | Always `IdLE.Identity`. | +| `IdentityKey` | `string` | The identity key used by the workflow (GUID/UPN/sAMAccountName). | +| `Enabled` | `bool` | Derived from AD user `Enabled`. | +| `Attributes` | `hashtable` | Key/value bag; keys are strings; values are typically `string`. | + +`Attributes` keys populated by this provider (when present on the AD user object): + +| Attribute key | Type | +| --- | --- | +| `GivenName` | `string` | +| `Surname` | `string` | +| `DisplayName` | `string` | +| `Description` | `string` | +| `Department` | `string` | +| `Title` | `string` | +| `EmailAddress` | `string` | +| `UserPrincipalName` | `string` | +| `sAMAccountName` | `string` | +| `DistinguishedName` | `string` | + +### Capability: `IdLE.Entitlement.List` + +Writes to: `Request.Context.Identity.Entitlements` +Type: `object[]` (array of `PSCustomObject`, `PSTypeName = 'IdLE.Entitlement'`) + +Each element represents one AD group membership: + +| Property | Type | Notes | +| --- | --- | --- | +| `PSTypeName` | `string` | Always `IdLE.Entitlement`. | +| `Kind` | `string` | Always `Group`. | +| `Id` | `string` | AD group `DistinguishedName`. | +| `DisplayName` | `string` | AD group `Name`. | + +Notes: +- The output paths are fixed by the engine and cannot be changed. +- Use these values in **Conditions**, **Preconditions**, and **Templates** (resolved during planning). + ## Configuration ### Provider factory diff --git a/docs/reference/providers/provider-directorysync-entraconnect.md b/docs/reference/providers/provider-directorysync-entraconnect.md index e3ed42d3..f4a4ecb5 100644 --- a/docs/reference/providers/provider-directorysync-entraconnect.md +++ b/docs/reference/providers/provider-directorysync-entraconnect.md @@ -92,6 +92,13 @@ Those are typically used by step types like: - `IdLE.Step.TriggerDirectorySync` (trigger + optional wait/poll) +## Context Resolvers + +This provider does **not** support any of the allowlisted Context Resolver capabilities. + +Context Resolvers can only use read-only capabilities like `IdLE.Identity.Read` and `IdLE.Entitlement.List`. +This provider does not advertise these capabilities, so it cannot be used in the workflow `ContextResolvers` section. + ## Configuration This provider has no admin-facing option bag. Configuration is done through: diff --git a/docs/reference/providers/provider-entraID.md b/docs/reference/providers/provider-entraID.md index 1667fef5..da8a984e 100644 --- a/docs/reference/providers/provider-entraID.md +++ b/docs/reference/providers/provider-entraID.md @@ -86,6 +86,57 @@ Recommended wiring in examples: - `AuthSessionOptions = @{ Role = 'Admin' }` for routing (optional) - Use a more privileged role only for privileged actions (e.g. delete) +## Context Resolvers + +This provider supports Context Resolvers for the allowlisted, read-only capabilities below. + +### Capability: `IdLE.Identity.Read` + +Writes to: `Request.Context.Identity.Profile` +Type: `PSCustomObject` (`PSTypeName = 'IdLE.Identity'`) + +Top-level properties: + +| Property | Type | Notes | +| --- | --- | --- | +| `PSTypeName` | `string` | Always `IdLE.Identity`. | +| `IdentityKey` | `string` | The identity key used by the workflow (typically the Entra user `id`). | +| `Enabled` | `bool` | Derived from Entra user `accountEnabled`. | +| `Attributes` | `hashtable` | Key/value bag; keys are strings; values are typically `string`. | + +`Attributes` keys populated by this provider (when present on the user object): + +| Attribute key | Type | Source (Graph field) | +| --- | --- | --- | +| `GivenName` | `string` | `givenName` | +| `Surname` | `string` | `surname` | +| `DisplayName` | `string` | `displayName` | +| `UserPrincipalName` | `string` | `userPrincipalName` | +| `Mail` | `string` | `mail` | +| `Department` | `string` | `department` | +| `JobTitle` | `string` | `jobTitle` | +| `OfficeLocation` | `string` | `officeLocation` | +| `CompanyName` | `string` | `companyName` | + +### Capability: `IdLE.Entitlement.List` + +Writes to: `Request.Context.Identity.Entitlements` +Type: `object[]` (array of `PSCustomObject`, `PSTypeName = 'IdLE.Entitlement'`) + +Each element represents one Entra ID group membership: + +| Property | Type | Notes | +| --- | --- | --- | +| `PSTypeName` | `string` | Always `IdLE.Entitlement`. | +| `Kind` | `string` | Always `Group`. | +| `Id` | `string` | Entra group `id`. | +| `DisplayName` | `string` or `$null` | Group `displayName` (if returned by the adapter). | +| `Mail` | `string` or `$null` | Group `mail` (if returned by the adapter). | + +Notes: +- The output paths are fixed by the engine and cannot be changed. +- Use these values in **Conditions**, **Preconditions**, and **Templates** (resolved during planning). + ## Configuration ### Provider constructor / factory diff --git a/docs/reference/providers/provider-exchangeonline.md b/docs/reference/providers/provider-exchangeonline.md index 89c09ede..064d4790 100644 --- a/docs/reference/providers/provider-exchangeonline.md +++ b/docs/reference/providers/provider-exchangeonline.md @@ -156,6 +156,13 @@ For **app-only** flows, the token's `roles` claim must include: --- +## Context Resolvers + +This provider does **not** support any of the allowlisted Context Resolver capabilities. + +Context Resolvers can only use read-only capabilities like `IdLE.Identity.Read` and `IdLE.Entitlement.List`. +This provider does not advertise these capabilities, so it cannot be used in the workflow `ContextResolvers` section. + ## Configuration ### Provider creation diff --git a/docs/reference/providers/provider-mock.md b/docs/reference/providers/provider-mock.md index 149e5b01..92df852c 100644 --- a/docs/reference/providers/provider-mock.md +++ b/docs/reference/providers/provider-mock.md @@ -61,6 +61,46 @@ No authentication is required. The Mock provider ignores `AuthSessionName`. - Identity: create/update attributes (in-memory) - Entitlements: ensure/remove group memberships (in-memory) +## Context Resolvers + +This provider supports Context Resolvers for the allowlisted, read-only capabilities below. + +### Capability: `IdLE.Identity.Read` + +Writes to: `Request.Context.Identity.Profile` +Type: `PSCustomObject` (`PSTypeName = 'IdLE.Identity'`) + +Top-level properties: + +| Property | Type | Notes | +| --- | --- | --- | +| `PSTypeName` | `string` | Always `IdLE.Identity`. | +| `IdentityKey` | `string` | The identity key used by the workflow. | +| `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. | + +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). + +### Capability: `IdLE.Entitlement.List` + +Writes to: `Request.Context.Identity.Entitlements` +Type: `object[]` (array of `PSCustomObject`, `PSTypeName = 'IdLE.Entitlement'`) + +Each element is normalized via `ConvertToEntitlement`: + +| Property | Type | Notes | +| --- | --- | --- | +| `PSTypeName` | `string` | Always `IdLE.Entitlement`. | +| `Kind` | `string` | Required; non-empty. | +| `Id` | `string` | Required; non-empty. | +| `DisplayName` | `string` or `$null` | Optional. | + +Notes: +- The output paths are fixed by the engine and cannot be changed. +- Use these values in **Conditions**, **Preconditions**, and **Templates** (resolved during planning). + ## Configuration This provider has no admin-facing options. diff --git a/docs/use/workflows.md b/docs/use/workflows.md index b5b4b108..c1f16518 100644 --- a/docs/use/workflows.md +++ b/docs/use/workflows.md @@ -3,163 +3,171 @@ title: Workflows & Steps sidebar_label: Workflows / Steps --- -Workflows define **what** IdLE should do for a lifecycle event (Joiner/Mover/Leaver). +Workflows are **data-only** PowerShell hashtables (`.psd1`) that describe **which steps** should be planned and executed for a specific lifecycle event. Workflows define **what** IdLE should do for a lifecycle event (Joiner/Mover/Leaver). A workflow is a **data-only** PowerShell hashtable stored in a `.psd1` file. It describes the ordered steps to execute, plus optional conditions and error handling. +Workflows are designed for **admins and workflow authors**: + +- You define *what should happen* (steps and their configuration). +- IdLE builds a **plan** and then **executes** it. +- Providers implement the system-specific operations. + +--- + +## How workflows are used in the lifecycle + +1. You write the workflow definition (`.psd1`). +2. You create a request (intent + inputs). +3. You build a plan (IdLE validates and resolves templates). +4. You invoke the plan. + :::info -For specification-level details (schema, templates, conditions, and validation rules), use the [Reference](../reference/intro-reference.md) section. +For specification-level details on step types, use the [Step Reference](../reference/steps.md) section. \ +Otherwise, start with [Quick Start](quickstart.md). ::: --- -## What a workflow contains - -At a high level, a workflow contains: +## Plan vs Execute -- metadata (name, lifecycle event) -- a list of steps (ordered) -- per-step configuration (`With`) -- optional execution logic (conditions, `OnFailureSteps`, etc.) +When you run IdLE, it happens in two distinct phases: -The Big Picture is described in [Concepts](../about/concepts.md). +1. **Planning (Plan Build)** + IdLE reads the workflow definition and builds a plan of steps. -### Step execution controls + - `Condition` is evaluated here. + - If a condition is false, the step is marked as `NotApplicable`. -Each step supports several optional execution control properties: +2. **Execution (Plan Run)** + IdLE executes the planned steps and records results. -| Property | Evaluated at | Purpose | -|---|---|---| -| `Condition` | Plan time | Include or skip the step based on request/intent data. | -| `Precondition` | Execution time (runtime) | Guard the step against stale or unsafe state immediately before it runs. See [Runtime Preconditions](workflows/preconditions.md). | -| `OnFailureSteps` | After failure (workflow-level) | Cleanup/rollback steps run after a primary step fails. | + - `Precondition` is evaluated here. + - If a precondition is false, `OnPreconditionFalse` decides what happens (`Blocked`, `Fail`, or `Continue`). `Continue` causes the step to be recorded with status `PreconditionSkipped`. --- -## Minimal workflow example +## Workflow example + +This example shows a small workflow with: + +- a value containing a [template substitution](./workflows/templates.md) +- a step that is only applicable for `Joiner` ([Condition](./workflows/conditions.md)) +- a step that is guarded at runtime ([Preconditions](./workflows/preconditions.md)) + ```powershell @{ - Name = 'Joiner - Minimal' + Name = 'Joiner - Standard' LifecycleEvent = 'Joiner' Steps = @( @{ Name = 'Emit start' Type = 'IdLE.Step.EmitEvent' - With = @{ - Message = 'Starting Joiner workflow' - } + With = @{ Message = 'Starting Joiner for {{Request.Intent.FullName}}' } } - ) -} -``` - ---- -## How workflows are used in the lifecycle + @{ + Name = 'Provision only for Joiner' + Type = 'IdLE.Step.EmitEvent' -1. You write the workflow definition (`.psd1`). -2. You create a request (intent + inputs). -3. You build a plan (IdLE validates and resolves templates). -4. You invoke the plan. + Condition = @{ + Equals = @{ Path = 'Plan.LifecycleEvent'; Value = 'Joiner' } + } -Start with [Quick Start](quickstart.md). + With = @{ Message = 'Provisioning for Joiner' } + } ---- + @{ + Name = 'Disable identity only if it exists' + Type = 'IdLE.Step.DisableIdentity' -## Common pitfalls + Precondition = @{ + Equals = @{ Path = 'Request.Context.IdentityExists'; Value = 'True' } + } -- **Not data-only:** embedding ScriptBlocks or secrets in workflow files (not allowed). -- **Wrong StepType name:** the step module is not imported or the type name is wrong. -- **Missing provider alias:** `With.Provider = 'Identity'` but the host did not supply that alias. -- **Template paths resolve to null:** the referenced request/identity data is missing. + OnPreconditionFalse = 'Continue' + } + ) +} +``` --- -## Template substitution +## What a workflow contains -Step configuration values (`With.*`) support `{{path}}` placeholders that are resolved against the -request during plan build (`New-IdlePlan`). Multiple placeholders may appear in a single value. +At a high level, a workflow contains: -```powershell -IdentityKey = '{{Request.IdentityKeys.sAMAccountName}}' -DisplayName = '{{Request.Intent.GivenName}}' -Message = 'User {{Request.Intent.DisplayName}} is joining.' -``` +- metadata (name, lifecycle event) +- a list of context resolver instructions (`ContextResolvers`) +- a list of steps (ordered) (`Steps`) +- per-step configuration (`With`) +- per-step optional execution logic (`Condition`, `Precondition`, `OnFailureSteps`, etc.) -### Allowed roots +The Big Picture is described in [Concepts](../about/concepts.md). -For security, only these path roots are permitted: +### Context Resolvers -| Root | Description | -| ---- | ----------- | -| `Request.Intent.*` | Caller-provided action inputs | -| `Request.Context.*` | Read-only associated context (host/resolver-provided) | -| `Request.IdentityKeys.*` | Identifiers of the target identity | -| `Request.LifecycleEvent` | Lifecycle event type (e.g. `Joiner`) | -| `Request.CorrelationId` | Stable correlation identifier | -| `Request.Actor` | Originator of the request | +Workflows may define a `ContextResolvers` section at workflow root level. -### Pure vs. mixed placeholders +Resolvers run during **plan build** and populate `Request.Context.*` +using read-only provider capabilities. -A value containing **only** a single placeholder preserves the resolved type (bool, int, datetime, guid, string): +This allows Conditions, Preconditions and Templates to rely on +stable pre-resolved associated data. -```powershell -# Resolves to the actual [bool] value, not the string "True" -Enabled = '{{Request.Intent.IsEnabled}}' -``` +See: [Context Resolvers](./workflows/context-resolver.md) + +### Template Substitution -A value with surrounding text always produces a **string**: +Many step configurations use **template substitution** to insert values from the incoming request into strings (for example to build a UPN or display name). \ +These `{{Request.*}}` placeholders are resolved against the +request during plan build (`New-IdlePlan`). Only `Request.*` roots are allowed (for example `Request.Intent`, `Request.Context`, `Request.IdentityKeys`, `Request.LifecycleEvent`). Multiple placeholders may appear in a single value. ```powershell -Message = 'Account for {{Request.Intent.DisplayName}} created.' +IdentityKey = '{{Request.IdentityKeys.sAMAccountName}}' +DisplayName = '{{Request.Intent.GivenName}} {{Request.Intent.Surname}}' +Message = 'User {{Request.Intent.DisplayName}} is joining.' ``` -### Backslash and special characters +See: [Template Substitution](./workflows/templates.md) -Backslash (`\`) is a **literal character** in template strings and requires no escaping. -Windows-style paths and domain-qualified names work as-is: +### What a step contains -```powershell -# \ is kept as-is; only the placeholder is substituted -IdentityKey = 'DOMAIN\{{Request.IdentityKeys.sAMAccountName}}' -# → e.g. 'DOMAIN\jdoe' -``` +A step is a self-contained unit of work. Most steps follow this pattern: -### Escaping a literal `{{` +- `Name` (string) – a human-readable identifier +- `Type` (string) – the step type (for example `IdLE.Step.EnsureAttributes`) +- `With` (hashtable) – step-specific configuration +- `Condition` (hashtable, optional) – optional planning-time applicability +- `Precondition` (hashtable, optional) – optional execution-time guard +- `OnPreconditionFalse` (string, optional) – behavior when the precondition is false -To include a literal `{{` in the output, prefix it with `\`. The escape is applied whenever -`\{{` is **not** immediately followed by a valid allowed-root template path and `}}`: +> Step types define which keys are supported inside `With`. See the step reference for details. -```powershell -# \{{ not followed by a valid path+}} → literal {{ in output -Value = 'Literal \{{ braces here' -# → 'Literal {{ braces here' +#### Step execution controls -# \{{ followed by an invalid/disallowed path → also escaped (literal {{ in output) -Value = '\{{Request.InvalidRoot}}' -# → '{{Request.InvalidRoot}}' -``` +Each step supports several optional execution control properties: -Summary of backslash behaviour: +| Property | Evaluated at | Purpose | +|---|---|---| +| `Condition` | Plan time | Include or skip the step based on request/intent data during planning. See [Conditions](workflows/conditions.md) | +| `Precondition` | Execution time (runtime) | Guard the step against stale or unsafe state immediately before execution. See Runtime [Preconditions](workflows/preconditions.md). | +| `OnFailureSteps` | After failure (workflow-level) | Cleanup/rollback steps run after a primary step fails. | -| Input | Result | -| ----- | ------ | -| `DOMAIN\{{Request.IdentityKeys.sAMAccountName}}` | `DOMAIN\jdoe` — `\` literal, valid template resolved | -| `Literal \{{ braces here` | `Literal {{ braces here` — escape applied | -| `\{{Request.InvalidRoot}}` | `{{Request.InvalidRoot}}` — invalid root, escape applied | -| `Literal \{{ and {{Request.Intent.Name}}` | `Literal {{ and TestName` — escape + template | +:::warning Do not confuse Conditions and Preconditions +**Conditions** decide step applicability during **planning** (a step becomes `NotApplicable`). +**Preconditions** guard step behavior during **execution** (`OnPreconditionFalse` can mark the step `Blocked`, `Failed`, or allow it to `Continue`). -### Validation +--- -During plan build, IdLE validates every template value: +## Common pitfalls -- **Unbalanced braces** — mismatched `{{`/`}}` pairs throw a syntax error. -- **Invalid path** — paths must use dot-separated identifiers (letters, numbers, underscores). -- **Disallowed root** — paths outside the allowlist throw a security error. -- **Null or missing value** — if the resolved path does not exist, an error is thrown. -- **Non-scalar value** — resolving to a hashtable or array is not allowed. +- **Not data-only:** embedding ScriptBlocks or secrets in workflow files (not allowed). +- **Wrong StepType name:** the step module is not imported or the type name is wrong. +- **Missing provider alias:** `With.Provider = 'Identity'` but the host did not supply that alias. +- **Template paths resolve to null:** the referenced request/identity data is missing. --- @@ -170,11 +178,3 @@ For full definitions and reference, see: - [Reference](../reference/intro-reference.md) - [Reference: Step Types](../reference/steps.md) - [Reference: Providers](../reference/providers.md) - ---- - -## Next steps - -- Add runtime safety guards: [Runtime Preconditions](workflows/preconditions.md) -- Map external systems: [Providers](providers.md) -- Review and export plans: [Plan Export](plan-export.md) (e.g. for CI systems) diff --git a/docs/use/workflows/conditions.md b/docs/use/workflows/conditions.md new file mode 100644 index 00000000..22a96bd7 --- /dev/null +++ b/docs/use/workflows/conditions.md @@ -0,0 +1,193 @@ +--- +title: Conditions +sidebar_label: Conditions +--- + +# Conditions + +## What are Conditions? + +Conditions control **step applicability during planning**. + +- Evaluated while the plan is being built +- If the condition evaluates to `false`, the step becomes `NotApplicable` +- Conditions shape the plan, not execution + +Think of **Conditions** as a *planning-time filter*. \ +They decide whether a step becomes part of the executable plan. + +--- + +## ⚠️ Context Resolvers vs Templates vs Conditions vs Preconditions + +:::warning Do not confuse these concepts +**[Context Resolvers](./context-resolver.md)** populate `Request.Context.*` during **planning**. +**[Template Substitution](./templates.md)** consumes `Request.*` values to build strings. +**Conditions** decide step applicability during **planning** (`NotApplicable`). +**[Preconditions](./preconditions.md)** guard step behavior during **execution** (`Blocked` / `Fail` / `Continue`). +::: + +| Condition | Precondition | +|------------|--------------| +| Planning time | Execution time | +| Marks step `NotApplicable` | Controls runtime behavior | +| Affects plan shape | Affects execution flow | + +--- + +## Full Example + +```powershell +@{ + Name = 'Provision EU Joiner' + Type = 'IdLE.Step.EmitEvent' + + Condition = @{ + All = @( + @{ Equals = @{ Path = 'Plan.LifecycleEvent'; Value = 'Joiner' } } + @{ In = @{ Path = 'Request.Context.Region'; Values = @('EU','DE') } } + @{ Exists = 'Request.IdentityKeys.EmployeeId' } + ) + } + + With = @{ + Message = 'Provisioning for EU Joiner' + } +} +``` + +### Explanation + +The step is applicable only if: + +1. The lifecycle event is `Joiner` +2. The region is `EU` or `DE` +3. An `EmployeeId` exists + +If any condition evaluates to false, the step is marked as `NotApplicable` during planning. + +--- + +## Condition DSL + +Preconditions use the **same DSL** as Conditions. +This section is the authoritative DSL reference. + +### Groups + +- `All` — all child conditions must be true (AND) +- `Any` — at least one child condition must be true (OR) +- `None` — none of the child conditions must be true (NOR) + +### Operators + +#### Equals + +```powershell +@{ Equals = @{ Path = 'Plan.LifecycleEvent'; Value = 'Joiner' } } +``` + +#### NotEquals + +```powershell +@{ NotEquals = @{ Path = 'Request.Context.Tenant'; Value = 'DEV' } } +``` + +#### Exists + +```powershell +@{ Exists = 'Request.Context.ManagerUpn' } +``` + +#### In + +```powershell +@{ + In = @{ + Path = 'Plan.LifecycleEvent' + Values = @('Joiner','Mover') + } +} +``` + +--- + +## Comparison Semantics + +- Comparisons are string-based +- Deterministic evaluation +- Values are converted to string before comparison + +--- + +## Validation Rules + +- Each node may contain exactly one operator or group +- Unknown keys cause planning-time errors +- Missing or empty `Path` causes validation errors + +--- + +## Common Patterns + +### Only for a lifecycle event + +```powershell +Condition = @{ Equals = @{ Path = 'Plan.LifecycleEvent'; Value = 'Leaver' } } +``` + +### Only if a request field exists + +```powershell +Condition = @{ Exists = 'Request.Context.ManagerUpn' } +``` + +### Allowlist values (In) + +```powershell +Condition = @{ In = @{ Path = 'Request.Context.Region'; Values = @('EU','US') } } +``` + +### Negation (NOT via None) + +```powershell +Condition = @{ None = @( @{ Equals = @{ Path = 'Request.Context.Tenant'; Value = 'DEV' } } ) } +``` + +### Combine multiple checks (All / AND) + +```powershell +Condition = @{ + All = @( + @{ Equals = @{ Path = 'Plan.LifecycleEvent'; Value = 'Joiner' } } + @{ Exists = 'Request.IdentityKeys.EmployeeId' } + ) +} +``` + +--- + +## Troubleshooting + +### Step is `NotApplicable` but you expected it to run + +- Verify the used `Path` values are correct and exist at planning time (for example `Request.Context.*` vs `Request.IdentityKeys.*`). +- Remember comparisons are string-based. Normalize boolean-like values in the request (for example use `'True'` / `'False'` consistently). + +### Planning fails with “Unknown key … in condition node” + +Each node may contain exactly one of: + +- a group: `All`, `Any`, `None` +- an operator: `Equals`, `NotEquals`, `Exists`, `In` + +Any additional keys cause a planning-time validation error. + +### Planning fails with “Missing or empty Path” + +Operators like `Equals`, `NotEquals`, and `In` require a non-empty `Path`. +For `Exists`, prefer the short form `Exists = '…'` to avoid shape errors. + +### Confusion about “Skipped” + +Conditions do not “skip” execution. They decide applicability during planning and mark the step as `NotApplicable`. diff --git a/docs/use/workflows/context-resolver.md b/docs/use/workflows/context-resolver.md new file mode 100644 index 00000000..d02281a8 --- /dev/null +++ b/docs/use/workflows/context-resolver.md @@ -0,0 +1,168 @@ +--- +title: Context Resolvers +sidebar_label: Context Resolvers +--- + +## What are Context Resolvers? + +Context Resolvers populate **`Request.Context.*` during planning** using **read-only provider capabilities**. + +- They run during **plan build** +- They execute before step `Condition` evaluation +- They enrich the request with stable, pre-resolved associated data +- They are strictly validated and fail fast on invalid configuration + +Context Resolvers allow **Conditions**, **Preconditions**, and **Template Substitution** +to rely on data that was resolved once during planning. + +--- + +## ⚠️ Context Resolvers vs Templates vs Conditions vs Preconditions + +:::warning Do not confuse these concepts +**Context Resolvers** populate `Request.Context.*` during **planning**. +**[Template Substitution](./templates.md)** consumes `Request.*` values to build strings. +**[Conditions](conditions.md)** decide step applicability during **planning** (`NotApplicable`). +**[Preconditions](./preconditions.md)** guard step behavior during **execution** (`Blocked` / `Fail` / `Continue`). +::: + +--- + +## Full Example + +A resolver entry is defined at workflow root level: + + +```powershell +@{ + Name = 'Joiner - Context Resolver Demo' + LifecycleEvent = 'Joiner' + + ContextResolvers = @( + @{ + Capability = 'IdLE.Identity.Read' + Provider = 'Identity' # optional + With = @{ + IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' + } + } + + @{ + Capability = 'IdLE.Entitlement.List' + With = @{ + IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' + } + } + ) + + Steps = @( + + @{ + Name = 'Disable only if identity exists' + Type = 'IdLE.Step.DisableIdentity' + + Condition = @{ + Exists = 'Request.Context.Identity.Profile' + } + } + + @{ + Name = 'Emit audit event' + Type = 'IdLE.Step.EmitEvent' + + With = @{ + Message = 'Disabled identity {{Request.Context.Identity.Profile.DisplayName}}' + } + } + ) +} +``` + +### Keys + +- `Capability` (required) + A permitted read-only capability. + +- `Provider` (optional) + Provider alias. If omitted, IdLE selects a provider advertising the capability. + +- `With` (required) + Inputs required by the capability. Template substitution is supported. + +Output paths are predefined and cannot be changed. + +--- + +## Common Patterns + +### Resolve once, use everywhere + +Resolve identity or entitlements once and reuse the result in: + +- Conditions +- Preconditions +- Templates + +Example: + +```powershell +Condition = @{ Exists = 'Request.Context.Identity.Profile' } + +DisplayName = '{{Request.Context.Identity.Profile.DisplayName}}' +``` + +### Guard destructive steps + +Only perform destructive actions if identity exists: + +```powershell +Condition = @{ + Exists = 'Request.Context.Identity.Profile' +} +``` + +### Entitlement snapshot usage + +Resolve entitlements once: + +```powershell +ContextResolvers = @( + @{ + Capability = 'IdLE.Entitlement.List' + With = @{ IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' } + } +) +``` + +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 `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. diff --git a/docs/use/workflows/preconditions.md b/docs/use/workflows/preconditions.md index 3d9932fe..c6ec8775 100644 --- a/docs/use/workflows/preconditions.md +++ b/docs/use/workflows/preconditions.md @@ -1,132 +1,94 @@ --- -title: Runtime Preconditions -sidebar_label: Runtime Preconditions +title: Preconditions +sidebar_label: Preconditions --- -Runtime Preconditions are **read-only execution guards** evaluated immediately before a step runs. -They protect against stale plans: when time passes between plan creation and execution, external -state may have changed. Preconditions check live (or request-context) data at execution time and -stop the run before an unsafe action is taken. +# Preconditions -:::info Planning-time vs. runtime -`Condition` is evaluated at **planning time** and controls whether a step is included in the plan -(`Status = Planned | NotApplicable`). Preconditions are evaluated at **execution time**, after the -plan is built, immediately before each step runs. This keeps planning deterministic while enabling -safety guards. -::: - ---- - -## When to use preconditions +## What are Preconditions? -Use preconditions when: - -- The validity of a step depends on **current state** that may change after plan creation. -- A policy or compliance rule must be checked **live** before an action is allowed to proceed. -- You want to surface a structured, human-readable message to an operator when a gate fails. +Preconditions guard **step execution**. -**Example — BYOD policy:** +- Evaluated during execution +- Do not change plan shape +- Controlled via `OnPreconditionFalse` -Before disabling an identity, the system should verify that company data has been wiped from any -BYOD (Bring Your Own Device) device. If the wipe confirmation is missing, execution must stop with -a `Blocked` outcome and a message instructing the operator to perform the wipe manually. +Think of **Preconditions** as runtime safety checks. \ +They protect execution but do not affect planning. --- -## Schema - -Add these optional properties to a workflow step definition: +## ⚠️ Context Resolvers vs Templates vs Conditions vs Preconditions -| Property | Type | Required | Description | -|---|---|---|---| -| `Precondition` | `Condition` | No | One condition node (same DSL as `Condition`). It must evaluate to true for the step to execute. | -| `OnPreconditionFalse` | `String` | No | Behavior when a precondition fails. `Blocked` (default), `Fail`, or `Continue`. | -| `PreconditionEvent` | `Hashtable` | No | Structured event emitted when a precondition fails. | +:::warning Do not confuse these concepts +**[Context Resolvers](./context-resolver.md)** populate `Request.Context.*` during **planning**. +**[Template Substitution](./templates.md)** uses allowlisted `Request.*` values (such as `Request.Context.*`) to build strings. +**[Conditions](./conditions.md)** decide step applicability during **planning** (`NotApplicable`). +**Preconditions** guard step behavior during **execution** (`Blocked` / `Fail` / `Continue`). +::: -### PreconditionEvent schema - -| Key | Type | Required | Description | -|---|---|---|---| -| `Type` | `String` | **Yes** | Event type string (for example: `ManualActionRequired`). | -| `Message` | `String` | **Yes** | Human-readable description of the required action. | -| `Data` | `Hashtable` | No | Optional key-value payload. Must not contain secrets. | +| Precondition | Condition | +|--------------|------------| +| Execution time | Planning time | +| Controls runtime behavior | Marks step `NotApplicable` | +| Affects execution result | Affects plan shape | --- -## Example +## Full Example ```powershell @{ - Name = 'Leaver' - LifecycleEvent = 'Leaver' - - Steps = @( - @{ - Name = 'DisableIdentity' - Type = 'IdLE.Step.DisableIdentity' - With = @{ - Provider = 'Identity' - IdentityKey = '{{Request.IdentityKeys.sAMAccountName}}' - } + Name = 'Disable existing identity' + Type = 'IdLE.Step.DisableIdentity' - # Runtime guard: only execute if BYOD wipe is confirmed. - # Note: the condition DSL compares values as strings. - # Request.Context.Byod.WipeConfirmed must be the string 'true' (e.g. set by a ContextResolver). - Precondition = @{ - All = @( - @{ - Equals = @{ - Path = 'Request.Context.Byod.WipeConfirmed' - Value = 'true' - } - } - ) - } - OnPreconditionFalse = 'Blocked' - PreconditionEvent = @{ - Type = 'ManualActionRequired' - Message = 'Perform Intune retire / wipe company data for BYOD device before disabling the identity.' - Data = @{ - Reason = 'BYOD wipe not confirmed' - } - } - } - ) + Precondition = @{ + Equals = @{ Path = 'Request.Context.IdentityExists'; Value = 'True' } + } + + OnPreconditionFalse = 'Continue' } ``` +### Explanation + +The step executes only if: + +- `IdentityExists` equals `True` + +If the precondition evaluates to false: + +- `Blocked` → step is marked `Blocked`; workflow stops at this step +- `Fail` → execution fails immediately +- `Continue` → step is skipped; workflow continues + --- ## Condition DSL -`Precondition` uses the same **declarative condition DSL** as the `Condition` -property. Supported operators: - -| Operator | Shape | Description | -|---|---|---| -| `Equals` | `@{ Path = '...'; Value = '...' }` | True when the resolved path equals the value (string comparison). | -| `NotEquals` | `@{ Path = '...'; Value = '...' }` | True when the resolved path does not equal the value. | -| `Exists` | `'path'` or `@{ Path = '...' }` | True when the resolved path is non-null. | -| `In` | `@{ Path = '...'; Values = @(...) }` | True when the resolved path value is in the list. | -| `All` | `@{ All = @( ... ) }` | True when all child conditions are true (AND). | -| `Any` | `@{ Any = @( ... ) }` | True when at least one child condition is true (OR). | -| `None` | `@{ None = @( ... ) }` | True when no child conditions are true (NOR). | +:::tip Preconditions use the same **Condition DSL** as Conditions. +For the complete DSL reference, see: [Conditions → Condition DSL](./conditions.md) +::: -### Path resolution +--- -Paths are resolved against the **execution-time context**, which includes: +## Schema -| Root | Description | -|---|---| -| `Plan.*` | The plan object (e.g. `Plan.LifecycleEvent`). | -| `Request.*` | The lifecycle request, including `Request.Intent.*`, `Request.Context.*`, `Request.IdentityKeys.*`. | +Add these optional properties to a workflow step definition: -A leading `context.` prefix is ignored for readability (e.g. `context.Request.Intent.Department` -resolves identically to `Request.Intent.Department`). +| Property | Type | Required | Description | +|---|---|---|---| +| `Precondition` | `Condition` | No | One condition node (same DSL as `Condition`). It must evaluate to true for the step to execute. | +| `OnPreconditionFalse` | `String` | No | Behavior when a precondition fails. `Blocked` (default), `Fail`, or `Continue`. | +| `PreconditionEvent` | `Hashtable` | No | Structured event emitted when a precondition fails. | -At planning time, IdLE validates `Path` references to fail fast on typos and wrong roots. For `Precondition`, unresolved paths under `Request.Context.*` are treated as soft (non-fatal) to support context enrichment that may arrive later at runtime (for example via host/runtime context resolver behavior). Other unresolved roots still fail fast. -When this soft-check path is used, IdLE records a planning warning (`PreconditionContextPathUnresolvedAtPlan`) in `Plan.Warnings`, and the warning is included in `Export-IdlePlan` output for CI policy checks. +### PreconditionEvent schema +| Key | Type | Required | Description | +|---|---|---|---| +| `Type` | `String` | **Yes** | Event type string (for example: `ManualActionRequired`). | +| `Message` | `String` | **Yes** | Human-readable description of the required action. | +| `Data` | `Hashtable` | No | Optional key-value payload. Must not contain secrets. | --- @@ -218,14 +180,68 @@ Ensure context values are stored as strings when using `Equals` or `In` operator --- -## Backward compatibility +## Common Patterns + +### Guard destructive operations (Skip if not safe) +Use preconditions when: + +- The validity of a step depends on **current state** that may change after plan creation. +- A policy or compliance rule must be checked **live** before an action is allowed to proceed. +- You want to surface a structured, human-readable message to an operator when a gate fails. -Steps without `Precondition` behave exactly as before. Adding a precondition to a step does not -affect any other steps. +**Example — BYOD policy:** + +Before disabling an identity, the system should verify that company data has been wiped from any +BYOD (Bring Your Own Device) device. If the wipe confirmation is missing, execution must stop with +a `Blocked` outcome and a message instructing the operator to perform the wipe manually. + +### Fail fast if a mandatory prerequisite is missing +```powershell + # Runtime guard: only execute if BYOD wipe is confirmed. + # Note: the condition DSL compares values as strings. + # Request.Context.Byod.WipeConfirmed must be the string 'true' (e.g. set by a ContextResolver). + Precondition = @{ + All = @( + @{ + Equals = @{ + Path = 'Request.Context.Byod.WipeConfirmed' + Value = 'true' + } + } + ) + } + OnPreconditionFalse = 'Blocked' + PreconditionEvent = @{ + Type = 'ManualActionRequired' + Message = 'Perform Intune retire / wipe company data for BYOD device before disabling the identity.' + Data = @{ + Reason = 'BYOD wipe not confirmed' + } + } +``` --- -## Reference +## Troubleshooting + +### Step is skipped unexpectedly +`Precondition` uses the same **declarative condition DSL** as the `Condition` +property. Supported operators: + +- Check `OnPreconditionFalse`. If it is set to `Continue`, a false precondition will skip execution by design. +- Validate that the precondition `Path` resolves to the expected runtime value. + +### Step fails due to precondition + +- If `OnPreconditionFalse = 'Fail'`, the step will fail intentionally when the precondition is false. +- Ensure required request values are prepared before execution (often host-side request preparation). + +### Precondition seems correct but still evaluates false + +- Remember comparisons are string-based. Normalize values (especially booleans) consistently (for example `'True'` / `'False'`). +- Confirm you are using the correct path (`Plan.*`, `Request.*`). + +### Where is the DSL documented? -- [Steps reference](../../reference/steps.md) -- [Concepts: Plan → Execute separation](../../about/concepts.md) +Preconditions use the same Condition DSL as Conditions. For the complete DSL reference, see: +[Conditions → Condition DSL](./conditions.md) diff --git a/docs/use/workflows/templates.md b/docs/use/workflows/templates.md new file mode 100644 index 00000000..02a8daa0 --- /dev/null +++ b/docs/use/workflows/templates.md @@ -0,0 +1,160 @@ +--- +title: Template Substitution +sidebar_label: Template Substitution +--- + +# Template Substitution + +Template substitution allows you to reference values from the **planning context** inside step configuration (`With`) values during planning. Conditions and Preconditions use the condition DSL and path resolution and do **not** support `{{...}}` templates. + +Templates are **data-only** and safe. +No ScriptBlocks or dynamic PowerShell expressions are supported. + +--- + +## What is Template Substitution? + +Template substitution resolves values from: `Request.*`g + +It replaces template placeholders with actual values before execution. + +Think of template substitution as **value resolution**, not logic execution. \ +It simply reads values from the context and inserts them into configuration fields. + +--- + +## ⚠️ Context Resolvers vs Templates vs Conditions vs Preconditions + +:::warning Do not confuse these concepts +**[Context Resolvers](./context-resolver.md)** populate `Request.Context.*` during **planning**. +**Template Substitution** consumes allowlisted `Request.*` values to build strings. +**[Conditions](./conditions.md)** decide step applicability during **planning** (`NotApplicable`). +**[Preconditions](./preconditions.md)** guard step behavior during **execution** (`Blocked` / `Fail` / `Continue`). +::: + +--- + +## Resolution Context + +Templates can reference: + +| Root | Description | +| ---- | ----------- | +| `Request.Intent.*` | Caller-provided action inputs | +| `Request.Context.*` | Read-only associated context (host/resolver-provided) | +| `Request.IdentityKeys.*` | Identifiers of the target identity | +| `Request.LifecycleEvent` | Lifecycle event type (e.g. `Joiner`) | +| `Request.CorrelationId` | Stable correlation identifier | +| `Request.Actor` | Originator of the request | + +--- + +## Example + +```powershell +@{ + Name = 'Create UPN' + Type = 'IdLE.Step.EnsureAttributes' + + With = @{ + UserPrincipalName = '{{Request.IdentityKeys.FirstName}}.{{Request.IdentityKeys.LastName}}@example.com' + } +} +``` + +If: + +- FirstName = John +- LastName = Doe + +The resolved value becomes: + +``` +John.Doe@example.com +``` + +--- + +## Common Patterns + +### Pure placeholder resolution + +A value containing **only** a single placeholder preserves the resolved type (bool, int, datetime, guid, string): + +```powershell +# Resolves to the actual [bool] value, not the string "True" +Enabled = '{{Request.Intent.IsEnabled}}' +``` + +### Build composite attributes + +```powershell +DisplayName = '{{Request.IdentityKeys.FirstName}} {{Request.IdentityKeys.LastName}}' +``` + +### Include lifecycle event + +A value with surrounding text always produces a **string**: + +```powershell +Description = 'Provisioned during {{Request.LifecycleEvent}}' +``` + +### Backslash and special characters + +Backslash (`\`) is a **literal character** in template strings and requires no escaping. +Windows-style paths and domain-qualified names work as-is: + +```powershell +# \ is kept as-is; only the placeholder is substituted +IdentityKey = 'DOMAIN\{{Request.IdentityKeys.sAMAccountName}}' +# → e.g. 'DOMAIN\jdoe' +``` + +### Escaping a literal `{{` + +To include a literal `{{` in the output, prefix it with `\`. The escape is applied whenever +`\{{` is **not** immediately followed by a valid allowed-root template path and `}}`: + +```powershell +# \{{ not followed by a valid path+}} → literal {{ in output +Value = 'Literal \{{ braces here' +# → 'Literal {{ braces here' + +# \{{ followed by an invalid/disallowed path → also escaped (literal {{ in output) +Value = '\{{Request.InvalidRoot}}' +# → '{{Request.InvalidRoot}}' +``` + +Summary of backslash behaviour: + +| Input | Result | +| ----- | ------ | +| `DOMAIN\{{Request.IdentityKeys.sAMAccountName}}` | `DOMAIN\jdoe` — `\` literal, valid template resolved | +| `Literal \{{ braces here` | `Literal {{ braces here` — escape applied | +| `\{{Request.InvalidRoot}}` | `{{Request.InvalidRoot}}` — invalid root, escape applied | +| `Literal \{{ and {{Request.Intent.Name}}` | `Literal {{ and TestName` — escape + template | + +### Template Validation + +During plan build, IdLE validates every template value: + +- **Unbalanced braces** — mismatched `{{`/`}}` pairs throw a syntax error. +- **Invalid path** — paths must use dot-separated identifiers (letters, numbers, underscores). +- **Disallowed root** — paths outside the allowlist throw a security error. +- **Null or missing value** — if the resolved path does not exist, an error is thrown. +- **Non-scalar value** — resolving to a hashtable or array is not allowed. + +--- + +## Troubleshooting + +### Placeholder not resolved + +- Verify the path exists on the request object (allowed `Request.*` roots only). +- Ensure correct casing and full path (for example, `Request.Context.*`). + +### Empty value after substitution + +- The referenced path may be `$null`. +- Validate the request preparation logic before execution. diff --git a/website/sidebars.js b/website/sidebars.js index 1fd0815f..157ffc35 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -52,7 +52,10 @@ const sidebars = { collapsed: true, items: [ 'use/workflows', + 'use/workflows/conditions', 'use/workflows/preconditions', + 'use/workflows/templates', + 'use/workflows/context-resolver', ], }, 'use/providers',