diff --git a/.gitignore b/.gitignore index c017079e..44df8178 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ FunctionBackups/* BuildOutput/* *config.csv PSGSuite.zip +PSGSuite*.json diff --git a/CHANGELOG.md b/CHANGELOG.md index f7884e99..bbc4dfae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog * [Changelog](#changelog) + * [2.22.0](#2220) * [2.21.3](#2213) * [2.21.2](#2212) * [2.21.1](#2211) @@ -68,6 +69,15 @@ *** +## 2.22.0 + +* Miscellaneous: _Config management and portability updates_ + * Added: `Export-PSGSuiteConfig` function to export key parts of your config in a transportable JSON file. + * Added: `Import-PSGSuiteConfig` function to import a config from a JSON file (i.e. one created with `Export-PSGSuiteConfig`) or from a JSON string (i.e. stored in a secure variable in a CI/CD system.) + * Updated: All config functions now store the P12Key or the ClientSecrets JSON string in the encrypted config directly. This is to allow removal of the secrets files as well as enable PSGSuite to run in a contained environment via importing the config from a secure JSON string. + * Updated: `[Get|Set|Switch]-PSGSuiteConfig` to include the P12Key and ClientSecrets parameters that enable housing of the key/secret directly on the encrypted config. + * Updated: If the global PSGSuite variable `$global:PSGSuite` exists during module import, it will default to using that as it's configuration, otherwise it will import the default config if set. + ## 2.21.3 * [Issue #131](https://github.com/scrthq/PSGSuite/issues/131) diff --git a/PSGSuite/PSGSuite.psd1 b/PSGSuite/PSGSuite.psd1 index 6d2b3ff0..2dc1438b 100644 --- a/PSGSuite/PSGSuite.psd1 +++ b/PSGSuite/PSGSuite.psd1 @@ -12,7 +12,7 @@ RootModule = 'PSGSuite.psm1' # Version number of this module. - ModuleVersion = '2.21.3' + ModuleVersion = '2.22.0' # ID used to uniquely identify this module GUID = '9d751152-e83e-40bb-a6db-4c329092aaec' diff --git a/PSGSuite/Public/Authentication/New-GoogleService.ps1 b/PSGSuite/Public/Authentication/New-GoogleService.ps1 index 305076d1..9b4b9871 100644 --- a/PSGSuite/Public/Authentication/New-GoogleService.ps1 +++ b/PSGSuite/Public/Authentication/New-GoogleService.ps1 @@ -14,10 +14,14 @@ function New-GoogleService { $User = $script:PSGSuite.AdminEmail ) Process { - if ($script:PSGSuite.P12KeyPath) { + if ($script:PSGSuite.P12KeyPath -or $script:PSGSuite.P12Key) { try { Write-Verbose "Building ServiceAccountCredential from P12Key as user '$User'" - $certificate = New-Object 'System.Security.Cryptography.X509Certificates.X509Certificate2' -ArgumentList (Resolve-Path $script:PSGSuite.P12KeyPath),"notasecret",([System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable) + if (-not $script:PSGSuite.P12Key) { + $script:PSGSuite.P12Key = ([System.IO.File]::ReadAllBytes($script:PSGSuite.P12KeyPath)) + Set-PSGSuiteConfig -ConfigName $script:PSGSuite.ConfigName -P12Key $script:PSGSuite.P12Key -Verbose:$false + } + $certificate = New-Object 'System.Security.Cryptography.X509Certificates.X509Certificate2' -ArgumentList ([System.Byte[]]$script:PSGSuite.P12Key),"notasecret",([System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable) $credential = New-Object 'Google.Apis.Auth.OAuth2.ServiceAccountCredential' (New-Object 'Google.Apis.Auth.OAuth2.ServiceAccountCredential+Initializer' $script:PSGSuite.AppEmail -Property @{ User = $User Scopes = [string[]]$Scope @@ -41,8 +45,8 @@ function New-GoogleService { 'https://www.googleapis.com/auth/tasks.readonly' ) if (-not $script:PSGSuite.ClientSecrets) { - $script:PSGSuite.ClientSecrets = (Get-Content $script:PSGSuite.ClientSecretsPath -Raw) - Set-PSGSuiteConfig -ConfigName $script:PSGSuite.ConfigName -ClientSecretsPath $script:PSGSuite.ClientSecretsPath -Verbose:$false + $script:PSGSuite.ClientSecrets = ([System.IO.File]::ReadAllText($script:PSGSuite.ClientSecretsPath)) + Set-PSGSuiteConfig -ConfigName $script:PSGSuite.ConfigName -ClientSecrets $script:PSGSuite.ClientSecrets -Verbose:$false } $credPath = Join-Path (Resolve-Path (Join-Path "~" ".scrthq")) "PSGSuite" Write-Verbose "Building UserCredentials from ClientSecrets as user '$User' and prompting for authorization if necessary." diff --git a/PSGSuite/Public/Configuration/Export-PSGSuiteConfig.ps1 b/PSGSuite/Public/Configuration/Export-PSGSuiteConfig.ps1 new file mode 100644 index 00000000..a1fe48ef --- /dev/null +++ b/PSGSuite/Public/Configuration/Export-PSGSuiteConfig.ps1 @@ -0,0 +1,52 @@ +function Export-PSGSuiteConfig { + <# + .SYNOPSIS + Allows you to export an unecrypted PSGSuite config in a portable JSON string format. Useful for moving a config to a new machine or storing the full as an encrypted string in your CI/CD / Automation tools. + + .DESCRIPTION + Allows you to export an unecrypted PSGSuite config in a portable JSON string format. Useful for moving a config to a new machine or storing the full as an encrypted string in your CI/CD / Automation tools. + + .PARAMETER Path + The path you would like to save the JSON file to. Defaults to a named path in the current directory. + + .PARAMETER ConfigName + The config that you would like to export. Defaults to the currently loaded config. + + .EXAMPLE + Export-PSGSuiteConfig -ConfigName Personal -Path ".\PSGSuite_personal_config.json" + + Exports the config named 'Personal' to the path specified. + #> + [CmdletBinding()] + Param ( + [parameter(Mandatory = $false,Position = 0)] + [Alias('OutPath','OutFile','JsonPath')] + [String] + $Path, + [parameter(Mandatory = $false)] + [String] + $ConfigName = $script:PSGSuite.ConfigName + ) + Begin { + $baseConf = if ($PSBoundParameters.Keys -contains 'ConfigName'){ + Get-PSGSuiteConfig -ConfigName $ConfigName -NoImport -PassThru -Verbose:$false + } + else { + Show-PSGSuiteConfig -Verbose:$false + } + if ($PSBoundParameters.Keys -notcontains 'Path') { + $Path = (Join-Path $PWD.Path "PSGSuite_$($baseConf.AdminEmail)_$($baseConf.ConfigName).json") + } + } + Process { + try { + Write-Verbose "Updating current saved config for '$ConfigName' with P12Key and ClientSecrets contents if missing." + $baseConf | Set-PSGSuiteConfig -NoImport -Verbose:$false + Write-Verbose "Exporting config '$ConfigName' to path: $Path" + Get-PSGSuiteConfig -ConfigName $ConfigName -NoImport -PassThru -Verbose:$false | Select-Object ConfigName,P12Key,ClientSecrets,AppEmail,AdminEmail,CustomerId,Domain,Preference | ConvertTo-Json -Depth 5 -Compress -Verbose:$false | Set-Content -Path $Path -Verbose:$false + } + catch { + $PSCmdlet.ThrowTerminatingError($_) + } + } +} diff --git a/PSGSuite/Public/Configuration/Get-PSGSuiteConfig.ps1 b/PSGSuite/Public/Configuration/Get-PSGSuiteConfig.ps1 index 4d740419..e71116af 100644 --- a/PSGSuite/Public/Configuration/Get-PSGSuiteConfig.ps1 +++ b/PSGSuite/Public/Configuration/Get-PSGSuiteConfig.ps1 @@ -87,6 +87,7 @@ function Get-PSGSuiteConfig { $decryptedConfig = $encConf | Select-Object -Property @{l = 'ConfigName';e = {$choice}}, @{l = 'P12KeyPath';e = {Decrypt $_.P12KeyPath}}, + P12Key, @{l = 'ClientSecretsPath';e = {Decrypt $_.ClientSecretsPath}}, @{l = 'ClientSecrets';e = {Decrypt $_.ClientSecrets}}, @{l = 'AppEmail';e = {Decrypt $_.AppEmail}}, diff --git a/PSGSuite/Public/Configuration/Import-PSGSuiteConfig.ps1 b/PSGSuite/Public/Configuration/Import-PSGSuiteConfig.ps1 new file mode 100644 index 00000000..a2ef50db --- /dev/null +++ b/PSGSuite/Public/Configuration/Import-PSGSuiteConfig.ps1 @@ -0,0 +1,69 @@ +function Import-PSGSuiteConfig { + <# + .SYNOPSIS + Allows you to import an unecrypted PSGSuite config from a portable JSON string format, typically created with Export-PSGSuiteConfig. Useful for moving a config to a new machine or storing the full as an encrypted string in your CI/CD / Automation tools. + + .DESCRIPTION + Allows you to import an unecrypted PSGSuite config from a portable JSON string format, typically created with Export-PSGSuiteConfig. Useful for moving a config to a new machine or storing the full as an encrypted string in your CI/CD / Automation tools. + + .PARAMETER Json + The Json string to import. + + .PARAMETER Path + The path of the Json file you would like import. + + .PARAMETER Temporary + If $true, the imported config is not stored in the config file and the imported config persists only for the current session. + + .PARAMETER PassThru + If $true, outputs the resulting config object to the pipeline. + + .EXAMPLE + Import-Module PSGSuite -MinimumVersion 2.22.0 + Import-PSGSuiteConfig -Json '$(PSGSuiteConfigJson)' -Temporary + + Azure Pipelines inline script task that uses a Secure Variable named 'PSGSuiteConfigJson' with the Config JSON string stored in it, removing the need to include credential or key files anywhere. + #> + [CmdletBinding(DefaultParameterSetName = "Json")] + Param ( + [parameter(Mandatory = $true,Position = 0,ValueFromPipeline = $true,ParameterSetName = "Json")] + [Alias('J')] + [String] + $Json, + [parameter(Mandatory = $true,ValueFromPipelineByPropertyName = $true,ParameterSetName = "Path")] + [Alias('P')] + [String] + $Path, + [parameter(Mandatory = $false)] + [Alias('Temp','T')] + [Switch] + $Temporary, + [parameter(Mandatory = $false)] + [Switch] + $PassThru + ) + Process { + try { + switch ($PSCmdlet.ParameterSetName) { + Path { + Write-Verbose "Importing config from path: $Path" + $script:PSGSuite = (ConvertFrom-Json (Get-Content $Path -Raw)) + } + Json { + Write-Verbose "Importing config from Json string" + $script:PSGSuite = (ConvertFrom-Json $Json) + } + } + if (-not $Temporary) { + Write-Verbose "Saving imported config" + $script:PSGSuite | Set-PSGSuiteConfig + } + if ($PassThru) { + return $script:PSGSuite + } + } + catch { + $PSCmdlet.ThrowTerminatingError($_) + } + } +} diff --git a/PSGSuite/Public/Configuration/Set-PSGSuiteConfig.ps1 b/PSGSuite/Public/Configuration/Set-PSGSuiteConfig.ps1 index 6029c648..357d1446 100644 --- a/PSGSuite/Public/Configuration/Set-PSGSuiteConfig.ps1 +++ b/PSGSuite/Public/Configuration/Set-PSGSuiteConfig.ps1 @@ -10,10 +10,13 @@ function Set-PSGSuiteConfig { The friendly name for the config you are creating or updating .PARAMETER P12KeyPath - The path to the P12 Key file downloaded from the Google Developer's Console. If both P12KeyPath and ClientSecretsPath are specified, P12KeyPath takes precedence + The path to the P12 Key file downloaded from the Google Developer's Console. If both P12KeyPath and ClientSecretsPath are specified, P12KeyPath takes precedence. + + .PARAMETER P12Key + The P12Key in byte array format. If the actual P12Key is present on the config, the P12KeyPath is not needed. The config will auto-update with this value after running any command, if P12KeyPath is filled and this value is not already present. .PARAMETER ClientSecretsPath - The path to the Client Secrets JSON file downloaded from the Google Developer's Console. Using the ClientSecrets JSON will prompt the user to complete OAuth2 authentication in their browser on the first run and store the retrieved Refresh and Access tokens in the user's home directory. If P12KeyPath is also specified, ClientSecretsPath will be ignored. + The path to the Client Secrets JSON file downloaded from the Google Developer's Console. Using the ClientSecrets JSON will prompt the user to complete OAuth2 authentication in their browser on the first run and store the retrieved Refresh and Access tokens in the user's home directory. The config will auto-update with this value after running any command, if ClientSecretsPath is filled and this value is not already present. If P12KeyPath is also specified, ClientSecretsPath will be ignored. .PARAMETER ClientSecrets The string contents of the Client Secrets JSON file downloaded from the Google Developer's Console. Using the ClientSecrets JSON will prompt the user to complete OAuth2 authentication in their browser on the first run and store the retrieved Refresh and Access tokens in the user's home directory. If P12KeyPath is also specified, ClientSecrets will be ignored. @@ -86,6 +89,9 @@ function Set-PSGSuiteConfig { [string] $P12KeyPath, [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)] + [Byte[]] + $P12Key, + [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)] [string] $ClientSecretsPath, [parameter(Mandatory = $false,ValueFromPipelineByPropertyName = $true)] @@ -155,7 +161,7 @@ function Set-PSGSuiteConfig { } } Write-Verbose "Setting config name '$ConfigName'" - $configParams = @('P12KeyPath','ClientSecretsPath','ClientSecrets','AppEmail','AdminEmail','CustomerID','Domain','Preference','ServiceAccountClientID','Webhook','Space') + $configParams = @('P12Key','P12KeyPath','ClientSecretsPath','ClientSecrets','AppEmail','AdminEmail','CustomerID','Domain','Preference','ServiceAccountClientID','Webhook','Space') if ($SetAsDefaultConfig -or !$configHash["DefaultConfig"]) { $configHash["DefaultConfig"] = $ConfigName } @@ -164,9 +170,28 @@ function Set-PSGSuiteConfig { } foreach ($key in ($PSBoundParameters.Keys | Where-Object {$configParams -contains $_})) { switch ($key) { + P12Key { + if (-not $_p12Key) { + $_p12Key = @() + } + if ($P12Key.Count -gt 1) { + $_p12Key = $P12Key + } + else { + $_p12Key += $P12Key + } + } + P12KeyPath { + if (-not [System.String]::IsNullOrWhiteSpace($PSBoundParameters[$key].Trim())) { + $configHash["$ConfigName"][$key] = (Encrypt $PSBoundParameters[$key]) + $configHash["$ConfigName"]['P12Key'] = ([System.IO.File]::ReadAllBytes($PSBoundParameters[$key])) + } + } ClientSecretsPath { - $configHash["$ConfigName"][$key] = (Encrypt $PSBoundParameters[$key]) - $configHash["$ConfigName"]['ClientSecrets'] = (Encrypt $(Get-Content $PSBoundParameters[$key] -Raw)) + if (-not [System.String]::IsNullOrWhiteSpace($PSBoundParameters[$key].Trim())) { + $configHash["$ConfigName"][$key] = (Encrypt $PSBoundParameters[$key]) + $configHash["$ConfigName"]['ClientSecrets'] = (Encrypt $(Get-Content $PSBoundParameters[$key] -Raw)) + } } Webhook { if ($configHash["$ConfigName"].Keys -notcontains 'Chat') { @@ -200,10 +225,13 @@ function Set-PSGSuiteConfig { } } } - $configHash["$ConfigName"]['ConfigPath'] = (Join-Path $(Get-Module PSGSuite | Get-StoragePath -Scope $Script:ConfigScope) "Configuration.psd1") - $configHash | Export-Configuration -CompanyName 'SCRT HQ' -Name 'PSGSuite' -Scope $script:ConfigScope } End { + if ($_p12Key) { + $configHash["$ConfigName"]['P12Key'] = $_p12Key + } + $configHash["$ConfigName"]['ConfigPath'] = (Join-Path $(Get-Module PSGSuite | Get-StoragePath -Scope $Script:ConfigScope) "Configuration.psd1") + $configHash | Export-Configuration -CompanyName 'SCRT HQ' -Name 'PSGSuite' -Scope $script:ConfigScope if (!$NoImport) { Get-PSGSuiteConfig -ConfigName $ConfigName -Verbose:$false } diff --git a/PSGSuite/Public/Configuration/Switch-PSGSuiteConfig.ps1 b/PSGSuite/Public/Configuration/Switch-PSGSuiteConfig.ps1 index 541d5486..793b33d0 100644 --- a/PSGSuite/Public/Configuration/Switch-PSGSuiteConfig.ps1 +++ b/PSGSuite/Public/Configuration/Switch-PSGSuiteConfig.ps1 @@ -77,6 +77,7 @@ $script:PSGSuite = [PSCustomObject]($fullConf[$choice]) | Select-Object -Property @{l = 'ConfigName';e = {$choice}}, @{l = 'P12KeyPath';e = {Decrypt $_.P12KeyPath}}, + P12Key, @{l = 'ClientSecretsPath';e = {Decrypt $_.ClientSecretsPath}}, @{l = 'ClientSecrets';e = {Decrypt $_.ClientSecrets}}, @{l = 'AppEmail';e = {Decrypt $_.AppEmail}}, diff --git a/README.md b/README.md index e2069354..6c090a35 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,15 @@ Update-GSSheetValue Export-GSSheet ### Most recent changes +#### 2.22.0 + +* Miscellaneous: _Config management and portability updates_ + * Added: `Export-PSGSuiteConfig` function to export key parts of your config in a transportable JSON file. + * Added: `Import-PSGSuiteConfig` function to import a config from a JSON file (i.e. one created with `Export-PSGSuiteConfig`) or from a JSON string (i.e. stored in a secure variable in a CI/CD system.) + * Updated: All config functions now store the P12Key or the ClientSecrets JSON string in the encrypted config directly. This is to allow removal of the secrets files as well as enable PSGSuite to run in a contained environment via importing the config from a secure JSON string. + * Updated: `[Get|Set|Switch]-PSGSuiteConfig` to include the P12Key and ClientSecrets parameters that enable housing of the key/secret directly on the encrypted config. + * Updated: If the global PSGSuite variable `$global:PSGSuite` exists during module import, it will default to using that as it's configuration, otherwise it will import the default config if set. + #### 2.21.3 * [Issue #131](https://github.com/scrthq/PSGSuite/issues/131) diff --git a/psake.ps1 b/psake.ps1 index 28381cbe..4f0625cd 100644 --- a/psake.ps1 +++ b/psake.ps1 @@ -177,7 +177,17 @@ try { `$Script:ConfigName = `$ConfigName } try { - Get-PSGSuiteConfig @confParams -ErrorAction Stop + if (`$global:PSGSuite) { + Write-Warning "Using config `$(if (`$global:PSGSuite.ConfigName){"name '`$(`$global:PSGSuite.ConfigName)' "})found in variable: ```$global:PSGSuite" + Write-Verbose "`$((`$global:PSGSuite | Format-List | Out-String).Trim())" + if (`$global:PSGSuite -is [System.Collections.Hashtable]) { + `$global:PSGSuite = New-Object PSObject -Property `$global:PSGSuite + } + `$script:PSGSuite = `$global:PSGSuite + } + else { + Get-PSGSuiteConfig @confParams -ErrorAction Stop + } } catch { if (Test-Path "`$ModuleRoot\`$env:USERNAME-`$env:COMPUTERNAME-`$env:PSGSuiteDefaultDomain-PSGSuite.xml") {