From 151e5008aa78701807661579dfd846c60c796205 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Mar 2026 17:53:46 +0000 Subject: [PATCH 1/8] Initial plan From c27fcc6f120896d6de06c7c8d5b6f82998feea18 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Mar 2026 18:01:58 +0000 Subject: [PATCH 2/8] Remove DisplayName from generic entitlement model Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/reference/capabilities.md | 3 +- docs/reference/providers/provider-ad.md | 1 - docs/reference/providers/provider-entraID.md | 1 - docs/reference/providers/provider-mock.md | 1 - .../steps/step-ensure-entitlement.md | 2 +- .../step-prune-entitlements-ensure-keep.md | 2 +- .../steps/step-prune-entitlements.md | 2 +- docs/use/workflows/conditions.md | 9 ++--- examples/workflows/templates/ad-leaver.psd1 | 2 +- .../workflows/templates/entraid-leaver.psd1 | 2 +- .../Private/ConvertTo-IdleADEntitlement.ps1 | 18 ++------- .../Public/New-IdleADIdentityProvider.ps1 | 14 +++---- .../ConvertTo-IdleEntraIDEntitlement.ps1 | 22 +++------- .../New-IdleEntraIDIdentityProvider.ps1 | 26 +++++------- .../Public/New-IdleMockIdentityProvider.ps1 | 17 ++------ .../ConvertTo-IdlePruneEntitlement.ps1 | 13 +----- .../Test-IdlePruneEntitlementShouldKeep.ps1 | 5 +-- .../Invoke-IdleStepEnsureEntitlement.ps1 | 13 +----- .../Invoke-IdleStepPruneEntitlements.ps1 | 2 +- ...ke-IdleStepPruneEntitlementsEnsureKeep.ps1 | 2 +- tests/Core/Get-IdlePropertyValue.Tests.ps1 | 18 ++++----- tests/Core/Test-IdleCondition.Tests.ps1 | 40 +++++++++---------- .../EntitlementProvider.Contract.ps1 | 5 +-- ...Invoke-IdleStepEnsureEntitlement.Tests.ps1 | 2 +- ...Invoke-IdleStepPruneEntitlements.Tests.ps1 | 16 ++++---- 25 files changed, 87 insertions(+), 151 deletions(-) diff --git a/docs/reference/capabilities.md b/docs/reference/capabilities.md index 7301c8cc..66193a80 100644 --- a/docs/reference/capabilities.md +++ b/docs/reference/capabilities.md @@ -168,8 +168,9 @@ For `IdLE.Entitlement.List`, the engine additionally builds (list merge — all | One provider, one session | `Request.Context.Views.Providers..Sessions..Identity.Entitlements` | > **Note**: `IdLE.Entitlement.List` writes an array of entitlement objects. Each entry includes: -> `Kind` (string), `Id` (string), and optionally `DisplayName` (string), +> `Kind` (string) and `Id` (string), > plus source metadata: `SourceProvider` (string) and `SourceAuthSessionName` (string). +> Provider implementations may include additional provider-specific fields (e.g., `Mail` for Entra ID). > To reference entitlement Ids in Conditions, use the `.Id` member-access pattern. > See [Conditions - Member-Access Enumeration](../use/workflows/conditions.md#member-access-enumeration). diff --git a/docs/reference/providers/provider-ad.md b/docs/reference/providers/provider-ad.md index 801fa2dd..b7f68661 100644 --- a/docs/reference/providers/provider-ad.md +++ b/docs/reference/providers/provider-ad.md @@ -135,7 +135,6 @@ Each element represents one AD group membership: | `PSTypeName` | `string` | Always `IdLE.Entitlement`. | | `Kind` | `string` | Always `Group`. | | `Id` | `string` | AD group `DistinguishedName`. | -| `DisplayName` | `string` | AD group `Name`. | Notes: - The output paths are fixed by the engine and cannot be changed. diff --git a/docs/reference/providers/provider-entraID.md b/docs/reference/providers/provider-entraID.md index 0a0a176f..d27c71d0 100644 --- a/docs/reference/providers/provider-entraID.md +++ b/docs/reference/providers/provider-entraID.md @@ -134,7 +134,6 @@ Each element represents one Entra ID group membership: | `PSTypeName` | `string` | Always `IdLE.Entitlement`. | | `Kind` | `string` | Always `Group`. | | `Id` | `string` | Entra group `id`. | -| `DisplayName` | `string` or `$null` | Group `displayName` (if returned by the adapter). | | `Mail` | `string` or `$null` | Group `mail` (if returned by the adapter). | Notes: diff --git a/docs/reference/providers/provider-mock.md b/docs/reference/providers/provider-mock.md index 937d6eab..35f518ed 100644 --- a/docs/reference/providers/provider-mock.md +++ b/docs/reference/providers/provider-mock.md @@ -99,7 +99,6 @@ Each element is normalized via `ConvertToEntitlement`: | `PSTypeName` | `string` | Always `IdLE.Entitlement`. | | `Kind` | `string` | Required; non-empty. | | `Id` | `string` | Required; non-empty. | -| `DisplayName` | `string` or `$null` | Optional. | Notes: - The output paths are fixed by the engine and cannot be changed. diff --git a/docs/reference/steps/step-ensure-entitlement.md b/docs/reference/steps/step-ensure-entitlement.md index c714c536..e68e7d6c 100644 --- a/docs/reference/steps/step-ensure-entitlement.md +++ b/docs/reference/steps/step-ensure-entitlement.md @@ -57,7 +57,7 @@ The following keys are required in the step's ``With`` configuration: Name = 'IdLE.Step.EnsureEntitlement Example' Type = 'IdLE.Step.EnsureEntitlement' With = @{ - Entitlement = @{ Kind = 'Group'; Id = 'GroupId'; DisplayName = 'Example Group' } + Entitlement = @{ Kind = 'Group'; Id = 'GroupId' } IdentityKey = 'user.name' State = 'Present' } diff --git a/docs/reference/steps/step-prune-entitlements-ensure-keep.md b/docs/reference/steps/step-prune-entitlements-ensure-keep.md index 85b9d535..580e1421 100644 --- a/docs/reference/steps/step-prune-entitlements-ensure-keep.md +++ b/docs/reference/steps/step-prune-entitlements-ensure-keep.md @@ -70,7 +70,7 @@ Authentication: | -------------------- | -------- | ------------ | ----------- | | IdentityKey | Yes | string | Unique identity reference (e.g. sAMAccountName, UPN, or objectId). | | Kind | Yes | string | Entitlement kind to prune (provider-defined, e.g. Group, Role, License). | -| Keep | No | array | Explicit entitlement objects to retain AND ensure are present. Each entry must have an Id property; Kind and DisplayName are optional. **These entries are GRANTED if missing after the prune.** If omitted, all entitlements of the given Kind are removed and no grants are made. | +| Keep | No | array | Explicit entitlement objects to retain AND ensure are present. Each entry must have an Id property; Kind is optional. **These entries are GRANTED if missing after the prune.** If omitted, all entitlements of the given Kind are removed and no grants are made. | | Provider | No | string | Provider alias from Context.Providers (default: Identity). | | AuthSessionName | No | string | Name of the auth session to acquire via Context.AcquireAuthSession. | | AuthSessionOptions | No | hashtable | Options passed to AcquireAuthSession for session selection (e.g. role-scoped sessions). | diff --git a/docs/reference/steps/step-prune-entitlements.md b/docs/reference/steps/step-prune-entitlements.md index 54921853..75cc145b 100644 --- a/docs/reference/steps/step-prune-entitlements.md +++ b/docs/reference/steps/step-prune-entitlements.md @@ -66,7 +66,7 @@ Authentication: | -------------------- | -------- | ------------ | ----------- | | IdentityKey | Yes | string | Unique identity reference (e.g. sAMAccountName, UPN, or objectId). | | Kind | Yes | string | Entitlement kind to prune (provider-defined, e.g. Group, Role, License). | -| Keep | No | array | Explicit entitlement objects to retain (kept, never removed). Each entry must have an Id property; Kind and DisplayName are optional. These entries are NOT granted — use PruneEntitlementsEnsureKeep for that. | +| Keep | No | array | Explicit entitlement objects to retain (kept, never removed). Each entry must have an Id property; Kind is optional. These entries are NOT granted — use PruneEntitlementsEnsureKeep for that. | | KeepPattern | No | string array | Wildcard strings (PowerShell -like semantics). Current entitlements whose Id matches any pattern are kept. Patterns are NEVER granted. | | Provider | No | string | Provider alias from Context.Providers (default: Identity). | | AuthSessionName | No | string | Name of the auth session to acquire via Context.AcquireAuthSession. | diff --git a/docs/use/workflows/conditions.md b/docs/use/workflows/conditions.md index b5d31b46..bc21e7c4 100644 --- a/docs/use/workflows/conditions.md +++ b/docs/use/workflows/conditions.md @@ -128,7 +128,7 @@ This section is the authoritative DSL reference. } ``` -> **Note**: When `Request.Context.Views.Identity.Entitlements` contains objects (e.g., `@{ Kind = 'Group'; Id = '...'; DisplayName = '...' }`), use `.Id` or `.DisplayName` to extract the property values: `Entitlements.Id` returns an array of all Id values. +> **Note**: When `Request.Context.Views.Identity.Entitlements` contains objects (e.g., `@{ Kind = 'Group'; Id = '...' }`), use `.Id` to extract Id values: `Entitlements.Id` returns an array of all Id values. #### NotContains @@ -174,7 +174,7 @@ This section is the authoritative DSL reference. } ``` -> **Note**: When checking entitlement Ids or DisplayNames, use `.Id` or `.DisplayName` to extract property values from entitlement objects. The path `Entitlements.Id` uses member-access enumeration to return an array of all Id values. +> **Note**: When checking entitlement Ids, use `.Id` to extract property values from entitlement objects. The path `Entitlements.Id` uses member-access enumeration to return an array of all Id values. #### NotLike @@ -218,7 +218,6 @@ When a `Path` points to a list of objects, you can access properties of those ob - `Request.Context.Views.Identity.Entitlements` → returns array of entitlement objects - `Request.Context.Views.Identity.Entitlements.Id` → returns array of all `Id` values -- `Request.Context.Views.Identity.Entitlements.DisplayName` → returns array of all `DisplayName` values > **Note**: These paths reference the **global View** populated by a `ContextResolvers` entry with `IdLE.Entitlement.List`. See [Context Resolvers](./context-resolver.md) for details. > For provider-specific entitlements, use the scoped path: `Request.Context.Providers...Identity.Entitlements.Id` (where `` is the auth session key; `Default` is used when no `With.AuthSessionName` is specified). @@ -226,8 +225,8 @@ When a `Path` points to a list of objects, you can access properties of those ob **Example**: ```powershell # Entitlements contains: @( -# @{ Kind = 'Group'; Id = 'CN=Users,...'; DisplayName = 'Users' } -# @{ Kind = 'Group'; Id = 'CN=Admins,...'; DisplayName = 'Admins' } +# @{ Kind = 'Group'; Id = 'CN=Users,...' } +# @{ Kind = 'Group'; Id = 'CN=Admins,...' } # ) # Check if any entitlement Id matches a pattern diff --git a/examples/workflows/templates/ad-leaver.psd1 b/examples/workflows/templates/ad-leaver.psd1 index f4e2ef45..5ac54d4c 100644 --- a/examples/workflows/templates/ad-leaver.psd1 +++ b/examples/workflows/templates/ad-leaver.psd1 @@ -41,7 +41,7 @@ # Explicitly retain this group and ensure it is present after pruning. Keep = @( - @{ Kind = 'Group'; Id = '{{Request.Intent.LeaverRetainGroupDn}}'; DisplayName = 'Leaver Retain' } + @{ Kind = 'Group'; Id = '{{Request.Intent.LeaverRetainGroupDn}}' } ) # Pattern-based retention is not supported by PruneEntitlementsEnsureKeep. Use a # separate IdLE.Step.PruneEntitlements step earlier in the workflow if you need to diff --git a/examples/workflows/templates/entraid-leaver.psd1 b/examples/workflows/templates/entraid-leaver.psd1 index e0e2caac..c60da599 100644 --- a/examples/workflows/templates/entraid-leaver.psd1 +++ b/examples/workflows/templates/entraid-leaver.psd1 @@ -91,7 +91,7 @@ # Retain this specific leaver group and ensure it is present. Keep = @( - @{ Kind = 'Group'; Id = '{{Request.Intent.LeaverRetainGroupId}}'; DisplayName = 'Leaver Retain' } + @{ Kind = 'Group'; Id = '{{Request.Intent.LeaverRetainGroupId}}' } ) # Pattern-based retention is not supported by PruneEntitlementsEnsureKeep. Use a # separate IdLE.Step.PruneEntitlements step earlier if you must protect wildcard diff --git a/src/IdLE.Provider.AD/Private/ConvertTo-IdleADEntitlement.ps1 b/src/IdLE.Provider.AD/Private/ConvertTo-IdleADEntitlement.ps1 index 1c09644c..e47aa639 100644 --- a/src/IdLE.Provider.AD/Private/ConvertTo-IdleADEntitlement.ps1 +++ b/src/IdLE.Provider.AD/Private/ConvertTo-IdleADEntitlement.ps1 @@ -13,14 +13,13 @@ function ConvertTo-IdleADEntitlement { .PARAMETER Value The input value to convert. Can be a hashtable or PSCustomObject with - Kind, Id, and optionally DisplayName properties. + Kind and Id properties. .OUTPUTS PSCustomObject with PSTypeName 'IdLE.Entitlement' - PSTypeName: 'IdLE.Entitlement' - Kind: Entitlement kind (e.g., 'Group') - Id: Entitlement identifier (e.g., Group DN) - - DisplayName: Optional display name (null if not provided or empty) .EXAMPLE $ent = ConvertTo-IdleADEntitlement -Value @{ Kind = 'Group'; Id = 'CN=MyGroup,OU=Groups,DC=contoso,DC=com' } @@ -34,18 +33,15 @@ function ConvertTo-IdleADEntitlement { $kind = $null $id = $null - $displayName = $null if ($Value -is [System.Collections.IDictionary]) { $kind = $Value['Kind'] $id = $Value['Id'] - if ($Value.Contains('DisplayName')) { $displayName = $Value['DisplayName'] } } else { $props = $Value.PSObject.Properties if ($props.Name -contains 'Kind') { $kind = $Value.Kind } if ($props.Name -contains 'Id') { $id = $Value.Id } - if ($props.Name -contains 'DisplayName') { $displayName = $Value.DisplayName } } if ([string]::IsNullOrWhiteSpace([string]$kind)) { @@ -56,14 +52,8 @@ function ConvertTo-IdleADEntitlement { } return [pscustomobject]@{ - PSTypeName = 'IdLE.Entitlement' - Kind = [string]$kind - Id = [string]$id - DisplayName = if ($null -eq $displayName -or [string]::IsNullOrWhiteSpace([string]$displayName)) { - $null - } - else { - [string]$displayName - } + PSTypeName = 'IdLE.Entitlement' + Kind = [string]$kind + Id = [string]$id } } diff --git a/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 b/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 index 9f229322..6dbf6b07 100644 --- a/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 +++ b/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 @@ -782,10 +782,9 @@ function New-IdleADIdentityProvider { continue } $result += [pscustomobject]@{ - PSTypeName = 'IdLE.Entitlement' - Kind = 'Group' - Id = $group.DistinguishedName - DisplayName = $group.Name + PSTypeName = 'IdLE.Entitlement' + Kind = 'Group' + Id = $group.DistinguishedName } } @@ -880,10 +879,9 @@ function New-IdleADIdentityProvider { if ([string]::Equals($converted.Kind, 'Group', [System.StringComparison]::OrdinalIgnoreCase)) { $canonicalId = $this.ResolveGroup($converted.Id, $AuthSession) return [pscustomobject]@{ - PSTypeName = 'IdLE.Entitlement' - Kind = $converted.Kind - Id = $canonicalId - DisplayName = $converted.PSObject.Properties.Name -contains 'DisplayName' ? $converted.DisplayName : $null + PSTypeName = 'IdLE.Entitlement' + Kind = $converted.Kind + Id = $canonicalId } } diff --git a/src/IdLE.Provider.EntraID/Private/ConvertTo-IdleEntraIDEntitlement.ps1 b/src/IdLE.Provider.EntraID/Private/ConvertTo-IdleEntraIDEntitlement.ps1 index dffe314a..05653cd4 100644 --- a/src/IdLE.Provider.EntraID/Private/ConvertTo-IdleEntraIDEntitlement.ps1 +++ b/src/IdLE.Provider.EntraID/Private/ConvertTo-IdleEntraIDEntitlement.ps1 @@ -10,18 +10,17 @@ function ConvertTo-IdleEntraIDEntitlement { (hashtable, PSCustomObject) into a standard IdLE.Entitlement object. The function validates that required fields (Kind, Id) are present and not empty. - Supports optional fields: DisplayName, Mail. + Supports optional field: Mail. .PARAMETER Value The input value to convert. Can be a hashtable or PSCustomObject with - Kind, Id, and optionally DisplayName and Mail properties. + Kind, Id, and optionally Mail properties. .OUTPUTS PSCustomObject with PSTypeName 'IdLE.Entitlement' - PSTypeName: 'IdLE.Entitlement' - Kind: Entitlement kind (e.g., 'Group') - Id: Entitlement identifier (e.g., Group objectId) - - DisplayName: Optional display name (null if not provided or empty) - Mail: Optional mail address (null if not provided or empty) .EXAMPLE @@ -36,20 +35,17 @@ function ConvertTo-IdleEntraIDEntitlement { $kind = $null $id = $null - $displayName = $null $mail = $null if ($Value -is [System.Collections.IDictionary]) { $kind = $Value['Kind'] $id = $Value['Id'] - if ($Value.Contains('DisplayName')) { $displayName = $Value['DisplayName'] } if ($Value.Contains('Mail')) { $mail = $Value['Mail'] } } else { $props = $Value.PSObject.Properties if ($props.Name -contains 'Kind') { $kind = $Value.Kind } if ($props.Name -contains 'Id') { $id = $Value.Id } - if ($props.Name -contains 'DisplayName') { $displayName = $Value.DisplayName } if ($props.Name -contains 'Mail') { $mail = $Value.Mail } } @@ -61,16 +57,10 @@ function ConvertTo-IdleEntraIDEntitlement { } return [pscustomobject]@{ - PSTypeName = 'IdLE.Entitlement' - Kind = [string]$kind - Id = [string]$id - DisplayName = if ($null -eq $displayName -or [string]::IsNullOrWhiteSpace([string]$displayName)) { - $null - } - else { - [string]$displayName - } - Mail = if ($null -eq $mail -or [string]::IsNullOrWhiteSpace([string]$mail)) { + PSTypeName = 'IdLE.Entitlement' + Kind = [string]$kind + Id = [string]$id + Mail = if ($null -eq $mail -or [string]::IsNullOrWhiteSpace([string]$mail)) { $null } else { diff --git a/src/IdLE.Provider.EntraID/Public/New-IdleEntraIDIdentityProvider.ps1 b/src/IdLE.Provider.EntraID/Public/New-IdleEntraIDIdentityProvider.ps1 index 6558a27d..60c376aa 100644 --- a/src/IdLE.Provider.EntraID/Public/New-IdleEntraIDIdentityProvider.ps1 +++ b/src/IdLE.Provider.EntraID/Public/New-IdleEntraIDIdentityProvider.ps1 @@ -814,25 +814,18 @@ function New-IdleEntraIDIdentityProvider { } else { $group.id } - - $displayName = if ($group -is [System.Collections.IDictionary]) { - if ($group.ContainsKey('displayName')) { $group['displayName'] } else { $null } - } else { - if ($group.PSObject.Properties.Name -contains 'displayName') { $group.displayName } else { $null } - } - + $mail = if ($group -is [System.Collections.IDictionary]) { if ($group.ContainsKey('mail')) { $group['mail'] } else { $null } } else { if ($group.PSObject.Properties.Name -contains 'mail') { $group.mail } else { $null } } - + $result += [pscustomobject]@{ - PSTypeName = 'IdLE.Entitlement' - Kind = 'Group' - Id = $groupId - DisplayName = $displayName - Mail = $mail + PSTypeName = 'IdLE.Entitlement' + Kind = 'Group' + Id = $groupId + Mail = $mail } } @@ -1078,10 +1071,9 @@ function New-IdleEntraIDIdentityProvider { if ([string]::Equals($converted.Kind, 'Group', [System.StringComparison]::OrdinalIgnoreCase)) { $canonicalId = $this.ResolveGroup($converted.Id, $AuthSession) return [pscustomobject]@{ - PSTypeName = 'IdLE.Entitlement' - Kind = $converted.Kind - Id = $canonicalId - DisplayName = $converted.PSObject.Properties.Name -contains 'DisplayName' ? $converted.DisplayName : $null + PSTypeName = 'IdLE.Entitlement' + Kind = $converted.Kind + Id = $canonicalId } } diff --git a/src/IdLE.Provider.Mock/Public/New-IdleMockIdentityProvider.ps1 b/src/IdLE.Provider.Mock/Public/New-IdleMockIdentityProvider.ps1 index cfac762a..bf69d47e 100644 --- a/src/IdLE.Provider.Mock/Public/New-IdleMockIdentityProvider.ps1 +++ b/src/IdLE.Provider.Mock/Public/New-IdleMockIdentityProvider.ps1 @@ -29,7 +29,7 @@ function New-IdleMockIdentityProvider { Department = 'IT' } Entitlements = @( - @{ Kind = 'Group'; Id = 'demo-group'; DisplayName = 'Demo Group' } + @{ Kind = 'Group'; Id = 'demo-group' } ) } } @@ -58,18 +58,15 @@ function New-IdleMockIdentityProvider { $kind = $null $id = $null - $displayName = $null if ($Value -is [System.Collections.IDictionary]) { $kind = $Value['Kind'] $id = $Value['Id'] - if ($Value.Contains('DisplayName')) { $displayName = $Value['DisplayName'] } } else { $props = $Value.PSObject.Properties if ($props.Name -contains 'Kind') { $kind = $Value.Kind } if ($props.Name -contains 'Id') { $id = $Value.Id } - if ($props.Name -contains 'DisplayName') { $displayName = $Value.DisplayName } } if ([string]::IsNullOrWhiteSpace([string]$kind)) { @@ -80,15 +77,9 @@ function New-IdleMockIdentityProvider { } return [pscustomobject]@{ - PSTypeName = 'IdLE.Entitlement' - Kind = [string]$kind - Id = [string]$id - DisplayName = if ($null -eq $displayName -or [string]::IsNullOrWhiteSpace([string]$displayName)) { - $null - } - else { - [string]$displayName - } + PSTypeName = 'IdLE.Entitlement' + Kind = [string]$kind + Id = [string]$id } } diff --git a/src/IdLE.Steps.Common/Private/ConvertTo-IdlePruneEntitlement.ps1 b/src/IdLE.Steps.Common/Private/ConvertTo-IdlePruneEntitlement.ps1 index 3f08c20b..87e4be86 100644 --- a/src/IdLE.Steps.Common/Private/ConvertTo-IdlePruneEntitlement.ps1 +++ b/src/IdLE.Steps.Common/Private/ConvertTo-IdlePruneEntitlement.ps1 @@ -1,5 +1,5 @@ function ConvertTo-IdlePruneEntitlement { - # Converts a raw hashtable or object Keep entry into a normalized pscustomobject with Kind, Id and optional DisplayName. + # Converts a raw hashtable or object Keep entry into a normalized pscustomobject with Kind and Id. [CmdletBinding()] param( [Parameter(Mandatory)] @@ -13,18 +13,15 @@ function ConvertTo-IdlePruneEntitlement { $kind = $null $id = $null - $displayName = $null if ($Value -is [System.Collections.IDictionary]) { if ($Value.Contains('Kind')) { $kind = $Value['Kind'] } if ($Value.Contains('Id')) { $id = $Value['Id'] } - if ($Value.Contains('DisplayName')) { $displayName = $Value['DisplayName'] } } else { $props = $Value.PSObject.Properties if ($props.Name -contains 'Kind') { $kind = $Value.Kind } if ($props.Name -contains 'Id') { $id = $Value.Id } - if ($props.Name -contains 'DisplayName') { $displayName = $Value.DisplayName } } if ([string]::IsNullOrWhiteSpace([string]$kind)) { @@ -35,14 +32,8 @@ function ConvertTo-IdlePruneEntitlement { throw "PruneEntitlements: each Keep entry requires an Id." } - $normalized = [ordered]@{ + return [pscustomobject]@{ Kind = [string]$kind Id = [string]$id } - - if ($null -ne $displayName -and -not [string]::IsNullOrWhiteSpace([string]$displayName)) { - $normalized['DisplayName'] = [string]$displayName - } - - return [pscustomobject]$normalized } diff --git a/src/IdLE.Steps.Common/Private/Test-IdlePruneEntitlementShouldKeep.ps1 b/src/IdLE.Steps.Common/Private/Test-IdlePruneEntitlementShouldKeep.ps1 index 07e64162..97f355af 100644 --- a/src/IdLE.Steps.Common/Private/Test-IdlePruneEntitlementShouldKeep.ps1 +++ b/src/IdLE.Steps.Common/Private/Test-IdlePruneEntitlementShouldKeep.ps1 @@ -25,12 +25,9 @@ function Test-IdlePruneEntitlementShouldKeep { } } - # Check KeepPattern (wildcard -like against Id and DisplayName) + # Check KeepPattern (wildcard -like against Id) foreach ($pattern in $KeepPatterns) { if ([string]$Ent.Id -like $pattern) { return $true } - if ($Ent.PSObject.Properties.Name -contains 'DisplayName' -and - $null -ne $Ent.DisplayName -and - [string]$Ent.DisplayName -like $pattern) { return $true } } return $false diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 index 99a69cc0..545ae8f8 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEnsureEntitlement.ps1 @@ -35,7 +35,7 @@ function Invoke-IdleStepEnsureEntitlement { Type = 'IdLE.Step.EnsureEntitlement' With = @{ IdentityKey = 'user1' - Entitlement = @{ Kind = 'Group'; Id = 'example-group'; DisplayName = 'Example Group' } + Entitlement = @{ Kind = 'Group'; Id = 'example-group' } State = 'Present' Provider = 'Identity' } @@ -65,18 +65,15 @@ function Invoke-IdleStepEnsureEntitlement { $kind = $null $id = $null - $displayName = $null if ($Value -is [System.Collections.IDictionary]) { $kind = $Value['Kind'] $id = $Value['Id'] - if ($Value.Contains('DisplayName')) { $displayName = $Value['DisplayName'] } } else { $props = $Value.PSObject.Properties if ($props.Name -contains 'Kind') { $kind = $Value.Kind } if ($props.Name -contains 'Id') { $id = $Value.Id } - if ($props.Name -contains 'DisplayName') { $displayName = $Value.DisplayName } } if ([string]::IsNullOrWhiteSpace([string]$kind)) { @@ -86,16 +83,10 @@ function Invoke-IdleStepEnsureEntitlement { throw "EnsureEntitlement requires Entitlement.Id." } - $normalized = [ordered]@{ + return [pscustomobject]@{ Kind = [string]$kind Id = [string]$id } - - if ($null -ne $displayName -and -not [string]::IsNullOrWhiteSpace([string]$displayName)) { - $normalized['DisplayName'] = [string]$displayName - } - - return [pscustomobject]$normalized } function Test-IdleStepEntitlementEquals { diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepPruneEntitlements.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepPruneEntitlements.ps1 index 70cbb3a0..8bd6931e 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepPruneEntitlements.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepPruneEntitlements.ps1 @@ -46,7 +46,7 @@ function Invoke-IdleStepPruneEntitlements { | -------------------- | -------- | ------------ | ----------- | | IdentityKey | Yes | string | Unique identity reference (e.g. sAMAccountName, UPN, or objectId). | | Kind | Yes | string | Entitlement kind to prune (provider-defined, e.g. Group, Role, License). | - | Keep | No | array | Explicit entitlement objects to retain (kept, never removed). Each entry must have an Id property; Kind and DisplayName are optional. These entries are NOT granted — use PruneEntitlementsEnsureKeep for that. | + | Keep | No | array | Explicit entitlement objects to retain (kept, never removed). Each entry must have an Id property; Kind is optional. These entries are NOT granted — use PruneEntitlementsEnsureKeep for that. | | KeepPattern | No | string array | Wildcard strings (PowerShell -like semantics). Current entitlements whose Id matches any pattern are kept. Patterns are NEVER granted. | | Provider | No | string | Provider alias from Context.Providers (default: Identity). | | AuthSessionName | No | string | Name of the auth session to acquire via Context.AcquireAuthSession. | diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepPruneEntitlementsEnsureKeep.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepPruneEntitlementsEnsureKeep.ps1 index 09959bbd..22c35281 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepPruneEntitlementsEnsureKeep.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepPruneEntitlementsEnsureKeep.ps1 @@ -50,7 +50,7 @@ function Invoke-IdleStepPruneEntitlementsEnsureKeep { | -------------------- | -------- | ------------ | ----------- | | IdentityKey | Yes | string | Unique identity reference (e.g. sAMAccountName, UPN, or objectId). | | Kind | Yes | string | Entitlement kind to prune (provider-defined, e.g. Group, Role, License). | - | Keep | No | array | Explicit entitlement objects to retain AND ensure are present. Each entry must have an Id property; Kind and DisplayName are optional. **These entries are GRANTED if missing after the prune.** If omitted, all entitlements of the given Kind are removed and no grants are made. | + | Keep | No | array | Explicit entitlement objects to retain AND ensure are present. Each entry must have an Id property; Kind is optional. **These entries are GRANTED if missing after the prune.** If omitted, all entitlements of the given Kind are removed and no grants are made. | | Provider | No | string | Provider alias from Context.Providers (default: Identity). | | AuthSessionName | No | string | Name of the auth session to acquire via Context.AcquireAuthSession. | | AuthSessionOptions | No | hashtable | Options passed to AcquireAuthSession for session selection (e.g. role-scoped sessions). | diff --git a/tests/Core/Get-IdlePropertyValue.Tests.ps1 b/tests/Core/Get-IdlePropertyValue.Tests.ps1 index a8c7510b..2d506dba 100644 --- a/tests/Core/Get-IdlePropertyValue.Tests.ps1 +++ b/tests/Core/Get-IdlePropertyValue.Tests.ps1 @@ -43,9 +43,9 @@ Describe 'Get-IdlePropertyValue' { Context 'Member-access enumeration (array property access)' { It 'extracts property from all array items' { $list = @( - [pscustomobject]@{ Kind = 'Group'; Id = 'g1'; DisplayName = 'Group 1' } - [pscustomobject]@{ Kind = 'Group'; Id = 'g2'; DisplayName = 'Group 2' } - [pscustomobject]@{ Kind = 'Group'; Id = 'g3'; DisplayName = 'Group 3' } + [pscustomobject]@{ Kind = 'Group'; Id = 'g1' } + [pscustomobject]@{ Kind = 'Group'; Id = 'g2' } + [pscustomobject]@{ Kind = 'Group'; Id = 'g3' } ) $result = Get-IdlePropertyValue -Object $list -Name 'Id' @@ -57,18 +57,18 @@ Describe 'Get-IdlePropertyValue' { $result[2] | Should -Be 'g3' } - It 'extracts DisplayName from entitlement objects' { + It 'extracts Kind from entitlement objects' { $list = @( - [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,DC=example,DC=com'; DisplayName = 'Users' } - [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Admins,DC=example,DC=com'; DisplayName = 'Admins' } + [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,DC=example,DC=com' } + [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Admins,DC=example,DC=com' } ) - $result = Get-IdlePropertyValue -Object $list -Name 'DisplayName' + $result = Get-IdlePropertyValue -Object $list -Name 'Kind' $result | Should -Not -BeNullOrEmpty $result.Count | Should -Be 2 - $result[0] | Should -Be 'Users' - $result[1] | Should -Be 'Admins' + $result[0] | Should -Be 'Group' + $result[1] | Should -Be 'Group' } It 'returns null when array items do not have the property' { diff --git a/tests/Core/Test-IdleCondition.Tests.ps1 b/tests/Core/Test-IdleCondition.Tests.ps1 index 1aaa1a07..f0ffa25e 100644 --- a/tests/Core/Test-IdleCondition.Tests.ps1 +++ b/tests/Core/Test-IdleCondition.Tests.ps1 @@ -379,9 +379,9 @@ Describe 'Condition DSL (schema + evaluator)' { Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Entitlements = @( - [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com'; DisplayName = 'Users' } - [pscustomobject]@{ Kind = 'Group'; Id = 'CN=BreakGlass-Users,OU=Groups,DC=example,DC=com'; DisplayName = 'BreakGlass Users' } - [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Admins,OU=Groups,DC=example,DC=com'; DisplayName = 'Admins' } + [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com' } + [pscustomobject]@{ Kind = 'Group'; Id = 'CN=BreakGlass-Users,OU=Groups,DC=example,DC=com' } + [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Admins,OU=Groups,DC=example,DC=com' } ) } } @@ -406,8 +406,8 @@ Describe 'Condition DSL (schema + evaluator)' { Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Entitlements = @( - [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com'; DisplayName = 'Users' } - [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Admins,OU=Groups,DC=example,DC=com'; DisplayName = 'Admins' } + [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com' } + [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Admins,OU=Groups,DC=example,DC=com' } ) } } @@ -549,8 +549,8 @@ Describe 'Condition DSL (schema + evaluator)' { Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Entitlements = @( - [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com'; DisplayName = 'Users' } - [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Admins,OU=Groups,DC=example,DC=com'; DisplayName = 'Admins' } + [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com' } + [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Admins,OU=Groups,DC=example,DC=com' } ) } } @@ -575,8 +575,8 @@ Describe 'Condition DSL (schema + evaluator)' { Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Entitlements = @( - [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com'; DisplayName = 'Users' } - [pscustomobject]@{ Kind = 'Group'; Id = 'CN=BreakGlass-Users,OU=Groups,DC=example,DC=com'; DisplayName = 'BreakGlass Users' } + [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com' } + [pscustomobject]@{ Kind = 'Group'; Id = 'CN=BreakGlass-Users,OU=Groups,DC=example,DC=com' } ) } } @@ -651,9 +651,9 @@ Describe 'Condition DSL (schema + evaluator)' { Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Entitlements = @( - [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com'; DisplayName = 'Users' } - [pscustomobject]@{ Kind = 'Group'; Id = 'CN=HR-Employees,OU=Groups,DC=example,DC=com'; DisplayName = 'HR Employees' } - [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Admins,OU=Groups,DC=example,DC=com'; DisplayName = 'Admins' } + [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com' } + [pscustomobject]@{ Kind = 'Group'; Id = 'CN=HR-Employees,OU=Groups,DC=example,DC=com' } + [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Admins,OU=Groups,DC=example,DC=com' } ) } } @@ -678,8 +678,8 @@ Describe 'Condition DSL (schema + evaluator)' { Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Entitlements = @( - [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com'; DisplayName = 'Users' } - [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Admins,OU=Groups,DC=example,DC=com'; DisplayName = 'Admins' } + [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com' } + [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Admins,OU=Groups,DC=example,DC=com' } ) } } @@ -754,8 +754,8 @@ Describe 'Condition DSL (schema + evaluator)' { Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Entitlements = @( - [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com'; DisplayName = 'Users' } - [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Admins,OU=Groups,DC=example,DC=com'; DisplayName = 'Admins' } + [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com' } + [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Admins,OU=Groups,DC=example,DC=com' } ) } } @@ -780,8 +780,8 @@ Describe 'Condition DSL (schema + evaluator)' { Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Entitlements = @( - [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com'; DisplayName = 'Users' } - [pscustomobject]@{ Kind = 'Group'; Id = 'CN=HR-Employees,OU=Groups,DC=example,DC=com'; DisplayName = 'HR Employees' } + [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,OU=Groups,DC=example,DC=com' } + [pscustomobject]@{ Kind = 'Group'; Id = 'CN=HR-Employees,OU=Groups,DC=example,DC=com' } ) } } @@ -806,8 +806,8 @@ Describe 'Condition DSL (schema + evaluator)' { Views = [pscustomobject]@{ Identity = [pscustomobject]@{ Entitlements = @( - [pscustomobject]@{ Kind = 'Group'; Id = 'CN=admins,OU=Groups,DC=example,DC=com'; DisplayName = 'Admins' } - [pscustomobject]@{ Kind = 'Group'; Id = 'CN=users,OU=Groups,DC=example,DC=com'; DisplayName = 'Users' } + [pscustomobject]@{ Kind = 'Group'; Id = 'CN=admins,OU=Groups,DC=example,DC=com' } + [pscustomobject]@{ Kind = 'Group'; Id = 'CN=users,OU=Groups,DC=example,DC=com' } ) } } diff --git a/tests/ProviderContracts/EntitlementProvider.Contract.ps1 b/tests/ProviderContracts/EntitlementProvider.Contract.ps1 index 13a53aa9..fa5bf107 100644 --- a/tests/ProviderContracts/EntitlementProvider.Contract.ps1 +++ b/tests/ProviderContracts/EntitlementProvider.Contract.ps1 @@ -43,9 +43,8 @@ function Invoke-IdleEntitlementProviderContractTests { [void]$script:Provider.GetIdentity($id) $entitlement = [pscustomobject]@{ - Kind = 'Contract' - Id = "entitlement-$([guid]::NewGuid().ToString('N'))" - DisplayName = 'Contract Entitlement' + Kind = 'Contract' + Id = "entitlement-$([guid]::NewGuid().ToString('N'))" } $result = $script:Provider.GrantEntitlement($id, $entitlement) diff --git a/tests/Steps/Invoke-IdleStepEnsureEntitlement.Tests.ps1 b/tests/Steps/Invoke-IdleStepEnsureEntitlement.Tests.ps1 index be1bf574..5385c714 100644 --- a/tests/Steps/Invoke-IdleStepEnsureEntitlement.Tests.ps1 +++ b/tests/Steps/Invoke-IdleStepEnsureEntitlement.Tests.ps1 @@ -20,7 +20,7 @@ Describe 'Invoke-IdleStepEnsureEntitlement (built-in step)' { Type = 'IdLE.Step.EnsureEntitlement' With = @{ IdentityKey = 'user1' - Entitlement = @{ Kind = 'Group'; Id = 'demo-group'; DisplayName = 'Demo Group' } + Entitlement = @{ Kind = 'Group'; Id = 'demo-group' } State = 'Present' Provider = 'Identity' } diff --git a/tests/Steps/Invoke-IdleStepPruneEntitlements.Tests.ps1 b/tests/Steps/Invoke-IdleStepPruneEntitlements.Tests.ps1 index 850778df..270570d2 100644 --- a/tests/Steps/Invoke-IdleStepPruneEntitlements.Tests.ps1 +++ b/tests/Steps/Invoke-IdleStepPruneEntitlements.Tests.ps1 @@ -18,9 +18,9 @@ Describe 'Invoke-IdleStepPruneEntitlements (built-in step)' { # Seed the identity with some entitlements $null = $script:Provider.EnsureAttribute('user1', 'Seed', 'Value') $null = $script:Provider.GrantEntitlement('user1', @{ Kind = 'Group'; Id = 'CN=G-All,DC=contoso,DC=com' }) - $null = $script:Provider.GrantEntitlement('user1', @{ Kind = 'Group'; Id = 'CN=G-HR,DC=contoso,DC=com'; DisplayName = 'HR Group' }) - $null = $script:Provider.GrantEntitlement('user1', @{ Kind = 'Group'; Id = 'CN=LEAVER-RETAIN,DC=contoso,DC=com'; DisplayName = 'Leaver Retain' }) - $null = $script:Provider.GrantEntitlement('user1', @{ Kind = 'Group'; Id = 'CN=LEAVER-EXTRA,DC=contoso,DC=com'; DisplayName = 'Leaver Extra' }) + $null = $script:Provider.GrantEntitlement('user1', @{ Kind = 'Group'; Id = 'CN=G-HR,DC=contoso,DC=com' }) + $null = $script:Provider.GrantEntitlement('user1', @{ Kind = 'Group'; Id = 'CN=LEAVER-RETAIN,DC=contoso,DC=com' }) + $null = $script:Provider.GrantEntitlement('user1', @{ Kind = 'Group'; Id = 'CN=LEAVER-EXTRA,DC=contoso,DC=com' }) $script:StepTemplate = [pscustomobject]@{ Name = 'Prune group memberships' @@ -185,7 +185,7 @@ Describe 'Invoke-IdleStepPruneEntitlements (built-in step)' { # Add a Keep item that does NOT exist in current entitlements $step.With.Keep = @( @{ Kind = 'Group'; Id = 'CN=LEAVER-RETAIN,DC=contoso,DC=com' } - @{ Kind = 'Group'; Id = 'CN=LEAVER-NEW,DC=contoso,DC=com'; DisplayName = 'Leaver New' } + @{ Kind = 'Group'; Id = 'CN=LEAVER-NEW,DC=contoso,DC=com' } ) $step.With.EnsureKeepEntitlements = $true @@ -535,9 +535,9 @@ Describe 'Invoke-IdleStepPruneEntitlementsEnsureKeep (built-in step)' { # Seed the identity with some entitlements $null = $script:Provider.EnsureAttribute('user1', 'Seed', 'Value') $null = $script:Provider.GrantEntitlement('user1', @{ Kind = 'Group'; Id = 'CN=G-All,DC=contoso,DC=com' }) - $null = $script:Provider.GrantEntitlement('user1', @{ Kind = 'Group'; Id = 'CN=G-HR,DC=contoso,DC=com'; DisplayName = 'HR Group' }) - $null = $script:Provider.GrantEntitlement('user1', @{ Kind = 'Group'; Id = 'CN=LEAVER-RETAIN,DC=contoso,DC=com'; DisplayName = 'Leaver Retain' }) - $null = $script:Provider.GrantEntitlement('user1', @{ Kind = 'Group'; Id = 'CN=LEAVER-EXTRA,DC=contoso,DC=com'; DisplayName = 'Leaver Extra' }) + $null = $script:Provider.GrantEntitlement('user1', @{ Kind = 'Group'; Id = 'CN=G-HR,DC=contoso,DC=com' }) + $null = $script:Provider.GrantEntitlement('user1', @{ Kind = 'Group'; Id = 'CN=LEAVER-RETAIN,DC=contoso,DC=com' }) + $null = $script:Provider.GrantEntitlement('user1', @{ Kind = 'Group'; Id = 'CN=LEAVER-EXTRA,DC=contoso,DC=com' }) $script:Handler = 'IdLE.Steps.Common\Invoke-IdleStepPruneEntitlementsEnsureKeep' $script:StepTemplate = [pscustomobject]@{ @@ -587,7 +587,7 @@ Describe 'Invoke-IdleStepPruneEntitlementsEnsureKeep (built-in step)' { $step = $script:StepTemplate $step.With.Keep = @( @{ Kind = 'Group'; Id = 'CN=LEAVER-RETAIN,DC=contoso,DC=com' } - @{ Kind = 'Group'; Id = 'CN=LEAVER-NEW,DC=contoso,DC=com'; DisplayName = 'Leaver New' } + @{ Kind = 'Group'; Id = 'CN=LEAVER-NEW,DC=contoso,DC=com' } ) $result = & $script:Handler -Context $script:Context -Step $step From 4c0645912f06b96f0ce66fa5c69ff17b5cd8a132 Mon Sep 17 00:00:00 2001 From: Matthias <13959569+blindzero@users.noreply.github.com> Date: Sun, 15 Mar 2026 19:29:40 +0100 Subject: [PATCH 3/8] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docs/reference/capabilities.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/capabilities.md b/docs/reference/capabilities.md index 66193a80..b9c7e426 100644 --- a/docs/reference/capabilities.md +++ b/docs/reference/capabilities.md @@ -168,8 +168,8 @@ For `IdLE.Entitlement.List`, the engine additionally builds (list merge — all | One provider, one session | `Request.Context.Views.Providers..Sessions..Identity.Entitlements` | > **Note**: `IdLE.Entitlement.List` writes an array of entitlement objects. Each entry includes: -> `Kind` (string) and `Id` (string), -> plus source metadata: `SourceProvider` (string) and `SourceAuthSessionName` (string). +> `Kind` (string) and `Id` (string). +> It also includes source metadata: `SourceProvider` (string) and `SourceAuthSessionName` (string). > Provider implementations may include additional provider-specific fields (e.g., `Mail` for Entra ID). > To reference entitlement Ids in Conditions, use the `.Id` member-access pattern. > See [Conditions - Member-Access Enumeration](../use/workflows/conditions.md#member-access-enumeration). From 5a842e53a93929584f6500bfff6661a8912a17c1 Mon Sep 17 00:00:00 2001 From: Matthias <13959569+blindzero@users.noreply.github.com> Date: Sun, 15 Mar 2026 19:30:00 +0100 Subject: [PATCH 4/8] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tests/Core/Get-IdlePropertyValue.Tests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Core/Get-IdlePropertyValue.Tests.ps1 b/tests/Core/Get-IdlePropertyValue.Tests.ps1 index 2d506dba..c0c02920 100644 --- a/tests/Core/Get-IdlePropertyValue.Tests.ps1 +++ b/tests/Core/Get-IdlePropertyValue.Tests.ps1 @@ -60,7 +60,7 @@ Describe 'Get-IdlePropertyValue' { It 'extracts Kind from entitlement objects' { $list = @( [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Users,DC=example,DC=com' } - [pscustomobject]@{ Kind = 'Group'; Id = 'CN=Admins,DC=example,DC=com' } + [pscustomobject]@{ Kind = 'Role'; Id = 'CN=Admins,DC=example,DC=com' } ) $result = Get-IdlePropertyValue -Object $list -Name 'Kind' @@ -68,7 +68,7 @@ Describe 'Get-IdlePropertyValue' { $result | Should -Not -BeNullOrEmpty $result.Count | Should -Be 2 $result[0] | Should -Be 'Group' - $result[1] | Should -Be 'Group' + $result[1] | Should -Be 'Role' } It 'returns null when array items do not have the property' { From d9ad81f716b441c214267f4baec3fd3b8f8beefe Mon Sep 17 00:00:00 2001 From: Matthias Fleschuetz <13959569+blindzero@users.noreply.github.com> Date: Sun, 15 Mar 2026 19:35:18 +0100 Subject: [PATCH 5/8] docs: generated new step docu --- docs/reference/steps/step-ensure-entitlement.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/steps/step-ensure-entitlement.md b/docs/reference/steps/step-ensure-entitlement.md index e68e7d6c..c714c536 100644 --- a/docs/reference/steps/step-ensure-entitlement.md +++ b/docs/reference/steps/step-ensure-entitlement.md @@ -57,7 +57,7 @@ The following keys are required in the step's ``With`` configuration: Name = 'IdLE.Step.EnsureEntitlement Example' Type = 'IdLE.Step.EnsureEntitlement' With = @{ - Entitlement = @{ Kind = 'Group'; Id = 'GroupId' } + Entitlement = @{ Kind = 'Group'; Id = 'GroupId'; DisplayName = 'Example Group' } IdentityKey = 'user.name' State = 'Present' } From 397198bfc73e963cb990207e0906d694fada650a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Mar 2026 18:48:04 +0000 Subject: [PATCH 6/8] Fix PSSA warnings in changed files and document patterns in CONTRIBUTING.md Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- CONTRIBUTING.md | 12 ++++++++++++ .../Public/New-IdleADIdentityProvider.ps1 | 6 +++--- .../Public/New-IdleEntraIDIdentityProvider.ps1 | 3 +-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1344755c..88e1ecef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -174,6 +174,18 @@ The runner pins tool versions for deterministic CI results; update pins intentio > Note: `artifacts/` is a build output folder and should not be committed. +### PSSA guidance: avoiding common warnings + +PRs must not introduce new PSSA warnings. Common patterns and how to handle them: + +| Rule | Recommended fix | +|---|---| +| `PSUseDeclaredVarsMoreThanAssignments` | Replace `$result = Verb-Noun ...` with `$null = Verb-Noun ...` when the return value is intentionally discarded. | +| `PSReviewUnusedParameter` | For **intentionally unused** contract/interface parameters, add `$null = $ParamName` at the top of the function body. You may also apply `[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'ParamName', Justification = '...')]` on the parameter declaration, but note that PSSA may not honor this attribute inside `ScriptMethod` scriptblocks — use `$null = $ParamName` as the reliable fix. | +| `PSAvoidUsingEmptyCatchBlock` | Add at minimum a `Write-Verbose` or `Write-Debug` statement inside the `catch` block. | + +If you encounter a warning that cannot be fixed without changing observable behavior, raise it in the PR for discussion before suppressing. + --- ## Generated cmdlet reference (platyPS) diff --git a/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 b/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 index 6dbf6b07..6e0b72a7 100644 --- a/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 +++ b/src/IdLE.Provider.AD/Public/New-IdleADIdentityProvider.ps1 @@ -102,6 +102,7 @@ function New-IdleADIdentityProvider { AuthSessionBroker = $authSessionBroker } #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', 'PasswordGenerationSpecialCharSet', Justification = 'This parameter specifies allowed special characters for password generation, not a password value.')] [CmdletBinding()] param( [Parameter()] @@ -579,7 +580,7 @@ function New-IdleADIdentityProvider { ) # Validate attribute against contract (strict mode - will throw on unsupported attributes) - $validationResult = Test-IdleADAttributeContract -Operation 'EnsureAttributes' -AttributeName $Name + $null = Test-IdleADAttributeContract -Operation 'EnsureAttributes' -AttributeName $Name $adapter = $this.GetEffectiveAdapter($AuthSession) @@ -874,8 +875,7 @@ function New-IdleADIdentityProvider { ) $converted = $this.ConvertToEntitlement($Entitlement) - - # AD only supports Group entitlements; normalize to canonical DN + $null = $Kind # Contract parameter — reserved for future multi-Kind dispatch if ([string]::Equals($converted.Kind, 'Group', [System.StringComparison]::OrdinalIgnoreCase)) { $canonicalId = $this.ResolveGroup($converted.Id, $AuthSession) return [pscustomobject]@{ diff --git a/src/IdLE.Provider.EntraID/Public/New-IdleEntraIDIdentityProvider.ps1 b/src/IdLE.Provider.EntraID/Public/New-IdleEntraIDIdentityProvider.ps1 index 60c376aa..b333c684 100644 --- a/src/IdLE.Provider.EntraID/Public/New-IdleEntraIDIdentityProvider.ps1 +++ b/src/IdLE.Provider.EntraID/Public/New-IdleEntraIDIdentityProvider.ps1 @@ -1066,8 +1066,7 @@ function New-IdleEntraIDIdentityProvider { ) $converted = $this.ConvertToEntitlement($Entitlement) - - # Entra ID only supports Group entitlements; normalize to canonical objectId + $null = $Kind # Contract parameter — reserved for future multi-Kind dispatch if ([string]::Equals($converted.Kind, 'Group', [System.StringComparison]::OrdinalIgnoreCase)) { $canonicalId = $this.ResolveGroup($converted.Id, $AuthSession) return [pscustomobject]@{ From 7b62407732432dba9e1f6a0d546d299b1929cc50 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Mar 2026 18:49:29 +0000 Subject: [PATCH 7/8] Fix typo in CONTRIBUTING.md PSSA guidance Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 88e1ecef..eeea478a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -182,7 +182,7 @@ PRs must not introduce new PSSA warnings. Common patterns and how to handle them |---|---| | `PSUseDeclaredVarsMoreThanAssignments` | Replace `$result = Verb-Noun ...` with `$null = Verb-Noun ...` when the return value is intentionally discarded. | | `PSReviewUnusedParameter` | For **intentionally unused** contract/interface parameters, add `$null = $ParamName` at the top of the function body. You may also apply `[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'ParamName', Justification = '...')]` on the parameter declaration, but note that PSSA may not honor this attribute inside `ScriptMethod` scriptblocks — use `$null = $ParamName` as the reliable fix. | -| `PSAvoidUsingEmptyCatchBlock` | Add at minimum a `Write-Verbose` or `Write-Debug` statement inside the `catch` block. | +| `PSAvoidUsingEmptyCatchBlock` | Add at a minimum a `Write-Verbose` or `Write-Debug` statement inside the `catch` block. | If you encounter a warning that cannot be fixed without changing observable behavior, raise it in the PR for discussion before suppressing. From 89ac85b7ae82d199e1a23e95d47b3f0a2e83fd4b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Mar 2026 20:21:36 +0000 Subject: [PATCH 8/8] Fix all PSSA warnings (0 remaining), remove DisplayName from step reference generator Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- .../steps/step-ensure-entitlement.md | 2 +- .../Private/Resolve-IdleTemplateString.ps1 | 1 + .../Private/New-IdleADAdapter.ps1 | 35 ++++++++++--------- tools/Generate-IdleStepReference.ps1 | 5 +-- tools/Invoke-IdleScriptAnalyzer.ps1 | 1 + 5 files changed, 24 insertions(+), 20 deletions(-) diff --git a/docs/reference/steps/step-ensure-entitlement.md b/docs/reference/steps/step-ensure-entitlement.md index c714c536..e68e7d6c 100644 --- a/docs/reference/steps/step-ensure-entitlement.md +++ b/docs/reference/steps/step-ensure-entitlement.md @@ -57,7 +57,7 @@ The following keys are required in the step's ``With`` configuration: Name = 'IdLE.Step.EnsureEntitlement Example' Type = 'IdLE.Step.EnsureEntitlement' With = @{ - Entitlement = @{ Kind = 'Group'; Id = 'GroupId'; DisplayName = 'Example Group' } + Entitlement = @{ Kind = 'Group'; Id = 'GroupId' } IdentityKey = 'user.name' State = 'Present' } diff --git a/src/IdLE.Core/Private/Resolve-IdleTemplateString.ps1 b/src/IdLE.Core/Private/Resolve-IdleTemplateString.ps1 index 0090276e..625ef88c 100644 --- a/src/IdLE.Core/Private/Resolve-IdleTemplateString.ps1 +++ b/src/IdLE.Core/Private/Resolve-IdleTemplateString.ps1 @@ -39,6 +39,7 @@ function Resolve-IdleTemplateString { For pure placeholders (single placeholder with no surrounding text), returns the resolved value with its original type preserved (string, bool, int, datetime, guid, etc.). For mixed strings (string interpolation with multiple placeholders or surrounding text), returns a string with placeholders replaced. #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Request', Justification = 'Used inside nested scriptblock $resolvePath; PSSA does not track parameter usage across nested scriptblock boundaries.')] [CmdletBinding()] param( [Parameter()] diff --git a/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 b/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 index 3c3a3ec1..d40878c0 100644 --- a/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 +++ b/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 @@ -27,14 +27,15 @@ function New-IdleADAdapter { .PARAMETER PasswordGenerationSpecialCharSet Set of special characters to use in generated passwords. Default is '!@#$%&*+-_=?'. - + .NOTES PSScriptAnalyzer suppression: This function intentionally uses ConvertTo-SecureString -AsPlainText as an explicit escape hatch for AccountPasswordAsPlainText. This is a documented design decision with automatic redaction via Copy-IdleRedactedObject. #> - [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'Intentional escape hatch for AccountPasswordAsPlainText with explicit opt-in and automatic redaction')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', 'PasswordGenerationSpecialCharSet', Justification = 'This parameter specifies the set of allowed special characters for password generation — it is not a password value. Renaming or changing the type would break the API contract.')] + [CmdletBinding()] param( [Parameter()] [AllowNull()] @@ -296,7 +297,7 @@ function New-IdleADAdapter { # 1. Derive SamAccountName from IdentityKey if missing $hasSamAccountName = $effectiveAttributes.ContainsKey('SamAccountName') -and -not [string]::IsNullOrWhiteSpace($effectiveAttributes['SamAccountName']) - + if (-not $hasSamAccountName) { if ($isSamAccountNameLike) { $effectiveAttributes['SamAccountName'] = $IdentityKey @@ -312,7 +313,7 @@ function New-IdleADAdapter { # 2. Auto-set UserPrincipalName when IdentityKey is a UPN $hasUpn = $effectiveAttributes.ContainsKey('UserPrincipalName') -and -not [string]::IsNullOrWhiteSpace($effectiveAttributes['UserPrincipalName']) - + if (-not $hasUpn -and $isUpn) { $effectiveAttributes['UserPrincipalName'] = $IdentityKey Write-Verbose "AD Provider: Derived UserPrincipalName='$IdentityKey' from IdentityKey (UPN format)" @@ -321,7 +322,7 @@ function New-IdleADAdapter { # 3. Derive CN/RDN Name with priority: Name > DisplayName > GivenName+Surname > IdentityKey $derivedName = $null $hasExplicitName = $effectiveAttributes.ContainsKey('Name') -and -not [string]::IsNullOrWhiteSpace($effectiveAttributes['Name']) - + if ($hasExplicitName) { $derivedName = $effectiveAttributes['Name'] Write-Verbose "AD Provider: Using explicit Name='$derivedName' for CN/RDN" @@ -450,7 +451,7 @@ function New-IdleADAdapter { # Mode 4: Auto-generate password when enabled and no password provided # Generate a policy-compliant password Write-Verbose "AD Provider: No password provided for enabled account. Generating policy-compliant password..." - + $passwordGenParams = @{ FallbackMinLength = $this.PasswordGenerationFallbackMinLength FallbackRequireUpper = $this.PasswordGenerationRequireUpper @@ -459,14 +460,14 @@ function New-IdleADAdapter { FallbackRequireSpecial = $this.PasswordGenerationRequireSpecial FallbackSpecialCharSet = $this.PasswordGenerationSpecialCharSet } - + if ($null -ne $this.Credential) { $passwordGenParams['Credential'] = $this.Credential } - + $generatedPasswordInfo = New-IdleADPassword @passwordGenParams $params['AccountPassword'] = $generatedPasswordInfo.SecureString - + Write-Verbose "AD Provider: Generated password using $($generatedPasswordInfo.UsedPolicy) policy (MinLength=$($generatedPasswordInfo.MinLength))" } @@ -495,13 +496,13 @@ function New-IdleADAdapter { } $user = New-ADUser @params -PassThru - + # Return user with optional password generation info if ($null -ne $generatedPasswordInfo) { # Attach password generation info to user object for caller to access $user | Add-Member -MemberType NoteProperty -Name '_GeneratedPasswordInfo' -Value $generatedPasswordInfo -Force } - + return $user } -Force @@ -524,6 +525,8 @@ function New-IdleADAdapter { [object] $CurrentValue ) + $null = $CurrentValue # Contract parameter; reserved for future change-detection optimization + $params = @{ Identity = $Identity ErrorAction = 'Stop' @@ -690,9 +693,7 @@ function New-IdleADAdapter { try { # For large groups, short-circuit after finding the first match - $existingMember = Get-ADGroupMember @getMembersParams | - Where-Object { $_.DistinguishedName -eq $MemberIdentity } | - Select-Object -First 1 + $existingMember = Get-ADGroupMember @getMembersParams | Where-Object { $_.DistinguishedName -eq $MemberIdentity } | Select-Object -First 1 return ($null -ne $existingMember) } catch { @@ -715,7 +716,7 @@ function New-IdleADAdapter { # Check if already a member (idempotency + reliable change detection) $isMember = $this.TestGroupMembership($GroupIdentity, $MemberIdentity) - + if ($isMember -eq $true) { # Already a member - no change needed return $false @@ -748,7 +749,7 @@ function New-IdleADAdapter { # Check if actually a member (idempotency + reliable change detection) $isMember = $this.TestGroupMembership($GroupIdentity, $MemberIdentity) - + if ($isMember -eq $false) { # Not a member - no change needed return $false @@ -831,7 +832,7 @@ function New-IdleADAdapter { if ($null -ne $this.Credential) { $allGroupsParams['Credential'] = $this.Credential } try { - $user = Get-ADUser @userParams + $user = Get-ADUser @userParams $memberOfSet = [System.Collections.Generic.HashSet[string]]::new( [string[]]@($user.MemberOf), [System.StringComparer]::OrdinalIgnoreCase diff --git a/tools/Generate-IdleStepReference.ps1 b/tools/Generate-IdleStepReference.ps1 index b3a71fdb..4f289260 100644 --- a/tools/Generate-IdleStepReference.ps1 +++ b/tools/Generate-IdleStepReference.ps1 @@ -446,7 +446,8 @@ function Get-IdleStepRequiredCapabilities { } } catch { - # Metadata catalog not available or error loading it + # Metadata catalog not available or error loading it — return empty list + Write-Verbose "Get-IdleStepRequiredCapabilities: could not load metadata catalog for '$ModuleName': $_" } return @() @@ -684,7 +685,7 @@ function New-IdleStepDetailPageContent { 'Message' { '''Custom event message''' } 'EntitlementType' { '''Group''' } 'EntitlementValue' { '''CN=GroupName,OU=Groups,DC=domain,DC=com''' } - 'Entitlement' { "@{ Kind = 'Group'; Id = 'GroupId'; DisplayName = 'Example Group' }" } + 'Entitlement' { "@{ Kind = 'Group'; Id = 'GroupId' }" } 'State' { '''Present''' } 'Ensure' { '''Present''' } 'Provider' { '''Identity''' } diff --git a/tools/Invoke-IdleScriptAnalyzer.ps1 b/tools/Invoke-IdleScriptAnalyzer.ps1 index bf6ec6b1..cf9ac259 100644 --- a/tools/Invoke-IdleScriptAnalyzer.ps1 +++ b/tools/Invoke-IdleScriptAnalyzer.ps1 @@ -251,6 +251,7 @@ function Write-PssaSummary { [CmdletBinding()] param( [Parameter(Mandatory)] + [AllowEmptyCollection()] [object[]] $Findings, [Parameter(Mandatory)]