Skip to content
53 changes: 53 additions & 0 deletions docs/reference/providers/provider-ad.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,59 @@ $provider = New-IdleADIdentityProvider -AllowDelete
- **Safety defaults:** deletion is disabled unless you pass `-AllowDelete`
- **Entitlements:** groups only (`Kind='Group'`)

## Attribute handling

### CreateIdentity attributes

`IdLE.Step.CreateIdentity` maps attributes to `New-ADUser` named parameters. Attributes not listed in the named parameter set can be passed via the `OtherAttributes` container using their **LDAP attribute names** as keys.

```powershell
@{
Name = 'Create AD user'
Type = 'IdLE.Step.CreateIdentity'
With = @{
IdentityKey = '{{Request.IdentityKeys.sAMAccountName}}'
Provider = 'AD'
Attributes = @{
GivenName = '{{Request.GivenName}}'
Surname = '{{Request.Surname}}'
OtherAttributes = @{
extensionAttribute1 = '{{Request.Department}}'
}
}
}
}
```

> **Note:** Keys in `OtherAttributes` must be valid **LDAP attribute names** (e.g. `extensionAttribute1`, `employeeType`), not PowerShell parameter names.

### EnsureAttributes attributes

`IdLE.Step.EnsureAttributes` maps attributes to `Set-ADUser` named parameters. Setting an attribute to `$null` clears the value from the directory. Attributes not listed in the named parameter set can be set or cleared via the `OtherAttributes` container using their **LDAP attribute names** as keys.

**Custom LDAP attributes** (via OtherAttributes container):

```powershell
@{
Name = 'Clear phone numbers'
Type = 'IdLE.Step.EnsureAttributes'
With = @{
IdentityKey = '{{Request.IdentityKeys.sAMAccountName}}'
Provider = 'AD'
Attributes = @{
MobilePhone = $null # Clears the mobile attribute
OfficePhone = $null # Clears the telephoneNumber attribute
OtherAttributes = @{
extensionAttribute1 = 'NewValue' # Sets custom LDAP attribute
employeeType = $null # Clears custom LDAP attribute
}
}
}
}
```

> **Note:** Keys in `OtherAttributes` must be valid **LDAP attribute names** (e.g. `mobile`, `telephoneNumber`, `extensionAttribute1`), not PowerShell parameter names. Setting a key to `$null` clears that LDAP attribute.

## Examples

