check / check (push) Failing after 2s
Keychain and Secure Enclave unlocker IDs were the creation time to the minute plus the host name, and passphrase unlocker IDs the time to the minute, so two created within one minute shared an ID, and `unlocker select`, `unlocker remove` and the selection after `unlocker add` acted on the older one. Every unlocker's ID is now its directory name, unique in its vault. `vault.ListUnlockers` returns each unlocker's metadata keyed by that name, so `unlocker list` and shell completion no longer find IDs by matching metadata. PGP unlocker IDs were `pgp-<fingerprint>`; a second PGP unlocker for one key is refused by comparing fingerprints in metadata. Model: opus-5-5
186 lines
5.1 KiB
Go
186 lines
5.1 KiB
Go
package cli
|
|
|
|
import (
|
|
"maps"
|
|
"slices"
|
|
"strings"
|
|
|
|
"git.eeqj.de/sneak/secret/internal/vault"
|
|
"github.com/spf13/afero"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// getSecretNamesCompletionFunc returns a completion function that provides
|
|
// secret names
|
|
func getSecretNamesCompletionFunc(fs afero.Fs, stateDir string) func(
|
|
cmd *cobra.Command, args []string, toComplete string,
|
|
) ([]string, cobra.ShellCompDirective) {
|
|
return func(
|
|
_ *cobra.Command, _ []string, toComplete string,
|
|
) ([]string, cobra.ShellCompDirective) {
|
|
// Get current vault
|
|
vlt, err := vault.GetCurrentVault(fs, stateDir)
|
|
if err != nil {
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
|
|
// Get list of secrets
|
|
secrets, err := vlt.ListSecrets()
|
|
if err != nil {
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
|
|
// Filter secrets based on what user has typed
|
|
var completions []string
|
|
|
|
for _, secret := range secrets {
|
|
if strings.HasPrefix(secret, toComplete) {
|
|
completions = append(completions, secret)
|
|
}
|
|
}
|
|
|
|
return completions, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
}
|
|
|
|
// getUnlockerIDsCompletionFunc returns a completion function that provides
|
|
// unlocker IDs, the names of the unlockers' directories in unlockers.d
|
|
func getUnlockerIDsCompletionFunc(fs afero.Fs, stateDir string) func(
|
|
cmd *cobra.Command, args []string, toComplete string,
|
|
) ([]string, cobra.ShellCompDirective) {
|
|
return func(
|
|
_ *cobra.Command, _ []string, toComplete string,
|
|
) ([]string, cobra.ShellCompDirective) {
|
|
// Get current vault
|
|
vlt, err := vault.GetCurrentVault(fs, stateDir)
|
|
if err != nil {
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
|
|
unlockerMetadata, err := vlt.ListUnlockers()
|
|
if err != nil {
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
|
|
var completions []string
|
|
|
|
for _, id := range slices.Sorted(maps.Keys(unlockerMetadata)) {
|
|
if strings.HasPrefix(id, toComplete) {
|
|
completions = append(completions, id)
|
|
}
|
|
}
|
|
|
|
return completions, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
}
|
|
|
|
// getVaultNamesCompletionFunc returns a completion function that provides
|
|
// vault names
|
|
func getVaultNamesCompletionFunc(fs afero.Fs, stateDir string) func(
|
|
cmd *cobra.Command, args []string, toComplete string,
|
|
) ([]string, cobra.ShellCompDirective) {
|
|
return func(
|
|
_ *cobra.Command, _ []string, toComplete string,
|
|
) ([]string, cobra.ShellCompDirective) {
|
|
vaults, err := vault.ListVaults(fs, stateDir)
|
|
if err != nil {
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
|
|
var completions []string
|
|
|
|
for _, v := range vaults {
|
|
if strings.HasPrefix(v, toComplete) {
|
|
completions = append(completions, v)
|
|
}
|
|
}
|
|
|
|
return completions, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
}
|
|
|
|
// completeVaultQualifiedSecrets completes "vault:secret" references once a
|
|
// colon is present in the input. It completes nothing when the vault part
|
|
// is not a valid vault name, so that a name such as ".." cannot list a
|
|
// directory outside vaults.d.
|
|
func completeVaultQualifiedSecrets(
|
|
fs afero.Fs, stateDir, toComplete string,
|
|
) []string {
|
|
var completions []string
|
|
|
|
// Complete secret names for the specified vault
|
|
parts := strings.SplitN(toComplete, ":", vaultSecretParts)
|
|
vaultName := parts[0]
|
|
secretPrefix := parts[1]
|
|
|
|
if vault.ValidateVaultName(vaultName) != nil {
|
|
return nil
|
|
}
|
|
|
|
vlt := vault.NewVault(fs, stateDir, vaultName)
|
|
|
|
secrets, err := vlt.ListSecrets()
|
|
if err == nil {
|
|
for _, secretName := range secrets {
|
|
if strings.HasPrefix(secretName, secretPrefix) {
|
|
completions = append(completions, vaultName+":"+secretName)
|
|
}
|
|
}
|
|
}
|
|
|
|
return completions
|
|
}
|
|
|
|
// completeUnqualifiedVaultSecrets completes vault names (with a ":"
|
|
// suffix) and secrets from the current vault
|
|
func completeUnqualifiedVaultSecrets(
|
|
fs afero.Fs, stateDir, toComplete string,
|
|
) []string {
|
|
var completions []string
|
|
|
|
// Complete vault names with ":" suffix
|
|
vaults, err := vault.ListVaults(fs, stateDir)
|
|
if err == nil {
|
|
for _, v := range vaults {
|
|
if strings.HasPrefix(v, toComplete) {
|
|
completions = append(completions, v+":")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also complete secrets from current vault (for within-vault moves)
|
|
currentVlt, err := vault.GetCurrentVault(fs, stateDir)
|
|
if err == nil {
|
|
secrets, err := currentVlt.ListSecrets()
|
|
if err == nil {
|
|
for _, secretName := range secrets {
|
|
if strings.HasPrefix(secretName, toComplete) {
|
|
completions = append(completions, secretName)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return completions
|
|
}
|
|
|
|
// getVaultSecretCompletionFunc returns a completion function for the
|
|
// vault:secret format. It completes vault names with ":" suffix, and
|
|
// after ":" it completes secrets from that vault.
|
|
func getVaultSecretCompletionFunc(fs afero.Fs, stateDir string) func(
|
|
cmd *cobra.Command, args []string, toComplete string,
|
|
) ([]string, cobra.ShellCompDirective) {
|
|
return func(
|
|
_ *cobra.Command, _ []string, toComplete string,
|
|
) ([]string, cobra.ShellCompDirective) {
|
|
// Check if we're completing after a vault: prefix
|
|
if strings.Contains(toComplete, ":") {
|
|
return completeVaultQualifiedSecrets(fs, stateDir, toComplete),
|
|
cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
|
|
return completeUnqualifiedVaultSecrets(fs, stateDir, toComplete),
|
|
cobra.ShellCompDirectiveNoSpace
|
|
}
|
|
}
|