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
82 changes: 30 additions & 52 deletions examples/run-demo.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,20 @@ function Format-EventRow {
$time = ([DateTime]$Event.TimestampUtc).ToLocalTime().ToString('HH:mm:ss.fff')
$step = if ([string]::IsNullOrWhiteSpace($Event.StepName)) { '-' } else { [string]$Event.StepName }

$msg = [string]$Event.Message

# IMPORTANT: Show error details if the engine attached them.
if ($Event.PSObject.Properties.Name -contains 'Data' -and $Event.Data -is [hashtable]) {
if ($Event.Data.ContainsKey('Error') -and -not [string]::IsNullOrWhiteSpace([string]$Event.Data.Error)) {
$msg = "$msg | ERROR: $([string]$Event.Data.Error)"
}
}

[pscustomobject]@{
Time = $time
Type = "$icon $($Event.Type)"
Step = $step
Message = $Event.Message
Message = $msg
}
}

Expand All @@ -66,70 +75,39 @@ function Write-ResultSummary {
Write-Host ("Events: " + ($counts -join ', '))
}

Import-Module (Join-Path $PSScriptRoot '..\src\IdLE\IdLE.psd1') -Force
Import-Module (Join-Path $PSScriptRoot '..\src\IdLE.Steps.Common\IdLE.Steps.Common.psd1') -Force
# Import modules from the repo (path-based import, no global installation required).
Import-Module (Join-Path $PSScriptRoot '..\src\IdLE\IdLE.psd1') -Force -ErrorAction Stop
Import-Module (Join-Path $PSScriptRoot '..\src\IdLE.Steps.Common\IdLE.Steps.Common.psd1') -Force -ErrorAction Stop
Import-Module (Join-Path $PSScriptRoot '..\src\IdLE.Provider.Mock\IdLE.Provider.Mock.psd1') -Force -ErrorAction Stop

$workflowPath = Join-Path $PSScriptRoot 'workflows\joiner-with-when.psd1'
# Select demo workflow.
$workflowPath = Join-Path -Path $PSScriptRoot -ChildPath 'workflows\joiner-minimal-ensureattribute.psd1'

$request = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' -Actor 'example-user'
# Validate workflow early for clear errors.
Test-IdleWorkflow -WorkflowPath $workflowPath | Out-Null

# Create request and plan.
$request = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' -Actor 'example-user'
$plan = New-IdlePlan -WorkflowPath $workflowPath -Request $request

# Host-provided step registry:
# The handler can be a scriptblock (ideal for tests/examples) or a function name.
$emitHandler = {
param($Context, $Step)

# Support both hashtable/dictionary and PSCustomObject step shapes.
$stepName = $null
$stepType = $null
$with = $null

if ($Step -is [System.Collections.IDictionary]) {
$stepName = if ($Step.Contains('Name')) { [string]$Step['Name'] } else { $null }
$stepType = if ($Step.Contains('Type')) { [string]$Step['Type'] } else { $null }
$with = if ($Step.Contains('With')) { $Step['With'] } else { $null }
}
else {
$stepName = if ($Step.PSObject.Properties['Name']) { [string]$Step.Name } else { $null }
$stepType = if ($Step.PSObject.Properties['Type']) { [string]$Step.Type } else { $null }
$with = if ($Step.PSObject.Properties['With']) { $Step.With } else { $null }
}

$msg = $null
if ($with -is [System.Collections.IDictionary] -and $with.Contains('Message')) {
$msg = [string]$with['Message']
}
elseif ($null -ne $with -and $with.PSObject.Properties['Message']) {
$msg = [string]$with.Message
}

if ([string]::IsNullOrWhiteSpace($msg)) {
$msg = 'EmitEvent executed.'
}

& $Context.WriteEvent 'Custom' $msg $stepName @{ StepType = $stepType }

[pscustomobject]@{
PSTypeName = 'IdLE.StepResult'
Name = $stepName
Type = $stepType
Status = 'Completed'
Error = $null
}
}

# Host-provided providers.
$providers = @{
StepRegistry = @{
'IdLE.Step.EmitEvent' = $emitHandler
}
Identity = New-IdleMockIdentityProvider
}

$result = Invoke-IdlePlan -Plan $plan -Providers $providers

Write-DemoHeader "IdLE Demo – Plan Execution"
Write-ResultSummary -Result $result

Write-Host ""
Write-DemoHeader "Step Results"
$result.Steps |
Select-Object Name, Type, Status,
@{ Name = 'Changed'; Expression = { if ($_.PSObject.Properties.Name -contains 'Changed') { $_.Changed } else { $null } } },
Error |
Format-Table -AutoSize

Write-Host ""
Write-DemoHeader "Event Stream"
$result.Events |
Expand Down
33 changes: 33 additions & 0 deletions examples/workflows/joiner-minimal-ensureattribute.psd1
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
@{
Name = 'Joiner - Minimal (EnsureAttribute)'
LifecycleEvent = 'Joiner'

Steps = @(
@{
Name = 'Emit start'
Type = 'IdLE.Step.EmitEvent'
With = @{
Message = 'Joiner workflow started (minimalpack).'
}
}

@{
Name = 'Ensure Department'
Type = 'IdLE.Step.EnsureAttribute'
With = @{
Provider = 'Identity'
IdentityKey = 'user1'
Name = 'Department'
Value = 'IT'
}
}

@{
Name = 'Emit done'
Type = 'IdLE.Step.EmitEvent'
With = @{
Message = 'Joiner workflow completed (minimalpack).'
}
}
)
}
63 changes: 39 additions & 24 deletions src/IdLE.Core/Private/Get-IdleStepRegistry.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -6,47 +6,62 @@ function Get-IdleStepRegistry {
[object] $Providers
)

