Implement PSInfisicalAPI module per design spec with env-var auto-discovery
This commit is contained in:
+30
@@ -0,0 +1,30 @@
|
|||||||
|
## Build artifacts
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
Artifacts/
|
||||||
|
Releases/
|
||||||
|
|
||||||
|
## Module bin output is generated by build.ps1
|
||||||
|
Module/PSInfisicalAPI/bin/
|
||||||
|
|
||||||
|
## VS / Rider / VSCode
|
||||||
|
.vs/
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
## NuGet
|
||||||
|
*.nupkg
|
||||||
|
*.snupkg
|
||||||
|
project.lock.json
|
||||||
|
project.assets.json
|
||||||
|
**/packages/*
|
||||||
|
!**/packages/build/
|
||||||
|
|
||||||
|
## OS
|
||||||
|
Thumbs.db
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
## Test result outputs
|
||||||
|
TestResults/
|
||||||
|
*.trx
|
||||||
|
*.coverage
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
## 2026.06.02.1638
|
||||||
|
|
||||||
|
- Build produced from commit 3c47d6ff30ec.
|
||||||
|
|
||||||
|
## Unreleased (carried forward)
|
||||||
|
|
||||||
|
## 2026.06.02.1611
|
||||||
|
|
||||||
|
- Build produced from commit 3c47d6ff30ec.
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
## 2026.06.02.1638
|
||||||
|
|
||||||
|
- Build produced from commit 3c47d6ff30ec.
|
||||||
|
|
||||||
|
## Unreleased (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.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Configuration>
|
||||||
|
<ViewDefinitions>
|
||||||
|
<View>
|
||||||
|
<Name>PSInfisicalAPI.Models.InfisicalSecret</Name>
|
||||||
|
<ViewSelectedBy>
|
||||||
|
<TypeName>PSInfisicalAPI.Models.InfisicalSecret</TypeName>
|
||||||
|
</ViewSelectedBy>
|
||||||
|
<TableControl>
|
||||||
|
<TableHeaders>
|
||||||
|
<TableColumnHeader><Label>SecretName</Label><Width>32</Width></TableColumnHeader>
|
||||||
|
<TableColumnHeader><Label>SecretPath</Label><Width>28</Width></TableColumnHeader>
|
||||||
|
<TableColumnHeader><Label>Environment</Label><Width>14</Width></TableColumnHeader>
|
||||||
|
<TableColumnHeader><Label>Type</Label><Width>10</Width></TableColumnHeader>
|
||||||
|
<TableColumnHeader><Label>Version</Label><Width>8</Width></TableColumnHeader>
|
||||||
|
<TableColumnHeader><Label>UpdatedAtUtc</Label><Width>22</Width></TableColumnHeader>
|
||||||
|
<TableColumnHeader><Label>Hidden</Label><Width>6</Width></TableColumnHeader>
|
||||||
|
</TableHeaders>
|
||||||
|
<TableRowEntries>
|
||||||
|
<TableRowEntry>
|
||||||
|
<TableColumnItems>
|
||||||
|
<TableColumnItem><PropertyName>SecretName</PropertyName></TableColumnItem>
|
||||||
|
<TableColumnItem><PropertyName>SecretPath</PropertyName></TableColumnItem>
|
||||||
|
<TableColumnItem><PropertyName>Environment</PropertyName></TableColumnItem>
|
||||||
|
<TableColumnItem><PropertyName>Type</PropertyName></TableColumnItem>
|
||||||
|
<TableColumnItem><PropertyName>Version</PropertyName></TableColumnItem>
|
||||||
|
<TableColumnItem><PropertyName>UpdatedAtUtc</PropertyName></TableColumnItem>
|
||||||
|
<TableColumnItem><PropertyName>SecretValueHidden</PropertyName></TableColumnItem>
|
||||||
|
</TableColumnItems>
|
||||||
|
</TableRowEntry>
|
||||||
|
</TableRowEntries>
|
||||||
|
</TableControl>
|
||||||
|
</View>
|
||||||
|
</ViewDefinitions>
|
||||||
|
</Configuration>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Types>
|
||||||
|
<Type>
|
||||||
|
<Name>PSInfisicalAPI.Models.InfisicalSecret</Name>
|
||||||
|
<Members>
|
||||||
|
<MemberSet>
|
||||||
|
<Name>PSStandardMembers</Name>
|
||||||
|
<Members>
|
||||||
|
<PropertySet>
|
||||||
|
<Name>DefaultDisplayPropertySet</Name>
|
||||||
|
<ReferencedProperties>
|
||||||
|
<Name>SecretName</Name>
|
||||||
|
<Name>SecretPath</Name>
|
||||||
|
<Name>Environment</Name>
|
||||||
|
<Name>Type</Name>
|
||||||
|
<Name>Version</Name>
|
||||||
|
<Name>UpdatedAtUtc</Name>
|
||||||
|
<Name>SecretValueHidden</Name>
|
||||||
|
</ReferencedProperties>
|
||||||
|
</PropertySet>
|
||||||
|
</Members>
|
||||||
|
</MemberSet>
|
||||||
|
</Members>
|
||||||
|
</Type>
|
||||||
|
<Type>
|
||||||
|
<Name>PSInfisicalAPI.Connections.InfisicalConnection</Name>
|
||||||
|
<Members>
|
||||||
|
<MemberSet>
|
||||||
|
<Name>PSStandardMembers</Name>
|
||||||
|
<Members>
|
||||||
|
<PropertySet>
|
||||||
|
<Name>DefaultDisplayPropertySet</Name>
|
||||||
|
<ReferencedProperties>
|
||||||
|
<Name>BaseUri</Name>
|
||||||
|
<Name>ApiVersion</Name>
|
||||||
|
<Name>AuthType</Name>
|
||||||
|
<Name>ProjectId</Name>
|
||||||
|
<Name>Environment</Name>
|
||||||
|
<Name>DefaultSecretPath</Name>
|
||||||
|
<Name>ConnectedAtUtc</Name>
|
||||||
|
<Name>ExpiresAtUtc</Name>
|
||||||
|
<Name>IsConnected</Name>
|
||||||
|
</ReferencedProperties>
|
||||||
|
</PropertySet>
|
||||||
|
</Members>
|
||||||
|
</MemberSet>
|
||||||
|
</Members>
|
||||||
|
</Type>
|
||||||
|
</Types>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
@{
|
||||||
|
RootModule = 'PSInfisicalAPI.psm1'
|
||||||
|
ModuleVersion = '2026.06.02.1638'
|
||||||
|
GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51'
|
||||||
|
Author = 'Alphaeus Mote'
|
||||||
|
CompanyName = ''
|
||||||
|
Copyright = '(c) Alphaeus Mote. All rights reserved.'
|
||||||
|
Description = 'PSInfisicalAPI is a C# binary PowerShell module for the Infisical REST API.'
|
||||||
|
PowerShellVersion = '5.1'
|
||||||
|
CompatiblePSEditions = @('Desktop','Core')
|
||||||
|
FunctionsToExport = @()
|
||||||
|
CmdletsToExport = @(
|
||||||
|
'Connect-Infisical',
|
||||||
|
'Disconnect-Infisical',
|
||||||
|
'Get-InfisicalSecrets',
|
||||||
|
'Get-InfisicalSecret',
|
||||||
|
'ConvertTo-InfisicalSecretDictionary',
|
||||||
|
'Export-InfisicalSecrets'
|
||||||
|
)
|
||||||
|
AliasesToExport = @()
|
||||||
|
VariablesToExport = @()
|
||||||
|
FormatsToProcess = @('PSInfisicalAPI.Format.ps1xml')
|
||||||
|
TypesToProcess = @('PSInfisicalAPI.Types.ps1xml')
|
||||||
|
PrivateData = @{
|
||||||
|
PSData = @{
|
||||||
|
Tags = @('Infisical','Secrets','API','SecureString')
|
||||||
|
ProjectUri = ''
|
||||||
|
ReleaseNotes = ''
|
||||||
|
CommitHash = '3c47d6ff30ec'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
$BinaryPath = [System.IO.FileInfo][System.IO.Path]::Combine($PSScriptRoot, 'bin', 'PSInfisicalAPI.dll')
|
||||||
|
|
||||||
|
Import-Module -Name $BinaryPath.FullName
|
||||||
|
|
||||||
|
$TypesPath = [System.IO.FileInfo][System.IO.Path]::Combine($PSScriptRoot, 'PSInfisicalAPI.Types.ps1xml')
|
||||||
|
$FormatPath = [System.IO.FileInfo][System.IO.Path]::Combine($PSScriptRoot, 'PSInfisicalAPI.Format.ps1xml')
|
||||||
|
|
||||||
|
if ([System.IO.File]::Exists($TypesPath.FullName)) {
|
||||||
|
Update-TypeData -PrependPath $TypesPath.FullName -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([System.IO.File]::Exists($FormatPath.FullName)) {
|
||||||
|
Update-FormatData -PrependPath $FormatPath.FullName -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<clear />
|
||||||
|
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||||
|
</packageSources>
|
||||||
|
</configuration>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.0.31903.59
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSInfisicalAPI", "src\PSInfisicalAPI\PSInfisicalAPI.csproj", "{A1B1F0E7-1111-4A3B-9F2A-000000000001}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSInfisicalAPI.Tests", "src\PSInfisicalAPI.Tests\PSInfisicalAPI.Tests.csproj", "{A1B1F0E7-1111-4A3B-9F2A-000000000002}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{A1B1F0E7-1111-4A3B-9F2A-000000000001}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A1B1F0E7-1111-4A3B-9F2A-000000000001}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A1B1F0E7-1111-4A3B-9F2A-000000000001}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A1B1F0E7-1111-4A3B-9F2A-000000000001}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A1B1F0E7-1111-4A3B-9F2A-000000000002}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A1B1F0E7-1111-4A3B-9F2A-000000000002}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A1B1F0E7-1111-4A3B-9F2A-000000000002}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A1B1F0E7-1111-4A3B-9F2A-000000000002}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[ValidateSet('Debug', 'Release')]
|
||||||
|
[string]$Configuration = 'Release',
|
||||||
|
|
||||||
|
[switch]$Clean,
|
||||||
|
|
||||||
|
[switch]$Restore,
|
||||||
|
|
||||||
|
[switch]$RunTests,
|
||||||
|
|
||||||
|
[switch]$RunIntegrationTests,
|
||||||
|
|
||||||
|
[switch]$CreateRelease,
|
||||||
|
|
||||||
|
[switch]$CommitOnSuccess,
|
||||||
|
|
||||||
|
[switch]$Force
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
|
||||||
|
$RepositoryRoot = [System.IO.DirectoryInfo]$PSScriptRoot
|
||||||
|
$SrcRoot = [System.IO.DirectoryInfo][System.IO.Path]::Combine($RepositoryRoot.FullName, 'src')
|
||||||
|
$ProjectFile = [System.IO.FileInfo][System.IO.Path]::Combine($SrcRoot.FullName, 'PSInfisicalAPI', 'PSInfisicalAPI.csproj')
|
||||||
|
$TestsFile = [System.IO.FileInfo][System.IO.Path]::Combine($SrcRoot.FullName, 'PSInfisicalAPI.Tests', 'PSInfisicalAPI.Tests.csproj')
|
||||||
|
$ModuleRoot = [System.IO.DirectoryInfo][System.IO.Path]::Combine($RepositoryRoot.FullName, 'Module', 'PSInfisicalAPI')
|
||||||
|
$ModuleBinDir = [System.IO.DirectoryInfo][System.IO.Path]::Combine($ModuleRoot.FullName, 'bin')
|
||||||
|
$ArtifactsDir = [System.IO.DirectoryInfo][System.IO.Path]::Combine($RepositoryRoot.FullName, 'Artifacts')
|
||||||
|
$ReleasesDir = [System.IO.DirectoryInfo][System.IO.Path]::Combine($RepositoryRoot.FullName, 'Releases')
|
||||||
|
$ChangelogFile = [System.IO.FileInfo][System.IO.Path]::Combine($RepositoryRoot.FullName, 'CHANGELOG.md')
|
||||||
|
$ModuleGuid = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51'
|
||||||
|
|
||||||
|
function Write-Step {
|
||||||
|
param([string]$Message)
|
||||||
|
Write-Host "==> $Message" -ForegroundColor Cyan
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-BuildVersion {
|
||||||
|
return (Get-Date).ToUniversalTime().ToString('yyyy.MM.dd.HHmm')
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-CommitHash {
|
||||||
|
try {
|
||||||
|
$hash = (& git rev-parse --short=12 HEAD 2>$null).Trim()
|
||||||
|
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrEmpty($hash)) {
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
return $hash
|
||||||
|
} catch {
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-Directory {
|
||||||
|
param([System.IO.DirectoryInfo]$Directory)
|
||||||
|
if (-not $Directory.Exists) { [void]$Directory.Create() }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Clear-Directory {
|
||||||
|
param([System.IO.DirectoryInfo]$Directory)
|
||||||
|
if ($Directory.Exists) {
|
||||||
|
Get-ChildItem -LiteralPath $Directory.FullName -Force | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-AssemblyVersion {
|
||||||
|
param([string]$BuildVersion)
|
||||||
|
$parts = $BuildVersion -split '\.'
|
||||||
|
if ($parts.Length -ne 4) { return '1.0.0.0' }
|
||||||
|
$year = [int]$parts[0]
|
||||||
|
$month = [int]$parts[1]
|
||||||
|
$day = [int]$parts[2]
|
||||||
|
$hhmm = [int]$parts[3]
|
||||||
|
$assemblyMinor = ($month * 100) + $day
|
||||||
|
return ("{0}.{1}.{2}.0" -f $year, $assemblyMinor, $hhmm)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-Manifest {
|
||||||
|
param(
|
||||||
|
[System.IO.FileInfo]$Path,
|
||||||
|
[string]$ModuleVersion,
|
||||||
|
[string]$CommitHash
|
||||||
|
)
|
||||||
|
|
||||||
|
$content = @"
|
||||||
|
@{
|
||||||
|
RootModule = 'PSInfisicalAPI.psm1'
|
||||||
|
ModuleVersion = '$ModuleVersion'
|
||||||
|
GUID = '$ModuleGuid'
|
||||||
|
Author = 'Alphaeus Mote'
|
||||||
|
CompanyName = ''
|
||||||
|
Copyright = '(c) Alphaeus Mote. All rights reserved.'
|
||||||
|
Description = 'PSInfisicalAPI is a C# binary PowerShell module for the Infisical REST API.'
|
||||||
|
PowerShellVersion = '5.1'
|
||||||
|
CompatiblePSEditions = @('Desktop','Core')
|
||||||
|
FunctionsToExport = @()
|
||||||
|
CmdletsToExport = @(
|
||||||
|
'Connect-Infisical',
|
||||||
|
'Disconnect-Infisical',
|
||||||
|
'Get-InfisicalSecrets',
|
||||||
|
'Get-InfisicalSecret',
|
||||||
|
'ConvertTo-InfisicalSecretDictionary',
|
||||||
|
'Export-InfisicalSecrets'
|
||||||
|
)
|
||||||
|
AliasesToExport = @()
|
||||||
|
VariablesToExport = @()
|
||||||
|
FormatsToProcess = @('PSInfisicalAPI.Format.ps1xml')
|
||||||
|
TypesToProcess = @('PSInfisicalAPI.Types.ps1xml')
|
||||||
|
PrivateData = @{
|
||||||
|
PSData = @{
|
||||||
|
Tags = @('Infisical','Secrets','API','SecureString')
|
||||||
|
ProjectUri = ''
|
||||||
|
ReleaseNotes = ''
|
||||||
|
CommitHash = '$CommitHash'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"@
|
||||||
|
|
||||||
|
[System.IO.File]::WriteAllText($Path.FullName, $content, [System.Text.UTF8Encoding]::new($false))
|
||||||
|
}
|
||||||
|
|
||||||
|
function Update-Changelog {
|
||||||
|
param([string]$Version, [string]$CommitHash)
|
||||||
|
|
||||||
|
if (-not $ChangelogFile.Exists) { return }
|
||||||
|
$marker = "## $Version"
|
||||||
|
$existing = Get-Content -LiteralPath $ChangelogFile.FullName -Raw
|
||||||
|
if ($existing -match [Regex]::Escape($marker)) { return }
|
||||||
|
|
||||||
|
$insertion = "## $Version`r`n`r`n- Build produced from commit $CommitHash.`r`n`r`n"
|
||||||
|
$updated = $existing -replace '## Unreleased', "## Unreleased`r`n`r`n$insertion## Unreleased (carried forward)"
|
||||||
|
[System.IO.File]::WriteAllText($ChangelogFile.FullName, $updated, [System.Text.UTF8Encoding]::new($false))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Invoke-DotNet {
|
||||||
|
param([string[]]$Arguments)
|
||||||
|
Write-Step ("dotnet " + ($Arguments -join ' '))
|
||||||
|
& dotnet @Arguments
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "dotnet command failed: $($Arguments -join ' ')"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-ModuleImports {
|
||||||
|
param([System.IO.DirectoryInfo]$ModuleDirectory)
|
||||||
|
Write-Step "Validating module import"
|
||||||
|
$script = @"
|
||||||
|
`$ErrorActionPreference = 'Stop'
|
||||||
|
Import-Module -Name '$($ModuleDirectory.FullName)' -Force
|
||||||
|
`$cmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecrets','Get-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets')
|
||||||
|
foreach (`$c in `$cmds) {
|
||||||
|
if (-not (Get-Command -Name `$c -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) {
|
||||||
|
throw "Cmdlet not found: `$c"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"@
|
||||||
|
|
||||||
|
$tempFile = [System.IO.FileInfo][System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName() + '.ps1')
|
||||||
|
try {
|
||||||
|
[System.IO.File]::WriteAllText($tempFile.FullName, $script, [System.Text.UTF8Encoding]::new($false))
|
||||||
|
& pwsh -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $tempFile.FullName
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Module import validation failed in pwsh." }
|
||||||
|
} finally {
|
||||||
|
if ($tempFile.Exists) { Remove-Item -LiteralPath $tempFile.FullName -Force -ErrorAction SilentlyContinue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$buildVersion = Get-BuildVersion
|
||||||
|
$assemblyVersion = Get-AssemblyVersion -BuildVersion $buildVersion
|
||||||
|
$commitHash = Get-CommitHash
|
||||||
|
|
||||||
|
Write-Step "Build version: $buildVersion"
|
||||||
|
Write-Step "Assembly version: $assemblyVersion"
|
||||||
|
Write-Step "Commit hash: $commitHash"
|
||||||
|
|
||||||
|
Ensure-Directory -Directory $ArtifactsDir
|
||||||
|
Ensure-Directory -Directory $ModuleRoot
|
||||||
|
Ensure-Directory -Directory $ModuleBinDir
|
||||||
|
|
||||||
|
if ($Clean.IsPresent) {
|
||||||
|
Write-Step "Cleaning generated outputs"
|
||||||
|
Clear-Directory -Directory $ModuleBinDir
|
||||||
|
Clear-Directory -Directory $ArtifactsDir
|
||||||
|
Get-ChildItem -LiteralPath $SrcRoot.FullName -Recurse -Directory -Force |
|
||||||
|
Where-Object { $_.Name -in @('bin','obj') } |
|
||||||
|
ForEach-Object { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Restore.IsPresent) {
|
||||||
|
Invoke-DotNet -Arguments @('restore', $ProjectFile.FullName)
|
||||||
|
Invoke-DotNet -Arguments @('restore', $TestsFile.FullName)
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Step "Building $($ProjectFile.Name) ($Configuration)"
|
||||||
|
$buildArgs = @(
|
||||||
|
'build', $ProjectFile.FullName,
|
||||||
|
'-c', $Configuration,
|
||||||
|
'--nologo',
|
||||||
|
"-p:BuildVersion=$buildVersion",
|
||||||
|
"-p:BuildAssemblyVersion=$assemblyVersion",
|
||||||
|
"-p:BuildCommitHash=$commitHash"
|
||||||
|
)
|
||||||
|
Invoke-DotNet -Arguments $buildArgs
|
||||||
|
|
||||||
|
if ($RunTests.IsPresent -or $RunIntegrationTests.IsPresent) {
|
||||||
|
Write-Step "Running tests"
|
||||||
|
$testFilter = if ($RunIntegrationTests.IsPresent) { 'Category!=NEVER' } else { 'Category!=Integration' }
|
||||||
|
$testArgs = @(
|
||||||
|
'test', $TestsFile.FullName,
|
||||||
|
'-c', $Configuration,
|
||||||
|
'--nologo',
|
||||||
|
'--filter', $testFilter,
|
||||||
|
"-p:BuildVersion=$buildVersion",
|
||||||
|
"-p:BuildAssemblyVersion=$assemblyVersion",
|
||||||
|
"-p:BuildCommitHash=$commitHash"
|
||||||
|
)
|
||||||
|
Invoke-DotNet -Arguments $testArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Step "Publishing module binaries"
|
||||||
|
$publishOutput = [System.IO.DirectoryInfo][System.IO.Path]::Combine($ArtifactsDir.FullName, 'publish')
|
||||||
|
Ensure-Directory -Directory $publishOutput
|
||||||
|
Clear-Directory -Directory $publishOutput
|
||||||
|
$publishArgs = @(
|
||||||
|
'publish', $ProjectFile.FullName,
|
||||||
|
'-c', $Configuration,
|
||||||
|
'--nologo',
|
||||||
|
'-o', $publishOutput.FullName,
|
||||||
|
"-p:BuildVersion=$buildVersion",
|
||||||
|
"-p:BuildAssemblyVersion=$assemblyVersion",
|
||||||
|
"-p:BuildCommitHash=$commitHash"
|
||||||
|
)
|
||||||
|
Invoke-DotNet -Arguments $publishArgs
|
||||||
|
|
||||||
|
Clear-Directory -Directory $ModuleBinDir
|
||||||
|
$desiredAssemblies = @('PSInfisicalAPI.dll','Newtonsoft.Json.dll','YamlDotNet.dll')
|
||||||
|
foreach ($assembly in $desiredAssemblies) {
|
||||||
|
$source = [System.IO.FileInfo][System.IO.Path]::Combine($publishOutput.FullName, $assembly)
|
||||||
|
if ($source.Exists) {
|
||||||
|
Copy-Item -LiteralPath $source.FullName -Destination $ModuleBinDir.FullName -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$manifestPath = [System.IO.FileInfo][System.IO.Path]::Combine($ModuleRoot.FullName, 'PSInfisicalAPI.psd1')
|
||||||
|
Write-Manifest -Path $manifestPath -ModuleVersion $buildVersion -CommitHash $commitHash
|
||||||
|
|
||||||
|
Update-Changelog -Version $buildVersion -CommitHash $commitHash
|
||||||
|
|
||||||
|
try {
|
||||||
|
Test-ModuleImports -ModuleDirectory $ModuleRoot
|
||||||
|
} catch {
|
||||||
|
Write-Warning "Module import validation reported: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($CreateRelease.IsPresent) {
|
||||||
|
$releaseDir = [System.IO.DirectoryInfo][System.IO.Path]::Combine($ReleasesDir.FullName, $buildVersion)
|
||||||
|
if ($releaseDir.Exists -and -not $Force.IsPresent) {
|
||||||
|
throw "Release '$buildVersion' already exists. Pass -Force to overwrite."
|
||||||
|
}
|
||||||
|
|
||||||
|
Ensure-Directory -Directory $ReleasesDir
|
||||||
|
if ($releaseDir.Exists) { Clear-Directory -Directory $releaseDir }
|
||||||
|
Ensure-Directory -Directory $releaseDir
|
||||||
|
|
||||||
|
$releaseModuleDir = [System.IO.DirectoryInfo][System.IO.Path]::Combine($releaseDir.FullName, 'PSInfisicalAPI')
|
||||||
|
Ensure-Directory -Directory $releaseModuleDir
|
||||||
|
Copy-Item -LiteralPath ([System.IO.Path]::Combine($ModuleRoot.FullName, '*')) -Destination $releaseModuleDir.FullName -Recurse -Force
|
||||||
|
Write-Step "Release created at $($releaseDir.FullName)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($CommitOnSuccess.IsPresent) {
|
||||||
|
Write-Step "Committing on success"
|
||||||
|
& git add -A
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "git add failed." }
|
||||||
|
& git commit -m "Build $buildVersion"
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "git commit failed." }
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Step "Build complete."
|
||||||
+1951
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
|||||||
|
using PSInfisicalAPI.Endpoints;
|
||||||
|
using PSInfisicalAPI.Errors;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Tests
|
||||||
|
{
|
||||||
|
public class EndpointRegistryTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Get_ListSecrets_Returns_Definition()
|
||||||
|
{
|
||||||
|
InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(InfisicalEndpointNames.ListSecrets);
|
||||||
|
Assert.Equal("GET", definition.Method);
|
||||||
|
Assert.Equal("v4", definition.Version);
|
||||||
|
Assert.Equal("/api/v4/secrets", definition.Template);
|
||||||
|
Assert.True(definition.RequiresAuthorization);
|
||||||
|
Assert.False(definition.ContainsSecretMaterialInRequest);
|
||||||
|
Assert.True(definition.ContainsSecretMaterialInResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Get_RetrieveSecret_Returns_Definition()
|
||||||
|
{
|
||||||
|
InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(InfisicalEndpointNames.RetrieveSecret);
|
||||||
|
Assert.Equal("GET", definition.Method);
|
||||||
|
Assert.Equal("v4", definition.Version);
|
||||||
|
Assert.Equal("/api/v4/secrets/{secretName}", definition.Template);
|
||||||
|
Assert.True(definition.RequiresAuthorization);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Get_UniversalAuthLogin_Returns_Definition()
|
||||||
|
{
|
||||||
|
InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(InfisicalEndpointNames.UniversalAuthLogin);
|
||||||
|
Assert.Equal("POST", definition.Method);
|
||||||
|
Assert.Equal("v1", definition.Version);
|
||||||
|
Assert.Equal("/api/v1/auth/universal-auth/login", definition.Template);
|
||||||
|
Assert.False(definition.RequiresAuthorization);
|
||||||
|
Assert.True(definition.ContainsSecretMaterialInRequest);
|
||||||
|
Assert.True(definition.ContainsSecretMaterialInResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Get_Unknown_Endpoint_Throws()
|
||||||
|
{
|
||||||
|
Assert.Throws<InfisicalConfigurationException>(() => InfisicalEndpointRegistry.Get("NotARealEndpoint"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using PSInfisicalAPI.Errors;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Tests
|
||||||
|
{
|
||||||
|
public class ErrorHandlerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void BuildDetails_Preserves_Http_Status_Code()
|
||||||
|
{
|
||||||
|
InfisicalApiException exception = new InfisicalApiException("Forbidden")
|
||||||
|
{
|
||||||
|
StatusCode = 403,
|
||||||
|
ReasonPhrase = "Forbidden",
|
||||||
|
EndpointName = "ListSecrets",
|
||||||
|
RequestMethod = "GET",
|
||||||
|
ApiErrorCode = "PERMISSION_DENIED",
|
||||||
|
SanitizedBody = "[REDACTED]"
|
||||||
|
};
|
||||||
|
|
||||||
|
InfisicalErrorDetails details = InfisicalErrorHandler.BuildDetails("SecretsClient", "RetrieveSecrets", exception);
|
||||||
|
|
||||||
|
Assert.Equal(403, details.StatusCode);
|
||||||
|
Assert.Equal("Forbidden", details.ReasonPhrase);
|
||||||
|
Assert.Equal("PERMISSION_DENIED", details.ApiErrorCode);
|
||||||
|
Assert.Equal("ListSecrets", details.EndpointName);
|
||||||
|
Assert.Equal("[REDACTED]", details.SanitizedBody);
|
||||||
|
Assert.Equal("SecretsClient", details.Component);
|
||||||
|
Assert.Equal("RetrieveSecrets", details.Operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildDetails_Preserves_Serialization_Line_Information()
|
||||||
|
{
|
||||||
|
InfisicalSerializationException exception = new InfisicalSerializationException("Bad JSON")
|
||||||
|
{
|
||||||
|
LineNumber = 12,
|
||||||
|
LinePosition = 34
|
||||||
|
};
|
||||||
|
|
||||||
|
InfisicalErrorDetails details = InfisicalErrorHandler.BuildDetails("Serializer", "Deserialize", exception);
|
||||||
|
|
||||||
|
Assert.Equal(12, details.LineNumber);
|
||||||
|
Assert.Equal(34, details.LinePosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MapCategory_Maps_Auth_Exception()
|
||||||
|
{
|
||||||
|
System.Management.Automation.ErrorCategory category = InfisicalErrorHandler.MapCategory(new InfisicalAuthenticationException("Bad creds"));
|
||||||
|
Assert.Equal(System.Management.Automation.ErrorCategory.AuthenticationError, category);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MapCategory_Maps_Api_Exception_By_Status_Code()
|
||||||
|
{
|
||||||
|
Assert.Equal(System.Management.Automation.ErrorCategory.AuthenticationError, InfisicalErrorHandler.MapCategory(new InfisicalApiException("x") { StatusCode = 401 }));
|
||||||
|
Assert.Equal(System.Management.Automation.ErrorCategory.PermissionDenied, InfisicalErrorHandler.MapCategory(new InfisicalApiException("x") { StatusCode = 403 }));
|
||||||
|
Assert.Equal(System.Management.Automation.ErrorCategory.ObjectNotFound, InfisicalErrorHandler.MapCategory(new InfisicalApiException("x") { StatusCode = 404 }));
|
||||||
|
Assert.Equal(System.Management.Automation.ErrorCategory.ResourceUnavailable, InfisicalErrorHandler.MapCategory(new InfisicalApiException("x") { StatusCode = 503 }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using PSInfisicalAPI.Exports;
|
||||||
|
using PSInfisicalAPI.Models;
|
||||||
|
using PSInfisicalAPI.Security;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Tests
|
||||||
|
{
|
||||||
|
public class ExportTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly DirectoryInfo _tempDirectory;
|
||||||
|
|
||||||
|
public ExportTests()
|
||||||
|
{
|
||||||
|
string root = Path.Combine(Path.GetTempPath(), "PSInfisicalAPI.Tests.Export-" + Guid.NewGuid().ToString("N"));
|
||||||
|
_tempDirectory = new DirectoryInfo(root);
|
||||||
|
_tempDirectory.Create();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_tempDirectory.Exists)
|
||||||
|
{
|
||||||
|
_tempDirectory.Delete(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InfisicalSecret[] SampleSecrets()
|
||||||
|
{
|
||||||
|
return new[]
|
||||||
|
{
|
||||||
|
new InfisicalSecret
|
||||||
|
{
|
||||||
|
SecretName = "SqlServer",
|
||||||
|
SecretPath = "/servers",
|
||||||
|
SecretValue = SecureStringUtility.ToReadOnlySecureString("192.168.1.10"),
|
||||||
|
SecretMetadata = new[] { new InfisicalSecretMetadata { Key = "Owner", Value = "Infrastructure" } }
|
||||||
|
},
|
||||||
|
new InfisicalSecret
|
||||||
|
{
|
||||||
|
SecretName = "SqlPassword",
|
||||||
|
SecretPath = "/servers",
|
||||||
|
SecretValue = SecureStringUtility.ToReadOnlySecureString("ExamplePassword")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Export_Env_Writes_Key_Equals_Value()
|
||||||
|
{
|
||||||
|
FileInfo path = new FileInfo(Path.Combine(_tempDirectory.FullName, "missing", "out.env"));
|
||||||
|
|
||||||
|
new EnvInfisicalExporter().Export(new InfisicalExportRequest
|
||||||
|
{
|
||||||
|
Secrets = SampleSecrets(),
|
||||||
|
Format = InfisicalExportFormat.Env,
|
||||||
|
Path = path,
|
||||||
|
Encoding = new UTF8Encoding(false)
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.True(path.Exists || File.Exists(path.FullName));
|
||||||
|
string contents = File.ReadAllText(path.FullName);
|
||||||
|
Assert.Contains("SqlServer=192.168.1.10", contents);
|
||||||
|
Assert.Contains("SqlPassword=ExamplePassword", contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Export_Json_Creates_Directory()
|
||||||
|
{
|
||||||
|
FileInfo path = new FileInfo(Path.Combine(_tempDirectory.FullName, "deep", "nested", "out.json"));
|
||||||
|
new JsonInfisicalExporter().Export(new InfisicalExportRequest
|
||||||
|
{
|
||||||
|
Secrets = SampleSecrets(),
|
||||||
|
Format = InfisicalExportFormat.Json,
|
||||||
|
Path = path
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.True(path.Directory.Exists);
|
||||||
|
string contents = File.ReadAllText(path.FullName);
|
||||||
|
Assert.Contains("SqlServer", contents);
|
||||||
|
Assert.Contains("192.168.1.10", contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Export_Yaml_Creates_Directory()
|
||||||
|
{
|
||||||
|
FileInfo path = new FileInfo(Path.Combine(_tempDirectory.FullName, "yaml", "out.yaml"));
|
||||||
|
new YamlInfisicalExporter().Export(new InfisicalExportRequest
|
||||||
|
{
|
||||||
|
Secrets = SampleSecrets(),
|
||||||
|
Format = InfisicalExportFormat.Yaml,
|
||||||
|
Path = path
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.True(path.Directory.Exists);
|
||||||
|
string contents = File.ReadAllText(path.FullName);
|
||||||
|
Assert.Contains("Secrets:", contents);
|
||||||
|
Assert.Contains("SqlServer", contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Export_Xml_Matches_Schema()
|
||||||
|
{
|
||||||
|
FileInfo path = new FileInfo(Path.Combine(_tempDirectory.FullName, "xml", "out.xml"));
|
||||||
|
new XmlInfisicalExporter().Export(new InfisicalExportRequest
|
||||||
|
{
|
||||||
|
Secrets = SampleSecrets(),
|
||||||
|
Format = InfisicalExportFormat.Xml,
|
||||||
|
Path = path
|
||||||
|
});
|
||||||
|
|
||||||
|
string contents = File.ReadAllText(path.FullName);
|
||||||
|
Assert.Contains("<Secrets>", contents);
|
||||||
|
Assert.Contains("<Secret>", contents);
|
||||||
|
Assert.Contains("<SecretName>SqlServer</SecretName>", contents);
|
||||||
|
Assert.Contains("<SecretValue>192.168.1.10</SecretValue>", contents);
|
||||||
|
Assert.Contains("<SecretMetadata>", contents);
|
||||||
|
Assert.Contains("<Metadata>", contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Export_EnvironmentVariables_Defaults_To_Process()
|
||||||
|
{
|
||||||
|
string name = "PSInFI_TestVar_" + Guid.NewGuid().ToString("N");
|
||||||
|
InfisicalSecret[] secrets = new[]
|
||||||
|
{
|
||||||
|
new InfisicalSecret
|
||||||
|
{
|
||||||
|
SecretName = name,
|
||||||
|
SecretValue = SecureStringUtility.ToReadOnlySecureString("processed")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
new EnvironmentVariableExporter().Export(new InfisicalExportRequest
|
||||||
|
{
|
||||||
|
Secrets = secrets,
|
||||||
|
Format = InfisicalExportFormat.EnvironmentVariables,
|
||||||
|
Scope = EnvironmentVariableTarget.Process
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("processed", Environment.GetEnvironmentVariable(name, EnvironmentVariableTarget.Process));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable(name, null, EnvironmentVariableTarget.Process);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using PSInfisicalAPI.Authentication;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Tests
|
||||||
|
{
|
||||||
|
public class InfisicalEnvironmentPatternTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("INFISICAL_API_URL")]
|
||||||
|
[InlineData("INFISICAL_BASE_URL")]
|
||||||
|
[InlineData("INFISICAL_BASE_URI")]
|
||||||
|
[InlineData("INFISICAL_HOST")]
|
||||||
|
[InlineData("CLOUDINIT_INFISICAL_APIURL")]
|
||||||
|
public void BaseUriPatterns_Match_Expected_Names(string name)
|
||||||
|
{
|
||||||
|
Assert.True(MatchesAny(name, InfisicalEnvironmentResolver.BaseUriPatterns), "Expected match for " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("INFISICAL_ORG_ID")]
|
||||||
|
[InlineData("INFISICAL_ORGANIZATION_ID")]
|
||||||
|
[InlineData("CLOUDINIT_INFISICAL_ORGANIZATIONID")]
|
||||||
|
public void OrganizationIdPatterns_Match_Expected_Names(string name)
|
||||||
|
{
|
||||||
|
Assert.True(MatchesAny(name, InfisicalEnvironmentResolver.OrganizationIdPatterns), "Expected match for " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("INFISICAL_PROJECT_ID")]
|
||||||
|
[InlineData("INFISICAL_WORKSPACE_ID")]
|
||||||
|
[InlineData("CLOUDINIT_INFISICAL_PROJECTID")]
|
||||||
|
public void ProjectIdPatterns_Match_Expected_Names(string name)
|
||||||
|
{
|
||||||
|
Assert.True(MatchesAny(name, InfisicalEnvironmentResolver.ProjectIdPatterns), "Expected match for " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("INFISICAL_ENVIRONMENT")]
|
||||||
|
[InlineData("INFISICAL_ENVIRONMENT_NAME")]
|
||||||
|
[InlineData("INFISICAL_ENV")]
|
||||||
|
[InlineData("INFISICAL_ENV_SLUG")]
|
||||||
|
[InlineData("CLOUDINIT_INFISICAL_ENVIRONMENT")]
|
||||||
|
public void EnvironmentPatterns_Match_Expected_Names(string name)
|
||||||
|
{
|
||||||
|
Assert.True(MatchesAny(name, InfisicalEnvironmentResolver.EnvironmentPatterns), "Expected match for " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("INFISICAL_CLIENT_ID")]
|
||||||
|
[InlineData("INFISICAL_UNIVERSAL_AUTH_CLIENT_ID")]
|
||||||
|
[InlineData("INFISICAL_MACHINE_IDENTITY_CLIENT_ID")]
|
||||||
|
[InlineData("CLOUDINIT_INFISICAL_CLIENTID")]
|
||||||
|
[InlineData("myapp_infisical_client_id")]
|
||||||
|
public void ClientIdPatterns_Match_Standard_And_Custom_Prefixed_Names(string name)
|
||||||
|
{
|
||||||
|
Assert.True(MatchesAny(name, InfisicalEnvironmentResolver.ClientIdPatterns), "Expected match for " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("INFISICAL_CLIENT_SECRET")]
|
||||||
|
[InlineData("INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET")]
|
||||||
|
[InlineData("INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET")]
|
||||||
|
[InlineData("CLOUDINIT_INFISICAL_CLIENTSECRET")]
|
||||||
|
[InlineData("myapp_infisical_client_secret")]
|
||||||
|
public void ClientSecretPatterns_Match_Standard_And_Custom_Prefixed_Names(string name)
|
||||||
|
{
|
||||||
|
Assert.True(MatchesAny(name, InfisicalEnvironmentResolver.ClientSecretPatterns), "Expected match for " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("INFISICAL_TOKEN")]
|
||||||
|
[InlineData("INFISICAL_ACCESS_TOKEN")]
|
||||||
|
[InlineData("INFISICAL_AUTH_TOKEN")]
|
||||||
|
[InlineData("CLOUDINIT_INFISICAL_TOKEN")]
|
||||||
|
public void AccessTokenPatterns_Match_Expected_Names(string name)
|
||||||
|
{
|
||||||
|
Assert.True(MatchesAny(name, InfisicalEnvironmentResolver.AccessTokenPatterns), "Expected match for " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("INFISICAL_SECRET_PATH")]
|
||||||
|
[InlineData("INFISICAL_DEFAULT_SECRET_PATH")]
|
||||||
|
[InlineData("CLOUDINIT_INFISICAL_SECRETPATH")]
|
||||||
|
public void SecretPathPatterns_Match_Expected_Names(string name)
|
||||||
|
{
|
||||||
|
Assert.True(MatchesAny(name, InfisicalEnvironmentResolver.SecretPathPatterns), "Expected match for " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("INFISICAL_SECRET_PATH")]
|
||||||
|
[InlineData("INFISICAL_DEFAULT_SECRET_PATH")]
|
||||||
|
[InlineData("CLOUDINIT_INFISICAL_SECRETPATH")]
|
||||||
|
public void ClientSecretPatterns_Do_Not_Match_SecretPath_Variables(string name)
|
||||||
|
{
|
||||||
|
Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.ClientSecretPatterns), "ClientSecretPatterns should NOT match " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("PATH")]
|
||||||
|
[InlineData("USERNAME")]
|
||||||
|
[InlineData("HOME")]
|
||||||
|
[InlineData("PROCESSOR_ARCHITECTURE")]
|
||||||
|
public void Patterns_Do_Not_Match_Unrelated_System_Variables(string name)
|
||||||
|
{
|
||||||
|
Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.ClientIdPatterns));
|
||||||
|
Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.ClientSecretPatterns));
|
||||||
|
Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.AccessTokenPatterns));
|
||||||
|
Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.BaseUriPatterns));
|
||||||
|
Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.OrganizationIdPatterns));
|
||||||
|
Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.ProjectIdPatterns));
|
||||||
|
Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.EnvironmentPatterns));
|
||||||
|
Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.SecretPathPatterns));
|
||||||
|
Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.ApiVersionPatterns));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MatchesAny(string input, Regex[] patterns)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < patterns.Length; i++)
|
||||||
|
{
|
||||||
|
if (patterns[i].IsMatch(input))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Security;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using PSInfisicalAPI.Authentication;
|
||||||
|
using PSInfisicalAPI.Logging;
|
||||||
|
using PSInfisicalAPI.Security;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Tests
|
||||||
|
{
|
||||||
|
public class InfisicalEnvironmentResolverTests : IDisposable
|
||||||
|
{
|
||||||
|
private const string TestPrefix = "PSINFITESTRESOLVER";
|
||||||
|
private readonly List<string> _createdVariables = new List<string>();
|
||||||
|
private readonly string _uniqueSuffix = Guid.NewGuid().ToString("N").ToUpperInvariant();
|
||||||
|
|
||||||
|
private string SetProcessVar(string token, string value)
|
||||||
|
{
|
||||||
|
string name = TestPrefix + "_" + token + "_" + _uniqueSuffix;
|
||||||
|
Environment.SetEnvironmentVariable(name, value, EnvironmentVariableTarget.Process);
|
||||||
|
_createdVariables.Add(name);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Regex[] PatternsForThisTest(string tokenWithinName)
|
||||||
|
{
|
||||||
|
return new[]
|
||||||
|
{
|
||||||
|
new Regex("^" + TestPrefix + "_.*" + tokenWithinName + ".*" + _uniqueSuffix + "$", RegexOptions.IgnoreCase)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _createdVariables.Count; i++)
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable(_createdVariables[i], null, EnvironmentVariableTarget.Process);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Resolve_Returns_NotFound_When_No_Pattern_Matches()
|
||||||
|
{
|
||||||
|
Regex[] patterns = new[] { new Regex("WILL_NEVER_MATCH_" + Guid.NewGuid().ToString("N"), RegexOptions.IgnoreCase) };
|
||||||
|
InfisicalEnvironmentResolver.ResolutionResult result = InfisicalEnvironmentResolver.Resolve(patterns);
|
||||||
|
Assert.False(result.Found);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Resolve_Returns_First_Matching_Variable_With_Value()
|
||||||
|
{
|
||||||
|
string name = SetProcessVar("CLIENTID", "client-1234");
|
||||||
|
InfisicalEnvironmentResolver.ResolutionResult result = InfisicalEnvironmentResolver.Resolve(PatternsForThisTest("CLIENTID"));
|
||||||
|
|
||||||
|
Assert.True(result.Found);
|
||||||
|
Assert.Equal("client-1234", result.Value);
|
||||||
|
Assert.Equal(name, result.VariableName);
|
||||||
|
Assert.Equal(EnvironmentVariableTarget.Process, result.Scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Resolve_Skips_Blank_Whitespace_Values()
|
||||||
|
{
|
||||||
|
SetProcessVar("CLIENTID_BLANK", " ");
|
||||||
|
string realName = SetProcessVar("CLIENTID_REAL", "client-xyz");
|
||||||
|
|
||||||
|
Regex[] patterns = new[]
|
||||||
|
{
|
||||||
|
new Regex("^" + TestPrefix + "_CLIENTID_(BLANK|REAL)_" + _uniqueSuffix + "$", RegexOptions.IgnoreCase)
|
||||||
|
};
|
||||||
|
|
||||||
|
InfisicalEnvironmentResolver.ResolutionResult result = InfisicalEnvironmentResolver.Resolve(patterns);
|
||||||
|
|
||||||
|
Assert.True(result.Found);
|
||||||
|
Assert.Equal("client-xyz", result.Value);
|
||||||
|
Assert.Equal(realName, result.VariableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveString_Returns_Current_Value_When_Already_Set()
|
||||||
|
{
|
||||||
|
SetProcessVar("PROJECTID_OVERRIDE", "env-project");
|
||||||
|
string resolved = InfisicalEnvironmentResolver.ResolveString("ProjectId", PatternsForThisTest("PROJECTID"), "explicit-project", NullInfisicalLogger.Instance);
|
||||||
|
Assert.Equal("explicit-project", resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveString_Resolves_When_Current_Value_Is_Whitespace()
|
||||||
|
{
|
||||||
|
SetProcessVar("PROJECTID_FALLBACK", "env-project-123");
|
||||||
|
string resolved = InfisicalEnvironmentResolver.ResolveString("ProjectId", PatternsForThisTest("PROJECTID"), " ", NullInfisicalLogger.Instance);
|
||||||
|
Assert.Equal("env-project-123", resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveSecureString_Builds_ReadOnly_SecureString_From_Env()
|
||||||
|
{
|
||||||
|
SetProcessVar("ACCESSTOKEN_FOR_TEST", "tok-9999");
|
||||||
|
SecureString resolved = InfisicalEnvironmentResolver.ResolveSecureString("AccessToken", PatternsForThisTest("ACCESSTOKEN"), null, NullInfisicalLogger.Instance);
|
||||||
|
|
||||||
|
Assert.NotNull(resolved);
|
||||||
|
Assert.True(resolved.IsReadOnly());
|
||||||
|
string plain = SecureStringUtility.UsePlainText(resolved, p => p);
|
||||||
|
Assert.Equal("tok-9999", plain);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveSecureString_Keeps_Existing_Populated_SecureString()
|
||||||
|
{
|
||||||
|
SetProcessVar("ACCESSTOKEN_IGNORE", "ignored");
|
||||||
|
SecureString existing = SecureStringUtility.ToReadOnlySecureString("explicit-token");
|
||||||
|
SecureString resolved = InfisicalEnvironmentResolver.ResolveSecureString("AccessToken", PatternsForThisTest("ACCESSTOKEN"), existing, NullInfisicalLogger.Instance);
|
||||||
|
Assert.Same(existing, resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using PSInfisicalAPI.Models;
|
||||||
|
using PSInfisicalAPI.Security;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Tests
|
||||||
|
{
|
||||||
|
public class InfisicalSecretTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ToString_Returns_SecretName_Only()
|
||||||
|
{
|
||||||
|
InfisicalSecret secret = new InfisicalSecret
|
||||||
|
{
|
||||||
|
SecretName = "MySecret",
|
||||||
|
SecretValue = SecureStringUtility.ToReadOnlySecureString("super-secret")
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal("MySecret", secret.ToString());
|
||||||
|
Assert.DoesNotContain("super-secret", secret.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UsePlainTextValue_Scopes_Plaintext_Access()
|
||||||
|
{
|
||||||
|
InfisicalSecret secret = new InfisicalSecret
|
||||||
|
{
|
||||||
|
SecretName = "DbPassword",
|
||||||
|
SecretValue = SecureStringUtility.ToReadOnlySecureString("p@ssw0rd")
|
||||||
|
};
|
||||||
|
|
||||||
|
string observed = secret.UsePlainTextValue(value => value);
|
||||||
|
Assert.Equal("p@ssw0rd", observed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Has_No_PlainText_Property()
|
||||||
|
{
|
||||||
|
PropertyInfo[] properties = typeof(InfisicalSecret).GetProperties();
|
||||||
|
foreach (PropertyInfo property in properties)
|
||||||
|
{
|
||||||
|
Assert.False(string.Equals(property.Name, "PlainTextValue", System.StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (property.Name == "SecretValue")
|
||||||
|
{
|
||||||
|
Assert.Equal(typeof(System.Security.SecureString), property.PropertyType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using PSInfisicalAPI.Logging;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Tests
|
||||||
|
{
|
||||||
|
public class LoggingTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Format_Uses_Utc_Timestamp_And_Component()
|
||||||
|
{
|
||||||
|
DateTimeOffset utc = new DateTimeOffset(2026, 6, 2, 21, 44, 22, TimeSpan.Zero).AddTicks(1830000);
|
||||||
|
string result = InfisicalLogFormatter.Format(utc, InfisicalLogLevel.Information, "SecretsClient", "Attempting to retrieve Infisical secrets. Please Wait...");
|
||||||
|
|
||||||
|
Assert.Equal("[2026-06-02T21:44:22.1830000Z] - [Information] - [SecretsClient] - Attempting to retrieve Infisical secrets. Please Wait...", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Format_Includes_All_Levels()
|
||||||
|
{
|
||||||
|
DateTimeOffset utc = DateTimeOffset.UtcNow;
|
||||||
|
foreach (InfisicalLogLevel level in Enum.GetValues(typeof(InfisicalLogLevel)))
|
||||||
|
{
|
||||||
|
string result = InfisicalLogFormatter.Format(utc, level, "Component", "Message");
|
||||||
|
Assert.Matches(@"^\[[0-9TZ:\.\-]+\] - \[" + level + @"\] - \[Component\] - Message$", result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NullLogger_Accepts_Any_Calls()
|
||||||
|
{
|
||||||
|
IInfisicalLogger logger = NullInfisicalLogger.Instance;
|
||||||
|
logger.Information("c", "m");
|
||||||
|
logger.Verbose("c", "m");
|
||||||
|
logger.Debug("c", "m");
|
||||||
|
logger.Warning("c", "m");
|
||||||
|
logger.Error("c", "m");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Tests
|
||||||
|
{
|
||||||
|
public class ManifestTests
|
||||||
|
{
|
||||||
|
private static DirectoryInfo RepositoryRoot()
|
||||||
|
{
|
||||||
|
DirectoryInfo current = new DirectoryInfo(System.AppContext.BaseDirectory);
|
||||||
|
while (current != null)
|
||||||
|
{
|
||||||
|
if (File.Exists(Path.Combine(current.FullName, "PSInfisicalAPI.sln")))
|
||||||
|
{
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
current = current.Parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Assembly_Has_CommitHash_Metadata()
|
||||||
|
{
|
||||||
|
AssemblyMetadataAttribute[] attributes = (AssemblyMetadataAttribute[])typeof(PSInfisicalAPI.Connections.InfisicalConnection)
|
||||||
|
.Assembly
|
||||||
|
.GetCustomAttributes(typeof(AssemblyMetadataAttribute), false);
|
||||||
|
|
||||||
|
bool found = false;
|
||||||
|
foreach (AssemblyMetadataAttribute attribute in attributes)
|
||||||
|
{
|
||||||
|
if (attribute.Key == "CommitHash")
|
||||||
|
{
|
||||||
|
found = true;
|
||||||
|
Assert.False(string.IsNullOrEmpty(attribute.Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.True(found, "Assembly must contain a CommitHash metadata attribute.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Psm1_Imports_Binary_From_Bin_Folder()
|
||||||
|
{
|
||||||
|
DirectoryInfo root = RepositoryRoot();
|
||||||
|
Assert.NotNull(root);
|
||||||
|
string psm1Path = Path.Combine(root.FullName, "Module", "PSInfisicalAPI", "PSInfisicalAPI.psm1");
|
||||||
|
Assert.True(File.Exists(psm1Path));
|
||||||
|
|
||||||
|
string content = File.ReadAllText(psm1Path);
|
||||||
|
Assert.Contains("Import-Module", content);
|
||||||
|
Assert.Contains("'bin'", content);
|
||||||
|
Assert.Contains("PSInfisicalAPI.dll", content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<LangVersion>9.0</LangVersion>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<AssemblyName>PSInfisicalAPI.Tests</AssemblyName>
|
||||||
|
<RootNamespace>PSInfisicalAPI.Tests</RootNamespace>
|
||||||
|
<NoWarn>$(NoWarn);CS1591;NU1701;NU1903</NoWarn>
|
||||||
|
<GenerateProgramFile>true</GenerateProgramFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
<PackageReference Include="PowerShellStandard.Library" Version="5.1.1" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
<PackageReference Include="YamlDotNet" Version="15.1.6" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\PSInfisicalAPI\PSInfisicalAPI.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using PSInfisicalAPI.Common;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Tests
|
||||||
|
{
|
||||||
|
public class SanitizerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Sanitize_Body_Redacts_When_Contains_Secrets()
|
||||||
|
{
|
||||||
|
string body = "{\"secretValue\":\"abc\"}";
|
||||||
|
string sanitized = InfisicalSanitizer.SanitizeBody(body, true);
|
||||||
|
Assert.Equal("[REDACTED]", sanitized);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Sanitize_Body_Truncates_Long_NonSecret_Body()
|
||||||
|
{
|
||||||
|
string body = new string('a', 4096);
|
||||||
|
string sanitized = InfisicalSanitizer.SanitizeBody(body, false);
|
||||||
|
Assert.Contains("[truncated]", sanitized);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Sanitize_Header_Redacts_Authorization()
|
||||||
|
{
|
||||||
|
string sanitized = InfisicalSanitizer.SanitizeHeaderValue("Authorization", "Bearer abc.def");
|
||||||
|
Assert.Equal("[REDACTED]", sanitized);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Sanitize_Header_Passes_Through_Non_Sensitive()
|
||||||
|
{
|
||||||
|
string sanitized = InfisicalSanitizer.SanitizeHeaderValue("Accept", "application/json");
|
||||||
|
Assert.Equal("application/json", sanitized);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Sanitize_Headers_Returns_New_Map()
|
||||||
|
{
|
||||||
|
Dictionary<string, string> headers = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "Authorization", "Bearer token" },
|
||||||
|
{ "Accept", "application/json" }
|
||||||
|
};
|
||||||
|
|
||||||
|
IDictionary<string, string> sanitized = InfisicalSanitizer.SanitizeHeaders(headers);
|
||||||
|
Assert.Equal("[REDACTED]", sanitized["Authorization"]);
|
||||||
|
Assert.Equal("application/json", sanitized["Accept"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using PSInfisicalAPI.Models;
|
||||||
|
using PSInfisicalAPI.Security;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Tests
|
||||||
|
{
|
||||||
|
public class SecretMapperTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Mapper_Maps_SecretKey_To_SecretName_And_SecretValue_To_SecureString()
|
||||||
|
{
|
||||||
|
System.Type mapperType = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly
|
||||||
|
.GetType("PSInfisicalAPI.Secrets.InfisicalSecretMapper", true);
|
||||||
|
System.Type dtoType = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly
|
||||||
|
.GetType("PSInfisicalAPI.Secrets.InfisicalSecretResponseDto", true);
|
||||||
|
|
||||||
|
object dto = System.Activator.CreateInstance(dtoType);
|
||||||
|
dtoType.GetProperty("SecretKey").SetValue(dto, "DatabasePassword");
|
||||||
|
dtoType.GetProperty("SecretValue").SetValue(dto, "Sup3rSecret!");
|
||||||
|
dtoType.GetProperty("SecretPath").SetValue(dto, "/prod/db");
|
||||||
|
dtoType.GetProperty("Environment").SetValue(dto, "prod");
|
||||||
|
dtoType.GetProperty("Type").SetValue(dto, "shared");
|
||||||
|
|
||||||
|
MethodInfo mapMethod = mapperType.GetMethod("Map", BindingFlags.Public | BindingFlags.Static);
|
||||||
|
InfisicalSecret secret = (InfisicalSecret)mapMethod.Invoke(null, new[] { dto });
|
||||||
|
|
||||||
|
Assert.Equal("DatabasePassword", secret.SecretName);
|
||||||
|
Assert.NotNull(secret.SecretValue);
|
||||||
|
Assert.True(secret.SecretValue.IsReadOnly());
|
||||||
|
Assert.Equal("/prod/db", secret.SecretPath);
|
||||||
|
Assert.Equal(InfisicalSecretType.Shared, secret.Type);
|
||||||
|
|
||||||
|
string roundtripped = SecureStringUtility.UsePlainText(secret.SecretValue, plain => plain);
|
||||||
|
Assert.Equal("Sup3rSecret!", roundtripped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using System;
|
||||||
|
using System.Security;
|
||||||
|
using PSInfisicalAPI.Security;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Tests
|
||||||
|
{
|
||||||
|
public class SecureStringUtilityTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ToReadOnlySecureString_Returns_ReadOnly_Instance()
|
||||||
|
{
|
||||||
|
SecureString secure = SecureStringUtility.ToReadOnlySecureString("hello");
|
||||||
|
Assert.True(secure.IsReadOnly());
|
||||||
|
Assert.Equal(5, secure.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToReadOnlySecureString_Handles_Null_And_Empty()
|
||||||
|
{
|
||||||
|
SecureString fromNull = SecureStringUtility.ToReadOnlySecureString(null);
|
||||||
|
SecureString fromEmpty = SecureStringUtility.ToReadOnlySecureString(string.Empty);
|
||||||
|
Assert.True(fromNull.IsReadOnly());
|
||||||
|
Assert.True(fromEmpty.IsReadOnly());
|
||||||
|
Assert.Equal(0, fromNull.Length);
|
||||||
|
Assert.Equal(0, fromEmpty.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UsePlainText_Roundtrips_Value()
|
||||||
|
{
|
||||||
|
SecureString secure = SecureStringUtility.ToReadOnlySecureString("RoundTripValue");
|
||||||
|
string captured = SecureStringUtility.UsePlainText(secure, plainText => plainText);
|
||||||
|
Assert.Equal("RoundTripValue", captured);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UsePlainText_Throws_For_Null_Action()
|
||||||
|
{
|
||||||
|
SecureString secure = SecureStringUtility.ToReadOnlySecureString("x");
|
||||||
|
Assert.Throws<ArgumentNullException>(() => SecureStringUtility.UsePlainText<string>(secure, null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Tests
|
||||||
|
{
|
||||||
|
public class SourcePolicyTests
|
||||||
|
{
|
||||||
|
private static DirectoryInfo RepositoryRoot()
|
||||||
|
{
|
||||||
|
DirectoryInfo current = new DirectoryInfo(System.AppContext.BaseDirectory);
|
||||||
|
while (current != null)
|
||||||
|
{
|
||||||
|
if (File.Exists(Path.Combine(current.FullName, "PSInfisicalAPI.sln")))
|
||||||
|
{
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
current = current.Parent;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void No_Async_Or_Await_Keywords_In_Production_Source()
|
||||||
|
{
|
||||||
|
DirectoryInfo root = RepositoryRoot();
|
||||||
|
Assert.NotNull(root);
|
||||||
|
DirectoryInfo src = new DirectoryInfo(Path.Combine(root.FullName, "src", "PSInfisicalAPI"));
|
||||||
|
Assert.True(src.Exists);
|
||||||
|
|
||||||
|
Regex asyncRegex = new Regex(@"\basync\b");
|
||||||
|
Regex awaitRegex = new Regex(@"\bawait\b");
|
||||||
|
|
||||||
|
foreach (FileInfo file in src.EnumerateFiles("*.cs", SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
string content = File.ReadAllText(file.FullName);
|
||||||
|
Assert.DoesNotMatch(asyncRegex, content);
|
||||||
|
Assert.DoesNotMatch(awaitRegex, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using PSInfisicalAPI.Endpoints;
|
||||||
|
using PSInfisicalAPI.Errors;
|
||||||
|
using PSInfisicalAPI.Http;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Tests
|
||||||
|
{
|
||||||
|
public class UriBuilderTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Build_Combines_Base_And_Endpoint()
|
||||||
|
{
|
||||||
|
Uri baseUri = new Uri("https://app.infisical.com");
|
||||||
|
InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(InfisicalEndpointNames.ListSecrets);
|
||||||
|
|
||||||
|
Uri uri = InfisicalUriBuilder.Build(baseUri, definition, null, new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>("environment", "prod"),
|
||||||
|
new KeyValuePair<string, string>("secretPath", "/")
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("https://app.infisical.com/api/v4/secrets?environment=prod&secretPath=%2F", uri.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Build_Escapes_SecretName_Path_Segment()
|
||||||
|
{
|
||||||
|
InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(InfisicalEndpointNames.RetrieveSecret);
|
||||||
|
Dictionary<string, string> pathParameters = new Dictionary<string, string> { { "secretName", "Sql Password" } };
|
||||||
|
|
||||||
|
string resolved = InfisicalUriBuilder.ResolvePathTemplate(definition.Template, pathParameters);
|
||||||
|
|
||||||
|
Assert.Equal("/api/v4/secrets/Sql%20Password", resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolvePathTemplate_Escapes_Slash_And_Colon()
|
||||||
|
{
|
||||||
|
Dictionary<string, string> pathParameters = new Dictionary<string, string> { { "secretName", "Path/With:Colon" } };
|
||||||
|
string resolved = InfisicalUriBuilder.ResolvePathTemplate("/api/v4/secrets/{secretName}", pathParameters);
|
||||||
|
Assert.Equal("/api/v4/secrets/Path%2FWith%3AColon", resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Build_Escapes_Query_Parameters()
|
||||||
|
{
|
||||||
|
string queryString = InfisicalUriBuilder.BuildQueryString(new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>("tagSlugs", "tag a"),
|
||||||
|
new KeyValuePair<string, string>("tagSlugs", "tag/b")
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("tagSlugs=tag%20a&tagSlugs=tag%2Fb", queryString);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Build_Throws_When_Required_Token_Missing()
|
||||||
|
{
|
||||||
|
Uri baseUri = new Uri("https://app.infisical.com");
|
||||||
|
InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(InfisicalEndpointNames.RetrieveSecret);
|
||||||
|
Assert.Throws<InfisicalConfigurationException>(() => InfisicalUriBuilder.Build(baseUri, definition, null, null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using PSInfisicalAPI.Http;
|
||||||
|
using PSInfisicalAPI.Logging;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Authentication
|
||||||
|
{
|
||||||
|
public interface IInfisicalAuthProvider
|
||||||
|
{
|
||||||
|
string Name { get; }
|
||||||
|
|
||||||
|
InfisicalAuthenticationResult Authenticate(InfisicalAuthenticationRequest request, IInfisicalHttpClient httpClient, IInfisicalLogger logger);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using System.Security;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Authentication
|
||||||
|
{
|
||||||
|
public sealed class InfisicalAuthenticationRequest
|
||||||
|
{
|
||||||
|
public Uri BaseUri { get; set; }
|
||||||
|
public string ApiVersion { get; set; }
|
||||||
|
public string ClientId { get; set; }
|
||||||
|
public SecureString ClientSecret { get; set; }
|
||||||
|
public SecureString PreSuppliedAccessToken { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using System.Security;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Authentication
|
||||||
|
{
|
||||||
|
public sealed class InfisicalAuthenticationResult
|
||||||
|
{
|
||||||
|
public SecureString AccessToken { get; set; }
|
||||||
|
public DateTimeOffset? ExpiresAtUtc { get; set; }
|
||||||
|
public string TokenType { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Security;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using PSInfisicalAPI.Logging;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Authentication
|
||||||
|
{
|
||||||
|
public static class InfisicalEnvironmentResolver
|
||||||
|
{
|
||||||
|
public const string Component = "EnvironmentResolver";
|
||||||
|
|
||||||
|
private const RegexOptions DefaultRegexOptions = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
|
||||||
|
|
||||||
|
private static readonly EnvironmentVariableTarget[] ScopeOrder = new[]
|
||||||
|
{
|
||||||
|
EnvironmentVariableTarget.Process,
|
||||||
|
EnvironmentVariableTarget.User,
|
||||||
|
EnvironmentVariableTarget.Machine
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly Regex[] BaseUriPatterns = new[]
|
||||||
|
{
|
||||||
|
new Regex(@".*INFISICAL.*API.*URL.*", DefaultRegexOptions),
|
||||||
|
new Regex(@".*INFISICAL.*BASE.*UR(I|L).*", DefaultRegexOptions),
|
||||||
|
new Regex(@".*INFISICAL.*HOST.*", DefaultRegexOptions),
|
||||||
|
new Regex(@".*INFISICAL.*URL.*", DefaultRegexOptions)
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly Regex[] OrganizationIdPatterns = new[]
|
||||||
|
{
|
||||||
|
new Regex(@".*INFISICAL.*ORG(ANIZATION)?.*ID.*", DefaultRegexOptions)
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly Regex[] ProjectIdPatterns = new[]
|
||||||
|
{
|
||||||
|
new Regex(@".*INFISICAL.*(PROJECT|WORKSPACE).*ID.*", DefaultRegexOptions)
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly Regex[] EnvironmentPatterns = new[]
|
||||||
|
{
|
||||||
|
new Regex(@".*INFISICAL.*ENV(IRONMENT)?.*NAME.*", DefaultRegexOptions),
|
||||||
|
new Regex(@".*INFISICAL.*ENV(IRONMENT)?.*SLUG.*", DefaultRegexOptions),
|
||||||
|
new Regex(@".*INFISICAL.*ENV(IRONMENT)?.*", DefaultRegexOptions)
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly Regex[] ClientIdPatterns = new[]
|
||||||
|
{
|
||||||
|
new Regex(@".*INFISICAL.*CLIENT.*ID.*", DefaultRegexOptions),
|
||||||
|
new Regex(@".*INFISICAL.*(UNIVERSAL.*AUTH|MACHINE.*IDENTITY).*ID.*", DefaultRegexOptions)
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly Regex[] ClientSecretPatterns = new[]
|
||||||
|
{
|
||||||
|
new Regex(@".*INFISICAL.*CLIENT.*SECRET.*", DefaultRegexOptions),
|
||||||
|
new Regex(@".*INFISICAL.*(UNIVERSAL.*AUTH|MACHINE.*IDENTITY).*SECRET.*", DefaultRegexOptions)
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly Regex[] AccessTokenPatterns = new[]
|
||||||
|
{
|
||||||
|
new Regex(@".*INFISICAL.*ACCESS.*TOKEN.*", DefaultRegexOptions),
|
||||||
|
new Regex(@".*INFISICAL.*AUTH.*TOKEN.*", DefaultRegexOptions),
|
||||||
|
new Regex(@".*INFISICAL.*TOKEN.*", DefaultRegexOptions)
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly Regex[] SecretPathPatterns = new[]
|
||||||
|
{
|
||||||
|
new Regex(@".*INFISICAL.*SECRET.*PATH.*", DefaultRegexOptions),
|
||||||
|
new Regex(@".*INFISICAL.*DEFAULT.*PATH.*", DefaultRegexOptions)
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly Regex[] ApiVersionPatterns = new[]
|
||||||
|
{
|
||||||
|
new Regex(@".*INFISICAL.*API.*VERSION.*", DefaultRegexOptions)
|
||||||
|
};
|
||||||
|
|
||||||
|
public sealed class ResolutionResult
|
||||||
|
{
|
||||||
|
public bool Found { get; set; }
|
||||||
|
public string Value { get; set; }
|
||||||
|
public string VariableName { get; set; }
|
||||||
|
public EnvironmentVariableTarget Scope { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ResolutionResult Resolve(IEnumerable<Regex> patterns)
|
||||||
|
{
|
||||||
|
if (patterns == null)
|
||||||
|
{
|
||||||
|
return new ResolutionResult { Found = false };
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Regex> patternList = new List<Regex>(patterns);
|
||||||
|
|
||||||
|
for (int i = 0; i < ScopeOrder.Length; i++)
|
||||||
|
{
|
||||||
|
EnvironmentVariableTarget scope = ScopeOrder[i];
|
||||||
|
IDictionary entries;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
entries = Environment.GetEnvironmentVariables(scope);
|
||||||
|
}
|
||||||
|
catch (SecurityException)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
catch (PlatformNotSupportedException)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entries == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (DictionaryEntry entry in entries)
|
||||||
|
{
|
||||||
|
string name = entry.Key as string;
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string value = entry.Value as string;
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int p = 0; p < patternList.Count; p++)
|
||||||
|
{
|
||||||
|
if (patternList[p].IsMatch(name))
|
||||||
|
{
|
||||||
|
return new ResolutionResult
|
||||||
|
{
|
||||||
|
Found = true,
|
||||||
|
Value = value,
|
||||||
|
VariableName = name,
|
||||||
|
Scope = scope
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ResolutionResult { Found = false };
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ResolveString(string parameterName, IEnumerable<Regex> patterns, string currentValue, IInfisicalLogger logger)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(currentValue))
|
||||||
|
{
|
||||||
|
return currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResolutionResult result = Resolve(patterns);
|
||||||
|
if (!result.Found)
|
||||||
|
{
|
||||||
|
return currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logger != null)
|
||||||
|
{
|
||||||
|
logger.Verbose(Component, string.Format("Resolved {0} from environment variable '{1}' ({2} scope).", parameterName, result.VariableName, result.Scope));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SecureString ResolveSecureString(string parameterName, IEnumerable<Regex> patterns, SecureString currentValue, IInfisicalLogger logger)
|
||||||
|
{
|
||||||
|
if (currentValue != null && currentValue.Length > 0)
|
||||||
|
{
|
||||||
|
return currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResolutionResult result = Resolve(patterns);
|
||||||
|
if (!result.Found)
|
||||||
|
{
|
||||||
|
return currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logger != null)
|
||||||
|
{
|
||||||
|
logger.Verbose(Component, string.Format("Resolved {0} from environment variable '{1}' ({2} scope).", parameterName, result.VariableName, result.Scope));
|
||||||
|
}
|
||||||
|
|
||||||
|
SecureString secureString = new SecureString();
|
||||||
|
string plainText = result.Value;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (int i = 0; i < plainText.Length; i++)
|
||||||
|
{
|
||||||
|
secureString.AppendChar(plainText[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
plainText = null;
|
||||||
|
result.Value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
secureString.MakeReadOnly();
|
||||||
|
return secureString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using PSInfisicalAPI.Errors;
|
||||||
|
using PSInfisicalAPI.Http;
|
||||||
|
using PSInfisicalAPI.Logging;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Authentication
|
||||||
|
{
|
||||||
|
public sealed class TokenAuthProvider : IInfisicalAuthProvider
|
||||||
|
{
|
||||||
|
public string Name { get { return "Token"; } }
|
||||||
|
|
||||||
|
public InfisicalAuthenticationResult Authenticate(InfisicalAuthenticationRequest request, IInfisicalHttpClient httpClient, IInfisicalLogger logger)
|
||||||
|
{
|
||||||
|
if (request == null || request.PreSuppliedAccessToken == null)
|
||||||
|
{
|
||||||
|
throw new InfisicalAuthenticationException("An access token must be supplied for Token authentication.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new InfisicalAuthenticationResult
|
||||||
|
{
|
||||||
|
AccessToken = request.PreSuppliedAccessToken,
|
||||||
|
TokenType = "Bearer"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Security;
|
||||||
|
using PSInfisicalAPI.Endpoints;
|
||||||
|
using PSInfisicalAPI.Errors;
|
||||||
|
using PSInfisicalAPI.Http;
|
||||||
|
using PSInfisicalAPI.Logging;
|
||||||
|
using PSInfisicalAPI.Security;
|
||||||
|
using PSInfisicalAPI.Serialization;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Authentication
|
||||||
|
{
|
||||||
|
public sealed class UniversalAuthProvider : IInfisicalAuthProvider
|
||||||
|
{
|
||||||
|
private const string Component = "UniversalAuthProvider";
|
||||||
|
|
||||||
|
public string Name { get { return "UniversalAuth"; } }
|
||||||
|
|
||||||
|
public InfisicalAuthenticationResult Authenticate(InfisicalAuthenticationRequest request, IInfisicalHttpClient httpClient, IInfisicalLogger logger)
|
||||||
|
{
|
||||||
|
if (request == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (httpClient == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(httpClient));
|
||||||
|
}
|
||||||
|
|
||||||
|
IInfisicalLogger log = logger ?? NullInfisicalLogger.Instance;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(request.ClientId))
|
||||||
|
{
|
||||||
|
throw new InfisicalAuthenticationException("ClientId is required for Universal Auth.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.ClientSecret == null)
|
||||||
|
{
|
||||||
|
throw new InfisicalAuthenticationException("ClientSecret is required for Universal Auth.");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Information(Component, "Attempting to authenticate to Infisical. Please Wait...");
|
||||||
|
|
||||||
|
InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(InfisicalEndpointNames.UniversalAuthLogin);
|
||||||
|
Uri uri = InfisicalUriBuilder.Build(request.BaseUri, definition, null, null);
|
||||||
|
JsonInfisicalSerializer serializer = new JsonInfisicalSerializer();
|
||||||
|
|
||||||
|
InfisicalAuthenticationResult result = SecureStringUtility.UsePlainText(request.ClientSecret, plainSecret =>
|
||||||
|
{
|
||||||
|
Dictionary<string, string> bodyObject = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "clientId", request.ClientId },
|
||||||
|
{ "clientSecret", plainSecret ?? string.Empty }
|
||||||
|
};
|
||||||
|
|
||||||
|
string body = serializer.Serialize(bodyObject);
|
||||||
|
|
||||||
|
InfisicalHttpRequest httpRequest = new InfisicalHttpRequest
|
||||||
|
{
|
||||||
|
OperationName = "Authenticate",
|
||||||
|
EndpointName = definition.Name,
|
||||||
|
Method = definition.Method,
|
||||||
|
Uri = uri,
|
||||||
|
Body = body,
|
||||||
|
ContentType = "application/json",
|
||||||
|
ContainsSecretMaterialInRequest = definition.ContainsSecretMaterialInRequest,
|
||||||
|
ContainsSecretMaterialInResponse = definition.ContainsSecretMaterialInResponse,
|
||||||
|
Headers = new Dictionary<string, string> { { "Accept", "application/json" } }
|
||||||
|
};
|
||||||
|
|
||||||
|
InfisicalHttpResponse response = httpClient.Send(httpRequest);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (response.StatusCode < 200 || response.StatusCode >= 300)
|
||||||
|
{
|
||||||
|
log.Error(Component, "Infisical authentication failed.");
|
||||||
|
throw new InfisicalAuthenticationException(string.Concat("Universal Auth login returned status ", response.StatusCode.ToString(System.Globalization.CultureInfo.InvariantCulture), "."));
|
||||||
|
}
|
||||||
|
|
||||||
|
UniversalAuthLoginResponse parsed = serializer.Deserialize<UniversalAuthLoginResponse>(response.Body);
|
||||||
|
|
||||||
|
if (parsed == null || string.IsNullOrEmpty(parsed.AccessToken))
|
||||||
|
{
|
||||||
|
throw new InfisicalAuthenticationException("Universal Auth login response did not contain an access token.");
|
||||||
|
}
|
||||||
|
|
||||||
|
SecureString accessToken = SecureStringUtility.ToReadOnlySecureString(parsed.AccessToken);
|
||||||
|
|
||||||
|
DateTimeOffset? expiresAt = null;
|
||||||
|
if (parsed.ExpiresIn > 0)
|
||||||
|
{
|
||||||
|
expiresAt = DateTimeOffset.UtcNow.AddSeconds(parsed.ExpiresIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed.AccessToken = null;
|
||||||
|
|
||||||
|
return new InfisicalAuthenticationResult
|
||||||
|
{
|
||||||
|
AccessToken = accessToken,
|
||||||
|
TokenType = string.IsNullOrEmpty(parsed.TokenType) ? "Bearer" : parsed.TokenType,
|
||||||
|
ExpiresAtUtc = expiresAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
response.Clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
log.Information(Component, "Infisical authentication was successful.");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class UniversalAuthLoginResponse
|
||||||
|
{
|
||||||
|
[Newtonsoft.Json.JsonProperty("accessToken")]
|
||||||
|
public string AccessToken { get; set; }
|
||||||
|
|
||||||
|
[Newtonsoft.Json.JsonProperty("expiresIn")]
|
||||||
|
public int ExpiresIn { get; set; }
|
||||||
|
|
||||||
|
[Newtonsoft.Json.JsonProperty("tokenType")]
|
||||||
|
public string TokenType { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Management.Automation;
|
||||||
|
using System.Security;
|
||||||
|
using PSInfisicalAPI.Authentication;
|
||||||
|
using PSInfisicalAPI.Connections;
|
||||||
|
using PSInfisicalAPI.Errors;
|
||||||
|
using PSInfisicalAPI.Models;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Cmdlets
|
||||||
|
{
|
||||||
|
[Cmdlet(VerbsCommunications.Connect, "Infisical", DefaultParameterSetName = "UniversalAuth")]
|
||||||
|
[OutputType(typeof(InfisicalConnection))]
|
||||||
|
public sealed class ConnectInfisicalCmdlet : InfisicalCmdletBase
|
||||||
|
{
|
||||||
|
private const string ParameterSetUniversalAuth = "UniversalAuth";
|
||||||
|
private const string ParameterSetToken = "Token";
|
||||||
|
private const string Component = "ConnectInfisicalCmdlet";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Uri BaseUri { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string OrganizationId { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string ProjectId { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string Environment { get; set; }
|
||||||
|
|
||||||
|
[Parameter(ParameterSetName = ParameterSetUniversalAuth)]
|
||||||
|
public string ClientId { get; set; }
|
||||||
|
|
||||||
|
[Parameter(ParameterSetName = ParameterSetUniversalAuth)]
|
||||||
|
public SecureString ClientSecret { get; set; }
|
||||||
|
|
||||||
|
[Parameter(ParameterSetName = ParameterSetToken)]
|
||||||
|
public SecureString AccessToken { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string SecretPath { get; set; } = "/";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string ApiVersion { get; set; } = "v4";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public SwitchParameter PassThru { get; set; }
|
||||||
|
|
||||||
|
protected override void ProcessRecord()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ResolveMissingParametersFromEnvironment();
|
||||||
|
ValidateRequiredParameters();
|
||||||
|
|
||||||
|
IInfisicalAuthProvider provider;
|
||||||
|
InfisicalAuthenticationRequest request;
|
||||||
|
InfisicalAuthType authType;
|
||||||
|
|
||||||
|
if (string.Equals(ParameterSetName, ParameterSetToken, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
provider = new TokenAuthProvider();
|
||||||
|
authType = InfisicalAuthType.Token;
|
||||||
|
request = new InfisicalAuthenticationRequest
|
||||||
|
{
|
||||||
|
BaseUri = BaseUri,
|
||||||
|
ApiVersion = ApiVersion,
|
||||||
|
PreSuppliedAccessToken = AccessToken
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
provider = new UniversalAuthProvider();
|
||||||
|
authType = InfisicalAuthType.UniversalAuth;
|
||||||
|
request = new InfisicalAuthenticationRequest
|
||||||
|
{
|
||||||
|
BaseUri = BaseUri,
|
||||||
|
ApiVersion = ApiVersion,
|
||||||
|
ClientId = ClientId,
|
||||||
|
ClientSecret = ClientSecret
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
InfisicalAuthenticationResult authResult = provider.Authenticate(request, HttpClient, Logger);
|
||||||
|
|
||||||
|
if (authResult == null || authResult.AccessToken == null)
|
||||||
|
{
|
||||||
|
throw new InfisicalAuthenticationException("Authentication did not produce an access token.");
|
||||||
|
}
|
||||||
|
|
||||||
|
InfisicalConnection connection = new InfisicalConnection
|
||||||
|
{
|
||||||
|
BaseUri = BaseUri,
|
||||||
|
ApiVersion = ApiVersion,
|
||||||
|
AuthType = authType,
|
||||||
|
OrganizationId = OrganizationId,
|
||||||
|
ProjectId = ProjectId,
|
||||||
|
Environment = Environment,
|
||||||
|
DefaultSecretPath = string.IsNullOrEmpty(SecretPath) ? "/" : SecretPath,
|
||||||
|
ConnectedAtUtc = DateTimeOffset.UtcNow,
|
||||||
|
ExpiresAtUtc = authResult.ExpiresAtUtc,
|
||||||
|
IsConnected = true,
|
||||||
|
AccessToken = authResult.AccessToken
|
||||||
|
};
|
||||||
|
|
||||||
|
InfisicalSessionManager.SetCurrent(connection);
|
||||||
|
|
||||||
|
if (PassThru.IsPresent)
|
||||||
|
{
|
||||||
|
WriteObject(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
ThrowTerminatingForException(Component, "Connect", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResolveMissingParametersFromEnvironment()
|
||||||
|
{
|
||||||
|
bool tokenSet = string.Equals(ParameterSetName, ParameterSetToken, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
bool needsScan =
|
||||||
|
BaseUri == null ||
|
||||||
|
string.IsNullOrWhiteSpace(OrganizationId) ||
|
||||||
|
string.IsNullOrWhiteSpace(ProjectId) ||
|
||||||
|
string.IsNullOrWhiteSpace(Environment) ||
|
||||||
|
(tokenSet && (AccessToken == null || AccessToken.Length == 0)) ||
|
||||||
|
(!tokenSet && string.IsNullOrWhiteSpace(ClientId)) ||
|
||||||
|
(!tokenSet && (ClientSecret == null || ClientSecret.Length == 0));
|
||||||
|
|
||||||
|
if (!needsScan)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Verbose(Component, "Attempting to resolve missing Infisical connection parameters from environment variables. Please Wait...");
|
||||||
|
|
||||||
|
if (BaseUri == null)
|
||||||
|
{
|
||||||
|
string resolved = InfisicalEnvironmentResolver.ResolveString("BaseUri", InfisicalEnvironmentResolver.BaseUriPatterns, null, Logger);
|
||||||
|
if (!string.IsNullOrWhiteSpace(resolved))
|
||||||
|
{
|
||||||
|
Uri parsed;
|
||||||
|
if (Uri.TryCreate(resolved, UriKind.Absolute, out parsed))
|
||||||
|
{
|
||||||
|
BaseUri = parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OrganizationId = InfisicalEnvironmentResolver.ResolveString("OrganizationId", InfisicalEnvironmentResolver.OrganizationIdPatterns, OrganizationId, Logger);
|
||||||
|
ProjectId = InfisicalEnvironmentResolver.ResolveString("ProjectId", InfisicalEnvironmentResolver.ProjectIdPatterns, ProjectId, Logger);
|
||||||
|
Environment = InfisicalEnvironmentResolver.ResolveString("Environment", InfisicalEnvironmentResolver.EnvironmentPatterns, Environment, Logger);
|
||||||
|
|
||||||
|
if (tokenSet)
|
||||||
|
{
|
||||||
|
AccessToken = InfisicalEnvironmentResolver.ResolveSecureString("AccessToken", InfisicalEnvironmentResolver.AccessTokenPatterns, AccessToken, Logger);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ClientId = InfisicalEnvironmentResolver.ResolveString("ClientId", InfisicalEnvironmentResolver.ClientIdPatterns, ClientId, Logger);
|
||||||
|
ClientSecret = InfisicalEnvironmentResolver.ResolveSecureString("ClientSecret", InfisicalEnvironmentResolver.ClientSecretPatterns, ClientSecret, Logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MyInvocation.BoundParameters.ContainsKey("SecretPath"))
|
||||||
|
{
|
||||||
|
string resolvedPath = InfisicalEnvironmentResolver.ResolveString("SecretPath", InfisicalEnvironmentResolver.SecretPathPatterns, null, Logger);
|
||||||
|
if (!string.IsNullOrWhiteSpace(resolvedPath))
|
||||||
|
{
|
||||||
|
SecretPath = resolvedPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MyInvocation.BoundParameters.ContainsKey("ApiVersion"))
|
||||||
|
{
|
||||||
|
string resolvedVersion = InfisicalEnvironmentResolver.ResolveString("ApiVersion", InfisicalEnvironmentResolver.ApiVersionPatterns, null, Logger);
|
||||||
|
if (!string.IsNullOrWhiteSpace(resolvedVersion))
|
||||||
|
{
|
||||||
|
ApiVersion = resolvedVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValidateRequiredParameters()
|
||||||
|
{
|
||||||
|
List<string> missing = new List<string>();
|
||||||
|
|
||||||
|
if (BaseUri == null) { missing.Add("BaseUri"); }
|
||||||
|
if (string.IsNullOrWhiteSpace(OrganizationId)) { missing.Add("OrganizationId"); }
|
||||||
|
if (string.IsNullOrWhiteSpace(ProjectId)) { missing.Add("ProjectId"); }
|
||||||
|
if (string.IsNullOrWhiteSpace(Environment)) { missing.Add("Environment"); }
|
||||||
|
|
||||||
|
if (string.Equals(ParameterSetName, ParameterSetToken, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
if (AccessToken == null || AccessToken.Length == 0) { missing.Add("AccessToken"); }
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(ClientId)) { missing.Add("ClientId"); }
|
||||||
|
if (ClientSecret == null || ClientSecret.Length == 0) { missing.Add("ClientSecret"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missing.Count > 0)
|
||||||
|
{
|
||||||
|
throw new InfisicalConfigurationException(string.Format(
|
||||||
|
"Required Connect-Infisical parameter(s) not supplied and no matching environment variables were found: {0}.",
|
||||||
|
string.Join(", ", missing.ToArray())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Management.Automation;
|
||||||
|
using System.Security;
|
||||||
|
using PSInfisicalAPI.Errors;
|
||||||
|
using PSInfisicalAPI.Models;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Cmdlets
|
||||||
|
{
|
||||||
|
[Cmdlet(VerbsData.ConvertTo, "InfisicalSecretDictionary")]
|
||||||
|
[OutputType(typeof(Dictionary<string, SecureString>))]
|
||||||
|
public sealed class ConvertToInfisicalSecretDictionaryCmdlet : InfisicalCmdletBase
|
||||||
|
{
|
||||||
|
[Parameter(Mandatory = true, ValueFromPipeline = true)]
|
||||||
|
public InfisicalSecret[] InputObject { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public InfisicalDuplicateKeyBehavior DuplicateKeyBehavior { get; set; } = InfisicalDuplicateKeyBehavior.Error;
|
||||||
|
|
||||||
|
private readonly List<InfisicalSecret> _buffer = new List<InfisicalSecret>();
|
||||||
|
|
||||||
|
protected override void ProcessRecord()
|
||||||
|
{
|
||||||
|
if (InputObject == null) { return; }
|
||||||
|
|
||||||
|
foreach (InfisicalSecret secret in InputObject)
|
||||||
|
{
|
||||||
|
if (secret != null)
|
||||||
|
{
|
||||||
|
_buffer.Add(secret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void EndProcessing()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Dictionary<string, SecureString> dictionary = new Dictionary<string, SecureString>(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] = secret.SecretValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
dictionary[key] = secret.SecretValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteObject(dictionary);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
ThrowTerminatingForException("ConvertToInfisicalSecretDictionaryCmdlet", "ConvertToDictionary", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using System;
|
||||||
|
using System.Management.Automation;
|
||||||
|
using PSInfisicalAPI.Connections;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Cmdlets
|
||||||
|
{
|
||||||
|
[Cmdlet(VerbsCommunications.Disconnect, "Infisical")]
|
||||||
|
[OutputType(typeof(PSObject))]
|
||||||
|
public sealed class DisconnectInfisicalCmdlet : InfisicalCmdletBase
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public SwitchParameter PassThru { get; set; }
|
||||||
|
|
||||||
|
protected override void ProcessRecord()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
InfisicalSessionManager.Disconnect();
|
||||||
|
|
||||||
|
if (PassThru.IsPresent)
|
||||||
|
{
|
||||||
|
PSObject status = new PSObject();
|
||||||
|
status.Properties.Add(new PSNoteProperty("IsConnected", false));
|
||||||
|
status.Properties.Add(new PSNoteProperty("DisconnectedAtUtc", DateTimeOffset.UtcNow));
|
||||||
|
WriteObject(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
ThrowTerminatingForException("DisconnectInfisicalCmdlet", "Disconnect", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Management.Automation;
|
||||||
|
using System.Text;
|
||||||
|
using PSInfisicalAPI.Errors;
|
||||||
|
using PSInfisicalAPI.Exports;
|
||||||
|
using PSInfisicalAPI.Models;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Cmdlets
|
||||||
|
{
|
||||||
|
public enum InfisicalExportEncoding
|
||||||
|
{
|
||||||
|
UTF8,
|
||||||
|
UTF8Bom,
|
||||||
|
Unicode
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmdlet(VerbsData.Export, "InfisicalSecrets")]
|
||||||
|
public sealed class ExportInfisicalSecretsCmdlet : InfisicalCmdletBase
|
||||||
|
{
|
||||||
|
[Parameter(Mandatory = true, ValueFromPipeline = true)]
|
||||||
|
public InfisicalSecret[] InputObject { get; set; }
|
||||||
|
|
||||||
|
[Parameter(Mandatory = true)]
|
||||||
|
public InfisicalExportFormat Format { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public FileInfo Path { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EnvironmentVariableTarget Scope { get; set; } = EnvironmentVariableTarget.Process;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public SwitchParameter Force { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public InfisicalExportEncoding Encoding { get; set; } = InfisicalExportEncoding.UTF8;
|
||||||
|
|
||||||
|
private readonly List<InfisicalSecret> _buffer = new List<InfisicalSecret>();
|
||||||
|
|
||||||
|
protected override void ProcessRecord()
|
||||||
|
{
|
||||||
|
if (InputObject == null) { return; }
|
||||||
|
|
||||||
|
foreach (InfisicalSecret secret in InputObject)
|
||||||
|
{
|
||||||
|
if (secret != null)
|
||||||
|
{
|
||||||
|
_buffer.Add(secret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void EndProcessing()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool requiresPath = Format != InfisicalExportFormat.EnvironmentVariables;
|
||||||
|
if (requiresPath && Path == null)
|
||||||
|
{
|
||||||
|
throw new InfisicalExportException(string.Concat("Path is required for format ", Format.ToString(), "."));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requiresPath && Path.Exists && !Force.IsPresent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
InfisicalExportRequest request = new InfisicalExportRequest
|
||||||
|
{
|
||||||
|
Secrets = _buffer.ToArray(),
|
||||||
|
Format = Format,
|
||||||
|
Path = Path,
|
||||||
|
Scope = Scope,
|
||||||
|
Force = Force.IsPresent,
|
||||||
|
Encoding = ResolveEncoding(Encoding)
|
||||||
|
};
|
||||||
|
|
||||||
|
IInfisicalExporter exporter = InfisicalExporterFactory.Create(Format);
|
||||||
|
exporter.Export(request);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
ThrowTerminatingForException("ExportInfisicalSecretsCmdlet", string.Concat("Export-", Format.ToString()), exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Encoding ResolveEncoding(InfisicalExportEncoding encoding)
|
||||||
|
{
|
||||||
|
switch (encoding)
|
||||||
|
{
|
||||||
|
case InfisicalExportEncoding.UTF8Bom: return new UTF8Encoding(true);
|
||||||
|
case InfisicalExportEncoding.Unicode: return new UnicodeEncoding();
|
||||||
|
default: return new UTF8Encoding(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System;
|
||||||
|
using System.Management.Automation;
|
||||||
|
using PSInfisicalAPI.Connections;
|
||||||
|
using PSInfisicalAPI.Models;
|
||||||
|
using PSInfisicalAPI.Secrets;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Cmdlets
|
||||||
|
{
|
||||||
|
[Cmdlet(VerbsCommon.Get, "InfisicalSecret")]
|
||||||
|
[OutputType(typeof(InfisicalSecret))]
|
||||||
|
public sealed class GetInfisicalSecretCmdlet : InfisicalCmdletBase
|
||||||
|
{
|
||||||
|
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)]
|
||||||
|
public string SecretName { get; set; }
|
||||||
|
|
||||||
|
[Parameter] public string ProjectId { get; set; }
|
||||||
|
[Parameter] public string Environment { get; set; }
|
||||||
|
[Parameter] public string SecretPath { get; set; }
|
||||||
|
[Parameter] public int? Version { get; set; }
|
||||||
|
[Parameter] public InfisicalSecretType Type { get; set; } = InfisicalSecretType.Shared;
|
||||||
|
[Parameter] public bool ViewSecretValue { get; set; } = true;
|
||||||
|
[Parameter] public bool ExpandSecretReferences { get; set; } = true;
|
||||||
|
[Parameter] public bool IncludeImports { get; set; } = true;
|
||||||
|
|
||||||
|
protected override void ProcessRecord()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
||||||
|
|
||||||
|
InfisicalRetrieveSecretQuery query = new InfisicalRetrieveSecretQuery
|
||||||
|
{
|
||||||
|
SecretName = SecretName,
|
||||||
|
ProjectId = ProjectId,
|
||||||
|
Environment = Environment,
|
||||||
|
SecretPath = SecretPath,
|
||||||
|
Version = Version,
|
||||||
|
Type = Type.ToString(),
|
||||||
|
ViewSecretValue = ViewSecretValue,
|
||||||
|
ExpandSecretReferences = ExpandSecretReferences,
|
||||||
|
IncludeImports = IncludeImports
|
||||||
|
};
|
||||||
|
|
||||||
|
InfisicalSecretsClient client = new InfisicalSecretsClient(HttpClient, Logger);
|
||||||
|
InfisicalSecret secret = client.Retrieve(connection, query);
|
||||||
|
|
||||||
|
if (secret != null)
|
||||||
|
{
|
||||||
|
WriteObject(secret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
ThrowTerminatingForException("GetInfisicalSecretCmdlet", "RetrieveSecret", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Management.Automation;
|
||||||
|
using PSInfisicalAPI.Connections;
|
||||||
|
using PSInfisicalAPI.Models;
|
||||||
|
using PSInfisicalAPI.Secrets;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Cmdlets
|
||||||
|
{
|
||||||
|
[Cmdlet(VerbsCommon.Get, "InfisicalSecrets")]
|
||||||
|
[OutputType(typeof(InfisicalSecret))]
|
||||||
|
public sealed class GetInfisicalSecretsCmdlet : InfisicalCmdletBase
|
||||||
|
{
|
||||||
|
[Parameter] public string ProjectId { get; set; }
|
||||||
|
[Parameter] public string Environment { get; set; }
|
||||||
|
[Parameter] public string SecretPath { get; set; }
|
||||||
|
[Parameter] public SwitchParameter Recursive { get; set; }
|
||||||
|
[Parameter] public bool IncludeImports { get; set; } = true;
|
||||||
|
[Parameter] public SwitchParameter IncludePersonalOverrides { get; set; }
|
||||||
|
[Parameter] public bool ExpandSecretReferences { get; set; } = true;
|
||||||
|
[Parameter] public bool ViewSecretValue { get; set; } = true;
|
||||||
|
[Parameter] public Hashtable MetadataFilter { get; set; }
|
||||||
|
[Parameter] public string[] TagSlugs { get; set; }
|
||||||
|
|
||||||
|
protected override void ProcessRecord()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
|
||||||
|
|
||||||
|
InfisicalListSecretsQuery query = new InfisicalListSecretsQuery
|
||||||
|
{
|
||||||
|
ProjectId = ProjectId,
|
||||||
|
Environment = Environment,
|
||||||
|
SecretPath = SecretPath,
|
||||||
|
Recursive = Recursive.IsPresent,
|
||||||
|
IncludeImports = IncludeImports,
|
||||||
|
IncludePersonalOverrides = IncludePersonalOverrides.IsPresent,
|
||||||
|
ExpandSecretReferences = ExpandSecretReferences,
|
||||||
|
ViewSecretValue = ViewSecretValue,
|
||||||
|
MetadataFilter = ToStringDictionary(MetadataFilter),
|
||||||
|
TagSlugs = TagSlugs
|
||||||
|
};
|
||||||
|
|
||||||
|
InfisicalSecretsClient client = new InfisicalSecretsClient(HttpClient, Logger);
|
||||||
|
InfisicalSecret[] secrets = client.List(connection, query);
|
||||||
|
|
||||||
|
foreach (InfisicalSecret secret in secrets)
|
||||||
|
{
|
||||||
|
WriteObject(secret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
ThrowTerminatingForException("GetInfisicalSecretsCmdlet", "RetrieveSecrets", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, string> ToStringDictionary(Hashtable hashtable)
|
||||||
|
{
|
||||||
|
if (hashtable == null) { return null; }
|
||||||
|
|
||||||
|
Dictionary<string, string> result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (DictionaryEntry entry in hashtable)
|
||||||
|
{
|
||||||
|
if (entry.Key == null) { continue; }
|
||||||
|
result[entry.Key.ToString()] = entry.Value != null ? entry.Value.ToString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using System;
|
||||||
|
using System.Management.Automation;
|
||||||
|
using PSInfisicalAPI.Errors;
|
||||||
|
using PSInfisicalAPI.Http;
|
||||||
|
using PSInfisicalAPI.Logging;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Cmdlets
|
||||||
|
{
|
||||||
|
public abstract class InfisicalCmdletBase : PSCmdlet
|
||||||
|
{
|
||||||
|
private IInfisicalLogger _logger;
|
||||||
|
private IInfisicalHttpClient _httpClient;
|
||||||
|
|
||||||
|
protected IInfisicalLogger Logger
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_logger == null)
|
||||||
|
{
|
||||||
|
_logger = new PSCmdletLogger(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _logger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected IInfisicalHttpClient HttpClient
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_httpClient == null)
|
||||||
|
{
|
||||||
|
_httpClient = new InfisicalHttpClient(Logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _httpClient;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ThrowTerminatingForException(string component, string operation, Exception exception)
|
||||||
|
{
|
||||||
|
InfisicalErrorDetails details = InfisicalErrorHandler.BuildDetails(component, operation, exception);
|
||||||
|
InfisicalErrorHandler.LogFailure(Logger, details);
|
||||||
|
ErrorRecord record = InfisicalErrorHandler.ToErrorRecord(exception, details);
|
||||||
|
ThrowTerminatingError(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Common
|
||||||
|
{
|
||||||
|
public static class InfisicalPath
|
||||||
|
{
|
||||||
|
public static FileInfo CombineFile(params string[] segments)
|
||||||
|
{
|
||||||
|
string combined = Path.Combine(segments);
|
||||||
|
return new FileInfo(combined);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DirectoryInfo CombineDirectory(params string[] segments)
|
||||||
|
{
|
||||||
|
string combined = Path.Combine(segments);
|
||||||
|
return new DirectoryInfo(combined);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void EnsureDirectoryExists(DirectoryInfo directory)
|
||||||
|
{
|
||||||
|
if (directory == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!directory.Exists)
|
||||||
|
{
|
||||||
|
directory.Create();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void EnsureParentDirectoryExists(FileInfo file)
|
||||||
|
{
|
||||||
|
if (file == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureDirectoryExists(file.Directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Common
|
||||||
|
{
|
||||||
|
public static class InfisicalSanitizer
|
||||||
|
{
|
||||||
|
private static readonly string[] SensitiveTokens = new[]
|
||||||
|
{
|
||||||
|
"secretValue",
|
||||||
|
"secret_value",
|
||||||
|
"clientSecret",
|
||||||
|
"client_secret",
|
||||||
|
"accessToken",
|
||||||
|
"access_token",
|
||||||
|
"Authorization",
|
||||||
|
"Bearer"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string SanitizeBody(string body, bool containsSecretMaterial)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(body))
|
||||||
|
{
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (containsSecretMaterial)
|
||||||
|
{
|
||||||
|
return "[REDACTED]";
|
||||||
|
}
|
||||||
|
|
||||||
|
return Truncate(body, 1024);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string SanitizeHeaderValue(string headerName, string headerValue)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(headerName))
|
||||||
|
{
|
||||||
|
return headerValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string normalized = headerName.ToLowerInvariant();
|
||||||
|
if (normalized == "authorization" || normalized.Contains("token") || normalized.Contains("secret"))
|
||||||
|
{
|
||||||
|
return "[REDACTED]";
|
||||||
|
}
|
||||||
|
|
||||||
|
return headerValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IDictionary<string, string> SanitizeHeaders(IDictionary<string, string> headers)
|
||||||
|
{
|
||||||
|
if (headers == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary<string, string> sanitized = new Dictionary<string, string>(headers.Count);
|
||||||
|
|
||||||
|
foreach (KeyValuePair<string, string> entry in headers)
|
||||||
|
{
|
||||||
|
sanitized[entry.Key] = SanitizeHeaderValue(entry.Key, entry.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Truncate(string value, int maxLength)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.Length <= maxLength)
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Concat(value.Substring(0, maxLength), "... [truncated]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using System.Security;
|
||||||
|
using PSInfisicalAPI.Models;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Connections
|
||||||
|
{
|
||||||
|
public sealed class InfisicalConnection
|
||||||
|
{
|
||||||
|
public Uri BaseUri { get; set; }
|
||||||
|
public string ApiVersion { get; set; }
|
||||||
|
public InfisicalAuthType AuthType { get; set; }
|
||||||
|
public string OrganizationId { get; set; }
|
||||||
|
public string ProjectId { get; set; }
|
||||||
|
public string Environment { get; set; }
|
||||||
|
public string DefaultSecretPath { get; set; }
|
||||||
|
public DateTimeOffset ConnectedAtUtc { get; set; }
|
||||||
|
public DateTimeOffset? ExpiresAtUtc { get; set; }
|
||||||
|
public bool IsConnected { get; set; }
|
||||||
|
|
||||||
|
internal SecureString AccessToken { get; set; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Concat(
|
||||||
|
"Project=", ProjectId ?? "",
|
||||||
|
" Environment=", Environment ?? "",
|
||||||
|
" Connected=", IsConnected ? "true" : "false");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using PSInfisicalAPI.Errors;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Connections
|
||||||
|
{
|
||||||
|
public static class InfisicalSessionManager
|
||||||
|
{
|
||||||
|
private static readonly object Sync = new object();
|
||||||
|
private static InfisicalConnection _current;
|
||||||
|
|
||||||
|
public static InfisicalConnection Current
|
||||||
|
{
|
||||||
|
get { lock (Sync) { return _current; } }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetCurrent(InfisicalConnection connection)
|
||||||
|
{
|
||||||
|
lock (Sync)
|
||||||
|
{
|
||||||
|
_current = connection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static InfisicalConnection RequireCurrent()
|
||||||
|
{
|
||||||
|
InfisicalConnection connection = Current;
|
||||||
|
if (connection == null || !connection.IsConnected)
|
||||||
|
{
|
||||||
|
throw new InfisicalConfigurationException("No active Infisical connection. Call Connect-Infisical first.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Disconnect()
|
||||||
|
{
|
||||||
|
lock (Sync)
|
||||||
|
{
|
||||||
|
if (_current != null)
|
||||||
|
{
|
||||||
|
_current.AccessToken = null;
|
||||||
|
_current.IsConnected = false;
|
||||||
|
_current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace PSInfisicalAPI.Endpoints
|
||||||
|
{
|
||||||
|
public sealed class InfisicalEndpointDefinition
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Resource { get; set; }
|
||||||
|
public string Version { get; set; }
|
||||||
|
public string Method { get; set; }
|
||||||
|
public string Template { get; set; }
|
||||||
|
public bool RequiresAuthorization { get; set; }
|
||||||
|
public bool ContainsSecretMaterialInRequest { get; set; }
|
||||||
|
public bool ContainsSecretMaterialInResponse { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace PSInfisicalAPI.Endpoints
|
||||||
|
{
|
||||||
|
public static class InfisicalEndpointNames
|
||||||
|
{
|
||||||
|
public const string UniversalAuthLogin = "UniversalAuthLogin";
|
||||||
|
public const string ListSecrets = "ListSecrets";
|
||||||
|
public const string RetrieveSecret = "RetrieveSecret";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using PSInfisicalAPI.Errors;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Endpoints
|
||||||
|
{
|
||||||
|
public static class InfisicalEndpointRegistry
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<string, InfisicalEndpointDefinition> Definitions =
|
||||||
|
new Dictionary<string, InfisicalEndpointDefinition>
|
||||||
|
{
|
||||||
|
{
|
||||||
|
InfisicalEndpointNames.UniversalAuthLogin,
|
||||||
|
new InfisicalEndpointDefinition
|
||||||
|
{
|
||||||
|
Name = InfisicalEndpointNames.UniversalAuthLogin,
|
||||||
|
Resource = "Authentication",
|
||||||
|
Version = "v1",
|
||||||
|
Method = "POST",
|
||||||
|
Template = "/api/v1/auth/universal-auth/login",
|
||||||
|
RequiresAuthorization = false,
|
||||||
|
ContainsSecretMaterialInRequest = true,
|
||||||
|
ContainsSecretMaterialInResponse = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
InfisicalEndpointNames.ListSecrets,
|
||||||
|
new InfisicalEndpointDefinition
|
||||||
|
{
|
||||||
|
Name = InfisicalEndpointNames.ListSecrets,
|
||||||
|
Resource = "Secrets",
|
||||||
|
Version = "v4",
|
||||||
|
Method = "GET",
|
||||||
|
Template = "/api/v4/secrets",
|
||||||
|
RequiresAuthorization = true,
|
||||||
|
ContainsSecretMaterialInRequest = false,
|
||||||
|
ContainsSecretMaterialInResponse = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
InfisicalEndpointNames.RetrieveSecret,
|
||||||
|
new InfisicalEndpointDefinition
|
||||||
|
{
|
||||||
|
Name = InfisicalEndpointNames.RetrieveSecret,
|
||||||
|
Resource = "Secrets",
|
||||||
|
Version = "v4",
|
||||||
|
Method = "GET",
|
||||||
|
Template = "/api/v4/secrets/{secretName}",
|
||||||
|
RequiresAuthorization = true,
|
||||||
|
ContainsSecretMaterialInRequest = false,
|
||||||
|
ContainsSecretMaterialInResponse = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static InfisicalEndpointDefinition Get(string name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
{
|
||||||
|
definition = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Definitions.TryGetValue(name, out definition);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<InfisicalEndpointDefinition> All()
|
||||||
|
{
|
||||||
|
return Definitions.Values;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace PSInfisicalAPI.Errors
|
||||||
|
{
|
||||||
|
public sealed class InfisicalErrorDetails
|
||||||
|
{
|
||||||
|
public string Component { get; set; }
|
||||||
|
public string Operation { get; set; }
|
||||||
|
public string Message { get; set; }
|
||||||
|
public string ExceptionType { get; set; }
|
||||||
|
public string InnerExceptionMessage { get; set; }
|
||||||
|
public int? StatusCode { get; set; }
|
||||||
|
public string ReasonPhrase { get; set; }
|
||||||
|
public string ApiErrorCode { get; set; }
|
||||||
|
public string SanitizedBody { get; set; }
|
||||||
|
public int? LineNumber { get; set; }
|
||||||
|
public int? LinePosition { get; set; }
|
||||||
|
public string EndpointName { get; set; }
|
||||||
|
public string RequestMethod { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Management.Automation;
|
||||||
|
using PSInfisicalAPI.Logging;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Errors
|
||||||
|
{
|
||||||
|
public static class InfisicalErrorHandler
|
||||||
|
{
|
||||||
|
private const string Component = "ErrorHandler";
|
||||||
|
|
||||||
|
public static InfisicalErrorDetails BuildDetails(string component, string operation, Exception exception)
|
||||||
|
{
|
||||||
|
InfisicalErrorDetails details = new InfisicalErrorDetails
|
||||||
|
{
|
||||||
|
Component = component,
|
||||||
|
Operation = operation,
|
||||||
|
Message = exception?.Message,
|
||||||
|
ExceptionType = exception?.GetType().FullName,
|
||||||
|
InnerExceptionMessage = exception?.InnerException?.Message
|
||||||
|
};
|
||||||
|
|
||||||
|
InfisicalApiException apiException = exception as InfisicalApiException;
|
||||||
|
if (apiException != null)
|
||||||
|
{
|
||||||
|
details.StatusCode = apiException.StatusCode;
|
||||||
|
details.ReasonPhrase = apiException.ReasonPhrase;
|
||||||
|
details.ApiErrorCode = apiException.ApiErrorCode;
|
||||||
|
details.SanitizedBody = apiException.SanitizedBody;
|
||||||
|
details.EndpointName = apiException.EndpointName;
|
||||||
|
details.RequestMethod = apiException.RequestMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
InfisicalSerializationException serializationException = exception as InfisicalSerializationException;
|
||||||
|
if (serializationException != null)
|
||||||
|
{
|
||||||
|
details.LineNumber = serializationException.LineNumber;
|
||||||
|
details.LinePosition = serializationException.LinePosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void LogFailure(IInfisicalLogger logger, InfisicalErrorDetails details)
|
||||||
|
{
|
||||||
|
if (logger == null || details == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Error(Component, string.Concat("Operation failed: ", details.Operation ?? "Unspecified"));
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(details.Component))
|
||||||
|
{
|
||||||
|
logger.Error(Component, string.Concat("Error Component: ", details.Component));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(details.Message))
|
||||||
|
{
|
||||||
|
logger.Error(Component, string.Concat("Error Message: ", details.Message));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.StatusCode.HasValue)
|
||||||
|
{
|
||||||
|
logger.Error(Component, string.Concat("HTTP Status Code: ", details.StatusCode.Value.ToString(CultureInfo.InvariantCulture)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(details.ApiErrorCode))
|
||||||
|
{
|
||||||
|
logger.Error(Component, string.Concat("API Error Code: ", details.ApiErrorCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.LineNumber.HasValue)
|
||||||
|
{
|
||||||
|
logger.Error(Component, string.Concat("Line: ", details.LineNumber.Value.ToString(CultureInfo.InvariantCulture)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.LinePosition.HasValue)
|
||||||
|
{
|
||||||
|
logger.Error(Component, string.Concat("Position: ", details.LinePosition.Value.ToString(CultureInfo.InvariantCulture)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ErrorRecord ToErrorRecord(Exception exception, InfisicalErrorDetails details)
|
||||||
|
{
|
||||||
|
ErrorCategory category = MapCategory(exception);
|
||||||
|
string errorId = exception?.GetType().Name ?? "PSInfisicalAPI.Error";
|
||||||
|
object target = details?.Component;
|
||||||
|
|
||||||
|
ErrorRecord record = new ErrorRecord(exception ?? new InfisicalException("Unspecified error."), errorId, category, target);
|
||||||
|
|
||||||
|
if (details != null && !string.IsNullOrEmpty(details.Message))
|
||||||
|
{
|
||||||
|
record.ErrorDetails = new ErrorDetails(details.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static ErrorCategory MapCategory(Exception exception)
|
||||||
|
{
|
||||||
|
if (exception is InfisicalAuthenticationException) { return ErrorCategory.AuthenticationError; }
|
||||||
|
if (exception is InfisicalHttpException) { return ErrorCategory.ConnectionError; }
|
||||||
|
if (exception is InfisicalSerializationException) { return ErrorCategory.InvalidData; }
|
||||||
|
if (exception is InfisicalConfigurationException) { return ErrorCategory.InvalidArgument; }
|
||||||
|
if (exception is InfisicalExportException) { return ErrorCategory.WriteError; }
|
||||||
|
|
||||||
|
InfisicalApiException apiException = exception as InfisicalApiException;
|
||||||
|
if (apiException != null)
|
||||||
|
{
|
||||||
|
if (apiException.StatusCode == 401) { return ErrorCategory.AuthenticationError; }
|
||||||
|
if (apiException.StatusCode == 403) { return ErrorCategory.PermissionDenied; }
|
||||||
|
if (apiException.StatusCode == 404) { return ErrorCategory.ObjectNotFound; }
|
||||||
|
if (apiException.StatusCode == 503) { return ErrorCategory.ResourceUnavailable; }
|
||||||
|
|
||||||
|
return ErrorCategory.InvalidOperation;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ErrorCategory.NotSpecified;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Errors
|
||||||
|
{
|
||||||
|
public class InfisicalException : Exception
|
||||||
|
{
|
||||||
|
public string Component { get; set; }
|
||||||
|
public string Operation { get; set; }
|
||||||
|
|
||||||
|
public InfisicalException() { }
|
||||||
|
|
||||||
|
public InfisicalException(string message) : base(message) { }
|
||||||
|
|
||||||
|
public InfisicalException(string message, Exception innerException) : base(message, innerException) { }
|
||||||
|
|
||||||
|
public InfisicalException(string component, string operation, string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
Component = component;
|
||||||
|
Operation = operation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InfisicalException(string component, string operation, string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
Component = component;
|
||||||
|
Operation = operation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InfisicalApiException : InfisicalException
|
||||||
|
{
|
||||||
|
public int StatusCode { get; set; }
|
||||||
|
public string ReasonPhrase { get; set; }
|
||||||
|
public string ApiErrorCode { get; set; }
|
||||||
|
public string SanitizedBody { get; set; }
|
||||||
|
public string EndpointName { get; set; }
|
||||||
|
public string RequestMethod { get; set; }
|
||||||
|
|
||||||
|
public InfisicalApiException() { }
|
||||||
|
public InfisicalApiException(string message) : base(message) { }
|
||||||
|
public InfisicalApiException(string message, Exception innerException) : base(message, innerException) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InfisicalAuthenticationException : InfisicalException
|
||||||
|
{
|
||||||
|
public InfisicalAuthenticationException() { }
|
||||||
|
public InfisicalAuthenticationException(string message) : base(message) { }
|
||||||
|
public InfisicalAuthenticationException(string message, Exception innerException) : base(message, innerException) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InfisicalHttpException : InfisicalException
|
||||||
|
{
|
||||||
|
public InfisicalHttpException() { }
|
||||||
|
public InfisicalHttpException(string message) : base(message) { }
|
||||||
|
public InfisicalHttpException(string message, Exception innerException) : base(message, innerException) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InfisicalSerializationException : InfisicalException
|
||||||
|
{
|
||||||
|
public int? LineNumber { get; set; }
|
||||||
|
public int? LinePosition { get; set; }
|
||||||
|
|
||||||
|
public InfisicalSerializationException() { }
|
||||||
|
public InfisicalSerializationException(string message) : base(message) { }
|
||||||
|
public InfisicalSerializationException(string message, Exception innerException) : base(message, innerException) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InfisicalExportException : InfisicalException
|
||||||
|
{
|
||||||
|
public InfisicalExportException() { }
|
||||||
|
public InfisicalExportException(string message) : base(message) { }
|
||||||
|
public InfisicalExportException(string message, Exception innerException) : base(message, innerException) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InfisicalConfigurationException : InfisicalException
|
||||||
|
{
|
||||||
|
public InfisicalConfigurationException() { }
|
||||||
|
public InfisicalConfigurationException(string message) : base(message) { }
|
||||||
|
public InfisicalConfigurationException(string message, Exception innerException) : base(message, innerException) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using PSInfisicalAPI.Common;
|
||||||
|
using PSInfisicalAPI.Errors;
|
||||||
|
using PSInfisicalAPI.Models;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Exports
|
||||||
|
{
|
||||||
|
public sealed class EnvInfisicalExporter : IInfisicalExporter
|
||||||
|
{
|
||||||
|
public void Export(InfisicalExportRequest request)
|
||||||
|
{
|
||||||
|
if (request == null || request.Secrets == null) { throw new InfisicalExportException("Export request is invalid."); }
|
||||||
|
if (request.Path == null) { throw new InfisicalExportException("Path is required for ENV export."); }
|
||||||
|
|
||||||
|
InfisicalPath.EnsureParentDirectoryExists(request.Path);
|
||||||
|
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
|
||||||
|
foreach (InfisicalSecret secret in request.Secrets)
|
||||||
|
{
|
||||||
|
if (secret == null || string.IsNullOrEmpty(secret.SecretName)) { continue; }
|
||||||
|
|
||||||
|
secret.UsePlainTextValue(plainValue =>
|
||||||
|
{
|
||||||
|
builder.Append(secret.SecretName);
|
||||||
|
builder.Append('=');
|
||||||
|
builder.AppendLine(plainValue ?? string.Empty);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Encoding encoding = request.Encoding ?? new UTF8Encoding(false);
|
||||||
|
File.WriteAllText(request.Path.FullName, builder.ToString(), encoding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using System;
|
||||||
|
using PSInfisicalAPI.Errors;
|
||||||
|
using PSInfisicalAPI.Models;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Exports
|
||||||
|
{
|
||||||
|
public sealed class EnvironmentVariableExporter : IInfisicalExporter
|
||||||
|
{
|
||||||
|
public void Export(InfisicalExportRequest request)
|
||||||
|
{
|
||||||
|
if (request == null || request.Secrets == null) { throw new InfisicalExportException("Export request is invalid."); }
|
||||||
|
|
||||||
|
EnvironmentVariableTarget target = request.Scope;
|
||||||
|
|
||||||
|
foreach (InfisicalSecret secret in request.Secrets)
|
||||||
|
{
|
||||||
|
if (secret == null || string.IsNullOrEmpty(secret.SecretName)) { continue; }
|
||||||
|
|
||||||
|
string name = secret.SecretName;
|
||||||
|
|
||||||
|
secret.UsePlainTextValue(plainValue =>
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable(name, plainValue, target);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace PSInfisicalAPI.Exports
|
||||||
|
{
|
||||||
|
public interface IInfisicalExporter
|
||||||
|
{
|
||||||
|
void Export(InfisicalExportRequest request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using PSInfisicalAPI.Models;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Exports
|
||||||
|
{
|
||||||
|
public sealed class InfisicalExportRequest
|
||||||
|
{
|
||||||
|
public InfisicalSecret[] Secrets { get; set; }
|
||||||
|
public InfisicalExportFormat Format { get; set; }
|
||||||
|
public FileInfo Path { get; set; }
|
||||||
|
public EnvironmentVariableTarget Scope { get; set; }
|
||||||
|
public bool Force { get; set; }
|
||||||
|
public Encoding Encoding { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using PSInfisicalAPI.Errors;
|
||||||
|
using PSInfisicalAPI.Models;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Exports
|
||||||
|
{
|
||||||
|
public static class InfisicalExporterFactory
|
||||||
|
{
|
||||||
|
public static IInfisicalExporter Create(InfisicalExportFormat format)
|
||||||
|
{
|
||||||
|
switch (format)
|
||||||
|
{
|
||||||
|
case InfisicalExportFormat.Json: return new JsonInfisicalExporter();
|
||||||
|
case InfisicalExportFormat.Yaml: return new YamlInfisicalExporter();
|
||||||
|
case InfisicalExportFormat.Env: return new EnvInfisicalExporter();
|
||||||
|
case InfisicalExportFormat.Xml: return new XmlInfisicalExporter();
|
||||||
|
case InfisicalExportFormat.EnvironmentVariables: return new EnvironmentVariableExporter();
|
||||||
|
default: throw new InfisicalExportException(string.Concat("Unsupported export format: ", format.ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using PSInfisicalAPI.Common;
|
||||||
|
using PSInfisicalAPI.Errors;
|
||||||
|
using PSInfisicalAPI.Models;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Exports
|
||||||
|
{
|
||||||
|
public sealed class JsonInfisicalExporter : IInfisicalExporter
|
||||||
|
{
|
||||||
|
public void Export(InfisicalExportRequest request)
|
||||||
|
{
|
||||||
|
if (request == null || request.Secrets == null) { throw new InfisicalExportException("Export request is invalid."); }
|
||||||
|
if (request.Path == null) { throw new InfisicalExportException("Path is required for JSON export."); }
|
||||||
|
|
||||||
|
InfisicalPath.EnsureParentDirectoryExists(request.Path);
|
||||||
|
|
||||||
|
List<Dictionary<string, object>> payload = new List<Dictionary<string, object>>();
|
||||||
|
|
||||||
|
foreach (InfisicalSecret secret in request.Secrets)
|
||||||
|
{
|
||||||
|
if (secret == null) { continue; }
|
||||||
|
|
||||||
|
secret.UsePlainTextValue(plainValue =>
|
||||||
|
{
|
||||||
|
Dictionary<string, object> entry = new Dictionary<string, object>();
|
||||||
|
entry["SecretName"] = secret.SecretName;
|
||||||
|
entry["SecretValue"] = plainValue;
|
||||||
|
entry["SecretPath"] = secret.SecretPath;
|
||||||
|
entry["SecretMetadata"] = MetadataToObject(secret);
|
||||||
|
payload.Add(entry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
string serialized = JsonConvert.SerializeObject(payload, Formatting.Indented);
|
||||||
|
|
||||||
|
Encoding encoding = request.Encoding ?? new UTF8Encoding(false);
|
||||||
|
File.WriteAllText(request.Path.FullName, serialized, encoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, string> MetadataToObject(InfisicalSecret secret)
|
||||||
|
{
|
||||||
|
Dictionary<string, string> metadata = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
if (secret.SecretMetadata == null) { return metadata; }
|
||||||
|
|
||||||
|
foreach (InfisicalSecretMetadata entry in secret.SecretMetadata)
|
||||||
|
{
|
||||||
|
if (entry == null || string.IsNullOrEmpty(entry.Key)) { continue; }
|
||||||
|
metadata[entry.Key] = entry.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Xml;
|
||||||
|
using PSInfisicalAPI.Common;
|
||||||
|
using PSInfisicalAPI.Errors;
|
||||||
|
using PSInfisicalAPI.Models;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Exports
|
||||||
|
{
|
||||||
|
public sealed class XmlInfisicalExporter : IInfisicalExporter
|
||||||
|
{
|
||||||
|
public void Export(InfisicalExportRequest request)
|
||||||
|
{
|
||||||
|
if (request == null || request.Secrets == null) { throw new InfisicalExportException("Export request is invalid."); }
|
||||||
|
if (request.Path == null) { throw new InfisicalExportException("Path is required for XML export."); }
|
||||||
|
|
||||||
|
InfisicalPath.EnsureParentDirectoryExists(request.Path);
|
||||||
|
|
||||||
|
Encoding encoding = request.Encoding ?? new UTF8Encoding(false);
|
||||||
|
|
||||||
|
XmlWriterSettings settings = new XmlWriterSettings
|
||||||
|
{
|
||||||
|
Indent = true,
|
||||||
|
Encoding = encoding,
|
||||||
|
OmitXmlDeclaration = false
|
||||||
|
};
|
||||||
|
|
||||||
|
using (FileStream stream = new FileStream(request.Path.FullName, FileMode.Create, FileAccess.Write))
|
||||||
|
using (XmlWriter writer = XmlWriter.Create(stream, settings))
|
||||||
|
{
|
||||||
|
writer.WriteStartDocument();
|
||||||
|
writer.WriteStartElement("Secrets");
|
||||||
|
|
||||||
|
foreach (InfisicalSecret secret in request.Secrets)
|
||||||
|
{
|
||||||
|
if (secret == null) { continue; }
|
||||||
|
|
||||||
|
secret.UsePlainTextValue(plainValue =>
|
||||||
|
{
|
||||||
|
writer.WriteStartElement("Secret");
|
||||||
|
writer.WriteElementString("SecretName", secret.SecretName ?? string.Empty);
|
||||||
|
writer.WriteElementString("SecretValue", plainValue ?? string.Empty);
|
||||||
|
writer.WriteElementString("SecretPath", secret.SecretPath ?? string.Empty);
|
||||||
|
writer.WriteStartElement("SecretMetadata");
|
||||||
|
|
||||||
|
if (secret.SecretMetadata != null)
|
||||||
|
{
|
||||||
|
foreach (InfisicalSecretMetadata entry in secret.SecretMetadata)
|
||||||
|
{
|
||||||
|
if (entry == null || string.IsNullOrEmpty(entry.Key)) { continue; }
|
||||||
|
writer.WriteStartElement("Metadata");
|
||||||
|
writer.WriteElementString("Key", entry.Key);
|
||||||
|
writer.WriteElementString("Value", entry.Value ?? string.Empty);
|
||||||
|
writer.WriteEndElement();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteEndElement();
|
||||||
|
writer.WriteEndElement();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteEndElement();
|
||||||
|
writer.WriteEndDocument();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using PSInfisicalAPI.Common;
|
||||||
|
using PSInfisicalAPI.Errors;
|
||||||
|
using PSInfisicalAPI.Models;
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Exports
|
||||||
|
{
|
||||||
|
public sealed class YamlInfisicalExporter : IInfisicalExporter
|
||||||
|
{
|
||||||
|
public void Export(InfisicalExportRequest request)
|
||||||
|
{
|
||||||
|
if (request == null || request.Secrets == null) { throw new InfisicalExportException("Export request is invalid."); }
|
||||||
|
if (request.Path == null) { throw new InfisicalExportException("Path is required for YAML export."); }
|
||||||
|
|
||||||
|
InfisicalPath.EnsureParentDirectoryExists(request.Path);
|
||||||
|
|
||||||
|
List<Dictionary<string, object>> entries = new List<Dictionary<string, object>>();
|
||||||
|
|
||||||
|
foreach (InfisicalSecret secret in request.Secrets)
|
||||||
|
{
|
||||||
|
if (secret == null) { continue; }
|
||||||
|
|
||||||
|
secret.UsePlainTextValue(plainValue =>
|
||||||
|
{
|
||||||
|
Dictionary<string, object> entry = new Dictionary<string, object>();
|
||||||
|
entry["SecretName"] = secret.SecretName;
|
||||||
|
entry["SecretValue"] = plainValue;
|
||||||
|
entry["SecretPath"] = secret.SecretPath;
|
||||||
|
entry["SecretMetadata"] = MetadataToDictionary(secret);
|
||||||
|
entries.Add(entry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary<string, object> root = new Dictionary<string, object>();
|
||||||
|
root["Secrets"] = entries;
|
||||||
|
|
||||||
|
ISerializer serializer = new SerializerBuilder().Build();
|
||||||
|
string serialized = serializer.Serialize(root);
|
||||||
|
|
||||||
|
Encoding encoding = request.Encoding ?? new UTF8Encoding(false);
|
||||||
|
File.WriteAllText(request.Path.FullName, serialized, encoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, string> MetadataToDictionary(InfisicalSecret secret)
|
||||||
|
{
|
||||||
|
Dictionary<string, string> metadata = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
if (secret.SecretMetadata == null) { return metadata; }
|
||||||
|
|
||||||
|
foreach (InfisicalSecretMetadata entry in secret.SecretMetadata)
|
||||||
|
{
|
||||||
|
if (entry == null || string.IsNullOrEmpty(entry.Key)) { continue; }
|
||||||
|
metadata[entry.Key] = entry.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace PSInfisicalAPI.Http
|
||||||
|
{
|
||||||
|
public interface IInfisicalHttpClient
|
||||||
|
{
|
||||||
|
InfisicalHttpResponse Send(InfisicalHttpRequest request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using PSInfisicalAPI.Errors;
|
||||||
|
using PSInfisicalAPI.Logging;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Http
|
||||||
|
{
|
||||||
|
public sealed class InfisicalHttpClient : IInfisicalHttpClient
|
||||||
|
{
|
||||||
|
private const string Component = "HttpClient";
|
||||||
|
private readonly IInfisicalLogger _logger;
|
||||||
|
private readonly int _timeoutSeconds;
|
||||||
|
|
||||||
|
public InfisicalHttpClient(IInfisicalLogger logger, int timeoutSeconds = 100)
|
||||||
|
{
|
||||||
|
_logger = logger ?? NullInfisicalLogger.Instance;
|
||||||
|
_timeoutSeconds = timeoutSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InfisicalHttpResponse Send(InfisicalHttpRequest request)
|
||||||
|
{
|
||||||
|
if (request == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.Uri == null)
|
||||||
|
{
|
||||||
|
throw new InfisicalHttpException("Request URI must be provided.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string operation = string.IsNullOrEmpty(request.OperationName) ? request.EndpointName : request.OperationName;
|
||||||
|
|
||||||
|
_logger.Verbose(Component, string.Concat("Attempting HTTP ", request.Method, " to ", request.Uri.GetLeftPart(UriPartial.Path), ". Please Wait..."));
|
||||||
|
|
||||||
|
HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(request.Uri);
|
||||||
|
webRequest.Method = string.IsNullOrEmpty(request.Method) ? "GET" : request.Method.ToUpperInvariant();
|
||||||
|
webRequest.Accept = "application/json";
|
||||||
|
webRequest.UserAgent = "PSInfisicalAPI";
|
||||||
|
webRequest.Timeout = _timeoutSeconds * 1000;
|
||||||
|
webRequest.ReadWriteTimeout = _timeoutSeconds * 1000;
|
||||||
|
|
||||||
|
ApplyHeaders(webRequest, request.Headers);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(request.Body))
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(request.Body);
|
||||||
|
webRequest.ContentType = string.IsNullOrEmpty(request.ContentType) ? "application/json" : request.ContentType;
|
||||||
|
webRequest.ContentLength = bytes.Length;
|
||||||
|
|
||||||
|
using (Stream requestStream = webRequest.GetRequestStream())
|
||||||
|
{
|
||||||
|
requestStream.Write(bytes, 0, bytes.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InfisicalHttpResponse response = new InfisicalHttpResponse
|
||||||
|
{
|
||||||
|
ContainsSecretMaterialInResponse = request.ContainsSecretMaterialInResponse
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (HttpWebResponse webResponse = (HttpWebResponse)webRequest.GetResponse())
|
||||||
|
{
|
||||||
|
PopulateResponse(response, webResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Verbose(Component, string.Concat("HTTP ", webRequest.Method, " completed with status ", response.StatusCode.ToString(System.Globalization.CultureInfo.InvariantCulture), "."));
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
catch (WebException webException)
|
||||||
|
{
|
||||||
|
HttpWebResponse errorResponse = webException.Response as HttpWebResponse;
|
||||||
|
if (errorResponse != null)
|
||||||
|
{
|
||||||
|
PopulateResponse(response, errorResponse);
|
||||||
|
errorResponse.Close();
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InfisicalHttpException(string.Concat("HTTP request failed: ", webException.Message), webException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyHeaders(HttpWebRequest webRequest, IDictionary<string, string> headers)
|
||||||
|
{
|
||||||
|
if (headers == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (KeyValuePair<string, string> entry in headers)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(entry.Key))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string headerName = entry.Key;
|
||||||
|
string headerValue = entry.Value ?? string.Empty;
|
||||||
|
|
||||||
|
if (string.Equals(headerName, "Accept", StringComparison.OrdinalIgnoreCase)) { webRequest.Accept = headerValue; continue; }
|
||||||
|
if (string.Equals(headerName, "User-Agent", StringComparison.OrdinalIgnoreCase)) { webRequest.UserAgent = headerValue; continue; }
|
||||||
|
if (string.Equals(headerName, "Content-Type", StringComparison.OrdinalIgnoreCase)) { webRequest.ContentType = headerValue; continue; }
|
||||||
|
|
||||||
|
webRequest.Headers[headerName] = headerValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PopulateResponse(InfisicalHttpResponse response, HttpWebResponse webResponse)
|
||||||
|
{
|
||||||
|
response.StatusCode = (int)webResponse.StatusCode;
|
||||||
|
response.ReasonPhrase = webResponse.StatusDescription;
|
||||||
|
response.Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (string headerName in webResponse.Headers.AllKeys)
|
||||||
|
{
|
||||||
|
response.Headers[headerName] = webResponse.Headers[headerName];
|
||||||
|
}
|
||||||
|
|
||||||
|
using (Stream responseStream = webResponse.GetResponseStream())
|
||||||
|
{
|
||||||
|
if (responseStream != null)
|
||||||
|
{
|
||||||
|
using (StreamReader reader = new StreamReader(responseStream, Encoding.UTF8))
|
||||||
|
{
|
||||||
|
response.Body = reader.ReadToEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
response.Body = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Http
|
||||||
|
{
|
||||||
|
public sealed class InfisicalHttpRequest
|
||||||
|
{
|
||||||
|
public string OperationName { get; set; }
|
||||||
|
public string EndpointName { get; set; }
|
||||||
|
public string Method { get; set; }
|
||||||
|
public Uri Uri { get; set; }
|
||||||
|
public Dictionary<string, string> Headers { get; set; }
|
||||||
|
public string Body { get; set; }
|
||||||
|
public string ContentType { get; set; }
|
||||||
|
public bool ContainsSecretMaterialInRequest { get; set; }
|
||||||
|
public bool ContainsSecretMaterialInResponse { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Http
|
||||||
|
{
|
||||||
|
public sealed class InfisicalHttpResponse
|
||||||
|
{
|
||||||
|
public int StatusCode { get; set; }
|
||||||
|
public string ReasonPhrase { get; set; }
|
||||||
|
public string Body { get; set; }
|
||||||
|
public Dictionary<string, string> Headers { get; set; }
|
||||||
|
public bool ContainsSecretMaterialInResponse { get; set; }
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
Body = null;
|
||||||
|
if (Headers != null)
|
||||||
|
{
|
||||||
|
Headers.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using PSInfisicalAPI.Endpoints;
|
||||||
|
using PSInfisicalAPI.Errors;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Http
|
||||||
|
{
|
||||||
|
public static class InfisicalUriBuilder
|
||||||
|
{
|
||||||
|
public static Uri Build(Uri baseUri, InfisicalEndpointDefinition definition, IDictionary<string, string> pathParameters, IEnumerable<KeyValuePair<string, string>> queryParameters)
|
||||||
|
{
|
||||||
|
if (baseUri == null)
|
||||||
|
{
|
||||||
|
throw new InfisicalConfigurationException("Base URI must be provided.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (definition == null)
|
||||||
|
{
|
||||||
|
throw new InfisicalConfigurationException("Endpoint definition must be provided.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string template = definition.Template ?? string.Empty;
|
||||||
|
string resolvedPath = ResolvePathTemplate(template, pathParameters);
|
||||||
|
|
||||||
|
UriBuilder uriBuilder = new UriBuilder(baseUri);
|
||||||
|
uriBuilder.Path = CombinePaths(uriBuilder.Path, resolvedPath);
|
||||||
|
uriBuilder.Query = BuildQueryString(queryParameters);
|
||||||
|
|
||||||
|
return uriBuilder.Uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string BuildQueryString(IEnumerable<KeyValuePair<string, string>> queryParameters)
|
||||||
|
{
|
||||||
|
if (queryParameters == null)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
bool first = true;
|
||||||
|
|
||||||
|
foreach (KeyValuePair<string, string> entry in queryParameters)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(entry.Key))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.Value == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!first)
|
||||||
|
{
|
||||||
|
builder.Append('&');
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(Uri.EscapeDataString(entry.Key));
|
||||||
|
builder.Append('=');
|
||||||
|
builder.Append(Uri.EscapeDataString(entry.Value));
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ResolvePathTemplate(string template, IDictionary<string, string> pathParameters)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(template))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathParameters == null || pathParameters.Count == 0)
|
||||||
|
{
|
||||||
|
if (template.IndexOf('{') >= 0)
|
||||||
|
{
|
||||||
|
throw new InfisicalConfigurationException(string.Concat("Path template '", template, "' requires parameters but none were supplied."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
string resolved = template;
|
||||||
|
foreach (KeyValuePair<string, string> entry in pathParameters)
|
||||||
|
{
|
||||||
|
string token = string.Concat("{", entry.Key, "}");
|
||||||
|
if (resolved.IndexOf(token, StringComparison.Ordinal) < 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string escaped = Uri.EscapeDataString(entry.Value ?? string.Empty);
|
||||||
|
resolved = resolved.Replace(token, escaped);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolved.IndexOf('{') >= 0)
|
||||||
|
{
|
||||||
|
throw new InfisicalConfigurationException(string.Concat("Path template '", template, "' has unresolved tokens."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CombinePaths(string left, string right)
|
||||||
|
{
|
||||||
|
string l = string.IsNullOrEmpty(left) ? "/" : left;
|
||||||
|
string r = right ?? string.Empty;
|
||||||
|
|
||||||
|
if (l.EndsWith("/", StringComparison.Ordinal) && r.StartsWith("/", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return string.Concat(l, r.Substring(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!l.EndsWith("/", StringComparison.Ordinal) && !r.StartsWith("/", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return string.Concat(l, "/", r);
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Concat(l, r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace PSInfisicalAPI.Logging
|
||||||
|
{
|
||||||
|
public interface IInfisicalLogger
|
||||||
|
{
|
||||||
|
void Information(string component, string message);
|
||||||
|
void Verbose(string component, string message);
|
||||||
|
void Debug(string component, string message);
|
||||||
|
void Warning(string component, string message);
|
||||||
|
void Error(string component, string message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Logging
|
||||||
|
{
|
||||||
|
public static class InfisicalLogFormatter
|
||||||
|
{
|
||||||
|
public static string Format(DateTimeOffset utcTimestamp, InfisicalLogLevel level, string component, string message)
|
||||||
|
{
|
||||||
|
string timestamp = utcTimestamp.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ", CultureInfo.InvariantCulture);
|
||||||
|
string normalizedComponent = string.IsNullOrEmpty(component) ? "Unspecified" : component;
|
||||||
|
string normalizedMessage = message ?? string.Empty;
|
||||||
|
|
||||||
|
return string.Concat("[", timestamp, "] - [", level.ToString(), "] - [", normalizedComponent, "] - ", normalizedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string FormatNow(InfisicalLogLevel level, string component, string message)
|
||||||
|
{
|
||||||
|
return Format(DateTimeOffset.UtcNow, level, component, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace PSInfisicalAPI.Logging
|
||||||
|
{
|
||||||
|
public enum InfisicalLogLevel
|
||||||
|
{
|
||||||
|
Information,
|
||||||
|
Verbose,
|
||||||
|
Debug,
|
||||||
|
Warning,
|
||||||
|
Error
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace PSInfisicalAPI.Logging
|
||||||
|
{
|
||||||
|
public sealed class NullInfisicalLogger : IInfisicalLogger
|
||||||
|
{
|
||||||
|
public static readonly NullInfisicalLogger Instance = new NullInfisicalLogger();
|
||||||
|
|
||||||
|
private NullInfisicalLogger() { }
|
||||||
|
|
||||||
|
public void Information(string component, string message) { }
|
||||||
|
public void Verbose(string component, string message) { }
|
||||||
|
public void Debug(string component, string message) { }
|
||||||
|
public void Warning(string component, string message) { }
|
||||||
|
public void Error(string component, string message) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using System;
|
||||||
|
using System.Management.Automation;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Logging
|
||||||
|
{
|
||||||
|
public sealed class PSCmdletLogger : IInfisicalLogger
|
||||||
|
{
|
||||||
|
private readonly Cmdlet _cmdlet;
|
||||||
|
|
||||||
|
public PSCmdletLogger(Cmdlet cmdlet)
|
||||||
|
{
|
||||||
|
_cmdlet = cmdlet ?? throw new ArgumentNullException(nameof(cmdlet));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Information(string component, string message)
|
||||||
|
{
|
||||||
|
string line = InfisicalLogFormatter.FormatNow(InfisicalLogLevel.Information, component, message);
|
||||||
|
_cmdlet.WriteVerbose(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Verbose(string component, string message)
|
||||||
|
{
|
||||||
|
string line = InfisicalLogFormatter.FormatNow(InfisicalLogLevel.Verbose, component, message);
|
||||||
|
_cmdlet.WriteVerbose(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Debug(string component, string message)
|
||||||
|
{
|
||||||
|
string line = InfisicalLogFormatter.FormatNow(InfisicalLogLevel.Debug, component, message);
|
||||||
|
_cmdlet.WriteDebug(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Warning(string component, string message)
|
||||||
|
{
|
||||||
|
string line = InfisicalLogFormatter.FormatNow(InfisicalLogLevel.Warning, component, message);
|
||||||
|
_cmdlet.WriteWarning(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Error(string component, string message)
|
||||||
|
{
|
||||||
|
string line = InfisicalLogFormatter.FormatNow(InfisicalLogLevel.Error, component, message);
|
||||||
|
ErrorRecord record = new ErrorRecord(
|
||||||
|
new InvalidOperationException(message ?? string.Empty),
|
||||||
|
"PSInfisicalAPI.Error",
|
||||||
|
ErrorCategory.NotSpecified,
|
||||||
|
component);
|
||||||
|
record.ErrorDetails = new ErrorDetails(line);
|
||||||
|
_cmdlet.WriteError(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace PSInfisicalAPI.Models
|
||||||
|
{
|
||||||
|
public enum InfisicalAuthType
|
||||||
|
{
|
||||||
|
UniversalAuth,
|
||||||
|
Token
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace PSInfisicalAPI.Models
|
||||||
|
{
|
||||||
|
public enum InfisicalDuplicateKeyBehavior
|
||||||
|
{
|
||||||
|
Error,
|
||||||
|
FirstWins,
|
||||||
|
LastWins
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace PSInfisicalAPI.Models
|
||||||
|
{
|
||||||
|
public enum InfisicalExportFormat
|
||||||
|
{
|
||||||
|
Json,
|
||||||
|
Yaml,
|
||||||
|
Env,
|
||||||
|
Xml,
|
||||||
|
EnvironmentVariables
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using System;
|
||||||
|
using System.Security;
|
||||||
|
using PSInfisicalAPI.Security;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Models
|
||||||
|
{
|
||||||
|
public sealed class InfisicalSecret
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string InternalId { get; set; }
|
||||||
|
public string Workspace { get; set; }
|
||||||
|
public string Environment { get; set; }
|
||||||
|
public int? Version { get; set; }
|
||||||
|
public InfisicalSecretType Type { get; set; }
|
||||||
|
public string SecretName { get; set; }
|
||||||
|
public SecureString SecretValue { get; set; }
|
||||||
|
public bool SecretValueHidden { get; set; }
|
||||||
|
public string SecretPath { get; set; }
|
||||||
|
public string SecretComment { get; set; }
|
||||||
|
public DateTimeOffset? CreatedAtUtc { get; set; }
|
||||||
|
public DateTimeOffset? UpdatedAtUtc { get; set; }
|
||||||
|
public bool IsRotatedSecret { get; set; }
|
||||||
|
public Guid? RotationId { get; set; }
|
||||||
|
public InfisicalSecretTag[] Tags { get; set; }
|
||||||
|
public InfisicalSecretMetadata[] SecretMetadata { get; set; }
|
||||||
|
|
||||||
|
public T UsePlainTextValue<T>(Func<string, T> action)
|
||||||
|
{
|
||||||
|
return SecureStringUtility.UsePlainText(SecretValue, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UsePlainTextValue(Action<string> action)
|
||||||
|
{
|
||||||
|
SecureStringUtility.UsePlainText(SecretValue, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return SecretName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace PSInfisicalAPI.Models
|
||||||
|
{
|
||||||
|
public sealed class InfisicalSecretMetadata
|
||||||
|
{
|
||||||
|
public string Key { get; set; }
|
||||||
|
public string Value { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace PSInfisicalAPI.Models
|
||||||
|
{
|
||||||
|
public sealed class InfisicalSecretTag
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string Slug { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Color { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace PSInfisicalAPI.Models
|
||||||
|
{
|
||||||
|
public enum InfisicalSecretType
|
||||||
|
{
|
||||||
|
Shared,
|
||||||
|
Personal
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<LangVersion>9.0</LangVersion>
|
||||||
|
<AssemblyName>PSInfisicalAPI</AssemblyName>
|
||||||
|
<RootNamespace>PSInfisicalAPI</RootNamespace>
|
||||||
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
|
<NoWarn>$(NoWarn);CS1591;NU1701</NoWarn>
|
||||||
|
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||||
|
<Version>$(BuildVersion)</Version>
|
||||||
|
<AssemblyVersion>$(BuildAssemblyVersion)</AssemblyVersion>
|
||||||
|
<FileVersion>$(BuildAssemblyVersion)</FileVersion>
|
||||||
|
<InformationalVersion>$(BuildVersion)</InformationalVersion>
|
||||||
|
<Company />
|
||||||
|
<Authors>Alphaeus Mote</Authors>
|
||||||
|
<Product>PSInfisicalAPI</Product>
|
||||||
|
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||||
|
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||||
|
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="PowerShellStandard.Library" Version="5.1.1" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
<PackageReference Include="YamlDotNet" Version="15.1.6" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
||||||
|
<_Parameter1>CommitHash</_Parameter1>
|
||||||
|
<_Parameter2>$(BuildCommitHash)</_Parameter2>
|
||||||
|
</AssemblyAttribute>
|
||||||
|
<InternalsVisibleTo Include="PSInfisicalAPI.Tests" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Secrets
|
||||||
|
{
|
||||||
|
internal sealed class InfisicalSecretResponseDto
|
||||||
|
{
|
||||||
|
[JsonProperty("id")] public string Id { get; set; }
|
||||||
|
[JsonProperty("_id")] public string InternalId { get; set; }
|
||||||
|
[JsonProperty("workspace")] public string Workspace { get; set; }
|
||||||
|
[JsonProperty("environment")] public string Environment { get; set; }
|
||||||
|
[JsonProperty("version")] public int? Version { get; set; }
|
||||||
|
[JsonProperty("type")] public string Type { get; set; }
|
||||||
|
[JsonProperty("secretKey")] public string SecretKey { get; set; }
|
||||||
|
[JsonProperty("secretValue")] public string SecretValue { get; set; }
|
||||||
|
[JsonProperty("secretValueHidden")] public bool SecretValueHidden { get; set; }
|
||||||
|
[JsonProperty("secretPath")] public string SecretPath { get; set; }
|
||||||
|
[JsonProperty("secretComment")] public string SecretComment { get; set; }
|
||||||
|
[JsonProperty("createdAt")] public string CreatedAt { get; set; }
|
||||||
|
[JsonProperty("updatedAt")] public string UpdatedAt { get; set; }
|
||||||
|
[JsonProperty("isRotatedSecret")] public bool IsRotatedSecret { get; set; }
|
||||||
|
[JsonProperty("rotationId")] public string RotationId { get; set; }
|
||||||
|
[JsonProperty("tags")] public List<InfisicalSecretTagDto> Tags { get; set; }
|
||||||
|
[JsonProperty("secretMetadata")] public List<InfisicalSecretMetadataDto> SecretMetadata { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class InfisicalSecretTagDto
|
||||||
|
{
|
||||||
|
[JsonProperty("id")] public string Id { get; set; }
|
||||||
|
[JsonProperty("slug")] public string Slug { get; set; }
|
||||||
|
[JsonProperty("name")] public string Name { get; set; }
|
||||||
|
[JsonProperty("color")] public string Color { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class InfisicalSecretMetadataDto
|
||||||
|
{
|
||||||
|
[JsonProperty("key")] public string Key { get; set; }
|
||||||
|
[JsonProperty("value")] public string Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class InfisicalSecretListResponseDto
|
||||||
|
{
|
||||||
|
[JsonProperty("secrets")] public List<InfisicalSecretResponseDto> Secrets { get; set; }
|
||||||
|
[JsonProperty("imports")] public List<InfisicalSecretImportDto> Imports { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class InfisicalSecretImportDto
|
||||||
|
{
|
||||||
|
[JsonProperty("secretPath")] public string SecretPath { get; set; }
|
||||||
|
[JsonProperty("environment")] public string Environment { get; set; }
|
||||||
|
[JsonProperty("secrets")] public List<InfisicalSecretResponseDto> Secrets { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class InfisicalSecretSingleResponseDto
|
||||||
|
{
|
||||||
|
[JsonProperty("secret")] public InfisicalSecretResponseDto Secret { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using PSInfisicalAPI.Models;
|
||||||
|
using PSInfisicalAPI.Security;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Secrets
|
||||||
|
{
|
||||||
|
internal static class InfisicalSecretMapper
|
||||||
|
{
|
||||||
|
public static InfisicalSecret Map(InfisicalSecretResponseDto dto)
|
||||||
|
{
|
||||||
|
if (dto == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
InfisicalSecret secret = new InfisicalSecret
|
||||||
|
{
|
||||||
|
Id = dto.Id,
|
||||||
|
InternalId = dto.InternalId,
|
||||||
|
Workspace = dto.Workspace,
|
||||||
|
Environment = dto.Environment,
|
||||||
|
Version = dto.Version,
|
||||||
|
Type = ParseType(dto.Type),
|
||||||
|
SecretName = dto.SecretKey,
|
||||||
|
SecretValue = SecureStringUtility.ToReadOnlySecureString(dto.SecretValue),
|
||||||
|
SecretValueHidden = dto.SecretValueHidden,
|
||||||
|
SecretPath = dto.SecretPath,
|
||||||
|
SecretComment = dto.SecretComment,
|
||||||
|
CreatedAtUtc = ParseTimestamp(dto.CreatedAt),
|
||||||
|
UpdatedAtUtc = ParseTimestamp(dto.UpdatedAt),
|
||||||
|
IsRotatedSecret = dto.IsRotatedSecret,
|
||||||
|
RotationId = ParseGuid(dto.RotationId),
|
||||||
|
Tags = MapTags(dto.Tags),
|
||||||
|
SecretMetadata = MapMetadata(dto.SecretMetadata)
|
||||||
|
};
|
||||||
|
|
||||||
|
dto.SecretValue = null;
|
||||||
|
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static InfisicalSecret[] MapMany(IEnumerable<InfisicalSecretResponseDto> items)
|
||||||
|
{
|
||||||
|
if (items == null)
|
||||||
|
{
|
||||||
|
return Array.Empty<InfisicalSecret>();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<InfisicalSecret> results = new List<InfisicalSecret>();
|
||||||
|
foreach (InfisicalSecretResponseDto dto in items)
|
||||||
|
{
|
||||||
|
InfisicalSecret mapped = Map(dto);
|
||||||
|
if (mapped != null)
|
||||||
|
{
|
||||||
|
results.Add(mapped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InfisicalSecretType ParseType(string value)
|
||||||
|
{
|
||||||
|
if (string.Equals(value, "personal", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return InfisicalSecretType.Personal;
|
||||||
|
}
|
||||||
|
|
||||||
|
return InfisicalSecretType.Shared;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Guid? ParseGuid(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Guid parsed;
|
||||||
|
if (Guid.TryParse(value, out parsed))
|
||||||
|
{
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InfisicalSecretTag[] MapTags(List<InfisicalSecretTagDto> tags)
|
||||||
|
{
|
||||||
|
if (tags == null || tags.Count == 0)
|
||||||
|
{
|
||||||
|
return Array.Empty<InfisicalSecretTag>();
|
||||||
|
}
|
||||||
|
|
||||||
|
InfisicalSecretTag[] mapped = new InfisicalSecretTag[tags.Count];
|
||||||
|
for (int index = 0; index < tags.Count; index++)
|
||||||
|
{
|
||||||
|
InfisicalSecretTagDto tag = tags[index];
|
||||||
|
mapped[index] = new InfisicalSecretTag
|
||||||
|
{
|
||||||
|
Id = tag.Id,
|
||||||
|
Slug = tag.Slug,
|
||||||
|
Name = tag.Name,
|
||||||
|
Color = tag.Color
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InfisicalSecretMetadata[] MapMetadata(List<InfisicalSecretMetadataDto> metadata)
|
||||||
|
{
|
||||||
|
if (metadata == null || metadata.Count == 0)
|
||||||
|
{
|
||||||
|
return Array.Empty<InfisicalSecretMetadata>();
|
||||||
|
}
|
||||||
|
|
||||||
|
InfisicalSecretMetadata[] mapped = new InfisicalSecretMetadata[metadata.Count];
|
||||||
|
for (int index = 0; index < metadata.Count; index++)
|
||||||
|
{
|
||||||
|
InfisicalSecretMetadataDto entry = metadata[index];
|
||||||
|
mapped[index] = new InfisicalSecretMetadata
|
||||||
|
{
|
||||||
|
Key = entry.Key,
|
||||||
|
Value = entry.Value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Secrets
|
||||||
|
{
|
||||||
|
public sealed class InfisicalListSecretsQuery
|
||||||
|
{
|
||||||
|
public string ProjectId { get; set; }
|
||||||
|
public string Environment { get; set; }
|
||||||
|
public string SecretPath { get; set; }
|
||||||
|
public bool Recursive { get; set; }
|
||||||
|
public bool? IncludeImports { get; set; }
|
||||||
|
public bool IncludePersonalOverrides { get; set; }
|
||||||
|
public bool? ExpandSecretReferences { get; set; }
|
||||||
|
public bool? ViewSecretValue { get; set; }
|
||||||
|
public Dictionary<string, string> MetadataFilter { get; set; }
|
||||||
|
public string[] TagSlugs { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class InfisicalRetrieveSecretQuery
|
||||||
|
{
|
||||||
|
public string SecretName { get; set; }
|
||||||
|
public string ProjectId { get; set; }
|
||||||
|
public string Environment { get; set; }
|
||||||
|
public string SecretPath { get; set; }
|
||||||
|
public int? Version { get; set; }
|
||||||
|
public string Type { get; set; }
|
||||||
|
public bool? ViewSecretValue { get; set; }
|
||||||
|
public bool? ExpandSecretReferences { get; set; }
|
||||||
|
public bool? IncludeImports { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
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.Security;
|
||||||
|
using PSInfisicalAPI.Serialization;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Secrets
|
||||||
|
{
|
||||||
|
public sealed class InfisicalSecretsClient
|
||||||
|
{
|
||||||
|
private const string Component = "SecretsClient";
|
||||||
|
|
||||||
|
private readonly IInfisicalHttpClient _httpClient;
|
||||||
|
private readonly IInfisicalLogger _logger;
|
||||||
|
private readonly JsonInfisicalSerializer _serializer;
|
||||||
|
|
||||||
|
public InfisicalSecretsClient(IInfisicalHttpClient httpClient, IInfisicalLogger logger)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||||
|
_logger = logger ?? NullInfisicalLogger.Instance;
|
||||||
|
_serializer = new JsonInfisicalSerializer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public InfisicalSecret[] List(InfisicalConnection connection, InfisicalListSecretsQuery query)
|
||||||
|
{
|
||||||
|
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
|
||||||
|
if (query == null) { throw new ArgumentNullException(nameof(query)); }
|
||||||
|
|
||||||
|
InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(InfisicalEndpointNames.ListSecrets);
|
||||||
|
|
||||||
|
List<KeyValuePair<string, string>> queryParameters = new List<KeyValuePair<string, string>>();
|
||||||
|
AddIfNotNull(queryParameters, "workspaceId", FirstNonEmpty(query.ProjectId, connection.ProjectId));
|
||||||
|
AddIfNotNull(queryParameters, "environment", FirstNonEmpty(query.Environment, connection.Environment));
|
||||||
|
AddIfNotNull(queryParameters, "secretPath", FirstNonEmpty(query.SecretPath, connection.DefaultSecretPath, "/"));
|
||||||
|
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.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.ViewSecretValue.HasValue) { queryParameters.Add(new KeyValuePair<string, string>("viewSecretValue", query.ViewSecretValue.Value ? "true" : "false")); }
|
||||||
|
|
||||||
|
if (query.TagSlugs != null)
|
||||||
|
{
|
||||||
|
foreach (string tag in query.TagSlugs)
|
||||||
|
{
|
||||||
|
AddIfNotNull(queryParameters, "tagSlugs", tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.MetadataFilter != null)
|
||||||
|
{
|
||||||
|
foreach (KeyValuePair<string, string> meta in query.MetadataFilter)
|
||||||
|
{
|
||||||
|
AddIfNotNull(queryParameters, string.Concat("metadata[", meta.Key, "]"), meta.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri uri = InfisicalUriBuilder.Build(connection.BaseUri, definition, null, queryParameters);
|
||||||
|
InfisicalHttpResponse response = ExecuteAuthorized(connection, definition, "RetrieveSecrets", uri, null);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.Information(Component, "Attempting to retrieve Infisical secrets. Please Wait...");
|
||||||
|
EnsureSuccess(response, definition);
|
||||||
|
|
||||||
|
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.");
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
_logger.Error(Component, "Infisical secrets retrieval failed.");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public InfisicalSecret Retrieve(InfisicalConnection connection, InfisicalRetrieveSecretQuery query)
|
||||||
|
{
|
||||||
|
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
|
||||||
|
if (query == null) { throw new ArgumentNullException(nameof(query)); }
|
||||||
|
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 } };
|
||||||
|
|
||||||
|
List<KeyValuePair<string, string>> queryParameters = new List<KeyValuePair<string, string>>();
|
||||||
|
AddIfNotNull(queryParameters, "workspaceId", FirstNonEmpty(query.ProjectId, connection.ProjectId));
|
||||||
|
AddIfNotNull(queryParameters, "environment", FirstNonEmpty(query.Environment, connection.Environment));
|
||||||
|
AddIfNotNull(queryParameters, "secretPath", FirstNonEmpty(query.SecretPath, connection.DefaultSecretPath, "/"));
|
||||||
|
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.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.IncludeImports.HasValue) { queryParameters.Add(new KeyValuePair<string, string>("include_imports", query.IncludeImports.Value ? "true" : "false")); }
|
||||||
|
|
||||||
|
Uri uri = InfisicalUriBuilder.Build(connection.BaseUri, definition, pathParameters, queryParameters);
|
||||||
|
InfisicalHttpResponse response = ExecuteAuthorized(connection, definition, "RetrieveSecret", uri, null);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.Information(Component, string.Concat("Attempting to retrieve Infisical secret '", query.SecretName, "'. Please Wait..."));
|
||||||
|
EnsureSuccess(response, definition);
|
||||||
|
|
||||||
|
InfisicalSecretSingleResponseDto dto = _serializer.Deserialize<InfisicalSecretSingleResponseDto>(response.Body);
|
||||||
|
response.Clear();
|
||||||
|
|
||||||
|
InfisicalSecret mapped = InfisicalSecretMapper.Map(dto != null ? dto.Secret : null);
|
||||||
|
_logger.Information(Component, "Infisical secret retrieval was successful.");
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
_logger.Error(Component, "Infisical secret retrieval failed.");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private InfisicalHttpResponse ExecuteAuthorized(InfisicalConnection connection, InfisicalEndpointDefinition definition, string operationName, Uri uri, string body)
|
||||||
|
{
|
||||||
|
Dictionary<string, string> headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
headers["Accept"] = "application/json";
|
||||||
|
|
||||||
|
if (definition.RequiresAuthorization)
|
||||||
|
{
|
||||||
|
if (connection.AccessToken == null)
|
||||||
|
{
|
||||||
|
throw new InfisicalAuthenticationException("Connection does not contain an access token.");
|
||||||
|
}
|
||||||
|
|
||||||
|
SecureStringUtility.UsePlainText(connection.AccessToken, plainToken =>
|
||||||
|
{
|
||||||
|
headers["Authorization"] = string.Concat("Bearer ", plainToken ?? string.Empty);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
InfisicalHttpRequest request = new InfisicalHttpRequest
|
||||||
|
{
|
||||||
|
OperationName = operationName,
|
||||||
|
EndpointName = definition.Name,
|
||||||
|
Method = definition.Method,
|
||||||
|
Uri = uri,
|
||||||
|
Headers = headers,
|
||||||
|
Body = body,
|
||||||
|
ContainsSecretMaterialInRequest = definition.ContainsSecretMaterialInRequest,
|
||||||
|
ContainsSecretMaterialInResponse = definition.ContainsSecretMaterialInResponse
|
||||||
|
};
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
list.Add(new KeyValuePair<string, string>(key, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FirstNonEmpty(params string[] values)
|
||||||
|
{
|
||||||
|
if (values == null) { return null; }
|
||||||
|
foreach (string value in values)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Security;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Security
|
||||||
|
{
|
||||||
|
public static class SecureStringUtility
|
||||||
|
{
|
||||||
|
public static SecureString ToReadOnlySecureString(string value)
|
||||||
|
{
|
||||||
|
SecureString secureString = new SecureString();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
foreach (char character in value)
|
||||||
|
{
|
||||||
|
secureString.AppendChar(character);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
secureString.MakeReadOnly();
|
||||||
|
|
||||||
|
return secureString;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SecureString EmptyReadOnly()
|
||||||
|
{
|
||||||
|
SecureString secureString = new SecureString();
|
||||||
|
secureString.MakeReadOnly();
|
||||||
|
return secureString;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T UsePlainText<T>(SecureString secureString, Func<string, T> action)
|
||||||
|
{
|
||||||
|
if (secureString == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(secureString));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(action));
|
||||||
|
}
|
||||||
|
|
||||||
|
IntPtr pointer = IntPtr.Zero;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
pointer = Marshal.SecureStringToBSTR(secureString);
|
||||||
|
string plainText = Marshal.PtrToStringBSTR(pointer);
|
||||||
|
return action(plainText);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (pointer != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Marshal.ZeroFreeBSTR(pointer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void UsePlainText(SecureString secureString, Action<string> action)
|
||||||
|
{
|
||||||
|
if (action == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(action));
|
||||||
|
}
|
||||||
|
|
||||||
|
UsePlainText<bool>(secureString, plainText =>
|
||||||
|
{
|
||||||
|
action(plainText);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace PSInfisicalAPI.Serialization
|
||||||
|
{
|
||||||
|
public interface IInfisicalSerializer
|
||||||
|
{
|
||||||
|
string Serialize<T>(T value);
|
||||||
|
T Deserialize<T>(string value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Serialization;
|
||||||
|
using PSInfisicalAPI.Errors;
|
||||||
|
|
||||||
|
namespace PSInfisicalAPI.Serialization
|
||||||
|
{
|
||||||
|
public sealed class JsonInfisicalSerializer : IInfisicalSerializer
|
||||||
|
{
|
||||||
|
private readonly JsonSerializerSettings _settings;
|
||||||
|
|
||||||
|
public JsonInfisicalSerializer(bool useCamelCase = true, bool indent = false)
|
||||||
|
{
|
||||||
|
_settings = new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
NullValueHandling = NullValueHandling.Ignore,
|
||||||
|
Formatting = indent ? Formatting.Indented : Formatting.None,
|
||||||
|
DateParseHandling = DateParseHandling.DateTimeOffset
|
||||||
|
};
|
||||||
|
|
||||||
|
if (useCamelCase)
|
||||||
|
{
|
||||||
|
_settings.ContractResolver = new CamelCasePropertyNamesContractResolver();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Serialize<T>(T value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonConvert.SerializeObject(value, _settings);
|
||||||
|
}
|
||||||
|
catch (JsonException jsonException)
|
||||||
|
{
|
||||||
|
throw new InfisicalSerializationException(string.Concat("JSON serialization failed: ", jsonException.Message), jsonException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Deserialize<T>(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
return default(T);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonConvert.DeserializeObject<T>(value, _settings);
|
||||||
|
}
|
||||||
|
catch (JsonReaderException readerException)
|
||||||
|
{
|
||||||
|
InfisicalSerializationException exception = new InfisicalSerializationException(string.Concat("JSON deserialization failed: ", readerException.Message), readerException);
|
||||||
|
exception.LineNumber = readerException.LineNumber;
|
||||||
|
exception.LinePosition = readerException.LinePosition;
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
catch (JsonException jsonException)
|
||||||
|
{
|
||||||
|
throw new InfisicalSerializationException(string.Concat("JSON deserialization failed: ", jsonException.Message), jsonException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user