Skip to content
127 changes: 125 additions & 2 deletions docs/reference/providers/provider-ad.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
title: Provider Reference - IdLE.Provider.AD (Active Directory)
sidebar_label: Active Directory
Expand Down Expand Up @@ -255,10 +255,133 @@

---

## Attribute contracts

The AD provider enforces strict validation of attributes to ensure fail-fast behavior and prevent silent failures.

### CreateIdentity - Supported attributes

The following attributes are supported when creating identities via `CreateIdentity`:

#### Identity attributes
- `SamAccountName` (string) - User logon name (pre-Windows 2000)
- `UserPrincipalName` (string) - User principal name (email-style logon)
- `Path` (string) - DistinguishedName of the OU where the user should be created

#### Name attributes
- `Name` (string) - Full name (CN/RDN)
- `GivenName` (string) - First name
- `Surname` (string) - Last name
- `DisplayName` (string) - Display name

#### Organizational attributes
- `Description` (string) - User description
- `Department` (string) - Department
- `Title` (string) - Job title

#### Contact attributes
- `EmailAddress` (string) - Email address

#### Relationship attributes
- `Manager` (string) - Manager DN, GUID, UPN, or sAMAccountName (auto-resolved to DN)

#### Password attributes
- `AccountPassword` (SecureString or ProtectedString) - Password as SecureString or DPAPI-protected string
- `AccountPasswordAsPlainText` (string) - Plaintext password (explicit opt-in, automatically redacted in events)

:::warning
Only one password attribute can be used at a time. Using both `AccountPassword` and `AccountPasswordAsPlainText` will throw an error.
:::

#### State attributes
- `Enabled` (boolean) - Account enabled state (default: `$true`)

#### Extension container
- `OtherAttributes` (hashtable) - Custom LDAP attributes not covered by named parameters
- Must be a hashtable
- Keys are interpreted as LDAP attribute names and are validated by the underlying AD cmdlets at runtime
- Values must use types supported by the AD cmdlets for `-OtherAttributes` (for example: string, string[], byte[])

**Example:**
```powershell
$attrs = @{
GivenName = 'John'
Surname = 'Doe'
DisplayName = 'John Doe'
Department = 'IT'
Title = 'Engineer'
EmailAddress = 'john.doe@example.com'
OtherAttributes = @{
extensionAttribute1 = 'CustomValue'
employeeType = 'Contractor'
}
}
```

### EnsureAttribute - Supported attributes

The following attributes are supported when updating identities via `EnsureAttribute`:

#### Name attributes
- `GivenName` (string) - First name
- `Surname` (string) - Last name
- `DisplayName` (string) - Display name

#### Organizational attributes
- `Description` (string) - User description
- `Department` (string) - Department
- `Title` (string) - Job title

#### Contact attributes
- `EmailAddress` (string) - Email address

#### Identity attributes
- `UserPrincipalName` (string) - User principal name

#### Relationship attributes
- `Manager` (string) - Manager DN, GUID, UPN, or sAMAccountName (auto-resolved to DN)
Comment thread
blindzero marked this conversation as resolved.

:::info
**Note:** Custom LDAP attributes (via `OtherAttributes`) are not supported in `EnsureAttribute`. They can only be set during identity creation via `CreateIdentity`.

Password, Path, Name, and Enabled attributes are also CreateIdentity-only and cannot be modified via `EnsureAttribute`.
:::

### Validation behavior

**Strict mode (default):**
- Unsupported attribute keys cause an immediate error
- Error messages list the unsupported attributes and provide guidance
- No silent attribute dropping

**Example error:**
```
AD Provider: Unsupported attributes in CreateIdentity operation.
Unsupported attributes: InvalidAttr1, InvalidAttr2

Supported attributes for CreateIdentity:
- Identity: SamAccountName, UserPrincipalName, Path
- Name: Name, GivenName, Surname, DisplayName
- Organization: Description, Department, Title
- Contact: EmailAddress
- Relationship: Manager
- Password: AccountPassword, AccountPasswordAsPlainText
- State: Enabled
- Extension: OtherAttributes (hashtable of LDAP attributes)

To set custom LDAP attributes, use the 'OtherAttributes' container.
```

---

## Observability

