From db0fa1cf4ad658e7185172a394efa31bad918190 Mon Sep 17 00:00:00 2001 From: pinguinfuss Date: Mon, 22 May 2023 21:44:13 +0200 Subject: [PATCH 1/4] Implement a function to "find" a CredentialStoreItem in 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 + } + } +} -- 2.45.2 From fd6dc42099c4688fce41f025cd3bd8a4434564a5 Mon Sep 17 00:00:00 2001 From: pinguinfuss Date: Mon, 22 May 2023 21:51:21 +0200 Subject: [PATCH 2/4] Revert "Implement a function to "find" a CredentialStoreItem in CredentialStores" This reverts commit db0fa1cf4ad658e7185172a394efa31bad918190. --- src/Item/Find-CredentialStoreItem.Tests.ps1 | 150 ------------------- src/Item/Find-CredentialStoreItem.ps1 | 152 -------------------- 2 files changed, 302 deletions(-) delete mode 100644 src/Item/Find-CredentialStoreItem.Tests.ps1 delete mode 100644 src/Item/Find-CredentialStoreItem.ps1 diff --git a/src/Item/Find-CredentialStoreItem.Tests.ps1 b/src/Item/Find-CredentialStoreItem.Tests.ps1 deleted file mode 100644 index 3dec6ea..0000000 --- a/src/Item/Find-CredentialStoreItem.Tests.ps1 +++ /dev/null @@ -1,150 +0,0 @@ -[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 deleted file mode 100644 index d4936d8..0000000 --- a/src/Item/Find-CredentialStoreItem.ps1 +++ /dev/null @@ -1,152 +0,0 @@ -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 - } - } -} -- 2.45.2 From 16b4ea1d13020cf7a82995eea41882b7eede0534 Mon Sep 17 00:00:00 2001 From: pinguinfuss Date: Mon, 22 May 2023 21:54:58 +0200 Subject: [PATCH 3/4] 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 + } + } +} -- 2.45.2 From 6aa732397d080343eb0fddab74b9400d747f65f1 Mon Sep 17 00:00:00 2001 From: pinguinfuss Date: Mon, 22 May 2023 22:00:32 +0200 Subject: [PATCH 4/4] Add the PSD export for Find-CredentialStoreItem --- src/PSCredentialStore.psd1 | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PSCredentialStore.psd1 b/src/PSCredentialStore.psd1 index c8ce1cf..a7af314 100644 --- a/src/PSCredentialStore.psd1 +++ b/src/PSCredentialStore.psd1 @@ -85,6 +85,7 @@ 'Test-CSConnection', # Item 'Get-CredentialStoreItem', + 'Find-CredentialStoreItem', 'New-CredentialStoreItem', 'Remove-CredentialStoreItem', 'Set-CredentialStoreItem', -- 2.45.2