Compare commits
19 Commits
2026.7.30.2321
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 22c2db4a96 | |||
| b27fe6f002 | |||
| c75cc145cb | |||
| c40160f789 | |||
| 3015138484 | |||
| 74d22a941c | |||
| f2a1492b66 | |||
| 9f6e81607d | |||
| 139d1f3a05 | |||
| 62131e7501 | |||
| f1a6db14c7 | |||
| e7674af161 | |||
| a1e1d22e72 | |||
| 276958e3a8 | |||
| 633f40c1fa | |||
| 93b0cc1924 | |||
| d47a5af6b3 | |||
| d5c9eec3fb | |||
| 67cf0abac2 |
@@ -129,6 +129,7 @@ jobs:
|
|||||||
COMMIT_SHA: ${{ github.sha }}
|
COMMIT_SHA: ${{ github.sha }}
|
||||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||||
|
PR_BODY: ${{ github.event.pull_request.body }}
|
||||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||||
SERVER_URL: ${{ github.server_url }}
|
SERVER_URL: ${{ github.server_url }}
|
||||||
RUN_ID: ${{ github.run_id }}
|
RUN_ID: ${{ github.run_id }}
|
||||||
@@ -175,7 +176,17 @@ jobs:
|
|||||||
Write-Host " CHANGELOG section length: $($changelogSection.Length) chars"
|
Write-Host " CHANGELOG section length: $($changelogSection.Length) chars"
|
||||||
|
|
||||||
Write-Host "==> [4/8] Building release body"
|
Write-Host "==> [4/8] Building release body"
|
||||||
$changelogText = if ($changelogSection) { $changelogSection } else { '_No CHANGELOG section found for this version._' }
|
|
||||||
|
# The merged pull request description is the account written for humans, so it leads when present.
|
||||||
|
# Trailing co-author and generation trailers are dropped; they belong on the commit, not the release.
|
||||||
|
$prBody = ''
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($env:PR_BODY)) {
|
||||||
|
$prBody = ($env:PR_BODY -replace '(?m)^\s*(Co-Authored-By|Co-authored-by):.*$', '')
|
||||||
|
$prBody = ($prBody -replace '(?m)^\s*(Generated with|🤖 Generated with).*$', '')
|
||||||
|
$prBody = $prBody.Trim()
|
||||||
|
}
|
||||||
|
Write-Host " PR description length: $($prBody.Length) chars"
|
||||||
|
|
||||||
$sb = New-Object System.Text.StringBuilder
|
$sb = New-Object System.Text.StringBuilder
|
||||||
[void]$sb.AppendLine("**PSInfisicalAPI $($env:VERSION)**")
|
[void]$sb.AppendLine("**PSInfisicalAPI $($env:VERSION)**")
|
||||||
[void]$sb.AppendLine('')
|
[void]$sb.AppendLine('')
|
||||||
@@ -188,8 +199,33 @@ jobs:
|
|||||||
[void]$sb.AppendLine("| Merged PR | [#$($env:PR_NUMBER) $($env:PR_TITLE)]($prUrl) by @$($env:PR_AUTHOR) |")
|
[void]$sb.AppendLine("| Merged PR | [#$($env:PR_NUMBER) $($env:PR_TITLE)]($prUrl) by @$($env:PR_AUTHOR) |")
|
||||||
[void]$sb.AppendLine("| Workflow run | [$($env:RUN_ID)]($runUrl) |")
|
[void]$sb.AppendLine("| Workflow run | [$($env:RUN_ID)]($runUrl) |")
|
||||||
[void]$sb.AppendLine('')
|
[void]$sb.AppendLine('')
|
||||||
[void]$sb.AppendLine('## Changes')
|
|
||||||
[void]$sb.AppendLine($changelogText)
|
if ($prBody) {
|
||||||
|
# The description carries its own headings, so it is emitted without a wrapper.
|
||||||
|
[void]$sb.AppendLine($prBody)
|
||||||
|
|
||||||
|
# Folded so the release leads with the narrative but still records the changelog entry.
|
||||||
|
if ($changelogSection) {
|
||||||
|
[void]$sb.AppendLine('')
|
||||||
|
[void]$sb.AppendLine('<details>')
|
||||||
|
[void]$sb.AppendLine("<summary>CHANGELOG entry for $($env:VERSION)</summary>")
|
||||||
|
[void]$sb.AppendLine('')
|
||||||
|
[void]$sb.AppendLine($changelogSection)
|
||||||
|
[void]$sb.AppendLine('')
|
||||||
|
[void]$sb.AppendLine('</details>')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($changelogSection) {
|
||||||
|
[void]$sb.AppendLine('## Changes')
|
||||||
|
[void]$sb.AppendLine('')
|
||||||
|
[void]$sb.AppendLine($changelogSection)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[void]$sb.AppendLine('## Changes')
|
||||||
|
[void]$sb.AppendLine('')
|
||||||
|
[void]$sb.AppendLine('_No pull request description or CHANGELOG section found for this version._')
|
||||||
|
}
|
||||||
|
|
||||||
[void]$sb.AppendLine('')
|
[void]$sb.AppendLine('')
|
||||||
[void]$sb.AppendLine('## Install')
|
[void]$sb.AppendLine('## Install')
|
||||||
[void]$sb.AppendLine('```powershell')
|
[void]$sb.AppendLine('```powershell')
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ jobs:
|
|||||||
COMMIT_SHA: ${{ github.sha }}
|
COMMIT_SHA: ${{ github.sha }}
|
||||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||||
|
PR_BODY: ${{ github.event.pull_request.body }}
|
||||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||||
SERVER_URL: ${{ github.server_url }}
|
SERVER_URL: ${{ github.server_url }}
|
||||||
RUN_ID: ${{ github.run_id }}
|
RUN_ID: ${{ github.run_id }}
|
||||||
@@ -177,7 +178,17 @@ jobs:
|
|||||||
Write-Host " CHANGELOG section length: $($changelogSection.Length) chars"
|
Write-Host " CHANGELOG section length: $($changelogSection.Length) chars"
|
||||||
|
|
||||||
Write-Host "==> [4/8] Building release body"
|
Write-Host "==> [4/8] Building release body"
|
||||||
$changelogText = if ($changelogSection) { $changelogSection } else { '_No CHANGELOG section found for this version._' }
|
|
||||||
|
# The merged pull request description is the account written for humans, so it leads when present.
|
||||||
|
# Trailing co-author and generation trailers are dropped; they belong on the commit, not the release.
|
||||||
|
$prBody = ''
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($env:PR_BODY)) {
|
||||||
|
$prBody = ($env:PR_BODY -replace '(?m)^\s*(Co-Authored-By|Co-authored-by):.*$', '')
|
||||||
|
$prBody = ($prBody -replace '(?m)^\s*(Generated with|🤖 Generated with).*$', '')
|
||||||
|
$prBody = $prBody.Trim()
|
||||||
|
}
|
||||||
|
Write-Host " PR description length: $($prBody.Length) chars"
|
||||||
|
|
||||||
$sb = New-Object System.Text.StringBuilder
|
$sb = New-Object System.Text.StringBuilder
|
||||||
[void]$sb.AppendLine("**PSInfisicalAPI $($env:VERSION)**")
|
[void]$sb.AppendLine("**PSInfisicalAPI $($env:VERSION)**")
|
||||||
[void]$sb.AppendLine('')
|
[void]$sb.AppendLine('')
|
||||||
@@ -190,8 +201,33 @@ jobs:
|
|||||||
[void]$sb.AppendLine("| Merged PR | [#$($env:PR_NUMBER) $($env:PR_TITLE)]($prUrl) by @$($env:PR_AUTHOR) |")
|
[void]$sb.AppendLine("| Merged PR | [#$($env:PR_NUMBER) $($env:PR_TITLE)]($prUrl) by @$($env:PR_AUTHOR) |")
|
||||||
[void]$sb.AppendLine("| Workflow run | [$($env:RUN_ID)]($runUrl) |")
|
[void]$sb.AppendLine("| Workflow run | [$($env:RUN_ID)]($runUrl) |")
|
||||||
[void]$sb.AppendLine('')
|
[void]$sb.AppendLine('')
|
||||||
[void]$sb.AppendLine('## Changes')
|
|
||||||
[void]$sb.AppendLine($changelogText)
|
if ($prBody) {
|
||||||
|
# The description carries its own headings, so it is emitted without a wrapper.
|
||||||
|
[void]$sb.AppendLine($prBody)
|
||||||
|
|
||||||
|
# Folded so the release leads with the narrative but still records the changelog entry.
|
||||||
|
if ($changelogSection) {
|
||||||
|
[void]$sb.AppendLine('')
|
||||||
|
[void]$sb.AppendLine('<details>')
|
||||||
|
[void]$sb.AppendLine("<summary>CHANGELOG entry for $($env:VERSION)</summary>")
|
||||||
|
[void]$sb.AppendLine('')
|
||||||
|
[void]$sb.AppendLine($changelogSection)
|
||||||
|
[void]$sb.AppendLine('')
|
||||||
|
[void]$sb.AppendLine('</details>')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($changelogSection) {
|
||||||
|
[void]$sb.AppendLine('## Changes')
|
||||||
|
[void]$sb.AppendLine('')
|
||||||
|
[void]$sb.AppendLine($changelogSection)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[void]$sb.AppendLine('## Changes')
|
||||||
|
[void]$sb.AppendLine('')
|
||||||
|
[void]$sb.AppendLine('_No pull request description or CHANGELOG section found for this version._')
|
||||||
|
}
|
||||||
|
|
||||||
[void]$sb.AppendLine('')
|
[void]$sb.AppendLine('')
|
||||||
[void]$sb.AppendLine('## Install')
|
[void]$sb.AppendLine('## Install')
|
||||||
[void]$sb.AppendLine('```powershell')
|
[void]$sb.AppendLine('```powershell')
|
||||||
|
|||||||
+2
-1
@@ -30,5 +30,6 @@ TestResults/
|
|||||||
*.trx
|
*.trx
|
||||||
*.coverage
|
*.coverage
|
||||||
|
|
||||||
## Local helper scripts (not part of the module)
|
## Local helper scripts (not part of the module). Case-insensitive on Windows, so this also matches
|
||||||
|
## Scripts/ - shipped tooling lives in Tools/ instead.
|
||||||
scripts/
|
scripts/
|
||||||
|
|||||||
+420
-1140
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
|||||||
@{
|
@{
|
||||||
RootModule = 'PSInfisicalAPI.psm1'
|
RootModule = 'PSInfisicalAPI.psm1'
|
||||||
ModuleVersion = '2026.07.30.2305'
|
ModuleVersion = '2026.08.01.0245'
|
||||||
GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51'
|
GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51'
|
||||||
Author = 'Grace Solutions'
|
Author = 'Grace Solutions'
|
||||||
CompanyName = 'Grace Solutions'
|
CompanyName = 'Grace Solutions'
|
||||||
@@ -62,7 +62,22 @@
|
|||||||
'Write-InfisicalScepMdmProfileToWmi',
|
'Write-InfisicalScepMdmProfileToWmi',
|
||||||
'Start-InfisicalProcess',
|
'Start-InfisicalProcess',
|
||||||
'Get-InfisicalEnvironmentVariable',
|
'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 = @()
|
AliasesToExport = @()
|
||||||
VariablesToExport = @()
|
VariablesToExport = @()
|
||||||
@@ -74,7 +89,7 @@
|
|||||||
LicenseUri = 'https://www.gnu.org/licenses/agpl-3.0.html'
|
LicenseUri = 'https://www.gnu.org/licenses/agpl-3.0.html'
|
||||||
ProjectUri = 'https://prod.git.gracesolution.info/gsadmin/PSInfisicalAPI'
|
ProjectUri = 'https://prod.git.gracesolution.info/gsadmin/PSInfisicalAPI'
|
||||||
ReleaseNotes = 'See CHANGELOG.md in the project repository for release history.'
|
ReleaseNotes = 'See CHANGELOG.md in the project repository for release history.'
|
||||||
CommitHash = 'f65124fd9911'
|
CommitHash = 'b27fe6f002f4'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
@@ -1066,6 +1066,7 @@ $RemoveInfisicalTagResult = Remove-InfisicalTag @RemoveInfisicalTagParameters</d
|
|||||||
<maml:alertSet>
|
<maml:alertSet>
|
||||||
<maml:title>Notes</maml:title>
|
<maml:title>Notes</maml:title>
|
||||||
<maml:alert>
|
<maml:alert>
|
||||||
|
<maml:para>-ProjectId is optional. The Infisical console never asks which Certificate Manager project to use, because its resolver selects the single cert-manager project when an organization has exactly one; omitting -ProjectId applies the same rule and reports the resolved project on the verbose stream. Pass it explicitly when an organization has more than one, in which case the error lists the candidates. Note that a project contains applications: Get-InfisicalProject -Type cert-manager returns the project, while Get-InfisicalCertificateApplication returns the applications inside it.</maml:para>
|
||||||
<maml:para>ByID retrieval currently always resolves against the internal CA endpoint. CA Ids returned here are the values to pass on -CertificateAuthorityId to Request-InfisicalCertificate. The Type property distinguishes 'internal' from 'acme' when -Kind Any is used. Only CAs whose EnableDirectIssuance property is True can sign a CSR through -CertificateAuthorityId; the others must issue through Request-InfisicalCertificate -CertificateProfileId, which bypasses that check. EnableDirectIssuance is fixed at CA creation and appears in no Infisical update schema, so it cannot be toggled afterwards; a CA migrated from the older requireTemplateForIssuance column reads False permanently.</maml:para>
|
<maml:para>ByID retrieval currently always resolves against the internal CA endpoint. CA Ids returned here are the values to pass on -CertificateAuthorityId to Request-InfisicalCertificate. The Type property distinguishes 'internal' from 'acme' when -Kind Any is used. Only CAs whose EnableDirectIssuance property is True can sign a CSR through -CertificateAuthorityId; the others must issue through Request-InfisicalCertificate -CertificateProfileId, which bypasses that check. EnableDirectIssuance is fixed at CA creation and appears in no Infisical update schema, so it cannot be toggled afterwards; a CA migrated from the older requireTemplateForIssuance column reads False permanently.</maml:para>
|
||||||
</maml:alert>
|
</maml:alert>
|
||||||
</maml:alertSet>
|
</maml:alertSet>
|
||||||
@@ -1289,6 +1290,8 @@ $GetInfisicalCertificatePolicyResult = Get-InfisicalCertificatePolicy @GetInfisi
|
|||||||
<maml:para>Installing a root into CurrentUser\Root makes Windows display a modal trust confirmation dialog, and the call blocks until it is answered; in a non-interactive session this looks like a hang. The cmdlet emits a warning before blocking. Run elevated or pass -StoreLocation LocalMachine to install machine-wide without a prompt.</maml:para>
|
<maml:para>Installing a root into CurrentUser\Root makes Windows display a modal trust confirmation dialog, and the call blocks until it is answered; in a non-interactive session this looks like a hang. The cmdlet emits a warning before blocking. Run elevated or pass -StoreLocation LocalMachine to install machine-wide without a prompt.</maml:para>
|
||||||
<maml:para>Only the leaf honours -StoreName (default My). Chain members are routed by what they are: a self-signed certificate is a root and goes to the trusted-root store, anything with an issuer above it is a subordinate CA and goes to the intermediate store, for a chain of any depth. Issuers are installed before the leaf, and the chain is then validated against the machine's stores; an incomplete chain is reported as a warning naming the missing issuer, which is the condition Windows shows as "The issuer of this certificate could not be found".</maml:para>
|
<maml:para>Only the leaf honours -StoreName (default My). Chain members are routed by what they are: a self-signed certificate is a root and goes to the trusted-root store, anything with an issuer above it is a subordinate CA and goes to the intermediate store, for a chain of any depth. Issuers are installed before the leaf, and the chain is then validated against the machine's stores; an incomplete chain is reported as a warning naming the missing issuer, which is the condition Windows shows as "The issuer of this certificate could not be found".</maml:para>
|
||||||
<maml:para>The installed certificate's Windows friendly name defaults to the common name in upper case. -FriendlyName overrides it and is accepted on every parameter set; on the -CertificateAuthorityId path the same value is additionally sent to Infisical as the issued certificate's friendlyName.</maml:para>
|
<maml:para>The installed certificate's Windows friendly name defaults to the common name in upper case. -FriendlyName overrides it and is accepted on every parameter set; on the -CertificateAuthorityId path the same value is additionally sent to Infisical as the issued certificate's friendlyName.</maml:para>
|
||||||
|
<maml:para>-Metadata attaches key/value pairs to the certificate in Infisical and accepts any IDictionary, such as a hashtable or an [Ordered] dictionary. Only the supplied keys are reconciled; keys already on the certificate that the call does not mention are left alone, so several callers can each own their own keys. This is performed client-side because Infisical's PATCH replaces a certificate's metadata wholesale, so the module reads the current set, merges the supplied keys over it, and writes back the union; when nothing would change no request is sent. Reconciliation also runs on the reuse path, so a metadata change lands without forcing reissuance. Values are flattened to strings, keys are trimmed and compared case-insensitively, and blank keys are dropped. The resulting metadata is returned on the result's Metadata property. A metadata failure is reported as a warning and does not fail an issuance that otherwise succeeded.</maml:para>
|
||||||
|
<maml:para>The reuse check is scoped to the issuer being requested: the search is filtered by -CertificateProfileId or -CertificateAuthorityId, so a certificate issued by a different profile is not reused. This matters when two profiles over one CA differ in key usage, such as server authentication versus client authentication, where a common-name match alone would return a certificate with the wrong extended key usages. Reuse additionally requires the existing certificate to carry every requested subject alternative name, so adding an entry to -DnsName or -IpAddress issues a new certificate instead of returning one that would fail validation for the new name. The rule is coverage rather than equality: a certificate carrying more names than requested still qualifies, DNS names compare case-insensitively, and IP addresses are normalized so ::1 matches 0:0:0:0:0:0:0:1. Use -Force when the SAN set needs trimming rather than extending. When Infisical cannot be reached the check falls back to matching on the common name alone and says so with a warning.</maml:para>
|
||||||
</maml:alert>
|
</maml:alert>
|
||||||
</maml:alertSet>
|
</maml:alertSet>
|
||||||
<command:examples>
|
<command:examples>
|
||||||
@@ -2101,4 +2104,439 @@ $Sans = Get-InfisicalSANList @GetInfisicalSANListParameters</dev:code>
|
|||||||
</command:examples>
|
</command:examples>
|
||||||
</command:command>
|
</command:command>
|
||||||
|
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>New-InfisicalCertificateAuthority</command:name>
|
||||||
|
<maml:description><maml:para>Creates an internal Infisical certificate authority, signing a subordinate with its parent.</maml:para></maml:description>
|
||||||
|
<command:verb>New</command:verb>
|
||||||
|
<command:noun>InfisicalCertificateAuthority</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>$Root = New-InfisicalCertificateAuthority -Name 'root-ca' -Type Root -CommonName 'Contoso Root Certificate Authority' -Organization 'Contoso' -Country 'US'</dev:code>
|
||||||
|
<dev:remarks><maml:para>Creates a self-signed root valid for ten years.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>New-InfisicalCertificateAuthority -Name 'issuing-ca' -Type Intermediate -ParentCaId $Root.Id -CommonName 'Contoso Issuing Certificate Authority' -KeyAlgorithm 'EC_secp384r1'</dev:code>
|
||||||
|
<dev:remarks><maml:para>Creates a subordinate, signs it with the root, and imports the signed certificate so it can issue immediately.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>Set-InfisicalCertificateAuthority</command:name>
|
||||||
|
<maml:description><maml:para>Renames an internal Infisical certificate authority or changes its status.</maml:para></maml:description>
|
||||||
|
<command:verb>Set</command:verb>
|
||||||
|
<command:noun>InfisicalCertificateAuthority</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>Disabling an authority stops it issuing without deleting it or the certificates it has already signed. Honors -WhatIf and -Confirm.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>Set-InfisicalCertificateAuthority -CaId $Ca.Id -Status disabled</dev:code>
|
||||||
|
<dev:remarks><maml:para>Stops the authority issuing new certificates.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>Set-InfisicalCertificateAuthority -CaId $Ca.Id -Name 'retired-issuing-ca' -PassThru</dev:code>
|
||||||
|
<dev:remarks><maml:para>Renames the authority and emits the updated record.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>Remove-InfisicalCertificateAuthority</command:name>
|
||||||
|
<maml:description><maml:para>Deletes an internal Infisical certificate authority.</maml:para></maml:description>
|
||||||
|
<command:verb>Remove</command:verb>
|
||||||
|
<command:noun>InfisicalCertificateAuthority</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>Deletes an internal certificate authority from a Certificate Manager project. -PassThru emits the removed identifier for logging.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>Remove-InfisicalCertificateAuthority -CaId $Ca.Id -Confirm:$False</dev:code>
|
||||||
|
<dev:remarks><maml:para>Deletes the authority without prompting.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>Get-InfisicalCertificateAuthority -Kind Internal | Where-Object {($_.Status -eq 'disabled')} | Remove-InfisicalCertificateAuthority</dev:code>
|
||||||
|
<dev:remarks><maml:para>Removes every disabled authority, prompting for each.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>New-InfisicalCertificatePolicy</command:name>
|
||||||
|
<maml:description><maml:para>Creates an Infisical certificate policy that constrains what a profile may issue.</maml:para></maml:description>
|
||||||
|
<command:verb>New</command:verb>
|
||||||
|
<command:noun>InfisicalCertificatePolicy</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>New-InfisicalCertificatePolicy -Name 'server-auth' -MaxValidity '90d' -KeyAlgorithm 'RSA_2048','EC_secp384r1' -KeyUsage @{ Required = @('digital_signature','key_encipherment') } -ExtendedKeyUsage @{ Required = @('server_auth','client_auth') }</dev:code>
|
||||||
|
<dev:remarks><maml:para>Creates a policy for server and client authentication, leaving subject and SANs unconstrained.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>New-InfisicalCertificatePolicy -Name 'code-signing' -MaxValidity '365d' -ExtendedKeyUsage @{ Required = @('code_signing'); Denied = @('server_auth','client_auth') } -SubjectAlternativeName @(@{ Type = 'dns_name'; Allowed = @('*.contoso.com') })</dev:code>
|
||||||
|
<dev:remarks><maml:para>Creates a code signing policy that forbids TLS usage and restricts DNS names to one suffix.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>Set-InfisicalCertificatePolicy</command:name>
|
||||||
|
<maml:description><maml:para>Updates an Infisical certificate policy.</maml:para></maml:description>
|
||||||
|
<command:verb>Set</command:verb>
|
||||||
|
<command:noun>InfisicalCertificatePolicy</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>Set-InfisicalCertificatePolicy -PolicyId $Policy.Id -MaxValidity '30d'</dev:code>
|
||||||
|
<dev:remarks><maml:para>Shortens the maximum lifetime, leaving every other constraint as it was.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>Set-InfisicalCertificatePolicy -PolicyId $Policy.Id -ExtendedKeyUsage @{ Required = @('server_auth') } -PassThru</dev:code>
|
||||||
|
<dev:remarks><maml:para>Narrows the extended key usage and emits the updated policy.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>Remove-InfisicalCertificatePolicy</command:name>
|
||||||
|
<maml:description><maml:para>Deletes an Infisical certificate policy.</maml:para></maml:description>
|
||||||
|
<command:verb>Remove</command:verb>
|
||||||
|
<command:noun>InfisicalCertificatePolicy</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>Deletes a certificate policy from a Certificate Manager project. -PassThru emits the removed identifier for logging.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>Remove-InfisicalCertificatePolicy -PolicyId $Policy.Id -Confirm:$False</dev:code>
|
||||||
|
<dev:remarks><maml:para>Deletes the policy without prompting.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>Get-InfisicalCertificatePolicy | Where-Object {($_.Name -like 'test-*')} | Remove-InfisicalCertificatePolicy</dev:code>
|
||||||
|
<dev:remarks><maml:para>Removes every policy whose name begins with test-, prompting for each.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>New-InfisicalCertificateProfile</command:name>
|
||||||
|
<maml:description><maml:para>Creates an Infisical certificate profile that binds an issuing authority to a policy.</maml:para></maml:description>
|
||||||
|
<command:verb>New</command:verb>
|
||||||
|
<command:noun>InfisicalCertificateProfile</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>New-InfisicalCertificateProfile -Slug 'server-auth' -CertificatePolicyId $Policy.Id -CaId $Ca.Id</dev:code>
|
||||||
|
<dev:remarks><maml:para>Creates an API enrollment profile bound to a policy and issuing authority.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>New-InfisicalCertificateProfile -Slug 'workload' -CertificatePolicyId $Policy.Id -CaId $Ca.Id -AutoRenew -RenewBeforeDays 14 -Defaults @{ ttlDays = 90 }</dev:code>
|
||||||
|
<dev:remarks><maml:para>Creates a profile that renews issued certificates fourteen days before expiry and defaults to a ninety day lifetime.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>Set-InfisicalCertificateProfile</command:name>
|
||||||
|
<maml:description><maml:para>Updates an Infisical certificate profile.</maml:para></maml:description>
|
||||||
|
<command:verb>Set</command:verb>
|
||||||
|
<command:noun>InfisicalCertificateProfile</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>Set-InfisicalCertificateProfile -ProfileId $Profile.Id -CertificatePolicyId $NewPolicy.Id</dev:code>
|
||||||
|
<dev:remarks><maml:para>Repoints the profile at a different policy.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>Set-InfisicalCertificateProfile -ProfileId $Profile.Id -AutoRenew -RenewBeforeDays 7 -PassThru</dev:code>
|
||||||
|
<dev:remarks><maml:para>Enables automatic renewal seven days before expiry and emits the updated profile.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>Remove-InfisicalCertificateProfile</command:name>
|
||||||
|
<maml:description><maml:para>Deletes an Infisical certificate profile.</maml:para></maml:description>
|
||||||
|
<command:verb>Remove</command:verb>
|
||||||
|
<command:noun>InfisicalCertificateProfile</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>Deletes a certificate profile from a Certificate Manager project. -PassThru emits the removed identifier for logging.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>Remove-InfisicalCertificateProfile -ProfileId $Profile.Id -Confirm:$False</dev:code>
|
||||||
|
<dev:remarks><maml:para>Deletes the profile without prompting.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>Get-InfisicalCertificateProfile -ApplicationId $Application.Id | Remove-InfisicalCertificateProfile</dev:code>
|
||||||
|
<dev:remarks><maml:para>Removes every profile attached to an application, prompting for each.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>New-InfisicalCertificateApplication</command:name>
|
||||||
|
<maml:description><maml:para>Creates an Infisical certificate application to group profiles and certificates.</maml:para></maml:description>
|
||||||
|
<command:verb>New</command:verb>
|
||||||
|
<command:noun>InfisicalCertificateApplication</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>New-InfisicalCertificateApplication -Name 'platform' -Description 'Endpoint and workload certificates'</dev:code>
|
||||||
|
<dev:remarks><maml:para>Creates an empty application.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>New-InfisicalCertificateApplication -Name 'platform' -ProfileId $ServerProfile.Id, $CodeSigningProfile.Id</dev:code>
|
||||||
|
<dev:remarks><maml:para>Creates an application with two profiles already attached.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>Set-InfisicalCertificateApplication</command:name>
|
||||||
|
<maml:description><maml:para>Renames an Infisical certificate application or changes which profiles it holds.</maml:para></maml:description>
|
||||||
|
<command:verb>Set</command:verb>
|
||||||
|
<command:noun>InfisicalCertificateApplication</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>Set-InfisicalCertificateApplication -ApplicationId $Application.Id -AddProfileId $Profile.Id</dev:code>
|
||||||
|
<dev:remarks><maml:para>Attaches a profile to the application.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>Set-InfisicalCertificateApplication -ApplicationId $Application.Id -Name 'endpoint-management' -RemoveProfileId $Old.Id -PassThru</dev:code>
|
||||||
|
<dev:remarks><maml:para>Renames the application, detaches a profile, and emits the updated record.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>Remove-InfisicalCertificateApplication</command:name>
|
||||||
|
<maml:description><maml:para>Deletes an Infisical certificate application.</maml:para></maml:description>
|
||||||
|
<command:verb>Remove</command:verb>
|
||||||
|
<command:noun>InfisicalCertificateApplication</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>Deletes a certificate application from a Certificate Manager project. -PassThru emits the removed identifier for logging.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>Remove-InfisicalCertificateApplication -ApplicationId $Application.Id -Confirm:$False</dev:code>
|
||||||
|
<dev:remarks><maml:para>Deletes the application without prompting.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>Get-InfisicalCertificateApplication | Where-Object {($_.CertificateCount -eq 0)} | Remove-InfisicalCertificateApplication</dev:code>
|
||||||
|
<dev:remarks><maml:para>Removes every application holding no certificates, prompting for each.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>New-InfisicalPkiSubscriber</command:name>
|
||||||
|
<maml:description><maml:para>Creates an Infisical PKI subscriber, a named enrollment identity with a fixed common name.</maml:para></maml:description>
|
||||||
|
<command:verb>New</command:verb>
|
||||||
|
<command:noun>InfisicalPkiSubscriber</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>New-InfisicalPkiSubscriber -Name 'web01' -CommonName 'WEB01.contoso.com' -CaId $Ca.Id -Ttl '90d'</dev:code>
|
||||||
|
<dev:remarks><maml:para>Creates a subscriber for one host.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>New-InfisicalPkiSubscriber -Name 'web01' -CommonName 'WEB01.contoso.com' -CaId $Ca.Id -Ttl '90d' -SubjectAlternativeName 'WEB01','WEB01.contoso.com' -ExtendedKeyUsage 'serverAuth','clientAuth'</dev:code>
|
||||||
|
<dev:remarks><maml:para>Creates a subscriber that also permits two subject alternative names and restricts extended key usage.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>Set-InfisicalPkiSubscriber</command:name>
|
||||||
|
<maml:description><maml:para>Updates an Infisical PKI subscriber.</maml:para></maml:description>
|
||||||
|
<command:verb>Set</command:verb>
|
||||||
|
<command:noun>InfisicalPkiSubscriber</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>Set-InfisicalPkiSubscriber -Name 'web01' -Ttl '30d'</dev:code>
|
||||||
|
<dev:remarks><maml:para>Shortens the lifetime of certificates issued for the subscriber.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>Set-InfisicalPkiSubscriber -Name 'web01' -SubjectAlternativeName 'WEB01','WEB01.contoso.com','www.contoso.com' -PassThru</dev:code>
|
||||||
|
<dev:remarks><maml:para>Extends the permitted subject alternative names and emits the updated subscriber.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>Remove-InfisicalPkiSubscriber</command:name>
|
||||||
|
<maml:description><maml:para>Deletes an Infisical PKI subscriber.</maml:para></maml:description>
|
||||||
|
<command:verb>Remove</command:verb>
|
||||||
|
<command:noun>InfisicalPkiSubscriber</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>Deletes a PKI subscriber from a Certificate Manager project, addressed by name. -PassThru emits the removed name for logging.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>Remove-InfisicalPkiSubscriber -Name 'web01' -Confirm:$False</dev:code>
|
||||||
|
<dev:remarks><maml:para>Deletes the subscriber without prompting.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>Get-InfisicalPkiSubscriber | Where-Object {($_.Status -ne 'active')} | Remove-InfisicalPkiSubscriber</dev:code>
|
||||||
|
<dev:remarks><maml:para>Removes every inactive subscriber, prompting for each.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
</helpItems>
|
</helpItems>
|
||||||
|
|||||||
@@ -1066,6 +1066,7 @@ $RemoveInfisicalTagResult = Remove-InfisicalTag @RemoveInfisicalTagParameters</d
|
|||||||
<maml:alertSet>
|
<maml:alertSet>
|
||||||
<maml:title>Notes</maml:title>
|
<maml:title>Notes</maml:title>
|
||||||
<maml:alert>
|
<maml:alert>
|
||||||
|
<maml:para>-ProjectId is optional. The Infisical console never asks which Certificate Manager project to use, because its resolver selects the single cert-manager project when an organization has exactly one; omitting -ProjectId applies the same rule and reports the resolved project on the verbose stream. Pass it explicitly when an organization has more than one, in which case the error lists the candidates. Note that a project contains applications: Get-InfisicalProject -Type cert-manager returns the project, while Get-InfisicalCertificateApplication returns the applications inside it.</maml:para>
|
||||||
<maml:para>ByID retrieval currently always resolves against the internal CA endpoint. CA Ids returned here are the values to pass on -CertificateAuthorityId to Request-InfisicalCertificate. The Type property distinguishes 'internal' from 'acme' when -Kind Any is used. Only CAs whose EnableDirectIssuance property is True can sign a CSR through -CertificateAuthorityId; the others must issue through Request-InfisicalCertificate -CertificateProfileId, which bypasses that check. EnableDirectIssuance is fixed at CA creation and appears in no Infisical update schema, so it cannot be toggled afterwards; a CA migrated from the older requireTemplateForIssuance column reads False permanently.</maml:para>
|
<maml:para>ByID retrieval currently always resolves against the internal CA endpoint. CA Ids returned here are the values to pass on -CertificateAuthorityId to Request-InfisicalCertificate. The Type property distinguishes 'internal' from 'acme' when -Kind Any is used. Only CAs whose EnableDirectIssuance property is True can sign a CSR through -CertificateAuthorityId; the others must issue through Request-InfisicalCertificate -CertificateProfileId, which bypasses that check. EnableDirectIssuance is fixed at CA creation and appears in no Infisical update schema, so it cannot be toggled afterwards; a CA migrated from the older requireTemplateForIssuance column reads False permanently.</maml:para>
|
||||||
</maml:alert>
|
</maml:alert>
|
||||||
</maml:alertSet>
|
</maml:alertSet>
|
||||||
@@ -1289,6 +1290,8 @@ $GetInfisicalCertificatePolicyResult = Get-InfisicalCertificatePolicy @GetInfisi
|
|||||||
<maml:para>Installing a root into CurrentUser\Root makes Windows display a modal trust confirmation dialog, and the call blocks until it is answered; in a non-interactive session this looks like a hang. The cmdlet emits a warning before blocking. Run elevated or pass -StoreLocation LocalMachine to install machine-wide without a prompt.</maml:para>
|
<maml:para>Installing a root into CurrentUser\Root makes Windows display a modal trust confirmation dialog, and the call blocks until it is answered; in a non-interactive session this looks like a hang. The cmdlet emits a warning before blocking. Run elevated or pass -StoreLocation LocalMachine to install machine-wide without a prompt.</maml:para>
|
||||||
<maml:para>Only the leaf honours -StoreName (default My). Chain members are routed by what they are: a self-signed certificate is a root and goes to the trusted-root store, anything with an issuer above it is a subordinate CA and goes to the intermediate store, for a chain of any depth. Issuers are installed before the leaf, and the chain is then validated against the machine's stores; an incomplete chain is reported as a warning naming the missing issuer, which is the condition Windows shows as "The issuer of this certificate could not be found".</maml:para>
|
<maml:para>Only the leaf honours -StoreName (default My). Chain members are routed by what they are: a self-signed certificate is a root and goes to the trusted-root store, anything with an issuer above it is a subordinate CA and goes to the intermediate store, for a chain of any depth. Issuers are installed before the leaf, and the chain is then validated against the machine's stores; an incomplete chain is reported as a warning naming the missing issuer, which is the condition Windows shows as "The issuer of this certificate could not be found".</maml:para>
|
||||||
<maml:para>The installed certificate's Windows friendly name defaults to the common name in upper case. -FriendlyName overrides it and is accepted on every parameter set; on the -CertificateAuthorityId path the same value is additionally sent to Infisical as the issued certificate's friendlyName.</maml:para>
|
<maml:para>The installed certificate's Windows friendly name defaults to the common name in upper case. -FriendlyName overrides it and is accepted on every parameter set; on the -CertificateAuthorityId path the same value is additionally sent to Infisical as the issued certificate's friendlyName.</maml:para>
|
||||||
|
<maml:para>-Metadata attaches key/value pairs to the certificate in Infisical and accepts any IDictionary, such as a hashtable or an [Ordered] dictionary. Only the supplied keys are reconciled; keys already on the certificate that the call does not mention are left alone, so several callers can each own their own keys. This is performed client-side because Infisical's PATCH replaces a certificate's metadata wholesale, so the module reads the current set, merges the supplied keys over it, and writes back the union; when nothing would change no request is sent. Reconciliation also runs on the reuse path, so a metadata change lands without forcing reissuance. Values are flattened to strings, keys are trimmed and compared case-insensitively, and blank keys are dropped. The resulting metadata is returned on the result's Metadata property. A metadata failure is reported as a warning and does not fail an issuance that otherwise succeeded.</maml:para>
|
||||||
|
<maml:para>The reuse check is scoped to the issuer being requested: the search is filtered by -CertificateProfileId or -CertificateAuthorityId, so a certificate issued by a different profile is not reused. This matters when two profiles over one CA differ in key usage, such as server authentication versus client authentication, where a common-name match alone would return a certificate with the wrong extended key usages. Reuse additionally requires the existing certificate to carry every requested subject alternative name, so adding an entry to -DnsName or -IpAddress issues a new certificate instead of returning one that would fail validation for the new name. The rule is coverage rather than equality: a certificate carrying more names than requested still qualifies, DNS names compare case-insensitively, and IP addresses are normalized so ::1 matches 0:0:0:0:0:0:0:1. Use -Force when the SAN set needs trimming rather than extending. When Infisical cannot be reached the check falls back to matching on the common name alone and says so with a warning.</maml:para>
|
||||||
</maml:alert>
|
</maml:alert>
|
||||||
</maml:alertSet>
|
</maml:alertSet>
|
||||||
<command:examples>
|
<command:examples>
|
||||||
@@ -2101,4 +2104,439 @@ $Sans = Get-InfisicalSANList @GetInfisicalSANListParameters</dev:code>
|
|||||||
</command:examples>
|
</command:examples>
|
||||||
</command:command>
|
</command:command>
|
||||||
|
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>New-InfisicalCertificateAuthority</command:name>
|
||||||
|
<maml:description><maml:para>Creates an internal Infisical certificate authority, signing a subordinate with its parent.</maml:para></maml:description>
|
||||||
|
<command:verb>New</command:verb>
|
||||||
|
<command:noun>InfisicalCertificateAuthority</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>$Root = New-InfisicalCertificateAuthority -Name 'root-ca' -Type Root -CommonName 'Contoso Root Certificate Authority' -Organization 'Contoso' -Country 'US'</dev:code>
|
||||||
|
<dev:remarks><maml:para>Creates a self-signed root valid for ten years.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>New-InfisicalCertificateAuthority -Name 'issuing-ca' -Type Intermediate -ParentCaId $Root.Id -CommonName 'Contoso Issuing Certificate Authority' -KeyAlgorithm 'EC_secp384r1'</dev:code>
|
||||||
|
<dev:remarks><maml:para>Creates a subordinate, signs it with the root, and imports the signed certificate so it can issue immediately.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>Set-InfisicalCertificateAuthority</command:name>
|
||||||
|
<maml:description><maml:para>Renames an internal Infisical certificate authority or changes its status.</maml:para></maml:description>
|
||||||
|
<command:verb>Set</command:verb>
|
||||||
|
<command:noun>InfisicalCertificateAuthority</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>Disabling an authority stops it issuing without deleting it or the certificates it has already signed. Honors -WhatIf and -Confirm.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>Set-InfisicalCertificateAuthority -CaId $Ca.Id -Status disabled</dev:code>
|
||||||
|
<dev:remarks><maml:para>Stops the authority issuing new certificates.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>Set-InfisicalCertificateAuthority -CaId $Ca.Id -Name 'retired-issuing-ca' -PassThru</dev:code>
|
||||||
|
<dev:remarks><maml:para>Renames the authority and emits the updated record.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>Remove-InfisicalCertificateAuthority</command:name>
|
||||||
|
<maml:description><maml:para>Deletes an internal Infisical certificate authority.</maml:para></maml:description>
|
||||||
|
<command:verb>Remove</command:verb>
|
||||||
|
<command:noun>InfisicalCertificateAuthority</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>Deletes an internal certificate authority from a Certificate Manager project. -PassThru emits the removed identifier for logging.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>Remove-InfisicalCertificateAuthority -CaId $Ca.Id -Confirm:$False</dev:code>
|
||||||
|
<dev:remarks><maml:para>Deletes the authority without prompting.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>Get-InfisicalCertificateAuthority -Kind Internal | Where-Object {($_.Status -eq 'disabled')} | Remove-InfisicalCertificateAuthority</dev:code>
|
||||||
|
<dev:remarks><maml:para>Removes every disabled authority, prompting for each.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>New-InfisicalCertificatePolicy</command:name>
|
||||||
|
<maml:description><maml:para>Creates an Infisical certificate policy that constrains what a profile may issue.</maml:para></maml:description>
|
||||||
|
<command:verb>New</command:verb>
|
||||||
|
<command:noun>InfisicalCertificatePolicy</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>New-InfisicalCertificatePolicy -Name 'server-auth' -MaxValidity '90d' -KeyAlgorithm 'RSA_2048','EC_secp384r1' -KeyUsage @{ Required = @('digital_signature','key_encipherment') } -ExtendedKeyUsage @{ Required = @('server_auth','client_auth') }</dev:code>
|
||||||
|
<dev:remarks><maml:para>Creates a policy for server and client authentication, leaving subject and SANs unconstrained.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>New-InfisicalCertificatePolicy -Name 'code-signing' -MaxValidity '365d' -ExtendedKeyUsage @{ Required = @('code_signing'); Denied = @('server_auth','client_auth') } -SubjectAlternativeName @(@{ Type = 'dns_name'; Allowed = @('*.contoso.com') })</dev:code>
|
||||||
|
<dev:remarks><maml:para>Creates a code signing policy that forbids TLS usage and restricts DNS names to one suffix.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>Set-InfisicalCertificatePolicy</command:name>
|
||||||
|
<maml:description><maml:para>Updates an Infisical certificate policy.</maml:para></maml:description>
|
||||||
|
<command:verb>Set</command:verb>
|
||||||
|
<command:noun>InfisicalCertificatePolicy</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>Set-InfisicalCertificatePolicy -PolicyId $Policy.Id -MaxValidity '30d'</dev:code>
|
||||||
|
<dev:remarks><maml:para>Shortens the maximum lifetime, leaving every other constraint as it was.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>Set-InfisicalCertificatePolicy -PolicyId $Policy.Id -ExtendedKeyUsage @{ Required = @('server_auth') } -PassThru</dev:code>
|
||||||
|
<dev:remarks><maml:para>Narrows the extended key usage and emits the updated policy.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>Remove-InfisicalCertificatePolicy</command:name>
|
||||||
|
<maml:description><maml:para>Deletes an Infisical certificate policy.</maml:para></maml:description>
|
||||||
|
<command:verb>Remove</command:verb>
|
||||||
|
<command:noun>InfisicalCertificatePolicy</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>Deletes a certificate policy from a Certificate Manager project. -PassThru emits the removed identifier for logging.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>Remove-InfisicalCertificatePolicy -PolicyId $Policy.Id -Confirm:$False</dev:code>
|
||||||
|
<dev:remarks><maml:para>Deletes the policy without prompting.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>Get-InfisicalCertificatePolicy | Where-Object {($_.Name -like 'test-*')} | Remove-InfisicalCertificatePolicy</dev:code>
|
||||||
|
<dev:remarks><maml:para>Removes every policy whose name begins with test-, prompting for each.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>New-InfisicalCertificateProfile</command:name>
|
||||||
|
<maml:description><maml:para>Creates an Infisical certificate profile that binds an issuing authority to a policy.</maml:para></maml:description>
|
||||||
|
<command:verb>New</command:verb>
|
||||||
|
<command:noun>InfisicalCertificateProfile</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>New-InfisicalCertificateProfile -Slug 'server-auth' -CertificatePolicyId $Policy.Id -CaId $Ca.Id</dev:code>
|
||||||
|
<dev:remarks><maml:para>Creates an API enrollment profile bound to a policy and issuing authority.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>New-InfisicalCertificateProfile -Slug 'workload' -CertificatePolicyId $Policy.Id -CaId $Ca.Id -AutoRenew -RenewBeforeDays 14 -Defaults @{ ttlDays = 90 }</dev:code>
|
||||||
|
<dev:remarks><maml:para>Creates a profile that renews issued certificates fourteen days before expiry and defaults to a ninety day lifetime.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>Set-InfisicalCertificateProfile</command:name>
|
||||||
|
<maml:description><maml:para>Updates an Infisical certificate profile.</maml:para></maml:description>
|
||||||
|
<command:verb>Set</command:verb>
|
||||||
|
<command:noun>InfisicalCertificateProfile</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>Set-InfisicalCertificateProfile -ProfileId $Profile.Id -CertificatePolicyId $NewPolicy.Id</dev:code>
|
||||||
|
<dev:remarks><maml:para>Repoints the profile at a different policy.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>Set-InfisicalCertificateProfile -ProfileId $Profile.Id -AutoRenew -RenewBeforeDays 7 -PassThru</dev:code>
|
||||||
|
<dev:remarks><maml:para>Enables automatic renewal seven days before expiry and emits the updated profile.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>Remove-InfisicalCertificateProfile</command:name>
|
||||||
|
<maml:description><maml:para>Deletes an Infisical certificate profile.</maml:para></maml:description>
|
||||||
|
<command:verb>Remove</command:verb>
|
||||||
|
<command:noun>InfisicalCertificateProfile</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>Deletes a certificate profile from a Certificate Manager project. -PassThru emits the removed identifier for logging.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>Remove-InfisicalCertificateProfile -ProfileId $Profile.Id -Confirm:$False</dev:code>
|
||||||
|
<dev:remarks><maml:para>Deletes the profile without prompting.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>Get-InfisicalCertificateProfile -ApplicationId $Application.Id | Remove-InfisicalCertificateProfile</dev:code>
|
||||||
|
<dev:remarks><maml:para>Removes every profile attached to an application, prompting for each.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>New-InfisicalCertificateApplication</command:name>
|
||||||
|
<maml:description><maml:para>Creates an Infisical certificate application to group profiles and certificates.</maml:para></maml:description>
|
||||||
|
<command:verb>New</command:verb>
|
||||||
|
<command:noun>InfisicalCertificateApplication</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>New-InfisicalCertificateApplication -Name 'platform' -Description 'Endpoint and workload certificates'</dev:code>
|
||||||
|
<dev:remarks><maml:para>Creates an empty application.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>New-InfisicalCertificateApplication -Name 'platform' -ProfileId $ServerProfile.Id, $CodeSigningProfile.Id</dev:code>
|
||||||
|
<dev:remarks><maml:para>Creates an application with two profiles already attached.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>Set-InfisicalCertificateApplication</command:name>
|
||||||
|
<maml:description><maml:para>Renames an Infisical certificate application or changes which profiles it holds.</maml:para></maml:description>
|
||||||
|
<command:verb>Set</command:verb>
|
||||||
|
<command:noun>InfisicalCertificateApplication</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>Set-InfisicalCertificateApplication -ApplicationId $Application.Id -AddProfileId $Profile.Id</dev:code>
|
||||||
|
<dev:remarks><maml:para>Attaches a profile to the application.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>Set-InfisicalCertificateApplication -ApplicationId $Application.Id -Name 'endpoint-management' -RemoveProfileId $Old.Id -PassThru</dev:code>
|
||||||
|
<dev:remarks><maml:para>Renames the application, detaches a profile, and emits the updated record.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>Remove-InfisicalCertificateApplication</command:name>
|
||||||
|
<maml:description><maml:para>Deletes an Infisical certificate application.</maml:para></maml:description>
|
||||||
|
<command:verb>Remove</command:verb>
|
||||||
|
<command:noun>InfisicalCertificateApplication</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>Deletes a certificate application from a Certificate Manager project. -PassThru emits the removed identifier for logging.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>Remove-InfisicalCertificateApplication -ApplicationId $Application.Id -Confirm:$False</dev:code>
|
||||||
|
<dev:remarks><maml:para>Deletes the application without prompting.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>Get-InfisicalCertificateApplication | Where-Object {($_.CertificateCount -eq 0)} | Remove-InfisicalCertificateApplication</dev:code>
|
||||||
|
<dev:remarks><maml:para>Removes every application holding no certificates, prompting for each.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>New-InfisicalPkiSubscriber</command:name>
|
||||||
|
<maml:description><maml:para>Creates an Infisical PKI subscriber, a named enrollment identity with a fixed common name.</maml:para></maml:description>
|
||||||
|
<command:verb>New</command:verb>
|
||||||
|
<command:noun>InfisicalPkiSubscriber</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>New-InfisicalPkiSubscriber -Name 'web01' -CommonName 'WEB01.contoso.com' -CaId $Ca.Id -Ttl '90d'</dev:code>
|
||||||
|
<dev:remarks><maml:para>Creates a subscriber for one host.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>New-InfisicalPkiSubscriber -Name 'web01' -CommonName 'WEB01.contoso.com' -CaId $Ca.Id -Ttl '90d' -SubjectAlternativeName 'WEB01','WEB01.contoso.com' -ExtendedKeyUsage 'serverAuth','clientAuth'</dev:code>
|
||||||
|
<dev:remarks><maml:para>Creates a subscriber that also permits two subject alternative names and restricts extended key usage.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>Set-InfisicalPkiSubscriber</command:name>
|
||||||
|
<maml:description><maml:para>Updates an Infisical PKI subscriber.</maml:para></maml:description>
|
||||||
|
<command:verb>Set</command:verb>
|
||||||
|
<command:noun>InfisicalPkiSubscriber</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>Set-InfisicalPkiSubscriber -Name 'web01' -Ttl '30d'</dev:code>
|
||||||
|
<dev:remarks><maml:para>Shortens the lifetime of certificates issued for the subscriber.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>Set-InfisicalPkiSubscriber -Name 'web01' -SubjectAlternativeName 'WEB01','WEB01.contoso.com','www.contoso.com' -PassThru</dev:code>
|
||||||
|
<dev:remarks><maml:para>Extends the permitted subject alternative names and emits the updated subscriber.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
|
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
|
||||||
|
<command:details>
|
||||||
|
<command:name>Remove-InfisicalPkiSubscriber</command:name>
|
||||||
|
<maml:description><maml:para>Deletes an Infisical PKI subscriber.</maml:para></maml:description>
|
||||||
|
<command:verb>Remove</command:verb>
|
||||||
|
<command:noun>InfisicalPkiSubscriber</command:noun>
|
||||||
|
</command:details>
|
||||||
|
<maml:description>
|
||||||
|
<maml:para>Deletes a PKI subscriber from a Certificate Manager project, addressed by name. -PassThru emits the removed name for logging.</maml:para>
|
||||||
|
</maml:description>
|
||||||
|
<maml:alertSet>
|
||||||
|
<maml:title>Notes</maml:title>
|
||||||
|
<maml:alert>
|
||||||
|
<maml:para>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.</maml:para>
|
||||||
|
</maml:alert>
|
||||||
|
</maml:alertSet>
|
||||||
|
<command:examples>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 1</maml:title>
|
||||||
|
<dev:code>Remove-InfisicalPkiSubscriber -Name 'web01' -Confirm:$False</dev:code>
|
||||||
|
<dev:remarks><maml:para>Deletes the subscriber without prompting.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
<command:example>
|
||||||
|
<maml:title>EXAMPLE 2</maml:title>
|
||||||
|
<dev:code>Get-InfisicalPkiSubscriber | Where-Object {($_.Status -ne 'active')} | Remove-InfisicalPkiSubscriber</dev:code>
|
||||||
|
<dev:remarks><maml:para>Removes every inactive subscriber, prompting for each.</maml:para></dev:remarks>
|
||||||
|
</command:example>
|
||||||
|
</command:examples>
|
||||||
|
</command:command>
|
||||||
</helpItems>
|
</helpItems>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Import-Module -Name .\Module\PSInfisicalAPI
|
|||||||
|
|
||||||
## Cmdlets
|
## 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
|
### 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. |
|
| `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, `<device>.<suffix>` per adapter DNS suffix, RFC 1918 + CGNAT IPv4 addresses, IPv4/IPv6 loopback) for `Request-InfisicalCertificate -DnsName`. |
|
| `Get-InfisicalSANList` | Builds a SAN candidate list (device name, `<device>.<suffix>` 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
|
### Process
|
||||||
|
|
||||||
| Cmdlet | Purpose |
|
| Cmdlet | Purpose |
|
||||||
@@ -134,16 +172,16 @@ $secureSecret = Read-Host -AsSecureString 'Client Secret'
|
|||||||
$connection = Connect-Infisical `
|
$connection = Connect-Infisical `
|
||||||
-BaseUri 'https://app.infisical.com' `
|
-BaseUri 'https://app.infisical.com' `
|
||||||
-OrganizationId '00000000-0000-0000-0000-000000000000' `
|
-OrganizationId '00000000-0000-0000-0000-000000000000' `
|
||||||
-ProjectId '11111111-1111-1111-1111-111111111111' `
|
|
||||||
-Environment 'dev' `
|
|
||||||
-ClientId 'machine-identity-client-id' `
|
-ClientId 'machine-identity-client-id' `
|
||||||
-ClientSecret $secureSecret `
|
-ClientSecret $secureSecret `
|
||||||
-PassThru
|
-PassThru
|
||||||
|
|
||||||
Get-InfisicalSecret -SecretPath '/'
|
Get-InfisicalSecret -ProjectId '11111111-1111-1111-1111-111111111111' -Environment 'dev' -SecretPath '/'
|
||||||
Disconnect-Infisical
|
Disconnect-Infisical
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`Connect-Infisical` establishes the session; project, environment, and secret path are supplied per call. On the PKI cmdlets `-ProjectId` is optional — see [Project scoping](#project-scoping).
|
||||||
|
|
||||||
## End-to-end: request and install a chained certificate
|
## End-to-end: request and install a chained certificate
|
||||||
|
|
||||||
Connects, selects a `cert-manager` project, sources SANs from `Get-InfisicalSANList`, requests a certificate through a certificate profile, installs it (and its chain) into the current-user store, and disconnects. Each call uses a splatted `OrderedDictionary` constructed with `OrdinalIgnoreCase` so parameter names round-trip case-insensitively.
|
Connects, selects a `cert-manager` project, sources SANs from `Get-InfisicalSANList`, requests a certificate through a certificate profile, installs it (and its chain) into the current-user store, and disconnects. Each call uses a splatted `OrderedDictionary` constructed with `OrdinalIgnoreCase` so parameter names round-trip case-insensitively.
|
||||||
@@ -161,43 +199,20 @@ $ConnectInfisicalParameters = New-Object -TypeName 'System.Collections.Specializ
|
|||||||
|
|
||||||
$Connection = Connect-Infisical @ConnectInfisicalParameters
|
$Connection = Connect-Infisical @ConnectInfisicalParameters
|
||||||
|
|
||||||
$Project = Get-InfisicalProject -Type cert-manager | Select-Object -First 1
|
$Application = Get-InfisicalCertificateApplication | Where-Object {($_.Name -ieq 'platform')}
|
||||||
|
|
||||||
$Project
|
$CertificateProfile = Get-InfisicalCertificateProfile -ApplicationId ($Application.Id) -IncludeConfigs | Where-Object {($_.EnrollmentType -ieq 'api') -and ($_.Slug -imatch 'server')} | Select-Object -First 1
|
||||||
|
|
||||||
#region Certificate authorities. Not required for profile issuance - the profile already binds its CA - but
|
|
||||||
# useful for confirming the chain you expect to be installed.
|
|
||||||
$CAList = Get-InfisicalCertificateAuthority -ProjectId ($Project.Id) -Kind Internal
|
|
||||||
|
|
||||||
$RootCA = $CAList | Where-Object {([String]::IsNullOrEmpty($_.ParentCaId) -eq $True)} | Select-Object -First 1
|
|
||||||
|
|
||||||
$RootCA
|
|
||||||
|
|
||||||
$IntermediateCA = $CAList | Where-Object {([String]::IsNullOrEmpty($_.ParentCaId) -eq $False)}
|
|
||||||
|
|
||||||
$IntermediateCA
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
$CertificateProfile = Get-InfisicalCertificateProfile -ProjectId ($Project.Id) -IncludeConfigs |
|
|
||||||
Where-Object {($_.EnrollmentType -iin @('API')) -and ($_.Slug -imatch '.*Server.*')} |
|
|
||||||
Select-Object -First 1
|
|
||||||
|
|
||||||
$CertificateProfile
|
|
||||||
|
|
||||||
$SanList = Get-InfisicalSANList
|
$SanList = Get-InfisicalSANList
|
||||||
|
|
||||||
$SanList
|
|
||||||
|
|
||||||
$RequestInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
|
$RequestInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
$RequestInfisicalCertificateParameters.ProjectId = $Project.Id
|
|
||||||
$RequestInfisicalCertificateParameters.CertificateProfileId = $CertificateProfile.Id
|
$RequestInfisicalCertificateParameters.CertificateProfileId = $CertificateProfile.Id
|
||||||
$RequestInfisicalCertificateParameters.CommonName = $Env:ComputerName.ToUpper()
|
$RequestInfisicalCertificateParameters.CommonName = $Env:ComputerName.ToUpper()
|
||||||
$RequestInfisicalCertificateParameters.DnsName = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
|
$RequestInfisicalCertificateParameters.DnsName = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
|
||||||
$RequestInfisicalCertificateParameters.DnsName.AddRange($SanList)
|
$RequestInfisicalCertificateParameters.DnsName.AddRange($SanList)
|
||||||
$RequestInfisicalCertificateParameters.DnsName.Add('app.contoso.com')
|
$RequestInfisicalCertificateParameters.DnsName.Add('app.contoso.com')
|
||||||
$RequestInfisicalCertificateParameters.DnsName.Add('api.contoso.com')
|
|
||||||
$RequestInfisicalCertificateParameters.DnsName.Add('boot.contoso.com')
|
|
||||||
$RequestInfisicalCertificateParameters.Ttl = '90d'
|
$RequestInfisicalCertificateParameters.Ttl = '90d'
|
||||||
|
$RequestInfisicalCertificateParameters.Metadata = [Ordered]@{ Environment = 'Production'; Owner = 'Platform' }
|
||||||
$RequestInfisicalCertificateParameters.Install = $True
|
$RequestInfisicalCertificateParameters.Install = $True
|
||||||
$RequestInfisicalCertificateParameters.InstallChain = $True
|
$RequestInfisicalCertificateParameters.InstallChain = $True
|
||||||
$RequestInfisicalCertificateParameters.Verbose = $True
|
$RequestInfisicalCertificateParameters.Verbose = $True
|
||||||
@@ -207,39 +222,118 @@ $Certificate = Request-InfisicalCertificate @RequestInfisicalCertificateParamete
|
|||||||
$Null = Disconnect-Infisical -Verbose
|
$Null = Disconnect-Infisical -Verbose
|
||||||
```
|
```
|
||||||
|
|
||||||
Note `$CertificateProfile` rather than `$Profile`: `$Profile` is an automatic variable in PowerShell (the path to the current profile script), and assigning to it works but shadows something the host relies on.
|
Four calls: find the application, pick its profile, gather SANs, request. No project lookup — `-ProjectId` resolves itself — and no CA lookup, because the profile already binds its issuing CA and `-InstallChain` installs the whole chain regardless.
|
||||||
|
|
||||||
`-StoreName`/`-StoreLocation` are omitted deliberately — see [Where certificates get installed](#where-certificates-get-installed).
|
`-StoreName`/`-StoreLocation` are omitted deliberately — see [Where certificates get installed](#where-certificates-get-installed).
|
||||||
|
|
||||||
|
## Renewing that certificate
|
||||||
|
|
||||||
|
Run the same request again with `-AllowRenewal`. Nothing else changes — the parameters below are the ones built above, so this is the shape to put on a schedule:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$RequestInfisicalCertificateParameters.AllowRenewal = $True
|
||||||
|
$RequestInfisicalCertificateParameters.RenewalThresholdDays = 30
|
||||||
|
|
||||||
|
$Certificate = Request-InfisicalCertificate @RequestInfisicalCertificateParameters
|
||||||
|
```
|
||||||
|
|
||||||
|
The call is safe to repeat. Outside the renewal window it finds the installed certificate and returns it untouched; inside the window it issues a replacement and installs it:
|
||||||
|
|
||||||
|
```text
|
||||||
|
VERBOSE: Reuse search for CN=WEB01 scoped to certificate profile 'a42f8446-...' returned 1 active certificate(s).
|
||||||
|
VERBOSE: Reusing existing certificate (Thumbprint=F480A920..., NotAfter=2026-10-28 19:36:49Z).
|
||||||
|
```
|
||||||
|
|
||||||
|
Reuse is matched on the issuing profile *and* the requested SAN set, so the same script issues a new certificate — rather than silently returning the old one — whenever the profile changes or a name is added to `-DnsName`. `-Force` issues unconditionally, ignoring both the renewal window and any existing certificate.
|
||||||
|
|
||||||
|
Run it daily and it does nothing until the certificate is within 30 days of expiry, then rotates it. Nothing to gate it with, and no state to keep.
|
||||||
|
|
||||||
|
### Project scoping
|
||||||
|
|
||||||
|
Opening **Certificate Manager** in the Infisical console never asks which project to use. The project is in the URL — `/organizations/{orgId}/projects/cert-manager/{projectId}/applications` — but it is chosen for you, and everything below it is presented as **applications**. That is because Infisical's own resolver takes the single Certificate Manager project when an organization has exactly one:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const projects = await projectDAL.find({ orgId: actorOrgId, type: ProjectType.CertificateManager });
|
||||||
|
if (projects.length === 1) return projects[0].id;
|
||||||
|
```
|
||||||
|
|
||||||
|
The PKI cmdlets follow the same rule: **`-ProjectId` is optional**. Omit it and the module resolves the organization's only Certificate Manager project, reporting the choice on the verbose stream:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-InfisicalCertificateApplication
|
||||||
|
Get-InfisicalCertificateAuthority -Kind Internal
|
||||||
|
Get-InfisicalCertificateProfile -IncludeConfigs
|
||||||
|
Get-InfisicalPkiSubscriber
|
||||||
|
Get-InfisicalCertificate -Status active
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
VERBOSE: -ProjectId was not supplied; resolved the organization's only Certificate Manager project
|
||||||
|
'Microsoft Endpoint Configuration Manager' (2122628e-...).
|
||||||
|
```
|
||||||
|
|
||||||
|
An organization with **no** Certificate Manager project is not an error either. There is nothing to list, so the PKI `Get-*` cmdlets return nothing and `-Verbose` explains why:
|
||||||
|
|
||||||
|
```text
|
||||||
|
VERBOSE: -ProjectId was not supplied and this organization has no Certificate Manager project, so there is
|
||||||
|
nothing to resolve to. Create one in Infisical (Certificate Management), or pass -ProjectId to
|
||||||
|
target a specific project.
|
||||||
|
```
|
||||||
|
|
||||||
|
Several Certificate Manager projects in one organization is not an error. Infisical designates one as the organization's **active** project, and that is what resolution picks:
|
||||||
|
|
||||||
|
```text
|
||||||
|
VERBOSE: -ProjectId was not supplied; resolved the organization's active Certificate Manager project
|
||||||
|
'Platform PKI' (aaaa...).
|
||||||
|
```
|
||||||
|
|
||||||
|
If no active project is designated, the first is used and the verbose line says so; pass `-ProjectId` to target another.
|
||||||
|
|
||||||
|
This is resolved client-side rather than left to the server because several PKI endpoints carry the project in the URL path (`/api/v1/projects/{projectId}/pki-subscribers`, `/certificates/search`) and cannot defer to the server's resolver.
|
||||||
|
|
||||||
|
#### One project per organization, in practice
|
||||||
|
|
||||||
|
An organization *can* hold several Certificate Manager projects, but **certificate applications are served only from the active one**. The applications router rejects anything else outright:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
if (req.internalCertManagerProjectId !== activeProjectId) {
|
||||||
|
throw new BadRequestError({ message: "Applications are only available on this organization's active Certificate Manager project." });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
So an application-centric workflow is single-project by design. Additional Certificate Manager projects can exist and hold their own CAs, policies, profiles, and certificates, but they are reachable only by passing `-ProjectId` explicitly, and applications will not work in them.
|
||||||
|
|
||||||
|
#### Projects contain applications
|
||||||
|
|
||||||
|
The two are different levels, which is worth keeping straight when reading output:
|
||||||
|
|
||||||
|
| | What it is | Cmdlet |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **Project** | The Certificate Manager project itself. One per organization in most setups. | `Get-InfisicalProject -Type cert-manager` |
|
||||||
|
| **Application** | A grouping of profiles, members, and certificates inside that project. | `Get-InfisicalCertificateApplication` |
|
||||||
|
|
||||||
|
So a project named `pki` can contain applications named `platform` and `endpoint-management`; listing projects returns only `pki`, because the others are not projects. Every application carries the `ProjectId` it belongs to, which is why that field is real rather than vestigial.
|
||||||
|
|
||||||
|
Profiles can be filtered to an application, matching how the console groups them:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$Application = Get-InfisicalCertificateApplication | Where-Object {($_.Name -ieq 'platform')}
|
||||||
|
Get-InfisicalCertificateProfile -ApplicationId $Application.Id -IncludeConfigs
|
||||||
|
Get-InfisicalCertificate -ApplicationId $Application.Id
|
||||||
|
```
|
||||||
|
|
||||||
### Example output
|
### Example output
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Id : 00000000-0000-0000-0000-000000000000
|
Id : 11111111-1111-1111-1111-111111111111
|
||||||
Name : Microsoft Endpoint Configuration Manager
|
ProjectId : 00000000-0000-0000-0000-000000000000
|
||||||
Slug : mecm
|
Name : platform
|
||||||
Description :
|
Description :
|
||||||
OrganizationId : 11111111-1111-1111-1111-111111111111
|
ProfileCount : 3
|
||||||
Type : cert-manager
|
MemberCount : 2
|
||||||
AutoCapitalization : False
|
CertificateCount : 0
|
||||||
EnvironmentSlugs : {dev, staging, prod}
|
CreatedAtUtc : 7/30/2026 10:05:37 PM +00:00
|
||||||
CreatedAtUtc : 3/12/2026 8:32:52 PM +00:00
|
UpdatedAtUtc : 7/30/2026 10:05:37 PM +00:00
|
||||||
UpdatedAtUtc : 6/21/2026 7:00:28 PM +00:00
|
|
||||||
|
|
||||||
Name : root-ca
|
|
||||||
CommonName : Contoso Root Certificate Authority
|
|
||||||
Type : internal
|
|
||||||
Status : active
|
|
||||||
KeyAlgorithm : RSA_2048
|
|
||||||
NotAfter : 03/25/2036 00:00:00
|
|
||||||
Id : 22222222-2222-2222-2222-222222222222
|
|
||||||
|
|
||||||
Name : intermediate-ca
|
|
||||||
CommonName : Contoso Intermediate Certificate Authority
|
|
||||||
Type : internal
|
|
||||||
Status : active
|
|
||||||
KeyAlgorithm : RSA_2048
|
|
||||||
NotAfter : 03/25/2031 00:00:00
|
|
||||||
Id : 33333333-3333-3333-3333-333333333333
|
|
||||||
|
|
||||||
Id : 44444444-4444-4444-4444-444444444444
|
Id : 44444444-4444-4444-4444-444444444444
|
||||||
ProjectId : 00000000-0000-0000-0000-000000000000
|
ProjectId : 00000000-0000-0000-0000-000000000000
|
||||||
@@ -266,6 +360,7 @@ WEB01.contoso.com
|
|||||||
127.0.0.1
|
127.0.0.1
|
||||||
::1
|
::1
|
||||||
|
|
||||||
|
VERBOSE: [...] - [Information] - [GetInfisicalCertificateApplicationCmdlet] - -ProjectId was not supplied; resolved the organization's only Certificate Manager project 'Platform PKI' (00000000-0000-0000-0000-000000000000).
|
||||||
VERBOSE: [...] - [Information] - [PkiClient] - Attempting to search Infisical certificates. Please Wait...
|
VERBOSE: [...] - [Information] - [PkiClient] - Attempting to search Infisical certificates. Please Wait...
|
||||||
VERBOSE: [...] - [Verbose] - [HttpClient] - Attempting HTTP POST to https://infisical.contoso.com/api/v1/projects/00000000-0000-0000-0000-000000000000/certificates/search. Please Wait...
|
VERBOSE: [...] - [Verbose] - [HttpClient] - Attempting HTTP POST to https://infisical.contoso.com/api/v1/projects/00000000-0000-0000-0000-000000000000/certificates/search. Please Wait...
|
||||||
VERBOSE: [...] - [Verbose] - [HttpClient] - HTTP POST completed with status 200.
|
VERBOSE: [...] - [Verbose] - [HttpClient] - HTTP POST completed with status 200.
|
||||||
@@ -309,6 +404,65 @@ Windows will report "The issuer of this certificate could not be found" until th
|
|||||||
|
|
||||||
The installed certificate's Windows friendly name defaults to the common name in upper case (`WEB01`), which is what shows in `certmgr`. Pass `-FriendlyName` on any parameter set to override it; on the `-CertificateAuthorityId` path the same value is also forwarded to Infisical as the issued certificate's `friendlyName`.
|
The installed certificate's Windows friendly name defaults to the common name in upper case (`WEB01`), which is what shows in `certmgr`. Pass `-FriendlyName` on any parameter set to override it; on the `-CertificateAuthorityId` path the same value is also forwarded to Infisical as the issued certificate's `friendlyName`.
|
||||||
|
|
||||||
|
### Metadata
|
||||||
|
|
||||||
|
`-Metadata` attaches key/value pairs to the certificate in Infisical, and accepts any `IDictionary` — a hashtable, an `[Ordered]` dictionary, or a generic `Dictionary[String,String]`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$RequestInfisicalCertificateParameters.Metadata = [Ordered]@{
|
||||||
|
Environment = 'Production'
|
||||||
|
Owner = 'Platform Engineering'
|
||||||
|
ManagedBy = 'Invoke-SecretStaging'
|
||||||
|
Site = 'HQ'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Only the supplied keys are reconciled.** Keys already on the certificate that this call does not mention are left alone, so several callers can each own their own keys without clobbering each other:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Request-InfisicalCertificate @Parameters -Metadata @{ Owner = 'Platform' } # certificate now has Owner
|
||||||
|
Request-InfisicalCertificate @Parameters -Metadata @{ Site = 'HQ' } # Owner survives; Site added
|
||||||
|
Request-InfisicalCertificate @Parameters -Metadata @{ Owner = 'Security' } # Owner updated; Site survives
|
||||||
|
```
|
||||||
|
|
||||||
|
This is done client-side. Infisical's `PATCH /certificates/{id}` replaces a certificate's metadata wholesale — the service deletes every row before inserting what it was sent — so the module reads the current set, merges the supplied keys over it, and writes back the union. When nothing would change, no request is sent at all.
|
||||||
|
|
||||||
|
Reconciliation runs on the reuse path too, so a metadata change lands without forcing reissuance. Values are flattened to strings (`443` becomes `"443"`, `$True` becomes `"True"`, `$Null` becomes `""`), keys are trimmed and compared case-insensitively, and blank keys are dropped.
|
||||||
|
|
||||||
|
The result carries the certificate's metadata after reconciliation:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$Certificate = Request-InfisicalCertificate @RequestInfisicalCertificateParameters
|
||||||
|
$Certificate.Metadata['Environment'] # Production
|
||||||
|
```
|
||||||
|
|
||||||
|
Metadata never fails an issuance that otherwise succeeded. By the time it is applied the certificate exists and may already be installed, so a failure is reported as a warning and the certificate is still returned.
|
||||||
|
|
||||||
|
Metadata is also a search filter — `Get-InfisicalCertificate` accepts `-Metadata` to find certificates by the keys you stamped on them.
|
||||||
|
|
||||||
|
### Reuse and renewal
|
||||||
|
|
||||||
|
A second run does not issue a new certificate if a still-valid one is already installed. That check is **scoped to the issuer you asked for**, not just the common name: the reuse search is filtered by `-CertificateProfileId` or `-CertificateAuthorityId`, so switching profiles issues a new certificate rather than handing back the old one.
|
||||||
|
|
||||||
|
This matters when two profiles over the same CA differ in key usage. Requesting from a client-authentication profile on a host that already holds a server-authentication certificate for the same name issues a new certificate, because a name match alone would return one with the wrong EKUs:
|
||||||
|
|
||||||
|
```text
|
||||||
|
VERBOSE: Reuse search for CN=WEB01 scoped to certificate profile 'a42f8446-...' returned 0 active certificate(s).
|
||||||
|
```
|
||||||
|
|
||||||
|
Reuse also requires the existing certificate to carry **every** name being requested. Adding an entry to `-DnsName` and re-running issues a new certificate rather than returning one that would fail validation for the name you just added:
|
||||||
|
|
||||||
|
```text
|
||||||
|
VERBOSE: An existing certificate for CN=WEB01 does not carry the requested name DNS:api.contoso.com;
|
||||||
|
requesting a new certificate rather than reusing one that would fail validation for it.
|
||||||
|
```
|
||||||
|
|
||||||
|
The rule is coverage, not equality — a certificate carrying more names than requested still satisfies the request. DNS names compare case-insensitively and IP addresses are normalized, so `::1` matches `0:0:0:0:0:0:0:1`. Removing a name from the request therefore reuses the existing certificate; use `-Force` when you need the SAN set trimmed rather than extended.
|
||||||
|
|
||||||
|
`-Force` issues unconditionally, and `-AllowRenewal` with `-RenewalThresholdDays` rotates a certificate that is inside its renewal window.
|
||||||
|
|
||||||
|
If Infisical cannot be reached, the reuse check cannot confirm which certificates belong to which issuer and falls back to matching on the common name alone. That is announced as a warning, since it can return a certificate from a different profile.
|
||||||
|
|
||||||
When the resolved location is `LocalMachine` and `-KeyStorageFlags` was not supplied, the private key is written to the machine key store. Without that the key lands in the calling user's profile while the certificate sits in `LocalMachine\My`, which is the usual cause of an installed certificate that reports no usable private key to a service.
|
When the resolved location is `LocalMachine` and `-KeyStorageFlags` was not supplied, the private key is written to the machine key store. Without that the key lands in the calling user's profile while the certificate sits in `LocalMachine\My`, which is the usual cause of an installed certificate that reports no usable private key to a service.
|
||||||
|
|
||||||
> **Non-elevated root installs prompt.** Adding a root to `CurrentUser\Root` makes Windows raise a modal trust dialog, and the call blocks until it is answered — if the dialog is hidden or the session is non-interactive (a scheduled task, an MECM task sequence), the cmdlet appears to hang indefinitely. It warns before blocking. Run elevated, or pass `-StoreLocation LocalMachine`, to install machine-wide with no prompt.
|
> **Non-elevated root installs prompt.** Adding a root to `CurrentUser\Root` makes Windows raise a modal trust dialog, and the call blocks until it is answered — if the dialog is hidden or the session is non-interactive (a scheduled task, an MECM task sequence), the cmdlet appears to hang indefinitely. It warns before blocking. Run elevated, or pass `-StoreLocation LocalMachine`, to install machine-wide with no prompt.
|
||||||
@@ -320,7 +474,7 @@ When the resolved location is `LocalMachine` and `-KeyStorageFlags` was not supp
|
|||||||
| Parameter | Common name | Use when |
|
| Parameter | Common name | Use when |
|
||||||
| -------------------------- | ------------------------------------ | ------------------------------------------------------------------------ |
|
| -------------------------- | ------------------------------------ | ------------------------------------------------------------------------ |
|
||||||
| `-CertificateProfileId` | **Per request**, constrained by policy | Fleet enrollment — many machines, each with its own CN. Works on any CA. |
|
| `-CertificateProfileId` | **Per request**, constrained by policy | Fleet enrollment — many machines, each with its own CN. Works on any CA. |
|
||||||
| `-CertificateAuthorityId` | **Per request**, unconstrained | Fleet enrollment where no policy is wanted. Needs direct issuance on the CA. |
|
| `-CertificateAuthorityId` | **Per request**, unconstrained | Rarely usable — needs direct issuance, which cannot be enabled (see below). |
|
||||||
| `-PkiSubscriberSlug` | **Fixed** by the subscriber record | One named identity — a specific service or host, provisioned in advance. |
|
| `-PkiSubscriberSlug` | **Fixed** by the subscriber record | One named identity — a specific service or host, provisioned in advance. |
|
||||||
|
|
||||||
**A PKI subscriber is a per-identity object, not a fleet template.** `signSubscriberCert` rejects any CSR whose CN differs from the subscriber's:
|
**A PKI subscriber is a per-identity object, not a fleet template.** `signSubscriberCert` rejects any CSR whose CN differs from the subscriber's:
|
||||||
@@ -353,8 +507,8 @@ Request-InfisicalCertificate @RequestInfisicalCertificateParameters -WhatIf
|
|||||||
A certificate profile binds a CA to a certificate policy. The policy constrains the subject, key usages, and extended key usages with `allowed`/`required`/`denied` lists, so the common name still varies per request while staying inside guardrails.
|
A certificate profile binds a CA to a certificate policy. The policy constrains the subject, key usages, and extended key usages with `allowed`/`required`/`denied` lists, so the common name still varies per request while staying inside guardrails.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Get-InfisicalCertificateProfile -ProjectId ($Project.Id) | Format-Table Id, Name, CaId
|
Get-InfisicalCertificateProfile | Format-Table Id, Slug, CaId, EnrollmentType
|
||||||
Get-InfisicalCertificatePolicy -ProjectId ($Project.Id) | Format-Table Id, Name
|
Get-InfisicalCertificatePolicy | Format-Table Id, Name
|
||||||
```
|
```
|
||||||
|
|
||||||
Profile issuance is the only path that does **not** consult the CA's direct-issuance flag — the service short-circuits it:
|
Profile issuance is the only path that does **not** consult the CA's direct-issuance flag — the service short-circuits it:
|
||||||
@@ -368,7 +522,7 @@ So a profile issues successfully against a CA whose `EnableDirectIssuance` is `F
|
|||||||
#### Discovering subscribers
|
#### Discovering subscribers
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Get-InfisicalPkiSubscriber -ProjectId ($Project.Id) |
|
Get-InfisicalPkiSubscriber |
|
||||||
Format-Table Name, CommonName, Status, Ttl, CaId
|
Format-Table Name, CommonName, Status, Ttl, CaId
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -390,13 +544,21 @@ t.renameColumn("requireTemplateForIssuance", "enableDirectIssuance");
|
|||||||
.update({ name: slugifiedName, enableDirectIssuance: !ca.enableDirectIssuance });
|
.update({ name: slugifiedName, enableDirectIssuance: !ca.enableDirectIssuance });
|
||||||
```
|
```
|
||||||
|
|
||||||
Any CA created before that migration with "require template for issuance" enabled now reads `EnableDirectIssuance = False` permanently. The options are to **use a profile** (which ignores the flag), or to create a new CA — new CAs default to `true`.
|
Any CA created before that migration with "require template for issuance" enabled now reads `EnableDirectIssuance = False` permanently.
|
||||||
|
|
||||||
|
Creating a new CA does not help either. Although the database column defaults to `true`, the creation service passes `false` explicitly:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const ca = await certificateAuthorityDAL.create({ projectId, name: resolvedCaName, status, enableDirectIssuance: false }, tx);
|
||||||
|
```
|
||||||
|
|
||||||
|
So **every CA created through the API or UI has direct issuance disabled**, and nothing can turn it on afterwards. Use a certificate profile, which ignores the flag entirely.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Get-InfisicalCertificateAuthority -ProjectId ($Project.Id) -Kind Internal |
|
Get-InfisicalCertificateAuthority -Kind Internal |
|
||||||
Format-Table Name, CommonName, Status, EnableDirectIssuance
|
Format-Table Name, CommonName, Status, EnableDirectIssuance
|
||||||
|
|
||||||
$Ca = Get-InfisicalCertificateAuthority -ProjectId ($Project.Id) -Kind Internal |
|
$Ca = Get-InfisicalCertificateAuthority -Kind Internal |
|
||||||
Where-Object {($_.EnableDirectIssuance -eq $True)} |
|
Where-Object {($_.EnableDirectIssuance -eq $True)} |
|
||||||
Select-Object -First 1
|
Select-Object -First 1
|
||||||
|
|
||||||
@@ -452,7 +614,7 @@ A failing item does not abort the batch:
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
'web01', 'does-not-exist', 'web02' |
|
'web01', 'does-not-exist', 'web02' |
|
||||||
ForEach-Object { Get-InfisicalPkiSubscriber -ProjectId $ProjectId -Name $_ -ErrorAction SilentlyContinue }
|
ForEach-Object { Get-InfisicalPkiSubscriber -Name $_ -ErrorAction SilentlyContinue }
|
||||||
# emits web01 and web02; the failure is available in $Error
|
# emits web01 and web02; the failure is available in $Error
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -488,16 +650,15 @@ The resolver matches case-insensitively against patterns aligned with Infisical'
|
|||||||
|
|
||||||
| Parameter | Example variable names matched |
|
| Parameter | Example variable names matched |
|
||||||
| ----------------- | ------------------------------------------------------------------------------------ |
|
| ----------------- | ------------------------------------------------------------------------------------ |
|
||||||
| `BaseUri` | `INFISICAL_API_URL`, `INFISICAL_BASE_URL`, `INFISICAL_HOST` |
|
| `BaseUri` | `INFISICAL_API_URL`, `INFISICAL_BASE_URL`, `INFISICAL_BASE_URI`, `INFISICAL_HOST` |
|
||||||
| `OrganizationId` | `INFISICAL_ORG_ID`, `INFISICAL_ORGANIZATION_ID` |
|
| `OrganizationId` | `INFISICAL_ORG_ID`, `INFISICAL_ORGANIZATION_ID` |
|
||||||
| `ProjectId` | `INFISICAL_PROJECT_ID`, `INFISICAL_WORKSPACE_ID` |
|
|
||||||
| `Environment` | `INFISICAL_ENVIRONMENT`, `INFISICAL_ENV`, `INFISICAL_ENV_SLUG` |
|
|
||||||
| `ClientId` | `INFISICAL_CLIENT_ID`, `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` |
|
| `ClientId` | `INFISICAL_CLIENT_ID`, `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` |
|
||||||
| `ClientSecret` | `INFISICAL_CLIENT_SECRET`, `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` |
|
| `ClientSecret` | `INFISICAL_CLIENT_SECRET`, `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` |
|
||||||
| `AccessToken` | `INFISICAL_TOKEN`, `INFISICAL_ACCESS_TOKEN`, `INFISICAL_AUTH_TOKEN` |
|
| `AccessToken` | `INFISICAL_TOKEN`, `INFISICAL_ACCESS_TOKEN`, `INFISICAL_AUTH_TOKEN` |
|
||||||
| `SecretPath` | `INFISICAL_SECRET_PATH`, `INFISICAL_DEFAULT_SECRET_PATH` |
|
|
||||||
| `ApiVersion` | `INFISICAL_API_VERSION` |
|
| `ApiVersion` | `INFISICAL_API_VERSION` |
|
||||||
|
|
||||||
|
Discovery covers the connection itself. Project, environment, and secret path are per-call parameters, so they are not resolved from the environment.
|
||||||
|
|
||||||
Sensitive values (`ClientSecret`, `AccessToken`) are read directly into a read-only `SecureString` and never logged.
|
Sensitive values (`ClientSecret`, `AccessToken`) are read directly into a read-only `SecureString` and never logged.
|
||||||
|
|
||||||
### Zero-configuration example
|
### Zero-configuration example
|
||||||
@@ -552,7 +713,7 @@ To add a route:
|
|||||||
|
|
||||||
### Adding a new cmdlet
|
### Adding a new cmdlet
|
||||||
|
|
||||||
Cmdlets live in `src/PSInfisicalAPI/Cmdlets/` and derive from `InfisicalCmdletBase`, which exposes `HttpClient`, `Logger`, `ResolveProjectId`, and `ThrowTerminatingForException`. Follow the consolidated discovery pattern when the cmdlet supports both list and single-record retrieval:
|
Cmdlets live in `src/PSInfisicalAPI/Cmdlets/` and derive from `InfisicalCmdletBase`, which exposes `HttpClient`, `Logger`, `ResolveCertManagerProjectId`, and `ThrowTerminatingForException`. Follow the consolidated discovery pattern when the cmdlet supports both list and single-record retrieval:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
[Cmdlet(VerbsCommon.Get, "InfisicalPkiSubscriber", DefaultParameterSetName = "List")]
|
[Cmdlet(VerbsCommon.Get, "InfisicalPkiSubscriber", DefaultParameterSetName = "List")]
|
||||||
@@ -577,7 +738,7 @@ After adding (or removing) a cmdlet:
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$Params = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
|
$Params = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
$Params.ProjectId = (Get-InfisicalProject | Select-Object -First 1).Id
|
$Params.ProjectId = (Get-InfisicalProject -Type cert-manager | Select-Object -First 1).Id
|
||||||
$Result = Get-InfisicalPkiSubscriber @Params
|
$Result = Get-InfisicalPkiSubscriber @Params
|
||||||
```
|
```
|
||||||
4. Add a `## Unreleased` entry to `CHANGELOG.md` describing the change (mark removals of public cmdlets or parameters as **BREAKING**).
|
4. Add a `## Unreleased` entry to `CHANGELOG.md` describing the change (mark removals of public cmdlets or parameters as **BREAKING**).
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -156,7 +156,22 @@ function Write-Manifest {
|
|||||||
'Write-InfisicalScepMdmProfileToWmi',
|
'Write-InfisicalScepMdmProfileToWmi',
|
||||||
'Start-InfisicalProcess',
|
'Start-InfisicalProcess',
|
||||||
'Get-InfisicalEnvironmentVariable',
|
'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 = @()
|
AliasesToExport = @()
|
||||||
VariablesToExport = @()
|
VariablesToExport = @()
|
||||||
@@ -178,6 +193,15 @@ function Write-Manifest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Update-Changelog {
|
function Update-Changelog {
|
||||||
|
<#
|
||||||
|
Promotes whatever sits under "## Unreleased" into a section for this version, leaving a fresh empty
|
||||||
|
"## Unreleased" above it.
|
||||||
|
|
||||||
|
The release workflow builds its release body by extracting the section whose heading matches the version
|
||||||
|
it just produced. Inserting the version heading above the notes - as this previously did - left that
|
||||||
|
section containing only the build line while the actual notes stayed under "Unreleased", so every
|
||||||
|
release published an empty changelog.
|
||||||
|
#>
|
||||||
param([string]$Version, [string]$CommitHash)
|
param([string]$Version, [string]$CommitHash)
|
||||||
|
|
||||||
if (-not $ChangelogFile.Exists) { return }
|
if (-not $ChangelogFile.Exists) { return }
|
||||||
@@ -185,11 +209,31 @@ function Update-Changelog {
|
|||||||
$existing = Get-Content -LiteralPath $ChangelogFile.FullName -Raw
|
$existing = Get-Content -LiteralPath $ChangelogFile.FullName -Raw
|
||||||
if ($existing -match [Regex]::Escape($marker)) { return }
|
if ($existing -match [Regex]::Escape($marker)) { return }
|
||||||
|
|
||||||
$insertion = "## $Version`r`n`r`n- Build produced from commit $CommitHash.`r`n`r`n"
|
$unreleasedRegex = [regex]::new('(?m)^## Unreleased[^\r\n]*\r?$')
|
||||||
$unreleasedRegex = [regex]::new('(?m)^## Unreleased\r?$')
|
$unreleasedMatch = $unreleasedRegex.Match($existing)
|
||||||
if (-not $unreleasedRegex.IsMatch($existing)) { return }
|
if (-not $unreleasedMatch.Success) { return }
|
||||||
$updated = $unreleasedRegex.Replace($existing, "## Unreleased`r`n`r`n$insertion## Unreleased (carried forward)", 1)
|
|
||||||
[System.IO.File]::WriteAllText($ChangelogFile.FullName, $updated, [System.Text.UTF8Encoding]::new($false))
|
# Everything from just after the Unreleased heading to the next "## " heading is this version's notes.
|
||||||
|
$bodyStart = $unreleasedMatch.Index + $unreleasedMatch.Length
|
||||||
|
$nextHeading = [regex]::new('(?m)^## ').Match($existing, $bodyStart)
|
||||||
|
$bodyEnd = if ($nextHeading.Success) { $nextHeading.Index } else { $existing.Length }
|
||||||
|
|
||||||
|
$notes = $existing.Substring($bodyStart, $bodyEnd - $bodyStart).Trim()
|
||||||
|
|
||||||
|
# Italicised and last so it reads as provenance rather than as another entry in whichever section the
|
||||||
|
# notes happened to end on.
|
||||||
|
$buildLine = "_Build produced from commit $CommitHash._"
|
||||||
|
$versionBody = if ([string]::IsNullOrWhiteSpace($notes)) { $buildLine } else { "$notes`r`n`r`n$buildLine" }
|
||||||
|
|
||||||
|
$rebuilt = New-Object System.Text.StringBuilder
|
||||||
|
[void]$rebuilt.Append($existing.Substring(0, $unreleasedMatch.Index))
|
||||||
|
[void]$rebuilt.Append("## Unreleased`r`n`r`n")
|
||||||
|
[void]$rebuilt.Append("## $Version`r`n`r`n")
|
||||||
|
[void]$rebuilt.Append($versionBody)
|
||||||
|
[void]$rebuilt.Append("`r`n`r`n")
|
||||||
|
[void]$rebuilt.Append($existing.Substring($bodyEnd))
|
||||||
|
|
||||||
|
[System.IO.File]::WriteAllText($ChangelogFile.FullName, $rebuilt.ToString(), [System.Text.UTF8Encoding]::new($false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -221,7 +265,7 @@ if (`$cmds.Count -eq 0) {
|
|||||||
throw "No cmdlets were exported by the PSInfisicalAPI module."
|
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) {
|
foreach (`$expected in `$expectedCmds) {
|
||||||
if (-not (Get-Command -Name `$expected -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) {
|
if (-not (Get-Command -Name `$expected -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) {
|
||||||
throw "Cmdlet not found: `$expected"
|
throw "Cmdlet not found: `$expected"
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.Reflection;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Tests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// PATCH /certificates/{id} replaces a certificate's metadata wholesale, so "reconcile only the supplied
|
||||||
|
/// keys" has to be produced client-side by merging over the current set. These pin that merge.
|
||||||
|
/// </summary>
|
||||||
|
public class CertificateMetadataTests
|
||||||
|
{
|
||||||
|
private static readonly Assembly ModuleAssembly = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly;
|
||||||
|
|
||||||
|
private static Dictionary<string, string> NormalizeMetadata(IDictionary source)
|
||||||
|
{
|
||||||
|
Type cmdletType = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet", true);
|
||||||
|
MethodInfo method = cmdletType.GetMethod("NormalizeMetadata", BindingFlags.NonPublic | BindingFlags.Static);
|
||||||
|
Assert.NotNull(method);
|
||||||
|
return (Dictionary<string, string>)method.Invoke(null, new object[] { source });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Metadata_Parameter_Accepts_Any_IDictionary_On_Every_Parameter_Set()
|
||||||
|
{
|
||||||
|
Type cmdletType = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet", true);
|
||||||
|
PropertyInfo metadata = cmdletType.GetProperty("Metadata");
|
||||||
|
Assert.NotNull(metadata);
|
||||||
|
Assert.Equal(typeof(IDictionary), metadata.PropertyType);
|
||||||
|
|
||||||
|
foreach (CustomAttributeData attribute in metadata.GetCustomAttributesData())
|
||||||
|
{
|
||||||
|
if (attribute.AttributeType != typeof(System.Management.Automation.ParameterAttribute)) { continue; }
|
||||||
|
foreach (CustomAttributeNamedArgument named in attribute.NamedArguments)
|
||||||
|
{
|
||||||
|
Assert.NotEqual("ParameterSetName", named.MemberName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Hashtable_And_OrderedDictionary_Both_Normalize()
|
||||||
|
{
|
||||||
|
Hashtable hashtable = new Hashtable { { "Environment", "Production" }, { "Owner", "Platform" } };
|
||||||
|
Dictionary<string, string> fromHashtable = NormalizeMetadata(hashtable);
|
||||||
|
Assert.Equal(2, fromHashtable.Count);
|
||||||
|
Assert.Equal("Production", fromHashtable["Environment"]);
|
||||||
|
|
||||||
|
OrderedDictionary ordered = new OrderedDictionary();
|
||||||
|
ordered.Add("Environment", "Production");
|
||||||
|
ordered.Add("Owner", "Platform");
|
||||||
|
Dictionary<string, string> fromOrdered = NormalizeMetadata(ordered);
|
||||||
|
Assert.Equal(2, fromOrdered.Count);
|
||||||
|
Assert.Equal("Platform", fromOrdered["Owner"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Non_String_Values_Are_Flattened_For_An_Api_That_Takes_Only_Strings()
|
||||||
|
{
|
||||||
|
Hashtable source = new Hashtable
|
||||||
|
{
|
||||||
|
{ "Port", 443 },
|
||||||
|
{ "Enabled", true },
|
||||||
|
{ "Ratio", 1.5d },
|
||||||
|
{ "Issued", new DateTime(2026, 7, 30, 0, 0, 0, DateTimeKind.Utc) }
|
||||||
|
};
|
||||||
|
|
||||||
|
Dictionary<string, string> result = NormalizeMetadata(source);
|
||||||
|
|
||||||
|
Assert.Equal("443", result["Port"]);
|
||||||
|
Assert.Equal("True", result["Enabled"]);
|
||||||
|
Assert.Equal("1.5", result["Ratio"]);
|
||||||
|
Assert.False(string.IsNullOrEmpty(result["Issued"]));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Null_Values_Become_Empty_Strings_And_Blank_Keys_Are_Dropped()
|
||||||
|
{
|
||||||
|
// The API models a valueless key as an empty string, and rejects an empty key outright.
|
||||||
|
Hashtable source = new Hashtable
|
||||||
|
{
|
||||||
|
{ "Present", null },
|
||||||
|
{ " ", "orphan" },
|
||||||
|
{ " Padded ", "trimmed" }
|
||||||
|
};
|
||||||
|
|
||||||
|
Dictionary<string, string> result = NormalizeMetadata(source);
|
||||||
|
|
||||||
|
Assert.Equal(string.Empty, result["Present"]);
|
||||||
|
Assert.False(result.ContainsKey(" "));
|
||||||
|
Assert.True(result.ContainsKey("Padded"));
|
||||||
|
Assert.Equal("trimmed", result["Padded"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Keys_Are_Case_Insensitive()
|
||||||
|
{
|
||||||
|
Hashtable source = new Hashtable { { "Environment", "Production" } };
|
||||||
|
Dictionary<string, string> result = NormalizeMetadata(source);
|
||||||
|
Assert.Equal("Production", result["ENVIRONMENT"]);
|
||||||
|
Assert.Equal("Production", result["environment"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void A_Null_Or_Empty_Dictionary_Normalizes_To_Nothing()
|
||||||
|
{
|
||||||
|
Assert.Empty(NormalizeMetadata(null));
|
||||||
|
Assert.Empty(NormalizeMetadata(new Hashtable()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Update_Request_Serializes_As_The_Key_Value_Array_The_Api_Expects()
|
||||||
|
{
|
||||||
|
Type requestType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalUpdateCertificateMetadataRequestDto", true);
|
||||||
|
Type entryType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateMetadataEntryDto", true);
|
||||||
|
|
||||||
|
object entry = Activator.CreateInstance(entryType);
|
||||||
|
entryType.GetProperty("Key").SetValue(entry, "Environment");
|
||||||
|
entryType.GetProperty("Value").SetValue(entry, "Production");
|
||||||
|
|
||||||
|
Type listType = typeof(List<>).MakeGenericType(entryType);
|
||||||
|
object list = Activator.CreateInstance(listType);
|
||||||
|
listType.GetMethod("Add").Invoke(list, new object[] { entry });
|
||||||
|
|
||||||
|
object request = Activator.CreateInstance(requestType);
|
||||||
|
requestType.GetProperty("Metadata").SetValue(request, list);
|
||||||
|
|
||||||
|
string json = JsonConvert.SerializeObject(request);
|
||||||
|
Assert.Equal("{\"metadata\":[{\"key\":\"Environment\",\"value\":\"Production\"}]}", json);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Response_Metadata_Maps_To_A_Case_Insensitive_Dictionary()
|
||||||
|
{
|
||||||
|
Type mapper = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateMapper", true);
|
||||||
|
MethodInfo map = mapper.GetMethod("MapMetadata", BindingFlags.Public | BindingFlags.Static);
|
||||||
|
Assert.NotNull(map);
|
||||||
|
|
||||||
|
Type entryType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateSearchMetadataEntryDto", true);
|
||||||
|
Array entries = Array.CreateInstance(entryType, 2);
|
||||||
|
|
||||||
|
object first = Activator.CreateInstance(entryType);
|
||||||
|
entryType.GetProperty("Key").SetValue(first, "Environment");
|
||||||
|
entryType.GetProperty("Value").SetValue(first, "Production");
|
||||||
|
entries.SetValue(first, 0);
|
||||||
|
|
||||||
|
object second = Activator.CreateInstance(entryType);
|
||||||
|
entryType.GetProperty("Key").SetValue(second, "Owner");
|
||||||
|
entryType.GetProperty("Value").SetValue(second, null);
|
||||||
|
entries.SetValue(second, 1);
|
||||||
|
|
||||||
|
Dictionary<string, string> result = (Dictionary<string, string>)map.Invoke(null, new object[] { entries });
|
||||||
|
|
||||||
|
Assert.Equal(2, result.Count);
|
||||||
|
Assert.Equal("Production", result["ENVIRONMENT"]);
|
||||||
|
Assert.Equal(string.Empty, result["Owner"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Absent_Response_Metadata_Maps_To_An_Empty_Dictionary_Not_Null()
|
||||||
|
{
|
||||||
|
Type mapper = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateMapper", true);
|
||||||
|
MethodInfo map = mapper.GetMethod("MapMetadata", BindingFlags.Public | BindingFlags.Static);
|
||||||
|
|
||||||
|
Dictionary<string, string> result = (Dictionary<string, string>)map.Invoke(null, new object[] { null });
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
// current supplied expected merged
|
||||||
|
[InlineData("a=1;b=2", "c=3", "a=1;b=2;c=3")] // untouched keys survive a partial update
|
||||||
|
[InlineData("a=1;b=2", "b=9", "a=1;b=9")] // supplied key wins
|
||||||
|
[InlineData("", "a=1", "a=1")] // first write onto a bare certificate
|
||||||
|
[InlineData("a=1", "A=2", "a=2")] // case-insensitive key collision updates in place
|
||||||
|
[InlineData("a=1;b=2", "", "a=1;b=2")] // nothing supplied changes nothing
|
||||||
|
public void Merge_Reconciles_Only_The_Supplied_Keys(string current, string supplied, string expected)
|
||||||
|
{
|
||||||
|
// Mirrors ReconcileCertificateMetadata's merge, which cannot be exercised directly without an
|
||||||
|
// HTTP round trip. The rule under test: start from current, overlay supplied, never remove.
|
||||||
|
Dictionary<string, string> merged = new Dictionary<string, string>(Parse(current), StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (KeyValuePair<string, string> pair in Parse(supplied))
|
||||||
|
{
|
||||||
|
merged[pair.Key] = pair.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal(Format(Parse(expected)), Format(merged));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, string> Parse(string value)
|
||||||
|
{
|
||||||
|
Dictionary<string, string> result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
if (string.IsNullOrEmpty(value)) { return result; }
|
||||||
|
|
||||||
|
foreach (string pair in value.Split(';'))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(pair)) { continue; }
|
||||||
|
string[] parts = pair.Split('=');
|
||||||
|
result[parts[0]] = parts.Length > 1 ? parts[1] : string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Format(Dictionary<string, string> value)
|
||||||
|
{
|
||||||
|
List<string> pairs = new List<string>();
|
||||||
|
foreach (KeyValuePair<string, string> entry in value)
|
||||||
|
{
|
||||||
|
pairs.Add(string.Concat(entry.Key.ToLowerInvariant(), "=", entry.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
pairs.Sort(StringComparer.Ordinal);
|
||||||
|
return string.Join(";", pairs.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Result_Object_Carries_The_Reconciled_Metadata()
|
||||||
|
{
|
||||||
|
PSInfisicalAPI.Models.InfisicalCertificateResult result = new PSInfisicalAPI.Models.InfisicalCertificateResult();
|
||||||
|
Assert.Null(result.Metadata);
|
||||||
|
|
||||||
|
result.Metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { { "Environment", "Production" } };
|
||||||
|
Assert.Equal("Production", result.Metadata["environment"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Metadata_Update_Endpoint_Is_Registered_For_Both_Route_Namespaces()
|
||||||
|
{
|
||||||
|
IReadOnlyList<PSInfisicalAPI.Endpoints.InfisicalEndpointDefinition> candidates =
|
||||||
|
PSInfisicalAPI.Endpoints.InfisicalEndpointRegistry.GetCandidates(
|
||||||
|
PSInfisicalAPI.Endpoints.InfisicalEndpointNames.UpdateCertificateMetadata);
|
||||||
|
|
||||||
|
Assert.Contains(candidates, c => c.Template == "/api/v1/cert-manager/certificates/{certificateId}");
|
||||||
|
Assert.Contains(candidates, c => c.Template == "/api/v1/pki/certificates/{certificateId}");
|
||||||
|
Assert.All(candidates, c => Assert.Equal("PATCH", c.Method));
|
||||||
|
Assert.All(candidates, c => Assert.True(c.RequiresAuthorization));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
using PSInfisicalAPI.Pki;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Tests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Reuse must be scoped to the issuer being requested. Two profiles over the same CA issue certificates with
|
||||||
|
/// the same common name but different key usages (server authentication vs client authentication), so a
|
||||||
|
/// name-only match hands back a certificate that does not satisfy the request that was made.
|
||||||
|
/// </summary>
|
||||||
|
public class CertificateReuseScopingTests
|
||||||
|
{
|
||||||
|
private static readonly Assembly ModuleAssembly = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly;
|
||||||
|
|
||||||
|
private static string InvokeApplyIssuerScope(PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet cmdlet, InfisicalCertificateSearchQuery query)
|
||||||
|
{
|
||||||
|
MethodInfo method = cmdlet.GetType().GetMethod("ApplyIssuerScope", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
Assert.NotNull(method);
|
||||||
|
return (string)method.Invoke(cmdlet, new object[] { query });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Profile_Issuance_Scopes_The_Reuse_Search_To_That_Profile()
|
||||||
|
{
|
||||||
|
PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet cmdlet = new PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet
|
||||||
|
{
|
||||||
|
ProjectId = "proj-1",
|
||||||
|
CertificateProfileId = "profile-clientauth"
|
||||||
|
};
|
||||||
|
|
||||||
|
InfisicalCertificateSearchQuery query = new InfisicalCertificateSearchQuery();
|
||||||
|
string scope = InvokeApplyIssuerScope(cmdlet, query);
|
||||||
|
|
||||||
|
Assert.Equal(new[] { "profile-clientauth" }, query.ProfileIds);
|
||||||
|
Assert.Null(query.CaIds);
|
||||||
|
Assert.Contains("profile-clientauth", scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Ca_Issuance_Scopes_The_Reuse_Search_To_That_Ca()
|
||||||
|
{
|
||||||
|
PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet cmdlet = new PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet
|
||||||
|
{
|
||||||
|
ProjectId = "proj-1",
|
||||||
|
CertificateAuthorityId = "ca-1"
|
||||||
|
};
|
||||||
|
|
||||||
|
InfisicalCertificateSearchQuery query = new InfisicalCertificateSearchQuery();
|
||||||
|
string scope = InvokeApplyIssuerScope(cmdlet, query);
|
||||||
|
|
||||||
|
Assert.Equal(new[] { "ca-1" }, query.CaIds);
|
||||||
|
Assert.Null(query.ProfileIds);
|
||||||
|
Assert.Contains("ca-1", scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Subscriber_Issuance_Needs_No_Server_Side_Scope()
|
||||||
|
{
|
||||||
|
// A subscriber pins its own common name, so a name match is already a subscriber match.
|
||||||
|
PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet cmdlet = new PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet
|
||||||
|
{
|
||||||
|
ProjectId = "proj-1",
|
||||||
|
PkiSubscriberSlug = "web-tier"
|
||||||
|
};
|
||||||
|
|
||||||
|
InfisicalCertificateSearchQuery query = new InfisicalCertificateSearchQuery();
|
||||||
|
string scope = InvokeApplyIssuerScope(cmdlet, query);
|
||||||
|
|
||||||
|
Assert.Null(query.ProfileIds);
|
||||||
|
Assert.Null(query.CaIds);
|
||||||
|
Assert.Contains("web-tier", scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Two_Profiles_Produce_Distinct_Reuse_Scopes()
|
||||||
|
{
|
||||||
|
InfisicalCertificateSearchQuery serverQuery = new InfisicalCertificateSearchQuery();
|
||||||
|
InvokeApplyIssuerScope(new PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet { CertificateProfileId = "profile-serverauth" }, serverQuery);
|
||||||
|
|
||||||
|
InfisicalCertificateSearchQuery clientQuery = new InfisicalCertificateSearchQuery();
|
||||||
|
InvokeApplyIssuerScope(new PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet { CertificateProfileId = "profile-clientauth" }, clientQuery);
|
||||||
|
|
||||||
|
Assert.NotEqual(serverQuery.ProfileIds[0], clientQuery.ProfileIds[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Issuer_Scope_Survives_Serialization_Into_The_Search_Request()
|
||||||
|
{
|
||||||
|
// The scope is only effective if it actually reaches the wire.
|
||||||
|
Type clientType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalPkiClient", true);
|
||||||
|
MethodInfo build = clientType.GetMethod("BuildSearchRequest", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public);
|
||||||
|
Assert.NotNull(build);
|
||||||
|
|
||||||
|
InfisicalCertificateSearchQuery query = new InfisicalCertificateSearchQuery
|
||||||
|
{
|
||||||
|
ProjectId = "proj-1",
|
||||||
|
CommonName = "WEB01",
|
||||||
|
Status = "active",
|
||||||
|
ProfileIds = new[] { "profile-clientauth" }
|
||||||
|
};
|
||||||
|
|
||||||
|
object dto = build.Invoke(null, new object[] { query });
|
||||||
|
Assert.NotNull(dto);
|
||||||
|
|
||||||
|
PropertyInfo profileIds = dto.GetType().GetProperty("ProfileIds");
|
||||||
|
Assert.NotNull(profileIds);
|
||||||
|
Assert.Equal(new[] { "profile-clientauth" }, (string[])profileIds.GetValue(dto));
|
||||||
|
|
||||||
|
string json = Newtonsoft.Json.JsonConvert.SerializeObject(dto);
|
||||||
|
Assert.Contains("profileIds", json);
|
||||||
|
Assert.Contains("profile-clientauth", json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using PSInfisicalAPI.Pki;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Tests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Reuse must not return a certificate that predates a newly requested SAN. These build real certificates
|
||||||
|
/// through the module's own CSR path so the SAN reader is exercised against genuine DER, not a hand-rolled
|
||||||
|
/// approximation of it.
|
||||||
|
/// </summary>
|
||||||
|
public class CertificateSanCoverageTests
|
||||||
|
{
|
||||||
|
private static readonly Assembly ModuleAssembly = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Produces a self-signed certificate carrying exactly the requested SANs, by round-tripping the module's
|
||||||
|
/// CSR builder output into a signed certificate.
|
||||||
|
/// </summary>
|
||||||
|
private static X509Certificate2 CreateCertificateWithSans(string commonName, string[] dnsNames, string[] ipAddresses)
|
||||||
|
{
|
||||||
|
InfisicalCsrSubject subject = new InfisicalCsrSubject { CommonName = commonName };
|
||||||
|
InfisicalCsrResult csr = InfisicalCsrBuilder.Build(subject, dnsNames, ipAddresses, new InfisicalCsrOptions());
|
||||||
|
|
||||||
|
Org.BouncyCastle.Pkcs.Pkcs10CertificationRequest request;
|
||||||
|
using (System.IO.StringReader reader = new System.IO.StringReader(csr.CsrPem))
|
||||||
|
{
|
||||||
|
Org.BouncyCastle.OpenSsl.PemReader pemReader = new Org.BouncyCastle.OpenSsl.PemReader(reader);
|
||||||
|
request = (Org.BouncyCastle.Pkcs.Pkcs10CertificationRequest)pemReader.ReadObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
Org.BouncyCastle.Asn1.Pkcs.CertificationRequestInfo info = request.GetCertificationRequestInfo();
|
||||||
|
Org.BouncyCastle.Asn1.X509.X509Extensions extensions = null;
|
||||||
|
foreach (Org.BouncyCastle.Asn1.Asn1Encodable attributeEncodable in info.Attributes)
|
||||||
|
{
|
||||||
|
Org.BouncyCastle.Asn1.Cms.Attribute attribute = Org.BouncyCastle.Asn1.Cms.Attribute.GetInstance(attributeEncodable);
|
||||||
|
if (attribute.AttrType.Equals(Org.BouncyCastle.Asn1.Pkcs.PkcsObjectIdentifiers.Pkcs9AtExtensionRequest))
|
||||||
|
{
|
||||||
|
extensions = Org.BouncyCastle.Asn1.X509.X509Extensions.GetInstance(attribute.AttrValues[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.NotNull(extensions);
|
||||||
|
Org.BouncyCastle.Asn1.X509.X509Extension sanExtension =
|
||||||
|
extensions.GetExtension(Org.BouncyCastle.Asn1.X509.X509Extensions.SubjectAlternativeName);
|
||||||
|
Assert.NotNull(sanExtension);
|
||||||
|
|
||||||
|
using (RSA rsa = RSA.Create(2048))
|
||||||
|
{
|
||||||
|
CertificateRequest netRequest = new CertificateRequest(
|
||||||
|
string.Concat("CN=", commonName), rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||||
|
|
||||||
|
netRequest.CertificateExtensions.Add(new X509Extension(
|
||||||
|
new Oid("2.5.29.17"),
|
||||||
|
sanExtension.Value.GetOctets(),
|
||||||
|
false));
|
||||||
|
|
||||||
|
return netRequest.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddDays(30));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CoversRequestedNames(X509Certificate2 cert, string[] dns, string[] ips, out string missing)
|
||||||
|
{
|
||||||
|
Type reader = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateSanReader", true);
|
||||||
|
MethodInfo method = reader.GetMethod("CoversRequestedNames", BindingFlags.Public | BindingFlags.Static);
|
||||||
|
Assert.NotNull(method);
|
||||||
|
|
||||||
|
object[] args = new object[] { cert, dns, ips, null };
|
||||||
|
bool result = (bool)method.Invoke(null, args);
|
||||||
|
missing = (string)args[3];
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (HashSet<string> Dns, HashSet<string> Ips) ReadSans(X509Certificate2 cert)
|
||||||
|
{
|
||||||
|
Type reader = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateSanReader", true);
|
||||||
|
MethodInfo read = reader.GetMethod("Read", BindingFlags.Public | BindingFlags.Static);
|
||||||
|
object sans = read.Invoke(null, new object[] { cert });
|
||||||
|
|
||||||
|
HashSet<string> dns = (HashSet<string>)sans.GetType().GetProperty("DnsNames").GetValue(sans);
|
||||||
|
HashSet<string> ips = (HashSet<string>)sans.GetType().GetProperty("IpAddresses").GetValue(sans);
|
||||||
|
return (dns, ips);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Reader_Recovers_Both_Dns_And_Ip_Sans()
|
||||||
|
{
|
||||||
|
using (X509Certificate2 cert = CreateCertificateWithSans(
|
||||||
|
"WEB01",
|
||||||
|
new[] { "WEB01", "WEB01.contoso.com" },
|
||||||
|
new[] { "10.20.30.40", "127.0.0.1", "::1" }))
|
||||||
|
{
|
||||||
|
(HashSet<string> dns, HashSet<string> ips) = ReadSans(cert);
|
||||||
|
|
||||||
|
Assert.Equal(2, dns.Count);
|
||||||
|
Assert.Contains("WEB01", dns);
|
||||||
|
Assert.Contains("WEB01.contoso.com", dns);
|
||||||
|
|
||||||
|
Assert.Equal(3, ips.Count);
|
||||||
|
Assert.Contains("10.20.30.40", ips);
|
||||||
|
Assert.Contains("127.0.0.1", ips);
|
||||||
|
Assert.Contains("::1", ips);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void A_Certificate_Covering_Every_Requested_Name_Is_Reusable()
|
||||||
|
{
|
||||||
|
using (X509Certificate2 cert = CreateCertificateWithSans(
|
||||||
|
"WEB01", new[] { "WEB01", "WEB01.contoso.com" }, new[] { "10.20.30.40" }))
|
||||||
|
{
|
||||||
|
string missing;
|
||||||
|
Assert.True(CoversRequestedNames(cert, new[] { "WEB01", "WEB01.contoso.com" }, new[] { "10.20.30.40" }, out missing));
|
||||||
|
Assert.Null(missing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void A_Newly_Requested_Dns_Name_Disqualifies_The_Existing_Certificate()
|
||||||
|
{
|
||||||
|
// The reported gap: adding a name to -DnsName previously returned the old certificate.
|
||||||
|
using (X509Certificate2 cert = CreateCertificateWithSans(
|
||||||
|
"WEB01", new[] { "WEB01", "WEB01.contoso.com" }, null))
|
||||||
|
{
|
||||||
|
string missing;
|
||||||
|
bool covers = CoversRequestedNames(
|
||||||
|
cert,
|
||||||
|
new[] { "WEB01", "WEB01.contoso.com", "api.contoso.com" },
|
||||||
|
null,
|
||||||
|
out missing);
|
||||||
|
|
||||||
|
Assert.False(covers);
|
||||||
|
Assert.Equal("DNS:api.contoso.com", missing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void A_Newly_Requested_Ip_Disqualifies_The_Existing_Certificate()
|
||||||
|
{
|
||||||
|
using (X509Certificate2 cert = CreateCertificateWithSans("WEB01", new[] { "WEB01" }, new[] { "10.20.30.40" }))
|
||||||
|
{
|
||||||
|
string missing;
|
||||||
|
Assert.False(CoversRequestedNames(cert, new[] { "WEB01" }, new[] { "10.20.30.41" }, out missing));
|
||||||
|
Assert.Equal("IP:10.20.30.41", missing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Extra_Names_On_The_Certificate_Do_Not_Disqualify_It()
|
||||||
|
{
|
||||||
|
// A superset still satisfies the request; only a missing name forces reissuance.
|
||||||
|
using (X509Certificate2 cert = CreateCertificateWithSans(
|
||||||
|
"WEB01", new[] { "WEB01", "WEB01.contoso.com", "legacy.contoso.com" }, new[] { "10.20.30.40", "127.0.0.1" }))
|
||||||
|
{
|
||||||
|
string missing;
|
||||||
|
Assert.True(CoversRequestedNames(cert, new[] { "WEB01" }, new[] { "127.0.0.1" }, out missing));
|
||||||
|
Assert.Null(missing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Dns_Comparison_Is_Case_Insensitive()
|
||||||
|
{
|
||||||
|
using (X509Certificate2 cert = CreateCertificateWithSans("WEB01", new[] { "WEB01.Contoso.COM" }, null))
|
||||||
|
{
|
||||||
|
string missing;
|
||||||
|
Assert.True(CoversRequestedNames(cert, new[] { "web01.contoso.com" }, null, out missing));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("::1", "0:0:0:0:0:0:0:1")]
|
||||||
|
[InlineData("0:0:0:0:0:0:0:1", "::1")]
|
||||||
|
[InlineData("10.20.30.40", "10.20.30.40")]
|
||||||
|
public void Ip_Comparison_Normalizes_Textual_Variations(string inCertificate, string requested)
|
||||||
|
{
|
||||||
|
using (X509Certificate2 cert = CreateCertificateWithSans("WEB01", new[] { "WEB01" }, new[] { inCertificate }))
|
||||||
|
{
|
||||||
|
string missing;
|
||||||
|
Assert.True(CoversRequestedNames(cert, null, new[] { requested }, out missing), string.Concat("missing: ", missing));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void A_Certificate_Without_Any_San_Extension_Fails_A_San_Request()
|
||||||
|
{
|
||||||
|
using (RSA rsa = RSA.Create(2048))
|
||||||
|
{
|
||||||
|
CertificateRequest request = new CertificateRequest(
|
||||||
|
"CN=NoSans", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||||
|
using (X509Certificate2 cert = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddDays(1)))
|
||||||
|
{
|
||||||
|
string missing;
|
||||||
|
Assert.False(CoversRequestedNames(cert, new[] { "NoSans" }, null, out missing));
|
||||||
|
Assert.Equal("DNS:NoSans", missing);
|
||||||
|
|
||||||
|
// With nothing requested there is nothing to fail on.
|
||||||
|
Assert.True(CoversRequestedNames(cert, null, null, out missing));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FindMatch_Keeps_Its_Original_Signature_For_Callers_Without_San_Requirements()
|
||||||
|
{
|
||||||
|
Type lookup = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalLocalCertificateLookup", true);
|
||||||
|
MethodInfo original = lookup.GetMethod(
|
||||||
|
"FindMatch",
|
||||||
|
BindingFlags.Public | BindingFlags.Static,
|
||||||
|
null,
|
||||||
|
new[] { typeof(StoreName), typeof(StoreLocation), typeof(string), typeof(IEnumerable<string>) },
|
||||||
|
null);
|
||||||
|
Assert.NotNull(original);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,7 +50,9 @@ namespace PSInfisicalAPI.Tests
|
|||||||
[InlineData(InfisicalEndpointNames.CreateSecret, "POST", "/api/v3/secrets/raw/{secretName}")]
|
[InlineData(InfisicalEndpointNames.CreateSecret, "POST", "/api/v3/secrets/raw/{secretName}")]
|
||||||
[InlineData(InfisicalEndpointNames.UpdateSecret, "PATCH", "/api/v3/secrets/raw/{secretName}")]
|
[InlineData(InfisicalEndpointNames.UpdateSecret, "PATCH", "/api/v3/secrets/raw/{secretName}")]
|
||||||
[InlineData(InfisicalEndpointNames.DeleteSecret, "DELETE", "/api/v3/secrets/raw/{secretName}")]
|
[InlineData(InfisicalEndpointNames.DeleteSecret, "DELETE", "/api/v3/secrets/raw/{secretName}")]
|
||||||
[InlineData(InfisicalEndpointNames.ListProjects, "GET", "/api/v1/workspace")]
|
// /api/v1/workspace mounts Infisical's deprecated project router; /api/v1/projects is the current one
|
||||||
|
// and is preferred, with the deprecated route retained as a fallback candidate.
|
||||||
|
[InlineData(InfisicalEndpointNames.ListProjects, "GET", "/api/v1/projects")]
|
||||||
[InlineData(InfisicalEndpointNames.RetrieveProject, "GET", "/api/v1/workspace/{projectId}")]
|
[InlineData(InfisicalEndpointNames.RetrieveProject, "GET", "/api/v1/workspace/{projectId}")]
|
||||||
[InlineData(InfisicalEndpointNames.CreateProject, "POST", "/api/v2/workspace")]
|
[InlineData(InfisicalEndpointNames.CreateProject, "POST", "/api/v2/workspace")]
|
||||||
[InlineData(InfisicalEndpointNames.UpdateProject, "PATCH", "/api/v1/workspace/{projectId}")]
|
[InlineData(InfisicalEndpointNames.UpdateProject, "PATCH", "/api/v1/workspace/{projectId}")]
|
||||||
@@ -75,7 +77,9 @@ namespace PSInfisicalAPI.Tests
|
|||||||
[InlineData(InfisicalEndpointNames.BulkUpdateSecret, "PATCH", "/api/v4/secrets/batch")]
|
[InlineData(InfisicalEndpointNames.BulkUpdateSecret, "PATCH", "/api/v4/secrets/batch")]
|
||||||
[InlineData(InfisicalEndpointNames.BulkDeleteSecret, "DELETE", "/api/v4/secrets/batch")]
|
[InlineData(InfisicalEndpointNames.BulkDeleteSecret, "DELETE", "/api/v4/secrets/batch")]
|
||||||
[InlineData(InfisicalEndpointNames.DuplicateSecret, "POST", "/api/v4/secrets/duplicate")]
|
[InlineData(InfisicalEndpointNames.DuplicateSecret, "POST", "/api/v4/secrets/duplicate")]
|
||||||
[InlineData(InfisicalEndpointNames.ListOrganizations, "GET", "/api/v2/organizations")]
|
// /api/v2/organizations has no GET / route, only /:organizationId/* sub-routes, so the listing must
|
||||||
|
// prefer the singular v1 route; the v2 one is retained only as a fallback candidate.
|
||||||
|
[InlineData(InfisicalEndpointNames.ListOrganizations, "GET", "/api/v1/organization")]
|
||||||
[InlineData(InfisicalEndpointNames.RetrieveOrganization, "GET", "/api/v1/organization/{organizationId}")]
|
[InlineData(InfisicalEndpointNames.RetrieveOrganization, "GET", "/api/v1/organization/{organizationId}")]
|
||||||
[InlineData(InfisicalEndpointNames.CreateOrganization, "POST", "/api/v2/organizations")]
|
[InlineData(InfisicalEndpointNames.CreateOrganization, "POST", "/api/v2/organizations")]
|
||||||
[InlineData(InfisicalEndpointNames.UpdateOrganization, "PATCH", "/api/v1/organization/{organizationId}")]
|
[InlineData(InfisicalEndpointNames.UpdateOrganization, "PATCH", "/api/v1/organization/{organizationId}")]
|
||||||
|
|||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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<string, object> 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<string, object>)method.Invoke(null, new object[] { source });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Dictionary<string, object>> 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<Dictionary<string, object>>)method.Invoke(null, new object[] { source });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Every_Write_Cmdlet_Declares_ShouldProcess()
|
||||||
|
{
|
||||||
|
List<string> offenders = new List<string>();
|
||||||
|
|
||||||
|
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<string> offenders = new List<string>();
|
||||||
|
|
||||||
|
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<string> offenders = new List<string>();
|
||||||
|
|
||||||
|
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<string, object> 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<string, object> 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<string, object> 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<Hashtable> source = new List<Hashtable>
|
||||||
|
{
|
||||||
|
new Hashtable { { "Type", "dns_name" }, { "Allowed", new[] { "*.contoso.com" } } },
|
||||||
|
new Hashtable { { "Type", "ip_address" }, { "Allowed", new string[0] } }
|
||||||
|
};
|
||||||
|
|
||||||
|
List<Dictionary<string, object>> result = ToJsonObjectList(source);
|
||||||
|
|
||||||
|
Dictionary<string, object> 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<string, object> 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<string, object> typed = (Dictionary<string, object>)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<string, object> config = (Dictionary<string, object>)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<PSInfisicalAPI.Endpoints.InfisicalEndpointDefinition> 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")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,272 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Management.Automation;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Tests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The Infisical console never asks which Certificate Manager project to use when an organization has one,
|
||||||
|
/// because its resolver selects it implicitly. Requiring -ProjectId made the module stricter than the
|
||||||
|
/// service it wraps, so these pin the parameter as optional across the PKI surface.
|
||||||
|
/// </summary>
|
||||||
|
public class ProjectScopingTests
|
||||||
|
{
|
||||||
|
private static readonly Assembly ModuleAssembly = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly;
|
||||||
|
|
||||||
|
private static readonly string[] PkiCmdletTypes = new[]
|
||||||
|
{
|
||||||
|
"PSInfisicalAPI.Cmdlets.GetInfisicalCertificateApplicationCmdlet",
|
||||||
|
"PSInfisicalAPI.Cmdlets.GetInfisicalCertificateApplicationEnrollmentCmdlet",
|
||||||
|
"PSInfisicalAPI.Cmdlets.GetInfisicalCertificateAuthorityCmdlet",
|
||||||
|
"PSInfisicalAPI.Cmdlets.GetInfisicalCertificateCmdlet",
|
||||||
|
"PSInfisicalAPI.Cmdlets.GetInfisicalCertificatePolicyCmdlet",
|
||||||
|
"PSInfisicalAPI.Cmdlets.GetInfisicalCertificateProfileCmdlet",
|
||||||
|
"PSInfisicalAPI.Cmdlets.GetInfisicalPkiSubscriberCmdlet",
|
||||||
|
"PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet"
|
||||||
|
};
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProjectId_Is_Optional_On_Every_Pki_Cmdlet()
|
||||||
|
{
|
||||||
|
List<string> offenders = new List<string>();
|
||||||
|
|
||||||
|
foreach (string typeName in PkiCmdletTypes)
|
||||||
|
{
|
||||||
|
Type type = ModuleAssembly.GetType(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(type.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Empty(offenders);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Every_Pki_Cmdlet_Resolves_The_Project_Rather_Than_Assuming_One()
|
||||||
|
{
|
||||||
|
// Optional without resolution would simply send an empty projectId, so confirm each cmdlet calls
|
||||||
|
// the resolver.
|
||||||
|
List<string> missing = new List<string>();
|
||||||
|
|
||||||
|
foreach (string typeName in PkiCmdletTypes)
|
||||||
|
{
|
||||||
|
Type type = ModuleAssembly.GetType(typeName, true);
|
||||||
|
MethodInfo processRecord = type.GetMethod("ProcessRecord", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
|
||||||
|
Assert.NotNull(processRecord);
|
||||||
|
|
||||||
|
if (!GetCalledMethodNames(processRecord).Contains("ResolveCertManagerProjectId"))
|
||||||
|
{
|
||||||
|
missing.Add(type.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Empty(missing);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Resolver_Is_Available_To_Cmdlets_And_Returns_A_Project_Id()
|
||||||
|
{
|
||||||
|
MethodInfo resolver = typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase)
|
||||||
|
.GetMethod("ResolveCertManagerProjectId", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
|
||||||
|
Assert.NotNull(resolver);
|
||||||
|
Assert.Equal(typeof(string), resolver.ReturnType);
|
||||||
|
|
||||||
|
ParameterInfo[] parameters = resolver.GetParameters();
|
||||||
|
Assert.Equal(2, parameters.Length);
|
||||||
|
Assert.Equal(typeof(PSInfisicalAPI.Connections.InfisicalConnection), parameters[0].ParameterType);
|
||||||
|
Assert.Equal(typeof(string), parameters[1].ParameterType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Resolution_Does_Not_Error_When_An_Organization_Has_Several_Projects()
|
||||||
|
{
|
||||||
|
// Certificate applications are served only from the organization's active project, so several
|
||||||
|
// Certificate Manager projects is a normal configuration rather than an ambiguity to reject.
|
||||||
|
MethodInfo resolver = typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase)
|
||||||
|
.GetMethod("ResolveCertManagerProjectId", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
|
||||||
|
List<string> called = GetCalledMethodNames(resolver);
|
||||||
|
Assert.Contains("FindActiveCertManagerProject", called);
|
||||||
|
|
||||||
|
MethodInfo finder = typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase)
|
||||||
|
.GetMethod("FindActiveCertManagerProject", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
Assert.NotNull(finder);
|
||||||
|
Assert.Equal(typeof(PSInfisicalAPI.Models.InfisicalProject), finder.ReturnType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void An_Organization_Without_Any_Cert_Manager_Project_Is_Not_An_Error()
|
||||||
|
{
|
||||||
|
// Nothing to list is an empty result, not a failure, so resolution returns null and each Get-*
|
||||||
|
// cmdlet returns quietly rather than surfacing "ProjectId is required" from deep in the client.
|
||||||
|
MethodInfo resolver = typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase)
|
||||||
|
.GetMethod("ResolveCertManagerProjectId", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
|
||||||
|
List<string> called = GetCalledMethodNames(resolver);
|
||||||
|
Assert.DoesNotContain("ThrowTerminatingForException", called);
|
||||||
|
Assert.DoesNotContain("WriteErrorForException", called);
|
||||||
|
|
||||||
|
List<string> ungarded = new List<string>();
|
||||||
|
foreach (string typeName in PkiCmdletTypes)
|
||||||
|
{
|
||||||
|
// Request-InfisicalCertificate can still issue through a profile without a project, so it is
|
||||||
|
// deliberately allowed to proceed.
|
||||||
|
if (typeName.EndsWith("RequestInfisicalCertificateCmdlet", StringComparison.Ordinal)) { continue; }
|
||||||
|
|
||||||
|
Type type = ModuleAssembly.GetType(typeName, true);
|
||||||
|
MethodInfo processRecord = type.GetMethod("ProcessRecord", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
|
||||||
|
if (!GetCalledMethodNames(processRecord).Contains("IsNullOrEmpty"))
|
||||||
|
{
|
||||||
|
ungarded.Add(type.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Empty(ungarded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void The_Organizations_Active_Cert_Manager_Project_Is_Modelled()
|
||||||
|
{
|
||||||
|
PropertyInfo property = typeof(PSInfisicalAPI.Models.InfisicalOrganization)
|
||||||
|
.GetProperty("DefaultCertManagerProjectId");
|
||||||
|
Assert.NotNull(property);
|
||||||
|
Assert.Equal(typeof(string), property.PropertyType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Project_Listing_Prefers_The_Current_Route_Over_The_Deprecated_One()
|
||||||
|
{
|
||||||
|
// /api/v1/workspace mounts Infisical's deprecated project router; /api/v1/projects is current.
|
||||||
|
IReadOnlyList<PSInfisicalAPI.Endpoints.InfisicalEndpointDefinition> candidates =
|
||||||
|
PSInfisicalAPI.Endpoints.InfisicalEndpointRegistry.GetCandidates(
|
||||||
|
PSInfisicalAPI.Endpoints.InfisicalEndpointNames.ListProjects);
|
||||||
|
|
||||||
|
Assert.True(candidates.Count >= 2, "both the current and deprecated routes should be registered");
|
||||||
|
Assert.Equal("/api/v1/projects", candidates[0].Template);
|
||||||
|
Assert.Contains(candidates, c => c.Template == "/api/v1/workspace");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void An_Explicit_ProjectId_Short_Circuits_Resolution()
|
||||||
|
{
|
||||||
|
// Supplying the project must never trigger a lookup, so the explicit value has to be returned
|
||||||
|
// before any client is constructed.
|
||||||
|
PSInfisicalAPI.Cmdlets.GetInfisicalCertificateAuthorityCmdlet cmdlet =
|
||||||
|
new PSInfisicalAPI.Cmdlets.GetInfisicalCertificateAuthorityCmdlet();
|
||||||
|
|
||||||
|
MethodInfo resolver = typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase)
|
||||||
|
.GetMethod("ResolveCertManagerProjectId", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
|
||||||
|
// A null connection would throw the moment a lookup was attempted; returning cleanly proves it did not.
|
||||||
|
string result = (string)resolver.Invoke(cmdlet, new object[] { null, "explicit-project-id" });
|
||||||
|
Assert.Equal("explicit-project-id", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Certificate_Retrieval_By_Serial_Does_Not_Resolve_A_Project()
|
||||||
|
{
|
||||||
|
// Addressing a certificate by serial needs no project, so the Single parameter set must not pay
|
||||||
|
// for a lookup.
|
||||||
|
Type type = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.GetInfisicalCertificateCmdlet", true);
|
||||||
|
PropertyInfo serialNumber = type.GetProperty("SerialNumber");
|
||||||
|
Assert.NotNull(serialNumber);
|
||||||
|
|
||||||
|
bool serialIsMandatoryOnSingle = false;
|
||||||
|
foreach (CustomAttributeData attribute in serialNumber.GetCustomAttributesData())
|
||||||
|
{
|
||||||
|
if (attribute.AttributeType != typeof(ParameterAttribute)) { continue; }
|
||||||
|
bool isSingle = false;
|
||||||
|
bool isMandatory = false;
|
||||||
|
foreach (CustomAttributeNamedArgument named in attribute.NamedArguments)
|
||||||
|
{
|
||||||
|
if (named.MemberName == "ParameterSetName" && (string)named.TypedValue.Value == "Single") { isSingle = true; }
|
||||||
|
if (named.MemberName == "Mandatory" && (bool)named.TypedValue.Value) { isMandatory = true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSingle && isMandatory) { serialIsMandatoryOnSingle = true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.True(serialIsMandatoryOnSingle);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Profiles_Can_Be_Filtered_To_An_Application_The_Way_The_Console_Groups_Them()
|
||||||
|
{
|
||||||
|
Type type = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.GetInfisicalCertificateProfileCmdlet", true);
|
||||||
|
|
||||||
|
PropertyInfo applicationId = type.GetProperty("ApplicationId");
|
||||||
|
Assert.NotNull(applicationId);
|
||||||
|
Assert.Equal(typeof(string), applicationId.PropertyType);
|
||||||
|
|
||||||
|
PropertyInfo caId = type.GetProperty("CaId");
|
||||||
|
Assert.NotNull(caId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Profile_Listing_Keeps_Its_Original_Overload_For_Existing_Callers()
|
||||||
|
{
|
||||||
|
Type clientType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalPkiClient", true);
|
||||||
|
|
||||||
|
MethodInfo original = clientType.GetMethod(
|
||||||
|
"ListCertificateProfiles",
|
||||||
|
BindingFlags.Public | BindingFlags.Instance,
|
||||||
|
null,
|
||||||
|
new[] { typeof(PSInfisicalAPI.Connections.InfisicalConnection), typeof(string), typeof(int?), typeof(int?), typeof(bool?) },
|
||||||
|
null);
|
||||||
|
Assert.NotNull(original);
|
||||||
|
|
||||||
|
MethodInfo filtered = clientType.GetMethod(
|
||||||
|
"ListCertificateProfiles",
|
||||||
|
BindingFlags.Public | BindingFlags.Instance,
|
||||||
|
null,
|
||||||
|
new[] { typeof(PSInfisicalAPI.Connections.InfisicalConnection), typeof(string), typeof(int?), typeof(int?), typeof(bool?), typeof(string), typeof(string) },
|
||||||
|
null);
|
||||||
|
Assert.NotNull(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> GetCalledMethodNames(MethodInfo method)
|
||||||
|
{
|
||||||
|
List<string> names = new List<string>();
|
||||||
|
MethodBody body = method.GetMethodBody();
|
||||||
|
if (body == null) { return names; }
|
||||||
|
|
||||||
|
byte[] il = body.GetILAsByteArray();
|
||||||
|
if (il == null) { return names; }
|
||||||
|
|
||||||
|
const byte Call = 0x28;
|
||||||
|
const byte CallVirt = 0x6F;
|
||||||
|
|
||||||
|
for (int i = 0; i + 4 < il.Length; i++)
|
||||||
|
{
|
||||||
|
if (il[i] != Call && il[i] != CallVirt) { continue; }
|
||||||
|
|
||||||
|
int token = BitConverter.ToInt32(il, i + 1);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
MethodBase resolved = method.Module.ResolveMethod(token);
|
||||||
|
if (resolved != null) { names.Add(resolved.Name); }
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
using System;
|
||||||
|
using System.Management.Automation;
|
||||||
|
using PSInfisicalAPI.Connections;
|
||||||
|
using PSInfisicalAPI.Models;
|
||||||
|
using PSInfisicalAPI.Pki;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Cmdlets
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a certificate application: the grouping the Infisical console presents profiles, members, and
|
||||||
|
/// certificates under.
|
||||||
|
/// </summary>
|
||||||
|
[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; }
|
||||||
|
|
||||||
|
/// <summary>Certificate profiles to attach on creation.</summary>
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Renames a certificate application or changes its description, and attaches or detaches profiles.
|
||||||
|
/// </summary>
|
||||||
|
[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; }
|
||||||
|
|
||||||
|
/// <summary>Certificate profiles to attach. Profiles already attached are left alone.</summary>
|
||||||
|
[Parameter] public string[] AddProfileId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Certificate profiles to detach.</summary>
|
||||||
|
[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<string>())
|
||||||
|
{
|
||||||
|
client.RemoveCertificateApplicationProfile(connection, ProjectId, ApplicationId, profileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PassThru.IsPresent && updated != null) { WriteObject(updated); }
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
WriteErrorForException(Component, "UpdateCertificateApplication", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes a certificate application.
|
||||||
|
/// </summary>
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
using System;
|
||||||
|
using System.Management.Automation;
|
||||||
|
using PSInfisicalAPI.Connections;
|
||||||
|
using PSInfisicalAPI.Models;
|
||||||
|
using PSInfisicalAPI.Pki;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Cmdlets
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an internal certificate authority, signing a subordinate with its parent so it comes back ready
|
||||||
|
/// to issue.
|
||||||
|
/// </summary>
|
||||||
|
[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; }
|
||||||
|
|
||||||
|
/// <summary>Required for -Type Intermediate: the authority that signs this one.</summary>
|
||||||
|
[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";
|
||||||
|
|
||||||
|
/// <summary>Expiry. Defaults to ten years for a root and five for a subordinate.</summary>
|
||||||
|
[Parameter] public DateTimeOffset? NotAfter { get; set; }
|
||||||
|
|
||||||
|
[Parameter] public DateTimeOffset? NotBefore { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Subordinate authorities permitted beneath this one. Defaults to 1 for a root, 0 otherwise.</summary>
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Renames an internal certificate authority or changes its status.
|
||||||
|
/// </summary>
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes an internal certificate authority.
|
||||||
|
/// </summary>
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a certificate policy: the constraints a profile issues within.
|
||||||
|
/// </summary>
|
||||||
|
[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; }
|
||||||
|
|
||||||
|
/// <summary>Maximum certificate lifetime, for example '90d', '12m', or '1y'.</summary>
|
||||||
|
[Parameter] public string MaxValidity { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Permitted key algorithms, for example RSA_2048 or EC_secp384r1.</summary>
|
||||||
|
[Parameter] public string[] KeyAlgorithm { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Permitted signature algorithms.</summary>
|
||||||
|
[Parameter] public string[] SignatureAlgorithm { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Key usage constraint, as @{ Required = @('digital_signature'); Denied = @(...) }.</summary>
|
||||||
|
[Parameter] public IDictionary KeyUsage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Extended key usage constraint, as @{ Required = @('server_auth','client_auth') }.</summary>
|
||||||
|
[Parameter] public IDictionary ExtendedKeyUsage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Subject attribute constraints, as @( @{ Type = 'organization'; Allowed = @('Contoso') } ).</summary>
|
||||||
|
[Parameter] public IDictionary[] Subject { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Subject alternative name constraints, as @( @{ Type = 'dns_name'; Allowed = @('*.contoso.com') } ).</summary>
|
||||||
|
[Parameter] public IDictionary[] SubjectAlternativeName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Basic constraints, as @{ isCA = 'denied'; maxPathLength = 0 }.</summary>
|
||||||
|
[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<string, object> { { "max", MaxValidity } };
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Collections.Generic.Dictionary<string, object> algorithms = new System.Collections.Generic.Dictionary<string, object>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates a certificate policy. Only the supplied constraints are sent; the rest are left as they are.
|
||||||
|
/// </summary>
|
||||||
|
[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<string, object> { { "max", MaxValidity } };
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Collections.Generic.Dictionary<string, object> algorithms = new System.Collections.Generic.Dictionary<string, object>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes a certificate policy.
|
||||||
|
/// </summary>
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a certificate profile, binding an issuing certificate authority to a certificate policy and
|
||||||
|
/// exposing it for enrollment.
|
||||||
|
/// </summary>
|
||||||
|
[Cmdlet(VerbsCommon.New, "InfisicalCertificateProfile", SupportsShouldProcess = true)]
|
||||||
|
[OutputType(typeof(InfisicalCertificateProfile))]
|
||||||
|
public sealed class NewInfisicalCertificateProfileCmdlet : InfisicalPkiWriteCmdletBase
|
||||||
|
{
|
||||||
|
private const string Component = "NewInfisicalCertificateProfileCmdlet";
|
||||||
|
|
||||||
|
/// <summary>Lowercase letters, numbers, and hyphens only.</summary>
|
||||||
|
[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";
|
||||||
|
|
||||||
|
/// <summary>Renew issued certificates automatically.</summary>
|
||||||
|
[Parameter] public SwitchParameter AutoRenew { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Days before expiry at which automatic renewal runs, 1 to 30.</summary>
|
||||||
|
[Parameter] public int? RenewBeforeDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Issuance defaults, as @{ ttlDays = 90; keyAlgorithm = 'RSA_2048' }.</summary>
|
||||||
|
[Parameter] public IDictionary Defaults { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Enrollment configuration for -EnrollmentType est, acme, or scep.</summary>
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Routes the enrollment configuration to the block matching the enrollment type, so callers supply one
|
||||||
|
/// dictionary rather than choosing between four mutually exclusive parameters.
|
||||||
|
/// </summary>
|
||||||
|
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<string, object> 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<string, object>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config != null)
|
||||||
|
{
|
||||||
|
if (autoRenewBound) { config["autoRenew"] = autoRenew; }
|
||||||
|
if (renewBeforeDays.HasValue) { config["renewBeforeDays"] = renewBeforeDays.Value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
request.ApiConfig = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates a certificate profile. Only the supplied values are sent.
|
||||||
|
/// </summary>
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes a certificate profile.
|
||||||
|
/// </summary>
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
[Alias("Name")]
|
[Alias("Name")]
|
||||||
public string ApplicationName { get; set; }
|
public string ApplicationName { get; set; }
|
||||||
|
|
||||||
[Parameter(Mandatory = true)] public string ProjectId { get; set; }
|
[Parameter] public string ProjectId { get; set; }
|
||||||
|
|
||||||
[Parameter(ParameterSetName = "List")] public int? Limit { get; set; }
|
[Parameter(ParameterSetName = "List")] public int? Limit { get; set; }
|
||||||
|
|
||||||
@@ -29,6 +29,14 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
||||||
|
|
||||||
|
// The UI never asks which Certificate Manager project to use when an organization has only
|
||||||
|
// one; -ProjectId is optional here for the same reason. Assigned back so every call below
|
||||||
|
// sees the resolved value without threading a second variable through.
|
||||||
|
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
|
||||||
|
|
||||||
|
// No Certificate Manager project means nothing to list; that is an empty result, not a failure.
|
||||||
|
if (string.IsNullOrEmpty(ProjectId)) { return; }
|
||||||
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
|
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
|
||||||
|
|
||||||
if (string.Equals(ParameterSetName, "ById", StringComparison.Ordinal))
|
if (string.Equals(ParameterSetName, "ById", StringComparison.Ordinal))
|
||||||
|
|||||||
@@ -18,13 +18,21 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
[Alias("CertificateProfileId")]
|
[Alias("CertificateProfileId")]
|
||||||
public string ProfileId { get; set; }
|
public string ProfileId { get; set; }
|
||||||
|
|
||||||
[Parameter(Mandatory = true)] public string ProjectId { get; set; }
|
[Parameter] public string ProjectId { get; set; }
|
||||||
|
|
||||||
protected override void ProcessRecord()
|
protected override void ProcessRecord()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
||||||
|
|
||||||
|
// The UI never asks which Certificate Manager project to use when an organization has only
|
||||||
|
// one; -ProjectId is optional here for the same reason. Assigned back so every call below
|
||||||
|
// sees the resolved value without threading a second variable through.
|
||||||
|
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
|
||||||
|
|
||||||
|
// No Certificate Manager project means nothing to list; that is an empty result, not a failure.
|
||||||
|
if (string.IsNullOrEmpty(ProjectId)) { return; }
|
||||||
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
|
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
|
||||||
|
|
||||||
InfisicalCertificateApplicationEnrollment enrollment = client.GetCertificateApplicationEnrollment(connection, ApplicationId, ProfileId, ProjectId);
|
InfisicalCertificateApplicationEnrollment enrollment = client.GetCertificateApplicationEnrollment(connection, ApplicationId, ProfileId, ProjectId);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
[Alias("Id")]
|
[Alias("Id")]
|
||||||
public string CaId { get; set; }
|
public string CaId { get; set; }
|
||||||
|
|
||||||
[Parameter(Mandatory = true)] public string ProjectId { get; set; }
|
[Parameter] public string ProjectId { get; set; }
|
||||||
|
|
||||||
[Parameter(ParameterSetName = "List")]
|
[Parameter(ParameterSetName = "List")]
|
||||||
[ValidateSet("Internal", "Acme", "Any")]
|
[ValidateSet("Internal", "Acme", "Any")]
|
||||||
@@ -25,6 +25,14 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
||||||
|
|
||||||
|
// The UI never asks which Certificate Manager project to use when an organization has only
|
||||||
|
// one; -ProjectId is optional here for the same reason. Assigned back so every call below
|
||||||
|
// sees the resolved value without threading a second variable through.
|
||||||
|
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
|
||||||
|
|
||||||
|
// No Certificate Manager project means nothing to list; that is an empty result, not a failure.
|
||||||
|
if (string.IsNullOrEmpty(ProjectId)) { return; }
|
||||||
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
|
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
|
||||||
|
|
||||||
if (string.Equals(ParameterSetName, "ById", StringComparison.Ordinal))
|
if (string.Equals(ParameterSetName, "ById", StringComparison.Ordinal))
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
[Alias("Id", "Identifier")]
|
[Alias("Id", "Identifier")]
|
||||||
public string SerialNumber { get; set; }
|
public string SerialNumber { get; set; }
|
||||||
|
|
||||||
[Parameter(ParameterSetName = "List", Mandatory = true)] public string ProjectId { get; set; }
|
[Parameter(ParameterSetName = "List")] public string ProjectId { get; set; }
|
||||||
[Parameter(ParameterSetName = "List")] public string CommonName { get; set; }
|
[Parameter(ParameterSetName = "List")] public string CommonName { get; set; }
|
||||||
[Parameter(ParameterSetName = "List")] public string FriendlyName { get; set; }
|
[Parameter(ParameterSetName = "List")] public string FriendlyName { get; set; }
|
||||||
[Parameter(ParameterSetName = "List")] public string Search { get; set; }
|
[Parameter(ParameterSetName = "List")] public string Search { get; set; }
|
||||||
@@ -59,6 +59,7 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
||||||
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
|
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
|
||||||
|
|
||||||
|
// Retrieval by serial addresses the certificate directly, so it needs no project.
|
||||||
if (string.Equals(ParameterSetName, "Single", StringComparison.Ordinal))
|
if (string.Equals(ParameterSetName, "Single", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
InfisicalCertificate cert = client.RetrieveCertificate(connection, SerialNumber);
|
InfisicalCertificate cert = client.RetrieveCertificate(connection, SerialNumber);
|
||||||
@@ -70,6 +71,14 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The UI never asks which Certificate Manager project to use when an organization has only
|
||||||
|
// one; -ProjectId is optional here for the same reason. Assigned back so every call below
|
||||||
|
// sees the resolved value without threading a second variable through.
|
||||||
|
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
|
||||||
|
|
||||||
|
// No Certificate Manager project means nothing to list; that is an empty result, not a failure.
|
||||||
|
if (string.IsNullOrEmpty(ProjectId)) { return; }
|
||||||
|
|
||||||
InfisicalCertificateSearchQuery query = new InfisicalCertificateSearchQuery
|
InfisicalCertificateSearchQuery query = new InfisicalCertificateSearchQuery
|
||||||
{
|
{
|
||||||
ProjectId = ProjectId,
|
ProjectId = ProjectId,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
[Alias("Id", "CertificatePolicyId")]
|
[Alias("Id", "CertificatePolicyId")]
|
||||||
public string PolicyId { get; set; }
|
public string PolicyId { get; set; }
|
||||||
|
|
||||||
[Parameter(Mandatory = true)] public string ProjectId { get; set; }
|
[Parameter] public string ProjectId { get; set; }
|
||||||
|
|
||||||
[Parameter(ParameterSetName = "List")] public int? Limit { get; set; }
|
[Parameter(ParameterSetName = "List")] public int? Limit { get; set; }
|
||||||
|
|
||||||
@@ -25,6 +25,14 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
||||||
|
|
||||||
|
// The UI never asks which Certificate Manager project to use when an organization has only
|
||||||
|
// one; -ProjectId is optional here for the same reason. Assigned back so every call below
|
||||||
|
// sees the resolved value without threading a second variable through.
|
||||||
|
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
|
||||||
|
|
||||||
|
// No Certificate Manager project means nothing to list; that is an empty result, not a failure.
|
||||||
|
if (string.IsNullOrEmpty(ProjectId)) { return; }
|
||||||
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
|
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
|
||||||
|
|
||||||
if (string.Equals(ParameterSetName, "ById", StringComparison.Ordinal))
|
if (string.Equals(ParameterSetName, "ById", StringComparison.Ordinal))
|
||||||
|
|||||||
@@ -14,7 +14,19 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
[Alias("Id", "CertificateProfileId")]
|
[Alias("Id", "CertificateProfileId")]
|
||||||
public string ProfileId { get; set; }
|
public string ProfileId { get; set; }
|
||||||
|
|
||||||
[Parameter(Mandatory = true)] public string ProjectId { get; set; }
|
[Parameter] public string ProjectId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Narrows the listing to one certificate application, which is how the Infisical console groups
|
||||||
|
/// profiles. See Get-InfisicalCertificateApplication.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter(ParameterSetName = "List", ValueFromPipelineByPropertyName = true)]
|
||||||
|
public string ApplicationId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Narrows the listing to profiles issued by one certificate authority.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter(ParameterSetName = "List")] public string CaId { get; set; }
|
||||||
|
|
||||||
[Parameter(ParameterSetName = "List")] public int? Limit { get; set; }
|
[Parameter(ParameterSetName = "List")] public int? Limit { get; set; }
|
||||||
|
|
||||||
@@ -27,6 +39,14 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
||||||
|
|
||||||
|
// The UI never asks which Certificate Manager project to use when an organization has only
|
||||||
|
// one; -ProjectId is optional here for the same reason. Assigned back so every call below
|
||||||
|
// sees the resolved value without threading a second variable through.
|
||||||
|
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
|
||||||
|
|
||||||
|
// No Certificate Manager project means nothing to list; that is an empty result, not a failure.
|
||||||
|
if (string.IsNullOrEmpty(ProjectId)) { return; }
|
||||||
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
|
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
|
||||||
|
|
||||||
if (string.Equals(ParameterSetName, "ById", StringComparison.Ordinal))
|
if (string.Equals(ParameterSetName, "ById", StringComparison.Ordinal))
|
||||||
@@ -41,8 +61,12 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool? includeConfigs = MyInvocation.BoundParameters.ContainsKey("IncludeConfigs") ? (bool?)IncludeConfigs.IsPresent : null;
|
bool? includeConfigs = MyInvocation.BoundParameters.ContainsKey("IncludeConfigs") ? (bool?)IncludeConfigs.IsPresent : null;
|
||||||
InfisicalCertificateProfile[] all = client.ListCertificateProfiles(connection, ProjectId, Limit, Offset, includeConfigs);
|
InfisicalCertificateProfile[] all = client.ListCertificateProfiles(connection, ProjectId, Limit, Offset, includeConfigs, ApplicationId, CaId);
|
||||||
Logger.Information("Get-InfisicalCertificateProfile", string.Concat("Returned ", all.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " certificate profile(s)."));
|
|
||||||
|
string scope = !string.IsNullOrEmpty(ApplicationId)
|
||||||
|
? string.Concat(" for application '", ApplicationId, "'")
|
||||||
|
: (!string.IsNullOrEmpty(CaId) ? string.Concat(" for certificate authority '", CaId, "'") : string.Empty);
|
||||||
|
Logger.Information("Get-InfisicalCertificateProfile", string.Concat("Returned ", all.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " certificate profile(s)", scope, "."));
|
||||||
foreach (InfisicalCertificateProfile profile in all)
|
foreach (InfisicalCertificateProfile profile in all)
|
||||||
{
|
{
|
||||||
WriteObject(profile);
|
WriteObject(profile);
|
||||||
|
|||||||
@@ -14,13 +14,21 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
[Alias("SubscriberName", "Slug")]
|
[Alias("SubscriberName", "Slug")]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
[Parameter(Mandatory = true)] public string ProjectId { get; set; }
|
[Parameter] public string ProjectId { get; set; }
|
||||||
|
|
||||||
protected override void ProcessRecord()
|
protected override void ProcessRecord()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
||||||
|
|
||||||
|
// The UI never asks which Certificate Manager project to use when an organization has only
|
||||||
|
// one; -ProjectId is optional here for the same reason. Assigned back so every call below
|
||||||
|
// sees the resolved value without threading a second variable through.
|
||||||
|
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
|
||||||
|
|
||||||
|
// No Certificate Manager project means nothing to list; that is an empty result, not a failure.
|
||||||
|
if (string.IsNullOrEmpty(ProjectId)) { return; }
|
||||||
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
|
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
|
||||||
|
|
||||||
if (string.Equals(ParameterSetName, "ByName", StringComparison.Ordinal))
|
if (string.Equals(ParameterSetName, "ByName", StringComparison.Ordinal))
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Management.Automation;
|
using System.Management.Automation;
|
||||||
using System.Runtime.ExceptionServices;
|
using System.Runtime.ExceptionServices;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
using PSInfisicalAPI.Connections;
|
using PSInfisicalAPI.Connections;
|
||||||
|
using PSInfisicalAPI.Models;
|
||||||
|
using PSInfisicalAPI.Organizations;
|
||||||
|
using PSInfisicalAPI.Projects;
|
||||||
using PSInfisicalAPI.Errors;
|
using PSInfisicalAPI.Errors;
|
||||||
using PSInfisicalAPI.Http;
|
using PSInfisicalAPI.Http;
|
||||||
using PSInfisicalAPI.Logging;
|
using PSInfisicalAPI.Logging;
|
||||||
@@ -13,9 +17,13 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
{
|
{
|
||||||
public abstract class InfisicalCmdletBase : PSCmdlet
|
public abstract class InfisicalCmdletBase : PSCmdlet
|
||||||
{
|
{
|
||||||
|
private const string CertManagerProjectType = "cert-manager";
|
||||||
|
|
||||||
private IInfisicalLogger _logger;
|
private IInfisicalLogger _logger;
|
||||||
private IInfisicalHttpClient _httpClient;
|
private IInfisicalHttpClient _httpClient;
|
||||||
private bool? _isElevated;
|
private bool? _isElevated;
|
||||||
|
private string _resolvedCertManagerProjectId;
|
||||||
|
private bool _certManagerProjectResolved;
|
||||||
|
|
||||||
protected IInfisicalLogger Logger
|
protected IInfisicalLogger Logger
|
||||||
{
|
{
|
||||||
@@ -49,6 +57,130 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
return current != null && current.SkipCertificateCheck;
|
return current != null && current.SkipCertificateCheck;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves the Certificate Manager project a PKI call should target.
|
||||||
|
/// <para>
|
||||||
|
/// The Infisical UI never asks which project to use: an organization with a single Certificate Manager
|
||||||
|
/// project has it selected implicitly, which is why the project appears only in the URL and everything
|
||||||
|
/// below it is presented as applications. The API behaves the same way - its own resolver takes the
|
||||||
|
/// single cert-manager project when there is exactly one - so requiring -ProjectId on every cmdlet made
|
||||||
|
/// the module stricter than the service it wraps.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// This mirrors that rule client-side, which is necessary because several PKI endpoints carry the project
|
||||||
|
/// in the URL path and cannot defer to the server's resolver. Resolved once per cmdlet instance.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// The project to use, or <c>null</c> when the organization has no Certificate Manager project. Callers
|
||||||
|
/// that cannot proceed without one should return quietly rather than failing: an organization that has
|
||||||
|
/// not set up Certificate Manager has nothing to list, which is an empty result and not an error.
|
||||||
|
/// </returns>
|
||||||
|
protected string ResolveCertManagerProjectId(InfisicalConnection connection, string explicitValue)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(explicitValue)) { return explicitValue; }
|
||||||
|
if (_certManagerProjectResolved) { return _resolvedCertManagerProjectId; }
|
||||||
|
|
||||||
|
_certManagerProjectResolved = true;
|
||||||
|
|
||||||
|
InfisicalProjectClient client = new InfisicalProjectClient(HttpClient, Logger);
|
||||||
|
InfisicalProject[] projects = client.List(connection, CertManagerProjectType, false);
|
||||||
|
|
||||||
|
List<InfisicalProject> certManagerProjects = new List<InfisicalProject>();
|
||||||
|
if (projects != null)
|
||||||
|
{
|
||||||
|
foreach (InfisicalProject project in projects)
|
||||||
|
{
|
||||||
|
if (project == null) { continue; }
|
||||||
|
if (string.Equals(project.Type, CertManagerProjectType, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
certManagerProjects.Add(project);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (certManagerProjects.Count == 0)
|
||||||
|
{
|
||||||
|
Logger.Verbose(GetType().Name, string.Concat(
|
||||||
|
"-ProjectId was not supplied and this organization has no Certificate Manager project, so there is ",
|
||||||
|
"nothing to resolve to. Create one in Infisical (Certificate Management), or pass -ProjectId to ",
|
||||||
|
"target a specific project."));
|
||||||
|
|
||||||
|
_resolvedCertManagerProjectId = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
InfisicalProject chosen = certManagerProjects[0];
|
||||||
|
string reason = "the organization's only Certificate Manager project";
|
||||||
|
|
||||||
|
if (certManagerProjects.Count > 1)
|
||||||
|
{
|
||||||
|
// More than one is not an error. Infisical designates one of them as the organization's active
|
||||||
|
// Certificate Manager project, and certificate applications are only served from that one, so
|
||||||
|
// resolving to it is what makes an application-centric script work.
|
||||||
|
InfisicalProject active = FindActiveCertManagerProject(connection, certManagerProjects);
|
||||||
|
if (active != null)
|
||||||
|
{
|
||||||
|
chosen = active;
|
||||||
|
reason = "the organization's active Certificate Manager project";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
reason = string.Concat(
|
||||||
|
"the first of ", certManagerProjects.Count.ToString(CultureInfo.InvariantCulture),
|
||||||
|
" Certificate Manager projects (no active project is set on the organization; pass -ProjectId to choose another)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_resolvedCertManagerProjectId = chosen.Id;
|
||||||
|
Logger.Verbose(GetType().Name, string.Concat(
|
||||||
|
"-ProjectId was not supplied; resolved ", reason, ": '",
|
||||||
|
chosen.Name ?? chosen.Slug, "' (", _resolvedCertManagerProjectId, ")."));
|
||||||
|
|
||||||
|
return _resolvedCertManagerProjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds the organization's active Certificate Manager project among the candidates. Certificate
|
||||||
|
/// applications are served only from this project, so when several exist it is the one a PKI call
|
||||||
|
/// should target. Returns null when the organization designates none, leaving the caller to fall back.
|
||||||
|
/// </summary>
|
||||||
|
private InfisicalProject FindActiveCertManagerProject(InfisicalConnection connection, List<InfisicalProject> candidates)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
InfisicalOrganizationClient organizationClient = new InfisicalOrganizationClient(HttpClient, Logger);
|
||||||
|
InfisicalOrganization[] organizations = organizationClient.List(connection);
|
||||||
|
if (organizations == null) { return null; }
|
||||||
|
|
||||||
|
string organizationId = connection != null ? connection.OrganizationId : null;
|
||||||
|
foreach (InfisicalOrganization organization in organizations)
|
||||||
|
{
|
||||||
|
if (organization == null || string.IsNullOrEmpty(organization.DefaultCertManagerProjectId)) { continue; }
|
||||||
|
if (!string.IsNullOrEmpty(organizationId)
|
||||||
|
&& !string.Equals(organization.Id, organizationId, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (InfisicalProject candidate in candidates)
|
||||||
|
{
|
||||||
|
if (string.Equals(candidate.Id, organization.DefaultCertManagerProjectId, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
if (IsPipelineControlException(exception)) { throw; }
|
||||||
|
Logger.Verbose(GetType().Name, string.Concat("Could not read the organization's active Certificate Manager project (continuing): ", exception.Message));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reports whether the host process is running elevated. Evaluated through the PowerShell engine rather
|
/// Reports whether the host process is running elevated. Evaluated through the PowerShell engine rather
|
||||||
/// than WindowsIdentity directly, because the module targets netstandard2.0 and does not carry a
|
/// than WindowsIdentity directly, because the module targets netstandard2.0 and does not carry a
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Management.Automation;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Cmdlets
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Shared plumbing for the cmdlets that create, change, or remove Certificate Manager configuration.
|
||||||
|
/// <para>
|
||||||
|
/// 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 <c>-Subject</c> and
|
||||||
|
/// <c>-Metadata</c> already work, and are converted here.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public abstract class InfisicalPkiWriteCmdletBase : InfisicalCmdletBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <summary>
|
||||||
|
/// The constraint vocabulary Infisical expects in lower case. PowerShell callers naturally capitalise
|
||||||
|
/// hashtable keys, so <c>@{ Required = ... }</c> has to reach the API as <c>"required"</c> 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.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly Dictionary<string, string> CanonicalKeys = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
{ "allowed", "allowed" },
|
||||||
|
{ "required", "required" },
|
||||||
|
{ "denied", "denied" },
|
||||||
|
{ "type", "type" }
|
||||||
|
};
|
||||||
|
|
||||||
|
internal static Dictionary<string, object> ToJsonObject(IDictionary source)
|
||||||
|
{
|
||||||
|
if (source == null) { return null; }
|
||||||
|
|
||||||
|
Dictionary<string, object> result = new Dictionary<string, object>(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<object> asList = value as List<object>;
|
||||||
|
if (asList != null && asList.Count == 0) { continue; }
|
||||||
|
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Count > 0 ? result : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a list of dictionaries, which is the shape Infisical uses for policy subject and SAN
|
||||||
|
/// constraints.
|
||||||
|
/// </summary>
|
||||||
|
internal static List<Dictionary<string, object>> ToJsonObjectList(IEnumerable source)
|
||||||
|
{
|
||||||
|
if (source == null) { return null; }
|
||||||
|
|
||||||
|
List<Dictionary<string, object>> result = new List<Dictionary<string, object>>();
|
||||||
|
foreach (object item in source)
|
||||||
|
{
|
||||||
|
IDictionary dictionary = UnwrapDictionary(item);
|
||||||
|
if (dictionary == null) { continue; }
|
||||||
|
|
||||||
|
Dictionary<string, object> converted = ToJsonObject(dictionary);
|
||||||
|
if (converted == null) { continue; }
|
||||||
|
|
||||||
|
// An entry carrying only "type" states a dimension without constraining it, which Infisical
|
||||||
|
// rejects outright. Dropping it is what the caller meant by leaving the lists empty.
|
||||||
|
bool hasConstraint = converted.ContainsKey("allowed") || converted.ContainsKey("required") || converted.ContainsKey("denied");
|
||||||
|
if (!hasConstraint) { continue; }
|
||||||
|
|
||||||
|
result.Add(converted);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Count > 0 ? result : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object ToJsonValue(object value)
|
||||||
|
{
|
||||||
|
if (value == null) { return null; }
|
||||||
|
|
||||||
|
IDictionary nested = UnwrapDictionary(value);
|
||||||
|
if (nested != null) { return ToJsonObject(nested); }
|
||||||
|
|
||||||
|
if (value is string) { return value; }
|
||||||
|
|
||||||
|
IEnumerable enumerable = UnwrapEnumerable(value);
|
||||||
|
if (enumerable != null)
|
||||||
|
{
|
||||||
|
List<object> items = new List<object>();
|
||||||
|
foreach (object item in enumerable) { items.Add(ToJsonValue(item)); }
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PowerShell hands parameters over wrapped in PSObject often enough that unwrapping has to happen at
|
||||||
|
/// every level, not only at the top.
|
||||||
|
/// </summary>
|
||||||
|
private static IDictionary UnwrapDictionary(object value)
|
||||||
|
{
|
||||||
|
if (value == null) { return null; }
|
||||||
|
if (value is IDictionary direct) { return direct; }
|
||||||
|
|
||||||
|
PSObject wrapper = value as PSObject;
|
||||||
|
if (wrapper != null && wrapper.BaseObject is IDictionary wrapped) { return wrapped; }
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable UnwrapEnumerable(object value)
|
||||||
|
{
|
||||||
|
if (value is IEnumerable direct) { return direct; }
|
||||||
|
|
||||||
|
PSObject wrapper = value as PSObject;
|
||||||
|
if (wrapper != null && wrapper.BaseObject is IEnumerable wrapped) { return wrapped; }
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Formats an expiry the way Infisical's date validator accepts it.
|
||||||
|
/// </summary>
|
||||||
|
internal static string ToApiTimestamp(DateTimeOffset? value)
|
||||||
|
{
|
||||||
|
if (!value.HasValue) { return null; }
|
||||||
|
return value.Value.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static List<string> ToStringList(IEnumerable<string> values)
|
||||||
|
{
|
||||||
|
if (values == null) { return null; }
|
||||||
|
|
||||||
|
List<string> result = new List<string>();
|
||||||
|
foreach (string value in values)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(value)) { result.Add(value.Trim()); }
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Count > 0 ? result : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Management.Automation;
|
||||||
|
using PSInfisicalAPI.Connections;
|
||||||
|
using PSInfisicalAPI.Models;
|
||||||
|
using PSInfisicalAPI.Pki;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Cmdlets
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a PKI subscriber: a named enrollment identity pinning one common name, its SAN allowlist, and its
|
||||||
|
/// key usages.
|
||||||
|
/// </summary>
|
||||||
|
[Cmdlet(VerbsCommon.New, "InfisicalPkiSubscriber", SupportsShouldProcess = true)]
|
||||||
|
[OutputType(typeof(InfisicalPkiSubscriber))]
|
||||||
|
public sealed class NewInfisicalPkiSubscriberCmdlet : InfisicalPkiWriteCmdletBase
|
||||||
|
{
|
||||||
|
private const string Component = "NewInfisicalPkiSubscriberCmdlet";
|
||||||
|
|
||||||
|
[Parameter(Mandatory = true, Position = 0)] public string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The certificate signed for this subscriber must carry exactly this common name; Infisical rejects a
|
||||||
|
/// request whose CSR names anything else.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter(Mandatory = true)] public string CommonName { get; set; }
|
||||||
|
|
||||||
|
[Parameter(Mandatory = true)] public string CaId { get; set; }
|
||||||
|
[Parameter] public string ProjectId { get; set; }
|
||||||
|
[Parameter] public string Ttl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Allowlist of subject alternative names. A CSR naming anything outside it is rejected.</summary>
|
||||||
|
[Parameter] public string[] SubjectAlternativeName { get; set; }
|
||||||
|
|
||||||
|
[Parameter] public string[] KeyUsage { get; set; }
|
||||||
|
[Parameter] public string[] ExtendedKeyUsage { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
[ValidateSet("active", "disabled")]
|
||||||
|
public string Status { get; set; } = "active";
|
||||||
|
|
||||||
|
[Parameter] public SwitchParameter EnableAutoRenewal { get; set; }
|
||||||
|
[Parameter] public int? AutoRenewalPeriodInDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Additional subject attributes, as @{ organization = 'Contoso'; country = 'US' }.</summary>
|
||||||
|
[Parameter] public IDictionary Properties { get; set; }
|
||||||
|
|
||||||
|
protected override void ProcessRecord()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
||||||
|
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
|
||||||
|
if (string.IsNullOrEmpty(ProjectId)) { return; }
|
||||||
|
|
||||||
|
if (!ShouldProcess(Name, "Create PKI subscriber")) { return; }
|
||||||
|
|
||||||
|
InfisicalPkiSubscriberWriteRequestDto request = new InfisicalPkiSubscriberWriteRequestDto
|
||||||
|
{
|
||||||
|
Name = Name,
|
||||||
|
CommonName = CommonName,
|
||||||
|
CaId = CaId,
|
||||||
|
Status = Status,
|
||||||
|
Ttl = Ttl,
|
||||||
|
SubjectAlternativeNames = ToStringList(SubjectAlternativeName),
|
||||||
|
KeyUsages = ToStringList(KeyUsage),
|
||||||
|
ExtendedKeyUsages = ToStringList(ExtendedKeyUsage),
|
||||||
|
Properties = ToJsonObject(Properties)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (MyInvocation.BoundParameters.ContainsKey("EnableAutoRenewal")) { request.EnableAutoRenewal = EnableAutoRenewal.IsPresent; }
|
||||||
|
if (AutoRenewalPeriodInDays.HasValue) { request.AutoRenewalPeriodInDays = AutoRenewalPeriodInDays.Value; }
|
||||||
|
|
||||||
|
InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
|
||||||
|
WriteObject(client.WritePkiSubscriber(connection, ProjectId, null, request));
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
WriteErrorForException(Component, "CreatePkiSubscriber", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates a PKI subscriber. Only the supplied values are sent.
|
||||||
|
/// </summary>
|
||||||
|
[Cmdlet(VerbsCommon.Set, "InfisicalPkiSubscriber", SupportsShouldProcess = true)]
|
||||||
|
[OutputType(typeof(InfisicalPkiSubscriber))]
|
||||||
|
public sealed class SetInfisicalPkiSubscriberCmdlet : InfisicalPkiWriteCmdletBase
|
||||||
|
{
|
||||||
|
private const string Component = "SetInfisicalPkiSubscriberCmdlet";
|
||||||
|
|
||||||
|
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
|
||||||
|
[Alias("SubscriberName", "Slug")]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
[Parameter] public string ProjectId { get; set; }
|
||||||
|
[Parameter] public string NewName { get; set; }
|
||||||
|
[Parameter] public string CommonName { get; set; }
|
||||||
|
[Parameter] public string CaId { get; set; }
|
||||||
|
[Parameter] public string Ttl { get; set; }
|
||||||
|
[Parameter] public string[] SubjectAlternativeName { get; set; }
|
||||||
|
[Parameter] public string[] KeyUsage { get; set; }
|
||||||
|
[Parameter] public string[] ExtendedKeyUsage { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
[ValidateSet("active", "disabled")]
|
||||||
|
public string Status { get; set; }
|
||||||
|
|
||||||
|
[Parameter] public SwitchParameter EnableAutoRenewal { get; set; }
|
||||||
|
[Parameter] public int? AutoRenewalPeriodInDays { get; set; }
|
||||||
|
[Parameter] public IDictionary Properties { 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(Name, "Update PKI subscriber")) { return; }
|
||||||
|
|
||||||
|
InfisicalPkiSubscriberWriteRequestDto request = new InfisicalPkiSubscriberWriteRequestDto
|
||||||
|
{
|
||||||
|
Name = NewName,
|
||||||
|
CommonName = CommonName,
|
||||||
|
CaId = CaId,
|
||||||
|
Status = Status,
|
||||||
|
Ttl = Ttl,
|
||||||
|
SubjectAlternativeNames = ToStringList(SubjectAlternativeName),
|
||||||
|
KeyUsages = ToStringList(KeyUsage),
|
||||||
|
ExtendedKeyUsages = ToStringList(ExtendedKeyUsage),
|
||||||
|
Properties = ToJsonObject(Properties)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (MyInvocation.BoundParameters.ContainsKey("EnableAutoRenewal")) { request.EnableAutoRenewal = EnableAutoRenewal.IsPresent; }
|
||||||
|
if (AutoRenewalPeriodInDays.HasValue) { request.AutoRenewalPeriodInDays = AutoRenewalPeriodInDays.Value; }
|
||||||
|
|
||||||
|
InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
|
||||||
|
InfisicalPkiSubscriber updated = client.WritePkiSubscriber(connection, ProjectId, Name, request);
|
||||||
|
|
||||||
|
if (PassThru.IsPresent) { WriteObject(updated); }
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
WriteErrorForException(Component, "UpdatePkiSubscriber", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes a PKI subscriber.
|
||||||
|
/// </summary>
|
||||||
|
[Cmdlet(VerbsCommon.Remove, "InfisicalPkiSubscriber", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
|
||||||
|
public sealed class RemoveInfisicalPkiSubscriberCmdlet : InfisicalPkiWriteCmdletBase
|
||||||
|
{
|
||||||
|
private const string Component = "RemoveInfisicalPkiSubscriberCmdlet";
|
||||||
|
|
||||||
|
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
|
||||||
|
[Alias("SubscriberName", "Slug")]
|
||||||
|
public string Name { 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(Name, "Delete PKI subscriber")) { return; }
|
||||||
|
|
||||||
|
InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
|
||||||
|
client.DeletePkiSubscriber(connection, ProjectId, Name);
|
||||||
|
|
||||||
|
if (PassThru.IsPresent) { WriteObject(Name); }
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
WriteErrorForException(Component, "DeletePkiSubscriber", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
[Alias("ProfileId")]
|
[Alias("ProfileId")]
|
||||||
public string CertificateProfileId { get; set; }
|
public string CertificateProfileId { get; set; }
|
||||||
|
|
||||||
[Parameter(Mandatory = true)] public string ProjectId { get; set; }
|
[Parameter] public string ProjectId { get; set; }
|
||||||
[Parameter] public IDictionary Subject { get; set; }
|
[Parameter] public IDictionary Subject { get; set; }
|
||||||
[Parameter] public string CommonName { get; set; }
|
[Parameter] public string CommonName { get; set; }
|
||||||
[Parameter] public string Country { get; set; }
|
[Parameter] public string Country { get; set; }
|
||||||
@@ -52,6 +52,12 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
// Available on every parameter set: it names the installed certificate in the Windows store. The CA path
|
// Available on every parameter set: it names the installed certificate in the Windows store. The CA path
|
||||||
// additionally forwards it to Infisical as the issued certificate's friendlyName.
|
// additionally forwards it to Infisical as the issued certificate's friendlyName.
|
||||||
[Parameter] public string FriendlyName { get; set; }
|
[Parameter] public string FriendlyName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Metadata to attach to the issued or reused certificate. Only the supplied keys are reconciled;
|
||||||
|
/// any other metadata already on the certificate is left alone.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter] public IDictionary Metadata { get; set; }
|
||||||
[Parameter(ParameterSetName = "ByCa")] public string PkiCollectionId { get; set; }
|
[Parameter(ParameterSetName = "ByCa")] public string PkiCollectionId { get; set; }
|
||||||
[Parameter(ParameterSetName = "ByCa")]
|
[Parameter(ParameterSetName = "ByCa")]
|
||||||
[Parameter(ParameterSetName = "ByProfile")] public string[] KeyUsage { get; set; }
|
[Parameter(ParameterSetName = "ByProfile")] public string[] KeyUsage { get; set; }
|
||||||
@@ -79,6 +85,11 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
||||||
|
|
||||||
|
// The UI never asks which Certificate Manager project to use when an organization has only
|
||||||
|
// one; -ProjectId is optional here for the same reason. Assigned back so every call below
|
||||||
|
// sees the resolved value without threading a second variable through.
|
||||||
|
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
|
||||||
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
|
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
|
||||||
|
|
||||||
// Resolved once so reuse detection looks in the same stores the install will write to.
|
// Resolved once so reuse detection looks in the same stores the install will write to.
|
||||||
@@ -90,7 +101,7 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
if (string.IsNullOrEmpty(csrSubject.CommonName) && dnsNames.Count > 0) { csrSubject.CommonName = dnsNames[0]; }
|
if (string.IsNullOrEmpty(csrSubject.CommonName) && dnsNames.Count > 0) { csrSubject.CommonName = dnsNames[0]; }
|
||||||
if (string.IsNullOrEmpty(csrSubject.CommonName)) { throw new InvalidOperationException("Subject CommonName could not be determined and no DnsName was provided."); }
|
if (string.IsNullOrEmpty(csrSubject.CommonName)) { throw new InvalidOperationException("Subject CommonName could not be determined and no DnsName was provided."); }
|
||||||
|
|
||||||
X509Certificate2 existing = TryFindExisting(client, connection, ProjectId, csrSubject.CommonName, resolvedStoreLocation);
|
X509Certificate2 existing = TryFindExisting(client, connection, ProjectId, csrSubject.CommonName, resolvedStoreLocation, dnsNames, ipAddresses);
|
||||||
if (existing != null && !Force.IsPresent && !(AllowRenewal.IsPresent && InfisicalLocalCertificateLookup.IsRenewable(existing, RenewalThresholdDays)))
|
if (existing != null && !Force.IsPresent && !(AllowRenewal.IsPresent && InfisicalLocalCertificateLookup.IsRenewable(existing, RenewalThresholdDays)))
|
||||||
{
|
{
|
||||||
Logger.Information(Component, string.Concat("Reusing existing certificate (Thumbprint=", existing.Thumbprint, ", NotAfter=", existing.NotAfter.ToString("u"), ")."));
|
Logger.Information(Component, string.Concat("Reusing existing certificate (Thumbprint=", existing.Thumbprint, ", NotAfter=", existing.NotAfter.ToString("u"), ")."));
|
||||||
@@ -116,6 +127,9 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reconciled on the reuse path too, so a metadata change lands without forcing reissuance.
|
||||||
|
reuseResult.Metadata = ApplyMetadata(client, connection, existing.SerialNumber);
|
||||||
|
|
||||||
WriteObject(reuseResult);
|
WriteObject(reuseResult);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -161,6 +175,7 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
}
|
}
|
||||||
|
|
||||||
InfisicalCertificateResult resultObj = InfisicalCertificateRequestHelpers.BuildResult(cert, signed);
|
InfisicalCertificateResult resultObj = InfisicalCertificateRequestHelpers.BuildResult(cert, signed);
|
||||||
|
resultObj.Metadata = ApplyMetadata(client, connection, signed.SerialNumber);
|
||||||
|
|
||||||
bool hasExplicitPath = !string.IsNullOrEmpty(PrivateKeyPath);
|
bool hasExplicitPath = !string.IsNullOrEmpty(PrivateKeyPath);
|
||||||
if (hasExplicitPath && !string.IsNullOrEmpty(resultObj.PrivateKeyPem))
|
if (hasExplicitPath && !string.IsNullOrEmpty(resultObj.PrivateKeyPem))
|
||||||
@@ -183,6 +198,79 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reconciles -Metadata onto the certificate identified by <paramref name="serialNumber"/> and returns
|
||||||
|
/// the certificate's resulting metadata. Only the supplied keys are touched, so a certificate can carry
|
||||||
|
/// metadata from several sources without them overwriting each other.
|
||||||
|
/// <para>
|
||||||
|
/// Metadata never fails an issuance that otherwise succeeded: by the time this runs the certificate
|
||||||
|
/// exists and may already be installed, so a failure here is reported as a warning and the certificate
|
||||||
|
/// is still emitted.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
private Dictionary<string, string> ApplyMetadata(InfisicalPkiClient client, InfisicalConnection connection, string serialNumber)
|
||||||
|
{
|
||||||
|
Dictionary<string, string> desired = NormalizeMetadata(Metadata);
|
||||||
|
if (desired.Count == 0) { return null; }
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(serialNumber))
|
||||||
|
{
|
||||||
|
Logger.Warning(Component, "-Metadata was supplied but the certificate has no serial number to identify it by; metadata was not applied.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
InfisicalCertificate record = client.RetrieveCertificate(connection, serialNumber);
|
||||||
|
if (record == null || string.IsNullOrEmpty(record.Id))
|
||||||
|
{
|
||||||
|
Logger.Warning(Component, string.Concat("-Metadata was supplied but certificate '", serialNumber, "' could not be resolved in Infisical; metadata was not applied."));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary<string, string> result = client.ReconcileCertificateMetadata(connection, record.Id, desired);
|
||||||
|
Logger.Information(Component, string.Concat(
|
||||||
|
"Reconciled ", desired.Count.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||||||
|
" metadata key(s) onto certificate '", record.Id, "'; it now carries ",
|
||||||
|
(result != null ? result.Count : 0).ToString(System.Globalization.CultureInfo.InvariantCulture), " key(s)."));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception metadataException)
|
||||||
|
{
|
||||||
|
if (IsPipelineControlException(metadataException)) { throw; }
|
||||||
|
Logger.Warning(Component, string.Concat("The certificate was issued but its metadata could not be applied: ", metadataException.Message));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flattens the caller's dictionary into string key/value pairs. PowerShell hands over hashtables whose
|
||||||
|
/// keys and values are arbitrary objects, and the API accepts only strings.
|
||||||
|
/// </summary>
|
||||||
|
internal static Dictionary<string, string> NormalizeMetadata(IDictionary source)
|
||||||
|
{
|
||||||
|
Dictionary<string, string> result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
if (source == null) { return result; }
|
||||||
|
|
||||||
|
foreach (DictionaryEntry entry in source)
|
||||||
|
{
|
||||||
|
if (entry.Key == null) { continue; }
|
||||||
|
|
||||||
|
string key = Convert.ToString(entry.Key, System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
if (string.IsNullOrWhiteSpace(key)) { continue; }
|
||||||
|
key = key.Trim();
|
||||||
|
|
||||||
|
string value = entry.Value != null
|
||||||
|
? Convert.ToString(entry.Value, System.Globalization.CultureInfo.InvariantCulture)
|
||||||
|
: string.Empty;
|
||||||
|
|
||||||
|
result[key] = value ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The Windows friendly name shown in certmgr. Defaults to the common name in upper case, which is the
|
/// The Windows friendly name shown in certmgr. Defaults to the common name in upper case, which is the
|
||||||
/// host identity operators look for; -FriendlyName overrides it.
|
/// host identity operators look for; -FriendlyName overrides it.
|
||||||
@@ -307,17 +395,39 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
return System.Net.IPAddress.TryParse(value, out parsed);
|
return System.Net.IPAddress.TryParse(value, out parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
private X509Certificate2 TryFindExisting(InfisicalPkiClient client, InfisicalConnection connection, string projectId, string commonName, StoreLocation storeLocation)
|
/// <summary>
|
||||||
|
/// Finds a still-valid local certificate that this same request would have produced. The match is scoped
|
||||||
|
/// to the issuer being asked for: a certificate issued by a different profile or CA carries different key
|
||||||
|
/// usages and policy, so reusing one across issuers hands back a certificate that does not satisfy the
|
||||||
|
/// request that was actually made.
|
||||||
|
/// </summary>
|
||||||
|
private X509Certificate2 TryFindExisting(InfisicalPkiClient client, InfisicalConnection connection, string projectId, string commonName, StoreLocation storeLocation, List<string> requestedDnsNames, List<string> requestedIpAddresses)
|
||||||
{
|
{
|
||||||
List<string> candidateSerials = new List<string>();
|
List<string> candidateSerials = new List<string>();
|
||||||
|
bool searchCompleted = false;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
InfisicalCertificateSearchQuery query = new InfisicalCertificateSearchQuery { ProjectId = projectId, CommonName = commonName, Status = "active", Limit = 50 };
|
InfisicalCertificateSearchQuery query = new InfisicalCertificateSearchQuery
|
||||||
|
{
|
||||||
|
ProjectId = projectId,
|
||||||
|
CommonName = commonName,
|
||||||
|
Status = "active",
|
||||||
|
Limit = 50
|
||||||
|
};
|
||||||
|
|
||||||
|
string scope = ApplyIssuerScope(query);
|
||||||
|
|
||||||
InfisicalCertificateSearchResult page = client.SearchCertificates(connection, query);
|
InfisicalCertificateSearchResult page = client.SearchCertificates(connection, query);
|
||||||
|
searchCompleted = true;
|
||||||
if (page != null && page.Certificates != null)
|
if (page != null && page.Certificates != null)
|
||||||
{
|
{
|
||||||
foreach (InfisicalCertificate hit in page.Certificates) { if (!string.IsNullOrEmpty(hit.SerialNumber)) { candidateSerials.Add(hit.SerialNumber); } }
|
foreach (InfisicalCertificate hit in page.Certificates) { if (!string.IsNullOrEmpty(hit.SerialNumber)) { candidateSerials.Add(hit.SerialNumber); } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.Verbose(Component, string.Concat(
|
||||||
|
"Reuse search for CN=", commonName, " scoped to ", scope, " returned ",
|
||||||
|
candidateSerials.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " active certificate(s)."));
|
||||||
}
|
}
|
||||||
catch (Exception searchException)
|
catch (Exception searchException)
|
||||||
{
|
{
|
||||||
@@ -325,7 +435,61 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
Logger.Verbose(Component, string.Concat("Infisical search for idempotency check failed: ", searchException.Message));
|
Logger.Verbose(Component, string.Concat("Infisical search for idempotency check failed: ", searchException.Message));
|
||||||
}
|
}
|
||||||
|
|
||||||
return InfisicalLocalCertificateLookup.FindMatch(StoreName, storeLocation, commonName, candidateSerials);
|
// A completed search that found nothing is a definite answer: this issuer has never issued for this
|
||||||
|
// common name, so there is nothing to reuse. Falling through to a name-only local match here is what
|
||||||
|
// let a certificate from another profile be handed back.
|
||||||
|
if (searchCompleted && candidateSerials.Count == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!searchCompleted)
|
||||||
|
{
|
||||||
|
Logger.Warning(Component, string.Concat(
|
||||||
|
"Could not confirm with Infisical which certificates belong to this issuer, so reuse falls back to ",
|
||||||
|
"matching on the common name alone. That can return a certificate issued by a different profile or CA; ",
|
||||||
|
"pass -Force to issue unconditionally."));
|
||||||
|
}
|
||||||
|
|
||||||
|
string missingName;
|
||||||
|
X509Certificate2 match = InfisicalLocalCertificateLookup.FindMatch(
|
||||||
|
StoreName, storeLocation, commonName, candidateSerials, requestedDnsNames, requestedIpAddresses, out missingName);
|
||||||
|
|
||||||
|
if (match == null && missingName != null)
|
||||||
|
{
|
||||||
|
Logger.Information(Component, string.Concat(
|
||||||
|
"An existing certificate for CN=", commonName, " does not carry the requested name ", missingName,
|
||||||
|
"; requesting a new certificate rather than reusing one that would fail validation for it."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Narrows a certificate search to the issuer this invocation targets, and names that scope for logging.
|
||||||
|
/// The subscriber path has no server-side filter, but a subscriber pins its own common name, so matching
|
||||||
|
/// on the name is already equivalent to matching on the subscriber.
|
||||||
|
/// </summary>
|
||||||
|
private string ApplyIssuerScope(InfisicalCertificateSearchQuery query)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(CertificateProfileId))
|
||||||
|
{
|
||||||
|
query.ProfileIds = new[] { CertificateProfileId };
|
||||||
|
return string.Concat("certificate profile '", CertificateProfileId, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(CertificateAuthorityId))
|
||||||
|
{
|
||||||
|
query.CaIds = new[] { CertificateAuthorityId };
|
||||||
|
return string.Concat("certificate authority '", CertificateAuthorityId, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(PkiSubscriberSlug))
|
||||||
|
{
|
||||||
|
return string.Concat("PKI subscriber '", PkiSubscriberSlug, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
return "this project";
|
||||||
}
|
}
|
||||||
|
|
||||||
private X509KeyStorageFlags ResolveEffectiveKeyStorageFlags(StoreLocation storeLocation)
|
private X509KeyStorageFlags ResolveEffectiveKeyStorageFlags(StoreLocation storeLocation)
|
||||||
|
|||||||
@@ -61,6 +61,32 @@ namespace PSInfisicalAPI.Endpoints
|
|||||||
public const string SearchCertificates = "SearchCertificates";
|
public const string SearchCertificates = "SearchCertificates";
|
||||||
public const string RetrieveCertificate = "RetrieveCertificate";
|
public const string RetrieveCertificate = "RetrieveCertificate";
|
||||||
public const string GetCertificateBundle = "GetCertificateBundle";
|
public const string GetCertificateBundle = "GetCertificateBundle";
|
||||||
|
public const string UpdateCertificateMetadata = "UpdateCertificateMetadata";
|
||||||
|
|
||||||
|
public const string CreateInternalCertificateAuthority = "CreateInternalCertificateAuthority";
|
||||||
|
public const string UpdateInternalCertificateAuthority = "UpdateInternalCertificateAuthority";
|
||||||
|
public const string DeleteInternalCertificateAuthority = "DeleteInternalCertificateAuthority";
|
||||||
|
public const string GetCertificateAuthorityCsr = "GetCertificateAuthorityCsr";
|
||||||
|
public const string SignIntermediateCertificateAuthority = "SignIntermediateCertificateAuthority";
|
||||||
|
public const string ImportCertificateAuthorityCertificate = "ImportCertificateAuthorityCertificate";
|
||||||
|
|
||||||
|
public const string CreateCertificatePolicy = "CreateCertificatePolicy";
|
||||||
|
public const string UpdateCertificatePolicy = "UpdateCertificatePolicy";
|
||||||
|
public const string DeleteCertificatePolicy = "DeleteCertificatePolicy";
|
||||||
|
|
||||||
|
public const string CreateCertificateProfile = "CreateCertificateProfile";
|
||||||
|
public const string UpdateCertificateProfile = "UpdateCertificateProfile";
|
||||||
|
public const string DeleteCertificateProfile = "DeleteCertificateProfile";
|
||||||
|
|
||||||
|
public const string CreateCertificateApplication = "CreateCertificateApplication";
|
||||||
|
public const string UpdateCertificateApplication = "UpdateCertificateApplication";
|
||||||
|
public const string DeleteCertificateApplication = "DeleteCertificateApplication";
|
||||||
|
public const string AddCertificateApplicationProfiles = "AddCertificateApplicationProfiles";
|
||||||
|
public const string RemoveCertificateApplicationProfile = "RemoveCertificateApplicationProfile";
|
||||||
|
|
||||||
|
public const string CreatePkiSubscriber = "CreatePkiSubscriber";
|
||||||
|
public const string UpdatePkiSubscriber = "UpdatePkiSubscriber";
|
||||||
|
public const string DeletePkiSubscriber = "DeletePkiSubscriber";
|
||||||
public const string SignCertificateBySubscriber = "SignCertificateBySubscriber";
|
public const string SignCertificateBySubscriber = "SignCertificateBySubscriber";
|
||||||
public const string SignCertificateByCa = "SignCertificateByCa";
|
public const string SignCertificateByCa = "SignCertificateByCa";
|
||||||
public const string IssueCertificateByProfile = "IssueCertificateByProfile";
|
public const string IssueCertificateByProfile = "IssueCertificateByProfile";
|
||||||
|
|||||||
@@ -288,6 +288,18 @@ namespace PSInfisicalAPI.Endpoints
|
|||||||
|
|
||||||
private static void RegisterProjects(Dictionary<string, List<InfisicalEndpointDefinition>> map)
|
private static void RegisterProjects(Dictionary<string, List<InfisicalEndpointDefinition>> map)
|
||||||
{
|
{
|
||||||
|
// /api/v1/projects is the current route; /api/v1/workspace mounts Infisical's deprecated project
|
||||||
|
// router and is kept only as a fallback for older servers.
|
||||||
|
Add(map, new InfisicalEndpointDefinition
|
||||||
|
{
|
||||||
|
Name = InfisicalEndpointNames.ListProjects,
|
||||||
|
Resource = "Projects",
|
||||||
|
Version = "v1",
|
||||||
|
Method = "GET",
|
||||||
|
Template = "/api/v1/projects",
|
||||||
|
RequiresAuthorization = true
|
||||||
|
});
|
||||||
|
|
||||||
Add(map, new InfisicalEndpointDefinition
|
Add(map, new InfisicalEndpointDefinition
|
||||||
{
|
{
|
||||||
Name = InfisicalEndpointNames.ListProjects,
|
Name = InfisicalEndpointNames.ListProjects,
|
||||||
@@ -500,6 +512,19 @@ namespace PSInfisicalAPI.Endpoints
|
|||||||
|
|
||||||
private static void RegisterOrganizations(Dictionary<string, List<InfisicalEndpointDefinition>> map)
|
private static void RegisterOrganizations(Dictionary<string, List<InfisicalEndpointDefinition>> map)
|
||||||
{
|
{
|
||||||
|
Add(map, new InfisicalEndpointDefinition
|
||||||
|
{
|
||||||
|
// The listing lives on the singular v1 route. /api/v2/organizations mounts only
|
||||||
|
// /:organizationId/* sub-routes and has no GET /, so asking it for a list returns
|
||||||
|
// "Route GET:/api/v2/organizations not found". Kept as a fallback candidate all the same.
|
||||||
|
Name = InfisicalEndpointNames.ListOrganizations,
|
||||||
|
Resource = "Organizations",
|
||||||
|
Version = "v1",
|
||||||
|
Method = "GET",
|
||||||
|
Template = "/api/v1/organization",
|
||||||
|
RequiresAuthorization = true
|
||||||
|
});
|
||||||
|
|
||||||
Add(map, new InfisicalEndpointDefinition
|
Add(map, new InfisicalEndpointDefinition
|
||||||
{
|
{
|
||||||
Name = InfisicalEndpointNames.ListOrganizations,
|
Name = InfisicalEndpointNames.ListOrganizations,
|
||||||
@@ -656,6 +681,11 @@ namespace PSInfisicalAPI.Endpoints
|
|||||||
RequiresAuthorization = true
|
RequiresAuthorization = true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// RetrieveCertificate and GetCertificateBundle deliberately lead with the older /api/v1/pki route:
|
||||||
|
// it resolves a certificate by SERIAL NUMBER, which is what callers supply. The newer
|
||||||
|
// /api/v1/cert-manager route takes a certificate ID and passes it straight through as
|
||||||
|
// getCert({ id }), so it cannot answer a serial. Both are registered so either identifier resolves;
|
||||||
|
// reordering these two would send every serial lookup to the route that cannot serve it.
|
||||||
Add(map, new InfisicalEndpointDefinition
|
Add(map, new InfisicalEndpointDefinition
|
||||||
{
|
{
|
||||||
Name = InfisicalEndpointNames.RetrieveCertificate,
|
Name = InfisicalEndpointNames.RetrieveCertificate,
|
||||||
@@ -698,6 +728,63 @@ namespace PSInfisicalAPI.Endpoints
|
|||||||
ContainsSecretMaterialInResponse = true
|
ContainsSecretMaterialInResponse = true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
#region PKI configuration management
|
||||||
|
|
||||||
|
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.CreateInternalCertificateAuthority, Resource = "Pki", Version = "v1", Method = "POST", Template = "/api/v1/cert-manager/ca/internal", RequiresAuthorization = true });
|
||||||
|
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.CreateInternalCertificateAuthority, Resource = "Pki", Version = "v1", Method = "POST", Template = "/api/v1/pki/ca/internal", RequiresAuthorization = true });
|
||||||
|
|
||||||
|
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.UpdateInternalCertificateAuthority, Resource = "Pki", Version = "v1", Method = "PATCH", Template = "/api/v1/cert-manager/ca/internal/{caId}", RequiresAuthorization = true });
|
||||||
|
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.UpdateInternalCertificateAuthority, Resource = "Pki", Version = "v1", Method = "PATCH", Template = "/api/v1/pki/ca/internal/{caId}", RequiresAuthorization = true });
|
||||||
|
|
||||||
|
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.DeleteInternalCertificateAuthority, Resource = "Pki", Version = "v1", Method = "DELETE", Template = "/api/v1/cert-manager/ca/internal/{caId}", RequiresAuthorization = true });
|
||||||
|
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.DeleteInternalCertificateAuthority, Resource = "Pki", Version = "v1", Method = "DELETE", Template = "/api/v1/pki/ca/internal/{caId}", RequiresAuthorization = true });
|
||||||
|
|
||||||
|
// Standing up a subordinate takes three calls: Infisical creates it pending, then the CSR is signed
|
||||||
|
// by the parent and imported back.
|
||||||
|
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.GetCertificateAuthorityCsr, Resource = "Pki", Version = "v1", Method = "GET", Template = "/api/v1/pki/ca/{caId}/csr", RequiresAuthorization = true });
|
||||||
|
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.SignIntermediateCertificateAuthority, Resource = "Pki", Version = "v1", Method = "POST", Template = "/api/v1/pki/ca/{caId}/sign-intermediate", RequiresAuthorization = true, ContainsSecretMaterialInResponse = true });
|
||||||
|
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.ImportCertificateAuthorityCertificate, Resource = "Pki", Version = "v1", Method = "POST", Template = "/api/v1/pki/ca/{caId}/import-certificate", RequiresAuthorization = true });
|
||||||
|
|
||||||
|
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.CreateCertificatePolicy, Resource = "Pki", Version = "v1", Method = "POST", Template = "/api/v1/cert-manager/certificate-policies", RequiresAuthorization = true });
|
||||||
|
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.UpdateCertificatePolicy, Resource = "Pki", Version = "v1", Method = "PATCH", Template = "/api/v1/cert-manager/certificate-policies/{policyId}", RequiresAuthorization = true });
|
||||||
|
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.DeleteCertificatePolicy, Resource = "Pki", Version = "v1", Method = "DELETE", Template = "/api/v1/cert-manager/certificate-policies/{policyId}", RequiresAuthorization = true });
|
||||||
|
|
||||||
|
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.CreateCertificateProfile, Resource = "Pki", Version = "v1", Method = "POST", Template = "/api/v1/cert-manager/certificate-profiles", RequiresAuthorization = true });
|
||||||
|
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.UpdateCertificateProfile, Resource = "Pki", Version = "v1", Method = "PATCH", Template = "/api/v1/cert-manager/certificate-profiles/{profileId}", RequiresAuthorization = true });
|
||||||
|
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.DeleteCertificateProfile, Resource = "Pki", Version = "v1", Method = "DELETE", Template = "/api/v1/cert-manager/certificate-profiles/{profileId}", RequiresAuthorization = true });
|
||||||
|
|
||||||
|
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.CreateCertificateApplication, Resource = "Pki", Version = "v1", Method = "POST", Template = "/api/v1/cert-manager/applications", RequiresAuthorization = true });
|
||||||
|
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.UpdateCertificateApplication, Resource = "Pki", Version = "v1", Method = "PATCH", Template = "/api/v1/cert-manager/applications/{applicationId}", RequiresAuthorization = true });
|
||||||
|
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.DeleteCertificateApplication, Resource = "Pki", Version = "v1", Method = "DELETE", Template = "/api/v1/cert-manager/applications/{applicationId}", RequiresAuthorization = true });
|
||||||
|
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.AddCertificateApplicationProfiles, Resource = "Pki", Version = "v1", Method = "POST", Template = "/api/v1/cert-manager/applications/{applicationId}/profiles", RequiresAuthorization = true });
|
||||||
|
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.RemoveCertificateApplicationProfile, Resource = "Pki", Version = "v1", Method = "DELETE", Template = "/api/v1/cert-manager/applications/{applicationId}/profiles/{profileId}", RequiresAuthorization = true });
|
||||||
|
|
||||||
|
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.CreatePkiSubscriber, Resource = "Pki", Version = "v1", Method = "POST", Template = "/api/v1/pki/subscribers", RequiresAuthorization = true });
|
||||||
|
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.UpdatePkiSubscriber, Resource = "Pki", Version = "v1", Method = "PATCH", Template = "/api/v1/pki/subscribers/{subscriberName}", RequiresAuthorization = true });
|
||||||
|
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.DeletePkiSubscriber, Resource = "Pki", Version = "v1", Method = "DELETE", Template = "/api/v1/pki/subscribers/{subscriberName}", RequiresAuthorization = true });
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
Add(map, new InfisicalEndpointDefinition
|
||||||
|
{
|
||||||
|
Name = InfisicalEndpointNames.UpdateCertificateMetadata,
|
||||||
|
Resource = "Pki",
|
||||||
|
Version = "v1",
|
||||||
|
Method = "PATCH",
|
||||||
|
Template = "/api/v1/cert-manager/certificates/{certificateId}",
|
||||||
|
RequiresAuthorization = true
|
||||||
|
});
|
||||||
|
|
||||||
|
Add(map, new InfisicalEndpointDefinition
|
||||||
|
{
|
||||||
|
Name = InfisicalEndpointNames.UpdateCertificateMetadata,
|
||||||
|
Resource = "Pki",
|
||||||
|
Version = "v1",
|
||||||
|
Method = "PATCH",
|
||||||
|
Template = "/api/v1/pki/certificates/{certificateId}",
|
||||||
|
RequiresAuthorization = true
|
||||||
|
});
|
||||||
|
|
||||||
Add(map, new InfisicalEndpointDefinition
|
Add(map, new InfisicalEndpointDefinition
|
||||||
{
|
{
|
||||||
Name = InfisicalEndpointNames.SignCertificateBySubscriber,
|
Name = InfisicalEndpointNames.SignCertificateBySubscriber,
|
||||||
@@ -715,7 +802,7 @@ namespace PSInfisicalAPI.Endpoints
|
|||||||
Resource = "Pki",
|
Resource = "Pki",
|
||||||
Version = "v1",
|
Version = "v1",
|
||||||
Method = "POST",
|
Method = "POST",
|
||||||
Template = "/api/v1/pki/ca/{caId}/sign-certificate",
|
Template = "/api/v1/cert-manager/ca/{caId}/sign-certificate",
|
||||||
RequiresAuthorization = true,
|
RequiresAuthorization = true,
|
||||||
ContainsSecretMaterialInResponse = true
|
ContainsSecretMaterialInResponse = true
|
||||||
});
|
});
|
||||||
@@ -726,7 +813,7 @@ namespace PSInfisicalAPI.Endpoints
|
|||||||
Resource = "Pki",
|
Resource = "Pki",
|
||||||
Version = "v1",
|
Version = "v1",
|
||||||
Method = "POST",
|
Method = "POST",
|
||||||
Template = "/api/v1/cert-manager/ca/{caId}/sign-certificate",
|
Template = "/api/v1/pki/ca/{caId}/sign-certificate",
|
||||||
RequiresAuthorization = true,
|
RequiresAuthorization = true,
|
||||||
ContainsSecretMaterialInResponse = true
|
ContainsSecretMaterialInResponse = true
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ namespace PSInfisicalAPI.Models
|
|||||||
public string Source { get; set; }
|
public string Source { get; set; }
|
||||||
public string EnrollmentType { get; set; }
|
public string EnrollmentType { get; set; }
|
||||||
public bool HasPrivateKey { get; set; }
|
public bool HasPrivateKey { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Metadata key/value pairs attached to the certificate. Case-insensitive on key, matching how the
|
||||||
|
/// API treats them and how callers supply them from PowerShell.
|
||||||
|
/// </summary>
|
||||||
|
public System.Collections.Generic.Dictionary<string, string> Metadata { get; set; }
|
||||||
public int? RevocationReason { get; set; }
|
public int? RevocationReason { get; set; }
|
||||||
public string RenewalError { get; set; }
|
public string RenewalError { get; set; }
|
||||||
public int? RenewBeforeDays { get; set; }
|
public int? RenewBeforeDays { get; set; }
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ namespace PSInfisicalAPI.Models
|
|||||||
public string StatusMessage { get; set; }
|
public string StatusMessage { get; set; }
|
||||||
public string CertificateRequestId { get; set; }
|
public string CertificateRequestId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The certificate's metadata in Infisical after reconciliation, or null when -Metadata was not used.
|
||||||
|
/// </summary>
|
||||||
|
public System.Collections.Generic.Dictionary<string, string> Metadata { get; set; }
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
if (Leaf != null) { return Leaf.Subject; }
|
if (Leaf != null) { return Leaf.Subject; }
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ namespace PSInfisicalAPI.Models
|
|||||||
public string Slug { get; set; }
|
public string Slug { get; set; }
|
||||||
public string CustomerId { get; set; }
|
public string CustomerId { get; set; }
|
||||||
public bool AuthEnforced { get; set; }
|
public bool AuthEnforced { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The organization's active Certificate Manager project. Certificate applications are only available on
|
||||||
|
/// this project, so it is what a PKI call resolves to when an organization has more than one.
|
||||||
|
/// </summary>
|
||||||
|
public string DefaultCertManagerProjectId { get; set; }
|
||||||
public bool ScimEnabled { get; set; }
|
public bool ScimEnabled { get; set; }
|
||||||
public DateTimeOffset? CreatedAtUtc { get; set; }
|
public DateTimeOffset? CreatedAtUtc { get; set; }
|
||||||
public DateTimeOffset? UpdatedAtUtc { get; set; }
|
public DateTimeOffset? UpdatedAtUtc { get; set; }
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ namespace PSInfisicalAPI.Organizations
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.Information(Component, "Attempting to list Infisical organizations. Please Wait...");
|
_logger.Information(Component, "Attempting to list Infisical organizations. Please Wait...");
|
||||||
InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.ListOrganizations, "ListOrganizations", null, null, null);
|
InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.ListOrganizations, "ListOrganizations", null, null, null);
|
||||||
InfisicalOrganizationListResponseDto dto = _serializer.Deserialize<InfisicalOrganizationListResponseDto>(response.Body);
|
InfisicalOrganizationListResponseDto dto = _serializer.Deserialize<InfisicalOrganizationListResponseDto>(response.Body);
|
||||||
response.Clear();
|
response.Clear();
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace PSInfisicalAPI.Organizations
|
|||||||
[JsonProperty("slug")] public string Slug { get; set; }
|
[JsonProperty("slug")] public string Slug { get; set; }
|
||||||
[JsonProperty("customerId")] public string CustomerId { get; set; }
|
[JsonProperty("customerId")] public string CustomerId { get; set; }
|
||||||
[JsonProperty("authEnforced")] public bool AuthEnforced { get; set; }
|
[JsonProperty("authEnforced")] public bool AuthEnforced { get; set; }
|
||||||
|
[JsonProperty("defaultCertManagerProjectId", NullValueHandling = NullValueHandling.Ignore)] public string DefaultCertManagerProjectId { get; set; }
|
||||||
[JsonProperty("scimEnabled")] public bool ScimEnabled { get; set; }
|
[JsonProperty("scimEnabled")] public bool ScimEnabled { get; set; }
|
||||||
[JsonProperty("createdAt")] public string CreatedAt { get; set; }
|
[JsonProperty("createdAt")] public string CreatedAt { get; set; }
|
||||||
[JsonProperty("updatedAt")] public string UpdatedAt { get; set; }
|
[JsonProperty("updatedAt")] public string UpdatedAt { get; set; }
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ namespace PSInfisicalAPI.Organizations
|
|||||||
Slug = dto.Slug,
|
Slug = dto.Slug,
|
||||||
CustomerId = dto.CustomerId,
|
CustomerId = dto.CustomerId,
|
||||||
AuthEnforced = dto.AuthEnforced,
|
AuthEnforced = dto.AuthEnforced,
|
||||||
|
DefaultCertManagerProjectId = dto.DefaultCertManagerProjectId,
|
||||||
ScimEnabled = dto.ScimEnabled,
|
ScimEnabled = dto.ScimEnabled,
|
||||||
CreatedAtUtc = ParseTimestamp(dto.CreatedAt),
|
CreatedAtUtc = ParseTimestamp(dto.CreatedAt),
|
||||||
UpdatedAtUtc = ParseTimestamp(dto.UpdatedAt)
|
UpdatedAtUtc = ParseTimestamp(dto.UpdatedAt)
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ namespace PSInfisicalAPI.Pki
|
|||||||
[JsonProperty("source")] public string Source { get; set; }
|
[JsonProperty("source")] public string Source { get; set; }
|
||||||
[JsonProperty("enrollmentType")] public string EnrollmentType { get; set; }
|
[JsonProperty("enrollmentType")] public string EnrollmentType { get; set; }
|
||||||
[JsonProperty("hasPrivateKey")] public bool HasPrivateKey { get; set; }
|
[JsonProperty("hasPrivateKey")] public bool HasPrivateKey { get; set; }
|
||||||
|
[JsonProperty("metadata", NullValueHandling = NullValueHandling.Ignore)] public InfisicalCertificateSearchMetadataEntryDto[] Metadata { get; set; }
|
||||||
[JsonProperty("revocationReason")] public int? RevocationReason { get; set; }
|
[JsonProperty("revocationReason")] public int? RevocationReason { get; set; }
|
||||||
[JsonProperty("renewalError")] public string RenewalError { get; set; }
|
[JsonProperty("renewalError")] public string RenewalError { get; set; }
|
||||||
[JsonProperty("renewBeforeDays")] public int? RenewBeforeDays { get; set; }
|
[JsonProperty("renewBeforeDays")] public int? RenewBeforeDays { get; set; }
|
||||||
@@ -96,6 +97,26 @@ namespace PSInfisicalAPI.Pki
|
|||||||
[JsonProperty("value", NullValueHandling = NullValueHandling.Ignore)] public string Value { get; set; }
|
[JsonProperty("value", NullValueHandling = NullValueHandling.Ignore)] public string Value { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Body for PATCH /certificates/{id}. The API replaces the certificate's entire metadata set with what
|
||||||
|
/// is sent, so this must always carry the full desired set rather than a delta.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class InfisicalUpdateCertificateMetadataRequestDto
|
||||||
|
{
|
||||||
|
[JsonProperty("metadata")] public List<InfisicalCertificateMetadataEntryDto> Metadata { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class InfisicalCertificateMetadataEntryDto
|
||||||
|
{
|
||||||
|
[JsonProperty("key")] public string Key { get; set; }
|
||||||
|
[JsonProperty("value")] public string Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class InfisicalUpdateCertificateMetadataResponseDto
|
||||||
|
{
|
||||||
|
[JsonProperty("metadata")] public List<InfisicalCertificateMetadataEntryDto> Metadata { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
internal sealed class InfisicalCertificateBundleResponseDto
|
internal sealed class InfisicalCertificateBundleResponseDto
|
||||||
{
|
{
|
||||||
[JsonProperty("serialNumber")] public string SerialNumber { get; set; }
|
[JsonProperty("serialNumber")] public string SerialNumber { get; set; }
|
||||||
|
|||||||
@@ -7,6 +7,24 @@ namespace PSInfisicalAPI.Pki
|
|||||||
{
|
{
|
||||||
internal static class InfisicalCertificateMapper
|
internal static class InfisicalCertificateMapper
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Projects the API's metadata array into a case-insensitive dictionary. A certificate with no
|
||||||
|
/// metadata maps to an empty dictionary rather than null, so callers can index it unconditionally.
|
||||||
|
/// </summary>
|
||||||
|
public static Dictionary<string, string> MapMetadata(InfisicalCertificateSearchMetadataEntryDto[] entries)
|
||||||
|
{
|
||||||
|
Dictionary<string, string> result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
if (entries == null) { return result; }
|
||||||
|
|
||||||
|
foreach (InfisicalCertificateSearchMetadataEntryDto entry in entries)
|
||||||
|
{
|
||||||
|
if (entry == null || string.IsNullOrEmpty(entry.Key)) { continue; }
|
||||||
|
result[entry.Key] = entry.Value ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
public static InfisicalCertificate Map(InfisicalCertificateResponseDto dto, string fallbackProjectId)
|
public static InfisicalCertificate Map(InfisicalCertificateResponseDto dto, string fallbackProjectId)
|
||||||
{
|
{
|
||||||
if (dto == null)
|
if (dto == null)
|
||||||
@@ -17,6 +35,7 @@ namespace PSInfisicalAPI.Pki
|
|||||||
return new InfisicalCertificate
|
return new InfisicalCertificate
|
||||||
{
|
{
|
||||||
Id = dto.Id,
|
Id = dto.Id,
|
||||||
|
Metadata = MapMetadata(dto.Metadata),
|
||||||
ProjectId = !string.IsNullOrEmpty(dto.ProjectId) ? dto.ProjectId : fallbackProjectId,
|
ProjectId = !string.IsNullOrEmpty(dto.ProjectId) ? dto.ProjectId : fallbackProjectId,
|
||||||
CaId = dto.CaId,
|
CaId = dto.CaId,
|
||||||
CaName = dto.CaName,
|
CaName = dto.CaName,
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using Org.BouncyCastle.Asn1;
|
||||||
|
using Org.BouncyCastle.Asn1.X509;
|
||||||
|
using NetX509Extension = System.Security.Cryptography.X509Certificates.X509Extension;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Pki
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The subject alternative names carried by a certificate, split the way a request specifies them.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class InfisicalCertificateSans
|
||||||
|
{
|
||||||
|
public HashSet<string> DnsNames { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
public HashSet<string> IpAddresses { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads the subject alternative name extension from an installed certificate.
|
||||||
|
/// <para>
|
||||||
|
/// netstandard2.0 has no X509SubjectAlternativeNameExtension, and the string form produced by
|
||||||
|
/// X509Extension.Format is localized and therefore unusable for comparison, so the extension is decoded from
|
||||||
|
/// its DER bytes with BouncyCastle, which the module already carries for CSR generation.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
internal static class InfisicalCertificateSanReader
|
||||||
|
{
|
||||||
|
private const string SubjectAlternativeNameOid = "2.5.29.17";
|
||||||
|
|
||||||
|
public static InfisicalCertificateSans Read(X509Certificate2 cert)
|
||||||
|
{
|
||||||
|
InfisicalCertificateSans result = new InfisicalCertificateSans();
|
||||||
|
if (cert == null) { return result; }
|
||||||
|
|
||||||
|
foreach (NetX509Extension extension in cert.Extensions)
|
||||||
|
{
|
||||||
|
if (extension == null || extension.Oid == null) { continue; }
|
||||||
|
if (!string.Equals(extension.Oid.Value, SubjectAlternativeNameOid, StringComparison.Ordinal)) { continue; }
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// X509Extension.RawData is the content of the extnValue OCTET STRING, so it decodes straight
|
||||||
|
// into the GeneralNames SEQUENCE.
|
||||||
|
Asn1Object decoded = Asn1Object.FromByteArray(extension.RawData);
|
||||||
|
GeneralNames names = GeneralNames.GetInstance(decoded);
|
||||||
|
if (names == null) { continue; }
|
||||||
|
|
||||||
|
foreach (GeneralName name in names.GetNames())
|
||||||
|
{
|
||||||
|
if (name == null) { continue; }
|
||||||
|
AddName(result, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// A certificate this malformed cannot be matched against a request; treat it as carrying no
|
||||||
|
// usable SANs rather than failing the caller's issuance.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddName(InfisicalCertificateSans target, GeneralName name)
|
||||||
|
{
|
||||||
|
switch (name.TagNo)
|
||||||
|
{
|
||||||
|
case GeneralName.DnsName:
|
||||||
|
{
|
||||||
|
string value = name.Name != null ? name.Name.ToString() : null;
|
||||||
|
if (!string.IsNullOrEmpty(value)) { target.DnsNames.Add(value.Trim()); }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GeneralName.IPAddress:
|
||||||
|
{
|
||||||
|
string value = FormatIpAddress(name);
|
||||||
|
if (!string.IsNullOrEmpty(value)) { target.IpAddresses.Add(value); }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An iPAddress general name holds raw address octets, four for IPv4 and sixteen for IPv6.
|
||||||
|
/// </summary>
|
||||||
|
private static string FormatIpAddress(GeneralName name)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Asn1OctetString octets = Asn1OctetString.GetInstance(name.Name);
|
||||||
|
if (octets == null) { return null; }
|
||||||
|
|
||||||
|
byte[] bytes = octets.GetOctets();
|
||||||
|
if (bytes == null) { return null; }
|
||||||
|
if (bytes.Length != 4 && bytes.Length != 16) { return null; }
|
||||||
|
|
||||||
|
return NormalizeIpAddress(new IPAddress(bytes).ToString());
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Collapses the textual variations of one address so "::1" and "0:0:0:0:0:0:0:1" compare equal.
|
||||||
|
/// </summary>
|
||||||
|
public static string NormalizeIpAddress(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value)) { return value; }
|
||||||
|
|
||||||
|
IPAddress parsed;
|
||||||
|
if (IPAddress.TryParse(value.Trim(), out parsed))
|
||||||
|
{
|
||||||
|
return parsed.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reports whether a candidate certificate carries every name the caller asked for. A certificate with
|
||||||
|
/// extra names still satisfies the request; one missing a requested name does not, and reusing it would
|
||||||
|
/// hand back a certificate that fails validation for the name that was added.
|
||||||
|
/// </summary>
|
||||||
|
public static bool CoversRequestedNames(X509Certificate2 candidate, IEnumerable<string> dnsNames, IEnumerable<string> ipAddresses, out string missingName)
|
||||||
|
{
|
||||||
|
missingName = null;
|
||||||
|
if (candidate == null) { return false; }
|
||||||
|
|
||||||
|
InfisicalCertificateSans present = Read(candidate);
|
||||||
|
|
||||||
|
if (dnsNames != null)
|
||||||
|
{
|
||||||
|
foreach (string dns in dnsNames)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(dns)) { continue; }
|
||||||
|
if (!present.DnsNames.Contains(dns.Trim()))
|
||||||
|
{
|
||||||
|
missingName = string.Concat("DNS:", dns.Trim());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ipAddresses != null)
|
||||||
|
{
|
||||||
|
foreach (string ip in ipAddresses)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(ip)) { continue; }
|
||||||
|
if (!present.IpAddresses.Contains(NormalizeIpAddress(ip)))
|
||||||
|
{
|
||||||
|
missingName = string.Concat("IP:", NormalizeIpAddress(ip));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,9 +8,36 @@ namespace PSInfisicalAPI.Pki
|
|||||||
{
|
{
|
||||||
public static X509Certificate2 FindMatch(StoreName storeName, StoreLocation storeLocation, string commonName, IEnumerable<string> candidateSerialNumbers)
|
public static X509Certificate2 FindMatch(StoreName storeName, StoreLocation storeLocation, string commonName, IEnumerable<string> candidateSerialNumbers)
|
||||||
{
|
{
|
||||||
|
string ignored;
|
||||||
|
return FindMatch(storeName, storeLocation, commonName, candidateSerialNumbers, null, null, out ignored);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds the longest-lived installed certificate for a subject that also carries every requested subject
|
||||||
|
/// alternative name. A certificate that predates a newly added SAN would fail validation for that name,
|
||||||
|
/// so it is not a reusable answer to the current request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="rejectedForMissingName">
|
||||||
|
/// The first name that disqualified an otherwise-matching certificate, so the caller can explain why it
|
||||||
|
/// is reissuing rather than reusing.
|
||||||
|
/// </param>
|
||||||
|
public static X509Certificate2 FindMatch(
|
||||||
|
StoreName storeName,
|
||||||
|
StoreLocation storeLocation,
|
||||||
|
string commonName,
|
||||||
|
IEnumerable<string> candidateSerialNumbers,
|
||||||
|
IEnumerable<string> requiredDnsNames,
|
||||||
|
IEnumerable<string> requiredIpAddresses,
|
||||||
|
out string rejectedForMissingName)
|
||||||
|
{
|
||||||
|
rejectedForMissingName = null;
|
||||||
HashSet<string> serialSet = NormalizeSerials(candidateSerialNumbers);
|
HashSet<string> serialSet = NormalizeSerials(candidateSerialNumbers);
|
||||||
string subjectFilter = !string.IsNullOrEmpty(commonName) ? string.Concat("CN=", commonName) : null;
|
string subjectFilter = !string.IsNullOrEmpty(commonName) ? string.Concat("CN=", commonName) : null;
|
||||||
|
|
||||||
|
List<string> dnsList = ToList(requiredDnsNames);
|
||||||
|
List<string> ipList = ToList(requiredIpAddresses);
|
||||||
|
bool requireSans = dnsList.Count > 0 || ipList.Count > 0;
|
||||||
|
|
||||||
X509Store store = new X509Store(storeName, storeLocation);
|
X509Store store = new X509Store(storeName, storeLocation);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -33,12 +60,24 @@ namespace PSInfisicalAPI.Pki
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (requireSans)
|
||||||
|
{
|
||||||
|
string missingName;
|
||||||
|
if (!InfisicalCertificateSanReader.CoversRequestedNames(candidate, dnsList, ipList, out missingName))
|
||||||
|
{
|
||||||
|
if (rejectedForMissingName == null) { rejectedForMissingName = missingName; }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (bestMatch == null || candidate.NotAfter > bestMatch.NotAfter)
|
if (bestMatch == null || candidate.NotAfter > bestMatch.NotAfter)
|
||||||
{
|
{
|
||||||
bestMatch = candidate;
|
bestMatch = candidate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only report a rejection when nothing else qualified; a covering certificate makes it irrelevant.
|
||||||
|
if (bestMatch != null) { rejectedForMissingName = null; }
|
||||||
return bestMatch;
|
return bestMatch;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -47,6 +86,18 @@ namespace PSInfisicalAPI.Pki
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<string> ToList(IEnumerable<string> values)
|
||||||
|
{
|
||||||
|
List<string> result = new List<string>();
|
||||||
|
if (values == null) { return result; }
|
||||||
|
foreach (string value in values)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(value)) { result.Add(value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
public static bool IsRenewable(X509Certificate2 cert, int renewalThresholdDays)
|
public static bool IsRenewable(X509Certificate2 cert, int renewalThresholdDays)
|
||||||
{
|
{
|
||||||
if (cert == null) { return true; }
|
if (cert == null) { return true; }
|
||||||
|
|||||||
@@ -209,6 +209,104 @@ namespace PSInfisicalAPI.Pki
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies metadata to a certificate, reconciling only the supplied keys.
|
||||||
|
/// <para>
|
||||||
|
/// PATCH /certificates/{id} replaces a certificate's metadata wholesale - the service deletes every
|
||||||
|
/// existing row before inserting what it was sent - so sending just the caller's keys would silently
|
||||||
|
/// discard everything else attached to the certificate. The current set is read first and the supplied
|
||||||
|
/// keys are merged over it, leaving untouched keys intact. Re-running with the same input is a no-op.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The certificate's full metadata after reconciliation.</returns>
|
||||||
|
public Dictionary<string, string> ReconcileCertificateMetadata(InfisicalConnection connection, string certificateId, IReadOnlyDictionary<string, string> desired)
|
||||||
|
{
|
||||||
|
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
|
||||||
|
if (string.IsNullOrEmpty(certificateId)) { throw new InfisicalConfigurationException("CertificateId is required."); }
|
||||||
|
|
||||||
|
Dictionary<string, string> current = GetCertificateMetadata(connection, certificateId);
|
||||||
|
if (desired == null || desired.Count == 0) { return current; }
|
||||||
|
|
||||||
|
Dictionary<string, string> merged = new Dictionary<string, string>(current, StringComparer.OrdinalIgnoreCase);
|
||||||
|
bool changed = false;
|
||||||
|
foreach (KeyValuePair<string, string> pair in desired)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(pair.Key)) { continue; }
|
||||||
|
string value = pair.Value ?? string.Empty;
|
||||||
|
|
||||||
|
string existing;
|
||||||
|
if (merged.TryGetValue(pair.Key, out existing) && string.Equals(existing, value, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
merged[pair.Key] = value;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed)
|
||||||
|
{
|
||||||
|
_logger.Verbose(Component, string.Concat("Certificate metadata already matches the requested keys for '", certificateId, "'; no update sent."));
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<InfisicalCertificateMetadataEntryDto> entries = new List<InfisicalCertificateMetadataEntryDto>();
|
||||||
|
foreach (KeyValuePair<string, string> pair in merged)
|
||||||
|
{
|
||||||
|
entries.Add(new InfisicalCertificateMetadataEntryDto { Key = pair.Key, Value = pair.Value ?? string.Empty });
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary<string, string> pathParameters = new Dictionary<string, string> { { "certificateId", certificateId } };
|
||||||
|
string body = _serializer.Serialize(new InfisicalUpdateCertificateMetadataRequestDto { Metadata = entries });
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.Information(Component, string.Concat("Attempting to update metadata on certificate '", certificateId, "'. Please Wait..."));
|
||||||
|
InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.UpdateCertificateMetadata, "UpdateCertificateMetadata", pathParameters, null, body);
|
||||||
|
InfisicalUpdateCertificateMetadataResponseDto dto = _serializer.Deserialize<InfisicalUpdateCertificateMetadataResponseDto>(response.Body);
|
||||||
|
response.Clear();
|
||||||
|
|
||||||
|
Dictionary<string, string> result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
if (dto != null && dto.Metadata != null)
|
||||||
|
{
|
||||||
|
foreach (InfisicalCertificateMetadataEntryDto entry in dto.Metadata)
|
||||||
|
{
|
||||||
|
if (entry == null || string.IsNullOrEmpty(entry.Key)) { continue; }
|
||||||
|
result[entry.Key] = entry.Value ?? string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Information(Component, "Infisical certificate metadata update was successful.");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
_logger.Error(Component, "Infisical certificate metadata update failed.");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads the metadata currently attached to a certificate, addressed by its identifier.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, string> GetCertificateMetadata(InfisicalConnection connection, string certificateId)
|
||||||
|
{
|
||||||
|
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
|
||||||
|
if (string.IsNullOrEmpty(certificateId)) { throw new InfisicalConfigurationException("CertificateId is required."); }
|
||||||
|
|
||||||
|
InfisicalCertificate certificate = RetrieveCertificate(connection, certificateId);
|
||||||
|
if (certificate == null || certificate.Metadata == null)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Dictionary<string, string>(certificate.Metadata, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
public InfisicalSignedCertificate SignCertificateBySubscriber(InfisicalConnection connection, string subscriberName, string projectId, string csrPem)
|
public InfisicalSignedCertificate SignCertificateBySubscriber(InfisicalConnection connection, string subscriberName, string projectId, string csrPem)
|
||||||
{
|
{
|
||||||
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
|
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
|
||||||
@@ -432,6 +530,16 @@ namespace PSInfisicalAPI.Pki
|
|||||||
}
|
}
|
||||||
|
|
||||||
public InfisicalCertificateProfile[] ListCertificateProfiles(InfisicalConnection connection, string projectId, int? limit, int? offset, bool? includeConfigs)
|
public InfisicalCertificateProfile[] ListCertificateProfiles(InfisicalConnection connection, string projectId, int? limit, int? offset, bool? includeConfigs)
|
||||||
|
{
|
||||||
|
return ListCertificateProfiles(connection, projectId, limit, offset, includeConfigs, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lists certificate profiles, optionally narrowed to one application or issuing CA. Applications are
|
||||||
|
/// how the UI groups profiles, so filtering by application is what makes a script read the way the
|
||||||
|
/// console does.
|
||||||
|
/// </summary>
|
||||||
|
public InfisicalCertificateProfile[] ListCertificateProfiles(InfisicalConnection connection, string projectId, int? limit, int? offset, bool? includeConfigs, string applicationId, string caId)
|
||||||
{
|
{
|
||||||
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
|
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
|
||||||
if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); }
|
if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); }
|
||||||
@@ -443,6 +551,8 @@ namespace PSInfisicalAPI.Pki
|
|||||||
if (limit.HasValue) { query.Add(new KeyValuePair<string, string>("limit", limit.Value.ToString(CultureInfo.InvariantCulture))); }
|
if (limit.HasValue) { query.Add(new KeyValuePair<string, string>("limit", limit.Value.ToString(CultureInfo.InvariantCulture))); }
|
||||||
if (offset.HasValue) { query.Add(new KeyValuePair<string, string>("offset", offset.Value.ToString(CultureInfo.InvariantCulture))); }
|
if (offset.HasValue) { query.Add(new KeyValuePair<string, string>("offset", offset.Value.ToString(CultureInfo.InvariantCulture))); }
|
||||||
if (includeConfigs.HasValue) { query.Add(new KeyValuePair<string, string>("includeConfigs", includeConfigs.Value ? "true" : "false")); }
|
if (includeConfigs.HasValue) { query.Add(new KeyValuePair<string, string>("includeConfigs", includeConfigs.Value ? "true" : "false")); }
|
||||||
|
if (!string.IsNullOrEmpty(applicationId)) { query.Add(new KeyValuePair<string, string>("applicationId", applicationId)); }
|
||||||
|
if (!string.IsNullOrEmpty(caId)) { query.Add(new KeyValuePair<string, string>("caId", caId)); }
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,443 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using PSInfisicalAPI.Connections;
|
||||||
|
using PSInfisicalAPI.Endpoints;
|
||||||
|
using PSInfisicalAPI.Errors;
|
||||||
|
using PSInfisicalAPI.Http;
|
||||||
|
using PSInfisicalAPI.Logging;
|
||||||
|
using PSInfisicalAPI.Models;
|
||||||
|
using PSInfisicalAPI.Serialization;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Pki
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create, update, and delete operations for the objects that make up a Certificate Manager environment:
|
||||||
|
/// certificate authorities, policies, profiles, applications, and PKI subscribers.
|
||||||
|
/// <para>
|
||||||
|
/// Kept apart from <see cref="InfisicalPkiClient"/>, which reads and issues. These are the operations that
|
||||||
|
/// change how a project is configured rather than what it has issued.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class InfisicalPkiManagementClient
|
||||||
|
{
|
||||||
|
private const string Component = "PkiManagementClient";
|
||||||
|
|
||||||
|
private readonly InfisicalApiInvoker _invoker;
|
||||||
|
private readonly IInfisicalLogger _logger;
|
||||||
|
private readonly JsonInfisicalSerializer _serializer;
|
||||||
|
|
||||||
|
public InfisicalPkiManagementClient(IInfisicalHttpClient httpClient, IInfisicalLogger logger)
|
||||||
|
{
|
||||||
|
if (httpClient == null) { throw new ArgumentNullException(nameof(httpClient)); }
|
||||||
|
_logger = logger ?? NullInfisicalLogger.Instance;
|
||||||
|
_invoker = new InfisicalApiInvoker(httpClient);
|
||||||
|
_serializer = new JsonInfisicalSerializer();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Certificate authority
|
||||||
|
|
||||||
|
public InfisicalCertificateAuthority CreateInternalCertificateAuthority(
|
||||||
|
InfisicalConnection connection,
|
||||||
|
string projectId,
|
||||||
|
string name,
|
||||||
|
string type,
|
||||||
|
string commonName,
|
||||||
|
string organization,
|
||||||
|
string organizationalUnit,
|
||||||
|
string country,
|
||||||
|
string state,
|
||||||
|
string locality,
|
||||||
|
string keyAlgorithm,
|
||||||
|
string friendlyName,
|
||||||
|
string notBefore,
|
||||||
|
string notAfter,
|
||||||
|
int? maxPathLength)
|
||||||
|
{
|
||||||
|
Require(connection, projectId);
|
||||||
|
if (string.IsNullOrEmpty(name)) { throw new InfisicalConfigurationException("Name is required."); }
|
||||||
|
if (string.IsNullOrEmpty(type)) { throw new InfisicalConfigurationException("Type is required."); }
|
||||||
|
|
||||||
|
bool isRoot = string.Equals(type, "root", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
InfisicalCreateInternalCaRequestDto request = new InfisicalCreateInternalCaRequestDto
|
||||||
|
{
|
||||||
|
ProjectId = projectId,
|
||||||
|
Name = name,
|
||||||
|
Status = "active",
|
||||||
|
Configuration = new InfisicalCreateInternalCaConfigurationDto
|
||||||
|
{
|
||||||
|
Type = type,
|
||||||
|
CommonName = commonName ?? string.Empty,
|
||||||
|
Organization = organization ?? string.Empty,
|
||||||
|
OrganizationalUnit = organizationalUnit ?? string.Empty,
|
||||||
|
Country = country ?? string.Empty,
|
||||||
|
State = state ?? string.Empty,
|
||||||
|
Locality = locality ?? string.Empty,
|
||||||
|
KeyAlgorithm = keyAlgorithm,
|
||||||
|
FriendlyName = friendlyName,
|
||||||
|
// Infisical self-signs on creation only for a root, and only when given an expiry. A
|
||||||
|
// subordinate is created pending a certificate and signed separately.
|
||||||
|
NotBefore = isRoot ? notBefore : null,
|
||||||
|
NotAfter = isRoot ? notAfter : null,
|
||||||
|
MaxPathLength = isRoot ? maxPathLength : null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Execute(
|
||||||
|
connection,
|
||||||
|
InfisicalEndpointNames.CreateInternalCertificateAuthority,
|
||||||
|
null,
|
||||||
|
_serializer.Serialize(request),
|
||||||
|
string.Concat("create ", type, " certificate authority '", name, "'"),
|
||||||
|
body =>
|
||||||
|
{
|
||||||
|
InfisicalInternalCaSingleResponseDto dto = _serializer.Deserialize<InfisicalInternalCaSingleResponseDto>(body);
|
||||||
|
InfisicalInternalCaResponseDto inner = dto != null ? (dto.CertificateAuthority ?? dto.Ca) : null;
|
||||||
|
return InfisicalCaMapper.Map(inner, projectId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public InfisicalCertificateAuthority UpdateInternalCertificateAuthority(InfisicalConnection connection, string projectId, string caId, string name, string status)
|
||||||
|
{
|
||||||
|
Require(connection, projectId);
|
||||||
|
if (string.IsNullOrEmpty(caId)) { throw new InfisicalConfigurationException("CaId is required."); }
|
||||||
|
if (string.IsNullOrEmpty(name) && string.IsNullOrEmpty(status))
|
||||||
|
{
|
||||||
|
throw new InfisicalConfigurationException("Supply -Name or -Status; there is nothing else to update on an internal certificate authority.");
|
||||||
|
}
|
||||||
|
|
||||||
|
InfisicalUpdateInternalCaRequestDto request = new InfisicalUpdateInternalCaRequestDto { ProjectId = projectId, Name = name, Status = status };
|
||||||
|
|
||||||
|
return Execute(
|
||||||
|
connection,
|
||||||
|
InfisicalEndpointNames.UpdateInternalCertificateAuthority,
|
||||||
|
new Dictionary<string, string> { { "caId", caId } },
|
||||||
|
_serializer.Serialize(request),
|
||||||
|
string.Concat("update certificate authority '", caId, "'"),
|
||||||
|
body =>
|
||||||
|
{
|
||||||
|
InfisicalInternalCaSingleResponseDto dto = _serializer.Deserialize<InfisicalInternalCaSingleResponseDto>(body);
|
||||||
|
InfisicalInternalCaResponseDto inner = dto != null ? (dto.CertificateAuthority ?? dto.Ca) : null;
|
||||||
|
return InfisicalCaMapper.Map(inner, projectId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteInternalCertificateAuthority(InfisicalConnection connection, string projectId, string caId)
|
||||||
|
{
|
||||||
|
Require(connection, projectId);
|
||||||
|
if (string.IsNullOrEmpty(caId)) { throw new InfisicalConfigurationException("CaId is required."); }
|
||||||
|
|
||||||
|
Execute<object>(
|
||||||
|
connection,
|
||||||
|
InfisicalEndpointNames.DeleteInternalCertificateAuthority,
|
||||||
|
new Dictionary<string, string> { { "caId", caId } },
|
||||||
|
null,
|
||||||
|
string.Concat("delete certificate authority '", caId, "'"),
|
||||||
|
body => null,
|
||||||
|
BuildProjectQuery(projectId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Signs a newly created subordinate with its parent and imports the result, taking it from
|
||||||
|
/// pending-certificate to active. Infisical creates a subordinate without a certificate and exposes no
|
||||||
|
/// single call that does this.
|
||||||
|
/// </summary>
|
||||||
|
public void CompleteSubordinateCertificateAuthority(InfisicalConnection connection, string projectId, string subordinateCaId, string parentCaId, string notAfter, int maxPathLength)
|
||||||
|
{
|
||||||
|
Require(connection, projectId);
|
||||||
|
if (string.IsNullOrEmpty(subordinateCaId)) { throw new InfisicalConfigurationException("CaId is required."); }
|
||||||
|
if (string.IsNullOrEmpty(parentCaId)) { throw new InfisicalConfigurationException("ParentCaId is required."); }
|
||||||
|
if (string.IsNullOrEmpty(notAfter)) { throw new InfisicalConfigurationException("NotAfter is required to sign a subordinate certificate authority."); }
|
||||||
|
|
||||||
|
string csr = Execute(
|
||||||
|
connection,
|
||||||
|
InfisicalEndpointNames.GetCertificateAuthorityCsr,
|
||||||
|
new Dictionary<string, string> { { "caId", subordinateCaId } },
|
||||||
|
null,
|
||||||
|
string.Concat("read the certificate signing request for '", subordinateCaId, "'"),
|
||||||
|
body =>
|
||||||
|
{
|
||||||
|
InfisicalCaCsrResponseDto dto = _serializer.Deserialize<InfisicalCaCsrResponseDto>(body);
|
||||||
|
return dto != null ? dto.Csr : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(csr))
|
||||||
|
{
|
||||||
|
throw new InfisicalApiException(string.Concat("Certificate authority '", subordinateCaId, "' returned no certificate signing request to sign."));
|
||||||
|
}
|
||||||
|
|
||||||
|
InfisicalSignIntermediateRequestDto signRequest = new InfisicalSignIntermediateRequestDto
|
||||||
|
{
|
||||||
|
Csr = csr,
|
||||||
|
NotAfter = notAfter,
|
||||||
|
MaxPathLength = maxPathLength
|
||||||
|
};
|
||||||
|
|
||||||
|
InfisicalSignIntermediateResponseDto signed = Execute(
|
||||||
|
connection,
|
||||||
|
InfisicalEndpointNames.SignIntermediateCertificateAuthority,
|
||||||
|
new Dictionary<string, string> { { "caId", parentCaId } },
|
||||||
|
_serializer.Serialize(signRequest),
|
||||||
|
string.Concat("sign '", subordinateCaId, "' with '", parentCaId, "'"),
|
||||||
|
body => _serializer.Deserialize<InfisicalSignIntermediateResponseDto>(body));
|
||||||
|
|
||||||
|
if (signed == null || string.IsNullOrEmpty(signed.Certificate))
|
||||||
|
{
|
||||||
|
throw new InfisicalApiException("Signing the subordinate certificate authority returned no certificate.");
|
||||||
|
}
|
||||||
|
|
||||||
|
InfisicalImportCaCertificateRequestDto importRequest = new InfisicalImportCaCertificateRequestDto
|
||||||
|
{
|
||||||
|
Certificate = signed.Certificate,
|
||||||
|
CertificateChain = signed.CertificateChain
|
||||||
|
};
|
||||||
|
|
||||||
|
Execute<object>(
|
||||||
|
connection,
|
||||||
|
InfisicalEndpointNames.ImportCertificateAuthorityCertificate,
|
||||||
|
new Dictionary<string, string> { { "caId", subordinateCaId } },
|
||||||
|
_serializer.Serialize(importRequest),
|
||||||
|
string.Concat("import the signed certificate onto '", subordinateCaId, "'"),
|
||||||
|
body => null);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Certificate policy
|
||||||
|
|
||||||
|
public InfisicalCertificatePolicy WriteCertificatePolicy(InfisicalConnection connection, string projectId, string policyId, InfisicalCertificatePolicyWriteRequestDto request)
|
||||||
|
{
|
||||||
|
Require(connection, projectId);
|
||||||
|
if (request == null) { throw new ArgumentNullException(nameof(request)); }
|
||||||
|
|
||||||
|
bool creating = string.IsNullOrEmpty(policyId);
|
||||||
|
request.ProjectId = projectId;
|
||||||
|
|
||||||
|
return Execute(
|
||||||
|
connection,
|
||||||
|
creating ? InfisicalEndpointNames.CreateCertificatePolicy : InfisicalEndpointNames.UpdateCertificatePolicy,
|
||||||
|
creating ? null : new Dictionary<string, string> { { "policyId", policyId } },
|
||||||
|
_serializer.Serialize(request),
|
||||||
|
creating ? string.Concat("create certificate policy '", request.Name, "'") : string.Concat("update certificate policy '", policyId, "'"),
|
||||||
|
body =>
|
||||||
|
{
|
||||||
|
InfisicalCertificatePolicySingleResponseDto dto = _serializer.Deserialize<InfisicalCertificatePolicySingleResponseDto>(body);
|
||||||
|
return InfisicalCertificatePolicyMapper.Map(dto != null ? dto.CertificatePolicy : null, projectId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteCertificatePolicy(InfisicalConnection connection, string projectId, string policyId)
|
||||||
|
{
|
||||||
|
Require(connection, projectId);
|
||||||
|
if (string.IsNullOrEmpty(policyId)) { throw new InfisicalConfigurationException("PolicyId is required."); }
|
||||||
|
|
||||||
|
Execute<object>(
|
||||||
|
connection,
|
||||||
|
InfisicalEndpointNames.DeleteCertificatePolicy,
|
||||||
|
new Dictionary<string, string> { { "policyId", policyId } },
|
||||||
|
null,
|
||||||
|
string.Concat("delete certificate policy '", policyId, "'"),
|
||||||
|
body => null,
|
||||||
|
BuildProjectQuery(projectId));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Certificate profile
|
||||||
|
|
||||||
|
public InfisicalCertificateProfile WriteCertificateProfile(InfisicalConnection connection, string projectId, string profileId, InfisicalCertificateProfileWriteRequestDto request)
|
||||||
|
{
|
||||||
|
Require(connection, projectId);
|
||||||
|
if (request == null) { throw new ArgumentNullException(nameof(request)); }
|
||||||
|
|
||||||
|
bool creating = string.IsNullOrEmpty(profileId);
|
||||||
|
request.ProjectId = projectId;
|
||||||
|
|
||||||
|
return Execute(
|
||||||
|
connection,
|
||||||
|
creating ? InfisicalEndpointNames.CreateCertificateProfile : InfisicalEndpointNames.UpdateCertificateProfile,
|
||||||
|
creating ? null : new Dictionary<string, string> { { "profileId", profileId } },
|
||||||
|
_serializer.Serialize(request),
|
||||||
|
creating ? string.Concat("create certificate profile '", request.Slug, "'") : string.Concat("update certificate profile '", profileId, "'"),
|
||||||
|
body =>
|
||||||
|
{
|
||||||
|
InfisicalCertificateProfileSingleResponseDto dto = _serializer.Deserialize<InfisicalCertificateProfileSingleResponseDto>(body);
|
||||||
|
return InfisicalCertificateProfileMapper.Map(dto != null ? dto.CertificateProfile : null, projectId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteCertificateProfile(InfisicalConnection connection, string projectId, string profileId)
|
||||||
|
{
|
||||||
|
Require(connection, projectId);
|
||||||
|
if (string.IsNullOrEmpty(profileId)) { throw new InfisicalConfigurationException("ProfileId is required."); }
|
||||||
|
|
||||||
|
Execute<object>(
|
||||||
|
connection,
|
||||||
|
InfisicalEndpointNames.DeleteCertificateProfile,
|
||||||
|
new Dictionary<string, string> { { "profileId", profileId } },
|
||||||
|
null,
|
||||||
|
string.Concat("delete certificate profile '", profileId, "'"),
|
||||||
|
body => null,
|
||||||
|
BuildProjectQuery(projectId));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Certificate application
|
||||||
|
|
||||||
|
public InfisicalCertificateApplication WriteCertificateApplication(InfisicalConnection connection, string projectId, string applicationId, InfisicalCertificateApplicationWriteRequestDto request)
|
||||||
|
{
|
||||||
|
Require(connection, projectId);
|
||||||
|
if (request == null) { throw new ArgumentNullException(nameof(request)); }
|
||||||
|
|
||||||
|
bool creating = string.IsNullOrEmpty(applicationId);
|
||||||
|
request.ProjectId = projectId;
|
||||||
|
|
||||||
|
return Execute(
|
||||||
|
connection,
|
||||||
|
creating ? InfisicalEndpointNames.CreateCertificateApplication : InfisicalEndpointNames.UpdateCertificateApplication,
|
||||||
|
creating ? null : new Dictionary<string, string> { { "applicationId", applicationId } },
|
||||||
|
_serializer.Serialize(request),
|
||||||
|
creating ? string.Concat("create certificate application '", request.Name, "'") : string.Concat("update certificate application '", applicationId, "'"),
|
||||||
|
body =>
|
||||||
|
{
|
||||||
|
InfisicalCertificateApplicationSingleResponseDto dto = _serializer.Deserialize<InfisicalCertificateApplicationSingleResponseDto>(body);
|
||||||
|
return InfisicalCertificateApplicationMapper.Map(dto != null ? dto.Application : null, projectId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteCertificateApplication(InfisicalConnection connection, string projectId, string applicationId)
|
||||||
|
{
|
||||||
|
Require(connection, projectId);
|
||||||
|
if (string.IsNullOrEmpty(applicationId)) { throw new InfisicalConfigurationException("ApplicationId is required."); }
|
||||||
|
|
||||||
|
Execute<object>(
|
||||||
|
connection,
|
||||||
|
InfisicalEndpointNames.DeleteCertificateApplication,
|
||||||
|
new Dictionary<string, string> { { "applicationId", applicationId } },
|
||||||
|
null,
|
||||||
|
string.Concat("delete certificate application '", applicationId, "'"),
|
||||||
|
body => null,
|
||||||
|
BuildProjectQuery(projectId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddCertificateApplicationProfiles(InfisicalConnection connection, string projectId, string applicationId, IEnumerable<string> profileIds)
|
||||||
|
{
|
||||||
|
Require(connection, projectId);
|
||||||
|
if (string.IsNullOrEmpty(applicationId)) { throw new InfisicalConfigurationException("ApplicationId is required."); }
|
||||||
|
|
||||||
|
List<string> ids = new List<string>();
|
||||||
|
if (profileIds != null) { foreach (string id in profileIds) { if (!string.IsNullOrEmpty(id)) { ids.Add(id); } } }
|
||||||
|
if (ids.Count == 0) { throw new InfisicalConfigurationException("At least one ProfileId is required."); }
|
||||||
|
|
||||||
|
InfisicalCertificateApplicationWriteRequestDto request = new InfisicalCertificateApplicationWriteRequestDto { ProjectId = projectId, ProfileIds = ids };
|
||||||
|
|
||||||
|
Execute<object>(
|
||||||
|
connection,
|
||||||
|
InfisicalEndpointNames.AddCertificateApplicationProfiles,
|
||||||
|
new Dictionary<string, string> { { "applicationId", applicationId } },
|
||||||
|
_serializer.Serialize(request),
|
||||||
|
string.Concat("attach ", ids.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " profile(s) to application '", applicationId, "'"),
|
||||||
|
body => null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveCertificateApplicationProfile(InfisicalConnection connection, string projectId, string applicationId, string profileId)
|
||||||
|
{
|
||||||
|
Require(connection, projectId);
|
||||||
|
if (string.IsNullOrEmpty(applicationId)) { throw new InfisicalConfigurationException("ApplicationId is required."); }
|
||||||
|
if (string.IsNullOrEmpty(profileId)) { throw new InfisicalConfigurationException("ProfileId is required."); }
|
||||||
|
|
||||||
|
Execute<object>(
|
||||||
|
connection,
|
||||||
|
InfisicalEndpointNames.RemoveCertificateApplicationProfile,
|
||||||
|
new Dictionary<string, string> { { "applicationId", applicationId }, { "profileId", profileId } },
|
||||||
|
null,
|
||||||
|
string.Concat("detach profile '", profileId, "' from application '", applicationId, "'"),
|
||||||
|
body => null,
|
||||||
|
BuildProjectQuery(projectId));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region PKI subscriber
|
||||||
|
|
||||||
|
public InfisicalPkiSubscriber WritePkiSubscriber(InfisicalConnection connection, string projectId, string subscriberName, InfisicalPkiSubscriberWriteRequestDto request)
|
||||||
|
{
|
||||||
|
Require(connection, projectId);
|
||||||
|
if (request == null) { throw new ArgumentNullException(nameof(request)); }
|
||||||
|
|
||||||
|
bool creating = string.IsNullOrEmpty(subscriberName);
|
||||||
|
request.ProjectId = projectId;
|
||||||
|
|
||||||
|
return Execute(
|
||||||
|
connection,
|
||||||
|
creating ? InfisicalEndpointNames.CreatePkiSubscriber : InfisicalEndpointNames.UpdatePkiSubscriber,
|
||||||
|
creating ? null : new Dictionary<string, string> { { "subscriberName", subscriberName } },
|
||||||
|
_serializer.Serialize(request),
|
||||||
|
creating ? string.Concat("create PKI subscriber '", request.Name, "'") : string.Concat("update PKI subscriber '", subscriberName, "'"),
|
||||||
|
// The subscriber routes return the record unwrapped, matching how GetPkiSubscriber reads it.
|
||||||
|
body => InfisicalPkiSubscriberMapper.Map(_serializer.Deserialize<InfisicalPkiSubscriberResponseDto>(body), projectId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeletePkiSubscriber(InfisicalConnection connection, string projectId, string subscriberName)
|
||||||
|
{
|
||||||
|
Require(connection, projectId);
|
||||||
|
if (string.IsNullOrEmpty(subscriberName)) { throw new InfisicalConfigurationException("Name is required."); }
|
||||||
|
|
||||||
|
Execute<object>(
|
||||||
|
connection,
|
||||||
|
InfisicalEndpointNames.DeletePkiSubscriber,
|
||||||
|
new Dictionary<string, string> { { "subscriberName", subscriberName } },
|
||||||
|
null,
|
||||||
|
string.Concat("delete PKI subscriber '", subscriberName, "'"),
|
||||||
|
body => null,
|
||||||
|
BuildProjectQuery(projectId));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Plumbing
|
||||||
|
|
||||||
|
private static void Require(InfisicalConnection connection, string projectId)
|
||||||
|
{
|
||||||
|
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
|
||||||
|
if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<KeyValuePair<string, string>> BuildProjectQuery(string projectId)
|
||||||
|
{
|
||||||
|
// DELETE carries no body, so the project has to travel on the query string for Infisical's
|
||||||
|
// project-injection middleware to see it.
|
||||||
|
return new List<KeyValuePair<string, string>> { new KeyValuePair<string, string>("projectId", projectId) };
|
||||||
|
}
|
||||||
|
|
||||||
|
private T Execute<T>(
|
||||||
|
InfisicalConnection connection,
|
||||||
|
string endpointName,
|
||||||
|
Dictionary<string, string> pathParameters,
|
||||||
|
string body,
|
||||||
|
string description,
|
||||||
|
Func<string, T> project,
|
||||||
|
List<KeyValuePair<string, string>> query = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.Information(Component, string.Concat("Attempting to ", description, ". Please Wait..."));
|
||||||
|
InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, endpointName, endpointName, pathParameters, query, body);
|
||||||
|
string payload = response.Body;
|
||||||
|
response.Clear();
|
||||||
|
|
||||||
|
T result = project(payload);
|
||||||
|
_logger.Information(Component, string.Concat("Infisical operation succeeded: ", description, "."));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
_logger.Error(Component, string.Concat("Infisical operation failed: ", description, "."));
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Pki
|
||||||
|
{
|
||||||
|
#region Certificate authority
|
||||||
|
|
||||||
|
internal sealed class InfisicalCreateInternalCaRequestDto
|
||||||
|
{
|
||||||
|
[JsonProperty("projectId", NullValueHandling = NullValueHandling.Ignore)] public string ProjectId { get; set; }
|
||||||
|
[JsonProperty("name")] public string Name { get; set; }
|
||||||
|
[JsonProperty("status")] public string Status { get; set; }
|
||||||
|
[JsonProperty("configuration")] public InfisicalCreateInternalCaConfigurationDto Configuration { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class InfisicalCreateInternalCaConfigurationDto
|
||||||
|
{
|
||||||
|
[JsonProperty("type")] public string Type { get; set; }
|
||||||
|
[JsonProperty("commonName")] public string CommonName { get; set; }
|
||||||
|
[JsonProperty("organization")] public string Organization { get; set; }
|
||||||
|
[JsonProperty("ou")] public string OrganizationalUnit { get; set; }
|
||||||
|
[JsonProperty("country")] public string Country { get; set; }
|
||||||
|
[JsonProperty("province")] public string State { get; set; }
|
||||||
|
[JsonProperty("locality")] public string Locality { get; set; }
|
||||||
|
[JsonProperty("keyAlgorithm")] public string KeyAlgorithm { get; set; }
|
||||||
|
[JsonProperty("friendlyName", NullValueHandling = NullValueHandling.Ignore)] public string FriendlyName { get; set; }
|
||||||
|
[JsonProperty("notBefore", NullValueHandling = NullValueHandling.Ignore)] public string NotBefore { get; set; }
|
||||||
|
[JsonProperty("notAfter", NullValueHandling = NullValueHandling.Ignore)] public string NotAfter { get; set; }
|
||||||
|
[JsonProperty("maxPathLength", NullValueHandling = NullValueHandling.Ignore)] public int? MaxPathLength { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class InfisicalUpdateInternalCaRequestDto
|
||||||
|
{
|
||||||
|
[JsonProperty("projectId", NullValueHandling = NullValueHandling.Ignore)] public string ProjectId { get; set; }
|
||||||
|
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; set; }
|
||||||
|
[JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] public string Status { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class InfisicalCaCsrResponseDto
|
||||||
|
{
|
||||||
|
[JsonProperty("csr")] public string Csr { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class InfisicalSignIntermediateRequestDto
|
||||||
|
{
|
||||||
|
[JsonProperty("csr")] public string Csr { get; set; }
|
||||||
|
[JsonProperty("notBefore", NullValueHandling = NullValueHandling.Ignore)] public string NotBefore { get; set; }
|
||||||
|
[JsonProperty("notAfter")] public string NotAfter { get; set; }
|
||||||
|
[JsonProperty("maxPathLength")] public int MaxPathLength { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class InfisicalSignIntermediateResponseDto
|
||||||
|
{
|
||||||
|
[JsonProperty("certificate")] public string Certificate { get; set; }
|
||||||
|
[JsonProperty("certificateChain")] public string CertificateChain { get; set; }
|
||||||
|
[JsonProperty("issuingCaCertificate")] public string IssuingCaCertificate { get; set; }
|
||||||
|
[JsonProperty("serialNumber")] public string SerialNumber { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class InfisicalImportCaCertificateRequestDto
|
||||||
|
{
|
||||||
|
[JsonProperty("certificate")] public string Certificate { get; set; }
|
||||||
|
[JsonProperty("certificateChain")] public string CertificateChain { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Certificate policy
|
||||||
|
|
||||||
|
internal sealed class InfisicalCertificatePolicyWriteRequestDto
|
||||||
|
{
|
||||||
|
[JsonProperty("projectId", NullValueHandling = NullValueHandling.Ignore)] public string ProjectId { get; set; }
|
||||||
|
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; set; }
|
||||||
|
[JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] public string Description { get; set; }
|
||||||
|
[JsonProperty("subject", NullValueHandling = NullValueHandling.Ignore)] public List<Dictionary<string, object>> Subject { get; set; }
|
||||||
|
[JsonProperty("sans", NullValueHandling = NullValueHandling.Ignore)] public List<Dictionary<string, object>> Sans { get; set; }
|
||||||
|
[JsonProperty("keyUsages", NullValueHandling = NullValueHandling.Ignore)] public Dictionary<string, object> KeyUsages { get; set; }
|
||||||
|
[JsonProperty("extendedKeyUsages", NullValueHandling = NullValueHandling.Ignore)] public Dictionary<string, object> ExtendedKeyUsages { get; set; }
|
||||||
|
[JsonProperty("algorithms", NullValueHandling = NullValueHandling.Ignore)] public Dictionary<string, object> Algorithms { get; set; }
|
||||||
|
[JsonProperty("validity", NullValueHandling = NullValueHandling.Ignore)] public Dictionary<string, object> Validity { get; set; }
|
||||||
|
[JsonProperty("basicConstraints", NullValueHandling = NullValueHandling.Ignore)] public Dictionary<string, object> BasicConstraints { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class InfisicalCertificatePolicySingleResponseDto
|
||||||
|
{
|
||||||
|
[JsonProperty("certificatePolicy")] public InfisicalCertificatePolicyResponseDto CertificatePolicy { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Certificate profile
|
||||||
|
|
||||||
|
internal sealed class InfisicalCertificateProfileWriteRequestDto
|
||||||
|
{
|
||||||
|
[JsonProperty("projectId", NullValueHandling = NullValueHandling.Ignore)] public string ProjectId { get; set; }
|
||||||
|
[JsonProperty("slug", NullValueHandling = NullValueHandling.Ignore)] public string Slug { get; set; }
|
||||||
|
[JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] public string Description { get; set; }
|
||||||
|
[JsonProperty("caId", NullValueHandling = NullValueHandling.Ignore)] public string CaId { get; set; }
|
||||||
|
[JsonProperty("certificatePolicyId", NullValueHandling = NullValueHandling.Ignore)] public string CertificatePolicyId { get; set; }
|
||||||
|
[JsonProperty("enrollmentType", NullValueHandling = NullValueHandling.Ignore)] public string EnrollmentType { get; set; }
|
||||||
|
[JsonProperty("issuerType", NullValueHandling = NullValueHandling.Ignore)] public string IssuerType { get; set; }
|
||||||
|
[JsonProperty("apiConfig", NullValueHandling = NullValueHandling.Ignore)] public Dictionary<string, object> ApiConfig { get; set; }
|
||||||
|
[JsonProperty("scepConfig", NullValueHandling = NullValueHandling.Ignore)] public Dictionary<string, object> ScepConfig { get; set; }
|
||||||
|
[JsonProperty("estConfig", NullValueHandling = NullValueHandling.Ignore)] public Dictionary<string, object> EstConfig { get; set; }
|
||||||
|
[JsonProperty("acmeConfig", NullValueHandling = NullValueHandling.Ignore)] public Dictionary<string, object> AcmeConfig { get; set; }
|
||||||
|
[JsonProperty("defaults", NullValueHandling = NullValueHandling.Ignore)] public Dictionary<string, object> Defaults { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class InfisicalCertificateProfileSingleResponseDto
|
||||||
|
{
|
||||||
|
[JsonProperty("certificateProfile")] public InfisicalCertificateProfileResponseDto CertificateProfile { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Certificate application
|
||||||
|
|
||||||
|
internal sealed class InfisicalCertificateApplicationWriteRequestDto
|
||||||
|
{
|
||||||
|
[JsonProperty("projectId", NullValueHandling = NullValueHandling.Ignore)] public string ProjectId { get; set; }
|
||||||
|
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; set; }
|
||||||
|
[JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] public string Description { get; set; }
|
||||||
|
[JsonProperty("profileIds", NullValueHandling = NullValueHandling.Ignore)] public List<string> ProfileIds { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class InfisicalCertificateApplicationSingleResponseDto
|
||||||
|
{
|
||||||
|
[JsonProperty("application")] public InfisicalCertificateApplicationResponseDto Application { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region PKI subscriber
|
||||||
|
|
||||||
|
internal sealed class InfisicalPkiSubscriberWriteRequestDto
|
||||||
|
{
|
||||||
|
[JsonProperty("projectId", NullValueHandling = NullValueHandling.Ignore)] public string ProjectId { get; set; }
|
||||||
|
[JsonProperty("caId", NullValueHandling = NullValueHandling.Ignore)] public string CaId { get; set; }
|
||||||
|
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; set; }
|
||||||
|
[JsonProperty("commonName", NullValueHandling = NullValueHandling.Ignore)] public string CommonName { get; set; }
|
||||||
|
[JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] public string Status { get; set; }
|
||||||
|
[JsonProperty("ttl", NullValueHandling = NullValueHandling.Ignore)] public string Ttl { get; set; }
|
||||||
|
[JsonProperty("subjectAlternativeNames", NullValueHandling = NullValueHandling.Ignore)] public List<string> SubjectAlternativeNames { get; set; }
|
||||||
|
[JsonProperty("keyUsages", NullValueHandling = NullValueHandling.Ignore)] public List<string> KeyUsages { get; set; }
|
||||||
|
[JsonProperty("extendedKeyUsages", NullValueHandling = NullValueHandling.Ignore)] public List<string> ExtendedKeyUsages { get; set; }
|
||||||
|
[JsonProperty("enableAutoRenewal", NullValueHandling = NullValueHandling.Ignore)] public bool? EnableAutoRenewal { get; set; }
|
||||||
|
[JsonProperty("autoRenewalPeriodInDays", NullValueHandling = NullValueHandling.Ignore)] public int? AutoRenewalPeriodInDays { get; set; }
|
||||||
|
[JsonProperty("properties", NullValueHandling = NullValueHandling.Ignore)] public Dictionary<string, object> Properties { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -45,7 +45,7 @@ namespace PSInfisicalAPI.Projects
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.Information(Component, "Attempting to list Infisical projects. Please Wait...");
|
_logger.Information(Component, "Attempting to list Infisical projects. Please Wait...");
|
||||||
InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.ListProjects, "ListProjects", null, queryParameters, null);
|
InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.ListProjects, "ListProjects", null, queryParameters, null);
|
||||||
InfisicalProjectListResponseDto dto = _serializer.Deserialize<InfisicalProjectListResponseDto>(response.Body);
|
InfisicalProjectListResponseDto dto = _serializer.Deserialize<InfisicalProjectListResponseDto>(response.Body);
|
||||||
response.Clear();
|
response.Clear();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user