Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b6823344f | |||
| f62b3e90b1 | |||
| 4c7ce00504 | |||
| 14c8c4f384 | |||
| 5e5145fdc7 | |||
| 6318d06362 | |||
| 98f5d7704e | |||
| 94bd15a8f8 | |||
| daf1cdce65 |
@@ -0,0 +1,328 @@
|
||||
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_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"
|
||||
$changelogText = if ($changelogSection) { $changelogSection } else { '_No CHANGELOG section found for this version._' }
|
||||
$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('')
|
||||
[void]$sb.AppendLine('## Changes')
|
||||
[void]$sb.AppendLine($changelogText)
|
||||
[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'
|
||||
Write-Host "Publishing module from: $moduleDir"
|
||||
Publish-PSResource `
|
||||
-Path $moduleDir `
|
||||
-Repository PSGallery `
|
||||
-ApiKey $env:PSGALLERY_API_KEY `
|
||||
-Verbose
|
||||
+82
-11
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.07.1435'
|
||||
ModuleVersion = '2026.07.30.2151'
|
||||
GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51'
|
||||
Author = 'Grace Solutions'
|
||||
CompanyName = 'Grace Solutions'
|
||||
@@ -74,7 +74,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 = '97193d46f2ff'
|
||||
CommitHash = '14c8c4f3845b'
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -1066,7 +1066,7 @@ $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>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 require a subscriber or certificate profile instead.</maml:para>
|
||||
</maml:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
@@ -1281,6 +1281,8 @@ $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>-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. 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 -PkiSubscriberSlug or -CertificateProfileId, or enable direct issuance on the CA.</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:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
|
||||
@@ -1066,7 +1066,7 @@ $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>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 require a subscriber or certificate profile instead.</maml:para>
|
||||
</maml:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
@@ -1281,6 +1281,8 @@ $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>-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. 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 -PkiSubscriberSlug or -CertificateProfileId, or enable direct issuance on the CA.</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:alert>
|
||||
</maml:alertSet>
|
||||
<command:examples>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -146,7 +146,7 @@ Disconnect-Infisical
|
||||
|
||||
## End-to-end: request and install a chained certificate
|
||||
|
||||
Connects, selects a project by name, sources SANs from `Get-InfisicalSANList`, picks the first available internal CA, requests a certificate, installs it (and its chain) into the current-user store, and disconnects. Each call uses a splatted `OrderedDictionary` constructed with `OrdinalIgnoreCase` so parameter names round-trip case-insensitively.
|
||||
Connects, selects a `cert-manager` project, sources SANs from `Get-InfisicalSANList`, requests a certificate through a PKI subscriber, 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.
|
||||
|
||||
```powershell
|
||||
$ConnectInfisicalParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
@@ -159,25 +159,159 @@ $ConnectInfisicalParameters = New-Object -TypeName 'System.Collections.Specializ
|
||||
|
||||
$Connection = Connect-Infisical @ConnectInfisicalParameters
|
||||
|
||||
$Project = Get-InfisicalProject | Where-Object {($_.Name -eq 'Platform')} | Select-Object -First 1
|
||||
$Ca = Get-InfisicalCertificateAuthority -ProjectId ($Project.Id) | Select-Object -First 1
|
||||
$SanList = Get-InfisicalSANList
|
||||
$Project = Get-InfisicalProject -Type cert-manager | Where-Object {($_.Name -eq 'Platform')} | Select-Object -First 1
|
||||
$Subscriber = Get-InfisicalPkiSubscriber -ProjectId ($Project.Id) | Select-Object -First 1
|
||||
$SanList = Get-InfisicalSANList
|
||||
|
||||
$RequestInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
$RequestInfisicalCertificateParameters.ProjectId = $Project.Id
|
||||
$RequestInfisicalCertificateParameters.CertificateAuthorityId = $Ca.Id
|
||||
$RequestInfisicalCertificateParameters.CommonName = "CN=$($Env:ComputerName.ToUpper())"
|
||||
$RequestInfisicalCertificateParameters.DnsName = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
|
||||
$RequestInfisicalCertificateParameters.ProjectId = $Project.Id
|
||||
$RequestInfisicalCertificateParameters.PkiSubscriberSlug = $Subscriber.Name
|
||||
$RequestInfisicalCertificateParameters.CommonName = $Env:ComputerName.ToUpper()
|
||||
$RequestInfisicalCertificateParameters.DnsName = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
|
||||
$RequestInfisicalCertificateParameters.DnsName.AddRange($SanList)
|
||||
$RequestInfisicalCertificateParameters.DnsName.Add('myrecord.mydomain.com')
|
||||
$RequestInfisicalCertificateParameters.Ttl = '90d'
|
||||
$RequestInfisicalCertificateParameters.Install = $True
|
||||
$RequestInfisicalCertificateParameters.InstallChain = $True
|
||||
$Certificate = Request-InfisicalCertificate @RequestInfisicalCertificateParameters
|
||||
$RequestInfisicalCertificateParameters.Install = $True
|
||||
$RequestInfisicalCertificateParameters.InstallChain = $True
|
||||
$RequestInfisicalCertificateParameters.Verbose = $True
|
||||
|
||||
$Certificate = Request-InfisicalCertificate @RequestInfisicalCertificateParameters
|
||||
|
||||
$Null = Disconnect-Infisical -Verbose
|
||||
```
|
||||
|
||||
### Choosing an issuance path
|
||||
|
||||
`Request-InfisicalCertificate` has three mutually exclusive issuance parameter sets. Which one works depends on how the project is configured in Infisical:
|
||||
|
||||
| Parameter | Use when |
|
||||
| -------------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| `-PkiSubscriberSlug` | The project defines PKI subscribers (`Get-InfisicalPkiSubscriber`). Preferred for most setups. |
|
||||
| `-CertificateProfileId` | The project issues through certificate profiles (`Get-InfisicalCertificateProfile`). |
|
||||
| `-CertificateAuthorityId` | Signing straight against a CA. Requires **direct issuance** to be enabled on that CA. |
|
||||
|
||||
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 subscriber, a profile, or enabling 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 PKI subscriber 'web-tier' 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
|
||||
# "PKI subscriber 'web-tier' for CN=WEB01".
|
||||
```
|
||||
|
||||
#### Discovering subscribers
|
||||
|
||||
A subscriber is a named enrollment identity that pins the CA, TTL, key usages, and SAN policy, so the request carries only a CSR. List what a project offers:
|
||||
|
||||
```powershell
|
||||
Get-InfisicalPkiSubscriber -ProjectId ($Project.Id) |
|
||||
Format-Table Name, CommonName, Status, Ttl, CaId
|
||||
```
|
||||
|
||||
Pass the subscriber's `Name` to `-PkiSubscriberSlug`. 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.
|
||||
|
||||
If the project has no subscribers, either create one (**Certificate Management > Subscribers > Add Subscriber**) or use one of the other two paths.
|
||||
|
||||
#### Enabling direct issuance on a CA
|
||||
|
||||
Direct issuance lets a CA sign a bare CSR with no subscriber or template in front of it. It is a per-CA setting, and Infisical rejects the request with `400 Certificate template or subscriber is required for issuance` when it is off. This module now catches that before building a CSR:
|
||||
|
||||
```text
|
||||
Request-InfisicalCertificate : Certificate authority 'intermediate-ca' (bf661d78-...) 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> ... or -CertificateProfileId <id> ...
|
||||
```
|
||||
|
||||
To turn it on, in the Infisical UI open the `cert-manager` project, go to **Certificate Authorities**, select the CA, and enable **Direct Issuance** in its settings. The setting is surfaced by this module as `EnableDirectIssuance`, so you can confirm it and pick an eligible CA in one step:
|
||||
|
||||
```powershell
|
||||
Get-InfisicalCertificateAuthority -ProjectId ($Project.Id) -Kind Internal |
|
||||
Format-Table Name, CommonName, Status, EnableDirectIssuance
|
||||
|
||||
$Ca = Get-InfisicalCertificateAuthority -ProjectId ($Project.Id) -Kind Internal |
|
||||
Where-Object {($_.EnableDirectIssuance -eq $True)} |
|
||||
Select-Object -First 1
|
||||
|
||||
$RequestInfisicalCertificateParameters.CertificateAuthorityId = $Ca.Id
|
||||
$RequestInfisicalCertificateParameters.Ttl = '90d' # required by the CA path
|
||||
```
|
||||
|
||||
Prefer a subscriber or profile for routine enrollment: direct issuance bypasses the naming and key-usage constraints those layers enforce. Reserve it for bootstrap or break-glass cases.
|
||||
|
||||
### 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 -ProjectId $ProjectId -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.
|
||||
|
||||
@@ -1222,7 +1222,8 @@ Export-InfisicalSecrets `
|
||||
[-Scope <Process|User|Machine>] `
|
||||
[-Force] `
|
||||
[-Encoding <UTF8|UTF8Bom|Unicode>] `
|
||||
[-Prefix <string>]
|
||||
[-SecretsPrefix <string>] `
|
||||
[-ForceSecretsPrefix]
|
||||
```
|
||||
|
||||
## Parameter Rules
|
||||
@@ -1521,8 +1522,9 @@ Start-InfisicalProcess
|
||||
[-SecureArgumentList]
|
||||
[-LogOutput]
|
||||
[-ContinueOnError]
|
||||
[-Secret <InfisicalSecret[]>]
|
||||
[-Prefix <string>]
|
||||
[-Secrets <InfisicalSecret[]>]
|
||||
[-SecretsPrefix <string>]
|
||||
[-ForceSecretsPrefix]
|
||||
```
|
||||
|
||||
Behavior:
|
||||
@@ -1530,7 +1532,7 @@ Behavior:
|
||||
```text
|
||||
Buffer pipeline InfisicalSecret objects in ProcessRecord.
|
||||
Decrypt secrets only into ProcessStartInfo.Environment.
|
||||
Apply -Prefix to each secret name before injection.
|
||||
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').
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,10 +23,12 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
public SwitchParameter AsPlainText { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string Prefix { get; set; }
|
||||
[Alias("Prefix")]
|
||||
public string SecretsPrefix { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public SwitchParameter ForcePrefix { get; set; }
|
||||
[Alias("ForcePrefix")]
|
||||
public SwitchParameter ForceSecretsPrefix { get; set; }
|
||||
|
||||
private readonly List<InfisicalSecret> _buffer = new List<InfisicalSecret>();
|
||||
|
||||
@@ -47,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +76,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
|
||||
foreach (InfisicalSecret secret in _buffer)
|
||||
{
|
||||
string key = InfisicalPrefix.Apply(secret.SecretName ?? string.Empty, Prefix, ForcePrefix.IsPresent);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,10 +39,12 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
public InfisicalExportEncoding Encoding { get; set; } = InfisicalExportEncoding.UTF8;
|
||||
|
||||
[Parameter]
|
||||
public string Prefix { get; set; }
|
||||
[Alias("Prefix")]
|
||||
public string SecretsPrefix { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public SwitchParameter ForcePrefix { get; set; }
|
||||
[Alias("ForcePrefix")]
|
||||
public SwitchParameter ForceSecretsPrefix { get; set; }
|
||||
|
||||
private readonly List<InfisicalSecret> _buffer = new List<InfisicalSecret>();
|
||||
|
||||
@@ -73,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 = ApplyPrefix(_buffer, Prefix, ForcePrefix.IsPresent),
|
||||
Secrets = ApplySecretsPrefix(_buffer, SecretsPrefix, ForceSecretsPrefix.IsPresent),
|
||||
Format = Format,
|
||||
Path = Path,
|
||||
Scope = Scope,
|
||||
@@ -88,11 +92,11 @@ 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[] ApplyPrefix(List<InfisicalSecret> source, string prefix, bool force)
|
||||
private static InfisicalSecret[] ApplySecretsPrefix(List<InfisicalSecret> source, string prefix, bool force)
|
||||
{
|
||||
if (string.IsNullOrEmpty(prefix)) { return source.ToArray(); }
|
||||
|
||||
|
||||
@@ -46,6 +46,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 +54,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("GetInfisicalCertificateApplicationCmdlet", "GetCertificateApplication", exception);
|
||||
WriteErrorForException("GetInfisicalCertificateApplicationCmdlet", "GetCertificateApplication", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("GetInfisicalCertificateApplicationEnrollmentCmdlet", "GetCertificateApplicationEnrollment", exception);
|
||||
WriteErrorForException("GetInfisicalCertificateApplicationEnrollmentCmdlet", "GetCertificateApplicationEnrollment", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,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 +60,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("GetInfisicalCertificateAuthorityCmdlet", "GetCertificateAuthority", exception);
|
||||
WriteErrorForException("GetInfisicalCertificateAuthorityCmdlet", "GetCertificateAuthority", exception);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -129,10 +129,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,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 +47,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("GetInfisicalCertificatePolicyCmdlet", "GetCertificatePolicy", exception);
|
||||
WriteErrorForException("GetInfisicalCertificatePolicyCmdlet", "GetCertificatePolicy", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
|
||||
bool? includeConfigs = MyInvocation.BoundParameters.ContainsKey("IncludeConfigs") ? (bool?)IncludeConfigs.IsPresent : null;
|
||||
InfisicalCertificateProfile[] all = client.ListCertificateProfiles(connection, ProjectId, Limit, Offset, includeConfigs);
|
||||
Logger.Information("Get-InfisicalCertificateProfile", string.Concat("Returned ", all.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " certificate profile(s)."));
|
||||
foreach (InfisicalCertificateProfile profile in all)
|
||||
{
|
||||
WriteObject(profile);
|
||||
@@ -49,7 +50,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
{
|
||||
[Cmdlet(VerbsCommon.Get, "InfisicalEnvironmentVariable")]
|
||||
[OutputType(typeof(string))]
|
||||
public sealed class GetInfisicalEnvironmentVariableCmdlet : PSCmdlet
|
||||
public sealed class GetInfisicalEnvironmentVariableCmdlet : InfisicalCmdletBase
|
||||
{
|
||||
private const string Component = "Get-InfisicalEnvironmentVariable";
|
||||
|
||||
private static readonly EnvironmentVariableTarget[] TargetOrder = new[]
|
||||
{
|
||||
EnvironmentVariableTarget.Process,
|
||||
@@ -18,26 +20,38 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
[ValidateNotNullOrEmpty]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Parameter(Position = 1)]
|
||||
public EnvironmentVariableTarget? Scope { get; set; }
|
||||
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
foreach (EnvironmentVariableTarget target in TargetOrder)
|
||||
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
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -40,7 +41,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("GetInfisicalOrganizationCmdlet", "GetOrganization", exception);
|
||||
WriteErrorForException("GetInfisicalOrganizationCmdlet", "GetOrganization", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,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 +43,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException(Component, "GetSANList", exception);
|
||||
WriteErrorForException(Component, "GetSANList", exception);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
|
||||
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);
|
||||
@@ -52,7 +53,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("GetInfisicalSubOrganizationCmdlet", "GetSubOrganization", 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,10 +30,12 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
public SwitchParameter AsPlainText { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string Prefix { get; set; }
|
||||
[Alias("Prefix")]
|
||||
public string SecretsPrefix { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public SwitchParameter ForcePrefix { get; set; }
|
||||
[Alias("ForcePrefix")]
|
||||
public SwitchParameter ForceSecretsPrefix { get; set; }
|
||||
|
||||
protected override void EndProcessing()
|
||||
{
|
||||
@@ -47,21 +49,24 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
|
||||
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)
|
||||
{
|
||||
ThrowTerminatingForException("ImportInfisicalSecretCmdlet", "ImportSecret", exception);
|
||||
WriteErrorForException("ImportInfisicalSecretCmdlet", "ImportSecret", exception);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +79,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
foreach (KeyValuePair<string, string> pair in pairs)
|
||||
{
|
||||
if (pair.Key == null) { continue; }
|
||||
string key = InfisicalPrefix.Apply(pair.Key, Prefix, ForcePrefix.IsPresent);
|
||||
string key = InfisicalPrefix.Apply(pair.Key, SecretsPrefix, ForceSecretsPrefix.IsPresent);
|
||||
|
||||
if (dictionary.ContainsKey(key))
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Management.Automation;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using PSInfisicalAPI.Connections;
|
||||
using PSInfisicalAPI.Errors;
|
||||
using PSInfisicalAPI.Http;
|
||||
@@ -44,12 +45,54 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
return current != null && current.SkipCertificateCheck;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
|
||||
@@ -56,7 +56,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("InstallInfisicalCertificateCmdlet", "InstallCertificate", exception);
|
||||
WriteErrorForException("InstallInfisicalCertificateCmdlet", "InstallCertificate", exception);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("NewInfisicalOrganizationCmdlet", "CreateOrganization", 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("NewInfisicalSubOrganizationCmdlet", "CreateSubOrganization", exception);
|
||||
WriteErrorForException("NewInfisicalSubOrganizationCmdlet", "CreateSubOrganization", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("NewInfisicalTagCmdlet", "CreateTag", exception);
|
||||
WriteErrorForException("NewInfisicalTagCmdlet", "CreateTag", 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("RemoveInfisicalOrganizationCmdlet", "DeleteOrganization", 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("RemoveInfisicalSubOrganizationCmdlet", "DeleteSubOrganization", 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;
|
||||
|
||||
@@ -79,7 +80,8 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
|
||||
|
||||
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."); }
|
||||
|
||||
@@ -104,6 +106,7 @@ 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));
|
||||
}
|
||||
}
|
||||
@@ -112,12 +115,14 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
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))
|
||||
@@ -160,24 +165,123 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException(Component, "RequestCertificate", exception);
|
||||
WriteErrorForException(Component, "RequestCertificate", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private List<string> BuildDnsNames(InfisicalCsrSubject subject)
|
||||
/// <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 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);
|
||||
}
|
||||
|
||||
private X509Certificate2 TryFindExisting(InfisicalPkiClient client, InfisicalConnection connection, string projectId, string commonName)
|
||||
{
|
||||
List<string> candidateSerials = new List<string>();
|
||||
@@ -192,6 +296,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception searchException)
|
||||
{
|
||||
if (IsPipelineControlException(searchException)) { throw; }
|
||||
Logger.Verbose(Component, string.Concat("Infisical search for idempotency check failed: ", searchException.Message));
|
||||
}
|
||||
|
||||
@@ -208,7 +313,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
return InfisicalCertificateRequestHelpers.ResolveKeyStorageFlags(PrivateKeyProtection, PersistKey.IsPresent, MachineKey.IsPresent);
|
||||
}
|
||||
|
||||
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 +322,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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,21 +89,23 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
public SwitchParameter ContinueOnError { get; set; }
|
||||
|
||||
[Parameter(ValueFromPipeline = true)]
|
||||
[Alias("Secrets", "InputObject")]
|
||||
public InfisicalSecret[] Secret { get; set; }
|
||||
[Alias("Secret", "InputObject")]
|
||||
public InfisicalSecret[] Secrets { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string Prefix { get; set; }
|
||||
[Alias("Prefix")]
|
||||
public string SecretsPrefix { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public SwitchParameter ForcePrefix { get; set; }
|
||||
[Alias("ForcePrefix")]
|
||||
public SwitchParameter ForceSecretsPrefix { get; set; }
|
||||
|
||||
private readonly List<InfisicalSecret> _secretBuffer = new List<InfisicalSecret>();
|
||||
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
if (Secret == null) { return; }
|
||||
foreach (InfisicalSecret secret in Secret)
|
||||
if (Secrets == null) { return; }
|
||||
foreach (InfisicalSecret secret in Secrets)
|
||||
{
|
||||
if (secret != null) { _secretBuffer.Add(secret); }
|
||||
}
|
||||
@@ -119,6 +121,9 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
|
||||
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,
|
||||
@@ -138,8 +143,8 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
LogOutput = LogOutput.IsPresent,
|
||||
ContinueOnError = ContinueOnError.IsPresent,
|
||||
Secrets = _secretBuffer.ToArray(),
|
||||
Prefix = Prefix,
|
||||
ForcePrefix = ForcePrefix.IsPresent
|
||||
SecretsPrefix = SecretsPrefix,
|
||||
ForceSecretsPrefix = ForceSecretsPrefix.IsPresent
|
||||
};
|
||||
|
||||
InfisicalProcessResult result = InfisicalProcessRunner.Run(options, Logger);
|
||||
@@ -150,13 +155,13 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
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);
|
||||
ThrowTerminatingError(error);
|
||||
WriteError(error);
|
||||
}
|
||||
}
|
||||
catch (PipelineStoppedException) { throw; }
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException(Component, "StartProcess", 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("UpdateInfisicalOrganizationCmdlet", "UpdateOrganization", 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("UpdateInfisicalSubOrganizationCmdlet", "UpdateSubOrganization", exception);
|
||||
WriteErrorForException("UpdateInfisicalSubOrganizationCmdlet", "UpdateSubOrganization", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException("UpdateInfisicalTagCmdlet", "UpdateTag", exception);
|
||||
WriteErrorForException("UpdateInfisicalTagCmdlet", "UpdateTag", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace PSInfisicalAPI.Cmdlets
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ThrowTerminatingForException(Component, "WriteScepMdmProfileToWmi", exception);
|
||||
WriteErrorForException(Component, "WriteScepMdmProfileToWmi", exception);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,9 +32,29 @@ namespace PSInfisicalAPI.Pki
|
||||
if (!string.IsNullOrEmpty(organizationalUnit)) { result.OrganizationalUnit = organizationalUnit; }
|
||||
if (!string.IsNullOrEmpty(emailAddress)) { result.EmailAddress = emailAddress; }
|
||||
|
||||
result.CommonName = NormalizeCommonName(result.CommonName);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reduces a caller-supplied common name to the bare CN value. Callers commonly pass the RDN form
|
||||
/// ("CN=HOST") or a full DN ("CN=HOST,OU=IT"); using either verbatim produces a doubled "CN=CN=HOST"
|
||||
/// subject and a bogus "CN=HOST" DNS SAN, since the CSR builder adds the CN= prefix itself.
|
||||
/// </summary>
|
||||
public static string NormalizeCommonName(string commonName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(commonName)) { return commonName; }
|
||||
|
||||
string value = commonName.Trim();
|
||||
if (!value.StartsWith("CN=", StringComparison.OrdinalIgnoreCase)) { return value; }
|
||||
|
||||
value = value.Substring(3);
|
||||
int separator = value.IndexOf(',');
|
||||
if (separator >= 0) { value = value.Substring(0, separator); }
|
||||
|
||||
return value.Trim();
|
||||
}
|
||||
|
||||
public static string ResolveLocalFqdn()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace PSInfisicalAPI.Process
|
||||
public bool LogOutput { get; set; }
|
||||
public bool ContinueOnError { get; set; }
|
||||
public InfisicalSecret[] Secrets { get; set; }
|
||||
public string Prefix { get; set; }
|
||||
public bool ForcePrefix { get; set; }
|
||||
public string SecretsPrefix { get; set; }
|
||||
public bool ForceSecretsPrefix { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace PSInfisicalAPI.Process
|
||||
|
||||
if (options.EnvironmentVariables != null && options.EnvironmentVariables.Count > 0)
|
||||
{
|
||||
Log(logger, string.Concat("Injecting ", options.EnvironmentVariables.Count, " explicit environment variable(s) into the process."));
|
||||
LogInformation(logger, string.Concat("Injecting ", options.EnvironmentVariables.Count, " explicit environment variable(s) into the process."));
|
||||
foreach (DictionaryEntry entry in options.EnvironmentVariables)
|
||||
{
|
||||
if (entry.Key == null) { continue; }
|
||||
@@ -36,11 +36,11 @@ namespace PSInfisicalAPI.Process
|
||||
|
||||
if (options.Secrets == null || options.Secrets.Length == 0) { return; }
|
||||
|
||||
Log(logger, string.Concat("Injecting ", options.Secrets.Length, " Infisical secret(s) into the process environment."));
|
||||
LogInformation(logger, string.Concat("Injecting ", options.Secrets.Length, " Infisical secret(s) into the process environment."));
|
||||
foreach (InfisicalSecret secret in options.Secrets)
|
||||
{
|
||||
if (secret == null || string.IsNullOrEmpty(secret.SecretName) || secret.SecretValue == null) { continue; }
|
||||
string name = InfisicalPrefix.Apply(secret.SecretName, options.Prefix, options.ForcePrefix);
|
||||
string name = InfisicalPrefix.Apply(secret.SecretName, options.SecretsPrefix, options.ForceSecretsPrefix);
|
||||
SecureStringUtility.UsePlainText(secret.SecretValue, plain =>
|
||||
{
|
||||
processEnv[name] = plain;
|
||||
@@ -193,5 +193,10 @@ namespace PSInfisicalAPI.Process
|
||||
{
|
||||
if (logger != null) { logger.Verbose(Component, message); }
|
||||
}
|
||||
|
||||
private static void LogInformation(IInfisicalLogger logger, string message)
|
||||
{
|
||||
if (logger != null) { logger.Information(Component, message); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,8 +75,8 @@ namespace PSInfisicalAPI.Secrets
|
||||
InfisicalSecretListResponseDto dto = _serializer.Deserialize<InfisicalSecretListResponseDto>(response.Body);
|
||||
response.Clear();
|
||||
|
||||
InfisicalSecret[] mapped = InfisicalSecretMapper.MapMany(dto != null ? dto.Secrets : null);
|
||||
_logger.Information(Component, "Infisical secrets retrieval was successful.");
|
||||
InfisicalSecret[] mapped = MergeListAndImports(dto);
|
||||
_logger.Information(Component, string.Concat("Infisical secrets retrieval was successful. Returned ", mapped.Length.ToString(CultureInfo.InvariantCulture), " secret(s)."));
|
||||
return mapped;
|
||||
}
|
||||
catch (Exception)
|
||||
@@ -465,6 +465,66 @@ namespace PSInfisicalAPI.Secrets
|
||||
}
|
||||
}
|
||||
|
||||
private InfisicalSecret[] MergeListAndImports(InfisicalSecretListResponseDto dto)
|
||||
{
|
||||
if (dto == null) { return Array.Empty<InfisicalSecret>(); }
|
||||
|
||||
InfisicalSecret[] local = InfisicalSecretMapper.MapMany(dto.Secrets);
|
||||
|
||||
if (dto.Imports == null || dto.Imports.Count == 0)
|
||||
{
|
||||
return local;
|
||||
}
|
||||
|
||||
Dictionary<string, InfisicalSecret> merged = new Dictionary<string, InfisicalSecret>(StringComparer.Ordinal);
|
||||
int importsTotal = 0;
|
||||
|
||||
foreach (InfisicalSecretImportDto import in dto.Imports)
|
||||
{
|
||||
if (import == null) { continue; }
|
||||
InfisicalSecret[] importedSecrets = InfisicalSecretMapper.MapMany(import.Secrets);
|
||||
importsTotal += importedSecrets.Length;
|
||||
|
||||
_logger.Information(Component, string.Concat(
|
||||
"Including ",
|
||||
importedSecrets.Length.ToString(CultureInfo.InvariantCulture),
|
||||
" secret(s) from import '",
|
||||
import.SecretPath ?? string.Empty,
|
||||
"' (environment='",
|
||||
import.Environment ?? string.Empty,
|
||||
"')."));
|
||||
|
||||
foreach (InfisicalSecret secret in importedSecrets)
|
||||
{
|
||||
if (secret == null || string.IsNullOrEmpty(secret.SecretName)) { continue; }
|
||||
merged[secret.SecretName] = secret;
|
||||
}
|
||||
}
|
||||
|
||||
int overrides = 0;
|
||||
foreach (InfisicalSecret secret in local)
|
||||
{
|
||||
if (secret == null || string.IsNullOrEmpty(secret.SecretName)) { continue; }
|
||||
if (merged.ContainsKey(secret.SecretName)) { overrides++; }
|
||||
merged[secret.SecretName] = secret;
|
||||
}
|
||||
|
||||
_logger.Information(Component, string.Concat(
|
||||
"Merged secrets: local=",
|
||||
local.Length.ToString(CultureInfo.InvariantCulture),
|
||||
", imports=",
|
||||
importsTotal.ToString(CultureInfo.InvariantCulture),
|
||||
", local-overrode-import=",
|
||||
overrides.ToString(CultureInfo.InvariantCulture),
|
||||
", final=",
|
||||
merged.Count.ToString(CultureInfo.InvariantCulture),
|
||||
"."));
|
||||
|
||||
InfisicalSecret[] result = new InfisicalSecret[merged.Count];
|
||||
merged.Values.CopyTo(result, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
private InfisicalHttpResponse SendWithVersionFallback(
|
||||
InfisicalConnection connection,
|
||||
string endpointName,
|
||||
|
||||
Reference in New Issue
Block a user