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
90 changes: 89 additions & 1 deletion 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 @@ -441,6 +441,66 @@

---

## CreateIdentity Derivation Behavior

When creating identities with `CreateIdentity()` (or `IdLE.Step.CreateIdentity`), the AD provider implements smart defaults to reduce boilerplate and improve workflow usability:

### 1. SamAccountName Derivation

**If `Attributes.SamAccountName` is missing or empty:**

- When `IdentityKey` is **SamAccountName-like** (no `@`, not a GUID):
- **Derives** `SamAccountName = IdentityKey`
- Example: `IdentityKey='jdoe'` → `SamAccountName='jdoe'`

- When `IdentityKey` is a **UPN** (contains `@`):
- **Fails fast** with a clear error requiring explicit `SamAccountName`
- Rationale: Automatic truncation/sanitization introduces org-specific policy decisions and collision risks

- When `IdentityKey` is a **GUID**:
- **Fails fast** with a clear error requiring explicit `SamAccountName`
- Rationale: GUIDs are not valid SamAccountNames

**Explicit values are never overridden:** If `Attributes.SamAccountName` is provided, it is always used as-is.

### 2. UserPrincipalName Auto-Set

**If `IdentityKey` is a UPN and `Attributes.UserPrincipalName` is missing or empty:**

- **Auto-sets** `UserPrincipalName = IdentityKey`
- Example: `IdentityKey='john.doe@contoso.com'` → `UserPrincipalName='john.doe@contoso.com'`

**Explicit values are never overridden:** If `Attributes.UserPrincipalName` is provided, it is always used as-is.

### 3. CN/RDN Name Derivation

The AD object's Common Name (CN/RDN, used in the DistinguishedName) is derived using this priority order:

1. **`Attributes.Name`** (explicit CN/RDN)
2. **`Attributes.DisplayName`**
3. **`GivenName + Surname`** (if both are present)
4. **`IdentityKey`** (fallback)

**Example:**
```powershell
# IdentityKey='jdoe', GivenName='John', Surname='Doe', DisplayName='John Doe'
# → CN/RDN = 'John Doe' (from DisplayName)

# IdentityKey='jdoe', GivenName='John', Surname='Doe'
# → CN/RDN = 'John Doe' (from GivenName+Surname)

# IdentityKey='jdoe'
# → CN/RDN = 'jdoe' (fallback to IdentityKey)
```

**Verbose logging:** All derivations emit `Write-Verbose` messages for observability.

### 4. Path Pass-Through

**`Attributes.Path`** is always passed through to `New-ADUser -Path` when provided. No derivation or defaulting occurs at the provider level.

---

## Entitlement Model

Active Directory entitlements use:
Expand Down Expand Up @@ -468,7 +528,7 @@
- **IdLE.Step.DisableIdentity** - Disable user accounts
- **IdLE.Step.EnableIdentity** - Enable user accounts
- **IdLE.Step.MoveIdentity** - Move users between OUs
- **IdLE.Step.DeleteIdentity** - Delete user accounts (requires `IdLE.Identity.Delete` capability)
- **IdLE.Step.DeleteIdentity** - Delete user accounts (requires provider initialization with `-AllowDelete` switch)
- **IdLE.Step.EnsureAttribute** - Set/update user attributes
- **IdLE.Step.EnsureEntitlement** - Manage group memberships