# Registry maps workflow Step.Type -> handler.
# Registry maps workflow Step.Type -> handler
# Handler can be:
# - string : PowerShell function name
# - scriptblock : executable handler (ideal for tests / hosts)
# - [string] : PowerShell function name
# - [scriptblock] : executable handler (useful for tests / hosts)
$registry = [hashtable]::new([System.StringComparer]::OrdinalIgnoreCase)

if ($null -eq $Providers) {
return $registry
}
# 1) Copy host-provided StepRegistry (optional)
# We support two shapes for compatibility:
# - StepRegistry['Type'] = 'FunctionName' | { scriptblock }
# - StepRegistry['Type'] = @{ Handler = 'FunctionName' } (legacy/demo style)
if ($null -ne $Providers) {

$source = $null

# 1) Providers as hashtable / dictionary (most common in tests)
if ($Providers -is [hashtable] -or $Providers -is [System.Collections.IDictionary]) {
if ($Providers.Contains('StepRegistry') -and $Providers['StepRegistry'] -is [hashtable]) {
# Clone to avoid mutating host-provided hashtable during execution.
$source = $Providers['StepRegistry']
foreach ($k in $source.Keys) {
$registry[[string]$k] = $source[$k]
if ($Providers -is [System.Collections.IDictionary]) {
if ($Providers.Contains('StepRegistry')) {
$source = $Providers['StepRegistry']
}
}
else {
$prop = $Providers.PSObject.Properties['StepRegistry']
if ($null -ne $prop) {
$source = $prop.Value
}
}

return $registry
}
if ($null -ne $source -and ($source -is [System.Collections.IDictionary])) {
foreach ($k in @($source.Keys)) {

$v = $source[$k]

# 2) Providers as object with property StepRegistry (host objects)
# StrictMode-safe: do NOT access $Providers.StepRegistry unless the property exists.
$prop = $Providers.PSObject.Properties['StepRegistry']
if ($null -ne $prop -and $prop.Value -is [hashtable]) {
$source = $prop.Value
foreach ($k in $source.Keys) {
$registry[[string]$k] = $source[$k]
# Allow legacy shape: @{ Handler = 'Invoke-...' }
if ($v -is [hashtable] -and $v.ContainsKey('Handler')) {
$v = $v['Handler']
}

$registry[[string]$k] = $v
}
}
}

# Add built-in defaults only if the step implementation is actually available.
# This keeps IdLE's public surface minimal: steps are optional modules.
# 2) Built-in defaults (only if commands are available)
# Do not overwrite host-provided entries.
if (-not $registry.ContainsKey('IdLE.Step.EmitEvent')) {
$cmd = Get-Command -Name 'Invoke-IdleStepEmitEvent' -ErrorAction SilentlyContinue
if ($null -ne $cmd) {
$registry['IdLE.Step.EmitEvent'] = $cmd.Name
}
}

if (-not $registry.ContainsKey('IdLE.Step.EnsureAttribute')) {
$cmd = Get-Command -Name 'Invoke-IdleStepEnsureAttribute' -ErrorAction SilentlyContinue
if ($null -ne $cmd) {
$registry['IdLE.Step.EnsureAttribute'] = $cmd.Name
}
}

return $registry
}
22 changes: 22 additions & 0 deletions src/IdLE.Provider.Mock/IdLE.Provider.Mock.psd1
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
@{
RootModule = 'IdLE.Provider.Mock.psm1'
ModuleVersion = '0.2.0'
GUID = 'e661d3d6-1797-4cb1-b173-474982dbd653'
Author = 'Matthias Fleschuetz'
Copyright = '(c) Matthias Fleschuetz. All rights reserved.'
Description = 'Mock provider implementation for IdLE (in-memory, deterministic).'
PowerShellVersion = '7.0'

FunctionsToExport = @(
'New-IdleMockIdentityProvider'
)

PrivateData = @{
PSData = @{
Tags = @('Identity Lifecycle Engine', 'IdLE', 'Provider', 'Mock')
LicenseUri = 'https://www.apache.org/licenses/LICENSE-2.0'
ProjectUri = 'https://github.com/blindzero/IdentityLifecycleEngine'
ContactEmail = '13959569+blindzero@users.noreply.github.com'
}
}
}
17 changes: 17 additions & 0 deletions src/IdLE.Provider.Mock/IdLE.Provider.Mock.psm1
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#requires -Version 7.0
Set-StrictMode -Version Latest

$PublicPath = Join-Path -Path $PSScriptRoot -ChildPath 'Public'
if (Test-Path -Path $PublicPath) {

# Materialize the list first to avoid enumeration issues if the session/module state changes during import.
$publicScripts = @(Get-ChildItem -Path $PublicPath -Filter '*.ps1' -File | Sort-Object -Property FullName)

foreach ($script in $publicScripts) {
. $script.FullName
}
}

Export-ModuleMember -Function @(
'New-IdleMockIdentityProvider'
)
Loading