Private/Secrets.ps1

function Initialize-PodeSecretVault
{
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [hashtable]
        $VaultConfig,

        [Parameter(Mandatory=$true)]
        [scriptblock]
        $ScriptBlock
    )

    Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -Splat -Arguments @($VaultConfig.Parameters)
}

function Register-PodeSecretManagementVault
{
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [hashtable]
        $VaultConfig,

        [Parameter()]
        [string]
        $VaultName,

        [Parameter(Mandatory=$true)]
        [string]
        $ModuleName
    )

    # use the Name for VaultName if not passed
    if ([string]::IsNullOrWhiteSpace($VaultName)) {
        $VaultName = $VaultConfig.Name
    }

    # import the modules
    $null = Import-Module -Name Microsoft.PowerShell.SecretManagement -Force -DisableNameChecking -Scope Global -ErrorAction Stop -Verbose:$false
    $null = Import-Module -Name $ModuleName -Force -DisableNameChecking -Scope Global -ErrorAction Stop -Verbose:$false

    # attempt to register the vault
    $null = Register-SecretVault -Name $VaultName -ModuleName $ModuleName -VaultParameters $VaultConfig.Parameters -Confirm:$false -AllowClobber -ErrorAction Stop

    # all is good, so set the config
    $VaultConfig['SecretManagement'] = @{
        VaultName = $VaultName
        ModuleName = $ModuleName
    }
}

function Register-PodeSecretCustomVault
{
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [hashtable]
        $VaultConfig,

        [Parameter(Mandatory=$true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [scriptblock]
        $UnlockScriptBlock,

        [Parameter()]
        [scriptblock]
        $RemoveScriptBlock,

        [Parameter()]
        [scriptblock]
        $SetScriptBlock,

        [Parameter()]
        [scriptblock]
        $UnregisterScriptBlock
    )

    # unlock secret with no script?
    if ($VaultConfig.Unlock.Enabled -and (Test-PodeIsEmpty $UnlockScriptBlock)) {
        throw 'Unlock secret supplied for custom Secret Vault type, but not Unlock ScriptBlock supplied'
    }

    # all is good, so set the config
    $VaultConfig['Custom'] = @{
        Read = $ScriptBlock
        Unlock = $UnlockScriptBlock
        Remove = $RemoveScriptBlock
        Set = $SetScriptBlock
        Unregister = $UnregisterScriptBlock
    }
}

function Unlock-PodeSecretManagementVault
{
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [hashtable]
        $VaultConfig
    )

    # do we need to unlock the vault?
    if (!$VaultConfig.Unlock.Enabled) {
        return $null
    }

    # unlock the vault
    $null = Unlock-SecretVault -Name $VaultConfig.SecretManagement.VaultName -Password $VaultConfig.Unlock.Secret -ErrorAction Stop

    # interval?
    if ($VaultConfig.Unlock.Interval -gt 0) {
        return ([datetime]::UtcNow.AddMinutes($VaultConfig.Unlock.Interval))
    }

    return $null
}

function Unlock-PodeSecretCustomVault
{
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [hashtable]
        $VaultConfig
    )

    # do we need to unlock the vault?
    if (!$VaultConfig.Unlock.Enabled) {
        return
    }

    # do we have an unlock scriptblock
    if ($null -eq $VaultConfig.Custom.Unlock) {
        throw "No Unlock ScriptBlock supplied for unlocking the vault '$($VaultConfig.Name)'"
    }

    # unlock the vault, and get back an expiry
    $expiry = (Invoke-PodeScriptBlock -ScriptBlock $VaultConfig.Custom.Unlock -Splat -Return -Arguments @(
        $VaultConfig.Parameters,
        (ConvertFrom-SecureString -SecureString $VaultConfig.Unlock.Secret -AsPlainText)
    ))

    # return expiry if given, otherwise check interval
    if ($null -ne $expiry) {
        return $expiry
    }

    if ($VaultConfig.Unlock.Interval -gt 0) {
        return ([datetime]::UtcNow.AddMinutes($VaultConfig.Unlock.Interval))
    }

    return $null
}

