Context.psm1

[CmdletBinding()]
param()

$scriptName = 'Context'
Write-Verbose "[$scriptName] - Importing module"

#region - From [functions] - [private]
Write-Verbose "[$scriptName] - [functions] - [private] - Processing folder"

#region - From [functions] - [private] - [Get-ContextVault]
Write-Verbose "[$scriptName] - [functions] - [private] - [Get-ContextVault] - Importing"

function Get-ContextVault {
    <#
        .SYNOPSIS
        Retrieves the context vault.

        .DESCRIPTION
        Connects to a context vault.
        If the vault name is not set in the configuration, it throws an error.
        If the specified vault is not found, it throws an error.
        Otherwise, it returns the secret vault object.

        .EXAMPLE
        Get-ContextVault

        This example retrieves the context vault.
    #>

    [CmdletBinding()]
    param()

    if (-not $script:Config.Context.VaultName) {
        throw 'Context vault name not set'
    }

    Write-Verbose "Connecting to context vault [$($script:Config.Context.VaultName)]"
    $secretVault = Get-SecretVault | Where-Object { $_.Name -eq $script:Config.Context.VaultName }
    if (-not $secretVault) {
        Write-Error $_
        throw "Context vault [$($script:Config.Context.VaultName)] not found"
    }

    return $secretVault
}

Write-Verbose "[$scriptName] - [functions] - [private] - [Get-ContextVault] - Done"
#endregion - From [functions] - [private] - [Get-ContextVault]
#region - From [functions] - [private] - [Initialize-ContextVault]
Write-Verbose "[$scriptName] - [functions] - [private] - [Initialize-ContextVault] - Importing"

#Requires -Modules Microsoft.PowerShell.SecretManagement
#Requires -Modules Microsoft.PowerShell.SecretStore

function Initialize-ContextVault {
    <#
        .SYNOPSIS
        Initialize a context vault.

        .DESCRIPTION
        Initialize a context 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-ContextVault

        Initializes a context vault named 'ContextVault' using the 'Microsoft.PowerShell.SecretStore' module.
    #>

    [OutputType([Microsoft.PowerShell.SecretManagement.SecretVaultInfo])]
    [CmdletBinding()]
    param (
        # The name of the secret vault.
        [Parameter()]
        [string] $Name = $script:Config.Context.VaultName,

        # The type of the secret vault.
        [Parameter()]
        [string] $Type = $script:Config.Context.VaultType
    )
    $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-ContextVault] - Done"
#endregion - From [functions] - [private] - [Initialize-ContextVault]

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] - [Context]
Write-Verbose "[$scriptName] - [functions] - [public] - [Context] - Processing folder"

#region - From [functions] - [public] - [Context] - [Get-Context]
Write-Verbose "[$scriptName] - [functions] - [public] - [Context] - [Get-Context] - Importing"

function Get-Context {
    <#
        .SYNOPSIS
        Retrieves a context from the context vault.

        .DESCRIPTION
        Retrieves contexts from a specified context vault. You can specify the name of the context to retrieve or use a wildcard pattern to retrieve
        multiple contexts. If no name is specified, all contexts from the context vault will be retrieved.
        Optionally, you can choose to retrieve the contexts as plain text by providing the -AsPlainText switch.

        .EXAMPLE
        Get-Context

        Get all contexts from the context vault.

        .EXAMPLE
        Get-Context -Name 'MySecret'

        Get the context called 'MySecret' from the vault.

        .EXAMPLE
        Get-Context -Name 'My*'

        Get all contexts that match the pattern 'My*' from the vault.
    #>

    [OutputType([pscustomobject])]
    [CmdletBinding()]
    param (
        # The name of the context to retrieve from the vault. Supports wildcard patterns.
        [Parameter()]
        [SupportsWildcards()]
        [Alias('Context', 'ContextName')]
        [string] $Name = '*',

        # Switch to retrieve the contexts as plain text.
        [Parameter()]
        [switch] $AsPlainText
    )

    $contextVault = Get-ContextVault

    Write-Verbose "Retrieving contexts from vault [$($contextVault.Name)]"
    $contexts = Get-SecretInfo -Vault $contextVault.Name
    if (-not $contexts) {
        Write-Error $_
        throw "No context found in vault [$($contextVault.Name)]"
    }

    if ($Name) {
        Write-Verbose "Filtering contexts with name pattern [$Name]"
        $contexts = $contexts | Where-Object { $_.Name -like $Name }
    }

    Write-Verbose "Found [$($contexts.Count)] contexts in context vault [$($contextVault.Name)]"
    foreach ($context in $contexts) {
        [pscustomobject](
            $context.Metadata + @{
                Name   = $context.Name
                Secret = Get-Secret -Name $context.Name -Vault $contextVault.Name -AsPlainText:$AsPlainText
            }
        )
    }
}

