PowerShell / Modules

MIT License
111 stars 25 forks source link

[SecretManagement] ArgumentCompleter / cached secret info #87

Open itfranck opened 4 years ago

itfranck commented 4 years ago

Implement an argument completer for the SecretManagement module.

Currently if you want to use the cmdlet, you need to know your secrets / vault name. The Get-SecretInfo cmdlet is super useful for that.

That being said, it could be implemented directly into the module using an argument completer for the vaults and secrets name

Proposed technical implementation details (optional)

Here's a simple example that work with cmdlets for the Vault parameters and secrets parameters.


Register-ArgumentCompleter -CommandName Get-Secret, Set-Secret, Get-SecretInfo -ParameterName Vault -ScriptBlock {
    Get-SecretVault  | Select -ExpandProperty Name | foreach-object {
        [System.Management.Automation.CompletionResult]::new($_)
    }
}

Register-ArgumentCompleter -CommandName Get-Secret, Remove-Secret -ParameterName Name -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)

    $VaultNameFilter = $fakeBoundParameter.Vault
    (Get-SecretInfo -Vault $VaultNameFilter )  | Select -ExpandProperty Name  | foreach-object {
        [System.Management.Automation.CompletionResult]::new($_)
    }
}

The only drawback is that relying on Get-SecretInfo as it is means that you have to wait on remote vault querying, which can add a couple of seconds. (But since the value completion is not triggered automatically, that shouldn't be a problem)

Still, for the Vault parameter, it is perfect. For the Name, I think it make sense anyway (value completion must be manuall triggered so it's a choice of whether you enter your name manually (if you know it) or trigger the value completion, which is the equivalent of running a Get-SecretInfo in the console then copy/paste the name.

itfranck commented 4 years ago

I think it could be nice to have a the possibility to cache results from Get-SecretInfo. That would benefit the argument completion.

Unless having secret name saved somewhere is a security concern for some ? Anyway, expanding on that idea, I made a second version that implement a new Get-SecretInfo.

That new Get-SecretInfo (exposed through an alias to override the original) expose a new [Switch] Cache parameter which will use a cached version of the credentials, rendering the argument completion for secret names instant.

Calling Get-SecretInfo normally will refresh the cached informations. Get-SecretInfo -Cache will use cached informations and / or generate them if they are not present.

function Get-SecretInfo {
    [CmdletBinding()]
    param (
        [String]$Vault,
        [String]$Name,
        [Switch]$Cache 
    )

    $CacheParamstr = 'Cache'
    $VaultPath = "$env:USERPROFILE\Powershell\SecretsManagement\Vaults"
    $UseCache = $False 

    if ($PSBoundParameters.ContainsKey($CacheParamstr)) {
        $UseCache = $PSBoundParameters.Item($CacheParamstr)
        [void]$PSBoundParameters.Remove($CacheParamstr)
    }

    if (! (Test-Path $VaultPath)) { New-Item $VaultPath -ItemType Directory }

    $VaultNames = [System.Collections.Generic.List[String]]::new()
    $CachedOutput = [System.Collections.Generic.List[PSObject]]::new()

    if ($UseCache) {
        if ($PSBoundParameters.ContainsKey('Vault')) {
            $VaultNames.Add($PSBoundParameters.Item('Vault'))
        }
        else {
            $VaultNames = @(Get-SecretVault | Select -ExpandProperty Name)
        }

        foreach ($Vault in $VaultNames) {
            if ($Vault -eq 'BuiltInLocalVault') {
                $CachedOutput.Add((Get-SecretInfo -Vault 'BuiltInLocalVault'))
                continue
            }

            $CurrVaultPath = "$VaultPath\$Vault.json"
            if (Test-Path -Path $CurrVaultPath) {
                $value = (Get-Content -Path $CurrVaultPath -Raw | ConvertFrom-Json) | Select -Property Name, @{n = 'Type'; e = { [Microsoft.PowerShell.SecretManagement.SecretType]$_.Type } }, VaultName
                $CachedOutput.Add($value)
            }
            else {
                try {
                    $CurrentVault = @(Microsoft.PowerShell.SecretManagement\Get-SecretInfo -Vault $Vault)
                    $CurrentVault | Select  | ConvertTo-Json | Out-File $CurrVaultPath
                    $CachedOutput.Add($CurrentVault)
                }
                catch {

                }
            }
        }
        return $CachedOutput
    }

    if (! $PSBoundParameters.ContainsKey('Vault')) { Get-ChildItem -Path $VaultPath | Remove-Item }
    $Output = Microsoft.PowerShell.SecretManagement\Get-SecretInfo @PSBoundParameters
    $GrpVault = $Output | Group-Object -Property VaultName

    foreach ($grp in $GrpVault) {
        if ($grp.Name -eq 'BuiltInLocalVault') { Continue }
        $CurrVaultPath = "$VaultPath\$($grp.Name).json"
        $grp.Group | ConvertTo-Json | Out-File $CurrVaultPath
    }

    return $Output
}

Register-ArgumentCompleter -CommandName Get-Secret, Set-Secret, Get-SecretInfo, Test-SecretVault -ParameterName Vault -ScriptBlock {
    Get-SecretVault  | Select -ExpandProperty Name | foreach-object {
        [System.Management.Automation.CompletionResult]::new($_)
    }
}

Register-ArgumentCompleter -CommandName Unregister-SecretVault -ParameterName Name -ScriptBlock {
    Get-SecretVault  | Select -ExpandProperty Name | foreach-object {
        [System.Management.Automation.CompletionResult]::new($_)
    }
}

Register-ArgumentCompleter -CommandName Get-Secret, Remove-Secret -ParameterName Name -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)

    $VaultNameFilter = $fakeBoundParameter.Vault
    (Get-SecretInfo -Vault $VaultNameFilter -cache)  | Select -ExpandProperty Name  | foreach-object {
        [System.Management.Automation.CompletionResult]::new($_)
    }
}

Both solution (original without caching and my favorite, with caching), if implemented, would provide a greater code writing experience.

itfranck commented 4 years ago

I added missing cmdlet and published a version on the Powershell Gallery. https://www.powershellgallery.com/packages/SecretManagementArgumentCompleter/1.0.0

I am leaving this feature request open as I still think it would be great if supported natively.