- **Events emitted by provider (if any):**
- Steps emit events via the execution context; provider operations are traced through step events
- **Events emitted by provider:**
- `Provider.AD.CreateIdentity.AttributesRequested` - Emitted after identity creation with requested attributes
- `Provider.AD.EnsureAttribute.AttributeChanged` - Emitted when an attribute is modified
- **Event data includes:**
- Requested attributes (for CreateIdentity)
- Old and new values (for attribute changes in EnsureAttribute)
- **Sensitive data redaction:** Credential objects and secure strings are not included in operation results or events

---
Expand Down
6 changes: 6 additions & 0 deletions examples/workflows/templates/ad-joiner-complete.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
DisplayName = 'New User'
Description = 'New employee account'
Path = 'OU=Joiners,OU=Users,DC=contoso,DC=local'
OtherAttributes = @{
# Custom LDAP attributes for organization-specific needs
employeeType = 'Employee'
extensionAttribute1 = 'EMPL-2024-001'
company = 'Contoso Ltd'
}
}
# Provider alias - references the key in the provider hashtable.
# The host chooses this name when creating the provider hashtable.
Expand Down
86 changes: 86 additions & 0 deletions src/IdLE.Provider.AD/Private/Get-IdleADAttributeContract.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
function Get-IdleADAttributeContract {
<#
.SYNOPSIS
Returns the supported attribute contract for AD Provider operations.

.DESCRIPTION
Defines which attributes are supported for CreateIdentity and EnsureAttribute operations.
This contract serves as the single source of truth for attribute validation.

.PARAMETER Operation
The operation to get the contract for: 'CreateIdentity' or 'EnsureAttribute'.

.OUTPUTS
System.Collections.Hashtable
Returns a hashtable where keys are supported attribute names and values contain metadata.

.EXAMPLE
$contract = Get-IdleADAttributeContract -Operation 'CreateIdentity'
$supportedKeys = $contract.Keys
#>
[CmdletBinding()]
[OutputType([hashtable])]
param(
[Parameter(Mandatory)]
[ValidateSet('CreateIdentity', 'EnsureAttribute')]
[string] $Operation
)

if ($Operation -eq 'CreateIdentity') {
return @{
# Identity Attributes
SamAccountName = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
UserPrincipalName = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
Path = @{ Target = 'Parameter'; Type = 'String'; Required = $false }

# Name Attributes
Name = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
GivenName = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
Surname = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
DisplayName = @{ Target = 'Parameter'; Type = 'String'; Required = $false }

# Organizational Attributes
Description = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
Department = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
Title = @{ Target = 'Parameter'; Type = 'String'; Required = $false }

# Contact Attributes
EmailAddress = @{ Target = 'Parameter'; Type = 'String'; Required = $false }

# Relationship Attributes
Manager = @{ Target = 'Parameter'; Type = 'String'; Required = $false }

# Password Attributes
AccountPassword = @{ Target = 'Parameter'; Type = 'SecureString|String'; Required = $false }
AccountPasswordAsPlainText = @{ Target = 'Parameter'; Type = 'String'; Required = $false }

# State Attributes
Enabled = @{ Target = 'Parameter'; Type = 'Boolean'; Required = $false }

# Extension Container
OtherAttributes = @{ Target = 'Container'; Type = 'Hashtable'; Required = $false }
}
}
elseif ($Operation -eq 'EnsureAttribute') {
return @{
# Name Attributes
GivenName = @{ Target = 'Parameter'; Type = 'String' }
Surname = @{ Target = 'Parameter'; Type = 'String' }
DisplayName = @{ Target = 'Parameter'; Type = 'String' }

# Organizational Attributes
Description = @{ Target = 'Parameter'; Type = 'String' }
Department = @{ Target = 'Parameter'; Type = 'String' }
Title = @{ Target = 'Parameter'; Type = 'String' }

# Contact Attributes
EmailAddress = @{ Target = 'Parameter'; Type = 'String' }

# Identity Attributes
UserPrincipalName = @{ Target = 'Parameter'; Type = 'String' }

# Relationship Attributes
Manager = @{ Target = 'Parameter'; Type = 'String' }
}
}
}
8 changes: 8 additions & 0 deletions src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,14 @@ function New-IdleADAdapter {
$params['AccountPassword'] = ConvertTo-SecureString -String $plainTextPassword -AsPlainText -Force
}

# Handle OtherAttributes for custom LDAP attributes
if ($effectiveAttributes.ContainsKey('OtherAttributes')) {
$otherAttrs = $effectiveAttributes['OtherAttributes']
if ($null -ne $otherAttrs -and $otherAttrs.Count -gt 0) {
$params['OtherAttributes'] = $otherAttrs
}
}

