Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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.

---

## Generated cmdlet reference (platyPS)
Expand Down
5 changes: 3 additions & 2 deletions docs/reference/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,9 @@ For `IdLE.Entitlement.List`, the engine additionally builds (list merge — all
| One provider, one session | `Request.Context.Views.Providers.<ProviderAlias>.Sessions.<AuthSessionKey>.Identity.Entitlements` |

> **Note**: `IdLE.Entitlement.List` writes an array of entitlement objects. Each entry includes:
> `Kind` (string), `Id` (string), and optionally `DisplayName` (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).

Expand Down
1 change: 0 additions & 1 deletion docs/reference/providers/provider-ad.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 0 additions & 1 deletion docs/reference/providers/provider-entraID.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 0 additions & 1 deletion docs/reference/providers/provider-mock.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/steps/step-ensure-entitlement.md
Comment thread
blindzero marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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). |
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/steps/step-prune-entitlements.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
9 changes: 4 additions & 5 deletions docs/use/workflows/conditions.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
title: Conditions
sidebar_label: Conditions
Expand Down Expand Up @@ -128,7 +128,7 @@
}
```

> **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

Expand Down Expand Up @@ -174,7 +174,7 @@
}
```

> **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

Expand Down Expand Up @@ -218,16 +218,15 @@

- `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.<ProviderAlias>.<AuthSessionKey>.Identity.Entitlements.Id` (where `<AuthSessionKey>` is the auth session key; `Default` is used when no `With.AuthSessionName` is specified).

**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
Expand Down
2 changes: 1 addition & 1 deletion examples/workflows/templates/ad-leaver.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion examples/workflows/templates/entraid-leaver.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/IdLE.Core/Private/Resolve-IdleTemplateString.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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()]
Expand Down
18 changes: 4 additions & 14 deletions src/IdLE.Provider.AD/Private/ConvertTo-IdleADEntitlement.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
Expand All @@ -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)) {
Expand All @@ -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
}
}
35 changes: 18 additions & 17 deletions src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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()]
Expand Down Expand Up @@ -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
Expand All @@ -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)"
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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))"
}

Expand Down Expand Up @@ -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

Expand All @@ -524,6 +525,8 @@ function New-IdleADAdapter {
[object] $CurrentValue
)

$null = $CurrentValue # Contract parameter; reserved for future change-detection optimization

$params = @{
Identity = $Identity
ErrorAction = 'Stop'
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading