Store.psm1
[CmdletBinding()] param() $scriptName = 'Store' Write-Verbose "[$scriptName] - Importing module" #region - From [functions] - [private] Write-Verbose "[$scriptName] - [functions] - [private] - Processing folder" #region - From [functions] - [private] - [Initialize-SecretVault] Write-Verbose "[$scriptName] - [functions] - [private] - [Initialize-SecretVault] - Importing" #Requires -Modules Microsoft.PowerShell.SecretManagement #Requires -Modules Microsoft.PowerShell.SecretStore function Initialize-SecretVault { <# .SYNOPSIS Initialize a SecretStore with open config. .DESCRIPTION Initialize a secret vault. If the vault does not exist, it will be created and registered. The SecretStore is created with the following parameters: - Authentication: None - PasswordTimeout: -1 (infinite) - Interaction: None - Scope: CurrentUser .EXAMPLE Initialize-SecretStore Initializes a secret vault named 'SecretStore' using the 'Microsoft.PowerShell.SecretStore' module. .NOTES For more information about secret vaults, see https://learn.microsoft.com/en-us/powershell/utility-modules/secretmanagement/overview?view=ps-modules #> [OutputType([Microsoft.PowerShell.SecretManagement.SecretVaultInfo])] [CmdletBinding()] param ( # The name of the secret vault. [Parameter()] [string] $Name = $script:Config.SecretVaultName, # The type of the secret vault. [Parameter()] [string] $Type = $script:Config.SecretVaultType ) $vault = Get-SecretVault | Where-Object { $_.ModuleName -eq $Type } if (-not $vault) { Write-Verbose "[$Type] - Configuring vault type" $vaultParameters = @{ Authentication = 'None' PasswordTimeout = -1 Interaction = 'None' Scope = 'CurrentUser' WarningAction = 'SilentlyContinue' Confirm = $false Force = $true } Reset-SecretStore @vaultParameters Write-Verbose "[$Type] - Done" Write-Verbose "[$Name] - Registering vault" $secretVault = @{ Name = $Name ModuleName = $Type DefaultVault = $true Description = 'SecretStore' } Register-SecretVault @secretVault Write-Verbose "[$Name] - Done" } else { Write-Verbose "[$Name] - Vault already registered" } Get-SecretVault | Where-Object { $_.ModuleName -eq $Type } } Write-Verbose "[$scriptName] - [functions] - [private] - [Initialize-SecretVault] - Done" #endregion - From [functions] - [private] - [Initialize-SecretVault] Write-Verbose "[$scriptName] - [functions] - [private] - Done" #endregion - From [functions] - [private] #region - From [functions] - [public] Write-Verbose "[$scriptName] - [functions] - [public] - Processing folder" #region - From [functions] - [public] - [Store] Write-Verbose "[$scriptName] - [functions] - [public] - [Store] - Processing folder" #region - From [functions] - [public] - [Store] - [Get-Store] Write-Verbose "[$scriptName] - [functions] - [public] - [Store] - [Get-Store] - Importing" function Get-Store { <# .SYNOPSIS Retrieves secrets from a specified secret vault. .DESCRIPTION The `Get-Store` cmdlet retrieves secrets from a specified secret vault. You can specify the name of the secret to retrieve or use a wildcard pattern to retrieve multiple secrets. If no name is specified, all secrets from the vault will be retrieved. Optionally, you can choose to retrieve the secrets as plain text. .EXAMPLE Get-Store Get all stores from the vault. .EXAMPLE Get-Store -Name 'MySecret' Get the store called 'MySecret' from the vault. .EXAMPLE Get-Store -Name 'My*' Get all stores that match the pattern 'My*' from the vault. #> [OutputType([pscustomobject])] [CmdletBinding()] param ( # The name of the secret to retrieve from the vault. Supports wildcard patterns. [Parameter()] [string] $Name, # Switch to retrieve the secrets as plain text. [Parameter()] [switch] $AsPlainText ) Write-Verbose "Retrieving secret vault with name [$($script:Config.SecretVaultName)]" $secretVault = Get-SecretVault | Where-Object { $_.Name -eq $script:Config.SecretVaultName } if (-not $secretVault) { Write-Verbose "No secret vault found with name [$($script:Config.SecretVaultName)]" return $null } Write-Verbose "Retrieving secret infos from vault [$($secretVault.Name)]" $secretInfos = Get-SecretInfo -Vault $secretVault.Name if (-not $secretInfos) { Write-Verbose "No secret infos found in vault [$($secretVault.Name)]" return $null } if ($Name) { Write-Verbose "Filtering secret infos with name pattern [$Name]" $secretInfos = $secretInfos | Where-Object { $_.Name -like $Name } } $stores = @() foreach ($secretInfo in $secretInfos) { $metadata = $secretInfo | Select-Object -ExpandProperty Metadata $store = $metadata + @{ Name = $secretInfo.Name Secret = Get-Secret -Name $secretInfo.Name -Vault $script:Config.SecretVaultName -AsPlainText:$AsPlainText } $stores += [pscustomobject]$store } return $stores } # Register tab completer for the Name parameter Register-ArgumentCompleter -CommandName Get-Store -ParameterName Name -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $null) $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters # Suppress unused variable warning $secretVault = Get-SecretVault | Where-Object { $_.Name -eq $script:Config.SecretVaultName } if (-not $secretVault) { return } $secretInfos = Get-SecretInfo -Vault $secretVault.Name if (-not $secretInfos) { return } $secretInfos | Where-Object { $_.Name -like "$wordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, 'ParameterValue', $_.Name) } } Write-Verbose "[$scriptName] - [functions] - [public] - [Store] - [Get-Store] - Done" #endregion - From [functions] - [public] - [Store] - [Get-Store] #region - From [functions] - [public] - [Store] - [Remove-Store] Write-Verbose "[$scriptName] - [functions] - [public] - [Store] - [Remove-Store] - Importing" filter Remove-Store { <# .SYNOPSIS Remove a store from the vault. .DESCRIPTION This function removes a store from the vault. It supports removing a single store by name, multiple stores using wildcard patterns, and can also accept input from the pipeline. If the specified store(s) exist, they will be removed from the vault. .EXAMPLE Remove-Store -Name 'MySecret' Removes the store called 'MySecret' from the vault. .EXAMPLE 'MySecret*' | Remove-Store Removes all stores matching the pattern 'MySecret*' from the vault. .EXAMPLE Get-Store -Name 'MySecret*' | Remove-Store Retrieves all stores matching the pattern 'MySecret*' and removes them from the vault. #> [OutputType([void])] [CmdletBinding(SupportsShouldProcess)] param ( # The name of the secret vault. [Parameter( Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName )] [string] $Name ) $secretVault = Get-SecretVault | Where-Object { $_.Name -eq $script:Config.SecretVaultName } if (-not $secretVault) { Write-Error 'Secret vault not found.' return } $secretInfos = Get-SecretInfo -Vault $secretVault.Name | Where-Object { $_.Name -like $Name } if (-not $secretInfos) { Write-Error 'No matching stores found.' return } foreach ($secretInfo in $secretInfos) { if ($PSCmdlet.ShouldProcess('Remove-Secret', $secretInfo.Name)) { Remove-Secret -Name $secretInfo.Name -Vault $script:Config.SecretVaultName } } } Write-Verbose "[$scriptName] - [functions] - [public] - [Store] - [Remove-Store] - Done" #endregion - From [functions] - [public] - [Store] - [Remove-Store] #region - From [functions] - [public] - [Store] - [Set-Store] Write-Verbose "[$scriptName] - [functions] - [public] - [Store] - [Set-Store] - Importing" function Set-Store { <# .SYNOPSIS Set a store in the vault. .DESCRIPTION If the store does not exist, it will be created. If it already exists, it will be updated. .EXAMPLE Set-Store -Name 'MySecret' Create a store called 'MySecret' in the vault. .EXAMPLE Set-Store -Name 'MySecret' -Secret 'MySecret' Creates a store called 'MySecret' in the vault with the secret. .EXAMPLE Set-Store -Name 'MySecret' -Secret 'MySecret' -Variables @{ 'Key' = 'Value' } Creates a store called 'MySecret' in the vault with the secret and variables. #> [OutputType([void])] [CmdletBinding(SupportsShouldProcess)] param ( # The name of the store. [Parameter()] [Alias('Store', 'StoreName')] [string] $Name, # The secret of the store. [Parameter()] [object] $Secret = 'null', # The variables of the store. [Parameter()] [hashtable] $Variables ) $param = @{ Name = $Name Vault = $script:Config.SecretVaultName } #Map secret based on type, to Secret or SecureStringSecret if ($Secret -is [System.Security.SecureString]) { $param['SecureStringSecret'] = $Secret } elseif ($Secret -is [string]) { $param['Secret'] = $Secret } else { throw 'Invalid secret type' } if ($Variables) { $param['Metadata'] = $Variables } if ($PSCmdlet.ShouldProcess('Set-Secret', $param)) { Set-Secret @param } } Write-Verbose "[$scriptName] - [functions] - [public] - [Store] - [Set-Store] - Done" #endregion - From [functions] - [public] - [Store] - [Set-Store] Write-Verbose "[$scriptName] - [functions] - [public] - [Store] - Done" #endregion - From [functions] - [public] - [Store] #region - From [functions] - [public] - [StoreConfig] Write-Verbose "[$scriptName] - [functions] - [public] - [StoreConfig] - Processing folder" #region - From [functions] - [public] - [StoreConfig] - [Get-StoreConfig] Write-Verbose "[$scriptName] - [functions] - [public] - [StoreConfig] - [Get-StoreConfig] - Importing" #Requires -Modules Microsoft.PowerShell.SecretManagement function Get-StoreConfig { <# .SYNOPSIS Retrieve a named value from the store. .DESCRIPTION This function retrieves a named value from the specified store. If the value is a secret, it can be returned as plain text using the -AsPlainText switch. .EXAMPLE Get-StoreConfig -Name 'ApiBaseUri' -Store 'GitHub' Get the value of 'ApiBaseUri' config from the GitHub store. .EXAMPLE Get-StoreConfig -Name 'Api*' -Store 'GitHub' Get all configuration values from the GitHub store that match the wildcard pattern 'Api*'. #> [OutputType([object])] [CmdletBinding()] param ( # The store to get the configuration from. [Parameter(Mandatory)] [string] $Store, # Name of a value to get. [Parameter(Mandatory)] [string] $Name, # Return the value as plain text if it is a secret. [Parameter()] [switch] $AsPlainText ) Write-Verbose "Getting store configuration for store: [$Store]" $storeConfig = Get-Store -Name $Store -AsPlainText:$AsPlainText if ($null -eq $storeConfig) { Write-Verbose "No configuration found for store: [$Store]" return } $storeConfig.$Name } Write-Verbose "[$scriptName] - [functions] - [public] - [StoreConfig] - [Get-StoreConfig] - Done" #endregion - From [functions] - [public] - [StoreConfig] - [Get-StoreConfig] #region - From [functions] - [public] - [StoreConfig] - [Remove-StoreConfig] Write-Verbose "[$scriptName] - [functions] - [public] - [StoreConfig] - [Remove-StoreConfig] - Importing" filter Remove-StoreConfig { <# .SYNOPSIS Remove a named value from the store. .DESCRIPTION This function removes a named value from the specified store. It supports wildcard patterns for the name and can accept pipeline input from `Get-StoreConfig`. .EXAMPLE Remove-StoreConfig -Name 'APIBaseUri' -Store 'GitHub' Remove the APIBaseUri value from the 'GitHub' store. .EXAMPLE Get-StoreConfig -Store 'GitHub' | Remove-StoreConfig -Name 'API*' Remove all values starting with 'API' from the 'GitHub' store. .EXAMPLE Remove-StoreConfig -Name 'API*' -Store 'GitHub' Remove all values starting with 'API' from the 'GitHub' store. .EXAMPLE Get-StoreConfig -Store 'GitHub' | Where-Object { $_.Name -like 'API*' } | Remove-StoreConfig Remove all values starting with 'API' from the 'GitHub' store using pipeline input. #> [CmdletBinding(SupportsShouldProcess)] param ( # Name of a value to remove. [Parameter( Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName )] [string] $Name, # The store to remove the value from. [Parameter(ValueFromPipelineByPropertyName)] [string] $Store ) if ($PSCmdlet.ShouldProcess("Target", "Remove value [$Name] from store [$Store]")) { Set-StoreConfig -Name $Name -Value $null -Store $Store } } Write-Verbose "[$scriptName] - [functions] - [public] - [StoreConfig] - [Remove-StoreConfig] - Done" #endregion - From [functions] - [public] - [StoreConfig] - [Remove-StoreConfig] #region - From [functions] - [public] - [StoreConfig] - [Set-StoreConfig] Write-Verbose "[$scriptName] - [functions] - [public] - [StoreConfig] - [Set-StoreConfig] - Importing" #Requires -Modules Microsoft.PowerShell.SecretManagement function Set-StoreConfig { <# .SYNOPSIS Sets a variable or secret in the store. .DESCRIPTION The `Set-StoreConfig` function sets a variable or secret in the specified store. To store a secret, set the name to 'Secret'. .EXAMPLE Set-StoreConfig -Name 'ApiBaseUri' -Value 'https://api.github.com' -Store 'GitHub' Sets a variable called 'ApiBaseUri' in the store called 'GitHub'. .EXAMPLE $secret = 'myAccessToken' | ConvertTo-SecureString -AsPlainText -Force Set-StoreConfig -Name 'Secret' -Value $secret -Store 'GitHub' Sets a secret called 'AccessToken' in the configuration store called 'GitHub'. .NOTES This function requires the Microsoft.PowerShell.SecretManagement module. #> [OutputType([void])] [CmdletBinding(SupportsShouldProcess)] param ( # The name of the variable or secret to set. [Parameter(Mandatory)] [string] $Name, # The value to set for the specified name. This can be a plain text string or a secure string. [Parameter(Mandatory)] [AllowNull()] [AllowEmptyString()] [object] $Value, # The name of the store where the variable or secret will be set. [Parameter(Mandatory)] [string] $Store ) $secretVault = Get-SecretVault | Where-Object { $_.Name -eq $script:Config.SecretVaultName } if (-not $secretVault) { Write-Error "Vault [$($script:Config.SecretVaultName)] not found" return } Write-Verbose "Retrieving secret info for store [$Store] from vault [$($secretVault.Name)]" $secretInfo = Get-SecretInfo -Name $Store -Vault $script:Config.SecretVaultName $secretValue = Get-Secret -Name $Store -Vault $script:Config.SecretVaultName if (-not $secretValue) { Write-Error "Store [$Store] not found" return } if ($PSCmdlet.ShouldProcess($Name, "Set value [$Value]")) { Write-Verbose "Processing [$Name] with value [$Value]" switch ($Name) { 'Secret' { if ([string]::IsNullOrEmpty($Value)) { Write-Verbose "Value is null or empty, setting to 'null'" $Value = 'null' } if ($Value -is [SecureString]) { Write-Verbose "Value is a SecureString, setting secret in vault [$($script:Config.SecretVaultName)]" Set-Secret -Name $Store -SecureStringSecret $Value -Vault $script:Config.SecretVaultName } else { Write-Verbose "Value is $($Value.GetType().FullName), setting secret in vault [$($script:Config.SecretVaultName)]" Set-Secret -Name $Store -Value $Value -Vault $script:Config.SecretVaultName } break } 'Name' { if ([string]::IsNullOrEmpty($Value)) { Write-Error 'Name cannot be null or empty' return } Set-Secret -Name $Value -SecureStringSecret $secretValue -Vault $Store -Metadata $secretInfo.Metadata $newSecretInfo = Get-SecretInfo -Name $Value -Vault $Store if ($newSecretInfo) { Remove-Secret -Name $Name -Vault $Store } else { Remove-Secret -Name $Value -Vault $Store } break } default { Write-Verbose 'Updating metadata' $metadata = ($secretInfo | Select-Object -ExpandProperty Metadata) + @{} if ([string]::IsNullOrEmpty($Value)) { Write-Verbose " - Removing [$Name] from metadata" $metadata.Remove($Name) } else { Write-Verbose " - Setting [$Name] to [$Value] in metadata" $metadata[$Name] = $Value } Write-Verbose "Updating secret info for store [$Store] in vault [$($script:Config.SecretVaultName)]" Set-SecretInfo -Name $Store -Metadata $metadata -Vault $script:Config.SecretVaultName } } } } Write-Verbose "[$scriptName] - [functions] - [public] - [StoreConfig] - [Set-StoreConfig] - Done" #endregion - From [functions] - [public] - [StoreConfig] - [Set-StoreConfig] Write-Verbose "[$scriptName] - [functions] - [public] - [StoreConfig] - Done" #endregion - From [functions] - [public] - [StoreConfig] Write-Verbose "[$scriptName] - [functions] - [public] - Done" #endregion - From [functions] - [public] #region - From [variables] - [private] Write-Verbose "[$scriptName] - [variables] - [private] - Processing folder" #region - From [variables] - [private] - [Config] Write-Verbose "[$scriptName] - [variables] - [private] - [Config] - Importing" $script:Config = @{ Name = 'PSModule.Store' # $script:Config.Name SecretVaultName = 'SecretStore' # $script:Config.SecretVaultName SecretVaultType = 'Microsoft.PowerShell.SecretStore' # $script:Config.SecretVaultType } Write-Verbose "[$scriptName] - [variables] - [private] - [Config] - Done" #endregion - From [variables] - [private] - [Config] Write-Verbose "[$scriptName] - [variables] - [private] - Done" #endregion - From [variables] - [private] #region - From [loader] Write-Verbose "[$scriptName] - [loader] - Importing" ### This is the backend configuration for the functionality try { $initStoreParams = @{ Name = (Get-StoreConfig -Name SecretVaultName -Store $script:Config.Name) ?? $script:Config.SecretVaultName Type = (Get-StoreConfig -Name SecretVaultType -Store $script:Config.Name) ?? $script:Config.SecretVaultType } $vault = Initialize-SecretVault @initStoreParams $script:Config.SecretVaultName = $vault.Name $script:Config.SecretVaultType = $vault.ModuleName } catch { Write-Error "Failed to initialize secret vault: $_" return } ### This is the store config for this module $storeParams = @{ Name = $script:Config.Name Variables = @{ SecretVaultName = $script:Config.SecretVaultName SecretVaultType = $script:Config.SecretVaultType } } try { Set-Store @storeParams } catch { Write-Error "Failed to set store parameters: $_" } Write-Verbose "[$scriptName] - [loader] - Done" #endregion - From [loader] $exports = @{ Alias = '*' Cmdlet = '' Function = @( 'Get-Store' 'Remove-Store' 'Set-Store' 'Get-StoreConfig' 'Remove-StoreConfig' 'Set-StoreConfig' ) } Export-ModuleMember @exports |