21 Commits

Author SHA1 Message Date
gsadmin 8b6823344f Merge pull request 'Honor -ErrorAction, fix pipeline-stop noise, and correct certificate request paths' (#18) from dev into main
Reviewed-on: #18
2026-07-30 21:56:28 +00:00
gsadmin f62b3e90b1 Honor -ErrorAction, fix pipeline-stop noise, and correct certificate request paths
Publish to PowerShell Gallery / build (pull_request) Successful in 43s
Publish to PowerShell Gallery / release (pull_request) Successful in 18s
Publish to PowerShell Gallery / publish (pull_request) Successful in 14s
BREAKING: operation failures are now non-terminating errors, so -ErrorAction
(and $ErrorActionPreference) decides the outcome. try/catch around these cmdlets
now requires -ErrorAction Stop or $ErrorActionPreference = 'Stop'. A failing
pipeline item no longer aborts the batch.

Cmdlets no longer report "The pipeline has been stopped." as an error.
Select-Object -First, and Where-Object feeding it, stop the upstream cmdlet by
design; the shared error path in InfisicalCmdletBase now lets pipeline-control
exceptions propagate untouched instead of logging them and raising an error.

Error-level diagnostics moved off the warning stream to verbose. Every
Logger.Error call site logs and then throws, so the failure already reaches the
caller as an ErrorRecord; emitting it again as eight warning lines put failures
under -WarningAction instead of -ErrorAction. One error per failure now.

Request-InfisicalCertificate:
- -CommonName accepts the RDN form (CN=WEB01) and reduces it to the bare value,
  which previously produced a CN=CN=WEB01 subject plus a bogus DNS SAN.
- -DnsName routes IP literals to iPAddress SAN entries, so the mixed output of
  Get-InfisicalSANList can be splatted in as documented.
- The CA path sends the normalized common name to the signing endpoint.
- The issuance path is resolved and reported before a keypair is generated, and
  a CA with direct issuance disabled fails fast with guidance naming
  -PkiSubscriberSlug and -CertificateProfileId. Infisical exposes no
  template-based issuance route, so no -CertificateTemplateId is added.

Get-InfisicalCertificateAuthority table output gains a DirectIssue column
(EnableDirectIssuance) so CAs eligible for -CertificateAuthorityId are visible.

README, about_PSInfisicalAPI, and cmdlet help document the stream/-ErrorAction
contract, subscriber discovery, and direct-issuance setup. Adds regression tests
for pipeline-stop propagation, logger stream routing, SAN splitting, common-name
normalization, and the non-terminating convention across all cmdlets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 17:52:32 -04:00
gsadmin 4c7ce00504 Merge pull request 'Fix import merging and add count/scope logging across cmdlets' (#17) from dev into main
Reviewed-on: #17
2026-06-16 02:19:28 +00:00
GraceSolutions 14c8c4f384 Fix import merging and add count/scope logging across cmdlets
Publish to PowerShell Gallery / build (pull_request) Successful in 24s
Publish to PowerShell Gallery / release (pull_request) Successful in 15s
Publish to PowerShell Gallery / publish (pull_request) Successful in 8s
InfisicalSecretsClient.List now merges dto.Imports[].Secrets with dto.Secrets using local-wins precedence, restoring imported secrets that were previously dropped when -IncludeImports was set.

Get-InfisicalSecret, ConvertTo-InfisicalSecretDictionary, Export-InfisicalSecrets, Import-InfisicalSecret, New/Update/Remove-InfisicalSecret, and Start-InfisicalProcess now emit Information-level counts for retrieved/processed/injected items.

All collection-returning Get-* cmdlets (Folder, Project, Organization, Environment, Tag, SubOrganization, Certificate, CertificateAuthority, CertificateApplication, CertificatePolicy, CertificateProfile, PkiSubscriber) now log returned counts.

Get-InfisicalEnvironmentVariable gains an optional -Scope (EnvironmentVariableTarget) parameter plus per-scope verbose tracing and Information-level found/not-found outcome lines.
2026-06-15 22:18:02 -04:00
gsadmin 5e5145fdc7 Merge pull request 'Add GitHub Actions workflow for PowerShell Gallery publish' (#16) from dev into main
Reviewed-on: #16
2026-06-10 20:57:05 +00:00
GraceSolutions 6318d06362 Add GitHub Actions workflow for PowerShell Gallery publish
Publish to PowerShell Gallery / release (pull_request) Has been cancelled
Publish to PowerShell Gallery / publish (pull_request) Has been cancelled
Publish to PowerShell Gallery / build (pull_request) Has been cancelled
Mirrors the Gitea workflow with GitHub-specific adaptations: ubuntu-latest runner, actions/upload-artifact and actions/download-artifact v4, Bearer auth with X-GitHub-Api-Version header, /pull/ URL path, upload_url URI template handling on uploads.github.com, contents:write permission on the release job, and on-demand Install-Module of Microsoft.PowerShell.PSResourceGet for CurrentUser.
2026-06-10 16:54:22 -04:00
gsadmin 98f5d7704e Merge pull request 'Rename spec' (#15) from dev into main
Reviewed-on: #15
2026-06-10 20:43:05 +00:00
GraceSolutions 94bd15a8f8 Rename prefix parameters to SecretsPrefix/ForceSecretsPrefix and -Secret to -Secrets
Publish to PowerShell Gallery / build (pull_request) Successful in 23s
Publish to PowerShell Gallery / release (pull_request) Successful in 11s
Publish to PowerShell Gallery / publish (pull_request) Successful in 8s
Renames -Prefix to -SecretsPrefix and -ForcePrefix to -ForceSecretsPrefix across ConvertTo-InfisicalSecretDictionary, Import-InfisicalSecret, Export-InfisicalSecrets, and Start-InfisicalProcess. Start-InfisicalProcess also renames the pipeline parameter -Secret to -Secrets. The previous names remain available as parameter aliases (Prefix, ForcePrefix, Secret) for backward compatibility. Internal InfisicalProcessOptions properties renamed to match.
2026-06-10 16:20:13 -04:00
GraceSolutions daf1cdce65 Rename spec 2026-06-08 17:59:52 -04:00
gsadmin 80871b73af Merge pull request 'feat: add Import-InfisicalSecret + Get-InfisicalEnvironmentVariable + -Prefix on ConvertTo-InfisicalSecretDictionary' (#14) from dev into main
Reviewed-on: #14
2026-06-07 14:38:32 +00:00
GraceSolutions 9a2f81fc02 Build artifacts for 97193d46f2
Publish to PowerShell Gallery / build (pull_request) Successful in 30s
Publish to PowerShell Gallery / release (pull_request) Successful in 16s
Publish to PowerShell Gallery / publish (pull_request) Successful in 7s
Auto-generated by build.ps1 -CommitArtifacts. Build 2026.06.07.1435. Module DLL and manifest embed BuildCommitHash=97193d46f2ff, matching the source commit they were produced from.
2026-06-07 10:35:49 -04:00
GraceSolutions 97193d46f2 feat: gate -Prefix to skip already-prefixed names; add -ForcePrefix override on ConvertTo/Import/Export/Start cmdlets 2026-06-07 10:35:18 -04:00
GraceSolutions b5575222eb feat: add Import-InfisicalSecret + Get-InfisicalEnvironmentVariable + -Prefix on ConvertTo-InfisicalSecretDictionary 2026-06-07 10:16:20 -04:00
gsadmin f82312433c Merge pull request 'feat: add Organization/Sub-Organization CRUD cmdlets and Get-InfisicalSANList' (#13) from dev into main
Reviewed-on: #13
2026-06-07 00:21:09 +00:00
GraceSolutions 1aa51b8cbf Build artifacts for 77cb03ec98
Publish to PowerShell Gallery / build (pull_request) Successful in 46s
Publish to PowerShell Gallery / release (pull_request) Successful in 10s
Publish to PowerShell Gallery / publish (pull_request) Successful in 7s
2026-06-06 20:18:46 -04:00
GraceSolutions 77cb03ec98 feat: add Organization/Sub-Organization CRUD cmdlets and Get-InfisicalSANList
Adds 8 cmdlets for Organization and Sub-Organization CRUD (Get/New/Update/Remove for each), targeting /api/v2/organizations and /api/v1/sub-organizations. Get cmdlets default to List parameter set and switch to Single when -OrganizationId or -SubOrganizationId is supplied. New/Update/Remove honor -WhatIf/-Confirm; Remove defaults to High ConfirmImpact and supports -PassThru. No project context required.

Adds Get-InfisicalSANList: emits a deduplicated SAN candidate set containing the local device name, the device name suffixed with each non-empty DNS suffix found across operational adapters and the system primary domain, every IPv4 unicast address falling within RFC 1918 or CGNAT, and the IPv4/IPv6 loopback addresses. Supports optional case-insensitive -InclusionExpression and -ExclusionExpression regex filters applied in fetch -> include -> exclude -> output order. Output is a single strongly-typed System.String[] array emitted non-enumerated so List<string>.AddRange consumes it directly.

Registers 10 new endpoints, adds InfisicalOrganization/InfisicalSubOrganization models with DTOs, mappers, and clients, full MAML help for all 9 new cmdlets, mapper unit tests, EndpointRegistry inline-data coverage, and docs/DesignSpec.md sections 16.7 and 16.8. build.ps1 CmdletsToExport and Test-ModuleImports expected list now contain 51 cmdlets. README updated with Organization/Sub-Organization tables, the new Get-InfisicalSANList entry, and an end-to-end certificate request example using splatted OrderedDictionary blocks.
2026-06-06 20:17:49 -04:00
gsadmin d0395b54ac Merge pull request 'feat: add Start-InfisicalProcess cmdlet and -Prefix support on Export-InfisicalSecrets' (#12) from dev into main
Reviewed-on: #12
2026-06-06 22:36:22 +00:00
GraceSolutions 15fadd01a4 Build artifacts for 207e7429e4
Publish to PowerShell Gallery / build (pull_request) Successful in 24s
Publish to PowerShell Gallery / release (pull_request) Successful in 9s
Publish to PowerShell Gallery / publish (pull_request) Successful in 8s
Auto-generated by build.ps1 -CommitArtifacts. Build 2026.06.06.2229. Module DLL and manifest embed BuildCommitHash=207e7429e448, matching the source commit they were produced from.
2026-06-06 18:29:45 -04:00
GraceSolutions 207e7429e4 feat(process): add Start-InfisicalProcess with event-based capture and friendly TimeSpan logging
- New cmdlet Start-InfisicalProcess: launches a child process with InfisicalSecret
  objects decrypted directly into ProcessStartInfo.Environment (optional -Prefix),
  additional -EnvironmentVariables, stdout/stderr capture, -AcceptableExitCodeList,
  -ParsingExpression regex parsing, -ExecutionTimeout / -ExecutionTimeoutInterval,
  -NoWait, -WindowStyle / -CreateNoWindow parameter sets, -Priority,
  -StandardInputObjectList, -SecureArgumentList, -LogOutput, -ContinueOnError, and
  ShouldProcess support. Secret plaintext is never written to user or machine scope.
- Stream capture uses event-based OutputDataReceived/ErrorDataReceived with
  BeginOutputReadLine/BeginErrorReadLine (no Task / ReadToEndAsync /
  GetAwaiter().GetResult()) to avoid PowerShell SynchronizationContext deadlocks.
- Restored the do { log; sleep } while (!HasExited) polling pattern using
  Thread.Sleep(pollInterval) so verbose "has been running for X" / "Checking again
  in Y" messages fire at the configured cadence even when no -ExecutionTimeout is
  supplied.
- TimeSpan values in verbose logs and on the result now use a friendly format
  ("7 seconds, and 364 milliseconds", "1 minute, and 30 seconds", "N/A" when zero)
  matching the legacy Start-ProcessWithOutput GetTimeSpanMessage scriptblock.
- Added DurationFriendly property to InfisicalProcessResult and a "The command
  execution took X" verbose line at completion.
- build.ps1 CmdletsToExport and Test-ModuleImports expected list contain 42 cmdlets.
- Added 9 xUnit tests covering FormatFriendly singular/plural, multi-unit joining,
  zero, sub-millisecond, and skip-zero-components behavior.
2026-06-06 18:29:30 -04:00
GraceSolutions d3c7b83da7 Build artifacts for 318db70480
Auto-generated by build.ps1 -CommitArtifacts. Build 2026.06.06.2138. Module DLL and manifest embed BuildCommitHash=318db7048017, matching the source commit they were produced from.
2026-06-06 17:38:10 -04:00
GraceSolutions 318db70480 feat(export): add -Prefix parameter to Export-InfisicalSecrets
Adds an optional [string] -Prefix parameter that prepends the supplied
string to every emitted variable name, regardless of -Format
(Json/Yaml/Xml/Env/EnvironmentVariables). When omitted or empty the
exporter buffer is forwarded unchanged (no-op).

Implementation clones each InfisicalSecret with SecretName = Prefix +
SecretName so the caller's pipeline objects are never mutated; the
SecureString and Tags/SecretMetadata array references are shared
(read-only usage downstream).

Also updates the cmdlet help XML description + adds a -Prefix example,
and reflects the new parameter in docs/DesignSpec.md.
2026-06-06 17:37:56 -04:00
97 changed files with 4858 additions and 91 deletions
+328
View File
@@ -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
+171 -14
View File
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>
+15 -3
View File
@@ -1,6 +1,6 @@
@{
RootModule = 'PSInfisicalAPI.psm1'
ModuleVersion = '2026.06.05.2040'
ModuleVersion = '2026.07.30.2151'
GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51'
Author = 'Grace Solutions'
CompanyName = 'Grace Solutions'
@@ -19,6 +19,7 @@
'Copy-InfisicalSecret',
'ConvertTo-InfisicalSecretDictionary',
'Export-InfisicalSecrets',
'Import-InfisicalSecret',
'Get-InfisicalProject',
'New-InfisicalProject',
'Update-InfisicalProject',
@@ -35,6 +36,14 @@
'New-InfisicalTag',
'Update-InfisicalTag',
'Remove-InfisicalTag',
'Get-InfisicalOrganization',
'New-InfisicalOrganization',
'Update-InfisicalOrganization',
'Remove-InfisicalOrganization',
'Get-InfisicalSubOrganization',
'New-InfisicalSubOrganization',
'Update-InfisicalSubOrganization',
'Remove-InfisicalSubOrganization',
'Get-InfisicalCertificateAuthority',
'Get-InfisicalPkiSubscriber',
'Get-InfisicalCertificateProfile',
@@ -50,7 +59,10 @@
'New-InfisicalScepDynamicChallenge',
'Get-InfisicalScepMdmProfile',
'Export-InfisicalScepMdmProfile',
'Write-InfisicalScepMdmProfileToWmi'
'Write-InfisicalScepMdmProfileToWmi',
'Start-InfisicalProcess',
'Get-InfisicalEnvironmentVariable',
'Get-InfisicalSANList'
)
AliasesToExport = @()
VariablesToExport = @()
@@ -62,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 = '1270c9099cae'
CommitHash = '14c8c4f3845b'
}
}
}
Binary file not shown.
@@ -295,7 +295,7 @@ $CopyInfisicalSecretResult = Copy-InfisicalSecret @CopyInfisicalSecretParameters
<command:noun>InfisicalSecretDictionary</command:noun>
</command:details>
<maml:description>
<maml:para>Aggregates an incoming pipeline of InfisicalSecret objects into a case-insensitive Dictionary keyed by SecretName. By default values are SecureString; pass -AsPlainText to materialize string values. Duplicate keys are handled via the -DuplicateKeyBehavior parameter (Error, FirstWins, LastWins).</maml:para>
<maml:para>Aggregates an incoming pipeline of InfisicalSecret objects into a case-insensitive Dictionary keyed by SecretName. By default values are SecureString; pass -AsPlainText to materialize string values. Duplicate keys are handled via the -DuplicateKeyBehavior parameter (Error, FirstWins, LastWins). -Prefix prepends a string to every dictionary key (e.g. SecretName 'API_KEY' with -Prefix 'MYAPP_' becomes key 'MYAPP_API_KEY'); the underlying InfisicalSecret objects are not mutated. A SecretName that already starts with -Prefix (case-insensitive) is left as-is to avoid double-prefixing; pass -ForcePrefix to always prepend.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
@@ -322,6 +322,11 @@ $ConvertToInfisicalSecretDictionaryParameters.Verbose = $True
$ConvertToInfisicalSecretDictionaryResult = ConvertTo-InfisicalSecretDictionary @ConvertToInfisicalSecretDictionaryParameters</dev:code>
<dev:remarks><maml:para>Aggregates recursive secret results into a plain-text dictionary, with the last value winning on key collisions.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 3</maml:title>
<dev:code>Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' | ConvertTo-InfisicalSecretDictionary -Prefix 'MYAPP_' -AsPlainText</dev:code>
<dev:remarks><maml:para>Builds a plain-text dictionary whose keys are namespaced with 'MYAPP_' (e.g. API_KEY becomes MYAPP_API_KEY); the source InfisicalSecret objects are unchanged.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
@@ -333,7 +338,7 @@ $ConvertToInfisicalSecretDictionaryResult = ConvertTo-InfisicalSecretDictionary
<command:noun>InfisicalSecrets</command:noun>
</command:details>
<maml:description>
<maml:para>Buffers an incoming pipeline of InfisicalSecret objects and writes them to a file in the requested format (DotEnv, Json, Yaml, EnvironmentVariables, etc.) or sets them as environment variables on the chosen scope (Process, User, Machine). -Encoding controls text encoding for file outputs.</maml:para>
<maml:para>Buffers an incoming pipeline of InfisicalSecret objects and writes them to a file in the requested format (DotEnv, Json, Yaml, EnvironmentVariables, etc.) or sets them as environment variables on the chosen scope (Process, User, Machine). -Encoding controls text encoding for file outputs. -Prefix prepends a string to every emitted variable name regardless of format; names that already start with -Prefix (case-insensitive) are left as-is to avoid double-prefixing. Pass -ForcePrefix to always prepend.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
@@ -361,9 +366,52 @@ $ExportInfisicalSecretsParameters.Verbose = $True
$ExportInfisicalSecretsResult = Export-InfisicalSecrets @ExportInfisicalSecretsParameters</dev:code>
<dev:remarks><maml:para>Projects the recursive secret result into Process-scope environment variables for the current PowerShell session.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 3</maml:title>
<dev:code>Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' | Export-InfisicalSecrets -Format EnvironmentVariables -Scope Process -Prefix 'MYAPP_'</dev:code>
<dev:remarks><maml:para>Imports secrets into the process environment with every variable name prefixed by 'MYAPP_' (e.g. API_KEY becomes MYAPP_API_KEY).</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>Import-InfisicalSecret</command:name>
<maml:description><maml:para>Reads a previously exported secrets file (Json, Yaml, Env, or Xml) back into a name-keyed Dictionary.</maml:para></maml:description>
<command:verb>Import</command:verb>
<command:noun>InfisicalSecret</command:noun>
</command:details>
<maml:description>
<maml:para>Loads the file at -Path (which must exist) using the parser matching -Format and returns a case-insensitive Dictionary keyed by SecretName. By default values are SecureString; pass -AsPlainText for a plain string dictionary. -Prefix prepends to every emitted key; keys already starting with -Prefix (case-insensitive) are left as-is to avoid double-prefixing, and -ForcePrefix overrides that gate. -DuplicateKeyBehavior controls collision handling (Error, FirstWins, LastWins). The EnvironmentVariables format is intentionally not supported here; use [Environment]::GetEnvironmentVariable or Get-InfisicalEnvironmentVariable for environment-backed values.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
<maml:alert>
<maml:para>Importers expect the same schema that Export-InfisicalSecrets produces (Json/Yaml = array or 'Secrets' root list of {SecretName, SecretValue}; Xml = &lt;Secrets&gt;&lt;Secret&gt;&lt;SecretName/&gt;&lt;SecretValue/&gt;; Env = KEY=VALUE per line, '#' comments allowed). JSON and YAML additionally accept a flat key/value object as a convenience.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
<command:example>
<maml:title>EXAMPLE 1</maml:title>
<dev:code>$Secrets = Import-InfisicalSecret -Path '.\secrets.json' -Format Json</dev:code>
<dev:remarks><maml:para>Reads a JSON export back into a Dictionary&lt;string, SecureString&gt; keyed by the original SecretName values.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 2</maml:title>
<dev:code>$ImportInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$ImportInfisicalSecretParameters.Path = [System.IO.FileInfo]'.\secrets.env'
$ImportInfisicalSecretParameters.Format = 'Env'
$ImportInfisicalSecretParameters.Prefix = 'MYAPP_'
$ImportInfisicalSecretParameters.DuplicateKeyBehavior = 'LastWins'
$ImportInfisicalSecretParameters.AsPlainText = $True
$ImportInfisicalSecretResult = Import-InfisicalSecret @ImportInfisicalSecretParameters</dev:code>
<dev:remarks><maml:para>Loads a .env file into a plain-text dictionary, namespacing every key with 'MYAPP_' and letting the last occurrence win on duplicates.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>Get-InfisicalProject</command:name>
@@ -1018,7 +1066,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>
@@ -1233,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>
@@ -1649,4 +1699,400 @@ $WriteInfisicalScepMdmProfileToWmiResult = Write-InfisicalScepMdmProfileToWmi @W
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>Start-InfisicalProcess</command:name>
<maml:description><maml:para>Starts a child process with Infisical secrets injected directly into its environment block.</maml:para></maml:description>
<command:verb>Start</command:verb>
<command:noun>InfisicalProcess</command:noun>
</command:details>
<maml:description>
<maml:para>Launches the executable specified by -FilePath, captures stdout/stderr, validates the exit code against -AcceptableExitCodeList, and optionally parses output with -ParsingExpression. InfisicalSecret objects supplied via -Secret (pipeline or by name) are decrypted into the ProcessStartInfo.Environment dictionary only, never written to the user or machine scope; -Prefix prepends a string to each injected variable name, skipping names that already start with -Prefix (case-insensitive) unless -ForcePrefix is supplied. -EnvironmentVariables adds additional non-secret values. -ExecutionTimeout, -NoWait, -CreateNoWindow, -WindowStyle, -Priority, -StandardInputObjectList, -SecureArgumentList, -LogOutput, and -ContinueOnError mirror the semantics of the upstream Start-ProcessWithOutput helper. Honors -WhatIf and -Confirm.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
<maml:alert>
<maml:para>Secret values exist as plain strings only within the child process environment block; they are never persisted to the calling shell, the user scope, or the machine scope. Use -SecureArgumentList to mask sensitive command-line arguments in verbose output.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
<command:example>
<maml:title>EXAMPLE 1</maml:title>
<dev:code>Get-InfisicalSecret -SecretPath '/build' | Start-InfisicalProcess -FilePath 'dotnet.exe' -ArgumentList @('publish','-c','Release') -AcceptableExitCodeList @('0') -CreateNoWindow</dev:code>
<dev:remarks><maml:para>Decrypts every secret at /build, exposes each one as a process environment variable, and runs dotnet publish with no visible window.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 2</maml:title>
<dev:code>$Secrets = Get-InfisicalSecret -SecretPath '/runtime'
Start-InfisicalProcess -FilePath 'node.exe' -ArgumentList @('app.js') -Secret $Secrets -Prefix 'APP_' -ExecutionTimeout ([TimeSpan]::FromMinutes(5)) -LogOutput</dev:code>
<dev:remarks><maml:para>Injects the /runtime secrets as APP_-prefixed environment variables, runs node app.js, and forcibly terminates the process after five minutes if it has not exited.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 3</maml:title>
<dev:code>$StartInfisicalProcessParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$StartInfisicalProcessParameters.FilePath = 'pwsh.exe'
$StartInfisicalProcessParameters.ArgumentList = @('-NoProfile','-Command','Write-Host $env:DEPLOY_TOKEN.Length')
$StartInfisicalProcessParameters.Secret = Get-InfisicalSecret -SecretPath '/deploy'
$StartInfisicalProcessParameters.Prefix = 'DEPLOY_'
$StartInfisicalProcessParameters.AcceptableExitCodeList = @('0')
$StartInfisicalProcessParameters.CreateNoWindow = $True
$StartInfisicalProcessParameters.SecureArgumentList = $True
$StartInfisicalProcessParameters.LogOutput = $True
$StartInfisicalProcessParameters.Verbose = $True
$StartInfisicalProcessResult = Start-InfisicalProcess @StartInfisicalProcessParameters</dev:code>
<dev:remarks><maml:para>Splatted invocation that runs pwsh with DEPLOY_-prefixed secrets in scope, masks the command line in verbose output, and echoes both stdout and stderr to the verbose stream after exit.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>Get-InfisicalEnvironmentVariable</command:name>
<maml:description><maml:para>Reads an environment variable from the first scope that has a non-empty value (Process > User > Machine).</maml:para></maml:description>
<command:verb>Get</command:verb>
<command:noun>InfisicalEnvironmentVariable</command:noun>
</command:details>
<maml:description>
<maml:para>Returns the value of -Name from the first scope that contains a non-empty value, checking Process, then User, then Machine in that order. Emits nothing when the variable is missing or blank in every scope, so an assignment yields $null without writing errors or warnings. Platform-unsupported scopes (User and Machine on non-Windows) are silently skipped.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
<maml:alert>
<maml:para>Designed for the same discovery semantics the rest of PSInfisicalAPI uses when resolving connection inputs from the environment. Pipe-friendly: accepts -Name from the pipeline by value and by property name.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
<command:example>
<maml:title>EXAMPLE 1</maml:title>
<dev:code>$Value = Get-InfisicalEnvironmentVariable -Name 'INFISICAL_CLIENT_ID'</dev:code>
<dev:remarks><maml:para>Returns the first non-empty value of INFISICAL_CLIENT_ID across Process, User, and Machine scopes; assigns $null when the variable is unset everywhere.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 2</maml:title>
<dev:code>@('INFISICAL_BASE_URI','INFISICAL_PROJECT_ID') | Get-InfisicalEnvironmentVariable</dev:code>
<dev:remarks><maml:para>Pipes a list of variable names through the cmdlet and emits one value per name that is set in any scope.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>Get-InfisicalOrganization</command:name>
<maml:description><maml:para>Lists or retrieves Infisical organizations accessible to the current identity.</maml:para></maml:description>
<command:verb>Get</command:verb>
<command:noun>InfisicalOrganization</command:noun>
</command:details>
<maml:description>
<maml:para>Default (List parameter set) returns every organization the active session can see; visibility is governed by Infisical's role assignments. When -OrganizationId is supplied (Single parameter set) the cmdlet returns one organization. Does not require a project context.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
<maml:alert>
<maml:para>The List-mode result is an array of InfisicalOrganization objects; pipe into Where-Object or Select-Object to filter by Slug, Name, or Id. The cmdlet accepts pipeline input by property name on -OrganizationId.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
<command:example>
<maml:title>EXAMPLE 1</maml:title>
<dev:code>Get-InfisicalOrganization</dev:code>
<dev:remarks><maml:para>Lists every organization the current session can see.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 2</maml:title>
<dev:code>Get-InfisicalOrganization -OrganizationId $OrganizationId</dev:code>
<dev:remarks><maml:para>Retrieves the canonical record for a single organization by id.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>New-InfisicalOrganization</command:name>
<maml:description><maml:para>Creates a new Infisical organization.</maml:para></maml:description>
<command:verb>New</command:verb>
<command:noun>InfisicalOrganization</command:noun>
</command:details>
<maml:description>
<maml:para>Creates a new organization with the supplied name and optional slug. Honors -WhatIf and -Confirm. Requires server-side permission to create organizations.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
<maml:alert>
<maml:para>Slug must be unique server-side; if omitted, the server derives one from the name.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
<command:example>
<maml:title>EXAMPLE 1</maml:title>
<dev:code>New-InfisicalOrganization -Name 'Acme Corporation'</dev:code>
<dev:remarks><maml:para>Creates a new organization named 'Acme Corporation' with a server-derived slug.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 2</maml:title>
<dev:code>$NewInfisicalOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$NewInfisicalOrganizationParameters.Name = 'Acme Corporation'
$NewInfisicalOrganizationParameters.Slug = 'acme-corp'
$NewInfisicalOrganizationParameters.Verbose = $True
$NewInfisicalOrganizationResult = New-InfisicalOrganization @NewInfisicalOrganizationParameters</dev:code>
<dev:remarks><maml:para>Creates an organization with an explicit slug.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>Update-InfisicalOrganization</command:name>
<maml:description><maml:para>Updates mutable attributes on an existing Infisical organization.</maml:para></maml:description>
<command:verb>Update</command:verb>
<command:noun>InfisicalOrganization</command:noun>
</command:details>
<maml:description>
<maml:para>Updates the name or slug of an organization. -OrganizationId is required. Only bound parameters are transmitted. Honors -WhatIf and -Confirm.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
<maml:alert>
<maml:para>Renaming or re-slugging an organization may affect billing exports and identity URLs; coordinate with downstream consumers.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
<command:example>
<maml:title>EXAMPLE 1</maml:title>
<dev:code>Update-InfisicalOrganization -OrganizationId $OrganizationId -Name 'Acme Corp.'</dev:code>
<dev:remarks><maml:para>Renames the supplied organization.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 2</maml:title>
<dev:code>$UpdateInfisicalOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$UpdateInfisicalOrganizationParameters.OrganizationId = $OrganizationId
$UpdateInfisicalOrganizationParameters.Name = 'Acme Corp.'
$UpdateInfisicalOrganizationParameters.Slug = 'acme-corp'
$UpdateInfisicalOrganizationParameters.Verbose = $True
$UpdateInfisicalOrganizationResult = Update-InfisicalOrganization @UpdateInfisicalOrganizationParameters</dev:code>
<dev:remarks><maml:para>Renames the organization and updates its slug.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>Remove-InfisicalOrganization</command:name>
<maml:description><maml:para>Deletes an Infisical organization.</maml:para></maml:description>
<command:verb>Remove</command:verb>
<command:noun>InfisicalOrganization</command:noun>
</command:details>
<maml:description>
<maml:para>Deletes an organization by id. -OrganizationId is required. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed organization id.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
<maml:alert>
<maml:para>This is irreversible and removes all projects, sub-organizations, secrets, and identities owned by the organization. Honors -WhatIf and -Confirm.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
<command:example>
<maml:title>EXAMPLE 1</maml:title>
<dev:code>Remove-InfisicalOrganization -OrganizationId $OrganizationId -Confirm:$False</dev:code>
<dev:remarks><maml:para>Deletes the supplied organization without prompting.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 2</maml:title>
<dev:code>$RemoveInfisicalOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$RemoveInfisicalOrganizationParameters.OrganizationId = $OrganizationId
$RemoveInfisicalOrganizationParameters.PassThru = $True
$RemoveInfisicalOrganizationParameters.Confirm = $False
$RemoveInfisicalOrganizationParameters.Verbose = $True
$RemoveInfisicalOrganizationResult = Remove-InfisicalOrganization @RemoveInfisicalOrganizationParameters</dev:code>
<dev:remarks><maml:para>Removes the organization without confirmation and emits the removed organization id for logging.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>Get-InfisicalSubOrganization</command:name>
<maml:description><maml:para>Lists or retrieves Infisical sub-organizations accessible to the current identity.</maml:para></maml:description>
<command:verb>Get</command:verb>
<command:noun>InfisicalSubOrganization</command:noun>
</command:details>
<maml:description>
<maml:para>Default (List parameter set) returns every sub-organization the active session can see. Optional -Limit, -Offset, -Search, -OrderBy, -OrderDirection, and -IsAccessible are forwarded to the server as query parameters. When -SubOrganizationId is supplied (Single parameter set) the cmdlet returns one sub-organization. Does not require a project context.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
<maml:alert>
<maml:para>Sub-organizations are a beta Infisical feature. The List result is an array of InfisicalSubOrganization objects; pipe into Where-Object or Select-Object to filter further. The cmdlet accepts pipeline input by property name on -SubOrganizationId.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
<command:example>
<maml:title>EXAMPLE 1</maml:title>
<dev:code>Get-InfisicalSubOrganization</dev:code>
<dev:remarks><maml:para>Lists every sub-organization the current session can see.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 2</maml:title>
<dev:code>Get-InfisicalSubOrganization -SubOrganizationId $SubOrganizationId</dev:code>
<dev:remarks><maml:para>Retrieves the canonical record for a single sub-organization by id.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 3</maml:title>
<dev:code>Get-InfisicalSubOrganization -Search 'platform' -OrderBy 'name' -OrderDirection 'asc' -Limit 25 -IsAccessible</dev:code>
<dev:remarks><maml:para>Lists up to 25 sub-organizations matching 'platform', sorted ascending by name, restricted to those the current identity has access to.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>New-InfisicalSubOrganization</command:name>
<maml:description><maml:para>Creates a new Infisical sub-organization.</maml:para></maml:description>
<command:verb>New</command:verb>
<command:noun>InfisicalSubOrganization</command:noun>
</command:details>
<maml:description>
<maml:para>Creates a sub-organization with the supplied name and slug. Both are required by the server. Honors -WhatIf and -Confirm.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
<maml:alert>
<maml:para>Slug must be unique within the parent organization. Sub-organizations are a beta Infisical feature.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
<command:example>
<maml:title>EXAMPLE 1</maml:title>
<dev:code>New-InfisicalSubOrganization -Name 'Platform Engineering' -Slug 'platform-eng'</dev:code>
<dev:remarks><maml:para>Creates a new sub-organization named 'Platform Engineering' with slug 'platform-eng'.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 2</maml:title>
<dev:code>$NewInfisicalSubOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$NewInfisicalSubOrganizationParameters.Name = 'Platform Engineering'
$NewInfisicalSubOrganizationParameters.Slug = 'platform-eng'
$NewInfisicalSubOrganizationParameters.Verbose = $True
$NewInfisicalSubOrganizationResult = New-InfisicalSubOrganization @NewInfisicalSubOrganizationParameters</dev:code>
<dev:remarks><maml:para>Splatted invocation that creates a sub-organization and logs the request via the verbose stream.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>Update-InfisicalSubOrganization</command:name>
<maml:description><maml:para>Updates mutable attributes on an existing Infisical sub-organization.</maml:para></maml:description>
<command:verb>Update</command:verb>
<command:noun>InfisicalSubOrganization</command:noun>
</command:details>
<maml:description>
<maml:para>Updates the name or slug of a sub-organization. -SubOrganizationId is required. Only bound parameters are transmitted. Honors -WhatIf and -Confirm.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
<maml:alert>
<maml:para>Sub-organizations are a beta Infisical feature; coordinate slug changes with downstream consumers that pin the slug in scripts or configuration files.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
<command:example>
<maml:title>EXAMPLE 1</maml:title>
<dev:code>Update-InfisicalSubOrganization -SubOrganizationId $SubOrganizationId -Name 'Platform (v2)'</dev:code>
<dev:remarks><maml:para>Renames the supplied sub-organization.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 2</maml:title>
<dev:code>$UpdateInfisicalSubOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$UpdateInfisicalSubOrganizationParameters.SubOrganizationId = $SubOrganizationId
$UpdateInfisicalSubOrganizationParameters.Name = 'Platform (v2)'
$UpdateInfisicalSubOrganizationParameters.Slug = 'platform-v2'
$UpdateInfisicalSubOrganizationParameters.Verbose = $True
$UpdateInfisicalSubOrganizationResult = Update-InfisicalSubOrganization @UpdateInfisicalSubOrganizationParameters</dev:code>
<dev:remarks><maml:para>Renames the sub-organization and updates its slug in a single call.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>Remove-InfisicalSubOrganization</command:name>
<maml:description><maml:para>Deletes an Infisical sub-organization.</maml:para></maml:description>
<command:verb>Remove</command:verb>
<command:noun>InfisicalSubOrganization</command:noun>
</command:details>
<maml:description>
<maml:para>Deletes a sub-organization by id. -SubOrganizationId is required. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed sub-organization id.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
<maml:alert>
<maml:para>This is destructive and removes all projects, secrets, and identities scoped to the sub-organization. Honors -WhatIf and -Confirm.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
<command:example>
<maml:title>EXAMPLE 1</maml:title>
<dev:code>Remove-InfisicalSubOrganization -SubOrganizationId $SubOrganizationId -Confirm:$False</dev:code>
<dev:remarks><maml:para>Deletes the supplied sub-organization without prompting.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 2</maml:title>
<dev:code>$RemoveInfisicalSubOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$RemoveInfisicalSubOrganizationParameters.SubOrganizationId = $SubOrganizationId
$RemoveInfisicalSubOrganizationParameters.PassThru = $True
$RemoveInfisicalSubOrganizationParameters.Confirm = $False
$RemoveInfisicalSubOrganizationParameters.Verbose = $True
$RemoveInfisicalSubOrganizationResult = Remove-InfisicalSubOrganization @RemoveInfisicalSubOrganizationParameters</dev:code>
<dev:remarks><maml:para>Removes the sub-organization without confirmation and emits the removed id for logging.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>Get-InfisicalSANList</command:name>
<maml:description><maml:para>Builds a deduplicated list of Subject Alternative Name candidates for the local device.</maml:para></maml:description>
<command:verb>Get</command:verb>
<command:noun>InfisicalSANList</command:noun>
</command:details>
<maml:description>
<maml:para>Returns, in order: the local device name; the device name suffixed with each non-empty DNS suffix found on any operational (non-loopback) network adapter and the system primary domain; every IPv4 unicast address whose first octets fall within RFC 1918 (10/8, 172.16/12, 192.168/16) or CGNAT (100.64/10); and the IPv4 and IPv6 loopback addresses (127.0.0.1, ::1). Optional -InclusionExpression and -ExclusionExpression regex filters are applied in that order after collection, before output. Suitable as a one-shot SAN provider for Request-InfisicalCertificate -DnsName.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
<maml:alert>
<maml:para>Output is a single strongly-typed System.String[] array (emitted non-enumerated) so it round-trips into [System.Collections.Generic.List[string]]::AddRange() and binds directly to string[] parameters such as Request-InfisicalCertificate -DnsName. The device name comes first so it can be reused as a CommonName. Routable public IPv4 addresses, link-local addresses, and IPv6 unicast addresses other than loopback are intentionally excluded. -InclusionExpression and -ExclusionExpression are case-insensitive .NET regular expressions; inclusion is applied first, then exclusion.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
<command:example>
<maml:title>EXAMPLE 1</maml:title>
<dev:code>Get-InfisicalSANList</dev:code>
<dev:remarks><maml:para>Returns the SAN candidate list for the current device.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 2</maml:title>
<dev:code>$Sans = Get-InfisicalSANList
Request-InfisicalCertificate -ProjectId $ProjectId -CertificateAuthorityId $CaId -CommonName $Sans[0] -DnsName $Sans -Ttl '90d'</dev:code>
<dev:remarks><maml:para>Captures the SAN list, then uses the device name as the CommonName and the full list as DnsName when requesting a certificate.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 3</maml:title>
<dev:code>$GetInfisicalSANListParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$GetInfisicalSANListParameters.InclusionExpression = '\.gracesolution\.prv$|^10\.|^172\.'
$GetInfisicalSANListParameters.ExclusionExpression = '^127\.|^::1$'
$Sans = Get-InfisicalSANList @GetInfisicalSANListParameters</dev:code>
<dev:remarks><maml:para>Keeps only entries ending in the corporate DNS suffix or sitting in the 10/8 or 172/12 ranges, then drops loopback. Filters are case-insensitive and applied in fetch -> include -> exclude -> output order.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
</helpItems>
@@ -295,7 +295,7 @@ $CopyInfisicalSecretResult = Copy-InfisicalSecret @CopyInfisicalSecretParameters
<command:noun>InfisicalSecretDictionary</command:noun>
</command:details>
<maml:description>
<maml:para>Aggregates an incoming pipeline of InfisicalSecret objects into a case-insensitive Dictionary keyed by SecretName. By default values are SecureString; pass -AsPlainText to materialize string values. Duplicate keys are handled via the -DuplicateKeyBehavior parameter (Error, FirstWins, LastWins).</maml:para>
<maml:para>Aggregates an incoming pipeline of InfisicalSecret objects into a case-insensitive Dictionary keyed by SecretName. By default values are SecureString; pass -AsPlainText to materialize string values. Duplicate keys are handled via the -DuplicateKeyBehavior parameter (Error, FirstWins, LastWins). -Prefix prepends a string to every dictionary key (e.g. SecretName 'API_KEY' with -Prefix 'MYAPP_' becomes key 'MYAPP_API_KEY'); the underlying InfisicalSecret objects are not mutated. A SecretName that already starts with -Prefix (case-insensitive) is left as-is to avoid double-prefixing; pass -ForcePrefix to always prepend.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
@@ -322,6 +322,11 @@ $ConvertToInfisicalSecretDictionaryParameters.Verbose = $True
$ConvertToInfisicalSecretDictionaryResult = ConvertTo-InfisicalSecretDictionary @ConvertToInfisicalSecretDictionaryParameters</dev:code>
<dev:remarks><maml:para>Aggregates recursive secret results into a plain-text dictionary, with the last value winning on key collisions.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 3</maml:title>
<dev:code>Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' | ConvertTo-InfisicalSecretDictionary -Prefix 'MYAPP_' -AsPlainText</dev:code>
<dev:remarks><maml:para>Builds a plain-text dictionary whose keys are namespaced with 'MYAPP_' (e.g. API_KEY becomes MYAPP_API_KEY); the source InfisicalSecret objects are unchanged.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
@@ -333,7 +338,7 @@ $ConvertToInfisicalSecretDictionaryResult = ConvertTo-InfisicalSecretDictionary
<command:noun>InfisicalSecrets</command:noun>
</command:details>
<maml:description>
<maml:para>Buffers an incoming pipeline of InfisicalSecret objects and writes them to a file in the requested format (DotEnv, Json, Yaml, EnvironmentVariables, etc.) or sets them as environment variables on the chosen scope (Process, User, Machine). -Encoding controls text encoding for file outputs.</maml:para>
<maml:para>Buffers an incoming pipeline of InfisicalSecret objects and writes them to a file in the requested format (DotEnv, Json, Yaml, EnvironmentVariables, etc.) or sets them as environment variables on the chosen scope (Process, User, Machine). -Encoding controls text encoding for file outputs. -Prefix prepends a string to every emitted variable name regardless of format; names that already start with -Prefix (case-insensitive) are left as-is to avoid double-prefixing. Pass -ForcePrefix to always prepend.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
@@ -361,9 +366,52 @@ $ExportInfisicalSecretsParameters.Verbose = $True
$ExportInfisicalSecretsResult = Export-InfisicalSecrets @ExportInfisicalSecretsParameters</dev:code>
<dev:remarks><maml:para>Projects the recursive secret result into Process-scope environment variables for the current PowerShell session.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 3</maml:title>
<dev:code>Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' | Export-InfisicalSecrets -Format EnvironmentVariables -Scope Process -Prefix 'MYAPP_'</dev:code>
<dev:remarks><maml:para>Imports secrets into the process environment with every variable name prefixed by 'MYAPP_' (e.g. API_KEY becomes MYAPP_API_KEY).</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>Import-InfisicalSecret</command:name>
<maml:description><maml:para>Reads a previously exported secrets file (Json, Yaml, Env, or Xml) back into a name-keyed Dictionary.</maml:para></maml:description>
<command:verb>Import</command:verb>
<command:noun>InfisicalSecret</command:noun>
</command:details>
<maml:description>
<maml:para>Loads the file at -Path (which must exist) using the parser matching -Format and returns a case-insensitive Dictionary keyed by SecretName. By default values are SecureString; pass -AsPlainText for a plain string dictionary. -Prefix prepends to every emitted key; keys already starting with -Prefix (case-insensitive) are left as-is to avoid double-prefixing, and -ForcePrefix overrides that gate. -DuplicateKeyBehavior controls collision handling (Error, FirstWins, LastWins). The EnvironmentVariables format is intentionally not supported here; use [Environment]::GetEnvironmentVariable or Get-InfisicalEnvironmentVariable for environment-backed values.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
<maml:alert>
<maml:para>Importers expect the same schema that Export-InfisicalSecrets produces (Json/Yaml = array or 'Secrets' root list of {SecretName, SecretValue}; Xml = &lt;Secrets&gt;&lt;Secret&gt;&lt;SecretName/&gt;&lt;SecretValue/&gt;; Env = KEY=VALUE per line, '#' comments allowed). JSON and YAML additionally accept a flat key/value object as a convenience.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
<command:example>
<maml:title>EXAMPLE 1</maml:title>
<dev:code>$Secrets = Import-InfisicalSecret -Path '.\secrets.json' -Format Json</dev:code>
<dev:remarks><maml:para>Reads a JSON export back into a Dictionary&lt;string, SecureString&gt; keyed by the original SecretName values.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 2</maml:title>
<dev:code>$ImportInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$ImportInfisicalSecretParameters.Path = [System.IO.FileInfo]'.\secrets.env'
$ImportInfisicalSecretParameters.Format = 'Env'
$ImportInfisicalSecretParameters.Prefix = 'MYAPP_'
$ImportInfisicalSecretParameters.DuplicateKeyBehavior = 'LastWins'
$ImportInfisicalSecretParameters.AsPlainText = $True
$ImportInfisicalSecretResult = Import-InfisicalSecret @ImportInfisicalSecretParameters</dev:code>
<dev:remarks><maml:para>Loads a .env file into a plain-text dictionary, namespacing every key with 'MYAPP_' and letting the last occurrence win on duplicates.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>Get-InfisicalProject</command:name>
@@ -1018,7 +1066,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>
@@ -1233,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>
@@ -1649,4 +1699,400 @@ $WriteInfisicalScepMdmProfileToWmiResult = Write-InfisicalScepMdmProfileToWmi @W
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>Start-InfisicalProcess</command:name>
<maml:description><maml:para>Starts a child process with Infisical secrets injected directly into its environment block.</maml:para></maml:description>
<command:verb>Start</command:verb>
<command:noun>InfisicalProcess</command:noun>
</command:details>
<maml:description>
<maml:para>Launches the executable specified by -FilePath, captures stdout/stderr, validates the exit code against -AcceptableExitCodeList, and optionally parses output with -ParsingExpression. InfisicalSecret objects supplied via -Secret (pipeline or by name) are decrypted into the ProcessStartInfo.Environment dictionary only, never written to the user or machine scope; -Prefix prepends a string to each injected variable name, skipping names that already start with -Prefix (case-insensitive) unless -ForcePrefix is supplied. -EnvironmentVariables adds additional non-secret values. -ExecutionTimeout, -NoWait, -CreateNoWindow, -WindowStyle, -Priority, -StandardInputObjectList, -SecureArgumentList, -LogOutput, and -ContinueOnError mirror the semantics of the upstream Start-ProcessWithOutput helper. Honors -WhatIf and -Confirm.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
<maml:alert>
<maml:para>Secret values exist as plain strings only within the child process environment block; they are never persisted to the calling shell, the user scope, or the machine scope. Use -SecureArgumentList to mask sensitive command-line arguments in verbose output.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
<command:example>
<maml:title>EXAMPLE 1</maml:title>
<dev:code>Get-InfisicalSecret -SecretPath '/build' | Start-InfisicalProcess -FilePath 'dotnet.exe' -ArgumentList @('publish','-c','Release') -AcceptableExitCodeList @('0') -CreateNoWindow</dev:code>
<dev:remarks><maml:para>Decrypts every secret at /build, exposes each one as a process environment variable, and runs dotnet publish with no visible window.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 2</maml:title>
<dev:code>$Secrets = Get-InfisicalSecret -SecretPath '/runtime'
Start-InfisicalProcess -FilePath 'node.exe' -ArgumentList @('app.js') -Secret $Secrets -Prefix 'APP_' -ExecutionTimeout ([TimeSpan]::FromMinutes(5)) -LogOutput</dev:code>
<dev:remarks><maml:para>Injects the /runtime secrets as APP_-prefixed environment variables, runs node app.js, and forcibly terminates the process after five minutes if it has not exited.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 3</maml:title>
<dev:code>$StartInfisicalProcessParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$StartInfisicalProcessParameters.FilePath = 'pwsh.exe'
$StartInfisicalProcessParameters.ArgumentList = @('-NoProfile','-Command','Write-Host $env:DEPLOY_TOKEN.Length')
$StartInfisicalProcessParameters.Secret = Get-InfisicalSecret -SecretPath '/deploy'
$StartInfisicalProcessParameters.Prefix = 'DEPLOY_'
$StartInfisicalProcessParameters.AcceptableExitCodeList = @('0')
$StartInfisicalProcessParameters.CreateNoWindow = $True
$StartInfisicalProcessParameters.SecureArgumentList = $True
$StartInfisicalProcessParameters.LogOutput = $True
$StartInfisicalProcessParameters.Verbose = $True
$StartInfisicalProcessResult = Start-InfisicalProcess @StartInfisicalProcessParameters</dev:code>
<dev:remarks><maml:para>Splatted invocation that runs pwsh with DEPLOY_-prefixed secrets in scope, masks the command line in verbose output, and echoes both stdout and stderr to the verbose stream after exit.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>Get-InfisicalEnvironmentVariable</command:name>
<maml:description><maml:para>Reads an environment variable from the first scope that has a non-empty value (Process > User > Machine).</maml:para></maml:description>
<command:verb>Get</command:verb>
<command:noun>InfisicalEnvironmentVariable</command:noun>
</command:details>
<maml:description>
<maml:para>Returns the value of -Name from the first scope that contains a non-empty value, checking Process, then User, then Machine in that order. Emits nothing when the variable is missing or blank in every scope, so an assignment yields $null without writing errors or warnings. Platform-unsupported scopes (User and Machine on non-Windows) are silently skipped.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
<maml:alert>
<maml:para>Designed for the same discovery semantics the rest of PSInfisicalAPI uses when resolving connection inputs from the environment. Pipe-friendly: accepts -Name from the pipeline by value and by property name.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
<command:example>
<maml:title>EXAMPLE 1</maml:title>
<dev:code>$Value = Get-InfisicalEnvironmentVariable -Name 'INFISICAL_CLIENT_ID'</dev:code>
<dev:remarks><maml:para>Returns the first non-empty value of INFISICAL_CLIENT_ID across Process, User, and Machine scopes; assigns $null when the variable is unset everywhere.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 2</maml:title>
<dev:code>@('INFISICAL_BASE_URI','INFISICAL_PROJECT_ID') | Get-InfisicalEnvironmentVariable</dev:code>
<dev:remarks><maml:para>Pipes a list of variable names through the cmdlet and emits one value per name that is set in any scope.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>Get-InfisicalOrganization</command:name>
<maml:description><maml:para>Lists or retrieves Infisical organizations accessible to the current identity.</maml:para></maml:description>
<command:verb>Get</command:verb>
<command:noun>InfisicalOrganization</command:noun>
</command:details>
<maml:description>
<maml:para>Default (List parameter set) returns every organization the active session can see; visibility is governed by Infisical's role assignments. When -OrganizationId is supplied (Single parameter set) the cmdlet returns one organization. Does not require a project context.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
<maml:alert>
<maml:para>The List-mode result is an array of InfisicalOrganization objects; pipe into Where-Object or Select-Object to filter by Slug, Name, or Id. The cmdlet accepts pipeline input by property name on -OrganizationId.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
<command:example>
<maml:title>EXAMPLE 1</maml:title>
<dev:code>Get-InfisicalOrganization</dev:code>
<dev:remarks><maml:para>Lists every organization the current session can see.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 2</maml:title>
<dev:code>Get-InfisicalOrganization -OrganizationId $OrganizationId</dev:code>
<dev:remarks><maml:para>Retrieves the canonical record for a single organization by id.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>New-InfisicalOrganization</command:name>
<maml:description><maml:para>Creates a new Infisical organization.</maml:para></maml:description>
<command:verb>New</command:verb>
<command:noun>InfisicalOrganization</command:noun>
</command:details>
<maml:description>
<maml:para>Creates a new organization with the supplied name and optional slug. Honors -WhatIf and -Confirm. Requires server-side permission to create organizations.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
<maml:alert>
<maml:para>Slug must be unique server-side; if omitted, the server derives one from the name.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
<command:example>
<maml:title>EXAMPLE 1</maml:title>
<dev:code>New-InfisicalOrganization -Name 'Acme Corporation'</dev:code>
<dev:remarks><maml:para>Creates a new organization named 'Acme Corporation' with a server-derived slug.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 2</maml:title>
<dev:code>$NewInfisicalOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$NewInfisicalOrganizationParameters.Name = 'Acme Corporation'
$NewInfisicalOrganizationParameters.Slug = 'acme-corp'
$NewInfisicalOrganizationParameters.Verbose = $True
$NewInfisicalOrganizationResult = New-InfisicalOrganization @NewInfisicalOrganizationParameters</dev:code>
<dev:remarks><maml:para>Creates an organization with an explicit slug.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>Update-InfisicalOrganization</command:name>
<maml:description><maml:para>Updates mutable attributes on an existing Infisical organization.</maml:para></maml:description>
<command:verb>Update</command:verb>
<command:noun>InfisicalOrganization</command:noun>
</command:details>
<maml:description>
<maml:para>Updates the name or slug of an organization. -OrganizationId is required. Only bound parameters are transmitted. Honors -WhatIf and -Confirm.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
<maml:alert>
<maml:para>Renaming or re-slugging an organization may affect billing exports and identity URLs; coordinate with downstream consumers.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
<command:example>
<maml:title>EXAMPLE 1</maml:title>
<dev:code>Update-InfisicalOrganization -OrganizationId $OrganizationId -Name 'Acme Corp.'</dev:code>
<dev:remarks><maml:para>Renames the supplied organization.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 2</maml:title>
<dev:code>$UpdateInfisicalOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$UpdateInfisicalOrganizationParameters.OrganizationId = $OrganizationId
$UpdateInfisicalOrganizationParameters.Name = 'Acme Corp.'
$UpdateInfisicalOrganizationParameters.Slug = 'acme-corp'
$UpdateInfisicalOrganizationParameters.Verbose = $True
$UpdateInfisicalOrganizationResult = Update-InfisicalOrganization @UpdateInfisicalOrganizationParameters</dev:code>
<dev:remarks><maml:para>Renames the organization and updates its slug.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>Remove-InfisicalOrganization</command:name>
<maml:description><maml:para>Deletes an Infisical organization.</maml:para></maml:description>
<command:verb>Remove</command:verb>
<command:noun>InfisicalOrganization</command:noun>
</command:details>
<maml:description>
<maml:para>Deletes an organization by id. -OrganizationId is required. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed organization id.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
<maml:alert>
<maml:para>This is irreversible and removes all projects, sub-organizations, secrets, and identities owned by the organization. Honors -WhatIf and -Confirm.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
<command:example>
<maml:title>EXAMPLE 1</maml:title>
<dev:code>Remove-InfisicalOrganization -OrganizationId $OrganizationId -Confirm:$False</dev:code>
<dev:remarks><maml:para>Deletes the supplied organization without prompting.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 2</maml:title>
<dev:code>$RemoveInfisicalOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$RemoveInfisicalOrganizationParameters.OrganizationId = $OrganizationId
$RemoveInfisicalOrganizationParameters.PassThru = $True
$RemoveInfisicalOrganizationParameters.Confirm = $False
$RemoveInfisicalOrganizationParameters.Verbose = $True
$RemoveInfisicalOrganizationResult = Remove-InfisicalOrganization @RemoveInfisicalOrganizationParameters</dev:code>
<dev:remarks><maml:para>Removes the organization without confirmation and emits the removed organization id for logging.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>Get-InfisicalSubOrganization</command:name>
<maml:description><maml:para>Lists or retrieves Infisical sub-organizations accessible to the current identity.</maml:para></maml:description>
<command:verb>Get</command:verb>
<command:noun>InfisicalSubOrganization</command:noun>
</command:details>
<maml:description>
<maml:para>Default (List parameter set) returns every sub-organization the active session can see. Optional -Limit, -Offset, -Search, -OrderBy, -OrderDirection, and -IsAccessible are forwarded to the server as query parameters. When -SubOrganizationId is supplied (Single parameter set) the cmdlet returns one sub-organization. Does not require a project context.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
<maml:alert>
<maml:para>Sub-organizations are a beta Infisical feature. The List result is an array of InfisicalSubOrganization objects; pipe into Where-Object or Select-Object to filter further. The cmdlet accepts pipeline input by property name on -SubOrganizationId.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
<command:example>
<maml:title>EXAMPLE 1</maml:title>
<dev:code>Get-InfisicalSubOrganization</dev:code>
<dev:remarks><maml:para>Lists every sub-organization the current session can see.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 2</maml:title>
<dev:code>Get-InfisicalSubOrganization -SubOrganizationId $SubOrganizationId</dev:code>
<dev:remarks><maml:para>Retrieves the canonical record for a single sub-organization by id.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 3</maml:title>
<dev:code>Get-InfisicalSubOrganization -Search 'platform' -OrderBy 'name' -OrderDirection 'asc' -Limit 25 -IsAccessible</dev:code>
<dev:remarks><maml:para>Lists up to 25 sub-organizations matching 'platform', sorted ascending by name, restricted to those the current identity has access to.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>New-InfisicalSubOrganization</command:name>
<maml:description><maml:para>Creates a new Infisical sub-organization.</maml:para></maml:description>
<command:verb>New</command:verb>
<command:noun>InfisicalSubOrganization</command:noun>
</command:details>
<maml:description>
<maml:para>Creates a sub-organization with the supplied name and slug. Both are required by the server. Honors -WhatIf and -Confirm.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
<maml:alert>
<maml:para>Slug must be unique within the parent organization. Sub-organizations are a beta Infisical feature.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
<command:example>
<maml:title>EXAMPLE 1</maml:title>
<dev:code>New-InfisicalSubOrganization -Name 'Platform Engineering' -Slug 'platform-eng'</dev:code>
<dev:remarks><maml:para>Creates a new sub-organization named 'Platform Engineering' with slug 'platform-eng'.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 2</maml:title>
<dev:code>$NewInfisicalSubOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$NewInfisicalSubOrganizationParameters.Name = 'Platform Engineering'
$NewInfisicalSubOrganizationParameters.Slug = 'platform-eng'
$NewInfisicalSubOrganizationParameters.Verbose = $True
$NewInfisicalSubOrganizationResult = New-InfisicalSubOrganization @NewInfisicalSubOrganizationParameters</dev:code>
<dev:remarks><maml:para>Splatted invocation that creates a sub-organization and logs the request via the verbose stream.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>Update-InfisicalSubOrganization</command:name>
<maml:description><maml:para>Updates mutable attributes on an existing Infisical sub-organization.</maml:para></maml:description>
<command:verb>Update</command:verb>
<command:noun>InfisicalSubOrganization</command:noun>
</command:details>
<maml:description>
<maml:para>Updates the name or slug of a sub-organization. -SubOrganizationId is required. Only bound parameters are transmitted. Honors -WhatIf and -Confirm.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
<maml:alert>
<maml:para>Sub-organizations are a beta Infisical feature; coordinate slug changes with downstream consumers that pin the slug in scripts or configuration files.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
<command:example>
<maml:title>EXAMPLE 1</maml:title>
<dev:code>Update-InfisicalSubOrganization -SubOrganizationId $SubOrganizationId -Name 'Platform (v2)'</dev:code>
<dev:remarks><maml:para>Renames the supplied sub-organization.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 2</maml:title>
<dev:code>$UpdateInfisicalSubOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$UpdateInfisicalSubOrganizationParameters.SubOrganizationId = $SubOrganizationId
$UpdateInfisicalSubOrganizationParameters.Name = 'Platform (v2)'
$UpdateInfisicalSubOrganizationParameters.Slug = 'platform-v2'
$UpdateInfisicalSubOrganizationParameters.Verbose = $True
$UpdateInfisicalSubOrganizationResult = Update-InfisicalSubOrganization @UpdateInfisicalSubOrganizationParameters</dev:code>
<dev:remarks><maml:para>Renames the sub-organization and updates its slug in a single call.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>Remove-InfisicalSubOrganization</command:name>
<maml:description><maml:para>Deletes an Infisical sub-organization.</maml:para></maml:description>
<command:verb>Remove</command:verb>
<command:noun>InfisicalSubOrganization</command:noun>
</command:details>
<maml:description>
<maml:para>Deletes a sub-organization by id. -SubOrganizationId is required. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed sub-organization id.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
<maml:alert>
<maml:para>This is destructive and removes all projects, secrets, and identities scoped to the sub-organization. Honors -WhatIf and -Confirm.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
<command:example>
<maml:title>EXAMPLE 1</maml:title>
<dev:code>Remove-InfisicalSubOrganization -SubOrganizationId $SubOrganizationId -Confirm:$False</dev:code>
<dev:remarks><maml:para>Deletes the supplied sub-organization without prompting.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 2</maml:title>
<dev:code>$RemoveInfisicalSubOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$RemoveInfisicalSubOrganizationParameters.SubOrganizationId = $SubOrganizationId
$RemoveInfisicalSubOrganizationParameters.PassThru = $True
$RemoveInfisicalSubOrganizationParameters.Confirm = $False
$RemoveInfisicalSubOrganizationParameters.Verbose = $True
$RemoveInfisicalSubOrganizationResult = Remove-InfisicalSubOrganization @RemoveInfisicalSubOrganizationParameters</dev:code>
<dev:remarks><maml:para>Removes the sub-organization without confirmation and emits the removed id for logging.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>Get-InfisicalSANList</command:name>
<maml:description><maml:para>Builds a deduplicated list of Subject Alternative Name candidates for the local device.</maml:para></maml:description>
<command:verb>Get</command:verb>
<command:noun>InfisicalSANList</command:noun>
</command:details>
<maml:description>
<maml:para>Returns, in order: the local device name; the device name suffixed with each non-empty DNS suffix found on any operational (non-loopback) network adapter and the system primary domain; every IPv4 unicast address whose first octets fall within RFC 1918 (10/8, 172.16/12, 192.168/16) or CGNAT (100.64/10); and the IPv4 and IPv6 loopback addresses (127.0.0.1, ::1). Optional -InclusionExpression and -ExclusionExpression regex filters are applied in that order after collection, before output. Suitable as a one-shot SAN provider for Request-InfisicalCertificate -DnsName.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
<maml:alert>
<maml:para>Output is a single strongly-typed System.String[] array (emitted non-enumerated) so it round-trips into [System.Collections.Generic.List[string]]::AddRange() and binds directly to string[] parameters such as Request-InfisicalCertificate -DnsName. The device name comes first so it can be reused as a CommonName. Routable public IPv4 addresses, link-local addresses, and IPv6 unicast addresses other than loopback are intentionally excluded. -InclusionExpression and -ExclusionExpression are case-insensitive .NET regular expressions; inclusion is applied first, then exclusion.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
<command:example>
<maml:title>EXAMPLE 1</maml:title>
<dev:code>Get-InfisicalSANList</dev:code>
<dev:remarks><maml:para>Returns the SAN candidate list for the current device.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 2</maml:title>
<dev:code>$Sans = Get-InfisicalSANList
Request-InfisicalCertificate -ProjectId $ProjectId -CertificateAuthorityId $CaId -CommonName $Sans[0] -DnsName $Sans -Ttl '90d'</dev:code>
<dev:remarks><maml:para>Captures the SAN list, then uses the device name as the CommonName and the full list as DnsName when requesting a certificate.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 3</maml:title>
<dev:code>$GetInfisicalSANListParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$GetInfisicalSANListParameters.InclusionExpression = '\.gracesolution\.prv$|^10\.|^172\.'
$GetInfisicalSANListParameters.ExclusionExpression = '^127\.|^::1$'
$Sans = Get-InfisicalSANList @GetInfisicalSANListParameters</dev:code>
<dev:remarks><maml:para>Keeps only entries ending in the corporate DNS suffix or sitting in the 10/8 or 172/12 ranges, then drops loopback. Filters are case-insensitive and applied in fetch -> include -> exclude -> output order.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
</helpItems>
@@ -84,6 +84,39 @@ EXAMPLES
Get-InfisicalSecrets |
Export-InfisicalSecrets -Path .\secrets.env -Format Env
ERROR HANDLING AND STREAMS
Every cmdlet derives from PSCmdlet, so the common parameters are bound:
-Verbose, -Debug, -ErrorAction, -ErrorVariable, -WarningAction,
-WarningVariable, -InformationAction, -InformationVariable, -OutVariable,
and -PipelineVariable, plus -WhatIf/-Confirm where ShouldProcess applies.
Output is separated by stream so those parameters mean what they say:
Error The failure itself, once, as a non-terminating ErrorRecord.
Warning Advisories that are not failures.
Verbose Request/response trace and the trail leading up to a failure.
Debug Low-level detail.
Operation failures are NON-TERMINATING, so -ErrorAction decides the
outcome:
Continue (default) Error is written; a pipeline keeps processing.
SilentlyContinue Nothing printed; still in $Error/-ErrorVariable.
Ignore Nothing printed and nothing recorded.
Stop Promoted to terminating; try/catch catches it.
To catch a failure you must ask for it:
try {
Request-InfisicalCertificate @Parameters -ErrorAction Stop
} catch [PSInfisicalAPI.Errors.InfisicalApiException] {
"HTTP $($_.Exception.StatusCode): $($_.Exception.ApiErrorMessage)"
}
The ErrorRecord carries the API detail, so log scraping is unnecessary:
$Error[0].Exception exposes StatusCode, ApiErrorCode, ApiErrorMessage, and
ApiRequestId on InfisicalApiException.
SECURITY NOTES
- SecureString is used for ClientSecret, AccessToken, and any secret
payloads returned by the API.
+194 -1
View File
@@ -26,7 +26,7 @@ Import-Module -Name .\Module\PSInfisicalAPI
## Cmdlets
The module exports 37 cmdlets. Discovery cmdlets (`Get-Infisical*`) use a `List` (default) / single-record parameter-set pair: invoking without the identity parameter returns the collection, supplying the identity parameter returns one record.
The module exports 51 cmdlets. Discovery cmdlets (`Get-Infisical*`) use a `List` (default) / single-record parameter-set pair: invoking without the identity parameter returns the collection, supplying the identity parameter returns one record.
### Session
@@ -47,6 +47,24 @@ The module exports 37 cmdlets. Discovery cmdlets (`Get-Infisical*`) use a `List`
| `ConvertTo-InfisicalSecretDictionary` | Converts a stream of InfisicalSecret objects into a name-keyed Dictionary of SecureString or plain text values. |
| `Export-InfisicalSecrets` | Exports InfisicalSecret objects to disk or environment variables in a chosen file format. |
### Organizations
| Cmdlet | Purpose |
| ------------------------------ | -------------------------------------------------------------------------------------------------- |
| `Get-InfisicalOrganization` | Lists or retrieves Infisical organizations accessible to the current identity. |
| `New-InfisicalOrganization` | Creates a new Infisical organization. |
| `Update-InfisicalOrganization` | Updates the name or slug of an existing Infisical organization. |
| `Remove-InfisicalOrganization` | Deletes an Infisical organization. |
### Sub-Organizations
| Cmdlet | Purpose |
| --------------------------------- | -------------------------------------------------------------------------------------------------- |
| `Get-InfisicalSubOrganization` | Lists or retrieves Infisical sub-organizations, with optional search, paging, and ordering filters. |
| `New-InfisicalSubOrganization` | Creates a new Infisical sub-organization. |
| `Update-InfisicalSubOrganization` | Updates the name or slug of an existing Infisical sub-organization. |
| `Remove-InfisicalSubOrganization` | Deletes an Infisical sub-organization. |
### Projects
| Cmdlet | Purpose |
@@ -98,6 +116,13 @@ The module exports 37 cmdlets. Discovery cmdlets (`Get-Infisical*`) use a `List`
| `Get-InfisicalScepMdmProfile` | Projects an Infisical certificate profile into a Windows SCEP MDM profile model. |
| `Export-InfisicalScepMdmProfile` | Writes a SCEP MDM profile to disk as a SyncML payload suitable for MDM delivery. |
| `Write-InfisicalScepMdmProfileToWmi`| Submits a SCEP MDM profile to the local MDM Bridge WMI provider to trigger enrollment. |
| `Get-InfisicalSANList` | Builds a SAN candidate list (device name, `<device>.<suffix>` per adapter DNS suffix, RFC 1918 + CGNAT IPv4 addresses, IPv4/IPv6 loopback) for `Request-InfisicalCertificate -DnsName`. |
### Process
| Cmdlet | Purpose |
| ------------------------ | -------------------------------------------------------------------------------------------------- |
| `Start-InfisicalProcess` | Launches a child process with Infisical secrets injected directly into its environment block, capturing stdout/stderr and validating the exit code. |
Use `Get-Help <Cmdlet> -Full` for parameter details and `Get-Help about_PSInfisicalAPI` for the module overview.
@@ -119,6 +144,174 @@ Get-InfisicalSecret -SecretPath '/'
Disconnect-Infisical
```
## End-to-end: request and install a chained certificate
Connects, selects a `cert-manager` project, sources SANs from `Get-InfisicalSANList`, requests a certificate through a 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)
$ConnectInfisicalParameters.BaseUri = 'https://app.infisical.com'
$ConnectInfisicalParameters.OrganizationId = '00000000-0000-0000-0000-000000000000'
$ConnectInfisicalParameters.ClientId = 'machine-identity-client-id'
$ConnectInfisicalParameters.ClientSecret = ConvertTo-SecureString -String 'ClientSecret' -AsPlainText -Force
$ConnectInfisicalParameters.PassThru = $True
$ConnectInfisicalParameters.Verbose = $True
$Connection = Connect-Infisical @ConnectInfisicalParameters
$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.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.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.
+14 -2
View File
@@ -113,6 +113,7 @@ function Write-Manifest {
'Copy-InfisicalSecret',
'ConvertTo-InfisicalSecretDictionary',
'Export-InfisicalSecrets',
'Import-InfisicalSecret',
'Get-InfisicalProject',
'New-InfisicalProject',
'Update-InfisicalProject',
@@ -129,6 +130,14 @@ function Write-Manifest {
'New-InfisicalTag',
'Update-InfisicalTag',
'Remove-InfisicalTag',
'Get-InfisicalOrganization',
'New-InfisicalOrganization',
'Update-InfisicalOrganization',
'Remove-InfisicalOrganization',
'Get-InfisicalSubOrganization',
'New-InfisicalSubOrganization',
'Update-InfisicalSubOrganization',
'Remove-InfisicalSubOrganization',
'Get-InfisicalCertificateAuthority',
'Get-InfisicalPkiSubscriber',
'Get-InfisicalCertificateProfile',
@@ -144,7 +153,10 @@ function Write-Manifest {
'New-InfisicalScepDynamicChallenge',
'Get-InfisicalScepMdmProfile',
'Export-InfisicalScepMdmProfile',
'Write-InfisicalScepMdmProfileToWmi'
'Write-InfisicalScepMdmProfileToWmi',
'Start-InfisicalProcess',
'Get-InfisicalEnvironmentVariable',
'Get-InfisicalSANList'
)
AliasesToExport = @()
VariablesToExport = @()
@@ -209,7 +221,7 @@ if (`$cmds.Count -eq 0) {
throw "No cmdlets were exported by the PSInfisicalAPI module."
}
`$expectedCmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecret','New-InfisicalSecret','Update-InfisicalSecret','Remove-InfisicalSecret','Copy-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder','Get-InfisicalTag','New-InfisicalTag','Update-InfisicalTag','Remove-InfisicalTag','Get-InfisicalCertificateAuthority','Get-InfisicalPkiSubscriber','Get-InfisicalCertificateProfile','Get-InfisicalCertificatePolicy','Get-InfisicalCertificate','Request-InfisicalCertificate','ConvertTo-InfisicalCertificate','Install-InfisicalCertificate','Uninstall-InfisicalCertificate','Export-InfisicalCertificate','Get-InfisicalCertificateApplication','Get-InfisicalCertificateApplicationEnrollment','New-InfisicalScepDynamicChallenge','Get-InfisicalScepMdmProfile','Export-InfisicalScepMdmProfile','Write-InfisicalScepMdmProfileToWmi')
`$expectedCmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecret','New-InfisicalSecret','Update-InfisicalSecret','Remove-InfisicalSecret','Copy-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Import-InfisicalSecret','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder','Get-InfisicalTag','New-InfisicalTag','Update-InfisicalTag','Remove-InfisicalTag','Get-InfisicalOrganization','New-InfisicalOrganization','Update-InfisicalOrganization','Remove-InfisicalOrganization','Get-InfisicalSubOrganization','New-InfisicalSubOrganization','Update-InfisicalSubOrganization','Remove-InfisicalSubOrganization','Get-InfisicalCertificateAuthority','Get-InfisicalPkiSubscriber','Get-InfisicalCertificateProfile','Get-InfisicalCertificatePolicy','Get-InfisicalCertificate','Request-InfisicalCertificate','ConvertTo-InfisicalCertificate','Install-InfisicalCertificate','Uninstall-InfisicalCertificate','Export-InfisicalCertificate','Get-InfisicalCertificateApplication','Get-InfisicalCertificateApplicationEnrollment','New-InfisicalScepDynamicChallenge','Get-InfisicalScepMdmProfile','Export-InfisicalScepMdmProfile','Write-InfisicalScepMdmProfileToWmi','Start-InfisicalProcess','Get-InfisicalEnvironmentVariable','Get-InfisicalSANList')
foreach (`$expected in `$expectedCmds) {
if (-not (Get-Command -Name `$expected -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) {
throw "Cmdlet not found: `$expected"
@@ -39,6 +39,14 @@ Get-InfisicalTag
New-InfisicalTag
Update-InfisicalTag
Remove-InfisicalTag
Get-InfisicalOrganization
New-InfisicalOrganization
Update-InfisicalOrganization
Remove-InfisicalOrganization
Get-InfisicalSubOrganization
New-InfisicalSubOrganization
Update-InfisicalSubOrganization
Remove-InfisicalSubOrganization
```
Infisicals public API is REST-based and provides programmatic access for managing secrets and related resources. Current Infisical documentation shows the list-secrets endpoint under `/api/v4/secrets`, the single-secret retrieval endpoint under `/api/v4/secrets/{secretName}`, and Universal Auth login under `/api/v1/auth/universal-auth/login`. The implementation must centralize API endpoint definitions because Infisical uses different API versions across resource families. ([Infisical Blog][1])
@@ -1213,7 +1221,9 @@ Export-InfisicalSecrets `
[-Path <FileInfo>] `
[-Scope <Process|User|Machine>] `
[-Force] `
[-Encoding <UTF8|UTF8Bom|Unicode>]
[-Encoding <UTF8|UTF8Bom|Unicode>] `
[-SecretsPrefix <string>] `
[-ForceSecretsPrefix]
```
## Parameter Rules
@@ -1490,6 +1500,121 @@ No warnings should be emitted.
---
# 16.6 Start-InfisicalProcess
Signature:
```text
Start-InfisicalProcess
-FilePath <string>
[-WorkingDirectory <DirectoryInfo>]
[-ArgumentList <string[]>]
[-AcceptableExitCodeList <string[]>]
[-WindowStyle <Normal|Hidden|Minimized|Maximized>]
[-CreateNoWindow]
[-NoWait]
[-Priority <AboveNormal|BelowNormal|High|Idle|Normal|RealTime>]
[-ExecutionTimeout <TimeSpan>]
[-ExecutionTimeoutInterval <TimeSpan>]
[-StandardInputObjectList <object[]>]
[-EnvironmentVariables <IDictionary>]
[-ParsingExpression <Regex>]
[-SecureArgumentList]
[-LogOutput]
[-ContinueOnError]
[-Secrets <InfisicalSecret[]>]
[-SecretsPrefix <string>]
[-ForceSecretsPrefix]
```
Behavior:
```text
Buffer pipeline InfisicalSecret objects in ProcessRecord.
Decrypt secrets only into ProcessStartInfo.Environment.
Apply -SecretsPrefix to each secret name before injection.
Never write secret plaintext to user or machine environment scope.
Honor -WhatIf / -Confirm.
Default -AcceptableExitCodeList = @('0','3010').
Throw a terminating error on unacceptable exit code unless -ContinueOnError is set.
```
Output: `InfisicalProcessResult` with `ExitCode`, `ExitCodeAsHex`, `ExitCodeAsInteger`, `ExitCodeAsDecimal`, `StandardOutput`, `StandardError`, `StandardOutputObject`, `StandardErrorObject`, `StartTime`, `ExitTime`, `Duration`, `DurationFriendly`, `ProcessId`, `TimedOut`, `Succeeded`, `SecretCount`.
---
# 16.7 Organization Cmdlets
Organizations are the top-level tenancy boundary in Infisical. They are not scoped under a project; the active connection's `OrganizationId` is used as the default identifier when an explicit one is not supplied.
Cmdlet signatures:
```powershell
Get-InfisicalOrganization [[-OrganizationId] <string>] # default = List
New-InfisicalOrganization [-Name] <string> [-Slug <string>] [-WhatIf] [-Confirm]
Update-InfisicalOrganization [-OrganizationId] <string> [-Name <string>] [-Slug <string>] [-WhatIf] [-Confirm]
Remove-InfisicalOrganization [-OrganizationId] <string> [-PassThru] [-WhatIf] [-Confirm]
```
Parameter sets:
| Cmdlet | Default set | Single set | Notes |
|---|---|---|---|
| `Get-InfisicalOrganization` | `List` (no `-Id`) | `Single` (`-OrganizationId`/`-Id`) | No `-ProjectId`. |
| `New-InfisicalOrganization` | n/a | `-Name` mandatory, `-Slug` optional | ShouldProcess. |
| `Update-InfisicalOrganization` | n/a | `-OrganizationId` mandatory | ShouldProcess; only bound parameters are sent. |
| `Remove-InfisicalOrganization` | n/a | `-OrganizationId` mandatory | `ConfirmImpact.High`; `-PassThru` emits removed id. |
Endpoints:
| Operation | Method | Template | Version |
|---|---|---|---|
| List | `GET` | `/api/v2/organizations` | v2 |
| Retrieve | `GET` | `/api/v1/organization/{organizationId}` | v1 |
| Create | `POST` | `/api/v2/organizations` | v2 |
| Update | `PATCH` | `/api/v1/organization/{organizationId}` | v1 |
| Delete | `DELETE` | `/api/v1/organization/{organizationId}` | v1 |
Output: `InfisicalOrganization` with `Id`, `Name`, `Slug`, `CustomerId`, `AuthEnforced`, `ScimEnabled`, `CreatedAtUtc`, `UpdatedAtUtc`.
---
# 16.8 Sub-Organization Cmdlets
Sub-organizations partition an organization into isolated child tenants. They are not scoped under a project; the active connection is used for the parent organization context.
Cmdlet signatures:
```powershell
Get-InfisicalSubOrganization [[-SubOrganizationId] <string>] [-Limit <int>] [-Offset <int>] [-Search <string>] [-OrderBy <string>] [-OrderDirection <string>] [-IsAccessible]
New-InfisicalSubOrganization [-Name] <string> [-Slug] <string> [-WhatIf] [-Confirm]
Update-InfisicalSubOrganization [-SubOrganizationId] <string> [-Name <string>] [-Slug <string>] [-WhatIf] [-Confirm]
Remove-InfisicalSubOrganization [-SubOrganizationId] <string> [-PassThru] [-WhatIf] [-Confirm]
```
Parameter sets:
| Cmdlet | Default set | Single set | Notes |
|---|---|---|---|
| `Get-InfisicalSubOrganization` | `List` (no `-Id`) | `Single` (`-SubOrganizationId`/`-Id`) | List supports server-side `-Limit`, `-Offset`, `-Search`, `-OrderBy`, `-OrderDirection`, `-IsAccessible`. |
| `New-InfisicalSubOrganization` | n/a | `-Name` + `-Slug` mandatory | ShouldProcess. |
| `Update-InfisicalSubOrganization` | n/a | `-SubOrganizationId` mandatory | ShouldProcess; only bound parameters are sent. |
| `Remove-InfisicalSubOrganization` | n/a | `-SubOrganizationId` mandatory | `ConfirmImpact.High`; `-PassThru` emits removed id. |
Endpoints (beta):
| Operation | Method | Template | Version |
|---|---|---|---|
| List | `GET` | `/api/v1/sub-organizations` | v1 |
| Retrieve | `GET` | `/api/v1/sub-organizations/{subOrgId}` | v1 |
| Create | `POST` | `/api/v1/sub-organizations` | v1 |
| Update | `PATCH` | `/api/v1/sub-organizations/{subOrgId}` | v1 |
| Delete | `DELETE` | `/api/v1/sub-organizations/{subOrgId}` | v1 |
Output: `InfisicalSubOrganization` with `Id`, `Name`, `Slug`, `OrganizationId`, `IsAccessible`, `CreatedAtUtc`, `UpdatedAtUtc`.
---
# 17. SecureString Utility
Required utility:
@@ -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()
{
@@ -75,6 +75,16 @@ namespace PSInfisicalAPI.Tests
[InlineData(InfisicalEndpointNames.BulkUpdateSecret, "PATCH", "/api/v4/secrets/batch")]
[InlineData(InfisicalEndpointNames.BulkDeleteSecret, "DELETE", "/api/v4/secrets/batch")]
[InlineData(InfisicalEndpointNames.DuplicateSecret, "POST", "/api/v4/secrets/duplicate")]
[InlineData(InfisicalEndpointNames.ListOrganizations, "GET", "/api/v2/organizations")]
[InlineData(InfisicalEndpointNames.RetrieveOrganization, "GET", "/api/v1/organization/{organizationId}")]
[InlineData(InfisicalEndpointNames.CreateOrganization, "POST", "/api/v2/organizations")]
[InlineData(InfisicalEndpointNames.UpdateOrganization, "PATCH", "/api/v1/organization/{organizationId}")]
[InlineData(InfisicalEndpointNames.DeleteOrganization, "DELETE", "/api/v1/organization/{organizationId}")]
[InlineData(InfisicalEndpointNames.ListSubOrganizations, "GET", "/api/v1/sub-organizations")]
[InlineData(InfisicalEndpointNames.RetrieveSubOrganization, "GET", "/api/v1/sub-organizations/{subOrgId}")]
[InlineData(InfisicalEndpointNames.CreateSubOrganization, "POST", "/api/v1/sub-organizations")]
[InlineData(InfisicalEndpointNames.UpdateSubOrganization, "PATCH", "/api/v1/sub-organizations/{subOrgId}")]
[InlineData(InfisicalEndpointNames.DeleteSubOrganization, "DELETE", "/api/v1/sub-organizations/{subOrgId}")]
public void Registered_Endpoints_Have_Expected_Shape(string name, string method, string template)
{
InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(name);
@@ -0,0 +1,75 @@
using System;
using PSInfisicalAPI.Process;
using Xunit;
namespace PSInfisicalAPI.Tests
{
public class InfisicalProcessRunnerHelpersTests
{
[Fact]
public void FormatFriendly_Zero_Returns_NotAvailable()
{
Assert.Equal("N/A", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.Zero));
}
[Fact]
public void FormatFriendly_Single_Unit_Plural()
{
Assert.Equal("30 seconds", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.FromSeconds(30)));
Assert.Equal("5 minutes", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.FromMinutes(5)));
Assert.Equal("250 milliseconds", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.FromMilliseconds(250)));
}
[Fact]
public void FormatFriendly_Single_Unit_Singular()
{
Assert.Equal("1 second", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.FromSeconds(1)));
Assert.Equal("1 minute", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.FromMinutes(1)));
Assert.Equal("1 hour", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.FromHours(1)));
Assert.Equal("1 day", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.FromDays(1)));
Assert.Equal("1 millisecond", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.FromMilliseconds(1)));
}
[Fact]
public void FormatFriendly_Two_Units_Uses_And_Join()
{
TimeSpan value = TimeSpan.FromSeconds(7) + TimeSpan.FromMilliseconds(364);
Assert.Equal("7 seconds, and 364 milliseconds", InfisicalProcessRunnerHelpers.FormatFriendly(value));
}
[Fact]
public void FormatFriendly_Multiple_Units_Uses_Comma_And_Trailing_And()
{
TimeSpan value = TimeSpan.FromHours(1) + TimeSpan.FromMinutes(2) + TimeSpan.FromSeconds(3) + TimeSpan.FromMilliseconds(45);
Assert.Equal("1 hour, 2 minutes, 3 seconds, and 45 milliseconds", InfisicalProcessRunnerHelpers.FormatFriendly(value));
}
[Fact]
public void FormatFriendly_Skips_Zero_Components()
{
TimeSpan value = TimeSpan.FromHours(2) + TimeSpan.FromMilliseconds(500);
Assert.Equal("2 hours, and 500 milliseconds", InfisicalProcessRunnerHelpers.FormatFriendly(value));
}
[Fact]
public void FormatFriendly_Mixed_Singular_And_Plural()
{
TimeSpan value = TimeSpan.FromMinutes(1) + TimeSpan.FromSeconds(30);
Assert.Equal("1 minute, and 30 seconds", InfisicalProcessRunnerHelpers.FormatFriendly(value));
}
[Fact]
public void FormatFriendly_Days_Component()
{
TimeSpan value = TimeSpan.FromDays(2) + TimeSpan.FromHours(3);
Assert.Equal("2 days, and 3 hours", InfisicalProcessRunnerHelpers.FormatFriendly(value));
}
[Fact]
public void FormatFriendly_SubMillisecond_Returns_NotAvailable()
{
TimeSpan value = TimeSpan.FromTicks(100);
Assert.Equal("N/A", InfisicalProcessRunnerHelpers.FormatFriendly(value));
}
}
}
@@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using PSInfisicalAPI.Logging;
using Xunit;
namespace PSInfisicalAPI.Tests
{
/// <summary>
/// The test project references PowerShellStandard.Library, which cannot host a runspace, so the logger's
/// stream choice is asserted structurally: which Cmdlet.Write* method each level compiles down to.
/// </summary>
public class LoggerStreamRoutingTests
{
[Theory]
[InlineData("Error", "WriteVerbose")]
[InlineData("Warning", "WriteWarning")]
[InlineData("Information", "WriteVerbose")]
[InlineData("Verbose", "WriteVerbose")]
[InlineData("Debug", "WriteDebug")]
public void PSCmdletLogger_Routes_Level_To_Expected_Stream(string levelMethod, string expectedWriteMethod)
{
MethodInfo method = typeof(PSCmdletLogger).GetMethod(levelMethod, BindingFlags.Public | BindingFlags.Instance);
Assert.NotNull(method);
List<string> called = GetCalledMethodNames(method);
Assert.Contains(expectedWriteMethod, called);
}
[Fact]
public void PSCmdletLogger_Error_Does_Not_Write_To_Warning_Stream()
{
// Every Logger.Error call site in this module logs and then throws, so the failure already reaches the
// caller as an ErrorRecord. Duplicating it on the warning stream put failures under -WarningAction
// instead of -ErrorAction and buried the real error under eight lines of noise.
MethodInfo error = typeof(PSCmdletLogger).GetMethod("Error", BindingFlags.Public | BindingFlags.Instance);
List<string> called = GetCalledMethodNames(error);
Assert.DoesNotContain("WriteWarning", called);
Assert.DoesNotContain("WriteError", called);
}
[Fact]
public void No_Cmdlet_Reports_Operation_Failures_As_Terminating_Errors()
{
// Operation failures go through WriteErrorForException so -ErrorAction decides the outcome.
// ThrowTerminatingForException remains available for aborts that ignore -ErrorAction, but no cmdlet
// should be using it for ordinary failures; this pins the convention against drift.
Assembly assembly = typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase).Assembly;
List<string> offenders = new List<string>();
int inspected = 0;
foreach (Type type in assembly.GetTypes())
{
if (!typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase).IsAssignableFrom(type)) { continue; }
if (type == typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase)) { continue; }
inspected++;
foreach (MethodInfo method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly))
{
if (method.GetMethodBody() == null) { continue; }
if (GetCalledMethodNames(method).Contains("ThrowTerminatingForException"))
{
offenders.Add(string.Concat(type.Name, ".", method.Name));
}
}
}
Assert.True(inspected > 40, string.Concat("Expected to inspect the cmdlet set, saw ", inspected.ToString()));
Assert.Empty(offenders);
}
[Fact]
public void Cmdlets_Route_Failures_Through_WriteErrorForException()
{
Assembly assembly = typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase).Assembly;
Type cmdletType = assembly.GetType("PSInfisicalAPI.Cmdlets.GetInfisicalCertificateAuthorityCmdlet", true);
MethodInfo processRecord = cmdletType.GetMethod("ProcessRecord", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
Assert.NotNull(processRecord);
Assert.Contains("WriteErrorForException", GetCalledMethodNames(processRecord));
}
private static List<string> GetCalledMethodNames(MethodInfo method)
{
List<string> names = new List<string>();
MethodBody body = method.GetMethodBody();
Assert.NotNull(body);
byte[] il = body.GetILAsByteArray();
Assert.NotNull(il);
const byte Call = 0x28;
const byte CallVirt = 0x6F;
for (int i = 0; i + 4 < il.Length; i++)
{
if (il[i] != Call && il[i] != CallVirt) { continue; }
int token = BitConverter.ToInt32(il, i + 1);
try
{
MethodBase resolved = method.Module.ResolveMethod(token);
if (resolved != null) { names.Add(resolved.Name); }
}
catch (ArgumentException)
{
// Byte sequence was operand data rather than an opcode; ignore.
}
}
return names;
}
}
}
@@ -0,0 +1,72 @@
using System.Reflection;
using PSInfisicalAPI.Models;
using Xunit;
namespace PSInfisicalAPI.Tests
{
public class OrganizationMapperTests
{
private static readonly System.Type MapperType = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly
.GetType("PSInfisicalAPI.Organizations.InfisicalOrganizationMapper", true);
private static readonly System.Type DtoType = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly
.GetType("PSInfisicalAPI.Organizations.InfisicalOrganizationResponseDto", true);
private static InfisicalOrganization InvokeMap(object dto)
{
MethodInfo map = MapperType.GetMethod("Map", BindingFlags.Public | BindingFlags.Static);
return (InfisicalOrganization)map.Invoke(null, new[] { dto });
}
[Fact]
public void Map_Null_Dto_Returns_Null()
{
Assert.Null(InvokeMap(null));
}
[Fact]
public void Map_Populates_Core_Fields()
{
object dto = System.Activator.CreateInstance(DtoType);
DtoType.GetProperty("Id").SetValue(dto, "org-001");
DtoType.GetProperty("Name").SetValue(dto, "Acme");
DtoType.GetProperty("Slug").SetValue(dto, "acme");
DtoType.GetProperty("CustomerId").SetValue(dto, "cust-9");
DtoType.GetProperty("AuthEnforced").SetValue(dto, true);
DtoType.GetProperty("ScimEnabled").SetValue(dto, true);
DtoType.GetProperty("CreatedAt").SetValue(dto, "2026-01-15T12:34:56Z");
DtoType.GetProperty("UpdatedAt").SetValue(dto, "2026-02-20T09:00:00Z");
InfisicalOrganization organization = InvokeMap(dto);
Assert.Equal("org-001", organization.Id);
Assert.Equal("Acme", organization.Name);
Assert.Equal("acme", organization.Slug);
Assert.Equal("cust-9", organization.CustomerId);
Assert.True(organization.AuthEnforced);
Assert.True(organization.ScimEnabled);
Assert.NotNull(organization.CreatedAtUtc);
Assert.NotNull(organization.UpdatedAtUtc);
}
[Fact]
public void Map_Falls_Back_To_InternalId()
{
object dto = System.Activator.CreateInstance(DtoType);
DtoType.GetProperty("InternalId").SetValue(dto, "internal-id-1");
InfisicalOrganization organization = InvokeMap(dto);
Assert.Equal("internal-id-1", organization.Id);
}
[Fact]
public void MapMany_Null_Returns_Empty()
{
MethodInfo mapMany = MapperType.GetMethod("MapMany", BindingFlags.Public | BindingFlags.Static);
InfisicalOrganization[] result = (InfisicalOrganization[])mapMany.Invoke(null, new object[] { null });
Assert.NotNull(result);
Assert.Empty(result);
}
}
}
@@ -0,0 +1,72 @@
using System.Reflection;
using PSInfisicalAPI.Models;
using Xunit;
namespace PSInfisicalAPI.Tests
{
public class SubOrganizationMapperTests
{
private static readonly System.Type MapperType = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly
.GetType("PSInfisicalAPI.SubOrganizations.InfisicalSubOrganizationMapper", true);
private static readonly System.Type DtoType = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly
.GetType("PSInfisicalAPI.SubOrganizations.InfisicalSubOrganizationResponseDto", true);
private static InfisicalSubOrganization InvokeMap(object dto)
{
MethodInfo map = MapperType.GetMethod("Map", BindingFlags.Public | BindingFlags.Static);
return (InfisicalSubOrganization)map.Invoke(null, new[] { dto });
}
[Fact]
public void Map_Null_Dto_Returns_Null()
{
Assert.Null(InvokeMap(null));
}
[Fact]
public void Map_Populates_Core_Fields()
{
object dto = System.Activator.CreateInstance(DtoType);
DtoType.GetProperty("Id").SetValue(dto, "sub-001");
DtoType.GetProperty("Name").SetValue(dto, "Platform Engineering");
DtoType.GetProperty("Slug").SetValue(dto, "platform-eng");
DtoType.GetProperty("OrganizationId").SetValue(dto, "org-001");
DtoType.GetProperty("IsAccessible").SetValue(dto, true);
DtoType.GetProperty("CreatedAt").SetValue(dto, "2026-01-15T12:34:56Z");
DtoType.GetProperty("UpdatedAt").SetValue(dto, "2026-02-20T09:00:00Z");
InfisicalSubOrganization subOrganization = InvokeMap(dto);
Assert.Equal("sub-001", subOrganization.Id);
Assert.Equal("Platform Engineering", subOrganization.Name);
Assert.Equal("platform-eng", subOrganization.Slug);
Assert.Equal("org-001", subOrganization.OrganizationId);
Assert.True(subOrganization.IsAccessible);
Assert.NotNull(subOrganization.CreatedAtUtc);
Assert.NotNull(subOrganization.UpdatedAtUtc);
}
[Fact]
public void Map_Falls_Back_To_InternalId_And_OrgId()
{
object dto = System.Activator.CreateInstance(DtoType);
DtoType.GetProperty("InternalId").SetValue(dto, "internal-id-1");
DtoType.GetProperty("OrgId").SetValue(dto, "org-fallback");
InfisicalSubOrganization subOrganization = InvokeMap(dto);
Assert.Equal("internal-id-1", subOrganization.Id);
Assert.Equal("org-fallback", subOrganization.OrganizationId);
}
[Fact]
public void MapMany_Null_Returns_Empty()
{
MethodInfo mapMany = MapperType.GetMethod("MapMany", BindingFlags.Public | BindingFlags.Static);
InfisicalSubOrganization[] result = (InfisicalSubOrganization[])mapMany.Invoke(null, new object[] { null });
Assert.NotNull(result);
Assert.Empty(result);
}
}
}
@@ -205,7 +205,7 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException(Component, "Connect", exception);
WriteErrorForException(Component, "Connect", exception);
}
}
@@ -50,7 +50,7 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException("ConvertToInfisicalCertificateCmdlet", "ConvertToCertificate", exception);
WriteErrorForException("ConvertToInfisicalCertificateCmdlet", "ConvertToCertificate", exception);
}
}
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Management.Automation;
using System.Security;
using PSInfisicalAPI.Common;
using PSInfisicalAPI.Errors;
using PSInfisicalAPI.Models;
@@ -21,6 +22,14 @@ namespace PSInfisicalAPI.Cmdlets
[Parameter]
public SwitchParameter AsPlainText { get; set; }
[Parameter]
[Alias("Prefix")]
public string SecretsPrefix { get; set; }
[Parameter]
[Alias("ForcePrefix")]
public SwitchParameter ForceSecretsPrefix { get; set; }
private readonly List<InfisicalSecret> _buffer = new List<InfisicalSecret>();
protected override void ProcessRecord()
@@ -40,20 +49,24 @@ namespace PSInfisicalAPI.Cmdlets
{
try
{
Logger.Information("ConvertTo-InfisicalSecretDictionary", string.Concat("Processing ", _buffer.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " input secret(s)."));
if (AsPlainText.IsPresent)
{
Dictionary<string, string> plain = BuildDictionary<string>(secret => secret.GetPlainTextValue());
Logger.Information("ConvertTo-InfisicalSecretDictionary", string.Concat("Built plain-text dictionary with ", plain.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " entry/entries."));
WriteObject(plain);
}
else
{
Dictionary<string, SecureString> secure = BuildDictionary<SecureString>(secret => secret.SecretValue);
Logger.Information("ConvertTo-InfisicalSecretDictionary", string.Concat("Built SecureString dictionary with ", secure.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " entry/entries."));
WriteObject(secure);
}
}
catch (Exception exception)
{
ThrowTerminatingForException("ConvertToInfisicalSecretDictionaryCmdlet", "ConvertToDictionary", exception);
WriteErrorForException("ConvertToInfisicalSecretDictionaryCmdlet", "ConvertToDictionary", exception);
}
}
@@ -63,7 +76,7 @@ namespace PSInfisicalAPI.Cmdlets
foreach (InfisicalSecret secret in _buffer)
{
string key = secret.SecretName ?? string.Empty;
string key = InfisicalPrefix.Apply(secret.SecretName ?? string.Empty, SecretsPrefix, ForceSecretsPrefix.IsPresent);
if (dictionary.ContainsKey(key))
{
@@ -65,7 +65,7 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException("CopyInfisicalSecretCmdlet", "DuplicateSecrets", exception);
WriteErrorForException("CopyInfisicalSecretCmdlet", "DuplicateSecrets", exception);
}
}
}
@@ -27,7 +27,7 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException("DisconnectInfisicalCmdlet", "Disconnect", exception);
WriteErrorForException("DisconnectInfisicalCmdlet", "Disconnect", exception);
}
}
}
@@ -85,7 +85,7 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException("ExportInfisicalCertificateCmdlet", "ExportCertificate", exception);
WriteErrorForException("ExportInfisicalCertificateCmdlet", "ExportCertificate", exception);
}
}
@@ -58,7 +58,7 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException(Component, "ExportScepMdmProfile", exception);
WriteErrorForException(Component, "ExportScepMdmProfile", exception);
}
}
}
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Management.Automation;
using System.Text;
using PSInfisicalAPI.Common;
using PSInfisicalAPI.Errors;
using PSInfisicalAPI.Exports;
using PSInfisicalAPI.Models;
@@ -37,6 +38,14 @@ namespace PSInfisicalAPI.Cmdlets
[Parameter]
public InfisicalExportEncoding Encoding { get; set; } = InfisicalExportEncoding.UTF8;
[Parameter]
[Alias("Prefix")]
public string SecretsPrefix { get; set; }
[Parameter]
[Alias("ForcePrefix")]
public SwitchParameter ForceSecretsPrefix { get; set; }
private readonly List<InfisicalSecret> _buffer = new List<InfisicalSecret>();
protected override void ProcessRecord()
@@ -66,9 +75,11 @@ namespace PSInfisicalAPI.Cmdlets
{
}
Logger.Information("Export-InfisicalSecrets", string.Concat("Exporting ", _buffer.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " secret(s) as ", Format.ToString(), (Path != null ? string.Concat(" to '", Path.FullName, "'") : string.Empty), "."));
InfisicalExportRequest request = new InfisicalExportRequest
{
Secrets = _buffer.ToArray(),
Secrets = ApplySecretsPrefix(_buffer, SecretsPrefix, ForceSecretsPrefix.IsPresent),
Format = Format,
Path = Path,
Scope = Scope,
@@ -81,10 +92,42 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException("ExportInfisicalSecretsCmdlet", string.Concat("Export-", Format.ToString()), exception);
WriteErrorForException("ExportInfisicalSecretsCmdlet", string.Concat("Export-", Format.ToString()), exception);
}
}
private static InfisicalSecret[] ApplySecretsPrefix(List<InfisicalSecret> source, string prefix, bool force)
{
if (string.IsNullOrEmpty(prefix)) { return source.ToArray(); }
InfisicalSecret[] result = new InfisicalSecret[source.Count];
for (int i = 0; i < source.Count; i++)
{
InfisicalSecret original = source[i];
result[i] = new InfisicalSecret
{
Id = original.Id,
InternalId = original.InternalId,
Workspace = original.Workspace,
Environment = original.Environment,
Version = original.Version,
Type = original.Type,
SecretName = InfisicalPrefix.Apply(original.SecretName, prefix, force),
SecretValue = original.SecretValue,
SecretValueHidden = original.SecretValueHidden,
SecretPath = original.SecretPath,
SecretComment = original.SecretComment,
CreatedAtUtc = original.CreatedAtUtc,
UpdatedAtUtc = original.UpdatedAtUtc,
IsRotatedSecret = original.IsRotatedSecret,
RotationId = original.RotationId,
Tags = original.Tags,
SecretMetadata = original.SecretMetadata
};
}
return result;
}
private static Encoding ResolveEncoding(InfisicalExportEncoding encoding)
{
switch (encoding)
@@ -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);
}
}
}
@@ -0,0 +1,57 @@
using System;
using System.Management.Automation;
namespace PSInfisicalAPI.Cmdlets
{
[Cmdlet(VerbsCommon.Get, "InfisicalEnvironmentVariable")]
[OutputType(typeof(string))]
public sealed class GetInfisicalEnvironmentVariableCmdlet : InfisicalCmdletBase
{
private const string Component = "Get-InfisicalEnvironmentVariable";
private static readonly EnvironmentVariableTarget[] TargetOrder = new[]
{
EnvironmentVariableTarget.Process,
EnvironmentVariableTarget.User,
EnvironmentVariableTarget.Machine
};
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
public string Name { get; set; }
[Parameter(Position = 1)]
public EnvironmentVariableTarget? Scope { get; set; }
protected override void ProcessRecord()
{
EnvironmentVariableTarget[] targets = Scope.HasValue ? new[] { Scope.Value } : TargetOrder;
foreach (EnvironmentVariableTarget target in targets)
{
Logger.Verbose(Component, string.Concat("Searching ", target.ToString(), " scope for environment variable '", Name, "'."));
string value;
try
{
value = Environment.GetEnvironmentVariable(Name, target);
}
catch (Exception exception)
{
Logger.Verbose(Component, string.Concat("Failed to read ", target.ToString(), " scope for environment variable '", Name, "': ", exception.Message));
continue;
}
if (!string.IsNullOrEmpty(value))
{
Logger.Information(Component, string.Concat("Found environment variable '", Name, "' in ", target.ToString(), " scope."));
WriteObject(value);
return;
}
}
string scopeDescription = Scope.HasValue ? string.Concat(Scope.Value.ToString(), " scope") : "Process, User, or Machine scope";
Logger.Information(Component, string.Concat("Environment variable '", Name, "' was not found in ", scopeDescription, "."));
}
}
}
@@ -37,6 +37,7 @@ namespace PSInfisicalAPI.Cmdlets
}
InfisicalFolder[] folders = client.List(connection, ProjectId, Environment, Path);
Logger.Information("Get-InfisicalFolder", string.Concat("Returned ", folders.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " folder(s) from '", Path ?? "/", "'."));
foreach (InfisicalFolder folder in folders)
{
WriteObject(folder);
@@ -44,7 +45,7 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException("GetInfisicalFolderCmdlet", "GetFolder", exception);
WriteErrorForException("GetInfisicalFolderCmdlet", "GetFolder", exception);
}
}
}
@@ -0,0 +1,48 @@
using System;
using System.Management.Automation;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Organizations;
namespace PSInfisicalAPI.Cmdlets
{
[Cmdlet(VerbsCommon.Get, "InfisicalOrganization", DefaultParameterSetName = "List")]
[OutputType(typeof(InfisicalOrganization))]
public sealed class GetInfisicalOrganizationCmdlet : InfisicalCmdletBase
{
[Parameter(ParameterSetName = "Single", Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)]
[Alias("Id")]
public string OrganizationId { get; set; }
protected override void ProcessRecord()
{
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
InfisicalOrganizationClient client = new InfisicalOrganizationClient(HttpClient, Logger);
if (string.Equals(ParameterSetName, "Single", StringComparison.Ordinal))
{
InfisicalOrganization organization = client.Retrieve(connection, OrganizationId);
if (organization != null)
{
WriteObject(organization);
}
return;
}
InfisicalOrganization[] organizations = client.List(connection);
Logger.Information("Get-InfisicalOrganization", string.Concat("Returned ", organizations.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " organization(s)."));
foreach (InfisicalOrganization organization in organizations)
{
WriteObject(organization);
}
}
catch (Exception exception)
{
WriteErrorForException("GetInfisicalOrganizationCmdlet", "GetOrganization", exception);
}
}
}
}
@@ -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);
}
}
}
@@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using System.Management.Automation;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Text.RegularExpressions;
namespace PSInfisicalAPI.Cmdlets
{
[Cmdlet(VerbsCommon.Get, "InfisicalSANList")]
[OutputType(typeof(string[]))]
public sealed class GetInfisicalSANListCmdlet : InfisicalCmdletBase
{
private const string Component = "GetInfisicalSANListCmdlet";
[Parameter]
[ValidateNotNullOrEmpty]
public string InclusionExpression { get; set; }
[Parameter]
[ValidateNotNullOrEmpty]
public string ExclusionExpression { get; set; }
protected override void ProcessRecord()
{
try
{
Regex includeRegex = !string.IsNullOrEmpty(InclusionExpression) ? new Regex(InclusionExpression, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant) : null;
Regex excludeRegex = !string.IsNullOrEmpty(ExclusionExpression) ? new Regex(ExclusionExpression, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant) : null;
List<string> sans = new List<string>();
HashSet<string> seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
string deviceName = Dns.GetHostName();
AddUnique(sans, seen, deviceName);
HashSet<string> suffixes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
string globalDomain = IPGlobalProperties.GetIPGlobalProperties().DomainName;
if (!string.IsNullOrEmpty(globalDomain)) { suffixes.Add(globalDomain); }
NetworkInterface[] adapters = NetworkInterface.GetAllNetworkInterfaces();
foreach (NetworkInterface adapter in adapters)
{
if (adapter.OperationalStatus != OperationalStatus.Up) { continue; }
if (adapter.NetworkInterfaceType == NetworkInterfaceType.Loopback) { continue; }
IPInterfaceProperties props = adapter.GetIPProperties();
if (!string.IsNullOrEmpty(props.DnsSuffix)) { suffixes.Add(props.DnsSuffix); }
foreach (UnicastIPAddressInformation unicast in props.UnicastAddresses)
{
IPAddress ip = unicast.Address;
if (ip.AddressFamily == AddressFamily.InterNetwork && IsRfc1918OrCgnat(ip))
{
AddUnique(sans, seen, ip.ToString());
}
}
}
foreach (string suffix in suffixes)
{
string trimmed = suffix.Trim().TrimStart('.');
if (!string.IsNullOrEmpty(trimmed))
{
AddUnique(sans, seen, string.Concat(deviceName, ".", trimmed));
}
}
AddUnique(sans, seen, "127.0.0.1");
AddUnique(sans, seen, "::1");
List<string> filtered = new List<string>(sans.Count);
foreach (string san in sans)
{
if (includeRegex != null && !includeRegex.IsMatch(san)) { continue; }
if (excludeRegex != null && excludeRegex.IsMatch(san)) { continue; }
filtered.Add(san);
}
WriteObject(filtered.ToArray(), false);
}
catch (Exception exception)
{
WriteErrorForException(Component, "GetSANList", exception);
}
}
private static bool IsRfc1918OrCgnat(IPAddress ip)
{
byte[] bytes = ip.GetAddressBytes();
if (bytes.Length != 4) { return false; }
if (bytes[0] == 10) { return true; }
if (bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31) { return true; }
if (bytes[0] == 192 && bytes[1] == 168) { return true; }
if (bytes[0] == 100 && bytes[1] >= 64 && bytes[1] <= 127) { return true; }
return false;
}
private static void AddUnique(List<string> list, HashSet<string> seen, string value)
{
if (string.IsNullOrEmpty(value)) { return; }
if (seen.Add(value)) { list.Add(value); }
}
}
}
@@ -86,7 +86,7 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException(Component, "GetScepMdmProfile", exception);
WriteErrorForException(Component, "GetScepMdmProfile", exception);
}
}
@@ -1,6 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Management.Automation;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Models;
@@ -57,8 +58,13 @@ namespace PSInfisicalAPI.Cmdlets
InfisicalSecret secret = client.Retrieve(connection, query);
if (secret != null)
{
Logger.Information("Get-InfisicalSecret", string.Concat("Returned 1 secret for '", SecretName, "'."));
WriteObject(secret);
}
else
{
Logger.Information("Get-InfisicalSecret", string.Concat("No secret returned for '", SecretName, "'."));
}
return;
}
@@ -79,6 +85,7 @@ namespace PSInfisicalAPI.Cmdlets
};
InfisicalSecret[] secrets = client.List(connection, listQuery);
Logger.Information("Get-InfisicalSecret", string.Concat("Returned ", secrets.Length.ToString(CultureInfo.InvariantCulture), " secret(s) from '", SecretPath ?? "/", "' (recursive=", Recursive.IsPresent ? "true" : "false", ", includeImports=", IncludeImports.IsPresent ? "true" : "false", ")."));
foreach (InfisicalSecret secret in secrets)
{
WriteObject(secret);
@@ -86,7 +93,7 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException("GetInfisicalSecretCmdlet", "GetSecret", exception);
WriteErrorForException("GetInfisicalSecretCmdlet", "GetSecret", exception);
}
}
@@ -0,0 +1,60 @@
using System;
using System.Management.Automation;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.SubOrganizations;
namespace PSInfisicalAPI.Cmdlets
{
[Cmdlet(VerbsCommon.Get, "InfisicalSubOrganization", DefaultParameterSetName = "List")]
[OutputType(typeof(InfisicalSubOrganization))]
public sealed class GetInfisicalSubOrganizationCmdlet : InfisicalCmdletBase
{
[Parameter(ParameterSetName = "Single", Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)]
[Alias("Id")]
public string SubOrganizationId { get; set; }
[Parameter(ParameterSetName = "List")] public int? Limit { get; set; }
[Parameter(ParameterSetName = "List")] public int? Offset { get; set; }
[Parameter(ParameterSetName = "List")] public string Search { get; set; }
[Parameter(ParameterSetName = "List")] public string OrderBy { get; set; }
[Parameter(ParameterSetName = "List")]
[ValidateSet("asc", "desc")]
public string OrderDirection { get; set; }
[Parameter(ParameterSetName = "List")] public SwitchParameter IsAccessible { get; set; }
protected override void ProcessRecord()
{
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
InfisicalSubOrganizationClient client = new InfisicalSubOrganizationClient(HttpClient, Logger);
if (string.Equals(ParameterSetName, "Single", StringComparison.Ordinal))
{
InfisicalSubOrganization subOrganization = client.Retrieve(connection, SubOrganizationId);
if (subOrganization != null)
{
WriteObject(subOrganization);
}
return;
}
bool? isAccessible = MyInvocation.BoundParameters.ContainsKey("IsAccessible") ? (bool?)IsAccessible.IsPresent : null;
InfisicalSubOrganization[] subOrganizations = client.List(connection, Limit, Offset, Search, OrderBy, OrderDirection, isAccessible);
Logger.Information("Get-InfisicalSubOrganization", string.Concat("Returned ", subOrganizations.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " sub-organization(s)."));
foreach (InfisicalSubOrganization subOrganization in subOrganizations)
{
WriteObject(subOrganization);
}
}
catch (Exception exception)
{
WriteErrorForException("GetInfisicalSubOrganizationCmdlet", "GetSubOrganization", exception);
}
}
}
}
@@ -35,6 +35,7 @@ namespace PSInfisicalAPI.Cmdlets
}
InfisicalTag[] tags = client.List(connection, ProjectId);
Logger.Information("Get-InfisicalTag", string.Concat("Returned ", tags.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " tag(s)."));
foreach (InfisicalTag tag in tags)
{
WriteObject(tag);
@@ -42,7 +43,7 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException("GetInfisicalTagCmdlet", "GetTag", exception);
WriteErrorForException("GetInfisicalTagCmdlet", "GetTag", exception);
}
}
}
@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Management.Automation;
using System.Security;
using PSInfisicalAPI.Common;
using PSInfisicalAPI.Errors;
using PSInfisicalAPI.Imports;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Security;
namespace PSInfisicalAPI.Cmdlets
{
[Cmdlet(VerbsData.Import, "InfisicalSecret")]
[OutputType(typeof(Dictionary<string, SecureString>))]
[OutputType(typeof(Dictionary<string, string>))]
public sealed class ImportInfisicalSecretCmdlet : InfisicalCmdletBase
{
[Parameter(Mandatory = true, Position = 0)]
[ValidateNotNull]
public FileInfo Path { get; set; }
[Parameter(Mandatory = true)]
public InfisicalImportFormat Format { get; set; }
[Parameter]
public InfisicalDuplicateKeyBehavior DuplicateKeyBehavior { get; set; } = InfisicalDuplicateKeyBehavior.Error;
[Parameter]
public SwitchParameter AsPlainText { get; set; }
[Parameter]
[Alias("Prefix")]
public string SecretsPrefix { get; set; }
[Parameter]
[Alias("ForcePrefix")]
public SwitchParameter ForceSecretsPrefix { get; set; }
protected override void EndProcessing()
{
try
{
Path.Refresh();
if (!Path.Exists)
{
throw new InfisicalImportException(string.Concat("Import path does not exist: ", Path.FullName));
}
IInfisicalImporter importer = InfisicalImporterFactory.Create(Format);
IList<KeyValuePair<string, string>> pairs = importer.Import(Path);
Logger.Information("Import-InfisicalSecret", string.Concat("Parsed ", pairs.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " secret pair(s) from '", Path.FullName, "' (format=", Format.ToString(), ")."));
if (AsPlainText.IsPresent)
{
Dictionary<string, string> plain = BuildDictionary<string>(pairs, value => value ?? string.Empty);
Logger.Information("Import-InfisicalSecret", string.Concat("Built plain-text dictionary with ", plain.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " entry/entries."));
WriteObject(plain);
}
else
{
Dictionary<string, SecureString> secure = BuildDictionary<SecureString>(pairs, value => SecureStringUtility.ToReadOnlySecureString(value ?? string.Empty));
Logger.Information("Import-InfisicalSecret", string.Concat("Built SecureString dictionary with ", secure.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " entry/entries."));
WriteObject(secure);
}
}
catch (Exception exception)
{
WriteErrorForException("ImportInfisicalSecretCmdlet", "ImportSecret", exception);
}
}
private Dictionary<string, TValue> BuildDictionary<TValue>(
IList<KeyValuePair<string, string>> pairs,
Func<string, TValue> valueSelector)
{
Dictionary<string, TValue> dictionary = new Dictionary<string, TValue>(StringComparer.OrdinalIgnoreCase);
foreach (KeyValuePair<string, string> pair in pairs)
{
if (pair.Key == null) { continue; }
string key = InfisicalPrefix.Apply(pair.Key, SecretsPrefix, ForceSecretsPrefix.IsPresent);
if (dictionary.ContainsKey(key))
{
if (DuplicateKeyBehavior == InfisicalDuplicateKeyBehavior.Error)
{
throw new InfisicalConfigurationException(string.Concat("Duplicate secret name encountered: ", key));
}
if (DuplicateKeyBehavior == InfisicalDuplicateKeyBehavior.LastWins)
{
dictionary[key] = valueSelector(pair.Value);
}
continue;
}
dictionary[key] = valueSelector(pair.Value);
}
return dictionary;
}
}
}
@@ -1,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);
}
}
}
@@ -0,0 +1,39 @@
using System;
using System.Management.Automation;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Organizations;
namespace PSInfisicalAPI.Cmdlets
{
[Cmdlet(VerbsCommon.New, "InfisicalOrganization", SupportsShouldProcess = true)]
[OutputType(typeof(InfisicalOrganization))]
public sealed class NewInfisicalOrganizationCmdlet : InfisicalCmdletBase
{
[Parameter(Mandatory = true, Position = 0)] public string Name { get; set; }
[Parameter] public string Slug { get; set; }
protected override void ProcessRecord()
{
try
{
if (!ShouldProcess(Name, "Create Infisical organization"))
{
return;
}
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
InfisicalOrganizationClient client = new InfisicalOrganizationClient(HttpClient, Logger);
InfisicalOrganization organization = client.Create(connection, Name, Slug);
if (organization != null)
{
WriteObject(organization);
}
}
catch (Exception exception)
{
WriteErrorForException("NewInfisicalOrganizationCmdlet", "CreateOrganization", exception);
}
}
}
}
@@ -40,7 +40,7 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException("NewInfisicalProjectCmdlet", "CreateProject", exception);
WriteErrorForException("NewInfisicalProjectCmdlet", "CreateProject", exception);
}
}
}
@@ -42,7 +42,7 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException("NewInfisicalScepDynamicChallengeCmdlet", "GenerateScepDynamicChallenge", exception);
WriteErrorForException("NewInfisicalScepDynamicChallengeCmdlet", "GenerateScepDynamicChallenge", exception);
}
}
}
@@ -58,10 +58,12 @@ namespace PSInfisicalAPI.Cmdlets
Secrets = InfisicalBulkSecretConverter.ToCreateItems(Secrets)
};
Logger.Information("New-InfisicalSecret", string.Concat("Bulk-creating ", Secrets.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " secret(s)."));
InfisicalSecretsClient bulkClient = new InfisicalSecretsClient(HttpClient, Logger);
InfisicalSecret[] created = bulkClient.CreateBatch(connection, bulk);
if (created != null)
{
Logger.Information("New-InfisicalSecret", string.Concat("Server returned ", created.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " created secret(s)."));
foreach (InfisicalSecret secret in created) { WriteObject(secret); }
}
@@ -97,7 +99,7 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException("NewInfisicalSecretCmdlet", "CreateSecret", exception);
WriteErrorForException("NewInfisicalSecretCmdlet", "CreateSecret", exception);
}
}
}
@@ -0,0 +1,39 @@
using System;
using System.Management.Automation;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.SubOrganizations;
namespace PSInfisicalAPI.Cmdlets
{
[Cmdlet(VerbsCommon.New, "InfisicalSubOrganization", SupportsShouldProcess = true)]
[OutputType(typeof(InfisicalSubOrganization))]
public sealed class NewInfisicalSubOrganizationCmdlet : InfisicalCmdletBase
{
[Parameter(Mandatory = true, Position = 0)] public string Name { get; set; }
[Parameter(Mandatory = true, Position = 1)] public string Slug { get; set; }
protected override void ProcessRecord()
{
try
{
if (!ShouldProcess(Name, "Create Infisical sub-organization"))
{
return;
}
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
InfisicalSubOrganizationClient client = new InfisicalSubOrganizationClient(HttpClient, Logger);
InfisicalSubOrganization subOrganization = client.Create(connection, Name, Slug);
if (subOrganization != null)
{
WriteObject(subOrganization);
}
}
catch (Exception exception)
{
WriteErrorForException("NewInfisicalSubOrganizationCmdlet", "CreateSubOrganization", exception);
}
}
}
}
@@ -34,7 +34,7 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException("NewInfisicalTagCmdlet", "CreateTag", exception);
WriteErrorForException("NewInfisicalTagCmdlet", "CreateTag", exception);
}
}
}
@@ -35,7 +35,7 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException("RemoveInfisicalEnvironmentCmdlet", "DeleteEnvironment", exception);
WriteErrorForException("RemoveInfisicalEnvironmentCmdlet", "DeleteEnvironment", exception);
}
}
}
@@ -37,7 +37,7 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException("RemoveInfisicalFolderCmdlet", "DeleteFolder", exception);
WriteErrorForException("RemoveInfisicalFolderCmdlet", "DeleteFolder", exception);
}
}
}
@@ -0,0 +1,42 @@
using System;
using System.Management.Automation;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Organizations;
namespace PSInfisicalAPI.Cmdlets
{
[Cmdlet(VerbsCommon.Remove, "InfisicalOrganization", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
public sealed class RemoveInfisicalOrganizationCmdlet : InfisicalCmdletBase
{
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)]
[Alias("Id")]
public string OrganizationId { get; set; }
[Parameter] public SwitchParameter PassThru { get; set; }
protected override void ProcessRecord()
{
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
if (!ShouldProcess(OrganizationId, "Remove Infisical organization"))
{
return;
}
InfisicalOrganizationClient client = new InfisicalOrganizationClient(HttpClient, Logger);
client.Delete(connection, OrganizationId);
if (PassThru.IsPresent)
{
WriteObject(OrganizationId);
}
}
catch (Exception exception)
{
WriteErrorForException("RemoveInfisicalOrganizationCmdlet", "DeleteOrganization", exception);
}
}
}
}
@@ -35,7 +35,7 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException("RemoveInfisicalProjectCmdlet", "DeleteProject", exception);
WriteErrorForException("RemoveInfisicalProjectCmdlet", "DeleteProject", exception);
}
}
}
@@ -47,6 +47,7 @@ namespace PSInfisicalAPI.Cmdlets
SecretNames = SecretNames
};
Logger.Information("Remove-InfisicalSecret", string.Concat("Bulk-removing ", SecretNames.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " secret(s)."));
client.DeleteBatch(connection, bulk);
if (PassThru.IsPresent)
@@ -78,7 +79,7 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException("RemoveInfisicalSecretCmdlet", "DeleteSecret", exception);
WriteErrorForException("RemoveInfisicalSecretCmdlet", "DeleteSecret", exception);
}
}
}
@@ -0,0 +1,42 @@
using System;
using System.Management.Automation;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.SubOrganizations;
namespace PSInfisicalAPI.Cmdlets
{
[Cmdlet(VerbsCommon.Remove, "InfisicalSubOrganization", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
public sealed class RemoveInfisicalSubOrganizationCmdlet : InfisicalCmdletBase
{
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)]
[Alias("Id")]
public string SubOrganizationId { get; set; }
[Parameter] public SwitchParameter PassThru { get; set; }
protected override void ProcessRecord()
{
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
if (!ShouldProcess(SubOrganizationId, "Remove Infisical sub-organization"))
{
return;
}
InfisicalSubOrganizationClient client = new InfisicalSubOrganizationClient(HttpClient, Logger);
client.Delete(connection, SubOrganizationId);
if (PassThru.IsPresent)
{
WriteObject(SubOrganizationId);
}
}
catch (Exception exception)
{
WriteErrorForException("RemoveInfisicalSubOrganizationCmdlet", "DeleteSubOrganization", exception);
}
}
}
}
@@ -35,7 +35,7 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException("RemoveInfisicalTagCmdlet", "DeleteTag", exception);
WriteErrorForException("RemoveInfisicalTagCmdlet", "DeleteTag", exception);
}
}
}
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Management.Automation;
using System.Security.Cryptography.X509Certificates;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Errors;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Pki;
@@ -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
};
}
}
}
@@ -0,0 +1,168 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Management.Automation;
using System.Text.RegularExpressions;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Process;
namespace PSInfisicalAPI.Cmdlets
{
[Cmdlet(VerbsLifecycle.Start, "InfisicalProcess", DefaultParameterSetName = WindowStyleSet, SupportsShouldProcess = true)]
[OutputType(typeof(InfisicalProcessResult))]
public sealed class StartInfisicalProcessCmdlet : InfisicalCmdletBase
{
private const string Component = "StartInfisicalProcessCmdlet";
private const string WindowStyleSet = "WindowStyle";
private const string CreateNoWindowSet = "CreateNoWindow";
[Parameter(Mandatory = true, Position = 0)]
[ValidateNotNullOrEmpty]
[Alias("FP")]
public string FilePath { get; set; }
[Parameter]
[Alias("WD")]
public DirectoryInfo WorkingDirectory { get; set; }
[Parameter]
[AllowEmptyCollection]
[AllowNull]
[Alias("AL")]
public string[] ArgumentList { get; set; }
[Parameter]
[AllowEmptyCollection]
[AllowNull]
[Alias("AECL")]
public string[] AcceptableExitCodeList { get; set; }
[Parameter(ParameterSetName = WindowStyleSet)]
[ValidateSet("Normal", "Hidden", "Minimized", "Maximized")]
[Alias("WS")]
public string WindowStyle { get; set; } = "Hidden";
[Parameter(ParameterSetName = CreateNoWindowSet)]
[Alias("CNW")]
public SwitchParameter CreateNoWindow { get; set; }
[Parameter]
[Alias("NW")]
public SwitchParameter NoWait { get; set; }
[Parameter]
[ValidateSet("AboveNormal", "BelowNormal", "High", "Idle", "Normal", "RealTime")]
[Alias("P")]
public string Priority { get; set; } = "Normal";
[Parameter]
[Alias("ET")]
public TimeSpan ExecutionTimeout { get; set; }
[Parameter]
[Alias("ETI")]
public TimeSpan ExecutionTimeoutInterval { get; set; } = TimeSpan.FromSeconds(15);
[Parameter]
[Alias("SIO")]
public object[] StandardInputObjectList { get; set; }
[Parameter]
[Alias("ENV")]
public IDictionary EnvironmentVariables { get; set; }
[Parameter]
[Alias("StandardOutputParsingExpression", "SOPE", "PE")]
public Regex ParsingExpression { get; set; }
[Parameter]
[Alias("SAL")]
public SwitchParameter SecureArgumentList { get; set; }
[Parameter]
[Alias("LO")]
public SwitchParameter LogOutput { get; set; }
[Parameter]
[Alias("COE")]
public SwitchParameter ContinueOnError { get; set; }
[Parameter(ValueFromPipeline = true)]
[Alias("Secret", "InputObject")]
public InfisicalSecret[] Secrets { get; set; }
[Parameter]
[Alias("Prefix")]
public string SecretsPrefix { get; set; }
[Parameter]
[Alias("ForcePrefix")]
public SwitchParameter ForceSecretsPrefix { get; set; }
private readonly List<InfisicalSecret> _secretBuffer = new List<InfisicalSecret>();
protected override void ProcessRecord()
{
if (Secrets == null) { return; }
foreach (InfisicalSecret secret in Secrets)
{
if (secret != null) { _secretBuffer.Add(secret); }
}
}
protected override void EndProcessing()
{
try
{
string target = string.IsNullOrEmpty(WorkingDirectory != null ? WorkingDirectory.FullName : null)
? FilePath
: string.Concat(FilePath, " (in ", WorkingDirectory.FullName, ")");
if (!ShouldProcess(target, "Start process with Infisical secrets")) { return; }
int envVarCount = EnvironmentVariables != null ? EnvironmentVariables.Count : 0;
Logger.Information("Start-InfisicalProcess", string.Concat("Injecting ", _secretBuffer.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " secret(s) and ", envVarCount.ToString(System.Globalization.CultureInfo.InvariantCulture), " explicit environment variable(s) into process environment."));
InfisicalProcessOptions options = new InfisicalProcessOptions
{
FilePath = FilePath,
WorkingDirectory = WorkingDirectory,
ArgumentList = ArgumentList,
AcceptableExitCodeList = AcceptableExitCodeList,
WindowStyle = WindowStyle,
CreateNoWindow = CreateNoWindow.IsPresent,
NoWait = NoWait.IsPresent,
Priority = Priority,
ExecutionTimeout = MyInvocation.BoundParameters.ContainsKey("ExecutionTimeout") ? (TimeSpan?)ExecutionTimeout : null,
ExecutionTimeoutInterval = ExecutionTimeoutInterval,
StandardInputObjectList = StandardInputObjectList,
EnvironmentVariables = EnvironmentVariables,
ParsingExpression = ParsingExpression,
SecureArgumentList = SecureArgumentList.IsPresent,
LogOutput = LogOutput.IsPresent,
ContinueOnError = ContinueOnError.IsPresent,
Secrets = _secretBuffer.ToArray(),
SecretsPrefix = SecretsPrefix,
ForceSecretsPrefix = ForceSecretsPrefix.IsPresent
};
InfisicalProcessResult result = InfisicalProcessRunner.Run(options, Logger);
WriteObject(result);
if (!result.Succeeded && !NoWait.IsPresent && !ContinueOnError.IsPresent)
{
string message = string.Concat("Process '", FilePath, "' exited with code ", result.ExitCode.HasValue ? result.ExitCode.Value.ToString() : "<null>", " which is not in the acceptable exit code list.");
InvalidOperationException exception = new InvalidOperationException(message);
ErrorRecord error = new ErrorRecord(exception, "StartInfisicalProcess.UnacceptableExitCode", ErrorCategory.InvalidResult, result);
WriteError(error);
}
}
catch (PipelineStoppedException) { throw; }
catch (Exception exception)
{
WriteErrorForException(Component, "StartProcess", exception);
}
}
}
}
@@ -74,7 +74,7 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException("UninstallInfisicalCertificateCmdlet", "UninstallCertificate", exception);
WriteErrorForException("UninstallInfisicalCertificateCmdlet", "UninstallCertificate", exception);
}
}
@@ -38,7 +38,7 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException("UpdateInfisicalEnvironmentCmdlet", "UpdateEnvironment", exception);
WriteErrorForException("UpdateInfisicalEnvironmentCmdlet", "UpdateEnvironment", exception);
}
}
}
@@ -38,7 +38,7 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException("UpdateInfisicalFolderCmdlet", "UpdateFolder", exception);
WriteErrorForException("UpdateInfisicalFolderCmdlet", "UpdateFolder", exception);
}
}
}
@@ -0,0 +1,44 @@
using System;
using System.Management.Automation;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Organizations;
namespace PSInfisicalAPI.Cmdlets
{
[Cmdlet(VerbsData.Update, "InfisicalOrganization", SupportsShouldProcess = true)]
[OutputType(typeof(InfisicalOrganization))]
public sealed class UpdateInfisicalOrganizationCmdlet : InfisicalCmdletBase
{
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)]
[Alias("Id")]
public string OrganizationId { get; set; }
[Parameter] public string Name { get; set; }
[Parameter] public string Slug { get; set; }
protected override void ProcessRecord()
{
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
if (!ShouldProcess(OrganizationId, "Update Infisical organization"))
{
return;
}
InfisicalOrganizationClient client = new InfisicalOrganizationClient(HttpClient, Logger);
InfisicalOrganization organization = client.Update(connection, OrganizationId, Name, Slug);
if (organization != null)
{
WriteObject(organization);
}
}
catch (Exception exception)
{
WriteErrorForException("UpdateInfisicalOrganizationCmdlet", "UpdateOrganization", exception);
}
}
}
}
@@ -38,7 +38,7 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException("UpdateInfisicalProjectCmdlet", "UpdateProject", exception);
WriteErrorForException("UpdateInfisicalProjectCmdlet", "UpdateProject", exception);
}
}
}
@@ -56,10 +56,12 @@ namespace PSInfisicalAPI.Cmdlets
Secrets = InfisicalBulkSecretConverter.ToUpdateItems(Secrets)
};
Logger.Information("Update-InfisicalSecret", string.Concat("Bulk-updating ", Secrets.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " secret(s)."));
InfisicalSecretsClient bulkClient = new InfisicalSecretsClient(HttpClient, Logger);
InfisicalSecret[] updated = bulkClient.UpdateBatch(connection, bulk);
if (updated != null)
{
Logger.Information("Update-InfisicalSecret", string.Concat("Server returned ", updated.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " updated secret(s)."));
foreach (InfisicalSecret secret in updated) { WriteObject(secret); }
}
@@ -96,7 +98,7 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException("UpdateInfisicalSecretCmdlet", "UpdateSecret", exception);
WriteErrorForException("UpdateInfisicalSecretCmdlet", "UpdateSecret", exception);
}
}
}
@@ -0,0 +1,44 @@
using System;
using System.Management.Automation;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.SubOrganizations;
namespace PSInfisicalAPI.Cmdlets
{
[Cmdlet(VerbsData.Update, "InfisicalSubOrganization", SupportsShouldProcess = true)]
[OutputType(typeof(InfisicalSubOrganization))]
public sealed class UpdateInfisicalSubOrganizationCmdlet : InfisicalCmdletBase
{
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)]
[Alias("Id")]
public string SubOrganizationId { get; set; }
[Parameter] public string Name { get; set; }
[Parameter] public string Slug { get; set; }
protected override void ProcessRecord()
{
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
if (!ShouldProcess(SubOrganizationId, "Update Infisical sub-organization"))
{
return;
}
InfisicalSubOrganizationClient client = new InfisicalSubOrganizationClient(HttpClient, Logger);
InfisicalSubOrganization subOrganization = client.Update(connection, SubOrganizationId, Name, Slug);
if (subOrganization != null)
{
WriteObject(subOrganization);
}
}
catch (Exception exception)
{
WriteErrorForException("UpdateInfisicalSubOrganizationCmdlet", "UpdateSubOrganization", exception);
}
}
}
}
@@ -38,7 +38,7 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException("UpdateInfisicalTagCmdlet", "UpdateTag", exception);
WriteErrorForException("UpdateInfisicalTagCmdlet", "UpdateTag", exception);
}
}
}
@@ -62,7 +62,7 @@ namespace PSInfisicalAPI.Cmdlets
}
catch (Exception exception)
{
ThrowTerminatingForException(Component, "WriteScepMdmProfileToWmi", exception);
WriteErrorForException(Component, "WriteScepMdmProfileToWmi", exception);
}
}
@@ -0,0 +1,15 @@
using System;
namespace PSInfisicalAPI.Common
{
public static class InfisicalPrefix
{
public static string Apply(string original, string prefix, bool force)
{
if (string.IsNullOrEmpty(prefix)) { return original ?? string.Empty; }
if (original == null) { return prefix; }
if (!force && original.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { return original; }
return string.Concat(prefix, original);
}
}
}
@@ -44,6 +44,18 @@ namespace PSInfisicalAPI.Endpoints
public const string UpdateTag = "UpdateTag";
public const string DeleteTag = "DeleteTag";
public const string ListOrganizations = "ListOrganizations";
public const string RetrieveOrganization = "RetrieveOrganization";
public const string CreateOrganization = "CreateOrganization";
public const string UpdateOrganization = "UpdateOrganization";
public const string DeleteOrganization = "DeleteOrganization";
public const string ListSubOrganizations = "ListSubOrganizations";
public const string RetrieveSubOrganization = "RetrieveSubOrganization";
public const string CreateSubOrganization = "CreateSubOrganization";
public const string UpdateSubOrganization = "UpdateSubOrganization";
public const string DeleteSubOrganization = "DeleteSubOrganization";
public const string ListInternalCertificateAuthorities = "ListInternalCertificateAuthorities";
public const string RetrieveInternalCertificateAuthority = "RetrieveInternalCertificateAuthority";
public const string SearchCertificates = "SearchCertificates";
@@ -16,6 +16,8 @@ namespace PSInfisicalAPI.Endpoints
RegisterEnvironments(Candidates);
RegisterFolders(Candidates);
RegisterTags(Candidates);
RegisterOrganizations(Candidates);
RegisterSubOrganizations(Candidates);
RegisterPki(Candidates);
}
@@ -496,6 +498,112 @@ namespace PSInfisicalAPI.Endpoints
});
}
private static void RegisterOrganizations(Dictionary<string, List<InfisicalEndpointDefinition>> map)
{
Add(map, new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.ListOrganizations,
Resource = "Organizations",
Version = "v2",
Method = "GET",
Template = "/api/v2/organizations",
RequiresAuthorization = true
});
Add(map, new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.RetrieveOrganization,
Resource = "Organizations",
Version = "v1",
Method = "GET",
Template = "/api/v1/organization/{organizationId}",
RequiresAuthorization = true
});
Add(map, new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.CreateOrganization,
Resource = "Organizations",
Version = "v2",
Method = "POST",
Template = "/api/v2/organizations",
RequiresAuthorization = true
});
Add(map, new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.UpdateOrganization,
Resource = "Organizations",
Version = "v1",
Method = "PATCH",
Template = "/api/v1/organization/{organizationId}",
RequiresAuthorization = true
});
Add(map, new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.DeleteOrganization,
Resource = "Organizations",
Version = "v1",
Method = "DELETE",
Template = "/api/v1/organization/{organizationId}",
RequiresAuthorization = true
});
}
private static void RegisterSubOrganizations(Dictionary<string, List<InfisicalEndpointDefinition>> map)
{
Add(map, new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.ListSubOrganizations,
Resource = "SubOrganizations",
Version = "v1",
Method = "GET",
Template = "/api/v1/sub-organizations",
RequiresAuthorization = true
});
Add(map, new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.RetrieveSubOrganization,
Resource = "SubOrganizations",
Version = "v1",
Method = "GET",
Template = "/api/v1/sub-organizations/{subOrgId}",
RequiresAuthorization = true
});
Add(map, new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.CreateSubOrganization,
Resource = "SubOrganizations",
Version = "v1",
Method = "POST",
Template = "/api/v1/sub-organizations",
RequiresAuthorization = true
});
Add(map, new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.UpdateSubOrganization,
Resource = "SubOrganizations",
Version = "v1",
Method = "PATCH",
Template = "/api/v1/sub-organizations/{subOrgId}",
RequiresAuthorization = true
});
Add(map, new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.DeleteSubOrganization,
Resource = "SubOrganizations",
Version = "v1",
Method = "DELETE",
Template = "/api/v1/sub-organizations/{subOrgId}",
RequiresAuthorization = true
});
}
private static void RegisterPki(Dictionary<string, List<InfisicalEndpointDefinition>> map)
{
Add(map, new InfisicalEndpointDefinition
@@ -75,6 +75,13 @@ namespace PSInfisicalAPI.Errors
public InfisicalExportException(string message, Exception innerException) : base(message, innerException) { }
}
public class InfisicalImportException : InfisicalException
{
public InfisicalImportException() { }
public InfisicalImportException(string message) : base(message) { }
public InfisicalImportException(string message, Exception innerException) : base(message, innerException) { }
}
public class InfisicalConfigurationException : InfisicalException
{
public InfisicalConfigurationException() { }
@@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.IO;
using PSInfisicalAPI.Errors;
namespace PSInfisicalAPI.Imports
{
public sealed class EnvInfisicalImporter : IInfisicalImporter
{
public IList<KeyValuePair<string, string>> Import(FileInfo path)
{
if (path == null) { throw new InfisicalImportException("Path is required for ENV import."); }
List<KeyValuePair<string, string>> result = new List<KeyValuePair<string, string>>();
string[] lines = File.ReadAllLines(path.FullName);
foreach (string raw in lines)
{
if (raw == null) { continue; }
string line = raw.Trim();
if (line.Length == 0) { continue; }
if (line[0] == '#') { continue; }
int idx = line.IndexOf('=');
if (idx <= 0) { continue; }
string key = line.Substring(0, idx).Trim();
string value = line.Substring(idx + 1);
if (key.Length == 0) { continue; }
result.Add(new KeyValuePair<string, string>(key, value));
}
return result;
}
}
}
@@ -0,0 +1,10 @@
using System.Collections.Generic;
using System.IO;
namespace PSInfisicalAPI.Imports
{
public interface IInfisicalImporter
{
IList<KeyValuePair<string, string>> Import(FileInfo path);
}
}
@@ -0,0 +1,20 @@
using PSInfisicalAPI.Errors;
using PSInfisicalAPI.Models;
namespace PSInfisicalAPI.Imports
{
public static class InfisicalImporterFactory
{
public static IInfisicalImporter Create(InfisicalImportFormat format)
{
switch (format)
{
case InfisicalImportFormat.Json: return new JsonInfisicalImporter();
case InfisicalImportFormat.Yaml: return new YamlInfisicalImporter();
case InfisicalImportFormat.Env: return new EnvInfisicalImporter();
case InfisicalImportFormat.Xml: return new XmlInfisicalImporter();
default: throw new InfisicalImportException(string.Concat("Unsupported import format: ", format.ToString()));
}
}
}
}
@@ -0,0 +1,53 @@
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json.Linq;
using PSInfisicalAPI.Errors;
namespace PSInfisicalAPI.Imports
{
public sealed class JsonInfisicalImporter : IInfisicalImporter
{
public IList<KeyValuePair<string, string>> Import(FileInfo path)
{
if (path == null) { throw new InfisicalImportException("Path is required for JSON import."); }
List<KeyValuePair<string, string>> result = new List<KeyValuePair<string, string>>();
string text = File.ReadAllText(path.FullName);
JToken root = JToken.Parse(text);
if (root.Type == JTokenType.Array)
{
foreach (JToken item in (JArray)root)
{
if (item == null || item.Type != JTokenType.Object) { continue; }
string key = ReadString((JObject)item, "SecretName") ?? ReadString((JObject)item, "secretName");
string value = ReadString((JObject)item, "SecretValue") ?? ReadString((JObject)item, "secretValue");
if (string.IsNullOrEmpty(key)) { continue; }
result.Add(new KeyValuePair<string, string>(key, value ?? string.Empty));
}
}
else if (root.Type == JTokenType.Object)
{
foreach (JProperty prop in ((JObject)root).Properties())
{
if (prop == null || string.IsNullOrEmpty(prop.Name)) { continue; }
string value = prop.Value != null && prop.Value.Type != JTokenType.Null ? prop.Value.ToString() : string.Empty;
result.Add(new KeyValuePair<string, string>(prop.Name, value));
}
}
else
{
throw new InfisicalImportException("JSON import expects an array of secret objects or a flat key/value object.");
}
return result;
}
private static string ReadString(JObject obj, string name)
{
JToken token;
if (!obj.TryGetValue(name, out token) || token == null || token.Type == JTokenType.Null) { return null; }
return token.ToString();
}
}
}
@@ -0,0 +1,51 @@
using System.Collections.Generic;
using System.IO;
using System.Xml;
using PSInfisicalAPI.Errors;
namespace PSInfisicalAPI.Imports
{
public sealed class XmlInfisicalImporter : IInfisicalImporter
{
public IList<KeyValuePair<string, string>> Import(FileInfo path)
{
if (path == null) { throw new InfisicalImportException("Path is required for XML import."); }
List<KeyValuePair<string, string>> result = new List<KeyValuePair<string, string>>();
XmlDocument document = new XmlDocument();
document.Load(path.FullName);
XmlNode root = document.DocumentElement;
if (root == null || !string.Equals(root.LocalName, "Secrets", System.StringComparison.Ordinal))
{
throw new InfisicalImportException("XML import expects a root <Secrets> element.");
}
foreach (XmlNode node in root.ChildNodes)
{
if (node == null || node.NodeType != XmlNodeType.Element) { continue; }
if (!string.Equals(node.LocalName, "Secret", System.StringComparison.Ordinal)) { continue; }
string key = ReadChild(node, "SecretName");
string value = ReadChild(node, "SecretValue");
if (string.IsNullOrEmpty(key)) { continue; }
result.Add(new KeyValuePair<string, string>(key, value ?? string.Empty));
}
return result;
}
private static string ReadChild(XmlNode parent, string name)
{
foreach (XmlNode child in parent.ChildNodes)
{
if (child == null || child.NodeType != XmlNodeType.Element) { continue; }
if (string.Equals(child.LocalName, name, System.StringComparison.Ordinal))
{
return child.InnerText;
}
}
return null;
}
}
}
@@ -0,0 +1,61 @@
using System.Collections;
using System.Collections.Generic;
using System.IO;
using PSInfisicalAPI.Errors;
using YamlDotNet.Serialization;
namespace PSInfisicalAPI.Imports
{
public sealed class YamlInfisicalImporter : IInfisicalImporter
{
public IList<KeyValuePair<string, string>> Import(FileInfo path)
{
if (path == null) { throw new InfisicalImportException("Path is required for YAML import."); }
List<KeyValuePair<string, string>> result = new List<KeyValuePair<string, string>>();
string text = File.ReadAllText(path.FullName);
IDeserializer deserializer = new DeserializerBuilder().Build();
object root = deserializer.Deserialize<object>(text);
if (root == null) { return result; }
IDictionary rootMap = root as IDictionary;
if (rootMap != null && rootMap.Contains("Secrets"))
{
IList entries = rootMap["Secrets"] as IList;
if (entries != null)
{
foreach (object entry in entries)
{
IDictionary map = entry as IDictionary;
if (map == null) { continue; }
string key = AsString(map["SecretName"]);
string value = AsString(map["SecretValue"]);
if (string.IsNullOrEmpty(key)) { continue; }
result.Add(new KeyValuePair<string, string>(key, value ?? string.Empty));
}
return result;
}
}
if (rootMap != null)
{
foreach (DictionaryEntry kvp in rootMap)
{
string key = AsString(kvp.Key);
if (string.IsNullOrEmpty(key)) { continue; }
result.Add(new KeyValuePair<string, string>(key, AsString(kvp.Value) ?? string.Empty));
}
return result;
}
throw new InfisicalImportException("YAML import expects a 'Secrets' root list or a flat key/value mapping.");
}
private static string AsString(object value)
{
if (value == null) { return null; }
return value.ToString();
}
}
}
+8 -1
View File
@@ -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);
}
}
}
@@ -0,0 +1,10 @@
namespace PSInfisicalAPI.Models
{
public enum InfisicalImportFormat
{
Json,
Yaml,
Env,
Xml
}
}
@@ -0,0 +1,21 @@
using System;
namespace PSInfisicalAPI.Models
{
public sealed class InfisicalOrganization
{
public string Id { get; set; }
public string Name { get; set; }
public string Slug { get; set; }
public string CustomerId { get; set; }
public bool AuthEnforced { get; set; }
public bool ScimEnabled { get; set; }
public DateTimeOffset? CreatedAtUtc { get; set; }
public DateTimeOffset? UpdatedAtUtc { get; set; }
public override string ToString()
{
return string.IsNullOrEmpty(Slug) ? (Name ?? Id) : Slug;
}
}
}
@@ -0,0 +1,26 @@
using System;
using System.Text.RegularExpressions;
namespace PSInfisicalAPI.Models
{
public sealed class InfisicalProcessResult
{
public string FilePath { get; set; }
public int? ProcessId { get; set; }
public int? ExitCode { get; set; }
public string ExitCodeAsHex { get; set; }
public int? ExitCodeAsInteger { get; set; }
public string ExitCodeAsDecimal { get; set; }
public DateTime? StartTime { get; set; }
public DateTime? ExitTime { get; set; }
public TimeSpan? Duration { get; set; }
public string DurationFriendly { get; set; }
public string StandardOutput { get; set; }
public string StandardError { get; set; }
public Match[] StandardOutputObject { get; set; }
public Match[] StandardErrorObject { get; set; }
public bool TimedOut { get; set; }
public bool Succeeded { get; set; }
public int SecretCount { get; set; }
}
}
@@ -0,0 +1,20 @@
using System;
namespace PSInfisicalAPI.Models
{
public sealed class InfisicalSubOrganization
{
public string Id { get; set; }
public string Name { get; set; }
public string Slug { get; set; }
public string OrganizationId { get; set; }
public bool IsAccessible { get; set; }
public DateTimeOffset? CreatedAtUtc { get; set; }
public DateTimeOffset? UpdatedAtUtc { get; set; }
public override string ToString()
{
return string.IsNullOrEmpty(Slug) ? (Name ?? Id) : Slug;
}
}
}
@@ -0,0 +1,150 @@
using System;
using System.Collections.Generic;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Endpoints;
using PSInfisicalAPI.Errors;
using PSInfisicalAPI.Http;
using PSInfisicalAPI.Logging;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Serialization;
namespace PSInfisicalAPI.Organizations
{
public sealed class InfisicalOrganizationClient
{
private const string Component = "OrganizationClient";
private readonly IInfisicalLogger _logger;
private readonly JsonInfisicalSerializer _serializer;
private readonly InfisicalApiInvoker _invoker;
public InfisicalOrganizationClient(IInfisicalHttpClient httpClient, IInfisicalLogger logger)
{
if (httpClient == null) { throw new ArgumentNullException(nameof(httpClient)); }
_logger = logger ?? NullInfisicalLogger.Instance;
_serializer = new JsonInfisicalSerializer();
_invoker = new InfisicalApiInvoker(httpClient);
}
public InfisicalOrganization[] List(InfisicalConnection connection)
{
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
try
{
_logger.Information(Component, "Attempting to list Infisical organizations. Please Wait...");
InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.ListOrganizations, "ListOrganizations", null, null, null);
InfisicalOrganizationListResponseDto dto = _serializer.Deserialize<InfisicalOrganizationListResponseDto>(response.Body);
response.Clear();
InfisicalOrganization[] mapped = InfisicalOrganizationMapper.MapMany(dto != null ? dto.Organizations : null);
_logger.Information(Component, "Infisical organization list retrieval was successful.");
return mapped;
}
catch (Exception)
{
_logger.Error(Component, "Infisical organization list retrieval failed.");
throw;
}
}
public InfisicalOrganization Retrieve(InfisicalConnection connection, string organizationId)
{
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
if (string.IsNullOrEmpty(organizationId)) { throw new InfisicalConfigurationException("OrganizationId is required."); }
Dictionary<string, string> pathParameters = new Dictionary<string, string> { { "organizationId", organizationId } };
try
{
_logger.Information(Component, string.Concat("Attempting to retrieve Infisical organization '", organizationId, "'. Please Wait..."));
InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.RetrieveOrganization, "RetrieveOrganization", pathParameters, null, null);
InfisicalOrganizationSingleResponseDto dto = _serializer.Deserialize<InfisicalOrganizationSingleResponseDto>(response.Body);
response.Clear();
InfisicalOrganization mapped = InfisicalOrganizationMapper.Map(dto != null ? dto.Organization : null);
_logger.Information(Component, "Infisical organization retrieval was successful.");
return mapped;
}
catch (Exception)
{
_logger.Error(Component, "Infisical organization retrieval failed.");
throw;
}
}
public InfisicalOrganization Create(InfisicalConnection connection, string name, string slug)
{
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
if (string.IsNullOrEmpty(name)) { throw new InfisicalConfigurationException("Name is required."); }
InfisicalOrganizationCreateRequestDto request = new InfisicalOrganizationCreateRequestDto { Name = name, Slug = slug };
string body = _serializer.Serialize(request);
try
{
_logger.Information(Component, string.Concat("Attempting to create Infisical organization '", name, "'. Please Wait..."));
InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.CreateOrganization, "CreateOrganization", null, null, body);
InfisicalOrganizationSingleResponseDto dto = _serializer.Deserialize<InfisicalOrganizationSingleResponseDto>(response.Body);
response.Clear();
InfisicalOrganization mapped = InfisicalOrganizationMapper.Map(dto != null ? dto.Organization : null);
_logger.Information(Component, "Infisical organization creation was successful.");
return mapped;
}
catch (Exception)
{
_logger.Error(Component, "Infisical organization creation failed.");
throw;
}
}
public InfisicalOrganization Update(InfisicalConnection connection, string organizationId, string name, string slug)
{
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
if (string.IsNullOrEmpty(organizationId)) { throw new InfisicalConfigurationException("OrganizationId is required."); }
Dictionary<string, string> pathParameters = new Dictionary<string, string> { { "organizationId", organizationId } };
InfisicalOrganizationUpdateRequestDto request = new InfisicalOrganizationUpdateRequestDto { Name = name, Slug = slug };
string body = _serializer.Serialize(request);
try
{
_logger.Information(Component, string.Concat("Attempting to update Infisical organization '", organizationId, "'. Please Wait..."));
InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.UpdateOrganization, "UpdateOrganization", pathParameters, null, body);
InfisicalOrganizationSingleResponseDto dto = _serializer.Deserialize<InfisicalOrganizationSingleResponseDto>(response.Body);
response.Clear();
InfisicalOrganization mapped = InfisicalOrganizationMapper.Map(dto != null ? dto.Organization : null);
_logger.Information(Component, "Infisical organization update was successful.");
return mapped;
}
catch (Exception)
{
_logger.Error(Component, "Infisical organization update failed.");
throw;
}
}
public void Delete(InfisicalConnection connection, string organizationId)
{
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
if (string.IsNullOrEmpty(organizationId)) { throw new InfisicalConfigurationException("OrganizationId is required."); }
Dictionary<string, string> pathParameters = new Dictionary<string, string> { { "organizationId", organizationId } };
try
{
_logger.Information(Component, string.Concat("Attempting to delete Infisical organization '", organizationId, "'. Please Wait..."));
InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.DeleteOrganization, "DeleteOrganization", pathParameters, null, null);
response.Clear();
_logger.Information(Component, "Infisical organization deletion was successful.");
}
catch (Exception)
{
_logger.Error(Component, "Infisical organization deletion failed.");
throw;
}
}
}
}
@@ -0,0 +1,40 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace PSInfisicalAPI.Organizations
{
internal sealed class InfisicalOrganizationResponseDto
{
[JsonProperty("id")] public string Id { get; set; }
[JsonProperty("_id")] public string InternalId { get; set; }
[JsonProperty("name")] public string Name { get; set; }
[JsonProperty("slug")] public string Slug { get; set; }
[JsonProperty("customerId")] public string CustomerId { get; set; }
[JsonProperty("authEnforced")] public bool AuthEnforced { get; set; }
[JsonProperty("scimEnabled")] public bool ScimEnabled { get; set; }
[JsonProperty("createdAt")] public string CreatedAt { get; set; }
[JsonProperty("updatedAt")] public string UpdatedAt { get; set; }
}
internal sealed class InfisicalOrganizationListResponseDto
{
[JsonProperty("organizations")] public List<InfisicalOrganizationResponseDto> Organizations { get; set; }
}
internal sealed class InfisicalOrganizationSingleResponseDto
{
[JsonProperty("organization")] public InfisicalOrganizationResponseDto Organization { get; set; }
}
internal sealed class InfisicalOrganizationCreateRequestDto
{
[JsonProperty("name")] public string Name { get; set; }
[JsonProperty("slug", NullValueHandling = NullValueHandling.Ignore)] public string Slug { get; set; }
}
internal sealed class InfisicalOrganizationUpdateRequestDto
{
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; set; }
[JsonProperty("slug", NullValueHandling = NullValueHandling.Ignore)] public string Slug { get; set; }
}
}
@@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using PSInfisicalAPI.Models;
namespace PSInfisicalAPI.Organizations
{
internal static class InfisicalOrganizationMapper
{
public static InfisicalOrganization Map(InfisicalOrganizationResponseDto dto)
{
if (dto == null)
{
return null;
}
return new InfisicalOrganization
{
Id = !string.IsNullOrEmpty(dto.Id) ? dto.Id : dto.InternalId,
Name = dto.Name,
Slug = dto.Slug,
CustomerId = dto.CustomerId,
AuthEnforced = dto.AuthEnforced,
ScimEnabled = dto.ScimEnabled,
CreatedAtUtc = ParseTimestamp(dto.CreatedAt),
UpdatedAtUtc = ParseTimestamp(dto.UpdatedAt)
};
}
public static InfisicalOrganization[] MapMany(IEnumerable<InfisicalOrganizationResponseDto> items)
{
if (items == null)
{
return Array.Empty<InfisicalOrganization>();
}
List<InfisicalOrganization> results = new List<InfisicalOrganization>();
foreach (InfisicalOrganizationResponseDto dto in items)
{
InfisicalOrganization mapped = Map(dto);
if (mapped != null)
{
results.Add(mapped);
}
}
return results.ToArray();
}
private static DateTimeOffset? ParseTimestamp(string value)
{
if (string.IsNullOrEmpty(value))
{
return null;
}
DateTimeOffset parsed;
if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out parsed))
{
return parsed;
}
return null;
}
}
}
@@ -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
@@ -0,0 +1,31 @@
using System;
using System.Collections;
using System.IO;
using System.Text.RegularExpressions;
using PSInfisicalAPI.Models;
namespace PSInfisicalAPI.Process
{
public sealed class InfisicalProcessOptions
{
public string FilePath { get; set; }
public DirectoryInfo WorkingDirectory { get; set; }
public string[] ArgumentList { get; set; }
public string[] AcceptableExitCodeList { get; set; }
public string WindowStyle { get; set; }
public bool CreateNoWindow { get; set; }
public bool NoWait { get; set; }
public string Priority { get; set; }
public TimeSpan? ExecutionTimeout { get; set; }
public TimeSpan ExecutionTimeoutInterval { get; set; }
public object[] StandardInputObjectList { get; set; }
public IDictionary EnvironmentVariables { get; set; }
public Regex ParsingExpression { get; set; }
public bool SecureArgumentList { get; set; }
public bool LogOutput { get; set; }
public bool ContinueOnError { get; set; }
public InfisicalSecret[] Secrets { get; set; }
public string SecretsPrefix { get; set; }
public bool ForceSecretsPrefix { get; set; }
}
}
@@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using PSInfisicalAPI.Logging;
using PSInfisicalAPI.Models;
using SystemProcess = System.Diagnostics.Process;
using static PSInfisicalAPI.Process.InfisicalProcessRunnerHelpers;
namespace PSInfisicalAPI.Process
{
public static class InfisicalProcessRunner
{
private const string Component = "InfisicalProcessRunner";
public static InfisicalProcessResult Run(InfisicalProcessOptions options, IInfisicalLogger logger)
{
if (options == null) { throw new ArgumentNullException(nameof(options)); }
if (string.IsNullOrWhiteSpace(options.FilePath)) { throw new ArgumentException("FilePath is required.", nameof(options)); }
string[] acceptable = (options.AcceptableExitCodeList != null && options.AcceptableExitCodeList.Length > 0)
? options.AcceptableExitCodeList
: new[] { "0", "3010" };
InfisicalProcessResult result = new InfisicalProcessResult
{
FilePath = options.FilePath,
ExitCode = -1,
SecretCount = options.Secrets != null ? options.Secrets.Length : 0
};
SystemProcess process = new SystemProcess();
process.StartInfo.FileName = options.FilePath;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.RedirectStandardInput = true;
if (options.CreateNoWindow) { process.StartInfo.CreateNoWindow = true; }
else if (!string.IsNullOrWhiteSpace(options.WindowStyle))
{
process.StartInfo.WindowStyle = (ProcessWindowStyle)Enum.Parse(typeof(ProcessWindowStyle), options.WindowStyle, true);
}
if (options.WorkingDirectory != null && !string.IsNullOrWhiteSpace(options.WorkingDirectory.FullName))
{
if (!Directory.Exists(options.WorkingDirectory.FullName))
{
Directory.CreateDirectory(options.WorkingDirectory.FullName);
}
process.StartInfo.WorkingDirectory = options.WorkingDirectory.FullName;
}
ApplyEnvironment(process.StartInfo.Environment, options, logger);
if (options.ArgumentList != null && options.ArgumentList.Length > 0)
{
process.StartInfo.Arguments = string.Join(" ", options.ArgumentList);
}
LogCommand(process.StartInfo, options, logger);
LogVerbose(logger, string.Concat("Acceptable exit codes: ", string.Join("; ", acceptable)));
Stopwatch timer = Stopwatch.StartNew();
process.Start();
result.ProcessId = process.Id;
try { result.StartTime = process.StartTime; } catch { }
ApplyPriority(process, options.Priority, logger);
StringBuilder stdoutBuffer = new StringBuilder();
StringBuilder stderrBuffer = new StringBuilder();
object stdoutGate = new object();
object stderrGate = new object();
process.OutputDataReceived += (sender, args) => { if (args.Data != null) { lock (stdoutGate) { stdoutBuffer.AppendLine(args.Data); } } };
process.ErrorDataReceived += (sender, args) => { if (args.Data != null) { lock (stderrGate) { stderrBuffer.AppendLine(args.Data); } } };
process.BeginOutputReadLine();
process.BeginErrorReadLine();
WriteStandardInput(process, options.StandardInputObjectList, options.SecureArgumentList, logger);
if (options.NoWait)
{
LogVerbose(logger, string.Concat("Skipping wait for process ID ", process.Id, "."));
return result;
}
WaitForExit(process, options.ExecutionTimeout, options.ExecutionTimeoutInterval, timer, result, logger);
try { process.WaitForExit(); } catch { }
lock (stdoutGate) { result.StandardOutput = stdoutBuffer.Length > 0 ? stdoutBuffer.ToString() : null; }
lock (stderrGate) { result.StandardError = stderrBuffer.Length > 0 ? stderrBuffer.ToString() : null; }
try { result.ExitCode = process.ExitCode; } catch { result.ExitCode = null; }
FormatExitCodes(result);
try { result.ExitTime = process.ExitTime; } catch { }
if (result.StartTime.HasValue && result.ExitTime.HasValue) { result.Duration = result.ExitTime.Value - result.StartTime.Value; }
if (result.Duration.HasValue)
{
result.DurationFriendly = FormatFriendly(result.Duration.Value);
LogVerbose(logger, string.Concat("The command execution took ", result.DurationFriendly, "."));
}
ApplyRegex(result, options.ParsingExpression);
result.Succeeded = IsAcceptable(result, acceptable);
try { process.Dispose(); } catch { }
if (options.LogOutput || !result.Succeeded)
{
LogVerbose(logger, string.Concat("StandardOutput: ", string.IsNullOrEmpty(result.StandardOutput) ? "N/A" : result.StandardOutput));
LogVerbose(logger, string.Concat("StandardError: ", string.IsNullOrEmpty(result.StandardError) ? "N/A" : result.StandardError));
}
return result;
}
private static bool IsAcceptable(InfisicalProcessResult result, string[] acceptable)
{
if (acceptable.Any(c => string.Equals(c, "*", StringComparison.Ordinal))) { return true; }
HashSet<string> set = new HashSet<string>(acceptable, StringComparer.OrdinalIgnoreCase);
if (result.ExitCode.HasValue && set.Contains(result.ExitCode.Value.ToString(CultureInfo.InvariantCulture))) { return true; }
if (!string.IsNullOrEmpty(result.ExitCodeAsHex) && set.Contains(result.ExitCodeAsHex)) { return true; }
if (result.ExitCodeAsInteger.HasValue && set.Contains(result.ExitCodeAsInteger.Value.ToString(CultureInfo.InvariantCulture))) { return true; }
if (!string.IsNullOrEmpty(result.ExitCodeAsDecimal) && set.Contains(result.ExitCodeAsDecimal)) { return true; }
return false;
}
private static void FormatExitCodes(InfisicalProcessResult result)
{
if (!result.ExitCode.HasValue) { return; }
try
{
result.ExitCodeAsHex = "0x" + Convert.ToString(result.ExitCode.Value, 16).PadLeft(8, '0').ToUpperInvariant();
int parsed;
if (int.TryParse(result.ExitCodeAsHex.Substring(2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out parsed))
{
result.ExitCodeAsInteger = parsed;
}
result.ExitCodeAsDecimal = result.ExitCode.Value.ToString(CultureInfo.InvariantCulture);
}
catch { }
}
private static void LogVerbose(IInfisicalLogger logger, string message)
{
if (logger != null) { logger.Verbose(Component, message); }
}
}
}
@@ -0,0 +1,202 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using PSInfisicalAPI.Common;
using PSInfisicalAPI.Logging;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Security;
using SystemProcess = System.Diagnostics.Process;
namespace PSInfisicalAPI.Process
{
internal static class InfisicalProcessRunnerHelpers
{
private const string Component = "InfisicalProcessRunner";
internal static void ApplyEnvironment(IDictionary<string, string> processEnv, InfisicalProcessOptions options, IInfisicalLogger logger)
{
if (processEnv == null) { return; }
if (options.EnvironmentVariables != null && options.EnvironmentVariables.Count > 0)
{
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; }
string key = entry.Key.ToString();
string value = entry.Value != null ? entry.Value.ToString() : string.Empty;
processEnv[key] = value;
}
}
if (options.Secrets == null || options.Secrets.Length == 0) { return; }
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.SecretsPrefix, options.ForceSecretsPrefix);
SecureStringUtility.UsePlainText(secret.SecretValue, plain =>
{
processEnv[name] = plain;
return true;
});
}
}
internal static void LogCommand(ProcessStartInfo startInfo, InfisicalProcessOptions options, IInfisicalLogger logger)
{
StringBuilder builder = new StringBuilder();
builder.Append("Attempting to execute: ");
builder.Append(startInfo.FileName);
if (!string.IsNullOrEmpty(startInfo.Arguments))
{
builder.Append(' ');
if (options.SecureArgumentList)
{
int len = Math.Min(20, Math.Max(5, startInfo.Arguments.Length));
builder.Append(new string('*', len));
}
else
{
builder.Append(startInfo.Arguments);
}
}
Log(logger, builder.ToString());
}
internal static void ApplyPriority(SystemProcess process, string priority, IInfisicalLogger logger)
{
if (string.IsNullOrWhiteSpace(priority)) { return; }
ProcessPriorityClass desired;
if (!Enum.TryParse(priority, true, out desired)) { return; }
try
{
if (process.PriorityClass != desired)
{
process.PriorityClass = desired;
Log(logger, string.Concat("Set process priority class to '", desired, "' for process ID ", process.Id, "."));
}
}
catch (Exception exception)
{
Log(logger, string.Concat("Unable to set process priority class to '", desired, "': ", exception.Message));
}
}
internal static void WriteStandardInput(SystemProcess process, object[] inputs, bool secureArguments, IInfisicalLogger logger)
{
if (inputs == null || inputs.Length == 0) { return; }
for (int i = 0; i < inputs.Length; i++)
{
try
{
object value = inputs[i];
string preview = secureArguments ? new string('*', 8) : (value != null ? value.ToString() : string.Empty);
Log(logger, string.Concat("Writing standard input object ", i + 1, " of ", inputs.Length, " to process ID ", process.Id, ": ", preview));
process.StandardInput.WriteLine(value);
}
catch (Exception exception)
{
Log(logger, string.Concat("Failed to write standard input object ", i + 1, ": ", exception.Message));
}
}
}
internal static void WaitForExit(SystemProcess process, TimeSpan? timeout, TimeSpan interval, Stopwatch timer, InfisicalProcessResult result, IInfisicalLogger logger)
{
TimeSpan pollInterval = interval.TotalMilliseconds > 0 ? interval : TimeSpan.FromSeconds(15);
int processId = process.Id;
if (!timeout.HasValue)
{
Log(logger, string.Concat("A timeout was not specified for process ID ", processId, "."));
Log(logger, string.Concat("The wait for process ID ", processId, " termination will be indefinite."));
}
else
{
Log(logger, string.Concat("Process timeout duration: ", FormatFriendly(timeout.Value)));
}
while (!GetProcessHasExited(processId))
{
Log(logger, string.Concat("Process ID ", processId, " has been running for ", FormatFriendly(timer.Elapsed), "."));
if (timeout.HasValue && timer.Elapsed >= timeout.Value)
{
Log(logger, string.Concat("Process ID ", processId, " exceeded the maximum timeout duration of ", FormatFriendly(timeout.Value), "; terminating."));
try { process.Kill(); result.TimedOut = true; } catch { }
break;
}
Log(logger, string.Concat("Checking again in another ", FormatFriendly(pollInterval), ". Please wait..."));
System.Threading.Thread.Sleep(pollInterval);
}
}
private static bool GetProcessHasExited(int processId)
{
try { return SystemProcess.GetProcessById(processId).HasExited; }
catch { return true; }
}
internal static string FormatFriendly(TimeSpan value)
{
string[] names = new[] { "Days", "Hours", "Minutes", "Seconds", "Milliseconds" };
int[] values = new[] { value.Days, value.Hours, value.Minutes, value.Seconds, value.Milliseconds };
List<int> nonZeroIndices = new List<int>();
for (int i = 0; i < values.Length; i++) { if (values[i] > 0) { nonZeroIndices.Add(i); } }
if (nonZeroIndices.Count == 0) { return "N/A"; }
StringBuilder builder = new StringBuilder();
int last = nonZeroIndices.Count - 1;
for (int i = 0; i < nonZeroIndices.Count; i++)
{
int index = nonZeroIndices[i];
int amount = values[index];
string name = names[index].ToLowerInvariant();
if (amount == 1) { name = name.TrimEnd('s'); }
if (nonZeroIndices.Count > 1 && i == last) { builder.Append("and "); }
builder.Append(amount);
builder.Append(' ');
builder.Append(name);
if (nonZeroIndices.Count > 1 && i != last) { builder.Append(", "); }
}
return builder.ToString();
}
internal static void ApplyRegex(InfisicalProcessResult result, Regex expression)
{
if (expression == null) { return; }
if (!string.IsNullOrEmpty(result.StandardOutput))
{
result.StandardOutputObject = expression.Matches(result.StandardOutput).Cast<Match>().ToArray();
}
if (!string.IsNullOrEmpty(result.StandardError))
{
result.StandardErrorObject = expression.Matches(result.StandardError).Cast<Match>().ToArray();
}
}
private static void Log(IInfisicalLogger logger, string message)
{
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,
@@ -0,0 +1,160 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Endpoints;
using PSInfisicalAPI.Errors;
using PSInfisicalAPI.Http;
using PSInfisicalAPI.Logging;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Serialization;
namespace PSInfisicalAPI.SubOrganizations
{
public sealed class InfisicalSubOrganizationClient
{
private const string Component = "SubOrganizationClient";
private readonly IInfisicalLogger _logger;
private readonly JsonInfisicalSerializer _serializer;
private readonly InfisicalApiInvoker _invoker;
public InfisicalSubOrganizationClient(IInfisicalHttpClient httpClient, IInfisicalLogger logger)
{
if (httpClient == null) { throw new ArgumentNullException(nameof(httpClient)); }
_logger = logger ?? NullInfisicalLogger.Instance;
_serializer = new JsonInfisicalSerializer();
_invoker = new InfisicalApiInvoker(httpClient);
}
public InfisicalSubOrganization[] List(InfisicalConnection connection, int? limit, int? offset, string search, string orderBy, string orderDirection, bool? isAccessible)
{
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
List<KeyValuePair<string, string>> queryParameters = new List<KeyValuePair<string, string>>();
if (limit.HasValue) { queryParameters.Add(new KeyValuePair<string, string>("limit", limit.Value.ToString(CultureInfo.InvariantCulture))); }
if (offset.HasValue) { queryParameters.Add(new KeyValuePair<string, string>("offset", offset.Value.ToString(CultureInfo.InvariantCulture))); }
if (!string.IsNullOrEmpty(search)) { queryParameters.Add(new KeyValuePair<string, string>("search", search)); }
if (!string.IsNullOrEmpty(orderBy)) { queryParameters.Add(new KeyValuePair<string, string>("orderBy", orderBy)); }
if (!string.IsNullOrEmpty(orderDirection)) { queryParameters.Add(new KeyValuePair<string, string>("orderDirection", orderDirection)); }
if (isAccessible.HasValue) { queryParameters.Add(new KeyValuePair<string, string>("isAccessible", isAccessible.Value ? "true" : "false")); }
try
{
_logger.Information(Component, "Attempting to list Infisical sub-organizations. Please Wait...");
InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.ListSubOrganizations, "ListSubOrganizations", null, queryParameters, null);
InfisicalSubOrganizationListResponseDto dto = _serializer.Deserialize<InfisicalSubOrganizationListResponseDto>(response.Body);
response.Clear();
InfisicalSubOrganization[] mapped = InfisicalSubOrganizationMapper.MapMany(dto != null ? dto.SubOrganizations : null);
_logger.Information(Component, "Infisical sub-organization list retrieval was successful.");
return mapped;
}
catch (Exception)
{
_logger.Error(Component, "Infisical sub-organization list retrieval failed.");
throw;
}
}
public InfisicalSubOrganization Retrieve(InfisicalConnection connection, string subOrganizationId)
{
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
if (string.IsNullOrEmpty(subOrganizationId)) { throw new InfisicalConfigurationException("SubOrganizationId is required."); }
Dictionary<string, string> pathParameters = new Dictionary<string, string> { { "subOrgId", subOrganizationId } };
try
{
_logger.Information(Component, string.Concat("Attempting to retrieve Infisical sub-organization '", subOrganizationId, "'. Please Wait..."));
InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.RetrieveSubOrganization, "RetrieveSubOrganization", pathParameters, null, null);
InfisicalSubOrganizationSingleResponseDto dto = _serializer.Deserialize<InfisicalSubOrganizationSingleResponseDto>(response.Body);
response.Clear();
InfisicalSubOrganization mapped = InfisicalSubOrganizationMapper.Map(dto != null ? dto.SubOrganization : null);
_logger.Information(Component, "Infisical sub-organization retrieval was successful.");
return mapped;
}
catch (Exception)
{
_logger.Error(Component, "Infisical sub-organization retrieval failed.");
throw;
}
}
public InfisicalSubOrganization Create(InfisicalConnection connection, string name, string slug)
{
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
if (string.IsNullOrEmpty(name)) { throw new InfisicalConfigurationException("Name is required."); }
if (string.IsNullOrEmpty(slug)) { throw new InfisicalConfigurationException("Slug is required."); }
InfisicalSubOrganizationCreateRequestDto request = new InfisicalSubOrganizationCreateRequestDto { Name = name, Slug = slug };
string body = _serializer.Serialize(request);
try
{
_logger.Information(Component, string.Concat("Attempting to create Infisical sub-organization '", name, "'. Please Wait..."));
InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.CreateSubOrganization, "CreateSubOrganization", null, null, body);
InfisicalSubOrganizationSingleResponseDto dto = _serializer.Deserialize<InfisicalSubOrganizationSingleResponseDto>(response.Body);
response.Clear();
InfisicalSubOrganization mapped = InfisicalSubOrganizationMapper.Map(dto != null ? dto.SubOrganization : null);
_logger.Information(Component, "Infisical sub-organization creation was successful.");
return mapped;
}
catch (Exception)
{
_logger.Error(Component, "Infisical sub-organization creation failed.");
throw;
}
}
public InfisicalSubOrganization Update(InfisicalConnection connection, string subOrganizationId, string name, string slug)
{
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
if (string.IsNullOrEmpty(subOrganizationId)) { throw new InfisicalConfigurationException("SubOrganizationId is required."); }
Dictionary<string, string> pathParameters = new Dictionary<string, string> { { "subOrgId", subOrganizationId } };
InfisicalSubOrganizationUpdateRequestDto request = new InfisicalSubOrganizationUpdateRequestDto { Name = name, Slug = slug };
string body = _serializer.Serialize(request);
try
{
_logger.Information(Component, string.Concat("Attempting to update Infisical sub-organization '", subOrganizationId, "'. Please Wait..."));
InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.UpdateSubOrganization, "UpdateSubOrganization", pathParameters, null, body);
InfisicalSubOrganizationSingleResponseDto dto = _serializer.Deserialize<InfisicalSubOrganizationSingleResponseDto>(response.Body);
response.Clear();
InfisicalSubOrganization mapped = InfisicalSubOrganizationMapper.Map(dto != null ? dto.SubOrganization : null);
_logger.Information(Component, "Infisical sub-organization update was successful.");
return mapped;
}
catch (Exception)
{
_logger.Error(Component, "Infisical sub-organization update failed.");
throw;
}
}
public void Delete(InfisicalConnection connection, string subOrganizationId)
{
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
if (string.IsNullOrEmpty(subOrganizationId)) { throw new InfisicalConfigurationException("SubOrganizationId is required."); }
Dictionary<string, string> pathParameters = new Dictionary<string, string> { { "subOrgId", subOrganizationId } };
try
{
_logger.Information(Component, string.Concat("Attempting to delete Infisical sub-organization '", subOrganizationId, "'. Please Wait..."));
InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.DeleteSubOrganization, "DeleteSubOrganization", pathParameters, null, null);
response.Clear();
_logger.Information(Component, "Infisical sub-organization deletion was successful.");
}
catch (Exception)
{
_logger.Error(Component, "Infisical sub-organization deletion failed.");
throw;
}
}
}
}
@@ -0,0 +1,40 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace PSInfisicalAPI.SubOrganizations
{
internal sealed class InfisicalSubOrganizationResponseDto
{
[JsonProperty("id")] public string Id { get; set; }
[JsonProperty("_id")] public string InternalId { get; set; }
[JsonProperty("name")] public string Name { get; set; }
[JsonProperty("slug")] public string Slug { get; set; }
[JsonProperty("organizationId")] public string OrganizationId { get; set; }
[JsonProperty("orgId")] public string OrgId { get; set; }
[JsonProperty("isAccessible")] public bool IsAccessible { get; set; }
[JsonProperty("createdAt")] public string CreatedAt { get; set; }
[JsonProperty("updatedAt")] public string UpdatedAt { get; set; }
}
internal sealed class InfisicalSubOrganizationListResponseDto
{
[JsonProperty("subOrganizations")] public List<InfisicalSubOrganizationResponseDto> SubOrganizations { get; set; }
}
internal sealed class InfisicalSubOrganizationSingleResponseDto
{
[JsonProperty("subOrganization")] public InfisicalSubOrganizationResponseDto SubOrganization { get; set; }
}
internal sealed class InfisicalSubOrganizationCreateRequestDto
{
[JsonProperty("name")] public string Name { get; set; }
[JsonProperty("slug")] public string Slug { get; set; }
}
internal sealed class InfisicalSubOrganizationUpdateRequestDto
{
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; set; }
[JsonProperty("slug", NullValueHandling = NullValueHandling.Ignore)] public string Slug { get; set; }
}
}
@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using PSInfisicalAPI.Models;
namespace PSInfisicalAPI.SubOrganizations
{
internal static class InfisicalSubOrganizationMapper
{
public static InfisicalSubOrganization Map(InfisicalSubOrganizationResponseDto dto)
{
if (dto == null)
{
return null;
}
return new InfisicalSubOrganization
{
Id = !string.IsNullOrEmpty(dto.Id) ? dto.Id : dto.InternalId,
Name = dto.Name,
Slug = dto.Slug,
OrganizationId = !string.IsNullOrEmpty(dto.OrganizationId) ? dto.OrganizationId : dto.OrgId,
IsAccessible = dto.IsAccessible,
CreatedAtUtc = ParseTimestamp(dto.CreatedAt),
UpdatedAtUtc = ParseTimestamp(dto.UpdatedAt)
};
}
public static InfisicalSubOrganization[] MapMany(IEnumerable<InfisicalSubOrganizationResponseDto> items)
{
if (items == null)
{
return Array.Empty<InfisicalSubOrganization>();
}
List<InfisicalSubOrganization> results = new List<InfisicalSubOrganization>();
foreach (InfisicalSubOrganizationResponseDto dto in items)
{
InfisicalSubOrganization mapped = Map(dto);
if (mapped != null)
{
results.Add(mapped);
}
}
return results.ToArray();
}
private static DateTimeOffset? ParseTimestamp(string value)
{
if (string.IsNullOrEmpty(value))
{
return null;
}
DateTimeOffset parsed;
if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out parsed))
{
return parsed;
}
return null;
}
}
}