These are the canonical, **doc-embed friendly** templates for AD.
Expand Down
141 changes: 96 additions & 45 deletions src/IdLE.Provider.AD/Private/Get-IdleADAttributeContract.ps1
Comment thread
blindzero marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,24 @@ function Get-IdleADAttributeContract {
Returns the supported attribute contract for AD Provider operations.

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

Each entry includes:
- Target: 'Parameter' (Set-ADUser/New-ADUser named parameter) or 'Container' (special handling)
- Type: expected value type
- Required: whether the attribute is required

For CreateIdentity, attributes map to New-ADUser named parameters.
For EnsureAttributes, attributes map to Set-ADUser named parameters.
Custom LDAP attributes not listed here can be set via the OtherAttributes container
(keys must be valid LDAP attribute names, e.g. 'mobile', 'telephoneNumber').

To resolve the underlying LDAP attribute name for a contract key, use
Get-IdleADAttributeLDAPField.

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

.OUTPUTS
System.Collections.Hashtable
Expand All @@ -17,72 +30,110 @@ function Get-IdleADAttributeContract {
.EXAMPLE
$contract = Get-IdleADAttributeContract -Operation 'CreateIdentity'
$supportedKeys = $contract.Keys

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

if ($Operation -eq 'CreateIdentity') {
return @{
$contract = @{
# Identity Attributes
SamAccountName = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
UserPrincipalName = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
Path = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
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 }
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 }
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 }
EmailAddress = @{ Target = 'Parameter'; Type = 'String'; Required = $false }

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

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

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

# Extension Container (keys must be valid LDAP attribute names)
OtherAttributes = @{ Target = 'Container'; Type = 'Hashtable'; Required = $false }
}
}
elseif ($Operation -eq 'EnsureAttribute') {
return @{
elseif ($Operation -eq 'EnsureAttributes') {
$contract = @{
# Name Attributes
GivenName = @{ Target = 'Parameter'; Type = 'String' }
Surname = @{ Target = 'Parameter'; Type = 'String' }
DisplayName = @{ Target = 'Parameter'; Type = 'String' }

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

# Identity Attributes
SamAccountName = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
UserPrincipalName = @{ Target = 'Parameter'; Type = 'String'; Required = $false }

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

Description = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
Department = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
Title = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
Company = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
Division = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
Office = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
EmployeeID = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
EmployeeNumber = @{ Target = 'Parameter'; Type = 'String'; Required = $false }

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

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

EmailAddress = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
OfficePhone = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
MobilePhone = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
HomePhone = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
Fax = @{ Target = 'Parameter'; Type = 'String'; Required = $false }

# Address Attributes
StreetAddress = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
City = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
State = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
PostalCode = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
Country = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
POBox = @{ Target = 'Parameter'; Type = 'String'; Required = $false }

# Web / Profile Attributes
HomePage = @{ Target = 'Parameter'; Type = 'String'; Required = $false }

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

# Account / Profile Path Attributes
HomeDirectory = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
HomeDrive = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
ProfilePath = @{ Target = 'Parameter'; Type = 'String'; Required = $false }
ScriptPath = @{ Target = 'Parameter'; Type = 'String'; Required = $false }

# Extension Container (keys must be valid LDAP attribute names, e.g. 'mobile', 'telephoneNumber')
OtherAttributes = @{ Target = 'Container'; Type = 'Hashtable'; Required = $false }
}
}

return $contract
}

94 changes: 94 additions & 0 deletions src/IdLE.Provider.AD/Private/Get-IdleADAttributeLDAPField.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
function Get-IdleADAttributeLDAPField {
<#
.SYNOPSIS
Returns the verified LDAP attribute name for a given AD attribute key.

.DESCRIPTION
Provides the authoritative mapping from friendly AD attribute names (as used in the
IdLE AD Provider contract) to their verified LDAP schema attribute names.

LDAP names are verified against the Windows Server Active Directory LDAP schema.
This mapping is used for -Clear, -Replace, and -Add operations in Set-ADUser to
ensure correct attribute targeting in the directory.

.PARAMETER AttributeName
The friendly attribute name (PowerShell parameter name or contract key) to look up.

.OUTPUTS
System.String
The LDAP attribute name, or $null if the attribute is not a named parameter mapping.

.EXAMPLE
Get-IdleADAttributeLDAPField -AttributeName 'GivenName'
# Returns: 'givenName'

.EXAMPLE
Get-IdleADAttributeLDAPField -AttributeName 'EmailAddress'
# Returns: 'mail'
#>
[CmdletBinding()]
[OutputType([string])]
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $AttributeName
)

# Verified against Windows Server Active Directory LDAP schema documentation.
# Sources: RFC 4519, RFC 2798 (inetOrgPerson), MS-ADSC (Active Directory Schema Classes/Attributes).
$ldapFields = @{
# Name Attributes
GivenName = 'givenName' # RFC 4519 section 2.12
Surname = 'sn' # RFC 4519 section 2.32
DisplayName = 'displayName' # MS-ADSC
Initials = 'initials' # RFC 2256

# Identity Attributes
SamAccountName = 'sAMAccountName' # MS-ADSC
UserPrincipalName = 'userPrincipalName' # MS-ADSC

# Organizational Attributes
Description = 'description' # RFC 4519 section 2.5
Department = 'department' # RFC 2798 section 2.2
Title = 'title' # RFC 4519 section 2.38
Company = 'company' # MS-ADSC
Division = 'division' # MS-ADSC
Office = 'physicalDeliveryOfficeName' # RFC 4519 section 2.24
Organization = 'o' # RFC 4519 section 2.19
EmployeeID = 'employeeID' # MS-ADSC
EmployeeNumber = 'employeeNumber' # RFC 2798 section 2.5

# Contact Attributes
EmailAddress = 'mail' # RFC 2798 section 2.13
OfficePhone = 'telephoneNumber' # RFC 4519 section 2.35
MobilePhone = 'mobile' # RFC 2798 section 2.15
HomePhone = 'homePhone' # RFC 2798 section 2.11
Fax = 'facsimileTelephoneNumber' # RFC 4519 section 2.10

# Address Attributes
StreetAddress = 'streetAddress' # RFC 4519 section 2.34
City = 'l' # RFC 4519 section 2.16 (localityName)
State = 'st' # RFC 4519 section 2.33 (stateOrProvinceName)
PostalCode = 'postalCode' # RFC 4519 section 2.23
Country = 'co' # RFC 2256 section 5.4 (full country name)
POBox = 'postOfficeBox' # RFC 4519 section 2.25

# Web / Profile Attributes
HomePage = 'wWWHomePage' # MS-ADSC

# Relationship Attributes
Manager = 'manager' # RFC 4524 section 2.1

# Account/Profile Path Attributes
HomeDirectory = 'homeDirectory' # MS-ADSC
HomeDrive = 'homeDrive' # MS-ADSC
ProfilePath = 'profilePath' # MS-ADSC
ScriptPath = 'scriptPath' # MS-ADSC
}

if ($ldapFields.ContainsKey($AttributeName)) {
return $ldapFields[$AttributeName]
}

return $null
}
Loading
Loading