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
4 changes: 4 additions & 0 deletions docs/about/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ IdLE applies **secure defaults** to reduce accidental code execution:
- Step registry handlers must be function names (strings); ScriptBlock handlers are rejected.
- Event streaming uses an object-based contract (`WriteEvent(event)`); ScriptBlock event sinks are rejected.
- AuthSessionBroker objects should not contain ScriptBlock properties; use ScriptMethods or direct function calls instead.
- Step `State` (values later steps can read) must match the step's `StateSchema` and is rejected if it contains
ScriptBlocks, `PSCredential` or `SecureString`. `With` templates that read `State` come from the workflow
definition only; resolved request values are never scanned for templates again
(see [Step State](../extend/steps.md#step-state)).

## Redaction at output boundaries

Expand Down
76 changes: 67 additions & 9 deletions docs/extend/steps.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@ not set (used by the mailbox steps: `ExchangeOnline`). Steps without `Provider`
- Invalid metadata fails when the catalog is loaded.
- Metadata must be data-only — ScriptBlocks are rejected.

**`StateSchema`** (optional): declares the `State` the step publishes for later steps, with the same key schema
format as `WithSchema` (`Keys`, `AllowAdditionalKeys`). A step without `StateSchema` must not return `State`.
See [Step State](#step-state).

```powershell
StateSchema = @{ Keys = @{ Mailbox = @{ Type = 'Any'; Required = $true } } }
```

Host-supplied step types may supplement (but not override) the catalog via `Providers.StepMetadata`.

### Step handler contract
Expand Down Expand Up @@ -185,11 +193,55 @@ looks it up in `Providers` and passes a handle. The same alias feeds `Request.Co
| --- | --- | --- |
| `Changed` | Bool | The step changed external state. Default `$false`. |
| `Data` | Hashtable | Structured step output (e.g. per-attribute results, skipped entitlements). |
| `State` | Hashtable | Values intended for later steps. Reserved: returned in the step result, not yet consumed by the engine. |
| `State` | Hashtable | Values for later steps of the same run; must match the step's `StateSchema` (see [Step State](#step-state)). |

The engine builds `IdLE.StepResult` (`Name`, `Type`, `Status`, `Changed`, `Data`, `State`, `Error`, `Attempts`,
`Source`). Failures are reported by throwing; transient failures use the transient marker for retries.

### Step State

A step publishes `State` under its step name. Later steps read it as `State.<StepName>.<Key>[.<Nested>...]`:

```powershell
Steps = @(
@{
Name = 'GetMailbox' # referenced names must be identifiers (letters, digits, underscores)
Type = 'IdLE.Step.Mailbox.GetInfo'
With = @{ IdentityKey = '{{Request.Intent.UserPrincipalName}}' }
}
@{
Name = 'ConvertToShared'
Type = 'IdLE.Step.Mailbox.EnsureType'
Precondition = @{ Equals = @{ Path = 'State.GetMailbox.Mailbox.Type'; Value = 'User' } }
OnPreconditionFalse = 'Continue'
With = @{ IdentityKey = '{{State.GetMailbox.Mailbox.PrimarySmtpAddress}}'; MailboxType = 'Shared' }
}
)
```

| Place | State supported |
| --- | --- |
| `Condition` | No: conditions are evaluated at planning time, before any step has run (`StateReferenceNotAllowed`). |
| `Precondition` | Yes: evaluated right before the step. |
| `With` templates | Yes: a `With` string with a `{{State...}}` placeholder is resolved right before the step (not for `Provider`, `AuthSessionName`, `AuthSessionOptions`). |

**Planning** validates every reference: the producing step must run before the referencing step (`Steps` see earlier
`Steps`; `OnFailureSteps` see all `Steps` and earlier `OnFailureSteps`) and must declare `<Key>` in its
`StateSchema`. Paths below `<Key>` are not validated. A `With` string that references State stays unresolved in the
plan (and in the plan export); its `Request` placeholders must resolve at planning time, and its `WithSchema` check
runs after the execution-time resolution. Only these strings are resolved again, so request values cannot inject
`State` placeholders.

**Execution:**

- The engine validates the returned `State` against the `StateSchema` and stores a data-only copy after the step
completes. Steps that are `NotApplicable`, `PreconditionSkipped`, `Blocked` or `Failed` publish nothing.
- In a `Precondition`, a State path whose producer published nothing resolves to `$null`; use `Exists` to check
that the producer ran.
- A `With` placeholder that resolves to `$null` fails the step (`StateReferenceUnresolved`).
- State must not contain ScriptBlocks, `PSCredential` or `SecureString`; authentication stays with the
`AuthSessionBroker`. State is not written to events.

### Error codes

Error messages start with the code, followed by the step name, the step type and, for `With` errors, the path.
Expand All @@ -201,12 +253,19 @@ Error messages start with the code, followed by the step name, the step type and
| `WithKeyMissing` | Required key absent or `$null`. | Plan |
| `WithKeyUnknown` | Key not in the schema (and no `AllowAdditionalKeys`). | Plan |
| `WithKeyTypeMismatch` | Value does not match `Type`. | Plan |
| `WithValueNotAllowed` | Value not in `AllowedValues`, or empty without `AllowEmpty`. | Plan |
| `WithValueNotAllowed` | Value not in `AllowedValues`, or empty without `AllowEmpty`. | Plan (State templates: execution) |
| `InvalidStateSchema` | Invalid `StateSchema` (missing `Keys`, invalid key schema, ...). | Catalog load |
| `StateReferenceNotAllowed` | `State.*` in `Condition`, or in `With.Provider` / `AuthSessionName` / `AuthSessionOptions`. | Plan |
| `StateReferenceInvalid` | No step name after `State`, or the step name is not an identifier. | Plan |
| `StateReferenceUnknownStep` | No step with that name runs before the referencing step. | Plan |
| `StateReferenceAmbiguous` | Referenced from `OnFailureSteps`, and the name exists in both `Steps` and `OnFailureSteps`. | Plan |
| `StateReferenceNotDeclared` | The producing step declares no `StateSchema`, or not the referenced key. | Plan |
| `LegacyStepHandler` | Handler has no `-With` parameter (pre-1.0 contract). | Before the first attempt |
| `InvalidStepHandlerSignature` | `-Provider` declared without metadata `Provider`, or vice versa. | Before the first attempt |
| `StateReferenceUnresolved` | A `With` State placeholder resolves to `$null` (producer did not run or did not return the value). | Before the first attempt |
| `ProviderNotSupplied` | Resolved alias not in `Providers`. | Execution (hosts may pass providers only to `Invoke-IdlePlan`) |
| `ProviderMethodNotImplemented` | `Invoke` of a method the provider does not implement. | Execution |
| `InvalidStepHandlerResult` | Result is not `$null` / hashtable, has unknown keys, or the handler leaked pipeline output. | Execution |
| `InvalidStepHandlerResult` | Result is not `$null` / hashtable, has unknown keys, the handler leaked pipeline output, or `State` does not match the `StateSchema`. | Execution |

### Migration from earlier versions

Expand Down Expand Up @@ -425,11 +484,10 @@ Steps must not:

## Inputs

Steps receive inputs from the workflow under `Inputs` and may reference:
Steps receive their inputs from the workflow under `With`. `With` templates may reference:

- `Request.*`
- `State.*`
- `Policy.*` (optional root, host-defined)
- `Request.*` (resolved at planning time)
- `State.<StepName>.*` (resolved right before the step runs, see [Step State](#step-state))

### Security: Data-only constraint

Expand All @@ -449,8 +507,8 @@ Assert-IdleNoScriptBlock -InputObject $value -Path 'Request.Context.Something'

## Outputs

Steps may write to `State.*` only, and only to declared output paths.
This prevents hidden coupling between steps.
Steps pass values to later steps only through the `State` result key, and only with the keys declared in their
`StateSchema` (see [Step State](#step-state)). This prevents hidden coupling between steps.

### Eventing

Expand Down
8 changes: 8 additions & 0 deletions docs/reference/steps/step-mailbox-get-info.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ Validated at plan time against the step's ``WithSchema`` (see [Step metadata con
| `AuthSessionOptions` | Hashtable | No | | Data-only options passed to the AuthSessionBroker. |
| `Provider` | String | No | `ExchangeOnline` | Provider alias in `Providers`. |

## Outputs (State.*)

Published under `State.<StepName>.<Key>` for later steps (`Precondition` and `With` templates); validated against the step's `StateSchema` (see [Step State](../../extend/steps.md#step-state)).

| Key | Type | Always set | Notes |
| --- | --- | --- | --- |
| `Mailbox` | Any | Yes | |

## Example

```powershell
Expand Down
2 changes: 2 additions & 0 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 @@ -290,6 +290,8 @@
- Each node may contain exactly one operator or group
- Unknown keys cause planning-time errors
- Missing or empty `Path` causes validation errors
- `State.*` paths are rejected (`StateReferenceNotAllowed`): no step has run at planning time. Use a
[Precondition](./preconditions.md#state-of-earlier-steps) to act on values of earlier steps

---

Expand Down
32 changes: 32 additions & 0 deletions docs/use/workflows/preconditions.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,38 @@ If the precondition evaluates to false:
For the complete DSL reference, see: [Conditions → Condition DSL](./conditions.md)
:::

Preconditions resolve paths against `Request.*`, `Plan.*` and `State.*`.

---

## State of earlier steps

Steps such as `IdLE.Step.Mailbox.GetInfo` publish values for later steps (see the **Outputs (State.\*)** table
on the step's reference page). A precondition reads them as `State.<StepName>.<Key>`:

```powershell
@{
Name = 'GetMailbox'
Type = 'IdLE.Step.Mailbox.GetInfo'
With = @{ IdentityKey = '{{Request.Intent.UserPrincipalName}}' }
}
@{
Name = 'ConvertToShared'
Type = 'IdLE.Step.Mailbox.EnsureType'
Precondition = @{ Equals = @{ Path = 'State.GetMailbox.Mailbox.Type'; Value = 'User' } }
OnPreconditionFalse = 'Continue'
With = @{ IdentityKey = '{{Request.Intent.UserPrincipalName}}'; MailboxType = 'Shared' }
}
```

- The producing step must run before the step (planning fails otherwise), and its `Name` must be an identifier
(`GetMailbox`, not `Get mailbox`).
- If the producing step did not run (e.g. `NotApplicable`), its State paths resolve to `$null`. Use `Exists` to
check that the producer ran: `@{ Exists = 'State.GetMailbox.Mailbox' }`.
- `Condition` cannot use `State.*`: conditions are evaluated at planning time, before any step has run.

Details: [Steps → Step State](../../extend/steps.md#step-state).

---

## Schema
Expand Down
30 changes: 28 additions & 2 deletions docs/use/workflows/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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.
Template substitution allows you to reference values from the **planning context** inside step configuration (`With`) values during planning, and values published by earlier steps (`State.*`) right before a step runs. 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.
Expand All @@ -14,7 +14,7 @@ No ScriptBlocks or dynamic PowerShell expressions are supported.

## What is Template Substitution?

Template substitution resolves values from: `Request.*`
Template substitution resolves values from: `Request.*` and `State.<StepName>.*`

It replaces template placeholders with actual values before execution.

Expand Down Expand Up @@ -46,6 +46,32 @@ Templates can reference:
| `Request.LifecycleEvent` | Lifecycle event type (e.g. `Joiner`) |
| `Request.CorrelationId` | Stable correlation identifier |
| `Request.Actor` | Originator of the request |
| `State.<StepName>.*` | State published by an earlier step (step `With` only, see below) |

---

## State of earlier steps

`{{State.<StepName>.<Key>}}` reads a value that an earlier step published (see the **Outputs (State.\*)** table on
the step's reference page):

```powershell
@{
Name = 'NotifyServiceDesk'
Type = 'IdLE.Step.EmitEvent'
With = @{
Message = 'Offboarded {{Request.Intent.UserPrincipalName}} ({{State.GetMailbox.Mailbox.PrimarySmtpAddress}}).'
}
}
```

- A string with a `State` placeholder is resolved **right before the step runs**, not during planning. The plan
(and the plan export) shows the template as written. All other strings are resolved during planning.
- Planning checks that the producing step runs earlier and declares the key, and that the `Request` placeholders of
the string resolve. The `With` type check of the value runs after the resolution.
- If the producing step did not run or did not return the value, the step fails (`StateReferenceUnresolved`).
Guard it with a [Precondition](./preconditions.md#state-of-earlier-steps) if the producer is optional.
- `State` is not available in `Provider`, `AuthSessionName`, `AuthSessionOptions` and in Context Resolvers.

---

Expand Down
17 changes: 12 additions & 5 deletions examples/workflows/templates/exo-leaver.psd1
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@{
Name = 'ExchangeOnline Leaver - Mailbox Offboarding'
LifecycleEvent = 'Leaver'
Description = 'Converts mailbox to shared and enables Out of Office with dynamic manager/service-desk contact information.'
Description = 'Converts a user mailbox to shared and enables Out of Office with dynamic manager/service-desk contact information.'

Steps = @(
@{
Expand All @@ -12,10 +12,16 @@
IdentityKey = '{{Request.Intent.UserPrincipalName}}'
}
}
# GetMailboxInfo publishes State.GetMailboxInfo.Mailbox for the following steps.
# Room and equipment mailboxes are left as they are.
@{
Name = 'ConvertToSharedMailbox'
Type = 'IdLE.Step.Mailbox.EnsureType'
With = @{
Name = 'ConvertToSharedMailbox'
Type = 'IdLE.Step.Mailbox.EnsureType'
Precondition = @{
In = @{ Path = 'State.GetMailboxInfo.Mailbox.Type'; Values = @('User', 'Shared') }
}
OnPreconditionFalse = 'Continue'
With = @{
Provider = 'ExchangeOnline'
IdentityKey = '{{Request.Intent.UserPrincipalName}}'
MailboxType = 'Shared'
Expand Down Expand Up @@ -53,7 +59,8 @@
Name = 'EmitCompletionEvent'
Type = 'IdLE.Step.EmitEvent'
With = @{
Message = 'Mailbox offboarding completed for {{Request.Intent.UserPrincipalName}}.'
# Resolved when the step runs, after GetMailboxInfo has published its State.
Message = 'Mailbox offboarding completed for {{Request.Intent.UserPrincipalName}} ({{State.GetMailboxInfo.Mailbox.PrimarySmtpAddress}}).'
}
}
)
Expand Down
Loading
Loading