From 65b8824e9eeca49bfc89749769f2fc08927a69f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 14:27:00 +0000 Subject: [PATCH 01/23] Initial plan From 3cee7b5122aae3c6c9d7493c85fb3f4d07c8f9fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 14:34:25 +0000 Subject: [PATCH 02/23] fix: support ComputerName+Credential flow for EntraConnect DirectorySync Agent-Logs-Url: https://github.com/blindzero/IdentityLifecycleEngine/sessions/b70820fb-7961-4838-961b-84daf403564c Co-authored-by: ntt-matthias-fleschuetz <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> --- .../provider-directorysync-entraconnect.md | 27 ++-- .../steps/step-trigger-directory-sync.md | 6 +- .../ad-joiner-entraconnect-entraid.psd1 | 4 +- ...rectorysync-entraconnect-trigger-sync.psd1 | 8 +- ...-IdleEntraConnectDirectorySyncProvider.ps1 | 135 +++++++++++++----- .../Invoke-IdleStepTriggerDirectorySync.ps1 | 22 ++- .../StepMetadataCatalog.psd1 | 2 +- ...roviderComputerNameCredential.Contract.ps1 | 83 +++++++++++ ...ntraConnectDirectorySyncProvider.Tests.ps1 | 118 +++++++++------ ...oke-IdleStepTriggerDirectorySync.Tests.ps1 | 25 +++- .../workflows/joiner-with-dirsync.psd1 | 1 + 11 files changed, 322 insertions(+), 109 deletions(-) create mode 100644 tests/ProviderContracts/DirectorySyncProviderComputerNameCredential.Contract.ps1 diff --git a/docs/reference/providers/provider-directorysync-entraconnect.md b/docs/reference/providers/provider-directorysync-entraconnect.md index f4a4ecb5..1432649f 100644 --- a/docs/reference/providers/provider-directorysync-entraconnect.md +++ b/docs/reference/providers/provider-directorysync-entraconnect.md @@ -11,7 +11,7 @@ import EntraConnectTriggerSync from '@site/../examples/workflows/templates/direc - **Module:** `IdLE.Provider.DirectorySync.EntraConnect` - **What it’s for:** Triggering and monitoring **Entra Connect (ADSync)** sync cycles on an on-prem server -- **Execution model:** Remote execution via a host-provided AuthSession (elevated context) +- **Execution model:** Remote execution via provider-managed PSRemoting using a host-provided credential ## When to use @@ -35,7 +35,7 @@ Non-goals: ### Requirements - An Entra Connect (Azure AD Connect) server with ADSync installed (ADSync cmdlets available) -- A host/runtime that can provide an **elevated remote execution handle** to IdLE via AuthSessionBroker +- A host/runtime that can provide an **elevated credential** to IdLE via AuthSessionBroker - Rights to run `Start-ADSyncSyncCycle` and `Get-ADSyncScheduler` in that remote context ### Install (PowerShell Gallery) @@ -68,18 +68,15 @@ $providers = @{ ## Authentication (important) -This provider requires an AuthSession that supports remote execution and **must be elevated**. +This provider requires an AuthSession credential ([PSCredential]) and **must be elevated**. +The provider creates and cleans up PSRemoting sessions internally. -The AuthSession object must provide a method: - -- `InvokeCommand(CommandName, Parameters)` - -Your host/runtime should provide this session via the AuthSessionBroker and you reference it in the step via: +Your host/runtime should provide this credential via the AuthSessionBroker and you reference it in the step via: - `AuthSessionName = 'EntraConnect'` -- `AuthSessionOptions = @{ Role = 'EntraConnectAdmin' }` (optional routing key) +- `ComputerName = 'ad-sync1.corp.local'` -> No interactive prompts are made. If the remote context is not elevated, triggering a sync cycle will fail with a privilege/elevation error. +> No interactive prompts are made. If the credential does not have elevated rights on the target server, triggering a sync cycle will fail with a privilege/elevation error. ## Supported operations @@ -102,8 +99,8 @@ This provider does not advertise these capabilities, so it cannot be used in the ## Configuration This provider has no admin-facing option bag. Configuration is done through: -- step inputs (`PolicyType`, `Wait`, `TimeoutSeconds`, `PollIntervalSeconds`) -- host configuration (remote connection and elevation) +- step inputs (`ComputerName`, `PolicyType`, `Wait`, `TimeoutSeconds`, `PollIntervalSeconds`) +- host configuration (credential broker) ## Examples (canonical template) @@ -111,7 +108,7 @@ This provider has no admin-facing option bag. Configuration is done through: ## Troubleshooting -- **“Missing privileges or elevation”**: your AuthSession must run commands in an elevated context on the Entra Connect server. -- **“AuthSession must implement InvokeCommand”**: your host must provide an AuthSession object with an `InvokeCommand()` method. -- **Get-ADSyncScheduler not found**: ensure ADSync cmdlets are available in the remote session (module installed/accessible). +- **“Missing privileges or elevation”**: ensure the provided credential is elevated on the Entra Connect server. +- **“AuthSession must be a [PSCredential]”**: configure `New-IdleAuthSession -AuthSessionType Credential`. +- **Get-ADSyncScheduler not found**: ensure ADSync cmdlets are available on the target server. - **Timeout waiting for completion**: increase `TimeoutSeconds` or check the scheduler state on the server. diff --git a/docs/reference/steps/step-trigger-directory-sync.md b/docs/reference/steps/step-trigger-directory-sync.md index f4797ae9..93acac7b 100644 --- a/docs/reference/steps/step-trigger-directory-sync.md +++ b/docs/reference/steps/step-trigger-directory-sync.md @@ -19,9 +19,9 @@ Triggers a directory sync cycle and optionally waits for completion. The host must supply a provider instance via Context.Providers[<ProviderAlias>] that implements: -- StartSyncCycle(PolicyType, AuthSession) +- StartSyncCycle(PolicyType, ComputerName, AuthSession) -- GetSyncCycleState(AuthSession) +- GetSyncCycleState(ComputerName, AuthSession) The step is designed for remote execution and requires an elevated auth session provided by the host's AuthSessionBroker. @@ -41,6 +41,7 @@ The following keys are required in the step's ``With`` configuration: | Key | Required | Description | | --- | --- | --- | | `AuthSessionName` | Yes | Name of auth session to use (optional) | +| `ComputerName` | Yes | See step description for details | | `PolicyType` | Yes | Type of policy (e.g., Delta, Initial) | ## Example @@ -51,6 +52,7 @@ $step = @{ Type = 'IdLE.Step.TriggerDirectorySync' With = @{ AuthSessionName = 'DirectorySync' + ComputerName = 'ad-sync1.corp.local' PolicyType = 'Delta' Wait = $true } diff --git a/examples/workflows/templates/ad-joiner-entraconnect-entraid.psd1 b/examples/workflows/templates/ad-joiner-entraconnect-entraid.psd1 index 9e0d2711..66e8d52a 100644 --- a/examples/workflows/templates/ad-joiner-entraconnect-entraid.psd1 +++ b/examples/workflows/templates/ad-joiner-entraconnect-entraid.psd1 @@ -27,9 +27,7 @@ With = @{ Provider = 'DirectorySync' AuthSessionName = 'EntraConnect' - AuthSessionOptions = @{ - Role = 'EntraConnectAdmin' - } + ComputerName = '{{Request.Intent.EntraConnectServer}}' PolicyType = 'Delta' Wait = $true diff --git a/examples/workflows/templates/directorysync-entraconnect-trigger-sync.psd1 b/examples/workflows/templates/directorysync-entraconnect-trigger-sync.psd1 index a8470938..4b9b3780 100644 --- a/examples/workflows/templates/directorysync-entraconnect-trigger-sync.psd1 +++ b/examples/workflows/templates/directorysync-entraconnect-trigger-sync.psd1 @@ -10,11 +10,9 @@ With = @{ Provider = 'DirectorySync' - # Auth session is provided by the host (remote execution handle). + # Auth session is provided by the host (credential). AuthSessionName = 'EntraConnect' - AuthSessionOptions = @{ - Role = 'EntraConnectAdmin' - } + ComputerName = '{{Request.Intent.ComputerName}}' # Delta or Initial PolicyType = '{{Request.Intent.PolicyType}}' @@ -34,4 +32,4 @@ } } ) -} \ No newline at end of file +} diff --git a/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 b/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 index 297bb91c..5ccd52b0 100644 --- a/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 +++ b/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 @@ -7,16 +7,15 @@ function New-IdleEntraConnectDirectorySyncProvider { This provider triggers and monitors Entra ID Connect (ADSync) sync cycles on an on-premises server via remote execution. - The provider uses an AuthSession object (remote execution handle) provided by the host. - The AuthSession must implement InvokeCommand(CommandName, Parameters) to execute - commands in an elevated/privileged context on the Entra Connect server. + The provider uses a credential AuthSession provided by the host and establishes + a PSRemoting session to the target Entra Connect server internally. No interactive prompts are made; elevation and authentication are the host's responsibility via the AuthSessionBroker. .OUTPUTS PSCustomObject - Provider instance with methods: GetCapabilities(), StartSyncCycle(PolicyType, AuthSession), GetSyncCycleState(AuthSession) + Provider instance with methods: GetCapabilities(), StartSyncCycle(PolicyType, ComputerName, AuthSession), GetSyncCycleState(ComputerName, AuthSession) .EXAMPLE $provider = New-IdleEntraConnectDirectorySyncProvider @@ -24,15 +23,10 @@ function New-IdleEntraConnectDirectorySyncProvider { # Returns: @('IdLE.DirectorySync.Trigger', 'IdLE.DirectorySync.Status') .EXAMPLE - # With a mock remote execution handle - $mockAuthSession = [pscustomobject]@{ - InvokeCommand = { param($CommandName, $Parameters) - # Mock implementation - return @{ Started = $true } - } - } + # With a credential from AuthSessionBroker (AuthSessionType='Credential') + $credential = Get-Credential $provider = New-IdleEntraConnectDirectorySyncProvider - $result = $provider.StartSyncCycle('Delta', $mockAuthSession) + $result = $provider.StartSyncCycle('Delta', 'ad-sync1.corp.local', $credential) #> [CmdletBinding()] param() @@ -58,6 +52,50 @@ function New-IdleEntraConnectDirectorySyncProvider { ) } -Force + $provider | Add-Member -MemberType ScriptMethod -Name NewRemoteSession -Value { + param( + [Parameter(Mandatory)] + [string] $ComputerName, + + [Parameter(Mandatory)] + [pscredential] $Credential + ) + + return New-PSSession -ComputerName $ComputerName -Credential $Credential -ErrorAction Stop + } -Force + + $provider | Add-Member -MemberType ScriptMethod -Name InvokeRemoteCommand -Value { + param( + [Parameter(Mandatory)] + [object] $Session, + + [Parameter(Mandatory)] + [scriptblock] $ScriptBlock, + + [Parameter()] + [AllowNull()] + [object[]] $ArgumentList + ) + + if ($null -eq $ArgumentList -or $ArgumentList.Count -eq 0) { + return Invoke-Command -Session $Session -ScriptBlock $ScriptBlock -ErrorAction Stop + } + + return Invoke-Command -Session $Session -ScriptBlock $ScriptBlock -ArgumentList $ArgumentList -ErrorAction Stop + } -Force + + $provider | Add-Member -MemberType ScriptMethod -Name RemoveRemoteSession -Value { + param( + [Parameter(Mandatory)] + [AllowNull()] + [object] $Session + ) + + if ($null -ne $Session) { + Remove-PSSession -Session $Session -ErrorAction SilentlyContinue + } + } -Force + $provider | Add-Member -MemberType ScriptMethod -Name StartSyncCycle -Value { <# .SYNOPSIS @@ -69,9 +107,11 @@ function New-IdleEntraConnectDirectorySyncProvider { .PARAMETER PolicyType The sync policy type: 'Delta' or 'Initial'. + .PARAMETER ComputerName + Target Entra Connect server hostname for PSRemoting. + .PARAMETER AuthSession - Remote execution handle provided by the host's AuthSessionBroker. - Must implement InvokeCommand(CommandName, Parameters). + Credential ([PSCredential]) provided by the host's AuthSessionBroker. .OUTPUTS PSCustomObject with properties: @@ -83,29 +123,33 @@ function New-IdleEntraConnectDirectorySyncProvider { [ValidateSet('Delta', 'Initial', IgnoreCase = $true)] [string] $PolicyType, + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $ComputerName, + [Parameter(Mandatory)] [ValidateNotNull()] [object] $AuthSession ) - # Validate AuthSession contract - if ($null -eq $AuthSession.PSObject.Methods['InvokeCommand']) { - throw "AuthSession must implement InvokeCommand(CommandName, Parameters) method. " + ` - "The host must provide an elevated remote session via AuthSessionBroker." + if ($AuthSession -isnot [pscredential]) { + $actualType = $AuthSession.GetType().FullName + throw "AuthSession must be a [PSCredential] for PSRemoting session creation. Received: [$actualType]" } + $remoteSession = $null try { - # Execute Start-ADSyncSyncCycle remotely - # The remote session should already have ADSync module available or will import it - $AuthSession.InvokeCommand('Start-ADSyncSyncCycle', @{ - PolicyType = $PolicyType - }) | Out-Null - - # Start-ADSyncSyncCycle returns a result object or throws on error - # Success case: return Started = true + $remoteSession = $this.NewRemoteSession($ComputerName, $AuthSession) + + $this.InvokeRemoteCommand($remoteSession, { + param([string] $RemotePolicyType) + Import-Module -Name ADSync -ErrorAction Stop + Start-ADSyncSyncCycle -PolicyType $RemotePolicyType -ErrorAction Stop + }, @($PolicyType)) | Out-Null + return [pscustomobject]@{ Started = $true - Message = "Sync cycle triggered with PolicyType: $PolicyType" + Message = "Sync cycle triggered with PolicyType: $PolicyType on $ComputerName" } } catch { @@ -117,9 +161,11 @@ function New-IdleEntraConnectDirectorySyncProvider { "The AuthSession must provide an elevated execution context. Original error: $errorMessage" } - # Re-throw other errors throw "Failed to start sync cycle: $errorMessage" } + finally { + $this.RemoveRemoteSession($remoteSession) + } } -Force $provider | Add-Member -MemberType ScriptMethod -Name GetSyncCycleState -Value { @@ -131,9 +177,11 @@ function New-IdleEntraConnectDirectorySyncProvider { Queries the sync scheduler state via Get-ADSyncScheduler to determine if a sync cycle is currently in progress. + .PARAMETER ComputerName + Target Entra Connect server hostname for PSRemoting. + .PARAMETER AuthSession - Remote execution handle provided by the host's AuthSessionBroker. - Must implement InvokeCommand(CommandName, Parameters). + Credential ([PSCredential]) provided by the host's AuthSessionBroker. .OUTPUTS PSCustomObject with properties: @@ -142,20 +190,32 @@ function New-IdleEntraConnectDirectorySyncProvider { - Details (hashtable, optional): additional state information #> param( + [Parameter(Mandatory)] + [ValidateNotNull()] + [string] $ComputerName, + [Parameter(Mandatory)] [ValidateNotNull()] [object] $AuthSession ) - # Validate AuthSession contract - if ($null -eq $AuthSession.PSObject.Methods['InvokeCommand']) { - throw "AuthSession must implement InvokeCommand(CommandName, Parameters) method. " + ` - "The host must provide an elevated remote session via AuthSessionBroker." + if ([string]::IsNullOrWhiteSpace($ComputerName)) { + throw "ComputerName must not be null, empty, or whitespace." + } + + if ($AuthSession -isnot [pscredential]) { + $actualType = $AuthSession.GetType().FullName + throw "AuthSession must be a [PSCredential] for PSRemoting session creation. Received: [$actualType]" } + $remoteSession = $null try { - # Execute Get-ADSyncScheduler remotely - $scheduler = $AuthSession.InvokeCommand('Get-ADSyncScheduler', @{}) + $remoteSession = $this.NewRemoteSession($ComputerName, $AuthSession) + + $scheduler = $this.InvokeRemoteCommand($remoteSession, { + Import-Module -Name ADSync -ErrorAction Stop + Get-ADSyncScheduler -ErrorAction Stop + }, @()) # Determine if sync is in progress # Get-ADSyncScheduler returns an object with SyncCycleInProgress property @@ -195,6 +255,9 @@ function New-IdleEntraConnectDirectorySyncProvider { throw "Failed to get sync cycle state: $errorMessage" } + finally { + $this.RemoveRemoteSession($remoteSession) + } } -Force return $provider diff --git a/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 b/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 index b1733cb1..bf19d8da 100644 --- a/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 +++ b/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 @@ -6,8 +6,8 @@ function Invoke-IdleStepTriggerDirectorySync { .DESCRIPTION This is a provider-agnostic step. The host must supply a provider instance via Context.Providers[] that implements: - - StartSyncCycle(PolicyType, AuthSession) - - GetSyncCycleState(AuthSession) + - StartSyncCycle(PolicyType, ComputerName, AuthSession) + - GetSyncCycleState(ComputerName, AuthSession) The step is designed for remote execution and requires an elevated auth session provided by the host's AuthSessionBroker. @@ -23,6 +23,7 @@ function Invoke-IdleStepTriggerDirectorySync { .PARAMETER Step Normalized step object from the plan. Must contain a 'With' hashtable with keys: - AuthSessionName (required, string): auth session name for broker + - ComputerName (required, string): target Entra Connect server - PolicyType (required, string): 'Delta' or 'Initial' (case-insensitive) - Provider (optional, string): provider alias, defaults to 'DirectorySync' - Wait (optional, bool): wait for cycle completion, defaults to $false @@ -39,6 +40,7 @@ function Invoke-IdleStepTriggerDirectorySync { Type = 'IdLE.Step.TriggerDirectorySync' With = @{ AuthSessionName = 'DirectorySync' + ComputerName = 'ad-sync1.corp.local' PolicyType = 'Delta' Wait = $true } @@ -69,11 +71,20 @@ function Invoke-IdleStepTriggerDirectorySync { throw "TriggerDirectorySync requires With.PolicyType." } + if (-not $with.ContainsKey('ComputerName')) { + throw "TriggerDirectorySync requires With.ComputerName." + } + $policyType = [string]$with.PolicyType if ($policyType -notin @('Delta', 'Initial')) { throw "TriggerDirectorySync: With.PolicyType must be 'Delta' or 'Initial' (case-insensitive). Got: $policyType" } + $computerName = [string]$with.ComputerName + if ([string]::IsNullOrWhiteSpace($computerName)) { + throw "TriggerDirectorySync: With.ComputerName must not be null, empty, or whitespace." + } + # Optional inputs with defaults $providerAlias = if ($with.ContainsKey('Provider')) { [string]$with.Provider } else { 'DirectorySync' } $wait = if ($with.ContainsKey('Wait')) { [bool]$with.Wait } else { $false } @@ -105,6 +116,7 @@ function Invoke-IdleStepTriggerDirectorySync { # Trigger sync cycle $Context.EventSink.WriteEvent('DirectorySyncTriggered', "Triggering $policyType sync cycle", $stepName, @{ PolicyType = $policyType + ComputerName = $computerName }) $startResult = Invoke-IdleProviderMethod ` @@ -112,7 +124,7 @@ function Invoke-IdleStepTriggerDirectorySync { -With $with ` -ProviderAlias $providerAlias ` -MethodName 'StartSyncCycle' ` - -MethodArguments @($policyType) + -MethodArguments @($policyType, $computerName) $changed = $false if ($null -ne $startResult -and ($startResult.PSObject.Properties.Name -contains 'Started')) { @@ -140,7 +152,7 @@ function Invoke-IdleStepTriggerDirectorySync { -With $with ` -ProviderAlias $providerAlias ` -MethodName 'GetSyncCycleState' ` - -MethodArguments @() + -MethodArguments @($computerName) $lastState = if ($null -ne $stateResult) { $stateResult.State } else { 'Unknown' } @@ -159,7 +171,7 @@ function Invoke-IdleStepTriggerDirectorySync { -With $with ` -ProviderAlias $providerAlias ` -MethodName 'GetSyncCycleState' ` - -MethodArguments @() + -MethodArguments @($computerName) $inProgress = $true if ($null -ne $stateResult -and ($stateResult.PSObject.Properties.Name -contains 'InProgress')) { diff --git a/src/IdLE.Steps.DirectorySync/StepMetadataCatalog.psd1 b/src/IdLE.Steps.DirectorySync/StepMetadataCatalog.psd1 index 5a0720e0..86435d21 100644 --- a/src/IdLE.Steps.DirectorySync/StepMetadataCatalog.psd1 +++ b/src/IdLE.Steps.DirectorySync/StepMetadataCatalog.psd1 @@ -15,7 +15,7 @@ 'IdLE.Step.TriggerDirectorySync' = @{ RequiredCapabilities = @('IdLE.DirectorySync.Trigger', 'IdLE.DirectorySync.Status') WithSchema = @{ - RequiredKeys = @('AuthSessionName', 'PolicyType') + RequiredKeys = @('AuthSessionName', 'ComputerName', 'PolicyType') OptionalKeys = @('Provider', 'Wait', 'TimeoutSeconds', 'PollIntervalSeconds', 'AuthSessionOptions') } } diff --git a/tests/ProviderContracts/DirectorySyncProviderComputerNameCredential.Contract.ps1 b/tests/ProviderContracts/DirectorySyncProviderComputerNameCredential.Contract.ps1 new file mode 100644 index 00000000..e7d98102 --- /dev/null +++ b/tests/ProviderContracts/DirectorySyncProviderComputerNameCredential.Contract.ps1 @@ -0,0 +1,83 @@ +Set-StrictMode -Version Latest + +function Invoke-IdleDirectorySyncProviderComputerNameCredentialContractTests { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNull()] + [scriptblock] $ProviderFactory + ) + + $cases = @( + @{ + ProviderFactory = $ProviderFactory + } + ) + + Context 'ComputerName + Credential contract' -ForEach $cases { + BeforeEach { + $providerFactory = $_.ProviderFactory + if ($providerFactory -is [scriptblock]) { + $script:Provider = & $providerFactory + } + elseif ($providerFactory -is [string]) { + $script:Provider = & (Get-Command -Name $providerFactory -ErrorAction Stop) + } + else { + throw "ProviderFactory must be a scriptblock or command name string. Got: $($providerFactory.GetType().FullName)" + } + if ($null -eq $script:Provider) { + throw 'ProviderFactory returned $null. A provider instance is required for contract tests.' + } + + $script:Credential = New-Object System.Management.Automation.PSCredential ( + 'contoso\\syncadmin', + (ConvertTo-SecureString -String 'P@ssw0rd!' -AsPlainText -Force) + ) + + $script:MockSession = [pscustomobject]@{ + Id = 1 + } + $script:Provider | Add-Member -NotePropertyName LastComputerName -NotePropertyValue $null -Force + $script:Provider | Add-Member -NotePropertyName LastCredential -NotePropertyValue $null -Force + $script:Provider | Add-Member -NotePropertyName RemovedSession -NotePropertyValue $null -Force + + $script:Provider | Add-Member -MemberType ScriptMethod -Name NewRemoteSession -Value { + param([string] $ComputerName, [pscredential] $Credential) + $this.LastComputerName = $ComputerName + $this.LastCredential = $Credential + return $script:MockSession + } -Force + + $script:Provider | Add-Member -MemberType ScriptMethod -Name InvokeRemoteCommand -Value { + param([object] $Session, [scriptblock] $ScriptBlock, [object[]] $ArgumentList) + return [pscustomobject]@{ + SyncCycleInProgress = $false + } + } -Force + + $script:Provider | Add-Member -MemberType ScriptMethod -Name RemoveRemoteSession -Value { + param([object] $Session) + $this.RemovedSession = $Session + } -Force + } + + It 'StartSyncCycle accepts ComputerName and Credential auth session' { + $result = $script:Provider.StartSyncCycle('Delta', 'ad-sync1.corp.local', $script:Credential) + + $result.Started | Should -BeTrue + $script:Provider.LastComputerName | Should -Be 'ad-sync1.corp.local' + $script:Provider.LastCredential | Should -Be $script:Credential + $script:Provider.RemovedSession | Should -Be $script:MockSession + } + + It 'GetSyncCycleState accepts ComputerName and Credential auth session' { + $result = $script:Provider.GetSyncCycleState('ad-sync1.corp.local', $script:Credential) + + $result.InProgress | Should -BeFalse + $script:Provider.LastComputerName | Should -Be 'ad-sync1.corp.local' + $script:Provider.LastCredential | Should -Be $script:Credential + $script:Provider.RemovedSession | Should -Be $script:MockSession + } + } +} diff --git a/tests/Providers/EntraConnectDirectorySyncProvider.Tests.ps1 b/tests/Providers/EntraConnectDirectorySyncProvider.Tests.ps1 index 42a24090..156eabbc 100644 --- a/tests/Providers/EntraConnectDirectorySyncProvider.Tests.ps1 +++ b/tests/Providers/EntraConnectDirectorySyncProvider.Tests.ps1 @@ -14,46 +14,50 @@ BeforeDiscovery { throw "Provider capabilities contract not found at: $capabilitiesContractPath" } . $capabilitiesContractPath + + $computerNameCredentialContractPath = Join-Path -Path $repoRoot -ChildPath 'tests\ProviderContracts\DirectorySyncProviderComputerNameCredential.Contract.ps1' + if (-not (Test-Path -LiteralPath $computerNameCredentialContractPath -PathType Leaf)) { + throw "Directory sync provider ComputerName+Credential contract not found at: $computerNameCredentialContractPath" + } + . $computerNameCredentialContractPath } Describe 'Entra Connect directory sync provider contracts' { Invoke-IdleProviderCapabilitiesContractTests -ProviderFactory { New-IdleEntraConnectDirectorySyncProvider } + Invoke-IdleDirectorySyncProviderComputerNameCredentialContractTests -ProviderFactory { New-IdleEntraConnectDirectorySyncProvider } Context 'Directory sync provider methods' { - BeforeAll { + BeforeEach { $script:Provider = New-IdleEntraConnectDirectorySyncProvider + $script:ComputerName = 'ad-sync1.corp.local' + $script:MockCredential = New-Object System.Management.Automation.PSCredential ( + 'contoso\syncadmin', + (ConvertTo-SecureString -String 'P@ssw0rd!' -AsPlainText -Force) + ) + $script:MockSession = [pscustomobject]@{ Id = 1 } + $script:Provider | Add-Member -NotePropertyName LastComputerName -NotePropertyValue $null -Force + $script:Provider | Add-Member -NotePropertyName LastCredential -NotePropertyValue $null -Force + $script:Provider | Add-Member -NotePropertyName RemovedSession -NotePropertyValue $null -Force + + $script:Provider | Add-Member -MemberType ScriptMethod -Name NewRemoteSession -Value { + param([string] $ComputerName, [pscredential] $Credential) + $this.LastComputerName = $ComputerName + $this.LastCredential = $Credential + return $script:MockSession + } -Force - # Mock AuthSession with InvokeCommand method - $script:MockAuthSession = [pscustomobject]@{ - PSTypeName = 'Mock.AuthSession' - } - - $script:MockAuthSession | Add-Member -MemberType ScriptMethod -Name InvokeCommand -Value { - param( - [Parameter(Mandatory)] - [string] $CommandName, - - [Parameter(Mandatory)] - [hashtable] $Parameters - ) - - # Mock behavior for Start-ADSyncSyncCycle - if ($CommandName -eq 'Start-ADSyncSyncCycle') { - return [pscustomobject]@{ - Result = 'Success' - } - } - - # Mock behavior for Get-ADSyncScheduler - if ($CommandName -eq 'Get-ADSyncScheduler') { - return [pscustomobject]@{ - SyncCycleInProgress = $false - AllowedSyncCycleInterval = '00:30:00' - NextSyncCyclePolicyType = 'Delta' - } + $script:Provider | Add-Member -MemberType ScriptMethod -Name InvokeRemoteCommand -Value { + param([object] $Session, [scriptblock] $ScriptBlock, [object[]] $ArgumentList) + return [pscustomobject]@{ + SyncCycleInProgress = $false + AllowedSyncCycleInterval = '00:30:00' + NextSyncCyclePolicyType = 'Delta' } + } -Force - throw "Unexpected command: $CommandName" + $script:Provider | Add-Member -MemberType ScriptMethod -Name RemoveRemoteSession -Value { + param([object] $Session) + $this.RemovedSession = $Session } -Force } @@ -61,45 +65,79 @@ Describe 'Entra Connect directory sync provider contracts' { $script:Provider.PSObject.Methods.Name | Should -Contain 'StartSyncCycle' } - It 'StartSyncCycle accepts PolicyType and AuthSession parameters' { - $result = $script:Provider.StartSyncCycle('Delta', $script:MockAuthSession) + It 'StartSyncCycle accepts PolicyType, ComputerName, and AuthSession parameters' { + $result = $script:Provider.StartSyncCycle('Delta', $script:ComputerName, $script:MockCredential) $result | Should -Not -BeNullOrEmpty $result.PSObject.Properties.Name | Should -Contain 'Started' $result.PSObject.Properties.Name | Should -Contain 'Message' + $script:Provider.LastComputerName | Should -Be $script:ComputerName + $script:Provider.LastCredential | Should -Be $script:MockCredential + $script:Provider.RemovedSession | Should -Be $script:MockSession } It 'StartSyncCycle validates PolicyType' { - { $script:Provider.StartSyncCycle('Invalid', $script:MockAuthSession) } | Should -Throw + { $script:Provider.StartSyncCycle('Invalid', $script:ComputerName, $script:MockCredential) } | Should -Throw } - It 'StartSyncCycle validates AuthSession implements InvokeCommand' { + It 'StartSyncCycle validates ComputerName' { + { $script:Provider.StartSyncCycle('Delta', '', $script:MockCredential) } | Should -Throw -ErrorId * -ExpectedMessage '*ComputerName*' + } + + It 'StartSyncCycle validates AuthSession is PSCredential' { $badSession = [pscustomobject]@{ Name = 'BadSession' } - { $script:Provider.StartSyncCycle('Delta', $badSession) } | Should -Throw -ErrorId * -ExpectedMessage '*InvokeCommand*' + { $script:Provider.StartSyncCycle('Delta', $script:ComputerName, $badSession) } | Should -Throw -ErrorId * -ExpectedMessage '*PSCredential*' + } + + It 'StartSyncCycle always closes remoting session' { + $script:Provider | Add-Member -MemberType ScriptMethod -Name InvokeRemoteCommand -Value { + param([object] $Session, [scriptblock] $ScriptBlock, [object[]] $ArgumentList) + throw 'remote failure' + } -Force + + { $script:Provider.StartSyncCycle('Delta', $script:ComputerName, $script:MockCredential) } | Should -Throw + $script:Provider.RemovedSession | Should -Be $script:MockSession } It 'Exposes GetSyncCycleState method' { $script:Provider.PSObject.Methods.Name | Should -Contain 'GetSyncCycleState' } - It 'GetSyncCycleState accepts AuthSession parameter' { - $result = $script:Provider.GetSyncCycleState($script:MockAuthSession) + It 'GetSyncCycleState accepts ComputerName and AuthSession parameters' { + $result = $script:Provider.GetSyncCycleState($script:ComputerName, $script:MockCredential) $result | Should -Not -BeNullOrEmpty $result.PSObject.Properties.Name | Should -Contain 'InProgress' $result.PSObject.Properties.Name | Should -Contain 'State' $result.PSObject.Properties.Name | Should -Contain 'Details' + $script:Provider.LastComputerName | Should -Be $script:ComputerName + $script:Provider.LastCredential | Should -Be $script:MockCredential + $script:Provider.RemovedSession | Should -Be $script:MockSession } It 'GetSyncCycleState returns correct InProgress value' { - $result = $script:Provider.GetSyncCycleState($script:MockAuthSession) + $result = $script:Provider.GetSyncCycleState($script:ComputerName, $script:MockCredential) $result.InProgress | Should -BeOfType [bool] } - It 'GetSyncCycleState validates AuthSession implements InvokeCommand' { + It 'GetSyncCycleState validates ComputerName' { + { $script:Provider.GetSyncCycleState('', $script:MockCredential) } | Should -Throw -ErrorId * -ExpectedMessage '*ComputerName*' + } + + It 'GetSyncCycleState validates AuthSession is PSCredential' { $badSession = [pscustomobject]@{ Name = 'BadSession' } - { $script:Provider.GetSyncCycleState($badSession) } | Should -Throw -ErrorId * -ExpectedMessage '*InvokeCommand*' + { $script:Provider.GetSyncCycleState($script:ComputerName, $badSession) } | Should -Throw -ErrorId * -ExpectedMessage '*PSCredential*' + } + + It 'GetSyncCycleState always closes remoting session' { + $script:Provider | Add-Member -MemberType ScriptMethod -Name InvokeRemoteCommand -Value { + param([object] $Session, [scriptblock] $ScriptBlock, [object[]] $ArgumentList) + throw 'remote failure' + } -Force + + { $script:Provider.GetSyncCycleState($script:ComputerName, $script:MockCredential) } | Should -Throw + $script:Provider.RemovedSession | Should -Be $script:MockSession } } diff --git a/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 b/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 index d2629c89..93a58c11 100644 --- a/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 +++ b/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 @@ -12,6 +12,7 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { PSTypeName = 'Mock.DirectorySyncProvider' Name = 'MockDirectorySyncProvider' PollCount = 0 + LastComputerName = $null } $script:MockProvider | Add-Member -MemberType ScriptMethod -Name GetCapabilities -Value { @@ -23,10 +24,15 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { [Parameter(Mandatory)] [string] $PolicyType, + [Parameter(Mandatory)] + [string] $ComputerName, + [Parameter(Mandatory)] [object] $AuthSession ) + $this.LastComputerName = $ComputerName + return [pscustomobject]@{ Started = $true Message = "Sync cycle triggered with PolicyType: $PolicyType" @@ -35,10 +41,15 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { $script:MockProvider | Add-Member -MemberType ScriptMethod -Name GetSyncCycleState -Value { param( + [Parameter(Mandatory)] + [string] $ComputerName, + [Parameter(Mandatory)] [object] $AuthSession ) + $this.LastComputerName = $ComputerName + # Increment poll count and determine state $this.PollCount++ @@ -87,6 +98,7 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { Type = 'IdLE.Step.TriggerDirectorySync' With = @{ AuthSessionName = 'EntraConnect' + ComputerName = 'ad-sync1.corp.local' PolicyType = 'Delta' Provider = 'DirectorySync' } @@ -120,6 +132,14 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { { & $handler -Context $script:Context -Step $step } | Should -Throw -ErrorId * -ExpectedMessage '*PolicyType*' } + It 'throws when With.ComputerName is missing' { + $step = $script:StepTemplate + $step.With.Remove('ComputerName') + + $handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync' + { & $handler -Context $script:Context -Step $step } | Should -Throw -ErrorId * -ExpectedMessage '*ComputerName*' + } + It 'throws when With.PolicyType is invalid' { $step = $script:StepTemplate $step.With.PolicyType = 'Invalid' @@ -186,6 +206,7 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { $result.Status | Should -Be 'Completed' $result.Changed | Should -BeTrue $result.Error | Should -BeNullOrEmpty + $script:MockProvider.LastComputerName | Should -Be 'ad-sync1.corp.local' } It 'defaults to not waiting when Wait is not specified' { @@ -214,7 +235,7 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { It 'throws timeout error when sync does not complete in time' { # Mock provider that never completes $script:MockProvider | Add-Member -MemberType ScriptMethod -Name GetSyncCycleState -Value { - param([object] $AuthSession) + param([string] $ComputerName, [object] $AuthSession) return [pscustomobject]@{ InProgress = $true State = 'InProgress' @@ -235,7 +256,7 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { # Use the provider's PollCount property which is already initialized $script:MockProvider.PollCount = 0 $script:MockProvider | Add-Member -MemberType ScriptMethod -Name GetSyncCycleState -Value { - param([object] $AuthSession) + param([string] $ComputerName, [object] $AuthSession) $this.PollCount++ $inProgress = $this.PollCount -le 2 return [pscustomobject]@{ diff --git a/tests/fixtures/workflows/joiner-with-dirsync.psd1 b/tests/fixtures/workflows/joiner-with-dirsync.psd1 index cf20dbba..404092b5 100644 --- a/tests/fixtures/workflows/joiner-with-dirsync.psd1 +++ b/tests/fixtures/workflows/joiner-with-dirsync.psd1 @@ -9,6 +9,7 @@ Type = 'IdLE.Step.TriggerDirectorySync' With = @{ AuthSessionName = 'DirSync' + ComputerName = 'ad-sync1.corp.local' PolicyType = 'Delta' Wait = $false Provider = 'DirectorySync' From 139978a648023d33d1f060c91c79c168b0c0bb8a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 14:36:59 +0000 Subject: [PATCH 03/23] test: refine provider contract coverage and validation polish Agent-Logs-Url: https://github.com/blindzero/IdentityLifecycleEngine/sessions/b70820fb-7961-4838-961b-84daf403564c Co-authored-by: ntt-matthias-fleschuetz <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> --- .../New-IdleEntraConnectDirectorySyncProvider.ps1 | 13 +++++++------ ...ySyncProviderComputerNameCredential.Contract.ps1 | 4 ++-- .../EntraConnectDirectorySyncProvider.Tests.ps1 | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 b/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 index 5ccd52b0..0b792240 100644 --- a/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 +++ b/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 @@ -61,7 +61,12 @@ function New-IdleEntraConnectDirectorySyncProvider { [pscredential] $Credential ) - return New-PSSession -ComputerName $ComputerName -Credential $Credential -ErrorAction Stop + try { + return New-PSSession -ComputerName $ComputerName -Credential $Credential -ErrorAction Stop + } + catch { + throw "Failed to establish PSRemoting session to '$ComputerName': $($_.Exception.Message)" + } } -Force $provider | Add-Member -MemberType ScriptMethod -Name InvokeRemoteCommand -Value { @@ -191,7 +196,7 @@ function New-IdleEntraConnectDirectorySyncProvider { #> param( [Parameter(Mandatory)] - [ValidateNotNull()] + [ValidateNotNullOrEmpty()] [string] $ComputerName, [Parameter(Mandatory)] @@ -199,10 +204,6 @@ function New-IdleEntraConnectDirectorySyncProvider { [object] $AuthSession ) - if ([string]::IsNullOrWhiteSpace($ComputerName)) { - throw "ComputerName must not be null, empty, or whitespace." - } - if ($AuthSession -isnot [pscredential]) { $actualType = $AuthSession.GetType().FullName throw "AuthSession must be a [PSCredential] for PSRemoting session creation. Received: [$actualType]" diff --git a/tests/ProviderContracts/DirectorySyncProviderComputerNameCredential.Contract.ps1 b/tests/ProviderContracts/DirectorySyncProviderComputerNameCredential.Contract.ps1 index e7d98102..77f5076d 100644 --- a/tests/ProviderContracts/DirectorySyncProviderComputerNameCredential.Contract.ps1 +++ b/tests/ProviderContracts/DirectorySyncProviderComputerNameCredential.Contract.ps1 @@ -30,8 +30,8 @@ function Invoke-IdleDirectorySyncProviderComputerNameCredentialContractTests { throw 'ProviderFactory returned $null. A provider instance is required for contract tests.' } - $script:Credential = New-Object System.Management.Automation.PSCredential ( - 'contoso\\syncadmin', + $script:Credential = [PSCredential]::new( + 'contoso\syncadmin', (ConvertTo-SecureString -String 'P@ssw0rd!' -AsPlainText -Force) ) diff --git a/tests/Providers/EntraConnectDirectorySyncProvider.Tests.ps1 b/tests/Providers/EntraConnectDirectorySyncProvider.Tests.ps1 index 156eabbc..db41d374 100644 --- a/tests/Providers/EntraConnectDirectorySyncProvider.Tests.ps1 +++ b/tests/Providers/EntraConnectDirectorySyncProvider.Tests.ps1 @@ -30,7 +30,7 @@ Describe 'Entra Connect directory sync provider contracts' { BeforeEach { $script:Provider = New-IdleEntraConnectDirectorySyncProvider $script:ComputerName = 'ad-sync1.corp.local' - $script:MockCredential = New-Object System.Management.Automation.PSCredential ( + $script:MockCredential = [PSCredential]::new( 'contoso\syncadmin', (ConvertTo-SecureString -String 'P@ssw0rd!' -AsPlainText -Force) ) From cf8525a4f260688aff2779a1e6808cc47ad0fa75 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 14:49:35 +0000 Subject: [PATCH 04/23] refactor: move DirectorySync provider-specific inputs into ProviderInput bag Agent-Logs-Url: https://github.com/blindzero/IdentityLifecycleEngine/sessions/9b8fa8e3-45d0-45c6-abc2-984967098764 Co-authored-by: ntt-matthias-fleschuetz <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> --- .../provider-directorysync-entraconnect.md | 5 +- .../steps/step-trigger-directory-sync.md | 13 ++-- .../ad-joiner-entraconnect-entraid.psd1 | 7 ++- ...rectorysync-entraconnect-trigger-sync.psd1 | 9 +-- ...-IdleEntraConnectDirectorySyncProvider.ps1 | 58 ++++++++++++----- .../Invoke-IdleStepTriggerDirectorySync.ps1 | 45 ++++++------- .../StepMetadataCatalog.psd1 | 2 +- ...roviderComputerNameCredential.Contract.ps1 | 15 +++-- ...ntraConnectDirectorySyncProvider.Tests.ps1 | 46 +++++++++----- ...oke-IdleStepTriggerDirectorySync.Tests.ps1 | 63 +++++-------------- .../workflows/joiner-with-dirsync.psd1 | 6 +- 11 files changed, 142 insertions(+), 127 deletions(-) diff --git a/docs/reference/providers/provider-directorysync-entraconnect.md b/docs/reference/providers/provider-directorysync-entraconnect.md index 1432649f..32980d2c 100644 --- a/docs/reference/providers/provider-directorysync-entraconnect.md +++ b/docs/reference/providers/provider-directorysync-entraconnect.md @@ -74,7 +74,7 @@ The provider creates and cleans up PSRemoting sessions internally. Your host/runtime should provide this credential via the AuthSessionBroker and you reference it in the step via: - `AuthSessionName = 'EntraConnect'` -- `ComputerName = 'ad-sync1.corp.local'` +- `ProviderInput = @{ ComputerName = 'ad-sync1.corp.local'; PolicyType = 'Delta' }` > No interactive prompts are made. If the credential does not have elevated rights on the target server, triggering a sync cycle will fail with a privilege/elevation error. @@ -99,7 +99,8 @@ This provider does not advertise these capabilities, so it cannot be used in the ## Configuration This provider has no admin-facing option bag. Configuration is done through: -- step inputs (`ComputerName`, `PolicyType`, `Wait`, `TimeoutSeconds`, `PollIntervalSeconds`) +- provider input (`ProviderInput.ComputerName`, `ProviderInput.PolicyType`) +- step-generic inputs (`Wait`, `TimeoutSeconds`, `PollIntervalSeconds`) - host configuration (credential broker) ## Examples (canonical template) diff --git a/docs/reference/steps/step-trigger-directory-sync.md b/docs/reference/steps/step-trigger-directory-sync.md index 93acac7b..b464eebe 100644 --- a/docs/reference/steps/step-trigger-directory-sync.md +++ b/docs/reference/steps/step-trigger-directory-sync.md @@ -19,9 +19,9 @@ Triggers a directory sync cycle and optionally waits for completion. The host must supply a provider instance via Context.Providers[<ProviderAlias>] that implements: -- StartSyncCycle(PolicyType, ComputerName, AuthSession) +- StartSyncCycle(ProviderInput, AuthSession) -- GetSyncCycleState(ComputerName, AuthSession) +- GetSyncCycleState(ProviderInput, AuthSession) The step is designed for remote execution and requires an elevated auth session provided by the host's AuthSessionBroker. @@ -41,8 +41,7 @@ The following keys are required in the step's ``With`` configuration: | Key | Required | Description | | --- | --- | --- | | `AuthSessionName` | Yes | Name of auth session to use (optional) | -| `ComputerName` | Yes | See step description for details | -| `PolicyType` | Yes | Type of policy (e.g., Delta, Initial) | +| `ProviderInput` | Yes | See step description for details | ## Example @@ -52,8 +51,10 @@ $step = @{ Type = 'IdLE.Step.TriggerDirectorySync' With = @{ AuthSessionName = 'DirectorySync' - ComputerName = 'ad-sync1.corp.local' - PolicyType = 'Delta' + ProviderInput = @{ + ComputerName = 'ad-sync1.corp.local' + PolicyType = 'Delta' + } Wait = $true } } diff --git a/examples/workflows/templates/ad-joiner-entraconnect-entraid.psd1 b/examples/workflows/templates/ad-joiner-entraconnect-entraid.psd1 index 66e8d52a..ff59a3cf 100644 --- a/examples/workflows/templates/ad-joiner-entraconnect-entraid.psd1 +++ b/examples/workflows/templates/ad-joiner-entraconnect-entraid.psd1 @@ -27,9 +27,10 @@ With = @{ Provider = 'DirectorySync' AuthSessionName = 'EntraConnect' - ComputerName = '{{Request.Intent.EntraConnectServer}}' - - PolicyType = 'Delta' + ProviderInput = @{ + ComputerName = '{{Request.Intent.EntraConnectServer}}' + PolicyType = 'Delta' + } Wait = $true TimeoutSeconds = 300 PollIntervalSeconds = 10 diff --git a/examples/workflows/templates/directorysync-entraconnect-trigger-sync.psd1 b/examples/workflows/templates/directorysync-entraconnect-trigger-sync.psd1 index 4b9b3780..086369c2 100644 --- a/examples/workflows/templates/directorysync-entraconnect-trigger-sync.psd1 +++ b/examples/workflows/templates/directorysync-entraconnect-trigger-sync.psd1 @@ -12,10 +12,11 @@ # Auth session is provided by the host (credential). AuthSessionName = 'EntraConnect' - ComputerName = '{{Request.Intent.ComputerName}}' - - # Delta or Initial - PolicyType = '{{Request.Intent.PolicyType}}' + ProviderInput = @{ + ComputerName = '{{Request.Intent.ComputerName}}' + # Delta or Initial + PolicyType = '{{Request.Intent.PolicyType}}' + } # Optional wait/polling behavior (step-specific) Wait = $true diff --git a/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 b/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 index 0b792240..0b9b0145 100644 --- a/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 +++ b/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 @@ -15,7 +15,7 @@ function New-IdleEntraConnectDirectorySyncProvider { .OUTPUTS PSCustomObject - Provider instance with methods: GetCapabilities(), StartSyncCycle(PolicyType, ComputerName, AuthSession), GetSyncCycleState(ComputerName, AuthSession) + Provider instance with methods: GetCapabilities(), StartSyncCycle(ProviderInput, AuthSession), GetSyncCycleState(ProviderInput, AuthSession) .EXAMPLE $provider = New-IdleEntraConnectDirectorySyncProvider @@ -26,7 +26,10 @@ function New-IdleEntraConnectDirectorySyncProvider { # With a credential from AuthSessionBroker (AuthSessionType='Credential') $credential = Get-Credential $provider = New-IdleEntraConnectDirectorySyncProvider - $result = $provider.StartSyncCycle('Delta', 'ad-sync1.corp.local', $credential) + $result = $provider.StartSyncCycle(@{ + ComputerName = 'ad-sync1.corp.local' + PolicyType = 'Delta' + }, $credential) #> [CmdletBinding()] param() @@ -109,11 +112,10 @@ function New-IdleEntraConnectDirectorySyncProvider { .DESCRIPTION Triggers a sync cycle via Start-ADSyncSyncCycle on the remote Entra Connect server. - .PARAMETER PolicyType - The sync policy type: 'Delta' or 'Initial'. - - .PARAMETER ComputerName - Target Entra Connect server hostname for PSRemoting. + .PARAMETER ProviderInput + Provider-owned input bag. For Entra Connect this must include: + - ComputerName (string): target Entra Connect server hostname for PSRemoting. + - PolicyType (string): sync policy type ('Delta' or 'Initial'). .PARAMETER AuthSession Credential ([PSCredential]) provided by the host's AuthSessionBroker. @@ -125,18 +127,31 @@ function New-IdleEntraConnectDirectorySyncProvider { #> param( [Parameter(Mandatory)] - [ValidateSet('Delta', 'Initial', IgnoreCase = $true)] - [string] $PolicyType, - - [Parameter(Mandatory)] - [ValidateNotNullOrEmpty()] - [string] $ComputerName, + [ValidateNotNull()] + [hashtable] $ProviderInput, [Parameter(Mandatory)] [ValidateNotNull()] [object] $AuthSession ) + if (-not $ProviderInput.ContainsKey('ComputerName')) { + throw "StartSyncCycle requires ProviderInput.ComputerName." + } + if (-not $ProviderInput.ContainsKey('PolicyType')) { + throw "StartSyncCycle requires ProviderInput.PolicyType." + } + + $computerName = [string]$ProviderInput.ComputerName + if ([string]::IsNullOrWhiteSpace($computerName)) { + throw "StartSyncCycle: ProviderInput.ComputerName must not be null, empty, or whitespace." + } + + $policyType = [string]$ProviderInput.PolicyType + if ($policyType -notin @('Delta', 'Initial')) { + throw "StartSyncCycle: ProviderInput.PolicyType must be 'Delta' or 'Initial' (case-insensitive). Got: $policyType" + } + if ($AuthSession -isnot [pscredential]) { $actualType = $AuthSession.GetType().FullName throw "AuthSession must be a [PSCredential] for PSRemoting session creation. Received: [$actualType]" @@ -182,8 +197,9 @@ function New-IdleEntraConnectDirectorySyncProvider { Queries the sync scheduler state via Get-ADSyncScheduler to determine if a sync cycle is currently in progress. - .PARAMETER ComputerName - Target Entra Connect server hostname for PSRemoting. + .PARAMETER ProviderInput + Provider-owned input bag. For Entra Connect this must include: + - ComputerName (string): target Entra Connect server hostname for PSRemoting. .PARAMETER AuthSession Credential ([PSCredential]) provided by the host's AuthSessionBroker. @@ -196,14 +212,22 @@ function New-IdleEntraConnectDirectorySyncProvider { #> param( [Parameter(Mandatory)] - [ValidateNotNullOrEmpty()] - [string] $ComputerName, + [ValidateNotNull()] + [hashtable] $ProviderInput, [Parameter(Mandatory)] [ValidateNotNull()] [object] $AuthSession ) + if (-not $ProviderInput.ContainsKey('ComputerName')) { + throw "GetSyncCycleState requires ProviderInput.ComputerName." + } + $computerName = [string]$ProviderInput.ComputerName + if ([string]::IsNullOrWhiteSpace($computerName)) { + throw "GetSyncCycleState: ProviderInput.ComputerName must not be null, empty, or whitespace." + } + if ($AuthSession -isnot [pscredential]) { $actualType = $AuthSession.GetType().FullName throw "AuthSession must be a [PSCredential] for PSRemoting session creation. Received: [$actualType]" diff --git a/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 b/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 index bf19d8da..677cbfaa 100644 --- a/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 +++ b/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 @@ -6,8 +6,8 @@ function Invoke-IdleStepTriggerDirectorySync { .DESCRIPTION This is a provider-agnostic step. The host must supply a provider instance via Context.Providers[] that implements: - - StartSyncCycle(PolicyType, ComputerName, AuthSession) - - GetSyncCycleState(ComputerName, AuthSession) + - StartSyncCycle(ProviderInput, AuthSession) + - GetSyncCycleState(ProviderInput, AuthSession) The step is designed for remote execution and requires an elevated auth session provided by the host's AuthSessionBroker. @@ -23,8 +23,7 @@ function Invoke-IdleStepTriggerDirectorySync { .PARAMETER Step Normalized step object from the plan. Must contain a 'With' hashtable with keys: - AuthSessionName (required, string): auth session name for broker - - ComputerName (required, string): target Entra Connect server - - PolicyType (required, string): 'Delta' or 'Initial' (case-insensitive) + - ProviderInput (required, hashtable): provider-owned input bag - Provider (optional, string): provider alias, defaults to 'DirectorySync' - Wait (optional, bool): wait for cycle completion, defaults to $false - TimeoutSeconds (optional, int): wait timeout, defaults to 600 @@ -40,8 +39,10 @@ function Invoke-IdleStepTriggerDirectorySync { Type = 'IdLE.Step.TriggerDirectorySync' With = @{ AuthSessionName = 'DirectorySync' - ComputerName = 'ad-sync1.corp.local' - PolicyType = 'Delta' + ProviderInput = @{ + ComputerName = 'ad-sync1.corp.local' + PolicyType = 'Delta' + } Wait = $true } } @@ -67,22 +68,13 @@ function Invoke-IdleStepTriggerDirectorySync { throw "TriggerDirectorySync requires With.AuthSessionName." } - if (-not $with.ContainsKey('PolicyType')) { - throw "TriggerDirectorySync requires With.PolicyType." - } - - if (-not $with.ContainsKey('ComputerName')) { - throw "TriggerDirectorySync requires With.ComputerName." - } - - $policyType = [string]$with.PolicyType - if ($policyType -notin @('Delta', 'Initial')) { - throw "TriggerDirectorySync: With.PolicyType must be 'Delta' or 'Initial' (case-insensitive). Got: $policyType" + if (-not $with.ContainsKey('ProviderInput')) { + throw "TriggerDirectorySync requires With.ProviderInput." } - $computerName = [string]$with.ComputerName - if ([string]::IsNullOrWhiteSpace($computerName)) { - throw "TriggerDirectorySync: With.ComputerName must not be null, empty, or whitespace." + $providerInput = $with.ProviderInput + if ($null -eq $providerInput -or -not ($providerInput -is [hashtable])) { + throw "TriggerDirectorySync requires With.ProviderInput to be a hashtable." } # Optional inputs with defaults @@ -114,9 +106,8 @@ function Invoke-IdleStepTriggerDirectorySync { try { # Trigger sync cycle - $Context.EventSink.WriteEvent('DirectorySyncTriggered', "Triggering $policyType sync cycle", $stepName, @{ - PolicyType = $policyType - ComputerName = $computerName + $Context.EventSink.WriteEvent('DirectorySyncTriggered', "Triggering directory sync cycle", $stepName, @{ + ProviderInput = $providerInput }) $startResult = Invoke-IdleProviderMethod ` @@ -124,7 +115,7 @@ function Invoke-IdleStepTriggerDirectorySync { -With $with ` -ProviderAlias $providerAlias ` -MethodName 'StartSyncCycle' ` - -MethodArguments @($policyType, $computerName) + -MethodArguments @($providerInput) $changed = $false if ($null -ne $startResult -and ($startResult.PSObject.Properties.Name -contains 'Started')) { @@ -152,7 +143,7 @@ function Invoke-IdleStepTriggerDirectorySync { -With $with ` -ProviderAlias $providerAlias ` -MethodName 'GetSyncCycleState' ` - -MethodArguments @($computerName) + -MethodArguments @($providerInput) $lastState = if ($null -ne $stateResult) { $stateResult.State } else { 'Unknown' } @@ -171,7 +162,7 @@ function Invoke-IdleStepTriggerDirectorySync { -With $with ` -ProviderAlias $providerAlias ` -MethodName 'GetSyncCycleState' ` - -MethodArguments @($computerName) + -MethodArguments @($providerInput) $inProgress = $true if ($null -ne $stateResult -and ($stateResult.PSObject.Properties.Name -contains 'InProgress')) { @@ -203,7 +194,7 @@ function Invoke-IdleStepTriggerDirectorySync { else { # Not waiting - sync triggered successfully $Context.EventSink.WriteEvent('DirectorySyncCompleted', "Sync cycle triggered (not waiting)", $stepName, @{ - PolicyType = $policyType + ProviderInput = $providerInput }) } diff --git a/src/IdLE.Steps.DirectorySync/StepMetadataCatalog.psd1 b/src/IdLE.Steps.DirectorySync/StepMetadataCatalog.psd1 index 86435d21..fce71b74 100644 --- a/src/IdLE.Steps.DirectorySync/StepMetadataCatalog.psd1 +++ b/src/IdLE.Steps.DirectorySync/StepMetadataCatalog.psd1 @@ -15,7 +15,7 @@ 'IdLE.Step.TriggerDirectorySync' = @{ RequiredCapabilities = @('IdLE.DirectorySync.Trigger', 'IdLE.DirectorySync.Status') WithSchema = @{ - RequiredKeys = @('AuthSessionName', 'ComputerName', 'PolicyType') + RequiredKeys = @('AuthSessionName', 'ProviderInput') OptionalKeys = @('Provider', 'Wait', 'TimeoutSeconds', 'PollIntervalSeconds', 'AuthSessionOptions') } } diff --git a/tests/ProviderContracts/DirectorySyncProviderComputerNameCredential.Contract.ps1 b/tests/ProviderContracts/DirectorySyncProviderComputerNameCredential.Contract.ps1 index 77f5076d..211db96d 100644 --- a/tests/ProviderContracts/DirectorySyncProviderComputerNameCredential.Contract.ps1 +++ b/tests/ProviderContracts/DirectorySyncProviderComputerNameCredential.Contract.ps1 @@ -62,8 +62,12 @@ function Invoke-IdleDirectorySyncProviderComputerNameCredentialContractTests { } -Force } - It 'StartSyncCycle accepts ComputerName and Credential auth session' { - $result = $script:Provider.StartSyncCycle('Delta', 'ad-sync1.corp.local', $script:Credential) + It 'StartSyncCycle accepts ProviderInput and Credential auth session' { + $providerInput = @{ + ComputerName = 'ad-sync1.corp.local' + PolicyType = 'Delta' + } + $result = $script:Provider.StartSyncCycle($providerInput, $script:Credential) $result.Started | Should -BeTrue $script:Provider.LastComputerName | Should -Be 'ad-sync1.corp.local' @@ -71,8 +75,11 @@ function Invoke-IdleDirectorySyncProviderComputerNameCredentialContractTests { $script:Provider.RemovedSession | Should -Be $script:MockSession } - It 'GetSyncCycleState accepts ComputerName and Credential auth session' { - $result = $script:Provider.GetSyncCycleState('ad-sync1.corp.local', $script:Credential) + It 'GetSyncCycleState accepts ProviderInput and Credential auth session' { + $providerInput = @{ + ComputerName = 'ad-sync1.corp.local' + } + $result = $script:Provider.GetSyncCycleState($providerInput, $script:Credential) $result.InProgress | Should -BeFalse $script:Provider.LastComputerName | Should -Be 'ad-sync1.corp.local' diff --git a/tests/Providers/EntraConnectDirectorySyncProvider.Tests.ps1 b/tests/Providers/EntraConnectDirectorySyncProvider.Tests.ps1 index db41d374..6542d2be 100644 --- a/tests/Providers/EntraConnectDirectorySyncProvider.Tests.ps1 +++ b/tests/Providers/EntraConnectDirectorySyncProvider.Tests.ps1 @@ -30,6 +30,10 @@ Describe 'Entra Connect directory sync provider contracts' { BeforeEach { $script:Provider = New-IdleEntraConnectDirectorySyncProvider $script:ComputerName = 'ad-sync1.corp.local' + $script:ProviderInput = @{ + ComputerName = $script:ComputerName + PolicyType = 'Delta' + } $script:MockCredential = [PSCredential]::new( 'contoso\syncadmin', (ConvertTo-SecureString -String 'P@ssw0rd!' -AsPlainText -Force) @@ -65,8 +69,8 @@ Describe 'Entra Connect directory sync provider contracts' { $script:Provider.PSObject.Methods.Name | Should -Contain 'StartSyncCycle' } - It 'StartSyncCycle accepts PolicyType, ComputerName, and AuthSession parameters' { - $result = $script:Provider.StartSyncCycle('Delta', $script:ComputerName, $script:MockCredential) + It 'StartSyncCycle accepts ProviderInput and AuthSession parameters' { + $result = $script:Provider.StartSyncCycle($script:ProviderInput, $script:MockCredential) $result | Should -Not -BeNullOrEmpty $result.PSObject.Properties.Name | Should -Contain 'Started' @@ -76,17 +80,25 @@ Describe 'Entra Connect directory sync provider contracts' { $script:Provider.RemovedSession | Should -Be $script:MockSession } - It 'StartSyncCycle validates PolicyType' { - { $script:Provider.StartSyncCycle('Invalid', $script:ComputerName, $script:MockCredential) } | Should -Throw + It 'StartSyncCycle validates ProviderInput.PolicyType' { + $providerInput = @{ + ComputerName = $script:ComputerName + PolicyType = 'Invalid' + } + { $script:Provider.StartSyncCycle($providerInput, $script:MockCredential) } | Should -Throw -ErrorId * -ExpectedMessage '*PolicyType*' } - It 'StartSyncCycle validates ComputerName' { - { $script:Provider.StartSyncCycle('Delta', '', $script:MockCredential) } | Should -Throw -ErrorId * -ExpectedMessage '*ComputerName*' + It 'StartSyncCycle validates ProviderInput.ComputerName' { + $providerInput = @{ + ComputerName = '' + PolicyType = 'Delta' + } + { $script:Provider.StartSyncCycle($providerInput, $script:MockCredential) } | Should -Throw -ErrorId * -ExpectedMessage '*ComputerName*' } It 'StartSyncCycle validates AuthSession is PSCredential' { $badSession = [pscustomobject]@{ Name = 'BadSession' } - { $script:Provider.StartSyncCycle('Delta', $script:ComputerName, $badSession) } | Should -Throw -ErrorId * -ExpectedMessage '*PSCredential*' + { $script:Provider.StartSyncCycle($script:ProviderInput, $badSession) } | Should -Throw -ErrorId * -ExpectedMessage '*PSCredential*' } It 'StartSyncCycle always closes remoting session' { @@ -95,7 +107,7 @@ Describe 'Entra Connect directory sync provider contracts' { throw 'remote failure' } -Force - { $script:Provider.StartSyncCycle('Delta', $script:ComputerName, $script:MockCredential) } | Should -Throw + { $script:Provider.StartSyncCycle($script:ProviderInput, $script:MockCredential) } | Should -Throw $script:Provider.RemovedSession | Should -Be $script:MockSession } @@ -103,8 +115,8 @@ Describe 'Entra Connect directory sync provider contracts' { $script:Provider.PSObject.Methods.Name | Should -Contain 'GetSyncCycleState' } - It 'GetSyncCycleState accepts ComputerName and AuthSession parameters' { - $result = $script:Provider.GetSyncCycleState($script:ComputerName, $script:MockCredential) + It 'GetSyncCycleState accepts ProviderInput and AuthSession parameters' { + $result = $script:Provider.GetSyncCycleState($script:ProviderInput, $script:MockCredential) $result | Should -Not -BeNullOrEmpty $result.PSObject.Properties.Name | Should -Contain 'InProgress' @@ -116,18 +128,22 @@ Describe 'Entra Connect directory sync provider contracts' { } It 'GetSyncCycleState returns correct InProgress value' { - $result = $script:Provider.GetSyncCycleState($script:ComputerName, $script:MockCredential) + $result = $script:Provider.GetSyncCycleState($script:ProviderInput, $script:MockCredential) $result.InProgress | Should -BeOfType [bool] } - It 'GetSyncCycleState validates ComputerName' { - { $script:Provider.GetSyncCycleState('', $script:MockCredential) } | Should -Throw -ErrorId * -ExpectedMessage '*ComputerName*' + It 'GetSyncCycleState validates ProviderInput.ComputerName' { + $providerInput = @{ + ComputerName = '' + PolicyType = 'Delta' + } + { $script:Provider.GetSyncCycleState($providerInput, $script:MockCredential) } | Should -Throw -ErrorId * -ExpectedMessage '*ComputerName*' } It 'GetSyncCycleState validates AuthSession is PSCredential' { $badSession = [pscustomobject]@{ Name = 'BadSession' } - { $script:Provider.GetSyncCycleState($script:ComputerName, $badSession) } | Should -Throw -ErrorId * -ExpectedMessage '*PSCredential*' + { $script:Provider.GetSyncCycleState($script:ProviderInput, $badSession) } | Should -Throw -ErrorId * -ExpectedMessage '*PSCredential*' } It 'GetSyncCycleState always closes remoting session' { @@ -136,7 +152,7 @@ Describe 'Entra Connect directory sync provider contracts' { throw 'remote failure' } -Force - { $script:Provider.GetSyncCycleState($script:ComputerName, $script:MockCredential) } | Should -Throw + { $script:Provider.GetSyncCycleState($script:ProviderInput, $script:MockCredential) } | Should -Throw $script:Provider.RemovedSession | Should -Be $script:MockSession } } diff --git a/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 b/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 index 93a58c11..67c8185b 100644 --- a/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 +++ b/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 @@ -22,33 +22,30 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { $script:MockProvider | Add-Member -MemberType ScriptMethod -Name StartSyncCycle -Value { param( [Parameter(Mandatory)] - [string] $PolicyType, - - [Parameter(Mandatory)] - [string] $ComputerName, + [hashtable] $ProviderInput, [Parameter(Mandatory)] [object] $AuthSession ) - $this.LastComputerName = $ComputerName + $this.LastComputerName = [string]$ProviderInput.ComputerName return [pscustomobject]@{ Started = $true - Message = "Sync cycle triggered with PolicyType: $PolicyType" + Message = "Sync cycle triggered with PolicyType: $($ProviderInput.PolicyType)" } } -Force $script:MockProvider | Add-Member -MemberType ScriptMethod -Name GetSyncCycleState -Value { param( [Parameter(Mandatory)] - [string] $ComputerName, + [hashtable] $ProviderInput, [Parameter(Mandatory)] [object] $AuthSession ) - $this.LastComputerName = $ComputerName + $this.LastComputerName = [string]$ProviderInput.ComputerName # Increment poll count and determine state $this.PollCount++ @@ -98,8 +95,10 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { Type = 'IdLE.Step.TriggerDirectorySync' With = @{ AuthSessionName = 'EntraConnect' - ComputerName = 'ad-sync1.corp.local' - PolicyType = 'Delta' + ProviderInput = @{ + ComputerName = 'ad-sync1.corp.local' + PolicyType = 'Delta' + } Provider = 'DirectorySync' } } @@ -124,48 +123,20 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { { & $handler -Context $script:Context -Step $step } | Should -Throw -ErrorId * -ExpectedMessage '*AuthSessionName*' } - It 'throws when With.PolicyType is missing' { - $step = $script:StepTemplate - $step.With.Remove('PolicyType') - - $handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync' - { & $handler -Context $script:Context -Step $step } | Should -Throw -ErrorId * -ExpectedMessage '*PolicyType*' - } - - It 'throws when With.ComputerName is missing' { - $step = $script:StepTemplate - $step.With.Remove('ComputerName') - - $handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync' - { & $handler -Context $script:Context -Step $step } | Should -Throw -ErrorId * -ExpectedMessage '*ComputerName*' - } - - It 'throws when With.PolicyType is invalid' { - $step = $script:StepTemplate - $step.With.PolicyType = 'Invalid' - - $handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync' - { & $handler -Context $script:Context -Step $step } | Should -Throw -ErrorId * -ExpectedMessage '*PolicyType*' - } - - It 'accepts Delta as PolicyType' { + It 'throws when With.ProviderInput is missing' { $step = $script:StepTemplate - $step.With.PolicyType = 'Delta' + $step.With.Remove('ProviderInput') $handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync' - $result = & $handler -Context $script:Context -Step $step - - $result.Status | Should -Be 'Completed' + { & $handler -Context $script:Context -Step $step } | Should -Throw -ErrorId * -ExpectedMessage '*ProviderInput*' } - It 'accepts Initial as PolicyType' { + It 'throws when With.ProviderInput is not a hashtable' { $step = $script:StepTemplate - $step.With.PolicyType = 'Initial' + $step.With.ProviderInput = 'invalid' $handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync' - $result = & $handler -Context $script:Context -Step $step - - $result.Status | Should -Be 'Completed' + { & $handler -Context $script:Context -Step $step } | Should -Throw -ErrorId * -ExpectedMessage '*ProviderInput*' } It 'uses default provider alias when not specified' { @@ -235,7 +206,7 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { It 'throws timeout error when sync does not complete in time' { # Mock provider that never completes $script:MockProvider | Add-Member -MemberType ScriptMethod -Name GetSyncCycleState -Value { - param([string] $ComputerName, [object] $AuthSession) + param([hashtable] $ProviderInput, [object] $AuthSession) return [pscustomobject]@{ InProgress = $true State = 'InProgress' @@ -256,7 +227,7 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { # Use the provider's PollCount property which is already initialized $script:MockProvider.PollCount = 0 $script:MockProvider | Add-Member -MemberType ScriptMethod -Name GetSyncCycleState -Value { - param([string] $ComputerName, [object] $AuthSession) + param([hashtable] $ProviderInput, [object] $AuthSession) $this.PollCount++ $inProgress = $this.PollCount -le 2 return [pscustomobject]@{ diff --git a/tests/fixtures/workflows/joiner-with-dirsync.psd1 b/tests/fixtures/workflows/joiner-with-dirsync.psd1 index 404092b5..e9ccd899 100644 --- a/tests/fixtures/workflows/joiner-with-dirsync.psd1 +++ b/tests/fixtures/workflows/joiner-with-dirsync.psd1 @@ -9,8 +9,10 @@ Type = 'IdLE.Step.TriggerDirectorySync' With = @{ AuthSessionName = 'DirSync' - ComputerName = 'ad-sync1.corp.local' - PolicyType = 'Delta' + ProviderInput = @{ + ComputerName = 'ad-sync1.corp.local' + PolicyType = 'Delta' + } Wait = $false Provider = 'DirectorySync' } From 7e57052625aae76946991795f4672297181f0f8c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 14:52:14 +0000 Subject: [PATCH 05/23] docs: finalize provider-input contract wording and consistency Agent-Logs-Url: https://github.com/blindzero/IdentityLifecycleEngine/sessions/9b8fa8e3-45d0-45c6-abc2-984967098764 Co-authored-by: ntt-matthias-fleschuetz <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> --- .../Public/New-IdleEntraConnectDirectorySyncProvider.ps1 | 8 ++++---- ...rectorySyncProviderComputerNameCredential.Contract.ps1 | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 b/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 index 0b9b0145..a6fa1f3a 100644 --- a/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 +++ b/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 @@ -159,17 +159,17 @@ function New-IdleEntraConnectDirectorySyncProvider { $remoteSession = $null try { - $remoteSession = $this.NewRemoteSession($ComputerName, $AuthSession) + $remoteSession = $this.NewRemoteSession($computerName, $AuthSession) $this.InvokeRemoteCommand($remoteSession, { param([string] $RemotePolicyType) Import-Module -Name ADSync -ErrorAction Stop Start-ADSyncSyncCycle -PolicyType $RemotePolicyType -ErrorAction Stop - }, @($PolicyType)) | Out-Null + }, @($policyType)) | Out-Null return [pscustomobject]@{ Started = $true - Message = "Sync cycle triggered with PolicyType: $PolicyType on $ComputerName" + Message = "Sync cycle triggered with PolicyType: $policyType on $computerName" } } catch { @@ -235,7 +235,7 @@ function New-IdleEntraConnectDirectorySyncProvider { $remoteSession = $null try { - $remoteSession = $this.NewRemoteSession($ComputerName, $AuthSession) + $remoteSession = $this.NewRemoteSession($computerName, $AuthSession) $scheduler = $this.InvokeRemoteCommand($remoteSession, { Import-Module -Name ADSync -ErrorAction Stop diff --git a/tests/ProviderContracts/DirectorySyncProviderComputerNameCredential.Contract.ps1 b/tests/ProviderContracts/DirectorySyncProviderComputerNameCredential.Contract.ps1 index 211db96d..1c919b18 100644 --- a/tests/ProviderContracts/DirectorySyncProviderComputerNameCredential.Contract.ps1 +++ b/tests/ProviderContracts/DirectorySyncProviderComputerNameCredential.Contract.ps1 @@ -62,7 +62,7 @@ function Invoke-IdleDirectorySyncProviderComputerNameCredentialContractTests { } -Force } - It 'StartSyncCycle accepts ProviderInput and Credential auth session' { + It 'StartSyncCycle accepts ProviderInput and Credential' { $providerInput = @{ ComputerName = 'ad-sync1.corp.local' PolicyType = 'Delta' @@ -75,7 +75,7 @@ function Invoke-IdleDirectorySyncProviderComputerNameCredentialContractTests { $script:Provider.RemovedSession | Should -Be $script:MockSession } - It 'GetSyncCycleState accepts ProviderInput and Credential auth session' { + It 'GetSyncCycleState accepts ProviderInput and Credential' { $providerInput = @{ ComputerName = 'ad-sync1.corp.local' } From 611c22b49d5639f94de84327fd16aa08b3f2a9a0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 15:00:05 +0000 Subject: [PATCH 06/23] refactor: simplify TriggerDirectorySync inputs to direct With keys Agent-Logs-Url: https://github.com/blindzero/IdentityLifecycleEngine/sessions/73833856-5f99-4ea9-92c2-88a8d0ab57ab Co-authored-by: ntt-matthias-fleschuetz <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> --- .../provider-directorysync-entraconnect.md | 6 +- .../steps/step-trigger-directory-sync.md | 13 ++-- .../ad-joiner-entraconnect-entraid.psd1 | 7 +- ...rectorysync-entraconnect-trigger-sync.psd1 | 9 ++- ...-IdleEntraConnectDirectorySyncProvider.ps1 | 66 ++++++------------- .../Invoke-IdleStepTriggerDirectorySync.ps1 | 45 ++++++++----- .../StepMetadataCatalog.psd1 | 2 +- ...roviderComputerNameCredential.Contract.ps1 | 15 ++--- ...ntraConnectDirectorySyncProvider.Tests.ps1 | 46 +++++-------- ...oke-IdleStepTriggerDirectorySync.Tests.ps1 | 43 +++++++----- .../workflows/joiner-with-dirsync.psd1 | 6 +- 11 files changed, 112 insertions(+), 146 deletions(-) diff --git a/docs/reference/providers/provider-directorysync-entraconnect.md b/docs/reference/providers/provider-directorysync-entraconnect.md index 32980d2c..bdb8900f 100644 --- a/docs/reference/providers/provider-directorysync-entraconnect.md +++ b/docs/reference/providers/provider-directorysync-entraconnect.md @@ -74,7 +74,8 @@ The provider creates and cleans up PSRemoting sessions internally. Your host/runtime should provide this credential via the AuthSessionBroker and you reference it in the step via: - `AuthSessionName = 'EntraConnect'` -- `ProviderInput = @{ ComputerName = 'ad-sync1.corp.local'; PolicyType = 'Delta' }` +- `ComputerName = 'ad-sync1.corp.local'` +- `PolicyType = 'Delta'` > No interactive prompts are made. If the credential does not have elevated rights on the target server, triggering a sync cycle will fail with a privilege/elevation error. @@ -99,8 +100,7 @@ This provider does not advertise these capabilities, so it cannot be used in the ## Configuration This provider has no admin-facing option bag. Configuration is done through: -- provider input (`ProviderInput.ComputerName`, `ProviderInput.PolicyType`) -- step-generic inputs (`Wait`, `TimeoutSeconds`, `PollIntervalSeconds`) +- step inputs (`ComputerName`, `PolicyType`, `Wait`, `TimeoutSeconds`, `PollIntervalSeconds`) - host configuration (credential broker) ## Examples (canonical template) diff --git a/docs/reference/steps/step-trigger-directory-sync.md b/docs/reference/steps/step-trigger-directory-sync.md index b464eebe..93acac7b 100644 --- a/docs/reference/steps/step-trigger-directory-sync.md +++ b/docs/reference/steps/step-trigger-directory-sync.md @@ -19,9 +19,9 @@ Triggers a directory sync cycle and optionally waits for completion. The host must supply a provider instance via Context.Providers[<ProviderAlias>] that implements: -- StartSyncCycle(ProviderInput, AuthSession) +- StartSyncCycle(PolicyType, ComputerName, AuthSession) -- GetSyncCycleState(ProviderInput, AuthSession) +- GetSyncCycleState(ComputerName, AuthSession) The step is designed for remote execution and requires an elevated auth session provided by the host's AuthSessionBroker. @@ -41,7 +41,8 @@ The following keys are required in the step's ``With`` configuration: | Key | Required | Description | | --- | --- | --- | | `AuthSessionName` | Yes | Name of auth session to use (optional) | -| `ProviderInput` | Yes | See step description for details | +| `ComputerName` | Yes | See step description for details | +| `PolicyType` | Yes | Type of policy (e.g., Delta, Initial) | ## Example @@ -51,10 +52,8 @@ $step = @{ Type = 'IdLE.Step.TriggerDirectorySync' With = @{ AuthSessionName = 'DirectorySync' - ProviderInput = @{ - ComputerName = 'ad-sync1.corp.local' - PolicyType = 'Delta' - } + ComputerName = 'ad-sync1.corp.local' + PolicyType = 'Delta' Wait = $true } } diff --git a/examples/workflows/templates/ad-joiner-entraconnect-entraid.psd1 b/examples/workflows/templates/ad-joiner-entraconnect-entraid.psd1 index ff59a3cf..66e8d52a 100644 --- a/examples/workflows/templates/ad-joiner-entraconnect-entraid.psd1 +++ b/examples/workflows/templates/ad-joiner-entraconnect-entraid.psd1 @@ -27,10 +27,9 @@ With = @{ Provider = 'DirectorySync' AuthSessionName = 'EntraConnect' - ProviderInput = @{ - ComputerName = '{{Request.Intent.EntraConnectServer}}' - PolicyType = 'Delta' - } + ComputerName = '{{Request.Intent.EntraConnectServer}}' + + PolicyType = 'Delta' Wait = $true TimeoutSeconds = 300 PollIntervalSeconds = 10 diff --git a/examples/workflows/templates/directorysync-entraconnect-trigger-sync.psd1 b/examples/workflows/templates/directorysync-entraconnect-trigger-sync.psd1 index 086369c2..4b9b3780 100644 --- a/examples/workflows/templates/directorysync-entraconnect-trigger-sync.psd1 +++ b/examples/workflows/templates/directorysync-entraconnect-trigger-sync.psd1 @@ -12,11 +12,10 @@ # Auth session is provided by the host (credential). AuthSessionName = 'EntraConnect' - ProviderInput = @{ - ComputerName = '{{Request.Intent.ComputerName}}' - # Delta or Initial - PolicyType = '{{Request.Intent.PolicyType}}' - } + ComputerName = '{{Request.Intent.ComputerName}}' + + # Delta or Initial + PolicyType = '{{Request.Intent.PolicyType}}' # Optional wait/polling behavior (step-specific) Wait = $true diff --git a/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 b/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 index a6fa1f3a..0b792240 100644 --- a/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 +++ b/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 @@ -15,7 +15,7 @@ function New-IdleEntraConnectDirectorySyncProvider { .OUTPUTS PSCustomObject - Provider instance with methods: GetCapabilities(), StartSyncCycle(ProviderInput, AuthSession), GetSyncCycleState(ProviderInput, AuthSession) + Provider instance with methods: GetCapabilities(), StartSyncCycle(PolicyType, ComputerName, AuthSession), GetSyncCycleState(ComputerName, AuthSession) .EXAMPLE $provider = New-IdleEntraConnectDirectorySyncProvider @@ -26,10 +26,7 @@ function New-IdleEntraConnectDirectorySyncProvider { # With a credential from AuthSessionBroker (AuthSessionType='Credential') $credential = Get-Credential $provider = New-IdleEntraConnectDirectorySyncProvider - $result = $provider.StartSyncCycle(@{ - ComputerName = 'ad-sync1.corp.local' - PolicyType = 'Delta' - }, $credential) + $result = $provider.StartSyncCycle('Delta', 'ad-sync1.corp.local', $credential) #> [CmdletBinding()] param() @@ -112,10 +109,11 @@ function New-IdleEntraConnectDirectorySyncProvider { .DESCRIPTION Triggers a sync cycle via Start-ADSyncSyncCycle on the remote Entra Connect server. - .PARAMETER ProviderInput - Provider-owned input bag. For Entra Connect this must include: - - ComputerName (string): target Entra Connect server hostname for PSRemoting. - - PolicyType (string): sync policy type ('Delta' or 'Initial'). + .PARAMETER PolicyType + The sync policy type: 'Delta' or 'Initial'. + + .PARAMETER ComputerName + Target Entra Connect server hostname for PSRemoting. .PARAMETER AuthSession Credential ([PSCredential]) provided by the host's AuthSessionBroker. @@ -127,31 +125,18 @@ function New-IdleEntraConnectDirectorySyncProvider { #> param( [Parameter(Mandatory)] - [ValidateNotNull()] - [hashtable] $ProviderInput, + [ValidateSet('Delta', 'Initial', IgnoreCase = $true)] + [string] $PolicyType, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $ComputerName, [Parameter(Mandatory)] [ValidateNotNull()] [object] $AuthSession ) - if (-not $ProviderInput.ContainsKey('ComputerName')) { - throw "StartSyncCycle requires ProviderInput.ComputerName." - } - if (-not $ProviderInput.ContainsKey('PolicyType')) { - throw "StartSyncCycle requires ProviderInput.PolicyType." - } - - $computerName = [string]$ProviderInput.ComputerName - if ([string]::IsNullOrWhiteSpace($computerName)) { - throw "StartSyncCycle: ProviderInput.ComputerName must not be null, empty, or whitespace." - } - - $policyType = [string]$ProviderInput.PolicyType - if ($policyType -notin @('Delta', 'Initial')) { - throw "StartSyncCycle: ProviderInput.PolicyType must be 'Delta' or 'Initial' (case-insensitive). Got: $policyType" - } - if ($AuthSession -isnot [pscredential]) { $actualType = $AuthSession.GetType().FullName throw "AuthSession must be a [PSCredential] for PSRemoting session creation. Received: [$actualType]" @@ -159,17 +144,17 @@ function New-IdleEntraConnectDirectorySyncProvider { $remoteSession = $null try { - $remoteSession = $this.NewRemoteSession($computerName, $AuthSession) + $remoteSession = $this.NewRemoteSession($ComputerName, $AuthSession) $this.InvokeRemoteCommand($remoteSession, { param([string] $RemotePolicyType) Import-Module -Name ADSync -ErrorAction Stop Start-ADSyncSyncCycle -PolicyType $RemotePolicyType -ErrorAction Stop - }, @($policyType)) | Out-Null + }, @($PolicyType)) | Out-Null return [pscustomobject]@{ Started = $true - Message = "Sync cycle triggered with PolicyType: $policyType on $computerName" + Message = "Sync cycle triggered with PolicyType: $PolicyType on $ComputerName" } } catch { @@ -197,9 +182,8 @@ function New-IdleEntraConnectDirectorySyncProvider { Queries the sync scheduler state via Get-ADSyncScheduler to determine if a sync cycle is currently in progress. - .PARAMETER ProviderInput - Provider-owned input bag. For Entra Connect this must include: - - ComputerName (string): target Entra Connect server hostname for PSRemoting. + .PARAMETER ComputerName + Target Entra Connect server hostname for PSRemoting. .PARAMETER AuthSession Credential ([PSCredential]) provided by the host's AuthSessionBroker. @@ -212,22 +196,14 @@ function New-IdleEntraConnectDirectorySyncProvider { #> param( [Parameter(Mandatory)] - [ValidateNotNull()] - [hashtable] $ProviderInput, + [ValidateNotNullOrEmpty()] + [string] $ComputerName, [Parameter(Mandatory)] [ValidateNotNull()] [object] $AuthSession ) - if (-not $ProviderInput.ContainsKey('ComputerName')) { - throw "GetSyncCycleState requires ProviderInput.ComputerName." - } - $computerName = [string]$ProviderInput.ComputerName - if ([string]::IsNullOrWhiteSpace($computerName)) { - throw "GetSyncCycleState: ProviderInput.ComputerName must not be null, empty, or whitespace." - } - if ($AuthSession -isnot [pscredential]) { $actualType = $AuthSession.GetType().FullName throw "AuthSession must be a [PSCredential] for PSRemoting session creation. Received: [$actualType]" @@ -235,7 +211,7 @@ function New-IdleEntraConnectDirectorySyncProvider { $remoteSession = $null try { - $remoteSession = $this.NewRemoteSession($computerName, $AuthSession) + $remoteSession = $this.NewRemoteSession($ComputerName, $AuthSession) $scheduler = $this.InvokeRemoteCommand($remoteSession, { Import-Module -Name ADSync -ErrorAction Stop diff --git a/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 b/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 index 677cbfaa..bf19d8da 100644 --- a/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 +++ b/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 @@ -6,8 +6,8 @@ function Invoke-IdleStepTriggerDirectorySync { .DESCRIPTION This is a provider-agnostic step. The host must supply a provider instance via Context.Providers[] that implements: - - StartSyncCycle(ProviderInput, AuthSession) - - GetSyncCycleState(ProviderInput, AuthSession) + - StartSyncCycle(PolicyType, ComputerName, AuthSession) + - GetSyncCycleState(ComputerName, AuthSession) The step is designed for remote execution and requires an elevated auth session provided by the host's AuthSessionBroker. @@ -23,7 +23,8 @@ function Invoke-IdleStepTriggerDirectorySync { .PARAMETER Step Normalized step object from the plan. Must contain a 'With' hashtable with keys: - AuthSessionName (required, string): auth session name for broker - - ProviderInput (required, hashtable): provider-owned input bag + - ComputerName (required, string): target Entra Connect server + - PolicyType (required, string): 'Delta' or 'Initial' (case-insensitive) - Provider (optional, string): provider alias, defaults to 'DirectorySync' - Wait (optional, bool): wait for cycle completion, defaults to $false - TimeoutSeconds (optional, int): wait timeout, defaults to 600 @@ -39,10 +40,8 @@ function Invoke-IdleStepTriggerDirectorySync { Type = 'IdLE.Step.TriggerDirectorySync' With = @{ AuthSessionName = 'DirectorySync' - ProviderInput = @{ - ComputerName = 'ad-sync1.corp.local' - PolicyType = 'Delta' - } + ComputerName = 'ad-sync1.corp.local' + PolicyType = 'Delta' Wait = $true } } @@ -68,13 +67,22 @@ function Invoke-IdleStepTriggerDirectorySync { throw "TriggerDirectorySync requires With.AuthSessionName." } - if (-not $with.ContainsKey('ProviderInput')) { - throw "TriggerDirectorySync requires With.ProviderInput." + if (-not $with.ContainsKey('PolicyType')) { + throw "TriggerDirectorySync requires With.PolicyType." + } + + if (-not $with.ContainsKey('ComputerName')) { + throw "TriggerDirectorySync requires With.ComputerName." + } + + $policyType = [string]$with.PolicyType + if ($policyType -notin @('Delta', 'Initial')) { + throw "TriggerDirectorySync: With.PolicyType must be 'Delta' or 'Initial' (case-insensitive). Got: $policyType" } - $providerInput = $with.ProviderInput - if ($null -eq $providerInput -or -not ($providerInput -is [hashtable])) { - throw "TriggerDirectorySync requires With.ProviderInput to be a hashtable." + $computerName = [string]$with.ComputerName + if ([string]::IsNullOrWhiteSpace($computerName)) { + throw "TriggerDirectorySync: With.ComputerName must not be null, empty, or whitespace." } # Optional inputs with defaults @@ -106,8 +114,9 @@ function Invoke-IdleStepTriggerDirectorySync { try { # Trigger sync cycle - $Context.EventSink.WriteEvent('DirectorySyncTriggered', "Triggering directory sync cycle", $stepName, @{ - ProviderInput = $providerInput + $Context.EventSink.WriteEvent('DirectorySyncTriggered', "Triggering $policyType sync cycle", $stepName, @{ + PolicyType = $policyType + ComputerName = $computerName }) $startResult = Invoke-IdleProviderMethod ` @@ -115,7 +124,7 @@ function Invoke-IdleStepTriggerDirectorySync { -With $with ` -ProviderAlias $providerAlias ` -MethodName 'StartSyncCycle' ` - -MethodArguments @($providerInput) + -MethodArguments @($policyType, $computerName) $changed = $false if ($null -ne $startResult -and ($startResult.PSObject.Properties.Name -contains 'Started')) { @@ -143,7 +152,7 @@ function Invoke-IdleStepTriggerDirectorySync { -With $with ` -ProviderAlias $providerAlias ` -MethodName 'GetSyncCycleState' ` - -MethodArguments @($providerInput) + -MethodArguments @($computerName) $lastState = if ($null -ne $stateResult) { $stateResult.State } else { 'Unknown' } @@ -162,7 +171,7 @@ function Invoke-IdleStepTriggerDirectorySync { -With $with ` -ProviderAlias $providerAlias ` -MethodName 'GetSyncCycleState' ` - -MethodArguments @($providerInput) + -MethodArguments @($computerName) $inProgress = $true if ($null -ne $stateResult -and ($stateResult.PSObject.Properties.Name -contains 'InProgress')) { @@ -194,7 +203,7 @@ function Invoke-IdleStepTriggerDirectorySync { else { # Not waiting - sync triggered successfully $Context.EventSink.WriteEvent('DirectorySyncCompleted', "Sync cycle triggered (not waiting)", $stepName, @{ - ProviderInput = $providerInput + PolicyType = $policyType }) } diff --git a/src/IdLE.Steps.DirectorySync/StepMetadataCatalog.psd1 b/src/IdLE.Steps.DirectorySync/StepMetadataCatalog.psd1 index fce71b74..86435d21 100644 --- a/src/IdLE.Steps.DirectorySync/StepMetadataCatalog.psd1 +++ b/src/IdLE.Steps.DirectorySync/StepMetadataCatalog.psd1 @@ -15,7 +15,7 @@ 'IdLE.Step.TriggerDirectorySync' = @{ RequiredCapabilities = @('IdLE.DirectorySync.Trigger', 'IdLE.DirectorySync.Status') WithSchema = @{ - RequiredKeys = @('AuthSessionName', 'ProviderInput') + RequiredKeys = @('AuthSessionName', 'ComputerName', 'PolicyType') OptionalKeys = @('Provider', 'Wait', 'TimeoutSeconds', 'PollIntervalSeconds', 'AuthSessionOptions') } } diff --git a/tests/ProviderContracts/DirectorySyncProviderComputerNameCredential.Contract.ps1 b/tests/ProviderContracts/DirectorySyncProviderComputerNameCredential.Contract.ps1 index 1c919b18..77f5076d 100644 --- a/tests/ProviderContracts/DirectorySyncProviderComputerNameCredential.Contract.ps1 +++ b/tests/ProviderContracts/DirectorySyncProviderComputerNameCredential.Contract.ps1 @@ -62,12 +62,8 @@ function Invoke-IdleDirectorySyncProviderComputerNameCredentialContractTests { } -Force } - It 'StartSyncCycle accepts ProviderInput and Credential' { - $providerInput = @{ - ComputerName = 'ad-sync1.corp.local' - PolicyType = 'Delta' - } - $result = $script:Provider.StartSyncCycle($providerInput, $script:Credential) + It 'StartSyncCycle accepts ComputerName and Credential auth session' { + $result = $script:Provider.StartSyncCycle('Delta', 'ad-sync1.corp.local', $script:Credential) $result.Started | Should -BeTrue $script:Provider.LastComputerName | Should -Be 'ad-sync1.corp.local' @@ -75,11 +71,8 @@ function Invoke-IdleDirectorySyncProviderComputerNameCredentialContractTests { $script:Provider.RemovedSession | Should -Be $script:MockSession } - It 'GetSyncCycleState accepts ProviderInput and Credential' { - $providerInput = @{ - ComputerName = 'ad-sync1.corp.local' - } - $result = $script:Provider.GetSyncCycleState($providerInput, $script:Credential) + It 'GetSyncCycleState accepts ComputerName and Credential auth session' { + $result = $script:Provider.GetSyncCycleState('ad-sync1.corp.local', $script:Credential) $result.InProgress | Should -BeFalse $script:Provider.LastComputerName | Should -Be 'ad-sync1.corp.local' diff --git a/tests/Providers/EntraConnectDirectorySyncProvider.Tests.ps1 b/tests/Providers/EntraConnectDirectorySyncProvider.Tests.ps1 index 6542d2be..db41d374 100644 --- a/tests/Providers/EntraConnectDirectorySyncProvider.Tests.ps1 +++ b/tests/Providers/EntraConnectDirectorySyncProvider.Tests.ps1 @@ -30,10 +30,6 @@ Describe 'Entra Connect directory sync provider contracts' { BeforeEach { $script:Provider = New-IdleEntraConnectDirectorySyncProvider $script:ComputerName = 'ad-sync1.corp.local' - $script:ProviderInput = @{ - ComputerName = $script:ComputerName - PolicyType = 'Delta' - } $script:MockCredential = [PSCredential]::new( 'contoso\syncadmin', (ConvertTo-SecureString -String 'P@ssw0rd!' -AsPlainText -Force) @@ -69,8 +65,8 @@ Describe 'Entra Connect directory sync provider contracts' { $script:Provider.PSObject.Methods.Name | Should -Contain 'StartSyncCycle' } - It 'StartSyncCycle accepts ProviderInput and AuthSession parameters' { - $result = $script:Provider.StartSyncCycle($script:ProviderInput, $script:MockCredential) + It 'StartSyncCycle accepts PolicyType, ComputerName, and AuthSession parameters' { + $result = $script:Provider.StartSyncCycle('Delta', $script:ComputerName, $script:MockCredential) $result | Should -Not -BeNullOrEmpty $result.PSObject.Properties.Name | Should -Contain 'Started' @@ -80,25 +76,17 @@ Describe 'Entra Connect directory sync provider contracts' { $script:Provider.RemovedSession | Should -Be $script:MockSession } - It 'StartSyncCycle validates ProviderInput.PolicyType' { - $providerInput = @{ - ComputerName = $script:ComputerName - PolicyType = 'Invalid' - } - { $script:Provider.StartSyncCycle($providerInput, $script:MockCredential) } | Should -Throw -ErrorId * -ExpectedMessage '*PolicyType*' + It 'StartSyncCycle validates PolicyType' { + { $script:Provider.StartSyncCycle('Invalid', $script:ComputerName, $script:MockCredential) } | Should -Throw } - It 'StartSyncCycle validates ProviderInput.ComputerName' { - $providerInput = @{ - ComputerName = '' - PolicyType = 'Delta' - } - { $script:Provider.StartSyncCycle($providerInput, $script:MockCredential) } | Should -Throw -ErrorId * -ExpectedMessage '*ComputerName*' + It 'StartSyncCycle validates ComputerName' { + { $script:Provider.StartSyncCycle('Delta', '', $script:MockCredential) } | Should -Throw -ErrorId * -ExpectedMessage '*ComputerName*' } It 'StartSyncCycle validates AuthSession is PSCredential' { $badSession = [pscustomobject]@{ Name = 'BadSession' } - { $script:Provider.StartSyncCycle($script:ProviderInput, $badSession) } | Should -Throw -ErrorId * -ExpectedMessage '*PSCredential*' + { $script:Provider.StartSyncCycle('Delta', $script:ComputerName, $badSession) } | Should -Throw -ErrorId * -ExpectedMessage '*PSCredential*' } It 'StartSyncCycle always closes remoting session' { @@ -107,7 +95,7 @@ Describe 'Entra Connect directory sync provider contracts' { throw 'remote failure' } -Force - { $script:Provider.StartSyncCycle($script:ProviderInput, $script:MockCredential) } | Should -Throw + { $script:Provider.StartSyncCycle('Delta', $script:ComputerName, $script:MockCredential) } | Should -Throw $script:Provider.RemovedSession | Should -Be $script:MockSession } @@ -115,8 +103,8 @@ Describe 'Entra Connect directory sync provider contracts' { $script:Provider.PSObject.Methods.Name | Should -Contain 'GetSyncCycleState' } - It 'GetSyncCycleState accepts ProviderInput and AuthSession parameters' { - $result = $script:Provider.GetSyncCycleState($script:ProviderInput, $script:MockCredential) + It 'GetSyncCycleState accepts ComputerName and AuthSession parameters' { + $result = $script:Provider.GetSyncCycleState($script:ComputerName, $script:MockCredential) $result | Should -Not -BeNullOrEmpty $result.PSObject.Properties.Name | Should -Contain 'InProgress' @@ -128,22 +116,18 @@ Describe 'Entra Connect directory sync provider contracts' { } It 'GetSyncCycleState returns correct InProgress value' { - $result = $script:Provider.GetSyncCycleState($script:ProviderInput, $script:MockCredential) + $result = $script:Provider.GetSyncCycleState($script:ComputerName, $script:MockCredential) $result.InProgress | Should -BeOfType [bool] } - It 'GetSyncCycleState validates ProviderInput.ComputerName' { - $providerInput = @{ - ComputerName = '' - PolicyType = 'Delta' - } - { $script:Provider.GetSyncCycleState($providerInput, $script:MockCredential) } | Should -Throw -ErrorId * -ExpectedMessage '*ComputerName*' + It 'GetSyncCycleState validates ComputerName' { + { $script:Provider.GetSyncCycleState('', $script:MockCredential) } | Should -Throw -ErrorId * -ExpectedMessage '*ComputerName*' } It 'GetSyncCycleState validates AuthSession is PSCredential' { $badSession = [pscustomobject]@{ Name = 'BadSession' } - { $script:Provider.GetSyncCycleState($script:ProviderInput, $badSession) } | Should -Throw -ErrorId * -ExpectedMessage '*PSCredential*' + { $script:Provider.GetSyncCycleState($script:ComputerName, $badSession) } | Should -Throw -ErrorId * -ExpectedMessage '*PSCredential*' } It 'GetSyncCycleState always closes remoting session' { @@ -152,7 +136,7 @@ Describe 'Entra Connect directory sync provider contracts' { throw 'remote failure' } -Force - { $script:Provider.GetSyncCycleState($script:ProviderInput, $script:MockCredential) } | Should -Throw + { $script:Provider.GetSyncCycleState($script:ComputerName, $script:MockCredential) } | Should -Throw $script:Provider.RemovedSession | Should -Be $script:MockSession } } diff --git a/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 b/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 index 67c8185b..5e27d2ec 100644 --- a/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 +++ b/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 @@ -22,30 +22,33 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { $script:MockProvider | Add-Member -MemberType ScriptMethod -Name StartSyncCycle -Value { param( [Parameter(Mandatory)] - [hashtable] $ProviderInput, + [string] $PolicyType, + + [Parameter(Mandatory)] + [string] $ComputerName, [Parameter(Mandatory)] [object] $AuthSession ) - $this.LastComputerName = [string]$ProviderInput.ComputerName + $this.LastComputerName = $ComputerName return [pscustomobject]@{ Started = $true - Message = "Sync cycle triggered with PolicyType: $($ProviderInput.PolicyType)" + Message = "Sync cycle triggered with PolicyType: $PolicyType" } } -Force $script:MockProvider | Add-Member -MemberType ScriptMethod -Name GetSyncCycleState -Value { param( [Parameter(Mandatory)] - [hashtable] $ProviderInput, + [string] $ComputerName, [Parameter(Mandatory)] [object] $AuthSession ) - $this.LastComputerName = [string]$ProviderInput.ComputerName + $this.LastComputerName = $ComputerName # Increment poll count and determine state $this.PollCount++ @@ -95,10 +98,8 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { Type = 'IdLE.Step.TriggerDirectorySync' With = @{ AuthSessionName = 'EntraConnect' - ProviderInput = @{ - ComputerName = 'ad-sync1.corp.local' - PolicyType = 'Delta' - } + ComputerName = 'ad-sync1.corp.local' + PolicyType = 'Delta' Provider = 'DirectorySync' } } @@ -123,20 +124,28 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { { & $handler -Context $script:Context -Step $step } | Should -Throw -ErrorId * -ExpectedMessage '*AuthSessionName*' } - It 'throws when With.ProviderInput is missing' { + It 'throws when With.PolicyType is missing' { + $step = $script:StepTemplate + $step.With.Remove('PolicyType') + + $handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync' + { & $handler -Context $script:Context -Step $step } | Should -Throw -ErrorId * -ExpectedMessage '*PolicyType*' + } + + It 'throws when With.ComputerName is missing' { $step = $script:StepTemplate - $step.With.Remove('ProviderInput') + $step.With.Remove('ComputerName') $handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync' - { & $handler -Context $script:Context -Step $step } | Should -Throw -ErrorId * -ExpectedMessage '*ProviderInput*' + { & $handler -Context $script:Context -Step $step } | Should -Throw -ErrorId * -ExpectedMessage '*ComputerName*' } - It 'throws when With.ProviderInput is not a hashtable' { + It 'throws when With.ComputerName is whitespace' { $step = $script:StepTemplate - $step.With.ProviderInput = 'invalid' + $step.With.ComputerName = ' ' $handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync' - { & $handler -Context $script:Context -Step $step } | Should -Throw -ErrorId * -ExpectedMessage '*ProviderInput*' + { & $handler -Context $script:Context -Step $step } | Should -Throw -ErrorId * -ExpectedMessage '*ComputerName*' } It 'uses default provider alias when not specified' { @@ -206,7 +215,7 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { It 'throws timeout error when sync does not complete in time' { # Mock provider that never completes $script:MockProvider | Add-Member -MemberType ScriptMethod -Name GetSyncCycleState -Value { - param([hashtable] $ProviderInput, [object] $AuthSession) + param([string] $ComputerName, [object] $AuthSession) return [pscustomobject]@{ InProgress = $true State = 'InProgress' @@ -227,7 +236,7 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { # Use the provider's PollCount property which is already initialized $script:MockProvider.PollCount = 0 $script:MockProvider | Add-Member -MemberType ScriptMethod -Name GetSyncCycleState -Value { - param([hashtable] $ProviderInput, [object] $AuthSession) + param([string] $ComputerName, [object] $AuthSession) $this.PollCount++ $inProgress = $this.PollCount -le 2 return [pscustomobject]@{ diff --git a/tests/fixtures/workflows/joiner-with-dirsync.psd1 b/tests/fixtures/workflows/joiner-with-dirsync.psd1 index e9ccd899..404092b5 100644 --- a/tests/fixtures/workflows/joiner-with-dirsync.psd1 +++ b/tests/fixtures/workflows/joiner-with-dirsync.psd1 @@ -9,10 +9,8 @@ Type = 'IdLE.Step.TriggerDirectorySync' With = @{ AuthSessionName = 'DirSync' - ProviderInput = @{ - ComputerName = 'ad-sync1.corp.local' - PolicyType = 'Delta' - } + ComputerName = 'ad-sync1.corp.local' + PolicyType = 'Delta' Wait = $false Provider = 'DirectorySync' } From 3f76964599ba4271072d936845c25d15fd5b8310 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 15:02:28 +0000 Subject: [PATCH 07/23] docs: add provider key to EntraConnect auth example Agent-Logs-Url: https://github.com/blindzero/IdentityLifecycleEngine/sessions/b84c0287-8b71-4286-a986-5d9f19e324fe Co-authored-by: ntt-matthias-fleschuetz <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> --- docs/reference/providers/provider-directorysync-entraconnect.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/reference/providers/provider-directorysync-entraconnect.md b/docs/reference/providers/provider-directorysync-entraconnect.md index bdb8900f..a1e111d6 100644 --- a/docs/reference/providers/provider-directorysync-entraconnect.md +++ b/docs/reference/providers/provider-directorysync-entraconnect.md @@ -73,6 +73,7 @@ The provider creates and cleans up PSRemoting sessions internally. Your host/runtime should provide this credential via the AuthSessionBroker and you reference it in the step via: +- `Provider = 'DirectorySync'` - `AuthSessionName = 'EntraConnect'` - `ComputerName = 'ad-sync1.corp.local'` - `PolicyType = 'Delta'` From 7a15f33cdb3d149dac04d8afd65b9ff50aaa33d5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 15:06:58 +0000 Subject: [PATCH 08/23] refactor: make TriggerDirectorySync auth session routing optional Agent-Logs-Url: https://github.com/blindzero/IdentityLifecycleEngine/sessions/635b8c6e-5ebf-4a09-9f40-8192625d5bae Co-authored-by: ntt-matthias-fleschuetz <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> --- .../providers/provider-directorysync-entraconnect.md | 1 + docs/reference/steps/step-trigger-directory-sync.md | 3 +-- .../Public/Invoke-IdleStepTriggerDirectorySync.ps1 | 8 ++------ src/IdLE.Steps.DirectorySync/StepMetadataCatalog.psd1 | 4 ++-- tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 | 6 ++++-- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/docs/reference/providers/provider-directorysync-entraconnect.md b/docs/reference/providers/provider-directorysync-entraconnect.md index a1e111d6..839a532a 100644 --- a/docs/reference/providers/provider-directorysync-entraconnect.md +++ b/docs/reference/providers/provider-directorysync-entraconnect.md @@ -75,6 +75,7 @@ Your host/runtime should provide this credential via the AuthSessionBroker and y - `Provider = 'DirectorySync'` - `AuthSessionName = 'EntraConnect'` +- `AuthSessionOptions = @{ Role = 'Admin' }` (optional, recommended for role-scoped session routing) - `ComputerName = 'ad-sync1.corp.local'` - `PolicyType = 'Delta'` diff --git a/docs/reference/steps/step-trigger-directory-sync.md b/docs/reference/steps/step-trigger-directory-sync.md index 93acac7b..54cefeae 100644 --- a/docs/reference/steps/step-trigger-directory-sync.md +++ b/docs/reference/steps/step-trigger-directory-sync.md @@ -28,7 +28,7 @@ provided by the host's AuthSessionBroker. Authentication: -- With.AuthSessionName (required): routing key for AuthSessionBroker +- With.AuthSessionName (optional): routing key for AuthSessionBroker - With.AuthSessionOptions (optional, hashtable): forwarded to broker for session selection @@ -40,7 +40,6 @@ The following keys are required in the step's ``With`` configuration: | Key | Required | Description | | --- | --- | --- | -| `AuthSessionName` | Yes | Name of auth session to use (optional) | | `ComputerName` | Yes | See step description for details | | `PolicyType` | Yes | Type of policy (e.g., Delta, Initial) | diff --git a/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 b/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 index bf19d8da..2c4fb4aa 100644 --- a/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 +++ b/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 @@ -13,7 +13,7 @@ function Invoke-IdleStepTriggerDirectorySync { provided by the host's AuthSessionBroker. Authentication: - - With.AuthSessionName (required): routing key for AuthSessionBroker + - With.AuthSessionName (optional): routing key for AuthSessionBroker - With.AuthSessionOptions (optional, hashtable): forwarded to broker for session selection - ScriptBlocks in AuthSessionOptions are rejected (security boundary) @@ -22,7 +22,7 @@ function Invoke-IdleStepTriggerDirectorySync { .PARAMETER Step Normalized step object from the plan. Must contain a 'With' hashtable with keys: - - AuthSessionName (required, string): auth session name for broker + - AuthSessionName (optional, string): auth session name for broker - ComputerName (required, string): target Entra Connect server - PolicyType (required, string): 'Delta' or 'Initial' (case-insensitive) - Provider (optional, string): provider alias, defaults to 'DirectorySync' @@ -63,10 +63,6 @@ function Invoke-IdleStepTriggerDirectorySync { } # Validate required inputs - if (-not $with.ContainsKey('AuthSessionName')) { - throw "TriggerDirectorySync requires With.AuthSessionName." - } - if (-not $with.ContainsKey('PolicyType')) { throw "TriggerDirectorySync requires With.PolicyType." } diff --git a/src/IdLE.Steps.DirectorySync/StepMetadataCatalog.psd1 b/src/IdLE.Steps.DirectorySync/StepMetadataCatalog.psd1 index 86435d21..695f10f3 100644 --- a/src/IdLE.Steps.DirectorySync/StepMetadataCatalog.psd1 +++ b/src/IdLE.Steps.DirectorySync/StepMetadataCatalog.psd1 @@ -15,8 +15,8 @@ 'IdLE.Step.TriggerDirectorySync' = @{ RequiredCapabilities = @('IdLE.DirectorySync.Trigger', 'IdLE.DirectorySync.Status') WithSchema = @{ - RequiredKeys = @('AuthSessionName', 'ComputerName', 'PolicyType') - OptionalKeys = @('Provider', 'Wait', 'TimeoutSeconds', 'PollIntervalSeconds', 'AuthSessionOptions') + RequiredKeys = @('ComputerName', 'PolicyType') + OptionalKeys = @('Provider', 'AuthSessionName', 'Wait', 'TimeoutSeconds', 'PollIntervalSeconds', 'AuthSessionOptions') } } } diff --git a/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 b/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 index 5e27d2ec..8f07805e 100644 --- a/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 +++ b/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 @@ -116,12 +116,14 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { { & $handler -Context $script:Context -Step $step } | Should -Throw } - It 'throws when With.AuthSessionName is missing' { + It 'allows missing With.AuthSessionName when provider supports default auth session routing' { $step = $script:StepTemplate $step.With.Remove('AuthSessionName') $handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync' - { & $handler -Context $script:Context -Step $step } | Should -Throw -ErrorId * -ExpectedMessage '*AuthSessionName*' + $result = & $handler -Context $script:Context -Step $step + + $result.Status | Should -Be 'Completed' } It 'throws when With.PolicyType is missing' { From 2c1e632f9ed6736f5c2d84022a9c92a27b68fd19 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 15:07:42 +0000 Subject: [PATCH 09/23] docs: clarify default auth session behavior in TriggerDirectorySync Agent-Logs-Url: https://github.com/blindzero/IdentityLifecycleEngine/sessions/635b8c6e-5ebf-4a09-9f40-8192625d5bae Co-authored-by: ntt-matthias-fleschuetz <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> --- docs/reference/steps/step-trigger-directory-sync.md | 2 ++ .../Public/Invoke-IdleStepTriggerDirectorySync.ps1 | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/reference/steps/step-trigger-directory-sync.md b/docs/reference/steps/step-trigger-directory-sync.md index 54cefeae..9a4de255 100644 --- a/docs/reference/steps/step-trigger-directory-sync.md +++ b/docs/reference/steps/step-trigger-directory-sync.md @@ -32,6 +32,8 @@ Authentication: - With.AuthSessionOptions (optional, hashtable): forwarded to broker for session selection +- If AuthSessionName is omitted, the broker is asked for a default session + - ScriptBlocks in AuthSessionOptions are rejected (security boundary) ## Inputs (With.*) diff --git a/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 b/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 index 2c4fb4aa..40c040a6 100644 --- a/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 +++ b/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 @@ -15,6 +15,7 @@ function Invoke-IdleStepTriggerDirectorySync { Authentication: - With.AuthSessionName (optional): routing key for AuthSessionBroker - With.AuthSessionOptions (optional, hashtable): forwarded to broker for session selection + - If AuthSessionName is omitted, the broker is asked for a default session - ScriptBlocks in AuthSessionOptions are rejected (security boundary) .PARAMETER Context @@ -22,7 +23,7 @@ function Invoke-IdleStepTriggerDirectorySync { .PARAMETER Step Normalized step object from the plan. Must contain a 'With' hashtable with keys: - - AuthSessionName (optional, string): auth session name for broker + - AuthSessionName (optional, string): auth session name for broker (default session is used when omitted) - ComputerName (required, string): target Entra Connect server - PolicyType (required, string): 'Delta' or 'Initial' (case-insensitive) - Provider (optional, string): provider alias, defaults to 'DirectorySync' From b490c58bf427aae355ed92feb6e6ce84c790ff50 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 15:15:19 +0000 Subject: [PATCH 10/23] refactor: make dirsync provider-specific inputs optional at step level Agent-Logs-Url: https://github.com/blindzero/IdentityLifecycleEngine/sessions/312a7759-704b-4fce-b114-2086c54c4102 Co-authored-by: ntt-matthias-fleschuetz <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> --- .../steps/step-trigger-directory-sync.md | 10 +++---- .../Invoke-IdleStepTriggerDirectorySync.ps1 | 26 ++++------------ .../StepMetadataCatalog.psd1 | 4 +-- ...oke-IdleStepTriggerDirectorySync.Tests.ps1 | 30 +++++++++++++++---- 4 files changed, 36 insertions(+), 34 deletions(-) diff --git a/docs/reference/steps/step-trigger-directory-sync.md b/docs/reference/steps/step-trigger-directory-sync.md index 9a4de255..361878e6 100644 --- a/docs/reference/steps/step-trigger-directory-sync.md +++ b/docs/reference/steps/step-trigger-directory-sync.md @@ -34,16 +34,14 @@ Authentication: - If AuthSessionName is omitted, the broker is asked for a default session +- ComputerName and PolicyType are provider-specific inputs and are validated by the selected provider + - ScriptBlocks in AuthSessionOptions are rejected (security boundary) ## Inputs (With.*) -The following keys are required in the step's ``With`` configuration: - -| Key | Required | Description | -| --- | --- | --- | -| `ComputerName` | Yes | See step description for details | -| `PolicyType` | Yes | Type of policy (e.g., Delta, Initial) | +The required input keys could not be detected automatically. +Please refer to the step description and examples for usage details. ## Example diff --git a/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 b/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 index 40c040a6..bd6e47ce 100644 --- a/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 +++ b/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 @@ -16,6 +16,7 @@ function Invoke-IdleStepTriggerDirectorySync { - With.AuthSessionName (optional): routing key for AuthSessionBroker - With.AuthSessionOptions (optional, hashtable): forwarded to broker for session selection - If AuthSessionName is omitted, the broker is asked for a default session + - ComputerName and PolicyType are provider-specific inputs and are validated by the selected provider - ScriptBlocks in AuthSessionOptions are rejected (security boundary) .PARAMETER Context @@ -24,8 +25,8 @@ function Invoke-IdleStepTriggerDirectorySync { .PARAMETER Step Normalized step object from the plan. Must contain a 'With' hashtable with keys: - AuthSessionName (optional, string): auth session name for broker (default session is used when omitted) - - ComputerName (required, string): target Entra Connect server - - PolicyType (required, string): 'Delta' or 'Initial' (case-insensitive) + - ComputerName (optional, string): provider-specific target server input + - PolicyType (optional, string): provider-specific policy input - Provider (optional, string): provider alias, defaults to 'DirectorySync' - Wait (optional, bool): wait for cycle completion, defaults to $false - TimeoutSeconds (optional, int): wait timeout, defaults to 600 @@ -63,24 +64,9 @@ function Invoke-IdleStepTriggerDirectorySync { throw "TriggerDirectorySync requires 'With' to be a hashtable." } - # Validate required inputs - if (-not $with.ContainsKey('PolicyType')) { - throw "TriggerDirectorySync requires With.PolicyType." - } - - if (-not $with.ContainsKey('ComputerName')) { - throw "TriggerDirectorySync requires With.ComputerName." - } - - $policyType = [string]$with.PolicyType - if ($policyType -notin @('Delta', 'Initial')) { - throw "TriggerDirectorySync: With.PolicyType must be 'Delta' or 'Initial' (case-insensitive). Got: $policyType" - } - - $computerName = [string]$with.ComputerName - if ([string]::IsNullOrWhiteSpace($computerName)) { - throw "TriggerDirectorySync: With.ComputerName must not be null, empty, or whitespace." - } + # Provider-specific inputs are validated by the selected provider implementation + $policyType = if ($with.ContainsKey('PolicyType')) { [string]$with.PolicyType } else { $null } + $computerName = if ($with.ContainsKey('ComputerName')) { [string]$with.ComputerName } else { $null } # Optional inputs with defaults $providerAlias = if ($with.ContainsKey('Provider')) { [string]$with.Provider } else { 'DirectorySync' } diff --git a/src/IdLE.Steps.DirectorySync/StepMetadataCatalog.psd1 b/src/IdLE.Steps.DirectorySync/StepMetadataCatalog.psd1 index 695f10f3..54109194 100644 --- a/src/IdLE.Steps.DirectorySync/StepMetadataCatalog.psd1 +++ b/src/IdLE.Steps.DirectorySync/StepMetadataCatalog.psd1 @@ -15,8 +15,8 @@ 'IdLE.Step.TriggerDirectorySync' = @{ RequiredCapabilities = @('IdLE.DirectorySync.Trigger', 'IdLE.DirectorySync.Status') WithSchema = @{ - RequiredKeys = @('ComputerName', 'PolicyType') - OptionalKeys = @('Provider', 'AuthSessionName', 'Wait', 'TimeoutSeconds', 'PollIntervalSeconds', 'AuthSessionOptions') + RequiredKeys = @() + OptionalKeys = @('Provider', 'AuthSessionName', 'AuthSessionOptions', 'ComputerName', 'PolicyType', 'Wait', 'TimeoutSeconds', 'PollIntervalSeconds') } } } diff --git a/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 b/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 index 8f07805e..741ec86b 100644 --- a/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 +++ b/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 @@ -126,28 +126,46 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { $result.Status | Should -Be 'Completed' } - It 'throws when With.PolicyType is missing' { + It 'does not enforce With.PolicyType at step level' { $step = $script:StepTemplate $step.With.Remove('PolicyType') + $script:MockProvider | Add-Member -MemberType ScriptMethod -Name StartSyncCycle -Value { + param([AllowNull()][object] $PolicyType, [AllowNull()][object] $ComputerName, [object] $AuthSession) + return [pscustomobject]@{ Started = $true } + } -Force $handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync' - { & $handler -Context $script:Context -Step $step } | Should -Throw -ErrorId * -ExpectedMessage '*PolicyType*' + $result = & $handler -Context $script:Context -Step $step + + $result.Status | Should -Be 'Completed' } - It 'throws when With.ComputerName is missing' { + It 'does not enforce With.ComputerName at step level' { $step = $script:StepTemplate $step.With.Remove('ComputerName') + $script:MockProvider | Add-Member -MemberType ScriptMethod -Name StartSyncCycle -Value { + param([AllowNull()][object] $PolicyType, [AllowNull()][object] $ComputerName, [object] $AuthSession) + return [pscustomobject]@{ Started = $true } + } -Force $handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync' - { & $handler -Context $script:Context -Step $step } | Should -Throw -ErrorId * -ExpectedMessage '*ComputerName*' + $result = & $handler -Context $script:Context -Step $step + + $result.Status | Should -Be 'Completed' } - It 'throws when With.ComputerName is whitespace' { + It 'does not enforce With.ComputerName whitespace validation at step level' { $step = $script:StepTemplate $step.With.ComputerName = ' ' + $script:MockProvider | Add-Member -MemberType ScriptMethod -Name StartSyncCycle -Value { + param([AllowNull()][object] $PolicyType, [AllowNull()][object] $ComputerName, [object] $AuthSession) + return [pscustomobject]@{ Started = $true } + } -Force $handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync' - { & $handler -Context $script:Context -Step $step } | Should -Throw -ErrorId * -ExpectedMessage '*ComputerName*' + $result = & $handler -Context $script:Context -Step $step + + $result.Status | Should -Be 'Completed' } It 'uses default provider alias when not specified' { From ae966e5de28e20af177ac6fb5d555a3f1833a361 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 15:16:15 +0000 Subject: [PATCH 11/23] test: reduce duplication and preserve provider-side null validation path Agent-Logs-Url: https://github.com/blindzero/IdentityLifecycleEngine/sessions/312a7759-704b-4fce-b114-2086c54c4102 Co-authored-by: ntt-matthias-fleschuetz <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> --- .../Invoke-IdleStepTriggerDirectorySync.ps1 | 4 ++-- ...oke-IdleStepTriggerDirectorySync.Tests.ps1 | 22 +++++++++---------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 b/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 index bd6e47ce..fb8d1d28 100644 --- a/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 +++ b/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 @@ -65,8 +65,8 @@ function Invoke-IdleStepTriggerDirectorySync { } # Provider-specific inputs are validated by the selected provider implementation - $policyType = if ($with.ContainsKey('PolicyType')) { [string]$with.PolicyType } else { $null } - $computerName = if ($with.ContainsKey('ComputerName')) { [string]$with.ComputerName } else { $null } + $policyType = if ($with.ContainsKey('PolicyType')) { $with.PolicyType } else { $null } + $computerName = if ($with.ContainsKey('ComputerName')) { $with.ComputerName } else { $null } # Optional inputs with defaults $providerAlias = if ($with.ContainsKey('Provider')) { [string]$with.Provider } else { 'DirectorySync' } diff --git a/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 b/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 index 741ec86b..013c6567 100644 --- a/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 +++ b/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 @@ -103,6 +103,13 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { Provider = 'DirectorySync' } } + + $script:SetPermissiveStartSyncCycle = { + $script:MockProvider | Add-Member -MemberType ScriptMethod -Name StartSyncCycle -Value { + param([AllowNull()][object] $PolicyType, [AllowNull()][object] $ComputerName, [object] $AuthSession) + return [pscustomobject]@{ Started = $true } + } -Force + } } Context 'Input validation' { @@ -129,10 +136,7 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { It 'does not enforce With.PolicyType at step level' { $step = $script:StepTemplate $step.With.Remove('PolicyType') - $script:MockProvider | Add-Member -MemberType ScriptMethod -Name StartSyncCycle -Value { - param([AllowNull()][object] $PolicyType, [AllowNull()][object] $ComputerName, [object] $AuthSession) - return [pscustomobject]@{ Started = $true } - } -Force + & $script:SetPermissiveStartSyncCycle $handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync' $result = & $handler -Context $script:Context -Step $step @@ -143,10 +147,7 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { It 'does not enforce With.ComputerName at step level' { $step = $script:StepTemplate $step.With.Remove('ComputerName') - $script:MockProvider | Add-Member -MemberType ScriptMethod -Name StartSyncCycle -Value { - param([AllowNull()][object] $PolicyType, [AllowNull()][object] $ComputerName, [object] $AuthSession) - return [pscustomobject]@{ Started = $true } - } -Force + & $script:SetPermissiveStartSyncCycle $handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync' $result = & $handler -Context $script:Context -Step $step @@ -157,10 +158,7 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { It 'does not enforce With.ComputerName whitespace validation at step level' { $step = $script:StepTemplate $step.With.ComputerName = ' ' - $script:MockProvider | Add-Member -MemberType ScriptMethod -Name StartSyncCycle -Value { - param([AllowNull()][object] $PolicyType, [AllowNull()][object] $ComputerName, [object] $AuthSession) - return [pscustomobject]@{ Started = $true } - } -Force + & $script:SetPermissiveStartSyncCycle $handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync' $result = & $handler -Context $script:Context -Step $step From d96d8273209e2431a1c5624840027831921b9f5a Mon Sep 17 00:00:00 2001 From: Matthias Fleschuetz <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> Date: Sat, 16 May 2026 17:49:00 +0200 Subject: [PATCH 12/23] provider: fix reference docs for directorysync entraconnect provider --- .../provider-directorysync-entraconnect.md | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/docs/reference/providers/provider-directorysync-entraconnect.md b/docs/reference/providers/provider-directorysync-entraconnect.md index 839a532a..f628aa15 100644 --- a/docs/reference/providers/provider-directorysync-entraconnect.md +++ b/docs/reference/providers/provider-directorysync-entraconnect.md @@ -66,46 +66,44 @@ $providers = @{ } ``` -## Authentication (important) +## Authentication This provider requires an AuthSession credential ([PSCredential]) and **must be elevated**. The provider creates and cleans up PSRemoting sessions internally. -Your host/runtime should provide this credential via the AuthSessionBroker and you reference it in the step via: +By default, the AD provider uses the run-as identity (integrated authentication). +For explicit runtime credential selection, use the AuthSessionBroker and pass an AuthSession via step configuration: -- `Provider = 'DirectorySync'` -- `AuthSessionName = 'EntraConnect'` -- `AuthSessionOptions = @{ Role = 'Admin' }` (optional, recommended for role-scoped session routing) -- `ComputerName = 'ad-sync1.corp.local'` -- `PolicyType = 'Delta'` +- With.AuthSessionName +- With.AuthSessionOptions (optional) -> No interactive prompts are made. If the credential does not have elevated rights on the target server, triggering a sync cycle will fail with a privilege/elevation error. +> Keep credentials/secrets out of workflow files. Use the broker/host to resolve them at runtime. -## Supported operations +## Supported Step Types -This provider advertises these capabilities: +The Directory Sync (Entra Connect) provider supports the common identity lifecycle and entitlement operations used by these step types: -- `IdLE.DirectorySync.Trigger` -- `IdLE.DirectorySync.Status` - -Those are typically used by step types like: - -- `IdLE.Step.TriggerDirectorySync` (trigger + optional wait/poll) +| Step type | Typical use | Notes | +| --- | --- | --- | +| `IdLE.Step.TriggerDirectorySync` | Trigger Directory Sync | Initiated by PSRemote session execution, with 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: -- step inputs (`ComputerName`, `PolicyType`, `Wait`, `TimeoutSeconds`, `PollIntervalSeconds`) -- host configuration (credential broker) +### Options reference + +| Option | Type | Default | Meaning | +| --- | --- | --- | --- | +| `ComputerName` | `string` | `` | ComputerName for PSSession connection | +| `PolicyType` | `string` | `Delta` | `Delta` or `Full` sync policy | +| `Wait` | `bool` | `true` | Poll sync status and wait for result (or timeout) | +| `PollIntervalSeconds` | `int` | `10` | Interval in seconds to poll for sync status | +| `TimeoutSeconds` | `int` | `600` | Timeout for poll wait in seconds. Will result in `StepFailed` | -## Examples (canonical template) +## Examples {EntraConnectTriggerSync} From 026a7134661030c491148b2fe1340f3f18295e84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Flesch=C3=BCtz?= <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> Date: Sat, 16 May 2026 17:59:59 +0200 Subject: [PATCH 13/23] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../providers/provider-directorysync-entraconnect.md | 10 +++++----- .../New-IdleEntraConnectDirectorySyncProvider.ps1 | 7 ++++++- .../Public/Invoke-IdleStepTriggerDirectorySync.ps1 | 12 ++++++++++-- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/docs/reference/providers/provider-directorysync-entraconnect.md b/docs/reference/providers/provider-directorysync-entraconnect.md index f628aa15..edb3628d 100644 --- a/docs/reference/providers/provider-directorysync-entraconnect.md +++ b/docs/reference/providers/provider-directorysync-entraconnect.md @@ -71,8 +71,8 @@ $providers = @{ This provider requires an AuthSession credential ([PSCredential]) and **must be elevated**. The provider creates and cleans up PSRemoting sessions internally. -By default, the AD provider uses the run-as identity (integrated authentication). -For explicit runtime credential selection, use the AuthSessionBroker and pass an AuthSession via step configuration: +This provider does not document a default integrated/run-as authentication fallback; provide the credential at runtime via the AuthSessionBroker. +To select the runtime credential for this provider, pass the AuthSession via step configuration: - With.AuthSessionName - With.AuthSessionOptions (optional) @@ -97,9 +97,9 @@ This provider does **not** support any of the allowlisted Context Resolver capab | Option | Type | Default | Meaning | | --- | --- | --- | --- | -| `ComputerName` | `string` | `` | ComputerName for PSSession connection | -| `PolicyType` | `string` | `Delta` | `Delta` or `Full` sync policy | -| `Wait` | `bool` | `true` | Poll sync status and wait for result (or timeout) | +| `ComputerName` | `string` | Required | ComputerName for PSSession connection | +| `PolicyType` | `string` | Required | `Delta` or `Initial` sync policy | +| `Wait` | `bool` | `false` | Poll sync status and wait for result (or timeout) | | `PollIntervalSeconds` | `int` | `10` | Interval in seconds to poll for sync status | | `TimeoutSeconds` | `int` | `600` | Timeout for poll wait in seconds. Will result in `StepFailed` | diff --git a/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 b/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 index 0b792240..dd73958d 100644 --- a/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 +++ b/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 @@ -97,7 +97,12 @@ function New-IdleEntraConnectDirectorySyncProvider { ) if ($null -ne $Session) { - Remove-PSSession -Session $Session -ErrorAction SilentlyContinue + try { + Remove-PSSession -Session $Session -ErrorAction Stop + } + catch { + Write-Warning "Failed to remove PSRemoting session: $($_.Exception.Message)" + } } } -Force diff --git a/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 b/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 index fb8d1d28..d01e7108 100644 --- a/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 +++ b/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 @@ -64,8 +64,16 @@ function Invoke-IdleStepTriggerDirectorySync { throw "TriggerDirectorySync requires 'With' to be a hashtable." } - # Provider-specific inputs are validated by the selected provider implementation - $policyType = if ($with.ContainsKey('PolicyType')) { $with.PolicyType } else { $null } + # Keep lightweight validation here so workflow configuration errors point to With.* keys. + if (-not $with.ContainsKey('PolicyType')) { + throw "TriggerDirectorySync requires With.PolicyType." + } + + $policyType = [string]$with.PolicyType + if ($policyType -notin @('Delta', 'Initial')) { + throw "TriggerDirectorySync: With.PolicyType must be 'Delta' or 'Initial'. Got: $policyType" + } + $computerName = if ($with.ContainsKey('ComputerName')) { $with.ComputerName } else { $null } # Optional inputs with defaults From 56618f07682f397980617e798fb98333d0d6584b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 16:03:59 +0000 Subject: [PATCH 14/23] fix: align directory sync step/docs/tests with review feedback Agent-Logs-Url: https://github.com/blindzero/IdentityLifecycleEngine/sessions/39c5f8d7-7f77-4665-84dd-58ac63c367f3 Co-authored-by: ntt-matthias-fleschuetz <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> --- docs/reference/steps/step-emit-event.md | 4 +-- .../step-prune-entitlements-ensure-keep.md | 4 +-- .../steps/step-trigger-directory-sync.md | 4 +-- ...rectorysync-entraconnect-trigger-sync.psd1 | 3 ++ .../Invoke-IdleStepTriggerDirectorySync.ps1 | 11 +------ ...oke-IdleStepTriggerDirectorySync.Tests.ps1 | 32 ++++++++++++++++++- tools/Generate-IdleStepReference.ps1 | 4 +-- 7 files changed, 43 insertions(+), 19 deletions(-) diff --git a/docs/reference/steps/step-emit-event.md b/docs/reference/steps/step-emit-event.md index 6bb78348..bd882516 100644 --- a/docs/reference/steps/step-emit-event.md +++ b/docs/reference/steps/step-emit-event.md @@ -22,8 +22,8 @@ to write structured events. ## Inputs (With.*) -The required input keys could not be detected automatically. -Please refer to the step description and examples for usage details. +This step has no required ``With.*`` keys at step schema level. +Inputs may still be provider-specific; refer to the step description and examples for usage details. ## Example diff --git a/docs/reference/steps/step-prune-entitlements-ensure-keep.md b/docs/reference/steps/step-prune-entitlements-ensure-keep.md index 580e1421..047346c6 100644 --- a/docs/reference/steps/step-prune-entitlements-ensure-keep.md +++ b/docs/reference/steps/step-prune-entitlements-ensure-keep.md @@ -77,8 +77,8 @@ Authentication: ## Inputs (With.*) -The required input keys could not be detected automatically. -Please refer to the step description and examples for usage details. +This step has no required ``With.*`` keys at step schema level. +Inputs may still be provider-specific; refer to the step description and examples for usage details. ## Example diff --git a/docs/reference/steps/step-trigger-directory-sync.md b/docs/reference/steps/step-trigger-directory-sync.md index 361878e6..da98cdca 100644 --- a/docs/reference/steps/step-trigger-directory-sync.md +++ b/docs/reference/steps/step-trigger-directory-sync.md @@ -40,8 +40,8 @@ Authentication: ## Inputs (With.*) -The required input keys could not be detected automatically. -Please refer to the step description and examples for usage details. +This step has no required ``With.*`` keys at step schema level. +Inputs may still be provider-specific; refer to the step description and examples for usage details. ## Example diff --git a/examples/workflows/templates/directorysync-entraconnect-trigger-sync.psd1 b/examples/workflows/templates/directorysync-entraconnect-trigger-sync.psd1 index 4b9b3780..e4efab9d 100644 --- a/examples/workflows/templates/directorysync-entraconnect-trigger-sync.psd1 +++ b/examples/workflows/templates/directorysync-entraconnect-trigger-sync.psd1 @@ -12,6 +12,9 @@ # Auth session is provided by the host (credential). AuthSessionName = 'EntraConnect' + AuthSessionOptions = @{ + Role = 'EntraConnectAdmin' + } ComputerName = '{{Request.Intent.ComputerName}}' # Delta or Initial diff --git a/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 b/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 index d01e7108..ca59148d 100644 --- a/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 +++ b/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 @@ -64,16 +64,7 @@ function Invoke-IdleStepTriggerDirectorySync { throw "TriggerDirectorySync requires 'With' to be a hashtable." } - # Keep lightweight validation here so workflow configuration errors point to With.* keys. - if (-not $with.ContainsKey('PolicyType')) { - throw "TriggerDirectorySync requires With.PolicyType." - } - - $policyType = [string]$with.PolicyType - if ($policyType -notin @('Delta', 'Initial')) { - throw "TriggerDirectorySync: With.PolicyType must be 'Delta' or 'Initial'. Got: $policyType" - } - + $policyType = if ($with.ContainsKey('PolicyType')) { $with.PolicyType } else { $null } $computerName = if ($with.ContainsKey('ComputerName')) { $with.ComputerName } else { $null } # Optional inputs with defaults diff --git a/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 b/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 index 013c6567..93c4e697 100644 --- a/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 +++ b/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 @@ -123,7 +123,7 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { { & $handler -Context $script:Context -Step $step } | Should -Throw } - It 'allows missing With.AuthSessionName when provider supports default auth session routing' { + It 'does not enforce With.AuthSessionName at step level' { $step = $script:StepTemplate $step.With.Remove('AuthSessionName') @@ -144,6 +144,36 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { $result.Status | Should -Be 'Completed' } + It 'passes With.PolicyType = Initial through to the provider path' { + $step = $script:StepTemplate + $step.With.PolicyType = 'Initial' + + $handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync' + $result = & $handler -Context $script:Context -Step $step + + $result.Status | Should -Be 'Completed' + } + + It 'passes invalid With.PolicyType through to provider validation' { + $step = $script:StepTemplate + $step.With.PolicyType = 'Invalid' + $script:MockProvider | Add-Member -MemberType ScriptMethod -Name StartSyncCycle -Value { + param( + [Parameter(Mandatory)] + [ValidateSet('Delta', 'Initial')] + [string] $PolicyType, + [Parameter(Mandatory)] + [string] $ComputerName, + [Parameter(Mandatory)] + [object] $AuthSession + ) + return [pscustomobject]@{ Started = $true } + } -Force + + $handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync' + { & $handler -Context $script:Context -Step $step } | Should -Throw -ErrorId * -ExpectedMessage '*PolicyType*' + } + It 'does not enforce With.ComputerName at step level' { $step = $script:StepTemplate $step.With.Remove('ComputerName') diff --git a/tools/Generate-IdleStepReference.ps1 b/tools/Generate-IdleStepReference.ps1 index 4f289260..3a1b1b56 100644 --- a/tools/Generate-IdleStepReference.ps1 +++ b/tools/Generate-IdleStepReference.ps1 @@ -608,8 +608,8 @@ function New-IdleStepDetailPageContent { [void]$sb.AppendLine() if ($Model.RequiredWithKeys.Count -eq 0) { - [void]$sb.AppendLine('The required input keys could not be detected automatically.') - [void]$sb.AppendLine('Please refer to the step description and examples for usage details.') + [void]$sb.AppendLine('This step has no required ``With.*`` keys at step schema level.') + [void]$sb.AppendLine('Inputs may still be provider-specific; refer to the step description and examples for usage details.') [void]$sb.AppendLine() } else { From 28c0e0491f5b7785a217b1b83577815655846b56 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 16:08:37 +0000 Subject: [PATCH 15/23] test: clarify provider validation coverage for TriggerDirectorySync Agent-Logs-Url: https://github.com/blindzero/IdentityLifecycleEngine/sessions/39c5f8d7-7f77-4665-84dd-58ac63c367f3 Co-authored-by: ntt-matthias-fleschuetz <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> --- .../Invoke-IdleStepTriggerDirectorySync.Tests.ps1 | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 b/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 index 93c4e697..6c35148f 100644 --- a/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 +++ b/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 @@ -154,7 +154,7 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { $result.Status | Should -Be 'Completed' } - It 'passes invalid With.PolicyType through to provider validation' { + It 'throws error when provider rejects invalid With.PolicyType value' { $step = $script:StepTemplate $step.With.PolicyType = 'Invalid' $script:MockProvider | Add-Member -MemberType ScriptMethod -Name StartSyncCycle -Value { @@ -171,7 +171,18 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { } -Force $handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync' - { & $handler -Context $script:Context -Step $step } | Should -Throw -ErrorId * -ExpectedMessage '*PolicyType*' + $thrown = $null + try { + $null = & $handler -Context $script:Context -Step $step + } + catch { + $thrown = $_ + } + + $thrown | Should -Not -BeNullOrEmpty + $thrown.Exception.Message | Should -Match 'Exception calling "StartSyncCycle"' + $thrown.Exception.Message | Should -Match 'Invalid' + $thrown.Exception.Message | Should -Not -Match 'TriggerDirectorySync: With.PolicyType' } It 'does not enforce With.ComputerName at step level' { From e8e304850d96dc87c358de9d9cf1d2423ee9ae21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Flesch=C3=BCtz?= <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> Date: Sat, 16 May 2026 18:10:38 +0200 Subject: [PATCH 16/23] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../templates/directorysync-entraconnect-trigger-sync.psd1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/workflows/templates/directorysync-entraconnect-trigger-sync.psd1 b/examples/workflows/templates/directorysync-entraconnect-trigger-sync.psd1 index e4efab9d..6c06cf9c 100644 --- a/examples/workflows/templates/directorysync-entraconnect-trigger-sync.psd1 +++ b/examples/workflows/templates/directorysync-entraconnect-trigger-sync.psd1 @@ -10,7 +10,7 @@ With = @{ Provider = 'DirectorySync' - # Auth session is provided by the host (credential). + # Auth session is provided by the host (credential), with an optional routing key. AuthSessionName = 'EntraConnect' AuthSessionOptions = @{ Role = 'EntraConnectAdmin' From 66158140ee5aeb038dea7bed31b4446766dd7bc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Flesch=C3=BCtz?= <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> Date: Sat, 16 May 2026 18:14:41 +0200 Subject: [PATCH 17/23] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../providers/provider-directorysync-entraconnect.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/providers/provider-directorysync-entraconnect.md b/docs/reference/providers/provider-directorysync-entraconnect.md index edb3628d..d66c7d13 100644 --- a/docs/reference/providers/provider-directorysync-entraconnect.md +++ b/docs/reference/providers/provider-directorysync-entraconnect.md @@ -81,7 +81,7 @@ To select the runtime credential for this provider, pass the AuthSession via ste ## Supported Step Types -The Directory Sync (Entra Connect) provider supports the common identity lifecycle and entitlement operations used by these step types: +The Directory Sync (Entra Connect) provider supports the directory sync step types listed below: | Step type | Typical use | Notes | | --- | --- | --- | @@ -110,6 +110,6 @@ This provider does **not** support any of the allowlisted Context Resolver capab ## Troubleshooting - **“Missing privileges or elevation”**: ensure the provided credential is elevated on the Entra Connect server. -- **“AuthSession must be a [PSCredential]”**: configure `New-IdleAuthSession -AuthSessionType Credential`. +- **“AuthSession must be a [PSCredential]”**: configure the AuthSessionBroker/host runtime to provide a credential-backed AuthSession ([PSCredential]) for this provider. - **Get-ADSyncScheduler not found**: ensure ADSync cmdlets are available on the target server. - **Timeout waiting for completion**: increase `TimeoutSeconds` or check the scheduler state on the server. From 681ccefca132b53cfdf3317d4f6f06a7a273b584 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 16:17:27 +0000 Subject: [PATCH 18/23] fix: improve TriggerDirectorySync event message readability Agent-Logs-Url: https://github.com/blindzero/IdentityLifecycleEngine/sessions/144ff7f6-0d17-4b98-ac2a-6653f07f7fc3 Co-authored-by: ntt-matthias-fleschuetz <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> --- .../Invoke-IdleStepTriggerDirectorySync.ps1 | 10 +++++++++- ...oke-IdleStepTriggerDirectorySync.Tests.ps1 | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 b/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 index ca59148d..0b533b4b 100644 --- a/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 +++ b/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 @@ -96,7 +96,15 @@ function Invoke-IdleStepTriggerDirectorySync { try { # Trigger sync cycle - $Context.EventSink.WriteEvent('DirectorySyncTriggered', "Triggering $policyType sync cycle", $stepName, @{ + $policyTypeText = [string]$policyType + $triggerMessage = if ([string]::IsNullOrWhiteSpace($policyTypeText)) { + 'Triggering directory sync cycle' + } + else { + "Triggering $policyTypeText sync cycle" + } + + $Context.EventSink.WriteEvent('DirectorySyncTriggered', $triggerMessage, $stepName, @{ PolicyType = $policyType ComputerName = $computerName }) diff --git a/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 b/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 index 6c35148f..1aba6a51 100644 --- a/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 +++ b/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 @@ -348,6 +348,25 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { $capturedEvents.Type | Should -Contain 'DirectorySyncTriggered' } + It 'emits readable trigger message when With.PolicyType is omitted' { + $capturedEvents = [System.Collections.ArrayList]::new() + $script:Context.EventSink = [pscustomobject]@{} + $script:Context.EventSink | Add-Member -MemberType ScriptMethod -Name WriteEvent -Value { + param($Type, $Message, $StepName, $Data) + $null = $capturedEvents.Add(@{ Type = $Type; Message = $Message; StepName = $StepName; Data = $Data }) + } -Force + + $step = $script:StepTemplate + $step.With.Remove('PolicyType') + & $script:SetPermissiveStartSyncCycle + + $handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync' + $null = & $handler -Context $script:Context -Step $step + + $triggerEvent = $capturedEvents | Where-Object { $_.Type -eq 'DirectorySyncTriggered' } | Select-Object -First 1 + $triggerEvent.Message | Should -Be 'Triggering directory sync cycle' + } + It 'emits DirectorySyncCompleted event' { $capturedEvents = [System.Collections.ArrayList]::new() $script:Context.EventSink = [pscustomobject]@{} From 6c4d654db3154e173c09c372fff50a8132c0cc8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Flesch=C3=BCtz?= <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> Date: Sat, 16 May 2026 18:21:29 +0200 Subject: [PATCH 19/23] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../provider-directorysync-entraconnect.md | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/reference/providers/provider-directorysync-entraconnect.md b/docs/reference/providers/provider-directorysync-entraconnect.md index d66c7d13..d92a1f78 100644 --- a/docs/reference/providers/provider-directorysync-entraconnect.md +++ b/docs/reference/providers/provider-directorysync-entraconnect.md @@ -71,7 +71,7 @@ $providers = @{ This provider requires an AuthSession credential ([PSCredential]) and **must be elevated**. The provider creates and cleans up PSRemoting sessions internally. -This provider does not document a default integrated/run-as authentication fallback; provide the credential at runtime via the AuthSessionBroker. +There is no integrated/run-as authentication fallback; a credential-backed AuthSession must be supplied at runtime via the AuthSessionBroker. To select the runtime credential for this provider, pass the AuthSession via step configuration: - With.AuthSessionName @@ -85,7 +85,7 @@ The Directory Sync (Entra Connect) provider supports the directory sync step typ | Step type | Typical use | Notes | | --- | --- | --- | -| `IdLE.Step.TriggerDirectorySync` | Trigger Directory Sync | Initiated by PSRemote session execution, with optional wait/poll | +| `IdLE.Step.TriggerDirectorySync` | Trigger Directory Sync | Executed via a provider-managed PSRemoting session, with optional wait/poll | ## Context Resolvers @@ -93,15 +93,19 @@ This provider does **not** support any of the allowlisted Context Resolver capab ## Configuration -### Options reference +This provider does **not** expose an admin-facing provider option bag. +Configuration for triggering and monitoring sync is supplied through the +`IdLE.Step.TriggerDirectorySync` step inputs via `With.*` keys. -| Option | Type | Default | Meaning | +### Step input reference + +| Step input | Type | Default | Meaning | | --- | --- | --- | --- | -| `ComputerName` | `string` | Required | ComputerName for PSSession connection | -| `PolicyType` | `string` | Required | `Delta` or `Initial` sync policy | -| `Wait` | `bool` | `false` | Poll sync status and wait for result (or timeout) | -| `PollIntervalSeconds` | `int` | `10` | Interval in seconds to poll for sync status | -| `TimeoutSeconds` | `int` | `600` | Timeout for poll wait in seconds. Will result in `StepFailed` | +| `With.ComputerName` | `string` | Required | ComputerName for PSSession connection | +| `With.PolicyType` | `string` | Required | `Delta` or `Initial` sync policy | +| `With.Wait` | `bool` | `false` | Poll sync status and wait for result (or timeout) | +| `With.PollIntervalSeconds` | `int` | `10` | Interval in seconds to poll for sync status | +| `With.TimeoutSeconds` | `int` | `600` | Timeout for poll wait in seconds. Will result in `StepFailed` | ## Examples From c166441920aa0cfea7387a983591801bc42ac22e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Flesch=C3=BCtz?= <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> Date: Sat, 16 May 2026 18:27:01 +0200 Subject: [PATCH 20/23] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Public/New-IdleEntraConnectDirectorySyncProvider.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 b/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 index dd73958d..5a570e83 100644 --- a/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 +++ b/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 @@ -135,6 +135,7 @@ function New-IdleEntraConnectDirectorySyncProvider { [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] + [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] [string] $ComputerName, [Parameter(Mandatory)] From 8b3b57ee54f2b38209f244bad260c37ec372821d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 16:29:56 +0000 Subject: [PATCH 21/23] fix: coerce directory sync trigger inputs and restore auth session options example Agent-Logs-Url: https://github.com/blindzero/IdentityLifecycleEngine/sessions/5633de42-e667-431e-b4db-be20d1cf8f2a Co-authored-by: ntt-matthias-fleschuetz <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> --- .../ad-joiner-entraconnect-entraid.psd1 | 3 ++ .../Invoke-IdleStepTriggerDirectorySync.ps1 | 4 +-- ...oke-IdleStepTriggerDirectorySync.Tests.ps1 | 35 +++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/examples/workflows/templates/ad-joiner-entraconnect-entraid.psd1 b/examples/workflows/templates/ad-joiner-entraconnect-entraid.psd1 index 66e8d52a..5a9c9c4f 100644 --- a/examples/workflows/templates/ad-joiner-entraconnect-entraid.psd1 +++ b/examples/workflows/templates/ad-joiner-entraconnect-entraid.psd1 @@ -27,6 +27,9 @@ With = @{ Provider = 'DirectorySync' AuthSessionName = 'EntraConnect' + AuthSessionOptions = @{ + Role = 'EntraConnectAdmin' + } ComputerName = '{{Request.Intent.EntraConnectServer}}' PolicyType = 'Delta' diff --git a/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 b/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 index 0b533b4b..cac31134 100644 --- a/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 +++ b/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 @@ -64,8 +64,8 @@ function Invoke-IdleStepTriggerDirectorySync { throw "TriggerDirectorySync requires 'With' to be a hashtable." } - $policyType = if ($with.ContainsKey('PolicyType')) { $with.PolicyType } else { $null } - $computerName = if ($with.ContainsKey('ComputerName')) { $with.ComputerName } else { $null } + $policyType = if ($with.ContainsKey('PolicyType')) { [string]$with.PolicyType } else { $null } + $computerName = if ($with.ContainsKey('ComputerName')) { [string]$with.ComputerName } else { $null } # Optional inputs with defaults $providerAlias = if ($with.ContainsKey('Provider')) { [string]$with.Provider } else { 'DirectorySync' } diff --git a/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 b/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 index 1aba6a51..be8e4c70 100644 --- a/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 +++ b/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 @@ -12,6 +12,7 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { PSTypeName = 'Mock.DirectorySyncProvider' Name = 'MockDirectorySyncProvider' PollCount = 0 + LastPolicyType = $null LastComputerName = $null } @@ -31,6 +32,7 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { [object] $AuthSession ) + $this.LastPolicyType = $PolicyType $this.LastComputerName = $ComputerName return [pscustomobject]@{ @@ -207,6 +209,19 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { $result.Status | Should -Be 'Completed' } + It 'coerces non-string PolicyType and ComputerName to strings before provider invocation' { + $step = $script:StepTemplate + $step.With.PolicyType = 123 + $step.With.ComputerName = 456 + + $handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync' + $result = & $handler -Context $script:Context -Step $step + + $result.Status | Should -Be 'Completed' + $script:MockProvider.LastPolicyType | Should -Be '123' + $script:MockProvider.LastComputerName | Should -Be '456' + } + It 'uses default provider alias when not specified' { $step = $script:StepTemplate $step.With.Remove('Provider') @@ -367,6 +382,26 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { $triggerEvent.Message | Should -Be 'Triggering directory sync cycle' } + It 'emits string values in DirectorySyncTriggered event data for PolicyType and ComputerName' { + $capturedEvents = [System.Collections.ArrayList]::new() + $script:Context.EventSink = [pscustomobject]@{} + $script:Context.EventSink | Add-Member -MemberType ScriptMethod -Name WriteEvent -Value { + param($Type, $Message, $StepName, $Data) + $null = $capturedEvents.Add(@{ Type = $Type; Message = $Message; StepName = $StepName; Data = $Data }) + } -Force + + $step = $script:StepTemplate + $step.With.PolicyType = 123 + $step.With.ComputerName = 456 + + $handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync' + $null = & $handler -Context $script:Context -Step $step + + $triggerEvent = $capturedEvents | Where-Object { $_.Type -eq 'DirectorySyncTriggered' } | Select-Object -First 1 + $triggerEvent.Data.PolicyType | Should -Be '123' + $triggerEvent.Data.ComputerName | Should -Be '456' + } + It 'emits DirectorySyncCompleted event' { $capturedEvents = [System.Collections.ArrayList]::new() $script:Context.EventSink = [pscustomobject]@{} From 520be14c962531d3ea1ac59237517027326d2fc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Flesch=C3=BCtz?= <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> Date: Sat, 16 May 2026 18:32:47 +0200 Subject: [PATCH 22/23] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../providers/provider-directorysync-entraconnect.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/reference/providers/provider-directorysync-entraconnect.md b/docs/reference/providers/provider-directorysync-entraconnect.md index d92a1f78..568d48f7 100644 --- a/docs/reference/providers/provider-directorysync-entraconnect.md +++ b/docs/reference/providers/provider-directorysync-entraconnect.md @@ -97,12 +97,16 @@ This provider does **not** expose an admin-facing provider option bag. Configuration for triggering and monitoring sync is supplied through the `IdLE.Step.TriggerDirectorySync` step inputs via `With.*` keys. +The generic step schema does not require any `With.*` keys at schema level for this +step type. However, this provider requires specific inputs during provider validation +and execution, as noted below. + ### Step input reference | Step input | Type | Default | Meaning | | --- | --- | --- | --- | -| `With.ComputerName` | `string` | Required | ComputerName for PSSession connection | -| `With.PolicyType` | `string` | Required | `Delta` or `Initial` sync policy | +| `With.ComputerName` | `string` | Required by provider | ComputerName for PSSession connection | +| `With.PolicyType` | `string` | Required by provider | `Delta` or `Initial` sync policy | | `With.Wait` | `bool` | `false` | Poll sync status and wait for result (or timeout) | | `With.PollIntervalSeconds` | `int` | `10` | Interval in seconds to poll for sync status | | `With.TimeoutSeconds` | `int` | `600` | Timeout for poll wait in seconds. Will result in `StepFailed` | From eefe01c168dc7f9b87385944443bb252633a5b9d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 16:34:54 +0000 Subject: [PATCH 23/23] fix: enforce computer name whitespace validation in both EntraConnect provider methods Agent-Logs-Url: https://github.com/blindzero/IdentityLifecycleEngine/sessions/82a977ca-af34-4190-81ce-0a03cc4327f8 Co-authored-by: ntt-matthias-fleschuetz <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> --- .../Public/New-IdleEntraConnectDirectorySyncProvider.ps1 | 1 + tests/Providers/EntraConnectDirectorySyncProvider.Tests.ps1 | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 b/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 index 5a570e83..68e90a6f 100644 --- a/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 +++ b/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 @@ -203,6 +203,7 @@ function New-IdleEntraConnectDirectorySyncProvider { param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] + [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] [string] $ComputerName, [Parameter(Mandatory)] diff --git a/tests/Providers/EntraConnectDirectorySyncProvider.Tests.ps1 b/tests/Providers/EntraConnectDirectorySyncProvider.Tests.ps1 index db41d374..7e83912e 100644 --- a/tests/Providers/EntraConnectDirectorySyncProvider.Tests.ps1 +++ b/tests/Providers/EntraConnectDirectorySyncProvider.Tests.ps1 @@ -125,6 +125,10 @@ Describe 'Entra Connect directory sync provider contracts' { { $script:Provider.GetSyncCycleState('', $script:MockCredential) } | Should -Throw -ErrorId * -ExpectedMessage '*ComputerName*' } + It 'GetSyncCycleState validates ComputerName whitespace' { + { $script:Provider.GetSyncCycleState(' ', $script:MockCredential) } | Should -Throw -ErrorId * -ExpectedMessage '*ComputerName*' + } + It 'GetSyncCycleState validates AuthSession is PSCredential' { $badSession = [pscustomobject]@{ Name = 'BadSession' } { $script:Provider.GetSyncCycleState($script:ComputerName, $badSession) } | Should -Throw -ErrorId * -ExpectedMessage '*PSCredential*'