Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 45 additions & 27 deletions src/IdLE.Core/Private/Assert-IdleConditionPathsResolvable.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ function Assert-IdleConditionPathsResolvable {

[Parameter()]
[AllowNull()]
[object] $WarningSink
[object] $WarningSink,

# When set, skips validation of paths used by the Exists operator.
# Exists semantics intentionally allow missing paths (returns $false if absent),
# so strict execution-time path validation should exclude those paths.
[Parameter()]
[switch] $ExcludeExistsOperatorPaths
)

function Add-IdlePathIfPresent {
Expand Down Expand Up @@ -61,13 +67,16 @@ function Assert-IdleConditionPathsResolvable {

[Parameter(Mandatory)]
[AllowEmptyCollection()]
[System.Collections.Generic.List[string]] $PathList
[System.Collections.Generic.List[string]] $PathList,

[Parameter()]
[switch] $ExcludeExistsPaths
)

if ($Node.Contains('All')) {
foreach ($child in @($Node.All)) {
if ($child -is [System.Collections.IDictionary]) {
Get-IdleConditionPaths -Node $child -PathList $PathList
Get-IdleConditionPaths -Node $child -PathList $PathList -ExcludeExistsPaths:$ExcludeExistsPaths
}
}
return
Expand All @@ -76,7 +85,7 @@ function Assert-IdleConditionPathsResolvable {
if ($Node.Contains('Any')) {
foreach ($child in @($Node.Any)) {
if ($child -is [System.Collections.IDictionary]) {
Get-IdleConditionPaths -Node $child -PathList $PathList
Get-IdleConditionPaths -Node $child -PathList $PathList -ExcludeExistsPaths:$ExcludeExistsPaths
}
}
return
Expand All @@ -85,7 +94,7 @@ function Assert-IdleConditionPathsResolvable {
if ($Node.Contains('None')) {
foreach ($child in @($Node.None)) {
if ($child -is [System.Collections.IDictionary]) {
Get-IdleConditionPaths -Node $child -PathList $PathList
Get-IdleConditionPaths -Node $child -PathList $PathList -ExcludeExistsPaths:$ExcludeExistsPaths
}
}
return
Expand All @@ -102,12 +111,17 @@ function Assert-IdleConditionPathsResolvable {
}

if ($Node.Contains('Exists')) {
$existsVal = $Node.Exists
if ($existsVal -is [string]) {
Add-IdlePathIfPresent -PathList $PathList -PathCandidate $existsVal
}
elseif ($existsVal -is [System.Collections.IDictionary]) {
Add-IdlePathIfPresent -PathList $PathList -PathCandidate $existsVal.Path
# Exists operator semantics: checking for the presence of a path is intentional.
# When -ExcludeExistsPaths is set (e.g. strict execution-time validation), skip these
# so that Exists can still return $false without causing a path-not-found error.
if (-not $ExcludeExistsPaths) {
$existsVal = $Node.Exists
if ($existsVal -is [string]) {
Add-IdlePathIfPresent -PathList $PathList -PathCandidate $existsVal
}
elseif ($existsVal -is [System.Collections.IDictionary]) {
Add-IdlePathIfPresent -PathList $PathList -PathCandidate $existsVal.Path
}
}
return
}
Expand All @@ -119,7 +133,7 @@ function Assert-IdleConditionPathsResolvable {
}

$paths = [System.Collections.Generic.List[string]]::new()
Get-IdleConditionPaths -Node $Condition -PathList $paths
Get-IdleConditionPaths -Node $Condition -PathList $paths -ExcludeExistsPaths:$ExcludeExistsOperatorPaths

$uniquePaths = @($paths | Select-Object -Unique)
if ($uniquePaths.Count -eq 0) {
Expand All @@ -138,22 +152,26 @@ function Assert-IdleConditionPathsResolvable {
}
}

if ($softMissingContextPaths.Count -gt 0 -and $null -ne $WarningSink) {
$warningItem = [ordered]@{
Code = 'PreconditionContextPathUnresolvedAtPlan'
Type = 'Warning'
Step = $StepName
Source = $Source
Paths = @($softMissingContextPaths | Select-Object -Unique)
Message = ("Workflow step '{0}' references Request.Context path(s) in {1} that are not yet available at planning time: [{2}]. Evaluation will continue and paths may be resolved at runtime." -f $StepName, $Source, ([string]::Join(', ', @($softMissingContextPaths | Select-Object -Unique))))
}
if ($softMissingContextPaths.Count -gt 0) {
$uniqueSoftPaths = @($softMissingContextPaths | Select-Object -Unique)
$warningMessage = "Workflow step '{0}' references Request.Context path(s) in {1} that are not yet available at planning time: [{2}]. Evaluation will continue and paths may be resolved at runtime." -f $StepName, $Source, ([string]::Join(', ', $uniqueSoftPaths))

# Emit a visible PowerShell warning for immediate host feedback during planning.
Write-Warning $warningMessage

if ($null -ne $WarningSink) {
$warningItem = [ordered]@{
Code = 'PreconditionContextPathUnresolvedAtPlan'
Type = 'Warning'
Step = $StepName
Source = $Source
Paths = $uniqueSoftPaths
Message = $warningMessage
}

if ($WarningSink -is [System.Collections.IList]) {
$null = $WarningSink.Add($warningItem)
}
elseif ($WarningSink -is [object[]]) {
# Fallback for fixed arrays: cannot mutate by reference safely.
# Caller should pass an IList (plan.Warnings is an ArrayList) for collection.
if ($WarningSink -is [System.Collections.IList]) {
$null = $WarningSink.Add($warningItem)
}
}
}

Expand Down
17 changes: 17 additions & 0 deletions src/IdLE.Core/Private/ConvertTo-IdlePlanExportObject.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,23 @@ function ConvertTo-IdlePlanExportObject {
$stepMap.inputs = $redactedInputs
$stepMap.expectedState = $redactedExpectedState

# Per-step planning warnings (e.g. unresolved precondition context paths).
$rawStepWarnings = Get-FirstPropertyValue -Object $step -Names @('Warnings', 'PlanningWarnings', 'StepWarnings')
$stepWarningList = @()
foreach ($sw in @($rawStepWarnings)) {
if ($null -eq $sw) { continue }

$stepWarningMap = New-OrderedMap
$stepWarningMap.code = ConvertTo-NullIfEmptyString -Value (Get-FirstPropertyValue -Object $sw -Names @('Code', 'code'))
$stepWarningMap.type = ConvertTo-NullIfEmptyString -Value (Get-FirstPropertyValue -Object $sw -Names @('Type', 'type'))
$stepWarningMap.source = ConvertTo-NullIfEmptyString -Value (Get-FirstPropertyValue -Object $sw -Names @('Source', 'source'))
$stepWarningMap.paths = Get-FirstPropertyValue -Object $sw -Names @('Paths', 'paths')
$stepWarningMap.message = ConvertTo-NullIfEmptyString -Value (Get-FirstPropertyValue -Object $sw -Names @('Message', 'message'))

$stepWarningList += $stepWarningMap
}
$stepMap.warnings = $stepWarningList

$stepList += $stepMap
}

Expand Down
29 changes: 29 additions & 0 deletions src/IdLE.Core/Private/ConvertTo-IdleWorkflowSteps.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,38 @@ function ConvertTo-IdleWorkflowSteps {
$null
}

$planWarnings = $null
$planObj = $PlanningContext.Plan
if ($null -ne $planObj) {
if ($planObj -is [System.Collections.IDictionary]) {
if ($planObj.Contains('Warnings')) { $planWarnings = $planObj['Warnings'] }
} else {
$wProp = $planObj.PSObject.Properties['Warnings']
if ($null -ne $wProp) { $planWarnings = $wProp.Value }
}
}
$planWarningsCanTrackCount = $planWarnings -is [System.Collections.IList]
$warningCountBefore = if ($planWarningsCanTrackCount) { [int]$planWarnings.Count } else { 0 }

$preconditionSettings = ConvertTo-IdleWorkflowStepPreconditionSettings -Step $s -StepName $stepName -PlanningContext $PlanningContext
$precondition = $preconditionSettings.Precondition
$onPreconditionFalse = $preconditionSettings.OnPreconditionFalse
$preconditionEvent = $preconditionSettings.PreconditionEvent
$preconditionWarnings = @()

if ($planWarningsCanTrackCount) {
$warningCountAfter = [int]$planWarnings.Count
if ($warningCountAfter -gt $warningCountBefore) {
for ($warningIndex = $warningCountBefore; $warningIndex -lt $warningCountAfter; $warningIndex++) {
$warning = $planWarnings[$warningIndex]
$warningSource = Get-IdlePropertyValue -Object $warning -Name 'Source'
$warningStep = Get-IdlePropertyValue -Object $warning -Name 'Step'
if ($warningSource -eq 'Precondition' -and $warningStep -eq $stepName) {
$preconditionWarnings += $warning
}
}
}
}

$normalizedSteps += [pscustomobject]@{
PSTypeName = 'IdLE.PlanStep'
Expand All @@ -142,6 +170,7 @@ function ConvertTo-IdleWorkflowSteps {
Precondition = $precondition
OnPreconditionFalse = $onPreconditionFalse
PreconditionEvent = $preconditionEvent
Warnings = $preconditionWarnings
With = $with
RequiresCapabilities = $requiresCaps
Status = $status
Expand Down
2 changes: 1 addition & 1 deletion src/IdLE.Core/Private/Get-IdleReadOnlyCapabilities.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ function Get-IdleCapabilityContextPath {

switch ($Capability) {
'IdLE.Entitlement.List' { return 'Identity.Entitlements' }
'IdLE.Identity.Read' { return 'Identity.Profile' }
'IdLE.Identity.Read' { return 'Identity.Profile' }
default {
throw [System.ArgumentException]::new(
"No predefined context path defined for capability '$Capability'.",
Expand Down
23 changes: 16 additions & 7 deletions src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ function Invoke-IdlePlanObject {
# Accept both IDictionary (hashtables) and PSCustomObject-shaped provider registries
if ($null -ne $planProviders) {
$isValidProvider = ($planProviders -is [System.Collections.IDictionary]) -or
($planProviders.PSObject -and $planProviders.PSObject.Properties)
($planProviders.PSObject -and $planProviders.PSObject.Properties)
if ($isValidProvider) {
$effectiveProviders = $planProviders
}
Expand Down Expand Up @@ -330,8 +330,13 @@ function Invoke-IdlePlanObject {
# Fail closed: a malformed or unexpected node type is treated as a failed precondition.
$preconditionPassed = $false
}
elseif (-not (Test-IdleCondition -Condition ([hashtable]$stepPrecondition) -Context $preconditionContext)) {
$preconditionPassed = $false
else {
# Validate that all non-Exists paths exist at execution time.
# Exists operator paths are excluded because Exists semantics intentionally allow missing paths.
Assert-IdleConditionPathsResolvable -Condition ([hashtable]$stepPrecondition) -Context $preconditionContext -StepName $stepName -Source 'Precondition' -ExcludeExistsOperatorPaths
if (-not (Test-IdleCondition -Condition ([hashtable]$stepPrecondition) -Context $preconditionContext)) {
$preconditionPassed = $false
}
}

if (-not $preconditionPassed) {
Expand All @@ -354,7 +359,7 @@ function Invoke-IdlePlanObject {
$pcEvt = Get-IdlePropertyValue -Object $step -Name 'PreconditionEvent'
if ($null -ne $pcEvt) {
$pcEvtType = [string](Get-IdlePropertyValue -Object $pcEvt -Name 'Type')
$pcEvtMsg = [string](Get-IdlePropertyValue -Object $pcEvt -Name 'Message')
$pcEvtMsg = [string](Get-IdlePropertyValue -Object $pcEvt -Name 'Message')
$pcEvtData = Get-IdlePropertyValue -Object $pcEvt -Name 'Data'
# PreconditionEvent.Data is validated as a hashtable at planning time and
# stored via Copy-IdleDataObject, so it will be a hashtable (IDictionary) here.
Expand Down Expand Up @@ -626,8 +631,12 @@ function Invoke-IdlePlanObject {
if ($ofPrecondition -isnot [System.Collections.IDictionary]) {
$ofPreconditionPassed = $false
}
elseif (-not (Test-IdleCondition -Condition ([hashtable]$ofPrecondition) -Context $preconditionContext)) {
$ofPreconditionPassed = $false
else {
# Validate that all non-Exists paths exist at execution time.
Assert-IdleConditionPathsResolvable -Condition ([hashtable]$ofPrecondition) -Context $preconditionContext -StepName $ofName -Source 'Precondition' -ExcludeExistsOperatorPaths
if (-not (Test-IdleCondition -Condition ([hashtable]$ofPrecondition) -Context $preconditionContext)) {
$ofPreconditionPassed = $false
}
}

if (-not $ofPreconditionPassed) {
Expand All @@ -650,7 +659,7 @@ function Invoke-IdlePlanObject {
$ofPcEvt = Get-IdlePropertyValue -Object $ofStep -Name 'PreconditionEvent'
if ($null -ne $ofPcEvt) {
$ofPcEvtType = [string](Get-IdlePropertyValue -Object $ofPcEvt -Name 'Type')
$ofPcEvtMsg = [string](Get-IdlePropertyValue -Object $ofPcEvt -Name 'Message')
$ofPcEvtMsg = [string](Get-IdlePropertyValue -Object $ofPcEvt -Name 'Message')
$ofPcEvtData = Get-IdlePropertyValue -Object $ofPcEvt -Name 'Data'
$ofPcEvtDataHt = if ($ofPcEvtData -is [System.Collections.IDictionary]) { [hashtable]$ofPcEvtData } else { $null }
$context.EventSink.WriteEvent($ofPcEvtType, $ofPcEvtMsg, $ofName, $ofPcEvtDataHt)
Expand Down
8 changes: 4 additions & 4 deletions src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -370,10 +370,10 @@ function New-IdleAuthSessionBroker {
# If multiple matches, this is ambiguous - fail with clear error
if ($matchingEntries.Count -gt 1) {
$matchDetails = ($matchingEntries | ForEach-Object {
$currentEntry = $_
$keyStr = ($currentEntry.Key.Keys | ForEach-Object { "$_=$($currentEntry.Key[$_])" }) -join ', '
"{ $keyStr }"
}) -join '; '
$currentEntry = $_
$keyStr = ($currentEntry.Key.Keys | ForEach-Object { "$_=$($currentEntry.Key[$_])" }) -join ', '
"{ $keyStr }"
}) -join '; '
throw "Ambiguous auth session match for Name='$Name'. Multiple entries matched: $matchDetails. Provide AuthSessionOptions to disambiguate."
}

Expand Down
4 changes: 2 additions & 2 deletions src/IdLE.Core/Public/New-IdleRequestObject.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ function New-IdleRequestObject {
# Clone hashtables to avoid external mutation after object creation
# shallow clone is sufficient as we have already validated no ScriptBlocks are present
$IdentityKeys = if ($null -eq $IdentityKeys) { @{} } else { $IdentityKeys.Clone() }
$Intent = if ($null -eq $Intent) { @{} } else { $Intent.Clone() }
$Context = if ($null -eq $Context) { @{} } else { $Context.Clone() }
$Intent = if ($null -eq $Intent) { @{} } else { $Intent.Clone() }
$Context = if ($null -eq $Context) { @{} } else { $Context.Clone() }

# Construct and return the core domain object defined in Private/IdleLifecycleRequest.ps1
return [IdleLifecycleRequest]::new(
Expand Down
Loading