diff --git a/CHANGELOG.md b/CHANGELOG.md
index 36811c2..c43977d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,11 +6,19 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos
## Unreleased
+## 2026.07.31.2140
+
+_Build produced from commit 62131e750109._
+
+## 2026.07.31.2136
+
+_Build produced from commit 62131e750109._
+
## 2026.07.31.2019
- Build produced from commit e7674af1617c.
-## Unreleased (carried forward)
+## Unreleased (carried forward)
## 2026.07.31.2006
@@ -90,6 +98,22 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos
- `InfisicalCertificate.Metadata` and `InfisicalCertificateResult.Metadata` expose a certificate's metadata as a case-insensitive dictionary.
- A metadata failure is reported as a warning rather than failing an issuance that otherwise succeeded, since by that point the certificate exists and may already be installed.
+### Added (PKI configuration management)
+
+- Full create, update, and delete for the objects a Certificate Manager project is built from — 15 cmdlets across certificate authorities, policies, profiles, applications, and PKI subscribers. The module was previously read-only for PKI configuration, which is why environment setup meant raw REST.
+- `New-InfisicalCertificateAuthority -Type Intermediate` returns an authority that can issue. Infisical creates a subordinate pending a certificate and exposes no call that completes it, so the cmdlet reads the CSR, signs it with `-ParentCaId`, and imports the signed certificate and chain.
+- Constraint dictionaries accept `Allowed`/`Required`/`Denied` in any casing and emit the lower-case form the API requires; an empty collection is omitted rather than sent, since `"allowed": []` means "allow nothing" rather than "unconstrained".
+- `-EnrollmentConfig` on the profile cmdlets routes to the block matching `-EnrollmentType`, so EST, ACME, and SCEP settings arrive through one parameter.
+- `-ProjectId` is optional on all 15, resolving the same way the read cmdlets do. Every one supports `-WhatIf`, and `Remove-*` default to high confirm impact.
+
+### Fixed (build)
+
+- `Update-Changelog` inserted the version heading above the release notes, so the section the release workflow extracts contained only the build line while the notes stayed under "Unreleased" — every release published an empty changelog. Notes are now promoted into the version section, with a fresh empty "Unreleased" left above.
+
+### Changed (routing)
+
+- `SignCertificateByCa` prefers `/api/v1/cert-manager` over the older `/api/v1/pki` route. `RetrieveCertificate` and `GetCertificateBundle` deliberately keep the older route first: it resolves by serial number, which is what callers supply, whereas the cert-manager route takes a certificate id. Both remain registered so either identifier resolves.
+
### Added (tooling)
- `Scripts/Initialize-InfisicalCertManagerEnvironment.ps1` seeds a Certificate Manager project, CA hierarchy, certificate policies, API enrollment profiles, and a certificate application from one declarative configuration block, taking only a base URI, client id, and client secret. Idempotent and `-WhatIf`-aware. Seeds an RSA hierarchy for SCCM/MECM server and client authentication, and an ECDSA P-384 hierarchy for server/client authentication and code signing.
diff --git a/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 b/Module/PSInfisicalAPI/PSInfisicalAPI.psd1
index 342cb7b..8053431 100644
--- a/Module/PSInfisicalAPI/PSInfisicalAPI.psd1
+++ b/Module/PSInfisicalAPI/PSInfisicalAPI.psd1
@@ -1,6 +1,6 @@
@{
RootModule = 'PSInfisicalAPI.psm1'
- ModuleVersion = '2026.07.31.2019'
+ ModuleVersion = '2026.07.31.2140'
GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51'
Author = 'Grace Solutions'
CompanyName = 'Grace Solutions'
@@ -62,7 +62,22 @@
'Write-InfisicalScepMdmProfileToWmi',
'Start-InfisicalProcess',
'Get-InfisicalEnvironmentVariable',
- 'Get-InfisicalSANList'
+ 'Get-InfisicalSANList',
+ 'New-InfisicalCertificateAuthority',
+ 'Set-InfisicalCertificateAuthority',
+ 'Remove-InfisicalCertificateAuthority',
+ 'New-InfisicalCertificatePolicy',
+ 'Set-InfisicalCertificatePolicy',
+ 'Remove-InfisicalCertificatePolicy',
+ 'New-InfisicalCertificateProfile',
+ 'Set-InfisicalCertificateProfile',
+ 'Remove-InfisicalCertificateProfile',
+ 'New-InfisicalCertificateApplication',
+ 'Set-InfisicalCertificateApplication',
+ 'Remove-InfisicalCertificateApplication',
+ 'New-InfisicalPkiSubscriber',
+ 'Set-InfisicalPkiSubscriber',
+ 'Remove-InfisicalPkiSubscriber'
)
AliasesToExport = @()
VariablesToExport = @()
@@ -74,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 = 'e7674af1617c'
+ CommitHash = '62131e750109'
}
}
}
\ No newline at end of file
diff --git a/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll b/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll
index b4658d0..4f22bac 100644
Binary files a/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll and b/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll differ
diff --git a/Module/PSInfisicalAPI/bin/en-US/PSInfisicalAPI.dll-Help.xml b/Module/PSInfisicalAPI/bin/en-US/PSInfisicalAPI.dll-Help.xml
index 79cb0b8..6a2776c 100644
--- a/Module/PSInfisicalAPI/bin/en-US/PSInfisicalAPI.dll-Help.xml
+++ b/Module/PSInfisicalAPI/bin/en-US/PSInfisicalAPI.dll-Help.xml
@@ -2104,4 +2104,439 @@ $Sans = Get-InfisicalSANList @GetInfisicalSANListParameters
+
+
+ New-InfisicalCertificateAuthority
+ Creates an internal Infisical certificate authority, signing a subordinate with its parent.
+ New
+ InfisicalCertificateAuthority
+
+
+ Creates a root or intermediate internal certificate authority in a Certificate Manager project. A root is self-signed on creation. Infisical creates an intermediate pending a certificate and exposes no single call that completes it, so this cmdlet performs the remaining sequence itself: it reads the certificate signing request, signs it with the authority named by -ParentCaId, and imports the signed certificate and chain back, returning an authority that is ready to issue. -NotAfter defaults to ten years for a root and five for an intermediate; -MaxPathLength defaults to 1 for a root and 0 otherwise. -ProjectId is optional and resolves to the organization's Certificate Manager project.
+
+
+ Notes
+
+ Certificate authorities created through the API always have direct issuance disabled, because Infisical's creation service sets it explicitly and exposes no way to change it afterwards. Issue through a certificate profile, which does not consult that flag. Honors -WhatIf and -Confirm.
+
+
+
+
+ EXAMPLE 1
+ $Root = New-InfisicalCertificateAuthority -Name 'root-ca' -Type Root -CommonName 'Contoso Root Certificate Authority' -Organization 'Contoso' -Country 'US'
+ Creates a self-signed root valid for ten years.
+
+
+ EXAMPLE 2
+ New-InfisicalCertificateAuthority -Name 'issuing-ca' -Type Intermediate -ParentCaId $Root.Id -CommonName 'Contoso Issuing Certificate Authority' -KeyAlgorithm 'EC_secp384r1'
+ Creates a subordinate, signs it with the root, and imports the signed certificate so it can issue immediately.
+
+
+
+
+
+ Set-InfisicalCertificateAuthority
+ Renames an internal Infisical certificate authority or changes its status.
+ Set
+ InfisicalCertificateAuthority
+
+
+ Updates the name or status of an internal certificate authority. Infisical's update schema accepts only these two fields; subject, key algorithm, and validity are fixed when the authority is created. Supply -PassThru to emit the updated record.
+
+
+ Notes
+
+ Disabling an authority stops it issuing without deleting it or the certificates it has already signed. Honors -WhatIf and -Confirm.
+
+
+
+
+ EXAMPLE 1
+ Set-InfisicalCertificateAuthority -CaId $Ca.Id -Status disabled
+ Stops the authority issuing new certificates.
+
+
+ EXAMPLE 2
+ Set-InfisicalCertificateAuthority -CaId $Ca.Id -Name 'retired-issuing-ca' -PassThru
+ Renames the authority and emits the updated record.
+
+
+
+
+
+ Remove-InfisicalCertificateAuthority
+ Deletes an internal Infisical certificate authority.
+ Remove
+ InfisicalCertificateAuthority
+
+
+ Deletes an internal certificate authority from a Certificate Manager project. -PassThru emits the removed identifier for logging.
+
+
+ Notes
+
+ Destructive. Certificates already issued by the authority stop chaining to a known issuer once it is gone, and any subordinate beneath it is orphaned. High ConfirmImpact prompts unless -Confirm:$False is supplied.
+
+
+
+
+ EXAMPLE 1
+ Remove-InfisicalCertificateAuthority -CaId $Ca.Id -Confirm:$False
+ Deletes the authority without prompting.
+
+
+ EXAMPLE 2
+ Get-InfisicalCertificateAuthority -Kind Internal | Where-Object {($_.Status -eq 'disabled')} | Remove-InfisicalCertificateAuthority
+ Removes every disabled authority, prompting for each.
+
+
+
+
+
+ New-InfisicalCertificatePolicy
+ Creates an Infisical certificate policy that constrains what a profile may issue.
+ New
+ InfisicalCertificatePolicy
+
+
+ Creates a certificate policy: the constraints a certificate profile issues within. Subject attributes, subject alternative names, key usages, and extended key usages are each expressed as allowed, required, and denied sets, supplied as dictionaries so the nested shape stays readable. -MaxValidity caps certificate lifetime, -KeyAlgorithm and -SignatureAlgorithm restrict the cryptography. A constraint that is not supplied leaves that dimension unconstrained, which is what fleet enrollment needs so each machine can present its own name.
+
+
+ Notes
+
+ Constraint values use Infisical's snake_case names: digital_signature, key_encipherment, server_auth, client_auth, code_signing, common_name, dns_name, ip_address. Honors -WhatIf and -Confirm.
+
+
+
+
+ EXAMPLE 1
+ New-InfisicalCertificatePolicy -Name 'server-auth' -MaxValidity '90d' -KeyAlgorithm 'RSA_2048','EC_secp384r1' -KeyUsage @{ Required = @('digital_signature','key_encipherment') } -ExtendedKeyUsage @{ Required = @('server_auth','client_auth') }
+ Creates a policy for server and client authentication, leaving subject and SANs unconstrained.
+
+
+ EXAMPLE 2
+ New-InfisicalCertificatePolicy -Name 'code-signing' -MaxValidity '365d' -ExtendedKeyUsage @{ Required = @('code_signing'); Denied = @('server_auth','client_auth') } -SubjectAlternativeName @(@{ Type = 'dns_name'; Allowed = @('*.contoso.com') })
+ Creates a code signing policy that forbids TLS usage and restricts DNS names to one suffix.
+
+
+
+
+
+ Set-InfisicalCertificatePolicy
+ Updates an Infisical certificate policy.
+ Set
+ InfisicalCertificatePolicy
+
+
+ Updates a certificate policy. Only the constraints supplied on the command line are sent; anything omitted keeps its stored value. Supply -PassThru to emit the updated policy.
+
+
+ Notes
+
+ Changing a policy affects every profile bound to it, and therefore every future certificate those profiles issue. Certificates already issued are unaffected. Honors -WhatIf and -Confirm.
+
+
+
+
+ EXAMPLE 1
+ Set-InfisicalCertificatePolicy -PolicyId $Policy.Id -MaxValidity '30d'
+ Shortens the maximum lifetime, leaving every other constraint as it was.
+
+
+ EXAMPLE 2
+ Set-InfisicalCertificatePolicy -PolicyId $Policy.Id -ExtendedKeyUsage @{ Required = @('server_auth') } -PassThru
+ Narrows the extended key usage and emits the updated policy.
+
+
+
+
+
+ Remove-InfisicalCertificatePolicy
+ Deletes an Infisical certificate policy.
+ Remove
+ InfisicalCertificatePolicy
+
+
+ Deletes a certificate policy from a Certificate Manager project. -PassThru emits the removed identifier for logging.
+
+
+ Notes
+
+ Destructive. A profile bound to the policy cannot issue once it is gone, so remove or repoint dependent profiles first. High ConfirmImpact prompts unless -Confirm:$False is supplied.
+
+
+
+
+ EXAMPLE 1
+ Remove-InfisicalCertificatePolicy -PolicyId $Policy.Id -Confirm:$False
+ Deletes the policy without prompting.
+
+
+ EXAMPLE 2
+ Get-InfisicalCertificatePolicy | Where-Object {($_.Name -like 'test-*')} | Remove-InfisicalCertificatePolicy
+ Removes every policy whose name begins with test-, prompting for each.
+
+
+
+
+
+ New-InfisicalCertificateProfile
+ Creates an Infisical certificate profile that binds an issuing authority to a policy.
+ New
+ InfisicalCertificateProfile
+
+
+ Creates a certificate profile: the object Request-InfisicalCertificate -CertificateProfileId issues against. A profile binds an issuing certificate authority to a certificate policy and exposes it for one enrollment type. -Slug accepts lowercase letters, numbers, and hyphens. -EnrollmentConfig carries the settings for the chosen -EnrollmentType, so EST, ACME, and SCEP settings all arrive through one parameter; for the default api type, -AutoRenew and -RenewBeforeDays are folded into it.
+
+
+ Notes
+
+ Profile issuance is the only path that does not consult the issuing authority's direct-issuance flag, so a profile issues successfully against an authority whose EnableDirectIssuance is False. Unlike a PKI subscriber, a profile accepts a per-request common name, which is what makes it suitable for fleet enrollment. Honors -WhatIf and -Confirm.
+
+
+
+
+ EXAMPLE 1
+ New-InfisicalCertificateProfile -Slug 'server-auth' -CertificatePolicyId $Policy.Id -CaId $Ca.Id
+ Creates an API enrollment profile bound to a policy and issuing authority.
+
+
+ EXAMPLE 2
+ New-InfisicalCertificateProfile -Slug 'workload' -CertificatePolicyId $Policy.Id -CaId $Ca.Id -AutoRenew -RenewBeforeDays 14 -Defaults @{ ttlDays = 90 }
+ Creates a profile that renews issued certificates fourteen days before expiry and defaults to a ninety day lifetime.
+
+
+
+
+
+ Set-InfisicalCertificateProfile
+ Updates an Infisical certificate profile.
+ Set
+ InfisicalCertificateProfile
+
+
+ Updates a certificate profile. Only the values supplied on the command line are sent; anything omitted keeps its stored value, including the enrollment type unless -EnrollmentType is passed explicitly. Supply -PassThru to emit the updated profile.
+
+
+ Notes
+
+ Repointing a profile at a different policy or issuing authority changes what future requests produce. Because certificate reuse is scoped by profile, Request-InfisicalCertificate keeps reusing certificates the profile issued previously until they fall inside their renewal window. Honors -WhatIf and -Confirm.
+
+
+
+
+ EXAMPLE 1
+ Set-InfisicalCertificateProfile -ProfileId $Profile.Id -CertificatePolicyId $NewPolicy.Id
+ Repoints the profile at a different policy.
+
+
+ EXAMPLE 2
+ Set-InfisicalCertificateProfile -ProfileId $Profile.Id -AutoRenew -RenewBeforeDays 7 -PassThru
+ Enables automatic renewal seven days before expiry and emits the updated profile.
+
+
+
+
+
+ Remove-InfisicalCertificateProfile
+ Deletes an Infisical certificate profile.
+ Remove
+ InfisicalCertificateProfile
+
+
+ Deletes a certificate profile from a Certificate Manager project. -PassThru emits the removed identifier for logging.
+
+
+ Notes
+
+ Destructive. Any script requesting certificates through the profile fails once it is gone, and the profile is detached from every application that referenced it. High ConfirmImpact prompts unless -Confirm:$False is supplied.
+
+
+
+
+ EXAMPLE 1
+ Remove-InfisicalCertificateProfile -ProfileId $Profile.Id -Confirm:$False
+ Deletes the profile without prompting.
+
+
+ EXAMPLE 2
+ Get-InfisicalCertificateProfile -ApplicationId $Application.Id | Remove-InfisicalCertificateProfile
+ Removes every profile attached to an application, prompting for each.
+
+
+
+
+
+ New-InfisicalCertificateApplication
+ Creates an Infisical certificate application to group profiles and certificates.
+ New
+ InfisicalCertificateApplication
+
+
+ Creates a certificate application: the grouping the Infisical console presents profiles, members, and certificates under, and the scope Get-InfisicalCertificateProfile -ApplicationId and Get-InfisicalCertificate -ApplicationId filter by. Certificate profiles can be attached at creation with -ProfileId.
+
+
+ Notes
+
+ Applications are served only from the organization's active Certificate Manager project. Creating one in any other cert-manager project fails, which is why -ProjectId resolves to the active project when it is not supplied. Honors -WhatIf and -Confirm.
+
+
+
+
+ EXAMPLE 1
+ New-InfisicalCertificateApplication -Name 'platform' -Description 'Endpoint and workload certificates'
+ Creates an empty application.
+
+
+ EXAMPLE 2
+ New-InfisicalCertificateApplication -Name 'platform' -ProfileId $ServerProfile.Id, $CodeSigningProfile.Id
+ Creates an application with two profiles already attached.
+
+
+
+
+
+ Set-InfisicalCertificateApplication
+ Renames an Infisical certificate application or changes which profiles it holds.
+ Set
+ InfisicalCertificateApplication
+
+
+ Updates a certificate application. -Name and -Description change the record; -AddProfileId and -RemoveProfileId change which certificate profiles the application groups. The record and its profile attachments are separate endpoints, so supplying only profile parameters skips the record update entirely. Supply -PassThru to emit the updated application.
+
+
+ Notes
+
+ Detaching a profile does not delete it; the profile continues to exist and issue, it is simply no longer grouped under the application. Honors -WhatIf and -Confirm.
+
+
+
+
+ EXAMPLE 1
+ Set-InfisicalCertificateApplication -ApplicationId $Application.Id -AddProfileId $Profile.Id
+ Attaches a profile to the application.
+
+
+ EXAMPLE 2
+ Set-InfisicalCertificateApplication -ApplicationId $Application.Id -Name 'endpoint-management' -RemoveProfileId $Old.Id -PassThru
+ Renames the application, detaches a profile, and emits the updated record.
+
+
+
+
+
+ Remove-InfisicalCertificateApplication
+ Deletes an Infisical certificate application.
+ Remove
+ InfisicalCertificateApplication
+
+
+ Deletes a certificate application from a Certificate Manager project. -PassThru emits the removed identifier for logging.
+
+
+ Notes
+
+ Destructive. The profiles the application grouped are not deleted, but scripts that locate a profile by application can no longer find it. High ConfirmImpact prompts unless -Confirm:$False is supplied.
+
+
+
+
+ EXAMPLE 1
+ Remove-InfisicalCertificateApplication -ApplicationId $Application.Id -Confirm:$False
+ Deletes the application without prompting.
+
+
+ EXAMPLE 2
+ Get-InfisicalCertificateApplication | Where-Object {($_.CertificateCount -eq 0)} | Remove-InfisicalCertificateApplication
+ Removes every application holding no certificates, prompting for each.
+
+
+
+
+
+ New-InfisicalPkiSubscriber
+ Creates an Infisical PKI subscriber, a named enrollment identity with a fixed common name.
+ New
+ InfisicalPkiSubscriber
+
+
+ Creates a PKI subscriber: a named enrollment identity that pins one common name, an allowlist of subject alternative names, a lifetime, and the permitted key usages, so a request carries only a certificate signing request. -CommonName is the identity the subscriber issues for, and -SubjectAlternativeName is an allowlist rather than a default.
+
+
+ Notes
+
+ A subscriber is a single identity, not a template. Infisical rejects any request whose certificate signing request names a different common name, and rejects any subject alternative name outside the allowlist, so enrolling many machines through subscribers means one subscriber per machine. Use a certificate profile for fleet enrollment. Honors -WhatIf and -Confirm.
+
+
+
+
+ EXAMPLE 1
+ New-InfisicalPkiSubscriber -Name 'web01' -CommonName 'WEB01.contoso.com' -CaId $Ca.Id -Ttl '90d'
+ Creates a subscriber for one host.
+
+
+ EXAMPLE 2
+ New-InfisicalPkiSubscriber -Name 'web01' -CommonName 'WEB01.contoso.com' -CaId $Ca.Id -Ttl '90d' -SubjectAlternativeName 'WEB01','WEB01.contoso.com' -ExtendedKeyUsage 'serverAuth','clientAuth'
+ Creates a subscriber that also permits two subject alternative names and restricts extended key usage.
+
+
+
+
+
+ Set-InfisicalPkiSubscriber
+ Updates an Infisical PKI subscriber.
+ Set
+ InfisicalPkiSubscriber
+
+
+ Updates a PKI subscriber, addressed by its current -Name. Only the values supplied on the command line are sent; anything omitted keeps its stored value. -NewName renames the subscriber. Supply -PassThru to emit the updated record.
+
+
+ Notes
+
+ Changing -CommonName changes the identity the subscriber issues for, so any script signing against it must present a matching certificate signing request afterwards. Honors -WhatIf and -Confirm.
+
+
+
+
+ EXAMPLE 1
+ Set-InfisicalPkiSubscriber -Name 'web01' -Ttl '30d'
+ Shortens the lifetime of certificates issued for the subscriber.
+
+
+ EXAMPLE 2
+ Set-InfisicalPkiSubscriber -Name 'web01' -SubjectAlternativeName 'WEB01','WEB01.contoso.com','www.contoso.com' -PassThru
+ Extends the permitted subject alternative names and emits the updated subscriber.
+
+
+
+
+
+ Remove-InfisicalPkiSubscriber
+ Deletes an Infisical PKI subscriber.
+ Remove
+ InfisicalPkiSubscriber
+
+
+ Deletes a PKI subscriber from a Certificate Manager project, addressed by name. -PassThru emits the removed name for logging.
+
+
+ Notes
+
+ Destructive. Any script signing through the subscriber fails once it is gone. Certificates already issued are unaffected. High ConfirmImpact prompts unless -Confirm:$False is supplied.
+
+
+
+
+ EXAMPLE 1
+ Remove-InfisicalPkiSubscriber -Name 'web01' -Confirm:$False
+ Deletes the subscriber without prompting.
+
+
+ EXAMPLE 2
+ Get-InfisicalPkiSubscriber | Where-Object {($_.Status -ne 'active')} | Remove-InfisicalPkiSubscriber
+ Removes every inactive subscriber, prompting for each.
+
+
+
diff --git a/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml b/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml
index 79cb0b8..6a2776c 100644
--- a/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml
+++ b/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml
@@ -2104,4 +2104,439 @@ $Sans = Get-InfisicalSANList @GetInfisicalSANListParameters
+
+
+ New-InfisicalCertificateAuthority
+ Creates an internal Infisical certificate authority, signing a subordinate with its parent.
+ New
+ InfisicalCertificateAuthority
+
+
+ Creates a root or intermediate internal certificate authority in a Certificate Manager project. A root is self-signed on creation. Infisical creates an intermediate pending a certificate and exposes no single call that completes it, so this cmdlet performs the remaining sequence itself: it reads the certificate signing request, signs it with the authority named by -ParentCaId, and imports the signed certificate and chain back, returning an authority that is ready to issue. -NotAfter defaults to ten years for a root and five for an intermediate; -MaxPathLength defaults to 1 for a root and 0 otherwise. -ProjectId is optional and resolves to the organization's Certificate Manager project.
+
+
+ Notes
+
+ Certificate authorities created through the API always have direct issuance disabled, because Infisical's creation service sets it explicitly and exposes no way to change it afterwards. Issue through a certificate profile, which does not consult that flag. Honors -WhatIf and -Confirm.
+
+
+
+
+ EXAMPLE 1
+ $Root = New-InfisicalCertificateAuthority -Name 'root-ca' -Type Root -CommonName 'Contoso Root Certificate Authority' -Organization 'Contoso' -Country 'US'
+ Creates a self-signed root valid for ten years.
+
+
+ EXAMPLE 2
+ New-InfisicalCertificateAuthority -Name 'issuing-ca' -Type Intermediate -ParentCaId $Root.Id -CommonName 'Contoso Issuing Certificate Authority' -KeyAlgorithm 'EC_secp384r1'
+ Creates a subordinate, signs it with the root, and imports the signed certificate so it can issue immediately.
+
+
+
+
+
+ Set-InfisicalCertificateAuthority
+ Renames an internal Infisical certificate authority or changes its status.
+ Set
+ InfisicalCertificateAuthority
+
+
+ Updates the name or status of an internal certificate authority. Infisical's update schema accepts only these two fields; subject, key algorithm, and validity are fixed when the authority is created. Supply -PassThru to emit the updated record.
+
+
+ Notes
+
+ Disabling an authority stops it issuing without deleting it or the certificates it has already signed. Honors -WhatIf and -Confirm.
+
+
+
+
+ EXAMPLE 1
+ Set-InfisicalCertificateAuthority -CaId $Ca.Id -Status disabled
+ Stops the authority issuing new certificates.
+
+
+ EXAMPLE 2
+ Set-InfisicalCertificateAuthority -CaId $Ca.Id -Name 'retired-issuing-ca' -PassThru
+ Renames the authority and emits the updated record.
+
+
+
+
+
+ Remove-InfisicalCertificateAuthority
+ Deletes an internal Infisical certificate authority.
+ Remove
+ InfisicalCertificateAuthority
+
+
+ Deletes an internal certificate authority from a Certificate Manager project. -PassThru emits the removed identifier for logging.
+
+
+ Notes
+
+ Destructive. Certificates already issued by the authority stop chaining to a known issuer once it is gone, and any subordinate beneath it is orphaned. High ConfirmImpact prompts unless -Confirm:$False is supplied.
+
+
+
+
+ EXAMPLE 1
+ Remove-InfisicalCertificateAuthority -CaId $Ca.Id -Confirm:$False
+ Deletes the authority without prompting.
+
+
+ EXAMPLE 2
+ Get-InfisicalCertificateAuthority -Kind Internal | Where-Object {($_.Status -eq 'disabled')} | Remove-InfisicalCertificateAuthority
+ Removes every disabled authority, prompting for each.
+
+
+
+
+
+ New-InfisicalCertificatePolicy
+ Creates an Infisical certificate policy that constrains what a profile may issue.
+ New
+ InfisicalCertificatePolicy
+
+
+ Creates a certificate policy: the constraints a certificate profile issues within. Subject attributes, subject alternative names, key usages, and extended key usages are each expressed as allowed, required, and denied sets, supplied as dictionaries so the nested shape stays readable. -MaxValidity caps certificate lifetime, -KeyAlgorithm and -SignatureAlgorithm restrict the cryptography. A constraint that is not supplied leaves that dimension unconstrained, which is what fleet enrollment needs so each machine can present its own name.
+
+
+ Notes
+
+ Constraint values use Infisical's snake_case names: digital_signature, key_encipherment, server_auth, client_auth, code_signing, common_name, dns_name, ip_address. Honors -WhatIf and -Confirm.
+
+
+
+
+ EXAMPLE 1
+ New-InfisicalCertificatePolicy -Name 'server-auth' -MaxValidity '90d' -KeyAlgorithm 'RSA_2048','EC_secp384r1' -KeyUsage @{ Required = @('digital_signature','key_encipherment') } -ExtendedKeyUsage @{ Required = @('server_auth','client_auth') }
+ Creates a policy for server and client authentication, leaving subject and SANs unconstrained.
+
+
+ EXAMPLE 2
+ New-InfisicalCertificatePolicy -Name 'code-signing' -MaxValidity '365d' -ExtendedKeyUsage @{ Required = @('code_signing'); Denied = @('server_auth','client_auth') } -SubjectAlternativeName @(@{ Type = 'dns_name'; Allowed = @('*.contoso.com') })
+ Creates a code signing policy that forbids TLS usage and restricts DNS names to one suffix.
+
+
+
+
+
+ Set-InfisicalCertificatePolicy
+ Updates an Infisical certificate policy.
+ Set
+ InfisicalCertificatePolicy
+
+
+ Updates a certificate policy. Only the constraints supplied on the command line are sent; anything omitted keeps its stored value. Supply -PassThru to emit the updated policy.
+
+
+ Notes
+
+ Changing a policy affects every profile bound to it, and therefore every future certificate those profiles issue. Certificates already issued are unaffected. Honors -WhatIf and -Confirm.
+
+
+
+
+ EXAMPLE 1
+ Set-InfisicalCertificatePolicy -PolicyId $Policy.Id -MaxValidity '30d'
+ Shortens the maximum lifetime, leaving every other constraint as it was.
+
+
+ EXAMPLE 2
+ Set-InfisicalCertificatePolicy -PolicyId $Policy.Id -ExtendedKeyUsage @{ Required = @('server_auth') } -PassThru
+ Narrows the extended key usage and emits the updated policy.
+
+
+
+
+
+ Remove-InfisicalCertificatePolicy
+ Deletes an Infisical certificate policy.
+ Remove
+ InfisicalCertificatePolicy
+
+
+ Deletes a certificate policy from a Certificate Manager project. -PassThru emits the removed identifier for logging.
+
+
+ Notes
+
+ Destructive. A profile bound to the policy cannot issue once it is gone, so remove or repoint dependent profiles first. High ConfirmImpact prompts unless -Confirm:$False is supplied.
+
+
+
+
+ EXAMPLE 1
+ Remove-InfisicalCertificatePolicy -PolicyId $Policy.Id -Confirm:$False
+ Deletes the policy without prompting.
+
+
+ EXAMPLE 2
+ Get-InfisicalCertificatePolicy | Where-Object {($_.Name -like 'test-*')} | Remove-InfisicalCertificatePolicy
+ Removes every policy whose name begins with test-, prompting for each.
+
+
+
+
+
+ New-InfisicalCertificateProfile
+ Creates an Infisical certificate profile that binds an issuing authority to a policy.
+ New
+ InfisicalCertificateProfile
+
+
+ Creates a certificate profile: the object Request-InfisicalCertificate -CertificateProfileId issues against. A profile binds an issuing certificate authority to a certificate policy and exposes it for one enrollment type. -Slug accepts lowercase letters, numbers, and hyphens. -EnrollmentConfig carries the settings for the chosen -EnrollmentType, so EST, ACME, and SCEP settings all arrive through one parameter; for the default api type, -AutoRenew and -RenewBeforeDays are folded into it.
+
+
+ Notes
+
+ Profile issuance is the only path that does not consult the issuing authority's direct-issuance flag, so a profile issues successfully against an authority whose EnableDirectIssuance is False. Unlike a PKI subscriber, a profile accepts a per-request common name, which is what makes it suitable for fleet enrollment. Honors -WhatIf and -Confirm.
+
+
+
+
+ EXAMPLE 1
+ New-InfisicalCertificateProfile -Slug 'server-auth' -CertificatePolicyId $Policy.Id -CaId $Ca.Id
+ Creates an API enrollment profile bound to a policy and issuing authority.
+
+
+ EXAMPLE 2
+ New-InfisicalCertificateProfile -Slug 'workload' -CertificatePolicyId $Policy.Id -CaId $Ca.Id -AutoRenew -RenewBeforeDays 14 -Defaults @{ ttlDays = 90 }
+ Creates a profile that renews issued certificates fourteen days before expiry and defaults to a ninety day lifetime.
+
+
+
+
+
+ Set-InfisicalCertificateProfile
+ Updates an Infisical certificate profile.
+ Set
+ InfisicalCertificateProfile
+
+
+ Updates a certificate profile. Only the values supplied on the command line are sent; anything omitted keeps its stored value, including the enrollment type unless -EnrollmentType is passed explicitly. Supply -PassThru to emit the updated profile.
+
+
+ Notes
+
+ Repointing a profile at a different policy or issuing authority changes what future requests produce. Because certificate reuse is scoped by profile, Request-InfisicalCertificate keeps reusing certificates the profile issued previously until they fall inside their renewal window. Honors -WhatIf and -Confirm.
+
+
+
+
+ EXAMPLE 1
+ Set-InfisicalCertificateProfile -ProfileId $Profile.Id -CertificatePolicyId $NewPolicy.Id
+ Repoints the profile at a different policy.
+
+
+ EXAMPLE 2
+ Set-InfisicalCertificateProfile -ProfileId $Profile.Id -AutoRenew -RenewBeforeDays 7 -PassThru
+ Enables automatic renewal seven days before expiry and emits the updated profile.
+
+
+
+
+
+ Remove-InfisicalCertificateProfile
+ Deletes an Infisical certificate profile.
+ Remove
+ InfisicalCertificateProfile
+
+
+ Deletes a certificate profile from a Certificate Manager project. -PassThru emits the removed identifier for logging.
+
+
+ Notes
+
+ Destructive. Any script requesting certificates through the profile fails once it is gone, and the profile is detached from every application that referenced it. High ConfirmImpact prompts unless -Confirm:$False is supplied.
+
+
+
+
+ EXAMPLE 1
+ Remove-InfisicalCertificateProfile -ProfileId $Profile.Id -Confirm:$False
+ Deletes the profile without prompting.
+
+
+ EXAMPLE 2
+ Get-InfisicalCertificateProfile -ApplicationId $Application.Id | Remove-InfisicalCertificateProfile
+ Removes every profile attached to an application, prompting for each.
+
+
+
+
+
+ New-InfisicalCertificateApplication
+ Creates an Infisical certificate application to group profiles and certificates.
+ New
+ InfisicalCertificateApplication
+
+
+ Creates a certificate application: the grouping the Infisical console presents profiles, members, and certificates under, and the scope Get-InfisicalCertificateProfile -ApplicationId and Get-InfisicalCertificate -ApplicationId filter by. Certificate profiles can be attached at creation with -ProfileId.
+
+
+ Notes
+
+ Applications are served only from the organization's active Certificate Manager project. Creating one in any other cert-manager project fails, which is why -ProjectId resolves to the active project when it is not supplied. Honors -WhatIf and -Confirm.
+
+
+
+
+ EXAMPLE 1
+ New-InfisicalCertificateApplication -Name 'platform' -Description 'Endpoint and workload certificates'
+ Creates an empty application.
+
+
+ EXAMPLE 2
+ New-InfisicalCertificateApplication -Name 'platform' -ProfileId $ServerProfile.Id, $CodeSigningProfile.Id
+ Creates an application with two profiles already attached.
+
+
+
+
+
+ Set-InfisicalCertificateApplication
+ Renames an Infisical certificate application or changes which profiles it holds.
+ Set
+ InfisicalCertificateApplication
+
+
+ Updates a certificate application. -Name and -Description change the record; -AddProfileId and -RemoveProfileId change which certificate profiles the application groups. The record and its profile attachments are separate endpoints, so supplying only profile parameters skips the record update entirely. Supply -PassThru to emit the updated application.
+
+
+ Notes
+
+ Detaching a profile does not delete it; the profile continues to exist and issue, it is simply no longer grouped under the application. Honors -WhatIf and -Confirm.
+
+
+
+
+ EXAMPLE 1
+ Set-InfisicalCertificateApplication -ApplicationId $Application.Id -AddProfileId $Profile.Id
+ Attaches a profile to the application.
+
+
+ EXAMPLE 2
+ Set-InfisicalCertificateApplication -ApplicationId $Application.Id -Name 'endpoint-management' -RemoveProfileId $Old.Id -PassThru
+ Renames the application, detaches a profile, and emits the updated record.
+
+
+
+
+
+ Remove-InfisicalCertificateApplication
+ Deletes an Infisical certificate application.
+ Remove
+ InfisicalCertificateApplication
+
+
+ Deletes a certificate application from a Certificate Manager project. -PassThru emits the removed identifier for logging.
+
+
+ Notes
+
+ Destructive. The profiles the application grouped are not deleted, but scripts that locate a profile by application can no longer find it. High ConfirmImpact prompts unless -Confirm:$False is supplied.
+
+
+
+
+ EXAMPLE 1
+ Remove-InfisicalCertificateApplication -ApplicationId $Application.Id -Confirm:$False
+ Deletes the application without prompting.
+
+
+ EXAMPLE 2
+ Get-InfisicalCertificateApplication | Where-Object {($_.CertificateCount -eq 0)} | Remove-InfisicalCertificateApplication
+ Removes every application holding no certificates, prompting for each.
+
+
+
+
+
+ New-InfisicalPkiSubscriber
+ Creates an Infisical PKI subscriber, a named enrollment identity with a fixed common name.
+ New
+ InfisicalPkiSubscriber
+
+
+ Creates a PKI subscriber: a named enrollment identity that pins one common name, an allowlist of subject alternative names, a lifetime, and the permitted key usages, so a request carries only a certificate signing request. -CommonName is the identity the subscriber issues for, and -SubjectAlternativeName is an allowlist rather than a default.
+
+
+ Notes
+
+ A subscriber is a single identity, not a template. Infisical rejects any request whose certificate signing request names a different common name, and rejects any subject alternative name outside the allowlist, so enrolling many machines through subscribers means one subscriber per machine. Use a certificate profile for fleet enrollment. Honors -WhatIf and -Confirm.
+
+
+
+
+ EXAMPLE 1
+ New-InfisicalPkiSubscriber -Name 'web01' -CommonName 'WEB01.contoso.com' -CaId $Ca.Id -Ttl '90d'
+ Creates a subscriber for one host.
+
+
+ EXAMPLE 2
+ New-InfisicalPkiSubscriber -Name 'web01' -CommonName 'WEB01.contoso.com' -CaId $Ca.Id -Ttl '90d' -SubjectAlternativeName 'WEB01','WEB01.contoso.com' -ExtendedKeyUsage 'serverAuth','clientAuth'
+ Creates a subscriber that also permits two subject alternative names and restricts extended key usage.
+
+
+
+
+
+ Set-InfisicalPkiSubscriber
+ Updates an Infisical PKI subscriber.
+ Set
+ InfisicalPkiSubscriber
+
+
+ Updates a PKI subscriber, addressed by its current -Name. Only the values supplied on the command line are sent; anything omitted keeps its stored value. -NewName renames the subscriber. Supply -PassThru to emit the updated record.
+
+
+ Notes
+
+ Changing -CommonName changes the identity the subscriber issues for, so any script signing against it must present a matching certificate signing request afterwards. Honors -WhatIf and -Confirm.
+
+
+
+
+ EXAMPLE 1
+ Set-InfisicalPkiSubscriber -Name 'web01' -Ttl '30d'
+ Shortens the lifetime of certificates issued for the subscriber.
+
+
+ EXAMPLE 2
+ Set-InfisicalPkiSubscriber -Name 'web01' -SubjectAlternativeName 'WEB01','WEB01.contoso.com','www.contoso.com' -PassThru
+ Extends the permitted subject alternative names and emits the updated subscriber.
+
+
+
+
+
+ Remove-InfisicalPkiSubscriber
+ Deletes an Infisical PKI subscriber.
+ Remove
+ InfisicalPkiSubscriber
+
+
+ Deletes a PKI subscriber from a Certificate Manager project, addressed by name. -PassThru emits the removed name for logging.
+
+
+ Notes
+
+ Destructive. Any script signing through the subscriber fails once it is gone. Certificates already issued are unaffected. High ConfirmImpact prompts unless -Confirm:$False is supplied.
+
+
+
+
+ EXAMPLE 1
+ Remove-InfisicalPkiSubscriber -Name 'web01' -Confirm:$False
+ Deletes the subscriber without prompting.
+
+
+ EXAMPLE 2
+ Get-InfisicalPkiSubscriber | Where-Object {($_.Status -ne 'active')} | Remove-InfisicalPkiSubscriber
+ Removes every inactive subscriber, prompting for each.
+
+
+
diff --git a/README.md b/README.md
index f81cf69..630ef88 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ Import-Module -Name .\Module\PSInfisicalAPI
## Cmdlets
-The module exports 51 cmdlets. Discovery cmdlets (`Get-Infisical*`) use a `List` (default) / single-record parameter-set pair: invoking without the identity parameter returns the collection, supplying the identity parameter returns one record.
+The module exports 68 cmdlets. Discovery cmdlets (`Get-Infisical*`) use a `List` (default) / single-record parameter-set pair: invoking without the identity parameter returns the collection, supplying the identity parameter returns one record.
### Session
@@ -118,6 +118,44 @@ The module exports 51 cmdlets. Discovery cmdlets (`Get-Infisical*`) use a `List`
| `Write-InfisicalScepMdmProfileToWmi`| Submits a SCEP MDM profile to the local MDM Bridge WMI provider to trigger enrollment. |
| `Get-InfisicalSANList` | Builds a SAN candidate list (device name, `.` per adapter DNS suffix, RFC 1918 + CGNAT IPv4 addresses, IPv4/IPv6 loopback) for `Request-InfisicalCertificate -DnsName`. |
+### PKI configuration
+
+Creating and changing the objects a Certificate Manager project is built from. `-ProjectId` is optional on all of them, and every one honours `-WhatIf`.
+
+| Cmdlet | Purpose |
+| ---------------------------------------- | -------------------------------------------------------------------------------------------------- |
+| `New-InfisicalCertificateAuthority` | Creates a root or intermediate internal CA, signing a subordinate with its parent so it can issue. |
+| `Set-InfisicalCertificateAuthority` | Renames an internal CA or changes its status. |
+| `Remove-InfisicalCertificateAuthority` | Deletes an internal CA. |
+| `New-InfisicalCertificatePolicy` | Creates a certificate policy constraining subject, SANs, key usages, and validity. |
+| `Set-InfisicalCertificatePolicy` | Updates a certificate policy; only supplied constraints are sent. |
+| `Remove-InfisicalCertificatePolicy` | Deletes a certificate policy. |
+| `New-InfisicalCertificateProfile` | Creates a certificate profile binding an issuing CA to a policy for enrollment. |
+| `Set-InfisicalCertificateProfile` | Updates a certificate profile. |
+| `Remove-InfisicalCertificateProfile` | Deletes a certificate profile. |
+| `New-InfisicalCertificateApplication` | Creates a certificate application and optionally attaches profiles. |
+| `Set-InfisicalCertificateApplication` | Renames an application, or attaches and detaches profiles. |
+| `Remove-InfisicalCertificateApplication` | Deletes a certificate application. |
+| `New-InfisicalPkiSubscriber` | Creates a PKI subscriber: one named identity with a fixed common name. |
+| `Set-InfisicalPkiSubscriber` | Updates a PKI subscriber. |
+| `Remove-InfisicalPkiSubscriber` | Deletes a PKI subscriber. |
+
+Constraint dictionaries take `Allowed`, `Required`, and `Denied` in whatever casing reads naturally — they reach the API lower-cased — and an empty list is omitted rather than sent as "allow nothing":
+
+```powershell
+$Root = New-InfisicalCertificateAuthority -Name 'root-ca' -Type Root -CommonName 'Contoso Root CA' -Organization 'Contoso' -Country 'US'
+$Ca = New-InfisicalCertificateAuthority -Name 'issuing-ca' -Type Intermediate -ParentCaId $Root.Id -CommonName 'Contoso Issuing CA'
+
+$Policy = New-InfisicalCertificatePolicy -Name 'server-auth' -MaxValidity '90d' `
+ -KeyAlgorithm 'RSA_2048','EC_secp384r1' `
+ -KeyUsage @{ Required = @('digital_signature','key_encipherment') } `
+ -ExtendedKeyUsage @{ Required = @('server_auth','client_auth') }
+
+$CertificateProfile = New-InfisicalCertificateProfile -Slug 'server-auth' -CertificatePolicyId $Policy.Id -CaId $Ca.Id -AutoRenew -RenewBeforeDays 14
+$Application = New-InfisicalCertificateApplication -Name 'platform' -ProfileId $CertificateProfile.Id
+```
+
+The intermediate comes back ready to issue: Infisical creates a subordinate pending a certificate, and `New-InfisicalCertificateAuthority` performs the remaining sequence — read the CSR, sign it with `-ParentCaId`, import the result.
### Process
| Cmdlet | Purpose |
diff --git a/build.ps1 b/build.ps1
index b560d8c..2b5e436 100644
--- a/build.ps1
+++ b/build.ps1
@@ -156,7 +156,22 @@ function Write-Manifest {
'Write-InfisicalScepMdmProfileToWmi',
'Start-InfisicalProcess',
'Get-InfisicalEnvironmentVariable',
- 'Get-InfisicalSANList'
+ 'Get-InfisicalSANList',
+ 'New-InfisicalCertificateAuthority',
+ 'Set-InfisicalCertificateAuthority',
+ 'Remove-InfisicalCertificateAuthority',
+ 'New-InfisicalCertificatePolicy',
+ 'Set-InfisicalCertificatePolicy',
+ 'Remove-InfisicalCertificatePolicy',
+ 'New-InfisicalCertificateProfile',
+ 'Set-InfisicalCertificateProfile',
+ 'Remove-InfisicalCertificateProfile',
+ 'New-InfisicalCertificateApplication',
+ 'Set-InfisicalCertificateApplication',
+ 'Remove-InfisicalCertificateApplication',
+ 'New-InfisicalPkiSubscriber',
+ 'Set-InfisicalPkiSubscriber',
+ 'Remove-InfisicalPkiSubscriber'
)
AliasesToExport = @()
VariablesToExport = @()
@@ -250,7 +265,7 @@ if (`$cmds.Count -eq 0) {
throw "No cmdlets were exported by the PSInfisicalAPI module."
}
-`$expectedCmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecret','New-InfisicalSecret','Update-InfisicalSecret','Remove-InfisicalSecret','Copy-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Import-InfisicalSecret','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder','Get-InfisicalTag','New-InfisicalTag','Update-InfisicalTag','Remove-InfisicalTag','Get-InfisicalOrganization','New-InfisicalOrganization','Update-InfisicalOrganization','Remove-InfisicalOrganization','Get-InfisicalSubOrganization','New-InfisicalSubOrganization','Update-InfisicalSubOrganization','Remove-InfisicalSubOrganization','Get-InfisicalCertificateAuthority','Get-InfisicalPkiSubscriber','Get-InfisicalCertificateProfile','Get-InfisicalCertificatePolicy','Get-InfisicalCertificate','Request-InfisicalCertificate','ConvertTo-InfisicalCertificate','Install-InfisicalCertificate','Uninstall-InfisicalCertificate','Export-InfisicalCertificate','Get-InfisicalCertificateApplication','Get-InfisicalCertificateApplicationEnrollment','New-InfisicalScepDynamicChallenge','Get-InfisicalScepMdmProfile','Export-InfisicalScepMdmProfile','Write-InfisicalScepMdmProfileToWmi','Start-InfisicalProcess','Get-InfisicalEnvironmentVariable','Get-InfisicalSANList')
+`$expectedCmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecret','New-InfisicalSecret','Update-InfisicalSecret','Remove-InfisicalSecret','Copy-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Import-InfisicalSecret','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder','Get-InfisicalTag','New-InfisicalTag','Update-InfisicalTag','Remove-InfisicalTag','Get-InfisicalOrganization','New-InfisicalOrganization','Update-InfisicalOrganization','Remove-InfisicalOrganization','Get-InfisicalSubOrganization','New-InfisicalSubOrganization','Update-InfisicalSubOrganization','Remove-InfisicalSubOrganization','Get-InfisicalCertificateAuthority','Get-InfisicalPkiSubscriber','Get-InfisicalCertificateProfile','Get-InfisicalCertificatePolicy','Get-InfisicalCertificate','Request-InfisicalCertificate','ConvertTo-InfisicalCertificate','Install-InfisicalCertificate','Uninstall-InfisicalCertificate','Export-InfisicalCertificate','Get-InfisicalCertificateApplication','Get-InfisicalCertificateApplicationEnrollment','New-InfisicalScepDynamicChallenge','Get-InfisicalScepMdmProfile','Export-InfisicalScepMdmProfile','Write-InfisicalScepMdmProfileToWmi','Start-InfisicalProcess','Get-InfisicalEnvironmentVariable','Get-InfisicalSANList','New-InfisicalCertificateAuthority','Set-InfisicalCertificateAuthority','Remove-InfisicalCertificateAuthority','New-InfisicalCertificatePolicy','Set-InfisicalCertificatePolicy','Remove-InfisicalCertificatePolicy','New-InfisicalCertificateProfile','Set-InfisicalCertificateProfile','Remove-InfisicalCertificateProfile','New-InfisicalCertificateApplication','Set-InfisicalCertificateApplication','Remove-InfisicalCertificateApplication','New-InfisicalPkiSubscriber','Set-InfisicalPkiSubscriber','Remove-InfisicalPkiSubscriber')
foreach (`$expected in `$expectedCmds) {
if (-not (Get-Command -Name `$expected -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) {
throw "Cmdlet not found: `$expected"
diff --git a/src/PSInfisicalAPI.Tests/PkiWriteCmdletTests.cs b/src/PSInfisicalAPI.Tests/PkiWriteCmdletTests.cs
new file mode 100644
index 0000000..c96f773
--- /dev/null
+++ b/src/PSInfisicalAPI.Tests/PkiWriteCmdletTests.cs
@@ -0,0 +1,280 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Management.Automation;
+using System.Reflection;
+using Newtonsoft.Json;
+using Xunit;
+
+namespace PSInfisicalAPI.Tests
+{
+ ///
+ /// The create/update/delete cmdlets for Certificate Manager configuration. Their bodies are built from
+ /// caller-supplied dictionaries, so the conversion into the exact JSON Infisical's schemas accept is what
+ /// these pin.
+ ///
+ public class PkiWriteCmdletTests
+ {
+ private static readonly Assembly ModuleAssembly = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly;
+
+ private static readonly string[] WriteCmdletTypes = new[]
+ {
+ "NewInfisicalCertificateAuthorityCmdlet", "SetInfisicalCertificateAuthorityCmdlet", "RemoveInfisicalCertificateAuthorityCmdlet",
+ "NewInfisicalCertificatePolicyCmdlet", "SetInfisicalCertificatePolicyCmdlet", "RemoveInfisicalCertificatePolicyCmdlet",
+ "NewInfisicalCertificateProfileCmdlet", "SetInfisicalCertificateProfileCmdlet", "RemoveInfisicalCertificateProfileCmdlet",
+ "NewInfisicalCertificateApplicationCmdlet", "SetInfisicalCertificateApplicationCmdlet", "RemoveInfisicalCertificateApplicationCmdlet",
+ "NewInfisicalPkiSubscriberCmdlet", "SetInfisicalPkiSubscriberCmdlet", "RemoveInfisicalPkiSubscriberCmdlet"
+ };
+
+ private static Dictionary ToJsonObject(IDictionary source)
+ {
+ Type baseType = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.InfisicalPkiWriteCmdletBase", true);
+ MethodInfo method = baseType.GetMethod("ToJsonObject", BindingFlags.NonPublic | BindingFlags.Static);
+ Assert.NotNull(method);
+ return (Dictionary)method.Invoke(null, new object[] { source });
+ }
+
+ private static List> ToJsonObjectList(IEnumerable source)
+ {
+ Type baseType = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.InfisicalPkiWriteCmdletBase", true);
+ MethodInfo method = baseType.GetMethod("ToJsonObjectList", BindingFlags.NonPublic | BindingFlags.Static);
+ Assert.NotNull(method);
+ return (List>)method.Invoke(null, new object[] { source });
+ }
+
+ [Fact]
+ public void Every_Write_Cmdlet_Declares_ShouldProcess()
+ {
+ List offenders = new List();
+
+ foreach (string typeName in WriteCmdletTypes)
+ {
+ Type type = ModuleAssembly.GetType(string.Concat("PSInfisicalAPI.Cmdlets.", typeName), true);
+
+ bool supportsShouldProcess = false;
+ foreach (CustomAttributeData attribute in type.GetCustomAttributesData())
+ {
+ if (attribute.AttributeType != typeof(CmdletAttribute)) { continue; }
+ foreach (CustomAttributeNamedArgument named in attribute.NamedArguments)
+ {
+ if (named.MemberName == "SupportsShouldProcess" && (bool)named.TypedValue.Value) { supportsShouldProcess = true; }
+ }
+ }
+
+ if (!supportsShouldProcess) { offenders.Add(typeName); }
+ }
+
+ Assert.Empty(offenders);
+ }
+
+ [Fact]
+ public void Every_Remove_Cmdlet_Defaults_To_High_Confirm_Impact()
+ {
+ List offenders = new List();
+
+ foreach (string typeName in WriteCmdletTypes)
+ {
+ if (!typeName.StartsWith("Remove", StringComparison.Ordinal)) { continue; }
+
+ Type type = ModuleAssembly.GetType(string.Concat("PSInfisicalAPI.Cmdlets.", typeName), true);
+
+ bool high = false;
+ foreach (CustomAttributeData attribute in type.GetCustomAttributesData())
+ {
+ if (attribute.AttributeType != typeof(CmdletAttribute)) { continue; }
+ foreach (CustomAttributeNamedArgument named in attribute.NamedArguments)
+ {
+ if (named.MemberName == "ConfirmImpact" && (ConfirmImpact)named.TypedValue.Value == ConfirmImpact.High) { high = true; }
+ }
+ }
+
+ if (!high) { offenders.Add(typeName); }
+ }
+
+ Assert.Empty(offenders);
+ }
+
+ [Fact]
+ public void Every_Write_Cmdlet_Resolves_The_Project_Instead_Of_Requiring_It()
+ {
+ List offenders = new List();
+
+ foreach (string typeName in WriteCmdletTypes)
+ {
+ Type type = ModuleAssembly.GetType(string.Concat("PSInfisicalAPI.Cmdlets.", typeName), true);
+ PropertyInfo projectId = type.GetProperty("ProjectId");
+ Assert.True(projectId != null, string.Concat(typeName, " has no ProjectId property"));
+
+ foreach (CustomAttributeData attribute in projectId.GetCustomAttributesData())
+ {
+ if (attribute.AttributeType != typeof(ParameterAttribute)) { continue; }
+ foreach (CustomAttributeNamedArgument named in attribute.NamedArguments)
+ {
+ if (named.MemberName == "Mandatory" && (bool)named.TypedValue.Value) { offenders.Add(typeName); }
+ }
+ }
+ }
+
+ Assert.Empty(offenders);
+ }
+
+ [Theory]
+ // PowerShell callers capitalise hashtable keys; Infisical's schema is lower case.
+ [InlineData("Required", "required")]
+ [InlineData("required", "required")]
+ [InlineData("Allowed", "allowed")]
+ [InlineData("DENIED", "denied")]
+ public void Constraint_Keys_Are_Emitted_In_The_Casing_The_Api_Requires(string supplied, string expected)
+ {
+ Hashtable source = new Hashtable { { supplied, new[] { "server_auth" } } };
+ Dictionary result = ToJsonObject(source);
+
+ Assert.True(result.ContainsKey(expected), string.Concat("expected key '", expected, "' but got: ", JsonConvert.SerializeObject(result)));
+ }
+
+ [Fact]
+ public void Non_Constraint_Keys_Keep_Their_Camel_Case()
+ {
+ // Field names elsewhere in the body are camelCase and supplied verbatim; lowercasing them would
+ // silently drop settings the API would no longer recognise.
+ Hashtable source = new Hashtable { { "ttlDays", 90 }, { "keyAlgorithm", "RSA_2048" }, { "isCA", "denied" }, { "maxPathLength", 0 } };
+ Dictionary result = ToJsonObject(source);
+
+ Assert.True(result.ContainsKey("ttlDays"));
+ Assert.True(result.ContainsKey("keyAlgorithm"));
+ Assert.True(result.ContainsKey("isCA"));
+ Assert.True(result.ContainsKey("maxPathLength"));
+ }
+
+ [Fact]
+ public void An_Empty_Collection_Is_Dropped_Rather_Than_Sent_As_Allow_Nothing()
+ {
+ Hashtable source = new Hashtable { { "Allowed", new string[0] }, { "Required", new[] { "server_auth" } } };
+ Dictionary result = ToJsonObject(source);
+
+ Assert.False(result.ContainsKey("allowed"));
+ Assert.True(result.ContainsKey("required"));
+ }
+
+ [Fact]
+ public void A_Wholly_Empty_Constraint_Becomes_Null_So_It_Is_Omitted()
+ {
+ Assert.Null(ToJsonObject(new Hashtable()));
+ Assert.Null(ToJsonObject(new Hashtable { { "Allowed", new string[0] } }));
+ Assert.Null(ToJsonObject(null));
+ }
+
+ [Fact]
+ public void A_Constraint_List_Drops_Entries_That_Constrain_Nothing()
+ {
+ // An entry carrying only "type" is rejected by Infisical's refinement, and is what a caller writes
+ // when they meant to leave that dimension alone.
+ List source = new List
+ {
+ new Hashtable { { "Type", "dns_name" }, { "Allowed", new[] { "*.contoso.com" } } },
+ new Hashtable { { "Type", "ip_address" }, { "Allowed", new string[0] } }
+ };
+
+ List> result = ToJsonObjectList(source);
+
+ Dictionary only = Assert.Single(result);
+ Assert.Equal("dns_name", only["type"]);
+ Assert.True(only.ContainsKey("allowed"));
+ }
+
+ [Fact]
+ public void Nested_Dictionaries_Survive_Conversion()
+ {
+ Hashtable source = new Hashtable
+ {
+ { "outer", new Hashtable { { "inner", new Hashtable { { "Required", new[] { "a" } } } } } }
+ };
+
+ Dictionary result = ToJsonObject(source);
+ string json = JsonConvert.SerializeObject(result);
+
+ Assert.Contains("\"outer\"", json);
+ Assert.Contains("\"inner\"", json);
+ Assert.Contains("\"required\"", json);
+ }
+
+ [Fact]
+ public void Profile_Enrollment_Config_Is_Routed_To_The_Block_For_Its_Type()
+ {
+ Type cmdletType = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.NewInfisicalCertificateProfileCmdlet", true);
+ MethodInfo build = cmdletType.GetMethod("BuildRequest", BindingFlags.NonPublic | BindingFlags.Static);
+ Assert.NotNull(build);
+
+ Hashtable config = new Hashtable { { "passphrase", "secret" } };
+
+ object estRequest = build.Invoke(null, new object[] { "slug", null, "ca", "policy", "est", "ca", false, null, null, config, false });
+ Assert.NotNull(estRequest.GetType().GetProperty("EstConfig").GetValue(estRequest));
+ Assert.Null(estRequest.GetType().GetProperty("ApiConfig").GetValue(estRequest));
+
+ object scepRequest = build.Invoke(null, new object[] { "slug", null, "ca", "policy", "scep", "ca", false, null, null, config, false });
+ Assert.NotNull(scepRequest.GetType().GetProperty("ScepConfig").GetValue(scepRequest));
+
+ object apiRequest = build.Invoke(null, new object[] { "slug", null, "ca", "policy", "api", "ca", true, 14, null, null, true });
+ object apiConfig = apiRequest.GetType().GetProperty("ApiConfig").GetValue(apiRequest);
+ Assert.NotNull(apiConfig);
+
+ Dictionary typed = (Dictionary)apiConfig;
+ Assert.Equal(true, typed["autoRenew"]);
+ Assert.Equal(14, typed["renewBeforeDays"]);
+ }
+
+ [Fact]
+ public void Auto_Renew_Is_Only_Sent_When_The_Caller_Asked_For_It()
+ {
+ // A switch parameter is always false when absent, so sending it unconditionally would silently
+ // disable renewal on an update that never mentioned it.
+ Type cmdletType = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.NewInfisicalCertificateProfileCmdlet", true);
+ MethodInfo build = cmdletType.GetMethod("BuildRequest", BindingFlags.NonPublic | BindingFlags.Static);
+
+ object unbound = build.Invoke(null, new object[] { "slug", null, "ca", "policy", "api", "ca", false, null, null, null, false });
+ Assert.Null(unbound.GetType().GetProperty("ApiConfig").GetValue(unbound));
+
+ object bound = build.Invoke(null, new object[] { "slug", null, "ca", "policy", "api", "ca", false, null, null, null, true });
+ Dictionary config = (Dictionary)bound.GetType().GetProperty("ApiConfig").GetValue(bound);
+ Assert.NotNull(config);
+ Assert.Equal(false, config["autoRenew"]);
+ }
+
+ [Fact]
+ public void Every_Management_Endpoint_Is_Registered()
+ {
+ string[] names = new[]
+ {
+ PSInfisicalAPI.Endpoints.InfisicalEndpointNames.CreateInternalCertificateAuthority,
+ PSInfisicalAPI.Endpoints.InfisicalEndpointNames.UpdateInternalCertificateAuthority,
+ PSInfisicalAPI.Endpoints.InfisicalEndpointNames.DeleteInternalCertificateAuthority,
+ PSInfisicalAPI.Endpoints.InfisicalEndpointNames.GetCertificateAuthorityCsr,
+ PSInfisicalAPI.Endpoints.InfisicalEndpointNames.SignIntermediateCertificateAuthority,
+ PSInfisicalAPI.Endpoints.InfisicalEndpointNames.ImportCertificateAuthorityCertificate,
+ PSInfisicalAPI.Endpoints.InfisicalEndpointNames.CreateCertificatePolicy,
+ PSInfisicalAPI.Endpoints.InfisicalEndpointNames.UpdateCertificatePolicy,
+ PSInfisicalAPI.Endpoints.InfisicalEndpointNames.DeleteCertificatePolicy,
+ PSInfisicalAPI.Endpoints.InfisicalEndpointNames.CreateCertificateProfile,
+ PSInfisicalAPI.Endpoints.InfisicalEndpointNames.UpdateCertificateProfile,
+ PSInfisicalAPI.Endpoints.InfisicalEndpointNames.DeleteCertificateProfile,
+ PSInfisicalAPI.Endpoints.InfisicalEndpointNames.CreateCertificateApplication,
+ PSInfisicalAPI.Endpoints.InfisicalEndpointNames.UpdateCertificateApplication,
+ PSInfisicalAPI.Endpoints.InfisicalEndpointNames.DeleteCertificateApplication,
+ PSInfisicalAPI.Endpoints.InfisicalEndpointNames.AddCertificateApplicationProfiles,
+ PSInfisicalAPI.Endpoints.InfisicalEndpointNames.RemoveCertificateApplicationProfile,
+ PSInfisicalAPI.Endpoints.InfisicalEndpointNames.CreatePkiSubscriber,
+ PSInfisicalAPI.Endpoints.InfisicalEndpointNames.UpdatePkiSubscriber,
+ PSInfisicalAPI.Endpoints.InfisicalEndpointNames.DeletePkiSubscriber
+ };
+
+ foreach (string name in names)
+ {
+ IReadOnlyList candidates =
+ PSInfisicalAPI.Endpoints.InfisicalEndpointRegistry.GetCandidates(name);
+
+ Assert.True(candidates.Count > 0, string.Concat(name, " is not registered"));
+ Assert.All(candidates, c => Assert.True(c.RequiresAuthorization, string.Concat(name, " should require authorization")));
+ }
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI/Cmdlets/CertificateApplicationWriteCmdlets.cs b/src/PSInfisicalAPI/Cmdlets/CertificateApplicationWriteCmdlets.cs
new file mode 100644
index 0000000..9f75c51
--- /dev/null
+++ b/src/PSInfisicalAPI/Cmdlets/CertificateApplicationWriteCmdlets.cs
@@ -0,0 +1,159 @@
+using System;
+using System.Management.Automation;
+using PSInfisicalAPI.Connections;
+using PSInfisicalAPI.Models;
+using PSInfisicalAPI.Pki;
+
+namespace PSInfisicalAPI.Cmdlets
+{
+ ///
+ /// Creates a certificate application: the grouping the Infisical console presents profiles, members, and
+ /// certificates under.
+ ///
+ [Cmdlet(VerbsCommon.New, "InfisicalCertificateApplication", SupportsShouldProcess = true)]
+ [OutputType(typeof(InfisicalCertificateApplication))]
+ public sealed class NewInfisicalCertificateApplicationCmdlet : InfisicalPkiWriteCmdletBase
+ {
+ private const string Component = "NewInfisicalCertificateApplicationCmdlet";
+
+ [Parameter(Mandatory = true, Position = 0)] public string Name { get; set; }
+ [Parameter] public string ProjectId { get; set; }
+ [Parameter] public string Description { get; set; }
+
+ /// Certificate profiles to attach on creation.
+ [Parameter] public string[] ProfileId { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ try
+ {
+ InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+ ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
+ if (string.IsNullOrEmpty(ProjectId)) { return; }
+
+ if (!ShouldProcess(Name, "Create certificate application")) { return; }
+
+ InfisicalCertificateApplicationWriteRequestDto request = new InfisicalCertificateApplicationWriteRequestDto
+ {
+ Name = Name,
+ Description = Description,
+ ProfileIds = ToStringList(ProfileId)
+ };
+
+ InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
+ WriteObject(client.WriteCertificateApplication(connection, ProjectId, null, request));
+ }
+ catch (Exception exception)
+ {
+ WriteErrorForException(Component, "CreateCertificateApplication", exception);
+ }
+ }
+ }
+
+ ///
+ /// Renames a certificate application or changes its description, and attaches or detaches profiles.
+ ///
+ [Cmdlet(VerbsCommon.Set, "InfisicalCertificateApplication", SupportsShouldProcess = true)]
+ [OutputType(typeof(InfisicalCertificateApplication))]
+ public sealed class SetInfisicalCertificateApplicationCmdlet : InfisicalPkiWriteCmdletBase
+ {
+ private const string Component = "SetInfisicalCertificateApplicationCmdlet";
+
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
+ [Alias("Id")]
+ public string ApplicationId { get; set; }
+
+ [Parameter] public string ProjectId { get; set; }
+ [Parameter] public string Name { get; set; }
+ [Parameter] public string Description { get; set; }
+
+ /// Certificate profiles to attach. Profiles already attached are left alone.
+ [Parameter] public string[] AddProfileId { get; set; }
+
+ /// Certificate profiles to detach.
+ [Parameter] public string[] RemoveProfileId { get; set; }
+
+ [Parameter] public SwitchParameter PassThru { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ try
+ {
+ InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+ ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
+ if (string.IsNullOrEmpty(ProjectId)) { return; }
+
+ if (!ShouldProcess(ApplicationId, "Update certificate application")) { return; }
+
+ InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
+ InfisicalCertificateApplication updated = null;
+
+ // The application record and its profile attachments are separate endpoints, so the name and
+ // description update is skipped entirely when only profiles were supplied.
+ if (!string.IsNullOrEmpty(Name) || !string.IsNullOrEmpty(Description))
+ {
+ InfisicalCertificateApplicationWriteRequestDto request = new InfisicalCertificateApplicationWriteRequestDto
+ {
+ Name = Name,
+ Description = Description
+ };
+
+ updated = client.WriteCertificateApplication(connection, ProjectId, ApplicationId, request);
+ }
+
+ if (ToStringList(AddProfileId) != null)
+ {
+ client.AddCertificateApplicationProfiles(connection, ProjectId, ApplicationId, ToStringList(AddProfileId));
+ }
+
+ foreach (string profileId in ToStringList(RemoveProfileId) ?? new System.Collections.Generic.List())
+ {
+ client.RemoveCertificateApplicationProfile(connection, ProjectId, ApplicationId, profileId);
+ }
+
+ if (PassThru.IsPresent && updated != null) { WriteObject(updated); }
+ }
+ catch (Exception exception)
+ {
+ WriteErrorForException(Component, "UpdateCertificateApplication", exception);
+ }
+ }
+ }
+
+ ///
+ /// Deletes a certificate application.
+ ///
+ [Cmdlet(VerbsCommon.Remove, "InfisicalCertificateApplication", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
+ public sealed class RemoveInfisicalCertificateApplicationCmdlet : InfisicalPkiWriteCmdletBase
+ {
+ private const string Component = "RemoveInfisicalCertificateApplicationCmdlet";
+
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
+ [Alias("Id")]
+ public string ApplicationId { get; set; }
+
+ [Parameter] public string ProjectId { get; set; }
+ [Parameter] public SwitchParameter PassThru { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ try
+ {
+ InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+ ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
+ if (string.IsNullOrEmpty(ProjectId)) { return; }
+
+ if (!ShouldProcess(ApplicationId, "Delete certificate application")) { return; }
+
+ InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
+ client.DeleteCertificateApplication(connection, ProjectId, ApplicationId);
+
+ if (PassThru.IsPresent) { WriteObject(ApplicationId); }
+ }
+ catch (Exception exception)
+ {
+ WriteErrorForException(Component, "DeleteCertificateApplication", exception);
+ }
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI/Cmdlets/CertificateAuthorityWriteCmdlets.cs b/src/PSInfisicalAPI/Cmdlets/CertificateAuthorityWriteCmdlets.cs
new file mode 100644
index 0000000..31f5e32
--- /dev/null
+++ b/src/PSInfisicalAPI/Cmdlets/CertificateAuthorityWriteCmdlets.cs
@@ -0,0 +1,183 @@
+using System;
+using System.Management.Automation;
+using PSInfisicalAPI.Connections;
+using PSInfisicalAPI.Models;
+using PSInfisicalAPI.Pki;
+
+namespace PSInfisicalAPI.Cmdlets
+{
+ ///
+ /// Creates an internal certificate authority, signing a subordinate with its parent so it comes back ready
+ /// to issue.
+ ///
+ [Cmdlet(VerbsCommon.New, "InfisicalCertificateAuthority", SupportsShouldProcess = true)]
+ [OutputType(typeof(InfisicalCertificateAuthority))]
+ public sealed class NewInfisicalCertificateAuthorityCmdlet : InfisicalPkiWriteCmdletBase
+ {
+ private const string Component = "NewInfisicalCertificateAuthorityCmdlet";
+
+ [Parameter(Mandatory = true, Position = 0)] public string Name { get; set; }
+
+ [Parameter(Mandatory = true)]
+ [ValidateSet("Root", "Intermediate")]
+ public string Type { get; set; }
+
+ [Parameter(Mandatory = true)] public string CommonName { get; set; }
+
+ /// Required for -Type Intermediate: the authority that signs this one.
+ [Parameter] public string ParentCaId { get; set; }
+
+ [Parameter] public string ProjectId { get; set; }
+ [Parameter] public string Organization { get; set; }
+ [Parameter] public string OrganizationalUnit { get; set; }
+ [Parameter] public string Country { get; set; }
+ [Parameter] public string State { get; set; }
+ [Parameter] public string Locality { get; set; }
+ [Parameter] public string FriendlyName { get; set; }
+
+ [Parameter]
+ [ValidateSet("RSA_2048", "RSA_3072", "RSA_4096", "EC_prime256v1", "EC_secp384r1", "EC_secp521r1")]
+ public string KeyAlgorithm { get; set; } = "RSA_2048";
+
+ /// Expiry. Defaults to ten years for a root and five for a subordinate.
+ [Parameter] public DateTimeOffset? NotAfter { get; set; }
+
+ [Parameter] public DateTimeOffset? NotBefore { get; set; }
+
+ /// Subordinate authorities permitted beneath this one. Defaults to 1 for a root, 0 otherwise.
+ [Parameter] public int? MaxPathLength { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ try
+ {
+ InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+ ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
+ if (string.IsNullOrEmpty(ProjectId)) { return; }
+
+ bool isRoot = string.Equals(Type, "Root", StringComparison.OrdinalIgnoreCase);
+ if (!isRoot && string.IsNullOrEmpty(ParentCaId))
+ {
+ throw new PSInfisicalAPI.Errors.InfisicalConfigurationException(
+ "-ParentCaId is required for an intermediate certificate authority; it names the authority that signs this one.");
+ }
+
+ if (!ShouldProcess(Name, string.Concat("Create ", Type.ToLowerInvariant(), " certificate authority"))) { return; }
+
+ DateTimeOffset expiry = NotAfter ?? DateTimeOffset.UtcNow.AddYears(isRoot ? 10 : 5);
+ int pathLength = MaxPathLength ?? (isRoot ? 1 : 0);
+
+ InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
+
+ InfisicalCertificateAuthority created = client.CreateInternalCertificateAuthority(
+ connection, ProjectId, Name, isRoot ? "root" : "intermediate", CommonName,
+ Organization, OrganizationalUnit, Country, State, Locality, KeyAlgorithm, FriendlyName,
+ ToApiTimestamp(NotBefore), ToApiTimestamp(expiry), pathLength);
+
+ if (created == null)
+ {
+ throw new PSInfisicalAPI.Errors.InfisicalApiException("Creating the certificate authority returned no record.");
+ }
+
+ if (!isRoot)
+ {
+ // Infisical creates a subordinate pending a certificate; without this it exists but cannot
+ // sign anything.
+ Logger.Information(Component, string.Concat("Signing '", Name, "' with parent certificate authority '", ParentCaId, "'."));
+ client.CompleteSubordinateCertificateAuthority(connection, ProjectId, created.Id, ParentCaId, ToApiTimestamp(expiry), pathLength);
+
+ InfisicalPkiClient readClient = new InfisicalPkiClient(HttpClient, Logger);
+ InfisicalCertificateAuthority refreshed = readClient.GetInternalCertificateAuthority(connection, created.Id, ProjectId);
+ if (refreshed != null) { created = refreshed; }
+ }
+
+ WriteObject(created);
+ }
+ catch (Exception exception)
+ {
+ WriteErrorForException(Component, "CreateCertificateAuthority", exception);
+ }
+ }
+ }
+
+ ///
+ /// Renames an internal certificate authority or changes its status.
+ ///
+ [Cmdlet(VerbsCommon.Set, "InfisicalCertificateAuthority", SupportsShouldProcess = true)]
+ [OutputType(typeof(InfisicalCertificateAuthority))]
+ public sealed class SetInfisicalCertificateAuthorityCmdlet : InfisicalPkiWriteCmdletBase
+ {
+ private const string Component = "SetInfisicalCertificateAuthorityCmdlet";
+
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
+ [Alias("Id")]
+ public string CaId { get; set; }
+
+ [Parameter] public string ProjectId { get; set; }
+ [Parameter] public string Name { get; set; }
+
+ [Parameter]
+ [ValidateSet("active", "disabled")]
+ public string Status { get; set; }
+
+ [Parameter] public SwitchParameter PassThru { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ try
+ {
+ InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+ ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
+ if (string.IsNullOrEmpty(ProjectId)) { return; }
+
+ if (!ShouldProcess(CaId, "Update certificate authority")) { return; }
+
+ InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
+ InfisicalCertificateAuthority updated = client.UpdateInternalCertificateAuthority(connection, ProjectId, CaId, Name, Status);
+
+ if (PassThru.IsPresent) { WriteObject(updated); }
+ }
+ catch (Exception exception)
+ {
+ WriteErrorForException(Component, "UpdateCertificateAuthority", exception);
+ }
+ }
+ }
+
+ ///
+ /// Deletes an internal certificate authority.
+ ///
+ [Cmdlet(VerbsCommon.Remove, "InfisicalCertificateAuthority", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
+ public sealed class RemoveInfisicalCertificateAuthorityCmdlet : InfisicalPkiWriteCmdletBase
+ {
+ private const string Component = "RemoveInfisicalCertificateAuthorityCmdlet";
+
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
+ [Alias("Id")]
+ public string CaId { get; set; }
+
+ [Parameter] public string ProjectId { get; set; }
+ [Parameter] public SwitchParameter PassThru { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ try
+ {
+ InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+ ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
+ if (string.IsNullOrEmpty(ProjectId)) { return; }
+
+ if (!ShouldProcess(CaId, "Delete certificate authority")) { return; }
+
+ InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
+ client.DeleteInternalCertificateAuthority(connection, ProjectId, CaId);
+
+ if (PassThru.IsPresent) { WriteObject(CaId); }
+ }
+ catch (Exception exception)
+ {
+ WriteErrorForException(Component, "DeleteCertificateAuthority", exception);
+ }
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI/Cmdlets/CertificatePolicyWriteCmdlets.cs b/src/PSInfisicalAPI/Cmdlets/CertificatePolicyWriteCmdlets.cs
new file mode 100644
index 0000000..67619f3
--- /dev/null
+++ b/src/PSInfisicalAPI/Cmdlets/CertificatePolicyWriteCmdlets.cs
@@ -0,0 +1,193 @@
+using System;
+using System.Collections;
+using System.Management.Automation;
+using PSInfisicalAPI.Connections;
+using PSInfisicalAPI.Models;
+using PSInfisicalAPI.Pki;
+
+namespace PSInfisicalAPI.Cmdlets
+{
+ ///
+ /// Creates a certificate policy: the constraints a profile issues within.
+ ///
+ [Cmdlet(VerbsCommon.New, "InfisicalCertificatePolicy", SupportsShouldProcess = true)]
+ [OutputType(typeof(InfisicalCertificatePolicy))]
+ public sealed class NewInfisicalCertificatePolicyCmdlet : InfisicalPkiWriteCmdletBase
+ {
+ private const string Component = "NewInfisicalCertificatePolicyCmdlet";
+
+ [Parameter(Mandatory = true, Position = 0)] public string Name { get; set; }
+ [Parameter] public string ProjectId { get; set; }
+ [Parameter] public string Description { get; set; }
+
+ /// Maximum certificate lifetime, for example '90d', '12m', or '1y'.
+ [Parameter] public string MaxValidity { get; set; }
+
+ /// Permitted key algorithms, for example RSA_2048 or EC_secp384r1.
+ [Parameter] public string[] KeyAlgorithm { get; set; }
+
+ /// Permitted signature algorithms.
+ [Parameter] public string[] SignatureAlgorithm { get; set; }
+
+ /// Key usage constraint, as @{ Required = @('digital_signature'); Denied = @(...) }.
+ [Parameter] public IDictionary KeyUsage { get; set; }
+
+ /// Extended key usage constraint, as @{ Required = @('server_auth','client_auth') }.
+ [Parameter] public IDictionary ExtendedKeyUsage { get; set; }
+
+ /// Subject attribute constraints, as @( @{ Type = 'organization'; Allowed = @('Contoso') } ).
+ [Parameter] public IDictionary[] Subject { get; set; }
+
+ /// Subject alternative name constraints, as @( @{ Type = 'dns_name'; Allowed = @('*.contoso.com') } ).
+ [Parameter] public IDictionary[] SubjectAlternativeName { get; set; }
+
+ /// Basic constraints, as @{ isCA = 'denied'; maxPathLength = 0 }.
+ [Parameter] public IDictionary BasicConstraints { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ try
+ {
+ InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+ ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
+ if (string.IsNullOrEmpty(ProjectId)) { return; }
+
+ if (!ShouldProcess(Name, "Create certificate policy")) { return; }
+
+ InfisicalCertificatePolicyWriteRequestDto request = new InfisicalCertificatePolicyWriteRequestDto
+ {
+ Name = Name,
+ Description = Description,
+ Subject = ToJsonObjectList(Subject),
+ Sans = ToJsonObjectList(SubjectAlternativeName),
+ KeyUsages = ToJsonObject(KeyUsage),
+ ExtendedKeyUsages = ToJsonObject(ExtendedKeyUsage),
+ BasicConstraints = ToJsonObject(BasicConstraints)
+ };
+
+ if (!string.IsNullOrEmpty(MaxValidity))
+ {
+ request.Validity = new System.Collections.Generic.Dictionary { { "max", MaxValidity } };
+ }
+
+ System.Collections.Generic.Dictionary algorithms = new System.Collections.Generic.Dictionary();
+ if (ToStringList(KeyAlgorithm) != null) { algorithms["keyAlgorithm"] = ToStringList(KeyAlgorithm); }
+ if (ToStringList(SignatureAlgorithm) != null) { algorithms["signature"] = ToStringList(SignatureAlgorithm); }
+ if (algorithms.Count > 0) { request.Algorithms = algorithms; }
+
+ InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
+ WriteObject(client.WriteCertificatePolicy(connection, ProjectId, null, request));
+ }
+ catch (Exception exception)
+ {
+ WriteErrorForException(Component, "CreateCertificatePolicy", exception);
+ }
+ }
+ }
+
+ ///
+ /// Updates a certificate policy. Only the supplied constraints are sent; the rest are left as they are.
+ ///
+ [Cmdlet(VerbsCommon.Set, "InfisicalCertificatePolicy", SupportsShouldProcess = true)]
+ [OutputType(typeof(InfisicalCertificatePolicy))]
+ public sealed class SetInfisicalCertificatePolicyCmdlet : InfisicalPkiWriteCmdletBase
+ {
+ private const string Component = "SetInfisicalCertificatePolicyCmdlet";
+
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
+ [Alias("Id")]
+ public string PolicyId { get; set; }
+
+ [Parameter] public string ProjectId { get; set; }
+ [Parameter] public string Name { get; set; }
+ [Parameter] public string Description { get; set; }
+ [Parameter] public string MaxValidity { get; set; }
+ [Parameter] public string[] KeyAlgorithm { get; set; }
+ [Parameter] public string[] SignatureAlgorithm { get; set; }
+ [Parameter] public IDictionary KeyUsage { get; set; }
+ [Parameter] public IDictionary ExtendedKeyUsage { get; set; }
+ [Parameter] public IDictionary[] Subject { get; set; }
+ [Parameter] public IDictionary[] SubjectAlternativeName { get; set; }
+ [Parameter] public IDictionary BasicConstraints { get; set; }
+ [Parameter] public SwitchParameter PassThru { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ try
+ {
+ InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+ ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
+ if (string.IsNullOrEmpty(ProjectId)) { return; }
+
+ if (!ShouldProcess(PolicyId, "Update certificate policy")) { return; }
+
+ InfisicalCertificatePolicyWriteRequestDto request = new InfisicalCertificatePolicyWriteRequestDto
+ {
+ Name = Name,
+ Description = Description,
+ Subject = ToJsonObjectList(Subject),
+ Sans = ToJsonObjectList(SubjectAlternativeName),
+ KeyUsages = ToJsonObject(KeyUsage),
+ ExtendedKeyUsages = ToJsonObject(ExtendedKeyUsage),
+ BasicConstraints = ToJsonObject(BasicConstraints)
+ };
+
+ if (!string.IsNullOrEmpty(MaxValidity))
+ {
+ request.Validity = new System.Collections.Generic.Dictionary { { "max", MaxValidity } };
+ }
+
+ System.Collections.Generic.Dictionary algorithms = new System.Collections.Generic.Dictionary();
+ if (ToStringList(KeyAlgorithm) != null) { algorithms["keyAlgorithm"] = ToStringList(KeyAlgorithm); }
+ if (ToStringList(SignatureAlgorithm) != null) { algorithms["signature"] = ToStringList(SignatureAlgorithm); }
+ if (algorithms.Count > 0) { request.Algorithms = algorithms; }
+
+ InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
+ InfisicalCertificatePolicy updated = client.WriteCertificatePolicy(connection, ProjectId, PolicyId, request);
+
+ if (PassThru.IsPresent) { WriteObject(updated); }
+ }
+ catch (Exception exception)
+ {
+ WriteErrorForException(Component, "UpdateCertificatePolicy", exception);
+ }
+ }
+ }
+
+ ///
+ /// Deletes a certificate policy.
+ ///
+ [Cmdlet(VerbsCommon.Remove, "InfisicalCertificatePolicy", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
+ public sealed class RemoveInfisicalCertificatePolicyCmdlet : InfisicalPkiWriteCmdletBase
+ {
+ private const string Component = "RemoveInfisicalCertificatePolicyCmdlet";
+
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
+ [Alias("Id")]
+ public string PolicyId { get; set; }
+
+ [Parameter] public string ProjectId { get; set; }
+ [Parameter] public SwitchParameter PassThru { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ try
+ {
+ InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+ ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
+ if (string.IsNullOrEmpty(ProjectId)) { return; }
+
+ if (!ShouldProcess(PolicyId, "Delete certificate policy")) { return; }
+
+ InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
+ client.DeleteCertificatePolicy(connection, ProjectId, PolicyId);
+
+ if (PassThru.IsPresent) { WriteObject(PolicyId); }
+ }
+ catch (Exception exception)
+ {
+ WriteErrorForException(Component, "DeleteCertificatePolicy", exception);
+ }
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI/Cmdlets/CertificateProfileWriteCmdlets.cs b/src/PSInfisicalAPI/Cmdlets/CertificateProfileWriteCmdlets.cs
new file mode 100644
index 0000000..bec8835
--- /dev/null
+++ b/src/PSInfisicalAPI/Cmdlets/CertificateProfileWriteCmdlets.cs
@@ -0,0 +1,213 @@
+using System;
+using System.Collections;
+using System.Management.Automation;
+using PSInfisicalAPI.Connections;
+using PSInfisicalAPI.Models;
+using PSInfisicalAPI.Pki;
+
+namespace PSInfisicalAPI.Cmdlets
+{
+ ///
+ /// Creates a certificate profile, binding an issuing certificate authority to a certificate policy and
+ /// exposing it for enrollment.
+ ///
+ [Cmdlet(VerbsCommon.New, "InfisicalCertificateProfile", SupportsShouldProcess = true)]
+ [OutputType(typeof(InfisicalCertificateProfile))]
+ public sealed class NewInfisicalCertificateProfileCmdlet : InfisicalPkiWriteCmdletBase
+ {
+ private const string Component = "NewInfisicalCertificateProfileCmdlet";
+
+ /// Lowercase letters, numbers, and hyphens only.
+ [Parameter(Mandatory = true, Position = 0)] public string Slug { get; set; }
+
+ [Parameter(Mandatory = true)] public string CertificatePolicyId { get; set; }
+ [Parameter] public string CaId { get; set; }
+ [Parameter] public string ProjectId { get; set; }
+ [Parameter] public string Description { get; set; }
+
+ [Parameter]
+ [ValidateSet("api", "est", "acme", "scep")]
+ public string EnrollmentType { get; set; } = "api";
+
+ [Parameter]
+ [ValidateSet("ca", "self-signed")]
+ public string IssuerType { get; set; } = "ca";
+
+ /// Renew issued certificates automatically.
+ [Parameter] public SwitchParameter AutoRenew { get; set; }
+
+ /// Days before expiry at which automatic renewal runs, 1 to 30.
+ [Parameter] public int? RenewBeforeDays { get; set; }
+
+ /// Issuance defaults, as @{ ttlDays = 90; keyAlgorithm = 'RSA_2048' }.
+ [Parameter] public IDictionary Defaults { get; set; }
+
+ /// Enrollment configuration for -EnrollmentType est, acme, or scep.
+ [Parameter] public IDictionary EnrollmentConfig { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ try
+ {
+ InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+ ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
+ if (string.IsNullOrEmpty(ProjectId)) { return; }
+
+ if (!ShouldProcess(Slug, "Create certificate profile")) { return; }
+
+ InfisicalCertificateProfileWriteRequestDto request = BuildRequest(
+ Slug, Description, CaId, CertificatePolicyId, EnrollmentType, IssuerType,
+ AutoRenew.IsPresent, RenewBeforeDays, Defaults, EnrollmentConfig,
+ MyInvocation.BoundParameters.ContainsKey("AutoRenew"));
+
+ InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
+ WriteObject(client.WriteCertificateProfile(connection, ProjectId, null, request));
+ }
+ catch (Exception exception)
+ {
+ WriteErrorForException(Component, "CreateCertificateProfile", exception);
+ }
+ }
+
+ ///
+ /// Routes the enrollment configuration to the block matching the enrollment type, so callers supply one
+ /// dictionary rather than choosing between four mutually exclusive parameters.
+ ///
+ internal static InfisicalCertificateProfileWriteRequestDto BuildRequest(
+ string slug, string description, string caId, string certificatePolicyId,
+ string enrollmentType, string issuerType, bool autoRenew, int? renewBeforeDays,
+ IDictionary defaults, IDictionary enrollmentConfig, bool autoRenewBound)
+ {
+ InfisicalCertificateProfileWriteRequestDto request = new InfisicalCertificateProfileWriteRequestDto
+ {
+ Slug = slug,
+ Description = description,
+ CaId = caId,
+ CertificatePolicyId = certificatePolicyId,
+ EnrollmentType = enrollmentType,
+ IssuerType = issuerType,
+ Defaults = ToJsonObject(defaults)
+ };
+
+ System.Collections.Generic.Dictionary config = ToJsonObject(enrollmentConfig);
+
+ if (string.Equals(enrollmentType, "est", StringComparison.OrdinalIgnoreCase)) { request.EstConfig = config; }
+ else if (string.Equals(enrollmentType, "acme", StringComparison.OrdinalIgnoreCase)) { request.AcmeConfig = config; }
+ else if (string.Equals(enrollmentType, "scep", StringComparison.OrdinalIgnoreCase)) { request.ScepConfig = config; }
+ else
+ {
+ if (config == null && (autoRenewBound || renewBeforeDays.HasValue))
+ {
+ config = new System.Collections.Generic.Dictionary();
+ }
+
+ if (config != null)
+ {
+ if (autoRenewBound) { config["autoRenew"] = autoRenew; }
+ if (renewBeforeDays.HasValue) { config["renewBeforeDays"] = renewBeforeDays.Value; }
+ }
+
+ request.ApiConfig = config;
+ }
+
+ return request;
+ }
+ }
+
+ ///
+ /// Updates a certificate profile. Only the supplied values are sent.
+ ///
+ [Cmdlet(VerbsCommon.Set, "InfisicalCertificateProfile", SupportsShouldProcess = true)]
+ [OutputType(typeof(InfisicalCertificateProfile))]
+ public sealed class SetInfisicalCertificateProfileCmdlet : InfisicalPkiWriteCmdletBase
+ {
+ private const string Component = "SetInfisicalCertificateProfileCmdlet";
+
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
+ [Alias("Id", "CertificateProfileId")]
+ public string ProfileId { get; set; }
+
+ [Parameter] public string ProjectId { get; set; }
+ [Parameter] public string Slug { get; set; }
+ [Parameter] public string Description { get; set; }
+ [Parameter] public string CaId { get; set; }
+ [Parameter] public string CertificatePolicyId { get; set; }
+
+ [Parameter]
+ [ValidateSet("api", "est", "acme", "scep")]
+ public string EnrollmentType { get; set; }
+
+ [Parameter] public SwitchParameter AutoRenew { get; set; }
+ [Parameter] public int? RenewBeforeDays { get; set; }
+ [Parameter] public IDictionary Defaults { get; set; }
+ [Parameter] public IDictionary EnrollmentConfig { get; set; }
+ [Parameter] public SwitchParameter PassThru { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ try
+ {
+ InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+ ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
+ if (string.IsNullOrEmpty(ProjectId)) { return; }
+
+ if (!ShouldProcess(ProfileId, "Update certificate profile")) { return; }
+
+ InfisicalCertificateProfileWriteRequestDto request = NewInfisicalCertificateProfileCmdlet.BuildRequest(
+ Slug, Description, CaId, CertificatePolicyId,
+ string.IsNullOrEmpty(EnrollmentType) ? "api" : EnrollmentType,
+ null, AutoRenew.IsPresent, RenewBeforeDays, Defaults, EnrollmentConfig,
+ MyInvocation.BoundParameters.ContainsKey("AutoRenew"));
+
+ // Only sent when the caller asked for it; the API keeps the stored value otherwise.
+ if (!MyInvocation.BoundParameters.ContainsKey("EnrollmentType")) { request.EnrollmentType = null; }
+
+ InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
+ InfisicalCertificateProfile updated = client.WriteCertificateProfile(connection, ProjectId, ProfileId, request);
+
+ if (PassThru.IsPresent) { WriteObject(updated); }
+ }
+ catch (Exception exception)
+ {
+ WriteErrorForException(Component, "UpdateCertificateProfile", exception);
+ }
+ }
+ }
+
+ ///
+ /// Deletes a certificate profile.
+ ///
+ [Cmdlet(VerbsCommon.Remove, "InfisicalCertificateProfile", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
+ public sealed class RemoveInfisicalCertificateProfileCmdlet : InfisicalPkiWriteCmdletBase
+ {
+ private const string Component = "RemoveInfisicalCertificateProfileCmdlet";
+
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
+ [Alias("Id", "CertificateProfileId")]
+ public string ProfileId { get; set; }
+
+ [Parameter] public string ProjectId { get; set; }
+ [Parameter] public SwitchParameter PassThru { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ try
+ {
+ InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+ ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
+ if (string.IsNullOrEmpty(ProjectId)) { return; }
+
+ if (!ShouldProcess(ProfileId, "Delete certificate profile")) { return; }
+
+ InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
+ client.DeleteCertificateProfile(connection, ProjectId, ProfileId);
+
+ if (PassThru.IsPresent) { WriteObject(ProfileId); }
+ }
+ catch (Exception exception)
+ {
+ WriteErrorForException(Component, "DeleteCertificateProfile", exception);
+ }
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI/Cmdlets/InfisicalPkiWriteCmdletBase.cs b/src/PSInfisicalAPI/Cmdlets/InfisicalPkiWriteCmdletBase.cs
new file mode 100644
index 0000000..0cae310
--- /dev/null
+++ b/src/PSInfisicalAPI/Cmdlets/InfisicalPkiWriteCmdletBase.cs
@@ -0,0 +1,164 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Management.Automation;
+
+namespace PSInfisicalAPI.Cmdlets
+{
+ ///
+ /// Shared plumbing for the cmdlets that create, change, or remove Certificate Manager configuration.
+ ///
+ /// Infisical's policy and profile bodies are deeply nested — allowed/required/denied constraint objects,
+ /// per-enrollment-type config blocks — and expressing every leaf as a parameter would produce cmdlets nobody
+ /// could read. Those structures are accepted as dictionaries instead, matching how -Subject and
+ /// -Metadata already work, and are converted here.
+ ///
+ ///
+ public abstract class InfisicalPkiWriteCmdletBase : InfisicalCmdletBase
+ {
+ ///
+ /// Converts a caller's dictionary into the plain string-keyed form the serializer emits as a JSON
+ /// object. Nested dictionaries and collections are converted too, so a hashtable of hashtables round
+ /// trips into the nested body Infisical expects.
+ ///
+ ///
+ /// The constraint vocabulary Infisical expects in lower case. PowerShell callers naturally capitalise
+ /// hashtable keys, so @{ Required = ... } has to reach the API as "required" or the request
+ /// is rejected for a missing constraint. Every other key is passed through untouched, since the rest of
+ /// the body uses camelCase field names the caller supplies verbatim.
+ ///
+ private static readonly Dictionary CanonicalKeys = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ { "allowed", "allowed" },
+ { "required", "required" },
+ { "denied", "denied" },
+ { "type", "type" }
+ };
+
+ internal static Dictionary ToJsonObject(IDictionary source)
+ {
+ if (source == null) { return null; }
+
+ Dictionary result = new Dictionary(StringComparer.Ordinal);
+ foreach (DictionaryEntry entry in source)
+ {
+ if (entry.Key == null) { continue; }
+ string key = Convert.ToString(entry.Key, CultureInfo.InvariantCulture);
+ if (string.IsNullOrWhiteSpace(key)) { continue; }
+
+ key = key.Trim();
+
+ string canonical;
+ if (CanonicalKeys.TryGetValue(key, out canonical)) { key = canonical; }
+
+ object value = ToJsonValue(entry.Value);
+
+ // An empty list is dropped rather than sent. "allowed": [] does not read as "unconstrained" to
+ // Infisical, it reads as "allow nothing", which is never what a caller writing @{ Allowed = @() }
+ // intends; omitting the key leaves that dimension genuinely unconstrained.
+ List