# Register tab completer for the Name parameter
Register-ArgumentCompleter -CommandName Get-Context -ParameterName Name -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $null)
    $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters # Suppress unused variable warning
    $contextVault = Get-SecretVault | Where-Object { $_.Name -eq $script:Config.Context.VaultName }
    if (-not $contextVault) {
        return
    }

    $contexts = Get-SecretInfo -Vault $contextVault.Name
    if (-not $contexts) {
        return
    }

    $contexts | Where-Object { $_.Name -like "$wordToComplete*" } | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, 'ParameterValue', $_.Name)
    }
}

Write-Verbose "[$scriptName] - [functions] - [public] - [Context] - [Get-Context] - Done"
#endregion - From [functions] - [public] - [Context] - [Get-Context]
#region - From [functions] - [public] - [Context] - [Remove-Context]
Write-Verbose "[$scriptName] - [functions] - [public] - [Context] - [Remove-Context] - Importing"

filter Remove-Context {
    <#
        .SYNOPSIS
        Remove a context from the context vault.

        .DESCRIPTION
        This function removes a context from the vault. It supports removing a single context by name,
        multiple contexts using wildcard patterns, and can also accept input from the pipeline.
        If the specified context(s) exist, they will be removed from the vault.

        .EXAMPLE
        Remove-Context -Name 'MySecret'

        Removes the context called 'MySecret' from the vault.

        .EXAMPLE
        'MySecret*' | Remove-Context

        Removes all contexts matching the pattern 'MySecret*' from the vault.

        .EXAMPLE
        Get-Context -Name 'MySecret*' | Remove-Context

        Retrieves all contexts 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
        )]
        [Alias('Context', 'ContextName')]
        [string] $Name
    )

    $contextVault = Get-ContextVault

    $contexts = Get-Context -Name $Name

    foreach ($context in $contexts) {
        if ($PSCmdlet.ShouldProcess('Remove-Secret', $context.Name)) {
            Remove-Secret -Name $context.Name -Vault $contextVault.Name
        }
    }
}

Write-Verbose "[$scriptName] - [functions] - [public] - [Context] - [Remove-Context] - Done"
#endregion - From [functions] - [public] - [Context] - [Remove-Context]
#region - From [functions] - [public] - [Context] - [Set-Context]
Write-Verbose "[$scriptName] - [functions] - [public] - [Context] - [Set-Context] - Importing"

function Set-Context {
    <#
        .SYNOPSIS
        Set a context in the vault.

        .DESCRIPTION
        If the context does not exist, it will be created. If it already exists, it will be updated.

        .EXAMPLE
        Set-Context -Name 'MySecret'

        Create a context called 'MySecret' in the vault.

        .EXAMPLE
        Set-Context -Name 'MySecret' -Secret 'MySecret'

        Creates a context called 'MySecret' in the vault with the secret.

        .EXAMPLE
        Set-Context -Name 'MySecret' -Secret 'MySecret' -Variables @{ 'Key' = 'Value' }

        Creates a context called 'MySecret' in the vault with the secret and variables.
    #>

    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess)]
    param (
        # The name of the context.
        [Parameter()]
        [Alias('Context', 'ContextName')]
        [string] $Name,

        # The secret of the context.
        [Parameter()]
        [object] $Secret = 'null',

        # The variables of the context.
        [Parameter()]
        [hashtable] $Variables
    )

    $contextVault = Get-ContextVault

    $param = @{
        Name  = $Name
        Vault = $contextVault.Name
    }

    #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] - [Context] - [Set-Context] - Done"
#endregion - From [functions] - [public] - [Context] - [Set-Context]

Write-Verbose "[$scriptName] - [functions] - [public] - [Context] - Done"
#endregion - From [functions] - [public] - [Context]

