From 30151384845f5cc664f39accc36f93677afa5b8e Mon Sep 17 00:00:00 2001 From: gsadmin Date: Fri, 31 Jul 2026 22:02:24 -0400 Subject: [PATCH] Make the seeding script survive real API responses and emit complete policies Ran the script against a live Infisical instance for the first time. It failed at four separate points, each fixed here. Responses are read through Get-ApiProperty/Get-ApiCollection/Get-ApiObject rather than directly. Set-StrictMode makes an absent property terminating, and the cert-manager routes return bare payloads - a naked array for a list, the resource itself for a create - where the secrets and pki routes wrap theirs in a named property. All eighteen reads now handle either shape. The application-to-profile listing is a join row carrying profileId, not a profile object with an id, so it is read accordingly. Renamed the local $configuration inside the CA loop to $caConfiguration. PowerShell variable names are case-insensitive, so from the second CA onward it shadowed the script-level $Configuration block and every lookup into it failed. Renamed the $profile loop variables for the same reason: $profile is an automatic variable. Policies now carry subject, sans, and signature algorithms, and profiles carry defaults. The previous comment claimed omitting subject and sans left them unconstrained; the opposite is true. Infisical refuses any attribute with no policy entry ("no subject policies defined"), refuses a policy that defines no signature algorithm, and refuses a request missing a usage the policy marks required - Request-InfisicalCertificate sends none of its own, so the profile default has to supply it. The enumeration list in the header dropped domain_component, upn, and any_purpose, which the API does not accept. Verified end to end: a clean run builds both CA hierarchies including the subordinate CSR-sign-import sequence, and a re-run creates nothing. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 4 + Module/PSInfisicalAPI/PSInfisicalAPI.psd1 | 4 +- Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll | Bin 506368 -> 506368 bytes ...ialize-InfisicalCertManagerEnvironment.ps1 | 266 ++++++++++++++---- 4 files changed, 213 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a96f5e9..00211a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos ## Unreleased +## 2026.08.01.0140 + +_Build produced from commit 74d22a941c33._ + ## 2026.08.01.0123 _Build produced from commit f2a1492b66de._ diff --git a/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 b/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 index 1fe7074..81840fb 100644 --- a/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 +++ b/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'PSInfisicalAPI.psm1' - ModuleVersion = '2026.08.01.0123' + ModuleVersion = '2026.08.01.0140' GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51' Author = 'Grace Solutions' CompanyName = 'Grace Solutions' @@ -89,7 +89,7 @@ LicenseUri = 'https://www.gnu.org/licenses/agpl-3.0.html' ProjectUri = 'https://prod.git.gracesolution.info/gsadmin/PSInfisicalAPI' ReleaseNotes = 'See CHANGELOG.md in the project repository for release history.' - CommitHash = 'f2a1492b66de' + CommitHash = '74d22a941c33' } } } \ No newline at end of file diff --git a/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll b/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll index b5a4796c659d7c126a4b0c82cdd262bbae33bb01..6f2e6dc93070ae8b306a3202587cc76b330f8cc2 100644 GIT binary patch delta 343 zcmZqpBG>RmZbAo(Nfg8L#-7$5#?~ID)*j}qJuGz(7<<~eAF^!ce#rXtr$FF>pvZ}d zlRcMw?syUTYH#hf+iV+SdCW~xjEoX3O$?Kbji>LmU^ikiF_`|*f?ZVG#6TNa(!?~` zIK|Aw)I254FxlMFFwM}=%+Soh&^*n+bi1J?`|5I*o$GYYOn+cWMc1O;9z*g-m`t` z7WQaHW)lX3?Vq=@yEBFYSq$0?<_snbDGWwHn8;uWWE(OhGZ+J*341Afxpd3??fwX>DiR!Tz5S070^GS^xk5 delta 343 zcmZqpBG>RmZbAo(_4(728hcuM7+ZUoT6>tc_OR4FV61NEe#o+&`yuPop8`TQhc{X> z3;V6DpM5a-@ZV?KZnJHS{31p=K z`DqO1K+>4O0;oC_D4GVO4T17WKrsWL{$!v`%Jly2?2hapqxBdJCMz;&ZD-uU{+|&5 D8Rl=l diff --git a/Tools/Initialize-InfisicalCertManagerEnvironment.ps1 b/Tools/Initialize-InfisicalCertManagerEnvironment.ps1 index e26ae7f..e1c2d40 100644 --- a/Tools/Initialize-InfisicalCertManagerEnvironment.ps1 +++ b/Tools/Initialize-InfisicalCertManagerEnvironment.ps1 @@ -116,13 +116,15 @@ Set-StrictMode -Version Latest Enumeration values are the ones Infisical accepts verbatim: KeyAlgorithm RSA_2048 | RSA_3072 | RSA_4096 | EC_prime256v1 | EC_secp384r1 | EC_secp521r1 + SignatureAlgorithm RSA-SHA256 | RSA-SHA384 | RSA-SHA512 | ECDSA-SHA256 | ECDSA-SHA384 | + ECDSA-SHA512 KeyUsages digital_signature | key_encipherment | non_repudiation | data_encipherment | key_agreement | key_cert_sign | crl_sign | encipher_only | decipher_only - ExtendedKeyUsages server_auth | client_auth | code_signing | email_protection | ocsp_signing | - time_stamping | any_purpose + ExtendedKeyUsages client_auth | server_auth | code_signing | email_protection | ocsp_signing | + time_stamping SubjectAttribute common_name | organization | country | state | locality | - organizational_unit | domain_component - SanType dns_name | ip_address | email | uri | upn + organizational_unit + SanType dns_name | ip_address | email | uri #> $Configuration = [Ordered]@{ @@ -197,46 +199,80 @@ Set-StrictMode -Version Latest CertificatePolicies = @( <# - Subject and SubjectAltNames are deliberately left out below. A constraint is only sent when - it carries values, and omitting these leaves the common name and SAN list unconstrained, - which is what fleet enrollment needs: every machine presents its own name. + Every attribute a request carries must have a policy entry, or issuance is refused with + "not allowed by template policy (no subject policies defined)". An entry of Allowed = @('*') + is therefore not decoration: it is what keeps the common name and SAN list open, which is + what fleet enrollment needs, since every machine presents its own name. Narrow a value by + replacing the wildcard, for example Allowed = @('*.contoso.com'). - To constrain them, add entries carrying values, for example: - Subject = @( [Ordered]@{ Type = 'organization'; Allowed = @('Contoso') } ) - SubjectAltNames = @( [Ordered]@{ Type = 'dns_name'; Allowed = @('*.contoso.com') } ) + SignatureAlgorithms is likewise mandatory. A policy that omits it rejects every request with + "Signature algorithm ... not defined in template". #> [Ordered]@{ - Name = 'mecm-server-client-auth' - Description = 'SCCM/MECM site systems and clients. RSA, server and client authentication.' - MaxValidity = '365d' - KeyAlgorithms = @('RSA_2048', 'RSA_3072', 'RSA_4096') - KeyUsages = [Ordered]@{ Required = @('digital_signature', 'key_encipherment') } - ExtendedKeyUsages = [Ordered]@{ Required = @('server_auth', 'client_auth') } - Subject = @() - SubjectAltNames = @() + Name = 'mecm-server-client-auth' + Description = 'SCCM/MECM site systems and clients. RSA, server and client authentication.' + MaxValidity = '365d' + KeyAlgorithms = @('RSA_2048', 'RSA_3072', 'RSA_4096') + SignatureAlgorithms = @('RSA-SHA256', 'RSA-SHA384', 'RSA-SHA512') + KeyUsages = [Ordered]@{ Required = @('digital_signature', 'key_encipherment') } + ExtendedKeyUsages = [Ordered]@{ Required = @('server_auth', 'client_auth') } + Subject = @( + [Ordered]@{ Type = 'common_name'; Allowed = @('*') } + [Ordered]@{ Type = 'organization'; Allowed = @('*') } + [Ordered]@{ Type = 'organizational_unit'; Allowed = @('*') } + [Ordered]@{ Type = 'country'; Allowed = @('*') } + [Ordered]@{ Type = 'state'; Allowed = @('*') } + [Ordered]@{ Type = 'locality'; Allowed = @('*') } + ) + SubjectAltNames = @( + [Ordered]@{ Type = 'dns_name'; Allowed = @('*') } + [Ordered]@{ Type = 'ip_address'; Allowed = @('*') } + ) } [Ordered]@{ - Name = 'ec-server-client-auth' - Description = 'General workload certificates. ECDSA P-384, server and client authentication.' - MaxValidity = '90d' - KeyAlgorithms = @('EC_secp384r1') - KeyUsages = [Ordered]@{ Required = @('digital_signature') } - ExtendedKeyUsages = [Ordered]@{ Required = @('server_auth', 'client_auth') } - Subject = @() - SubjectAltNames = @() + Name = 'ec-server-client-auth' + Description = 'General workload certificates. ECDSA P-384, server and client authentication.' + MaxValidity = '90d' + KeyAlgorithms = @('EC_secp384r1') + SignatureAlgorithms = @('ECDSA-SHA384') + KeyUsages = [Ordered]@{ Required = @('digital_signature') } + ExtendedKeyUsages = [Ordered]@{ Required = @('server_auth', 'client_auth') } + Subject = @( + [Ordered]@{ Type = 'common_name'; Allowed = @('*') } + [Ordered]@{ Type = 'organization'; Allowed = @('*') } + [Ordered]@{ Type = 'organizational_unit'; Allowed = @('*') } + [Ordered]@{ Type = 'country'; Allowed = @('*') } + [Ordered]@{ Type = 'state'; Allowed = @('*') } + [Ordered]@{ Type = 'locality'; Allowed = @('*') } + ) + SubjectAltNames = @( + [Ordered]@{ Type = 'dns_name'; Allowed = @('*') } + [Ordered]@{ Type = 'ip_address'; Allowed = @('*') } + ) } [Ordered]@{ - Name = 'ec-code-signing' - Description = 'Code signing. ECDSA P-384, code signing only.' - MaxValidity = '365d' - KeyAlgorithms = @('EC_secp384r1') - KeyUsages = [Ordered]@{ Required = @('digital_signature') } - ExtendedKeyUsages = [Ordered]@{ Required = @('code_signing'); Denied = @('server_auth', 'client_auth') } - Subject = @() - SubjectAltNames = @() + Name = 'ec-code-signing' + Description = 'Code signing. ECDSA P-384, code signing only.' + MaxValidity = '365d' + KeyAlgorithms = @('EC_secp384r1') + SignatureAlgorithms = @('ECDSA-SHA384') + KeyUsages = [Ordered]@{ Required = @('digital_signature') } + ExtendedKeyUsages = [Ordered]@{ Required = @('code_signing'); Denied = @('server_auth', 'client_auth') } + Subject = @( + [Ordered]@{ Type = 'common_name'; Allowed = @('*') } + [Ordered]@{ Type = 'organization'; Allowed = @('*') } + [Ordered]@{ Type = 'organizational_unit'; Allowed = @('*') } + [Ordered]@{ Type = 'country'; Allowed = @('*') } + ) + SubjectAltNames = @() } ) + <# + Defaults fill in what a requester leaves out. They are required whenever the policy marks a + constraint Required: Request-InfisicalCertificate does not send key usages of its own, so + without a default the request arrives empty and is refused with "Missing required key usages". + #> CertificateProfiles = @( [Ordered]@{ Slug = 'mecm-server-client-auth' @@ -245,6 +281,12 @@ Set-StrictMode -Version Latest CertificatePolicy = 'mecm-server-client-auth' AutoRenew = $True RenewBeforeDays = 14 + Defaults = [Ordered]@{ + KeyAlgorithm = 'RSA_2048' + SignatureAlgorithm = 'RSA-SHA256' + KeyUsages = @('digital_signature', 'key_encipherment') + ExtendedKeyUsages = @('server_auth', 'client_auth') + } } [Ordered]@{ Slug = 'ec-server-client-auth' @@ -253,6 +295,12 @@ Set-StrictMode -Version Latest CertificatePolicy = 'ec-server-client-auth' AutoRenew = $True RenewBeforeDays = 14 + Defaults = [Ordered]@{ + KeyAlgorithm = 'EC_secp384r1' + SignatureAlgorithm = 'ECDSA-SHA384' + KeyUsages = @('digital_signature') + ExtendedKeyUsages = @('server_auth', 'client_auth') + } } [Ordered]@{ Slug = 'ec-code-signing' @@ -261,6 +309,12 @@ Set-StrictMode -Version Latest CertificatePolicy = 'ec-code-signing' AutoRenew = $False RenewBeforeDays = $Null + Defaults = [Ordered]@{ + KeyAlgorithm = 'EC_secp384r1' + SignatureAlgorithm = 'ECDSA-SHA384' + KeyUsages = @('digital_signature') + ExtendedKeyUsages = @('code_signing') + } } ) } @@ -282,6 +336,62 @@ Set-StrictMode -Version Latest Write-Host ("{0} {1}" -f $prefix, $Message) } + function Get-ApiProperty { + <# + Reads a property that may not be present. Set-StrictMode makes an absent property a terminating + error, and response shapes vary across Infisical versions and route namespaces, so every read of an + API response goes through here rather than risking a crash on a field the server did not send. + #> + param($InputObject, [Parameter(Mandatory = $True)][String]$Name) + + if ($Null -eq $InputObject) { return $Null } + + $property = $InputObject.PSObject.Properties[$Name] + if ($Null -eq $property) { return $Null } + + return $property.Value + } + + function Get-ApiCollection { + <# + Returns a list from a response, as an array. Infisical is not consistent about wrapping: the + secrets and pki routes wrap a list in a named property, while the cert-manager routes return a + bare JSON array. Named properties are tried first, then the response itself if it is already a + list, so one call site handles either shape. + #> + param($InputObject, [Parameter(Mandatory = $True)][String[]]$Name) + + foreach ($candidate in $Name) { + $value = Get-ApiProperty -InputObject $InputObject -Name $candidate + if ($Null -ne $value) { return @($value) } + } + + # A bare array carries no named property to find, so fall through to the response itself. Strings are + # excluded because they are enumerable but are never a list of resources. + if ($Null -ne $InputObject -and $InputObject -isnot [String] -and $InputObject -is [System.Collections.IEnumerable]) { + return @($InputObject) + } + + return @() + } + + function Get-ApiObject { + <# + Returns a single resource from a response, handling the same wrapped-or-bare split as + Get-ApiCollection. An unwrapped response is recognised by carrying an id of its own. + #> + param($InputObject, [Parameter(Mandatory = $True)][String[]]$Name) + + foreach ($candidate in $Name) { + $value = Get-ApiProperty -InputObject $InputObject -Name $candidate + if ($Null -ne $value) { return $value } + } + + if ($Null -ne (Get-ApiProperty -InputObject $InputObject -Name 'id')) { return $InputObject } + + return $Null + } + function ConvertFrom-SecureStringToPlainText { param([System.Security.SecureString]$Value) $pointer = [System.Runtime.InteropServices.Marshal]::SecureStringToGlobalAllocUnicode($Value) @@ -381,11 +491,11 @@ Set-StrictMode -Version Latest } $response = Invoke-InfisicalApi -Method 'POST' -Path '/api/v1/auth/universal-auth/login' -Body $body -NoAuth - if ($Null -eq $response -or [String]::IsNullOrWhiteSpace($response.accessToken)) { + if ($Null -eq $response -or [String]::IsNullOrWhiteSpace((Get-ApiProperty -InputObject $response -Name 'accessToken'))) { throw 'Authentication succeeded but returned no access token.' } - $Script:BearerToken = $response.accessToken + $Script:BearerToken = Get-ApiProperty -InputObject $response -Name 'accessToken' Write-Step 'Authenticated with universal auth.' -Status 'Sub' } @@ -394,7 +504,7 @@ Set-StrictMode -Version Latest # The singular v1 route is the listing; /api/v2/organizations mounts only /:organizationId/* sub-routes. $response = Invoke-InfisicalApi -Method 'GET' -Path '/api/v1/organization' - $organizations = @($response.organizations) + $organizations = Get-ApiCollection -InputObject $response -Name 'organizations' if ($organizations.Count -eq 0) { throw 'No organizations are visible to this identity; pass -OrganizationId.' } if ($organizations.Count -gt 1) { $names = ($organizations | ForEach-Object { "$($_.name) ($($_.id))" }) -join ', ' @@ -421,7 +531,7 @@ Set-StrictMode -Version Latest Write-Step 'Project' $response = Invoke-InfisicalApi -Method 'GET' -Path '/api/v1/projects' - $all = @($response.projects) + @($response.workspaces) | Where-Object { $Null -ne $_ } + $all = Get-ApiCollection -InputObject $response -Name 'projects','workspaces' $certManagerProjects = @($all | Where-Object { $_.type -eq 'cert-manager' }) if ($certManagerProjects.Count -gt 0) { @@ -452,7 +562,7 @@ Set-StrictMode -Version Latest } $created = Invoke-InfisicalApi -Method 'POST' -Path '/api/v2/workspace' -Body $body - $project = $created.project + $project = Get-ApiObject -InputObject $created -Name 'project','workspace' Write-Step "project '$($project.name)' ($($project.id))" -Status 'Created' return $project } @@ -479,7 +589,7 @@ Set-StrictMode -Version Latest $existing = $Null if (-not [String]::IsNullOrWhiteSpace($ProjectId)) { $response = Invoke-InfisicalApi -Method 'GET' -Path "/api/v1/cert-manager/applications?projectId=$ProjectId" - $existing = @($response.applications) | Where-Object { $_.name -eq $Configuration.Application.Name } | Select-Object -First 1 + $existing = (Get-ApiCollection -InputObject $response -Name 'applications') | Where-Object { $_.name -eq $Configuration.Application.Name } | Select-Object -First 1 } if ($Null -ne $existing) { @@ -502,7 +612,7 @@ Set-StrictMode -Version Latest if ($profileIds.Count -gt 0) { $body.profileIds = $profileIds } $created = Invoke-InfisicalApi -Method 'POST' -Path '/api/v1/cert-manager/applications' -Body $body - $application = $created.application + $application = Get-ApiObject -InputObject $created -Name 'application' Write-Step "application '$($application.name)' ($($application.id)) with $($profileIds.Count) profile(s)" -Status 'Created' return $application } @@ -513,7 +623,13 @@ Set-StrictMode -Version Latest if (@($ProfileIds).Count -eq 0) { return } $response = Invoke-InfisicalApi -Method 'GET' -Path "/api/v1/cert-manager/applications/$($Application.id)/profiles?projectId=$ProjectId" - $attached = @($response.profiles | ForEach-Object { $_.id }) + # This route returns application-to-profile join rows, which carry the profile under profileId rather + # than as an object with an id of its own. + $attached = @((Get-ApiCollection -InputObject $response -Name 'profiles') | ForEach-Object { + $attachedId = Get-ApiProperty -InputObject $_ -Name 'profileId' + if ($Null -eq $attachedId) { $attachedId = Get-ApiProperty -InputObject $_ -Name 'id' } + $attachedId + } | Where-Object { $Null -ne $_ }) $missing = @($ProfileIds | Where-Object { $attached -notcontains $_ }) if ($missing.Count -eq 0) { @@ -539,7 +655,7 @@ Set-StrictMode -Version Latest $existingByName = @{} if (-not [String]::IsNullOrWhiteSpace($ProjectId)) { $response = Invoke-InfisicalApi -Method 'GET' -Path "/api/v1/cert-manager/ca/internal?projectId=$ProjectId" - foreach ($ca in @($response.certificateAuthorities)) { $existingByName[$ca.name] = $ca } + foreach ($ca in (Get-ApiCollection -InputObject $response -Name 'certificateAuthorities','cas')) { $existingByName[$ca.name] = $ca } } foreach ($definition in $Configuration.CertificateAuthorities) { @@ -558,7 +674,7 @@ Set-StrictMode -Version Latest $notAfter = [DateTime]::UtcNow.AddYears($definition.ValidityYears).ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - $configuration = [Ordered]@{ + $caConfiguration = [Ordered]@{ type = $definition.Type commonName = $definition.CommonName organization = $Configuration.Organization.Name @@ -572,19 +688,19 @@ Set-StrictMode -Version Latest # Infisical only self-signs on creation for a root, and only when it is given an expiry. A # subordinate is created pending and signed in the step below. if ($definition.Type -eq 'root') { - $configuration.notAfter = $notAfter - $configuration.maxPathLength = $definition.MaxPathLength + $caConfiguration.notAfter = $notAfter + $caConfiguration.maxPathLength = $definition.MaxPathLength } $body = @{ projectId = $ProjectId name = $definition.Name status = 'active' - configuration = $configuration + configuration = $caConfiguration } $created = Invoke-InfisicalApi -Method 'POST' -Path '/api/v1/cert-manager/ca/internal' -Body $body - $ca = $created.certificateAuthority + $ca = Get-ApiObject -InputObject $created -Name 'certificateAuthority','ca' $resolved[$definition.Name] = $ca Write-Step "$($definition.Type) CA '$($definition.Name)' ($($ca.id))" -Status 'Created' @@ -616,12 +732,12 @@ Set-StrictMode -Version Latest Write-Step "signing '$($Definition.Name)' with '$($Definition.Parent)'" -Status 'Sub' $csrResponse = Invoke-InfisicalApi -Method 'GET' -Path "/api/v1/pki/ca/$($Subordinate.id)/csr" - if ([String]::IsNullOrWhiteSpace($csrResponse.csr)) { + if ([String]::IsNullOrWhiteSpace((Get-ApiProperty -InputObject $csrResponse -Name 'csr'))) { throw "CA '$($Definition.Name)' returned no CSR to sign." } $signBody = @{ - csr = $csrResponse.csr + csr = Get-ApiProperty -InputObject $csrResponse -Name 'csr' notAfter = $NotAfter maxPathLength = $Definition.MaxPathLength } @@ -629,8 +745,8 @@ Set-StrictMode -Version Latest $signed = Invoke-InfisicalApi -Method 'POST' -Path "/api/v1/pki/ca/$($parent.id)/sign-intermediate" -Body $signBody $importBody = @{ - certificate = $signed.certificate - certificateChain = $signed.certificateChain + certificate = Get-ApiProperty -InputObject $signed -Name 'certificate' + certificateChain = Get-ApiProperty -InputObject $signed -Name 'certificateChain' } $Null = Invoke-InfisicalApi -Method 'POST' -Path "/api/v1/pki/ca/$($Subordinate.id)/import-certificate" -Body $importBody @@ -646,7 +762,7 @@ Set-StrictMode -Version Latest $existingByName = @{} if (-not [String]::IsNullOrWhiteSpace($ProjectId)) { $response = Invoke-InfisicalApi -Method 'GET' -Path "/api/v1/cert-manager/certificate-policies?projectId=$ProjectId" - foreach ($policy in @($response.certificatePolicies)) { $existingByName[$policy.name] = $policy } + foreach ($policy in (Get-ApiCollection -InputObject $response -Name 'certificatePolicies','policies')) { $existingByName[$policy.name] = $policy } } foreach ($definition in $Configuration.CertificatePolicies) { @@ -668,7 +784,10 @@ Set-StrictMode -Version Latest name = $definition.Name description = $definition.Description validity = @{ max = $definition.MaxValidity } - algorithms = @{ keyAlgorithm = @($definition.KeyAlgorithms) } + algorithms = @{ + keyAlgorithm = @($definition.KeyAlgorithms) + signature = @($definition.SignatureAlgorithms) + } } $subject = @(ConvertTo-PolicyConstraintList -Definitions $definition.Subject) @@ -684,7 +803,7 @@ Set-StrictMode -Version Latest if ($Null -ne $extendedKeyUsages) { $body.extendedKeyUsages = $extendedKeyUsages } $created = Invoke-InfisicalApi -Method 'POST' -Path '/api/v1/cert-manager/certificate-policies' -Body $body - $policy = $created.certificatePolicy + $policy = Get-ApiObject -InputObject $created -Name 'certificatePolicy' $resolved[$definition.Name] = $policy Write-Step "policy '$($definition.Name)' ($($policy.id))" -Status 'Created' } @@ -713,6 +832,32 @@ Set-StrictMode -Version Latest return $result } + function ConvertTo-ProfileDefaults { + <# + Profile defaults are flat: scalars for the algorithms and plain string arrays for the usages, + unlike the allowed/required/denied objects a policy takes. Keys are camelCased to match the API. + #> + param($Definition) + + if ($Null -eq $Definition) { return $Null } + + $result = [Ordered]@{} + foreach ($key in @('KeyAlgorithm', 'SignatureAlgorithm')) { + if (-not $Definition.Contains($key)) { continue } + if ([String]::IsNullOrWhiteSpace($Definition[$key])) { continue } + $result[$key.Substring(0, 1).ToLowerInvariant() + $key.Substring(1)] = $Definition[$key] + } + foreach ($key in @('KeyUsages', 'ExtendedKeyUsages')) { + if (-not $Definition.Contains($key)) { continue } + $values = @($Definition[$key]) + if ($values.Count -eq 0) { continue } + $result[$key.Substring(0, 1).ToLowerInvariant() + $key.Substring(1)] = $values + } + + if ($result.Count -eq 0) { return $Null } + return $result + } + function ConvertTo-PolicyConstraintList { param($Definitions) @@ -738,7 +883,7 @@ Set-StrictMode -Version Latest $existingBySlug = @{} if (-not [String]::IsNullOrWhiteSpace($ProjectId)) { $response = Invoke-InfisicalApi -Method 'GET' -Path "/api/v1/cert-manager/certificate-profiles?projectId=$ProjectId" - foreach ($profile in @($response.certificateProfiles)) { $existingBySlug[$profile.slug] = $profile } + foreach ($existingProfile in (Get-ApiCollection -InputObject $response -Name 'certificateProfiles','profiles')) { $existingBySlug[$existingProfile.slug] = $existingProfile } } foreach ($definition in $Configuration.CertificateProfiles) { @@ -774,10 +919,13 @@ Set-StrictMode -Version Latest apiConfig = $apiConfig } + $defaults = ConvertTo-ProfileDefaults -Definition $definition.Defaults + if ($Null -ne $defaults) { $body.defaults = $defaults } + $created = Invoke-InfisicalApi -Method 'POST' -Path '/api/v1/cert-manager/certificate-profiles' -Body $body - $profile = $created.certificateProfile - $resolved[$definition.Slug] = $profile - Write-Step "profile '$($definition.Slug)' ($($profile.id))" -Status 'Created' + $createdProfile = Get-ApiObject -InputObject $created -Name 'certificateProfile' + $resolved[$definition.Slug] = $createdProfile + Write-Step "profile '$($definition.Slug)' ($($createdProfile.id))" -Status 'Created' } return $resolved