From a5b4f3a60ce05473b984bcc98d2b9834b9911db3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 12:41:23 +0000 Subject: [PATCH 1/6] Initial plan From 2210d9cad41371aa799c62c1e988e3eb1578445d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 12:49:08 +0000 Subject: [PATCH 2/6] Add Manager attribute support to AD provider with validation and tests Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- .../Private/New-IdleADAdapter.ps1 | 46 ++++- tests/Providers/ADIdentityProvider.Tests.ps1 | 193 ++++++++++++++++++ 2 files changed, 236 insertions(+), 3 deletions(-) diff --git a/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 b/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 index c420d11a..28951031 100644 --- a/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 +++ b/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 @@ -44,6 +44,33 @@ function New-IdleADAdapter { return $escaped } -Force + # Add Manager DN validation helper + $adapter | Add-Member -MemberType ScriptMethod -Name TestManagerDN -Value { + param( + [Parameter(Mandatory)] + [AllowNull()] + [object] $Value + ) + + if ($null -eq $Value) { + return $true + } + + if ($Value -isnot [string]) { + throw "Manager must be a DistinguishedName (DN) string, but received type: $($Value.GetType().FullName)" + } + + if ([string]::IsNullOrWhiteSpace($Value)) { + throw "Manager must be a DistinguishedName (DN) string, but received empty or whitespace-only value." + } + + if (-not ($Value -match '=' -and $Value -match ',')) { + throw "Manager must be a DistinguishedName (DN). Expected format: 'CN=Name,OU=Unit,DC=domain,DC=com'. Received: '$Value'" + } + + return $true + } -Force + $adapter | Add-Member -MemberType ScriptMethod -Name GetUserByUpn -Value { param( [Parameter(Mandatory)] @@ -56,7 +83,7 @@ function New-IdleADAdapter { $escapedUpn = $escapedUpn -replace '''', '''''' $params = @{ Filter = "UserPrincipalName -eq '$escapedUpn'" - Properties = @('Enabled', 'DistinguishedName', 'ObjectGuid', 'UserPrincipalName', 'sAMAccountName') + Properties = @('Enabled', 'DistinguishedName', 'ObjectGuid', 'UserPrincipalName', 'sAMAccountName', 'Manager') ErrorAction = 'Stop' } if ($null -ne $this.Credential) { @@ -85,7 +112,7 @@ function New-IdleADAdapter { $params = @{ Filter = "sAMAccountName -eq '$escapedSam'" - Properties = @('Enabled', 'DistinguishedName', 'ObjectGuid', 'UserPrincipalName', 'sAMAccountName') + Properties = @('Enabled', 'DistinguishedName', 'ObjectGuid', 'UserPrincipalName', 'sAMAccountName', 'Manager') ErrorAction = 'Stop' } if ($null -ne $this.Credential) { @@ -110,7 +137,7 @@ function New-IdleADAdapter { $params = @{ Identity = $Guid - Properties = @('Enabled', 'DistinguishedName', 'ObjectGuid', 'UserPrincipalName', 'sAMAccountName') + Properties = @('Enabled', 'DistinguishedName', 'ObjectGuid', 'UserPrincipalName', 'sAMAccountName', 'Manager') ErrorAction = 'Stop' } if ($null -ne $this.Credential) { @@ -241,6 +268,11 @@ function New-IdleADAdapter { if ($effectiveAttributes.ContainsKey('EmailAddress')) { $params['EmailAddress'] = $effectiveAttributes['EmailAddress'] } + if ($effectiveAttributes.ContainsKey('Manager')) { + $managerValue = $effectiveAttributes['Manager'] + $this.TestManagerDN($managerValue) | Out-Null + $params['Manager'] = $managerValue + } # Password handling: support SecureString, ProtectedString, and explicit PlainText $hasAccountPassword = $effectiveAttributes.ContainsKey('AccountPassword') @@ -346,6 +378,14 @@ function New-IdleADAdapter { 'Title' { $params['Title'] = $Value } 'EmailAddress' { $params['EmailAddress'] = $Value } 'UserPrincipalName' { $params['UserPrincipalName'] = $Value } + 'Manager' { + $this.TestManagerDN($Value) | Out-Null + if ($null -eq $Value) { + $params['Clear'] = 'manager' + } else { + $params['Manager'] = $Value + } + } default { $params['Replace'] = @{ $AttributeName = $Value } } diff --git a/tests/Providers/ADIdentityProvider.Tests.ps1 b/tests/Providers/ADIdentityProvider.Tests.ps1 index 44223a21..3d3b8c24 100644 --- a/tests/Providers/ADIdentityProvider.Tests.ps1 +++ b/tests/Providers/ADIdentityProvider.Tests.ps1 @@ -48,6 +48,33 @@ Describe 'AD identity provider' { Store = $store } + # Add Manager DN validation helper (matching real adapter) + $adapter | Add-Member -MemberType ScriptMethod -Name TestManagerDN -Value { + param( + [Parameter(Mandatory)] + [AllowNull()] + [object] $Value + ) + + if ($null -eq $Value) { + return $true + } + + if ($Value -isnot [string]) { + throw "Manager must be a DistinguishedName (DN) string, but received type: $($Value.GetType().FullName)" + } + + if ([string]::IsNullOrWhiteSpace($Value)) { + throw "Manager must be a DistinguishedName (DN) string, but received empty or whitespace-only value." + } + + if (-not ($Value -match '=' -and $Value -match ',')) { + throw "Manager must be a DistinguishedName (DN). Expected format: 'CN=Name,OU=Unit,DC=domain,DC=com'. Received: '$Value'" + } + + return $true + } -Force + # Auto-creation behavior: The fake adapter auto-creates identities on lookup # to support provider contract tests (which expect this behavior from test providers). # This differs from the real AD adapter which will throw when an identity is not found. @@ -79,6 +106,7 @@ Describe 'AD identity provider' { Department = $null Title = $null EmailAddress = $null + Manager = $null Groups = @() } $this.Store[$guid] = $user @@ -108,6 +136,7 @@ Describe 'AD identity provider' { Department = $null Title = $null EmailAddress = $null + Manager = $null Groups = @() } $this.Store[$guid] = $user @@ -197,6 +226,11 @@ Describe 'AD identity provider' { } } + # Manager validation (same as real adapter) + if ($Attributes.ContainsKey('Manager')) { + $this.TestManagerDN($Attributes['Manager']) | Out-Null + } + $newGuid = [guid]::NewGuid().ToString() $sam = $Attributes['SamAccountName'] $upn = if ($Attributes.ContainsKey('UserPrincipalName')) { $Attributes['UserPrincipalName'] } else { "$sam@domain.local" } @@ -215,6 +249,7 @@ Describe 'AD identity provider' { Department = $Attributes['Department'] Title = $Attributes['Title'] EmailAddress = $Attributes['EmailAddress'] + Manager = $Attributes['Manager'] Groups = @() } @@ -237,6 +272,17 @@ Describe 'AD identity provider' { throw "User not found: $Identity" } + # Handle Manager attribute with validation + if ($AttributeName -eq 'Manager') { + $this.TestManagerDN($Value) | Out-Null + if ($null -eq $user.PSObject.Properties['Manager']) { + $user | Add-Member -MemberType NoteProperty -Name 'Manager' -Value $Value -Force + } else { + $user.Manager = $Value + } + return + } + # Handle known properties $knownProps = @('GivenName', 'Surname', 'DisplayName', 'Description', 'Department', 'Title', 'EmailAddress', 'UserPrincipalName') if ($AttributeName -in $knownProps -and $null -ne $user.PSObject.Properties[$AttributeName]) { @@ -1143,4 +1189,151 @@ Describe 'AD identity provider' { $user.DistinguishedName | Should -BeLike '*OU=Users,DC=domain,DC=local' } } + + Context 'Manager attribute handling' { + BeforeAll { + $adapter = New-FakeADAdapter + $provider = New-IdleADIdentityProvider -Adapter $adapter + $script:ManagerTestProvider = $provider + $script:ManagerTestAdapter = $adapter + } + + It 'CreateIdentity sets Manager when valid DN is provided' { + $attrs = @{ + SamAccountName = 'managertest1' + GivenName = 'Test' + Surname = 'User' + Manager = 'CN=Jane Doe,OU=Managers,DC=contoso,DC=com' + } + + $user = $script:ManagerTestAdapter.NewUser('managertest1', $attrs, $true) + + $user.Manager | Should -Be 'CN=Jane Doe,OU=Managers,DC=contoso,DC=com' + } + + It 'CreateIdentity throws when Manager is not a DN format' { + $attrs = @{ + SamAccountName = 'managertest2' + Manager = 'InvalidFormat' + } + + { $script:ManagerTestAdapter.NewUser('managertest2', $attrs, $true) } | Should -Throw -ExpectedMessage '*Manager must be a DistinguishedName*' + } + + It 'CreateIdentity throws when Manager is not a string' { + $attrs = @{ + SamAccountName = 'managertest3' + Manager = 12345 + } + + { $script:ManagerTestAdapter.NewUser('managertest3', $attrs, $true) } | Should -Throw -ExpectedMessage '*Manager must be a DistinguishedName (DN) string*' + } + + It 'CreateIdentity throws when Manager is empty string' { + $attrs = @{ + SamAccountName = 'managertest4' + Manager = ' ' + } + + { $script:ManagerTestAdapter.NewUser('managertest4', $attrs, $true) } | Should -Throw -ExpectedMessage '*Manager must be a DistinguishedName (DN) string*' + } + + It 'EnsureAttribute sets Manager when valid DN is provided' { + # Create user without manager + $attrs = @{ + SamAccountName = 'managertest5' + } + $user = $script:ManagerTestAdapter.NewUser('managertest5', $attrs, $true) + + # Set manager via EnsureAttribute (use sAMAccountName as IdentityKey) + $result = $script:ManagerTestProvider.EnsureAttribute( + 'managertest5', + 'Manager', + 'CN=John Smith,OU=Managers,DC=contoso,DC=com', + $null + ) + + $result.Changed | Should -Be $true + + # Verify manager was set + $updatedUser = $script:ManagerTestAdapter.Store[$user.ObjectGuid.ToString()] + $updatedUser.Manager | Should -Be 'CN=John Smith,OU=Managers,DC=contoso,DC=com' + } + + It 'EnsureAttribute clears Manager when Value is $null' { + # Create user with manager + $attrs = @{ + SamAccountName = 'managertest6' + Manager = 'CN=Jane Doe,OU=Managers,DC=contoso,DC=com' + } + $user = $script:ManagerTestAdapter.NewUser('managertest6', $attrs, $true) + + # Clear manager via EnsureAttribute (use sAMAccountName as IdentityKey) + $result = $script:ManagerTestProvider.EnsureAttribute( + 'managertest6', + 'Manager', + $null, + $null + ) + + $result.Changed | Should -Be $true + + # Verify manager was cleared + $updatedUser = $script:ManagerTestAdapter.Store[$user.ObjectGuid.ToString()] + $updatedUser.Manager | Should -BeNullOrEmpty + } + + It 'EnsureAttribute throws when Manager value is not a DN format' { + $attrs = @{ + SamAccountName = 'managertest7' + } + $user = $script:ManagerTestAdapter.NewUser('managertest7', $attrs, $true) + + { + $script:ManagerTestProvider.EnsureAttribute( + 'managertest7', + 'Manager', + 'NotADN', + $null + ) + } | Should -Throw -ExpectedMessage '*Manager must be a DistinguishedName*' + } + + It 'EnsureAttribute is idempotent when Manager is already set to target value' { + # Create user with manager + $attrs = @{ + SamAccountName = 'managertest8' + Manager = 'CN=Jane Doe,OU=Managers,DC=contoso,DC=com' + } + $user = $script:ManagerTestAdapter.NewUser('managertest8', $attrs, $true) + + # Set same manager again (use sAMAccountName as IdentityKey) + $result = $script:ManagerTestProvider.EnsureAttribute( + 'managertest8', + 'Manager', + 'CN=Jane Doe,OU=Managers,DC=contoso,DC=com', + $null + ) + + $result.Changed | Should -Be $false + } + + It 'EnsureAttribute is idempotent when Manager is already cleared' { + # Create user without manager + $attrs = @{ + SamAccountName = 'managertest9' + } + $user = $script:ManagerTestAdapter.NewUser('managertest9', $attrs, $true) + + # Clear manager (already cleared) (use sAMAccountName as IdentityKey) + $result = $script:ManagerTestProvider.EnsureAttribute( + 'managertest9', + 'Manager', + $null, + $null + ) + + $result.Changed | Should -Be $false + } + } } From 2b15c606254d8e4e41b0b66573101e1a130e9374 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 12:50:20 +0000 Subject: [PATCH 3/6] Add Manager attribute documentation with examples Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/reference/providers/provider-ad.md | 63 +++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/docs/reference/providers/provider-ad.md b/docs/reference/providers/provider-ad.md index 27424def..4d608f53 100644 --- a/docs/reference/providers/provider-ad.md +++ b/docs/reference/providers/provider-ad.md @@ -604,6 +604,69 @@ $result = Invoke-IdlePlan -Plan $plan -Providers $providers } ``` +### Manager Attribute Handling + +The AD provider supports the `Manager` attribute for both `CreateIdentity` and `EnsureAttribute` operations. + +**Requirements:** +- Manager value MUST be a Distinguished Name (DN) string +- Format: `CN=Name,OU=Unit,DC=domain,DC=com` +- Invalid formats will throw a validation error + +**CreateIdentity with Manager:** + +```powershell +@{ + Name = 'CreateUserWithManager' + Type = 'IdLE.Step.CreateIdentity' + With = @{ + Provider = 'Identity' + IdentityKey = 'jdoe' + Attributes = @{ + GivenName = 'John' + Surname = 'Doe' + UserPrincipalName = 'jdoe@contoso.local' + Manager = 'CN=Jane Smith,OU=Managers,DC=contoso,DC=local' + } + AuthSessionName = 'ActiveDirectory' + } +} +``` + +**Setting Manager via EnsureAttribute:** + +```powershell +@{ + Name = 'SetManager' + Type = 'IdLE.Step.EnsureAttribute' + With = @{ + Provider = 'Identity' + IdentityKey = 'jdoe' + Name = 'Manager' + Value = 'CN=Jane Smith,OU=Managers,DC=contoso,DC=local' + AuthSessionName = 'ActiveDirectory' + } +} +``` + +**Clearing Manager:** + +To clear the Manager attribute, set the value to `$null`: + +```powershell +@{ + Name = 'ClearManager' + Type = 'IdLE.Step.EnsureAttribute' + With = @{ + Provider = 'Identity' + IdentityKey = 'jdoe' + Name = 'Manager' + Value = $null + AuthSessionName = 'ActiveDirectory' + } +} +``` + ### Complete example workflows Complete example workflows are available in the repository: From f22d99879a86856fd5556b3e627ca939c48c09c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:45:35 +0000 Subject: [PATCH 4/6] Add Manager DN resolution from GUID/UPN/sAMAccountName Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/reference/providers/provider-ad.md | 49 ++++- .../Private/New-IdleADAdapter.ps1 | 78 +++++++- tests/Providers/ADIdentityProvider.Tests.ps1 | 187 ++++++++++++++++-- 3 files changed, 285 insertions(+), 29 deletions(-) diff --git a/docs/reference/providers/provider-ad.md b/docs/reference/providers/provider-ad.md index 4d608f53..0338bba3 100644 --- a/docs/reference/providers/provider-ad.md +++ b/docs/reference/providers/provider-ad.md @@ -606,14 +606,24 @@ $result = Invoke-IdlePlan -Plan $plan -Providers $providers ### Manager Attribute Handling -The AD provider supports the `Manager` attribute for both `CreateIdentity` and `EnsureAttribute` operations. +The AD provider supports the `Manager` attribute for both `CreateIdentity` and `EnsureAttribute` operations with automatic DN resolution. -**Requirements:** -- Manager value MUST be a Distinguished Name (DN) string -- Format: `CN=Name,OU=Unit,DC=domain,DC=com` -- Invalid formats will throw a validation error +**Supported Input Formats:** -**CreateIdentity with Manager:** +The Manager value can be specified in multiple formats, which will be automatically resolved to a Distinguished Name (DN): + +- **Distinguished Name (DN)**: Direct DN string (no resolution needed) + - Example: `CN=Jane Smith,OU=Managers,DC=contoso,DC=local` +- **GUID**: User's ObjectGuid (resolved to DN) + - Example: `a1b2c3d4-e5f6-7890-abcd-ef1234567890` +- **UPN**: UserPrincipalName (resolved to DN) + - Example: `jsmith@contoso.local` +- **sAMAccountName**: Simple username (resolved to DN) + - Example: `jsmith` + +The provider automatically detects the format and resolves it to the manager's DN. If the manager cannot be found, an error is thrown with a clear message. + +**CreateIdentity with Manager (DN):** ```powershell @{ @@ -626,14 +636,33 @@ The AD provider supports the `Manager` attribute for both `CreateIdentity` and ` GivenName = 'John' Surname = 'Doe' UserPrincipalName = 'jdoe@contoso.local' - Manager = 'CN=Jane Smith,OU=Managers,DC=contoso,DC=local' + Manager = 'CN=Jane Smith,OU=Managers,DC=contoso,DC=local' # DN + } + AuthSessionName = 'ActiveDirectory' + } +} +``` + +**CreateIdentity with Manager (sAMAccountName):** + +```powershell +@{ + Name = 'CreateUserWithManagerSam' + Type = 'IdLE.Step.CreateIdentity' + With = @{ + Provider = 'Identity' + IdentityKey = 'jdoe' + Attributes = @{ + GivenName = 'John' + Surname = 'Doe' + Manager = 'jsmith' # Will be resolved to DN automatically } AuthSessionName = 'ActiveDirectory' } } ``` -**Setting Manager via EnsureAttribute:** +**Setting Manager via EnsureAttribute (UPN):** ```powershell @{ @@ -643,7 +672,7 @@ The AD provider supports the `Manager` attribute for both `CreateIdentity` and ` Provider = 'Identity' IdentityKey = 'jdoe' Name = 'Manager' - Value = 'CN=Jane Smith,OU=Managers,DC=contoso,DC=local' + Value = 'jsmith@contoso.local' # UPN - will be resolved to DN AuthSessionName = 'ActiveDirectory' } } @@ -667,6 +696,8 @@ To clear the Manager attribute, set the value to `$null`: } ``` +**Note:** Clearing the Manager attribute using `$null` works correctly in PSD1 workflow files. PowerShell evaluates `$null` at load time. + ### Complete example workflows Complete example workflows are available in the repository: diff --git a/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 b/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 index 28951031..530c9c51 100644 --- a/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 +++ b/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 @@ -71,6 +71,72 @@ function New-IdleADAdapter { return $true } -Force + # Add Manager DN resolution helper + # Accepts DN, GUID, UPN, or sAMAccountName and resolves to DN + $adapter | Add-Member -MemberType ScriptMethod -Name ResolveManagerDN -Value { + param( + [Parameter(Mandatory)] + [AllowNull()] + [object] $Value + ) + + if ($null -eq $Value) { + return $null + } + + if ($Value -isnot [string]) { + throw "Manager must be a string (DN, GUID, UPN, or sAMAccountName), but received type: $($Value.GetType().FullName)" + } + + if ([string]::IsNullOrWhiteSpace($Value)) { + throw "Manager cannot be an empty or whitespace-only string." + } + + # Check if already a DN (contains = and ,) + if ($Value -match '=' -and $Value -match ',') { + # Already a DN, validate and return + $this.TestManagerDN($Value) | Out-Null + return $Value + } + + # Try to resolve as GUID, UPN, or sAMAccountName + Write-Verbose "Manager value '$Value' is not a DN. Attempting to resolve to DN..." + + # Try GUID format first + $guid = [System.Guid]::Empty + if ([System.Guid]::TryParse($Value, [ref]$guid)) { + try { + $managerUser = $this.GetUserByGuid($guid.ToString()) + if ($null -ne $managerUser) { + Write-Verbose "Resolved Manager GUID '$Value' to DN: $($managerUser.DistinguishedName)" + return $managerUser.DistinguishedName + } + } + catch { + Write-Verbose "Failed to resolve Manager GUID '$Value': $_" + } + throw "Manager: Could not find user with GUID '$Value'." + } + + # Try UPN format (contains @) + if ($Value -match '@') { + $managerUser = $this.GetUserByUpn($Value) + if ($null -ne $managerUser) { + Write-Verbose "Resolved Manager UPN '$Value' to DN: $($managerUser.DistinguishedName)" + return $managerUser.DistinguishedName + } + throw "Manager: Could not find user with UPN '$Value'." + } + + # Fallback to sAMAccountName + $managerUser = $this.GetUserBySam($Value) + if ($null -ne $managerUser) { + Write-Verbose "Resolved Manager sAMAccountName '$Value' to DN: $($managerUser.DistinguishedName)" + return $managerUser.DistinguishedName + } + throw "Manager: Could not find user with sAMAccountName '$Value'." + } -Force + $adapter | Add-Member -MemberType ScriptMethod -Name GetUserByUpn -Value { param( [Parameter(Mandatory)] @@ -270,8 +336,10 @@ function New-IdleADAdapter { } if ($effectiveAttributes.ContainsKey('Manager')) { $managerValue = $effectiveAttributes['Manager'] - $this.TestManagerDN($managerValue) | Out-Null - $params['Manager'] = $managerValue + $resolvedManagerDN = $this.ResolveManagerDN($managerValue) + if ($null -ne $resolvedManagerDN) { + $params['Manager'] = $resolvedManagerDN + } } # Password handling: support SecureString, ProtectedString, and explicit PlainText @@ -379,11 +447,11 @@ function New-IdleADAdapter { 'EmailAddress' { $params['EmailAddress'] = $Value } 'UserPrincipalName' { $params['UserPrincipalName'] = $Value } 'Manager' { - $this.TestManagerDN($Value) | Out-Null - if ($null -eq $Value) { + $resolvedManagerDN = $this.ResolveManagerDN($Value) + if ($null -eq $resolvedManagerDN) { $params['Clear'] = 'manager' } else { - $params['Manager'] = $Value + $params['Manager'] = $resolvedManagerDN } } default { diff --git a/tests/Providers/ADIdentityProvider.Tests.ps1 b/tests/Providers/ADIdentityProvider.Tests.ps1 index 3d3b8c24..36b641f6 100644 --- a/tests/Providers/ADIdentityProvider.Tests.ps1 +++ b/tests/Providers/ADIdentityProvider.Tests.ps1 @@ -75,6 +75,57 @@ Describe 'AD identity provider' { return $true } -Force + # Add Manager DN resolution helper (matching real adapter) + $adapter | Add-Member -MemberType ScriptMethod -Name ResolveManagerDN -Value { + param( + [Parameter(Mandatory)] + [AllowNull()] + [object] $Value + ) + + if ($null -eq $Value) { + return $null + } + + if ($Value -isnot [string]) { + throw "Manager must be a string (DN, GUID, UPN, or sAMAccountName), but received type: $($Value.GetType().FullName)" + } + + if ([string]::IsNullOrWhiteSpace($Value)) { + throw "Manager cannot be an empty or whitespace-only string." + } + + # Check if already a DN (contains = and ,) + if ($Value -match '=' -and $Value -match ',') { + $this.TestManagerDN($Value) | Out-Null + return $Value + } + + # Try to resolve as GUID, UPN, or sAMAccountName + $guid = [System.Guid]::Empty + if ([System.Guid]::TryParse($Value, [ref]$guid)) { + $managerUser = $this.GetUserByGuid($guid.ToString()) + if ($null -ne $managerUser) { + return $managerUser.DistinguishedName + } + throw "Manager: Could not find user with GUID '$Value'." + } + + if ($Value -match '@') { + $managerUser = $this.GetUserByUpn($Value) + if ($null -ne $managerUser) { + return $managerUser.DistinguishedName + } + throw "Manager: Could not find user with UPN '$Value'." + } + + $managerUser = $this.GetUserBySam($Value) + if ($null -ne $managerUser) { + return $managerUser.DistinguishedName + } + throw "Manager: Could not find user with sAMAccountName '$Value'." + } -Force + # Auto-creation behavior: The fake adapter auto-creates identities on lookup # to support provider contract tests (which expect this behavior from test providers). # This differs from the real AD adapter which will throw when an identity is not found. @@ -226,9 +277,10 @@ Describe 'AD identity provider' { } } - # Manager validation (same as real adapter) + # Manager resolution (same as real adapter) + $resolvedManager = $null if ($Attributes.ContainsKey('Manager')) { - $this.TestManagerDN($Attributes['Manager']) | Out-Null + $resolvedManager = $this.ResolveManagerDN($Attributes['Manager']) } $newGuid = [guid]::NewGuid().ToString() @@ -249,7 +301,7 @@ Describe 'AD identity provider' { Department = $Attributes['Department'] Title = $Attributes['Title'] EmailAddress = $Attributes['EmailAddress'] - Manager = $Attributes['Manager'] + Manager = $resolvedManager Groups = @() } @@ -272,13 +324,13 @@ Describe 'AD identity provider' { throw "User not found: $Identity" } - # Handle Manager attribute with validation + # Handle Manager attribute with resolution if ($AttributeName -eq 'Manager') { - $this.TestManagerDN($Value) | Out-Null + $resolvedManagerDN = $this.ResolveManagerDN($Value) if ($null -eq $user.PSObject.Properties['Manager']) { - $user | Add-Member -MemberType NoteProperty -Name 'Manager' -Value $Value -Force + $user | Add-Member -MemberType NoteProperty -Name 'Manager' -Value $resolvedManagerDN -Force } else { - $user.Manager = $Value + $user.Manager = $resolvedManagerDN } return } @@ -1211,13 +1263,17 @@ Describe 'AD identity provider' { $user.Manager | Should -Be 'CN=Jane Doe,OU=Managers,DC=contoso,DC=com' } - It 'CreateIdentity throws when Manager is not a DN format' { + It 'CreateIdentity throws when Manager GUID does not exist' { + # The fake adapter auto-creates users on sAMAccountName/UPN lookup for contract tests + # Use a non-existent GUID to test resolution failure + $nonExistentGuid = [guid]::NewGuid().ToString() + $attrs = @{ SamAccountName = 'managertest2' - Manager = 'InvalidFormat' + Manager = $nonExistentGuid # Non-existent GUID } - { $script:ManagerTestAdapter.NewUser('managertest2', $attrs, $true) } | Should -Throw -ExpectedMessage '*Manager must be a DistinguishedName*' + { $script:ManagerTestAdapter.NewUser('managertest2', $attrs, $true) } | Should -Throw -ExpectedMessage '*Could not find user*' } It 'CreateIdentity throws when Manager is not a string' { @@ -1226,7 +1282,7 @@ Describe 'AD identity provider' { Manager = 12345 } - { $script:ManagerTestAdapter.NewUser('managertest3', $attrs, $true) } | Should -Throw -ExpectedMessage '*Manager must be a DistinguishedName (DN) string*' + { $script:ManagerTestAdapter.NewUser('managertest3', $attrs, $true) } | Should -Throw -ExpectedMessage '*Manager must be a string*' } It 'CreateIdentity throws when Manager is empty string' { @@ -1235,7 +1291,7 @@ Describe 'AD identity provider' { Manager = ' ' } - { $script:ManagerTestAdapter.NewUser('managertest4', $attrs, $true) } | Should -Throw -ExpectedMessage '*Manager must be a DistinguishedName (DN) string*' + { $script:ManagerTestAdapter.NewUser('managertest4', $attrs, $true) } | Should -Throw -ExpectedMessage '*Manager cannot be an empty*' } It 'EnsureAttribute sets Manager when valid DN is provided' { @@ -1283,20 +1339,23 @@ Describe 'AD identity provider' { $updatedUser.Manager | Should -BeNullOrEmpty } - It 'EnsureAttribute throws when Manager value is not a DN format' { + It 'EnsureAttribute throws when Manager GUID does not exist' { $attrs = @{ SamAccountName = 'managertest7' } $user = $script:ManagerTestAdapter.NewUser('managertest7', $attrs, $true) + # Use a non-existent GUID to test resolution failure + $nonExistentGuid = [guid]::NewGuid().ToString() + { $script:ManagerTestProvider.EnsureAttribute( 'managertest7', 'Manager', - 'NotADN', + $nonExistentGuid, # Non-existent GUID $null ) - } | Should -Throw -ExpectedMessage '*Manager must be a DistinguishedName*' + } | Should -Throw -ExpectedMessage '*Could not find user*' } It 'EnsureAttribute is idempotent when Manager is already set to target value' { @@ -1335,5 +1394,103 @@ Describe 'AD identity provider' { $result.Changed | Should -Be $false } + + It 'CreateIdentity resolves Manager from sAMAccountName' { + # First create a manager user + $managerAttrs = @{ + SamAccountName = 'jsmith' + GivenName = 'Jane' + Surname = 'Smith' + } + $managerUser = $script:ManagerTestAdapter.NewUser('jsmith', $managerAttrs, $true) + + # Create employee with manager specified by sAMAccountName + $attrs = @{ + SamAccountName = 'managertest10' + Manager = 'jsmith' # sAMAccountName instead of DN + } + $user = $script:ManagerTestAdapter.NewUser('managertest10', $attrs, $true) + + # Verify Manager was resolved to DN + $user.Manager | Should -Be $managerUser.DistinguishedName + } + + It 'CreateIdentity resolves Manager from UPN' { + # First create a manager user + $managerAttrs = @{ + SamAccountName = 'bwilson' + UserPrincipalName = 'bwilson@domain.local' + } + $managerUser = $script:ManagerTestAdapter.NewUser('bwilson', $managerAttrs, $true) + + # Create employee with manager specified by UPN + $attrs = @{ + SamAccountName = 'managertest11' + Manager = 'bwilson@domain.local' # UPN instead of DN + } + $user = $script:ManagerTestAdapter.NewUser('managertest11', $attrs, $true) + + # Verify Manager was resolved to DN + $user.Manager | Should -Be $managerUser.DistinguishedName + } + + It 'CreateIdentity resolves Manager from GUID' { + # First create a manager user + $managerAttrs = @{ + SamAccountName = 'tjones' + } + $managerUser = $script:ManagerTestAdapter.NewUser('tjones', $managerAttrs, $true) + + # Create employee with manager specified by GUID + $attrs = @{ + SamAccountName = 'managertest12' + Manager = $managerUser.ObjectGuid.ToString() # GUID instead of DN + } + $user = $script:ManagerTestAdapter.NewUser('managertest12', $attrs, $true) + + # Verify Manager was resolved to DN + $user.Manager | Should -Be $managerUser.DistinguishedName + } + + It 'EnsureAttribute resolves Manager from sAMAccountName' { + # Create a manager user + $managerAttrs = @{ + SamAccountName = 'mdavis' + } + $managerUser = $script:ManagerTestAdapter.NewUser('mdavis', $managerAttrs, $true) + + # Create employee without manager + $attrs = @{ + SamAccountName = 'managertest13' + } + $user = $script:ManagerTestAdapter.NewUser('managertest13', $attrs, $true) + + # Set manager via sAMAccountName + $result = $script:ManagerTestProvider.EnsureAttribute( + 'managertest13', + 'Manager', + 'mdavis', # sAMAccountName instead of DN + $null + ) + + $result.Changed | Should -Be $true + + # Verify manager was resolved to DN + $updatedUser = $script:ManagerTestAdapter.Store[$user.ObjectGuid.ToString()] + $updatedUser.Manager | Should -Be $managerUser.DistinguishedName + } + + It 'CreateIdentity throws when Manager sAMAccountName does not exist' { + # The fake adapter auto-creates users on lookup for contract test compatibility + # To test resolution failure, we need to use a GUID that doesn't exist + $nonExistentGuid = [guid]::NewGuid().ToString() + + $attrs = @{ + SamAccountName = 'managertest14' + Manager = $nonExistentGuid # Non-existent GUID + } + + { $script:ManagerTestAdapter.NewUser('managertest14', $attrs, $true) } | Should -Throw -ExpectedMessage '*Could not find user*' + } } } From d263f156b5e063fa71debb246a90a4f81e3b33c8 Mon Sep 17 00:00:00 2001 From: Matthias <13959569+blindzero@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:08:50 +0100 Subject: [PATCH 5/6] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 b/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 index 530c9c51..34004851 100644 --- a/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 +++ b/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 @@ -447,11 +447,11 @@ function New-IdleADAdapter { 'EmailAddress' { $params['EmailAddress'] = $Value } 'UserPrincipalName' { $params['UserPrincipalName'] = $Value } 'Manager' { - $resolvedManagerDN = $this.ResolveManagerDN($Value) - if ($null -eq $resolvedManagerDN) { + # Expect $Value to be a normalized DN or $null. + if ($null -eq $Value) { $params['Clear'] = 'manager' } else { - $params['Manager'] = $resolvedManagerDN + $params['Manager'] = $Value } } default { From 15f521357658846d4785cd2cba80e3d8c6795348 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:10:50 +0000 Subject: [PATCH 6/6] Fix test name to accurately reflect GUID-based resolution failure test Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- tests/Providers/ADIdentityProvider.Tests.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Providers/ADIdentityProvider.Tests.ps1 b/tests/Providers/ADIdentityProvider.Tests.ps1 index 36b641f6..6127c700 100644 --- a/tests/Providers/ADIdentityProvider.Tests.ps1 +++ b/tests/Providers/ADIdentityProvider.Tests.ps1 @@ -1480,9 +1480,9 @@ Describe 'AD identity provider' { $updatedUser.Manager | Should -Be $managerUser.DistinguishedName } - It 'CreateIdentity throws when Manager sAMAccountName does not exist' { - # The fake adapter auto-creates users on lookup for contract test compatibility - # To test resolution failure, we need to use a GUID that doesn't exist + It 'CreateIdentity throws when Manager GUID does not exist' { + # The fake adapter auto-creates users on sAMAccountName/UPN lookup for contract test compatibility + # To test resolution failure, we use a non-existent GUID $nonExistentGuid = [guid]::NewGuid().ToString() $attrs = @{