Expand Down Expand Up @@ -507,6 +567,8 @@
GivenName = 'John'
Surname = 'Doe'
UserPrincipalName = 'jdoe@contoso.local'
# SamAccountName is automatically derived from IdentityKey ('jdoe')
# CN/RDN Name will be derived from GivenName+Surname ('John Doe')
}
AuthSessionName = 'ActiveDirectory'
AuthSessionOptions = @{ Role = 'Admin' }
Expand All @@ -516,6 +578,32 @@
}
```

### Example with UPN IdentityKey

```powershell
@{
Steps = @(
@{
Name = 'CreateUserWithUPN'
Type = 'IdLE.Step.CreateIdentity'
With = @{
Provider = 'Identity'
IdentityKey = 'john.doe@contoso.com'
Attributes = @{
SamAccountName = 'jdoe' # Required when IdentityKey is UPN
GivenName = 'John'
Surname = 'Doe'
DisplayName = 'John Doe'
# UserPrincipalName is automatically set from IdentityKey
# CN/RDN Name will be 'John Doe' (from DisplayName)
}
AuthSessionName = 'ActiveDirectory'
}
}
)
}
```

### Complete example workflows

Complete example workflows are available in the repository:
Expand Down
117 changes: 91 additions & 26 deletions src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ function New-IdleADAdapter {
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $Name,
[string] $IdentityKey,

[Parameter(Mandatory)]
[ValidateNotNull()]
Expand All @@ -140,53 +140,118 @@ function New-IdleADAdapter {
[bool] $Enabled = $true
)

# Create a local copy of Attributes to avoid mutating the caller's hashtable
$effectiveAttributes = $Attributes.Clone()

# Classify IdentityKey: GUID, UPN, or SamAccountName-like
$isGuid = $false
$isUpn = $false
$isSamAccountNameLike = $false

$guid = [System.Guid]::Empty
if ([System.Guid]::TryParse($IdentityKey, [ref]$guid)) {
$isGuid = $true
}
elseif ($IdentityKey -match '@') {
$isUpn = $true
}
else {
$isSamAccountNameLike = $true
}

# 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
Write-Verbose "AD Provider: Derived SamAccountName='$IdentityKey' from IdentityKey (SamAccountName-like)"
Comment thread
blindzero marked this conversation as resolved.
}
elseif ($isUpn) {
throw "SamAccountName is required when IdentityKey is a UPN. IdentityKey='$IdentityKey' appears to be a UPN (contains '@'). Please provide an explicit 'SamAccountName' in Attributes."
}
elseif ($isGuid) {
throw "SamAccountName is required when IdentityKey is a GUID. IdentityKey='$IdentityKey' is a GUID. Please provide an explicit 'SamAccountName' in Attributes."
}
}

# 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)"
}

# 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"
}
elseif ($effectiveAttributes.ContainsKey('DisplayName') -and -not [string]::IsNullOrWhiteSpace($effectiveAttributes['DisplayName'])) {
$derivedName = $effectiveAttributes['DisplayName']
Write-Verbose "AD Provider: Derived CN/RDN Name='$derivedName' from DisplayName"
}
elseif ($effectiveAttributes.ContainsKey('GivenName') -and -not [string]::IsNullOrWhiteSpace($effectiveAttributes['GivenName']) -and
$effectiveAttributes.ContainsKey('Surname') -and -not [string]::IsNullOrWhiteSpace($effectiveAttributes['Surname'])) {
$derivedName = "$($effectiveAttributes['GivenName']) $($effectiveAttributes['Surname'])"
Write-Verbose "AD Provider: Derived CN/RDN Name='$derivedName' from GivenName+Surname"
}
else {
$derivedName = $IdentityKey
Write-Verbose "AD Provider: Falling back to IdentityKey='$derivedName' for CN/RDN Name (no DisplayName or GivenName+Surname provided)"
}

$params = @{
Name = $Name
Name = $derivedName
Enabled = $Enabled
ErrorAction = 'Stop'
}

if ($Attributes.ContainsKey('SamAccountName')) {
$params['SamAccountName'] = $Attributes['SamAccountName']
if ($effectiveAttributes.ContainsKey('SamAccountName')) {
$params['SamAccountName'] = $effectiveAttributes['SamAccountName']
}
if ($Attributes.ContainsKey('UserPrincipalName')) {
$params['UserPrincipalName'] = $Attributes['UserPrincipalName']
if ($effectiveAttributes.ContainsKey('UserPrincipalName')) {
$params['UserPrincipalName'] = $effectiveAttributes['UserPrincipalName']
}
if ($Attributes.ContainsKey('Path')) {
$params['Path'] = $Attributes['Path']
if ($effectiveAttributes.ContainsKey('Path')) {
$params['Path'] = $effectiveAttributes['Path']
}
if ($Attributes.ContainsKey('GivenName')) {
$params['GivenName'] = $Attributes['GivenName']
if ($effectiveAttributes.ContainsKey('GivenName')) {
$params['GivenName'] = $effectiveAttributes['GivenName']
}
if ($Attributes.ContainsKey('Surname')) {
$params['Surname'] = $Attributes['Surname']
if ($effectiveAttributes.ContainsKey('Surname')) {
$params['Surname'] = $effectiveAttributes['Surname']
}
if ($Attributes.ContainsKey('DisplayName')) {
$params['DisplayName'] = $Attributes['DisplayName']
if ($effectiveAttributes.ContainsKey('DisplayName')) {
$params['DisplayName'] = $effectiveAttributes['DisplayName']
}
if ($Attributes.ContainsKey('Description')) {
$params['Description'] = $Attributes['Description']
if ($effectiveAttributes.ContainsKey('Description')) {
$params['Description'] = $effectiveAttributes['Description']
}
if ($Attributes.ContainsKey('Department')) {
$params['Department'] = $Attributes['Department']
if ($effectiveAttributes.ContainsKey('Department')) {
$params['Department'] = $effectiveAttributes['Department']
}
if ($Attributes.ContainsKey('Title')) {
$params['Title'] = $Attributes['Title']
if ($effectiveAttributes.ContainsKey('Title')) {
$params['Title'] = $effectiveAttributes['Title']
}
if ($Attributes.ContainsKey('EmailAddress')) {
$params['EmailAddress'] = $Attributes['EmailAddress']
if ($effectiveAttributes.ContainsKey('EmailAddress')) {
$params['EmailAddress'] = $effectiveAttributes['EmailAddress']
}

# Password handling: support SecureString, ProtectedString, and explicit PlainText
$hasAccountPassword = $Attributes.ContainsKey('AccountPassword')
$hasAccountPasswordAsPlainText = $Attributes.ContainsKey('AccountPasswordAsPlainText')
$hasAccountPassword = $effectiveAttributes.ContainsKey('AccountPassword')
$hasAccountPasswordAsPlainText = $effectiveAttributes.ContainsKey('AccountPasswordAsPlainText')

if ($hasAccountPassword -and $hasAccountPasswordAsPlainText) {
throw "Ambiguous password configuration: both 'AccountPassword' and 'AccountPasswordAsPlainText' are provided. Use only one."
}

if ($hasAccountPassword) {
$passwordValue = $Attributes['AccountPassword']
$passwordValue = $effectiveAttributes['AccountPassword']

if ($null -eq $passwordValue) {
throw "AccountPassword: Value cannot be null. Provide a SecureString or ProtectedString (from ConvertFrom-SecureString)."
Expand Down Expand Up @@ -220,7 +285,7 @@ function New-IdleADAdapter {
}

if ($hasAccountPasswordAsPlainText) {
$plainTextPassword = $Attributes['AccountPasswordAsPlainText']
$plainTextPassword = $effectiveAttributes['AccountPasswordAsPlainText']

if ($null -eq $plainTextPassword) {
throw "AccountPasswordAsPlainText: Value cannot be null. Provide a non-empty plaintext password string."
Expand Down
Loading
Loading