From 90706f7894160ccdd2438355f639211840d0a89d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 16:15:16 +0000 Subject: [PATCH 1/9] Initial plan From 8dd743be367774a9264efffc7c751a88f8399d35 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 16:21:54 +0000 Subject: [PATCH 2/9] Add password support to AD Provider adapter and redaction - Updated New-IdleADAdapter.ps1 NewUser method to support three password input modes: * SecureString input (direct use) * ProtectedString input (from ConvertFrom-SecureString) * Explicit plaintext via AccountPasswordAsPlainText - Added validation to reject ambiguous input (both password fields) - Added clear error messages for invalid password formats - Updated Copy-IdleRedactedObject.ps1 to redact AccountPassword and AccountPasswordAsPlainText keys - Updated fake adapter in tests to mirror password validation - Added comprehensive password handling tests - Added redaction tests for new password keys Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- .../Private/Copy-IdleRedactedObject.ps1 | 4 +- .../Private/New-IdleADAdapter.ps1 | 47 +++++ tests/Core/Copy-IdleRedactedObject.Tests.ps1 | 48 +++++ tests/Providers/ADIdentityProvider.Tests.ps1 | 187 +++++++++++++++++- 4 files changed, 284 insertions(+), 2 deletions(-) diff --git a/src/IdLE.Core/Private/Copy-IdleRedactedObject.ps1 b/src/IdLE.Core/Private/Copy-IdleRedactedObject.ps1 index 6c5b1e3a..8dabd206 100644 --- a/src/IdLE.Core/Private/Copy-IdleRedactedObject.ps1 +++ b/src/IdLE.Core/Private/Copy-IdleRedactedObject.ps1 @@ -28,7 +28,9 @@ function Copy-IdleRedactedObject { 'accessToken', 'refreshToken', 'credential', - 'privateKey' + 'privateKey', + 'AccountPassword', + 'AccountPasswordAsPlainText' ) $effectiveKeys = if ($null -ne $RedactedKeys -and $RedactedKeys.Count -gt 0) { diff --git a/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 b/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 index f3e49383..ba6e8ef7 100644 --- a/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 +++ b/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 @@ -171,6 +171,53 @@ function New-IdleADAdapter { $params['EmailAddress'] = $Attributes['EmailAddress'] } + # Password handling: support SecureString, ProtectedString, and explicit PlainText + $hasAccountPassword = $Attributes.ContainsKey('AccountPassword') + $hasAccountPasswordAsPlainText = $Attributes.ContainsKey('AccountPasswordAsPlainText') + + if ($hasAccountPassword -and $hasAccountPasswordAsPlainText) { + throw "Ambiguous password configuration: both 'AccountPassword' and 'AccountPasswordAsPlainText' are provided. Use only one." + } + + if ($hasAccountPassword) { + $passwordValue = $Attributes['AccountPassword'] + + if ($passwordValue -is [securestring]) { + # Mode 1: SecureString - use directly + $params['AccountPassword'] = $passwordValue + } + elseif ($passwordValue -is [string]) { + # Mode 2: ProtectedString (from ConvertFrom-SecureString) + try { + $params['AccountPassword'] = ConvertTo-SecureString -String $passwordValue -ErrorAction Stop + } + catch { + $errorMsg = "AccountPassword: Expected a ProtectedString (output from ConvertFrom-SecureString) but conversion failed. " + $errorMsg += "ProtectedString only works when encryption and decryption occur under the same Windows user and machine (DPAPI scope). " + $errorMsg += "Original error: $_" + throw $errorMsg + } + } + else { + throw "AccountPassword: Expected a SecureString or ProtectedString (string from ConvertFrom-SecureString), but received type: $($passwordValue.GetType().FullName)" + } + } + + if ($hasAccountPasswordAsPlainText) { + $plainTextPassword = $Attributes['AccountPasswordAsPlainText'] + + if ($plainTextPassword -isnot [string]) { + throw "AccountPasswordAsPlainText: Expected a string but received type: $($plainTextPassword.GetType().FullName)" + } + + if ([string]::IsNullOrWhiteSpace($plainTextPassword)) { + throw "AccountPasswordAsPlainText: Password cannot be null or empty." + } + + # Mode 3: Explicit plaintext - convert with -AsPlainText + $params['AccountPassword'] = ConvertTo-SecureString -String $plainTextPassword -AsPlainText -Force + } + if ($null -ne $this.Credential) { $params['Credential'] = $this.Credential } diff --git a/tests/Core/Copy-IdleRedactedObject.Tests.ps1 b/tests/Core/Copy-IdleRedactedObject.Tests.ps1 index b124a588..fa19ebab 100644 --- a/tests/Core/Copy-IdleRedactedObject.Tests.ps1 +++ b/tests/Core/Copy-IdleRedactedObject.Tests.ps1 @@ -112,5 +112,53 @@ Describe 'Copy-IdleRedactedObject - deterministic redaction utility' { $copy.password | Should -Be '' } + + It 'redacts AccountPassword key' { + $input = @{ + userName = 'testuser' + AccountPassword = 'ProtectedStringValue' + otherField = 'visible' + } + + $copy = Copy-IdleRedactedObject -Value $input + + $copy.userName | Should -Be 'testuser' + $copy.AccountPassword | Should -Be '[REDACTED]' + $copy.otherField | Should -Be 'visible' + } + + It 'redacts AccountPasswordAsPlainText key' { + $input = @{ + userName = 'testuser' + AccountPasswordAsPlainText = 'PlainTextPassword' + otherField = 'visible' + } + + $copy = Copy-IdleRedactedObject -Value $input + + $copy.userName | Should -Be 'testuser' + $copy.AccountPasswordAsPlainText | Should -Be '[REDACTED]' + $copy.otherField | Should -Be 'visible' + } + + It 'redacts both password fields if present in nested structure' { + $input = @{ + userDetails = @{ + name = 'alice' + AccountPassword = 'SecureValue' + } + settings = [pscustomobject]@{ + AccountPasswordAsPlainText = 'PlainTextValue' + enabled = $true + } + } + + $copy = Copy-IdleRedactedObject -Value $input + + $copy.userDetails.name | Should -Be 'alice' + $copy.userDetails.AccountPassword | Should -Be '[REDACTED]' + $copy.settings.AccountPasswordAsPlainText | Should -Be '[REDACTED]' + $copy.settings.enabled | Should -Be $true + } } } diff --git a/tests/Providers/ADIdentityProvider.Tests.ps1 b/tests/Providers/ADIdentityProvider.Tests.ps1 index 749ca0fe..027ac75a 100644 --- a/tests/Providers/ADIdentityProvider.Tests.ps1 +++ b/tests/Providers/ADIdentityProvider.Tests.ps1 @@ -46,6 +46,8 @@ Describe 'AD identity provider' { # 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. + # However, we only auto-create for GetUserByUpn to support contract tests, + # while GetUserBySam returns null to enable proper CreateIdentity testing. $adapter | Add-Member -MemberType ScriptMethod -Name GetUserByUpn -Value { param([string]$Upn) @@ -54,7 +56,27 @@ Describe 'AD identity provider' { return $this.Store[$key] } } - return $null + + # Auto-create for contract test compatibility + $sam = $Upn -replace '@.*$', '' + $guid = [guid]::NewGuid().ToString() + $user = [pscustomobject]@{ + ObjectGuid = [guid]$guid + sAMAccountName = $sam + UserPrincipalName = $Upn + DistinguishedName = "CN=$sam,OU=Users,DC=domain,DC=local" + Enabled = $true + GivenName = $null + Surname = $null + DisplayName = $null + Description = $null + Department = $null + Title = $null + EmailAddress = $null + Groups = @() + } + $this.Store[$guid] = $user + return $user } -Force $adapter | Add-Member -MemberType ScriptMethod -Name GetUserBySam -Value { @@ -97,6 +119,49 @@ Describe 'AD identity provider' { $adapter | Add-Member -MemberType ScriptMethod -Name NewUser -Value { param([string]$Name, [hashtable]$Attributes, [bool]$Enabled) + # Password handling validation (same as real adapter) + $hasAccountPassword = $Attributes.ContainsKey('AccountPassword') + $hasAccountPasswordAsPlainText = $Attributes.ContainsKey('AccountPasswordAsPlainText') + + if ($hasAccountPassword -and $hasAccountPasswordAsPlainText) { + throw "Ambiguous password configuration: both 'AccountPassword' and 'AccountPasswordAsPlainText' are provided. Use only one." + } + + if ($hasAccountPassword) { + $passwordValue = $Attributes['AccountPassword'] + + if ($passwordValue -is [securestring]) { + # Valid: SecureString + } + elseif ($passwordValue -is [string]) { + # Valid: ProtectedString - test conversion + try { + $null = ConvertTo-SecureString -String $passwordValue -ErrorAction Stop + } + catch { + $errorMsg = "AccountPassword: Expected a ProtectedString (output from ConvertFrom-SecureString) but conversion failed. " + $errorMsg += "ProtectedString only works when encryption and decryption occur under the same Windows user and machine (DPAPI scope). " + $errorMsg += "Original error: $_" + throw $errorMsg + } + } + else { + throw "AccountPassword: Expected a SecureString or ProtectedString (string from ConvertFrom-SecureString), but received type: $($passwordValue.GetType().FullName)" + } + } + + if ($hasAccountPasswordAsPlainText) { + $plainTextPassword = $Attributes['AccountPasswordAsPlainText'] + + if ($plainTextPassword -isnot [string]) { + throw "AccountPasswordAsPlainText: Expected a string but received type: $($plainTextPassword.GetType().FullName)" + } + + if ([string]::IsNullOrWhiteSpace($plainTextPassword)) { + throw "AccountPasswordAsPlainText: Password cannot be null or empty." + } + } + $guid = [guid]::NewGuid().ToString() $sam = if ($Attributes.ContainsKey('SamAccountName')) { $Attributes['SamAccountName'] } else { $Name } $upn = if ($Attributes.ContainsKey('UserPrincipalName')) { $Attributes['UserPrincipalName'] } else { "$sam@domain.local" } @@ -681,4 +746,124 @@ Describe 'AD identity provider' { { $provider.DeleteIdentity($guid) } | Should -Throw -ExpectedMessage '*AllowDelete*' } } + + Context 'Password handling' { + BeforeEach { + $adapter = New-FakeADAdapter + $provider = New-IdleADIdentityProvider -Adapter $adapter + $script:TestProvider = $provider + $script:TestAdapter = $adapter + } + + It 'CreateIdentity accepts AccountPassword as SecureString' { + $password = ConvertTo-SecureString -String 'TestPass123!' -AsPlainText -Force + $attrs = @{ + SamAccountName = 'pwtest1' + AccountPassword = $password + } + + # Should not throw - validation passes + $result = $script:TestProvider.CreateIdentity('pwtest1', $attrs) + $result | Should -Not -BeNullOrEmpty + $result.IdentityKey | Should -Be 'pwtest1' + } + + It 'CreateIdentity accepts AccountPassword as ProtectedString' { + # Create a ProtectedString using ConvertFrom-SecureString + $securePassword = ConvertTo-SecureString -String 'TestPass456!' -AsPlainText -Force + $protectedString = ConvertFrom-SecureString -SecureString $securePassword + + $attrs = @{ + SamAccountName = 'pwtest2' + AccountPassword = $protectedString + } + + # Should not throw - validation passes + $result = $script:TestProvider.CreateIdentity('pwtest2', $attrs) + $result | Should -Not -BeNullOrEmpty + $result.IdentityKey | Should -Be 'pwtest2' + } + + It 'CreateIdentity accepts AccountPasswordAsPlainText' { + $attrs = @{ + SamAccountName = 'pwtest3' + AccountPasswordAsPlainText = 'PlainTextPass789!' + } + + # Should not throw - validation passes + $result = $script:TestProvider.CreateIdentity('pwtest3', $attrs) + $result | Should -Not -BeNullOrEmpty + $result.IdentityKey | Should -Be 'pwtest3' + } + + It 'Throws when both AccountPassword and AccountPasswordAsPlainText are provided' { + $password = ConvertTo-SecureString -String 'TestPass!' -AsPlainText -Force + $attrs = @{ + SamAccountName = 'pwtest4' + AccountPassword = $password + AccountPasswordAsPlainText = 'PlainText!' + } + + # Test the adapter's NewUser method directly to verify validation + { $script:TestAdapter.NewUser('pwtest4', $attrs, $true) } | + Should -Throw -ExpectedMessage '*Ambiguous password configuration*' + } + + It 'Throws when AccountPassword has invalid type' { + $attrs = @{ + SamAccountName = 'pwtest5' + AccountPassword = 12345 # Invalid: should be string or SecureString + } + + # Test the adapter's NewUser method directly to verify validation + { $script:TestAdapter.NewUser('pwtest5', $attrs, $true) } | + Should -Throw -ExpectedMessage '*Expected a SecureString or ProtectedString*' + } + + It 'Throws when AccountPassword string is not a valid ProtectedString' { + $attrs = @{ + SamAccountName = 'pwtest6' + AccountPassword = 'NotAProtectedString' # Plain string, not from ConvertFrom-SecureString + } + + # Test the adapter's NewUser method directly to verify validation + { $script:TestAdapter.NewUser('pwtest6', $attrs, $true) } | + Should -Throw -ExpectedMessage '*conversion failed*' + } + + It 'Throws when AccountPasswordAsPlainText has invalid type' { + $attrs = @{ + SamAccountName = 'pwtest7' + AccountPasswordAsPlainText = 12345 # Invalid: should be string + } + + # Test the adapter's NewUser method directly to verify validation + { $script:TestAdapter.NewUser('pwtest7', $attrs, $true) } | + Should -Throw -ExpectedMessage '*Expected a string but received type*' + } + + It 'Throws when AccountPasswordAsPlainText is empty' { + $attrs = @{ + SamAccountName = 'pwtest8' + AccountPasswordAsPlainText = '' + } + + # Test the adapter's NewUser method directly to verify validation + { $script:TestAdapter.NewUser('pwtest8', $attrs, $true) } | + Should -Throw -ExpectedMessage '*cannot be null or empty*' + } + + It 'CreateIdentity works without password (existing behavior)' { + $attrs = @{ + SamAccountName = 'pwtest9' + GivenName = 'Test' + Surname = 'User' + } + + # Should not throw - no password is valid + $result = $script:TestProvider.CreateIdentity('pwtest9', $attrs) + $result | Should -Not -BeNullOrEmpty + $result.IdentityKey | Should -Be 'pwtest9' + } + } } From 68857bdf2336485b65739b4b7a639cde2e6d21c9 Mon Sep 17 00:00:00 2001 From: Matthias <13959569+blindzero@users.noreply.github.com> Date: Sun, 8 Feb 2026 17:47:36 +0100 Subject: [PATCH 3/9] Update src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 b/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 index ba6e8ef7..0da611c0 100644 --- a/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 +++ b/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 @@ -194,7 +194,9 @@ function New-IdleADAdapter { catch { $errorMsg = "AccountPassword: Expected a ProtectedString (output from ConvertFrom-SecureString) but conversion failed. " $errorMsg += "ProtectedString only works when encryption and decryption occur under the same Windows user and machine (DPAPI scope). " - $errorMsg += "Original error: $_" + if ($null -ne $_.Exception) { + $errorMsg += " Inner exception type: $($PSItem.Exception.GetType().FullName)." + } throw $errorMsg } } From df9a943dfe43b95621e9387a63584f68f3b69c77 Mon Sep 17 00:00:00 2001 From: Matthias <13959569+blindzero@users.noreply.github.com> Date: Sun, 8 Feb 2026 17:48:03 +0100 Subject: [PATCH 4/9] Update tests/Providers/ADIdentityProvider.Tests.ps1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/Providers/ADIdentityProvider.Tests.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/Providers/ADIdentityProvider.Tests.ps1 b/tests/Providers/ADIdentityProvider.Tests.ps1 index 027ac75a..584ccc57 100644 --- a/tests/Providers/ADIdentityProvider.Tests.ps1 +++ b/tests/Providers/ADIdentityProvider.Tests.ps1 @@ -46,8 +46,9 @@ Describe 'AD identity provider' { # 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. - # However, we only auto-create for GetUserByUpn to support contract tests, - # while GetUserBySam returns null to enable proper CreateIdentity testing. + # Note: This fake adapter may auto-create identities for multiple lookup methods + # (e.g., UPN and sAMAccountName), so tests that rely on non-existence should not + # assume that a given lookup will return $null as the real adapter would. $adapter | Add-Member -MemberType ScriptMethod -Name GetUserByUpn -Value { param([string]$Upn) From 9bdb9a27635c80195003993fe5bf551770f03f3a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 16:53:23 +0000 Subject: [PATCH 5/9] Add null checks for password validation and null value tests - Added explicit null checks before GetType() calls in both real and fake adapters - Prevents unhelpful null-valued expression errors - Added tests for AccountPassword and AccountPasswordAsPlainText null values - Clear error messages guide users to provide valid values Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- .../Private/New-IdleADAdapter.ps1 | 8 +++++ tests/Providers/ADIdentityProvider.Tests.ps1 | 36 +++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 b/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 index 0da611c0..80f865b8 100644 --- a/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 +++ b/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 @@ -182,6 +182,10 @@ function New-IdleADAdapter { if ($hasAccountPassword) { $passwordValue = $Attributes['AccountPassword'] + if ($null -eq $passwordValue) { + throw "AccountPassword: Value cannot be null. Provide a SecureString or ProtectedString (from ConvertFrom-SecureString)." + } + if ($passwordValue -is [securestring]) { # Mode 1: SecureString - use directly $params['AccountPassword'] = $passwordValue @@ -208,6 +212,10 @@ function New-IdleADAdapter { if ($hasAccountPasswordAsPlainText) { $plainTextPassword = $Attributes['AccountPasswordAsPlainText'] + if ($null -eq $plainTextPassword) { + throw "AccountPasswordAsPlainText: Value cannot be null. Provide a non-empty plaintext password string." + } + if ($plainTextPassword -isnot [string]) { throw "AccountPasswordAsPlainText: Expected a string but received type: $($plainTextPassword.GetType().FullName)" } diff --git a/tests/Providers/ADIdentityProvider.Tests.ps1 b/tests/Providers/ADIdentityProvider.Tests.ps1 index 584ccc57..0c71a20d 100644 --- a/tests/Providers/ADIdentityProvider.Tests.ps1 +++ b/tests/Providers/ADIdentityProvider.Tests.ps1 @@ -131,6 +131,10 @@ Describe 'AD identity provider' { if ($hasAccountPassword) { $passwordValue = $Attributes['AccountPassword'] + if ($null -eq $passwordValue) { + throw "AccountPassword: Value cannot be null. Provide a SecureString or ProtectedString (from ConvertFrom-SecureString)." + } + if ($passwordValue -is [securestring]) { # Valid: SecureString } @@ -154,6 +158,10 @@ Describe 'AD identity provider' { if ($hasAccountPasswordAsPlainText) { $plainTextPassword = $Attributes['AccountPasswordAsPlainText'] + if ($null -eq $plainTextPassword) { + throw "AccountPasswordAsPlainText: Value cannot be null. Provide a non-empty plaintext password string." + } + if ($plainTextPassword -isnot [string]) { throw "AccountPasswordAsPlainText: Expected a string but received type: $($plainTextPassword.GetType().FullName)" } @@ -854,17 +862,39 @@ Describe 'AD identity provider' { Should -Throw -ExpectedMessage '*cannot be null or empty*' } - It 'CreateIdentity works without password (existing behavior)' { + It 'Throws when AccountPassword is null' { $attrs = @{ SamAccountName = 'pwtest9' + AccountPassword = $null + } + + # Test the adapter's NewUser method directly to verify validation + { $script:TestAdapter.NewUser('pwtest9', $attrs, $true) } | + Should -Throw -ExpectedMessage '*Value cannot be null*' + } + + It 'Throws when AccountPasswordAsPlainText is null' { + $attrs = @{ + SamAccountName = 'pwtest10' + AccountPasswordAsPlainText = $null + } + + # Test the adapter's NewUser method directly to verify validation + { $script:TestAdapter.NewUser('pwtest10', $attrs, $true) } | + Should -Throw -ExpectedMessage '*Value cannot be null*' + } + + It 'CreateIdentity works without password (existing behavior)' { + $attrs = @{ + SamAccountName = 'pwtest11' GivenName = 'Test' Surname = 'User' } # Should not throw - no password is valid - $result = $script:TestProvider.CreateIdentity('pwtest9', $attrs) + $result = $script:TestProvider.CreateIdentity('pwtest11', $attrs) $result | Should -Not -BeNullOrEmpty - $result.IdentityKey | Should -Be 'pwtest9' + $result.IdentityKey | Should -Be 'pwtest11' } } } From 7b18ea141368ef615b48c1fa3b9af6835e36fdd3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 16:54:22 +0000 Subject: [PATCH 6/9] Add clarifying comments to password acceptance tests - Explained why Changed=true is not asserted in valid password tests - Noted that fake adapter auto-creates during ResolveIdentity lookup - Clarified that password validation is tested via error-throwing tests that call NewUser directly Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- tests/Providers/ADIdentityProvider.Tests.ps1 | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/Providers/ADIdentityProvider.Tests.ps1 b/tests/Providers/ADIdentityProvider.Tests.ps1 index 0c71a20d..406d6064 100644 --- a/tests/Providers/ADIdentityProvider.Tests.ps1 +++ b/tests/Providers/ADIdentityProvider.Tests.ps1 @@ -771,7 +771,9 @@ Describe 'AD identity provider' { AccountPassword = $password } - # Should not throw - validation passes + # Note: This test verifies that valid SecureString passwords are accepted without throwing. + # The fake adapter auto-creates users during ResolveIdentity, so CreateIdentity returns + # Changed=false. Password validation is tested directly via NewUser in error-throwing tests. $result = $script:TestProvider.CreateIdentity('pwtest1', $attrs) $result | Should -Not -BeNullOrEmpty $result.IdentityKey | Should -Be 'pwtest1' @@ -787,7 +789,9 @@ Describe 'AD identity provider' { AccountPassword = $protectedString } - # Should not throw - validation passes + # Note: This test verifies that valid ProtectedString passwords are accepted without throwing. + # The fake adapter auto-creates users during ResolveIdentity, so CreateIdentity returns + # Changed=false. Password validation is tested directly via NewUser in error-throwing tests. $result = $script:TestProvider.CreateIdentity('pwtest2', $attrs) $result | Should -Not -BeNullOrEmpty $result.IdentityKey | Should -Be 'pwtest2' @@ -799,7 +803,9 @@ Describe 'AD identity provider' { AccountPasswordAsPlainText = 'PlainTextPass789!' } - # Should not throw - validation passes + # Note: This test verifies that valid plaintext passwords are accepted without throwing. + # The fake adapter auto-creates users during ResolveIdentity, so CreateIdentity returns + # Changed=false. Password validation is tested directly via NewUser in error-throwing tests. $result = $script:TestProvider.CreateIdentity('pwtest3', $attrs) $result | Should -Not -BeNullOrEmpty $result.IdentityKey | Should -Be 'pwtest3' From 8b2c57669b660c8d7157c8ab5fa7cc06e6386762 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 17:06:02 +0000 Subject: [PATCH 7/9] Improve error message clarity and test password handling directly - Updated ProtectedString error message to clarify only DPAPI-scoped strings are supported - Explicitly state that key-based protected strings are not supported - Fixed wording from "Inner exception type" to "Exception type" - Include exception message for better troubleshooting - Changed valid password tests to call NewUser directly instead of CreateIdentity - This ensures password handling code is actually exercised in tests Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- .../Private/New-IdleADAdapter.ps1 | 10 ++++--- tests/Providers/ADIdentityProvider.Tests.ps1 | 27 +++++++++---------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 b/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 index 80f865b8..e75d8be7 100644 --- a/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 +++ b/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 @@ -196,10 +196,14 @@ function New-IdleADAdapter { $params['AccountPassword'] = ConvertTo-SecureString -String $passwordValue -ErrorAction Stop } catch { - $errorMsg = "AccountPassword: Expected a ProtectedString (output from ConvertFrom-SecureString) but conversion failed. " - $errorMsg += "ProtectedString only works when encryption and decryption occur under the same Windows user and machine (DPAPI scope). " + $errorMsg = "AccountPassword: Expected a ProtectedString (output from ConvertFrom-SecureString without -Key) but conversion failed. " + $errorMsg += "Only DPAPI-scoped ProtectedStrings are supported (created under the same Windows user and machine). " + $errorMsg += "Key-based protected strings (using -Key or -SecureKey) are not supported. " if ($null -ne $_.Exception) { - $errorMsg += " Inner exception type: $($PSItem.Exception.GetType().FullName)." + $errorMsg += "Exception type: $($PSItem.Exception.GetType().FullName). " + if (-not [string]::IsNullOrWhiteSpace($_.Exception.Message)) { + $errorMsg += "Message: $($_.Exception.Message)" + } } throw $errorMsg } diff --git a/tests/Providers/ADIdentityProvider.Tests.ps1 b/tests/Providers/ADIdentityProvider.Tests.ps1 index 406d6064..e95dfca7 100644 --- a/tests/Providers/ADIdentityProvider.Tests.ps1 +++ b/tests/Providers/ADIdentityProvider.Tests.ps1 @@ -771,12 +771,11 @@ Describe 'AD identity provider' { AccountPassword = $password } - # Note: This test verifies that valid SecureString passwords are accepted without throwing. - # The fake adapter auto-creates users during ResolveIdentity, so CreateIdentity returns - # Changed=false. Password validation is tested directly via NewUser in error-throwing tests. - $result = $script:TestProvider.CreateIdentity('pwtest1', $attrs) + # Test the adapter's NewUser method directly to verify password handling + # (CreateIdentity would skip NewUser due to fake adapter auto-creation) + $result = $script:TestAdapter.NewUser('pwtest1', $attrs, $true) $result | Should -Not -BeNullOrEmpty - $result.IdentityKey | Should -Be 'pwtest1' + $result.sAMAccountName | Should -Be 'pwtest1' } It 'CreateIdentity accepts AccountPassword as ProtectedString' { @@ -789,12 +788,11 @@ Describe 'AD identity provider' { AccountPassword = $protectedString } - # Note: This test verifies that valid ProtectedString passwords are accepted without throwing. - # The fake adapter auto-creates users during ResolveIdentity, so CreateIdentity returns - # Changed=false. Password validation is tested directly via NewUser in error-throwing tests. - $result = $script:TestProvider.CreateIdentity('pwtest2', $attrs) + # Test the adapter's NewUser method directly to verify password handling + # (CreateIdentity would skip NewUser due to fake adapter auto-creation) + $result = $script:TestAdapter.NewUser('pwtest2', $attrs, $true) $result | Should -Not -BeNullOrEmpty - $result.IdentityKey | Should -Be 'pwtest2' + $result.sAMAccountName | Should -Be 'pwtest2' } It 'CreateIdentity accepts AccountPasswordAsPlainText' { @@ -803,12 +801,11 @@ Describe 'AD identity provider' { AccountPasswordAsPlainText = 'PlainTextPass789!' } - # Note: This test verifies that valid plaintext passwords are accepted without throwing. - # The fake adapter auto-creates users during ResolveIdentity, so CreateIdentity returns - # Changed=false. Password validation is tested directly via NewUser in error-throwing tests. - $result = $script:TestProvider.CreateIdentity('pwtest3', $attrs) + # Test the adapter's NewUser method directly to verify password handling + # (CreateIdentity would skip NewUser due to fake adapter auto-creation) + $result = $script:TestAdapter.NewUser('pwtest3', $attrs, $true) $result | Should -Not -BeNullOrEmpty - $result.IdentityKey | Should -Be 'pwtest3' + $result.sAMAccountName | Should -Be 'pwtest3' } It 'Throws when both AccountPassword and AccountPasswordAsPlainText are provided' { From 68b9ea660f5a081e6842bc59b73af67ce9ad8983 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 17:25:48 +0000 Subject: [PATCH 8/9] Fix PSSA error: Add suppression for ConvertTo-SecureString -AsPlainText - Added function-level SuppressMessageAttribute for PSAvoidUsingConvertToSecureStringWithPlainText - This is an intentional, documented escape hatch for AccountPasswordAsPlainText - Password values are automatically redacted via Copy-IdleRedactedObject - All tests passing, no PSSA errors Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 b/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 index e75d8be7..d01070a4 100644 --- a/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 +++ b/src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1 @@ -9,8 +9,14 @@ function New-IdleADAdapter { .PARAMETER Credential Optional PSCredential for AD operations. If not provided, uses integrated auth. + + .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')] param( [Parameter()] [AllowNull()] @@ -229,6 +235,8 @@ function New-IdleADAdapter { } # Mode 3: Explicit plaintext - convert with -AsPlainText + # This is an intentional escape hatch with explicit opt-in via AccountPasswordAsPlainText. + # The value is redacted from logs/events via Copy-IdleRedactedObject. $params['AccountPassword'] = ConvertTo-SecureString -String $plainTextPassword -AsPlainText -Force } From 37ee5dca284e0491a1b56e3d67e86912fb6f52a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 17:32:31 +0000 Subject: [PATCH 9/9] Add PSSA suppression to test file for intentional plaintext conversions - Added file-level SuppressMessageAttribute to ADIdentityProvider.Tests.ps1 - Test file intentionally uses ConvertTo-SecureString -AsPlainText to create test data - This suppression is appropriate for test files where plaintext passwords are needed for validation - All tests pass, no PSSA errors in test file Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- tests/Providers/ADIdentityProvider.Tests.ps1 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/Providers/ADIdentityProvider.Tests.ps1 b/tests/Providers/ADIdentityProvider.Tests.ps1 index e95dfca7..4d48f45b 100644 --- a/tests/Providers/ADIdentityProvider.Tests.ps1 +++ b/tests/Providers/ADIdentityProvider.Tests.ps1 @@ -1,3 +1,8 @@ +# PSScriptAnalyzer suppression: Test file intentionally uses ConvertTo-SecureString -AsPlainText +# to create test data for password handling validation +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +param() + Set-StrictMode -Version Latest BeforeDiscovery { @@ -765,6 +770,7 @@ Describe 'AD identity provider' { } It 'CreateIdentity accepts AccountPassword as SecureString' { + # Test setup requires plaintext conversion - this is intentional for test data $password = ConvertTo-SecureString -String 'TestPass123!' -AsPlainText -Force $attrs = @{ SamAccountName = 'pwtest1' @@ -780,6 +786,7 @@ Describe 'AD identity provider' { It 'CreateIdentity accepts AccountPassword as ProtectedString' { # Create a ProtectedString using ConvertFrom-SecureString + # Test setup requires plaintext conversion - this is intentional for test data $securePassword = ConvertTo-SecureString -String 'TestPass456!' -AsPlainText -Force $protectedString = ConvertFrom-SecureString -SecureString $securePassword @@ -809,6 +816,7 @@ Describe 'AD identity provider' { } It 'Throws when both AccountPassword and AccountPasswordAsPlainText are provided' { + # Test setup requires plaintext conversion - this is intentional for test data $password = ConvertTo-SecureString -String 'TestPass!' -AsPlainText -Force $attrs = @{ SamAccountName = 'pwtest4'