From 5003c3cc51e89bb4508ff3e0ca094fd1031c8df9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 12:36:50 +0000 Subject: [PATCH 1/5] Initial plan From d1debdda7e56b764f98de46b584580a803a6a95c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 12:44:01 +0000 Subject: [PATCH 2/5] Refactor: Standardize Identity capability names to IdLE.Identity.* Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/advanced/provider-capabilities.md | 10 +- docs/usage/steps.md | 2 +- .../workflows/joiner-ensureentitlement.psd1 | 2 +- examples/workflows/joiner-with-onfailure.psd1 | 2 +- .../ConvertTo-IdleCanonicalCapability.ps1 | 80 ++++++++++++++++ .../Private/Get-IdleProviderCapabilities.ps1 | 15 ++- src/IdLE.Core/Public/New-IdlePlanObject.ps1 | 7 +- .../Public/New-IdleMockIdentityProvider.ps1 | 6 +- tests/Get-IdleProviderCapabilities.Tests.ps1 | 70 +++++++++++--- tests/New-IdlePlan.Capabilities.Tests.ps1 | 94 +++++++++++++++++-- 10 files changed, 249 insertions(+), 39 deletions(-) create mode 100644 src/IdLE.Core/Private/ConvertTo-IdleCanonicalCapability.ps1 diff --git a/docs/advanced/provider-capabilities.md b/docs/advanced/provider-capabilities.md index 3960ce2..5d809d4 100644 --- a/docs/advanced/provider-capabilities.md +++ b/docs/advanced/provider-capabilities.md @@ -28,7 +28,7 @@ Naming convention: - dot-separated segments - no whitespace - starts with a letter -- examples: `Identity.Read`, `Identity.Disable`, `IdLE.Entitlement.List` +- examples: `IdLE.Identity.Read`, `IdLE.Identity.Disable`, `IdLE.Entitlement.List` ### Entitlement capability set @@ -79,9 +79,9 @@ The method returns a string list, e.g.: ```powershell $provider | Add-Member -MemberType ScriptMethod -Name GetCapabilities -Value { return @( - 'Identity.Read' - 'Identity.Attribute.Ensure' - 'Identity.Disable' + 'IdLE.Identity.Read' + 'IdLE.Identity.Attribute.Ensure' + 'IdLE.Identity.Disable' ) } -Force ``` @@ -114,7 +114,7 @@ Example: @{ Name = 'Disable identity' Type = 'DisableIdentity' - RequiresCapabilities = @('Identity.Read', 'Identity.Disable') + RequiresCapabilities = @('IdLE.Identity.Read', 'IdLE.Identity.Disable') } ``` diff --git a/docs/usage/steps.md b/docs/usage/steps.md index afb90db..27692f4 100644 --- a/docs/usage/steps.md +++ b/docs/usage/steps.md @@ -105,7 +105,7 @@ For details on declaring OnFailureSteps, see [Workflows](workflows.md). IdLE ships with a small set of built-in steps to keep demos and tests frictionless: -- **IdLE.Step.EnsureAttribute**: converges an identity attribute to the desired value using `With.IdentityKey`, `With.Name`, and `With.Value`. Requires a provider with `EnsureAttribute` and usually the `Identity.Attribute.Ensure` capability. +- **IdLE.Step.EnsureAttribute**: converges an identity attribute to the desired value using `With.IdentityKey`, `With.Name`, and `With.Value`. Requires a provider with `EnsureAttribute` and usually the `IdLE.Identity.Attribute.Ensure` capability. - **IdLE.Step.EnsureEntitlement**: converges an entitlement assignment to `Present` or `Absent` using `With.IdentityKey`, `With.Entitlement` (Kind + Id + optional DisplayName), `With.State`, and optional `With.Provider` (default `Identity`). Requires provider methods `ListEntitlements` plus `GrantEntitlement` or `RevokeEntitlement` and typically the capabilities `IdLE.Entitlement.List` plus `IdLE.Entitlement.Grant|Revoke`. ## Related diff --git a/examples/workflows/joiner-ensureentitlement.psd1 b/examples/workflows/joiner-ensureentitlement.psd1 index 1b02c8c..14347ad 100644 --- a/examples/workflows/joiner-ensureentitlement.psd1 +++ b/examples/workflows/joiner-ensureentitlement.psd1 @@ -6,7 +6,7 @@ Name = 'Ensure Department' Type = 'IdLE.Step.EnsureAttribute' With = @{ IdentityKey = 'user1'; Name = 'Department'; Value = 'IT'; Provider = 'Identity' } - RequiresCapabilities = 'Identity.Attribute.Ensure' + RequiresCapabilities = 'IdLE.Identity.Attribute.Ensure' }, @{ Name = 'Assign demo group' diff --git a/examples/workflows/joiner-with-onfailure.psd1 b/examples/workflows/joiner-with-onfailure.psd1 index f8e0b32..660602b 100644 --- a/examples/workflows/joiner-with-onfailure.psd1 +++ b/examples/workflows/joiner-with-onfailure.psd1 @@ -18,7 +18,7 @@ Value = 'IT' Provider = 'Identity' } - RequiresCapabilities = 'Identity.Attribute.Ensure' + RequiresCapabilities = 'IdLE.Identity.Attribute.Ensure' } @{ Name = 'Assign demo group' diff --git a/src/IdLE.Core/Private/ConvertTo-IdleCanonicalCapability.ps1 b/src/IdLE.Core/Private/ConvertTo-IdleCanonicalCapability.ps1 new file mode 100644 index 0000000..cb4aa54 --- /dev/null +++ b/src/IdLE.Core/Private/ConvertTo-IdleCanonicalCapability.ps1 @@ -0,0 +1,80 @@ +Set-StrictMode -Version Latest + +function ConvertTo-IdleCanonicalCapability { + <# + .SYNOPSIS + Normalizes capability names to their canonical form and emits warnings for legacy names. + + .DESCRIPTION + This function converts legacy Identity.* capability names to their canonical IdLE.Identity.* form. + + When a legacy capability name is encountered, a warning event is emitted through the provided + event sink (if available) to inform users they should migrate to the canonical form. + + This maintains backward compatibility while encouraging migration to the canonical namespace. + + .PARAMETER Capability + The capability string to normalize. + + .PARAMETER EventSink + Optional event sink for emitting deprecation warnings. If not provided, warnings are not emitted. + + .OUTPUTS + System.String - The normalized capability name. + + .NOTES + Legacy capability names are supported until v1.0.0 for backward compatibility. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $Capability, + + [Parameter()] + [AllowNull()] + [object] $EventSink + ) + + # Mapping of legacy capability names to canonical names + $legacyToCanonical = @{ + 'Identity.Read' = 'IdLE.Identity.Read' + 'Identity.Disable' = 'IdLE.Identity.Disable' + 'Identity.Enable' = 'IdLE.Identity.Enable' + 'Identity.Create' = 'IdLE.Identity.Create' + 'Identity.Delete' = 'IdLE.Identity.Delete' + 'Identity.Move' = 'IdLE.Identity.Move' + 'Identity.List' = 'IdLE.Identity.List' + 'Identity.Attribute.Ensure' = 'IdLE.Identity.Attribute.Ensure' + 'Identity.EnsureAttribute' = 'IdLE.Identity.Attribute.Ensure' + } + + # Check if this is a legacy capability name + if ($legacyToCanonical.ContainsKey($Capability)) { + $canonicalName = $legacyToCanonical[$Capability] + + # Emit warning event if EventSink is available + if ($null -ne $EventSink) { + $eventMessage = "Legacy capability name '$Capability' is deprecated and will be removed in v1.0.0. Use '$canonicalName' instead." + + # Check if EventSink has WriteEvent method + if ($EventSink.PSObject.Methods.Name -contains 'WriteEvent') { + try { + $EventSink.WriteEvent('Warning', $eventMessage, 'CapabilityNormalization', @{ + LegacyName = $Capability + CanonicalName = $canonicalName + }) + } + catch { + # Silently continue if event emission fails + # This ensures capability normalization isn't blocked by event sink issues + } + } + } + + return $canonicalName + } + + # Return the capability as-is if it's already canonical or not a known legacy name + return $Capability +} diff --git a/src/IdLE.Core/Private/Get-IdleProviderCapabilities.ps1 b/src/IdLE.Core/Private/Get-IdleProviderCapabilities.ps1 index 5a563f0..b561918 100644 --- a/src/IdLE.Core/Private/Get-IdleProviderCapabilities.ps1 +++ b/src/IdLE.Core/Private/Get-IdleProviderCapabilities.ps1 @@ -61,13 +61,13 @@ function Get-IdleProviderCapabilities { $capabilities += 'IdLE.Entitlement.Revoke' } if ($methodNames -contains 'EnsureAttribute') { - $capabilities += 'Identity.Attribute.Ensure' + $capabilities += 'IdLE.Identity.Attribute.Ensure' } if ($methodNames -contains 'DisableIdentity') { - $capabilities += 'Identity.Disable' + $capabilities += 'IdLE.Identity.Disable' } if ($methodNames -contains 'GetIdentity') { - $capabilities += 'Identity.Read' + $capabilities += 'IdLE.Identity.Read' } $capabilitySource = 'inferred' @@ -95,8 +95,13 @@ function Get-IdleProviderCapabilities { throw "Provider capability '$s' is invalid. Expected dot-separated segments like 'Identity.Read' or 'Entitlement.Write'." } - if ($seen.Add($s)) { - $null = $normalized.Add($s) + # Normalize legacy capability names to canonical form + # Note: EventSink is not available here during provider capability discovery + # Warnings will be emitted during plan-time validation instead + $canonical = ConvertTo-IdleCanonicalCapability -Capability $s -EventSink $null + + if ($seen.Add($canonical)) { + $null = $normalized.Add($canonical) } } diff --git a/src/IdLE.Core/Public/New-IdlePlanObject.ps1 b/src/IdLE.Core/Public/New-IdlePlanObject.ps1 index 8014d4e..9e14437 100644 --- a/src/IdLE.Core/Public/New-IdlePlanObject.ps1 +++ b/src/IdLE.Core/Public/New-IdlePlanObject.ps1 @@ -169,7 +169,12 @@ function New-IdlePlanObject { ) } - $normalized += $s + # Normalize legacy capability names to canonical form + # EventSink is not available during planning, so we normalize silently here + # Warnings about legacy usage should be emitted by hosts or during execution + $canonical = ConvertTo-IdleCanonicalCapability -Capability $s -EventSink $null + + $normalized += $canonical } return @($normalized | Sort-Object -Unique) diff --git a/src/IdLE.Provider.Mock/Public/New-IdleMockIdentityProvider.ps1 b/src/IdLE.Provider.Mock/Public/New-IdleMockIdentityProvider.ps1 index 1ce6950..0c8bd0b 100644 --- a/src/IdLE.Provider.Mock/Public/New-IdleMockIdentityProvider.ps1 +++ b/src/IdLE.Provider.Mock/Public/New-IdleMockIdentityProvider.ps1 @@ -137,9 +137,9 @@ function New-IdleMockIdentityProvider { #> return @( - 'Identity.Read' - 'Identity.Attribute.Ensure' - 'Identity.Disable' + 'IdLE.Identity.Read' + 'IdLE.Identity.Attribute.Ensure' + 'IdLE.Identity.Disable' 'IdLE.Entitlement.List' 'IdLE.Entitlement.Grant' 'IdLE.Entitlement.Revoke' diff --git a/tests/Get-IdleProviderCapabilities.Tests.ps1 b/tests/Get-IdleProviderCapabilities.Tests.ps1 index d3c21b3..db5ef0d 100644 --- a/tests/Get-IdleProviderCapabilities.Tests.ps1 +++ b/tests/Get-IdleProviderCapabilities.Tests.ps1 @@ -21,19 +21,19 @@ Describe 'IdLE.Core - Get-IdleProviderCapabilities (provider capability discover $provider | Add-Member -MemberType ScriptMethod -Name GetCapabilities -Value { return @( - 'Identity.Disable' - 'Identity.Read' - 'Identity.Read' # duplicate on purpose - 'Identity.Attribute.Ensure' + 'IdLE.Identity.Disable' + 'IdLE.Identity.Read' + 'IdLE.Identity.Read' # duplicate on purpose + 'IdLE.Identity.Attribute.Ensure' ) } -Force $caps = Get-IdleProviderCapabilities -Provider $provider $caps | Should -Be @( - 'Identity.Attribute.Ensure' - 'Identity.Disable' - 'Identity.Read' + 'IdLE.Identity.Attribute.Ensure' + 'IdLE.Identity.Disable' + 'IdLE.Identity.Read' ) } @@ -81,9 +81,9 @@ Describe 'IdLE.Core - Get-IdleProviderCapabilities (provider capability discover 'IdLE.Entitlement.Grant' 'IdLE.Entitlement.List' 'IdLE.Entitlement.Revoke' - 'Identity.Attribute.Ensure' - 'Identity.Disable' - 'Identity.Read' + 'IdLE.Identity.Attribute.Ensure' + 'IdLE.Identity.Disable' + 'IdLE.Identity.Read' ) } @@ -97,12 +97,58 @@ Describe 'IdLE.Core - Get-IdleProviderCapabilities (provider capability discover # Also add explicit GetCapabilities (must win) $provider | Add-Member -MemberType ScriptMethod -Name GetCapabilities -Value { - return @('Identity.Read') + return @('IdLE.Identity.Read') } -Force $caps = Get-IdleProviderCapabilities -Provider $provider -AllowInference - $caps | Should -Be @('Identity.Read') + $caps | Should -Be @('IdLE.Identity.Read') + } + + It 'normalizes legacy capability names to canonical form' { + $provider = [pscustomobject]@{ + Name = 'LegacyProvider' + } + + $provider | Add-Member -MemberType ScriptMethod -Name GetCapabilities -Value { + return @( + 'Identity.Read' + 'Identity.Disable' + 'Identity.Attribute.Ensure' + ) + } -Force + + $caps = Get-IdleProviderCapabilities -Provider $provider + + $caps | Should -Be @( + 'IdLE.Identity.Attribute.Ensure' + 'IdLE.Identity.Disable' + 'IdLE.Identity.Read' + ) + } + + It 'normalizes mixed legacy and canonical capability names' { + $provider = [pscustomobject]@{ + Name = 'MixedProvider' + } + + $provider | Add-Member -MemberType ScriptMethod -Name GetCapabilities -Value { + return @( + 'Identity.Read' # legacy + 'IdLE.Identity.Disable' # canonical + 'Identity.Attribute.Ensure' # legacy + 'IdLE.Entitlement.Grant' # canonical + ) + } -Force + + $caps = Get-IdleProviderCapabilities -Provider $provider + + $caps | Should -Be @( + 'IdLE.Entitlement.Grant' + 'IdLE.Identity.Attribute.Ensure' + 'IdLE.Identity.Disable' + 'IdLE.Identity.Read' + ) } } } diff --git a/tests/New-IdlePlan.Capabilities.Tests.ps1 b/tests/New-IdlePlan.Capabilities.Tests.ps1 index 85b962f..69f9bb1 100644 --- a/tests/New-IdlePlan.Capabilities.Tests.ps1 +++ b/tests/New-IdlePlan.Capabilities.Tests.ps1 @@ -16,7 +16,7 @@ Describe 'New-IdlePlan - required provider capabilities' { @{ Name = 'Disable identity' Type = 'IdLE.Step.DisableIdentity' - RequiresCapabilities = @('Identity.Disable') + RequiresCapabilities = @('IdLE.Identity.Disable') } ) } @@ -29,7 +29,7 @@ Describe 'New-IdlePlan - required provider capabilities' { throw 'Expected an exception but none was thrown.' } catch { - $_.Exception.Message | Should -Match 'MissingCapabilities: Identity\.Disable' + $_.Exception.Message | Should -Match 'MissingCapabilities: IdLE\.Identity\.Disable' $_.Exception.Message | Should -Match 'AffectedSteps: Disable identity' } } @@ -45,7 +45,7 @@ Describe 'New-IdlePlan - required provider capabilities' { @{ Name = 'Disable identity' Type = 'IdLE.Step.DisableIdentity' - RequiresCapabilities = @('Identity.Disable') + RequiresCapabilities = @('IdLE.Identity.Disable') } ) } @@ -55,7 +55,7 @@ Describe 'New-IdlePlan - required provider capabilities' { $provider = [pscustomobject]@{ Name = 'IdentityProvider' } $provider | Add-Member -MemberType ScriptMethod -Name GetCapabilities -Value { - return @('Identity.Disable') + return @('IdLE.Identity.Disable') } -Force $providers = @{ @@ -66,7 +66,7 @@ Describe 'New-IdlePlan - required provider capabilities' { $plan | Should -Not -BeNullOrEmpty $plan.Steps.Count | Should -Be 1 - $plan.Steps[0].RequiresCapabilities | Should -Be @('Identity.Disable') + $plan.Steps[0].RequiresCapabilities | Should -Be @('IdLE.Identity.Disable') } It 'fails fast when an OnFailure step requires capabilities that no provider advertises' { @@ -86,7 +86,7 @@ Describe 'New-IdlePlan - required provider capabilities' { @{ Name = 'Containment' Type = 'IdLE.Step.Containment' - RequiresCapabilities = @('Identity.Disable') + RequiresCapabilities = @('IdLE.Identity.Disable') } ) } @@ -99,7 +99,7 @@ Describe 'New-IdlePlan - required provider capabilities' { throw 'Expected an exception but none was thrown.' } catch { - $_.Exception.Message | Should -Match 'MissingCapabilities: Identity\.Disable' + $_.Exception.Message | Should -Match 'MissingCapabilities: IdLE\.Identity\.Disable' $_.Exception.Message | Should -Match 'AffectedSteps: Containment' } } @@ -121,7 +121,7 @@ Describe 'New-IdlePlan - required provider capabilities' { @{ Name = 'Containment' Type = 'IdLE.Step.Containment' - RequiresCapabilities = @('Identity.Disable') + RequiresCapabilities = @('IdLE.Identity.Disable') } ) } @@ -131,7 +131,7 @@ Describe 'New-IdlePlan - required provider capabilities' { $provider = [pscustomobject]@{ Name = 'IdentityProvider' } $provider | Add-Member -MemberType ScriptMethod -Name GetCapabilities -Value { - return @('Identity.Disable') + return @('IdLE.Identity.Disable') } -Force $providers = @{ @@ -142,7 +142,7 @@ Describe 'New-IdlePlan - required provider capabilities' { $plan | Should -Not -BeNullOrEmpty $plan.OnFailureSteps.Count | Should -Be 1 - $plan.OnFailureSteps[0].RequiresCapabilities | Should -Be @('Identity.Disable') + $plan.OnFailureSteps[0].RequiresCapabilities | Should -Be @('IdLE.Identity.Disable') } It 'validates entitlement capabilities for EnsureEntitlement steps' { @@ -186,4 +186,78 @@ Describe 'New-IdlePlan - required provider capabilities' { $plan.Steps.Count | Should -Be 1 $plan.Steps[0].RequiresCapabilities | Should -Be @('IdLE.Entitlement.Grant', 'IdLE.Entitlement.List') } + + It 'accepts legacy capability names and normalizes them to canonical form' { + $wfPath = Join-Path -Path $TestDrive -ChildPath 'joiner-legacy-capabilities.psd1' + + Set-Content -Path $wfPath -Encoding UTF8 -Value @' +@{ + Name = 'Joiner - Legacy Capability Names' + LifecycleEvent = 'Joiner' + Steps = @( + @{ + Name = 'Disable identity' + Type = 'IdLE.Step.DisableIdentity' + RequiresCapabilities = @('Identity.Disable') + } + ) +} +'@ + + $req = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' + + $provider = [pscustomobject]@{ Name = 'IdentityProvider' } + $provider | Add-Member -MemberType ScriptMethod -Name GetCapabilities -Value { + return @('IdLE.Identity.Disable') + } -Force + + $providers = @{ + IdentityProvider = $provider + } + + # Legacy capability name in workflow should be accepted and normalized + $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers + + $plan | Should -Not -BeNullOrEmpty + $plan.Steps.Count | Should -Be 1 + # The capability should be normalized to canonical form in the plan + $plan.Steps[0].RequiresCapabilities | Should -Be @('IdLE.Identity.Disable') + } + + It 'accepts legacy capability names from provider and normalizes them' { + $wfPath = Join-Path -Path $TestDrive -ChildPath 'joiner-legacy-provider.psd1' + + Set-Content -Path $wfPath -Encoding UTF8 -Value @' +@{ + Name = 'Joiner - Legacy Provider Capabilities' + LifecycleEvent = 'Joiner' + Steps = @( + @{ + Name = 'Disable identity' + Type = 'IdLE.Step.DisableIdentity' + RequiresCapabilities = @('IdLE.Identity.Disable') + } + ) +} +'@ + + $req = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' + + $provider = [pscustomobject]@{ Name = 'LegacyProvider' } + $provider | Add-Member -MemberType ScriptMethod -Name GetCapabilities -Value { + # Provider advertises legacy capability name + return @('Identity.Disable') + } -Force + + $providers = @{ + IdentityProvider = $provider + } + + # Legacy capability from provider should be normalized and match the canonical requirement + $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers + + $plan | Should -Not -BeNullOrEmpty + $plan.Steps.Count | Should -Be 1 + $plan.Steps[0].RequiresCapabilities | Should -Be @('IdLE.Identity.Disable') + } } From e380046b6e814876e5b6b0cefe0a5b386eb6b50e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 12:46:12 +0000 Subject: [PATCH 3/5] Fix: Normalize provider capabilities in New-IdlePlanObject local function Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- src/IdLE.Core/Public/New-IdlePlanObject.ps1 | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/IdLE.Core/Public/New-IdlePlanObject.ps1 b/src/IdLE.Core/Public/New-IdlePlanObject.ps1 index 9e14437..057f3bd 100644 --- a/src/IdLE.Core/Public/New-IdlePlanObject.ps1 +++ b/src/IdLE.Core/Public/New-IdlePlanObject.ps1 @@ -247,7 +247,16 @@ function New-IdlePlanObject { if ($null -eq $caps) { return @() } - return @($caps | Where-Object { $null -ne $_ } | ForEach-Object { ([string]$_).Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Sort-Object -Unique) + + # Normalize and deduplicate + $normalized = @() + foreach ($c in @($caps | Where-Object { $null -ne $_ } | ForEach-Object { ([string]$_).Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })) { + # Normalize legacy capability names to canonical form + $canonical = ConvertTo-IdleCanonicalCapability -Capability $c -EventSink $null + $normalized += $canonical + } + + return @($normalized | Sort-Object -Unique) } return @() From 2c9d7156e498a2dc89e87c180c694fff149f3b3a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 12:48:34 +0000 Subject: [PATCH 4/5] docs: Clarify alternative legacy capability name support Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- src/IdLE.Core/Private/ConvertTo-IdleCanonicalCapability.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/src/IdLE.Core/Private/ConvertTo-IdleCanonicalCapability.ps1 b/src/IdLE.Core/Private/ConvertTo-IdleCanonicalCapability.ps1 index cb4aa54..fc470cb 100644 --- a/src/IdLE.Core/Private/ConvertTo-IdleCanonicalCapability.ps1 +++ b/src/IdLE.Core/Private/ConvertTo-IdleCanonicalCapability.ps1 @@ -46,6 +46,7 @@ function ConvertTo-IdleCanonicalCapability { 'Identity.Move' = 'IdLE.Identity.Move' 'Identity.List' = 'IdLE.Identity.List' 'Identity.Attribute.Ensure' = 'IdLE.Identity.Attribute.Ensure' + # Alternative legacy form (if it existed in the wild) 'Identity.EnsureAttribute' = 'IdLE.Identity.Attribute.Ensure' } From 6765325b487b5efbac70abb8e5c0e81a3b3fbb03 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 14:01:17 +0000 Subject: [PATCH 5/5] Remove legacy capability normalization - use canonical names only Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- .../ConvertTo-IdleCanonicalCapability.ps1 | 81 ------------------- .../Private/Get-IdleProviderCapabilities.ps1 | 13 +-- src/IdLE.Core/Public/New-IdlePlanObject.ps1 | 20 +---- tests/Get-IdleProviderCapabilities.Tests.ps1 | 46 ----------- tests/New-IdlePlan.Capabilities.Tests.ps1 | 74 ----------------- 5 files changed, 7 insertions(+), 227 deletions(-) delete mode 100644 src/IdLE.Core/Private/ConvertTo-IdleCanonicalCapability.ps1 diff --git a/src/IdLE.Core/Private/ConvertTo-IdleCanonicalCapability.ps1 b/src/IdLE.Core/Private/ConvertTo-IdleCanonicalCapability.ps1 deleted file mode 100644 index fc470cb..0000000 --- a/src/IdLE.Core/Private/ConvertTo-IdleCanonicalCapability.ps1 +++ /dev/null @@ -1,81 +0,0 @@ -Set-StrictMode -Version Latest - -function ConvertTo-IdleCanonicalCapability { - <# - .SYNOPSIS - Normalizes capability names to their canonical form and emits warnings for legacy names. - - .DESCRIPTION - This function converts legacy Identity.* capability names to their canonical IdLE.Identity.* form. - - When a legacy capability name is encountered, a warning event is emitted through the provided - event sink (if available) to inform users they should migrate to the canonical form. - - This maintains backward compatibility while encouraging migration to the canonical namespace. - - .PARAMETER Capability - The capability string to normalize. - - .PARAMETER EventSink - Optional event sink for emitting deprecation warnings. If not provided, warnings are not emitted. - - .OUTPUTS - System.String - The normalized capability name. - - .NOTES - Legacy capability names are supported until v1.0.0 for backward compatibility. - #> - [CmdletBinding()] - param( - [Parameter(Mandatory)] - [ValidateNotNullOrEmpty()] - [string] $Capability, - - [Parameter()] - [AllowNull()] - [object] $EventSink - ) - - # Mapping of legacy capability names to canonical names - $legacyToCanonical = @{ - 'Identity.Read' = 'IdLE.Identity.Read' - 'Identity.Disable' = 'IdLE.Identity.Disable' - 'Identity.Enable' = 'IdLE.Identity.Enable' - 'Identity.Create' = 'IdLE.Identity.Create' - 'Identity.Delete' = 'IdLE.Identity.Delete' - 'Identity.Move' = 'IdLE.Identity.Move' - 'Identity.List' = 'IdLE.Identity.List' - 'Identity.Attribute.Ensure' = 'IdLE.Identity.Attribute.Ensure' - # Alternative legacy form (if it existed in the wild) - 'Identity.EnsureAttribute' = 'IdLE.Identity.Attribute.Ensure' - } - - # Check if this is a legacy capability name - if ($legacyToCanonical.ContainsKey($Capability)) { - $canonicalName = $legacyToCanonical[$Capability] - - # Emit warning event if EventSink is available - if ($null -ne $EventSink) { - $eventMessage = "Legacy capability name '$Capability' is deprecated and will be removed in v1.0.0. Use '$canonicalName' instead." - - # Check if EventSink has WriteEvent method - if ($EventSink.PSObject.Methods.Name -contains 'WriteEvent') { - try { - $EventSink.WriteEvent('Warning', $eventMessage, 'CapabilityNormalization', @{ - LegacyName = $Capability - CanonicalName = $canonicalName - }) - } - catch { - # Silently continue if event emission fails - # This ensures capability normalization isn't blocked by event sink issues - } - } - } - - return $canonicalName - } - - # Return the capability as-is if it's already canonical or not a known legacy name - return $Capability -} diff --git a/src/IdLE.Core/Private/Get-IdleProviderCapabilities.ps1 b/src/IdLE.Core/Private/Get-IdleProviderCapabilities.ps1 index b561918..ec3e4a7 100644 --- a/src/IdLE.Core/Private/Get-IdleProviderCapabilities.ps1 +++ b/src/IdLE.Core/Private/Get-IdleProviderCapabilities.ps1 @@ -90,18 +90,13 @@ function Get-IdleProviderCapabilities { # - dot-separated segments # - no whitespace # - starts with a letter - # Example: 'Entitlement.Write', 'Identity.Attribute.Ensure' + # Example: 'IdLE.Entitlement.Write', 'IdLE.Identity.Attribute.Ensure' if ($s -notmatch '^[A-Za-z][A-Za-z0-9]*(\.[A-Za-z0-9]+)+$') { - throw "Provider capability '$s' is invalid. Expected dot-separated segments like 'Identity.Read' or 'Entitlement.Write'." + throw "Provider capability '$s' is invalid. Expected dot-separated segments like 'IdLE.Identity.Read' or 'IdLE.Entitlement.Write'." } - # Normalize legacy capability names to canonical form - # Note: EventSink is not available here during provider capability discovery - # Warnings will be emitted during plan-time validation instead - $canonical = ConvertTo-IdleCanonicalCapability -Capability $s -EventSink $null - - if ($seen.Add($canonical)) { - $null = $normalized.Add($canonical) + if ($seen.Add($s)) { + $null = $normalized.Add($s) } } diff --git a/src/IdLE.Core/Public/New-IdlePlanObject.ps1 b/src/IdLE.Core/Public/New-IdlePlanObject.ps1 index 057f3bd..1f8f7a7 100644 --- a/src/IdLE.Core/Public/New-IdlePlanObject.ps1 +++ b/src/IdLE.Core/Public/New-IdlePlanObject.ps1 @@ -164,17 +164,12 @@ function New-IdlePlanObject { # - starts with a letter if ($s -notmatch '^[A-Za-z][A-Za-z0-9]*(\.[A-Za-z0-9]+)+$') { throw [System.ArgumentException]::new( - ("Workflow step '{0}' declares invalid capability '{1}'. Expected dot-separated segments like 'Identity.Read'." -f $StepName, $s), + ("Workflow step '{0}' declares invalid capability '{1}'. Expected dot-separated segments like 'IdLE.Identity.Read'." -f $StepName, $s), 'Workflow' ) } - # Normalize legacy capability names to canonical form - # EventSink is not available during planning, so we normalize silently here - # Warnings about legacy usage should be emitted by hosts or during execution - $canonical = ConvertTo-IdleCanonicalCapability -Capability $s -EventSink $null - - $normalized += $canonical + $normalized += $s } return @($normalized | Sort-Object -Unique) @@ -247,16 +242,7 @@ function New-IdlePlanObject { if ($null -eq $caps) { return @() } - - # Normalize and deduplicate - $normalized = @() - foreach ($c in @($caps | Where-Object { $null -ne $_ } | ForEach-Object { ([string]$_).Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })) { - # Normalize legacy capability names to canonical form - $canonical = ConvertTo-IdleCanonicalCapability -Capability $c -EventSink $null - $normalized += $canonical - } - - return @($normalized | Sort-Object -Unique) + return @($caps | Where-Object { $null -ne $_ } | ForEach-Object { ([string]$_).Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Sort-Object -Unique) } return @() diff --git a/tests/Get-IdleProviderCapabilities.Tests.ps1 b/tests/Get-IdleProviderCapabilities.Tests.ps1 index db5ef0d..0f68fc0 100644 --- a/tests/Get-IdleProviderCapabilities.Tests.ps1 +++ b/tests/Get-IdleProviderCapabilities.Tests.ps1 @@ -104,51 +104,5 @@ Describe 'IdLE.Core - Get-IdleProviderCapabilities (provider capability discover $caps | Should -Be @('IdLE.Identity.Read') } - - It 'normalizes legacy capability names to canonical form' { - $provider = [pscustomobject]@{ - Name = 'LegacyProvider' - } - - $provider | Add-Member -MemberType ScriptMethod -Name GetCapabilities -Value { - return @( - 'Identity.Read' - 'Identity.Disable' - 'Identity.Attribute.Ensure' - ) - } -Force - - $caps = Get-IdleProviderCapabilities -Provider $provider - - $caps | Should -Be @( - 'IdLE.Identity.Attribute.Ensure' - 'IdLE.Identity.Disable' - 'IdLE.Identity.Read' - ) - } - - It 'normalizes mixed legacy and canonical capability names' { - $provider = [pscustomobject]@{ - Name = 'MixedProvider' - } - - $provider | Add-Member -MemberType ScriptMethod -Name GetCapabilities -Value { - return @( - 'Identity.Read' # legacy - 'IdLE.Identity.Disable' # canonical - 'Identity.Attribute.Ensure' # legacy - 'IdLE.Entitlement.Grant' # canonical - ) - } -Force - - $caps = Get-IdleProviderCapabilities -Provider $provider - - $caps | Should -Be @( - 'IdLE.Entitlement.Grant' - 'IdLE.Identity.Attribute.Ensure' - 'IdLE.Identity.Disable' - 'IdLE.Identity.Read' - ) - } } } diff --git a/tests/New-IdlePlan.Capabilities.Tests.ps1 b/tests/New-IdlePlan.Capabilities.Tests.ps1 index 69f9bb1..da48845 100644 --- a/tests/New-IdlePlan.Capabilities.Tests.ps1 +++ b/tests/New-IdlePlan.Capabilities.Tests.ps1 @@ -186,78 +186,4 @@ Describe 'New-IdlePlan - required provider capabilities' { $plan.Steps.Count | Should -Be 1 $plan.Steps[0].RequiresCapabilities | Should -Be @('IdLE.Entitlement.Grant', 'IdLE.Entitlement.List') } - - It 'accepts legacy capability names and normalizes them to canonical form' { - $wfPath = Join-Path -Path $TestDrive -ChildPath 'joiner-legacy-capabilities.psd1' - - Set-Content -Path $wfPath -Encoding UTF8 -Value @' -@{ - Name = 'Joiner - Legacy Capability Names' - LifecycleEvent = 'Joiner' - Steps = @( - @{ - Name = 'Disable identity' - Type = 'IdLE.Step.DisableIdentity' - RequiresCapabilities = @('Identity.Disable') - } - ) -} -'@ - - $req = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' - - $provider = [pscustomobject]@{ Name = 'IdentityProvider' } - $provider | Add-Member -MemberType ScriptMethod -Name GetCapabilities -Value { - return @('IdLE.Identity.Disable') - } -Force - - $providers = @{ - IdentityProvider = $provider - } - - # Legacy capability name in workflow should be accepted and normalized - $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers - - $plan | Should -Not -BeNullOrEmpty - $plan.Steps.Count | Should -Be 1 - # The capability should be normalized to canonical form in the plan - $plan.Steps[0].RequiresCapabilities | Should -Be @('IdLE.Identity.Disable') - } - - It 'accepts legacy capability names from provider and normalizes them' { - $wfPath = Join-Path -Path $TestDrive -ChildPath 'joiner-legacy-provider.psd1' - - Set-Content -Path $wfPath -Encoding UTF8 -Value @' -@{ - Name = 'Joiner - Legacy Provider Capabilities' - LifecycleEvent = 'Joiner' - Steps = @( - @{ - Name = 'Disable identity' - Type = 'IdLE.Step.DisableIdentity' - RequiresCapabilities = @('IdLE.Identity.Disable') - } - ) -} -'@ - - $req = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' - - $provider = [pscustomobject]@{ Name = 'LegacyProvider' } - $provider | Add-Member -MemberType ScriptMethod -Name GetCapabilities -Value { - # Provider advertises legacy capability name - return @('Identity.Disable') - } -Force - - $providers = @{ - IdentityProvider = $provider - } - - # Legacy capability from provider should be normalized and match the canonical requirement - $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers - - $plan | Should -Not -BeNullOrEmpty - $plan.Steps.Count | Should -Be 1 - $plan.Steps[0].RequiresCapabilities | Should -Be @('IdLE.Identity.Disable') - } }