function Unregister-PodeSecretManagementVault
{
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [hashtable]
        $VaultConfig
    )

    # do we need to unregister the vault?
    if ($VaultConfig.AutoImported) {
        return
    }

    # unregister the vault
    $null = Unregister-SecretVault -Name $VaultConfig.SecretManagement.VaultName -Confirm:$false -ErrorAction Stop
}

function Unregister-PodeSecretCustomVault
{
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [hashtable]
        $VaultConfig
    )

    # do we need to unregister the vault?
    if ($VaultConfig.AutoImported) {
        return
    }

    # do we have an unregister scriptblock? if not, just do nothing
    if ($null -eq $VaultConfig.Custom.Unregister) {
        return
    }

    # unregister the vault
    Invoke-PodeScriptBlock -ScriptBlock $VaultConfig.Custom.Unregister -Splat -Arguments @(
        $VaultConfig.Parameters
    )
}

function Get-PodeSecretManagementKey
{
    param(
        [Parameter(Mandatory=$true)]
        [string]
        $Vault,

        [Parameter(Mandatory=$true)]
        [string]
        $Key
    )

    # get the vault
    $_vault = $PodeContext.Server.Secrets.Vaults[$Vault]

    # fetch the secret
    return (Get-Secret -Name $Key -Vault $_vault.SecretManagement.VaultName -AsPlainText -ErrorAction Stop)
}

function Get-PodeSecretCustomKey
{
    param(
        [Parameter(Mandatory=$true)]
        [string]
        $Vault,

        [Parameter(Mandatory=$true)]
        [string]
        $Key,

        [Parameter()]
        [object[]]
        $ArgumentList
    )

    # get the vault
    $_vault = $PodeContext.Server.Secrets.Vaults[$Vault]

    # fetch the secret
    return (Invoke-PodeScriptBlock -ScriptBlock $_vault.Custom.Read -Splat -Return -Arguments (@(
        $_vault.Parameters,
        $Key
    ) + $ArgumentList))
}

function Set-PodeSecretManagementKey
{
    param(
        [Parameter(Mandatory=$true)]
        [string]
        $Vault,

        [Parameter(Mandatory=$true)]
        [string]
        $Key,

        [Parameter(Mandatory=$true)]
        [object]
        $Value,

        [Parameter()]
        [hashtable]
        $Metadata
    )

    # get the vault
    $_vault = $PodeContext.Server.Secrets.Vaults[$Vault]

    # set the secret
    $null = Set-Secret -Name $Key -Secret $Value -Vault $_vault.SecretManagement.VaultName -Metadata $Metadata -Confirm:$false -ErrorAction Stop
}

function Set-PodeSecretCustomKey
{
    param(
        [Parameter(Mandatory=$true)]
        [string]
        $Vault,

        [Parameter(Mandatory=$true)]
        [string]
        $Key,

        [Parameter(Mandatory=$true)]
        [object]
        $Value,

        [Parameter()]
        [hashtable]
        $Metadata,

        [Parameter()]
        [object[]]
        $ArgumentList
    )

    # get the vault
    $_vault = $PodeContext.Server.Secrets.Vaults[$Vault]

    # do we have a set scriptblock?
    if ($null -eq $_vault.Custom.Set) {
        throw "No Set ScriptBlock supplied for updating/creating secrets in the vault '$($_vault.Name)'"
    }

    # set the secret
    Invoke-PodeScriptBlock -ScriptBlock $_vault.Custom.Set -Splat -Arguments (@(
        $_vault.Parameters,
        $Key,
        $Value,
        $Metadata
    ) + $ArgumentList)
}

