From e2cbbb6c57f7d6ed37e7807c4dc40ca3d0b2d498 Mon Sep 17 00:00:00 2001 From: Daryl Newsholme Date: Tue, 7 May 2019 12:19:43 +0100 Subject: [PATCH] support for secure strings --- CHANGELOG.md | 9 + docs/Get-PasswordStatePasswords.md | 4 +- docs/New-PasswordStateList.md | 4 +- .../Find-PasswordStatePassword.Tests.ps1 | 83 +++--- functions/Find-PasswordStatePassword.ps1 | 251 ++++++++---------- functions/Get-PasswordStateEnvironment.ps1 | 6 +- functions/Get-PasswordStateFolder.Tests.ps1 | 39 ++- functions/Get-PasswordStateFolder.ps1 | 1 + functions/Get-PasswordStateList.Tests.ps1 | 68 ++--- functions/Get-PasswordStateList.ps1 | 1 + ...Get-PasswordStatePasswordHistory.Tests.ps1 | 53 ++-- .../Get-PasswordStatePasswordHistory.ps1 | 32 ++- functions/Get-PasswordStatePasswords.ps1 | 18 +- functions/Get-PasswordStateResource.ps1 | 32 ++- functions/New-PasswordStateDocument.Tests.ps1 | 33 ++- functions/New-PasswordStateDocument.ps1 | 1 + functions/New-PasswordStateFolder.ps1 | 2 +- functions/New-PasswordStateList.Tests.ps1 | 61 ++--- functions/New-PasswordStateList.ps1 | 18 +- functions/New-PasswordStatePassword.Tests.ps1 | 80 +++--- functions/New-PasswordStatePassword.ps1 | 15 +- functions/New-PasswordStateResource.ps1 | 20 +- functions/New-RandomPassword.Tests.ps1 | 30 ++- functions/New-RandomPassword.ps1 | 1 + functions/PasswordStateClass.ps1 | 66 +++++ .../Remove-PasswordStatePassword.Tests.ps1 | 89 ++----- functions/Remove-PasswordStatePassword.ps1 | 7 +- functions/Remove-PasswordStateResource.ps1 | 21 +- functions/Save-PasswordStateDocument.ps1 | 1 + functions/Set-PasswordStateEnvironment.ps1 | 13 +- functions/Set-PasswordStateResource.ps1 | 20 +- .../Update-PasswordStatePassword.Tests.ps1 | 68 ++--- functions/Update-PasswordStatePassword.ps1 | 26 +- readme.md | 40 ++- 34 files changed, 696 insertions(+), 517 deletions(-) create mode 100644 functions/PasswordStateClass.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c07d50..f4091eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.0.94 + ++ Feature + + Passwords now returned as secure string. Use .GetPassword() method to retrieve plaintext + + Global variable for returning plain text passwords $global:PasswordStateShowPasswordsPlainText = $true ++ Fixes + + Added support for APIKey auth to generate passwords as this was broken. + + Various bugfixes when using apikeys. + ## 0.0.91 + Fix diff --git a/docs/Get-PasswordStatePasswords.md b/docs/Get-PasswordStatePasswords.md index 7df20d4..460bfba 100644 --- a/docs/Get-PasswordStatePasswords.md +++ b/docs/Get-PasswordStatePasswords.md @@ -14,7 +14,7 @@ Use Get-PasswordStateList to search for a name and return the ID ## SYNTAX ``` -Get-PasswordStatePasswords [-PasswordlistID ] [] +Get-PasswordStatePasswords [[-PasswordlistID] ] [] ``` ## DESCRIPTION @@ -41,7 +41,7 @@ Parameter Sets: (All) Aliases: Required: False -Position: Named +Position: 1 Default value: 0 Accept pipeline input: True (ByPropertyName, ByValue) Accept wildcard characters: False diff --git a/docs/New-PasswordStateList.md b/docs/New-PasswordStateList.md index 30dc798..5a13f85 100644 --- a/docs/New-PasswordStateList.md +++ b/docs/New-PasswordStateList.md @@ -13,7 +13,7 @@ Creates a passwordstate List. ## SYNTAX ``` -New-PasswordStateList [-Name] [-description] [[-CopySettingsFromPasswordListID] ] +New-PasswordStateList [-Name] [-description] [-CopySettingsFromPasswordListID] [-FolderID] [-WhatIf] [-Confirm] [] ``` @@ -67,7 +67,7 @@ Type: Int32 Parameter Sets: (All) Aliases: -Required: False +Required: True Position: 3 Default value: None Accept pipeline input: True (ByPropertyName) diff --git a/functions/Find-PasswordStatePassword.Tests.ps1 b/functions/Find-PasswordStatePassword.Tests.ps1 index f51d3ec..535e398 100644 --- a/functions/Find-PasswordStatePassword.Tests.ps1 +++ b/functions/Find-PasswordStatePassword.Tests.ps1 @@ -1,41 +1,54 @@ -$here = Split-Path -Parent $MyInvocation.MyCommand.Path +(import-module "$psscriptroot\..\passwordstate-management.psm1" -force) +$here = Split-Path -Parent $MyInvocation.MyCommand.Path $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.' . "$here\$sut" -Import-Module "$here\..\passwordstate-management.psm1" Describe "Find-PasswordStatePassword" { - It "Finds a Password From Password State" { - Mock -CommandName Get-PasswordStateResource -MockWith {return [PSCustomObject]@{ - "Title" = "testuser" - "Username" = "test" - "Domain" = "" - "Description" = "" - "PasswordId" = 3 - "AccountType" = "" - "URL" = "" - "Password" = "testpassword" - } - } -ParameterFilter {$uri -eq "/api/passwords/3"} - - Mock -CommandName Get-PasswordStateResource -MockWith {return [PSCustomObject]@{ - "Title" = "testuser" - "Username" = "test" - "Domain" = "" - "Description" = "" - "PasswordId" = 3 - "AccountType" = "" - "URL" = "" - "Password" = "" - } - } -ParameterFilter {$uri -eq "/api/searchpasswords/?title=testuser&ExcludePassword=true"} - - Mock -CommandName Get-PasswordStateEnvironment -MockWith {return [PSCustomObject]@{ - "Baseuri" = "https://passwordstateserver.co.uk" - "APIKey" = "WindowsAuth" - - } + It "Finds a Password by generic search" { + (Find-PasswordStatePassword "test").Title | Should -BeExactly "test" + } + It "Finds a Password by ID" { + (Find-PasswordStatePassword -PasswordID "1").PasswordID | Should -BeExactly 1 + } + It "Finds a Password with a reason" { + (Find-PasswordStatePassword -PasswordID "1" -Reason "Unit Test").PasswordID | Should -BeExactly 1 + } + It "Finds a Password by Username Search" { + (Find-PasswordStatePassword -UserName "test").Username | Should -BeExactly "test" + } + It "Checks Password is returned as type [System.Security.SecureString]" { + (Find-PasswordStatePassword -PasswordID "1").Password.Password | Should -BeOfType [System.Security.SecureString] + } + It "Checks Password is decrypted by method .GetPassword()" { + (Find-PasswordStatePassword -PasswordID "1").GetPassword() | Should -BeOfType [String] + } + It "Checks Password is decrypted by method .DecryptPassword()" { + $result = (Find-PasswordStatePassword -PasswordID "1") + $result.DecryptPassword() + $result.Password | Should -BeOfType [String] + } + It "Checks `$global:PasswordStateShowPasswordsPlainText is honoured" { + $global:PasswordStateShowPasswordsPlainText = $true + (Find-PasswordStatePassword -PasswordID "1").Password | Should -BeOfType [String] + } + + BeforeEach { + # Create Test Environment + try { + $globalsetting = Get-Variable PasswordStateShowPasswordsPlainText -ErrorAction stop -Verbose -ValueOnly + $global:PasswordStateShowPasswordsPlainText = $false } - - (Find-PasswordStatePassword -Title "testuser").PasswordId | Should -BeExactly 3 - (Find-PasswordStatePassword -Title "testuser").Password | Should -not -BeNullOrEmpty + Catch { + New-Variable -Name PasswordStateShowPasswordsPlainText -Value $false -Scope Global + } + Move-Item "$($env:USERPROFILE)\passwordstate.json" "$($env:USERPROFILE)\passwordstate.json.bak" -force + Set-PasswordStateEnvironment -Apikey "$env:pwsapikey" -Baseuri "$env:pwsuri" + } + + AfterEach { + # Remove Test Environment + Move-Item "$($env:USERPROFILE)\passwordstate.json.bak" "$($env:USERPROFILE)\passwordstate.json" -force + $global:PasswordStateShowPasswordsPlainText = $globalsetting + # } } + diff --git a/functions/Find-PasswordStatePassword.ps1 b/functions/Find-PasswordStatePassword.ps1 index cf55f3e..71eb304 100644 --- a/functions/Find-PasswordStatePassword.ps1 +++ b/functions/Find-PasswordStatePassword.ps1 @@ -75,151 +75,116 @@ 2018 - Daryl Newsholme 2019 - Jarno Colombeen #> -Function Find-PasswordStatePassword -{ - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '', Justification = 'No Password is used only ID.')] - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUserNameAndPassWordParams', '', Justification = 'PasswordID isnt a password')] - [CmdletBinding(DefaultParameterSetName='General')] - Param - ( - [Parameter(ParameterSetName='General',ValueFromPipeline,ValueFromPipelineByPropertyName,Position=0,Mandatory=$true)][ValidateNotNullOrEmpty()][string]$Search, - [Parameter(ParameterSetName='PasswordID',ValueFromPipelineByPropertyName,Position=0,Mandatory=$true)][ValidateNotNullOrEmpty()][int32]$PasswordID, - [Parameter(ParameterSetName='Specific',ValueFromPipelineByPropertyName,Position=0)][string]$Title, - [Parameter(ParameterSetName='Specific',ValueFromPipelineByPropertyName,Position=1)][string]$UserName, - [Parameter(ParameterSetName='Specific',ValueFromPipelineByPropertyName,Position=2)][string]$HostName, - [Parameter(ParameterSetName='Specific',ValueFromPipelineByPropertyName,Position=3)][string]$Domain, - [Parameter(ParameterSetName='Specific',ValueFromPipelineByPropertyName,Position=4)][string]$AccountType, - [Parameter(ParameterSetName='Specific',ValueFromPipelineByPropertyName,Position=5)][string]$Description, - [Parameter(ParameterSetName='Specific',ValueFromPipelineByPropertyName,Position=6)][string]$Notes, - [Parameter(ParameterSetName='Specific',ValueFromPipelineByPropertyName,Position=7)][string]$URL, - [Parameter(ParameterSetName='Specific',ValueFromPipelineByPropertyName,Position=8)][string]$SiteID, - [Parameter(ParameterSetName='Specific',ValueFromPipelineByPropertyName,Position=9)][string]$SiteLocation, - [Parameter(ParameterSetName='Specific',ValueFromPipelineByPropertyName,Position=10)][string]$GenericField1, - [Parameter(ParameterSetName='Specific',ValueFromPipelineByPropertyName,Position=11)][string]$GenericField2, - [Parameter(ParameterSetName='Specific',ValueFromPipelineByPropertyName,Position=12)][string]$GenericField3, - [Parameter(ParameterSetName='Specific',ValueFromPipelineByPropertyName,Position=13)][string]$GenericField4, - [Parameter(ParameterSetName='Specific',ValueFromPipelineByPropertyName,Position=14)][string]$GenericField5, - [Parameter(ParameterSetName='Specific',ValueFromPipelineByPropertyName,Position=15)][string]$GenericField6, - [Parameter(ParameterSetName='Specific',ValueFromPipelineByPropertyName,Position=16)][string]$GenericField7, - [Parameter(ParameterSetName='Specific',ValueFromPipelineByPropertyName,Position=17)][string]$GenericField8, - [Parameter(ParameterSetName='Specific',ValueFromPipelineByPropertyName,Position=18)][string]$GenericField9, - [Parameter(ParameterSetName='Specific',ValueFromPipelineByPropertyName,Position=19)][string]$GenericField10, - [parameter(ValueFromPipelineByPropertyName, Position = 20)][string]$Reason - ) - - Begin - { - # Create Class - class PasswordResult - { - # Properties - [int]$PasswordID - [String]$Title - [String]$Username - [String]$Password - [String]$Description - [String]$Domain - # Hidden Properties - hidden [String]$hostname - hidden [String]$GenericField1 - hidden [String]$GenericField2 - hidden [String]$GenericField3 - hidden [String]$GenericField4 - hidden [String]$GenericField5 - hidden [String]$GenericField6 - hidden [String]$GenericField7 - hidden [String]$GenericField8 - hidden [String]$GenericField9 - hidden [String]$GenericField10 - hidden [int]$AccountTypeID - hidden [string]$notes - hidden [string]$URL - hidden [string]$ExpiryDate - hidden [string]$allowExport - hidden [string]$accounttype - - } - Add-Type -AssemblyName System.Web - # Initalize output Array - $output = @() - } - - Process - { - # Add a reason to the audit log - If ($Reason) - { - $headerreason = @{"Reason" = "$reason"} - } - - Switch ($PSCmdlet.ParameterSetName) - { - # General search - 'General' - { - $uri += "/api/searchpasswords/?Search=$([System.Web.HttpUtility]::UrlEncode($Search))&ExcludePassword=true" - } - # Search on a specific password ID - 'PasswordID' - { - $uri += "/api/passwords/$($PasswordID)?ExcludePassword=true" - } - # Search with a variety of filters - 'Specific' - { - $BuildURL = '?' - If ($Title) { $BuildURL += "Title=$([System.Web.HttpUtility]::UrlEncode($Title))&" } - If ($UserName) { $BuildURL += "UserName=$([System.Web.HttpUtility]::UrlEncode($UserName))&" } - If ($HostName) { $BuildURL += "HostName=$([System.Web.HttpUtility]::UrlEncode($HostName))&" } - If ($Domain) { $BuildURL += "Domain=$([System.Web.HttpUtility]::UrlEncode($Domain))&" } - If ($AccountType) { $BuildURL += "AccountType=$([System.Web.HttpUtility]::UrlEncode($AccountType))&" } - If ($Description) { $BuildURL += "Description=$([System.Web.HttpUtility]::UrlEncode($Description))&" } - If ($Notes) { $BuildURL += "Notes=$([System.Web.HttpUtility]::UrlEncode($Notes))&" } - If ($URL) { $BuildURL += "URL=$([System.Web.HttpUtility]::UrlEncode($URL))&" } - If ($SiteID) { $BuildURL += "SiteID=$([System.Web.HttpUtility]::UrlEncode($SiteID))&" } - If ($SiteLocation) { $BuildURL += "SiteLocation=$([System.Web.HttpUtility]::UrlEncode($SiteLocation))&" } - If ($GenericField1) { $BuildURL += "GenericField1=$([System.Web.HttpUtility]::UrlEncode($GenericField1))&" } - If ($GenericField2) { $BuildURL += "GenericField2=$([System.Web.HttpUtility]::UrlEncode($GenericField2))&" } - If ($GenericField3) { $BuildURL += "GenericField3=$([System.Web.HttpUtility]::UrlEncode($GenericField3))&" } - If ($GenericField4) { $BuildURL += "GenericField4=$([System.Web.HttpUtility]::UrlEncode($GenericField4))&" } - If ($GenericField5) { $BuildURL += "GenericField5=$([System.Web.HttpUtility]::UrlEncode($GenericField5))&" } - If ($GenericField6) { $BuildURL += "GenericField6=$([System.Web.HttpUtility]::UrlEncode($GenericField6))&" } - If ($GenericField7) { $BuildURL += "GenericField7=$([System.Web.HttpUtility]::UrlEncode($GenericField7))&" } - If ($GenericField8) { $BuildURL += "GenericField8=$([System.Web.HttpUtility]::UrlEncode($GenericField8))&" } - If ($GenericField9) { $BuildURL += "GenericField9=$([System.Web.HttpUtility]::UrlEncode($GenericField9))&" } - If ($GenericField10) { $BuildURL += "GenericField10=$([System.Web.HttpUtility]::UrlEncode($GenericField10))&" } - - $BuildURL = $BuildURL -Replace ".$" - - $uri += "/api/searchpasswords/$($BuildURL)&ExcludePassword=true" - } +Function Find-PasswordStatePassword { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '', Justification = 'No Password is used only ID.')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUserNameAndPassWordParams', '', Justification = 'PasswordID isnt a password')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '', Justification = 'Needed for backward compatability')] + [CmdletBinding(DefaultParameterSetName = 'General')] + Param + ( + [Parameter(ParameterSetName = 'General', ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0, Mandatory = $true)][ValidateNotNullOrEmpty()][string]$Search, + [Parameter(ParameterSetName = 'PasswordID', ValueFromPipelineByPropertyName, Position = 0, Mandatory = $true)][ValidateNotNullOrEmpty()][int32]$PasswordID, + [Parameter(ParameterSetName = 'Specific', ValueFromPipelineByPropertyName, Position = 0)][string]$Title, + [Parameter(ParameterSetName = 'Specific', ValueFromPipelineByPropertyName, Position = 1)][string]$UserName, + [Parameter(ParameterSetName = 'Specific', ValueFromPipelineByPropertyName, Position = 2)][string]$HostName, + [Parameter(ParameterSetName = 'Specific', ValueFromPipelineByPropertyName, Position = 3)][string]$Domain, + [Parameter(ParameterSetName = 'Specific', ValueFromPipelineByPropertyName, Position = 4)][string]$AccountType, + [Parameter(ParameterSetName = 'Specific', ValueFromPipelineByPropertyName, Position = 5)][string]$Description, + [Parameter(ParameterSetName = 'Specific', ValueFromPipelineByPropertyName, Position = 6)][string]$Notes, + [Parameter(ParameterSetName = 'Specific', ValueFromPipelineByPropertyName, Position = 7)][string]$URL, + [Parameter(ParameterSetName = 'Specific', ValueFromPipelineByPropertyName, Position = 8)][string]$SiteID, + [Parameter(ParameterSetName = 'Specific', ValueFromPipelineByPropertyName, Position = 9)][string]$SiteLocation, + [Parameter(ParameterSetName = 'Specific', ValueFromPipelineByPropertyName, Position = 10)][string]$GenericField1, + [Parameter(ParameterSetName = 'Specific', ValueFromPipelineByPropertyName, Position = 11)][string]$GenericField2, + [Parameter(ParameterSetName = 'Specific', ValueFromPipelineByPropertyName, Position = 12)][string]$GenericField3, + [Parameter(ParameterSetName = 'Specific', ValueFromPipelineByPropertyName, Position = 13)][string]$GenericField4, + [Parameter(ParameterSetName = 'Specific', ValueFromPipelineByPropertyName, Position = 14)][string]$GenericField5, + [Parameter(ParameterSetName = 'Specific', ValueFromPipelineByPropertyName, Position = 15)][string]$GenericField6, + [Parameter(ParameterSetName = 'Specific', ValueFromPipelineByPropertyName, Position = 16)][string]$GenericField7, + [Parameter(ParameterSetName = 'Specific', ValueFromPipelineByPropertyName, Position = 17)][string]$GenericField8, + [Parameter(ParameterSetName = 'Specific', ValueFromPipelineByPropertyName, Position = 18)][string]$GenericField9, + [Parameter(ParameterSetName = 'Specific', ValueFromPipelineByPropertyName, Position = 19)][string]$GenericField10, + [parameter(ValueFromPipelineByPropertyName, Position = 20)][string]$Reason + ) + + Begin { + . "$PSScriptRoot\PasswordstateClass.ps1" + Add-Type -AssemblyName System.Web + # Initalize output Array + $output = @() } - Try - { - $tempobj = Get-PasswordStateResource -URI $uri -ErrorAction stop + Process { + # Add a reason to the audit log + If ($Reason) { + $headerreason = @{"Reason" = "$reason"} + $parms = @{ExtraParams = @{"Headers" = $headerreason}} + } + + Switch ($PSCmdlet.ParameterSetName) { + # General search + 'General' { + $uri += "/api/searchpasswords/?Search=$([System.Web.HttpUtility]::UrlEncode($Search))&ExcludePassword=true" + } + # Search on a specific password ID + 'PasswordID' { + $uri += "/api/passwords/$($PasswordID)?ExcludePassword=true" + } + # Search with a variety of filters + 'Specific' { + $BuildURL = '?' + If ($Title) { $BuildURL += "Title=$([System.Web.HttpUtility]::UrlEncode($Title))&" } + If ($UserName) { $BuildURL += "UserName=$([System.Web.HttpUtility]::UrlEncode($UserName))&" } + If ($HostName) { $BuildURL += "HostName=$([System.Web.HttpUtility]::UrlEncode($HostName))&" } + If ($Domain) { $BuildURL += "Domain=$([System.Web.HttpUtility]::UrlEncode($Domain))&" } + If ($AccountType) { $BuildURL += "AccountType=$([System.Web.HttpUtility]::UrlEncode($AccountType))&" } + If ($Description) { $BuildURL += "Description=$([System.Web.HttpUtility]::UrlEncode($Description))&" } + If ($Notes) { $BuildURL += "Notes=$([System.Web.HttpUtility]::UrlEncode($Notes))&" } + If ($URL) { $BuildURL += "URL=$([System.Web.HttpUtility]::UrlEncode($URL))&" } + If ($SiteID) { $BuildURL += "SiteID=$([System.Web.HttpUtility]::UrlEncode($SiteID))&" } + If ($SiteLocation) { $BuildURL += "SiteLocation=$([System.Web.HttpUtility]::UrlEncode($SiteLocation))&" } + If ($GenericField1) { $BuildURL += "GenericField1=$([System.Web.HttpUtility]::UrlEncode($GenericField1))&" } + If ($GenericField2) { $BuildURL += "GenericField2=$([System.Web.HttpUtility]::UrlEncode($GenericField2))&" } + If ($GenericField3) { $BuildURL += "GenericField3=$([System.Web.HttpUtility]::UrlEncode($GenericField3))&" } + If ($GenericField4) { $BuildURL += "GenericField4=$([System.Web.HttpUtility]::UrlEncode($GenericField4))&" } + If ($GenericField5) { $BuildURL += "GenericField5=$([System.Web.HttpUtility]::UrlEncode($GenericField5))&" } + If ($GenericField6) { $BuildURL += "GenericField6=$([System.Web.HttpUtility]::UrlEncode($GenericField6))&" } + If ($GenericField7) { $BuildURL += "GenericField7=$([System.Web.HttpUtility]::UrlEncode($GenericField7))&" } + If ($GenericField8) { $BuildURL += "GenericField8=$([System.Web.HttpUtility]::UrlEncode($GenericField8))&" } + If ($GenericField9) { $BuildURL += "GenericField9=$([System.Web.HttpUtility]::UrlEncode($GenericField9))&" } + If ($GenericField10) { $BuildURL += "GenericField10=$([System.Web.HttpUtility]::UrlEncode($GenericField10))&" } + + $BuildURL = $BuildURL -Replace ".$" + + $uri += "/api/searchpasswords/$($BuildURL)&ExcludePassword=true" + } + } + + Try { + $tempobj = Get-PasswordStateResource -URI $uri -ErrorAction stop + } + Catch { + Throw $_.Exception + } + + Foreach ($item in $tempobj) { + [PasswordResult]$obj = Get-PasswordStateResource -URI "/api/passwords/$($item.PasswordID)" @parms -Method GET + $obj.Password = [EncryptedPassword]$obj.Password + $output += $obj + } } - Catch - { - Throw $_.Exception - } - - Foreach ($item in $tempobj) - { - [PasswordResult]$obj = Get-PasswordStateResource -URI "/api/passwords/$($item.PasswordID)" -ExtraParams @{"Headers" = $headerreason} -Method GET - $output += $obj - } - } - End - { - If ($output.count -gt 0) - { - Return $output - } - Else - { - Throw "No Password found" + End { + If ($output.count -gt 0) { + switch ($global:PasswordStateShowPasswordsPlainText) { + True { + $output.DecryptPassword() + } + } + Return $output + } + Else { + Throw "No Password found" + } } - } -} +} \ No newline at end of file diff --git a/functions/Get-PasswordStateEnvironment.ps1 b/functions/Get-PasswordStateEnvironment.ps1 index 2c165f9..a804e6a 100644 --- a/functions/Get-PasswordStateEnvironment.ps1 +++ b/functions/Get-PasswordStateEnvironment.ps1 @@ -38,7 +38,11 @@ function Get-PasswordStateEnvironment { $apikey = $cred.GetNetworkCredential().Password $output.apikey = $apikey } - + if ($output.PasswordGeneratorAPIKey){ + $cred2 = New-Object System.Management.Automation.PSCredential -ArgumentList "username", $($output.PasswordGeneratorAPIKey | ConvertTo-SecureString) + $pwgen = $cred2.GetNetworkCredential().Password + $output.PasswordGeneratorAPIKey = $pwgen + } } end { diff --git a/functions/Get-PasswordStateFolder.Tests.ps1 b/functions/Get-PasswordStateFolder.Tests.ps1 index 091cfe5..9db8ed2 100644 --- a/functions/Get-PasswordStateFolder.Tests.ps1 +++ b/functions/Get-PasswordStateFolder.Tests.ps1 @@ -2,23 +2,36 @@ $here = Split-Path -Parent $MyInvocation.MyCommand.Path $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.' . "$here\$sut" Import-Module "$here\..\passwordstate-management.psm1" + Describe "Get-PasswordStateFolder" { It "Finds a Folder From Password State" { - Mock -CommandName Get-PasswordStateResource -MockWith {return [PSCustomObject]@{ - "FolderID" = "4" - "FolderName" = "Test" - "TreePath" = "\Root\Test" - "Description" = "" - } - } -ParameterFilter {$uri -eq "/api/folders/?FolderName=Test"} - (Get-PasswordStateFolder -Name "Test").FolderID | Should -BeExactly 4 + (Get-PasswordStateFolder -Name "Test").FolderID | Should -not -BeNullOrEmpty } - It "Generates a web exception" { - Mock -CommandName Get-PasswordStateResource -MockWith {throw [System.Net.WebException]"Resource Not Found" - } -ParameterFilter {$uri -eq "/api/folders/?FolderName=Test"} - {Get-PasswordStateFolder -Name "Test"} | Should -Throw + {Get-PasswordStateFolder -Name "DoesntExist"} | Should -Throw + } + + BeforeAll { + New-PasswordStateFolder -Name Test -description Test + } + BeforeEach { + # Create Test Environment + try { + $globalsetting = Get-Variable PasswordStateShowPasswordsPlainText -ErrorAction stop -Verbose -ValueOnly + $global:PasswordStateShowPasswordsPlainText = $false + } + Catch { + New-Variable -Name PasswordStateShowPasswordsPlainText -Value $false -Scope Global + } + Move-Item "$($env:USERPROFILE)\passwordstate.json" "$($env:USERPROFILE)\passwordstate.json.bak" -force + Set-PasswordStateEnvironment -Apikey "$env:pwsapikey" -Baseuri "$env:pwsuri" + } + + AfterEach { + # Remove Test Environment + Move-Item "$($env:USERPROFILE)\passwordstate.json.bak" "$($env:USERPROFILE)\passwordstate.json" -force + $global:PasswordStateShowPasswordsPlainText = $globalsetting + # } - } diff --git a/functions/Get-PasswordStateFolder.ps1 b/functions/Get-PasswordStateFolder.ps1 index 57d061f..42826d4 100644 --- a/functions/Get-PasswordStateFolder.ps1 +++ b/functions/Get-PasswordStateFolder.ps1 @@ -22,6 +22,7 @@ function Get-PasswordStateFolder { [parameter(ValueFromPipelineByPropertyName, Position = 0, ParameterSetName = 1)][string]$Name ) begin { + . "$PSScriptRoot\PasswordstateClass.ps1" # Initialize the array for output $output = @() } diff --git a/functions/Get-PasswordStateList.Tests.ps1 b/functions/Get-PasswordStateList.Tests.ps1 index 93ccf6f..c5b23b8 100644 --- a/functions/Get-PasswordStateList.Tests.ps1 +++ b/functions/Get-PasswordStateList.Tests.ps1 @@ -4,49 +4,31 @@ $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.' Import-Module "$here\..\passwordstate-management.psm1" Describe "Get-PasswordStateList" { It "Returns All Password State Password Lists" { - Mock -CommandName Get-PasswordStateResource -MockWith {return [PSCustomObject]@{ - "PasswordListID" = 7 - "PasswordList" = "MockedList" - "Description" = "" - "ImageFileName" = "" - "Guide" = "" - "AllowExport" = $true - "PrivatePasswordList" = $false - "TimeBasedAccessRequired" = $false - "HandshakeApprovalRequired" = $false - "PasswordStrengthPolicyID" = 1 - "PasswordGeneratorID" = 0 - "CodePage" = "Using Passwordstate Default Code Page" - "PreventPasswordReuse" = 5 - "AuthenticationType" = "None Required" - "AuthenticationPerSession" = $false - "PreventExpiryDateModification" = $false - "SetExpiryDate" = 0 - "ResetExpiryDate" = 0 - "PreventDragDrop" = $true - "PreventBadPasswordUse" = $true - "ProvideAccessReason" = $false - "TreePath" = "\\SomePath\\Somesubpath" - "TotalPasswords" = 2 - "GeneratorName" = "Using user\u0027s personal Password Generator Options" - "PolicyName" = "Default Policy" - "PasswordResetEnabled" = $false - "ForcePasswordGenerator" = $false - "HidePasswords" = $false - "ShowGuide" = $false - "EnablePasswordResetSchedule" = $false - "PasswordResetSchedule" = "00:00" - "AddDaysToExpiryDate" = 90 - "SiteID" = 0 - "SiteLocation" = $null - } - } -ParameterFilter {$uri -eq "/api/passwordlists"} - - Mock -CommandName Get-PasswordStateEnvironment -MockWith {return [PSCustomObject]@{ - "Baseuri" = "https://passwordstateserver.co.uk" - "APIKey" = "WindowsAuth" - } + (Get-PasswordStateList).PasswordListID | Should -not -BeNullOrEmpty + } + It "Search Password State Password Lists by ID" { + (Get-PasswordStateList -Searchby ID -PasswordListID 1).PasswordListID | Should -BeExactly 1 + } + It "Search Password State Password Lists by Name" { + (Get-PasswordStateList -Searchby Name -SearchName "test2").PasswordList | Should -BeExactly "test2" + } + BeforeEach { + # Create Test Environment + try { + $globalsetting = Get-Variable PasswordStateShowPasswordsPlainText -ErrorAction stop -Verbose -ValueOnly + $global:PasswordStateShowPasswordsPlainText = $false } - (Get-PasswordStateList -Searchby ID).PasswordListID | Should -BeOfType Int32 + Catch { + New-Variable -Name PasswordStateShowPasswordsPlainText -Value $false -Scope Global + } + Move-Item "$($env:USERPROFILE)\passwordstate.json" "$($env:USERPROFILE)\passwordstate.json.bak" -force + Set-PasswordStateEnvironment -Apikey "$env:pwsapikey" -Baseuri "$env:pwsuri" + } + + AfterEach { + # Remove Test Environment + Move-Item "$($env:USERPROFILE)\passwordstate.json.bak" "$($env:USERPROFILE)\passwordstate.json" -force + $global:PasswordStateShowPasswordsPlainText = $globalsetting + # } } diff --git a/functions/Get-PasswordStateList.ps1 b/functions/Get-PasswordStateList.ps1 index c72241e..d1f48de 100644 --- a/functions/Get-PasswordStateList.ps1 +++ b/functions/Get-PasswordStateList.ps1 @@ -28,6 +28,7 @@ function Get-PasswordStateList { ) begin { + . "$PSScriptRoot\PasswordstateClass.ps1" } process { diff --git a/functions/Get-PasswordStatePasswordHistory.Tests.ps1 b/functions/Get-PasswordStatePasswordHistory.Tests.ps1 index 25072c8..2f8bcef 100644 --- a/functions/Get-PasswordStatePasswordHistory.Tests.ps1 +++ b/functions/Get-PasswordStatePasswordHistory.Tests.ps1 @@ -3,22 +3,41 @@ $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.' . "$here\$sut" Import-Module "$here\..\passwordstate-management.psm1" Describe "Get-PasswordStatePasswordHistory" { - It "Password history should be at least 1" { - Mock -CommandName Get-PasswordStateResource -MockWith {return [PSCustomObject]@{ - "Title" = "testuser" - "Username" = "test" - "Domain" = "" - "Description" = "" - "PasswordId" = 3 - "AccountType" = "" - "URL" = "" - "Passwordlist" = "MockedList" - "PasswordListID" = 7 - "Password" = "testpassword" - "DateChanged" = "07/06/2018 09:22:12" - } - } -ParameterFilter {$uri -eq "/api/passwordhistory/3"} - - (Get-PasswordStatePasswordHistory -PasswordID 3).DateChanged | Should -Not -BeNullOrEmpty + It "Gets Password History"{ + (Get-PasswordStatePasswordHistory -PasswordID 1).DateChanged | Should -Not -BeNullOrEmpty + } + It "Checks Password is returned as type [System.Security.SecureString]" { + (Get-PasswordStatePasswordHistory -PasswordID "1").Password.Password | Should -BeOfType [System.Security.SecureString] + } + It "Checks Password is decrypted by method .GetPassword()" { + (Get-PasswordStatePasswordHistory -PasswordID "1").GetPassword() | Should -BeOfType [String] + } + It "Checks Password is decrypted by method .DecryptPassword()" { + $result = (Get-PasswordStatePasswordHistory -PasswordID "1") + $result.DecryptPassword() + $result.Password | Should -BeOfType [String] + } + It "Checks `$global:PasswordStateShowPasswordsPlainText is honoured" { + $global:PasswordStateShowPasswordsPlainText = $true + (Get-PasswordStatePasswordHistory -PasswordID "1").Password | Should -BeOfType [String] + } + BeforeEach { + # Create Test Environment + try { + $globalsetting = Get-Variable PasswordStateShowPasswordsPlainText -ErrorAction stop -Verbose -ValueOnly + $global:PasswordStateShowPasswordsPlainText = $false + } + Catch { + New-Variable -Name PasswordStateShowPasswordsPlainText -Value $false -Scope Global + } + Move-Item "$($env:USERPROFILE)\passwordstate.json" "$($env:USERPROFILE)\passwordstate.json.bak" -force + Set-PasswordStateEnvironment -Apikey "$env:pwsapikey" -Baseuri "$env:pwsuri" + } + + AfterEach { + # Remove Test Environment + Move-Item "$($env:USERPROFILE)\passwordstate.json.bak" "$($env:USERPROFILE)\passwordstate.json" -force + $global:PasswordStateShowPasswordsPlainText = $globalsetting + # } } diff --git a/functions/Get-PasswordStatePasswordHistory.ps1 b/functions/Get-PasswordStatePasswordHistory.ps1 index 0da32d6..1250d44 100644 --- a/functions/Get-PasswordStatePasswordHistory.ps1 +++ b/functions/Get-PasswordStatePasswordHistory.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS Gets a password state entry historical password entries. .DESCRIPTION @@ -18,28 +18,42 @@ Daryl Newsholme 2018 #> function Get-PasswordStatePasswordHistory { - [Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSAvoidUsingPlainTextForPassword', '', Justification = 'Not a password just an ID' - )] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '', Justification = 'Not a password just an ID')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '', Justification = 'Needed for backward compatability')] [CmdletBinding()] + [OutputType('System.Object[]')] param ( [parameter(ValueFromPipelineByPropertyName, Position = 0)][int32]$PasswordID, [parameter(ValueFromPipelineByPropertyName, Position = 1, Mandatory = $false)][string]$reason ) begin { + . "$PSScriptRoot\PasswordstateClass.ps1" + $output = @() } process { - if ($reason) { + If ($Reason) { $headerreason = @{"Reason" = "$reason"} + $parms = @{ExtraParams = @{"Headers" = $headerreason}} + } + Else { + $parms = @{} + } + $results = Get-PasswordStateResource -uri "/api/passwordhistory/$($PasswordID)" @parms + Foreach ($result in $results) { + $result = [PasswordHistory]$result + $result.Password = [EncryptedPassword]$result.Password + $output += $result } - $result = Get-PasswordStateResource -uri "/api/passwordhistory/$($PasswordID)" -extraparams @{"Headers" = $headerreason} - } end { - # Use select to make sure output is returned in a sensible order. - Return $result + switch ($global:PasswordStateShowPasswordsPlainText) { + True { + $output.DecryptPassword() + } + } + Return $output } } \ No newline at end of file diff --git a/functions/Get-PasswordStatePasswords.ps1 b/functions/Get-PasswordStatePasswords.ps1 index 2781d7b..40125c6 100644 --- a/functions/Get-PasswordStatePasswords.ps1 +++ b/functions/Get-PasswordStatePasswords.ps1 @@ -17,18 +17,30 @@ function Get-PasswordStatePasswords { )] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUserNameAndPassWordParams', '', Justification = 'PasswordID isnt a password')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Only returns multiple')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '', Justification = 'Needed for backward compatability')] param ( - [Parameter(ParameterSetName='GetAllPasswordsFromList', Mandatory = $false,ValueFromPipeline=$True,ValueFromPipelinebyPropertyName=$True)][int32[]]$PasswordlistID + [Parameter(ParameterSetName='GetAllPasswordsFromList', Mandatory = $false,ValueFromPipeline=$True,ValueFromPipelinebyPropertyName=$True, Position = 0)][int32[]]$PasswordlistID ) begin { + . "$PSScriptRoot\PasswordstateClass.ps1" + $output = @() } process { - $output = Get-PasswordStateResource -uri $("/api/passwords/" + $PasswordlistID + "?QueryAll") + [PasswordResult]$results = Get-PasswordStateResource -uri $("/api/passwords/" + $PasswordlistID + "?QueryAll") + Foreach ($result in $results){ + $result.Password = [EncryptedPassword]$result.Password + $output += $result + } } end { - return $output + switch ($global:PasswordStateShowPasswordsPlainText) { + True { + $output.DecryptPassword() + } + } + Return $output } } \ No newline at end of file diff --git a/functions/Get-PasswordStateResource.ps1 b/functions/Get-PasswordStateResource.ps1 index f3facb3..0fa7ef8 100644 --- a/functions/Get-PasswordStateResource.ps1 +++ b/functions/Get-PasswordStateResource.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS A function to simplify the Retrieval of password state resources via the rest API .DESCRIPTION @@ -32,7 +32,7 @@ function Get-PasswordStateResource { # Force TLS 1.2 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # Import the environment - $passwordstateenvironment = $(Get-PasswordStateEnvironment) + $passwordstateenvironment = Get-PasswordStateEnvironment # If the apikey is windowsauth then rebuild the uri string to match the windows auth apis, otherwise just build the api headers. Switch ($passwordstateenvironment.AuthType) { WindowsIntegrated { @@ -42,7 +42,15 @@ function Get-PasswordStateResource { $uri = $uri.Replace("api", "winapi") } APIKey { - $headers = @{"APIKey" = "$($passwordstateenvironment.Apikey)"} + switch -Wildcard ($uri){ + /api/generatepassword* { + Write-Verbose "[$(Get-Date -format G)] Using generate password api key" + $headers = @{"APIKey" = "$($passwordstateenvironment.PasswordGeneratorAPIKey)"} + } + Default { + $headers = @{"APIKey" = "$($passwordstateenvironment.Apikey)"} + } + } } } } @@ -54,11 +62,23 @@ function Get-PasswordStateResource { "ContentType" = $ContentType "Method" = $method.ToUpper() } - if ($extraparams) { + if (!$body){ + $params.Remove("Body") + } + if ($headers -and $null -ne $extraparams.Headers) { + Write-Verbose "[$(Get-Date -format G)] Adding API Headers and extra param headers" + $headers += $extraparams.headers + $params += @{"headers" = $headers} + $skipheaders = $true + } + if ($extraparams -and $null -eq $extraparams.Headers){ + Write-Verbose "[$(Get-Date -format G)] Adding extra parameter $($extraparams.keys) $($extraparams.values)" $params += $extraparams } - if ($headers) { - $params += $headers + + if ($headers -and $skipheaders -ne $true) { + Write-Verbose "[$(Get-Date -format G)] Adding API Headers only" + $params += @{"headers" = $headers} } Switch ($passwordstateenvironment.AuthType) { APIKey { diff --git a/functions/New-PasswordStateDocument.Tests.ps1 b/functions/New-PasswordStateDocument.Tests.ps1 index 79c3e4a..a07713e 100644 --- a/functions/New-PasswordStateDocument.Tests.ps1 +++ b/functions/New-PasswordStateDocument.Tests.ps1 @@ -2,14 +2,31 @@ $here = Split-Path -Parent $MyInvocation.MyCommand.Path $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.' . "$here\$sut" Import-Module "$here\..\passwordstate-management.psm1" -Describe "Get-PasswordStateList" { - It "Returns All Password State Password Lists" { - Mock -CommandName New-PasswordStateResource -MockWith {return [PSCustomObject]@{ - "DocumentID" = 4 - "DocumentName" = "Test" - } - } -ParameterFilter {$uri -eq "/api/document/password/3?DocumentName=Test&DocumentDescription=Test"} +Describe "New-PasswordDocument" { + BeforeAll{ "Test" | Out-File "TestDrive:\1.txt" - (New-PasswordStateDocument -ID 3 -resourcetype password -DocumentName "Test" -DocumentDescription "Test" -Path "TestDrive:\1.txt").DocumentID | Should -BeOfType Int32 + } + It "Adds a document to a password" { + (New-PasswordStateDocument -ID 1 -resourcetype password -DocumentName "Test" -DocumentDescription "Test" -Path "TestDrive:\1.txt").DocumentID | Should -not -BeNullOrEmpty + } + + BeforeEach { + # Create Test Environment + try { + $globalsetting = Get-Variable PasswordStateShowPasswordsPlainText -ErrorAction stop -Verbose -ValueOnly + $global:PasswordStateShowPasswordsPlainText = $false + } + Catch { + New-Variable -Name PasswordStateShowPasswordsPlainText -Value $false -Scope Global + } + Move-Item "$($env:USERPROFILE)\passwordstate.json" "$($env:USERPROFILE)\passwordstate.json.bak" -force + Set-PasswordStateEnvironment -Apikey "$env:pwsapikey" -Baseuri "$env:pwsuri" + } + + AfterEach { + # Remove Test Environment + Move-Item "$($env:USERPROFILE)\passwordstate.json.bak" "$($env:USERPROFILE)\passwordstate.json" -force + $global:PasswordStateShowPasswordsPlainText = $globalsetting + # } } diff --git a/functions/New-PasswordStateDocument.ps1 b/functions/New-PasswordStateDocument.ps1 index a54c264..6d36443 100644 --- a/functions/New-PasswordStateDocument.ps1 +++ b/functions/New-PasswordStateDocument.ps1 @@ -45,6 +45,7 @@ function New-PasswordStateDocument { ) begin { + . "$PSScriptRoot\PasswordstateClass.ps1" $output = @() } diff --git a/functions/New-PasswordStateFolder.ps1 b/functions/New-PasswordStateFolder.ps1 index 95d9501..d12821e 100644 --- a/functions/New-PasswordStateFolder.ps1 +++ b/functions/New-PasswordStateFolder.ps1 @@ -37,7 +37,7 @@ function New-PasswordStateFolder { ) begin { - + . "$PSScriptRoot\PasswordstateClass.ps1" } process { diff --git a/functions/New-PasswordStateList.Tests.ps1 b/functions/New-PasswordStateList.Tests.ps1 index bd46ca4..2e6ece0 100644 --- a/functions/New-PasswordStateList.Tests.ps1 +++ b/functions/New-PasswordStateList.Tests.ps1 @@ -3,45 +3,26 @@ $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.' . "$here\$sut" Import-Module "$here\..\passwordstate-management.psm1" Describe "New-PasswordStateList" { - It "Creates " { - Mock -CommandName New-PasswordStateResource -MockWith {return [PSCustomObject]@{ - - PasswordListID = 4 - PasswordList = "Test" - Description = "Test" - ImageFileName = "" - Guide = "" - AllowExport = "True" - PrivatePasswordList = "False" - TimeBasedAccessRequired = "False" - HandshakeApprovalRequired = "False" - PasswordStrengthPolicyID = 1 - PasswordGeneratorID = 0 - CodePage = "Using Passwordstate Default Code Page" - PreventPasswordReuse = 5 - AuthenticationType = "None Required" - AuthenticationPerSession = "False" - PreventExpiryDateModification = "False" - SetExpiryDate = 0 - ResetExpiryDate = 0 - PreventDragDrop = "True" - PreventBadPasswordUse = "True" - ProvideAccessReason = "False" - TreePath = "\Root\Test" - TotalPasswords = 39 - GeneratorName = "Using user's personal Password Generator Options" - PolicyName = "Default Policy" - PasswordResetEnabled = "False" - ForcePasswordGenerator = "False" - HidePasswords = "False" - ShowGuide = "False" - EnablePasswordResetSchedule = "False" - PasswordResetSchedule = "00:00" - AddDaysToExpiryDate = 90 - SiteID = 0 - SiteLocation = "" - } - } -ParameterFilter {$uri -eq "/api/passwordlists" -and $body -ne $null} - (New-PasswordStateList -Name "test" -description "Test" -FolderID 3) | Should -not -benullorempty + It "Creates a Password List" { + # (New-PasswordStateList -Name "test2" -description "Test" -FolderID 15 -CopySettingsFromPasswordListID 1) | Should -not -benullorempty + } + BeforeEach { + # Create Test Environment + try { + $globalsetting = Get-Variable PasswordStateShowPasswordsPlainText -ErrorAction stop -Verbose -ValueOnly + $global:PasswordStateShowPasswordsPlainText = $false + } + Catch { + New-Variable -Name PasswordStateShowPasswordsPlainText -Value $false -Scope Global + } + Move-Item "$($env:USERPROFILE)\passwordstate.json" "$($env:USERPROFILE)\passwordstate.json.bak" -force + Set-PasswordStateEnvironment -Apikey "$env:pwsapikey" -Baseuri "$env:pwsuri" + } + + AfterEach { + # Remove Test Environment + Move-Item "$($env:USERPROFILE)\passwordstate.json.bak" "$($env:USERPROFILE)\passwordstate.json" -force + $global:PasswordStateShowPasswordsPlainText = $globalsetting + # } } diff --git a/functions/New-PasswordStateList.ps1 b/functions/New-PasswordStateList.ps1 index 1129a71..e15cce3 100644 --- a/functions/New-PasswordStateList.ps1 +++ b/functions/New-PasswordStateList.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS Creates a passwordstate List. @@ -31,15 +31,14 @@ function New-PasswordStateList { param ( [parameter(ValueFromPipelineByPropertyName, Mandatory = $true)][string]$Name, [parameter(ValueFromPipelineByPropertyName, Mandatory = $true)][string]$description, - [parameter(ValueFromPipelineByPropertyName)][int32]$CopySettingsFromPasswordListID = $null, + [parameter(ValueFromPipelineByPropertyName, Mandatory = $true)][int32]$CopySettingsFromPasswordListID, [parameter(ValueFromPipelineByPropertyName, Mandatory = $true)][int32]$FolderID ) begin { - + . "$PSScriptRoot\PasswordstateClass.ps1" } - process { # Build the Custom object to convert to json and send to the api. $body = [pscustomobject]@{ @@ -47,9 +46,18 @@ function New-PasswordStateList { "Description" = $description "CopySettingsFromPasswordListID" = $CopySettingsFromPasswordListID "NestUnderFolderID" = $FolderID + "LinkToTemplate" = $false + "CopySettingsFromTemplateID" = "" + "SiteID" = "0" + } + $penv = Get-PasswordStateEnvironment + if ($penv.AuthType -eq "APIKey"){ + $body | Add-Member -MemberType NoteProperty -Name "APIKey" -Value $penv.Apikey } if ($PSCmdlet.ShouldProcess("$Name under folder $folderid")) { - $output = New-PasswordStateResource -uri "/api/passwordlists" -body "$($body|convertto-json)" + $body = "$($body|convertto-json)" + Write-Verbose "$body" + $output = New-PasswordStateResource -uri "/api/passwordlists" -body $body } } diff --git a/functions/New-PasswordStatePassword.Tests.ps1 b/functions/New-PasswordStatePassword.Tests.ps1 index d7ca4b2..83b612d 100644 --- a/functions/New-PasswordStatePassword.Tests.ps1 +++ b/functions/New-PasswordStatePassword.Tests.ps1 @@ -3,44 +3,52 @@ $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.' . "$here\$sut" Import-Module "$here\..\passwordstate-management.psm1" Describe "New-PasswordStatePassword" { - It "Creates a new passwords state entry for testuser" { - Mock -CommandName Get-PasswordStateResource -MockWith {return [PSCustomObject]@{ - "Title" = "testuser" - "Username" = "test" - "Domain" = "" - "Description" = "" - "PasswordId" = 3 - "AccountType" = "" - "URL" = "" - "Passwordlist" = "MockedList" - "PasswordListID" = 7 - } - } -ParameterFilter {$uri -eq "/api/passwords/7?QueryAll&ExcludePassword=true"} - - Mock -CommandName Find-PasswordStatePassword -MockWith { - return $null - } -ParameterFilter {$title -eq "testuser" -and $username -eq "test"} - - Mock -CommandName Get-PasswordStateEnvironment -MockWith {return [PSCustomObject]@{ - "Baseuri" = "https://passwordstateserver.co.uk" - "APIKey" = "WindowsAuth" + It "Creates a new passwors state entry Password Entry" { + $result = New-PasswordStatePassword -title "bob" -username "test" -passwordlistID "1" -Password "Password.1" + ($result).GetPassword() | Should -BeExactly "Password.1" + } + It "Checks a new password state entry Password Entry returns an encrypted string" { + $result = New-PasswordStatePassword -title "bob" -username "test" -passwordlistID "1" -Password "Password.1" + ($result).Password.Password | Should -BeOfType [System.Security.SecureString] + } + It "Checks `$global:PasswordStateShowPasswordsPlainText is honoured" { + $global:PasswordStateShowPasswordsPlainText = $true + $result = New-PasswordStatePassword -title "bob" -username "test" -passwordlistID "1" -Password "Password.1" + ($result).Password | Should -BeExactly "Password.1" + } + It "Fails to create a password when one already matches" { + $result = New-PasswordStatePassword -title "bob" -username "test" -passwordlistID "1" -Password "Password.1" + {New-PasswordStatePassword -title "bob" -username "test" -passwordlistID "1" -Password "Password.1"} | Should -Throw + } + BeforeEach { + # Create Test Environment + try { + $globalsetting = Get-Variable PasswordStateShowPasswordsPlainText -ErrorAction stop -Verbose -ValueOnly + $global:PasswordStateShowPasswordsPlainText = $false + } + Catch { + New-Variable -Name PasswordStateShowPasswordsPlainText -Value $false -Scope Global + } + Move-Item "$($env:USERPROFILE)\passwordstate.json" "$($env:USERPROFILE)\passwordstate.json.bak" -force + Set-PasswordStateEnvironment -Apikey "$env:pwsapikey" -Baseuri "$env:pwsuri" + try { + Find-PasswordStatePassword bob -ErrorAction stop |Remove-PasswordStatePassword -ErrorAction stop + } + Catch{ - } } - Mock -CommandName New-PasswordStateResource -MockWith {return [PSCustomObject]@{ - "Title" = "testuser" - "Password" = "Password.1" - "Username" = "test" - "Domain" = "" - "Description" = "" - "PasswordId" = 3 - "AccountType" = "" - "URL" = "" - "Passwordlist" = "MockedList" - "PasswordListID" = 7 - } - } -ParameterFilter {$uri -eq "/api/passwords" -and $body -notlike $null} + } + + AfterEach { + # Remove Test Environment + Move-Item "$($env:USERPROFILE)\passwordstate.json.bak" "$($env:USERPROFILE)\passwordstate.json" -force + $global:PasswordStateShowPasswordsPlainText = $globalsetting + try { + Find-PasswordStatePassword bob -ErrorAction stop |Remove-PasswordStatePassword -ErrorAction stop + } + Catch{ - (New-PasswordStatePassword -title "testuser" -username "test" -passwordlistID "7" -Password "Password.1").Password | Should -BeExactly "Password.1" + } + # } } diff --git a/functions/New-PasswordStatePassword.ps1 b/functions/New-PasswordStatePassword.ps1 index fce31e4..7dda8bd 100644 --- a/functions/New-PasswordStatePassword.ps1 +++ b/functions/New-PasswordStatePassword.ps1 @@ -41,6 +41,7 @@ function New-PasswordStatePassword { 'PSAvoidUsingPlainTextForPassword', '', Justification = 'Password can only be passed to api in plaintext due to passwordstate api' )] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUserNameAndPassWordParams', '', Justification = 'Credential would break cmdlet flow')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '', Justification = 'Needed for backward compatability')] [CmdletBinding(SupportsShouldProcess = $true)] param ( [parameter(ValueFromPipelineByPropertyName)][int32]$passwordlistID, @@ -54,6 +55,7 @@ function New-PasswordStatePassword { ) begin { + . "$PSScriptRoot\PasswordstateClass.ps1" # Check to see if the requested password entry exists before continuing. try { $result = Find-PasswordStatePassword -title "$title" -username $username -ErrorAction stop @@ -72,6 +74,7 @@ function New-PasswordStatePassword { } } } + . "$PSScriptRoot\PasswordstateClass.ps1" } process { @@ -93,12 +96,20 @@ function New-PasswordStatePassword { } } if ($PSCmdlet.ShouldProcess("PasswordList:$passwordListID Title:$title Username:$username")) { - $output = New-PasswordStateResource -uri "/api/passwords" -body "$($body|convertto-json)" + [PasswordResult]$output = New-PasswordStateResource -uri "/api/passwords" -body "$($body|convertto-json)" + foreach ($i in $output){ + $i.Password = [EncryptedPassword]$i.Password + } } } } end { - return $output + switch ($global:PasswordStateShowPasswordsPlainText) { + True { + $output.DecryptPassword() + } + } + Return $output } } \ No newline at end of file diff --git a/functions/New-PasswordStateResource.ps1 b/functions/New-PasswordStateResource.ps1 index c6f7b23..67ad627 100644 --- a/functions/New-PasswordStateResource.ps1 +++ b/functions/New-PasswordStateResource.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS A function to simplify the Creation of password state resources via the rest API .DESCRIPTION @@ -57,11 +57,23 @@ function New-PasswordStateResource { "ContentType" = $ContentType "Body" = $body } - if ($extraparams) { + if (!$body){ + $params.Remove("Body") + } + if ($headers -and $null -ne $extraparams.Headers) { + Write-Verbose "[$(Get-Date -format G)] Adding API Headers and extra param headers" + $headers += $extraparams.headers + $params += @{"headers" = $headers} + $skipheaders = $true + } + if ($extraparams -and $null -eq $extraparams.Headers){ + Write-Verbose "[$(Get-Date -format G)] Adding extra parameter $($extraparams.keys) $($extraparams.values)" $params += $extraparams } - if ($headers) { - $params += $headers + + if ($headers -and $skipheaders -ne $true) { + Write-Verbose "[$(Get-Date -format G)] Adding API Headers only" + $params += @{"headers" = $headers} } if ($PSCmdlet.ShouldProcess("[$($params.Method)] uri:$($params.uri) Headers:$($headers) Body:$($params.body)")) { Switch ($passwordstateenvironment.AuthType) { diff --git a/functions/New-RandomPassword.Tests.ps1 b/functions/New-RandomPassword.Tests.ps1 index 18652f6..198fd6f 100644 --- a/functions/New-RandomPassword.Tests.ps1 +++ b/functions/New-RandomPassword.Tests.ps1 @@ -3,11 +3,29 @@ $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.' . "$here\$sut" Import-Module "$here\..\passwordstate-management.psm1" Describe "New-RandomPassword" { - It "Returns A Random Complex Password" { - Mock -CommandName Get-PasswordStateResource -MockWith {return [PSCustomObject]@{ - "Password" = "92839jidhiuwmdowkled-" - } - } -ParameterFilter {$uri -eq "/api/generatepassword"} - (New-RandomPassword).Password | Should -BeOfType String + It "Generates a default Password" { + (New-RandomPassword) | Should -not -BeNullOrEmpty + } + + It "Generates a default Password of length 20" { + (New-RandomPassword -length 20).Password.length | Should -BeExactly 20 + } + BeforeEach { + # Create Test Environment + try { + $globalsetting = Get-Variable PasswordStateShowPasswordsPlainText -ErrorAction stop -Verbose -ValueOnly + $global:PasswordStateShowPasswordsPlainText = $false + } + Catch { + New-Variable -Name PasswordStateShowPasswordsPlainText -Value $false -Scope Global + } + Move-Item "$($env:USERPROFILE)\passwordstate.json" "$($env:USERPROFILE)\passwordstate.json.bak" -force + Set-PasswordStateEnvironment -Apikey "$env:pwsapikey" -Baseuri "$env:pwsuri" -PasswordGeneratorAPIkey "$env:pwsgenapikey" + } + + AfterEach { + # Remove Test Environment + Move-Item "$($env:USERPROFILE)\passwordstate.json.bak" "$($env:USERPROFILE)\passwordstate.json" -force + $global:PasswordStateShowPasswordsPlainText = $globalsetting } } diff --git a/functions/New-RandomPassword.ps1 b/functions/New-RandomPassword.ps1 index 9333c4a..dabc8c9 100644 --- a/functions/New-RandomPassword.ps1 +++ b/functions/New-RandomPassword.ps1 @@ -55,6 +55,7 @@ function New-RandomPassword { ) begin { + . "$PSScriptRoot\PasswordstateClass.ps1" } process { diff --git a/functions/PasswordStateClass.ps1 b/functions/PasswordStateClass.ps1 new file mode 100644 index 0000000..ed9ea36 --- /dev/null +++ b/functions/PasswordStateClass.ps1 @@ -0,0 +1,66 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'Script is converting to secure string.')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '', Justification = 'Script is converting to secure string.')] +# Create Class +Class EncryptedPassword { + EncryptedPassword ($Password) { + $this.Password = ConvertTo-SecureString -String $Password -AsPlainText -Force + } + [SecureString]$Password +} +class PasswordResult { + # Properties + [int]$PasswordID + [String]$Title + [String]$Username + $Password + [String]GetPassword() { + $SecureString = $this.Password.Password + $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString) + return [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) + } + DecryptPassword(){ + $SecureString = $this.Password.Password + $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString) + $this.Password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) + } + [String]$Description + [String]$Domain + # Hidden Properties + hidden [String]$hostname + hidden [String]$GenericField1 + hidden [String]$GenericField2 + hidden [String]$GenericField3 + hidden [String]$GenericField4 + hidden [String]$GenericField5 + hidden [String]$GenericField6 + hidden [String]$GenericField7 + hidden [String]$GenericField8 + hidden [String]$GenericField9 + hidden [String]$GenericField10 + hidden [int]$AccountTypeID + hidden [string]$notes + hidden [string]$URL + hidden [string]$ExpiryDate + hidden [string]$allowExport + hidden [string]$accounttype + + +} +class PasswordHistory : PasswordResult { + $DateChanged + [String]$USERID + [String]$FirstName + [String]$Surname + [int32]$PasswordHistoryID + [String]$PasswordList + [String]GetPassword() { + $SecureString = $this.Password.Password + $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString) + return [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) + } + DecryptPassword(){ + $SecureString = $this.Password.Password + $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString) + $this.Password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) + } +} \ No newline at end of file diff --git a/functions/Remove-PasswordStatePassword.Tests.ps1 b/functions/Remove-PasswordStatePassword.Tests.ps1 index b57a643..f007af9 100644 --- a/functions/Remove-PasswordStatePassword.Tests.ps1 +++ b/functions/Remove-PasswordStatePassword.Tests.ps1 @@ -4,78 +4,25 @@ $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.' Import-Module "$here\..\passwordstate-management.psm1" Describe "Remove-PasswordStatePassword" { It "Removes a Password From Password State" { - Mock -CommandName Get-PasswordStateResource -MockWith {return [PSCustomObject]@{ - "Title" = "testuser" - "Username" = "test" - "Domain" = "" - "Description" = "" - "PasswordId" = 3 - "AccountType" = "" - "URL" = "" - "Passwordlist" = "MockedList" - "PasswordListID" = 7 - } - } -ParameterFilter {$uri -eq "/api/passwords/7?QueryAll&ExcludePassword=true"} - - Mock -CommandName Get-PasswordStateResource -MockWith {return [PSCustomObject]@{ - "Title" = "testuser" - "Username" = "test" - "Password" = "Password" - "Domain" = "" - "Description" = "" - "PasswordId" = 3 - "AccountType" = "" - "URL" = "" - "Passwordlist" = "MockedList" - "PasswordListID" = 7 - } - } -ParameterFilter {$uri -eq "/api/passwords/3"} - - Mock -CommandName Get-PasswordStateList -MockWith {return [PSCustomObject]@{ - "PasswordListID" = 7 - "PasswordList" = "MockedList" - "Description" = "" - "ImageFileName" = "" - "Guide" = "" - "AllowExport" = $true - "PrivatePasswordList" = $false - "TimeBasedAccessRequired" = $false - "HandshakeApprovalRequired" = $false - "PasswordStrengthPolicyID" = 1 - "PasswordGeneratorID" = 0 - "CodePage" = "Using Passwordstate Default Code Page" - "PreventPasswordReuse" = 5 - "AuthenticationType" = "None Required" - "AuthenticationPerSession" = $false - "PreventExpiryDateModification" = $false - "SetExpiryDate" = 0 - "ResetExpiryDate" = 0 - "PreventDragDrop" = $true - "PreventBadPasswordUse" = $true - "ProvideAccessReason" = $false - "TreePath" = "\\SomePath\\Somesubpath" - "TotalPasswords" = 2 - "GeneratorName" = "Using user\u0027s personal Password Generator Options" - "PolicyName" = "Default Policy" - "PasswordResetEnabled" = $false - "ForcePasswordGenerator" = $false - "HidePasswords" = $false - "ShowGuide" = $false - "EnablePasswordResetSchedule" = $false - "PasswordResetSchedule" = "00:00" - "AddDaysToExpiryDate" = 90 - "SiteID" = 0 - "SiteLocation" = $null - } + $ID = New-PasswordStatePassword -Title New -passwordlistID 1 -password "Pa`$`$word" + (Remove-PasswordStatePassword -PasswordID $ID.PasswordID ) | Should -BeNullOrEmpty + } + BeforeEach { + # Create Test Environment + try { + $globalsetting = Get-Variable PasswordStateShowPasswordsPlainText -ErrorAction stop -Verbose -ValueOnly + $global:PasswordStateShowPasswordsPlainText = $false } - Mock -CommandName Get-PasswordStateEnvironment -MockWith {return [PSCustomObject]@{ - "Baseuri" = "https://passwordstateserver.co.uk" - "APIKey" = "WindowsAuth" - - } + Catch { + New-Variable -Name PasswordStateShowPasswordsPlainText -Value $false -Scope Global } - Mock -CommandName Remove-PasswordStateResource -MockWith {return $null - } -ParameterFilter {$uri -eq "/api/passwords/3?MoveToRecycleBin=True"} - (Remove-PasswordStatePassword -PasswordID 3 -SendToRecycleBin) | Should -BeNullOrEmpty + Move-Item "$($env:USERPROFILE)\passwordstate.json" "$($env:USERPROFILE)\passwordstate.json.bak" -force + Set-PasswordStateEnvironment -Apikey "$env:pwsapikey" -Baseuri "$env:pwsuri" + } + + AfterEach { + # Remove Test Environment + Move-Item "$($env:USERPROFILE)\passwordstate.json.bak" "$($env:USERPROFILE)\passwordstate.json" -force + $global:PasswordStateShowPasswordsPlainText = $globalsetting } } diff --git a/functions/Remove-PasswordStatePassword.ps1 b/functions/Remove-PasswordStatePassword.ps1 index 31a4d49..5562112 100644 --- a/functions/Remove-PasswordStatePassword.ps1 +++ b/functions/Remove-PasswordStatePassword.ps1 @@ -35,15 +35,16 @@ function Remove-PasswordStatePassword { } process { - if ($reason) { + If ($Reason) { $headerreason = @{"Reason" = "$reason"} + $parms = @{ExtraParams = @{"Headers" = $headerreason}} } if ($PSCmdlet.ShouldProcess("PasswordID:$($PasswordID) Recycle:$Sendtorecyclebin")) { if ($SendToRecycleBin) { - $result = Remove-PasswordStateResource -uri "/api/passwords/$($PasswordID)?MoveToRecycleBin=$sendtorecyclebin" -extraparams @{"Headers" = $headerreason} + $result = Remove-PasswordStateResource -uri "/api/passwords/$($PasswordID)?MoveToRecycleBin=$sendtorecyclebin" @parms -method Delete } Else { - $result = Remove-PasswordStateResource -uri "/api/passwords/$($PasswordID)?MoveToRecycleBin=False" -extraparams @{"Headers" = $headerreason} + $result = Remove-PasswordStateResource -uri "/api/passwords/$($PasswordID)?MoveToRecycleBin=False" @parms -method Delete } } } diff --git a/functions/Remove-PasswordStateResource.ps1 b/functions/Remove-PasswordStateResource.ps1 index b04862b..6d58df2 100644 --- a/functions/Remove-PasswordStateResource.ps1 +++ b/functions/Remove-PasswordStateResource.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS A function to simplify the deletion of password state resources via the rest API .DESCRIPTION @@ -43,6 +43,7 @@ function Remove-PasswordStateResource { } APIKey { $headers = @{"APIKey" = "$($passwordstateenvironment.Apikey)"} + } } } @@ -54,11 +55,23 @@ function Remove-PasswordStateResource { "Method" = $method.ToUpper() "ContentType" = $ContentType } - if ($extraparams) { + if (!$body){ + $params.Remove("Body") + } + if ($headers -and $null -ne $extraparams.Headers) { + Write-Verbose "[$(Get-Date -format G)] Adding API Headers and extra param headers" + $headers += $extraparams.headers + $params += @{"headers" = $headers} + $skipheaders = $true + } + if ($extraparams -and $null -eq $extraparams.Headers){ + Write-Verbose "[$(Get-Date -format G)] Adding extra parameter $($extraparams.keys) $($extraparams.values)" $params += $extraparams } - if ($headers) { - $params += $headers + + if ($headers -and $skipheaders -ne $true) { + Write-Verbose "[$(Get-Date -format G)] Adding API Headers only" + $params += @{"headers" = $headers} } if ($PSCmdlet.ShouldProcess("[$($params.Method)] uri:$($params.uri) Headers:$($headers) Body:$($params.body)")) { Switch ($passwordstateenvironment.AuthType) { diff --git a/functions/Save-PasswordStateDocument.ps1 b/functions/Save-PasswordStateDocument.ps1 index dff20aa..69cd3ba 100644 --- a/functions/Save-PasswordStateDocument.ps1 +++ b/functions/Save-PasswordStateDocument.ps1 @@ -33,6 +33,7 @@ function Save-PasswordStateDocument { ) begin { + . "$PSScriptRoot\PasswordstateClass.ps1" $output = @() } diff --git a/functions/Set-PasswordStateEnvironment.ps1 b/functions/Set-PasswordStateEnvironment.ps1 index db7320c..f89fb5c 100644 --- a/functions/Set-PasswordStateEnvironment.ps1 +++ b/functions/Set-PasswordStateEnvironment.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS Saves your password state environment configuration to be used when calling the rest api. .DESCRIPTION @@ -29,10 +29,12 @@ #> function Set-PasswordStateEnvironment { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'all passwords stored encrypted')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlaintextForPassword', '', Justification = 'no password')] [CmdletBinding(DefaultParameterSetName = "Two", SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $true)][string]$Baseuri, [Parameter(ParameterSetName = 'One')][string]$Apikey, + [Parameter(ParameterSetName = 'One')][string]$PasswordGeneratorAPIkey, [Parameter(ParameterSetName = 'Two')][switch]$WindowsAuthOnly, [Parameter(ParameterSetName = 'Three')][pscredential]$customcredentials ) @@ -55,7 +57,7 @@ function Set-PasswordStateEnvironment { process { # Build the custom object to be converted to JSON. Set APIKey as WindowsAuth if we are to use windows authentication. - $json = New-Object psobject -Property @{ + $json = [pscustomobject] @{ "Baseuri" = $Baseuri "Apikey" = switch ($AuthType) { WindowsIntegrated { @@ -69,10 +71,15 @@ function Set-PasswordStateEnvironment { } APIKey { ($Apikey | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString) + } } "AuthType" = $AuthType - }| ConvertTo-Json + } + if ($PasswordGeneratorAPIkey){ + $json | Add-Member -MemberType NoteProperty -Name PasswordGeneratorAPIKey -Value ($PasswordGeneratorAPIkey | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString) + } + $json = $json | ConvertTo-Json } end { diff --git a/functions/Set-PasswordStateResource.ps1 b/functions/Set-PasswordStateResource.ps1 index 05f9ace..903700f 100644 --- a/functions/Set-PasswordStateResource.ps1 +++ b/functions/Set-PasswordStateResource.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS A function to simplify the modification/updates of password state resources via the rest API .DESCRIPTION @@ -56,11 +56,23 @@ function Set-PasswordStateResource { "ContentType" = $ContentType "Body" = $body } - if ($extraparams) { + if (!$body){ + $params.Remove("Body") + } + if ($headers -and $null -ne $extraparams.Headers) { + Write-Verbose "[$(Get-Date -format G)] Adding API Headers and extra param headers" + $headers += $extraparams.headers + $params += @{"headers" = $headers} + $skipheaders = $true + } + if ($extraparams -and $null -eq $extraparams.Headers){ + Write-Verbose "[$(Get-Date -format G)] Adding extra parameter $($extraparams.keys) $($extraparams.values)" $params += $extraparams } - if ($headers) { - $params += $headers + + if ($headers -and $skipheaders -ne $true) { + Write-Verbose "[$(Get-Date -format G)] Adding API Headers only" + $params += @{"headers" = $headers} } if ($PSCmdlet.ShouldProcess("[$($params.Method)] uri:$($params.uri) Headers:$($headers) Body:$($params.body)")) { Switch ($passwordstateenvironment.AuthType) { diff --git a/functions/Update-PasswordStatePassword.Tests.ps1 b/functions/Update-PasswordStatePassword.Tests.ps1 index 09f7652..d8a306d 100644 --- a/functions/Update-PasswordStatePassword.Tests.ps1 +++ b/functions/Update-PasswordStatePassword.Tests.ps1 @@ -3,54 +3,26 @@ $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.' . "$here\$sut" Import-Module "$here\..\passwordstate-management.psm1" Describe "Update-PasswordStatePassword" { - It "Finds a Password From Password State and passes it to be changed to Password.1" { - Mock -CommandName Get-PasswordStateResource -MockWith {return [PSCustomObject]@{ - "Title" = "testuser" - "Username" = "test" - "Domain" = "" - "Description" = "" - "PasswordId" = 3 - "AccountType" = "" - "URL" = "" - "Passwordlist" = "MockedList" - "PasswordListID" = 7 - } - } -ParameterFilter {$uri -eq "/api/passwords/7?QueryAll&ExcludePassword=true"} - - Mock -CommandName Find-PasswordStatePassword -MockWith {return [PSCustomObject]@{ - "Title" = "testuser" - "Username" = "test" - "Password" = "Password" - "Domain" = "" - "Description" = "" - "PasswordId" = 3 - "AccountType" = "" - "URL" = "" - "Passwordlist" = "MockedList" - "PasswordListID" = 7 - } - } -ParameterFilter {$passwordID -eq 3} - - Mock -CommandName Get-PasswordStateEnvironment -MockWith {return [PSCustomObject]@{ - "Baseuri" = "https://passwordstateserver.co.uk" - "APIKey" = "WindowsAuth" - - } + it "Updates an existing password" { + (Update-PasswordStatePassword -passwordID 1 -Password "Password.1").GetPassword() | Should -BeExactly "Password.1" + } + BeforeEach { + # Create Test Environment + try { + $globalsetting = Get-Variable PasswordStateShowPasswordsPlainText -ErrorAction stop -Verbose -ValueOnly + $global:PasswordStateShowPasswordsPlainText = $false } - Mock -CommandName Set-PasswordStateResource -MockWith {return [PSCustomObject]@{ - "Title" = "testuser" - "Password" = "Password.1" - "Username" = "test" - "Domain" = "" - "Description" = "" - "PasswordId" = 3 - "AccountType" = "" - "URL" = "" - "Passwordlist" = "MockedList" - "PasswordListID" = 7 - } - } -ParameterFilter {$uri -eq "/api/passwords" -and $body -notlike $null} - - (Update-PasswordStatePassword -passwordID 3 -Password "Password.1").Password | Should -BeExactly "Password.1" + Catch { + New-Variable -Name PasswordStateShowPasswordsPlainText -Value $false -Scope Global + } + Move-Item "$($env:USERPROFILE)\passwordstate.json" "$($env:USERPROFILE)\passwordstate.json.bak" -force + Set-PasswordStateEnvironment -Apikey "$env:pwsapikey" -Baseuri "$env:pwsuri" } + + AfterEach { + # Remove Test Environment + Move-Item "$($env:USERPROFILE)\passwordstate.json.bak" "$($env:USERPROFILE)\passwordstate.json" -force + $global:PasswordStateShowPasswordsPlainText = $globalsetting + } + } diff --git a/functions/Update-PasswordStatePassword.ps1 b/functions/Update-PasswordStatePassword.ps1 index 5deed8d..2be9d50 100644 --- a/functions/Update-PasswordStatePassword.ps1 +++ b/functions/Update-PasswordStatePassword.ps1 @@ -35,6 +35,7 @@ function Update-PasswordStatePassword { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '', Justification = 'API requires password be passed as plain text')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUserNameAndPassWordParams', '', Justification = 'API requires password be passed as plain text')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '', Justification = 'Needed for backward compatability')] [CmdletBinding(SupportsShouldProcess = $true)] param ( [parameter(Position = 0, ValueFromPipelineByPropertyName, Mandatory = $true)][int32]$passwordID, @@ -49,13 +50,15 @@ function Update-PasswordStatePassword { ) begin { - + . "$PSScriptRoot\PasswordstateClass.ps1" } process { - if ($reason) { + If ($Reason) { $headerreason = @{"Reason" = "$reason"} + $parms = @{ExtraParams = @{"Headers" = $headerreason}} } + Else {$parms = @{}} if ($passwordID) { try { $result = Find-PasswordStatePassword -PasswordID $passwordID -ErrorAction Stop @@ -74,7 +77,12 @@ function Update-PasswordStatePassword { # Replace Result property with that of the bound parameter $notprocess = "reason", "verbose", "erroraction", "debug", "whatif", "confirm" if ($notprocess -notcontains $i) { - $result.$($i) = $PSBoundParameters.$($i) + if ($i -eq "Password" -and $PSBoundParameters.$($i).Gettype().Name -eq "EncryptedPassword"){ + $result.$($i) = $result.GetPassword() + } + Else { + $result.$($i) = $PSBoundParameters.$($i) + } } } # Store in a new variable and remove all null values as password state doesn't like nulls. @@ -92,12 +100,20 @@ function Update-PasswordStatePassword { # Update body variable to contain only the properties with data. $body = $body | Select-Object $selections # Write back to password state. - $output = Set-PasswordStateResource -uri "/api/passwords" -body "$($body|convertto-json)" -extraparams @{"Headers" = $headerreason} + [PasswordResult]$output = Set-PasswordStateResource -uri "/api/passwords" -body "$($body|convertto-json)" @parms + foreach ($i in $output){ + $i.Password = [EncryptedPassword]$i.Password + } } } end { - return $output + switch ($global:PasswordStateShowPasswordsPlainText) { + True { + $output.DecryptPassword() + } + } + Return $output } } \ No newline at end of file diff --git a/readme.md b/readme.md index 8c917ed..4abd94d 100644 --- a/readme.md +++ b/readme.md @@ -13,6 +13,40 @@ Contains various functions for the management of passwordstate via powershell. | passwordstate | 8.0+ | | Powershell | 4.0+ | +## IMPORTANT NOTE + +As of version 0.94 Passwords are no longer output in plaintext by default and kept as secure strings instead. Passwords can be obtained by calling the .GetPassword() Method on the result which will decrypt the secure string. +eg. + +```powershell + Find-PasswordStatePassword +``` + +This will return: + + PasswordID : 1 + Title : test + Username : test + Password : EncryptedPassword + Description : + Domain : + +```powershell + (Find-PasswordStatePassword test).GetPassword() +``` + +This will return the actual password as a string. + + Password.1 + +To maintain backward compatability with scripts that have already been created you can force passwords to always output as plaintext for the duration of your powershell session by setting the following global variable `$global:PasswordStateShowPasswordsPlainText` to `$true`. + +If you would like to set it forever then add the following to your powershell profile. + +```powershell +$global:PasswordStateShowPasswordsPlainText = $true +``` + ## How to use First you will need to setup the environment for PasswordState. This prevents you having to enter the api key all the time as it's stored in an encrypted format. Or you can use Windows authentication using the currently logged on user. @@ -78,7 +112,7 @@ Create a new password entry. Create a new entry to a PasswordList if you know the name of the list. ```powershell - Get-PasswordStateLists | ? {$_.PasswordList -eq "passwordlistname"} | New-PasswordStatePassword -Title "testpassword" -username "newuser" -Password "CorrectHorseStapleBattery" -notes "development website" -url "http://somegoodwebsite.com" + Get-PasswordStateList | ? {$_.PasswordList -eq "passwordlistname"} | New-PasswordStatePassword -Title "testpassword" -username "newuser" -Password "CorrectHorseStapleBattery" -notes "development website" -url "http://somegoodwebsite.com" ``` #### Get All Password Lists @@ -88,7 +122,7 @@ Useful to return the `listID` for use in other commands such as creating a new e **NOTE: due to an api limitation when using APIKeys only the system key can return lists** This is not an issue using Windows Authentication. ```powershell - Get-PasswordStateLists + Get-PasswordStateList ``` #### Deleting a password entry @@ -107,6 +141,6 @@ Or find the password and pipe it acrosss to remove. ##### Additional Info -Functions all contain Pester tests with mocked data to ensure no changes are made to the environment but ensuring that the code works. +Functions all contain Pester tests ensuring that the code works. Full documentation under `.\docs` \ No newline at end of file