From 16b4ea1d13020cf7a82995eea41882b7eede0534 Mon Sep 17 00:00:00 2001 From: pinguinfuss Date: Mon, 22 May 2023 21:54:58 +0200 Subject: [PATCH] Implement a function to "Find" a CredentialStoreItem in all possible (private and shared) CredentialStores --- src/Item/Find-CredentialStoreItem.Tests.ps1 | 150 +++++++++++++++++++ src/Item/Find-CredentialStoreItem.ps1 | 152 ++++++++++++++++++++ 2 files changed, 302 insertions(+) create mode 100644 src/Item/Find-CredentialStoreItem.Tests.ps1 create mode 100644 src/Item/Find-CredentialStoreItem.ps1 diff --git a/src/Item/Find-CredentialStoreItem.Tests.ps1 b/src/Item/Find-CredentialStoreItem.Tests.ps1 new file mode 100644 index 0000000..3dec6ea --- /dev/null +++ b/src/Item/Find-CredentialStoreItem.Tests.ps1 @@ -0,0 +1,150 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidUsingConvertToSecureStringWithPlainText', + '', + Justification = 'just used in pester tests.' +)] +param () + +BeforeAll { + $Repo = Get-RepoPath + Import-Module $Repo.Src.Manifest.Item.FullName -Force + + # Backup existing CredentialStores + $Paths = @(('{0}\AppData\Roaming' -f $env:USERPROFILE), ('{0}\ProgramData\PSCredentialStore\' -f $env:SystemDrive)) + $Files = @('CredentialStore.json', 'PSCredentialStore.pfx') + + foreach ($Filepath in $Paths) { + foreach ($File in $Files) { + $OrgPath = Join-Path -Path $FilePath -ChildPath $File + $NewPath = $OrgPath + '.orig' + if (Test-Path $OrgPath) { + try { + $null = Remove-Item -Path $NewPath -Force -Confirm:$false + $null = Rename-Item -Path $OrgPath -NewName $NewPath -Confirm:$false + } + catch { + $_.Exception.Message | Write-Warning + Write-Error -Message ('Unable to revert {0} to {1}' -f $OrgPath, $NewPath) + } + } + } + } + + # Construct the necessary CredentialStores for the Unit tests. + New-CredentialStore -Force + New-CredentialStore -Shared -Force + + # Construct the necessary CredentialStoreItems for the Unit tests. + $CredentialUserName = 'MyUser' + $CredentialPassword = 'FooBar' | ConvertTo-SecureString -AsPlainText -Force + $Credential = [PSCredential]::new($CredentialUserName, $CredentialPassword) + + # Create the CredentialStoreItems + New-CredentialStoreItem -RemoteHost 'test-case-a.domain.my' -Credential $Credential + New-CredentialStoreItem -Shared -RemoteHost 'test-case-a.domain.my' -Credential $Credential + + New-CredentialStoreItem -Shared -RemoteHost 'test-case-b.domain.my' -Credential $Credential + New-CredentialStoreItem -RemoteHost 'test-case-c.domain.my' -Credential $Credential + + New-CredentialStoreItem -RemoteHost 'test-case-a.domain.my' -Credential $Credential -Identifier 'Foo' + New-CredentialStoreItem -Shared -RemoteHost 'test-case-a.domain.my' -Credential $Credential -Identifier 'Foo' + + New-CredentialStoreItem -Shared -RemoteHost 'test-case-b.domain.my' -Credential $Credential -Identifier 'Foo' + New-CredentialStoreItem -RemoteHost 'test-case-c.domain.my' -Credential $Credential -Identifier 'Foo' +} + +AfterAll { + # Check if the private CredentialStore exists + $Paths = @(('{0}\AppData\Roaming' -f $env:USERPROFILE), ('{0}\ProgramData\PSCredentialStore\' -f $env:SystemDrive)) + $Files = @('CredentialStore.json.orig', 'PSCredentialStore.pfx.orig') + + foreach ($Filepath in $Paths) { + foreach ($File in $Files) { + $OrgPath = Join-Path -Path $FilePath -ChildPath $File + $NewPath = $OrgPath.Replace('.orig', '') + if (Test-Path $OrgPath) { + try { + $null = Remove-Item -Path $NewPath -Force -Confirm:$false -ErrorAction SilentlyContinue + $null = Rename-Item -Path $OrgPath -NewName $NewPath -Confirm:$false + } + catch { + $_.Exception.Message | Write-Warning + Write-Error -Message ('Unable to revert {0} to {1}' -f $OrgPath, $NewPath) + } + } + } + } +} + +Describe 'Find-CredentialStoreItem' { + Context 'Default tests' -Tag 'Default' { + It 'Test Function' { + { Get-Command -Name 'Find-CredentialStoreItem' -Module $Repo.Artifact } | Should -Not -Throw + } + + It 'Test Help' { + { Get-Help -Name 'Find-CredentialStoreItem' } | Should -Not -Throw + } + + It 'Help Content' { + $foo = Get-Help -Name 'Find-CredentialStoreItem' + $foo.Synopsis.Length | Should -BeGreaterThan 5 + $foo.Description.Count | Should -BeGreaterOrEqual 1 + $foo.Description[0].Text.Length | Should -BeGreaterThan 5 + } + } + + Context 'Coding tests' -Tag 'Coding' { + It 'Calling Find-CredentialStoreItem with wrong Type' { + { Find-CredentialStoreItem -RemoteHost 'test-case-a.domain.my' -Type 'Foo' } | Should -Throw + } + + It 'Calling Find-CredentialStoreItem present in both CredentialStores w/o Identifier' { + { Find-CredentialStoreItem -RemoteHost 'test-case-a.domain.my' } | Should -Not -Throw + + $foo = Find-CredentialStoreItem -RemoteHost 'test-case-a.domain.my' + $foo.UserName | Should -Be 'MyUser' + $foo.GetNetworkCredential().Password | Should -Be 'FooBar' + } + + It 'Calling Find-CredentialStoreItem present only in shared CredentialStore w/o Identifier' { + { Find-CredentialStoreItem -RemoteHost 'test-case-b.domain.my' } | Should -Not -Throw + + $foo = Find-CredentialStoreItem -RemoteHost 'test-case-b.domain.my' + $foo.UserName | Should -Be 'MyUser' + $foo.GetNetworkCredential().Password | Should -Be 'FooBar' + } + + It 'Calling Find-CredentialStoreItem present only in private CredentialStore w/o Identifier' { + { Find-CredentialStoreItem -RemoteHost 'test-case-c.domain.my' } | Should -Not -Throw + + $foo = Find-CredentialStoreItem -RemoteHost 'test-case-c.domain.my' + $foo.UserName | Should -Be 'MyUser' + $foo.GetNetworkCredential().Password | Should -Be 'FooBar' + } + + It 'Calling Find-CredentialStoreItem present in both CredentialStores w Identifier' { + { Find-CredentialStoreItem -RemoteHost 'test-case-a.domain.my' -Identifier 'Foo' } | Should -Not -Throw + + $foo = Find-CredentialStoreItem -RemoteHost 'test-case-a.domain.my' -Identifier 'Foo' + $foo.UserName | Should -Be 'MyUser' + $foo.GetNetworkCredential().Password | Should -Be 'FooBar' + } + + It 'Calling Find-CredentialStoreItem present only in shared CredentialStore w/o Identifier' { + { Find-CredentialStoreItem -RemoteHost 'test-case-b.domain.my' -Identifier 'Foo' } | Should -Not -Throw + + $foo = Find-CredentialStoreItem -RemoteHost 'test-case-b.domain.my' -Identifier 'Foo' + $foo.UserName | Should -Be 'MyUser' + $foo.GetNetworkCredential().Password | Should -Be 'FooBar' + } + + It 'Calling Find-CredentialStoreItem present only in private CredentialStore w/o Identifier' { + { Find-CredentialStoreItem -RemoteHost 'test-case-c.domain.my' -Identifier 'Foo' } | Should -Not -Throw + + $foo = Find-CredentialStoreItem -RemoteHost 'test-case-c.domain.my' -Identifier 'Foo' + $foo.UserName | Should -Be 'MyUser' + $foo.GetNetworkCredential().Password | Should -Be 'FooBar' + } + } +} diff --git a/src/Item/Find-CredentialStoreItem.ps1 b/src/Item/Find-CredentialStoreItem.ps1 new file mode 100644 index 0000000..d4936d8 --- /dev/null +++ b/src/Item/Find-CredentialStoreItem.ps1 @@ -0,0 +1,152 @@ +function Find-CredentialStoreItem { + <# + .SYNOPSIS + Locates a CredentialStoreItem in any CredentialStore from a given remote host item. + + .DESCRIPTION + Find the credential object and return it as PSCredential object. + + .PARAMETER RemoteHost + Specify the host, for which you would like to find the credentials. + + .PARAMETER Identifier + Provide a custom identifier to the given remote host key. This enables you to store multiple credentials + for a single remote host entry. For example ad/sys1, ftp/sys1, sql/sys1 + + .PARAMETER Type + Influence in which types of CredentialStore this function will look for a object. List of possible types: + - All (include private and shared CredentialStore) - this is also the default. + - Private (only look in a private CredentialStore) + - Shared (only look in the shared CredentialStore) + + .INPUTS + [None] + + .OUTPUTS + [System.Management.Automation.PSCredential] + + .EXAMPLE + $Credential = Find-CredentialStoreItem -RemoteHost 'support.komm-one.net' -Type 'All' + + .EXAMPLE + $params = @{ + RemoteHost = 'support.komm-one.net' + Type = 'Private' + Identifier = 'PersonId' + } + $Credential = Find-CredentialStoreItem @params + #> + + [CmdletBinding()] + + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $RemoteHost, + + [Parameter(Mandatory = $false)] + [string] $Identifier, + + [Parameter(Mandatory = $false)] + [ValidateSet('All', 'Private', 'Shared')] + [string] $Type = 'All' + ) + + begin { + # Define some defaults for the PreferenceVariables. + $ErrorActionPreference = 'Stop' + $InformationPreference = 'Continue' + $WarningPreference = 'Continue' + $ProgressPreference = 'SilentlyContinue' + + # Construct the CredentialStore list, based on what $Type says. + switch ($Type) { + 'All' { + $CredentialStoreList = @('Private', 'Shared') + break + } + 'Private' { + $CredentialStoreList = @('Private') + } + 'Shared' { + $CredentialStoreList = @('Shared') + } + } + } + + process { + # Now go and look for the CredentialStoreItem. + foreach ($Store in $CredentialStoreList) { + # First make sure, that the CredentialStore exists. Sadly I don't have a way to solve this any better + # programmatically, as PowerShell behaves oddly, if you try and pass an empty splatting to a function. + Write-Verbose -Message ('Checking if CredentialStore of type {0} exists' -f $Store) + + if ($Store -eq 'Private') { + if (-not (Test-CredentialStore)) { + Write-Warning -Message ('CredentialStore of type {0} not found, skipping ahead' -f $Store) + continue + } + } + elseif ($Store -eq 'Shared') { + if (-not (Test-CredentialStore -Shared)) { + Write-Warning -Message ('CredentialStore of type {0} not found, skipping ahead' -f $Store) + continue + } + } + else { + Write-Error -Message ('Invalid CredentialStore type {0} supplied' -f $Store) + continue + } + + # Now that we're here, means we have tested the CredentialStore for existence. We can check, if it + # contains a CredentialStoreItem that we are looking for. + $params = @{ + RemoteHost = $RemoteHost + } + + # Check if the user passed -Identifier, then we add it to the splatting. + if (-not [string]::IsNullOrWhiteSpace($Identifier)) { + $params.Add('Identifier', $Identifier) + } + + # Check the CredentialStore type we're currently looking at. + if ($Store -eq 'Shared') { + $params.Add('Shared', $true) + } + + # Now check if the CredentialStoreItem exists + $message = 'Checking if CredentialStoreItem {0}/{1} exists in CredentialStore {2}' + $argumentlist = @($RemoteHost, $Identifier, $Store) + Write-Verbose -Message ($message -f $argumentlist) + + if (Test-CredentialStoreItem @params) { + $message = 'Looking up CredentialStoreItem {0}/{1} from CredentialStore {2}' + $argumentlist = @($RemoteHost, $Identifier, $Store) + Write-Verbose -Message ($message -f $argumentlist) + + try { + Write-Information -MessageData ($message -f $argumentlist) + # Read the CredentialStoreItem from the CredentialStore and store it in $CredentialObject + $CredentialObject = Get-CredentialStoreItem @params + + # Now finish the loop, as we've found what we're looking for. + break + } + catch { + $_.Exception.Message | Write-Warning + $message = 'Unable to read CredentialStoreItem {0}/{1} from CredentialStore {2}' + $argumentlist = @($RemoteHost, $Identifier, $Store) + + Write-Warning -Message ($message -f $argumentlist) + } + } + } + } + + end { + # Only if we've found a CredentialStoreItem above, return it back to the caller. + if ($null -ne $CredentialObject) { + $CredentialObject + } + } +}