Public/Certificates/Add-JIMCertificate.ps1

# Copyright (c) Tetron Limited. All rights reserved.
# Licensed under the Tetron Commercial License. See LICENSE file in the project root.

function Add-JIMCertificate {
    <#
    .SYNOPSIS
        Adds a certificate to JIM's trusted certificate store.
 
    .DESCRIPTION
        Adds a certificate from a file path, from raw certificate data, or straight from the chain JIM's identity
        provider presents. Both PEM and DER formats are supported.
 
        A certificate is trusted for Connectors (the servers Connected Systems connect to), for sign-in (the
        identity provider), or both. Each purpose only ever uses the certificates trusted for it.
 
        -FromIdentityProvider trusts a certificate the identity provider presents, for sign-in. JIM connects to the
        identity provider again and trusts only a certificate it is presenting now. Name it with -Thumbprint, having
        checked the thumbprint against the one your certificate authority publishes. Without -Thumbprint, the cmdlet
        shows the certificate JIM suggests (the highest one presented) and asks before trusting it. Sign-in works
        straight away; nothing needs restarting. Before anyone can sign in, connect with the infrastructure API key.
 
    .PARAMETER Name
        The name the certificate is shown by in the store. Optional with -FromIdentityProvider, where the
        certificate's common name is used.
 
    .PARAMETER Path
        Path to a certificate file (PEM or DER format). The path should be
        accessible from the JIM server (e.g., in the connector-files volume).
 
    .PARAMETER CertificateData
        Raw certificate data as a byte array.
 
    .PARAMETER CertificateBase64
        Certificate data as a Base64-encoded string.
 
    .PARAMETER FromIdentityProvider
        Trusts a certificate JIM's identity provider presents now, for sign-in.
 
    .PARAMETER Thumbprint
        With -FromIdentityProvider, the thumbprint of the certificate to trust, as Get-JIMIdentityProviderTrust shows
        it. Spaces and colons are ignored. Without it, the cmdlet suggests one and asks first.
 
    .PARAMETER TrustedFor
        What the certificate is trusted for: Connectors, SignIn, or both. Connectors when not given.
 
    .PARAMETER Notes
        Optional notes about the certificate.
 
    .PARAMETER ChangeReason
        Optional reason for the change, recorded on the audit Activity and shown in the certificate's
        Configuration Change History.
 
    .PARAMETER Force
        With -FromIdentityProvider and no -Thumbprint, trusts the suggested certificate without asking.
 
    .PARAMETER PassThru
        Returns the certificate added, or with -FromIdentityProvider the outcome.
 
    .OUTPUTS
        With -PassThru, the certificate added, or with -FromIdentityProvider the outcome, the certificate as it now
        sits in the store, and the identity provider's status afterwards; otherwise nothing.
 
    .EXAMPLE
        Add-JIMCertificate -Name "LDAP CA" -Path "/connector-files/ldap-ca.pem" -PassThru
 
        Adds a certificate from a file path.
 
    .EXAMPLE
        $certData = [System.IO.File]::ReadAllBytes("./cert.der")
        Add-JIMCertificate -Name "My Cert" -CertificateData $certData -PassThru
 
        Adds a certificate from raw byte data.
 
    .EXAMPLE
        $base64 = [Convert]::ToBase64String([System.IO.File]::ReadAllBytes("./cert.der"))
        Add-JIMCertificate -Name "My Cert" -CertificateBase64 $base64 -PassThru
 
        Adds a certificate from Base64-encoded data.
 
    .EXAMPLE
        Connect-JIM -Url https://jim.corp.contoso.com -ApiKey $env:JIM_INFRASTRUCTURE_API_KEY
        Add-JIMCertificate -FromIdentityProvider -Thumbprint '8F3A91D06B2E47C2C21D9F0E1A2B3C4D5E6F7A8B' -PassThru
 
        Trusts the identity provider's issuing certificate authority for sign-in, before anyone can sign in. Check the
        thumbprint against the one your certificate authority publishes first.
 
    .EXAMPLE
        Add-JIMCertificate -FromIdentityProvider
 
        Shows the certificate JIM suggests trusting, and asks before trusting it for sign-in.
 
    .EXAMPLE
        $base64 = [Convert]::ToBase64String([System.IO.File]::ReadAllBytes("./contoso-root.cer"))
        Add-JIMCertificate -Name "Contoso Root CA" -CertificateBase64 $base64 -TrustedFor SignIn, Connectors
 
        Trusts an internal root for sign-in and for Connectors alike. A root outlasts every certificate it issues.
 
    .LINK
        Get-JIMIdentityProviderTrust
        Get-JIMCertificate
        Set-JIMCertificate
        Remove-JIMCertificate
        Test-JIMCertificate
    #>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'FromFile')]
    [OutputType('JIM.TrustedCertificateDetailDto', ParameterSetName = ('FromFile', 'FromData', 'FromBase64'))]
    [OutputType('JIM.TrustIdentityProviderCertificateResponse', ParameterSetName = 'FromIdentityProvider')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'FromFile')]
        [Parameter(Mandatory, ParameterSetName = 'FromData')]
        [Parameter(Mandatory, ParameterSetName = 'FromBase64')]
        [Parameter(ParameterSetName = 'FromIdentityProvider')]
        [string]$Name,

        [Parameter(Mandatory, ParameterSetName = 'FromFile')]
        [string]$Path,

        [Parameter(Mandatory, ParameterSetName = 'FromData')]
        [byte[]]$CertificateData,

        [Parameter(Mandatory, ParameterSetName = 'FromBase64')]
        [string]$CertificateBase64,

        [Parameter(Mandatory, ParameterSetName = 'FromIdentityProvider')]
        [switch]$FromIdentityProvider,

        [Parameter(ParameterSetName = 'FromIdentityProvider')]
        [ValidateNotNullOrEmpty()]
        [string]$Thumbprint,

        [Parameter(ParameterSetName = 'FromFile')]
        [Parameter(ParameterSetName = 'FromData')]
        [Parameter(ParameterSetName = 'FromBase64')]
        [ValidateSet('Connectors', 'SignIn')]
        [string[]]$TrustedFor,

        [Parameter(ParameterSetName = 'FromFile')]
        [Parameter(ParameterSetName = 'FromData')]
        [Parameter(ParameterSetName = 'FromBase64')]
        [string]$Notes,

        [ValidateNotNullOrEmpty()]
        [string]$ChangeReason,

        [Parameter(ParameterSetName = 'FromIdentityProvider')]
        [switch]$Force,

        [switch]$PassThru
    )

    process {
        # Check connection first
        if (-not $script:JIMConnection) {
            Write-Error "You are not connected to JIM. Run Connect-JIM -Url <your JIM URL> to authenticate, then try again."
            return
        }

        if ($PSCmdlet.ParameterSetName -eq 'FromIdentityProvider') {
            $chosen = $Thumbprint
            $description = $Thumbprint

            if (-not $chosen) {
                try {
                    $status = Invoke-JIMApi -Endpoint '/api/v1/auth/identity-provider-trust'
                }
                catch {
                    Write-Error "Failed to read the identity provider's trust status: $_"
                    return
                }

                $suggested = @($status.presentedChain) | Where-Object { $_.thumbprint -eq $status.suggestedThumbprint } | Select-Object -First 1
                if ($status.state -ne 'Untrusted' -or -not $suggested) {
                    $reason = if ($status.state -eq 'Trusted') { "JIM already trusts the identity provider." } else { $status.message }
                    Write-Error "There is no certificate to trust from the identity provider. $reason"
                    return
                }

                $chosen = $suggested.thumbprint
                $description = "'$($suggested.commonName)' ($chosen)"

                if (-not $PSCmdlet.ShouldProcess($description, "Trust for sign-in")) {
                    return
                }

                if (-not $Force) {
                    $role = if ($suggested.isServerCertificate) { "the identity provider's own certificate" } else { 'certificate authority' }
                    $validTo = ([datetime]$suggested.validTo).ToString('dd MMM yyyy', [System.Globalization.CultureInfo]::InvariantCulture)
                    $query = @(
                        "Identity provider: $($status.host)"
                        "Certificate: $($suggested.commonName) ($role)"
                        "Thumbprint: $chosen"
                        "Valid until: $validTo"
                        ''
                        'Check the thumbprint against the one your certificate authority publishes. Trust this certificate for sign-in?'
                    ) -join [Environment]::NewLine

                    if (-not (Request-JIMConfirmation -Cmdlet $PSCmdlet -Query $query -Caption 'Trust a certificate the identity provider presents')) {
                        return
                    }
                }
            }
            elseif (-not $PSCmdlet.ShouldProcess($description, "Trust for sign-in")) {
                return
            }

            Write-Verbose "Trusting the certificate the identity provider presents for sign-in: $description"
            try {
                $body = @{ thumbprint = $chosen }
                if ($Name) { $body.name = $Name }
                if ($ChangeReason) { $body.changeReason = $ChangeReason }

                $response = Invoke-JIMApi -Endpoint '/api/v1/certificates/identity-provider' -Method 'POST' -Body $body
                Write-Verbose "Outcome: $($response.outcome). $($response.message)"

                if ($PassThru) {
                    $response
                }
            }
            catch {
                Write-Error "Failed to trust the identity provider's certificate: $_"
            }
            return
        }

        if ($PSCmdlet.ShouldProcess($Name, "Add Certificate")) {
            Write-Verbose "Adding certificate: $Name"

            try {
                $body = @{ name = $Name }
                $endpoint = '/api/v1/certificates/upload'

                switch ($PSCmdlet.ParameterSetName) {
                    'FromFile' {
                        $body.filePath = $Path
                        $endpoint = '/api/v1/certificates/file'
                    }
                    'FromData' { $body.certificateDataBase64 = [Convert]::ToBase64String($CertificateData) }
                    'FromBase64' { $body.certificateDataBase64 = $CertificateBase64 }
                }

                if ($Notes) { $body.notes = $Notes }
                if ($ChangeReason) { $body.changeReason = $ChangeReason }
                if ($TrustedFor) { $body.trustedFor = ($TrustedFor | Select-Object -Unique) -join ', ' }

                $response = Invoke-JIMApi -Endpoint $endpoint -Method 'POST' -Body $body

                Write-Verbose "Added certificate with ID: $($response.id)"

                if ($PassThru) {
                    $response
                }
            }
            catch {
                Write-Error "Failed to add certificate: $_"
            }
        }
    }
}