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
17 changes: 9 additions & 8 deletions docs/extend/extensibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ IdLE is designed for change through modules instead of forks.

A new step typically involves:

1. A metadata definition declaring required capabilities and `WithSchema` (allowed `With.*` keys)
2. A planning function (test) that produces data-only actions
3. An execution function (invoke) that performs actions via providers
4. Unit tests (Pester)
1. A metadata definition declaring required capabilities, the provider it uses and a typed `WithSchema`
2. A handler function that receives the validated `With` and a provider handle and returns `@{ Changed; Data }`
3. Unit tests (Pester)

See [Steps and Metadata](steps.md#step-metadata-contract) for the required metadata shape.
The engine validates `With`, resolves the provider, handles authentication and builds the step result, so the
handler contains only the step's domain logic. See [Step metadata contract](steps.md#step-metadata-contract) and
[Step handler contract](steps.md#step-handler-contract).

Extension step types and capabilities use your own owner prefix: `<Owner>.IdLE.Step.<Area>.<Name>` and
`<Owner>.IdLE.<Domain>.<Verb>` (e.g. `Contoso.IdLE.Step.Hr.SyncManager`, `Contoso.IdLE.Hr.Manager.Read`).
Expand Down Expand Up @@ -121,9 +122,9 @@ Steps are executed via the step registry.

ScriptBlock handlers are intentionally not supported as a secure default.

Step handlers may optionally declare a `Context` parameter.
For backwards compatibility, the engine passes `-Context` only when the handler
supports it.
Handlers follow the [step handler contract](steps.md#step-handler-contract): they declare `-With` (and `-Provider`
if the metadata declares a provider; `-Context` and `-Step` as needed) and return `$null` or `@{ Changed; Data; State }`.
Handlers written for the pre-1.0 contract (`-Context`/`-Step` only) fail with `LegacyStepHandler`.

---

Expand Down
212 changes: 191 additions & 21 deletions docs/extend/steps.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,34 +69,204 @@ Typical conceptual metadata includes:

### Step metadata contract

Every step type registered with the engine **must** declare a data-only metadata entry containing:

- `RequiredCapabilities` — capability identifiers the step requires from providers
- `WithSchema` — declares the `With.*` key contract used for plan-time validation
Every step type registered with the engine **must** declare a data-only metadata entry:

```powershell
'IdLE.Step.Example' = @{
RequiredCapabilities = @('Some.Capability')
'IdLE.Step.EnsureEntitlement' = @{
Handler = 'Invoke-IdleStepEnsureEntitlement' # step packs only
RequiredCapabilities = @('IdLE.Entitlement.List', 'IdLE.Entitlement.Grant', 'IdLE.Entitlement.Revoke')
Provider = @{ DefaultAlias = 'Identity' } # optional: the step uses one provider
WithSchema = @{
RequiredKeys = @('IdentityKey')
OptionalKeys = @('Provider', 'AuthSessionName', 'AuthSessionOptions')
Keys = @{
IdentityKey = @{ Type = 'String'; Required = $true }
Entitlement = @{
Type = 'Hashtable'
Required = $true
Keys = @{
Kind = @{ Type = 'String'; Required = $true }
Id = @{ Type = 'String'; Required = $true }
}
}
State = @{ Type = 'String'; AllowedValues = @('Present', 'Absent'); Default = 'Present' }
}
}
}
```

**`WithSchema`** (mandatory):
**`Provider`** (optional): `@{ DefaultAlias = '<alias>' }` declares that the step uses exactly one provider.
The engine then accepts these `With` keys for the step (they must not be declared in `Keys`):

- `RequiredKeys` — keys that **must** be present in `With` (plan creation fails if any are missing).
- `OptionalKeys` — keys that **may** be present in `With`. Any key not in `RequiredKeys` or `OptionalKeys` causes a plan-creation error (fail-fast, with step name, type, and offending key in the message).
| Key | Type | Meaning |
| --- | --- | --- |
| `Provider` | String | Provider alias in `Providers`; defaults to `DefaultAlias`. |
| `AuthSessionName` | String | Auth session routing key passed to the `AuthSessionBroker`. |
| `AuthSessionOptions` | Hashtable | Data-only broker options. |

`AuthSessionNameFromAlias = $true` names the auth session after the provider alias when `With.AuthSessionName` is
not set (used by the mailbox steps: `ExchangeOnline`). Steps without `Provider` reject the three keys.

**`WithSchema`** (mandatory): `Keys` maps each `With` key to a key schema:

| Property | Applies to | Meaning |
| --- | --- | --- |
| `Type` | all (required) | `String`, `Int`, `Bool`, `Hashtable`, `Array`, `Any` |
| `Required` | all | Key must be present and not `$null`. |
| `Default` | all | Applied when the key is absent; must match `Type`; not combined with `Required`. |
| `AllowedValues` | `String`, `Int` | Strings match case-insensitively and are normalized to the listed casing. |
| `AllowEmpty` | `String`, `Array` | Allow `''` / `@()` (default: not allowed). |
| `Keys` | `Hashtable` | Nested key schemas; unknown nested keys fail. Without `Keys`, any content is accepted. |
| `AllowAdditionalKeys` | `Hashtable`, `WithSchema` | Accept keys not listed in `Keys`. |
| `Items` | `Array` | Key schema for every element. |

**Rules:**

- Both `RequiredKeys` and `OptionalKeys` must be non-null string arrays (may be empty: `@()`).
- A key must not appear in both sets.
- `With` is validated at plan time, after template resolution. Defaults are applied, so the plan (and the plan
export) contains the effective `With`, including the resolved `Provider`.
- No implicit type conversion: `'600'` is not an `Int`, `'true'` is not a `Bool`. Pure template placeholders
(`'{{Request.Intent.TimeoutSeconds}}'`) keep the type of the request value.
- `Int` accepts `[int]` and `[long]`; `Array` accepts any non-string enumerable except dictionaries.
- Optional keys set to `$null` are treated as absent (defaults apply). `$null` values inside a `Hashtable` without
`Keys` (e.g. "clear attribute" values in `EnsureAttributes.Attributes`) are passed through.
- Errors name the step, the step type and the full path, e.g.
`WithKeyMissing: Step 'Grant' (type 'IdLE.Step.EnsureEntitlement'): With.Entitlement.Kind is required.`
See [Error codes](#error-codes).
- Invalid metadata fails when the catalog is loaded.
- Metadata must be data-only — ScriptBlocks are rejected.

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

### Step handler contract

The engine validates inputs, resolves the provider, handles authentication and builds the step result.
A handler contains only the step's domain logic:

```powershell
function Invoke-ContosoStepSyncManager {
[CmdletBinding()]
param(
[Parameter(Mandatory)] [hashtable] $With,
[Parameter(Mandatory)] [object] $Provider
)

$result = $Provider.Invoke('SyncManager', @($With.IdentityKey))
return @{ Changed = [bool]$result.Changed }
}
```

| Parameter | Passed | Content |
| --- | --- | --- |
| `With` | always (required) | Validated `With` without `Provider`, `AuthSessionName`, `AuthSessionOptions`. |
| `Provider` | if and only if the metadata declares `Provider` | Provider handle (see below). |
| `Context` | if declared | Execution context (`EventSink`, `Plan`, `Request`, `CorrelationId`, `Actor`). |
| `Step` | if declared | Step metadata (`Name`, `Type`), e.g. for events. |

The engine checks the handler's parameters before the first attempt: `-With` is mandatory, and `-Provider` must be
declared if and only if the metadata declares `Provider`.

**Provider handle** (`IdLE.ProviderHandle`): the engine resolves the alias (`With.Provider` or `DefaultAlias`),
looks it up in `Providers` and passes a handle. The same alias feeds `Request.Context.Current` for preconditions.

| Member | Meaning |
| --- | --- |
| `Alias` | Resolved provider alias. |
| `Invoke(MethodName, Arguments)` | Calls a provider method. `Arguments` exclude `AuthSession`; the session is inserted at the position the method declares it: `ListEntitlements(IdentityKey, AuthSession, Kind)` is called as `Invoke('ListEntitlements', @($id, $kind))`. |
| `HasMethod(MethodName)` | `$true` if the provider implements the method (optional features such as bulk operations). |
| `HasParameter(MethodName, ParameterName)` | `$true` if the method declares the parameter (e.g. `Kind` on `ListEntitlements`). |

- The auth session is acquired on the first `Invoke` that needs it (the method declares `AuthSession`, or
`With.AuthSessionName` is set) and reused for the rest of the step attempt. Without `AuthSessionName` the broker is
asked for its default session.
- The raw provider object is not exposed, so steps cannot bypass the authentication handling.
- Providers that expose an `EventSink` property receive the run's event sink.

**Result:** return `$null` or a hashtable with the optional keys:

| Key | Type | Meaning |
| --- | --- | --- |
| `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. |

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.

### Error codes

Error messages start with the code, followed by the step name, the step type and, for `With` errors, the path.

| Code | When | Raised at |
| --- | --- | --- |
| `LegacyWithSchema` | Catalog uses `RequiredKeys` / `OptionalKeys`. | Catalog load |
| `InvalidWithSchema` | Invalid schema: unknown `Type`, `Default` not matching `Type`, `Keys` on a non-hashtable, common key declared in `Keys`, ... | Catalog load |
| `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 |
| `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 |
| `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 |

### Migration from earlier versions

Catalogs and handlers changed in one step without a compatibility layer.

**Catalog:** move provider handling into `Provider` and give every key a type.

```powershell
# Before
'IdLE.Step.DisableIdentity' = @{
Handler = 'Invoke-IdleStepDisableIdentity'
RequiredCapabilities = @('IdLE.Identity.Disable')
WithSchema = @{
RequiredKeys = @('IdentityKey')
OptionalKeys = @('Provider', 'AuthSessionName', 'AuthSessionOptions')
}
}

# After
'IdLE.Step.DisableIdentity' = @{
Handler = 'Invoke-IdleStepDisableIdentity'
RequiredCapabilities = @('IdLE.Identity.Disable')
Provider = @{ DefaultAlias = 'Identity' }
WithSchema = @{
Keys = @{ IdentityKey = @{ Type = 'String'; Required = $true } }
}
}
```

**Handler:** replace `-Context`/`-Step` input checks, provider lookup, `Invoke-IdleProviderMethod` and the
hand-built `IdLE.StepResult` with `-With`, `-Provider` and a result hashtable.

```powershell
# Before: checks With, Context.Providers and the alias, calls Invoke-IdleProviderMethod, builds IdLE.StepResult
function Invoke-IdleStepDisableIdentity {
param([object] $Context, [object] $Step)
# ...
}

# After
function Invoke-IdleStepDisableIdentity {
param(
[Parameter(Mandatory)] [hashtable] $With,
[Parameter(Mandatory)] [object] $Provider
)

$result = $Provider.Invoke('DisableIdentity', @($With.IdentityKey))
return @{ Changed = [bool]$result.Changed }
}
```

Other changes:

- `Invoke-IdleProviderMethod` was removed and `Test-IdleProviderMethodParameter` is no longer exported; use the
provider handle (`Invoke`, `HasParameter`).
- Step-specific result properties moved under `Data`: `PruneEntitlements*` `Skipped` -> `Data.Skipped`,
`EnsureAttributes` -> `Data.Attributes`.
- Workflows with wrongly typed `With` values (e.g. a number where a string is expected) now fail at plan time.
- `IdLE.Step.EmitEvent` no longer accepts `Provider`, `AuthSessionName` or `AuthSessionOptions`.

### Step packs

A step pack is a module that ships a `StepMetadataCatalog.psd1` data file and declares it in its
Expand Down Expand Up @@ -125,9 +295,9 @@ so hosts do not need `Providers.StepRegistry` for step pack steps:
'Contoso.IdLE.Step.Hr.SyncManager' = @{
Handler = 'Invoke-ContosoStepSyncManager'
RequiredCapabilities = @('Contoso.IdLE.Hr.Manager.Read')
Provider = @{ DefaultAlias = 'Hr' }
WithSchema = @{
RequiredKeys = @('IdentityKey')
OptionalKeys = @('Provider', 'AuthSessionName', 'AuthSessionOptions')
Keys = @{ IdentityKey = @{ Type = 'String'; Required = $true } }
}
}
}
Expand Down Expand Up @@ -265,16 +435,16 @@ Steps receive inputs from the workflow under `Inputs` and may reference:

**All step inputs must be data-only and must not contain ScriptBlocks.**

Step implementations MUST validate their inputs using the centralized helper:
The engine enforces this before a handler runs: workflow definitions and plans are scanned with
`Assert-IdleNoScriptBlock`, and `With` is validated against the step's `WithSchema`. Handlers do not repeat these checks.

If a step processes other untrusted data (for example values read from a request at execution time), validate it
with the centralized helper instead of custom ScriptBlock checks:

```powershell
Assert-IdleNoScriptBlock -InputObject $config -Path 'With.Config'
Assert-IdleNoScriptBlock -InputObject $value -Path 'Request.Context.Something'
```

The `Assert-IdleNoScriptBlock` function is exported from `IdLE.Core` and recursively validates hashtables, arrays, and PSCustomObjects.

**Do not implement custom ScriptBlock validation.** Use the centralized helper to ensure consistent enforcement across all steps.

---

## Outputs
Expand Down
29 changes: 12 additions & 17 deletions docs/reference/steps/step-create-identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,27 @@ Creates a new identity in the target system.

## Description

The host must supply a provider instance via
Context.Providers[&lt;ProviderAlias&gt;] that implements CreateIdentity(identityKey, attributes)
The engine passes the provider resolved from With.Provider (default 'Identity') as -Provider.
The provider implements CreateIdentity(identityKey, attributes)
and returns an object with properties 'IdentityKey' and 'Changed'.

The step is idempotent by design: if the identity already exists, the provider
should return Changed = $false without creating a duplicate.

Authentication:

- If With.AuthSessionName is present, the step acquires an auth session via
Context.AcquireAuthSession(Name, Options) and passes it to the provider method
if the provider supports an AuthSession parameter.

- With.AuthSessionOptions (optional, hashtable) is passed to the broker for
session selection (e.g., @\{ Role = 'Tier0' \}).

- ScriptBlocks in AuthSessionOptions are rejected (security boundary).
Authentication is handled by the engine: With.AuthSessionName and With.AuthSessionOptions select the
auth session passed to provider methods that declare an AuthSession parameter.

## Inputs (With.*)

The following keys are required in the step's ``With`` configuration:
Validated at plan time against the step's ``WithSchema`` (see [Step metadata contract](../../extend/steps.md#step-metadata-contract)).

| Key | Required | Description |
| --- | --- | --- |
| `Attributes` | Yes | Hashtable of attributes to set |
| `IdentityKey` | Yes | Unique identifier for the identity |
| Key | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `Attributes` | Hashtable | Yes | | |
| `IdentityKey` | String | Yes | | |
| `AuthSessionName` | String | No | broker default session | Auth session routing key for the AuthSessionBroker. |
| `AuthSessionOptions` | Hashtable | No | | Data-only options passed to the AuthSessionBroker. |
| `Provider` | String | No | `Identity` | Provider alias in `Providers`. |

## Example

Expand Down
27 changes: 11 additions & 16 deletions docs/reference/steps/step-delete-identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ Deletes an identity from the target system.

## Description

The host must supply a provider instance via
Context.Providers[&lt;ProviderAlias&gt;] that implements DeleteIdentity(identityKey)
The engine passes the provider resolved from With.Provider (default 'Identity') as -Provider.
The provider implements DeleteIdentity(identityKey)
and returns an object with properties 'IdentityKey' and 'Changed'.

The step is idempotent by design: if the identity is already deleted, the provider
Expand All @@ -27,24 +27,19 @@ IMPORTANT: This step requires the provider to advertise the IdLE.Identity.Delete
capability, which is typically opt-in for safety. The provider must be configured
to allow deletion (e.g., AllowDelete = $true for AD provider).

Authentication:

- If With.AuthSessionName is present, the step acquires an auth session via
Context.AcquireAuthSession(Name, Options) and passes it to the provider method
if the provider supports an AuthSession parameter.

- With.AuthSessionOptions (optional, hashtable) is passed to the broker for
session selection (e.g., @\{ Role = 'Tier0' \}).

- ScriptBlocks in AuthSessionOptions are rejected (security boundary).
Authentication is handled by the engine: With.AuthSessionName and With.AuthSessionOptions select the
auth session passed to provider methods that declare an AuthSession parameter.

## Inputs (With.*)

The following keys are required in the step's ``With`` configuration:
Validated at plan time against the step's ``WithSchema`` (see [Step metadata contract](../../extend/steps.md#step-metadata-contract)).

| Key | Required | Description |
| --- | --- | --- |
| `IdentityKey` | Yes | Unique identifier for the identity |
| Key | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `IdentityKey` | String | Yes | | |
| `AuthSessionName` | String | No | broker default session | Auth session routing key for the AuthSessionBroker. |
| `AuthSessionOptions` | Hashtable | No | | Data-only options passed to the AuthSessionBroker. |
| `Provider` | String | No | `Identity` | Provider alias in `Providers`. |

## Example

Expand Down
Loading
Loading