diff --git a/src/GitHub/private/Utilities/ConvertFrom-HashTable.ps1 b/src/GitHub/private/Utilities/ConvertFrom-HashTable.ps1 new file mode 100644 index 000000000..567e8a276 --- /dev/null +++ b/src/GitHub/private/Utilities/ConvertFrom-HashTable.ps1 @@ -0,0 +1,11 @@ +function ConvertFrom-HashTable { + [CmdletBinding()] + param ( + [Parameter( + Mandatory, + ValueFromPipeline + )] + [object]$InputObject + ) + ([pscustomobject](@{} + $InputObject)) +} diff --git a/src/GitHub/private/Utilities/ConvertTo-HashTable.ps1 b/src/GitHub/private/Utilities/ConvertTo-HashTable.ps1 new file mode 100644 index 000000000..c750d5f45 --- /dev/null +++ b/src/GitHub/private/Utilities/ConvertTo-HashTable.ps1 @@ -0,0 +1,17 @@ +function ConvertTo-HashTable { + [CmdletBinding()] + param ( + [Parameter( + Mandatory, + ValueFromPipeline + )] + [pscustomobject]$InputObject + ) + [hashtable]$hashtable = @{} + + foreach ($item in $InputObject.PSobject.Properties) { + Write-Verbose "$($item.Name) : $($item.Value) : $($item.TypeNameOfValue)" + $hashtable.$($item.Name) = $item.Value + } + $hashtable +} diff --git a/src/GitHub/private/Utilities/Join-Hashtable.ps1 b/src/GitHub/private/Utilities/Join-Hashtable.ps1 new file mode 100644 index 000000000..b53bedb49 --- /dev/null +++ b/src/GitHub/private/Utilities/Join-Hashtable.ps1 @@ -0,0 +1,17 @@ +function Join-Hashtable { + [OutputType([void])] + [Alias('Merge-HashTable')] + [CmdletBinding()] + param ( + [hashtable] $Main, + [hashtable] $Overrides + ) + $hashtable = @{} + $Main.Keys | ForEach-Object { + $hashtable[$_] = $Main[$_] + } + $Overrides.Keys | ForEach-Object { + $hashtable[$_] = $Overrides[$_] + } + $hashtable +} diff --git a/src/GitHub/private/Utilities/Remove-HashTableEntries.ps1 b/src/GitHub/private/Utilities/Remove-HashTableEntries.ps1 new file mode 100644 index 000000000..3d7a8c411 --- /dev/null +++ b/src/GitHub/private/Utilities/Remove-HashTableEntries.ps1 @@ -0,0 +1,57 @@ +function Remove-HashtableEntries { + [OutputType([void])] + [CmdletBinding()] + param ( + [Parameter( + Mandatory, + ValueFromPipeline + )] + [hashtable] $Hashtable, + [Parameter()] + [switch] $NullOrEmptyValues, + [Parameter()] + [string[]] $RemoveTypes, + [Parameter()] + [string[]] $RemoveNames, + [Parameter()] + [string[]] $KeepTypes, + [Parameter()] + [string[]] $KeepNames + + ) + if ($NullOrEmptyValues) { + Write-Verbose 'Remove keys with null or empty values' + ($Hashtable.GetEnumerator() | Where-Object { -not $_.Value }) | ForEach-Object { + Write-Verbose " - [$($_.Name)] - Value: [$($_.Value)] - Remove" + $Hashtable.Remove($_.Name) + } + } + if ($RemoveTypes) { + Write-Verbose "Remove keys of type: [$RemoveTypes]" + ($Hashtable.GetEnumerator() | Where-Object { ($_.Value.GetType().Name -in $RemoveTypes) }) | ForEach-Object { + Write-Verbose " - [$($_.Name)] - Type: [$($_.Value.GetType().Name)] - Remove" + $Hashtable.Remove($_.Name) + } + } + if ($KeepTypes) { + Write-Verbose "Remove keys NOT of type: [$KeepTypes]" + ($Hashtable.GetEnumerator() | Where-Object { ($_.Value.GetType().Name -notin $KeepTypes) }) | ForEach-Object { + Write-Verbose " - [$($_.Name)] - Type: [$($_.Value.GetType().Name)] - Remove" + $Hashtable.Remove($_.Name) + } + } + if ($RemoveNames) { + Write-Verbose "Remove keys named: [$RemoveNames]" + ($Hashtable.GetEnumerator() | Where-Object { $_.Name -in $RemoveNames }) | ForEach-Object { + Write-Verbose " - [$($_.Name)] - Remove" + $Hashtable.Remove($_.Name) + } + } + if ($KeepNames) { + Write-Verbose "Remove keys NOT named: [$KeepNames]" + ($Hashtable.GetEnumerator() | Where-Object { $_.Name -notin $KeepNames }) | ForEach-Object { + Write-Verbose " - [$($_.Name)] - Remove" + $Hashtable.Remove($_.Name) + } + } +} diff --git a/src/GitHub/public/API/Invoke-GitHubAPI.ps1 b/src/GitHub/public/API/Invoke-GitHubAPI.ps1 index 4dbc66c65..e3ec7793d 100644 --- a/src/GitHub/public/API/Invoke-GitHubAPI.ps1 +++ b/src/GitHub/public/API/Invoke-GitHubAPI.ps1 @@ -30,7 +30,7 @@ # The base URI for the GitHub API. This is usually 'https://api.github.com', but can be adjusted if necessary. [Parameter()] - [string] $ApiBaseUri = (Get-GitHubConfig -Name ApiBaseUri -AsPlainText), + [string] $ApiBaseUri = (Get-GitHubConfig -Name ApiBaseUri), # The specific endpoint for the API call, e.g., '/repos/user/repo/pulls'. [Parameter(Mandatory)] @@ -62,7 +62,7 @@ # The GitHub API version to be used. By default, it pulls from a configuration script variable. [Parameter()] - [string] $Version = (Get-GitHubConfig -Name ApiVersion -AsPlainText) + [string] $Version = (Get-GitHubConfig -Name ApiVersion) ) $functionName = $MyInvocation.MyCommand.Name @@ -72,7 +72,7 @@ 'X-GitHub-Api-Version' = $Version } - ($headers.GetEnumerator() | Where-Object { -not $_.Value }) | ForEach-Object { $headers.Remove($_.Name) } + Remove-HashTableEntries -Hashtable $headers -NullOrEmptyValues $URI = ("$ApiBaseUri/" -replace '/$', '') + ("/$ApiEndpoint" -replace '^/', '') @@ -88,6 +88,7 @@ StatusCodeVariable = 'StatusCode' ResponseHeadersVariable = 'ResponseHeaders' } + Remove-HashTableEntries -Hashtable $APICall -NullOrEmptyValues if ($Body) { if ($Body -is [string]) { diff --git a/src/GitHub/public/Auth/Connect-GitHubAccount.ps1 b/src/GitHub/public/Auth/Connect-GitHubAccount.ps1 index da99ded35..a91cce1e3 100644 --- a/src/GitHub/public/Auth/Connect-GitHubAccount.ps1 +++ b/src/GitHub/public/Auth/Connect-GitHubAccount.ps1 @@ -73,11 +73,11 @@ 'DeviceFlow' { Write-Verbose 'Logging in using device flow...' $clientID = $script:Auth.$Mode.ClientID - if ($Mode -ne (Get-GitHubConfig -Name DeviceFlowType -AsPlainText -ea SilentlyContinue)) { + if ($Mode -ne (Get-GitHubConfig -Name DeviceFlowType -ErrorAction SilentlyContinue)) { Write-Verbose "Using $Mode authentication..." $tokenResponse = Invoke-GitHubDeviceFlowLogin -ClientID $clientID -Scope $Scope } else { - $accessTokenValidity = [datetime](Get-GitHubConfig -Name 'AccessTokenExpirationDate' -AsPlainText) - (Get-Date) + $accessTokenValidity = [datetime](Get-GitHubConfig -Name 'AccessTokenExpirationDate') - (Get-Date) $accessTokenIsValid = $accessTokenValidity.Seconds -gt 0 $accessTokenValidityText = "$($accessTokenValidity.Hours):$($accessTokenValidity.Minutes):$($accessTokenValidity.Seconds)" if ($accessTokenIsValid) { @@ -91,7 +91,7 @@ $tokenResponse = Invoke-GitHubDeviceFlowLogin -ClientID $clientID -RefreshToken (Get-GitHubConfig -Name RefreshToken) } } else { - $refreshTokenValidity = [datetime](Get-GitHubConfig -Name 'RefreshTokenExpirationDate' -AsPlainText) - (Get-Date) + $refreshTokenValidity = [datetime](Get-GitHubConfig -Name 'RefreshTokenExpirationDate') - (Get-Date) $refreshTokenIsValid = $refreshTokenValidity.Seconds -gt 0 if ($refreshTokenIsValid) { Write-Host '⚠ ' -ForegroundColor Yellow -NoNewline @@ -140,14 +140,14 @@ Write-Host '! ' -ForegroundColor DarkYellow -NoNewline Start-Process 'https://github.com/settings/tokens' $accessTokenValue = Read-Host -Prompt 'Enter your personal access token' -AsSecureString - $prefix = (ConvertFrom-SecureString $accessTokenValue -AsPlainText) -replace '_.*$', '_*' - if ($prefix -notmatch '^ghp_|^github_pat_') { + $accessTokenType = (ConvertFrom-SecureString $accessTokenValue -AsPlainText) -replace '_.*$', '_*' + if ($accessTokenType -notmatch '^ghp_|^github_pat_') { Write-Host '⚠ ' -ForegroundColor Yellow -NoNewline - Write-Host "Unexpected access token format: $prefix" + Write-Host "Unexpected access token format: $accessTokenType" } $settings = @{ AccessToken = $accessTokenValue - AccessTokenType = $prefix + AccessTokenType = $accessTokenType ApiBaseUri = 'https://api.github.com' ApiVersion = '2022-11-28' AuthType = $AuthType diff --git a/src/GitHub/public/Config/Get-GitHubConfig.ps1 b/src/GitHub/public/Config/Get-GitHubConfig.ps1 index 8de2774f3..d872ed894 100644 --- a/src/GitHub/public/Config/Get-GitHubConfig.ps1 +++ b/src/GitHub/public/Config/Get-GitHubConfig.ps1 @@ -1,33 +1,42 @@ function Get-GitHubConfig { <# .SYNOPSIS - Get the current GitHub configuration. + Get configuration value. .DESCRIPTION - Get the current GitHub configuration. - The configuration is first loaded from the configuration file. + Get a named configuration value from the GitHub configuration file. .EXAMPLE - Get-GitHubConfig - - Returns the current GitHub configuration. + Get-GitHubConfig -Name ApiBaseUri + Get the current GitHub configuration for the ApiBaseUri. #> [Alias('Get-GHConfig')] [Alias('GGHC')] [OutputType([object])] [CmdletBinding()] param ( - [string] $Name, - [switch] $AsPlainText + # Choose a configuration name to get. + [Parameter()] + [string] $Name ) + $prefix = $script:SecretVault.Prefix - if ($Name) { - $Name = "$prefix$Name" - Get-Secret -Name $Name -Vault $script:SecretVault.Name -AsPlainText:$AsPlainText - } else { - Get-SecretInfo | Where-Object Name -like "$prefix*" | ForEach-Object { - Get-Secret -Name $_.Name -Vault $script:SecretVault.Name -AsPlainText:$AsPlainText + + switch($Name) { + 'AccessToken' { + Get-Secret -Name "$prefix`AccessToken" + } + 'RefreshToken' { + Get-Secret -Name "$prefix`RefreshToken" + } + 'RefreshTokenExpirationDate' { + $RefreshTokenData = Get-SecretInfo -Name "$prefix`RefreshToken" + $RefreshTokenData.Metadata | ConvertFrom-HashTable | ConvertTo-HashTable | Select-Object -ExpandProperty $Name + } + default { + $AccessTokenData = Get-SecretInfo -Name "$prefix`AccessToken" + $AccessTokenData.Metadata | ConvertFrom-HashTable | ConvertTo-HashTable | Select-Object -ExpandProperty $Name } } } diff --git a/src/GitHub/public/Config/Set-GitHubConfig.ps1 b/src/GitHub/public/Config/Set-GitHubConfig.ps1 index dd128166a..e4432fa35 100644 --- a/src/GitHub/public/Config/Set-GitHubConfig.ps1 +++ b/src/GitHub/public/Config/Set-GitHubConfig.ps1 @@ -21,11 +21,11 @@ param ( # Set the access token type. [Parameter()] - [string] $AccessTokenType = '', + [string] $AccessTokenType, # Set the access token. [Parameter()] - [securestring] $AccessToken = '', + [securestring] $AccessToken, # Set the access token expiration date. [Parameter()] @@ -69,65 +69,103 @@ # Set the GitHub username. [Parameter()] - [string] $UserName, - - # Choose a custom name to set. - [Parameter()] - [string] $Name, - - # Choose a custom value to set. - [Parameter()] - [string] $Value = '' + [string] $UserName ) $prefix = $script:SecretVault.Prefix - #All timestamps return in UTC time, ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ - #Also: use Set-Secret -NAme ... -Value ... -Metadata @{Type = 'DateTime'} to set a datetime value - # https://learn.microsoft.com/en-us/powershell/utility-modules/secretmanagement/how-to/manage-secretstore?view=ps-modules#adding-metadata - - switch ($PSBoundParameters.Keys) { - 'AccessToken' { - Set-Secret -Name "$prefix`AccessToken" -SecureStringSecret $AccessToken -Vault $script:SecretVault.Name - } - 'AccessTokenExpirationDate' { - Set-Secret -Name "$prefix`AccessTokenExpirationDate" -Secret $AccessTokenExpirationDate.ToString() -Vault $script:SecretVault.Name - } - 'AccessTokenType' { - Set-Secret -Name "$prefix`AccessTokenType" -Secret $AccessTokenType -Vault $script:SecretVault.Name - } - 'ApiBaseUri' { - Set-Secret -Name "$prefix`ApiBaseUri" -Secret $ApiBaseUri -Vault $script:SecretVault.Name - } - 'ApiVersion' { - Set-Secret -Name "$prefix`ApiVersion" -Secret $ApiVersion -Vault $script:SecretVault.Name - } - 'AuthType' { - Set-Secret -Name "$prefix`AuthType" -Secret $AuthType -Vault $script:SecretVault.Name - } - 'DeviceFlowType' { - Set-Secret -Name "$prefix`DeviceFlowType" -Secret $DeviceFlowType -Vault $script:SecretVault.Name + #region AccessToken + $secretName = "$prefix`AccessToken" + $removeKeys = 'AccessToken', 'RefreshToken', 'RefreshTokenExpirationDate' + $keepTypes = 'String', 'Int', 'DateTime' + + # Get existing metadata if it exists + $newSecretMetadata = @{} + if (Get-SecretInfo -Name $secretName) { + $secretGetInfoParam = @{ + Name = $secretName + Vault = $script:SecretVault.Name } - 'Owner' { - Set-Secret -Name "$prefix`Owner" -Secret $Owner -Vault $script:SecretVault.Name - } - 'RefreshToken' { - Set-Secret -Name "$prefix`RefreshToken" -SecureStringSecret $RefreshToken -Vault $script:SecretVault.Name - } - 'RefreshTokenExpirationDate' { - Set-Secret -Name "$prefix`RefreshTokenExpirationDate" -Secret $RefreshTokenExpirationDate.ToString() -Vault $script:SecretVault.Name + $secretInfo = Get-SecretInfo @secretGetInfoParam + Write-Verbose "$secretName - secretInfo : $($secretInfo | Out-String)" + $secretMetadata = $secretInfo.Metadata | ConvertFrom-HashTable | ConvertTo-HashTable + $newSecretMetadata = Join-Hashtable -Main $newSecretMetadata -Overrides $secretMetadata + } + + # Get metadata updates from parameters and clean up unwanted data + $updateSecretMetadata = $PSBoundParameters | ConvertFrom-HashTable | ConvertTo-HashTable + Write-Verbose "updateSecretMetadata : $($updateSecretMetadata | Out-String)" + Write-Verbose "updateSecretMetadataType : $($updateSecretMetadata.GetType())" + Remove-HashTableEntries -Hashtable $updateSecretMetadata -KeepTypes $keepTypes -RemoveNames $removeKeys + Write-Verbose "updateSecretMetadata : $($updateSecretMetadata | Out-String)" + + $newSecretMetadata = Join-HashTable -Main $newSecretMetadata -Overrides $updateSecretMetadata + Write-Verbose "newSecretMetadata : $($newSecretMetadata | Out-String)" + Write-Verbose "newSecretMetadataType : $($newSecretMetadata.GetType())" + + if ($AccessToken) { + $accessTokenSetParam = @{ + Name = $secretName + Vault = $script:SecretVault.Name + SecureStringSecret = $AccessToken } - 'Repo' { - Set-Secret -Name "$prefix`Repo" -Secret $Repo -Vault $script:SecretVault.Name + Set-Secret @accessTokenSetParam + } + + if (Get-SecretInfo -Name $secretName) { + $secretSetInfoParam = @{ + Name = $secretName + Vault = $script:SecretVault.Name + Metadata = $newSecretMetadata } - 'Scope' { - Set-Secret -Name "$prefix`Scope" -Secret $Scope -Vault $script:SecretVault.Name + Set-SecretInfo @secretSetInfoParam + } + #endregion AccessToken + + #region RefreshToken + $secretName = "$prefix`RefreshToken" + $removeKeys = 'AccessToken', 'RefreshToken', 'AccessTokenExpirationDate' + + # Get existing metadata if it exists + $newSecretMetadata = @{} + if (Get-SecretInfo -Name $secretName) { + $secretGetInfoParam = @{ + Name = $secretName + Vault = $script:SecretVault.Name } - 'UserName' { - Set-Secret -Name "$prefix`UserName" -Secret $UserName -Vault $script:SecretVault.Name + $secretInfo = Get-SecretInfo @secretGetInfoParam + Write-Verbose "$secretName - secretInfo : $($secretInfo | Out-String)" + $secretMetadata = $secretInfo.Metadata | ConvertFrom-HashTable | ConvertTo-HashTable + $newSecretMetadata = Join-Hashtable -Main $newSecretMetadata -Overrides $secretMetadata + } + + # Get metadata updates from parameters and clean up unwanted data + $updateSecretMetadata = $PSBoundParameters | ConvertFrom-HashTable | ConvertTo-HashTable + Write-Verbose "updateSecretMetadata : $($updateSecretMetadata | Out-String)" + Write-Verbose "updateSecretMetadataType : $($updateSecretMetadata.GetType())" + Remove-HashTableEntries -Hashtable $updateSecretMetadata -KeepTypes $keepTypes -RemoveNames $removeKeys + Write-Verbose "updateSecretMetadata : $($updateSecretMetadata | Out-String)" + + $newSecretMetadata = Join-HashTable -Main $newSecretMetadata -Overrides $updateSecretMetadata + Write-Verbose "newSecretMetadata : $($newSecretMetadata | Out-String)" + Write-Verbose "newSecretMetadataType : $($newSecretMetadata.GetType())" + + if ($RefreshToken) { + $refreshTokenSetParam = @{ + Name = $secretName + Vault = $script:SecretVault.Name + SecureStringSecret = $RefreshToken } - 'Name' { - Set-Secret -Name "$prefix$Name" -Secret $Value -Vault $script:SecretVault.Name + Set-Secret @refreshTokenSetParam + } + + if (Get-SecretInfo -Name $secretName) { + $secretSetInfoParam = @{ + Name = $secretName + Vault = $script:SecretVault.Name + Metadata = $newSecretMetadata } + Set-SecretInfo @secretSetInfoParam -Verbose } + #endregion AccessToken } diff --git a/tools/utilities/Local-Testing.ps1 b/tools/utilities/Local-Testing.ps1 index ecbe3441c..c99643b25 100644 --- a/tools/utilities/Local-Testing.ps1 +++ b/tools/utilities/Local-Testing.ps1 @@ -1,6 +1,6 @@ ##### -Get-Module -Name GitHub -ListAvailable | Remove-Module -Force -Get-Module -Name GitHub -ListAvailable | Uninstall-Module -Force -AllVersions +Get-Module -Name GitHub* -ListAvailable | Remove-Module -Force +Get-Module -Name GitHub* -ListAvailable | Uninstall-Module -Force -AllVersions Get-SecretVault | Unregister-SecretVault Get-SecretVault @@ -9,8 +9,8 @@ Get-Module -Name GitHub -ListAvailable $VerbosePreference = 'Continue' Install-Module -Name GitHub -Verbose -Force -AllowPrerelease -$env:PSModulePath += ';C:\Repos\GitHub\PSModule\Modules\GitHub\outputs' -Import-Module -Name 'C:\Repos\GitHub\PSModule\Modules\GitHub\src\GitHub\GitHub.psm1' -Verbose -Force +# $env:PSModulePath += ';C:\Repos\GitHub\PSModule\Modules\GitHub\outputs' +# Import-Module -Name 'C:\Repos\GitHub\PSModule\Modules\GitHub\src\GitHub\GitHub.psm1' -Verbose -Force Import-Module -Name GitHub -Verbose Clear-Host @@ -19,11 +19,11 @@ Get-Variable | Where-Object -Property Module -NE $null | Select-Object Name, Mod Connect-GitHubAccount Connect-GitHubAccount -Mode OAuthApp Connect-GitHubAccount -AccessToken -Get-GitHubConfig -Name AccessToken -AsPlainText -Get-GitHubConfig -Name AccessTokenExpirationDate -AsPlainText -Get-GitHubConfig -Name RefreshToken -AsPlainText -Get-GitHubConfig -Name RefreshTokenExpirationDate -AsPlainText -Get-GitHubConfig -Name ApiBaseUri -AsPlainText +Get-GitHubConfig -Name AccessToken +Get-GitHubConfig -Name AccessTokenExpirationDate +Get-GitHubConfig -Name RefreshToken +Get-GitHubConfig -Name RefreshTokenExpirationDate +Get-GitHubConfig -Name ApiBaseUri Invoke-GitHubAPI -Method Get -ApiEndpoint / Get-GitHubMeta Get-GitHubOctocat -S 'Hello, World!'