Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e1a0547e73 | |||
| 22c2db4a96 | |||
| b27fe6f002 | |||
| c75cc145cb | |||
| c40160f789 | |||
| 3015138484 | |||
| 74d22a941c | |||
| f2a1492b66 | |||
| 9f6e81607d | |||
| 139d1f3a05 | |||
| 62131e7501 | |||
| f1a6db14c7 | |||
| e7674af161 | |||
| a1e1d22e72 | |||
| 276958e3a8 | |||
| 633f40c1fa | |||
| 83b79b9109 | |||
| 93b0cc1924 | |||
| d47a5af6b3 | |||
| e8113ec073 | |||
| d5c9eec3fb | |||
| 67cf0abac2 | |||
| 09af8939c7 | |||
| dadba2f4c8 | |||
| c114bd3b9c | |||
| 883322cadf | |||
| f65124fd99 | |||
| f56fd15b38 | |||
| 8b6823344f | |||
| f62b3e90b1 | |||
| 4c7ce00504 | |||
| 14c8c4f384 | |||
| 5e5145fdc7 | |||
| 6318d06362 | |||
| 98f5d7704e | |||
| 94bd15a8f8 | |||
| daf1cdce65 | |||
| 80871b73af | |||
| 9a2f81fc02 | |||
| 97193d46f2 | |||
| b5575222eb | |||
| f82312433c | |||
| 1aa51b8cbf | |||
| 77cb03ec98 | |||
| d0395b54ac | |||
| 15fadd01a4 | |||
| 207e7429e4 | |||
| d3c7b83da7 | |||
| 318db70480 |
@@ -129,6 +129,7 @@ jobs:
|
||||
COMMIT_SHA: ${{ github.sha }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_BODY: ${{ github.event.pull_request.body }}
|
||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||
SERVER_URL: ${{ github.server_url }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
@@ -175,7 +176,17 @@ jobs:
|
||||
Write-Host " CHANGELOG section length: $($changelogSection.Length) chars"
|
||||
|
||||
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
|
||||
[void]$sb.AppendLine("**PSInfisicalAPI $($env:VERSION)**")
|
||||
[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("| Workflow run | [$($env:RUN_ID)]($runUrl) |")
|
||||
[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('## Install')
|
||||
[void]$sb.AppendLine('```powershell')
|
||||
@@ -319,9 +355,65 @@ jobs:
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$moduleDir = Join-Path $PWD 'Module/PSInfisicalAPI'
|
||||
Write-Host "Publishing module from: $moduleDir"
|
||||
Publish-PSResource `
|
||||
-Path $moduleDir `
|
||||
-Repository PSGallery `
|
||||
-ApiKey $env:PSGALLERY_API_KEY `
|
||||
-Verbose
|
||||
$manifest = Test-ModuleManifest -Path (Join-Path $moduleDir 'PSInfisicalAPI.psd1')
|
||||
$version = $manifest.Version.ToString()
|
||||
|
||||
# NuGet strips leading zeros from each segment, so the manifest's 2026.07.30.2309 is listed on the
|
||||
# gallery as 2026.7.30.2309. Compare on the normalized form or every lookup misses.
|
||||
function Get-NormalizedVersion {
|
||||
param([string]$Value)
|
||||
$parts = $Value -split '\.'
|
||||
$normalized = foreach ($part in $parts) {
|
||||
$number = 0
|
||||
if ([int]::TryParse($part, [ref]$number)) { $number.ToString([System.Globalization.CultureInfo]::InvariantCulture) } else { $part }
|
||||
}
|
||||
return ($normalized -join '.')
|
||||
}
|
||||
|
||||
function Test-PublishedVersion {
|
||||
param([string]$Normalized)
|
||||
$uri = "https://www.powershellgallery.com/api/v2/FindPackagesById()?id='PSInfisicalAPI'&`$select=Version"
|
||||
try {
|
||||
$feed = Invoke-RestMethod -Uri $uri -TimeoutSec 120
|
||||
} catch {
|
||||
Write-Host "==> Could not query the gallery for existing versions: $($_.Exception.Message)"
|
||||
return $false
|
||||
}
|
||||
|
||||
foreach ($entry in @($feed)) {
|
||||
$candidate = $entry.properties.Version
|
||||
if ([string]::IsNullOrWhiteSpace($candidate)) { continue }
|
||||
if ((Get-NormalizedVersion -Value $candidate) -eq $Normalized) { return $true }
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
$normalizedVersion = Get-NormalizedVersion -Value $version
|
||||
Write-Host "==> Module version : $version (gallery form: $normalizedVersion)"
|
||||
Write-Host "==> Publishing from: $moduleDir"
|
||||
|
||||
if (Test-PublishedVersion -Normalized $normalizedVersion) {
|
||||
Write-Host "==> Version $normalizedVersion is already on the PowerShell Gallery; nothing to publish."
|
||||
exit 0
|
||||
}
|
||||
|
||||
try {
|
||||
Publish-PSResource `
|
||||
-Path $moduleDir `
|
||||
-Repository PSGallery `
|
||||
-ApiKey $env:PSGALLERY_API_KEY `
|
||||
-Verbose
|
||||
Write-Host "==> Published $normalizedVersion to the PowerShell Gallery."
|
||||
} catch {
|
||||
# A push can be accepted by the gallery and still surface as an error here; when that happens the
|
||||
# retry comes back as 409. Re-check before failing the run over an upload that actually landed.
|
||||
Write-Host "==> Publish-PSResource reported: $($_.Exception.Message)"
|
||||
Start-Sleep -Seconds 15
|
||||
if (Test-PublishedVersion -Normalized $normalizedVersion) {
|
||||
Write-Host "==> Version $normalizedVersion is present on the gallery; treating the publish as successful."
|
||||
exit 0
|
||||
}
|
||||
|
||||
throw
|
||||
}
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
name: Publish to PowerShell Gallery
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Verify host prerequisites (pwsh, dotnet)
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$missing = @()
|
||||
if (-not (Get-Command pwsh -ErrorAction SilentlyContinue)) { $missing += 'pwsh' }
|
||||
if (-not (Get-Command dotnet -ErrorAction SilentlyContinue)) { $missing += 'dotnet' }
|
||||
if ($missing.Count -gt 0) {
|
||||
throw "Host runner is missing required tool(s): $($missing -join ', '). Provision them on the runner host."
|
||||
}
|
||||
Write-Host ("pwsh: " + (pwsh -NoProfile -Command '$PSVersionTable.PSVersion.ToString()'))
|
||||
Write-Host ("dotnet: " + (dotnet --version))
|
||||
Write-Host '--- dotnet --info ---'
|
||||
dotnet --info
|
||||
Write-Host '--- disk free ---'
|
||||
df -h .
|
||||
Write-Host '--- memory ---'
|
||||
free -m
|
||||
|
||||
- name: Restore NuGet packages
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Write-Host '==> dotnet restore src/PSInfisicalAPI/PSInfisicalAPI.csproj'
|
||||
dotnet restore src/PSInfisicalAPI/PSInfisicalAPI.csproj --verbosity normal
|
||||
if ($LASTEXITCODE -ne 0) { throw "Restore of PSInfisicalAPI.csproj failed with exit code $LASTEXITCODE" }
|
||||
Write-Host '==> dotnet restore src/PSInfisicalAPI.Tests/PSInfisicalAPI.Tests.csproj'
|
||||
dotnet restore src/PSInfisicalAPI.Tests/PSInfisicalAPI.Tests.csproj --verbosity normal
|
||||
if ($LASTEXITCODE -ne 0) { throw "Restore of PSInfisicalAPI.Tests.csproj failed with exit code $LASTEXITCODE" }
|
||||
|
||||
- name: Build module
|
||||
shell: pwsh
|
||||
run: ./build.ps1
|
||||
|
||||
- name: Validate module manifest
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$manifestPath = Join-Path $PWD 'Module/PSInfisicalAPI/PSInfisicalAPI.psd1'
|
||||
$manifest = Test-ModuleManifest -Path $manifestPath
|
||||
Write-Host "Manifest OK: $($manifest.Name) $($manifest.Version)"
|
||||
|
||||
- name: Upload module artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: PSInfisicalAPI-module
|
||||
path: Module/PSInfisicalAPI
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
release:
|
||||
needs: build
|
||||
if: ${{ success() && github.event.pull_request.merged == true }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
outputs:
|
||||
version: ${{ steps.meta.outputs.version }}
|
||||
tag: ${{ steps.meta.outputs.tag }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Verify host prerequisites (pwsh)
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if (-not (Get-Command pwsh -ErrorAction SilentlyContinue)) {
|
||||
throw "Host runner is missing required tool: pwsh. Provision it on the runner host."
|
||||
}
|
||||
Write-Host ("pwsh: " + (pwsh -NoProfile -Command '$PSVersionTable.PSVersion.ToString()'))
|
||||
|
||||
- name: Download module artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: PSInfisicalAPI-module
|
||||
path: Module/PSInfisicalAPI
|
||||
|
||||
- name: Resolve module version and tag
|
||||
id: meta
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$manifestPath = Join-Path $PWD 'Module/PSInfisicalAPI/PSInfisicalAPI.psd1'
|
||||
$manifest = Test-ModuleManifest -Path $manifestPath
|
||||
$version = $manifest.Version.ToString()
|
||||
$tag = $version
|
||||
Write-Host "Module version: $version"
|
||||
Write-Host "Release tag: $tag"
|
||||
"version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
|
||||
"tag=$tag" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
|
||||
|
||||
- name: Package module as release asset
|
||||
shell: pwsh
|
||||
env:
|
||||
VERSION: ${{ steps.meta.outputs.version }}
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$zipPath = Join-Path $PWD "PSInfisicalAPI-$($env:VERSION).zip"
|
||||
if (Test-Path $zipPath) { Remove-Item $zipPath -Force }
|
||||
Compress-Archive -Path 'Module/PSInfisicalAPI/*' -DestinationPath $zipPath -Force
|
||||
Write-Host "Created: $zipPath ($([math]::Round((Get-Item $zipPath).Length / 1KB, 1)) KB)"
|
||||
|
||||
- name: Create GitHub release
|
||||
shell: pwsh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
API_URL: ${{ github.api_url }}
|
||||
REPO: ${{ github.repository }}
|
||||
TAG: ${{ steps.meta.outputs.tag }}
|
||||
VERSION: ${{ steps.meta.outputs.version }}
|
||||
COMMIT_SHA: ${{ github.sha }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_BODY: ${{ github.event.pull_request.body }}
|
||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||
SERVER_URL: ${{ github.server_url }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
trap { Write-Host "==> RELEASE STEP FAILED: $($_ | Out-String)"; Write-Host ($_.ScriptStackTrace); exit 1 }
|
||||
|
||||
Write-Host "==> [1/8] Validating inputs"
|
||||
Write-Host " TAG=$($env:TAG)"
|
||||
Write-Host " VERSION=$($env:VERSION)"
|
||||
Write-Host " REPO=$($env:REPO)"
|
||||
Write-Host " API_URL=$($env:API_URL)"
|
||||
Write-Host " SERVER_URL=$($env:SERVER_URL)"
|
||||
Write-Host " PR_NUMBER=$($env:PR_NUMBER)"
|
||||
Write-Host " RUN_ID=$($env:RUN_ID)"
|
||||
if ([string]::IsNullOrWhiteSpace($env:GITHUB_TOKEN)) { throw "github.token is empty." }
|
||||
if ([string]::IsNullOrWhiteSpace($env:TAG)) { throw "TAG is empty." }
|
||||
if ([string]::IsNullOrWhiteSpace($env:VERSION)) { throw "VERSION is empty." }
|
||||
if ([string]::IsNullOrWhiteSpace($env:API_URL)) { throw "API_URL is empty." }
|
||||
if ([string]::IsNullOrWhiteSpace($env:REPO)) { throw "REPO is empty." }
|
||||
if ([string]::IsNullOrWhiteSpace($env:COMMIT_SHA)) { throw "COMMIT_SHA is empty." }
|
||||
|
||||
Write-Host "==> [2/8] Deriving metadata"
|
||||
$shortSha = $env:COMMIT_SHA.Substring(0, [Math]::Min(12, $env:COMMIT_SHA.Length))
|
||||
$buildUtc = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
|
||||
$runUrl = "$($env:SERVER_URL)/$($env:REPO)/actions/runs/$($env:RUN_ID)"
|
||||
$prUrl = "$($env:SERVER_URL)/$($env:REPO)/pull/$($env:PR_NUMBER)"
|
||||
Write-Host " shortSha=$shortSha"
|
||||
|
||||
Write-Host "==> [3/8] Extracting CHANGELOG section"
|
||||
$changelogSection = ''
|
||||
if (Test-Path 'CHANGELOG.md') {
|
||||
$lines = [System.IO.File]::ReadAllLines('CHANGELOG.md')
|
||||
$start = -1; $end = $lines.Length
|
||||
for ($i = 0; $i -lt $lines.Length; $i++) {
|
||||
if ($lines[$i] -match "^##\s+$([regex]::Escape($env:VERSION))\s*$") { $start = $i + 1; continue }
|
||||
if ($start -ge 0 -and $lines[$i] -match '^##\s+') { $end = $i; break }
|
||||
}
|
||||
if ($start -ge 0) {
|
||||
$changelogSection = ($lines[$start..($end - 1)] -join "`n").Trim()
|
||||
}
|
||||
}
|
||||
Write-Host " CHANGELOG section length: $($changelogSection.Length) chars"
|
||||
|
||||
Write-Host "==> [4/8] Building release body"
|
||||
|
||||
# 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
|
||||
[void]$sb.AppendLine("**PSInfisicalAPI $($env:VERSION)**")
|
||||
[void]$sb.AppendLine('')
|
||||
[void]$sb.AppendLine('| Field | Value |')
|
||||
[void]$sb.AppendLine('| --- | --- |')
|
||||
[void]$sb.AppendLine("| Version | ``$($env:VERSION)`` |")
|
||||
[void]$sb.AppendLine("| Tag | ``$($env:TAG)`` |")
|
||||
[void]$sb.AppendLine("| Commit | [``$shortSha``]($($env:SERVER_URL)/$($env:REPO)/commit/$($env:COMMIT_SHA)) |")
|
||||
[void]$sb.AppendLine("| Built (UTC) | $buildUtc |")
|
||||
[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('')
|
||||
|
||||
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('## Install')
|
||||
[void]$sb.AppendLine('```powershell')
|
||||
[void]$sb.AppendLine("Install-Module -Name PSInfisicalAPI -RequiredVersion $($env:VERSION) -Scope CurrentUser")
|
||||
[void]$sb.AppendLine('```')
|
||||
$body = $sb.ToString()
|
||||
Write-Host " body length: $($body.Length) chars"
|
||||
|
||||
$headers = @{
|
||||
Authorization = "Bearer $($env:GITHUB_TOKEN)"
|
||||
Accept = 'application/vnd.github+json'
|
||||
'X-GitHub-Api-Version' = '2022-11-28'
|
||||
}
|
||||
$createUri = "$($env:API_URL)/repos/$($env:REPO)/releases"
|
||||
|
||||
Write-Host "==> [5/8] Checking for existing release tag: $createUri/tags/$($env:TAG)"
|
||||
$existing = $null
|
||||
try {
|
||||
$existing = Invoke-RestMethod -Method Get -Headers $headers `
|
||||
-Uri "$createUri/tags/$($env:TAG)" -ErrorAction Stop
|
||||
} catch {
|
||||
$status = $null
|
||||
try { $status = $_.Exception.Response.StatusCode.value__ } catch { }
|
||||
if ($status -ne 404) {
|
||||
Write-Host " Lookup failed (status=$status): $($_.Exception.Message)"
|
||||
throw
|
||||
}
|
||||
Write-Host " No existing release (404)."
|
||||
}
|
||||
if ($existing) {
|
||||
Write-Host " Release tag '$($env:TAG)' already exists (id=$($existing.id)); skipping creation."
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "==> [6/8] Creating release"
|
||||
$payload = @{
|
||||
tag_name = $env:TAG
|
||||
target_commitish = $env:COMMIT_SHA
|
||||
name = "PSInfisicalAPI $($env:VERSION)"
|
||||
body = $body
|
||||
draft = $false
|
||||
prerelease = $false
|
||||
} | ConvertTo-Json -Depth 4
|
||||
Write-Host " payload bytes: $([System.Text.Encoding]::UTF8.GetByteCount($payload))"
|
||||
|
||||
$release = Invoke-RestMethod -Method Post -Uri $createUri -Headers $headers `
|
||||
-ContentType 'application/json' -Body $payload
|
||||
Write-Host " Created release id=$($release.id) at $($release.html_url)"
|
||||
|
||||
Write-Host "==> [7/8] Locating release asset"
|
||||
$assetPath = Join-Path $PWD "PSInfisicalAPI-$($env:VERSION).zip"
|
||||
if (-not (Test-Path $assetPath)) { throw "Release asset not found at: $assetPath" }
|
||||
$fileBytes = [System.IO.File]::ReadAllBytes($assetPath)
|
||||
Write-Host " Asset: $assetPath ($([math]::Round($fileBytes.Length / 1KB, 1)) KB)"
|
||||
|
||||
Write-Host "==> [8/8] Uploading asset"
|
||||
# GitHub returns a URI Template in upload_url (e.g. "https://uploads.github.com/.../assets{?name,label}").
|
||||
# Strip the template suffix and append the asset name query.
|
||||
$uploadBase = ($release.upload_url -replace '\{.*\}$', '')
|
||||
$uploadUri = "$uploadBase`?name=PSInfisicalAPI-$($env:VERSION).zip"
|
||||
Invoke-RestMethod -Method Post -Uri $uploadUri -Headers $headers `
|
||||
-ContentType 'application/zip' -Body $fileBytes | Out-Null
|
||||
Write-Host "==> Done: uploaded PSInfisicalAPI-$($env:VERSION).zip"
|
||||
|
||||
publish:
|
||||
needs: release
|
||||
if: ${{ success() && github.event.pull_request.merged == true }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Verify host prerequisites (pwsh)
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if (-not (Get-Command pwsh -ErrorAction SilentlyContinue)) {
|
||||
throw "Host runner is missing required tool: pwsh. Provision it on the runner host."
|
||||
}
|
||||
Write-Host ("pwsh: " + (pwsh -NoProfile -Command '$PSVersionTable.PSVersion.ToString()'))
|
||||
|
||||
- name: Download module artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: PSInfisicalAPI-module
|
||||
path: Module/PSInfisicalAPI
|
||||
|
||||
- name: Bootstrap Microsoft.PowerShell.PSResourceGet
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if (-not (Get-Module -ListAvailable -Name Microsoft.PowerShell.PSResourceGet)) {
|
||||
Write-Host "==> Installing Microsoft.PowerShell.PSResourceGet for CurrentUser"
|
||||
Install-Module -Name Microsoft.PowerShell.PSResourceGet -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop
|
||||
}
|
||||
Import-Module Microsoft.PowerShell.PSResourceGet -ErrorAction Stop
|
||||
|
||||
$existing = Get-PSResourceRepository -Name PSGallery -ErrorAction SilentlyContinue
|
||||
if (-not $existing) {
|
||||
Write-Host "==> Registering PSGallery repository"
|
||||
Register-PSResourceRepository -PSGallery -Trusted -ErrorAction Stop
|
||||
} else {
|
||||
Write-Host "==> PSGallery already registered; ensuring Trusted + ApiVersion v2"
|
||||
Set-PSResourceRepository -Name PSGallery -Trusted -ApiVersion v2 -ErrorAction Stop
|
||||
}
|
||||
Get-PSResourceRepository -Name PSGallery | Format-Table Name,Uri,Trusted,ApiVersion
|
||||
|
||||
- name: Verify PowerShell Gallery API key is configured
|
||||
shell: pwsh
|
||||
env:
|
||||
PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }}
|
||||
run: |
|
||||
if ([string]::IsNullOrWhiteSpace($env:PSGALLERY_API_KEY)) {
|
||||
throw "Repository secret 'PSGALLERY_API_KEY' is not configured."
|
||||
}
|
||||
|
||||
- name: Re-validate downloaded module manifest
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$manifestPath = Join-Path $PWD 'Module/PSInfisicalAPI/PSInfisicalAPI.psd1'
|
||||
$manifest = Test-ModuleManifest -Path $manifestPath
|
||||
Write-Host "Manifest OK: $($manifest.Name) $($manifest.Version)"
|
||||
|
||||
- name: Publish to PowerShell Gallery
|
||||
shell: pwsh
|
||||
env:
|
||||
PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }}
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$moduleDir = Join-Path $PWD 'Module/PSInfisicalAPI'
|
||||
$manifest = Test-ModuleManifest -Path (Join-Path $moduleDir 'PSInfisicalAPI.psd1')
|
||||
$version = $manifest.Version.ToString()
|
||||
|
||||
# NuGet strips leading zeros from each segment, so the manifest's 2026.07.30.2309 is listed on the
|
||||
# gallery as 2026.7.30.2309. Compare on the normalized form or every lookup misses.
|
||||
function Get-NormalizedVersion {
|
||||
param([string]$Value)
|
||||
$parts = $Value -split '\.'
|
||||
$normalized = foreach ($part in $parts) {
|
||||
$number = 0
|
||||
if ([int]::TryParse($part, [ref]$number)) { $number.ToString([System.Globalization.CultureInfo]::InvariantCulture) } else { $part }
|
||||
}
|
||||
return ($normalized -join '.')
|
||||
}
|
||||
|
||||
function Test-PublishedVersion {
|
||||
param([string]$Normalized)
|
||||
$uri = "https://www.powershellgallery.com/api/v2/FindPackagesById()?id='PSInfisicalAPI'&`$select=Version"
|
||||
try {
|
||||
$feed = Invoke-RestMethod -Uri $uri -TimeoutSec 120
|
||||
} catch {
|
||||
Write-Host "==> Could not query the gallery for existing versions: $($_.Exception.Message)"
|
||||
return $false
|
||||
}
|
||||
|
||||
foreach ($entry in @($feed)) {
|
||||
$candidate = $entry.properties.Version
|
||||
if ([string]::IsNullOrWhiteSpace($candidate)) { continue }
|
||||
if ((Get-NormalizedVersion -Value $candidate) -eq $Normalized) { return $true }
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
$normalizedVersion = Get-NormalizedVersion -Value $version
|
||||
Write-Host "==> Module version : $version (gallery form: $normalizedVersion)"
|
||||
Write-Host "==> Publishing from: $moduleDir"
|
||||
|
||||
if (Test-PublishedVersion -Normalized $normalizedVersion) {
|
||||
Write-Host "==> Version $normalizedVersion is already on the PowerShell Gallery; nothing to publish."
|
||||
exit 0
|
||||
}
|
||||
|
||||
try {
|
||||
Publish-PSResource `
|
||||
-Path $moduleDir `
|
||||
-Repository PSGallery `
|
||||
-ApiKey $env:PSGALLERY_API_KEY `
|
||||
-Verbose
|
||||
Write-Host "==> Published $normalizedVersion to the PowerShell Gallery."
|
||||
} catch {
|
||||
# A push can be accepted by the gallery and still surface as an error here; when that happens the
|
||||
# retry comes back as 409. Re-check before failing the run over an upload that actually landed.
|
||||
Write-Host "==> Publish-PSResource reported: $($_.Exception.Message)"
|
||||
Start-Sleep -Seconds 15
|
||||
if (Test-PublishedVersion -Normalized $normalizedVersion) {
|
||||
Write-Host "==> Version $normalizedVersion is present on the gallery; treating the publish as successful."
|
||||
exit 0
|
||||
}
|
||||
|
||||
throw
|
||||
}
|
||||
+2
-1
@@ -30,5 +30,6 @@ TestResults/
|
||||
*.trx
|
||||
*.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/
|
||||
|
||||
+474
-1004
File diff suppressed because one or more lines are too long
@@ -38,11 +38,12 @@
|
||||
</ViewSelectedBy>
|
||||
<TableControl>
|
||||
<TableHeaders>
|
||||
<TableColumnHeader><Label>Name</Label><Width>28</Width></TableColumnHeader>
|
||||
<TableColumnHeader><Label>CommonName</Label><Width>32</Width></TableColumnHeader>
|
||||
<TableColumnHeader><Label>Type</Label><Width>10</Width></TableColumnHeader>
|
||||
<TableColumnHeader><Label>Status</Label><Width>10</Width></TableColumnHeader>
|
||||
<TableColumnHeader><Label>KeyAlgorithm</Label><Width>14</Width></TableColumnHeader>
|
||||
<TableColumnHeader><Label>Name</Label><Width>24</Width></TableColumnHeader>
|
||||
<TableColumnHeader><Label>CommonName</Label><Width>28</Width></TableColumnHeader>
|
||||
<TableColumnHeader><Label>Type</Label><Width>9</Width></TableColumnHeader>
|
||||
<TableColumnHeader><Label>Status</Label><Width>8</Width></TableColumnHeader>
|
||||
<TableColumnHeader><Label>DirectIssue</Label><Width>11</Width></TableColumnHeader>
|
||||
<TableColumnHeader><Label>KeyAlgorithm</Label><Width>13</Width></TableColumnHeader>
|
||||
<TableColumnHeader><Label>NotAfter</Label><Width>22</Width></TableColumnHeader>
|
||||
</TableHeaders>
|
||||
<TableRowEntries>
|
||||
@@ -52,6 +53,7 @@
|
||||
<TableColumnItem><PropertyName>CommonName</PropertyName></TableColumnItem>
|
||||
<TableColumnItem><PropertyName>Type</PropertyName></TableColumnItem>
|
||||
<TableColumnItem><PropertyName>Status</PropertyName></TableColumnItem>
|
||||
<TableColumnItem><PropertyName>EnableDirectIssuance</PropertyName></TableColumnItem>
|
||||
<TableColumnItem><PropertyName>KeyAlgorithm</PropertyName></TableColumnItem>
|
||||
<TableColumnItem><PropertyName>NotAfter</PropertyName></TableColumnItem>
|
||||
</TableColumnItems>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@{
|
||||
RootModule = 'PSInfisicalAPI.psm1'
|
||||
ModuleVersion = '2026.06.05.2040'
|
||||
ModuleVersion = '2026.08.01.0245'
|
||||
GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51'
|
||||
Author = 'Grace Solutions'
|
||||
CompanyName = 'Grace Solutions'
|
||||
@@ -19,6 +19,7 @@
|
||||
'Copy-InfisicalSecret',
|
||||
'ConvertTo-InfisicalSecretDictionary',
|
||||
'Export-InfisicalSecrets',
|
||||
'Import-InfisicalSecret',
|
||||
'Get-InfisicalProject',
|
||||
'New-InfisicalProject',
|
||||
'Update-InfisicalProject',
|
||||
@@ -35,6 +36,14 @@
|
||||
'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',
|
||||
@@ -50,7 +59,25 @@
|
||||
'New-InfisicalScepDynamicChallenge',
|
||||
'Get-InfisicalScepMdmProfile',
|
||||
'Export-InfisicalScepMdmProfile',
|
||||
'Write-InfisicalScepMdmProfileToWmi'
|
||||
'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'
|
||||
)
|
||||
AliasesToExport = @()
|
||||
VariablesToExport = @()
|
||||
@@ -62,7 +89,7 @@
|
||||
LicenseUri = 'https://www.gnu.org/licenses/agpl-3.0.html'
|
||||
ProjectUri = 'https://prod.git.gracesolution.info/gsadmin/PSInfisicalAPI'
|
||||
ReleaseNotes = 'See CHANGELOG.md in the project repository for release history.'
|
||||
CommitHash = '1270c9099cae'
|
||||
CommitHash = 'b27fe6f002f4'
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -295,7 +295,7 @@ $CopyInfisicalSecretResult = Copy-InfisicalSecret @CopyInfisicalSecretParameters
|
||||
<command:noun>InfisicalSecretDictionary</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Aggregates an incoming pipeline of InfisicalSecret objects into a case-insensitive Dictionary keyed by SecretName. By default values are SecureString; pass -AsPlainText to materialize string values. Duplicate keys are handled via the -DuplicateKeyBehavior parameter (Error, FirstWins, LastWins).</maml:para>
|
||||
<maml:para>Aggregates an incoming pipeline of InfisicalSecret objects into a case-insensitive Dictionary keyed by SecretName. By default values are SecureString; pass -AsPlainText to materialize string values. Duplicate keys are handled via the -DuplicateKeyBehavior parameter (Error, FirstWins, LastWins). -Prefix prepends a string to every dictionary key (e.g. SecretName 'API_KEY' with -Prefix 'MYAPP_' becomes key 'MYAPP_API_KEY'); the underlying InfisicalSecret objects are not mutated. A SecretName that already starts with -Prefix (case-insensitive) is left as-is to avoid double-prefixing; pass -ForcePrefix to always prepend.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
@@ -322,6 +322,11 @@ $ConvertToInfisicalSecretDictionaryParameters.Verbose = $True
|
||||
$ConvertToInfisicalSecretDictionaryResult = ConvertTo-InfisicalSecretDictionary @ConvertToInfisicalSecretDictionaryParameters</dev:code>
|
||||
<dev:remarks><maml:para>Aggregates recursive secret results into a plain-text dictionary, with the last value winning on key collisions.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 3</maml:title>
|
||||
<dev:code>Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' | ConvertTo-InfisicalSecretDictionary -Prefix 'MYAPP_' -AsPlainText</dev:code>
|
||||
<dev:remarks><maml:para>Builds a plain-text dictionary whose keys are namespaced with 'MYAPP_' (e.g. API_KEY becomes MYAPP_API_KEY); the source InfisicalSecret objects are unchanged.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
</command:examples>
|
||||
</command:command>
|
||||
|
||||
@@ -333,7 +338,7 @@ $ConvertToInfisicalSecretDictionaryResult = ConvertTo-InfisicalSecretDictionary
|
||||
<command:noun>InfisicalSecrets</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Buffers an incoming pipeline of InfisicalSecret objects and writes them to a file in the requested format (DotEnv, Json, Yaml, EnvironmentVariables, etc.) or sets them as environment variables on the chosen scope (Process, User, Machine). -Encoding controls text encoding for file outputs.</maml:para>
|
||||
<maml:para>Buffers an incoming pipeline of InfisicalSecret objects and writes them to a file in the requested format (DotEnv, Json, Yaml, EnvironmentVariables, etc.) or sets them as environment variables on the chosen scope (Process, User, Machine). -Encoding controls text encoding for file outputs. -Prefix prepends a string to every emitted variable name regardless of format; names that already start with -Prefix (case-insensitive) are left as-is to avoid double-prefixing. Pass -ForcePrefix to always prepend.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
@@ -361,9 +366,52 @@ $ExportInfisicalSecretsParameters.Verbose = $True
|
||||
$ExportInfisicalSecretsResult = Export-InfisicalSecrets @ExportInfisicalSecretsParameters</dev:code>
|
||||
<dev:remarks><maml:para>Projects the recursive secret result into Process-scope environment variables for the current PowerShell session.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 3</maml:title>
|
||||
<dev:code>Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' | Export-InfisicalSecrets -Format EnvironmentVariables -Scope Process -Prefix 'MYAPP_'</dev:code>
|
||||
<dev:remarks><maml:para>Imports secrets into the process environment with every variable name prefixed by 'MYAPP_' (e.g. API_KEY becomes MYAPP_API_KEY).</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>Import-InfisicalSecret</command:name>
|
||||
<maml:description><maml:para>Reads a previously exported secrets file (Json, Yaml, Env, or Xml) back into a name-keyed Dictionary.</maml:para></maml:description>
|
||||
<command:verb>Import</command:verb>
|
||||
<command:noun>InfisicalSecret</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Loads the file at -Path (which must exist) using the parser matching -Format and returns a case-insensitive Dictionary keyed by SecretName. By default values are SecureString; pass -AsPlainText for a plain string dictionary. -Prefix prepends to every emitted key; keys already starting with -Prefix (case-insensitive) are left as-is to avoid double-prefixing, and -ForcePrefix overrides that gate. -DuplicateKeyBehavior controls collision handling (Error, FirstWins, LastWins). The EnvironmentVariables format is intentionally not supported here; use [Environment]::GetEnvironmentVariable or Get-InfisicalEnvironmentVariable for environment-backed values.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<maml:para>Importers expect the same schema that Export-InfisicalSecrets produces (Json/Yaml = array or 'Secrets' root list of {SecretName, SecretValue}; Xml = <Secrets><Secret><SecretName/><SecretValue/>; Env = KEY=VALUE per line, '#' comments allowed). JSON and YAML additionally accept a flat key/value object as a convenience.</maml:para>
|
||||
</maml:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 1</maml:title>
|
||||
<dev:code>$Secrets = Import-InfisicalSecret -Path '.\secrets.json' -Format Json</dev:code>
|
||||
<dev:remarks><maml:para>Reads a JSON export back into a Dictionary<string, SecureString> keyed by the original SecretName values.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 2</maml:title>
|
||||
<dev:code>$ImportInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
$ImportInfisicalSecretParameters.Path = [System.IO.FileInfo]'.\secrets.env'
|
||||
$ImportInfisicalSecretParameters.Format = 'Env'
|
||||
$ImportInfisicalSecretParameters.Prefix = 'MYAPP_'
|
||||
$ImportInfisicalSecretParameters.DuplicateKeyBehavior = 'LastWins'
|
||||
$ImportInfisicalSecretParameters.AsPlainText = $True
|
||||
|
||||
$ImportInfisicalSecretResult = Import-InfisicalSecret @ImportInfisicalSecretParameters</dev:code>
|
||||
<dev:remarks><maml:para>Loads a .env file into a plain-text dictionary, namespacing every key with 'MYAPP_' and letting the last occurrence win on duplicates.</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>Get-InfisicalProject</command:name>
|
||||
@@ -1018,7 +1066,8 @@ $RemoveInfisicalTagResult = Remove-InfisicalTag @RemoveInfisicalTagParameters</d
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<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.</maml:para>
|
||||
<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:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
@@ -1233,6 +1282,16 @@ $GetInfisicalCertificatePolicyResult = Get-InfisicalCertificatePolicy @GetInfisi
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<maml:para>Default -PrivateKeyProtection is 'LocalOnly': the leaf is loaded into memory without persisting the private key and PrivateKeyPem is scrubbed from the emitted result unless -PrivateKeyPath or an explicit -KeyStorageFlags binding overrides it. The reuse path completes its chain from the Infisical bundle when local stores are incomplete; pass -LocalChainOnly to suppress that fetch entirely.</maml:para>
|
||||
<maml:para>Choose the parameter set by whether the common name varies per request. -CertificateProfileId and -CertificateAuthorityId both accept a per-request common name and suit fleet enrollment; -PkiSubscriberSlug does not, because Infisical rejects any CSR whose CN differs from the subscriber's ('Common name (CN) in the CSR does not match the subscriber's common name') and allowlists the subscriber's subjectAlternativeNames. A subscriber is a single named identity, so enrolling many machines through subscribers requires one subscriber per machine.</maml:para>
|
||||
<maml:para>-CertificateAuthorityId only works against a CA that permits direct issuance (Get-InfisicalCertificateAuthority reports this as EnableDirectIssuance). The cmdlet resolves the issuer and validates this before generating a keypair, naming the subscriber, CA, or profile it will use on the verbose stream and in the -WhatIf target. Profile issuance is the only path that ignores that flag, so -CertificateProfileId works against a CA whose EnableDirectIssuance is False. Note that enableDirectIssuance appears in no Infisical create or update schema, so it cannot be changed through the API or UI after the CA exists.</maml:para>
|
||||
<maml:para>There is no -CertificateTemplateId parameter because Infisical's REST API exposes no template-based issuance route; when the API asks for 'a certificate template or subscriber', supply -CertificateProfileId or -PkiSubscriberSlug, or use a CA that allows direct issuance.</maml:para>
|
||||
<maml:para>-CommonName takes the bare value ('web01.contoso.com'), not an RDN; a leading 'CN=' is stripped because the CSR builder adds the prefix itself. -DnsName accepts the mixed output of Get-InfisicalSANList: IP literals in that list are emitted as iPAddress SAN entries rather than dNSName entries.</maml:para>
|
||||
<maml:para>When -StoreLocation is not supplied it is chosen from the process elevation: an elevated session installs to LocalMachine, otherwise CurrentUser. The choice is reported on the verbose stream. Chain members are routed by type regardless of location - self-signed certificates to the Root store, others to CertificateAuthority. When the resolved location is LocalMachine and -KeyStorageFlags was not supplied, the private key is placed in the machine key store so the installed certificate has a usable key outside the calling user's profile.</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>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:alertSet>
|
||||
<command:examples>
|
||||
@@ -1649,4 +1708,835 @@ $WriteInfisicalScepMdmProfileToWmiResult = Write-InfisicalScepMdmProfileToWmi @W
|
||||
</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>Start-InfisicalProcess</command:name>
|
||||
<maml:description><maml:para>Starts a child process with Infisical secrets injected directly into its environment block.</maml:para></maml:description>
|
||||
<command:verb>Start</command:verb>
|
||||
<command:noun>InfisicalProcess</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Launches the executable specified by -FilePath, captures stdout/stderr, validates the exit code against -AcceptableExitCodeList, and optionally parses output with -ParsingExpression. InfisicalSecret objects supplied via -Secret (pipeline or by name) are decrypted into the ProcessStartInfo.Environment dictionary only, never written to the user or machine scope; -Prefix prepends a string to each injected variable name, skipping names that already start with -Prefix (case-insensitive) unless -ForcePrefix is supplied. -EnvironmentVariables adds additional non-secret values. -ExecutionTimeout, -NoWait, -CreateNoWindow, -WindowStyle, -Priority, -StandardInputObjectList, -SecureArgumentList, -LogOutput, and -ContinueOnError mirror the semantics of the upstream Start-ProcessWithOutput helper. Honors -WhatIf and -Confirm.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<maml:para>Secret values exist as plain strings only within the child process environment block; they are never persisted to the calling shell, the user scope, or the machine scope. Use -SecureArgumentList to mask sensitive command-line arguments in verbose output.</maml:para>
|
||||
</maml:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 1</maml:title>
|
||||
<dev:code>Get-InfisicalSecret -SecretPath '/build' | Start-InfisicalProcess -FilePath 'dotnet.exe' -ArgumentList @('publish','-c','Release') -AcceptableExitCodeList @('0') -CreateNoWindow</dev:code>
|
||||
<dev:remarks><maml:para>Decrypts every secret at /build, exposes each one as a process environment variable, and runs dotnet publish with no visible window.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 2</maml:title>
|
||||
<dev:code>$Secrets = Get-InfisicalSecret -SecretPath '/runtime'
|
||||
Start-InfisicalProcess -FilePath 'node.exe' -ArgumentList @('app.js') -Secret $Secrets -Prefix 'APP_' -ExecutionTimeout ([TimeSpan]::FromMinutes(5)) -LogOutput</dev:code>
|
||||
<dev:remarks><maml:para>Injects the /runtime secrets as APP_-prefixed environment variables, runs node app.js, and forcibly terminates the process after five minutes if it has not exited.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 3</maml:title>
|
||||
<dev:code>$StartInfisicalProcessParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
$StartInfisicalProcessParameters.FilePath = 'pwsh.exe'
|
||||
$StartInfisicalProcessParameters.ArgumentList = @('-NoProfile','-Command','Write-Host $env:DEPLOY_TOKEN.Length')
|
||||
$StartInfisicalProcessParameters.Secret = Get-InfisicalSecret -SecretPath '/deploy'
|
||||
$StartInfisicalProcessParameters.Prefix = 'DEPLOY_'
|
||||
$StartInfisicalProcessParameters.AcceptableExitCodeList = @('0')
|
||||
$StartInfisicalProcessParameters.CreateNoWindow = $True
|
||||
$StartInfisicalProcessParameters.SecureArgumentList = $True
|
||||
$StartInfisicalProcessParameters.LogOutput = $True
|
||||
$StartInfisicalProcessParameters.Verbose = $True
|
||||
|
||||
$StartInfisicalProcessResult = Start-InfisicalProcess @StartInfisicalProcessParameters</dev:code>
|
||||
<dev:remarks><maml:para>Splatted invocation that runs pwsh with DEPLOY_-prefixed secrets in scope, masks the command line in verbose output, and echoes both stdout and stderr to the verbose stream after exit.</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>Get-InfisicalEnvironmentVariable</command:name>
|
||||
<maml:description><maml:para>Reads an environment variable from the first scope that has a non-empty value (Process > User > Machine).</maml:para></maml:description>
|
||||
<command:verb>Get</command:verb>
|
||||
<command:noun>InfisicalEnvironmentVariable</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Returns the value of -Name from the first scope that contains a non-empty value, checking Process, then User, then Machine in that order. Emits nothing when the variable is missing or blank in every scope, so an assignment yields $null without writing errors or warnings. Platform-unsupported scopes (User and Machine on non-Windows) are silently skipped.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<maml:para>Designed for the same discovery semantics the rest of PSInfisicalAPI uses when resolving connection inputs from the environment. Pipe-friendly: accepts -Name from the pipeline by value and by property name.</maml:para>
|
||||
</maml:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 1</maml:title>
|
||||
<dev:code>$Value = Get-InfisicalEnvironmentVariable -Name 'INFISICAL_CLIENT_ID'</dev:code>
|
||||
<dev:remarks><maml:para>Returns the first non-empty value of INFISICAL_CLIENT_ID across Process, User, and Machine scopes; assigns $null when the variable is unset everywhere.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 2</maml:title>
|
||||
<dev:code>@('INFISICAL_BASE_URI','INFISICAL_PROJECT_ID') | Get-InfisicalEnvironmentVariable</dev:code>
|
||||
<dev:remarks><maml:para>Pipes a list of variable names through the cmdlet and emits one value per name that is set in any scope.</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>Get-InfisicalOrganization</command:name>
|
||||
<maml:description><maml:para>Lists or retrieves Infisical organizations accessible to the current identity.</maml:para></maml:description>
|
||||
<command:verb>Get</command:verb>
|
||||
<command:noun>InfisicalOrganization</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Default (List parameter set) returns every organization the active session can see; visibility is governed by Infisical's role assignments. When -OrganizationId is supplied (Single parameter set) the cmdlet returns one organization. Does not require a project context.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<maml:para>The List-mode result is an array of InfisicalOrganization objects; pipe into Where-Object or Select-Object to filter by Slug, Name, or Id. The cmdlet accepts pipeline input by property name on -OrganizationId.</maml:para>
|
||||
</maml:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 1</maml:title>
|
||||
<dev:code>Get-InfisicalOrganization</dev:code>
|
||||
<dev:remarks><maml:para>Lists every organization the current session can see.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 2</maml:title>
|
||||
<dev:code>Get-InfisicalOrganization -OrganizationId $OrganizationId</dev:code>
|
||||
<dev:remarks><maml:para>Retrieves the canonical record for a single organization by id.</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-InfisicalOrganization</command:name>
|
||||
<maml:description><maml:para>Creates a new Infisical organization.</maml:para></maml:description>
|
||||
<command:verb>New</command:verb>
|
||||
<command:noun>InfisicalOrganization</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Creates a new organization with the supplied name and optional slug. Honors -WhatIf and -Confirm. Requires server-side permission to create organizations.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<maml:para>Slug must be unique server-side; if omitted, the server derives one from the name.</maml:para>
|
||||
</maml:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 1</maml:title>
|
||||
<dev:code>New-InfisicalOrganization -Name 'Acme Corporation'</dev:code>
|
||||
<dev:remarks><maml:para>Creates a new organization named 'Acme Corporation' with a server-derived slug.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 2</maml:title>
|
||||
<dev:code>$NewInfisicalOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
$NewInfisicalOrganizationParameters.Name = 'Acme Corporation'
|
||||
$NewInfisicalOrganizationParameters.Slug = 'acme-corp'
|
||||
$NewInfisicalOrganizationParameters.Verbose = $True
|
||||
|
||||
$NewInfisicalOrganizationResult = New-InfisicalOrganization @NewInfisicalOrganizationParameters</dev:code>
|
||||
<dev:remarks><maml:para>Creates an organization with an explicit slug.</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>Update-InfisicalOrganization</command:name>
|
||||
<maml:description><maml:para>Updates mutable attributes on an existing Infisical organization.</maml:para></maml:description>
|
||||
<command:verb>Update</command:verb>
|
||||
<command:noun>InfisicalOrganization</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Updates the name or slug of an organization. -OrganizationId is required. Only bound parameters are transmitted. Honors -WhatIf and -Confirm.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<maml:para>Renaming or re-slugging an organization may affect billing exports and identity URLs; coordinate with downstream consumers.</maml:para>
|
||||
</maml:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 1</maml:title>
|
||||
<dev:code>Update-InfisicalOrganization -OrganizationId $OrganizationId -Name 'Acme Corp.'</dev:code>
|
||||
<dev:remarks><maml:para>Renames the supplied organization.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 2</maml:title>
|
||||
<dev:code>$UpdateInfisicalOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
$UpdateInfisicalOrganizationParameters.OrganizationId = $OrganizationId
|
||||
$UpdateInfisicalOrganizationParameters.Name = 'Acme Corp.'
|
||||
$UpdateInfisicalOrganizationParameters.Slug = 'acme-corp'
|
||||
$UpdateInfisicalOrganizationParameters.Verbose = $True
|
||||
|
||||
$UpdateInfisicalOrganizationResult = Update-InfisicalOrganization @UpdateInfisicalOrganizationParameters</dev:code>
|
||||
<dev:remarks><maml:para>Renames the organization and updates its slug.</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-InfisicalOrganization</command:name>
|
||||
<maml:description><maml:para>Deletes an Infisical organization.</maml:para></maml:description>
|
||||
<command:verb>Remove</command:verb>
|
||||
<command:noun>InfisicalOrganization</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Deletes an organization by id. -OrganizationId is required. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed organization id.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<maml:para>This is irreversible and removes all projects, sub-organizations, secrets, and identities owned by the organization. Honors -WhatIf and -Confirm.</maml:para>
|
||||
</maml:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 1</maml:title>
|
||||
<dev:code>Remove-InfisicalOrganization -OrganizationId $OrganizationId -Confirm:$False</dev:code>
|
||||
<dev:remarks><maml:para>Deletes the supplied organization without prompting.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 2</maml:title>
|
||||
<dev:code>$RemoveInfisicalOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
$RemoveInfisicalOrganizationParameters.OrganizationId = $OrganizationId
|
||||
$RemoveInfisicalOrganizationParameters.PassThru = $True
|
||||
$RemoveInfisicalOrganizationParameters.Confirm = $False
|
||||
$RemoveInfisicalOrganizationParameters.Verbose = $True
|
||||
|
||||
$RemoveInfisicalOrganizationResult = Remove-InfisicalOrganization @RemoveInfisicalOrganizationParameters</dev:code>
|
||||
<dev:remarks><maml:para>Removes the organization without confirmation and emits the removed organization id for logging.</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>Get-InfisicalSubOrganization</command:name>
|
||||
<maml:description><maml:para>Lists or retrieves Infisical sub-organizations accessible to the current identity.</maml:para></maml:description>
|
||||
<command:verb>Get</command:verb>
|
||||
<command:noun>InfisicalSubOrganization</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Default (List parameter set) returns every sub-organization the active session can see. Optional -Limit, -Offset, -Search, -OrderBy, -OrderDirection, and -IsAccessible are forwarded to the server as query parameters. When -SubOrganizationId is supplied (Single parameter set) the cmdlet returns one sub-organization. Does not require a project context.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<maml:para>Sub-organizations are a beta Infisical feature. The List result is an array of InfisicalSubOrganization objects; pipe into Where-Object or Select-Object to filter further. The cmdlet accepts pipeline input by property name on -SubOrganizationId.</maml:para>
|
||||
</maml:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 1</maml:title>
|
||||
<dev:code>Get-InfisicalSubOrganization</dev:code>
|
||||
<dev:remarks><maml:para>Lists every sub-organization the current session can see.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 2</maml:title>
|
||||
<dev:code>Get-InfisicalSubOrganization -SubOrganizationId $SubOrganizationId</dev:code>
|
||||
<dev:remarks><maml:para>Retrieves the canonical record for a single sub-organization by id.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 3</maml:title>
|
||||
<dev:code>Get-InfisicalSubOrganization -Search 'platform' -OrderBy 'name' -OrderDirection 'asc' -Limit 25 -IsAccessible</dev:code>
|
||||
<dev:remarks><maml:para>Lists up to 25 sub-organizations matching 'platform', sorted ascending by name, restricted to those the current identity has access to.</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-InfisicalSubOrganization</command:name>
|
||||
<maml:description><maml:para>Creates a new Infisical sub-organization.</maml:para></maml:description>
|
||||
<command:verb>New</command:verb>
|
||||
<command:noun>InfisicalSubOrganization</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Creates a sub-organization with the supplied name and slug. Both are required by the server. Honors -WhatIf and -Confirm.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<maml:para>Slug must be unique within the parent organization. Sub-organizations are a beta Infisical feature.</maml:para>
|
||||
</maml:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 1</maml:title>
|
||||
<dev:code>New-InfisicalSubOrganization -Name 'Platform Engineering' -Slug 'platform-eng'</dev:code>
|
||||
<dev:remarks><maml:para>Creates a new sub-organization named 'Platform Engineering' with slug 'platform-eng'.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 2</maml:title>
|
||||
<dev:code>$NewInfisicalSubOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
$NewInfisicalSubOrganizationParameters.Name = 'Platform Engineering'
|
||||
$NewInfisicalSubOrganizationParameters.Slug = 'platform-eng'
|
||||
$NewInfisicalSubOrganizationParameters.Verbose = $True
|
||||
|
||||
$NewInfisicalSubOrganizationResult = New-InfisicalSubOrganization @NewInfisicalSubOrganizationParameters</dev:code>
|
||||
<dev:remarks><maml:para>Splatted invocation that creates a sub-organization and logs the request via the verbose stream.</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>Update-InfisicalSubOrganization</command:name>
|
||||
<maml:description><maml:para>Updates mutable attributes on an existing Infisical sub-organization.</maml:para></maml:description>
|
||||
<command:verb>Update</command:verb>
|
||||
<command:noun>InfisicalSubOrganization</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Updates the name or slug of a sub-organization. -SubOrganizationId is required. Only bound parameters are transmitted. Honors -WhatIf and -Confirm.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<maml:para>Sub-organizations are a beta Infisical feature; coordinate slug changes with downstream consumers that pin the slug in scripts or configuration files.</maml:para>
|
||||
</maml:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 1</maml:title>
|
||||
<dev:code>Update-InfisicalSubOrganization -SubOrganizationId $SubOrganizationId -Name 'Platform (v2)'</dev:code>
|
||||
<dev:remarks><maml:para>Renames the supplied sub-organization.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 2</maml:title>
|
||||
<dev:code>$UpdateInfisicalSubOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
$UpdateInfisicalSubOrganizationParameters.SubOrganizationId = $SubOrganizationId
|
||||
$UpdateInfisicalSubOrganizationParameters.Name = 'Platform (v2)'
|
||||
$UpdateInfisicalSubOrganizationParameters.Slug = 'platform-v2'
|
||||
$UpdateInfisicalSubOrganizationParameters.Verbose = $True
|
||||
|
||||
$UpdateInfisicalSubOrganizationResult = Update-InfisicalSubOrganization @UpdateInfisicalSubOrganizationParameters</dev:code>
|
||||
<dev:remarks><maml:para>Renames the sub-organization and updates its slug in a single call.</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-InfisicalSubOrganization</command:name>
|
||||
<maml:description><maml:para>Deletes an Infisical sub-organization.</maml:para></maml:description>
|
||||
<command:verb>Remove</command:verb>
|
||||
<command:noun>InfisicalSubOrganization</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Deletes a sub-organization by id. -SubOrganizationId is required. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed sub-organization id.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<maml:para>This is destructive and removes all projects, secrets, and identities scoped to the sub-organization. Honors -WhatIf and -Confirm.</maml:para>
|
||||
</maml:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 1</maml:title>
|
||||
<dev:code>Remove-InfisicalSubOrganization -SubOrganizationId $SubOrganizationId -Confirm:$False</dev:code>
|
||||
<dev:remarks><maml:para>Deletes the supplied sub-organization without prompting.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 2</maml:title>
|
||||
<dev:code>$RemoveInfisicalSubOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
$RemoveInfisicalSubOrganizationParameters.SubOrganizationId = $SubOrganizationId
|
||||
$RemoveInfisicalSubOrganizationParameters.PassThru = $True
|
||||
$RemoveInfisicalSubOrganizationParameters.Confirm = $False
|
||||
$RemoveInfisicalSubOrganizationParameters.Verbose = $True
|
||||
|
||||
$RemoveInfisicalSubOrganizationResult = Remove-InfisicalSubOrganization @RemoveInfisicalSubOrganizationParameters</dev:code>
|
||||
<dev:remarks><maml:para>Removes the sub-organization without confirmation and emits the removed id for logging.</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>Get-InfisicalSANList</command:name>
|
||||
<maml:description><maml:para>Builds a deduplicated list of Subject Alternative Name candidates for the local device.</maml:para></maml:description>
|
||||
<command:verb>Get</command:verb>
|
||||
<command:noun>InfisicalSANList</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Returns, in order: the local device name; the device name suffixed with each non-empty DNS suffix found on any operational (non-loopback) network adapter and the system primary domain; every IPv4 unicast address whose first octets fall within RFC 1918 (10/8, 172.16/12, 192.168/16) or CGNAT (100.64/10); and the IPv4 and IPv6 loopback addresses (127.0.0.1, ::1). Optional -InclusionExpression and -ExclusionExpression regex filters are applied in that order after collection, before output. Suitable as a one-shot SAN provider for Request-InfisicalCertificate -DnsName.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<maml:para>Output is a single strongly-typed System.String[] array (emitted non-enumerated) so it round-trips into [System.Collections.Generic.List[string]]::AddRange() and binds directly to string[] parameters such as Request-InfisicalCertificate -DnsName. The device name comes first so it can be reused as a CommonName. Routable public IPv4 addresses, link-local addresses, and IPv6 unicast addresses other than loopback are intentionally excluded. -InclusionExpression and -ExclusionExpression are case-insensitive .NET regular expressions; inclusion is applied first, then exclusion.</maml:para>
|
||||
</maml:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 1</maml:title>
|
||||
<dev:code>Get-InfisicalSANList</dev:code>
|
||||
<dev:remarks><maml:para>Returns the SAN candidate list for the current device.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 2</maml:title>
|
||||
<dev:code>$Sans = Get-InfisicalSANList
|
||||
Request-InfisicalCertificate -ProjectId $ProjectId -CertificateAuthorityId $CaId -CommonName $Sans[0] -DnsName $Sans -Ttl '90d'</dev:code>
|
||||
<dev:remarks><maml:para>Captures the SAN list, then uses the device name as the CommonName and the full list as DnsName when requesting a certificate.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 3</maml:title>
|
||||
<dev:code>$GetInfisicalSANListParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
$GetInfisicalSANListParameters.InclusionExpression = '\.gracesolution\.prv$|^10\.|^172\.'
|
||||
$GetInfisicalSANListParameters.ExclusionExpression = '^127\.|^::1$'
|
||||
$Sans = Get-InfisicalSANList @GetInfisicalSANListParameters</dev:code>
|
||||
<dev:remarks><maml:para>Keeps only entries ending in the corporate DNS suffix or sitting in the 10/8 or 172/12 ranges, then drops loopback. Filters are case-insensitive and applied in fetch -> include -> exclude -> output order.</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-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>
|
||||
|
||||
@@ -295,7 +295,7 @@ $CopyInfisicalSecretResult = Copy-InfisicalSecret @CopyInfisicalSecretParameters
|
||||
<command:noun>InfisicalSecretDictionary</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Aggregates an incoming pipeline of InfisicalSecret objects into a case-insensitive Dictionary keyed by SecretName. By default values are SecureString; pass -AsPlainText to materialize string values. Duplicate keys are handled via the -DuplicateKeyBehavior parameter (Error, FirstWins, LastWins).</maml:para>
|
||||
<maml:para>Aggregates an incoming pipeline of InfisicalSecret objects into a case-insensitive Dictionary keyed by SecretName. By default values are SecureString; pass -AsPlainText to materialize string values. Duplicate keys are handled via the -DuplicateKeyBehavior parameter (Error, FirstWins, LastWins). -Prefix prepends a string to every dictionary key (e.g. SecretName 'API_KEY' with -Prefix 'MYAPP_' becomes key 'MYAPP_API_KEY'); the underlying InfisicalSecret objects are not mutated. A SecretName that already starts with -Prefix (case-insensitive) is left as-is to avoid double-prefixing; pass -ForcePrefix to always prepend.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
@@ -322,6 +322,11 @@ $ConvertToInfisicalSecretDictionaryParameters.Verbose = $True
|
||||
$ConvertToInfisicalSecretDictionaryResult = ConvertTo-InfisicalSecretDictionary @ConvertToInfisicalSecretDictionaryParameters</dev:code>
|
||||
<dev:remarks><maml:para>Aggregates recursive secret results into a plain-text dictionary, with the last value winning on key collisions.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 3</maml:title>
|
||||
<dev:code>Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' | ConvertTo-InfisicalSecretDictionary -Prefix 'MYAPP_' -AsPlainText</dev:code>
|
||||
<dev:remarks><maml:para>Builds a plain-text dictionary whose keys are namespaced with 'MYAPP_' (e.g. API_KEY becomes MYAPP_API_KEY); the source InfisicalSecret objects are unchanged.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
</command:examples>
|
||||
</command:command>
|
||||
|
||||
@@ -333,7 +338,7 @@ $ConvertToInfisicalSecretDictionaryResult = ConvertTo-InfisicalSecretDictionary
|
||||
<command:noun>InfisicalSecrets</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Buffers an incoming pipeline of InfisicalSecret objects and writes them to a file in the requested format (DotEnv, Json, Yaml, EnvironmentVariables, etc.) or sets them as environment variables on the chosen scope (Process, User, Machine). -Encoding controls text encoding for file outputs.</maml:para>
|
||||
<maml:para>Buffers an incoming pipeline of InfisicalSecret objects and writes them to a file in the requested format (DotEnv, Json, Yaml, EnvironmentVariables, etc.) or sets them as environment variables on the chosen scope (Process, User, Machine). -Encoding controls text encoding for file outputs. -Prefix prepends a string to every emitted variable name regardless of format; names that already start with -Prefix (case-insensitive) are left as-is to avoid double-prefixing. Pass -ForcePrefix to always prepend.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
@@ -361,9 +366,52 @@ $ExportInfisicalSecretsParameters.Verbose = $True
|
||||
$ExportInfisicalSecretsResult = Export-InfisicalSecrets @ExportInfisicalSecretsParameters</dev:code>
|
||||
<dev:remarks><maml:para>Projects the recursive secret result into Process-scope environment variables for the current PowerShell session.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 3</maml:title>
|
||||
<dev:code>Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' | Export-InfisicalSecrets -Format EnvironmentVariables -Scope Process -Prefix 'MYAPP_'</dev:code>
|
||||
<dev:remarks><maml:para>Imports secrets into the process environment with every variable name prefixed by 'MYAPP_' (e.g. API_KEY becomes MYAPP_API_KEY).</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>Import-InfisicalSecret</command:name>
|
||||
<maml:description><maml:para>Reads a previously exported secrets file (Json, Yaml, Env, or Xml) back into a name-keyed Dictionary.</maml:para></maml:description>
|
||||
<command:verb>Import</command:verb>
|
||||
<command:noun>InfisicalSecret</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Loads the file at -Path (which must exist) using the parser matching -Format and returns a case-insensitive Dictionary keyed by SecretName. By default values are SecureString; pass -AsPlainText for a plain string dictionary. -Prefix prepends to every emitted key; keys already starting with -Prefix (case-insensitive) are left as-is to avoid double-prefixing, and -ForcePrefix overrides that gate. -DuplicateKeyBehavior controls collision handling (Error, FirstWins, LastWins). The EnvironmentVariables format is intentionally not supported here; use [Environment]::GetEnvironmentVariable or Get-InfisicalEnvironmentVariable for environment-backed values.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<maml:para>Importers expect the same schema that Export-InfisicalSecrets produces (Json/Yaml = array or 'Secrets' root list of {SecretName, SecretValue}; Xml = <Secrets><Secret><SecretName/><SecretValue/>; Env = KEY=VALUE per line, '#' comments allowed). JSON and YAML additionally accept a flat key/value object as a convenience.</maml:para>
|
||||
</maml:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 1</maml:title>
|
||||
<dev:code>$Secrets = Import-InfisicalSecret -Path '.\secrets.json' -Format Json</dev:code>
|
||||
<dev:remarks><maml:para>Reads a JSON export back into a Dictionary<string, SecureString> keyed by the original SecretName values.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 2</maml:title>
|
||||
<dev:code>$ImportInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
$ImportInfisicalSecretParameters.Path = [System.IO.FileInfo]'.\secrets.env'
|
||||
$ImportInfisicalSecretParameters.Format = 'Env'
|
||||
$ImportInfisicalSecretParameters.Prefix = 'MYAPP_'
|
||||
$ImportInfisicalSecretParameters.DuplicateKeyBehavior = 'LastWins'
|
||||
$ImportInfisicalSecretParameters.AsPlainText = $True
|
||||
|
||||
$ImportInfisicalSecretResult = Import-InfisicalSecret @ImportInfisicalSecretParameters</dev:code>
|
||||
<dev:remarks><maml:para>Loads a .env file into a plain-text dictionary, namespacing every key with 'MYAPP_' and letting the last occurrence win on duplicates.</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>Get-InfisicalProject</command:name>
|
||||
@@ -1018,7 +1066,8 @@ $RemoveInfisicalTagResult = Remove-InfisicalTag @RemoveInfisicalTagParameters</d
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<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.</maml:para>
|
||||
<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:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
@@ -1233,6 +1282,16 @@ $GetInfisicalCertificatePolicyResult = Get-InfisicalCertificatePolicy @GetInfisi
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<maml:para>Default -PrivateKeyProtection is 'LocalOnly': the leaf is loaded into memory without persisting the private key and PrivateKeyPem is scrubbed from the emitted result unless -PrivateKeyPath or an explicit -KeyStorageFlags binding overrides it. The reuse path completes its chain from the Infisical bundle when local stores are incomplete; pass -LocalChainOnly to suppress that fetch entirely.</maml:para>
|
||||
<maml:para>Choose the parameter set by whether the common name varies per request. -CertificateProfileId and -CertificateAuthorityId both accept a per-request common name and suit fleet enrollment; -PkiSubscriberSlug does not, because Infisical rejects any CSR whose CN differs from the subscriber's ('Common name (CN) in the CSR does not match the subscriber's common name') and allowlists the subscriber's subjectAlternativeNames. A subscriber is a single named identity, so enrolling many machines through subscribers requires one subscriber per machine.</maml:para>
|
||||
<maml:para>-CertificateAuthorityId only works against a CA that permits direct issuance (Get-InfisicalCertificateAuthority reports this as EnableDirectIssuance). The cmdlet resolves the issuer and validates this before generating a keypair, naming the subscriber, CA, or profile it will use on the verbose stream and in the -WhatIf target. Profile issuance is the only path that ignores that flag, so -CertificateProfileId works against a CA whose EnableDirectIssuance is False. Note that enableDirectIssuance appears in no Infisical create or update schema, so it cannot be changed through the API or UI after the CA exists.</maml:para>
|
||||
<maml:para>There is no -CertificateTemplateId parameter because Infisical's REST API exposes no template-based issuance route; when the API asks for 'a certificate template or subscriber', supply -CertificateProfileId or -PkiSubscriberSlug, or use a CA that allows direct issuance.</maml:para>
|
||||
<maml:para>-CommonName takes the bare value ('web01.contoso.com'), not an RDN; a leading 'CN=' is stripped because the CSR builder adds the prefix itself. -DnsName accepts the mixed output of Get-InfisicalSANList: IP literals in that list are emitted as iPAddress SAN entries rather than dNSName entries.</maml:para>
|
||||
<maml:para>When -StoreLocation is not supplied it is chosen from the process elevation: an elevated session installs to LocalMachine, otherwise CurrentUser. The choice is reported on the verbose stream. Chain members are routed by type regardless of location - self-signed certificates to the Root store, others to CertificateAuthority. When the resolved location is LocalMachine and -KeyStorageFlags was not supplied, the private key is placed in the machine key store so the installed certificate has a usable key outside the calling user's profile.</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>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:alertSet>
|
||||
<command:examples>
|
||||
@@ -1649,4 +1708,835 @@ $WriteInfisicalScepMdmProfileToWmiResult = Write-InfisicalScepMdmProfileToWmi @W
|
||||
</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>Start-InfisicalProcess</command:name>
|
||||
<maml:description><maml:para>Starts a child process with Infisical secrets injected directly into its environment block.</maml:para></maml:description>
|
||||
<command:verb>Start</command:verb>
|
||||
<command:noun>InfisicalProcess</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Launches the executable specified by -FilePath, captures stdout/stderr, validates the exit code against -AcceptableExitCodeList, and optionally parses output with -ParsingExpression. InfisicalSecret objects supplied via -Secret (pipeline or by name) are decrypted into the ProcessStartInfo.Environment dictionary only, never written to the user or machine scope; -Prefix prepends a string to each injected variable name, skipping names that already start with -Prefix (case-insensitive) unless -ForcePrefix is supplied. -EnvironmentVariables adds additional non-secret values. -ExecutionTimeout, -NoWait, -CreateNoWindow, -WindowStyle, -Priority, -StandardInputObjectList, -SecureArgumentList, -LogOutput, and -ContinueOnError mirror the semantics of the upstream Start-ProcessWithOutput helper. Honors -WhatIf and -Confirm.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<maml:para>Secret values exist as plain strings only within the child process environment block; they are never persisted to the calling shell, the user scope, or the machine scope. Use -SecureArgumentList to mask sensitive command-line arguments in verbose output.</maml:para>
|
||||
</maml:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 1</maml:title>
|
||||
<dev:code>Get-InfisicalSecret -SecretPath '/build' | Start-InfisicalProcess -FilePath 'dotnet.exe' -ArgumentList @('publish','-c','Release') -AcceptableExitCodeList @('0') -CreateNoWindow</dev:code>
|
||||
<dev:remarks><maml:para>Decrypts every secret at /build, exposes each one as a process environment variable, and runs dotnet publish with no visible window.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 2</maml:title>
|
||||
<dev:code>$Secrets = Get-InfisicalSecret -SecretPath '/runtime'
|
||||
Start-InfisicalProcess -FilePath 'node.exe' -ArgumentList @('app.js') -Secret $Secrets -Prefix 'APP_' -ExecutionTimeout ([TimeSpan]::FromMinutes(5)) -LogOutput</dev:code>
|
||||
<dev:remarks><maml:para>Injects the /runtime secrets as APP_-prefixed environment variables, runs node app.js, and forcibly terminates the process after five minutes if it has not exited.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 3</maml:title>
|
||||
<dev:code>$StartInfisicalProcessParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
$StartInfisicalProcessParameters.FilePath = 'pwsh.exe'
|
||||
$StartInfisicalProcessParameters.ArgumentList = @('-NoProfile','-Command','Write-Host $env:DEPLOY_TOKEN.Length')
|
||||
$StartInfisicalProcessParameters.Secret = Get-InfisicalSecret -SecretPath '/deploy'
|
||||
$StartInfisicalProcessParameters.Prefix = 'DEPLOY_'
|
||||
$StartInfisicalProcessParameters.AcceptableExitCodeList = @('0')
|
||||
$StartInfisicalProcessParameters.CreateNoWindow = $True
|
||||
$StartInfisicalProcessParameters.SecureArgumentList = $True
|
||||
$StartInfisicalProcessParameters.LogOutput = $True
|
||||
$StartInfisicalProcessParameters.Verbose = $True
|
||||
|
||||
$StartInfisicalProcessResult = Start-InfisicalProcess @StartInfisicalProcessParameters</dev:code>
|
||||
<dev:remarks><maml:para>Splatted invocation that runs pwsh with DEPLOY_-prefixed secrets in scope, masks the command line in verbose output, and echoes both stdout and stderr to the verbose stream after exit.</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>Get-InfisicalEnvironmentVariable</command:name>
|
||||
<maml:description><maml:para>Reads an environment variable from the first scope that has a non-empty value (Process > User > Machine).</maml:para></maml:description>
|
||||
<command:verb>Get</command:verb>
|
||||
<command:noun>InfisicalEnvironmentVariable</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Returns the value of -Name from the first scope that contains a non-empty value, checking Process, then User, then Machine in that order. Emits nothing when the variable is missing or blank in every scope, so an assignment yields $null without writing errors or warnings. Platform-unsupported scopes (User and Machine on non-Windows) are silently skipped.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<maml:para>Designed for the same discovery semantics the rest of PSInfisicalAPI uses when resolving connection inputs from the environment. Pipe-friendly: accepts -Name from the pipeline by value and by property name.</maml:para>
|
||||
</maml:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 1</maml:title>
|
||||
<dev:code>$Value = Get-InfisicalEnvironmentVariable -Name 'INFISICAL_CLIENT_ID'</dev:code>
|
||||
<dev:remarks><maml:para>Returns the first non-empty value of INFISICAL_CLIENT_ID across Process, User, and Machine scopes; assigns $null when the variable is unset everywhere.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 2</maml:title>
|
||||
<dev:code>@('INFISICAL_BASE_URI','INFISICAL_PROJECT_ID') | Get-InfisicalEnvironmentVariable</dev:code>
|
||||
<dev:remarks><maml:para>Pipes a list of variable names through the cmdlet and emits one value per name that is set in any scope.</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>Get-InfisicalOrganization</command:name>
|
||||
<maml:description><maml:para>Lists or retrieves Infisical organizations accessible to the current identity.</maml:para></maml:description>
|
||||
<command:verb>Get</command:verb>
|
||||
<command:noun>InfisicalOrganization</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Default (List parameter set) returns every organization the active session can see; visibility is governed by Infisical's role assignments. When -OrganizationId is supplied (Single parameter set) the cmdlet returns one organization. Does not require a project context.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<maml:para>The List-mode result is an array of InfisicalOrganization objects; pipe into Where-Object or Select-Object to filter by Slug, Name, or Id. The cmdlet accepts pipeline input by property name on -OrganizationId.</maml:para>
|
||||
</maml:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 1</maml:title>
|
||||
<dev:code>Get-InfisicalOrganization</dev:code>
|
||||
<dev:remarks><maml:para>Lists every organization the current session can see.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 2</maml:title>
|
||||
<dev:code>Get-InfisicalOrganization -OrganizationId $OrganizationId</dev:code>
|
||||
<dev:remarks><maml:para>Retrieves the canonical record for a single organization by id.</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-InfisicalOrganization</command:name>
|
||||
<maml:description><maml:para>Creates a new Infisical organization.</maml:para></maml:description>
|
||||
<command:verb>New</command:verb>
|
||||
<command:noun>InfisicalOrganization</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Creates a new organization with the supplied name and optional slug. Honors -WhatIf and -Confirm. Requires server-side permission to create organizations.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<maml:para>Slug must be unique server-side; if omitted, the server derives one from the name.</maml:para>
|
||||
</maml:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 1</maml:title>
|
||||
<dev:code>New-InfisicalOrganization -Name 'Acme Corporation'</dev:code>
|
||||
<dev:remarks><maml:para>Creates a new organization named 'Acme Corporation' with a server-derived slug.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 2</maml:title>
|
||||
<dev:code>$NewInfisicalOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
$NewInfisicalOrganizationParameters.Name = 'Acme Corporation'
|
||||
$NewInfisicalOrganizationParameters.Slug = 'acme-corp'
|
||||
$NewInfisicalOrganizationParameters.Verbose = $True
|
||||
|
||||
$NewInfisicalOrganizationResult = New-InfisicalOrganization @NewInfisicalOrganizationParameters</dev:code>
|
||||
<dev:remarks><maml:para>Creates an organization with an explicit slug.</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>Update-InfisicalOrganization</command:name>
|
||||
<maml:description><maml:para>Updates mutable attributes on an existing Infisical organization.</maml:para></maml:description>
|
||||
<command:verb>Update</command:verb>
|
||||
<command:noun>InfisicalOrganization</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Updates the name or slug of an organization. -OrganizationId is required. Only bound parameters are transmitted. Honors -WhatIf and -Confirm.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<maml:para>Renaming or re-slugging an organization may affect billing exports and identity URLs; coordinate with downstream consumers.</maml:para>
|
||||
</maml:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 1</maml:title>
|
||||
<dev:code>Update-InfisicalOrganization -OrganizationId $OrganizationId -Name 'Acme Corp.'</dev:code>
|
||||
<dev:remarks><maml:para>Renames the supplied organization.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 2</maml:title>
|
||||
<dev:code>$UpdateInfisicalOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
$UpdateInfisicalOrganizationParameters.OrganizationId = $OrganizationId
|
||||
$UpdateInfisicalOrganizationParameters.Name = 'Acme Corp.'
|
||||
$UpdateInfisicalOrganizationParameters.Slug = 'acme-corp'
|
||||
$UpdateInfisicalOrganizationParameters.Verbose = $True
|
||||
|
||||
$UpdateInfisicalOrganizationResult = Update-InfisicalOrganization @UpdateInfisicalOrganizationParameters</dev:code>
|
||||
<dev:remarks><maml:para>Renames the organization and updates its slug.</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-InfisicalOrganization</command:name>
|
||||
<maml:description><maml:para>Deletes an Infisical organization.</maml:para></maml:description>
|
||||
<command:verb>Remove</command:verb>
|
||||
<command:noun>InfisicalOrganization</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Deletes an organization by id. -OrganizationId is required. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed organization id.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<maml:para>This is irreversible and removes all projects, sub-organizations, secrets, and identities owned by the organization. Honors -WhatIf and -Confirm.</maml:para>
|
||||
</maml:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 1</maml:title>
|
||||
<dev:code>Remove-InfisicalOrganization -OrganizationId $OrganizationId -Confirm:$False</dev:code>
|
||||
<dev:remarks><maml:para>Deletes the supplied organization without prompting.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 2</maml:title>
|
||||
<dev:code>$RemoveInfisicalOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
$RemoveInfisicalOrganizationParameters.OrganizationId = $OrganizationId
|
||||
$RemoveInfisicalOrganizationParameters.PassThru = $True
|
||||
$RemoveInfisicalOrganizationParameters.Confirm = $False
|
||||
$RemoveInfisicalOrganizationParameters.Verbose = $True
|
||||
|
||||
$RemoveInfisicalOrganizationResult = Remove-InfisicalOrganization @RemoveInfisicalOrganizationParameters</dev:code>
|
||||
<dev:remarks><maml:para>Removes the organization without confirmation and emits the removed organization id for logging.</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>Get-InfisicalSubOrganization</command:name>
|
||||
<maml:description><maml:para>Lists or retrieves Infisical sub-organizations accessible to the current identity.</maml:para></maml:description>
|
||||
<command:verb>Get</command:verb>
|
||||
<command:noun>InfisicalSubOrganization</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Default (List parameter set) returns every sub-organization the active session can see. Optional -Limit, -Offset, -Search, -OrderBy, -OrderDirection, and -IsAccessible are forwarded to the server as query parameters. When -SubOrganizationId is supplied (Single parameter set) the cmdlet returns one sub-organization. Does not require a project context.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<maml:para>Sub-organizations are a beta Infisical feature. The List result is an array of InfisicalSubOrganization objects; pipe into Where-Object or Select-Object to filter further. The cmdlet accepts pipeline input by property name on -SubOrganizationId.</maml:para>
|
||||
</maml:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 1</maml:title>
|
||||
<dev:code>Get-InfisicalSubOrganization</dev:code>
|
||||
<dev:remarks><maml:para>Lists every sub-organization the current session can see.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 2</maml:title>
|
||||
<dev:code>Get-InfisicalSubOrganization -SubOrganizationId $SubOrganizationId</dev:code>
|
||||
<dev:remarks><maml:para>Retrieves the canonical record for a single sub-organization by id.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 3</maml:title>
|
||||
<dev:code>Get-InfisicalSubOrganization -Search 'platform' -OrderBy 'name' -OrderDirection 'asc' -Limit 25 -IsAccessible</dev:code>
|
||||
<dev:remarks><maml:para>Lists up to 25 sub-organizations matching 'platform', sorted ascending by name, restricted to those the current identity has access to.</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-InfisicalSubOrganization</command:name>
|
||||
<maml:description><maml:para>Creates a new Infisical sub-organization.</maml:para></maml:description>
|
||||
<command:verb>New</command:verb>
|
||||
<command:noun>InfisicalSubOrganization</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Creates a sub-organization with the supplied name and slug. Both are required by the server. Honors -WhatIf and -Confirm.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<maml:para>Slug must be unique within the parent organization. Sub-organizations are a beta Infisical feature.</maml:para>
|
||||
</maml:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 1</maml:title>
|
||||
<dev:code>New-InfisicalSubOrganization -Name 'Platform Engineering' -Slug 'platform-eng'</dev:code>
|
||||
<dev:remarks><maml:para>Creates a new sub-organization named 'Platform Engineering' with slug 'platform-eng'.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 2</maml:title>
|
||||
<dev:code>$NewInfisicalSubOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
$NewInfisicalSubOrganizationParameters.Name = 'Platform Engineering'
|
||||
$NewInfisicalSubOrganizationParameters.Slug = 'platform-eng'
|
||||
$NewInfisicalSubOrganizationParameters.Verbose = $True
|
||||
|
||||
$NewInfisicalSubOrganizationResult = New-InfisicalSubOrganization @NewInfisicalSubOrganizationParameters</dev:code>
|
||||
<dev:remarks><maml:para>Splatted invocation that creates a sub-organization and logs the request via the verbose stream.</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>Update-InfisicalSubOrganization</command:name>
|
||||
<maml:description><maml:para>Updates mutable attributes on an existing Infisical sub-organization.</maml:para></maml:description>
|
||||
<command:verb>Update</command:verb>
|
||||
<command:noun>InfisicalSubOrganization</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Updates the name or slug of a sub-organization. -SubOrganizationId is required. Only bound parameters are transmitted. Honors -WhatIf and -Confirm.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<maml:para>Sub-organizations are a beta Infisical feature; coordinate slug changes with downstream consumers that pin the slug in scripts or configuration files.</maml:para>
|
||||
</maml:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 1</maml:title>
|
||||
<dev:code>Update-InfisicalSubOrganization -SubOrganizationId $SubOrganizationId -Name 'Platform (v2)'</dev:code>
|
||||
<dev:remarks><maml:para>Renames the supplied sub-organization.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 2</maml:title>
|
||||
<dev:code>$UpdateInfisicalSubOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
$UpdateInfisicalSubOrganizationParameters.SubOrganizationId = $SubOrganizationId
|
||||
$UpdateInfisicalSubOrganizationParameters.Name = 'Platform (v2)'
|
||||
$UpdateInfisicalSubOrganizationParameters.Slug = 'platform-v2'
|
||||
$UpdateInfisicalSubOrganizationParameters.Verbose = $True
|
||||
|
||||
$UpdateInfisicalSubOrganizationResult = Update-InfisicalSubOrganization @UpdateInfisicalSubOrganizationParameters</dev:code>
|
||||
<dev:remarks><maml:para>Renames the sub-organization and updates its slug in a single call.</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-InfisicalSubOrganization</command:name>
|
||||
<maml:description><maml:para>Deletes an Infisical sub-organization.</maml:para></maml:description>
|
||||
<command:verb>Remove</command:verb>
|
||||
<command:noun>InfisicalSubOrganization</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Deletes a sub-organization by id. -SubOrganizationId is required. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed sub-organization id.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<maml:para>This is destructive and removes all projects, secrets, and identities scoped to the sub-organization. Honors -WhatIf and -Confirm.</maml:para>
|
||||
</maml:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 1</maml:title>
|
||||
<dev:code>Remove-InfisicalSubOrganization -SubOrganizationId $SubOrganizationId -Confirm:$False</dev:code>
|
||||
<dev:remarks><maml:para>Deletes the supplied sub-organization without prompting.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 2</maml:title>
|
||||
<dev:code>$RemoveInfisicalSubOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
$RemoveInfisicalSubOrganizationParameters.SubOrganizationId = $SubOrganizationId
|
||||
$RemoveInfisicalSubOrganizationParameters.PassThru = $True
|
||||
$RemoveInfisicalSubOrganizationParameters.Confirm = $False
|
||||
$RemoveInfisicalSubOrganizationParameters.Verbose = $True
|
||||
|
||||
$RemoveInfisicalSubOrganizationResult = Remove-InfisicalSubOrganization @RemoveInfisicalSubOrganizationParameters</dev:code>
|
||||
<dev:remarks><maml:para>Removes the sub-organization without confirmation and emits the removed id for logging.</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>Get-InfisicalSANList</command:name>
|
||||
<maml:description><maml:para>Builds a deduplicated list of Subject Alternative Name candidates for the local device.</maml:para></maml:description>
|
||||
<command:verb>Get</command:verb>
|
||||
<command:noun>InfisicalSANList</command:noun>
|
||||
</command:details>
|
||||
<maml:description>
|
||||
<maml:para>Returns, in order: the local device name; the device name suffixed with each non-empty DNS suffix found on any operational (non-loopback) network adapter and the system primary domain; every IPv4 unicast address whose first octets fall within RFC 1918 (10/8, 172.16/12, 192.168/16) or CGNAT (100.64/10); and the IPv4 and IPv6 loopback addresses (127.0.0.1, ::1). Optional -InclusionExpression and -ExclusionExpression regex filters are applied in that order after collection, before output. Suitable as a one-shot SAN provider for Request-InfisicalCertificate -DnsName.</maml:para>
|
||||
</maml:description>
|
||||
<maml:alertSet>
|
||||
<maml:title>Notes</maml:title>
|
||||
<maml:alert>
|
||||
<maml:para>Output is a single strongly-typed System.String[] array (emitted non-enumerated) so it round-trips into [System.Collections.Generic.List[string]]::AddRange() and binds directly to string[] parameters such as Request-InfisicalCertificate -DnsName. The device name comes first so it can be reused as a CommonName. Routable public IPv4 addresses, link-local addresses, and IPv6 unicast addresses other than loopback are intentionally excluded. -InclusionExpression and -ExclusionExpression are case-insensitive .NET regular expressions; inclusion is applied first, then exclusion.</maml:para>
|
||||
</maml:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 1</maml:title>
|
||||
<dev:code>Get-InfisicalSANList</dev:code>
|
||||
<dev:remarks><maml:para>Returns the SAN candidate list for the current device.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 2</maml:title>
|
||||
<dev:code>$Sans = Get-InfisicalSANList
|
||||
Request-InfisicalCertificate -ProjectId $ProjectId -CertificateAuthorityId $CaId -CommonName $Sans[0] -DnsName $Sans -Ttl '90d'</dev:code>
|
||||
<dev:remarks><maml:para>Captures the SAN list, then uses the device name as the CommonName and the full list as DnsName when requesting a certificate.</maml:para></dev:remarks>
|
||||
</command:example>
|
||||
<command:example>
|
||||
<maml:title>EXAMPLE 3</maml:title>
|
||||
<dev:code>$GetInfisicalSANListParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
$GetInfisicalSANListParameters.InclusionExpression = '\.gracesolution\.prv$|^10\.|^172\.'
|
||||
$GetInfisicalSANListParameters.ExclusionExpression = '^127\.|^::1$'
|
||||
$Sans = Get-InfisicalSANList @GetInfisicalSANListParameters</dev:code>
|
||||
<dev:remarks><maml:para>Keeps only entries ending in the corporate DNS suffix or sitting in the 10/8 or 172/12 ranges, then drops loopback. Filters are case-insensitive and applied in fetch -> include -> exclude -> output order.</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-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>
|
||||
|
||||
@@ -84,6 +84,39 @@ EXAMPLES
|
||||
Get-InfisicalSecrets |
|
||||
Export-InfisicalSecrets -Path .\secrets.env -Format Env
|
||||
|
||||
ERROR HANDLING AND STREAMS
|
||||
Every cmdlet derives from PSCmdlet, so the common parameters are bound:
|
||||
-Verbose, -Debug, -ErrorAction, -ErrorVariable, -WarningAction,
|
||||
-WarningVariable, -InformationAction, -InformationVariable, -OutVariable,
|
||||
and -PipelineVariable, plus -WhatIf/-Confirm where ShouldProcess applies.
|
||||
|
||||
Output is separated by stream so those parameters mean what they say:
|
||||
|
||||
Error The failure itself, once, as a non-terminating ErrorRecord.
|
||||
Warning Advisories that are not failures.
|
||||
Verbose Request/response trace and the trail leading up to a failure.
|
||||
Debug Low-level detail.
|
||||
|
||||
Operation failures are NON-TERMINATING, so -ErrorAction decides the
|
||||
outcome:
|
||||
|
||||
Continue (default) Error is written; a pipeline keeps processing.
|
||||
SilentlyContinue Nothing printed; still in $Error/-ErrorVariable.
|
||||
Ignore Nothing printed and nothing recorded.
|
||||
Stop Promoted to terminating; try/catch catches it.
|
||||
|
||||
To catch a failure you must ask for it:
|
||||
|
||||
try {
|
||||
Request-InfisicalCertificate @Parameters -ErrorAction Stop
|
||||
} catch [PSInfisicalAPI.Errors.InfisicalApiException] {
|
||||
"HTTP $($_.Exception.StatusCode): $($_.Exception.ApiErrorMessage)"
|
||||
}
|
||||
|
||||
The ErrorRecord carries the API detail, so log scraping is unnecessary:
|
||||
$Error[0].Exception exposes StatusCode, ApiErrorCode, ApiErrorMessage, and
|
||||
ApiRequestId on InfisicalApiException.
|
||||
|
||||
SECURITY NOTES
|
||||
- SecureString is used for ClientSecret, AccessToken, and any secret
|
||||
payloads returned by the API.
|
||||
|
||||
@@ -26,7 +26,7 @@ Import-Module -Name .\Module\PSInfisicalAPI
|
||||
|
||||
## Cmdlets
|
||||
|
||||
The module exports 37 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
|
||||
|
||||
@@ -47,6 +47,24 @@ The module exports 37 cmdlets. Discovery cmdlets (`Get-Infisical*`) use a `List`
|
||||
| `ConvertTo-InfisicalSecretDictionary` | Converts a stream of InfisicalSecret objects into a name-keyed Dictionary of SecureString or plain text values. |
|
||||
| `Export-InfisicalSecrets` | Exports InfisicalSecret objects to disk or environment variables in a chosen file format. |
|
||||
|
||||
### Organizations
|
||||
|
||||
| Cmdlet | Purpose |
|
||||
| ------------------------------ | -------------------------------------------------------------------------------------------------- |
|
||||
| `Get-InfisicalOrganization` | Lists or retrieves Infisical organizations accessible to the current identity. |
|
||||
| `New-InfisicalOrganization` | Creates a new Infisical organization. |
|
||||
| `Update-InfisicalOrganization` | Updates the name or slug of an existing Infisical organization. |
|
||||
| `Remove-InfisicalOrganization` | Deletes an Infisical organization. |
|
||||
|
||||
### Sub-Organizations
|
||||
|
||||
| Cmdlet | Purpose |
|
||||
| --------------------------------- | -------------------------------------------------------------------------------------------------- |
|
||||
| `Get-InfisicalSubOrganization` | Lists or retrieves Infisical sub-organizations, with optional search, paging, and ordering filters. |
|
||||
| `New-InfisicalSubOrganization` | Creates a new Infisical sub-organization. |
|
||||
| `Update-InfisicalSubOrganization` | Updates the name or slug of an existing Infisical sub-organization. |
|
||||
| `Remove-InfisicalSubOrganization` | Deletes an Infisical sub-organization. |
|
||||
|
||||
### Projects
|
||||
|
||||
| Cmdlet | Purpose |
|
||||
@@ -98,6 +116,51 @@ The module exports 37 cmdlets. Discovery cmdlets (`Get-Infisical*`) use a `List`
|
||||
| `Get-InfisicalScepMdmProfile` | Projects an Infisical certificate profile into a Windows SCEP MDM profile model. |
|
||||
| `Export-InfisicalScepMdmProfile` | Writes a SCEP MDM profile to disk as a SyncML payload suitable for MDM delivery. |
|
||||
| `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`. |
|
||||
|
||||
### PKI configuration
|
||||
|
||||
Creating and changing the objects a Certificate Manager project is built from. `-ProjectId` is optional on all of them, and every one honours `-WhatIf`.
|
||||
|
||||
| Cmdlet | Purpose |
|
||||
| ---------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
||||
| `New-InfisicalCertificateAuthority` | Creates a root or intermediate internal CA, signing a subordinate with its parent so it can issue. |
|
||||
| `Set-InfisicalCertificateAuthority` | Renames an internal CA or changes its status. |
|
||||
| `Remove-InfisicalCertificateAuthority` | Deletes an internal CA. |
|
||||
| `New-InfisicalCertificatePolicy` | Creates a certificate policy constraining subject, SANs, key usages, and validity. |
|
||||
| `Set-InfisicalCertificatePolicy` | Updates a certificate policy; only supplied constraints are sent. |
|
||||
| `Remove-InfisicalCertificatePolicy` | Deletes a certificate policy. |
|
||||
| `New-InfisicalCertificateProfile` | Creates a certificate profile binding an issuing CA to a policy for enrollment. |
|
||||
| `Set-InfisicalCertificateProfile` | Updates a certificate profile. |
|
||||
| `Remove-InfisicalCertificateProfile` | Deletes a certificate profile. |
|
||||
| `New-InfisicalCertificateApplication` | Creates a certificate application and optionally attaches profiles. |
|
||||
| `Set-InfisicalCertificateApplication` | Renames an application, or attaches and detaches profiles. |
|
||||
| `Remove-InfisicalCertificateApplication` | Deletes a certificate application. |
|
||||
| `New-InfisicalPkiSubscriber` | Creates a PKI subscriber: one named identity with a fixed common name. |
|
||||
| `Set-InfisicalPkiSubscriber` | Updates a PKI subscriber. |
|
||||
| `Remove-InfisicalPkiSubscriber` | Deletes a PKI subscriber. |
|
||||
|
||||
Constraint dictionaries take `Allowed`, `Required`, and `Denied` in whatever casing reads naturally — they reach the API lower-cased — and an empty list is omitted rather than sent as "allow nothing":
|
||||
|
||||
```powershell
|
||||
$Root = New-InfisicalCertificateAuthority -Name 'root-ca' -Type Root -CommonName 'Contoso Root CA' -Organization 'Contoso' -Country 'US'
|
||||
$Ca = New-InfisicalCertificateAuthority -Name 'issuing-ca' -Type Intermediate -ParentCaId $Root.Id -CommonName 'Contoso Issuing CA'
|
||||
|
||||
$Policy = New-InfisicalCertificatePolicy -Name 'server-auth' -MaxValidity '90d' `
|
||||
-KeyAlgorithm 'RSA_2048','EC_secp384r1' `
|
||||
-KeyUsage @{ Required = @('digital_signature','key_encipherment') } `
|
||||
-ExtendedKeyUsage @{ Required = @('server_auth','client_auth') }
|
||||
|
||||
$CertificateProfile = New-InfisicalCertificateProfile -Slug 'server-auth' -CertificatePolicyId $Policy.Id -CaId $Ca.Id -AutoRenew -RenewBeforeDays 14
|
||||
$Application = New-InfisicalCertificateApplication -Name 'platform' -ProfileId $CertificateProfile.Id
|
||||
```
|
||||
|
||||
The intermediate comes back ready to issue: Infisical creates a subordinate pending a certificate, and `New-InfisicalCertificateAuthority` performs the remaining sequence — read the CSR, sign it with `-ParentCaId`, import the result.
|
||||
### Process
|
||||
|
||||
| Cmdlet | Purpose |
|
||||
| ------------------------ | -------------------------------------------------------------------------------------------------- |
|
||||
| `Start-InfisicalProcess` | Launches a child process with Infisical secrets injected directly into its environment block, capturing stdout/stderr and validating the exit code. |
|
||||
|
||||
Use `Get-Help <Cmdlet> -Full` for parameter details and `Get-Help about_PSInfisicalAPI` for the module overview.
|
||||
|
||||
@@ -109,16 +172,466 @@ $secureSecret = Read-Host -AsSecureString 'Client Secret'
|
||||
$connection = Connect-Infisical `
|
||||
-BaseUri 'https://app.infisical.com' `
|
||||
-OrganizationId '00000000-0000-0000-0000-000000000000' `
|
||||
-ProjectId '11111111-1111-1111-1111-111111111111' `
|
||||
-Environment 'dev' `
|
||||
-ClientId 'machine-identity-client-id' `
|
||||
-ClientSecret $secureSecret `
|
||||
-PassThru
|
||||
|
||||
Get-InfisicalSecret -SecretPath '/'
|
||||
Get-InfisicalSecret -ProjectId '11111111-1111-1111-1111-111111111111' -Environment 'dev' -SecretPath '/'
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
This is the shape to use for **fleet enrollment**, where each machine needs its own common name. See [Choosing an issuance path](#choosing-an-issuance-path) — a PKI subscriber is *not* the right tool for this, because it pins one fixed common name.
|
||||
|
||||
```powershell
|
||||
$ConnectInfisicalParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
$ConnectInfisicalParameters.BaseUri = 'https://app.infisical.com'
|
||||
$ConnectInfisicalParameters.OrganizationId = '00000000-0000-0000-0000-000000000000'
|
||||
$ConnectInfisicalParameters.ClientId = 'machine-identity-client-id'
|
||||
$ConnectInfisicalParameters.ClientSecret = ConvertTo-SecureString -String 'ClientSecret' -AsPlainText -Force
|
||||
$ConnectInfisicalParameters.PassThru = $True
|
||||
$ConnectInfisicalParameters.Verbose = $True
|
||||
|
||||
$Connection = Connect-Infisical @ConnectInfisicalParameters
|
||||
|
||||
$Application = Get-InfisicalCertificateApplication | Where-Object {($_.Name -ieq 'platform')}
|
||||
|
||||
$CertificateProfile = Get-InfisicalCertificateProfile -ApplicationId ($Application.Id) -IncludeConfigs | Where-Object {($_.EnrollmentType -ieq 'api') -and ($_.Slug -imatch 'server')} | Select-Object -First 1
|
||||
|
||||
$SanList = Get-InfisicalSANList
|
||||
|
||||
$RequestInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
$RequestInfisicalCertificateParameters.CertificateProfileId = $CertificateProfile.Id
|
||||
$RequestInfisicalCertificateParameters.CommonName = $Env:ComputerName.ToUpper()
|
||||
$RequestInfisicalCertificateParameters.DnsName = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
|
||||
$RequestInfisicalCertificateParameters.DnsName.AddRange($SanList)
|
||||
$RequestInfisicalCertificateParameters.DnsName.Add('app.contoso.com')
|
||||
$RequestInfisicalCertificateParameters.Ttl = '90d'
|
||||
$RequestInfisicalCertificateParameters.Metadata = [Ordered]@{ Environment = 'Production'; Owner = 'Platform' }
|
||||
$RequestInfisicalCertificateParameters.Install = $True
|
||||
$RequestInfisicalCertificateParameters.InstallChain = $True
|
||||
$RequestInfisicalCertificateParameters.Verbose = $True
|
||||
|
||||
$Certificate = Request-InfisicalCertificate @RequestInfisicalCertificateParameters
|
||||
|
||||
$Null = Disconnect-Infisical -Verbose
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
## 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
|
||||
|
||||
```text
|
||||
Id : 11111111-1111-1111-1111-111111111111
|
||||
ProjectId : 00000000-0000-0000-0000-000000000000
|
||||
Name : platform
|
||||
Description :
|
||||
ProfileCount : 3
|
||||
MemberCount : 2
|
||||
CertificateCount : 0
|
||||
CreatedAtUtc : 7/30/2026 10:05:37 PM +00:00
|
||||
UpdatedAtUtc : 7/30/2026 10:05:37 PM +00:00
|
||||
|
||||
Id : 44444444-4444-4444-4444-444444444444
|
||||
ProjectId : 00000000-0000-0000-0000-000000000000
|
||||
CaId : 33333333-3333-3333-3333-333333333333
|
||||
CertificatePolicyId : 55555555-5555-5555-5555-555555555555
|
||||
Slug : serverauthentication
|
||||
Description :
|
||||
EnrollmentType : api
|
||||
IssuerType : ca
|
||||
EstConfigId :
|
||||
ApiConfigId : 66666666-6666-6666-6666-666666666666
|
||||
AcmeConfigId :
|
||||
ScepConfigId :
|
||||
CreatedAtUtc : 3/25/2026 4:53:53 PM +00:00
|
||||
UpdatedAtUtc : 3/25/2026 4:53:53 PM +00:00
|
||||
Defaults : PSInfisicalAPI.Models.InfisicalCertificateProfileDefaults
|
||||
CertificateAuthority : PSInfisicalAPI.Models.InfisicalCertificateAuthoritySummary
|
||||
CertificatePolicy :
|
||||
ApiConfig : PSInfisicalAPI.Models.InfisicalCertificateProfileApiConfig
|
||||
|
||||
WEB01
|
||||
10.20.30.40
|
||||
WEB01.contoso.com
|
||||
127.0.0.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: [...] - [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: [...] - [Information] - [PkiClient] - Infisical certificate search was successful.
|
||||
VERBOSE: [...] - [Information] - [RequestInfisicalCertificateCmdlet] - Process is elevated; defaulting -StoreLocation to LocalMachine. Pass -StoreLocation explicitly to override.
|
||||
VERBOSE: [...] - [Information] - [RequestInfisicalCertificateCmdlet] - Issuing via certificate profile '44444444-4444-4444-4444-444444444444' in project '00000000-0000-0000-0000-000000000000'.
|
||||
VERBOSE: Performing the operation "Request new certificate" on target "certificate profile '44444444-4444-4444-4444-444444444444' for CN=WEB01".
|
||||
VERBOSE: [...] - [Information] - [PkiClient] - Attempting to issue certificate via profile '44444444-4444-4444-4444-444444444444'. Please Wait...
|
||||
VERBOSE: [...] - [Verbose] - [HttpClient] - Attempting HTTP POST to https://infisical.contoso.com/api/v1/cert-manager/certificates. Please Wait...
|
||||
VERBOSE: [...] - [Verbose] - [HttpClient] - HTTP POST completed with status 200.
|
||||
VERBOSE: [...] - [Information] - [PkiClient] - Infisical certificate issuance (profile) was successful.
|
||||
VERBOSE: [...] - [Information] - [RequestInfisicalCertificateCmdlet] - Installed certificate to LocalMachine\My [F480A920DFB41EA8EE3E9178C1BC6A5EC7055B96].
|
||||
VERBOSE: [...] - [Information] - [RequestInfisicalCertificateCmdlet] - Installed certificate to LocalMachine\CertificateAuthority [89A486A532D94EFE4391BEF2EA7F5E7E2B654AB0].
|
||||
VERBOSE: [...] - [Information] - [RequestInfisicalCertificateCmdlet] - Installed certificate to LocalMachine\Root [1F3A77B0C2D45E6819AB3C7D0E5F2A9B4C81D6E7].
|
||||
```
|
||||
|
||||
### Where certificates get installed
|
||||
|
||||
When `-StoreLocation` is not supplied, the cmdlet picks it from the process's elevation, and says which it chose on the verbose stream:
|
||||
|
||||
| Session | Leaf | Intermediates | Roots |
|
||||
| ------------- | -------------------------- | -------------------------------------------- | ---------------------------- |
|
||||
| Elevated | `LocalMachine\My` | `LocalMachine\CertificateAuthority` | `LocalMachine\Root` |
|
||||
| Not elevated | `CurrentUser\My` | `CurrentUser\CertificateAuthority` | `CurrentUser\Root` |
|
||||
|
||||
The routing split is deliberate:
|
||||
|
||||
- **The leaf** honours `-StoreName` (default `My`) and lands in the resolved location.
|
||||
- **Chain members** ignore `-StoreName` and 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. A chain of any depth is walked, so an issuing subordinate installs alongside every CA above it up to the root.
|
||||
|
||||
`-StoreLocation` still applies to all of them, so an elevated run puts the whole chain machine-wide and a non-elevated run puts it all under the user.
|
||||
|
||||
Issuers are installed **before** the leaf, so the certificate is chainable the moment it appears in the store. Afterwards the chain is validated against the machine's own stores, and an incomplete result is called out by name rather than left to be discovered in `certmgr`:
|
||||
|
||||
```text
|
||||
WARNING: [...] Certificate chain is incomplete (PartialChain). The highest certificate installed is
|
||||
'CN=Contoso Intermediate Certificate Authority, OU=IT, O=Contoso, C=US', whose issuer
|
||||
'CN=Contoso Root Certificate Authority, OU=IT, O=Contoso, C=US' is not present in the trusted stores.
|
||||
Windows will report "The issuer of this certificate could not be found" until that issuer is installed.
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
> **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.
|
||||
|
||||
### Choosing an issuance path
|
||||
|
||||
`Request-InfisicalCertificate` has three mutually exclusive issuance parameter sets. The deciding question is **whether the common name varies per request**:
|
||||
|
||||
| Parameter | Common name | Use when |
|
||||
| -------------------------- | ------------------------------------ | ------------------------------------------------------------------------ |
|
||||
| `-CertificateProfileId` | **Per request**, constrained by policy | Fleet enrollment — many machines, each with its own CN. Works on any 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. |
|
||||
|
||||
**A PKI subscriber is a per-identity object, not a fleet template.** `signSubscriberCert` rejects any CSR whose CN differs from the subscriber's:
|
||||
|
||||
```text
|
||||
Common name (CN) in the CSR does not match the subscriber's common name
|
||||
```
|
||||
|
||||
It also allowlists SANs — every `dNSName`/`email` SAN in the CSR must appear in the subscriber's `subjectAlternativeNames` — and requires CSR key usages to be a subset of the subscriber's. (IP SANs are not covered by that check.) Enrolling *N* machines through subscribers therefore means creating *N* subscribers. Prefer a profile.
|
||||
|
||||
There is no `-CertificateTemplateId` parameter. Infisical's REST API exposes no template-based issuance route — templates are consumed internally by EST and subscribers — so when the API says *"Certificate template or subscriber is required for issuance"*, the reachable answers are a profile, a subscriber, or direct issuance.
|
||||
|
||||
The cmdlet resolves and reports the issuer before generating a keypair, so `-Verbose` tells you exactly what will sign the request:
|
||||
|
||||
```text
|
||||
VERBOSE: [...] - [Information] - [RequestInfisicalCertificateCmdlet] - Issuing via certificate profile 'a1b2c3d4-...' in project '2122628e-...'.
|
||||
VERBOSE: [...] - [Information] - [RequestInfisicalCertificateCmdlet] - Issuing directly via certificate authority 'intermediate-ca' (bf661d78-...); direct issuance is enabled.
|
||||
```
|
||||
|
||||
`-WhatIf` names the same issuer without issuing anything:
|
||||
|
||||
```powershell
|
||||
Request-InfisicalCertificate @RequestInfisicalCertificateParameters -WhatIf
|
||||
# What if: Performing the operation "Request new certificate" on target
|
||||
# "certificate profile 'a1b2c3d4-...' for CN=WEB01".
|
||||
```
|
||||
|
||||
#### Discovering profiles (recommended)
|
||||
|
||||
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
|
||||
Get-InfisicalCertificateProfile | Format-Table Id, Slug, CaId, EnrollmentType
|
||||
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:
|
||||
|
||||
```ts
|
||||
if (!isFromProfile && !ca.enableDirectIssuance && !certificateTemplate) { throw ... }
|
||||
```
|
||||
|
||||
So a profile issues successfully against a CA whose `EnableDirectIssuance` is `False`. If a project has no profiles, create a policy then a profile under **Certificate Management** in the Infisical UI.
|
||||
|
||||
#### Discovering subscribers
|
||||
|
||||
```powershell
|
||||
Get-InfisicalPkiSubscriber |
|
||||
Format-Table Name, CommonName, Status, Ttl, CaId
|
||||
```
|
||||
|
||||
Pass the subscriber's `Name` to `-PkiSubscriberSlug`, and set `-CommonName` to exactly that subscriber's `CommonName`. Because the subscriber owns the lifetime and usage policy, `-Ttl`, `-KeyUsage`, and `-ExtendedKeyUsage` are not accepted on this parameter set — set them on the subscriber in Infisical instead.
|
||||
|
||||
An empty result means the project has no subscribers; create one under **Certificate Management > Subscribers**. This module is read-only for subscribers, so creation is UI or raw API (`POST /api/v1/pki/subscribers`).
|
||||
|
||||
#### Direct issuance on a CA
|
||||
|
||||
Direct issuance lets a CA sign a bare CSR with no profile, subscriber, or template in front of it. When it is off, Infisical rejects the request with `400 Certificate template or subscriber is required for issuance`; this module catches that before building a CSR.
|
||||
|
||||
> **There is no UI toggle or API field for this.** `enableDirectIssuance` appears in no create or update schema — the generic CA schemas accept only `name` and `status`. It is set at CA creation (the column defaults to `true`) and is not editable afterwards through the public API.
|
||||
|
||||
A CA can therefore read `False` for a reason that is not obvious. Migration `20250521110635_add-external-ca-pki.ts` renamed the older `requireTemplateForIssuance` column to `enableDirectIssuance` and **inverted** every existing value:
|
||||
|
||||
```ts
|
||||
t.renameColumn("requireTemplateForIssuance", "enableDirectIssuance");
|
||||
...
|
||||
.update({ name: slugifiedName, enableDirectIssuance: !ca.enableDirectIssuance });
|
||||
```
|
||||
|
||||
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
|
||||
Get-InfisicalCertificateAuthority -Kind Internal |
|
||||
Format-Table Name, CommonName, Status, EnableDirectIssuance
|
||||
|
||||
$Ca = Get-InfisicalCertificateAuthority -Kind Internal |
|
||||
Where-Object {($_.EnableDirectIssuance -eq $True)} |
|
||||
Select-Object -First 1
|
||||
|
||||
$RequestInfisicalCertificateParameters.CertificateAuthorityId = $Ca.Id
|
||||
$RequestInfisicalCertificateParameters.Ttl = '90d' # required by the CA path
|
||||
```
|
||||
|
||||
### Subject and SAN handling
|
||||
|
||||
- `-CommonName` takes the bare value (`WEB01.contoso.com`), not an RDN. `CN=WEB01` is accepted and normalized, since the CSR builder adds the `CN=` prefix itself.
|
||||
- `Get-InfisicalSANList` returns DNS names *and* IP addresses in one list. Passing the whole list to `-DnsName` is fine: IP literals are detected and emitted as `iPAddress` SAN entries rather than malformed `dNSName` entries.
|
||||
- `-Ttl` (or `-NotAfter`) applies to the `-CertificateAuthorityId` and `-CertificateProfileId` paths. Subscriber-issued certificates take their lifetime from the subscriber definition.
|
||||
|
||||
## Diagnostics and error handling
|
||||
|
||||
Every cmdlet derives from `PSCmdlet`, so the full set of common parameters is bound: `-Verbose`, `-Debug`, `-ErrorAction`, `-ErrorVariable`, `-WarningAction`, `-WarningVariable`, `-InformationAction`, `-InformationVariable`, `-OutVariable`, `-PipelineVariable`, and `-WhatIf`/`-Confirm` on the cmdlets that declare `SupportsShouldProcess`.
|
||||
|
||||
Output is routed by stream so those parameters mean what they say:
|
||||
|
||||
| Stream | Carries | Controlled by |
|
||||
| ----------- | ----------------------------------------------------------------------- | ------------------------------------ |
|
||||
| Error | The failure itself, once, as a non-terminating `ErrorRecord` | `-ErrorAction`, `-ErrorVariable`, `2>` |
|
||||
| Warning | Genuine advisories that are not failures (e.g. issuance returned no certificate) | `-WarningAction`, `-WarningVariable` |
|
||||
| Verbose | Request/response trace and the diagnostic trail leading up to a failure | `-Verbose` |
|
||||
| Debug | Low-level detail | `-Debug` |
|
||||
|
||||
A failed call surfaces exactly one error. The `[Error]`-tagged diagnostic lines that precede it are on the verbose stream, so they appear only under `-Verbose` and never compete with the `ErrorRecord`:
|
||||
|
||||
```powershell
|
||||
# One error, no warning noise.
|
||||
Request-InfisicalCertificate @Parameters -ErrorVariable Failure -ErrorAction SilentlyContinue
|
||||
|
||||
# The ErrorRecord carries the API detail; no log scraping required.
|
||||
$Failure[0].Exception.StatusCode # 400
|
||||
$Failure[0].Exception.ApiErrorCode # BadRequest
|
||||
$Failure[0].Exception.ApiErrorMessage # Certificate template or subscriber is required for issuance
|
||||
$Failure[0].Exception.ApiRequestId # req-SSPFN1gc2zHvkV
|
||||
```
|
||||
|
||||
### `-ErrorAction` decides the outcome
|
||||
|
||||
Operation failures are reported as **non-terminating** errors, so `-ErrorAction` (or `$ErrorActionPreference`) governs what happens, exactly as it does for built-in cmdlets:
|
||||
|
||||
| `-ErrorAction` | Behavior |
|
||||
| ------------------ | ------------------------------------------------------------------------- |
|
||||
| `Continue` (default) | Error is written; a pipeline keeps processing its remaining input |
|
||||
| `SilentlyContinue` | Nothing is printed; the error is still in `$Error` and `-ErrorVariable` |
|
||||
| `Ignore` | Nothing is printed and nothing is recorded in `$Error` |
|
||||
| `Stop` | Promoted to a terminating error that `try`/`catch` catches |
|
||||
| `Inquire` | Prompts |
|
||||
|
||||
A failing item does not abort the batch:
|
||||
|
||||
```powershell
|
||||
'web01', 'does-not-exist', 'web02' |
|
||||
ForEach-Object { Get-InfisicalPkiSubscriber -Name $_ -ErrorAction SilentlyContinue }
|
||||
# emits web01 and web02; the failure is available in $Error
|
||||
```
|
||||
|
||||
**To catch failures you must ask for it** with `-ErrorAction Stop` or `$ErrorActionPreference = 'Stop'`:
|
||||
|
||||
```powershell
|
||||
try {
|
||||
$Certificate = Request-InfisicalCertificate @Parameters -ErrorAction Stop
|
||||
} catch [PSInfisicalAPI.Errors.InfisicalApiException] {
|
||||
Write-Warning "Issuance failed with HTTP $($_.Exception.StatusCode): $($_.Exception.ApiErrorMessage)"
|
||||
}
|
||||
```
|
||||
|
||||
> **Breaking change.** Failures were previously terminating, so `try`/`catch` caught them without `-ErrorAction Stop`. Existing `try`/`catch` blocks need `-ErrorAction Stop` added (or `$ErrorActionPreference = 'Stop'` set) to keep catching.
|
||||
|
||||
Exception types are `InfisicalApiException`, `InfisicalAuthenticationException`, `InfisicalHttpException`, `InfisicalSerializationException`, `InfisicalConfigurationException`, `InfisicalExportException`, and `InfisicalImportException`, all deriving from `InfisicalException`.
|
||||
|
||||
## Automatic environment-variable discovery
|
||||
|
||||
When `Connect-Infisical` is invoked with one or more parameters missing (or set to whitespace/empty), the cmdlet searches environment variables and uses the first value it finds. This makes invocation as simple as `Connect-Infisical` when variables are set up in advance.
|
||||
@@ -137,16 +650,15 @@ The resolver matches case-insensitively against patterns aligned with Infisical'
|
||||
|
||||
| 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` |
|
||||
| `ProjectId` | `INFISICAL_PROJECT_ID`, `INFISICAL_WORKSPACE_ID` |
|
||||
| `Environment` | `INFISICAL_ENVIRONMENT`, `INFISICAL_ENV`, `INFISICAL_ENV_SLUG` |
|
||||
| `ClientId` | `INFISICAL_CLIENT_ID`, `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` |
|
||||
| `ClientSecret` | `INFISICAL_CLIENT_SECRET`, `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` |
|
||||
| `AccessToken` | `INFISICAL_TOKEN`, `INFISICAL_ACCESS_TOKEN`, `INFISICAL_AUTH_TOKEN` |
|
||||
| `SecretPath` | `INFISICAL_SECRET_PATH`, `INFISICAL_DEFAULT_SECRET_PATH` |
|
||||
| `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.
|
||||
|
||||
### Zero-configuration example
|
||||
@@ -201,7 +713,7 @@ To add a route:
|
||||
|
||||
### 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
|
||||
[Cmdlet(VerbsCommon.Get, "InfisicalPkiSubscriber", DefaultParameterSetName = "List")]
|
||||
@@ -226,7 +738,7 @@ After adding (or removing) a cmdlet:
|
||||
|
||||
```powershell
|
||||
$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
|
||||
```
|
||||
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
@@ -113,6 +113,7 @@ function Write-Manifest {
|
||||
'Copy-InfisicalSecret',
|
||||
'ConvertTo-InfisicalSecretDictionary',
|
||||
'Export-InfisicalSecrets',
|
||||
'Import-InfisicalSecret',
|
||||
'Get-InfisicalProject',
|
||||
'New-InfisicalProject',
|
||||
'Update-InfisicalProject',
|
||||
@@ -129,6 +130,14 @@ function Write-Manifest {
|
||||
'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',
|
||||
@@ -144,7 +153,25 @@ function Write-Manifest {
|
||||
'New-InfisicalScepDynamicChallenge',
|
||||
'Get-InfisicalScepMdmProfile',
|
||||
'Export-InfisicalScepMdmProfile',
|
||||
'Write-InfisicalScepMdmProfileToWmi'
|
||||
'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'
|
||||
)
|
||||
AliasesToExport = @()
|
||||
VariablesToExport = @()
|
||||
@@ -166,6 +193,15 @@ function Write-Manifest {
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if (-not $ChangelogFile.Exists) { return }
|
||||
@@ -173,11 +209,31 @@ function Update-Changelog {
|
||||
$existing = Get-Content -LiteralPath $ChangelogFile.FullName -Raw
|
||||
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?$')
|
||||
if (-not $unreleasedRegex.IsMatch($existing)) { 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))
|
||||
$unreleasedRegex = [regex]::new('(?m)^## Unreleased[^\r\n]*\r?$')
|
||||
$unreleasedMatch = $unreleasedRegex.Match($existing)
|
||||
if (-not $unreleasedMatch.Success) { return }
|
||||
|
||||
# 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))
|
||||
}
|
||||
|
||||
|
||||
@@ -209,7 +265,7 @@ if (`$cmds.Count -eq 0) {
|
||||
throw "No cmdlets were exported by the PSInfisicalAPI module."
|
||||
}
|
||||
|
||||
`$expectedCmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecret','New-InfisicalSecret','Update-InfisicalSecret','Remove-InfisicalSecret','Copy-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','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-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')
|
||||
`$expectedCmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecret','New-InfisicalSecret','Update-InfisicalSecret','Remove-InfisicalSecret','Copy-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Import-InfisicalSecret','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder','Get-InfisicalTag','New-InfisicalTag','Update-InfisicalTag','Remove-InfisicalTag','Get-InfisicalOrganization','New-InfisicalOrganization','Update-InfisicalOrganization','Remove-InfisicalOrganization','Get-InfisicalSubOrganization','New-InfisicalSubOrganization','Update-InfisicalSubOrganization','Remove-InfisicalSubOrganization','Get-InfisicalCertificateAuthority','Get-InfisicalPkiSubscriber','Get-InfisicalCertificateProfile','Get-InfisicalCertificatePolicy','Get-InfisicalCertificate','Request-InfisicalCertificate','ConvertTo-InfisicalCertificate','Install-InfisicalCertificate','Uninstall-InfisicalCertificate','Export-InfisicalCertificate','Get-InfisicalCertificateApplication','Get-InfisicalCertificateApplicationEnrollment','New-InfisicalScepDynamicChallenge','Get-InfisicalScepMdmProfile','Export-InfisicalScepMdmProfile','Write-InfisicalScepMdmProfileToWmi','Start-InfisicalProcess','Get-InfisicalEnvironmentVariable','Get-InfisicalSANList','New-InfisicalCertificateAuthority','Set-InfisicalCertificateAuthority','Remove-InfisicalCertificateAuthority','New-InfisicalCertificatePolicy','Set-InfisicalCertificatePolicy','Remove-InfisicalCertificatePolicy','New-InfisicalCertificateProfile','Set-InfisicalCertificateProfile','Remove-InfisicalCertificateProfile','New-InfisicalCertificateApplication','Set-InfisicalCertificateApplication','Remove-InfisicalCertificateApplication','New-InfisicalPkiSubscriber','Set-InfisicalPkiSubscriber','Remove-InfisicalPkiSubscriber')
|
||||
foreach (`$expected in `$expectedCmds) {
|
||||
if (-not (Get-Command -Name `$expected -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) {
|
||||
throw "Cmdlet not found: `$expected"
|
||||
|
||||
@@ -39,6 +39,14 @@ Get-InfisicalTag
|
||||
New-InfisicalTag
|
||||
Update-InfisicalTag
|
||||
Remove-InfisicalTag
|
||||
Get-InfisicalOrganization
|
||||
New-InfisicalOrganization
|
||||
Update-InfisicalOrganization
|
||||
Remove-InfisicalOrganization
|
||||
Get-InfisicalSubOrganization
|
||||
New-InfisicalSubOrganization
|
||||
Update-InfisicalSubOrganization
|
||||
Remove-InfisicalSubOrganization
|
||||
```
|
||||
|
||||
Infisical’s public API is REST-based and provides programmatic access for managing secrets and related resources. Current Infisical documentation shows the list-secrets endpoint under `/api/v4/secrets`, the single-secret retrieval endpoint under `/api/v4/secrets/{secretName}`, and Universal Auth login under `/api/v1/auth/universal-auth/login`. The implementation must centralize API endpoint definitions because Infisical uses different API versions across resource families. ([Infisical Blog][1])
|
||||
@@ -1213,7 +1221,9 @@ Export-InfisicalSecrets `
|
||||
[-Path <FileInfo>] `
|
||||
[-Scope <Process|User|Machine>] `
|
||||
[-Force] `
|
||||
[-Encoding <UTF8|UTF8Bom|Unicode>]
|
||||
[-Encoding <UTF8|UTF8Bom|Unicode>] `
|
||||
[-SecretsPrefix <string>] `
|
||||
[-ForceSecretsPrefix]
|
||||
```
|
||||
|
||||
## Parameter Rules
|
||||
@@ -1490,6 +1500,121 @@ No warnings should be emitted.
|
||||
|
||||
---
|
||||
|
||||
# 16.6 Start-InfisicalProcess
|
||||
|
||||
Signature:
|
||||
|
||||
```text
|
||||
Start-InfisicalProcess
|
||||
-FilePath <string>
|
||||
[-WorkingDirectory <DirectoryInfo>]
|
||||
[-ArgumentList <string[]>]
|
||||
[-AcceptableExitCodeList <string[]>]
|
||||
[-WindowStyle <Normal|Hidden|Minimized|Maximized>]
|
||||
[-CreateNoWindow]
|
||||
[-NoWait]
|
||||
[-Priority <AboveNormal|BelowNormal|High|Idle|Normal|RealTime>]
|
||||
[-ExecutionTimeout <TimeSpan>]
|
||||
[-ExecutionTimeoutInterval <TimeSpan>]
|
||||
[-StandardInputObjectList <object[]>]
|
||||
[-EnvironmentVariables <IDictionary>]
|
||||
[-ParsingExpression <Regex>]
|
||||
[-SecureArgumentList]
|
||||
[-LogOutput]
|
||||
[-ContinueOnError]
|
||||
[-Secrets <InfisicalSecret[]>]
|
||||
[-SecretsPrefix <string>]
|
||||
[-ForceSecretsPrefix]
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
```text
|
||||
Buffer pipeline InfisicalSecret objects in ProcessRecord.
|
||||
Decrypt secrets only into ProcessStartInfo.Environment.
|
||||
Apply -SecretsPrefix to each secret name before injection.
|
||||
Never write secret plaintext to user or machine environment scope.
|
||||
Honor -WhatIf / -Confirm.
|
||||
Default -AcceptableExitCodeList = @('0','3010').
|
||||
Throw a terminating error on unacceptable exit code unless -ContinueOnError is set.
|
||||
```
|
||||
|
||||
Output: `InfisicalProcessResult` with `ExitCode`, `ExitCodeAsHex`, `ExitCodeAsInteger`, `ExitCodeAsDecimal`, `StandardOutput`, `StandardError`, `StandardOutputObject`, `StandardErrorObject`, `StartTime`, `ExitTime`, `Duration`, `DurationFriendly`, `ProcessId`, `TimedOut`, `Succeeded`, `SecretCount`.
|
||||
|
||||
---
|
||||
|
||||
# 16.7 Organization Cmdlets
|
||||
|
||||
Organizations are the top-level tenancy boundary in Infisical. They are not scoped under a project; the active connection's `OrganizationId` is used as the default identifier when an explicit one is not supplied.
|
||||
|
||||
Cmdlet signatures:
|
||||
|
||||
```powershell
|
||||
Get-InfisicalOrganization [[-OrganizationId] <string>] # default = List
|
||||
New-InfisicalOrganization [-Name] <string> [-Slug <string>] [-WhatIf] [-Confirm]
|
||||
Update-InfisicalOrganization [-OrganizationId] <string> [-Name <string>] [-Slug <string>] [-WhatIf] [-Confirm]
|
||||
Remove-InfisicalOrganization [-OrganizationId] <string> [-PassThru] [-WhatIf] [-Confirm]
|
||||
```
|
||||
|
||||
Parameter sets:
|
||||
|
||||
| Cmdlet | Default set | Single set | Notes |
|
||||
|---|---|---|---|
|
||||
| `Get-InfisicalOrganization` | `List` (no `-Id`) | `Single` (`-OrganizationId`/`-Id`) | No `-ProjectId`. |
|
||||
| `New-InfisicalOrganization` | n/a | `-Name` mandatory, `-Slug` optional | ShouldProcess. |
|
||||
| `Update-InfisicalOrganization` | n/a | `-OrganizationId` mandatory | ShouldProcess; only bound parameters are sent. |
|
||||
| `Remove-InfisicalOrganization` | n/a | `-OrganizationId` mandatory | `ConfirmImpact.High`; `-PassThru` emits removed id. |
|
||||
|
||||
Endpoints:
|
||||
|
||||
| Operation | Method | Template | Version |
|
||||
|---|---|---|---|
|
||||
| List | `GET` | `/api/v2/organizations` | v2 |
|
||||
| Retrieve | `GET` | `/api/v1/organization/{organizationId}` | v1 |
|
||||
| Create | `POST` | `/api/v2/organizations` | v2 |
|
||||
| Update | `PATCH` | `/api/v1/organization/{organizationId}` | v1 |
|
||||
| Delete | `DELETE` | `/api/v1/organization/{organizationId}` | v1 |
|
||||
|
||||
Output: `InfisicalOrganization` with `Id`, `Name`, `Slug`, `CustomerId`, `AuthEnforced`, `ScimEnabled`, `CreatedAtUtc`, `UpdatedAtUtc`.
|
||||
|
||||
---
|
||||
|
||||
# 16.8 Sub-Organization Cmdlets
|
||||
|
||||
Sub-organizations partition an organization into isolated child tenants. They are not scoped under a project; the active connection is used for the parent organization context.
|
||||
|
||||
Cmdlet signatures:
|
||||
|
||||
```powershell
|
||||
Get-InfisicalSubOrganization [[-SubOrganizationId] <string>] [-Limit <int>] [-Offset <int>] [-Search <string>] [-OrderBy <string>] [-OrderDirection <string>] [-IsAccessible]
|
||||
New-InfisicalSubOrganization [-Name] <string> [-Slug] <string> [-WhatIf] [-Confirm]
|
||||
Update-InfisicalSubOrganization [-SubOrganizationId] <string> [-Name <string>] [-Slug <string>] [-WhatIf] [-Confirm]
|
||||
Remove-InfisicalSubOrganization [-SubOrganizationId] <string> [-PassThru] [-WhatIf] [-Confirm]
|
||||
```
|
||||
|
||||
Parameter sets:
|
||||
|
||||
| Cmdlet | Default set | Single set | Notes |
|
||||
|---|---|---|---|
|
||||
| `Get-InfisicalSubOrganization` | `List` (no `-Id`) | `Single` (`-SubOrganizationId`/`-Id`) | List supports server-side `-Limit`, `-Offset`, `-Search`, `-OrderBy`, `-OrderDirection`, `-IsAccessible`. |
|
||||
| `New-InfisicalSubOrganization` | n/a | `-Name` + `-Slug` mandatory | ShouldProcess. |
|
||||
| `Update-InfisicalSubOrganization` | n/a | `-SubOrganizationId` mandatory | ShouldProcess; only bound parameters are sent. |
|
||||
| `Remove-InfisicalSubOrganization` | n/a | `-SubOrganizationId` mandatory | `ConfirmImpact.High`; `-PassThru` emits removed id. |
|
||||
|
||||
Endpoints (beta):
|
||||
|
||||
| Operation | Method | Template | Version |
|
||||
|---|---|---|---|
|
||||
| List | `GET` | `/api/v1/sub-organizations` | v1 |
|
||||
| Retrieve | `GET` | `/api/v1/sub-organizations/{subOrgId}` | v1 |
|
||||
| Create | `POST` | `/api/v1/sub-organizations` | v1 |
|
||||
| Update | `PATCH` | `/api/v1/sub-organizations/{subOrgId}` | v1 |
|
||||
| Delete | `DELETE` | `/api/v1/sub-organizations/{subOrgId}` | v1 |
|
||||
|
||||
Output: `InfisicalSubOrganization` with `Id`, `Name`, `Slug`, `OrganizationId`, `IsAccessible`, `CreatedAtUtc`, `UpdatedAtUtc`.
|
||||
|
||||
---
|
||||
|
||||
# 17. SecureString Utility
|
||||
|
||||
Required utility:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using PSInfisicalAPI.Logging;
|
||||
using Xunit;
|
||||
|
||||
namespace PSInfisicalAPI.Tests
|
||||
{
|
||||
public class CertificateStoreTargetingTests
|
||||
{
|
||||
private static readonly Assembly ModuleAssembly = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly;
|
||||
|
||||
private sealed class CapturingLogger : IInfisicalLogger
|
||||
{
|
||||
public List<string> Warnings { get; } = new List<string>();
|
||||
public List<string> Information_ { get; } = new List<string>();
|
||||
|
||||
public void Information(string component, string message) { Information_.Add(message); }
|
||||
public void Verbose(string component, string message) { }
|
||||
public void Debug(string component, string message) { }
|
||||
public void Warning(string component, string message) { Warnings.Add(message); }
|
||||
public void Error(string component, string message) { }
|
||||
}
|
||||
|
||||
private static X509Certificate2 CreateRoot(string name, out System.Security.Cryptography.RSA key)
|
||||
{
|
||||
key = System.Security.Cryptography.RSA.Create(2048);
|
||||
CertificateRequest request = new CertificateRequest(
|
||||
string.Concat("CN=", name), key,
|
||||
System.Security.Cryptography.HashAlgorithmName.SHA256,
|
||||
System.Security.Cryptography.RSASignaturePadding.Pkcs1);
|
||||
request.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, true, 1, true));
|
||||
return request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(10));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyFriendlyName_Sets_The_Windows_Friendly_Name()
|
||||
{
|
||||
Type helper = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true);
|
||||
MethodInfo apply = helper.GetMethod("ApplyFriendlyName", BindingFlags.Public | BindingFlags.Static);
|
||||
Assert.NotNull(apply);
|
||||
|
||||
System.Security.Cryptography.RSA key;
|
||||
using (X509Certificate2 cert = CreateRoot("FriendlyName.Probe", out key))
|
||||
using (key)
|
||||
{
|
||||
apply.Invoke(null, new object[] { cert, "WEB01", NullInfisicalLogger.Instance, "Test" });
|
||||
Assert.Equal("WEB01", cert.FriendlyName);
|
||||
|
||||
// A null or empty name must not clear a previously set value.
|
||||
apply.Invoke(null, new object[] { cert, null, NullInfisicalLogger.Instance, "Test" });
|
||||
apply.Invoke(null, new object[] { cert, string.Empty, NullInfisicalLogger.Instance, "Test" });
|
||||
Assert.Equal("WEB01", cert.FriendlyName);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyFriendlyName_Tolerates_Null_Certificate_And_Logger()
|
||||
{
|
||||
Type helper = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true);
|
||||
MethodInfo apply = helper.GetMethod("ApplyFriendlyName", BindingFlags.Public | BindingFlags.Static);
|
||||
apply.Invoke(null, new object[] { null, "WEB01", null, "Test" });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyInstalledChain_Names_The_Missing_Issuer_On_A_Partial_Chain()
|
||||
{
|
||||
// An intermediate whose root is not in any trusted store is exactly the state that surfaces in
|
||||
// Windows as "The issuer of this certificate could not be found".
|
||||
Type helper = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true);
|
||||
MethodInfo verify = helper.GetMethod("VerifyInstalledChain", BindingFlags.Public | BindingFlags.Static);
|
||||
Assert.NotNull(verify);
|
||||
|
||||
System.Security.Cryptography.RSA rootKey;
|
||||
using (X509Certificate2 root = CreateRoot("VerifyChain.Root", out rootKey))
|
||||
using (rootKey)
|
||||
using (System.Security.Cryptography.RSA interKey = System.Security.Cryptography.RSA.Create(2048))
|
||||
{
|
||||
CertificateRequest interRequest = new CertificateRequest(
|
||||
"CN=VerifyChain.Intermediate", interKey,
|
||||
System.Security.Cryptography.HashAlgorithmName.SHA256,
|
||||
System.Security.Cryptography.RSASignaturePadding.Pkcs1);
|
||||
interRequest.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
|
||||
|
||||
using (X509Certificate2 intermediate = interRequest.Create(root, DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(9), new byte[] { 9, 8, 7, 6, 5, 4, 3, 2 }))
|
||||
{
|
||||
CapturingLogger logger = new CapturingLogger();
|
||||
verify.Invoke(null, new object[] { intermediate, logger, "Test" });
|
||||
|
||||
string warning = Assert.Single(logger.Warnings);
|
||||
Assert.Contains("issuer", warning, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("VerifyChain.Root", warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyInstalledChain_Tolerates_Null_Inputs()
|
||||
{
|
||||
Type helper = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true);
|
||||
MethodInfo verify = helper.GetMethod("VerifyInstalledChain", BindingFlags.Public | BindingFlags.Static);
|
||||
verify.Invoke(null, new object[] { null, new CapturingLogger(), "Test" });
|
||||
|
||||
System.Security.Cryptography.RSA key;
|
||||
using (X509Certificate2 cert = CreateRoot("VerifyChain.NullLogger", out key))
|
||||
using (key)
|
||||
{
|
||||
verify.Invoke(null, new object[] { cert, null, "Test" });
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FriendlyName_Is_Available_On_Every_Issuance_Parameter_Set()
|
||||
{
|
||||
// The Windows friendly name applies regardless of how the certificate was issued.
|
||||
Type cmdletType = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet", true);
|
||||
PropertyInfo friendlyName = cmdletType.GetProperty("FriendlyName");
|
||||
Assert.NotNull(friendlyName);
|
||||
|
||||
foreach (CustomAttributeData attribute in friendlyName.GetCustomAttributesData())
|
||||
{
|
||||
if (attribute.AttributeType != typeof(System.Management.Automation.ParameterAttribute)) { continue; }
|
||||
foreach (CustomAttributeNamedArgument named in attribute.NamedArguments)
|
||||
{
|
||||
Assert.NotEqual("ParameterSetName", named.MemberName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void InvokeTrustPromptWarning(StoreName storeName, StoreLocation storeLocation, IInfisicalLogger logger)
|
||||
{
|
||||
Type helper = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true);
|
||||
MethodInfo method = helper.GetMethod("WarnIfInteractiveTrustPromptExpected", BindingFlags.Public | BindingFlags.Static);
|
||||
Assert.NotNull(method);
|
||||
method.Invoke(null, new object[] { storeName, storeLocation, logger, "TestComponent" });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CurrentUser_Root_Install_Warns_About_The_Blocking_Trust_Dialog()
|
||||
{
|
||||
// X509Store.Add on CurrentUser\Root raises a modal Windows trust dialog and blocks until answered.
|
||||
// Without this warning the caller sees an unexplained hang.
|
||||
CapturingLogger logger = new CapturingLogger();
|
||||
InvokeTrustPromptWarning(StoreName.Root, StoreLocation.CurrentUser, logger);
|
||||
|
||||
string warning = Assert.Single(logger.Warnings);
|
||||
Assert.Contains("CurrentUser\\Root", warning);
|
||||
Assert.Contains("security confirmation", warning);
|
||||
Assert.Contains("LocalMachine", warning);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(StoreName.Root, StoreLocation.LocalMachine)]
|
||||
[InlineData(StoreName.My, StoreLocation.CurrentUser)]
|
||||
[InlineData(StoreName.My, StoreLocation.LocalMachine)]
|
||||
[InlineData(StoreName.CertificateAuthority, StoreLocation.CurrentUser)]
|
||||
[InlineData(StoreName.CertificateAuthority, StoreLocation.LocalMachine)]
|
||||
public void Non_Prompting_Store_Targets_Stay_Silent(StoreName storeName, StoreLocation storeLocation)
|
||||
{
|
||||
CapturingLogger logger = new CapturingLogger();
|
||||
InvokeTrustPromptWarning(storeName, storeLocation, logger);
|
||||
Assert.Empty(logger.Warnings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarnIfInteractiveTrustPromptExpected_Tolerates_A_Null_Logger()
|
||||
{
|
||||
InvokeTrustPromptWarning(StoreName.Root, StoreLocation.CurrentUser, null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Base_Cmdlet_Exposes_Elevation_Aware_Store_Resolution()
|
||||
{
|
||||
Type baseType = typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase);
|
||||
|
||||
MethodInfo resolve = baseType.GetMethod("ResolveStoreLocation", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
Assert.NotNull(resolve);
|
||||
Assert.Equal(typeof(StoreLocation), resolve.ReturnType);
|
||||
|
||||
MethodInfo elevated = baseType.GetMethod("IsElevated", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
Assert.NotNull(elevated);
|
||||
Assert.Equal(typeof(bool), elevated.ReturnType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Chain_Certificates_Route_Root_And_Intermediate_To_Their_Own_Stores()
|
||||
{
|
||||
Type helper = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true);
|
||||
MethodInfo classify = helper.GetMethod("GetChainCertificateTargetStore", BindingFlags.Public | BindingFlags.Static);
|
||||
|
||||
using (System.Security.Cryptography.RSA rootRsa = System.Security.Cryptography.RSA.Create(2048))
|
||||
using (System.Security.Cryptography.RSA leafRsa = System.Security.Cryptography.RSA.Create(2048))
|
||||
{
|
||||
DateTimeOffset notBefore = DateTimeOffset.UtcNow.AddMinutes(-5);
|
||||
DateTimeOffset notAfter = DateTimeOffset.UtcNow.AddDays(1);
|
||||
|
||||
CertificateRequest rootRequest = new CertificateRequest(
|
||||
"CN=StoreTargeting.Root", rootRsa,
|
||||
System.Security.Cryptography.HashAlgorithmName.SHA256,
|
||||
System.Security.Cryptography.RSASignaturePadding.Pkcs1);
|
||||
rootRequest.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
|
||||
|
||||
using (X509Certificate2 root = rootRequest.CreateSelfSigned(notBefore, notAfter))
|
||||
{
|
||||
CertificateRequest interRequest = new CertificateRequest(
|
||||
"CN=StoreTargeting.Intermediate", leafRsa,
|
||||
System.Security.Cryptography.HashAlgorithmName.SHA256,
|
||||
System.Security.Cryptography.RSASignaturePadding.Pkcs1);
|
||||
interRequest.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
|
||||
|
||||
using (X509Certificate2 intermediate = interRequest.Create(root, notBefore, notAfter, new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }))
|
||||
{
|
||||
Assert.Equal(StoreName.Root, (StoreName)classify.Invoke(null, new object[] { root }));
|
||||
Assert.Equal(StoreName.CertificateAuthority, (StoreName)classify.Invoke(null, new object[] { intermediate }));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,6 +114,170 @@ namespace PSInfisicalAPI.Tests
|
||||
Assert.Equal("DE", countryProp.GetValue(result));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("CN=WEB01", "WEB01")]
|
||||
[InlineData("cn=web01.contoso.local", "web01.contoso.local")]
|
||||
[InlineData("CN=WEB01,OU=IT,O=Contoso", "WEB01")]
|
||||
[InlineData(" CN=WEB01 ", "WEB01")]
|
||||
[InlineData("WEB01.contoso.local", "WEB01.contoso.local")]
|
||||
[InlineData(null, null)]
|
||||
public void MergeSubject_Normalizes_Rdn_Style_CommonName(string supplied, string expected)
|
||||
{
|
||||
Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true);
|
||||
MethodInfo merge = helperType.GetMethod("MergeSubject", BindingFlags.Public | BindingFlags.Static);
|
||||
|
||||
object result = merge.Invoke(null, new object[] { null, supplied, null, null, null, null, null, null });
|
||||
|
||||
Assert.Equal(expected, result.GetType().GetProperty("CommonName").GetValue(result));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MergeSubject_Normalizes_CommonName_Supplied_Through_Subject_Hashtable()
|
||||
{
|
||||
Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true);
|
||||
MethodInfo merge = helperType.GetMethod("MergeSubject", BindingFlags.Public | BindingFlags.Static);
|
||||
|
||||
Hashtable subject = new Hashtable { { "CN", "CN=WEB01" } };
|
||||
object result = merge.Invoke(null, new object[] { subject, null, null, null, null, null, null, null });
|
||||
|
||||
Assert.Equal("WEB01", result.GetType().GetProperty("CommonName").GetValue(result));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildDnsNames_Routes_Ip_Literals_From_DnsName_To_IpAddress_Sans()
|
||||
{
|
||||
// Get-InfisicalSANList emits host names and IP addresses in one list, and the documented usage
|
||||
// splats that whole list into -DnsName.
|
||||
PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet cmdlet = new PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet
|
||||
{
|
||||
DnsName = new[] { "WEB01", "172.16.32.24", "WEB01.contoso.local", "127.0.0.1", "::1" }
|
||||
};
|
||||
|
||||
List<string> ipAddresses = new List<string>();
|
||||
List<string> dnsNames = InvokeBuildDnsNames(cmdlet, new InfisicalCsrSubject { CommonName = "WEB01" }, ipAddresses);
|
||||
|
||||
Assert.Equal(new[] { "WEB01", "WEB01.contoso.local" }, dnsNames);
|
||||
Assert.Equal(new[] { "172.16.32.24", "127.0.0.1", "::1" }, ipAddresses);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildDnsNames_Merges_Explicit_IpAddress_Parameter_And_Deduplicates()
|
||||
{
|
||||
PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet cmdlet = new PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet
|
||||
{
|
||||
DnsName = new[] { "WEB01", "10.0.0.5" },
|
||||
IpAddress = new[] { "10.0.0.5", "10.0.0.6" }
|
||||
};
|
||||
|
||||
List<string> ipAddresses = new List<string>();
|
||||
List<string> dnsNames = InvokeBuildDnsNames(cmdlet, new InfisicalCsrSubject { CommonName = "WEB01" }, ipAddresses);
|
||||
|
||||
Assert.Equal(new[] { "WEB01" }, dnsNames);
|
||||
Assert.Equal(new[] { "10.0.0.5", "10.0.0.6" }, ipAddresses);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildDnsNames_Mirrors_Ip_CommonName_Into_IpAddress_Sans_Not_Dns()
|
||||
{
|
||||
PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet cmdlet = new PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet
|
||||
{
|
||||
DnsName = new[] { "WEB01.contoso.local" }
|
||||
};
|
||||
|
||||
List<string> ipAddresses = new List<string>();
|
||||
List<string> dnsNames = InvokeBuildDnsNames(cmdlet, new InfisicalCsrSubject { CommonName = "10.0.0.5" }, ipAddresses);
|
||||
|
||||
Assert.Equal(new[] { "WEB01.contoso.local" }, dnsNames);
|
||||
Assert.Equal(new[] { "10.0.0.5" }, ipAddresses);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildDnsNames_Ip_Only_Request_Does_Not_Pick_Up_Local_Fqdn()
|
||||
{
|
||||
PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet cmdlet = new PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet
|
||||
{
|
||||
IpAddress = new[] { "10.0.0.5" }
|
||||
};
|
||||
|
||||
List<string> ipAddresses = new List<string>();
|
||||
List<string> dnsNames = InvokeBuildDnsNames(cmdlet, new InfisicalCsrSubject { CommonName = "10.0.0.5" }, ipAddresses);
|
||||
|
||||
Assert.Empty(dnsNames);
|
||||
Assert.Equal(new[] { "10.0.0.5" }, ipAddresses);
|
||||
}
|
||||
|
||||
private static List<string> InvokeBuildDnsNames(PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet cmdlet, InfisicalCsrSubject subject, List<string> ipAddresses)
|
||||
{
|
||||
MethodInfo build = cmdlet.GetType().GetMethod("BuildDnsNames", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
Assert.NotNull(build);
|
||||
return (List<string>)build.Invoke(cmdlet, new object[] { subject, ipAddresses });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DirectIssuance_Guidance_Names_The_Parameters_That_Resolve_It()
|
||||
{
|
||||
// Infisical exposes no certificate-template issuance route over REST, so the actionable alternatives
|
||||
// are enabling direct issuance on the CA, a subscriber, or a profile.
|
||||
PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet cmdlet = new PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet
|
||||
{
|
||||
CertificateAuthorityId = "ca-1234",
|
||||
ProjectId = "proj-5678"
|
||||
};
|
||||
|
||||
MethodInfo build = cmdlet.GetType().GetMethod("BuildDirectIssuanceGuidance", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
Assert.NotNull(build);
|
||||
|
||||
string guidance = (string)build.Invoke(cmdlet, new object[] { null });
|
||||
|
||||
Assert.Contains("ca-1234", guidance);
|
||||
Assert.Contains("proj-5678", guidance);
|
||||
Assert.Contains("-PkiSubscriberSlug", guidance);
|
||||
Assert.Contains("-CertificateProfileId", guidance);
|
||||
Assert.Contains("Get-InfisicalPkiSubscriber", guidance);
|
||||
Assert.Contains("Direct Issuance", guidance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DirectIssuance_Guidance_Prefers_The_Ca_Name_When_Known()
|
||||
{
|
||||
PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet cmdlet = new PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet
|
||||
{
|
||||
CertificateAuthorityId = "ca-1234",
|
||||
ProjectId = "proj-5678"
|
||||
};
|
||||
|
||||
PSInfisicalAPI.Models.InfisicalCertificateAuthority ca = new PSInfisicalAPI.Models.InfisicalCertificateAuthority
|
||||
{
|
||||
Id = "ca-1234",
|
||||
Name = "intermediate-ca",
|
||||
EnableDirectIssuance = false
|
||||
};
|
||||
|
||||
MethodInfo build = cmdlet.GetType().GetMethod("BuildDirectIssuanceGuidance", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
string guidance = (string)build.Invoke(cmdlet, new object[] { ca });
|
||||
|
||||
Assert.Contains("intermediate-ca", guidance);
|
||||
Assert.Contains("ca-1234", guidance);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("WriteErrorForException")]
|
||||
[InlineData("ThrowTerminatingForException")]
|
||||
public void Failure_Handlers_Rethrow_Pipeline_Stops_Untouched(string handlerName)
|
||||
{
|
||||
// Select-Object -First makes WriteObject throw a PipelineStoppedException-derived type. Reporting it
|
||||
// as an error surfaces spurious "The pipeline has been stopped." warnings on normal early exits.
|
||||
PSInfisicalAPI.Cmdlets.GetInfisicalCertificateAuthorityCmdlet cmdlet = new PSInfisicalAPI.Cmdlets.GetInfisicalCertificateAuthorityCmdlet();
|
||||
MethodInfo method = typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase).GetMethod(handlerName, BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
Assert.NotNull(method);
|
||||
|
||||
PipelineStoppedException stop = new PipelineStoppedException();
|
||||
TargetInvocationException wrapper = Assert.Throws<TargetInvocationException>(
|
||||
() => method.Invoke(cmdlet, new object[] { "TestComponent", "TestOperation", stop }));
|
||||
|
||||
Assert.Same(stop, wrapper.InnerException);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SignCertificateBySubscriber_Uses_Pki_Subscribers_Template()
|
||||
{
|
||||
|
||||
@@ -50,7 +50,9 @@ namespace PSInfisicalAPI.Tests
|
||||
[InlineData(InfisicalEndpointNames.CreateSecret, "POST", "/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.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.CreateProject, "POST", "/api/v2/workspace")]
|
||||
[InlineData(InfisicalEndpointNames.UpdateProject, "PATCH", "/api/v1/workspace/{projectId}")]
|
||||
@@ -75,6 +77,18 @@ namespace PSInfisicalAPI.Tests
|
||||
[InlineData(InfisicalEndpointNames.BulkUpdateSecret, "PATCH", "/api/v4/secrets/batch")]
|
||||
[InlineData(InfisicalEndpointNames.BulkDeleteSecret, "DELETE", "/api/v4/secrets/batch")]
|
||||
[InlineData(InfisicalEndpointNames.DuplicateSecret, "POST", "/api/v4/secrets/duplicate")]
|
||||
// /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.CreateOrganization, "POST", "/api/v2/organizations")]
|
||||
[InlineData(InfisicalEndpointNames.UpdateOrganization, "PATCH", "/api/v1/organization/{organizationId}")]
|
||||
[InlineData(InfisicalEndpointNames.DeleteOrganization, "DELETE", "/api/v1/organization/{organizationId}")]
|
||||
[InlineData(InfisicalEndpointNames.ListSubOrganizations, "GET", "/api/v1/sub-organizations")]
|
||||
[InlineData(InfisicalEndpointNames.RetrieveSubOrganization, "GET", "/api/v1/sub-organizations/{subOrgId}")]
|
||||
[InlineData(InfisicalEndpointNames.CreateSubOrganization, "POST", "/api/v1/sub-organizations")]
|
||||
[InlineData(InfisicalEndpointNames.UpdateSubOrganization, "PATCH", "/api/v1/sub-organizations/{subOrgId}")]
|
||||
[InlineData(InfisicalEndpointNames.DeleteSubOrganization, "DELETE", "/api/v1/sub-organizations/{subOrgId}")]
|
||||
public void Registered_Endpoints_Have_Expected_Shape(string name, string method, string template)
|
||||
{
|
||||
InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(name);
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using PSInfisicalAPI.Process;
|
||||
using Xunit;
|
||||
|
||||
namespace PSInfisicalAPI.Tests
|
||||
{
|
||||
public class InfisicalProcessRunnerHelpersTests
|
||||
{
|
||||
[Fact]
|
||||
public void FormatFriendly_Zero_Returns_NotAvailable()
|
||||
{
|
||||
Assert.Equal("N/A", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.Zero));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatFriendly_Single_Unit_Plural()
|
||||
{
|
||||
Assert.Equal("30 seconds", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.FromSeconds(30)));
|
||||
Assert.Equal("5 minutes", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.FromMinutes(5)));
|
||||
Assert.Equal("250 milliseconds", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.FromMilliseconds(250)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatFriendly_Single_Unit_Singular()
|
||||
{
|
||||
Assert.Equal("1 second", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.FromSeconds(1)));
|
||||
Assert.Equal("1 minute", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.FromMinutes(1)));
|
||||
Assert.Equal("1 hour", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.FromHours(1)));
|
||||
Assert.Equal("1 day", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.FromDays(1)));
|
||||
Assert.Equal("1 millisecond", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.FromMilliseconds(1)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatFriendly_Two_Units_Uses_And_Join()
|
||||
{
|
||||
TimeSpan value = TimeSpan.FromSeconds(7) + TimeSpan.FromMilliseconds(364);
|
||||
Assert.Equal("7 seconds, and 364 milliseconds", InfisicalProcessRunnerHelpers.FormatFriendly(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatFriendly_Multiple_Units_Uses_Comma_And_Trailing_And()
|
||||
{
|
||||
TimeSpan value = TimeSpan.FromHours(1) + TimeSpan.FromMinutes(2) + TimeSpan.FromSeconds(3) + TimeSpan.FromMilliseconds(45);
|
||||
Assert.Equal("1 hour, 2 minutes, 3 seconds, and 45 milliseconds", InfisicalProcessRunnerHelpers.FormatFriendly(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatFriendly_Skips_Zero_Components()
|
||||
{
|
||||
TimeSpan value = TimeSpan.FromHours(2) + TimeSpan.FromMilliseconds(500);
|
||||
Assert.Equal("2 hours, and 500 milliseconds", InfisicalProcessRunnerHelpers.FormatFriendly(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatFriendly_Mixed_Singular_And_Plural()
|
||||
{
|
||||
TimeSpan value = TimeSpan.FromMinutes(1) + TimeSpan.FromSeconds(30);
|
||||
Assert.Equal("1 minute, and 30 seconds", InfisicalProcessRunnerHelpers.FormatFriendly(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatFriendly_Days_Component()
|
||||
{
|
||||
TimeSpan value = TimeSpan.FromDays(2) + TimeSpan.FromHours(3);
|
||||
Assert.Equal("2 days, and 3 hours", InfisicalProcessRunnerHelpers.FormatFriendly(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatFriendly_SubMillisecond_Returns_NotAvailable()
|
||||
{
|
||||
TimeSpan value = TimeSpan.FromTicks(100);
|
||||
Assert.Equal("N/A", InfisicalProcessRunnerHelpers.FormatFriendly(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using PSInfisicalAPI.Logging;
|
||||
using Xunit;
|
||||
|
||||
namespace PSInfisicalAPI.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// The test project references PowerShellStandard.Library, which cannot host a runspace, so the logger's
|
||||
/// stream choice is asserted structurally: which Cmdlet.Write* method each level compiles down to.
|
||||
/// </summary>
|
||||
public class LoggerStreamRoutingTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("Error", "WriteVerbose")]
|
||||
[InlineData("Warning", "WriteWarning")]
|
||||
[InlineData("Information", "WriteVerbose")]
|
||||
[InlineData("Verbose", "WriteVerbose")]
|
||||
[InlineData("Debug", "WriteDebug")]
|
||||
public void PSCmdletLogger_Routes_Level_To_Expected_Stream(string levelMethod, string expectedWriteMethod)
|
||||
{
|
||||
MethodInfo method = typeof(PSCmdletLogger).GetMethod(levelMethod, BindingFlags.Public | BindingFlags.Instance);
|
||||
Assert.NotNull(method);
|
||||
|
||||
List<string> called = GetCalledMethodNames(method);
|
||||
Assert.Contains(expectedWriteMethod, called);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PSCmdletLogger_Error_Does_Not_Write_To_Warning_Stream()
|
||||
{
|
||||
// Every Logger.Error call site in this module logs and then throws, so the failure already reaches the
|
||||
// caller as an ErrorRecord. Duplicating it on the warning stream put failures under -WarningAction
|
||||
// instead of -ErrorAction and buried the real error under eight lines of noise.
|
||||
MethodInfo error = typeof(PSCmdletLogger).GetMethod("Error", BindingFlags.Public | BindingFlags.Instance);
|
||||
List<string> called = GetCalledMethodNames(error);
|
||||
|
||||
Assert.DoesNotContain("WriteWarning", called);
|
||||
Assert.DoesNotContain("WriteError", called);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void No_Cmdlet_Reports_Operation_Failures_As_Terminating_Errors()
|
||||
{
|
||||
// Operation failures go through WriteErrorForException so -ErrorAction decides the outcome.
|
||||
// ThrowTerminatingForException remains available for aborts that ignore -ErrorAction, but no cmdlet
|
||||
// should be using it for ordinary failures; this pins the convention against drift.
|
||||
Assembly assembly = typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase).Assembly;
|
||||
List<string> offenders = new List<string>();
|
||||
int inspected = 0;
|
||||
|
||||
foreach (Type type in assembly.GetTypes())
|
||||
{
|
||||
if (!typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase).IsAssignableFrom(type)) { continue; }
|
||||
if (type == typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase)) { continue; }
|
||||
|
||||
inspected++;
|
||||
foreach (MethodInfo method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly))
|
||||
{
|
||||
if (method.GetMethodBody() == null) { continue; }
|
||||
if (GetCalledMethodNames(method).Contains("ThrowTerminatingForException"))
|
||||
{
|
||||
offenders.Add(string.Concat(type.Name, ".", method.Name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(inspected > 40, string.Concat("Expected to inspect the cmdlet set, saw ", inspected.ToString()));
|
||||
Assert.Empty(offenders);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cmdlets_Route_Failures_Through_WriteErrorForException()
|
||||
{
|
||||
Assembly assembly = typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase).Assembly;
|
||||
Type cmdletType = assembly.GetType("PSInfisicalAPI.Cmdlets.GetInfisicalCertificateAuthorityCmdlet", true);
|
||||
MethodInfo processRecord = cmdletType.GetMethod("ProcessRecord", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
|
||||
Assert.NotNull(processRecord);
|
||||
|
||||
Assert.Contains("WriteErrorForException", GetCalledMethodNames(processRecord));
|
||||
}
|
||||
|
||||
private static List<string> GetCalledMethodNames(MethodInfo method)
|
||||
{
|
||||
List<string> names = new List<string>();
|
||||
MethodBody body = method.GetMethodBody();
|
||||
Assert.NotNull(body);
|
||||
|
||||
byte[] il = body.GetILAsByteArray();
|
||||
Assert.NotNull(il);
|
||||
|
||||
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)
|
||||
{
|
||||
// Byte sequence was operand data rather than an opcode; ignore.
|
||||
}
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.Reflection;
|
||||
using PSInfisicalAPI.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace PSInfisicalAPI.Tests
|
||||
{
|
||||
public class OrganizationMapperTests
|
||||
{
|
||||
private static readonly System.Type MapperType = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly
|
||||
.GetType("PSInfisicalAPI.Organizations.InfisicalOrganizationMapper", true);
|
||||
|
||||
private static readonly System.Type DtoType = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly
|
||||
.GetType("PSInfisicalAPI.Organizations.InfisicalOrganizationResponseDto", true);
|
||||
|
||||
private static InfisicalOrganization InvokeMap(object dto)
|
||||
{
|
||||
MethodInfo map = MapperType.GetMethod("Map", BindingFlags.Public | BindingFlags.Static);
|
||||
return (InfisicalOrganization)map.Invoke(null, new[] { dto });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_Null_Dto_Returns_Null()
|
||||
{
|
||||
Assert.Null(InvokeMap(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_Populates_Core_Fields()
|
||||
{
|
||||
object dto = System.Activator.CreateInstance(DtoType);
|
||||
DtoType.GetProperty("Id").SetValue(dto, "org-001");
|
||||
DtoType.GetProperty("Name").SetValue(dto, "Acme");
|
||||
DtoType.GetProperty("Slug").SetValue(dto, "acme");
|
||||
DtoType.GetProperty("CustomerId").SetValue(dto, "cust-9");
|
||||
DtoType.GetProperty("AuthEnforced").SetValue(dto, true);
|
||||
DtoType.GetProperty("ScimEnabled").SetValue(dto, true);
|
||||
DtoType.GetProperty("CreatedAt").SetValue(dto, "2026-01-15T12:34:56Z");
|
||||
DtoType.GetProperty("UpdatedAt").SetValue(dto, "2026-02-20T09:00:00Z");
|
||||
|
||||
InfisicalOrganization organization = InvokeMap(dto);
|
||||
|
||||
Assert.Equal("org-001", organization.Id);
|
||||
Assert.Equal("Acme", organization.Name);
|
||||
Assert.Equal("acme", organization.Slug);
|
||||
Assert.Equal("cust-9", organization.CustomerId);
|
||||
Assert.True(organization.AuthEnforced);
|
||||
Assert.True(organization.ScimEnabled);
|
||||
Assert.NotNull(organization.CreatedAtUtc);
|
||||
Assert.NotNull(organization.UpdatedAtUtc);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_Falls_Back_To_InternalId()
|
||||
{
|
||||
object dto = System.Activator.CreateInstance(DtoType);
|
||||
DtoType.GetProperty("InternalId").SetValue(dto, "internal-id-1");
|
||||
|
||||
InfisicalOrganization organization = InvokeMap(dto);
|
||||
|
||||
Assert.Equal("internal-id-1", organization.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapMany_Null_Returns_Empty()
|
||||
{
|
||||
MethodInfo mapMany = MapperType.GetMethod("MapMany", BindingFlags.Public | BindingFlags.Static);
|
||||
InfisicalOrganization[] result = (InfisicalOrganization[])mapMany.Invoke(null, new object[] { null });
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,72 @@
|
||||
using System.Reflection;
|
||||
using PSInfisicalAPI.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace PSInfisicalAPI.Tests
|
||||
{
|
||||
public class SubOrganizationMapperTests
|
||||
{
|
||||
private static readonly System.Type MapperType = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly
|
||||
.GetType("PSInfisicalAPI.SubOrganizations.InfisicalSubOrganizationMapper", true);
|
||||
|
||||
private static readonly System.Type DtoType = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly
|
||||
.GetType("PSInfisicalAPI.SubOrganizations.InfisicalSubOrganizationResponseDto", true);
|
||||
|
||||
private static InfisicalSubOrganization InvokeMap(object dto)
|
||||
{
|
||||
MethodInfo map = MapperType.GetMethod("Map", BindingFlags.Public | BindingFlags.Static);
|
||||
return (InfisicalSubOrganization)map.Invoke(null, new[] { dto });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_Null_Dto_Returns_Null()
|
||||
{
|
||||
Assert.Null(InvokeMap(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_Populates_Core_Fields()
|
||||
{
|
||||
object dto = System.Activator.CreateInstance(DtoType);
|
||||
DtoType.GetProperty("Id").SetValue(dto, "sub-001");
|
||||
DtoType.GetProperty("Name").SetValue(dto, "Platform Engineering");
|
||||
DtoType.GetProperty("Slug").SetValue(dto, "platform-eng");
|
||||
DtoType.GetProperty("OrganizationId").SetValue(dto, "org-001");
|
||||
DtoType.GetProperty("IsAccessible").SetValue(dto, true);
|
||||
DtoType.GetProperty("CreatedAt").SetValue(dto, "2026-01-15T12:34:56Z");
|
||||
DtoType.GetProperty("UpdatedAt").SetValue(dto, "2026-02-20T09:00:00Z");
|
||||
|
||||
InfisicalSubOrganization subOrganization = InvokeMap(dto);
|
||||
|
||||
Assert.Equal("sub-001", subOrganization.Id);
|
||||
Assert.Equal("Platform Engineering", subOrganization.Name);
|
||||
Assert.Equal("platform-eng", subOrganization.Slug);
|
||||
Assert.Equal("org-001", subOrganization.OrganizationId);
|
||||
Assert.True(subOrganization.IsAccessible);
|
||||
Assert.NotNull(subOrganization.CreatedAtUtc);
|
||||
Assert.NotNull(subOrganization.UpdatedAtUtc);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_Falls_Back_To_InternalId_And_OrgId()
|
||||
{
|
||||
object dto = System.Activator.CreateInstance(DtoType);
|
||||
DtoType.GetProperty("InternalId").SetValue(dto, "internal-id-1");
|
||||
DtoType.GetProperty("OrgId").SetValue(dto, "org-fallback");
|
||||
|
||||
InfisicalSubOrganization subOrganization = InvokeMap(dto);
|
||||
|
||||
Assert.Equal("internal-id-1", subOrganization.Id);
|
||||
Assert.Equal("org-fallback", subOrganization.OrganizationId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapMany_Null_Returns_Empty()
|
||||
{
|
||||
MethodInfo mapMany = MapperType.GetMethod("MapMany", BindingFlags.Public | BindingFlags.Static);
|
||||
InfisicalSubOrganization[] result = (InfisicalSubOrganization[])mapMany.Invoke(null, new object[] { null });
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,7 +205,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException(Component, "Connect", exception);
|
||||
WriteErrorForException(Component, "Connect", exception);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("ConvertToInfisicalCertificateCmdlet", "ConvertToCertificate", exception);
|
||||
WriteErrorForException("ConvertToInfisicalCertificateCmdlet", "ConvertToCertificate", exception);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Management.Automation;
|
||||
using System.Security;
|
||||
using PSInfisicalAPI.Common;
|
||||
using PSInfisicalAPI.Errors;
|
||||
using PSInfisicalAPI.Models;
|
||||
|
||||
@@ -21,6 +22,14 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
[Parameter]
|
||||
public SwitchParameter AsPlainText { get; set; }
|
||||
|
||||
[Parameter]
|
||||
[Alias("Prefix")]
|
||||
public string SecretsPrefix { get; set; }
|
||||
|
||||
[Parameter]
|
||||
[Alias("ForcePrefix")]
|
||||
public SwitchParameter ForceSecretsPrefix { get; set; }
|
||||
|
||||
private readonly List<InfisicalSecret> _buffer = new List<InfisicalSecret>();
|
||||
|
||||
protected override void ProcessRecord()
|
||||
@@ -40,20 +49,24 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.Information("ConvertTo-InfisicalSecretDictionary", string.Concat("Processing ", _buffer.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " input secret(s)."));
|
||||
|
||||
if (AsPlainText.IsPresent)
|
||||
{
|
||||
Dictionary<string, string> plain = BuildDictionary<string>(secret => secret.GetPlainTextValue());
|
||||
Logger.Information("ConvertTo-InfisicalSecretDictionary", string.Concat("Built plain-text dictionary with ", plain.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " entry/entries."));
|
||||
WriteObject(plain);
|
||||
}
|
||||
else
|
||||
{
|
||||
Dictionary<string, SecureString> secure = BuildDictionary<SecureString>(secret => secret.SecretValue);
|
||||
Logger.Information("ConvertTo-InfisicalSecretDictionary", string.Concat("Built SecureString dictionary with ", secure.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " entry/entries."));
|
||||
WriteObject(secure);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("ConvertToInfisicalSecretDictionaryCmdlet", "ConvertToDictionary", exception);
|
||||
WriteErrorForException("ConvertToInfisicalSecretDictionaryCmdlet", "ConvertToDictionary", exception);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +76,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
|
||||
foreach (InfisicalSecret secret in _buffer)
|
||||
{
|
||||
string key = secret.SecretName ?? string.Empty;
|
||||
string key = InfisicalPrefix.Apply(secret.SecretName ?? string.Empty, SecretsPrefix, ForceSecretsPrefix.IsPresent);
|
||||
|
||||
if (dictionary.ContainsKey(key))
|
||||
{
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("CopyInfisicalSecretCmdlet", "DuplicateSecrets", exception);
|
||||
WriteErrorForException("CopyInfisicalSecretCmdlet", "DuplicateSecrets", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("DisconnectInfisicalCmdlet", "Disconnect", exception);
|
||||
WriteErrorForException("DisconnectInfisicalCmdlet", "Disconnect", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("ExportInfisicalCertificateCmdlet", "ExportCertificate", exception);
|
||||
WriteErrorForException("ExportInfisicalCertificateCmdlet", "ExportCertificate", exception);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException(Component, "ExportScepMdmProfile", exception);
|
||||
WriteErrorForException(Component, "ExportScepMdmProfile", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Management.Automation;
|
||||
using System.Text;
|
||||
using PSInfisicalAPI.Common;
|
||||
using PSInfisicalAPI.Errors;
|
||||
using PSInfisicalAPI.Exports;
|
||||
using PSInfisicalAPI.Models;
|
||||
@@ -37,6 +38,14 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
[Parameter]
|
||||
public InfisicalExportEncoding Encoding { get; set; } = InfisicalExportEncoding.UTF8;
|
||||
|
||||
[Parameter]
|
||||
[Alias("Prefix")]
|
||||
public string SecretsPrefix { get; set; }
|
||||
|
||||
[Parameter]
|
||||
[Alias("ForcePrefix")]
|
||||
public SwitchParameter ForceSecretsPrefix { get; set; }
|
||||
|
||||
private readonly List<InfisicalSecret> _buffer = new List<InfisicalSecret>();
|
||||
|
||||
protected override void ProcessRecord()
|
||||
@@ -66,9 +75,11 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
{
|
||||
}
|
||||
|
||||
Logger.Information("Export-InfisicalSecrets", string.Concat("Exporting ", _buffer.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " secret(s) as ", Format.ToString(), (Path != null ? string.Concat(" to '", Path.FullName, "'") : string.Empty), "."));
|
||||
|
||||
InfisicalExportRequest request = new InfisicalExportRequest
|
||||
{
|
||||
Secrets = _buffer.ToArray(),
|
||||
Secrets = ApplySecretsPrefix(_buffer, SecretsPrefix, ForceSecretsPrefix.IsPresent),
|
||||
Format = Format,
|
||||
Path = Path,
|
||||
Scope = Scope,
|
||||
@@ -81,10 +92,42 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("ExportInfisicalSecretsCmdlet", string.Concat("Export-", Format.ToString()), exception);
|
||||
WriteErrorForException("ExportInfisicalSecretsCmdlet", string.Concat("Export-", Format.ToString()), exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static InfisicalSecret[] ApplySecretsPrefix(List<InfisicalSecret> source, string prefix, bool force)
|
||||
{
|
||||
if (string.IsNullOrEmpty(prefix)) { return source.ToArray(); }
|
||||
|
||||
InfisicalSecret[] result = new InfisicalSecret[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
InfisicalSecret original = source[i];
|
||||
result[i] = new InfisicalSecret
|
||||
{
|
||||
Id = original.Id,
|
||||
InternalId = original.InternalId,
|
||||
Workspace = original.Workspace,
|
||||
Environment = original.Environment,
|
||||
Version = original.Version,
|
||||
Type = original.Type,
|
||||
SecretName = InfisicalPrefix.Apply(original.SecretName, prefix, force),
|
||||
SecretValue = original.SecretValue,
|
||||
SecretValueHidden = original.SecretValueHidden,
|
||||
SecretPath = original.SecretPath,
|
||||
SecretComment = original.SecretComment,
|
||||
CreatedAtUtc = original.CreatedAtUtc,
|
||||
UpdatedAtUtc = original.UpdatedAtUtc,
|
||||
IsRotatedSecret = original.IsRotatedSecret,
|
||||
RotationId = original.RotationId,
|
||||
Tags = original.Tags,
|
||||
SecretMetadata = original.SecretMetadata
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Encoding ResolveEncoding(InfisicalExportEncoding encoding)
|
||||
{
|
||||
switch (encoding)
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
[Alias("Name")]
|
||||
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; }
|
||||
|
||||
@@ -29,6 +29,14 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
try
|
||||
{
|
||||
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);
|
||||
|
||||
if (string.Equals(ParameterSetName, "ById", StringComparison.Ordinal))
|
||||
@@ -46,6 +54,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
|
||||
InfisicalCertificateApplication[] all = client.ListCertificateApplications(connection, ProjectId, Limit, Offset);
|
||||
Logger.Information("Get-InfisicalCertificateApplication", string.Concat("Returned ", all.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " certificate application(s)."));
|
||||
foreach (InfisicalCertificateApplication app in all)
|
||||
{
|
||||
WriteObject(app);
|
||||
@@ -53,7 +62,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("GetInfisicalCertificateApplicationCmdlet", "GetCertificateApplication", exception);
|
||||
WriteErrorForException("GetInfisicalCertificateApplicationCmdlet", "GetCertificateApplication", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,21 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
[Alias("CertificateProfileId")]
|
||||
public string ProfileId { get; set; }
|
||||
|
||||
[Parameter(Mandatory = true)] public string ProjectId { get; set; }
|
||||
[Parameter] public string ProjectId { get; set; }
|
||||
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
try
|
||||
{
|
||||
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);
|
||||
|
||||
InfisicalCertificateApplicationEnrollment enrollment = client.GetCertificateApplicationEnrollment(connection, ApplicationId, ProfileId, ProjectId);
|
||||
@@ -35,7 +43,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("GetInfisicalCertificateApplicationEnrollmentCmdlet", "GetCertificateApplicationEnrollment", exception);
|
||||
WriteErrorForException("GetInfisicalCertificateApplicationEnrollmentCmdlet", "GetCertificateApplicationEnrollment", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
[Alias("Id")]
|
||||
public string CaId { get; set; }
|
||||
|
||||
[Parameter(Mandatory = true)] public string ProjectId { get; set; }
|
||||
[Parameter] public string ProjectId { get; set; }
|
||||
|
||||
[Parameter(ParameterSetName = "List")]
|
||||
[ValidateSet("Internal", "Acme", "Any")]
|
||||
@@ -25,6 +25,14 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
try
|
||||
{
|
||||
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);
|
||||
|
||||
if (string.Equals(ParameterSetName, "ById", StringComparison.Ordinal))
|
||||
@@ -52,6 +60,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Information("Get-InfisicalCertificateAuthority", string.Concat("Returned ", all.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " certificate authority/authorities (kind=", Kind, ")."));
|
||||
foreach (InfisicalCertificateAuthority ca in all)
|
||||
{
|
||||
WriteObject(ca);
|
||||
@@ -59,7 +68,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("GetInfisicalCertificateAuthorityCmdlet", "GetCertificateAuthority", exception);
|
||||
WriteErrorForException("GetInfisicalCertificateAuthorityCmdlet", "GetCertificateAuthority", exception);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
[Alias("Id", "Identifier")]
|
||||
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 FriendlyName { get; set; }
|
||||
[Parameter(ParameterSetName = "List")] public string Search { get; set; }
|
||||
@@ -59,6 +59,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
||||
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))
|
||||
{
|
||||
InfisicalCertificate cert = client.RetrieveCertificate(connection, SerialNumber);
|
||||
@@ -70,6 +71,14 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
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
|
||||
{
|
||||
ProjectId = ProjectId,
|
||||
@@ -129,10 +138,12 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
|
||||
query.Offset = (query.Offset ?? 0) + page.Certificates.Length;
|
||||
}
|
||||
|
||||
Logger.Information("Get-InfisicalCertificate", string.Concat("Returned ", emitted.ToString(System.Globalization.CultureInfo.InvariantCulture), " certificate(s)."));
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("GetInfisicalCertificateCmdlet", "GetCertificate", exception);
|
||||
WriteErrorForException("GetInfisicalCertificateCmdlet", "GetCertificate", exception);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
[Alias("Id", "CertificatePolicyId")]
|
||||
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; }
|
||||
|
||||
@@ -25,6 +25,14 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
try
|
||||
{
|
||||
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);
|
||||
|
||||
if (string.Equals(ParameterSetName, "ById", StringComparison.Ordinal))
|
||||
@@ -39,6 +47,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
|
||||
InfisicalCertificatePolicy[] all = client.ListCertificatePolicies(connection, ProjectId, Limit, Offset);
|
||||
Logger.Information("Get-InfisicalCertificatePolicy", string.Concat("Returned ", all.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " certificate policy/policies."));
|
||||
foreach (InfisicalCertificatePolicy policy in all)
|
||||
{
|
||||
WriteObject(policy);
|
||||
@@ -46,7 +55,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("GetInfisicalCertificatePolicyCmdlet", "GetCertificatePolicy", exception);
|
||||
WriteErrorForException("GetInfisicalCertificatePolicyCmdlet", "GetCertificatePolicy", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,19 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
[Alias("Id", "CertificateProfileId")]
|
||||
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; }
|
||||
|
||||
@@ -27,6 +39,14 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
try
|
||||
{
|
||||
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);
|
||||
|
||||
if (string.Equals(ParameterSetName, "ById", StringComparison.Ordinal))
|
||||
@@ -41,7 +61,12 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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)
|
||||
{
|
||||
WriteObject(profile);
|
||||
@@ -49,7 +74,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("GetInfisicalCertificateProfileCmdlet", "GetCertificateProfile", exception);
|
||||
WriteErrorForException("GetInfisicalCertificateProfileCmdlet", "GetCertificateProfile", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
|
||||
InfisicalEnvironment[] envs = client.List(connection, ProjectId);
|
||||
Logger.Information("Get-InfisicalEnvironment", string.Concat("Returned ", envs.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " environment(s)."));
|
||||
foreach (InfisicalEnvironment env in envs)
|
||||
{
|
||||
WriteObject(env);
|
||||
@@ -42,7 +43,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("GetInfisicalEnvironmentCmdlet", "GetEnvironment", exception);
|
||||
WriteErrorForException("GetInfisicalEnvironmentCmdlet", "GetEnvironment", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Management.Automation;
|
||||
|
||||
namespace PSInfisicalAPI.Cmdlets
|
||||
{
|
||||
[Cmdlet(VerbsCommon.Get, "InfisicalEnvironmentVariable")]
|
||||
[OutputType(typeof(string))]
|
||||
public sealed class GetInfisicalEnvironmentVariableCmdlet : InfisicalCmdletBase
|
||||
{
|
||||
private const string Component = "Get-InfisicalEnvironmentVariable";
|
||||
|
||||
private static readonly EnvironmentVariableTarget[] TargetOrder = new[]
|
||||
{
|
||||
EnvironmentVariableTarget.Process,
|
||||
EnvironmentVariableTarget.User,
|
||||
EnvironmentVariableTarget.Machine
|
||||
};
|
||||
|
||||
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
|
||||
[ValidateNotNullOrEmpty]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Parameter(Position = 1)]
|
||||
public EnvironmentVariableTarget? Scope { get; set; }
|
||||
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
EnvironmentVariableTarget[] targets = Scope.HasValue ? new[] { Scope.Value } : TargetOrder;
|
||||
|
||||
foreach (EnvironmentVariableTarget target in targets)
|
||||
{
|
||||
Logger.Verbose(Component, string.Concat("Searching ", target.ToString(), " scope for environment variable '", Name, "'."));
|
||||
|
||||
string value;
|
||||
try
|
||||
{
|
||||
value = Environment.GetEnvironmentVariable(Name, target);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Logger.Verbose(Component, string.Concat("Failed to read ", target.ToString(), " scope for environment variable '", Name, "': ", exception.Message));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
Logger.Information(Component, string.Concat("Found environment variable '", Name, "' in ", target.ToString(), " scope."));
|
||||
WriteObject(value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
string scopeDescription = Scope.HasValue ? string.Concat(Scope.Value.ToString(), " scope") : "Process, User, or Machine scope";
|
||||
Logger.Information(Component, string.Concat("Environment variable '", Name, "' was not found in ", scopeDescription, "."));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
|
||||
InfisicalFolder[] folders = client.List(connection, ProjectId, Environment, Path);
|
||||
Logger.Information("Get-InfisicalFolder", string.Concat("Returned ", folders.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " folder(s) from '", Path ?? "/", "'."));
|
||||
foreach (InfisicalFolder folder in folders)
|
||||
{
|
||||
WriteObject(folder);
|
||||
@@ -44,7 +45,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("GetInfisicalFolderCmdlet", "GetFolder", exception);
|
||||
WriteErrorForException("GetInfisicalFolderCmdlet", "GetFolder", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Management.Automation;
|
||||
using PSInfisicalAPI.Connections;
|
||||
using PSInfisicalAPI.Models;
|
||||
using PSInfisicalAPI.Organizations;
|
||||
|
||||
namespace PSInfisicalAPI.Cmdlets
|
||||
{
|
||||
[Cmdlet(VerbsCommon.Get, "InfisicalOrganization", DefaultParameterSetName = "List")]
|
||||
[OutputType(typeof(InfisicalOrganization))]
|
||||
public sealed class GetInfisicalOrganizationCmdlet : InfisicalCmdletBase
|
||||
{
|
||||
[Parameter(ParameterSetName = "Single", Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)]
|
||||
[Alias("Id")]
|
||||
public string OrganizationId { get; set; }
|
||||
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
try
|
||||
{
|
||||
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
||||
InfisicalOrganizationClient client = new InfisicalOrganizationClient(HttpClient, Logger);
|
||||
|
||||
if (string.Equals(ParameterSetName, "Single", StringComparison.Ordinal))
|
||||
{
|
||||
InfisicalOrganization organization = client.Retrieve(connection, OrganizationId);
|
||||
if (organization != null)
|
||||
{
|
||||
WriteObject(organization);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
InfisicalOrganization[] organizations = client.List(connection);
|
||||
Logger.Information("Get-InfisicalOrganization", string.Concat("Returned ", organizations.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " organization(s)."));
|
||||
foreach (InfisicalOrganization organization in organizations)
|
||||
{
|
||||
WriteObject(organization);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
WriteErrorForException("GetInfisicalOrganizationCmdlet", "GetOrganization", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,13 +14,21 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
[Alias("SubscriberName", "Slug")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Parameter(Mandatory = true)] public string ProjectId { get; set; }
|
||||
[Parameter] public string ProjectId { get; set; }
|
||||
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
try
|
||||
{
|
||||
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);
|
||||
|
||||
if (string.Equals(ParameterSetName, "ByName", StringComparison.Ordinal))
|
||||
@@ -35,6 +43,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
|
||||
InfisicalPkiSubscriber[] all = client.ListPkiSubscribers(connection, ProjectId);
|
||||
Logger.Information("Get-InfisicalPkiSubscriber", string.Concat("Returned ", all.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " PKI subscriber(s)."));
|
||||
foreach (InfisicalPkiSubscriber subscriber in all)
|
||||
{
|
||||
WriteObject(subscriber);
|
||||
@@ -42,7 +51,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("GetInfisicalPkiSubscriberCmdlet", "GetPkiSubscriber", exception);
|
||||
WriteErrorForException("GetInfisicalPkiSubscriberCmdlet", "GetPkiSubscriber", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
|
||||
InfisicalProject[] projects = client.List(connection, Type, IncludeRoles.IsPresent);
|
||||
Logger.Information("Get-InfisicalProject", string.Concat("Returned ", projects.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " project(s)."));
|
||||
foreach (InfisicalProject project in projects)
|
||||
{
|
||||
WriteObject(project);
|
||||
@@ -46,7 +47,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("GetInfisicalProjectCmdlet", "GetProject", exception);
|
||||
WriteErrorForException("GetInfisicalProjectCmdlet", "GetProject", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Management.Automation;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace PSInfisicalAPI.Cmdlets
|
||||
{
|
||||
[Cmdlet(VerbsCommon.Get, "InfisicalSANList")]
|
||||
[OutputType(typeof(string[]))]
|
||||
public sealed class GetInfisicalSANListCmdlet : InfisicalCmdletBase
|
||||
{
|
||||
private const string Component = "GetInfisicalSANListCmdlet";
|
||||
|
||||
[Parameter]
|
||||
[ValidateNotNullOrEmpty]
|
||||
public string InclusionExpression { get; set; }
|
||||
|
||||
[Parameter]
|
||||
[ValidateNotNullOrEmpty]
|
||||
public string ExclusionExpression { get; set; }
|
||||
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
try
|
||||
{
|
||||
Regex includeRegex = !string.IsNullOrEmpty(InclusionExpression) ? new Regex(InclusionExpression, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant) : null;
|
||||
Regex excludeRegex = !string.IsNullOrEmpty(ExclusionExpression) ? new Regex(ExclusionExpression, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant) : null;
|
||||
|
||||
List<string> sans = new List<string>();
|
||||
HashSet<string> seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
string deviceName = Dns.GetHostName();
|
||||
AddUnique(sans, seen, deviceName);
|
||||
|
||||
HashSet<string> suffixes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
string globalDomain = IPGlobalProperties.GetIPGlobalProperties().DomainName;
|
||||
if (!string.IsNullOrEmpty(globalDomain)) { suffixes.Add(globalDomain); }
|
||||
|
||||
NetworkInterface[] adapters = NetworkInterface.GetAllNetworkInterfaces();
|
||||
foreach (NetworkInterface adapter in adapters)
|
||||
{
|
||||
if (adapter.OperationalStatus != OperationalStatus.Up) { continue; }
|
||||
if (adapter.NetworkInterfaceType == NetworkInterfaceType.Loopback) { continue; }
|
||||
|
||||
IPInterfaceProperties props = adapter.GetIPProperties();
|
||||
if (!string.IsNullOrEmpty(props.DnsSuffix)) { suffixes.Add(props.DnsSuffix); }
|
||||
|
||||
foreach (UnicastIPAddressInformation unicast in props.UnicastAddresses)
|
||||
{
|
||||
IPAddress ip = unicast.Address;
|
||||
if (ip.AddressFamily == AddressFamily.InterNetwork && IsRfc1918OrCgnat(ip))
|
||||
{
|
||||
AddUnique(sans, seen, ip.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string suffix in suffixes)
|
||||
{
|
||||
string trimmed = suffix.Trim().TrimStart('.');
|
||||
if (!string.IsNullOrEmpty(trimmed))
|
||||
{
|
||||
AddUnique(sans, seen, string.Concat(deviceName, ".", trimmed));
|
||||
}
|
||||
}
|
||||
|
||||
AddUnique(sans, seen, "127.0.0.1");
|
||||
AddUnique(sans, seen, "::1");
|
||||
|
||||
List<string> filtered = new List<string>(sans.Count);
|
||||
foreach (string san in sans)
|
||||
{
|
||||
if (includeRegex != null && !includeRegex.IsMatch(san)) { continue; }
|
||||
if (excludeRegex != null && excludeRegex.IsMatch(san)) { continue; }
|
||||
filtered.Add(san);
|
||||
}
|
||||
|
||||
WriteObject(filtered.ToArray(), false);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
WriteErrorForException(Component, "GetSANList", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsRfc1918OrCgnat(IPAddress ip)
|
||||
{
|
||||
byte[] bytes = ip.GetAddressBytes();
|
||||
if (bytes.Length != 4) { return false; }
|
||||
|
||||
if (bytes[0] == 10) { return true; }
|
||||
if (bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31) { return true; }
|
||||
if (bytes[0] == 192 && bytes[1] == 168) { return true; }
|
||||
if (bytes[0] == 100 && bytes[1] >= 64 && bytes[1] <= 127) { return true; }
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void AddUnique(List<string> list, HashSet<string> seen, string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) { return; }
|
||||
if (seen.Add(value)) { list.Add(value); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException(Component, "GetScepMdmProfile", exception);
|
||||
WriteErrorForException(Component, "GetScepMdmProfile", exception);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Management.Automation;
|
||||
using PSInfisicalAPI.Connections;
|
||||
using PSInfisicalAPI.Models;
|
||||
@@ -57,8 +58,13 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
InfisicalSecret secret = client.Retrieve(connection, query);
|
||||
if (secret != null)
|
||||
{
|
||||
Logger.Information("Get-InfisicalSecret", string.Concat("Returned 1 secret for '", SecretName, "'."));
|
||||
WriteObject(secret);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Information("Get-InfisicalSecret", string.Concat("No secret returned for '", SecretName, "'."));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -79,6 +85,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
};
|
||||
|
||||
InfisicalSecret[] secrets = client.List(connection, listQuery);
|
||||
Logger.Information("Get-InfisicalSecret", string.Concat("Returned ", secrets.Length.ToString(CultureInfo.InvariantCulture), " secret(s) from '", SecretPath ?? "/", "' (recursive=", Recursive.IsPresent ? "true" : "false", ", includeImports=", IncludeImports.IsPresent ? "true" : "false", ")."));
|
||||
foreach (InfisicalSecret secret in secrets)
|
||||
{
|
||||
WriteObject(secret);
|
||||
@@ -86,7 +93,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("GetInfisicalSecretCmdlet", "GetSecret", exception);
|
||||
WriteErrorForException("GetInfisicalSecretCmdlet", "GetSecret", exception);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.Management.Automation;
|
||||
using PSInfisicalAPI.Connections;
|
||||
using PSInfisicalAPI.Models;
|
||||
using PSInfisicalAPI.SubOrganizations;
|
||||
|
||||
namespace PSInfisicalAPI.Cmdlets
|
||||
{
|
||||
[Cmdlet(VerbsCommon.Get, "InfisicalSubOrganization", DefaultParameterSetName = "List")]
|
||||
[OutputType(typeof(InfisicalSubOrganization))]
|
||||
public sealed class GetInfisicalSubOrganizationCmdlet : InfisicalCmdletBase
|
||||
{
|
||||
[Parameter(ParameterSetName = "Single", Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)]
|
||||
[Alias("Id")]
|
||||
public string SubOrganizationId { get; set; }
|
||||
|
||||
[Parameter(ParameterSetName = "List")] public int? Limit { get; set; }
|
||||
[Parameter(ParameterSetName = "List")] public int? Offset { get; set; }
|
||||
[Parameter(ParameterSetName = "List")] public string Search { get; set; }
|
||||
[Parameter(ParameterSetName = "List")] public string OrderBy { get; set; }
|
||||
|
||||
[Parameter(ParameterSetName = "List")]
|
||||
[ValidateSet("asc", "desc")]
|
||||
public string OrderDirection { get; set; }
|
||||
|
||||
[Parameter(ParameterSetName = "List")] public SwitchParameter IsAccessible { get; set; }
|
||||
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
try
|
||||
{
|
||||
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
||||
InfisicalSubOrganizationClient client = new InfisicalSubOrganizationClient(HttpClient, Logger);
|
||||
|
||||
if (string.Equals(ParameterSetName, "Single", StringComparison.Ordinal))
|
||||
{
|
||||
InfisicalSubOrganization subOrganization = client.Retrieve(connection, SubOrganizationId);
|
||||
if (subOrganization != null)
|
||||
{
|
||||
WriteObject(subOrganization);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
bool? isAccessible = MyInvocation.BoundParameters.ContainsKey("IsAccessible") ? (bool?)IsAccessible.IsPresent : null;
|
||||
InfisicalSubOrganization[] subOrganizations = client.List(connection, Limit, Offset, Search, OrderBy, OrderDirection, isAccessible);
|
||||
Logger.Information("Get-InfisicalSubOrganization", string.Concat("Returned ", subOrganizations.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " sub-organization(s)."));
|
||||
foreach (InfisicalSubOrganization subOrganization in subOrganizations)
|
||||
{
|
||||
WriteObject(subOrganization);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
WriteErrorForException("GetInfisicalSubOrganizationCmdlet", "GetSubOrganization", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
|
||||
InfisicalTag[] tags = client.List(connection, ProjectId);
|
||||
Logger.Information("Get-InfisicalTag", string.Concat("Returned ", tags.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " tag(s)."));
|
||||
foreach (InfisicalTag tag in tags)
|
||||
{
|
||||
WriteObject(tag);
|
||||
@@ -42,7 +43,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("GetInfisicalTagCmdlet", "GetTag", exception);
|
||||
WriteErrorForException("GetInfisicalTagCmdlet", "GetTag", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Management.Automation;
|
||||
using System.Security;
|
||||
using PSInfisicalAPI.Common;
|
||||
using PSInfisicalAPI.Errors;
|
||||
using PSInfisicalAPI.Imports;
|
||||
using PSInfisicalAPI.Models;
|
||||
using PSInfisicalAPI.Security;
|
||||
|
||||
namespace PSInfisicalAPI.Cmdlets
|
||||
{
|
||||
[Cmdlet(VerbsData.Import, "InfisicalSecret")]
|
||||
[OutputType(typeof(Dictionary<string, SecureString>))]
|
||||
[OutputType(typeof(Dictionary<string, string>))]
|
||||
public sealed class ImportInfisicalSecretCmdlet : InfisicalCmdletBase
|
||||
{
|
||||
[Parameter(Mandatory = true, Position = 0)]
|
||||
[ValidateNotNull]
|
||||
public FileInfo Path { get; set; }
|
||||
|
||||
[Parameter(Mandatory = true)]
|
||||
public InfisicalImportFormat Format { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public InfisicalDuplicateKeyBehavior DuplicateKeyBehavior { get; set; } = InfisicalDuplicateKeyBehavior.Error;
|
||||
|
||||
[Parameter]
|
||||
public SwitchParameter AsPlainText { get; set; }
|
||||
|
||||
[Parameter]
|
||||
[Alias("Prefix")]
|
||||
public string SecretsPrefix { get; set; }
|
||||
|
||||
[Parameter]
|
||||
[Alias("ForcePrefix")]
|
||||
public SwitchParameter ForceSecretsPrefix { get; set; }
|
||||
|
||||
protected override void EndProcessing()
|
||||
{
|
||||
try
|
||||
{
|
||||
Path.Refresh();
|
||||
if (!Path.Exists)
|
||||
{
|
||||
throw new InfisicalImportException(string.Concat("Import path does not exist: ", Path.FullName));
|
||||
}
|
||||
|
||||
IInfisicalImporter importer = InfisicalImporterFactory.Create(Format);
|
||||
IList<KeyValuePair<string, string>> pairs = importer.Import(Path);
|
||||
Logger.Information("Import-InfisicalSecret", string.Concat("Parsed ", pairs.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " secret pair(s) from '", Path.FullName, "' (format=", Format.ToString(), ")."));
|
||||
|
||||
if (AsPlainText.IsPresent)
|
||||
{
|
||||
Dictionary<string, string> plain = BuildDictionary<string>(pairs, value => value ?? string.Empty);
|
||||
Logger.Information("Import-InfisicalSecret", string.Concat("Built plain-text dictionary with ", plain.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " entry/entries."));
|
||||
WriteObject(plain);
|
||||
}
|
||||
else
|
||||
{
|
||||
Dictionary<string, SecureString> secure = BuildDictionary<SecureString>(pairs, value => SecureStringUtility.ToReadOnlySecureString(value ?? string.Empty));
|
||||
Logger.Information("Import-InfisicalSecret", string.Concat("Built SecureString dictionary with ", secure.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " entry/entries."));
|
||||
WriteObject(secure);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
WriteErrorForException("ImportInfisicalSecretCmdlet", "ImportSecret", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, TValue> BuildDictionary<TValue>(
|
||||
IList<KeyValuePair<string, string>> pairs,
|
||||
Func<string, TValue> valueSelector)
|
||||
{
|
||||
Dictionary<string, TValue> dictionary = new Dictionary<string, TValue>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (KeyValuePair<string, string> pair in pairs)
|
||||
{
|
||||
if (pair.Key == null) { continue; }
|
||||
string key = InfisicalPrefix.Apply(pair.Key, SecretsPrefix, ForceSecretsPrefix.IsPresent);
|
||||
|
||||
if (dictionary.ContainsKey(key))
|
||||
{
|
||||
if (DuplicateKeyBehavior == InfisicalDuplicateKeyBehavior.Error)
|
||||
{
|
||||
throw new InfisicalConfigurationException(string.Concat("Duplicate secret name encountered: ", key));
|
||||
}
|
||||
|
||||
if (DuplicateKeyBehavior == InfisicalDuplicateKeyBehavior.LastWins)
|
||||
{
|
||||
dictionary[key] = valueSelector(pair.Value);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
dictionary[key] = valueSelector(pair.Value);
|
||||
}
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Management.Automation;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using PSInfisicalAPI.Connections;
|
||||
using PSInfisicalAPI.Models;
|
||||
using PSInfisicalAPI.Organizations;
|
||||
using PSInfisicalAPI.Projects;
|
||||
using PSInfisicalAPI.Errors;
|
||||
using PSInfisicalAPI.Http;
|
||||
using PSInfisicalAPI.Logging;
|
||||
@@ -9,8 +17,13 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
{
|
||||
public abstract class InfisicalCmdletBase : PSCmdlet
|
||||
{
|
||||
private const string CertManagerProjectType = "cert-manager";
|
||||
|
||||
private IInfisicalLogger _logger;
|
||||
private IInfisicalHttpClient _httpClient;
|
||||
private bool? _isElevated;
|
||||
private string _resolvedCertManagerProjectId;
|
||||
private bool _certManagerProjectResolved;
|
||||
|
||||
protected IInfisicalLogger Logger
|
||||
{
|
||||
@@ -44,12 +57,228 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
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>
|
||||
/// 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
|
||||
/// System.Security.Principal.Windows reference. Cached for the lifetime of the cmdlet instance.
|
||||
/// </summary>
|
||||
protected bool IsElevated()
|
||||
{
|
||||
if (_isElevated.HasValue) { return _isElevated.Value; }
|
||||
|
||||
try
|
||||
{
|
||||
Collection<PSObject> results = InvokeCommand.InvokeScript(
|
||||
"[bool]([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)");
|
||||
_isElevated = results != null
|
||||
&& results.Count > 0
|
||||
&& results[0] != null
|
||||
&& results[0].BaseObject != null
|
||||
&& Convert.ToBoolean(results[0].BaseObject, CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Logger.Verbose(GetType().Name, string.Concat("Elevation check failed; assuming non-elevated. ", exception.Message));
|
||||
_isElevated = false;
|
||||
}
|
||||
|
||||
return _isElevated.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Picks the certificate store location when the caller did not bind -StoreLocation. An elevated process
|
||||
/// installs machine-wide so services and other users can use the certificate; a non-elevated one has no
|
||||
/// write access to LocalMachine and falls back to the user's own stores.
|
||||
/// </summary>
|
||||
protected StoreLocation ResolveStoreLocation(StoreLocation boundValue)
|
||||
{
|
||||
if (MyInvocation != null && MyInvocation.BoundParameters.ContainsKey("StoreLocation"))
|
||||
{
|
||||
return boundValue;
|
||||
}
|
||||
|
||||
bool elevated = IsElevated();
|
||||
StoreLocation resolved = elevated ? StoreLocation.LocalMachine : StoreLocation.CurrentUser;
|
||||
Logger.Information(GetType().Name, string.Concat(
|
||||
"Process is ", elevated ? "elevated" : "not elevated",
|
||||
"; defaulting -StoreLocation to ", resolved.ToString(),
|
||||
". Pass -StoreLocation explicitly to override."));
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reports an operation failure as a non-terminating error, which is what lets -ErrorAction decide the
|
||||
/// outcome: Continue prints and carries on, SilentlyContinue and Ignore suppress, Inquire prompts, and
|
||||
/// Stop is promoted by the engine into a terminating error that try/catch sees. Scripts that want to
|
||||
/// catch these must ask for it with -ErrorAction Stop or $ErrorActionPreference = 'Stop'.
|
||||
/// </summary>
|
||||
protected void WriteErrorForException(string component, string operation, Exception exception)
|
||||
{
|
||||
ErrorRecord record = BuildFailureRecord(component, operation, exception);
|
||||
WriteError(record);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reports a failure the cmdlet cannot continue past regardless of -ErrorAction. Reserved for aborts that
|
||||
/// are not per-item failures; ordinary operation failures belong on <see cref="WriteErrorForException"/>.
|
||||
/// </summary>
|
||||
protected void ThrowTerminatingForException(string component, string operation, Exception exception)
|
||||
{
|
||||
ErrorRecord record = BuildFailureRecord(component, operation, exception);
|
||||
ThrowTerminatingError(record);
|
||||
}
|
||||
|
||||
private ErrorRecord BuildFailureRecord(string component, string operation, Exception exception)
|
||||
{
|
||||
if (IsPipelineControlException(exception))
|
||||
{
|
||||
ExceptionDispatchInfo.Capture(exception).Throw();
|
||||
}
|
||||
|
||||
InfisicalErrorDetails details = InfisicalErrorHandler.BuildDetails(component, operation, exception);
|
||||
InfisicalErrorHandler.LogFailure(Logger, details);
|
||||
ErrorRecord record = InfisicalErrorHandler.ToErrorRecord(exception, details);
|
||||
ThrowTerminatingError(record);
|
||||
return InfisicalErrorHandler.ToErrorRecord(exception, details);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identifies exceptions the PowerShell engine uses to unwind a pipeline rather than to report a fault.
|
||||
/// Downstream cmdlets that stop early (<c>Select-Object -First</c>, <c>Where-Object</c> feeding such a
|
||||
/// cmdlet, Ctrl+C) make <see cref="System.Management.Automation.Cmdlet.WriteObject(object)"/> throw one of
|
||||
/// these. Reporting them as errors turns a normal early exit into spurious "The pipeline has been stopped."
|
||||
/// output, so they must propagate untouched.
|
||||
/// </summary>
|
||||
protected static bool IsPipelineControlException(Exception exception)
|
||||
{
|
||||
// StopUpstreamCommandsException (internal, thrown by Select-Object -First) derives from
|
||||
// PipelineStoppedException, so the base type covers it.
|
||||
return exception is PipelineStoppedException
|
||||
|| exception is PipelineClosedException
|
||||
|| exception is HaltCommandException;
|
||||
}
|
||||
|
||||
protected string ResolveApiVersion(InfisicalConnection connection, string explicitValue)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,20 +32,22 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
{
|
||||
try
|
||||
{
|
||||
StoreLocation resolvedStoreLocation = ResolveStoreLocation(StoreLocation);
|
||||
|
||||
X509Certificate2 cert = ResolveCertificate();
|
||||
if (cert == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
InstallCertificate(cert, StoreName, StoreLocation);
|
||||
InstallCertificate(cert, StoreName, resolvedStoreLocation);
|
||||
|
||||
if (IncludeChain.IsPresent && string.Equals(ParameterSetName, "FromCertificate", StringComparison.Ordinal) == false)
|
||||
{
|
||||
foreach (X509Certificate2 chainCert in ResolveChain())
|
||||
{
|
||||
StoreName chainStore = InfisicalCertificateRequestHelpers.GetChainCertificateTargetStore(chainCert);
|
||||
InstallCertificate(chainCert, chainStore, StoreLocation);
|
||||
InstallCertificate(chainCert, chainStore, resolvedStoreLocation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +58,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("InstallInfisicalCertificateCmdlet", "InstallCertificate", exception);
|
||||
WriteErrorForException("InstallInfisicalCertificateCmdlet", "InstallCertificate", exception);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +90,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
return;
|
||||
}
|
||||
|
||||
InfisicalCertificateRequestHelpers.WarnIfInteractiveTrustPromptExpected(storeName, storeLocation, Logger, "InstallInfisicalCertificateCmdlet");
|
||||
store.Add(cert);
|
||||
Logger.Information("InstallInfisicalCertificateCmdlet", string.Concat("Installed certificate to ", target, "."));
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("NewInfisicalEnvironmentCmdlet", "CreateEnvironment", exception);
|
||||
WriteErrorForException("NewInfisicalEnvironmentCmdlet", "CreateEnvironment", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("NewInfisicalFolderCmdlet", "CreateFolder", exception);
|
||||
WriteErrorForException("NewInfisicalFolderCmdlet", "CreateFolder", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Management.Automation;
|
||||
using PSInfisicalAPI.Connections;
|
||||
using PSInfisicalAPI.Models;
|
||||
using PSInfisicalAPI.Organizations;
|
||||
|
||||
namespace PSInfisicalAPI.Cmdlets
|
||||
{
|
||||
[Cmdlet(VerbsCommon.New, "InfisicalOrganization", SupportsShouldProcess = true)]
|
||||
[OutputType(typeof(InfisicalOrganization))]
|
||||
public sealed class NewInfisicalOrganizationCmdlet : InfisicalCmdletBase
|
||||
{
|
||||
[Parameter(Mandatory = true, Position = 0)] public string Name { get; set; }
|
||||
[Parameter] public string Slug { get; set; }
|
||||
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!ShouldProcess(Name, "Create Infisical organization"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
||||
InfisicalOrganizationClient client = new InfisicalOrganizationClient(HttpClient, Logger);
|
||||
InfisicalOrganization organization = client.Create(connection, Name, Slug);
|
||||
if (organization != null)
|
||||
{
|
||||
WriteObject(organization);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
WriteErrorForException("NewInfisicalOrganizationCmdlet", "CreateOrganization", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("NewInfisicalProjectCmdlet", "CreateProject", exception);
|
||||
WriteErrorForException("NewInfisicalProjectCmdlet", "CreateProject", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("NewInfisicalScepDynamicChallengeCmdlet", "GenerateScepDynamicChallenge", exception);
|
||||
WriteErrorForException("NewInfisicalScepDynamicChallengeCmdlet", "GenerateScepDynamicChallenge", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,10 +58,12 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
Secrets = InfisicalBulkSecretConverter.ToCreateItems(Secrets)
|
||||
};
|
||||
|
||||
Logger.Information("New-InfisicalSecret", string.Concat("Bulk-creating ", Secrets.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " secret(s)."));
|
||||
InfisicalSecretsClient bulkClient = new InfisicalSecretsClient(HttpClient, Logger);
|
||||
InfisicalSecret[] created = bulkClient.CreateBatch(connection, bulk);
|
||||
if (created != null)
|
||||
{
|
||||
Logger.Information("New-InfisicalSecret", string.Concat("Server returned ", created.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " created secret(s)."));
|
||||
foreach (InfisicalSecret secret in created) { WriteObject(secret); }
|
||||
}
|
||||
|
||||
@@ -97,7 +99,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("NewInfisicalSecretCmdlet", "CreateSecret", exception);
|
||||
WriteErrorForException("NewInfisicalSecretCmdlet", "CreateSecret", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Management.Automation;
|
||||
using PSInfisicalAPI.Connections;
|
||||
using PSInfisicalAPI.Models;
|
||||
using PSInfisicalAPI.SubOrganizations;
|
||||
|
||||
namespace PSInfisicalAPI.Cmdlets
|
||||
{
|
||||
[Cmdlet(VerbsCommon.New, "InfisicalSubOrganization", SupportsShouldProcess = true)]
|
||||
[OutputType(typeof(InfisicalSubOrganization))]
|
||||
public sealed class NewInfisicalSubOrganizationCmdlet : InfisicalCmdletBase
|
||||
{
|
||||
[Parameter(Mandatory = true, Position = 0)] public string Name { get; set; }
|
||||
[Parameter(Mandatory = true, Position = 1)] public string Slug { get; set; }
|
||||
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!ShouldProcess(Name, "Create Infisical sub-organization"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
||||
InfisicalSubOrganizationClient client = new InfisicalSubOrganizationClient(HttpClient, Logger);
|
||||
InfisicalSubOrganization subOrganization = client.Create(connection, Name, Slug);
|
||||
if (subOrganization != null)
|
||||
{
|
||||
WriteObject(subOrganization);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
WriteErrorForException("NewInfisicalSubOrganizationCmdlet", "CreateSubOrganization", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("NewInfisicalTagCmdlet", "CreateTag", exception);
|
||||
WriteErrorForException("NewInfisicalTagCmdlet", "CreateTag", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("RemoveInfisicalEnvironmentCmdlet", "DeleteEnvironment", exception);
|
||||
WriteErrorForException("RemoveInfisicalEnvironmentCmdlet", "DeleteEnvironment", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("RemoveInfisicalFolderCmdlet", "DeleteFolder", exception);
|
||||
WriteErrorForException("RemoveInfisicalFolderCmdlet", "DeleteFolder", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Management.Automation;
|
||||
using PSInfisicalAPI.Connections;
|
||||
using PSInfisicalAPI.Organizations;
|
||||
|
||||
namespace PSInfisicalAPI.Cmdlets
|
||||
{
|
||||
[Cmdlet(VerbsCommon.Remove, "InfisicalOrganization", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
|
||||
public sealed class RemoveInfisicalOrganizationCmdlet : InfisicalCmdletBase
|
||||
{
|
||||
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)]
|
||||
[Alias("Id")]
|
||||
public string OrganizationId { get; set; }
|
||||
|
||||
[Parameter] public SwitchParameter PassThru { get; set; }
|
||||
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
try
|
||||
{
|
||||
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
||||
|
||||
if (!ShouldProcess(OrganizationId, "Remove Infisical organization"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
InfisicalOrganizationClient client = new InfisicalOrganizationClient(HttpClient, Logger);
|
||||
client.Delete(connection, OrganizationId);
|
||||
|
||||
if (PassThru.IsPresent)
|
||||
{
|
||||
WriteObject(OrganizationId);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
WriteErrorForException("RemoveInfisicalOrganizationCmdlet", "DeleteOrganization", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("RemoveInfisicalProjectCmdlet", "DeleteProject", exception);
|
||||
WriteErrorForException("RemoveInfisicalProjectCmdlet", "DeleteProject", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
SecretNames = SecretNames
|
||||
};
|
||||
|
||||
Logger.Information("Remove-InfisicalSecret", string.Concat("Bulk-removing ", SecretNames.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " secret(s)."));
|
||||
client.DeleteBatch(connection, bulk);
|
||||
|
||||
if (PassThru.IsPresent)
|
||||
@@ -78,7 +79,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("RemoveInfisicalSecretCmdlet", "DeleteSecret", exception);
|
||||
WriteErrorForException("RemoveInfisicalSecretCmdlet", "DeleteSecret", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Management.Automation;
|
||||
using PSInfisicalAPI.Connections;
|
||||
using PSInfisicalAPI.SubOrganizations;
|
||||
|
||||
namespace PSInfisicalAPI.Cmdlets
|
||||
{
|
||||
[Cmdlet(VerbsCommon.Remove, "InfisicalSubOrganization", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
|
||||
public sealed class RemoveInfisicalSubOrganizationCmdlet : InfisicalCmdletBase
|
||||
{
|
||||
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)]
|
||||
[Alias("Id")]
|
||||
public string SubOrganizationId { get; set; }
|
||||
|
||||
[Parameter] public SwitchParameter PassThru { get; set; }
|
||||
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
try
|
||||
{
|
||||
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
||||
|
||||
if (!ShouldProcess(SubOrganizationId, "Remove Infisical sub-organization"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
InfisicalSubOrganizationClient client = new InfisicalSubOrganizationClient(HttpClient, Logger);
|
||||
client.Delete(connection, SubOrganizationId);
|
||||
|
||||
if (PassThru.IsPresent)
|
||||
{
|
||||
WriteObject(SubOrganizationId);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
WriteErrorForException("RemoveInfisicalSubOrganizationCmdlet", "DeleteSubOrganization", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("RemoveInfisicalTagCmdlet", "DeleteTag", exception);
|
||||
WriteErrorForException("RemoveInfisicalTagCmdlet", "DeleteTag", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Management.Automation;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using PSInfisicalAPI.Connections;
|
||||
using PSInfisicalAPI.Errors;
|
||||
using PSInfisicalAPI.Models;
|
||||
using PSInfisicalAPI.Pki;
|
||||
|
||||
@@ -27,7 +28,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
[Alias("ProfileId")]
|
||||
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 string CommonName { get; set; }
|
||||
[Parameter] public string Country { get; set; }
|
||||
@@ -48,7 +49,15 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
[Parameter(ParameterSetName = "ByProfile")] public string NotBefore { get; set; }
|
||||
[Parameter(ParameterSetName = "ByCa")]
|
||||
[Parameter(ParameterSetName = "ByProfile")] public string NotAfter { get; set; }
|
||||
[Parameter(ParameterSetName = "ByCa")] public string FriendlyName { get; set; }
|
||||
// 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.
|
||||
[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")]
|
||||
[Parameter(ParameterSetName = "ByProfile")] public string[] KeyUsage { get; set; }
|
||||
@@ -76,14 +85,23 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
try
|
||||
{
|
||||
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);
|
||||
|
||||
// Resolved once so reuse detection looks in the same stores the install will write to.
|
||||
StoreLocation resolvedStoreLocation = ResolveStoreLocation(StoreLocation);
|
||||
|
||||
InfisicalCsrSubject csrSubject = InfisicalCertificateRequestHelpers.MergeSubject(Subject, CommonName, Country, State, Locality, Organization, OrganizationalUnit, EmailAddress);
|
||||
List<string> dnsNames = BuildDnsNames(csrSubject);
|
||||
List<string> ipAddresses = new List<string>();
|
||||
List<string> dnsNames = BuildDnsNames(csrSubject, ipAddresses);
|
||||
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."); }
|
||||
|
||||
X509Certificate2 existing = TryFindExisting(client, connection, ProjectId, csrSubject.CommonName);
|
||||
X509Certificate2 existing = TryFindExisting(client, connection, ProjectId, csrSubject.CommonName, resolvedStoreLocation, dnsNames, ipAddresses);
|
||||
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"), ")."));
|
||||
@@ -104,20 +122,26 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception bundleException)
|
||||
{
|
||||
if (IsPipelineControlException(bundleException)) { throw; }
|
||||
Logger.Verbose(Component, string.Concat("Infisical bundle fetch for reuse path failed (continuing with local-only chain): ", bundleException.Message));
|
||||
}
|
||||
}
|
||||
|
||||
// Reconciled on the reuse path too, so a metadata change lands without forcing reissuance.
|
||||
reuseResult.Metadata = ApplyMetadata(client, connection, existing.SerialNumber);
|
||||
|
||||
WriteObject(reuseResult);
|
||||
return;
|
||||
}
|
||||
|
||||
string target = string.Concat("PKI subscriber '", PkiSubscriberSlug ?? "(n/a)", "', CA '", CertificateAuthorityId ?? "(n/a)", "', or profile '", CertificateProfileId ?? "(n/a)", "' for CN=", csrSubject.CommonName);
|
||||
string issuer = ResolveIssuancePath(client, connection);
|
||||
|
||||
string target = string.Concat(issuer, " for CN=", csrSubject.CommonName);
|
||||
if (!ShouldProcess(target, "Request new certificate")) { return; }
|
||||
|
||||
InfisicalCsrOptions csrOptions = new InfisicalCsrOptions { KeyAlgorithm = KeyAlgorithm, RsaKeySize = KeySize, EcCurve = Curve };
|
||||
InfisicalCsrResult csr = InfisicalCsrBuilder.Build(csrSubject, dnsNames, IpAddress, csrOptions);
|
||||
InfisicalSignedCertificate signed = SignCertificate(client, connection, ProjectId, csr.CsrPem);
|
||||
InfisicalCsrResult csr = InfisicalCsrBuilder.Build(csrSubject, dnsNames, ipAddresses, csrOptions);
|
||||
InfisicalSignedCertificate signed = SignCertificate(client, connection, ProjectId, csr.CsrPem, csrSubject);
|
||||
signed.PrivateKeyPem = csr.PrivateKeyPem;
|
||||
|
||||
if (string.IsNullOrEmpty(signed.CertificatePem))
|
||||
@@ -129,19 +153,29 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
return;
|
||||
}
|
||||
|
||||
X509KeyStorageFlags resolvedFlags = ResolveEffectiveKeyStorageFlags();
|
||||
X509KeyStorageFlags resolvedFlags = ResolveEffectiveKeyStorageFlags(resolvedStoreLocation);
|
||||
X509Certificate2 cert = PemCertificateBuilder.Build(signed.CertificatePem, signed.PrivateKeyPem, signed.CertificateChainPem, resolvedFlags);
|
||||
|
||||
InfisicalCertificateRequestHelpers.ApplyFriendlyName(cert, ResolveLocalFriendlyName(csrSubject), Logger, Component);
|
||||
|
||||
if (Install.IsPresent)
|
||||
{
|
||||
InfisicalCertificateRequestHelpers.InstallToStore(cert, StoreName, StoreLocation, Force.IsPresent, Logger, Component);
|
||||
// Issuers first, so the leaf is already chainable the moment it lands in the store.
|
||||
if (InstallChain.IsPresent)
|
||||
{
|
||||
InfisicalCertificateRequestHelpers.InstallChain(signed, StoreLocation, Force.IsPresent, Logger, Component);
|
||||
InfisicalCertificateRequestHelpers.InstallChain(signed, resolvedStoreLocation, Force.IsPresent, Logger, Component);
|
||||
}
|
||||
|
||||
InfisicalCertificateRequestHelpers.InstallToStore(cert, StoreName, resolvedStoreLocation, Force.IsPresent, Logger, Component);
|
||||
|
||||
if (InstallChain.IsPresent)
|
||||
{
|
||||
InfisicalCertificateRequestHelpers.VerifyInstalledChain(cert, Logger, Component);
|
||||
}
|
||||
}
|
||||
|
||||
InfisicalCertificateResult resultObj = InfisicalCertificateRequestHelpers.BuildResult(cert, signed);
|
||||
resultObj.Metadata = ApplyMetadata(client, connection, signed.SerialNumber);
|
||||
|
||||
bool hasExplicitPath = !string.IsNullOrEmpty(PrivateKeyPath);
|
||||
if (hasExplicitPath && !string.IsNullOrEmpty(resultObj.PrivateKeyPem))
|
||||
@@ -160,55 +194,324 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException(Component, "RequestCertificate", exception);
|
||||
WriteErrorForException(Component, "RequestCertificate", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private List<string> BuildDnsNames(InfisicalCsrSubject subject)
|
||||
/// <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>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private string ResolveLocalFriendlyName(InfisicalCsrSubject subject)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(FriendlyName)) { return FriendlyName; }
|
||||
if (subject == null || string.IsNullOrEmpty(subject.CommonName)) { return null; }
|
||||
return subject.CommonName.ToUpperInvariant();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// States which issuer will sign this request, and rejects an unusable one before a keypair is generated.
|
||||
/// Direct CA signing is only permitted when the CA has direct issuance enabled; without this check the
|
||||
/// cmdlet builds a CSR and learns that from a 400 at the very end.
|
||||
/// </summary>
|
||||
private string ResolveIssuancePath(InfisicalPkiClient client, InfisicalConnection connection)
|
||||
{
|
||||
if (string.Equals(ParameterSetName, "BySubscriber", StringComparison.Ordinal))
|
||||
{
|
||||
Logger.Information(Component, string.Concat("Issuing via PKI subscriber '", PkiSubscriberSlug, "' in project '", ProjectId, "'."));
|
||||
return string.Concat("PKI subscriber '", PkiSubscriberSlug, "'");
|
||||
}
|
||||
|
||||
if (string.Equals(ParameterSetName, "ByProfile", StringComparison.Ordinal))
|
||||
{
|
||||
Logger.Information(Component, string.Concat("Issuing via certificate profile '", CertificateProfileId, "' in project '", ProjectId, "'."));
|
||||
return string.Concat("certificate profile '", CertificateProfileId, "'");
|
||||
}
|
||||
|
||||
InfisicalCertificateAuthority ca = null;
|
||||
try
|
||||
{
|
||||
ca = client.GetInternalCertificateAuthority(connection, CertificateAuthorityId, ProjectId);
|
||||
}
|
||||
catch (Exception lookupException)
|
||||
{
|
||||
if (IsPipelineControlException(lookupException)) { throw; }
|
||||
|
||||
// A caller may be able to sign without permission to read the CA record. Defer to the API.
|
||||
Logger.Verbose(Component, string.Concat("Could not read certificate authority '", CertificateAuthorityId, "' for preflight (continuing): ", lookupException.Message));
|
||||
return string.Concat("certificate authority '", CertificateAuthorityId, "'");
|
||||
}
|
||||
|
||||
if (ca != null && ca.EnableDirectIssuance.HasValue && !ca.EnableDirectIssuance.Value)
|
||||
{
|
||||
throw new InfisicalConfigurationException(BuildDirectIssuanceGuidance(ca));
|
||||
}
|
||||
|
||||
string caLabel = ca != null ? (ca.Name ?? ca.FriendlyName ?? CertificateAuthorityId) : CertificateAuthorityId;
|
||||
Logger.Information(Component, string.Concat("Issuing directly via certificate authority '", caLabel, "' (", CertificateAuthorityId, "); direct issuance is enabled."));
|
||||
return string.Concat("certificate authority '", caLabel, "'");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restates the direct-issuance restriction in terms of the parameters that resolve it. Infisical's REST
|
||||
/// API exposes no certificate-template issuance route, so the alternatives are a subscriber or a profile.
|
||||
/// </summary>
|
||||
private string BuildDirectIssuanceGuidance(InfisicalCertificateAuthority ca)
|
||||
{
|
||||
string caLabel = ca != null ? (ca.Name ?? ca.FriendlyName ?? CertificateAuthorityId) : CertificateAuthorityId;
|
||||
return string.Concat(
|
||||
"Certificate authority '", caLabel, "' (", CertificateAuthorityId, ") has direct issuance disabled, so it cannot sign a CSR on its own. ",
|
||||
"Either enable direct issuance on the CA in Infisical (Certificate Authorities > the CA > Enable Direct Issuance), ",
|
||||
"or issue through a subscriber or profile instead: Request-InfisicalCertificate -PkiSubscriberSlug <name> (see Get-InfisicalPkiSubscriber -ProjectId '", ProjectId ?? "<projectId>", "') ",
|
||||
"or -CertificateProfileId <id> (see Get-InfisicalCertificateProfile).");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits the requested SAN values into DNS names and IP addresses. Get-InfisicalSANList emits both kinds
|
||||
/// in one list, so IP literals arriving through -DnsName are routed to the IP SAN bucket rather than
|
||||
/// emitted as malformed dNSName entries.
|
||||
/// </summary>
|
||||
private List<string> BuildDnsNames(InfisicalCsrSubject subject, List<string> ipAddresses)
|
||||
{
|
||||
List<string> result = new List<string>();
|
||||
if (DnsName != null) { foreach (string dns in DnsName) { if (!string.IsNullOrEmpty(dns)) { result.Add(dns); } } }
|
||||
if (result.Count == 0)
|
||||
AddSanCandidates(DnsName, result, ipAddresses);
|
||||
AddSanCandidates(IpAddress, null, ipAddresses);
|
||||
|
||||
// Fall back to the local FQDN only when no SAN of either kind was requested; an explicit IP-only
|
||||
// request must not silently pick up this machine's name.
|
||||
if (result.Count == 0 && ipAddresses.Count == 0)
|
||||
{
|
||||
string fqdn = InfisicalCertificateRequestHelpers.ResolveLocalFqdn();
|
||||
if (!string.IsNullOrEmpty(fqdn)) { result.Add(fqdn); }
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(subject.CommonName) && !result.Contains(subject.CommonName)) { result.Insert(0, subject.CommonName); }
|
||||
// The common name is mirrored into the SAN list because most validators ignore a CN that has no
|
||||
// matching SAN entry. An IP common name belongs in the iPAddress bucket, not the dNSName one.
|
||||
if (!string.IsNullOrEmpty(subject.CommonName))
|
||||
{
|
||||
if (IsIpLiteral(subject.CommonName))
|
||||
{
|
||||
if (!ipAddresses.Contains(subject.CommonName)) { ipAddresses.Insert(0, subject.CommonName); }
|
||||
}
|
||||
else if (!result.Contains(subject.CommonName))
|
||||
{
|
||||
result.Insert(0, subject.CommonName);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private X509Certificate2 TryFindExisting(InfisicalPkiClient client, InfisicalConnection connection, string projectId, string commonName)
|
||||
private static void AddSanCandidates(IEnumerable<string> candidates, List<string> dnsNames, List<string> ipAddresses)
|
||||
{
|
||||
if (candidates == null) { return; }
|
||||
foreach (string candidate in candidates)
|
||||
{
|
||||
if (string.IsNullOrEmpty(candidate)) { continue; }
|
||||
string value = candidate.Trim();
|
||||
if (value.Length == 0) { continue; }
|
||||
|
||||
List<string> bucket = IsIpLiteral(value) ? ipAddresses : dnsNames;
|
||||
if (bucket != null && !bucket.Contains(value)) { bucket.Add(value); }
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsIpLiteral(string value)
|
||||
{
|
||||
System.Net.IPAddress parsed;
|
||||
return System.Net.IPAddress.TryParse(value, out parsed);
|
||||
}
|
||||
|
||||
/// <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>();
|
||||
bool searchCompleted = false;
|
||||
|
||||
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);
|
||||
searchCompleted = true;
|
||||
if (page != null && page.Certificates != null)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (IsPipelineControlException(searchException)) { throw; }
|
||||
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;
|
||||
}
|
||||
|
||||
private X509KeyStorageFlags ResolveEffectiveKeyStorageFlags()
|
||||
/// <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)
|
||||
{
|
||||
if (MyInvocation.BoundParameters.ContainsKey("KeyStorageFlags"))
|
||||
{
|
||||
return KeyStorageFlags;
|
||||
}
|
||||
|
||||
return InfisicalCertificateRequestHelpers.ResolveKeyStorageFlags(PrivateKeyProtection, PersistKey.IsPresent, MachineKey.IsPresent);
|
||||
// A certificate installed into LocalMachine needs its private key in the machine key store, otherwise
|
||||
// the key lands in the calling user's profile and the installed certificate has no usable key for
|
||||
// services or other users.
|
||||
bool machineKey = MachineKey.IsPresent || (Install.IsPresent && storeLocation == StoreLocation.LocalMachine);
|
||||
if (machineKey && !MachineKey.IsPresent)
|
||||
{
|
||||
Logger.Verbose(Component, "Installing to LocalMachine; using a machine key store so the private key is usable outside this user profile.");
|
||||
}
|
||||
|
||||
return InfisicalCertificateRequestHelpers.ResolveKeyStorageFlags(PrivateKeyProtection, PersistKey.IsPresent, machineKey);
|
||||
}
|
||||
|
||||
private InfisicalSignedCertificate SignCertificate(InfisicalPkiClient client, InfisicalConnection connection, string projectId, string csrPem)
|
||||
private InfisicalSignedCertificate SignCertificate(InfisicalPkiClient client, InfisicalConnection connection, string projectId, string csrPem, InfisicalCsrSubject subject)
|
||||
{
|
||||
if (string.Equals(ParameterSetName, "BySubscriber", StringComparison.Ordinal))
|
||||
{
|
||||
@@ -217,11 +520,49 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
|
||||
if (string.Equals(ParameterSetName, "ByProfile", StringComparison.Ordinal))
|
||||
{
|
||||
InfisicalCsrSubject subject = InfisicalCertificateRequestHelpers.MergeSubject(Subject, CommonName, Country, State, Locality, Organization, OrganizationalUnit, EmailAddress);
|
||||
return client.IssueCertificateByProfile(connection, CertificateProfileId, csrPem, subject.CommonName, subject.Organization, subject.OrganizationalUnit, subject.Country, subject.State, subject.Locality, Ttl, NotBefore, NotAfter, KeyUsage, ExtendedKeyUsage);
|
||||
}
|
||||
|
||||
return client.SignCertificateByCa(connection, CertificateAuthorityId, csrPem, CommonName, null, Ttl, NotBefore, NotAfter, FriendlyName, PkiCollectionId, KeyUsage, ExtendedKeyUsage);
|
||||
try
|
||||
{
|
||||
return client.SignCertificateByCa(connection, CertificateAuthorityId, csrPem, subject.CommonName, null, Ttl, NotBefore, NotAfter, FriendlyName, PkiCollectionId, KeyUsage, ExtendedKeyUsage);
|
||||
}
|
||||
catch (InfisicalApiException apiException)
|
||||
{
|
||||
throw EnrichDirectIssuanceFailure(apiException);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Backstop for when the preflight in <see cref="ResolveIssuancePath"/> could not read the CA record and
|
||||
/// the API rejects the signing request instead. The raw 400 does not say which cmdlet parameter to reach
|
||||
/// for, so restate it in the module's own terms.
|
||||
/// </summary>
|
||||
private InfisicalApiException EnrichDirectIssuanceFailure(InfisicalApiException apiException)
|
||||
{
|
||||
if (apiException == null || apiException.StatusCode != 400) { return apiException; }
|
||||
|
||||
string apiMessage = apiException.ApiErrorMessage ?? apiException.Message ?? string.Empty;
|
||||
if (apiMessage.IndexOf("template or subscriber", StringComparison.OrdinalIgnoreCase) < 0)
|
||||
{
|
||||
return apiException;
|
||||
}
|
||||
|
||||
string guidance = string.Concat(BuildDirectIssuanceGuidance(null), " Original API error: ", apiMessage);
|
||||
|
||||
return new InfisicalApiException(guidance, apiException)
|
||||
{
|
||||
StatusCode = apiException.StatusCode,
|
||||
ReasonPhrase = apiException.ReasonPhrase,
|
||||
ApiErrorCode = apiException.ApiErrorCode,
|
||||
ApiErrorMessage = apiException.ApiErrorMessage,
|
||||
ApiRequestId = apiException.ApiRequestId,
|
||||
SanitizedBody = apiException.SanitizedBody,
|
||||
EndpointName = apiException.EndpointName,
|
||||
RequestMethod = apiException.RequestMethod,
|
||||
Component = apiException.Component,
|
||||
Operation = apiException.Operation
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Management.Automation;
|
||||
using System.Text.RegularExpressions;
|
||||
using PSInfisicalAPI.Models;
|
||||
using PSInfisicalAPI.Process;
|
||||
|
||||
namespace PSInfisicalAPI.Cmdlets
|
||||
{
|
||||
[Cmdlet(VerbsLifecycle.Start, "InfisicalProcess", DefaultParameterSetName = WindowStyleSet, SupportsShouldProcess = true)]
|
||||
[OutputType(typeof(InfisicalProcessResult))]
|
||||
public sealed class StartInfisicalProcessCmdlet : InfisicalCmdletBase
|
||||
{
|
||||
private const string Component = "StartInfisicalProcessCmdlet";
|
||||
private const string WindowStyleSet = "WindowStyle";
|
||||
private const string CreateNoWindowSet = "CreateNoWindow";
|
||||
|
||||
[Parameter(Mandatory = true, Position = 0)]
|
||||
[ValidateNotNullOrEmpty]
|
||||
[Alias("FP")]
|
||||
public string FilePath { get; set; }
|
||||
|
||||
[Parameter]
|
||||
[Alias("WD")]
|
||||
public DirectoryInfo WorkingDirectory { get; set; }
|
||||
|
||||
[Parameter]
|
||||
[AllowEmptyCollection]
|
||||
[AllowNull]
|
||||
[Alias("AL")]
|
||||
public string[] ArgumentList { get; set; }
|
||||
|
||||
[Parameter]
|
||||
[AllowEmptyCollection]
|
||||
[AllowNull]
|
||||
[Alias("AECL")]
|
||||
public string[] AcceptableExitCodeList { get; set; }
|
||||
|
||||
[Parameter(ParameterSetName = WindowStyleSet)]
|
||||
[ValidateSet("Normal", "Hidden", "Minimized", "Maximized")]
|
||||
[Alias("WS")]
|
||||
public string WindowStyle { get; set; } = "Hidden";
|
||||
|
||||
[Parameter(ParameterSetName = CreateNoWindowSet)]
|
||||
[Alias("CNW")]
|
||||
public SwitchParameter CreateNoWindow { get; set; }
|
||||
|
||||
[Parameter]
|
||||
[Alias("NW")]
|
||||
public SwitchParameter NoWait { get; set; }
|
||||
|
||||
[Parameter]
|
||||
[ValidateSet("AboveNormal", "BelowNormal", "High", "Idle", "Normal", "RealTime")]
|
||||
[Alias("P")]
|
||||
public string Priority { get; set; } = "Normal";
|
||||
|
||||
[Parameter]
|
||||
[Alias("ET")]
|
||||
public TimeSpan ExecutionTimeout { get; set; }
|
||||
|
||||
[Parameter]
|
||||
[Alias("ETI")]
|
||||
public TimeSpan ExecutionTimeoutInterval { get; set; } = TimeSpan.FromSeconds(15);
|
||||
|
||||
[Parameter]
|
||||
[Alias("SIO")]
|
||||
public object[] StandardInputObjectList { get; set; }
|
||||
|
||||
[Parameter]
|
||||
[Alias("ENV")]
|
||||
public IDictionary EnvironmentVariables { get; set; }
|
||||
|
||||
[Parameter]
|
||||
[Alias("StandardOutputParsingExpression", "SOPE", "PE")]
|
||||
public Regex ParsingExpression { get; set; }
|
||||
|
||||
[Parameter]
|
||||
[Alias("SAL")]
|
||||
public SwitchParameter SecureArgumentList { get; set; }
|
||||
|
||||
[Parameter]
|
||||
[Alias("LO")]
|
||||
public SwitchParameter LogOutput { get; set; }
|
||||
|
||||
[Parameter]
|
||||
[Alias("COE")]
|
||||
public SwitchParameter ContinueOnError { get; set; }
|
||||
|
||||
[Parameter(ValueFromPipeline = true)]
|
||||
[Alias("Secret", "InputObject")]
|
||||
public InfisicalSecret[] Secrets { get; set; }
|
||||
|
||||
[Parameter]
|
||||
[Alias("Prefix")]
|
||||
public string SecretsPrefix { get; set; }
|
||||
|
||||
[Parameter]
|
||||
[Alias("ForcePrefix")]
|
||||
public SwitchParameter ForceSecretsPrefix { get; set; }
|
||||
|
||||
private readonly List<InfisicalSecret> _secretBuffer = new List<InfisicalSecret>();
|
||||
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
if (Secrets == null) { return; }
|
||||
foreach (InfisicalSecret secret in Secrets)
|
||||
{
|
||||
if (secret != null) { _secretBuffer.Add(secret); }
|
||||
}
|
||||
}
|
||||
|
||||
protected override void EndProcessing()
|
||||
{
|
||||
try
|
||||
{
|
||||
string target = string.IsNullOrEmpty(WorkingDirectory != null ? WorkingDirectory.FullName : null)
|
||||
? FilePath
|
||||
: string.Concat(FilePath, " (in ", WorkingDirectory.FullName, ")");
|
||||
|
||||
if (!ShouldProcess(target, "Start process with Infisical secrets")) { return; }
|
||||
|
||||
int envVarCount = EnvironmentVariables != null ? EnvironmentVariables.Count : 0;
|
||||
Logger.Information("Start-InfisicalProcess", string.Concat("Injecting ", _secretBuffer.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " secret(s) and ", envVarCount.ToString(System.Globalization.CultureInfo.InvariantCulture), " explicit environment variable(s) into process environment."));
|
||||
|
||||
InfisicalProcessOptions options = new InfisicalProcessOptions
|
||||
{
|
||||
FilePath = FilePath,
|
||||
WorkingDirectory = WorkingDirectory,
|
||||
ArgumentList = ArgumentList,
|
||||
AcceptableExitCodeList = AcceptableExitCodeList,
|
||||
WindowStyle = WindowStyle,
|
||||
CreateNoWindow = CreateNoWindow.IsPresent,
|
||||
NoWait = NoWait.IsPresent,
|
||||
Priority = Priority,
|
||||
ExecutionTimeout = MyInvocation.BoundParameters.ContainsKey("ExecutionTimeout") ? (TimeSpan?)ExecutionTimeout : null,
|
||||
ExecutionTimeoutInterval = ExecutionTimeoutInterval,
|
||||
StandardInputObjectList = StandardInputObjectList,
|
||||
EnvironmentVariables = EnvironmentVariables,
|
||||
ParsingExpression = ParsingExpression,
|
||||
SecureArgumentList = SecureArgumentList.IsPresent,
|
||||
LogOutput = LogOutput.IsPresent,
|
||||
ContinueOnError = ContinueOnError.IsPresent,
|
||||
Secrets = _secretBuffer.ToArray(),
|
||||
SecretsPrefix = SecretsPrefix,
|
||||
ForceSecretsPrefix = ForceSecretsPrefix.IsPresent
|
||||
};
|
||||
|
||||
InfisicalProcessResult result = InfisicalProcessRunner.Run(options, Logger);
|
||||
WriteObject(result);
|
||||
|
||||
if (!result.Succeeded && !NoWait.IsPresent && !ContinueOnError.IsPresent)
|
||||
{
|
||||
string message = string.Concat("Process '", FilePath, "' exited with code ", result.ExitCode.HasValue ? result.ExitCode.Value.ToString() : "<null>", " which is not in the acceptable exit code list.");
|
||||
InvalidOperationException exception = new InvalidOperationException(message);
|
||||
ErrorRecord error = new ErrorRecord(exception, "StartInfisicalProcess.UnacceptableExitCode", ErrorCategory.InvalidResult, result);
|
||||
WriteError(error);
|
||||
}
|
||||
}
|
||||
catch (PipelineStoppedException) { throw; }
|
||||
catch (Exception exception)
|
||||
{
|
||||
WriteErrorForException(Component, "StartProcess", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("UninstallInfisicalCertificateCmdlet", "UninstallCertificate", exception);
|
||||
WriteErrorForException("UninstallInfisicalCertificateCmdlet", "UninstallCertificate", exception);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("UpdateInfisicalEnvironmentCmdlet", "UpdateEnvironment", exception);
|
||||
WriteErrorForException("UpdateInfisicalEnvironmentCmdlet", "UpdateEnvironment", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("UpdateInfisicalFolderCmdlet", "UpdateFolder", exception);
|
||||
WriteErrorForException("UpdateInfisicalFolderCmdlet", "UpdateFolder", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Management.Automation;
|
||||
using PSInfisicalAPI.Connections;
|
||||
using PSInfisicalAPI.Models;
|
||||
using PSInfisicalAPI.Organizations;
|
||||
|
||||
namespace PSInfisicalAPI.Cmdlets
|
||||
{
|
||||
[Cmdlet(VerbsData.Update, "InfisicalOrganization", SupportsShouldProcess = true)]
|
||||
[OutputType(typeof(InfisicalOrganization))]
|
||||
public sealed class UpdateInfisicalOrganizationCmdlet : InfisicalCmdletBase
|
||||
{
|
||||
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)]
|
||||
[Alias("Id")]
|
||||
public string OrganizationId { get; set; }
|
||||
|
||||
[Parameter] public string Name { get; set; }
|
||||
[Parameter] public string Slug { get; set; }
|
||||
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
try
|
||||
{
|
||||
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
||||
|
||||
if (!ShouldProcess(OrganizationId, "Update Infisical organization"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
InfisicalOrganizationClient client = new InfisicalOrganizationClient(HttpClient, Logger);
|
||||
InfisicalOrganization organization = client.Update(connection, OrganizationId, Name, Slug);
|
||||
if (organization != null)
|
||||
{
|
||||
WriteObject(organization);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
WriteErrorForException("UpdateInfisicalOrganizationCmdlet", "UpdateOrganization", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("UpdateInfisicalProjectCmdlet", "UpdateProject", exception);
|
||||
WriteErrorForException("UpdateInfisicalProjectCmdlet", "UpdateProject", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,10 +56,12 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
Secrets = InfisicalBulkSecretConverter.ToUpdateItems(Secrets)
|
||||
};
|
||||
|
||||
Logger.Information("Update-InfisicalSecret", string.Concat("Bulk-updating ", Secrets.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " secret(s)."));
|
||||
InfisicalSecretsClient bulkClient = new InfisicalSecretsClient(HttpClient, Logger);
|
||||
InfisicalSecret[] updated = bulkClient.UpdateBatch(connection, bulk);
|
||||
if (updated != null)
|
||||
{
|
||||
Logger.Information("Update-InfisicalSecret", string.Concat("Server returned ", updated.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " updated secret(s)."));
|
||||
foreach (InfisicalSecret secret in updated) { WriteObject(secret); }
|
||||
}
|
||||
|
||||
@@ -96,7 +98,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("UpdateInfisicalSecretCmdlet", "UpdateSecret", exception);
|
||||
WriteErrorForException("UpdateInfisicalSecretCmdlet", "UpdateSecret", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Management.Automation;
|
||||
using PSInfisicalAPI.Connections;
|
||||
using PSInfisicalAPI.Models;
|
||||
using PSInfisicalAPI.SubOrganizations;
|
||||
|
||||
namespace PSInfisicalAPI.Cmdlets
|
||||
{
|
||||
[Cmdlet(VerbsData.Update, "InfisicalSubOrganization", SupportsShouldProcess = true)]
|
||||
[OutputType(typeof(InfisicalSubOrganization))]
|
||||
public sealed class UpdateInfisicalSubOrganizationCmdlet : InfisicalCmdletBase
|
||||
{
|
||||
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)]
|
||||
[Alias("Id")]
|
||||
public string SubOrganizationId { get; set; }
|
||||
|
||||
[Parameter] public string Name { get; set; }
|
||||
[Parameter] public string Slug { get; set; }
|
||||
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
try
|
||||
{
|
||||
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
||||
|
||||
if (!ShouldProcess(SubOrganizationId, "Update Infisical sub-organization"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
InfisicalSubOrganizationClient client = new InfisicalSubOrganizationClient(HttpClient, Logger);
|
||||
InfisicalSubOrganization subOrganization = client.Update(connection, SubOrganizationId, Name, Slug);
|
||||
if (subOrganization != null)
|
||||
{
|
||||
WriteObject(subOrganization);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
WriteErrorForException("UpdateInfisicalSubOrganizationCmdlet", "UpdateSubOrganization", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("UpdateInfisicalTagCmdlet", "UpdateTag", exception);
|
||||
WriteErrorForException("UpdateInfisicalTagCmdlet", "UpdateTag", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,22 +62,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException(Component, "WriteScepMdmProfileToWmi", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsElevated()
|
||||
{
|
||||
try
|
||||
{
|
||||
Collection<PSObject> results = InvokeCommand.InvokeScript("[bool]([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)");
|
||||
if (results == null || results.Count == 0 || results[0] == null || results[0].BaseObject == null) { return false; }
|
||||
return Convert.ToBoolean(results[0].BaseObject, CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Verbose(Component, string.Concat("Elevation check failed; assuming non-elevated. ", ex.Message));
|
||||
return false;
|
||||
WriteErrorForException(Component, "WriteScepMdmProfileToWmi", exception);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace PSInfisicalAPI.Common
|
||||
{
|
||||
public static class InfisicalPrefix
|
||||
{
|
||||
public static string Apply(string original, string prefix, bool force)
|
||||
{
|
||||
if (string.IsNullOrEmpty(prefix)) { return original ?? string.Empty; }
|
||||
if (original == null) { return prefix; }
|
||||
if (!force && original.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { return original; }
|
||||
return string.Concat(prefix, original);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,11 +44,49 @@ namespace PSInfisicalAPI.Endpoints
|
||||
public const string UpdateTag = "UpdateTag";
|
||||
public const string DeleteTag = "DeleteTag";
|
||||
|
||||
public const string ListOrganizations = "ListOrganizations";
|
||||
public const string RetrieveOrganization = "RetrieveOrganization";
|
||||
public const string CreateOrganization = "CreateOrganization";
|
||||
public const string UpdateOrganization = "UpdateOrganization";
|
||||
public const string DeleteOrganization = "DeleteOrganization";
|
||||
|
||||
public const string ListSubOrganizations = "ListSubOrganizations";
|
||||
public const string RetrieveSubOrganization = "RetrieveSubOrganization";
|
||||
public const string CreateSubOrganization = "CreateSubOrganization";
|
||||
public const string UpdateSubOrganization = "UpdateSubOrganization";
|
||||
public const string DeleteSubOrganization = "DeleteSubOrganization";
|
||||
|
||||
public const string ListInternalCertificateAuthorities = "ListInternalCertificateAuthorities";
|
||||
public const string RetrieveInternalCertificateAuthority = "RetrieveInternalCertificateAuthority";
|
||||
public const string SearchCertificates = "SearchCertificates";
|
||||
public const string RetrieveCertificate = "RetrieveCertificate";
|
||||
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 SignCertificateByCa = "SignCertificateByCa";
|
||||
public const string IssueCertificateByProfile = "IssueCertificateByProfile";
|
||||
|
||||
@@ -16,6 +16,8 @@ namespace PSInfisicalAPI.Endpoints
|
||||
RegisterEnvironments(Candidates);
|
||||
RegisterFolders(Candidates);
|
||||
RegisterTags(Candidates);
|
||||
RegisterOrganizations(Candidates);
|
||||
RegisterSubOrganizations(Candidates);
|
||||
RegisterPki(Candidates);
|
||||
}
|
||||
|
||||
@@ -286,6 +288,18 @@ namespace PSInfisicalAPI.Endpoints
|
||||
|
||||
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
|
||||
{
|
||||
Name = InfisicalEndpointNames.ListProjects,
|
||||
@@ -496,6 +510,125 @@ namespace PSInfisicalAPI.Endpoints
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
Name = InfisicalEndpointNames.ListOrganizations,
|
||||
Resource = "Organizations",
|
||||
Version = "v2",
|
||||
Method = "GET",
|
||||
Template = "/api/v2/organizations",
|
||||
RequiresAuthorization = true
|
||||
});
|
||||
|
||||
Add(map, new InfisicalEndpointDefinition
|
||||
{
|
||||
Name = InfisicalEndpointNames.RetrieveOrganization,
|
||||
Resource = "Organizations",
|
||||
Version = "v1",
|
||||
Method = "GET",
|
||||
Template = "/api/v1/organization/{organizationId}",
|
||||
RequiresAuthorization = true
|
||||
});
|
||||
|
||||
Add(map, new InfisicalEndpointDefinition
|
||||
{
|
||||
Name = InfisicalEndpointNames.CreateOrganization,
|
||||
Resource = "Organizations",
|
||||
Version = "v2",
|
||||
Method = "POST",
|
||||
Template = "/api/v2/organizations",
|
||||
RequiresAuthorization = true
|
||||
});
|
||||
|
||||
Add(map, new InfisicalEndpointDefinition
|
||||
{
|
||||
Name = InfisicalEndpointNames.UpdateOrganization,
|
||||
Resource = "Organizations",
|
||||
Version = "v1",
|
||||
Method = "PATCH",
|
||||
Template = "/api/v1/organization/{organizationId}",
|
||||
RequiresAuthorization = true
|
||||
});
|
||||
|
||||
Add(map, new InfisicalEndpointDefinition
|
||||
{
|
||||
Name = InfisicalEndpointNames.DeleteOrganization,
|
||||
Resource = "Organizations",
|
||||
Version = "v1",
|
||||
Method = "DELETE",
|
||||
Template = "/api/v1/organization/{organizationId}",
|
||||
RequiresAuthorization = true
|
||||
});
|
||||
}
|
||||
|
||||
private static void RegisterSubOrganizations(Dictionary<string, List<InfisicalEndpointDefinition>> map)
|
||||
{
|
||||
Add(map, new InfisicalEndpointDefinition
|
||||
{
|
||||
Name = InfisicalEndpointNames.ListSubOrganizations,
|
||||
Resource = "SubOrganizations",
|
||||
Version = "v1",
|
||||
Method = "GET",
|
||||
Template = "/api/v1/sub-organizations",
|
||||
RequiresAuthorization = true
|
||||
});
|
||||
|
||||
Add(map, new InfisicalEndpointDefinition
|
||||
{
|
||||
Name = InfisicalEndpointNames.RetrieveSubOrganization,
|
||||
Resource = "SubOrganizations",
|
||||
Version = "v1",
|
||||
Method = "GET",
|
||||
Template = "/api/v1/sub-organizations/{subOrgId}",
|
||||
RequiresAuthorization = true
|
||||
});
|
||||
|
||||
Add(map, new InfisicalEndpointDefinition
|
||||
{
|
||||
Name = InfisicalEndpointNames.CreateSubOrganization,
|
||||
Resource = "SubOrganizations",
|
||||
Version = "v1",
|
||||
Method = "POST",
|
||||
Template = "/api/v1/sub-organizations",
|
||||
RequiresAuthorization = true
|
||||
});
|
||||
|
||||
Add(map, new InfisicalEndpointDefinition
|
||||
{
|
||||
Name = InfisicalEndpointNames.UpdateSubOrganization,
|
||||
Resource = "SubOrganizations",
|
||||
Version = "v1",
|
||||
Method = "PATCH",
|
||||
Template = "/api/v1/sub-organizations/{subOrgId}",
|
||||
RequiresAuthorization = true
|
||||
});
|
||||
|
||||
Add(map, new InfisicalEndpointDefinition
|
||||
{
|
||||
Name = InfisicalEndpointNames.DeleteSubOrganization,
|
||||
Resource = "SubOrganizations",
|
||||
Version = "v1",
|
||||
Method = "DELETE",
|
||||
Template = "/api/v1/sub-organizations/{subOrgId}",
|
||||
RequiresAuthorization = true
|
||||
});
|
||||
}
|
||||
|
||||
private static void RegisterPki(Dictionary<string, List<InfisicalEndpointDefinition>> map)
|
||||
{
|
||||
Add(map, new InfisicalEndpointDefinition
|
||||
@@ -548,6 +681,11 @@ namespace PSInfisicalAPI.Endpoints
|
||||
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
|
||||
{
|
||||
Name = InfisicalEndpointNames.RetrieveCertificate,
|
||||
@@ -590,6 +728,63 @@ namespace PSInfisicalAPI.Endpoints
|
||||
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
|
||||
{
|
||||
Name = InfisicalEndpointNames.SignCertificateBySubscriber,
|
||||
@@ -607,7 +802,7 @@ namespace PSInfisicalAPI.Endpoints
|
||||
Resource = "Pki",
|
||||
Version = "v1",
|
||||
Method = "POST",
|
||||
Template = "/api/v1/pki/ca/{caId}/sign-certificate",
|
||||
Template = "/api/v1/cert-manager/ca/{caId}/sign-certificate",
|
||||
RequiresAuthorization = true,
|
||||
ContainsSecretMaterialInResponse = true
|
||||
});
|
||||
@@ -618,7 +813,7 @@ namespace PSInfisicalAPI.Endpoints
|
||||
Resource = "Pki",
|
||||
Version = "v1",
|
||||
Method = "POST",
|
||||
Template = "/api/v1/cert-manager/ca/{caId}/sign-certificate",
|
||||
Template = "/api/v1/pki/ca/{caId}/sign-certificate",
|
||||
RequiresAuthorization = true,
|
||||
ContainsSecretMaterialInResponse = true
|
||||
});
|
||||
|
||||
@@ -75,6 +75,13 @@ namespace PSInfisicalAPI.Errors
|
||||
public InfisicalExportException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
|
||||
public class InfisicalImportException : InfisicalException
|
||||
{
|
||||
public InfisicalImportException() { }
|
||||
public InfisicalImportException(string message) : base(message) { }
|
||||
public InfisicalImportException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
|
||||
public class InfisicalConfigurationException : InfisicalException
|
||||
{
|
||||
public InfisicalConfigurationException() { }
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using PSInfisicalAPI.Errors;
|
||||
|
||||
namespace PSInfisicalAPI.Imports
|
||||
{
|
||||
public sealed class EnvInfisicalImporter : IInfisicalImporter
|
||||
{
|
||||
public IList<KeyValuePair<string, string>> Import(FileInfo path)
|
||||
{
|
||||
if (path == null) { throw new InfisicalImportException("Path is required for ENV import."); }
|
||||
|
||||
List<KeyValuePair<string, string>> result = new List<KeyValuePair<string, string>>();
|
||||
string[] lines = File.ReadAllLines(path.FullName);
|
||||
|
||||
foreach (string raw in lines)
|
||||
{
|
||||
if (raw == null) { continue; }
|
||||
string line = raw.Trim();
|
||||
if (line.Length == 0) { continue; }
|
||||
if (line[0] == '#') { continue; }
|
||||
|
||||
int idx = line.IndexOf('=');
|
||||
if (idx <= 0) { continue; }
|
||||
|
||||
string key = line.Substring(0, idx).Trim();
|
||||
string value = line.Substring(idx + 1);
|
||||
if (key.Length == 0) { continue; }
|
||||
|
||||
result.Add(new KeyValuePair<string, string>(key, value));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace PSInfisicalAPI.Imports
|
||||
{
|
||||
public interface IInfisicalImporter
|
||||
{
|
||||
IList<KeyValuePair<string, string>> Import(FileInfo path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using PSInfisicalAPI.Errors;
|
||||
using PSInfisicalAPI.Models;
|
||||
|
||||
namespace PSInfisicalAPI.Imports
|
||||
{
|
||||
public static class InfisicalImporterFactory
|
||||
{
|
||||
public static IInfisicalImporter Create(InfisicalImportFormat format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case InfisicalImportFormat.Json: return new JsonInfisicalImporter();
|
||||
case InfisicalImportFormat.Yaml: return new YamlInfisicalImporter();
|
||||
case InfisicalImportFormat.Env: return new EnvInfisicalImporter();
|
||||
case InfisicalImportFormat.Xml: return new XmlInfisicalImporter();
|
||||
default: throw new InfisicalImportException(string.Concat("Unsupported import format: ", format.ToString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PSInfisicalAPI.Errors;
|
||||
|
||||
namespace PSInfisicalAPI.Imports
|
||||
{
|
||||
public sealed class JsonInfisicalImporter : IInfisicalImporter
|
||||
{
|
||||
public IList<KeyValuePair<string, string>> Import(FileInfo path)
|
||||
{
|
||||
if (path == null) { throw new InfisicalImportException("Path is required for JSON import."); }
|
||||
|
||||
List<KeyValuePair<string, string>> result = new List<KeyValuePair<string, string>>();
|
||||
string text = File.ReadAllText(path.FullName);
|
||||
JToken root = JToken.Parse(text);
|
||||
|
||||
if (root.Type == JTokenType.Array)
|
||||
{
|
||||
foreach (JToken item in (JArray)root)
|
||||
{
|
||||
if (item == null || item.Type != JTokenType.Object) { continue; }
|
||||
string key = ReadString((JObject)item, "SecretName") ?? ReadString((JObject)item, "secretName");
|
||||
string value = ReadString((JObject)item, "SecretValue") ?? ReadString((JObject)item, "secretValue");
|
||||
if (string.IsNullOrEmpty(key)) { continue; }
|
||||
result.Add(new KeyValuePair<string, string>(key, value ?? string.Empty));
|
||||
}
|
||||
}
|
||||
else if (root.Type == JTokenType.Object)
|
||||
{
|
||||
foreach (JProperty prop in ((JObject)root).Properties())
|
||||
{
|
||||
if (prop == null || string.IsNullOrEmpty(prop.Name)) { continue; }
|
||||
string value = prop.Value != null && prop.Value.Type != JTokenType.Null ? prop.Value.ToString() : string.Empty;
|
||||
result.Add(new KeyValuePair<string, string>(prop.Name, value));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InfisicalImportException("JSON import expects an array of secret objects or a flat key/value object.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string ReadString(JObject obj, string name)
|
||||
{
|
||||
JToken token;
|
||||
if (!obj.TryGetValue(name, out token) || token == null || token.Type == JTokenType.Null) { return null; }
|
||||
return token.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Xml;
|
||||
using PSInfisicalAPI.Errors;
|
||||
|
||||
namespace PSInfisicalAPI.Imports
|
||||
{
|
||||
public sealed class XmlInfisicalImporter : IInfisicalImporter
|
||||
{
|
||||
public IList<KeyValuePair<string, string>> Import(FileInfo path)
|
||||
{
|
||||
if (path == null) { throw new InfisicalImportException("Path is required for XML import."); }
|
||||
|
||||
List<KeyValuePair<string, string>> result = new List<KeyValuePair<string, string>>();
|
||||
XmlDocument document = new XmlDocument();
|
||||
document.Load(path.FullName);
|
||||
|
||||
XmlNode root = document.DocumentElement;
|
||||
if (root == null || !string.Equals(root.LocalName, "Secrets", System.StringComparison.Ordinal))
|
||||
{
|
||||
throw new InfisicalImportException("XML import expects a root <Secrets> element.");
|
||||
}
|
||||
|
||||
foreach (XmlNode node in root.ChildNodes)
|
||||
{
|
||||
if (node == null || node.NodeType != XmlNodeType.Element) { continue; }
|
||||
if (!string.Equals(node.LocalName, "Secret", System.StringComparison.Ordinal)) { continue; }
|
||||
|
||||
string key = ReadChild(node, "SecretName");
|
||||
string value = ReadChild(node, "SecretValue");
|
||||
if (string.IsNullOrEmpty(key)) { continue; }
|
||||
result.Add(new KeyValuePair<string, string>(key, value ?? string.Empty));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string ReadChild(XmlNode parent, string name)
|
||||
{
|
||||
foreach (XmlNode child in parent.ChildNodes)
|
||||
{
|
||||
if (child == null || child.NodeType != XmlNodeType.Element) { continue; }
|
||||
if (string.Equals(child.LocalName, name, System.StringComparison.Ordinal))
|
||||
{
|
||||
return child.InnerText;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using PSInfisicalAPI.Errors;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace PSInfisicalAPI.Imports
|
||||
{
|
||||
public sealed class YamlInfisicalImporter : IInfisicalImporter
|
||||
{
|
||||
public IList<KeyValuePair<string, string>> Import(FileInfo path)
|
||||
{
|
||||
if (path == null) { throw new InfisicalImportException("Path is required for YAML import."); }
|
||||
|
||||
List<KeyValuePair<string, string>> result = new List<KeyValuePair<string, string>>();
|
||||
string text = File.ReadAllText(path.FullName);
|
||||
|
||||
IDeserializer deserializer = new DeserializerBuilder().Build();
|
||||
object root = deserializer.Deserialize<object>(text);
|
||||
if (root == null) { return result; }
|
||||
|
||||
IDictionary rootMap = root as IDictionary;
|
||||
if (rootMap != null && rootMap.Contains("Secrets"))
|
||||
{
|
||||
IList entries = rootMap["Secrets"] as IList;
|
||||
if (entries != null)
|
||||
{
|
||||
foreach (object entry in entries)
|
||||
{
|
||||
IDictionary map = entry as IDictionary;
|
||||
if (map == null) { continue; }
|
||||
string key = AsString(map["SecretName"]);
|
||||
string value = AsString(map["SecretValue"]);
|
||||
if (string.IsNullOrEmpty(key)) { continue; }
|
||||
result.Add(new KeyValuePair<string, string>(key, value ?? string.Empty));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (rootMap != null)
|
||||
{
|
||||
foreach (DictionaryEntry kvp in rootMap)
|
||||
{
|
||||
string key = AsString(kvp.Key);
|
||||
if (string.IsNullOrEmpty(key)) { continue; }
|
||||
result.Add(new KeyValuePair<string, string>(key, AsString(kvp.Value) ?? string.Empty));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new InfisicalImportException("YAML import expects a 'Secrets' root list or a flat key/value mapping.");
|
||||
}
|
||||
|
||||
private static string AsString(object value)
|
||||
{
|
||||
if (value == null) { return null; }
|
||||
return value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,10 +36,17 @@ namespace PSInfisicalAPI.Logging
|
||||
_cmdlet.WriteWarning(line);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Error-level lines are diagnostic breadcrumbs: every call site in this module logs one and then throws,
|
||||
/// so the failure itself always reaches the caller as an ErrorRecord carrying the same detail. Emitting
|
||||
/// them on the warning stream duplicated that failure eight lines deep and put it under -WarningAction
|
||||
/// instead of -ErrorAction. They belong on the verbose stream, where -Verbose opts into the trail and the
|
||||
/// ErrorRecord remains the single authority on what failed.
|
||||
/// </summary>
|
||||
public void Error(string component, string message)
|
||||
{
|
||||
string line = InfisicalLogFormatter.FormatNow(InfisicalLogLevel.Error, component, message);
|
||||
_cmdlet.WriteWarning(line);
|
||||
_cmdlet.WriteVerbose(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,12 @@ namespace PSInfisicalAPI.Models
|
||||
public string Source { get; set; }
|
||||
public string EnrollmentType { 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 string RenewalError { get; set; }
|
||||
public int? RenewBeforeDays { get; set; }
|
||||
|
||||
@@ -17,6 +17,11 @@ namespace PSInfisicalAPI.Models
|
||||
public string StatusMessage { 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()
|
||||
{
|
||||
if (Leaf != null) { return Leaf.Subject; }
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace PSInfisicalAPI.Models
|
||||
{
|
||||
public enum InfisicalImportFormat
|
||||
{
|
||||
Json,
|
||||
Yaml,
|
||||
Env,
|
||||
Xml
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user