PSInfisical.Extension/PSInfisical.Extension.psm1
|
# PSInfisical.Extension.psm1 # SecretManagement vault extension for PSInfisical. # Provides the 5 required functions: Get-Secret, Set-Secret, Remove-Secret, # Get-SecretInfo, Test-SecretVault. # Called by: Microsoft.PowerShell.SecretManagement when a registered vault is accessed. # Dependencies: PSInfisical module (parent), Microsoft.PowerShell.SecretManagement #Requires -Version 5.1 Set-StrictMode -Version Latest # The parent PSInfisical module's functions (Connect-Infisical, Get-InfisicalSecret, etc.) # are available because this module is loaded as a NestedModule of PSInfisical.psd1. # Do NOT import the parent here — that would create a circular dependency. # Session cache: keyed by vault name, value is an InfisicalSession object. # Avoids re-authenticating on every SecretManagement call. $script:SessionCache = @{} # --------------------------------------------------------------------------- # Internal helpers (not exported) # --------------------------------------------------------------------------- function Get-OrCreateSession { <# .SYNOPSIS Ensures a valid PSInfisical session exists for the specified vault. .DESCRIPTION Checks the session cache for an existing, non-expired session for the given vault name. If found, injects it into the PSInfisical module scope. If not found or expired, authenticates using credentials from AdditionalParameters and caches the new session. .OUTPUTS [void] #> [CmdletBinding()] [OutputType([void])] param( [Parameter(Mandatory)] [string] $VaultName, [Parameter(Mandatory)] [hashtable] $AdditionalParameters ) # Check for a cached session that is still valid if ($script:SessionCache.ContainsKey($VaultName)) { $cachedSession = $script:SessionCache[$VaultName] $cachedSession.UpdateConnectionStatus() if ($cachedSession.Connected -and -not $cachedSession.IsTokenExpiringSoon()) { # Inject cached session into PSInfisical module scope & (Get-Module PSInfisical) { param($s) $script:InfisicalSession = $s } $cachedSession return } # Session expired or expiring — will re-authenticate below } # Extract connection parameters with defaults $apiUrl = if ($AdditionalParameters.ContainsKey('ApiUrl')) { $AdditionalParameters['ApiUrl'] } else { 'https://app.infisical.com' } $projectId = $AdditionalParameters['ProjectId'] $environment = if ($AdditionalParameters.ContainsKey('Environment')) { $AdditionalParameters['Environment'] } else { 'prod' } # Validate required parameters if ([string]::IsNullOrEmpty($projectId)) { $errorRecord = [System.Management.Automation.ErrorRecord]::new( [System.ArgumentException]::new("Vault '$VaultName': VaultParameters must include 'ProjectId'."), 'InfisicalVaultMissingProjectId', [System.Management.Automation.ErrorCategory]::InvalidArgument, $VaultName ) $PSCmdlet.ThrowTerminatingError($errorRecord) } # Build Connect-Infisical parameters $connectParams = @{ ApiUrl = $apiUrl ProjectId = $projectId Environment = $environment PassThru = $true } # Determine auth method from provided parameters if ($AdditionalParameters.ContainsKey('ClientId') -and $AdditionalParameters.ContainsKey('ClientSecret')) { $connectParams['ClientId'] = $AdditionalParameters['ClientId'] $connectParams['ClientSecret'] = ConvertTo-SessionSecureString -Value $AdditionalParameters['ClientSecret'] } elseif ($AdditionalParameters.ContainsKey('Token')) { $connectParams['Token'] = ConvertTo-SessionSecureString -Value $AdditionalParameters['Token'] } elseif ($AdditionalParameters.ContainsKey('AccessToken')) { $connectParams['AccessToken'] = ConvertTo-SessionSecureString -Value $AdditionalParameters['AccessToken'] } elseif ($AdditionalParameters.ContainsKey('AWSIdentityDocument')) { $connectParams['AWSIdentityDocument'] = $AdditionalParameters['AWSIdentityDocument'] } elseif ($AdditionalParameters.ContainsKey('AzureJwt')) { $connectParams['AzureJwt'] = ConvertTo-SessionSecureString -Value $AdditionalParameters['AzureJwt'] } elseif ($AdditionalParameters.ContainsKey('GCPIdentityToken')) { $connectParams['GCPIdentityToken'] = ConvertTo-SessionSecureString -Value $AdditionalParameters['GCPIdentityToken'] } elseif ($AdditionalParameters.ContainsKey('KubernetesServiceAccountToken') -and $AdditionalParameters.ContainsKey('KubernetesIdentityId')) { $connectParams['KubernetesServiceAccountToken'] = ConvertTo-SessionSecureString -Value $AdditionalParameters['KubernetesServiceAccountToken'] $connectParams['KubernetesIdentityId'] = $AdditionalParameters['KubernetesIdentityId'] } elseif ($AdditionalParameters.ContainsKey('OIDCToken') -and $AdditionalParameters.ContainsKey('OIDCIdentityId')) { $connectParams['OIDCToken'] = ConvertTo-SessionSecureString -Value $AdditionalParameters['OIDCToken'] $connectParams['OIDCIdentityId'] = $AdditionalParameters['OIDCIdentityId'] } elseif ($AdditionalParameters.ContainsKey('Jwt') -and $AdditionalParameters.ContainsKey('JwtIdentityId')) { $connectParams['Jwt'] = ConvertTo-SessionSecureString -Value $AdditionalParameters['Jwt'] $connectParams['JwtIdentityId'] = $AdditionalParameters['JwtIdentityId'] } elseif ($AdditionalParameters.ContainsKey('LDAPUsername') -and $AdditionalParameters.ContainsKey('LDAPPassword')) { $connectParams['LDAPUsername'] = $AdditionalParameters['LDAPUsername'] $connectParams['LDAPPassword'] = ConvertTo-SessionSecureString -Value $AdditionalParameters['LDAPPassword'] } else { $errorRecord = [System.Management.Automation.ErrorRecord]::new( [System.ArgumentException]::new("Vault '$VaultName': VaultParameters must include authentication credentials (ClientId+ClientSecret, Token, or AccessToken)."), 'InfisicalVaultMissingCredentials', [System.Management.Automation.ErrorCategory]::InvalidArgument, $VaultName ) $PSCmdlet.ThrowTerminatingError($errorRecord) } # Null out the PSInfisical module's session to prevent Connect-Infisical # from disposing a cached session belonging to a different vault. & (Get-Module PSInfisical) { $script:InfisicalSession = $null } $session = Connect-Infisical @connectParams $script:SessionCache[$VaultName] = $session } function ConvertTo-SessionSecureString { <# .SYNOPSIS Converts a value to SecureString if it is not already one. .DESCRIPTION Register-SecretVault -VaultParameters stores values as their original types. Users may pass plain strings or SecureStrings for credentials. This helper normalises both to SecureString. .OUTPUTS [System.Security.SecureString] #> [CmdletBinding()] [OutputType([System.Security.SecureString])] param( [Parameter(Mandatory)] [object] $Value ) if ($Value -is [System.Security.SecureString]) { return $Value } $secureString = [System.Security.SecureString]::new() foreach ($char in $Value.ToString().ToCharArray()) { $secureString.AppendChar($char) } $secureString.MakeReadOnly() return $secureString } function Resolve-SecretPath { <# .SYNOPSIS Returns the secret path from AdditionalParameters or the default '/'. .OUTPUTS [string] #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [hashtable] $AdditionalParameters ) if ($AdditionalParameters.ContainsKey('SecretPath') -and -not [string]::IsNullOrEmpty($AdditionalParameters['SecretPath'])) { return $AdditionalParameters['SecretPath'] } return '/' } # --------------------------------------------------------------------------- # Required SecretManagement extension functions # --------------------------------------------------------------------------- function Get-Secret { <# .SYNOPSIS Retrieves a secret value from Infisical. .DESCRIPTION Called by Microsoft.PowerShell.SecretManagement when the user runs Get-Secret -Vault <VaultName>. Returns the secret value as a SecureString, or $null if the secret does not exist. .OUTPUTS [System.Security.SecureString] #> [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Name, [Parameter(Mandatory)] [string] $VaultName, [Parameter(Mandatory)] [hashtable] $AdditionalParameters ) Get-OrCreateSession -VaultName $VaultName -AdditionalParameters $AdditionalParameters $secretPath = Resolve-SecretPath -AdditionalParameters $AdditionalParameters $secret = Get-InfisicalSecret -Name $Name -SecretPath $secretPath -ErrorAction SilentlyContinue if ($null -eq $secret) { return $null } return $secret.Value } function Set-Secret { <# .SYNOPSIS Creates or updates a secret in Infisical. .DESCRIPTION Called by Microsoft.PowerShell.SecretManagement when the user runs Set-Secret -Vault <VaultName>. Checks whether the secret exists first, then updates or creates accordingly. .OUTPUTS [bool] #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function signature is defined by SecretManagement')] [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Name, [Parameter(Mandatory)] [object] $Secret, [Parameter(Mandatory)] [string] $VaultName, [Parameter(Mandatory)] [hashtable] $AdditionalParameters ) Get-OrCreateSession -VaultName $VaultName -AdditionalParameters $AdditionalParameters $secretPath = Resolve-SecretPath -AdditionalParameters $AdditionalParameters # Normalise the secret value to SecureString $secureValue = ConvertTo-SessionSecureString -Value $Secret # Check if the secret already exists to determine create vs update $existing = Get-InfisicalSecret -Name $Name -SecretPath $secretPath -ErrorAction SilentlyContinue if ($null -ne $existing) { Set-InfisicalSecret -Name $Name -Value $secureValue -SecretPath $secretPath -Confirm:$false -ErrorAction Stop } else { New-InfisicalSecret -Name $Name -Value $secureValue -SecretPath $secretPath -Confirm:$false -ErrorAction Stop } return $true } function Remove-Secret { <# .SYNOPSIS Removes a secret from Infisical. .DESCRIPTION Called by Microsoft.PowerShell.SecretManagement when the user runs Remove-Secret -Vault <VaultName>. .OUTPUTS [bool] #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function signature is defined by SecretManagement')] [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Name, [Parameter(Mandatory)] [string] $VaultName, [Parameter(Mandatory)] [hashtable] $AdditionalParameters ) Get-OrCreateSession -VaultName $VaultName -AdditionalParameters $AdditionalParameters $secretPath = Resolve-SecretPath -AdditionalParameters $AdditionalParameters Remove-InfisicalSecret -Name $Name -SecretPath $secretPath -Confirm:$false -ErrorAction Stop return $true } function Get-SecretInfo { <# .SYNOPSIS Lists secrets in Infisical as SecretInformation objects. .DESCRIPTION Called by Microsoft.PowerShell.SecretManagement when the user runs Get-SecretInfo -Vault <VaultName>. Returns metadata about secrets without exposing their values. .OUTPUTS [Microsoft.PowerShell.SecretManagement.SecretInformation] #> [CmdletBinding()] param( [Parameter()] [string] $Filter, [Parameter(Mandatory)] [string] $VaultName, [Parameter(Mandatory)] [hashtable] $AdditionalParameters ) Get-OrCreateSession -VaultName $VaultName -AdditionalParameters $AdditionalParameters $secretPath = Resolve-SecretPath -AdditionalParameters $AdditionalParameters $secrets = Get-InfisicalSecrets -SecretPath $secretPath -ErrorAction Stop if ($null -eq $secrets) { return @() } # Apply wildcard filter if provided if (-not [string]::IsNullOrEmpty($Filter)) { $secrets = @($secrets | Where-Object { $_.Name -like $Filter }) } # Resolve SecretManagement types at runtime rather than parse time. # When this module is loaded as a NestedModule of PSInfisical, SecretManagement # may not yet be imported, causing parse-time [type] literals to fail. $SecretInfoType = 'Microsoft.PowerShell.SecretManagement.SecretInformation' -as [type] $SecureStringType = ('Microsoft.PowerShell.SecretManagement.SecretType' -as [type])::SecureString foreach ($s in $secrets) { $SecretInfoType::new( $s.Name, $SecureStringType, $VaultName ) } } function Test-SecretVault { <# .SYNOPSIS Tests whether the Infisical vault is accessible. .DESCRIPTION Called by Microsoft.PowerShell.SecretManagement when the user runs Test-SecretVault -Name <VaultName>. Validates authentication and API connectivity by listing secrets. .OUTPUTS [bool] #> [CmdletBinding()] param( [Parameter(Mandatory)] [string] $VaultName, [Parameter(Mandatory)] [hashtable] $AdditionalParameters ) try { Get-OrCreateSession -VaultName $VaultName -AdditionalParameters $AdditionalParameters $secretPath = Resolve-SecretPath -AdditionalParameters $AdditionalParameters # Attempt a lightweight API call to verify connectivity and permissions $null = Get-InfisicalSecrets -SecretPath $secretPath -ErrorAction Stop return $true } catch { Write-Verbose "Test-SecretVault: Vault '$VaultName' test failed: $($_.Exception.Message)" return $false } } |