Rebrand to Grace Solutions; add README, about_ help, Gitea CI/CD, track Module bin #1

Merged
gsadmin merged 8 commits from dev into main 2026-06-03 01:53:11 +00:00
25 changed files with 1768 additions and 176 deletions
+296
View File
@@ -0,0 +1,296 @@
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: Set up .NET SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Install PowerShell 7 (if not present)
shell: bash
run: |
set -euo pipefail
if command -v pwsh >/dev/null 2>&1; then
echo "pwsh already installed: $(pwsh --version)"
exit 0
fi
sudo apt-get update
sudo apt-get install -y --no-install-recommends wget ca-certificates apt-transport-https gnupg
source /etc/os-release
wget -q "https://packages.microsoft.com/config/ubuntu/${VERSION_ID}/packages-microsoft-prod.deb" -O /tmp/ms-prod.deb
sudo dpkg -i /tmp/ms-prod.deb
rm -f /tmp/ms-prod.deb
sudo apt-get update
sudo apt-get install -y powershell
pwsh --version
- name: Build and test module
shell: pwsh
run: ./build.ps1 -RunTests
- 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
outputs:
version: ${{ steps.meta.outputs.version }}
tag: ${{ steps.meta.outputs.tag }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install PowerShell 7 (if not present)
shell: bash
run: |
set -euo pipefail
if command -v pwsh >/dev/null 2>&1; then
echo "pwsh already installed: $(pwsh --version)"
exit 0
fi
sudo apt-get update
sudo apt-get install -y --no-install-recommends wget ca-certificates apt-transport-https gnupg
source /etc/os-release
wget -q "https://packages.microsoft.com/config/ubuntu/${VERSION_ID}/packages-microsoft-prod.deb" -O /tmp/ms-prod.deb
sudo dpkg -i /tmp/ms-prod.deb
rm -f /tmp/ms-prod.deb
sudo apt-get update
sudo apt-get install -y powershell
pwsh --version
- 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 Gitea release
shell: pwsh
env:
GITEA_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'
if ([string]::IsNullOrWhiteSpace($env:GITEA_TOKEN)) {
throw "github.token is not available; cannot call the Gitea release API."
}
$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)/pulls/$($env:PR_NUMBER)"
$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()
}
}
$body = @"
**PSInfisicalAPI $($env:VERSION)**
| Field | Value |
| --- | --- |
| Version | ``$($env:VERSION)`` |
| Tag | ``$($env:TAG)`` |
| Commit | [``$shortSha``]($($env:SERVER_URL)/$($env:REPO)/commit/$($env:COMMIT_SHA)) |
| Built (UTC) | $buildUtc |
| Merged PR | [#$($env:PR_NUMBER) $($env:PR_TITLE)]($prUrl) by @$($env:PR_AUTHOR) |
| Workflow run | [$($env:RUN_ID)]($runUrl) |
## Changes
$(if ($changelogSection) { $changelogSection } else { '_No CHANGELOG section found for this version._' })
## Install
``````powershell
Install-Module -Name PSInfisicalAPI -RequiredVersion $($env:VERSION) -Scope CurrentUser
``````
"@
$headers = @{
Authorization = "token $($env:GITEA_TOKEN)"
Accept = 'application/json'
}
$createUri = "$($env:API_URL)/repos/$($env:REPO)/releases"
$existing = $null
try {
$existing = Invoke-RestMethod -Method Get -Headers $headers `
-Uri "$createUri/tags/$($env:TAG)" -ErrorAction Stop
} catch {
if ($_.Exception.Response.StatusCode.value__ -ne 404) { throw }
}
if ($existing) {
Write-Host "Release tag '$($env:TAG)' already exists (id=$($existing.id)); skipping release creation."
return
}
$payload = @{
tag_name = $env:TAG
target_commitish = $env:COMMIT_SHA
name = "PSInfisicalAPI $($env:VERSION)"
body = $body
draft = $false
prerelease = $false
} | ConvertTo-Json -Depth 4
$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)"
$assetPath = Join-Path $PWD "PSInfisicalAPI-$($env:VERSION).zip"
$uploadUri = "$createUri/$($release.id)/assets?name=PSInfisicalAPI-$($env:VERSION).zip"
$fileBytes = [System.IO.File]::ReadAllBytes($assetPath)
Invoke-RestMethod -Method Post -Uri $uploadUri -Headers $headers `
-ContentType 'application/zip' -Body $fileBytes | Out-Null
Write-Host "Uploaded asset: PSInfisicalAPI-$($env:VERSION).zip"
publish:
needs: release
if: ${{ success() && github.event.pull_request.merged == true }}
runs-on: ubuntu-latest
steps:
- name: Install PowerShell 7 (if not present)
shell: bash
run: |
set -euo pipefail
if command -v pwsh >/dev/null 2>&1; then
echo "pwsh already installed: $(pwsh --version)"
exit 0
fi
sudo apt-get update
sudo apt-get install -y --no-install-recommends wget ca-certificates apt-transport-https gnupg
source /etc/os-release
wget -q "https://packages.microsoft.com/config/ubuntu/${VERSION_ID}/packages-microsoft-prod.deb" -O /tmp/ms-prod.deb
sudo dpkg -i /tmp/ms-prod.deb
rm -f /tmp/ms-prod.deb
sudo apt-get update
sudo apt-get install -y powershell
pwsh --version
- name: Download module artifact
uses: actions/download-artifact@v4
with:
name: PSInfisicalAPI-module
path: Module/PSInfisicalAPI
- name: Bootstrap PowerShellGet / NuGet provider
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
if (-not (Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue)) {
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser | Out-Null
}
Get-PackageProvider -Name NuGet | Format-Table Name,Version
- 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-Module `
-Path $moduleDir `
-NuGetApiKey $env:PSGALLERY_API_KEY `
-Verbose
+6 -2
View File
@@ -4,8 +4,9 @@ obj/
Artifacts/ Artifacts/
Releases/ Releases/
## Module bin output is generated by build.ps1 ## Module bin output is generated by build.ps1, but tracked so the module is consumable from source
Module/PSInfisicalAPI/bin/ !Module/PSInfisicalAPI/bin/
!Module/PSInfisicalAPI/bin/**
## VS / Rider / VSCode ## VS / Rider / VSCode
.vs/ .vs/
@@ -28,3 +29,6 @@ Thumbs.db
TestResults/ TestResults/
*.trx *.trx
*.coverage *.coverage
## Local helper scripts (not part of the module)
scripts/
+814 -18
View File
@@ -1,36 +1,832 @@
# Changelog # Changelog
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loosely, but version numbers use the build timestamp format `yyyy.MM.dd.HHmm`. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loosely, but version numbers use the build timestamp format `yyyy.MM.dd.HHmm`.
## Unreleased ## Unreleased
## 2026.06.03.0131
- Build produced from commit 7be0b7b42008.
- **Behavior change**: `Get-InfisicalSecrets` and `Get-InfisicalSecret` now default `-ViewSecretValue` to `$true`. Real secret values are returned by default. To request the redacted/hidden response, pass `-ViewSecretValue:$false`.
- `InfisicalSecretMapper` now treats the server-side `<hidden-by-infisical>` placeholder as a hidden marker rather than a value: when `secretValueHidden=true` (or the placeholder string is detected) `SecretValue` is set to `null` instead of stuffing the literal into a `SecureString`. This prevents downstream consumers (auth, exports, dictionary conversion) from silently using `<hidden-by-infisical>` as if it were a real secret.
## Unreleased (carried forward)
## 2026.06.03.0113
- Build produced from commit 09c577ebd0fd.
- Added `InfisicalSecret.GetPlainTextValue()` for direct plain-text access to secret material from PowerShell without needing `Marshal.SecureStringToBSTR`.
- Added `-AsPlainText` switch to `ConvertTo-InfisicalSecretDictionary`; when present the cmdlet emits `Dictionary<string, string>` instead of the default `Dictionary<string, SecureString>`.
## Unreleased (carried forward)
## 2026.06.03.0057
- Build produced from commit 7e5209190ac2.
## Unreleased (carried forward)
## 2026.06.03.0056
- Build produced from commit 7e5209190ac2.
## Unreleased (carried forward)
## 2026.06.03.0055
- Build produced from commit 7e5209190ac2.
## Unreleased (carried forward)
## 2026.06.03.0047
- Build produced from commit 7e5209190ac2.
## Unreleased (carried forward)
## 2026.06.03.0046
- Build produced from commit 7e5209190ac2.
## Unreleased (carried forward)
## 2026.06.03.0032
- Build produced from commit c86676010532.
## Unreleased (carried forward)
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward)
## 2026.06.02.1737
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward) (carried forward)
## 2026.06.02.1724
- Build produced from commit 5801b4774af5.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward)
## 2026.06.02.1737
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward) (carried forward) (carried forward)
## 2026.06.02.1648
- Build produced from commit 430e3a00c921.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward)
## 2026.06.02.1737
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward) (carried forward)
## 2026.06.02.1724
- Build produced from commit 5801b4774af5.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward)
## 2026.06.02.1737
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward) (carried forward) (carried forward) (carried forward)
## 2026.06.02.1638 ## 2026.06.02.1638
- Build produced from commit 3c47d6ff30ec. - Build produced from commit 3c47d6ff30ec.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) ## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward)
## 2026.06.02.1737
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward) (carried forward)
## 2026.06.02.1724
- Build produced from commit 5801b4774af5.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward)
## 2026.06.02.1737
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward) (carried forward) (carried forward)
## 2026.06.02.1648
- Build produced from commit 430e3a00c921.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward)
## 2026.06.02.1737
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward) (carried forward)
## 2026.06.02.1724
- Build produced from commit 5801b4774af5.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward)
## 2026.06.02.1737
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward) (carried forward) (carried forward) (carried forward) (carried forward)
## 2026.06.02.1611 ## 2026.06.02.1611
- Build produced from commit 3c47d6ff30ec. - Build produced from commit 3c47d6ff30ec.
## Unreleased ## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward)
## 2026.06.02.1737
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward) (carried forward)
## 2026.06.02.1724
- Build produced from commit 5801b4774af5.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward)
## 2026.06.02.1737
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward) (carried forward) (carried forward)
## 2026.06.02.1648
- Build produced from commit 430e3a00c921.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward)
## 2026.06.02.1737
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward) (carried forward)
## 2026.06.02.1724
- Build produced from commit 5801b4774af5.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward)
## 2026.06.02.1737
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward) (carried forward) (carried forward) (carried forward)
## 2026.06.02.1638 ## 2026.06.02.1638
- Build produced from commit 3c47d6ff30ec. - Build produced from commit 3c47d6ff30ec.
## Unreleased (carried forward) (carried forward) ## Unreleased
### Added ## 2026.06.02.1907
- Initial repository skeleton, C# `netstandard2.0` project, and PowerShell module layout. - Build produced from commit fa65c18bc171.
- Centralized logging (`InfisicalLogger`), error types/handler, sanitizer, path utility, and `SecureString` utility.
- Endpoint registry covering `UniversalAuthLogin`, `ListSecrets`, and `RetrieveSecret`, and a `System.Uri`-based URI builder. ## Unreleased (carried forward)
- Synchronous HTTP client, JSON/YAML/XML/ENV serializers, and DTO/mapper for secrets.
- Connection model, process-level session manager, Universal Auth and Token Auth providers. ## 2026.06.02.1902
- Cmdlets: `Connect-Infisical`, `Disconnect-Infisical`, `Get-InfisicalSecrets`, `Get-InfisicalSecret`, `ConvertTo-InfisicalSecretDictionary`, `Export-InfisicalSecrets`.
- Build script (`build.ps1`) generating manifest, copying binaries, creating release folders, and supporting unit/integration tests. - Build produced from commit fa65c18bc171.
- xUnit test project with unit tests and opt-in integration tests.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward)
## 2026.06.02.1737
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward) (carried forward)
## 2026.06.02.1724
- Build produced from commit 5801b4774af5.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward)
## 2026.06.02.1737
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward) (carried forward) (carried forward)
## 2026.06.02.1648
- Build produced from commit 430e3a00c921.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward)
## 2026.06.02.1737
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward) (carried forward)
## 2026.06.02.1724
- Build produced from commit 5801b4774af5.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward)
## 2026.06.02.1737
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward)
## 2026.06.02.1902
- Build produced from commit fa65c18bc171.
## Unreleased
## 2026.06.02.1907
- Build produced from commit fa65c18bc171.
## Unreleased (carried forward) (carried forward) (carried forward) (carried forward) (carried forward) (carried forward) (carried forward)
### Added
- Initial repository skeleton, C# `netstandard2.0` project, and PowerShell module layout.
- Centralized logging (`InfisicalLogger`), error types/handler, sanitizer, path utility, and `SecureString` utility.
- Endpoint registry covering `UniversalAuthLogin`, `ListSecrets`, and `RetrieveSecret`, and a `System.Uri`-based URI builder.
- Synchronous HTTP client, JSON/YAML/XML/ENV serializers, and DTO/mapper for secrets.
- Connection model, process-level session manager, Universal Auth and Token Auth providers.
- Cmdlets: `Connect-Infisical`, `Disconnect-Infisical`, `Get-InfisicalSecrets`, `Get-InfisicalSecret`, `ConvertTo-InfisicalSecretDictionary`, `Export-InfisicalSecrets`.
- Build script (`build.ps1`) generating manifest, copying binaries, creating release folders, and supporting unit/integration tests.
- xUnit test project with unit tests and opt-in integration tests.
+10 -9
View File
@@ -1,11 +1,11 @@
@{ @{
RootModule = 'PSInfisicalAPI.psm1' RootModule = 'PSInfisicalAPI.psm1'
ModuleVersion = '2026.06.02.1638' ModuleVersion = '2026.06.03.0131'
GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51' GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51'
Author = 'Alphaeus Mote' Author = 'Grace Solutions'
CompanyName = '' CompanyName = 'Grace Solutions'
Copyright = '(c) Alphaeus Mote. All rights reserved.' Copyright = '(c) Grace Solutions. All rights reserved.'
Description = 'PSInfisicalAPI is a C# binary PowerShell module for the Infisical REST API.' Description = 'PSInfisicalAPI is a C# binary PowerShell module for the Infisical REST API, providing cmdlets for authentication, secret retrieval, and export with automatic environment-variable discovery across Process, User, and Machine scopes.'
PowerShellVersion = '5.1' PowerShellVersion = '5.1'
CompatiblePSEditions = @('Desktop','Core') CompatiblePSEditions = @('Desktop','Core')
FunctionsToExport = @() FunctionsToExport = @()
@@ -23,10 +23,11 @@
TypesToProcess = @('PSInfisicalAPI.Types.ps1xml') TypesToProcess = @('PSInfisicalAPI.Types.ps1xml')
PrivateData = @{ PrivateData = @{
PSData = @{ PSData = @{
Tags = @('Infisical','Secrets','API','SecureString') Tags = @('Infisical','Secrets','API','SecureString','Vault','Authentication')
ProjectUri = '' LicenseUri = 'https://www.gnu.org/licenses/agpl-3.0.html'
ReleaseNotes = '' ProjectUri = 'https://prod.git.gracesolution.info/gsadmin/PSInfisicalAPI'
CommitHash = '3c47d6ff30ec' ReleaseNotes = 'See CHANGELOG.md in the project repository for release history.'
CommitHash = '7be0b7b42008'
} }
} }
} }
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,97 @@
TOPIC
about_PSInfisicalAPI
SHORT DESCRIPTION
PSInfisicalAPI is a C# binary PowerShell module that exposes cmdlets for
authenticating against and retrieving secrets from the Infisical REST API,
with automatic environment-variable discovery for connection parameters.
LONG DESCRIPTION
The module provides the following cmdlets:
Connect-Infisical Establish a session.
Disconnect-Infisical Clear the current session.
Get-InfisicalSecrets List secrets at a path.
Get-InfisicalSecret Retrieve a single secret by name.
ConvertTo-InfisicalSecretDictionary Convert secret objects to a hashtable.
Export-InfisicalSecrets Export secrets to JSON, YAML, XML, or .env.
Use Get-Help <cmdlet> -Full for parameter details.
AUTHENTICATION
Connect-Infisical supports two parameter sets:
UniversalAuth -ClientId / -ClientSecret (SecureString)
Token -AccessToken (SecureString)
Common parameters apply to both sets:
-BaseUri, -OrganizationId, -ProjectId, -Environment,
-SecretPath (default '/'), -ApiVersion (default 'v4'), -PassThru.
ENVIRONMENT VARIABLE DISCOVERY
When any of the Connect-Infisical parameters are omitted, null, or contain
only whitespace, the cmdlet scans environment variables in three scopes,
in order:
1. Process
2. User
3. Machine
The first matching variable with a non-blank value is used. Explicitly
supplied parameter values always win.
Patterns are case-insensitive and match Infisical's CLI defaults plus
common variants such as CLOUDINIT_INFISICAL_* and custom-prefixed names
(for example "myapp_infisical_client_id").
Parameter Example variable names
---------------- ----------------------------------------------------------
BaseUri INFISICAL_API_URL, INFISICAL_BASE_URL, INFISICAL_HOST
OrganizationId INFISICAL_ORG_ID, INFISICAL_ORGANIZATION_ID
ProjectId INFISICAL_PROJECT_ID, INFISICAL_WORKSPACE_ID
Environment INFISICAL_ENVIRONMENT, INFISICAL_ENV, INFISICAL_ENV_SLUG
ClientId INFISICAL_CLIENT_ID, INFISICAL_UNIVERSAL_AUTH_CLIENT_ID
ClientSecret INFISICAL_CLIENT_SECRET, INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET
AccessToken INFISICAL_TOKEN, INFISICAL_ACCESS_TOKEN, INFISICAL_AUTH_TOKEN
SecretPath INFISICAL_SECRET_PATH, INFISICAL_DEFAULT_SECRET_PATH
ApiVersion INFISICAL_API_VERSION
Sensitive values (ClientSecret, AccessToken) are read directly into a
read-only SecureString. Discovered values are never written to logs;
only the variable name and the scope it was found in are recorded.
Use -Verbose to see the scan announcement and any discovered variable
names.
EXAMPLES
Example 1: Zero-configuration connect (all values from environment).
Connect-Infisical
Get-InfisicalSecrets
Example 2: Explicit parameters override discovery.
$secret = Read-Host -AsSecureString 'Client Secret'
Connect-Infisical -Environment prod -ClientSecret $secret
Example 3: Token-based authentication.
$token = Read-Host -AsSecureString 'Access Token'
Connect-Infisical -AccessToken $token
Example 4: Export to a .env file.
Get-InfisicalSecrets |
Export-InfisicalSecrets -Path .\secrets.env -Format Env
SECURITY NOTES
- SecureString is used for ClientSecret, AccessToken, and any secret
payloads returned by the API.
- Sanitization is applied before any value reaches the logging pipeline.
- Sessions are stored in process-local state only and never persisted.
SEE ALSO
Get-Help Connect-Infisical -Full
Get-Help Get-InfisicalSecrets -Full
Get-Help Export-InfisicalSecrets -Full
https://infisical.com/docs/api-reference/overview/introduction
+126
View File
@@ -1,2 +1,128 @@
# PSInfisicalAPI # PSInfisicalAPI
A C# binary PowerShell module for interacting with the [Infisical](https://infisical.com/) REST API. It provides cmdlets for authentication, secret retrieval, structured export, and includes automatic environment-variable discovery so connections can be established with little or no inline configuration.
- License: AGPL-3.0
- Author: Grace Solutions
- Target framework: .NET Standard 2.0 (compatible with Windows PowerShell 5.1 and PowerShell 7+)
## Installation
### From the PowerShell Gallery
```powershell
Install-Module -Name PSInfisicalAPI -Scope CurrentUser
Import-Module -Name PSInfisicalAPI
```
### From source
```powershell
git clone https://prod.git.gracesolution.info/gsadmin/PSInfisicalAPI.git
cd PSInfisicalAPI
pwsh -NoProfile -ExecutionPolicy Bypass -File .\build.ps1 -RunTests
Import-Module -Name .\Module\PSInfisicalAPI
```
## Cmdlets
| Cmdlet | Purpose |
| ------------------------------------- | -------------------------------------------------------------------------- |
| `Connect-Infisical` | Establish a session using Universal Auth or a pre-issued access token. |
| `Disconnect-Infisical` | Clear the current session. |
| `Get-InfisicalSecrets` | List secrets at a given path / environment. |
| `Get-InfisicalSecret` | Retrieve a single secret by name. |
| `ConvertTo-InfisicalSecretDictionary` | Convert secret objects into a `Hashtable` keyed by `SecretKey`. |
| `Export-InfisicalSecrets` | Export secrets to JSON, YAML, XML, or `.env` format. |
Use `Get-Help <Cmdlet> -Full` for parameter details and `Get-Help about_PSInfisicalAPI` for the module overview.
## Quick start
```powershell
$secureSecret = Read-Host -AsSecureString 'Client Secret'
$connection = Connect-Infisical `
-BaseUri 'https://app.infisical.com' `
-OrganizationId '00000000-0000-0000-0000-000000000000' `
-ProjectId '11111111-1111-1111-1111-111111111111' `
-Environment 'dev' `
-ClientId 'machine-identity-client-id' `
-ClientSecret $secureSecret `
-PassThru
Get-InfisicalSecrets -SecretPath '/'
Disconnect-Infisical
```
## 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.
### Scope precedence
Scopes are searched in order; the first matching variable with a non-blank value wins:
1. `Process`
2. `User`
3. `Machine`
### Patterns
The resolver matches case-insensitively against patterns aligned with Infisical's CLI defaults plus common variants such as `CLOUDINIT_INFISICAL_*` and custom-prefixed names (e.g., `myapp_infisical_client_id`).
| Parameter | Example variable names matched |
| ----------------- | ------------------------------------------------------------------------------------ |
| `BaseUri` | `INFISICAL_API_URL`, `INFISICAL_BASE_URL`, `INFISICAL_HOST` |
| `OrganizationId` | `INFISICAL_ORG_ID`, `INFISICAL_ORGANIZATION_ID` |
| `ProjectId` | `INFISICAL_PROJECT_ID`, `INFISICAL_WORKSPACE_ID` |
| `Environment` | `INFISICAL_ENVIRONMENT`, `INFISICAL_ENV`, `INFISICAL_ENV_SLUG` |
| `ClientId` | `INFISICAL_CLIENT_ID`, `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` |
| `ClientSecret` | `INFISICAL_CLIENT_SECRET`, `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` |
| `AccessToken` | `INFISICAL_TOKEN`, `INFISICAL_ACCESS_TOKEN`, `INFISICAL_AUTH_TOKEN` |
| `SecretPath` | `INFISICAL_SECRET_PATH`, `INFISICAL_DEFAULT_SECRET_PATH` |
| `ApiVersion` | `INFISICAL_API_VERSION` |
Sensitive values (`ClientSecret`, `AccessToken`) are read directly into a read-only `SecureString` and never logged.
### Zero-configuration example
```powershell
[Environment]::SetEnvironmentVariable('INFISICAL_API_URL', 'https://app.infisical.com', 'User')
[Environment]::SetEnvironmentVariable('INFISICAL_ORG_ID', '00000000-0000-0000-0000-000000000000', 'User')
[Environment]::SetEnvironmentVariable('INFISICAL_PROJECT_ID', '11111111-1111-1111-1111-111111111111', 'User')
[Environment]::SetEnvironmentVariable('INFISICAL_ENVIRONMENT', 'dev', 'User')
[Environment]::SetEnvironmentVariable('INFISICAL_CLIENT_ID', 'machine-identity-client-id', 'User')
[Environment]::SetEnvironmentVariable('INFISICAL_CLIENT_SECRET', 'super-secret-value', 'User')
Connect-Infisical
Get-InfisicalSecrets
```
### Mixed example (explicit values override discovery)
Explicit parameters always win over discovered values; blank/whitespace explicit values trigger discovery.
```powershell
Connect-Infisical -Environment 'prod' # everything else discovered from environment
```
### Logging
The resolver emits a single verbose line announcing the scan and one informational line per discovered variable (variable name and scope; values are never logged). Use `-Verbose` to see the scan announcement.
## Building
```powershell
pwsh -NoProfile -ExecutionPolicy Bypass -File .\build.ps1 -RunTests
```
The script builds the binary, runs unit tests, publishes binaries into `Module/PSInfisicalAPI/bin/`, regenerates the manifest, and validates that the module imports.
## Continuous integration
`.gitea/workflows/publish-psgallery.yml` publishes the module to the PowerShell Gallery whenever a pull request is merged into `main`. The workflow expects a repository secret named `PSGALLERY_API_KEY` containing a valid Gallery API key.
## License
Distributed under the GNU Affero General Public License v3.0. See [LICENSE](LICENSE).
+32 -15
View File
@@ -90,10 +90,10 @@ function Write-Manifest {
RootModule = 'PSInfisicalAPI.psm1' RootModule = 'PSInfisicalAPI.psm1'
ModuleVersion = '$ModuleVersion' ModuleVersion = '$ModuleVersion'
GUID = '$ModuleGuid' GUID = '$ModuleGuid'
Author = 'Alphaeus Mote' Author = 'Grace Solutions'
CompanyName = '' CompanyName = 'Grace Solutions'
Copyright = '(c) Alphaeus Mote. All rights reserved.' Copyright = '(c) Grace Solutions. All rights reserved.'
Description = 'PSInfisicalAPI is a C# binary PowerShell module for the Infisical REST API.' Description = 'PSInfisicalAPI is a C# binary PowerShell module for the Infisical REST API, providing cmdlets for authentication, secret retrieval, and export with automatic environment-variable discovery across Process, User, and Machine scopes.'
PowerShellVersion = '5.1' PowerShellVersion = '5.1'
CompatiblePSEditions = @('Desktop','Core') CompatiblePSEditions = @('Desktop','Core')
FunctionsToExport = @() FunctionsToExport = @()
@@ -111,10 +111,11 @@ function Write-Manifest {
TypesToProcess = @('PSInfisicalAPI.Types.ps1xml') TypesToProcess = @('PSInfisicalAPI.Types.ps1xml')
PrivateData = @{ PrivateData = @{
PSData = @{ PSData = @{
Tags = @('Infisical','Secrets','API','SecureString') Tags = @('Infisical','Secrets','API','SecureString','Vault','Authentication')
ProjectUri = '' LicenseUri = 'https://www.gnu.org/licenses/agpl-3.0.html'
ReleaseNotes = '' ProjectUri = 'https://prod.git.gracesolution.info/gsadmin/PSInfisicalAPI'
CommitHash = '$CommitHash' ReleaseNotes = 'See CHANGELOG.md in the project repository for release history.'
CommitHash = '$CommitHash'
} }
} }
} }
@@ -132,7 +133,9 @@ function Update-Changelog {
if ($existing -match [Regex]::Escape($marker)) { return } if ($existing -match [Regex]::Escape($marker)) { return }
$insertion = "## $Version`r`n`r`n- Build produced from commit $CommitHash.`r`n`r`n" $insertion = "## $Version`r`n`r`n- Build produced from commit $CommitHash.`r`n`r`n"
$updated = $existing -replace '## Unreleased', "## Unreleased`r`n`r`n$insertion## Unreleased (carried forward)" $unreleasedRegex = [regex]::new('(?m)^## Unreleased\r?$')
if (-not $unreleasedRegex.IsMatch($existing)) { return }
$updated = $unreleasedRegex.Replace($existing, "## Unreleased`r`n`r`n$insertion## Unreleased (carried forward)", 1)
[System.IO.File]::WriteAllText($ChangelogFile.FullName, $updated, [System.Text.UTF8Encoding]::new($false)) [System.IO.File]::WriteAllText($ChangelogFile.FullName, $updated, [System.Text.UTF8Encoding]::new($false))
} }
@@ -148,15 +151,33 @@ function Invoke-DotNet {
function Test-ModuleImports { function Test-ModuleImports {
param([System.IO.DirectoryInfo]$ModuleDirectory) param([System.IO.DirectoryInfo]$ModuleDirectory)
Write-Step "Validating module import" Write-Step "Validating module import, manifest, and help"
$manifestPath = [System.IO.Path]::Combine($ModuleDirectory.FullName, 'PSInfisicalAPI.psd1')
$script = @" $script = @"
`$ErrorActionPreference = 'Stop' `$ErrorActionPreference = 'Stop'
`$manifest = Test-ModuleManifest -Path '$manifestPath'
if (`$null -eq `$manifest) {
throw "Test-ModuleManifest returned no result for '$manifestPath'."
}
Import-Module -Name '$($ModuleDirectory.FullName)' -Force Import-Module -Name '$($ModuleDirectory.FullName)' -Force
`$cmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecrets','Get-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets') `$cmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecrets','Get-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets')
foreach (`$c in `$cmds) { foreach (`$c in `$cmds) {
if (-not (Get-Command -Name `$c -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) { if (-not (Get-Command -Name `$c -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) {
throw "Cmdlet not found: `$c" throw "Cmdlet not found: `$c"
} }
`$help = Get-Help -Name `$c -ErrorAction SilentlyContinue
if (`$null -eq `$help) {
throw "Get-Help returned nothing for cmdlet: `$c"
}
}
`$about = Get-Help -Name 'about_PSInfisicalAPI' -ErrorAction SilentlyContinue
if (`$null -eq `$about -or [string]::IsNullOrWhiteSpace((`$about | Out-String))) {
throw "Get-Help 'about_PSInfisicalAPI' returned no content. Ensure en-US/about_PSInfisicalAPI.help.txt is present."
} }
"@ "@
@@ -251,11 +272,7 @@ Write-Manifest -Path $manifestPath -ModuleVersion $buildVersion -CommitHash $com
Update-Changelog -Version $buildVersion -CommitHash $commitHash Update-Changelog -Version $buildVersion -CommitHash $commitHash
try { Test-ModuleImports -ModuleDirectory $ModuleRoot
Test-ModuleImports -ModuleDirectory $ModuleRoot
} catch {
Write-Warning "Module import validation reported: $($_.Exception.Message)"
}
if ($CreateRelease.IsPresent) { if ($CreateRelease.IsPresent) {
$releaseDir = [System.IO.DirectoryInfo][System.IO.Path]::Combine($ReleasesDir.FullName, $buildVersion) $releaseDir = [System.IO.DirectoryInfo][System.IO.Path]::Combine($ReleasesDir.FullName, $buildVersion)
+11 -11
View File
@@ -187,7 +187,7 @@ Example shape:
RootModule = 'PSInfisicalAPI.psm1' RootModule = 'PSInfisicalAPI.psm1'
ModuleVersion = 'yyyy.MM.dd.HHmm' ModuleVersion = 'yyyy.MM.dd.HHmm'
GUID = '<stable-guid>' GUID = '<stable-guid>'
Author = 'Alphaeus Mote' Author = 'Grace Solutions'
CompanyName = '' CompanyName = ''
Copyright = '' Copyright = ''
PowerShellVersion = '5.1' PowerShellVersion = '5.1'
@@ -954,10 +954,10 @@ Get-InfisicalSecrets `
[-Environment <string>] ` [-Environment <string>] `
[-SecretPath <string>] ` [-SecretPath <string>] `
[-Recursive] ` [-Recursive] `
[-IncludeImports <bool>] ` [-IncludeImports] `
[-IncludePersonalOverrides] ` [-IncludePersonalOverrides] `
[-ExpandSecretReferences <bool>] ` [-ExpandSecretReferences] `
[-ViewSecretValue <bool>] ` [-ViewSecretValue] `
[-MetadataFilter <hashtable>] ` [-MetadataFilter <hashtable>] `
[-TagSlugs <string[]>] [-TagSlugs <string[]>]
``` ```
@@ -969,8 +969,8 @@ ProjectId: Current connection ProjectId
Environment: Current connection Environment Environment: Current connection Environment
SecretPath: Current connection DefaultSecretPath or / SecretPath: Current connection DefaultSecretPath or /
Recursive: false Recursive: false
IncludeImports: true IncludeImports: false
ExpandSecretReferences: true ExpandSecretReferences: false
ViewSecretValue: true ViewSecretValue: true
``` ```
@@ -1020,9 +1020,9 @@ Get-InfisicalSecret `
[-SecretPath <string>] ` [-SecretPath <string>] `
[-Version <int>] ` [-Version <int>] `
[-Type <InfisicalSecretType>] ` [-Type <InfisicalSecretType>] `
[-ViewSecretValue <bool>] ` [-ViewSecretValue] `
[-ExpandSecretReferences <bool>] ` [-ExpandSecretReferences] `
[-IncludeImports <bool>] [-IncludeImports]
``` ```
## Parameter Attributes ## Parameter Attributes
@@ -1041,8 +1041,8 @@ Environment: Current connection Environment
SecretPath: Current connection DefaultSecretPath or / SecretPath: Current connection DefaultSecretPath or /
Type: Shared Type: Shared
ViewSecretValue: true ViewSecretValue: true
ExpandSecretReferences: true ExpandSecretReferences: false
IncludeImports: true IncludeImports: false
``` ```
## Behavior ## Behavior
@@ -89,10 +89,13 @@ namespace PSInfisicalAPI.Cmdlets
throw new InfisicalAuthenticationException("Authentication did not produce an access token."); throw new InfisicalAuthenticationException("Authentication did not produce an access token.");
} }
bool apiVersionExplicitlyBound = MyInvocation.BoundParameters.ContainsKey("ApiVersion");
InfisicalConnection connection = new InfisicalConnection InfisicalConnection connection = new InfisicalConnection
{ {
BaseUri = BaseUri, BaseUri = BaseUri,
ApiVersion = ApiVersion, ApiVersion = ApiVersion,
PinnedApiVersion = apiVersionExplicitlyBound ? ApiVersion : null,
AuthType = authType, AuthType = authType,
OrganizationId = OrganizationId, OrganizationId = OrganizationId,
ProjectId = ProjectId, ProjectId = ProjectId,
@@ -9,6 +9,7 @@ namespace PSInfisicalAPI.Cmdlets
{ {
[Cmdlet(VerbsData.ConvertTo, "InfisicalSecretDictionary")] [Cmdlet(VerbsData.ConvertTo, "InfisicalSecretDictionary")]
[OutputType(typeof(Dictionary<string, SecureString>))] [OutputType(typeof(Dictionary<string, SecureString>))]
[OutputType(typeof(Dictionary<string, string>))]
public sealed class ConvertToInfisicalSecretDictionaryCmdlet : InfisicalCmdletBase public sealed class ConvertToInfisicalSecretDictionaryCmdlet : InfisicalCmdletBase
{ {
[Parameter(Mandatory = true, ValueFromPipeline = true)] [Parameter(Mandatory = true, ValueFromPipeline = true)]
@@ -17,6 +18,9 @@ namespace PSInfisicalAPI.Cmdlets
[Parameter] [Parameter]
public InfisicalDuplicateKeyBehavior DuplicateKeyBehavior { get; set; } = InfisicalDuplicateKeyBehavior.Error; public InfisicalDuplicateKeyBehavior DuplicateKeyBehavior { get; set; } = InfisicalDuplicateKeyBehavior.Error;
[Parameter]
public SwitchParameter AsPlainText { get; set; }
private readonly List<InfisicalSecret> _buffer = new List<InfisicalSecret>(); private readonly List<InfisicalSecret> _buffer = new List<InfisicalSecret>();
protected override void ProcessRecord() protected override void ProcessRecord()
@@ -36,36 +40,50 @@ namespace PSInfisicalAPI.Cmdlets
{ {
try try
{ {
Dictionary<string, SecureString> dictionary = new Dictionary<string, SecureString>(StringComparer.OrdinalIgnoreCase); if (AsPlainText.IsPresent)
foreach (InfisicalSecret secret in _buffer)
{ {
string key = secret.SecretName ?? string.Empty; Dictionary<string, string> plain = BuildDictionary<string>(secret => secret.GetPlainTextValue());
WriteObject(plain);
if (dictionary.ContainsKey(key)) }
{ else
if (DuplicateKeyBehavior == InfisicalDuplicateKeyBehavior.Error) {
{ Dictionary<string, SecureString> secure = BuildDictionary<SecureString>(secret => secret.SecretValue);
throw new InfisicalConfigurationException(string.Concat("Duplicate secret name encountered: ", key)); WriteObject(secure);
}
if (DuplicateKeyBehavior == InfisicalDuplicateKeyBehavior.LastWins)
{
dictionary[key] = secret.SecretValue;
}
continue;
}
dictionary[key] = secret.SecretValue;
} }
WriteObject(dictionary);
} }
catch (Exception exception) catch (Exception exception)
{ {
ThrowTerminatingForException("ConvertToInfisicalSecretDictionaryCmdlet", "ConvertToDictionary", exception); ThrowTerminatingForException("ConvertToInfisicalSecretDictionaryCmdlet", "ConvertToDictionary", exception);
} }
} }
private Dictionary<string, TValue> BuildDictionary<TValue>(Func<InfisicalSecret, TValue> valueSelector)
{
Dictionary<string, TValue> dictionary = new Dictionary<string, TValue>(StringComparer.OrdinalIgnoreCase);
foreach (InfisicalSecret secret in _buffer)
{
string key = secret.SecretName ?? string.Empty;
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(secret);
}
continue;
}
dictionary[key] = valueSelector(secret);
}
return dictionary;
}
} }
} }
@@ -89,6 +89,7 @@ namespace PSInfisicalAPI.Cmdlets
{ {
switch (encoding) switch (encoding)
{ {
case InfisicalExportEncoding.UTF8: return new UTF8Encoding(false);
case InfisicalExportEncoding.UTF8Bom: return new UTF8Encoding(true); case InfisicalExportEncoding.UTF8Bom: return new UTF8Encoding(true);
case InfisicalExportEncoding.Unicode: return new UnicodeEncoding(); case InfisicalExportEncoding.Unicode: return new UnicodeEncoding();
default: return new UTF8Encoding(false); default: return new UTF8Encoding(false);
@@ -16,11 +16,12 @@ namespace PSInfisicalAPI.Cmdlets
[Parameter] public string ProjectId { get; set; } [Parameter] public string ProjectId { get; set; }
[Parameter] public string Environment { get; set; } [Parameter] public string Environment { get; set; }
[Parameter] public string SecretPath { get; set; } [Parameter] public string SecretPath { get; set; }
[Parameter] public string ApiVersion { get; set; }
[Parameter] public int? Version { get; set; } [Parameter] public int? Version { get; set; }
[Parameter] public InfisicalSecretType Type { get; set; } = InfisicalSecretType.Shared; [Parameter] public InfisicalSecretType Type { get; set; } = InfisicalSecretType.Shared;
[Parameter] public bool ViewSecretValue { get; set; } = true; [Parameter] public SwitchParameter ViewSecretValue { get; set; } = SwitchParameter.Present;
[Parameter] public bool ExpandSecretReferences { get; set; } = true; [Parameter] public SwitchParameter ExpandSecretReferences { get; set; }
[Parameter] public bool IncludeImports { get; set; } = true; [Parameter] public SwitchParameter IncludeImports { get; set; }
protected override void ProcessRecord() protected override void ProcessRecord()
{ {
@@ -34,11 +35,12 @@ namespace PSInfisicalAPI.Cmdlets
ProjectId = ProjectId, ProjectId = ProjectId,
Environment = Environment, Environment = Environment,
SecretPath = SecretPath, SecretPath = SecretPath,
ApiVersion = ApiVersion,
Version = Version, Version = Version,
Type = Type.ToString(), Type = Type.ToString(),
ViewSecretValue = ViewSecretValue, ViewSecretValue = ViewSecretValue.IsPresent,
ExpandSecretReferences = ExpandSecretReferences, ExpandSecretReferences = ExpandSecretReferences.IsPresent,
IncludeImports = IncludeImports IncludeImports = IncludeImports.IsPresent
}; };
InfisicalSecretsClient client = new InfisicalSecretsClient(HttpClient, Logger); InfisicalSecretsClient client = new InfisicalSecretsClient(HttpClient, Logger);
@@ -15,11 +15,12 @@ namespace PSInfisicalAPI.Cmdlets
[Parameter] public string ProjectId { get; set; } [Parameter] public string ProjectId { get; set; }
[Parameter] public string Environment { get; set; } [Parameter] public string Environment { get; set; }
[Parameter] public string SecretPath { get; set; } [Parameter] public string SecretPath { get; set; }
[Parameter] public string ApiVersion { get; set; }
[Parameter] public SwitchParameter Recursive { get; set; } [Parameter] public SwitchParameter Recursive { get; set; }
[Parameter] public bool IncludeImports { get; set; } = true; [Parameter] public SwitchParameter IncludeImports { get; set; }
[Parameter] public SwitchParameter IncludePersonalOverrides { get; set; } [Parameter] public SwitchParameter IncludePersonalOverrides { get; set; }
[Parameter] public bool ExpandSecretReferences { get; set; } = true; [Parameter] public SwitchParameter ExpandSecretReferences { get; set; }
[Parameter] public bool ViewSecretValue { get; set; } = true; [Parameter] public SwitchParameter ViewSecretValue { get; set; } = SwitchParameter.Present;
[Parameter] public Hashtable MetadataFilter { get; set; } [Parameter] public Hashtable MetadataFilter { get; set; }
[Parameter] public string[] TagSlugs { get; set; } [Parameter] public string[] TagSlugs { get; set; }
@@ -34,11 +35,12 @@ namespace PSInfisicalAPI.Cmdlets
ProjectId = ProjectId, ProjectId = ProjectId,
Environment = Environment, Environment = Environment,
SecretPath = SecretPath, SecretPath = SecretPath,
ApiVersion = ApiVersion,
Recursive = Recursive.IsPresent, Recursive = Recursive.IsPresent,
IncludeImports = IncludeImports, IncludeImports = IncludeImports.IsPresent,
IncludePersonalOverrides = IncludePersonalOverrides.IsPresent, IncludePersonalOverrides = IncludePersonalOverrides.IsPresent,
ExpandSecretReferences = ExpandSecretReferences, ExpandSecretReferences = ExpandSecretReferences.IsPresent,
ViewSecretValue = ViewSecretValue, ViewSecretValue = ViewSecretValue.IsPresent,
MetadataFilter = ToStringDictionary(MetadataFilter), MetadataFilter = ToStringDictionary(MetadataFilter),
TagSlugs = TagSlugs TagSlugs = TagSlugs
}; };
@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Security; using System.Security;
using PSInfisicalAPI.Models; using PSInfisicalAPI.Models;
@@ -8,6 +9,7 @@ namespace PSInfisicalAPI.Connections
{ {
public Uri BaseUri { get; set; } public Uri BaseUri { get; set; }
public string ApiVersion { get; set; } public string ApiVersion { get; set; }
public string PinnedApiVersion { get; set; }
public InfisicalAuthType AuthType { get; set; } public InfisicalAuthType AuthType { get; set; }
public string OrganizationId { get; set; } public string OrganizationId { get; set; }
public string ProjectId { get; set; } public string ProjectId { get; set; }
@@ -17,6 +19,8 @@ namespace PSInfisicalAPI.Connections
public DateTimeOffset? ExpiresAtUtc { get; set; } public DateTimeOffset? ExpiresAtUtc { get; set; }
public bool IsConnected { get; set; } public bool IsConnected { get; set; }
public Dictionary<string, string> ResolvedEndpointVersions { get; } = new Dictionary<string, string>(StringComparer.Ordinal);
internal SecureString AccessToken { get; set; } internal SecureString AccessToken { get; set; }
public override string ToString() public override string ToString()
@@ -5,67 +5,88 @@ namespace PSInfisicalAPI.Endpoints
{ {
public static class InfisicalEndpointRegistry public static class InfisicalEndpointRegistry
{ {
private static readonly Dictionary<string, InfisicalEndpointDefinition> Definitions = private static readonly Dictionary<string, List<InfisicalEndpointDefinition>> Candidates =
new Dictionary<string, InfisicalEndpointDefinition> new Dictionary<string, List<InfisicalEndpointDefinition>>
{ {
{ {
InfisicalEndpointNames.UniversalAuthLogin, InfisicalEndpointNames.UniversalAuthLogin,
new InfisicalEndpointDefinition new List<InfisicalEndpointDefinition>
{ {
Name = InfisicalEndpointNames.UniversalAuthLogin, new InfisicalEndpointDefinition
Resource = "Authentication", {
Version = "v1", Name = InfisicalEndpointNames.UniversalAuthLogin,
Method = "POST", Resource = "Authentication",
Template = "/api/v1/auth/universal-auth/login", Version = "v1",
RequiresAuthorization = false, Method = "POST",
ContainsSecretMaterialInRequest = true, Template = "/api/v1/auth/universal-auth/login",
ContainsSecretMaterialInResponse = true RequiresAuthorization = false,
ContainsSecretMaterialInRequest = true,
ContainsSecretMaterialInResponse = true
}
} }
}, },
{ {
InfisicalEndpointNames.ListSecrets, InfisicalEndpointNames.ListSecrets,
new InfisicalEndpointDefinition new List<InfisicalEndpointDefinition>
{ {
Name = InfisicalEndpointNames.ListSecrets, new InfisicalEndpointDefinition
Resource = "Secrets", {
Version = "v4", Name = InfisicalEndpointNames.ListSecrets,
Method = "GET", Resource = "Secrets",
Template = "/api/v4/secrets", Version = "v4",
RequiresAuthorization = true, Method = "GET",
ContainsSecretMaterialInRequest = false, Template = "/api/v4/secrets",
ContainsSecretMaterialInResponse = true RequiresAuthorization = true,
ContainsSecretMaterialInRequest = false,
ContainsSecretMaterialInResponse = true
},
new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.ListSecrets,
Resource = "Secrets",
Version = "v3",
Method = "GET",
Template = "/api/v3/secrets/raw",
RequiresAuthorization = true,
ContainsSecretMaterialInRequest = false,
ContainsSecretMaterialInResponse = true
}
} }
}, },
{ {
InfisicalEndpointNames.RetrieveSecret, InfisicalEndpointNames.RetrieveSecret,
new InfisicalEndpointDefinition new List<InfisicalEndpointDefinition>
{ {
Name = InfisicalEndpointNames.RetrieveSecret, new InfisicalEndpointDefinition
Resource = "Secrets", {
Version = "v4", Name = InfisicalEndpointNames.RetrieveSecret,
Method = "GET", Resource = "Secrets",
Template = "/api/v4/secrets/{secretName}", Version = "v4",
RequiresAuthorization = true, Method = "GET",
ContainsSecretMaterialInRequest = false, Template = "/api/v4/secrets/{secretName}",
ContainsSecretMaterialInResponse = true RequiresAuthorization = true,
ContainsSecretMaterialInRequest = false,
ContainsSecretMaterialInResponse = true
},
new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.RetrieveSecret,
Resource = "Secrets",
Version = "v3",
Method = "GET",
Template = "/api/v3/secrets/raw/{secretName}",
RequiresAuthorization = true,
ContainsSecretMaterialInRequest = false,
ContainsSecretMaterialInResponse = true
}
} }
} }
}; };
public static InfisicalEndpointDefinition Get(string name) public static InfisicalEndpointDefinition Get(string name)
{ {
if (string.IsNullOrEmpty(name)) List<InfisicalEndpointDefinition> list = GetCandidatesInternal(name);
{ return list[0];
throw new InfisicalConfigurationException("Endpoint name must be provided.");
}
InfisicalEndpointDefinition definition;
if (!Definitions.TryGetValue(name, out definition))
{
throw new InfisicalConfigurationException(string.Concat("Unknown endpoint name: ", name));
}
return definition;
} }
public static bool TryGet(string name, out InfisicalEndpointDefinition definition) public static bool TryGet(string name, out InfisicalEndpointDefinition definition)
@@ -76,12 +97,50 @@ namespace PSInfisicalAPI.Endpoints
return false; return false;
} }
return Definitions.TryGetValue(name, out definition); List<InfisicalEndpointDefinition> list;
if (!Candidates.TryGetValue(name, out list) || list == null || list.Count == 0)
{
definition = null;
return false;
}
definition = list[0];
return true;
}
public static IReadOnlyList<InfisicalEndpointDefinition> GetCandidates(string name)
{
return GetCandidatesInternal(name);
} }
public static IEnumerable<InfisicalEndpointDefinition> All() public static IEnumerable<InfisicalEndpointDefinition> All()
{ {
return Definitions.Values; List<InfisicalEndpointDefinition> result = new List<InfisicalEndpointDefinition>();
foreach (List<InfisicalEndpointDefinition> list in Candidates.Values)
{
foreach (InfisicalEndpointDefinition definition in list)
{
result.Add(definition);
}
}
return result;
}
private static List<InfisicalEndpointDefinition> GetCandidatesInternal(string name)
{
if (string.IsNullOrEmpty(name))
{
throw new InfisicalConfigurationException("Endpoint name must be provided.");
}
List<InfisicalEndpointDefinition> list;
if (!Candidates.TryGetValue(name, out list) || list == null || list.Count == 0)
{
throw new InfisicalConfigurationException(string.Concat("Unknown endpoint name: ", name));
}
return list;
} }
} }
} }
@@ -42,6 +42,14 @@ namespace PSInfisicalAPI.Http
webRequest.UserAgent = "PSInfisicalAPI"; webRequest.UserAgent = "PSInfisicalAPI";
webRequest.Timeout = _timeoutSeconds * 1000; webRequest.Timeout = _timeoutSeconds * 1000;
webRequest.ReadWriteTimeout = _timeoutSeconds * 1000; webRequest.ReadWriteTimeout = _timeoutSeconds * 1000;
webRequest.UseDefaultCredentials = true;
IWebProxy systemProxy = WebRequest.GetSystemWebProxy();
if (systemProxy != null)
{
systemProxy.Credentials = CredentialCache.DefaultNetworkCredentials;
webRequest.Proxy = systemProxy;
}
ApplyHeaders(webRequest, request.Headers); ApplyHeaders(webRequest, request.Headers);
+1 -7
View File
@@ -39,13 +39,7 @@ namespace PSInfisicalAPI.Logging
public void Error(string component, string message) public void Error(string component, string message)
{ {
string line = InfisicalLogFormatter.FormatNow(InfisicalLogLevel.Error, component, message); string line = InfisicalLogFormatter.FormatNow(InfisicalLogLevel.Error, component, message);
ErrorRecord record = new ErrorRecord( _cmdlet.WriteWarning(line);
new InvalidOperationException(message ?? string.Empty),
"PSInfisicalAPI.Error",
ErrorCategory.NotSpecified,
component);
record.ErrorDetails = new ErrorDetails(line);
_cmdlet.WriteError(record);
} }
} }
} }
@@ -34,6 +34,12 @@ namespace PSInfisicalAPI.Models
SecureStringUtility.UsePlainText(SecretValue, action); SecureStringUtility.UsePlainText(SecretValue, action);
} }
public string GetPlainTextValue()
{
if (SecretValue == null) { return null; }
return SecureStringUtility.UsePlainText(SecretValue, plainText => plainText);
}
public override string ToString() public override string ToString()
{ {
return SecretName; return SecretName;
+2 -2
View File
@@ -12,8 +12,8 @@
<AssemblyVersion>$(BuildAssemblyVersion)</AssemblyVersion> <AssemblyVersion>$(BuildAssemblyVersion)</AssemblyVersion>
<FileVersion>$(BuildAssemblyVersion)</FileVersion> <FileVersion>$(BuildAssemblyVersion)</FileVersion>
<InformationalVersion>$(BuildVersion)</InformationalVersion> <InformationalVersion>$(BuildVersion)</InformationalVersion>
<Company /> <Company>Grace Solutions</Company>
<Authors>Alphaeus Mote</Authors> <Authors>Grace Solutions</Authors>
<Product>PSInfisicalAPI</Product> <Product>PSInfisicalAPI</Product>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
@@ -15,6 +15,8 @@ namespace PSInfisicalAPI.Secrets
return null; return null;
} }
bool hidden = dto.SecretValueHidden || IsHiddenPlaceholder(dto.SecretValue);
InfisicalSecret secret = new InfisicalSecret InfisicalSecret secret = new InfisicalSecret
{ {
Id = dto.Id, Id = dto.Id,
@@ -24,8 +26,8 @@ namespace PSInfisicalAPI.Secrets
Version = dto.Version, Version = dto.Version,
Type = ParseType(dto.Type), Type = ParseType(dto.Type),
SecretName = dto.SecretKey, SecretName = dto.SecretKey,
SecretValue = SecureStringUtility.ToReadOnlySecureString(dto.SecretValue), SecretValue = hidden ? null : SecureStringUtility.ToReadOnlySecureString(dto.SecretValue),
SecretValueHidden = dto.SecretValueHidden, SecretValueHidden = hidden,
SecretPath = dto.SecretPath, SecretPath = dto.SecretPath,
SecretComment = dto.SecretComment, SecretComment = dto.SecretComment,
CreatedAtUtc = ParseTimestamp(dto.CreatedAt), CreatedAtUtc = ParseTimestamp(dto.CreatedAt),
@@ -41,6 +43,11 @@ namespace PSInfisicalAPI.Secrets
return secret; return secret;
} }
private static bool IsHiddenPlaceholder(string value)
{
return string.Equals(value, "<hidden-by-infisical>", StringComparison.Ordinal);
}
public static InfisicalSecret[] MapMany(IEnumerable<InfisicalSecretResponseDto> items) public static InfisicalSecret[] MapMany(IEnumerable<InfisicalSecretResponseDto> items)
{ {
if (items == null) if (items == null)
@@ -7,6 +7,7 @@ namespace PSInfisicalAPI.Secrets
public string ProjectId { get; set; } public string ProjectId { get; set; }
public string Environment { get; set; } public string Environment { get; set; }
public string SecretPath { get; set; } public string SecretPath { get; set; }
public string ApiVersion { get; set; }
public bool Recursive { get; set; } public bool Recursive { get; set; }
public bool? IncludeImports { get; set; } public bool? IncludeImports { get; set; }
public bool IncludePersonalOverrides { get; set; } public bool IncludePersonalOverrides { get; set; }
@@ -22,6 +23,7 @@ namespace PSInfisicalAPI.Secrets
public string ProjectId { get; set; } public string ProjectId { get; set; }
public string Environment { get; set; } public string Environment { get; set; }
public string SecretPath { get; set; } public string SecretPath { get; set; }
public string ApiVersion { get; set; }
public int? Version { get; set; } public int? Version { get; set; }
public string Type { get; set; } public string Type { get; set; }
public bool? ViewSecretValue { get; set; } public bool? ViewSecretValue { get; set; }
@@ -32,14 +32,15 @@ namespace PSInfisicalAPI.Secrets
if (connection == null) { throw new ArgumentNullException(nameof(connection)); } if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
if (query == null) { throw new ArgumentNullException(nameof(query)); } if (query == null) { throw new ArgumentNullException(nameof(query)); }
InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(InfisicalEndpointNames.ListSecrets); string resolvedProjectId = FirstNonEmpty(query.ProjectId, connection.ProjectId);
List<KeyValuePair<string, string>> queryParameters = new List<KeyValuePair<string, string>>(); List<KeyValuePair<string, string>> queryParameters = new List<KeyValuePair<string, string>>();
AddIfNotNull(queryParameters, "workspaceId", FirstNonEmpty(query.ProjectId, connection.ProjectId)); AddIfNotNull(queryParameters, "workspaceId", resolvedProjectId);
AddIfNotNull(queryParameters, "projectId", resolvedProjectId);
AddIfNotNull(queryParameters, "environment", FirstNonEmpty(query.Environment, connection.Environment)); AddIfNotNull(queryParameters, "environment", FirstNonEmpty(query.Environment, connection.Environment));
AddIfNotNull(queryParameters, "secretPath", FirstNonEmpty(query.SecretPath, connection.DefaultSecretPath, "/")); AddIfNotNull(queryParameters, "secretPath", FirstNonEmpty(query.SecretPath, connection.DefaultSecretPath, "/"));
queryParameters.Add(new KeyValuePair<string, string>("recursive", query.Recursive ? "true" : "false")); queryParameters.Add(new KeyValuePair<string, string>("recursive", query.Recursive ? "true" : "false"));
if (query.IncludeImports.HasValue) { queryParameters.Add(new KeyValuePair<string, string>("include_imports", query.IncludeImports.Value ? "true" : "false")); } if (query.IncludeImports.HasValue) { queryParameters.Add(new KeyValuePair<string, string>("includeImports", query.IncludeImports.Value ? "true" : "false")); }
if (query.IncludePersonalOverrides) { queryParameters.Add(new KeyValuePair<string, string>("includePersonalOverrides", "true")); } if (query.IncludePersonalOverrides) { queryParameters.Add(new KeyValuePair<string, string>("includePersonalOverrides", "true")); }
if (query.ExpandSecretReferences.HasValue) { queryParameters.Add(new KeyValuePair<string, string>("expandSecretReferences", query.ExpandSecretReferences.Value ? "true" : "false")); } if (query.ExpandSecretReferences.HasValue) { queryParameters.Add(new KeyValuePair<string, string>("expandSecretReferences", query.ExpandSecretReferences.Value ? "true" : "false")); }
if (query.ViewSecretValue.HasValue) { queryParameters.Add(new KeyValuePair<string, string>("viewSecretValue", query.ViewSecretValue.Value ? "true" : "false")); } if (query.ViewSecretValue.HasValue) { queryParameters.Add(new KeyValuePair<string, string>("viewSecretValue", query.ViewSecretValue.Value ? "true" : "false")); }
@@ -60,13 +61,18 @@ namespace PSInfisicalAPI.Secrets
} }
} }
Uri uri = InfisicalUriBuilder.Build(connection.BaseUri, definition, null, queryParameters);
InfisicalHttpResponse response = ExecuteAuthorized(connection, definition, "RetrieveSecrets", uri, null);
try try
{ {
_logger.Information(Component, "Attempting to retrieve Infisical secrets. Please Wait..."); _logger.Information(Component, "Attempting to retrieve Infisical secrets. Please Wait...");
EnsureSuccess(response, definition);
InfisicalHttpResponse response = SendWithVersionFallback(
connection,
InfisicalEndpointNames.ListSecrets,
query.ApiVersion,
"RetrieveSecrets",
null,
queryParameters,
null);
InfisicalSecretListResponseDto dto = _serializer.Deserialize<InfisicalSecretListResponseDto>(response.Body); InfisicalSecretListResponseDto dto = _serializer.Deserialize<InfisicalSecretListResponseDto>(response.Body);
response.Clear(); response.Clear();
@@ -88,27 +94,33 @@ namespace PSInfisicalAPI.Secrets
if (query == null) { throw new ArgumentNullException(nameof(query)); } if (query == null) { throw new ArgumentNullException(nameof(query)); }
if (string.IsNullOrEmpty(query.SecretName)) { throw new InfisicalConfigurationException("SecretName is required."); } if (string.IsNullOrEmpty(query.SecretName)) { throw new InfisicalConfigurationException("SecretName is required."); }
InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(InfisicalEndpointNames.RetrieveSecret);
Dictionary<string, string> pathParameters = new Dictionary<string, string> { { "secretName", query.SecretName } }; Dictionary<string, string> pathParameters = new Dictionary<string, string> { { "secretName", query.SecretName } };
string resolvedProjectId = FirstNonEmpty(query.ProjectId, connection.ProjectId);
List<KeyValuePair<string, string>> queryParameters = new List<KeyValuePair<string, string>>(); List<KeyValuePair<string, string>> queryParameters = new List<KeyValuePair<string, string>>();
AddIfNotNull(queryParameters, "workspaceId", FirstNonEmpty(query.ProjectId, connection.ProjectId)); AddIfNotNull(queryParameters, "workspaceId", resolvedProjectId);
AddIfNotNull(queryParameters, "projectId", resolvedProjectId);
AddIfNotNull(queryParameters, "environment", FirstNonEmpty(query.Environment, connection.Environment)); AddIfNotNull(queryParameters, "environment", FirstNonEmpty(query.Environment, connection.Environment));
AddIfNotNull(queryParameters, "secretPath", FirstNonEmpty(query.SecretPath, connection.DefaultSecretPath, "/")); AddIfNotNull(queryParameters, "secretPath", FirstNonEmpty(query.SecretPath, connection.DefaultSecretPath, "/"));
AddIfNotNull(queryParameters, "type", string.IsNullOrEmpty(query.Type) ? "shared" : query.Type.ToLowerInvariant()); AddIfNotNull(queryParameters, "type", string.IsNullOrEmpty(query.Type) ? "shared" : query.Type.ToLowerInvariant());
if (query.Version.HasValue) { queryParameters.Add(new KeyValuePair<string, string>("version", query.Version.Value.ToString(CultureInfo.InvariantCulture))); } if (query.Version.HasValue) { queryParameters.Add(new KeyValuePair<string, string>("version", query.Version.Value.ToString(CultureInfo.InvariantCulture))); }
if (query.ViewSecretValue.HasValue) { queryParameters.Add(new KeyValuePair<string, string>("viewSecretValue", query.ViewSecretValue.Value ? "true" : "false")); } if (query.ViewSecretValue.HasValue) { queryParameters.Add(new KeyValuePair<string, string>("viewSecretValue", query.ViewSecretValue.Value ? "true" : "false")); }
if (query.ExpandSecretReferences.HasValue) { queryParameters.Add(new KeyValuePair<string, string>("expandSecretReferences", query.ExpandSecretReferences.Value ? "true" : "false")); } if (query.ExpandSecretReferences.HasValue) { queryParameters.Add(new KeyValuePair<string, string>("expandSecretReferences", query.ExpandSecretReferences.Value ? "true" : "false")); }
if (query.IncludeImports.HasValue) { queryParameters.Add(new KeyValuePair<string, string>("include_imports", query.IncludeImports.Value ? "true" : "false")); } if (query.IncludeImports.HasValue) { queryParameters.Add(new KeyValuePair<string, string>("includeImports", query.IncludeImports.Value ? "true" : "false")); }
Uri uri = InfisicalUriBuilder.Build(connection.BaseUri, definition, pathParameters, queryParameters);
InfisicalHttpResponse response = ExecuteAuthorized(connection, definition, "RetrieveSecret", uri, null);
try try
{ {
_logger.Information(Component, string.Concat("Attempting to retrieve Infisical secret '", query.SecretName, "'. Please Wait...")); _logger.Information(Component, string.Concat("Attempting to retrieve Infisical secret '", query.SecretName, "'. Please Wait..."));
EnsureSuccess(response, definition);
InfisicalHttpResponse response = SendWithVersionFallback(
connection,
InfisicalEndpointNames.RetrieveSecret,
query.ApiVersion,
"RetrieveSecret",
pathParameters,
queryParameters,
null);
InfisicalSecretSingleResponseDto dto = _serializer.Deserialize<InfisicalSecretSingleResponseDto>(response.Body); InfisicalSecretSingleResponseDto dto = _serializer.Deserialize<InfisicalSecretSingleResponseDto>(response.Body);
response.Clear(); response.Clear();
@@ -124,6 +136,160 @@ namespace PSInfisicalAPI.Secrets
} }
} }
private InfisicalHttpResponse SendWithVersionFallback(
InfisicalConnection connection,
string endpointName,
string perCallApiVersion,
string operationName,
Dictionary<string, string> pathParameters,
List<KeyValuePair<string, string>> queryParameters,
string body)
{
IReadOnlyList<InfisicalEndpointDefinition> allCandidates = InfisicalEndpointRegistry.GetCandidates(endpointName);
string pinned = FirstNonEmpty(perCallApiVersion, connection.PinnedApiVersion);
string cached;
connection.ResolvedEndpointVersions.TryGetValue(endpointName, out cached);
List<InfisicalEndpointDefinition> candidates = OrderCandidates(allCandidates, pinned, cached);
if (candidates.Count == 0)
{
throw new InfisicalConfigurationException(string.Concat(
"No matching endpoint candidate for '", endpointName,
"' with ApiVersion='", pinned ?? string.Empty, "'."));
}
InfisicalApiException lastException = null;
for (int index = 0; index < candidates.Count; index++)
{
InfisicalEndpointDefinition definition = candidates[index];
Uri uri = InfisicalUriBuilder.Build(connection.BaseUri, definition, pathParameters, queryParameters);
InfisicalHttpResponse response = ExecuteAuthorized(connection, definition, operationName, uri, body);
if (response.StatusCode >= 200 && response.StatusCode < 300)
{
connection.ResolvedEndpointVersions[endpointName] = definition.Version;
return response;
}
InfisicalApiException exception = BuildApiException(response, definition);
response.Clear();
bool hasMoreCandidates = (index + 1) < candidates.Count;
bool pinnedHere = !string.IsNullOrEmpty(pinned);
if (!pinnedHere && hasMoreCandidates && IsVersionMismatch(exception))
{
_logger.Warning(Component, string.Concat(
"Endpoint '", endpointName, "' version '", definition.Version,
"' rejected by server (", exception.StatusCode.ToString(CultureInfo.InvariantCulture),
"); falling back to next candidate."));
lastException = exception;
continue;
}
throw exception;
}
throw lastException ?? new InfisicalApiException(string.Concat(
"All API version candidates exhausted for '", endpointName, "'."));
}
private static List<InfisicalEndpointDefinition> OrderCandidates(
IReadOnlyList<InfisicalEndpointDefinition> allCandidates,
string pinned,
string cached)
{
List<InfisicalEndpointDefinition> ordered = new List<InfisicalEndpointDefinition>();
if (!string.IsNullOrEmpty(pinned))
{
foreach (InfisicalEndpointDefinition candidate in allCandidates)
{
if (string.Equals(candidate.Version, pinned, StringComparison.OrdinalIgnoreCase))
{
ordered.Add(candidate);
}
}
return ordered;
}
if (!string.IsNullOrEmpty(cached))
{
foreach (InfisicalEndpointDefinition candidate in allCandidates)
{
if (string.Equals(candidate.Version, cached, StringComparison.OrdinalIgnoreCase))
{
ordered.Add(candidate);
break;
}
}
foreach (InfisicalEndpointDefinition candidate in allCandidates)
{
if (!string.Equals(candidate.Version, cached, StringComparison.OrdinalIgnoreCase))
{
ordered.Add(candidate);
}
}
return ordered;
}
foreach (InfisicalEndpointDefinition candidate in allCandidates)
{
ordered.Add(candidate);
}
return ordered;
}
private static bool IsVersionMismatch(InfisicalApiException exception)
{
string body = exception.SanitizedBody;
bool hasInfisicalErrorEnvelope = !string.IsNullOrEmpty(body)
&& body.IndexOf("\"reqId\"", StringComparison.OrdinalIgnoreCase) >= 0
&& body.IndexOf("\"error\"", StringComparison.OrdinalIgnoreCase) >= 0;
if (exception.StatusCode == 405)
{
return true;
}
if (exception.StatusCode == 404 && !hasInfisicalErrorEnvelope)
{
return true;
}
if (exception.StatusCode == 400 && !string.IsNullOrEmpty(body))
{
if (body.IndexOf("projectSlug", StringComparison.OrdinalIgnoreCase) >= 0 ||
body.IndexOf("workspaceId", StringComparison.OrdinalIgnoreCase) >= 0)
{
return true;
}
}
return false;
}
private static InfisicalApiException BuildApiException(InfisicalHttpResponse response, InfisicalEndpointDefinition definition)
{
InfisicalApiException exception = new InfisicalApiException(string.Concat(
"Infisical API returned ",
response.StatusCode.ToString(CultureInfo.InvariantCulture),
" (", response.ReasonPhrase ?? string.Empty, ")."));
exception.StatusCode = response.StatusCode;
exception.ReasonPhrase = response.ReasonPhrase;
exception.EndpointName = definition.Name;
exception.RequestMethod = definition.Method;
exception.SanitizedBody = response.Body;
return exception;
}
private InfisicalHttpResponse ExecuteAuthorized(InfisicalConnection connection, InfisicalEndpointDefinition definition, string operationName, Uri uri, string body) private InfisicalHttpResponse ExecuteAuthorized(InfisicalConnection connection, InfisicalEndpointDefinition definition, string operationName, Uri uri, string body)
{ {
Dictionary<string, string> headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); Dictionary<string, string> headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
@@ -157,23 +323,6 @@ namespace PSInfisicalAPI.Secrets
return _httpClient.Send(request); return _httpClient.Send(request);
} }
private static void EnsureSuccess(InfisicalHttpResponse response, InfisicalEndpointDefinition definition)
{
if (response.StatusCode >= 200 && response.StatusCode < 300)
{
return;
}
InfisicalApiException exception = new InfisicalApiException(string.Concat("Infisical API returned ", response.StatusCode.ToString(CultureInfo.InvariantCulture), " (", response.ReasonPhrase ?? string.Empty, ")."));
exception.StatusCode = response.StatusCode;
exception.ReasonPhrase = response.ReasonPhrase;
exception.EndpointName = definition.Name;
exception.RequestMethod = definition.Method;
exception.SanitizedBody = definition.ContainsSecretMaterialInResponse ? "[REDACTED]" : response.Body;
response.Clear();
throw exception;
}
private static void AddIfNotNull(List<KeyValuePair<string, string>> list, string key, string value) private static void AddIfNotNull(List<KeyValuePair<string, string>> list, string key, string value)
{ {
if (!string.IsNullOrEmpty(value)) if (!string.IsNullOrEmpty(value))