Providers/FreeIPA/Private/Export-FreeIPACredential.ps1

function Export-FreeIPACredential {
    <#
    .SYNOPSIS
        Writes the service account credential record, with the password protected

    .DESCRIPTION
        The record names the server, the account, the certificate authority the connection
        pinned, and where the password is. The password itself goes to one of two places, and
        the record is the authority on which: into a SecretStore vault under -UseSecretStore,
        which is encrypted and portable, or into the record DPAPI-protected, which is
        encrypted on Windows only. A record written before the vault was proven usable would
        name a secret that was never stored, so the vault is initialised first and the record
        last.

        The CA certificate is kept in the record as PEM so that a later
        Connect-FreeIPAEnvironment -ServiceAccount trusts the same authority without being
        told again. It is a public certificate and needs no protection.

        Written as UTF-8 bytes rather than through Set-Content, which on Windows PowerShell
        prepends a byte order mark that a strict JSON reader rejects. The folder is restricted
        to the current user before the file exists in it, and the file again afterwards.

    .PARAMETER Path
        Where to write the record.

    .PARAMETER BaseUrl
        The server the account belongs to.

    .PARAMETER Username
        The service account's login.

    .PARAMETER Password
        The password to store.

    .PARAMETER CaCertificate
        The PEM of the pinned certificate authority, if any.

    .PARAMETER UseSecretStore
        Keep the password in a SecretStore vault rather than in the record.

    .PARAMETER VaultName
        The vault to use with -UseSecretStore.

    .PARAMETER VaultPassword
        The vault's password, when it is not the module default.

    .OUTPUTS
        PSCustomObject with Path, Protection, VaultName and SecretName.

    .EXAMPLE
        PS> Export-FreeIPACredential -Path $path -BaseUrl $url -Username $name -Password $password -Confirm:$false

        DESCRIPTION: Writes the record with the password DPAPI-protected
        OUTPUT: Path and Protection 'DPAPI'
        USE CASE: The end of New-FreeIPAServiceApp, and a rotation on connect

    .NOTES
        Author: Jeffrey Stuhr
        Blog: https://www.techbyjeff.net
        LinkedIn: https://www.linkedin.com/in/jeffrey-stuhr-034214aa/
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', 'Password',
        Justification = 'The password is in memory as text from the API or a rotation and is protected here before it touches disk.')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUsernameAndPasswordParams', '',
        Justification = 'The record stores exactly a login and its password; a PSCredential here would be unwrapped on the next line.')]
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Path,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$BaseUrl,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Username,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Password,

        [Parameter()]
        [string]$CaCertificate,

        [Parameter()]
        [switch]$UseSecretStore,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$VaultName = 'FreeIPAEnvironment',

        [Parameter()]
        [System.Security.SecureString]$VaultPassword
    )

    if (-not $PSCmdlet.ShouldProcess($Path, 'Write the service account credential record')) {
        return $null
    }

    $folder = Split-Path -Path $Path -Parent
    if ($folder -and -not (Test-Path -LiteralPath $folder)) {
        $null = New-Item -ItemType Directory -Path $folder -Force
        $null = Protect-TestFile -Path $folder -Confirm:$false
    }

    $payload = [ordered]@{
        schemaVersion = 1
        provider      = 'FreeIPA'
        baseUrl       = $BaseUrl
        username      = $Username
        createdUtc    = [DateTime]::UtcNow.ToString('o')
    }
    if (-not [string]::IsNullOrWhiteSpace($CaCertificate)) { $payload['caCertificate'] = $CaCertificate }

    $secretName = $null
    if ($UseSecretStore) {
        $secretName = 'FreeIPAEnvironment-{0}-{1}' -f ([uri]$BaseUrl).Host, $Username
        $vault = Initialize-TestSecretVault -VaultName $VaultName -VaultPassword $VaultPassword -Install
        if (-not $vault -or -not $vault.Available) {
            throw "Vault '$VaultName' is not usable, so the password was not stored."
        }
        Set-TestVaultSecret -VaultName $VaultName -SecretName $secretName -PlainText $Password
        $payload['protection'] = 'SecretStore'
        $payload['vaultName'] = $VaultName
        $payload['secretName'] = $secretName
    }
    else {
        $protected = Protect-TestSecret -PlainText $Password
        $payload['protection'] = $protected.Method
        $payload['passwordProtected'] = $protected.Value
    }

    $json = $payload | ConvertTo-Json -Depth 5
    [System.IO.File]::WriteAllBytes($Path, [System.Text.Encoding]::UTF8.GetBytes($json))
    $null = Protect-TestFile -Path $Path -Confirm:$false

    return [PSCustomObject]@{
        Path       = $Path
        Protection = $payload['protection']
        VaultName  = $(if ($UseSecretStore) { $VaultName } else { $null })
        SecretName = $secretName
    }
}