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
        Get a store from the vault.

        .DESCRIPTION
        Get a store from the vault.

        .EXAMPLE
        Get-Store -Name 'MySecret'

        Get the store called 'MySecret' from the vault.
    #>

    [OutputType([hashtable])]
    param (
        # The name of the secret vault.
        [Parameter(Mandatory)]
        [string] $Name,

        # Set everything as plain text.
        [Parameter()]
        [switch] $AsPlainText
    )
    $secretVault = Get-SecretVault | Where-Object { $_.Name -eq $script:Config.SecretVaultName }
    if (-not $secretVault) {
        return $null
    }
    $secretInfo = Get-SecretInfo -Vault $secretVault.Name | Where-Object { $_.Name -eq $Name }
    if (-not $secretInfo) {
        return $null
    }
    $metadata = $secretInfo | Select-Object -ExpandProperty Metadata
    $metadata + @{
        Secret = Get-Secret -Name $Name -Vault $script:Config.SecretVaultName -AsPlainText:$AsPlainText
    }
}

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"

function Remove-Store {
    <#
        .SYNOPSIS
        Remove a store from the vault.

        .DESCRIPTION
        Remove a store from the vault.

        .EXAMPLE
        Remove-Store -Name 'MySecret'

        Removes the store called 'MySecret' from the vault.
    #>

    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess)]
    param (
        # The name of the secret vault.
        [Parameter(Mandatory)]
        [string] $Name
    )
    if ($PSCmdlet.ShouldProcess('Remove-Secret', $Name)) {
        Get-SecretInfo | Where-Object { $_.Name -eq $Name } | ForEach-Object {
            Remove-Secret -Name $_.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()]
        [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
        Get a named value from the store.

        .DESCRIPTION
        Get a named value from the store.

        .EXAMPLE
        Get-StoreConfig -Name 'ApiBaseUri' -Store 'GitHub'

        Get the value of 'ApiBaseUri' config from the GitHub store.
    #>

    [OutputType([object])]
    [CmdletBinding()]
    param (
        # Name of a value to get.
        [Parameter(Mandatory)]
        [string] $Name,

        # Return the value as plain text if it is a secret.
        [Parameter()]
        [switch] $AsPlainText,

        # The store to get the configuration from.
        [Parameter()]
        [string] $Store
    )

    (Get-Store -Name $Store -AsPlainText:$AsPLainText).$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"

function Remove-StoreConfig {
    <#
        .SYNOPSIS
        Remove a named value from the store.

        .DESCRIPTION
        Remove a named value from the store.

        .EXAMPLE
        Remove-StoreConfig -Name 'ApiBaseUri' -Store 'GitHub'

        Remove the ApiBaseUri value from the 'GitHub' store.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        # Name of a value to remove.
        [Parameter(Mandatory)]
        [string] $Name,

        # The store to remove the value from.
        [Parameter()]
        [string] $Store
    )

    if ($PSCmdlet.ShouldProcess("config '$Name' from '$Store'", 'Remove')) {

    }
    Set-StoreConfig -Store $Store -Name $Name -Value $null
}

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
        Set a variables or secret.

        .DESCRIPTION
        Set a variable or secret in the 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'.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        # The name of a value to set.
        [Parameter(Mandatory)]
        [string] $Name,

        # The value to set.
        [Parameter(Mandatory)]
        [AllowNull()]
        [AllowEmptyString()]
        [object] $Value,

        # The name of the store.
        [Parameter(Mandatory)]
        [string] $Store
    )

    $secretVault = Get-SecretVault | Where-Object { $_.Name -eq $script:Config.SecretVaultName }
    if (-not $secretVault) {
        throw "Vault '$($script:Config.SecretVaultName)' not found"
    }

    if ($PSCmdlet.ShouldProcess($Name, "Set value $Value]")) {
        if ($Name -eq 'Secret') {
            if ([string]::IsNullOrEmpty($Value)) {
                $Value = 'null'
            }
            if ($Value -is [SecureString]) {
                Set-Secret -Name $Store -SecureStringSecret $Value -Vault $script:Config.SecretVaultName
            } else {
                Set-Secret -Name $Store -Value $Value -Vault $script:Config.SecretVaultName
            }
        } else {
            $secretInfo = Get-SecretInfo -Vault $secretVault.Name | Where-Object { $_.Name -eq $Store }
            if (-not $secretInfo) {
                throw "Store '$Store' not found"
            }
            $metadata = ($secretInfo | Select-Object -ExpandProperty Metadata) + @{}
            if ([string]::IsNullOrEmpty($Value)) {
                $metadata.Remove($Name)
            } else {
                $metadata[$Name] = $Value
            }
            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
$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

### This is the store config for this module
$storeParams = @{
    Name      = $script:Config.Name
    Variables = @{
        SecretVaultName = $script:Config.SecretVaultName
        SecretVaultType = $script:Config.SecretVaultType
    }
}
Set-Store @storeParams
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