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