infisical.psm1
|
#!/usr/bin/env pwsh using namespace System using namespace System.IO using namespace System.Web using namespace System.Text using namespace System.Net.Http using namespace System.Reflection using namespace System.Threading.Tasks # Load all sub-modules : # (Get-ChildItem ./Private).Name.ForEach({ "using module Private/" + $_ }) using module Private/Enums.psm1 using module Private/Exceptions.psm1 using module Private/Model.psm1 using module Private/Api.psm1 using module Private/Util.psm1 using module Private/Client.psm1 #Requires -PSEdition Core #Requires -Modules PsModuleBase, cliHelper.env, clihelper.xconvert, clihelper.logger, argparser #region Classes # .SYNOPSIS # Main class of the module class InfisicalClient { hidden [ApiClient] $_apiClient hidden [AuthClient] $_authClient hidden [SecretsClient] $_secretsClient hidden [PkiClient] $_pkiClient hidden [IdentitiesClient] $_identitiesClient hidden [KmsClient] $_kmsClient InfisicalClient([InfisicalSdkSettings]$settings) { $this._apiClient = [ApiClient]::new($settings.HostUri) $this._secretsClient = [SecretsClient]::new($this._apiClient) $this._authClient = [AuthClient]::new($this._apiClient, { param($accessToken) $this._apiClient.SetAccessToken($accessToken) }) $this._pkiClient = [PkiClient]::new($this._apiClient) $this._identitiesClient = [IdentitiesClient]::new($this._apiClient) $this._kmsClient = [KmsClient]::new($this._apiClient) } [AuthClient] Auth() { return $this._authClient } [SecretsClient] Secrets() { return $this._secretsClient } [PkiClient] Pki() { return $this._pkiClient } [IdentitiesClient] Identities() { return $this._identitiesClient } [KmsClient] Kms() { return $this._kmsClient } } # .SYNOPSIS # CLASS used in the public function Invoke-InfisicalCli # .DESCRIPTION # Main entry class. has convinience static methods that makes it easy to use many features of thew module. class Infisical { static [Type[]] $ReturnTypes = ([Infisical]::Methods.ReturnType | Sort-Object -Unique Name) static [MethodInfo[]] $Methods = ([Infisical].GetMethods().Where({ $_.IsStatic -and !$_.IsHideBySig })) #region CLI Engine static [InfisicalClient] GetClient([string]$domain, [string]$token) { $settings = [InfisicalSdkSettingsBuilder]::new().WithHostUri($domain).Build() $client = [InfisicalClient]::new($settings) if (![string]::IsNullOrEmpty($token)) { $client._apiClient.SetAccessToken($token) } elseif (![string]::IsNullOrEmpty($env:INFISICAL_TOKEN)) { $client._apiClient.SetAccessToken($env:INFISICAL_TOKEN) } return $client } static [InfisicalClient] DefaultClient() { $domain = if ($env:INFISICAL_API_URL) { $env:INFISICAL_API_URL } else { "https://app.infisical.com" } return [Infisical]::GetClient($domain, $null) } static [AuthClient] Auth() { return [Infisical]::DefaultClient().Auth() } static [SecretsClient] Secrets() { return [Infisical]::DefaultClient().Secrets() } static [PkiClient] Pki() { return [Infisical]::DefaultClient().Pki() } static [IdentitiesClient] Identities() { return [Infisical]::DefaultClient().Identities() } static [KmsClient] Kms() { return [Infisical]::DefaultClient().Kms() } static hidden [void] ParseArgs([string[]]$InputArgs) { if ($InputArgs.Count -eq 0) { Write-Host ([Infisical]::WriteBanner()) -ForegroundColor Cyan Write-Host "Usage: infisical <command> [subcommand] [options]" return } $command = $InputArgs[0] $subArgs = @() if ($InputArgs.Count -gt 1) { $subArgs = $InputArgs[1..($InputArgs.Count - 1)] } try { switch ($command) { "login" { [Infisical]::Login($subArgs); break } "secrets" { [Infisical]::Secrets($subArgs); break } "export" { [Infisical]::Export($subArgs); break } "run" { [Infisical]::Run($subArgs); break } "init" { [Infisical]::Init($subArgs); break } "reset" { [Infisical]::Reset($subArgs); break } "token" { [Infisical]::Token($subArgs); break } "user" { [Infisical]::User($subArgs); break } "vault" { [Infisical]::Vault($subArgs); break } "scan" { [Infisical]::Scan($subArgs); break } "help" { [Infisical]::ShowHelp(); break } "version" { [Infisical]::ShowVersion(); break } "upgrade" { [Infisical]::UpdateModule(); break } "events" { $params = ConvertTo-Params $subArgs -schema @{ id = [string], $null limit = [int], 20 output = [string], 'table' } Write-Host ([Infisical]::GetEvent($params.id.Value, $params.limit.Value, $params.output.Value)) break } default { [Infisical]::ShowHelp() break } } } catch { Write-Error "Execution failed: $_" } } static [void] Login([string[]]$InputArgs) { $params = ConvertTo-Params $InputArgs -schema @{ method = [string], 'user' domain = [string], 'https://app.infisical.com' 'client-id' = [string], $env:INFISICAL_UNIVERSAL_AUTH_CLIENT_ID 'client-secret' = [string], $env:INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET email = [string], $env:INFISICAL_EMAIL password = [string], $env:INFISICAL_PASSWORD 'organization-id' = [string], $env:INFISICAL_ORGANIZATION_ID interactive = [switch], $false plain = [switch], $false silent = [switch], $false 'machine-identity-id' = [string], $env:INFISICAL_MACHINE_IDENTITY_ID 'organization-slug' = [string], $env:INFISICAL_AUTH_ORGANIZATION_SLUG } $domain = if ($params.domain.Value) { $params.domain.Value } else { "https://app.infisical.com" } if (![string]::IsNullOrEmpty($env:INFISICAL_API_URL)) { $domain = $env:INFISICAL_API_URL } $client = [Infisical]::GetClient($domain, $null) if ($params.method.Value -eq 'universal-auth') { $clientId = $params.'client-id'.Value $clientSecretStr = $params.'client-secret'.Value if ([string]::IsNullOrEmpty($clientId) -or [string]::IsNullOrEmpty($clientSecretStr)) { throw "client-id and client-secret are required for universal-auth." } $clientSecret = $clientSecretStr | xconvert ToSecurestring $res = $client.Auth().UniversalAuth().LoginAsync($clientId, $clientSecret).GetAwaiter().GetResult() if ($params.plain.Value) { if (!$params.silent.Value) { Write-Host $res.AccessToken } else { [Console]::WriteLine($res.AccessToken) } } else { Write-Host "Successfully logged in via Universal Auth." -ForegroundColor Green Write-Host "Token: $($res.AccessToken)" } } else { Write-Warning "Method $($params.method.Value) is currently not fully implemented in this CLI engine wrapper. Only universal-auth is supported via CLI args so far." } } static [void] Secrets([string[]]$InputArgs) { if ($InputArgs.Count -eq 0 -or $InputArgs[0] -match '^-') { # No subcommand, this means 'infisical secrets' $subCommand = "list" $remArgs = $InputArgs } else { $subCommand = $InputArgs[0] $remArgs = if ($InputArgs.Count -gt 1) { $InputArgs[1..($InputArgs.Count - 1)] } else { @() } } $params = ConvertTo-Params $remArgs -schema @{ projectId = [string], $null env = [string], 'dev' path = [string], '/' plain = [switch], $false silent = [switch], $false expand = [switch], $true domain = [string], 'https://app.infisical.com' token = [string], $null } $domain = if ($params.domain.Value) { $params.domain.Value } else { "https://app.infisical.com" } if (![string]::IsNullOrEmpty($env:INFISICAL_API_URL)) { $domain = $env:INFISICAL_API_URL } $client = [Infisical]::GetClient($domain, $params.token.Value) switch ($subCommand) { "get" { $secretNames = @() foreach ($arg in $remArgs) { if ($arg -notmatch '^-') { $secretNames += $arg } else { break } } foreach ($s in $secretNames) { $opts = [GetSecretOptions]::new() $opts.ProjectId = $params.projectId.Value $opts.EnvironmentSlug = $params.env.Value $opts.SecretPath = $params.path.Value $opts.SecretName = $s $opts.ExpandSecretReferences = $params.expand.Value $secret = $client.Secrets().GetAsync($opts).GetAwaiter().GetResult() if ($params.plain.Value) { [Console]::WriteLine($secret.SecretValue) } else { Write-Host "$s`: $($secret.SecretValue)" } } } "set" { # Partially implemented set $opts = [CreateSecretOptions]::new() $opts.ProjectId = $params.projectId.Value $opts.EnvironmentSlug = $params.env.Value $opts.SecretPath = $params.path.Value $kvPairs = @() foreach ($arg in $remArgs) { if ($arg -notmatch '^-' -and $arg -match '=') { $kvPairs += $arg } else { break } } foreach ($kv in $kvPairs) { $split = $kv.Split('=', 2) $opts.SecretName = $split[0] $opts.SecretValue = $split[1] # Update if exists, else create try { $client.Secrets().CreateAsync($opts).GetAwaiter().GetResult() | Out-Null if (!$params.silent.Value) { Write-Host "Set secret $($opts.SecretName)" -f Green } } catch { $upd = [UpdateSecretOptions]::new() $upd.ProjectId = $opts.ProjectId $upd.EnvironmentSlug = $opts.EnvironmentSlug $upd.SecretPath = $opts.SecretPath $upd.SecretName = $opts.SecretName $upd.NewSecretValue = $opts.SecretValue $client.Secrets().UpdateAsync($upd).GetAwaiter().GetResult() | Out-Null if (!$params.silent.Value) { Write-Host "Updated secret $($opts.SecretName)" -f Green } } } } "delete" { $secretNames = @() foreach ($arg in $remArgs) { if ($arg -notmatch '^-') { $secretNames += $arg } else { break } } foreach ($s in $secretNames) { $opts = [DeleteSecretOptions]::new() $opts.ProjectId = $params.projectId.Value $opts.EnvironmentSlug = $params.env.Value $opts.SecretPath = $params.path.Value $opts.SecretName = $s $client.Secrets().DeleteAsync($opts).GetAwaiter().GetResult() | Out-Null if (!$params.silent.Value) { Write-Host "Deleted secret $s" -f Green } } } "list" { $opts = [ListSecretsOptions]::new() $opts.ProjectId = $params.projectId.Value $opts.EnvironmentSlug = $params.env.Value $opts.SecretPath = $params.path.Value $opts.ExpandSecretReferences = $params.expand.Value $secrets = $client.Secrets().ListAsync($opts).GetAwaiter().GetResult() if ($params.plain.Value) { foreach ($s in $secrets) { [Console]::WriteLine("$($s.SecretKey)=$($s.SecretValue)") } } else { $secrets | Format-Table SecretKey, SecretValue, SecretPath, Environment } } default { Write-Host "Unknown secrets subcommand: $subCommand" } } } static [void] Export([string[]]$InputArgs) { $params = ConvertTo-Params $InputArgs -schema @{ format = [string], 'dotenv' 'output-file' = [string], $null env = [string], 'dev' projectId = [string], $null path = [string], '/' domain = [string], 'https://app.infisical.com' token = [string], $null expand = [switch], $true } $domain = if ($params.domain.Value) { $params.domain.Value } else { "https://app.infisical.com" } if (![string]::IsNullOrEmpty($env:INFISICAL_API_URL)) { $domain = $env:INFISICAL_API_URL } $client = [Infisical]::GetClient($domain, $params.token.Value) $opts = [ListSecretsOptions]::new() $opts.ProjectId = $params.projectId.Value $opts.EnvironmentSlug = $params.env.Value $opts.SecretPath = $params.path.Value $opts.ExpandSecretReferences = $params.expand.Value $secrets = $client.Secrets().ListAsync($opts).GetAwaiter().GetResult() $output = @() if ($params.format.Value -eq 'json') { $hash = @{} foreach ($s in $secrets) { $hash[$s.SecretKey] = $s.SecretValue } $output = $hash | ConvertTo-Json -Depth 10 } elseif ($params.format.Value -eq 'yaml') { foreach ($s in $secrets) { $output += "$($s.SecretKey): `"$($s.SecretValue)`"" } $output = $output -join "`n" } elseif ($params.format.Value -eq 'csv') { $output += "Key,Value" foreach ($s in $secrets) { $output += "$($s.SecretKey),$($s.SecretValue)" } $output = $output -join "`n" } elseif ($params.format.Value -eq 'dotenv-export') { foreach ($s in $secrets) { $output += "export $($s.SecretKey)=`"$($s.SecretValue)`"" } $output = $output -join "`n" } else { # Default dotenv foreach ($s in $secrets) { $output += "$($s.SecretKey)=`"$($s.SecretValue)`"" } $output = $output -join "`n" } if (![string]::IsNullOrEmpty($params.'output-file'.Value)) { [System.IO.File]::WriteAllText($params.'output-file'.Value, $output) Write-Host "Exported secrets to $($params.'output-file'.Value)" -f Green } else { [Console]::WriteLine($output) } } static [void] Run([string[]]$InputArgs) { $dashDashIndex = [Array]::IndexOf($InputArgs, "--") $infisicalArgs = @() $cmdArgs = @() if ($dashDashIndex -ge 0) { if ($dashDashIndex -gt 0) { $infisicalArgs = $InputArgs[0..($dashDashIndex - 1)] } if ($dashDashIndex -lt ($InputArgs.Count - 1)) { $cmdArgs = $InputArgs[($dashDashIndex + 1)..($InputArgs.Count - 1)] } } else { $infisicalArgs = $InputArgs } $params = ConvertTo-Params $infisicalArgs -schema @{ projectId = [string], $null env = [string], 'dev' path = [string[]], @('/') command = [string], $null expand = [switch], $true domain = [string], 'https://app.infisical.com' token = [string], $null watch = [switch], $false } $domain = if ($params.domain.Value) { $params.domain.Value } else { "https://app.infisical.com" } if (![string]::IsNullOrEmpty($env:INFISICAL_API_URL)) { $domain = $env:INFISICAL_API_URL } $projectId = $params.projectId.Value if ([string]::IsNullOrEmpty($projectId)) { $config = [Infisical]::GetProjectConfig() if ($null -ne $config -and $null -ne $config.workspaceId) { $projectId = $config.workspaceId } } if ([string]::IsNullOrEmpty($projectId) -and [string]::IsNullOrEmpty($params.token.Value) -and [string]::IsNullOrEmpty($env:INFISICAL_TOKEN)) { Write-Error "Project ID is required. Use --projectId or run 'infisical init' first." return } $client = [Infisical]::GetClient($domain, $params.token.Value) $opts = [ListSecretsOptions]::new() $opts.ProjectId = $projectId $opts.EnvironmentSlug = $params.env.Value $opts.ExpandSecretReferences = $params.expand.Value $opts.SetSecretsAsEnvironmentVariables = $true $paths = if ($params.path.Value -is [string[]]) { $params.path.Value } else { @($params.path.Value) } # Fetch secrets and set as env vars for each path # Note: ListAsync only sets environment variables if they are not already set, # ensuring that the first path provided takes precedence. foreach ($p in $paths) { $opts.SecretPath = $p $client.Secrets().ListAsync($opts).GetAwaiter().GetResult() | Out-Null } $finalCmd = if (![string]::IsNullOrEmpty($params.command.Value)) { $params.command.Value } else { $cmdArgs -join " " } if ([string]::IsNullOrEmpty($finalCmd)) { Write-Error "No command provided to run." return } if ($params.watch.Value) { Write-Warning "Watch mode is not yet implemented in this PowerShell module." } try { [scriptblock]::Create("$finalCmd").Invoke() } catch { Write-Error $_.Exception.Message } } static [void] Scan([string[]]$InputArgs) { Write-Warning "Secret scanning is not yet implemented in this PowerShell module." } static [void] Init([string]$projectId) { [Infisical]::Init(@("--projectId", $projectId)) } static [void] Init([string[]]$InputArgs) { $params = ConvertTo-Params $InputArgs -schema @{ projectId = [string], $null } $projectId = $params.projectId.Value if ([string]::IsNullOrEmpty($projectId)) { $projectId = Read-Host "Enter your Infisical Project ID" } if ([string]::IsNullOrEmpty($projectId)) { Write-Error "Project ID is required." return } $config = @{ workspaceId = $projectId } [Infisical]::SetProjectConfig($config) Write-Host "Initialized project in .infisical.json" -ForegroundColor Green } static [void] Reset([string[]]$InputArgs) { $configFile = Join-Path (Get-Location) ".infisical.json" if (Test-Path $configFile) { Remove-Item $configFile Write-Host "Reset Infisical configuration." -ForegroundColor Green } else { Write-Host "No Infisical configuration found to reset." } } static [void] Token([string[]]$InputArgs) { if ($InputArgs.Count -eq 0) { Write-Host "Usage: infisical token <renew> [options]" return } $subCommand = $InputArgs[0] switch ($subCommand) { "renew" { Write-Warning "Token renewal is not yet implemented." } default { Write-Error "Unknown token subcommand: $subCommand" } } } static [void] User([string[]]$InputArgs) { if ($InputArgs.Count -eq 0) { Write-Host "Usage: infisical user <get|switch|update> [options]" return } $subCommand = $InputArgs[0] $remArgs = if ($InputArgs.Count -gt 1) { $InputArgs[1..($InputArgs.Count - 1)] } else { @() } switch ($subCommand) { "get" { if ($remArgs.Count -gt 0 -and $remArgs[0] -eq "token") { $params = ConvertTo-Params $remArgs[1..($remArgs.Count - 1)] -schema @{ plain = [switch], $false } $token = $env:INFISICAL_TOKEN if ([string]::IsNullOrEmpty($token)) { Write-Error "Not logged in. No INFISICAL_TOKEN found." return } if ($params.plain.Value) { [Console]::WriteLine($token) } else { Write-Host "Token: $token" } } } default { Write-Warning "User subcommand $subCommand is not yet implemented." } } } static [void] Vault([string[]]$InputArgs) { Write-Warning "Vault management is not yet implemented." } static [object] GetProjectConfig() { $configFile = Join-Path (Get-Location) ".infisical.json" if (Test-Path $configFile) { return Get-Content $configFile | ConvertFrom-Json } return $null } static [void] SetProjectConfig([object]$Config) { $configFile = Join-Path (Get-Location) ".infisical.json" $Config | ConvertTo-Json | Set-Content $configFile } #endregion CLI Engine static [void] ShowHelp() { [Infisical]::WriteBanner() Write-Host ([Infisical]::GetHelp()) } static [void] ShowVersion() { $version = ([PsModuleBase]::ReadModuledata("infisical")["ModuleVersion"]) Write-Host "Infisical CLI version $version" } static [void] UpdateModule() { Write-Host "Updating Infisical module..." -ForegroundColor Cyan Update-Module -Name infisical -ErrorAction SilentlyContinue Write-Host "Update check complete." -ForegroundColor Green } static [string] GetEvent([string]$id, [int]$limit, [string]$output) { # TODO: Implement event retrieval in the API client return "Event retrieval ($id) is not yet implemented in this PowerShell module wrapper." } static [void] WriteBanner() { Write-Host ([PsModuleBase]::ReadModuledata("infisical")["BannerAscii"]) -f Green } static [string] GetHelp() { return [PsModuleBase]::ReadModuledata("infisical")["HelpMessage"] } } #endregion Classes # Types that will be available to users when they import the module. # Hint: To automatically generate typestoexport variable you can use this one liner to generate types to export variable # (Get-ChildItem *.psm1 -Recurse -File | ForEach-Object { [IO.File]::ReadAllLines((Get-Item $_.FullName)).Where({ $_.StartsWith("class") -or $_.StartsWith("enum ") }).ForEach({ $_.Replace("class ", '[').Replace("enum ", '[') }).ForEach({ ($_ -like "* : *") ? $_.split(" : ")[0] + ']' : $_.Replace(' {', ']') }) }) -join ', ' $typestoExport = @( [ApiClient], [QueryBuilder], [UniversalAuth], [LdapAuth], [AuthClient], [Subscribers], [PkiClient], [SecretsClient], [IdentitiesClient], [SecretType], [InfisicalAuthMethod], [InfisicalException], [IdentityProjectAdditionalPrivilegePermissionConditionEnvironment], [IdentityProjectAdditionalPrivilegePermissionCondition], [IdentityProjectAdditionalPrivilegePermission], [IdentityProjectAdditionalPrivilegeType], [AddIdentityProjectAdditionalPrivilegeOptions], [IdentityProjectAdditionalPrivilegeResponse], [MachineIdentityCredential], [UniversalAuthLoginRequest], [LdapAuthLoginRequest], [ListSecretsOptions], [GetSecretOptions], [SecretMetadata], [CreateSecretOptions], [UpdateSecretOptions], [DeleteSecretOptions], [IssueCertificateOptions], [SubscriberIssuedCertificate], [RetrieveLatestCertificateBundleOptions], [CertificateBundle], [InfisicalSecret], [SecretImport], [ListSecretsResponse], [GetSecretResponse], [CreateSecretResponse], [UpdateSecretResponse], [DeleteSecretResponse], [InfisicalUniversalAuth], [InfisicalTokenAuth], [InfisicalAuth], [InfisicalSdkSettings], [InfisicalSdkSettingsBuilder], [ObjectToDictionaryConverter], [SecretsUtil], [InfisicalClient], [Infisical], [KmsClient], [KmsKey], [ListKmsKeysOptions], [GetKmsKeyByIdOptions], [GetKmsKeyByNameOptions], [CreateKmsKeyOptions], [UpdateKmsKeyOptions], [DeleteKmsKeyOptions], [RetrieveKmsPublicKeyOptions], [ExportKmsPrivateKeyOptions], [BulkExportPrivateKeysOptions], [EncryptKmsDataOptions], [DecryptKmsDataOptions], [SignKmsDataOptions], [VerifyKmsSignatureOptions], [ListKmsSigningAlgorithmsOptions], [KmsKeyResponse], [KmsKeysResponse], [KmsEncryptResponse], [KmsDecryptResponse], [KmsSignResponse], [KmsVerifyResponse], [KmsPublicKeyResponse], [KmsPrivateKeyResponse], [KmsBulkExportPrivateKeysResponse], [KmsSigningAlgorithmsResponse] ) $TypeAcceleratorsClass = [PsObject].Assembly.GetType('System.Management.Automation.TypeAccelerators') # Add type accelerators for every exportable type. foreach ($Type in $typestoExport) { try { [void]$TypeAcceleratorsClass::Add($Type.FullName, $Type) } catch { # Ignore if already exists $null } } # Remove type accelerators when the module is removed. $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { foreach ($Type in $typestoExport) { $TypeAcceleratorsClass::Remove($Type.FullName) } }.GetNewClosure(); $scripts = @(); $Public = Get-ChildItem "$PSScriptRoot/Public" -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue $scripts += Get-ChildItem "$PSScriptRoot/Private" -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue $scripts += $Public foreach ($file in $scripts) { try { if ([string]::IsNullOrWhiteSpace($file.fullname)) { continue } . "$($file.fullname)" } catch { Write-Warning "Failed to import function $($file.BaseName): $_" $host.UI.WriteErrorLine($_) } } $Param = @{ Function = $Public.BaseName Cmdlet = '*' Alias = '*' Verbose = $false } Export-ModuleMember @Param |