Tests/Get-Secret.Tests.ps1
|
# Credit for most of this goes to https://github.com/JustinGrote/SecretManagement.KeePass/blob/main/SecretManagement.KeePass/Tests/Get-Secret.Tests.ps1 BeforeAll { Get-Module 'SecretManagement.CyberArk' | Remove-Module -Force Get-Module 'Microsoft.Powershell.SecretManagement' | Remove-Module -Force $ExtensionModule = Import-Module "$PSScriptRoot/../SecretManagement.CyberArk.Extension/*.psd1" -Force -PassThru $VaultName = 'CyberArk.Test' # Mock return objects built the way CredentialRetriever 4.0.0 builds them: a PSObject with ToSecureString and # ToCredential ScriptMethods attached via Add-Member. Returned as scriptblocks that are self-contained, so they # work as -MockWith whichever module instance SecretManagement loaded. function New-ProviderMock { param([string] $Kind, $UserName, $Secret) $SecretProperty = if ($Kind -eq 'CCP') { 'Content' } else { 'Password' } $Source = @" `$Result = [PSCustomObject]@{ UserName = `$UserName; $SecretProperty = `$Secret } `$Result | Add-Member -MemberType ScriptMethod -Name ToSecureString -Value { `$this.$SecretProperty | ConvertTo-SecureString -AsPlainText -Force } -Force `$Result | Add-Member -MemberType ScriptMethod -Name ToCredential -Value { New-Object System.Management.Automation.PSCredential(`$this.UserName, `$this.ToSecureString()) } -Force `$Result "@ $Prefix = '$UserName = {0}; $Secret = {1};' -f $(if ($null -eq $UserName) { '$null' } else { "'$UserName'" }), "'$Secret'" [scriptblock]::Create($Prefix + $Source) } } AfterAll { Remove-Module $ExtensionModule -Force } Describe 'Get-Secret' { Context 'when connection type is REST' { BeforeAll { $VaultParams = @{ConnectionType = 'REST' } Mock Get-PASSession -MockWith { [PSCustomObject]@{ BaseURI = 'https://pvwa' } } -ModuleName $ExtensionModule.Name } It 'should return a <PSType> for <SecretName>' { Mock Get-PASAccountPassword -MockWith { $Result = [PSCustomObject]@{ Password = 'fake' userName = 'localAdmin01' } $Result | Add-Member -MemberType ScriptMethod -Name 'ToSecureString' -Value { $this | Select-Object -ExpandProperty Password | ConvertTo-SecureString -AsPlainText -Force } return $Result } -ModuleName $ExtensionModule.Name Mock Get-PASAccount -MockWith { return [PSCustomObject]@{ name = 'localAdmin01' userName = 'localAdmin01' Id = '1' } } -ModuleName $ExtensionModule.Name $Secret = Get-Secret -Name $SecretName -VaultName $VaultName -AdditionalParameters $VaultParams $Secret | Should -Not -BeNullOrEmpty $Secret | Should -BeOfType $PSType } -TestCases ( @{SecretName = 'localAdmin01'; PSType = 'System.Management.Automation.PSCredential' } ) Context 'when the search returns several accounts' { BeforeEach { Mock Get-PASAccountPassword -MockWith { $Result = [PSCustomObject]@{ Password = 'fake'; userName = 'admin' } $Result | Add-Member -MemberType ScriptMethod -Name 'ToSecureString' -Value { $this | Select-Object -ExpandProperty Password | ConvertTo-SecureString -AsPlainText -Force } return $Result } -ModuleName $ExtensionModule.Name } It 'returns null and does not retrieve a password when only substring matches are found' { Mock Get-PASAccount -MockWith { @( [PSCustomObject]@{ name = 'localAdmin01'; userName = 'localAdmin01'; Id = '1' }, [PSCustomObject]@{ name = 'localAdmin02'; userName = 'localAdmin02'; Id = '2' } ) } -ModuleName $ExtensionModule.Name Get-Secret -Name 'admin' -VaultName $VaultName -AdditionalParameters $VaultParams | Should -BeNullOrEmpty Should -Invoke -CommandName Get-PASAccountPassword -ModuleName $ExtensionModule.Name -Times 0 -Exactly } It 'writes an error and does not retrieve a password when more than one exact match is found' { Mock Get-PASAccount -MockWith { @( [PSCustomObject]@{ name = 'admin'; userName = 'admin'; Id = '1' }, [PSCustomObject]@{ name = 'ADMIN'; userName = 'ADMIN'; Id = '2' } ) } -ModuleName $ExtensionModule.Name Get-Secret -Name 'admin' -VaultName $VaultName -AdditionalParameters $VaultParams -ErrorAction SilentlyContinue -ErrorVariable Err -WarningAction SilentlyContinue -WarningVariable Warn | Should -BeNullOrEmpty "$Err" | Should -BeLike '*2*admin*' $Warn | Should -BeNullOrEmpty Should -Invoke -CommandName Get-PASAccountPassword -ModuleName $ExtensionModule.Name -Times 0 -Exactly } It 'returns the single exact match among substring matches' { Mock Get-PASAccount -MockWith { @( [PSCustomObject]@{ name = 'admin2'; userName = 'admin2'; Id = '1' }, [PSCustomObject]@{ name = 'ADMIN'; userName = 'admin'; Id = '2' }, [PSCustomObject]@{ name = 'localadmin'; userName = 'localadmin'; Id = '3' } ) } -ModuleName $ExtensionModule.Name Get-Secret -Name 'admin' -VaultName $VaultName -AdditionalParameters $VaultParams | Should -BeOfType System.Management.Automation.PSCredential Should -Invoke -CommandName Get-PASAccountPassword -ModuleName $ExtensionModule.Name -Times 1 -Exactly -ParameterFilter { $AccountId -eq '2' } } } It 'returns null and retrieves no password when the exact-name match is in a different safe than the vault safeName' { Mock Get-PASAccount -MockWith { [PSCustomObject]@{ name = 'admin'; userName = 'admin'; Id = '1'; safeName = 'SafeB' } } -ModuleName $ExtensionModule.Name Mock Get-PASAccountPassword -MockWith {} -ModuleName $ExtensionModule.Name Get-Secret -Name 'admin' -VaultName $VaultName -AdditionalParameters ($VaultParams + @{ safeName = 'SafeA' }) | Should -BeNullOrEmpty Should -Invoke -CommandName Get-PASAccountPassword -ModuleName $ExtensionModule.Name -Times 0 -Exactly } Context 'direct PASAccount' { BeforeEach { Mock Get-PASAccountPassword -MockWith {} -ModuleName $ExtensionModule.Name } It 'errors and fetches no password when the account is in a different safe than the vault safeName' { Mock Get-PASAccount -MockWith { [PSCustomObject]@{ name = 'admin'; Id = '9'; safeName = 'SafeB' } } -ModuleName $ExtensionModule.Name Get-Secret -Name 'admin' -VaultName $VaultName -AdditionalParameters ($VaultParams + @{ safeName = 'safea' }) -PASAccount ([PSCustomObject]@{ Id = '9' }) -ErrorAction SilentlyContinue -ErrorVariable Err | Should -BeNullOrEmpty "$Err" | Should -BeLike '*safe*' Should -Invoke -CommandName Get-PASAccountPassword -ModuleName $ExtensionModule.Name -Times 0 -Exactly } It 'errors and fetches no password when the account has no safeName and the vault is scoped' { Mock Get-PASAccount -MockWith { [PSCustomObject]@{ name = 'admin'; Id = '9' } } -ModuleName $ExtensionModule.Name Get-Secret -Name 'admin' -VaultName $VaultName -AdditionalParameters ($VaultParams + @{ safeName = 'SafeA' }) -PASAccount ([PSCustomObject]@{ Id = '9' }) -ErrorAction SilentlyContinue -ErrorVariable Err | Should -BeNullOrEmpty "$Err" | Should -BeLike '*safe*' Should -Invoke -CommandName Get-PASAccountPassword -ModuleName $ExtensionModule.Name -Times 0 -Exactly } It 'fetches the password when the safe matches case-insensitively' { Mock Get-PASAccount -MockWith { [PSCustomObject]@{ name = 'admin'; userName = 'admin'; Id = '9'; safeName = 'SAFEA' } } -ModuleName $ExtensionModule.Name Mock Get-PASAccountPassword -MockWith { $R = [PSCustomObject]@{ Password = 'fake' } $R | Add-Member -MemberType ScriptMethod -Name ToSecureString -Value { $this.Password | ConvertTo-SecureString -AsPlainText -Force } $R } -ModuleName $ExtensionModule.Name Get-Secret -Name 'admin' -VaultName $VaultName -AdditionalParameters ($VaultParams + @{ safeName = 'safea' }) -PASAccount ([PSCustomObject]@{ Id = '9' }) | Should -BeOfType System.Management.Automation.PSCredential Should -Invoke -CommandName Get-PASAccountPassword -ModuleName $ExtensionModule.Name -Times 1 -Exactly -ParameterFilter { $AccountId -eq '9' } } It 'writes an error instead of throwing when the by-id lookup throws' { Mock Get-PASAccount -MockWith { throw 'id lookup failed' } -ModuleName $ExtensionModule.Name Get-Secret -Name 'admin' -VaultName $VaultName -AdditionalParameters $VaultParams -PASAccount ([PSCustomObject]@{ Id = '9' }) -ErrorAction SilentlyContinue -ErrorVariable Err | Should -BeNullOrEmpty "$Err" | Should -BeLike '*id lookup failed*' Should -Invoke -CommandName Get-PASAccountPassword -ModuleName $ExtensionModule.Name -Times 0 -Exactly } } It 'writes an error with the underlying message when retrieving the password fails' { Mock Get-PASAccount -MockWith { [PSCustomObject]@{ name = 'admin'; userName = 'admin'; Id = '1' } } -ModuleName $ExtensionModule.Name Mock Get-PASAccountPassword -MockWith { throw 'access denied by policy' } -ModuleName $ExtensionModule.Name Get-Secret -Name 'admin' -VaultName $VaultName -AdditionalParameters $VaultParams -ErrorAction SilentlyContinue -ErrorVariable Err | Should -BeNullOrEmpty "$Err" | Should -BeLike '*admin*access denied by policy*' } It 'writes an error with the underlying message when the account lookup throws' { Mock Get-PASAccount -MockWith { throw 'lookup timed out' } -ModuleName $ExtensionModule.Name Mock Get-PASAccountPassword -MockWith {} -ModuleName $ExtensionModule.Name Get-Secret -Name 'admin' -VaultName $VaultName -AdditionalParameters $VaultParams -ErrorAction SilentlyContinue -ErrorVariable Err | Should -BeNullOrEmpty "$Err" | Should -BeLike '*admin*lookup timed out*' Should -Invoke -CommandName Get-PASAccountPassword -ModuleName $ExtensionModule.Name -Times 0 -Exactly } It 'includes the exception message but not Reason, TicketId or secrets in the error' { Mock Get-PASAccount -MockWith { [PSCustomObject]@{ name = 'admin'; userName = 'admin'; Id = '1' } } -ModuleName $ExtensionModule.Name Mock Get-PASAccountPassword -MockWith { throw 'policy denied retrieval' } -ModuleName $ExtensionModule.Name $Params = $VaultParams + @{ Reason = 'reason-canary-123'; TicketId = 'ticket-canary-456' } Get-Secret -Name 'admin' -VaultName $VaultName -AdditionalParameters $Params -ErrorAction SilentlyContinue -ErrorVariable Err | Should -BeNullOrEmpty "$Err" | Should -BeLike '*policy denied retrieval*' "$Err" | Should -Not -BeLike '*reason-canary-123*' "$Err" | Should -Not -BeLike '*ticket-canary-456*' } It 'writes an error when there is no PASSession' { Mock Get-PASSession -MockWith { throw 'no session' } -ModuleName $ExtensionModule.Name Mock Get-PASAccount -MockWith {} -ModuleName $ExtensionModule.Name Get-Secret -Name 'admin' -VaultName $VaultName -AdditionalParameters $VaultParams -ErrorAction SilentlyContinue -ErrorVariable Err | Should -BeNullOrEmpty "$Err" | Should -BeLike '*Failed to get PASSession*' Should -Invoke -CommandName Get-PASAccount -ModuleName $ExtensionModule.Name -Times 0 -Exactly } It 'should have a PASAccount parameter' { 'PASAccount' | Should -BeIn ((Get-Command -Module $ExtensionModule.Name -Name 'Get-Secret').Parameters.Keys) } } Context 'when connection type is unknown' { It 'writes an error' { $VaultParams = @{ConnectionType = 'Bogus' } Get-Secret -Name 'admin' -VaultName $VaultName -AdditionalParameters $VaultParams -ErrorAction SilentlyContinue -ErrorVariable Err | Should -BeNullOrEmpty "$Err" | Should -BeLike '*ConnectionType Bogus is not supported*' } } Context 'when connection type is Central Credential Provider' { BeforeAll { $VaultParams = @{ConnectionType = 'CentralCredentialProvider'; AppID = 'banana'; URL = 'https://banana.com' } } It 'invokes Get-CCPCredential' { Mock Get-CCPCredential -MockWith (New-ProviderMock CCP 'svc-ccp' 'ccp-pass') -ModuleName $ExtensionModule.Name $Secret = Get-Secret -Name 'admin' -VaultName $VaultName -AdditionalParameters $VaultParams Should -Invoke -CommandName Get-CCPCredential -ModuleName $ExtensionModule.Name $Secret | Should -BeOfType System.Management.Automation.PSCredential $Secret.UserName | Should -Be 'svc-ccp' $Secret.GetNetworkCredential().Password | Should -Be 'ccp-pass' } It 'writes an error naming UserName when it is missing' { Mock Get-CCPCredential -MockWith (New-ProviderMock CCP $null 'ccp-pass') -ModuleName $ExtensionModule.Name Get-Secret -Name 'admin' -VaultName $VaultName -AdditionalParameters $VaultParams -ErrorAction SilentlyContinue -ErrorVariable Err | Should -BeNullOrEmpty "$Err" | Should -BeLike '*UserName*' "$Err" | Should -Not -BeLike '*ccp-pass*' } It 'writes an error naming Content when the password is empty' { Mock Get-CCPCredential -MockWith (New-ProviderMock CCP 'svc-ccp' '') -ModuleName $ExtensionModule.Name Get-Secret -Name 'admin' -VaultName $VaultName -AdditionalParameters $VaultParams -ErrorAction SilentlyContinue -ErrorVariable Err | Should -BeNullOrEmpty "$Err" | Should -BeLike '*Content*' } It 'writes an error with the exception message when Get-CCPCredential throws' { Mock Get-CCPCredential -MockWith { throw 'CCP timed out' } -ModuleName $ExtensionModule.Name Get-Secret -Name 'admin' -VaultName $VaultName -AdditionalParameters $VaultParams -ErrorAction SilentlyContinue -ErrorVariable Err | Should -BeNullOrEmpty "$Err" | Should -BeLike '*admin*CCP timed out*' } Context 'vault parameter allowlist' { BeforeEach { Mock Get-CCPCredential -MockWith (New-ProviderMock CCP 'svc-ccp' 'ccp-pass') -ModuleName $ExtensionModule.Name } It 'passes allowed connection settings and the object name' { $Params = @{ ConnectionType = 'CentralCredentialProvider'; AppID = 'banana'; URL = 'https://banana.com' SkipCertificateCheck = $true; UseDefaultCredentials = $true; CertificateThumbPrint = 'ABC123' WebServiceName = 'AIMWebService'; ConnectionTimeout = 30; Method = 'GET'; FailRequestOnPasswordChange = $true } $null = Get-Secret -Name 'admin' -VaultName $VaultName -AdditionalParameters $Params -WarningVariable Warn -WarningAction SilentlyContinue $Warn | Should -BeNullOrEmpty Should -Invoke -CommandName Get-CCPCredential -ModuleName $ExtensionModule.Name -Times 1 -Exactly -ParameterFilter { $AppID -eq 'banana' -and $URL -eq 'https://banana.com' -and $SkipCertificateCheck -and $UseDefaultCredentials -and $CertificateThumbPrint -eq 'ABC123' -and $WebServiceName -eq 'AIMWebService' -and $ConnectionTimeout -eq 30 -and $Method -eq 'GET' -and $FailRequestOnPasswordChange -and $Object -eq 'admin' } } It 'omits switch parameters whose value is false so Get-CCPCredential does not see them bound' { Mock Get-CCPCredential -MockWith ([scriptblock]::Create('$global:CCPBound = @($PesterBoundParameters.Keys);' + (New-ProviderMock CCP 'svc-ccp' 'ccp-pass').ToString())) -ModuleName $ExtensionModule.Name $Params = @{ ConnectionType = 'CentralCredentialProvider'; AppID = 'banana'; URL = 'https://banana.com' UseDefaultCredentials = $false; SkipCertificateCheck = $false; FailRequestOnPasswordChange = $false } $null = Get-Secret -Name 'admin' -VaultName $VaultName -AdditionalParameters $Params $global:CCPBound | Should -Contain 'AppID' $global:CCPBound | Should -Not -Contain 'UseDefaultCredentials' $global:CCPBound | Should -Not -Contain 'SkipCertificateCheck' $global:CCPBound | Should -Not -Contain 'FailRequestOnPasswordChange' } It 'treats the string "false" as false and "true" as true for switch parameters' { Mock Get-CCPCredential -MockWith ([scriptblock]::Create('$global:CCPBound = @($PesterBoundParameters.Keys);' + (New-ProviderMock CCP 'svc-ccp' 'ccp-pass').ToString())) -ModuleName $ExtensionModule.Name $Params = @{ ConnectionType = 'CentralCredentialProvider'; AppID = 'banana'; URL = 'https://banana.com'; UseDefaultCredentials = 'false'; SkipCertificateCheck = 'true' } $null = Get-Secret -Name 'admin' -VaultName $VaultName -AdditionalParameters $Params $global:CCPBound | Should -Not -Contain 'UseDefaultCredentials' $global:CCPBound | Should -Contain 'SkipCertificateCheck' } It 'ignores Verbose silently and warns about unknown keys, naming only the keys' { $Params = @{ ConnectionType = 'CentralCredentialProvider'; AppID = 'banana'; URL = 'https://banana.com'; Verbose = $false Bogus = 'sekrit-value'; Safe = 'sekrit-safe'; AsCredential = $true } $null = Get-Secret -Name 'admin' -VaultName $VaultName -AdditionalParameters $Params -WarningVariable Warn -WarningAction SilentlyContinue "$Warn" | Should -BeLike '*Bogus*' "$Warn" | Should -BeLike '*Safe*' "$Warn" | Should -BeLike '*AsCredential*' "$Warn" | Should -Not -BeLike '*sekrit*' "$Warn" | Should -Not -BeLike '*ConnectionType*' "$Warn" | Should -Not -BeLike '*Verbose*' Should -Invoke -CommandName Get-CCPCredential -ModuleName $ExtensionModule.Name -Times 1 -Exactly -ParameterFilter { -not $PesterBoundParameters.ContainsKey('Bogus') -and -not $PesterBoundParameters.ContainsKey('Safe') -and -not $PesterBoundParameters.ContainsKey('AsCredential') -and $AppID -eq 'banana' } } It 'does not pass Credential or Certificate and warns about them' { $Params = @{ ConnectionType = 'CentralCredentialProvider'; AppID = 'banana'; URL = 'https://banana.com' Credential = @{ UserName = 'u' }; Certificate = @{ Subject = 'cn=x' } } $null = Get-Secret -Name 'admin' -VaultName $VaultName -AdditionalParameters $Params -WarningVariable Warn -WarningAction SilentlyContinue "$Warn" | Should -BeLike '*Credential*' "$Warn" | Should -BeLike '*Certificate*' Should -Invoke -CommandName Get-CCPCredential -ModuleName $ExtensionModule.Name -Times 1 -Exactly -ParameterFilter { -not $PesterBoundParameters.ContainsKey('Credential') -and -not $PesterBoundParameters.ContainsKey('Certificate') } } } } Context 'when connection type is Credential Provder' { BeforeAll { $VaultParams = @{ConnectionType = 'CredentialProvider'; AppID = 'banana' } } It 'invokes Get-AIMCredential' { Mock Get-AIMCredential -MockWith (New-ProviderMock AIM 'svc-aim' 'aim-pass') -ModuleName $ExtensionModule.Name $Secret = Get-Secret -Name 'windowsAdmin01' -VaultName $VaultName -AdditionalParameters $VaultParams Should -Invoke -CommandName Get-AIMCredential -ModuleName $ExtensionModule.Name $Secret | Should -BeOfType System.Management.Automation.PSCredential $Secret.UserName | Should -Be 'svc-aim' $Secret.GetNetworkCredential().Password | Should -Be 'aim-pass' } It 'writes an error naming UserName when it is missing' { Mock Get-AIMCredential -MockWith (New-ProviderMock AIM $null 'aim-pass') -ModuleName $ExtensionModule.Name Get-Secret -Name 'windowsAdmin01' -VaultName $VaultName -AdditionalParameters $VaultParams -ErrorAction SilentlyContinue -ErrorVariable Err | Should -BeNullOrEmpty "$Err" | Should -BeLike '*UserName*' "$Err" | Should -Not -BeLike '*aim-pass*' } It 'writes an error naming Password when the password is empty' { Mock Get-AIMCredential -MockWith (New-ProviderMock AIM 'svc-aim' '') -ModuleName $ExtensionModule.Name Get-Secret -Name 'windowsAdmin01' -VaultName $VaultName -AdditionalParameters $VaultParams -ErrorAction SilentlyContinue -ErrorVariable Err | Should -BeNullOrEmpty "$Err" | Should -BeLike '*Password*' } It 'writes an error with the exception message when Get-AIMCredential throws' { Mock Get-AIMCredential -MockWith { throw 'APPAP004E not found' } -ModuleName $ExtensionModule.Name Get-Secret -Name 'windowsAdmin01' -VaultName $VaultName -AdditionalParameters $VaultParams -ErrorAction SilentlyContinue -ErrorVariable Err | Should -BeNullOrEmpty "$Err" | Should -BeLike '*windowsAdmin01*APPAP004E not found*' } It 'always requests UserName so a PSCredential can be built' { Mock Get-AIMCredential -MockWith (New-ProviderMock AIM 'svc-aim' 'aim-pass') -ModuleName $ExtensionModule.Name $null = Get-Secret -Name 'windowsAdmin01' -VaultName $VaultName -AdditionalParameters $VaultParams Should -Invoke -CommandName Get-AIMCredential -ModuleName $ExtensionModule.Name -ParameterFilter { $RequiredProps -contains 'UserName' } } It 'does not require SafeName and does not pass Safe to Get-AIMCredential when it is not a vault parameter' { Mock Get-AIMCredential -MockWith (New-ProviderMock AIM 'svc-aim' 'aim-pass') -ModuleName $ExtensionModule.Name $Secret = Get-Secret -Name 'windowsAdmin01' -VaultName $VaultName -AdditionalParameters $VaultParams $Secret | Should -BeOfType System.Management.Automation.PSCredential Should -Invoke -CommandName Get-AIMCredential -ModuleName $ExtensionModule.Name -Times 1 -Exactly -ParameterFilter { $null -eq $Safe } } It 'passes the SafeName vault parameter to Get-AIMCredential as Safe' { Mock Get-AIMCredential -MockWith (New-ProviderMock AIM 'svc-aim' 'aim-pass') -ModuleName $ExtensionModule.Name $Secret = Get-Secret -Name 'windowsAdmin01' -VaultName $VaultName -AdditionalParameters ($VaultParams + @{SafeName = 'Windows' }) $Secret | Should -BeOfType System.Management.Automation.PSCredential Should -Invoke -CommandName Get-AIMCredential -ModuleName $ExtensionModule.Name -Times 1 -Exactly -ParameterFilter { $Safe -eq 'Windows' } } } } |