if ($null -ne $this.Credential) {
$params['Credential'] = $this.Credential
}
Expand Down
122 changes: 122 additions & 0 deletions src/IdLE.Provider.AD/Private/Test-IdleADAttributeContract.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
function Test-IdleADAttributeContract {
<#
.SYNOPSIS
Validates attributes against the AD Provider attribute contract.

.DESCRIPTION
Performs strict validation of provided attributes against the supported attribute contract.
Throws an exception if unsupported attributes are detected.

.PARAMETER Attributes
Hashtable of attributes to validate.

.PARAMETER Operation
The operation context: 'CreateIdentity' or 'EnsureAttribute'.

.PARAMETER AttributeName
For EnsureAttribute, the specific attribute name being set.

.OUTPUTS
System.Collections.Hashtable
Returns a hashtable with validation results:
- Requested: array of requested attribute keys
- Supported: array of supported attribute keys
- Unsupported: array of unsupported attribute keys

.EXAMPLE
$result = Test-IdleADAttributeContract -Attributes $attrs -Operation 'CreateIdentity'
# Throws if unsupported attributes found

.EXAMPLE
$result = Test-IdleADAttributeContract -Operation 'EnsureAttribute' -AttributeName 'InvalidAttr'
# Throws if attribute not supported for EnsureAttribute
#>
[CmdletBinding()]
[OutputType([hashtable])]
param(
[Parameter()]
[AllowNull()]
[hashtable] $Attributes,

[Parameter(Mandatory)]
[ValidateSet('CreateIdentity', 'EnsureAttribute')]
[string] $Operation,

[Parameter()]
[string] $AttributeName
)

$contract = Get-IdleADAttributeContract -Operation $Operation

if ($Operation -eq 'CreateIdentity') {
if ($null -eq $Attributes) {
return @{
Requested = @()
Supported = @()
Unsupported = @()
}
}

$requestedKeys = @($Attributes.Keys)
$supportedKeys = @($contract.Keys)
$unsupportedKeys = @($requestedKeys | Where-Object { $_ -notin $supportedKeys })

if ($unsupportedKeys.Count -gt 0) {
$errorMessage = "AD Provider: Unsupported attributes in CreateIdentity operation.`n"
$errorMessage += "Unsupported attributes: $($unsupportedKeys -join ', ')`n`n"
$errorMessage += "Supported attributes for CreateIdentity:`n"

# Generate supported attributes list from contract
$supportedAttributesList = ($supportedKeys | Sort-Object | ForEach-Object { " - $_" }) -join "`n"
$errorMessage += "$supportedAttributesList`n`n"

if ('OtherAttributes' -in $supportedKeys) {
$errorMessage += "To set custom LDAP attributes, use the 'OtherAttributes' container."
}

throw $errorMessage
}

# Validate OtherAttributes if present
if ($Attributes.ContainsKey('OtherAttributes')) {
$otherAttrs = $Attributes['OtherAttributes']
if ($null -ne $otherAttrs -and $otherAttrs -isnot [hashtable]) {
throw "AD Provider: 'OtherAttributes' must be a hashtable. Received type: $($otherAttrs.GetType().FullName)"
}
}

return @{
Requested = $requestedKeys
Supported = @($requestedKeys | Where-Object { $_ -in $supportedKeys })
Unsupported = $unsupportedKeys
}
}
elseif ($Operation -eq 'EnsureAttribute') {
if ([string]::IsNullOrWhiteSpace($AttributeName)) {
throw "AD Provider: AttributeName is required for EnsureAttribute validation."
}

$supportedKeys = @($contract.Keys)

if ($AttributeName -notin $supportedKeys) {
$errorMessage = "AD Provider: Unsupported attribute in EnsureAttribute operation.`n"
$errorMessage += "Attribute: $AttributeName`n`n"
$errorMessage += "Supported attributes for EnsureAttribute:`n"

# Generate supported attributes list from contract
$supportedAttributesList = ($supportedKeys | Sort-Object | ForEach-Object { " - $_" }) -join "`n"
$errorMessage += "$supportedAttributesList`n`n"

$errorMessage += "Note: Custom LDAP attributes and password attributes are not supported in EnsureAttribute.`n"
$errorMessage += "For custom attributes, use CreateIdentity with OtherAttributes."

throw $errorMessage
}

return @{
Requested = @($AttributeName)
Supported = @($AttributeName)
Unsupported = @()
}
}
}
Loading
Loading