Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
fa7f66d
core: add AcquireAuthSession on execution context
blindzero Jan 16, 2026
8679e58
test(core): cover AcquireAuthSession via AuthSessionBroker
blindzero Jan 16, 2026
a02f3d6
chore: added use only approved verbs and no global surface for privat…
blindzero Jan 16, 2026
e719bb2
fix(core): handle IDictionary steps; fail fast on missing AuthSession…
blindzero Jan 16, 2026
4dc82cb
test(core): ensure AuthSessionBroker is redacted at output boundary
blindzero Jan 16, 2026
de19e34
tests: IgnoreAssignmentOperatorInsideHashTable in ScriptAnalyze to $t…
blindzero Jan 16, 2026
eb45e54
feat(core): Add backwards-compatible Context parameter passing
blindzero Jan 16, 2026
aa50df1
tests: fix scriptanalyzer warnings on equal operator space issues on …
blindzero Jan 16, 2026
c120cc3
docs: document AuthSessionBroker and AcquireAuthSession
blindzero Jan 16, 2026
e142dc6
chore: adding eol=lf for html files
blindzero Jan 16, 2026
de4aaf2
docs: document AuthSessionBroker contract
blindzero Jan 16, 2026
9b655c5
docs: expand events guidance for sinks and secret handling
blindzero Jan 16, 2026
02d55a1
Merge branch 'main' into issues/77-architecture-provider-authenticati…
blindzero Jan 16, 2026
f4b4ad3
core: extract Assert-IdleNoScriptBlockInAuthSessionOptions to private…
blindzero Jan 16, 2026
bc4599e
Merge branch 'issues/77-architecture-provider-authentication-session-…
blindzero Jan 16, 2026
4f7c653
fix(core): skip broker requirement for NotApplicable AcquireAuthSessi…
blindzero Jan 16, 2026
7e9ad77
tests: fix Use space after a comma. warning from PSSA but keep unary …
blindzero Jan 16, 2026
5ac2880
fix: removed inconsistent error field not available from StepCompleted
blindzero Jan 16, 2026
43a9838
fix Use space after a comma. warning from PSSA but keep unary comma b…
blindzero Jan 16, 2026
ce8ee78
Merge branch 'issues/77-architecture-provider-authentication-session-…
blindzero Jan 16, 2026
58d7430
fix: use defensive property access pattern for consistency
blindzero Jan 16, 2026
9b966e9
core: share Copy-IdleDataObject and deep-copy auth session options
blindzero Jan 16, 2026
b843640
Initial plan
Copilot Jan 16, 2026
eff92ac
test: add backwards compatibility test for step handlers without Cont…
Copilot Jan 16, 2026
42cbcc7
Merge pull request #82 from blindzero/copilot/sub-pr-81
blindzero Jan 17, 2026
1263b06
tests: assert Actor enrichment for auth session options
blindzero Jan 17, 2026
eb3ed35
tests: removed extra blank end lines
blindzero Jan 17, 2026
2ee4825
fix: removed duplicate $planPropNames definition
blindzero Jan 17, 2026
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
3 changes: 2 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
*.yaml text eol=lf
*.ps1 text eol=lf
*.psm1 text eol=lf
*.psd1 text eol=lf
*.psd1 text eol=lf
*.html text eol=lf
10 changes: 5 additions & 5 deletions PSScriptAnalyzerSettings.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@

Rules = @{
PSUseConsistentIndentation = @{
Enable = $true
IndentationSize = 4
Enable = $true
IndentationSize = 4
PipelineIndentation = 'IncreaseIndentationForFirstPipeline'
Kind = 'space'
Kind = 'space'
}

PSUseConsistentWhitespace = @{
PSUseConsistentWhitespace = @{
Enable = $true
CheckInnerBrace = $true
CheckOpenBrace = $true
Expand All @@ -52,7 +52,7 @@
CheckPipeForRedundantWhitespace = $false
CheckSeparator = $true
CheckParameter = $false
IgnoreAssignmentOperatorInsideHashTable = $false
IgnoreAssignmentOperatorInsideHashTable = $true
}
}
}
4 changes: 3 additions & 1 deletion STYLEGUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ It follows widely accepted **GitHub and PowerShell community conventions**.
- Verb-Noun cmdlet naming
- Singular nouns
- Avoid abbreviations
- Only approved Verbs

---

Expand Down Expand Up @@ -87,7 +88,7 @@ Steps must:
Providers:

- handle authentication
- use `ExecutionContext.AcquireSession()`
- use `ExecutionContext.AcquireAuthSession()`
- must be mockable
- must not assume global state

Expand Down Expand Up @@ -129,6 +130,7 @@ This style guide focuses on **in-code documentation rules** (comment-based help,
- validate early
- write tests
- document decisions
- keep surface small, no private function exports

### Don't

Expand Down
33 changes: 32 additions & 1 deletion docs/advanced/extensibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,35 @@ A new provider typically involves:

1. A contract interface (if not already present)
2. A provider implementation module
3. Session acquisition via host execution context
3. Auth session acquisition via host execution context (AuthSessionBroker)
4. Contract tests and unit tests

### Auth session acquisition (AuthSessionBroker)

IdLE keeps authentication out of the core engine. Hosts provide an auth session broker
that is responsible for obtaining and caching authenticated runtime handles (tokens,
Graph clients, Exchange Online sessions, LDAP binds, etc.).

- Hosts MUST pass the broker via `Providers.AuthSessionBroker`.
- Providers SHOULD acquire sessions through the execution context:
- `Context.AcquireAuthSession(Name, Options)`

Broker contract:

- The broker MUST expose an `AcquireAuthSession(Name, Options)` method.
- `Name` is a routing key (for example: `MicrosoftGraph`, `ExchangeOnline`, `ActiveDirectory`).
- `Options` is optional (`$null` is treated as an empty hashtable) and must be data-only:
- ScriptBlock values are rejected, including nested values.
- The engine enriches options with `CorrelationId` and `Actor` when available.
- The engine deep-copies `Options` before invoking the broker; brokers MUST treat
options as immutable and MUST NOT mutate nested values.

Security notes:

- Do not embed credentials directly in `Options`.
- Treat `Options` as configuration input, not a secret store.
- Use host secret management and keep secrets out of plans, events, and exports.

### Capability Advertisement

Providers must explicitly advertise their supported capabilities via a
Expand Down Expand Up @@ -56,6 +82,7 @@ Do not add:

- interactive prompts
- authentication code inside steps
- authentication flows inside providers (use AuthSessionBroker)
- UI or web server dependencies

Those belong in a host application.
Expand All @@ -68,3 +95,7 @@ Steps are executed via a host-provided step registry.
- The host maps this identifier to a **function name** (string) in 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.
36 changes: 36 additions & 0 deletions docs/reference/events-and-observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,24 @@ Buffering is the default because it keeps the core:

---

### Optional streaming to a host sink

Hosts may provide an **event sink** to receive events as they happen.

The engine expects an object with a `WriteEvent(event)` method.
If present, the engine forwards each event to that sink **in addition to** buffering
events in the execution result.

This enables patterns such as:

- forward events to a central logging system
- push events to a message bus
- render progress updates in an interactive host

The engine still does not assume anything about formatting, persistence, or transport.

---

## Usage

### Typical consumption patterns
Expand Down Expand Up @@ -142,6 +160,24 @@ This separation ensures that IdLE remains portable and reusable across environme

---

## Security and sensitive data

Events are a primary observability surface and therefore a common place where sensitive
values can accidentally leak.

Guidelines:

- **Do not emit secrets** in `Event.Data` (tokens, passwords, client secrets, API keys, certificates).
- Prefer **references** (for example: identity IDs, step names, correlation IDs) over raw payloads.
- The engine applies **redaction** at output boundaries (buffered events and host sinks) for
common sensitive key names (for example: `password`, `token`, `secret`, `apiKey`).
Redaction is a safety net, not a design strategy.
- When acquiring authentication sessions, use `Providers.AuthSessionBroker` via
`Context.AcquireAuthSession(Name, Options)`. Auth session options are a data-only boundary
and reject ScriptBlocks.

---

## Common pitfalls

### “Why don’t I see any events?”
Expand Down
66 changes: 66 additions & 0 deletions docs/reference/providers-and-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,72 @@ their implementation beyond contract usage.

---

## Auth session acquisition (AuthSessionBroker)

Many providers require authenticated connections (tokens, API clients, remote sessions).
IdLE keeps authentication out of the engine and out of individual providers by using a
host-supplied broker.

### Contract

The host injects an **AuthSessionBroker** into the providers map:

- `Providers.AuthSessionBroker`

During execution, steps and providers may acquire sessions via the execution context:

- `Context.AcquireAuthSession(Name, Options)`

Where:

- `Name` identifies the requested session (e.g. `Graph`, `ExchangeOnline`, `Ldap`, ...).
- `Options` is an optional **data-only** hashtable.
- `$null` is treated as an empty hashtable.
- ScriptBlocks are rejected, including nested values.

The broker must expose a method:

- `AcquireAuthSession(Name, Options)`

### Responsibility boundaries

- **Engine**
- Provides `Context.AcquireAuthSession()` as a stable API.
- Enforces the data-only boundary for `Options`.
- Does not implement authentication.

- **Host**
- Implements and configures the AuthSessionBroker.
- Decides how to authenticate (interactive, managed identity, certificate, secrets, ...).
- Must ensure secrets are not leaked into plans, events, or exports.

- **Providers / Steps**
- Request sessions through the execution context.
- Must not perform their own authentication flows.

### Enrichment

The execution context may enrich the broker request with common run metadata, such as:

- `CorrelationId`
- `Actor`

Providers and steps should treat these values as optional.

---

## Execution context injection (backwards compatibility)

IdLE step handlers can optionally accept a `Context` parameter.

To remain backwards compatible, the engine passes `-Context $Context` **only if** the
handler supports a `Context` parameter.

Guidance:

- New step handlers should accept `Context` to access providers, event sink, and auth session acquisition.
- Existing handlers without `Context` continue to work unchanged.

## Common pitfalls

### Treating providers as part of the engine
Expand Down
2 changes: 1 addition & 1 deletion src/IdLE.Core/IdLE.Core.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Set-StrictMode -Version Latest

$PublicPath = Join-Path -Path $PSScriptRoot -ChildPath 'Public'
$PublicPath = Join-Path -Path $PSScriptRoot -ChildPath 'Public'
$PrivatePath = Join-Path -Path $PSScriptRoot -ChildPath 'Private'

foreach ($path in @($PrivatePath, $PublicPath)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Validates that auth session options do not contain ScriptBlock objects.
# Recursively walks hashtables, enumerables, and PSCustomObjects.
# Enforces the security boundary: auth session options must be data-only.

function Assert-IdleNoScriptBlockInAuthSessionOptions {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[AllowNull()]
[object] $InputObject,

[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $Path
)

if ($null -eq $InputObject) { return }

if ($InputObject -is [scriptblock]) {
throw [System.ArgumentException]::new(
"ScriptBlocks are not allowed in auth session options. Found at: $Path",
$Path
)
}

if ($InputObject -is [System.Collections.IDictionary]) {
foreach ($key in $InputObject.Keys) {
Assert-IdleNoScriptBlockInAuthSessionOptions -InputObject $InputObject[$key] -Path "$Path.$key"
}
return
}

if (($InputObject -is [System.Collections.IEnumerable]) -and ($InputObject -isnot [string])) {
$i = 0
foreach ($item in $InputObject) {
Assert-IdleNoScriptBlockInAuthSessionOptions -InputObject $item -Path "$Path[$i]"
$i++
}
return
}

if ($InputObject -is [pscustomobject]) {
foreach ($p in $InputObject.PSObject.Properties) {
if ($p.MemberType -eq 'NoteProperty') {
Assert-IdleNoScriptBlockInAuthSessionOptions -InputObject $p.Value -Path "$Path.$($p.Name)"
}
}
}
}
10 changes: 5 additions & 5 deletions src/IdLE.Core/Private/ConvertTo-IdlePlanExportObject.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,13 @@ function ConvertTo-IdlePlanExportObject {
# When present, export these as the canonical request.input payload.
$identityKeys = Get-FirstPropertyValue -Object $request -Names @('IdentityKeys', 'IdentityKey', 'Keys')
$desiredState = Get-FirstPropertyValue -Object $request -Names @('DesiredState', 'TargetState')
$changes = Get-FirstPropertyValue -Object $request -Names @('Changes', 'Delta')
$changes = Get-FirstPropertyValue -Object $request -Names @('Changes', 'Delta')

if ($null -ne $identityKeys -or $null -ne $desiredState -or $null -ne $changes) {
$requestInput = New-OrderedMap
$requestInput.identityKeys = $identityKeys
$requestInput.desiredState = $desiredState
$requestInput.changes = $changes
$requestInput.changes = $changes
}
}
}
Expand Down Expand Up @@ -138,10 +138,10 @@ function ConvertTo-IdlePlanExportObject {
}

$requestMap = New-OrderedMap
$requestMap.type = $requestType
$requestMap.type = $requestType
$requestMap.correlationId = $correlationId
$requestMap.actor = $actor
$requestMap.input = $redactedRequestInput
$requestMap.actor = $actor
$requestMap.input = $redactedRequestInput

# ---- Plan block ----------------------------------------------------------
$planId = ConvertTo-NullIfEmptyString -Value (
Expand Down
67 changes: 67 additions & 0 deletions src/IdLE.Core/Private/Copy-IdleDataObject.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
function Copy-IdleDataObject {
<#
.SYNOPSIS
Creates a deep-ish, data-only copy of an object.

.DESCRIPTION
This helper is used to snapshot data-like objects so that exported or executed
artifacts do not retain references to caller-owned objects.

NOTE:
This is intentionally conservative and only supports data-like objects:
- Hashtable / OrderedDictionary
- PSCustomObject / NoteProperties
- Arrays/lists
- Primitive types

ScriptBlocks and other executable objects are rejected by upstream validation.
#>
[CmdletBinding()]
param(
[Parameter()]
[AllowNull()]
[object] $Value
)

if ($null -eq $Value) { return $null }

# Primitive / immutable types should be returned as-is before property inspection.
# This prevents strings from being converted to PSCustomObject with Length property.
if ($Value -is [string] -or
$Value -is [int] -or
$Value -is [long] -or
$Value -is [double] -or
$Value -is [decimal] -or
$Value -is [bool] -or
$Value -is [datetime] -or
$Value -is [guid]) {
return $Value
}

if ($Value -is [System.Collections.IDictionary]) {
$copy = @{}
foreach ($k in $Value.Keys) {
$copy[$k] = Copy-IdleDataObject -Value $Value[$k]
}
return $copy
}

if ($Value -is [System.Collections.IEnumerable] -and $Value -isnot [string]) {
$arr = @()
foreach ($item in $Value) {
$arr += Copy-IdleDataObject -Value $item
}
return $arr
}

$props = @($Value.PSObject.Properties | Where-Object MemberType -in @('NoteProperty', 'Property'))
if ($null -ne $props -and @($props).Count -gt 0) {
$o = [ordered]@{}
foreach ($p in $props) {
$o[$p.Name] = Copy-IdleDataObject -Value $p.Value
}
return [pscustomobject]$o
}

return $Value
}
Loading