#region - From [functions] - [public] - [ContextSettings]
Write-Verbose "[$scriptName] - [functions] - [public] - [ContextSettings] - Processing folder"

#region - From [functions] - [public] - [ContextSettings] - [Get-ContextSetting]
Write-Verbose "[$scriptName] - [functions] - [public] - [ContextSettings] - [Get-ContextSetting] - Importing"

#Requires -Modules Microsoft.PowerShell.SecretManagement

function Get-ContextSetting {
    <#
        .SYNOPSIS
        Retrieve a setting from a context.

        .DESCRIPTION
        This function retrieves a setting from a specified context.
        If the setting is a secret, it can be returned as plain text using the -AsPlainText switch.

        .EXAMPLE
        Get-ContextSetting -Name 'APIBaseUri' -Context 'GitHub'

        Get the value of the 'APIBaseUri' setting from the 'GitHub' context.
    #>

    [OutputType([object])]
    [CmdletBinding()]
    param (
        # The context to get the configuration from.
        [Parameter(Mandatory)]
        [Alias('ContextName')]
        [string] $Context,

        # Name of a setting to get.
        [Parameter(Mandatory)]
        [string] $Name,

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

    $null = Get-ContextVault

    Write-Verbose "Getting settings for context: [$Context]"
    $contextObj = Get-Context -Name $Context -AsPlainText:$AsPlainText
    if (-not $contextObj) {
        Write-Error $_
        throw "Context [$Context] not found"
    }
    Write-Verbose ($contextObj | Out-String)
    $contextObj.$Name
}

Write-Verbose "[$scriptName] - [functions] - [public] - [ContextSettings] - [Get-ContextSetting] - Done"
#endregion - From [functions] - [public] - [ContextSettings] - [Get-ContextSetting]
#region - From [functions] - [public] - [ContextSettings] - [Remove-ContextSetting]
Write-Verbose "[$scriptName] - [functions] - [public] - [ContextSettings] - [Remove-ContextSetting] - Importing"

filter Remove-ContextSetting {
    <#
        .SYNOPSIS
        Remove a setting from the context.

        .DESCRIPTION
        This function removes a setting from the specified context.
        It supports wildcard patterns for the name and does accept pipeline input.

        .EXAMPLE
        Remove-ContextSetting -Name 'APIBaseUri' -Context 'GitHub'

        Remove the APIBaseUri setting from the 'GitHub' context.

        .EXAMPLE
        Get-ContextSetting -Context 'GitHub' | Remove-ContextSetting

        Remove all settings starting with 'API' from the 'GitHub' context.

        .EXAMPLE
        Remove-ContextSetting -Name 'API*' -Context 'GitHub'

        Remove all settings starting with 'API' from the 'GitHub' context.

        .EXAMPLE
        Get-ContextSetting -Context 'GitHub' | Where-Object { $_.Name -like 'API*' } | Remove-ContextSetting

        Remove all settings starting with 'API' from the 'GitHub' context using pipeline input.
    #>

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

        # The context to remove the setting from.
        [Parameter(ValueFromPipelineByPropertyName)]
        [Alias('ContextName')]
        [string] $Context
    )

    $null = Get-ContextVault

    $contextObj = Get-Context -Name $Context

    if ($PSCmdlet.ShouldProcess('Target', "Remove value [$Name] from context [$($contextObj.Name)]")) {
        Set-ContextSetting -Name $Name -Value $null -Context $($contextObj.Name)
    }
}

Write-Verbose "[$scriptName] - [functions] - [public] - [ContextSettings] - [Remove-ContextSetting] - Done"
#endregion - From [functions] - [public] - [ContextSettings] - [Remove-ContextSetting]
#region - From [functions] - [public] - [ContextSettings] - [Set-ContextSetting]
Write-Verbose "[$scriptName] - [functions] - [public] - [ContextSettings] - [Set-ContextSetting] - Importing"

#Requires -Modules Microsoft.PowerShell.SecretManagement

