From 3754de74f6c819de7da809daaf07f05a31fdd4ba Mon Sep 17 00:00:00 2001 From: GraceSolutions Date: Thu, 4 Jun 2026 17:11:56 -0400 Subject: [PATCH] Treat profile pending-approval as warning instead of throw Issuance via Request-InfisicalCertificate -CertificateProfileId no longer throws when the API responds without a certificate body (e.g. status pending_approval / pending_validation). InfisicalPkiClient.IssueCertificateByProfile now logs a warning and returns an InfisicalSignedCertificate populated only with Status, StatusMessage, and CertificateRequestId. New Status, StatusMessage, CertificateRequestId properties on InfisicalSignedCertificate and InfisicalCertificateResult propagate the lifecycle state. The cmdlet short-circuits when CertificatePem is empty: it skips key build, install, chain install, and private-key write, scrubs PrivateKeyPem, and emits a status-only result so callers can poll or trigger approval. Whether issuance is immediate is dictated by the certificate policy bound to the profile. --- CHANGELOG.md | 2 +- .../Cmdlets/RequestInfisicalCertificateCmdlet.cs | 9 +++++++++ .../Models/InfisicalCertificateResult.cs | 3 +++ .../Models/InfisicalSignedCertificate.cs | 3 +++ .../Pki/InfisicalCertificateRequestHelpers.cs | 3 +++ src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs | 14 ++++++++++++-- 6 files changed, 31 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dee5a69..b41a5e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos - `Get-InfisicalCertificateProfile` added with `List` (default) and `ById` parameter sets. List binds to `GET /api/v1/cert-manager/certificate-profiles` (optional `-Limit`, `-Offset`, `-IncludeConfigs`); ById binds to `GET /api/v1/cert-manager/certificate-profiles/{certificateProfileId}`. New `InfisicalCertificateProfile` model surfaces ca/policy ids, slug, enrollment type, per-profile defaults (ttl, key/extended key usages), and the embedded CA/policy/apiConfig summaries. - `Get-InfisicalCertificatePolicy` added with `List` (default) and `ById` parameter sets. List binds to `GET /api/v1/cert-manager/certificate-policies` (optional `-Limit`, `-Offset`); ById binds to `GET /api/v1/cert-manager/certificate-policies/{certificatePolicyId}`. New `InfisicalCertificatePolicy` model surfaces subject, SANs, key usages, extended key usages, algorithms, and validity. Polymorphic string-or-array fields (`allowed`, `required`, `keyAlgorithm`) are normalized to arrays; `sans` is normalized whether the API returns an object or an array. - `Get-InfisicalCertificateAuthority` gains a `-Kind` parameter on the List parameter set with values `Internal` (default, preserves prior behavior against `/api/v1/cert-manager/ca/internal`), `Any` (binds to the generic `/api/v1/cert-manager/ca` endpoint which returns both internal and ACME CAs), and `Acme` (uses the generic endpoint and client-side filters to ACME issuers only). ById retrieval is unchanged and still resolves against the internal CA endpoint. -- `Request-InfisicalCertificate` gains a `ByProfile` parameter set bound by the new `-CertificateProfileId` parameter (alias `ProfileId`). The cmdlet generates a local keypair and CSR as usual, then POSTs to `/api/v1/cert-manager/certificates` with the profile id, the CSR, and a subject/attribute envelope (commonName, organization, organizationalUnit, country, state, locality, ttl, notBefore, notAfter, keyUsages, extendedKeyUsages). The wrapped response (`{certificate:{certificate,certificateChain,issuingCaCertificate,serialNumber,certificateId,privateKey}, certificateRequestId, status, message}`) is unwrapped into the existing `InfisicalSignedCertificate` shape so the install / reuse / chain-completion paths continue to work unchanged. Issuance that returns without a certificate (e.g. status `pending_approval`) raises a configuration exception that surfaces the reported status and message. +- `Request-InfisicalCertificate` gains a `ByProfile` parameter set bound by the new `-CertificateProfileId` parameter (alias `ProfileId`). The cmdlet generates a local keypair and CSR as usual, then POSTs to `/api/v1/cert-manager/certificates` with the profile id, the CSR, and a subject/attribute envelope (commonName, organization, organizationalUnit, country, state, locality, ttl, notBefore, notAfter, keyUsages, extendedKeyUsages). The wrapped response (`{certificate:{certificate,certificateChain,issuingCaCertificate,serialNumber,certificateId,privateKey}, certificateRequestId, status, message}`) is unwrapped into the existing `InfisicalSignedCertificate` shape so the install / reuse / chain-completion paths continue to work unchanged. Issuance that returns without a certificate body (e.g. status `pending_approval` or `pending_validation`) is logged as a warning and the cmdlet emits a status-only `InfisicalCertificateResult` (new `Status`, `StatusMessage`, `CertificateRequestId` properties) instead of throwing; install / chain / private-key-write steps are skipped in that case. Whether issuance is immediate or pending is dictated by the certificate policy bound to the profile (auto-approve vs. manual review and any required validation). ## 2026.06.04.1920 diff --git a/src/PSInfisicalAPI/Cmdlets/RequestInfisicalCertificateCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/RequestInfisicalCertificateCmdlet.cs index 7f0ac2e..58eed3b 100644 --- a/src/PSInfisicalAPI/Cmdlets/RequestInfisicalCertificateCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/RequestInfisicalCertificateCmdlet.cs @@ -121,6 +121,15 @@ namespace PSInfisicalAPI.Cmdlets InfisicalSignedCertificate signed = SignCertificate(client, connection, resolvedProjectId, csr.CsrPem); signed.PrivateKeyPem = csr.PrivateKeyPem; + if (string.IsNullOrEmpty(signed.CertificatePem)) + { + Logger.Warning(Component, string.Concat("Issuance returned without a certificate (status='", signed.Status ?? "unknown", "'", string.IsNullOrEmpty(signed.StatusMessage) ? "" : string.Concat(", message='", signed.StatusMessage, "'"), string.IsNullOrEmpty(signed.CertificateRequestId) ? "" : string.Concat(", certificateRequestId='", signed.CertificateRequestId, "'"), "). Install / chain / key-write steps are skipped; emitting status-only result.")); + InfisicalCertificateResult pending = InfisicalCertificateRequestHelpers.BuildResult(null, signed); + pending.PrivateKeyPem = null; + WriteObject(pending); + return; + } + X509KeyStorageFlags resolvedFlags = ResolveEffectiveKeyStorageFlags(); X509Certificate2 cert = PemCertificateBuilder.Build(signed.CertificatePem, signed.PrivateKeyPem, signed.CertificateChainPem, resolvedFlags); diff --git a/src/PSInfisicalAPI/Models/InfisicalCertificateResult.cs b/src/PSInfisicalAPI/Models/InfisicalCertificateResult.cs index 38dd62a..7dc5404 100644 --- a/src/PSInfisicalAPI/Models/InfisicalCertificateResult.cs +++ b/src/PSInfisicalAPI/Models/InfisicalCertificateResult.cs @@ -13,6 +13,9 @@ namespace PSInfisicalAPI.Models public string CertificatePem { get; set; } public string CertificateChainPem { get; set; } public string PrivateKeyPem { get; set; } + public string Status { get; set; } + public string StatusMessage { get; set; } + public string CertificateRequestId { get; set; } public override string ToString() { diff --git a/src/PSInfisicalAPI/Models/InfisicalSignedCertificate.cs b/src/PSInfisicalAPI/Models/InfisicalSignedCertificate.cs index e40d62f..24b1a91 100644 --- a/src/PSInfisicalAPI/Models/InfisicalSignedCertificate.cs +++ b/src/PSInfisicalAPI/Models/InfisicalSignedCertificate.cs @@ -7,6 +7,9 @@ namespace PSInfisicalAPI.Models public string CertificateChainPem { get; set; } public string IssuingCaCertificatePem { get; set; } public string PrivateKeyPem { get; set; } + public string Status { get; set; } + public string StatusMessage { get; set; } + public string CertificateRequestId { get; set; } public override string ToString() { diff --git a/src/PSInfisicalAPI/Pki/InfisicalCertificateRequestHelpers.cs b/src/PSInfisicalAPI/Pki/InfisicalCertificateRequestHelpers.cs index 15c296b..4395c75 100644 --- a/src/PSInfisicalAPI/Pki/InfisicalCertificateRequestHelpers.cs +++ b/src/PSInfisicalAPI/Pki/InfisicalCertificateRequestHelpers.cs @@ -220,6 +220,9 @@ namespace PSInfisicalAPI.Pki result.CertificatePem = signed.CertificatePem; result.CertificateChainPem = signed.CertificateChainPem; result.PrivateKeyPem = signed.PrivateKeyPem; + result.Status = signed.Status; + result.StatusMessage = signed.StatusMessage; + result.CertificateRequestId = signed.CertificateRequestId; } List chainCerts = signed != null ? CollectChainCertificates(signed) : new List(); diff --git a/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs b/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs index 03f76e7..e3e06b6 100644 --- a/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs +++ b/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs @@ -340,7 +340,14 @@ namespace PSInfisicalAPI.Pki { string status = dto != null ? dto.Status : "unknown"; string message = dto != null ? dto.Message : null; - throw new InfisicalConfigurationException(string.Concat("Certificate was not issued (status='", status ?? "unknown", "'", string.IsNullOrEmpty(message) ? "" : string.Concat(", message='", message, "'"), "). The certificate profile may require manual approval or additional validation.")); + string requestId = dto != null ? dto.CertificateRequestId : null; + _logger.Warning(Component, string.Concat("Profile issuance did not return a certificate (status='", status ?? "unknown", "'", string.IsNullOrEmpty(message) ? "" : string.Concat(", message='", message, "'"), string.IsNullOrEmpty(requestId) ? "" : string.Concat(", certificateRequestId='", requestId, "'"), "). The profile may require manual approval or additional validation; returning a status-only result.")); + return new InfisicalSignedCertificate + { + Status = status, + StatusMessage = message, + CertificateRequestId = requestId + }; } InfisicalSignedCertificate signed = new InfisicalSignedCertificate @@ -348,7 +355,10 @@ namespace PSInfisicalAPI.Pki SerialNumber = dto.Certificate.SerialNumber, CertificatePem = dto.Certificate.Certificate, CertificateChainPem = dto.Certificate.CertificateChain, - IssuingCaCertificatePem = dto.Certificate.IssuingCaCertificate + IssuingCaCertificatePem = dto.Certificate.IssuingCaCertificate, + Status = dto.Status, + StatusMessage = dto.Message, + CertificateRequestId = dto.CertificateRequestId }; _logger.Information(Component, "Infisical certificate issuance (profile) was successful."); return signed;