function Remove-PodeSecretManagementKey
{
    param(
        [Parameter(Mandatory=$true)]
        [string]
        $Vault,

        [Parameter(Mandatory=$true)]
        [string]
        $Key
    )

    # get the vault
    $_vault = $PodeContext.Server.Secrets.Vaults[$Vault]

    # remove the secret
    $null = Remove-Secret -Name $Key -Vault $_vault.SecretManagement.VaultName -Confirm:$false -ErrorAction Stop
}

function Remove-PodeSecretCustomKey
{
    param(
        [Parameter(Mandatory=$true)]
        [string]
        $Vault,

        [Parameter(Mandatory=$true)]
        [string]
        $Key,

        [Parameter()]
        [object[]]
        $ArgumentList
    )

    # get the vault
    $_vault = $PodeContext.Server.Secrets.Vaults[$Vault]

    # do we have a remove scriptblock?
    if ($null -eq $_vault.Custom.Remove) {
        throw "No Remove ScriptBlock supplied for removing secrets from the vault '$($_vault.Name)'"
    }

    # remove the secret
    Invoke-PodeScriptBlock -ScriptBlock $_vault.Custom.Remove -Splat -Arguments (@(
        $_vault.Parameters,
        $Key
    ) + $ArgumentList)
}

function Start-PodeSecretCacheHousekeeper
{
    if (Test-PodeTimer -Name '__pode_secrets_cache_expiry__') {
        return
    }

    Add-PodeTimer -Name '__pode_secrets_cache_expiry__' -Interval 60 -ScriptBlock {
        $now = [datetime]::UtcNow

        foreach ($key in $PodeContext.Server.Secrets.Keys.Values) {
            if (!$key.Cache.Enabled -or ($null -eq $key.Cache.Expiry) -or ($key.Cache.Expiry -gt $now)) {
                continue
            }

            $key.Cache.Expiry = $null
            $key.Cache.Value = $null
        }
    }
}

function Start-PodeSecretVaultUnlocker
{
    if (Test-PodeTimer -Name '__pode_secrets_vault_unlock__') {
        return
    }

    Add-PodeTimer -Name '__pode_secrets_vault_unlock__' -Interval 60 -ScriptBlock {
        $now = [datetime]::UtcNow

        foreach ($vault in $PodeContext.Server.Secrets.Vaults.Values) {
            if (!$vault.Unlock.Enabled -or ($null -eq $vault.Unlock.Expiry) -or ($vault.Unlock.Expiry -gt $now)) {
                continue
            }

            Unlock-PodeSecretVault -Name $vault.Name
        }
    }
}

function Unregister-PodeSecretVaults
{
    param(
        [switch]
        $ThrowError
    )

    if (Test-PodeIsEmpty $PodeContext.Server.Secrets.Vaults) {
        return
    }

    foreach ($vault in $PodeContext.Server.Secrets.Vaults.Values.Name) {
        if ([string]::IsNullOrEmpty($vault)) {
            continue
        }

        try {
            Unregister-PodeSecretVault -Name $vault
        }
        catch {
            if ($ThrowError) {
                throw
            }
            else {
                $_ | Write-PodeErrorLog
            }
        }
    }
}

function Protect-PodeSecretValueType
{
    param(
        [Parameter(Mandatory=$true)]
        [object]
        $Value
    )

    if ($Value -is [System.ValueType]) {
        $Value = $Value.ToString()
    }

    if ([string]::IsNullOrEmpty($Value)) {
        $Value = [string]::Empty
    }

    if ($Value -is [ordered]) {
        $Value = [hashtable]$Value
    }

    if (!(
         ($Value -is [string]) -or
         ($Value -is [securestring]) -or
         ($Value -is [hashtable]) -or
         ($Value -is [byte[]]) -or
         ($Value -is [pscredential]) -or
         ($Value -is [System.Management.Automation.OrderedHashtable])
       )) {
        throw "Value to set secret to is of an invalid type. Expected either String, SecureString, HashTable, Byte[], or PSCredential. But got: $($Value.GetType().Name)"
    }

    return $Value
}