function Set-ContextSetting {
    <#
        .SYNOPSIS
        Sets a setting in a context.

        .DESCRIPTION
        Sets a setting in the specified context.
        To store a secret, use the name 'Secret'.

        .EXAMPLE
        Set-ContextSetting -Name 'ApiBaseUri' -Value 'https://api.github.com' -Context 'GitHub'

        Sets a setting called 'ApiBaseUri' in the context called 'GitHub'.

        .EXAMPLE
        $secret = 'myAccessToken' | ConvertTo-SecureString -AsPlainText -Force
        Set-ContextSetting -Name 'Secret' -Value $secret -Context 'GitHub'

        Sets a secret in the configuration context called 'GitHub'.
    #>

    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess)]
    param (
        # The name of the setting to set.
        [Parameter(Mandatory)]
        [string] $Name,

        # The value to set for the specified setting. This can be a plain text string or a secure string.
        [Parameter(Mandatory)]
        [AllowNull()]
        [AllowEmptyString()]
        [object] $Value,

        # The name of the context where the setting will be set.
        [Parameter(Mandatory)]
        [Alias('ContextName')]
        [string] $Context
    )

    $contextVault = Get-ContextVault

    $contextObj = Get-Context -Name $Context

    if ($PSCmdlet.ShouldProcess($Name, "Set value [$Value]")) {
        Write-Verbose "Setting [$Name] to [$Value] in [$($contextObj.Name)]"
        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 [$Name] in context [$($contextObj.Name)]"
                    Set-Secret -Name $contextObj.Name -SecureStringSecret $Value -Vault $contextVault.Name
                } else {
                    Write-Verbose "Value is $($Value.GetType().FullName), setting [$Name] in context [$($contextObj.Name)]"
                    Set-Secret -Name $contextObj.Name -Value $Value -Vault $contextVault.Name
                }
                break
            }
            'Name' {
                if ([string]::IsNullOrEmpty($Value)) {
                    Write-Error $_
                    throw 'Name cannot be null or empty'
                }
                Set-Secret -Name $Value -SecureStringSecret $secretValue -Vault $contextObj.Name -Metadata $secretInfo.Metadata
                $newSecretInfo = Get-SecretInfo -Name $Value -Vault $contextObj.Name
                if ($newSecretInfo) {
                    Remove-Secret -Name $Name -Vault $contextObj.Name
                } else {
                    Remove-Secret -Name $Value -Vault $contextObj.Name
                }
                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 context [$($contextObj.Name)] in vault [$($contextVault.Name)]"
                Set-SecretInfo -Name $Context -Metadata $metadata -Vault $contextVault.Name
            }
        }
    }
}

Write-Verbose "[$scriptName] - [functions] - [public] - [ContextSettings] - [Set-ContextSetting] - Done"
#endregion - From [functions] - [public] - [ContextSettings] - [Set-ContextSetting]

Write-Verbose "[$scriptName] - [functions] - [public] - [ContextSettings] - Done"
#endregion - From [functions] - [public] - [ContextSettings]


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 = [pscustomobject]@{
    Name    = 'PSModule.Store'                         # $script:Config.Name
    Context = [pscustomobject]@{
        VaultName = 'SecretStore'                      # $script:Config.Context.VaultName
        VaultType = 'Microsoft.PowerShell.SecretStore' # $script:Config.Context.VaultType
    }
}

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 {
    $initContextParams = @{
        Name = (Get-ContextSetting -Name VaultName -Context $script:Config.Name) ?? $script:Config.Context.VaultName
        Type = (Get-ContextSetting -Name VaultType -Context $script:Config.Name) ?? $script:Config.Context.VaultType
    }
    $vault = Initialize-ContextVault @initContextParams
    $script:Config.Context.VaultName = $vault.Name
    $script:Config.Context.VaultType = $vault.ModuleName
} catch {
    Write-Error $_
    throw "Failed to initialize secret vault"
}

Write-Verbose "Initialized secret vault [$($script:Config.Context.VaultName)] of type [$($script:Config.Context.VaultType)]"


### This is the context config for this module
$contextParams = @{
    Name = $script:Config.Name
}
try {
    Set-Context @contextParams
} catch {
    Write-Error $_
    throw 'Failed to initialize secret vault'
}
Write-Verbose "[$scriptName] - [loader] - Done"
#endregion - From [loader]


$exports = @{
    Alias    = '*'
    Cmdlet   = ''
    Function = @(
        'Get-Context'
        'Remove-Context'
        'Set-Context'
        'Get-ContextSetting'
        'Remove-ContextSetting'
        'Set-ContextSetting'
    )
}
Export-ModuleMember @exports