Implement syntax: secret move/mv <vault>:<secret> <vault>[:<secret>] - Copies all versions to destination vault with re-encryption - Deletes source after successful copy (true move) - Add --force flag to overwrite existing destination - Support both within-vault rename and cross-vault move - Add shell completion for vault:secret syntax - Include integration tests for cross-vault move
200 lines
5.9 KiB
Go
200 lines
5.9 KiB
Go
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"git.eeqj.de/sneak/secret/internal/secret"
|
|
"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
|
|
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
|
|
}
|
|
|
|
// Get unlocker metadata list
|
|
unlockerMetadataList, err := vlt.ListUnlockers()
|
|
if err != nil {
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
|
|
// Get vault directory
|
|
vaultDir, err := vlt.GetDirectory()
|
|
if err != nil {
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
|
|
// Collect unlocker IDs
|
|
var completions []string
|
|
|
|
for _, metadata := range unlockerMetadataList {
|
|
// Get the actual unlocker ID by creating the unlocker instance
|
|
unlockersDir := filepath.Join(vaultDir, "unlockers.d")
|
|
files, err := afero.ReadDir(fs, unlockersDir)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
for _, file := range files {
|
|
if !file.IsDir() {
|
|
continue
|
|
}
|
|
|
|
unlockerDir := filepath.Join(unlockersDir, file.Name())
|
|
metadataPath := filepath.Join(unlockerDir, "unlocker-metadata.json")
|
|
|
|
// Check if this is the right unlocker by comparing metadata
|
|
metadataBytes, err := afero.ReadFile(fs, metadataPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
var diskMetadata secret.UnlockerMetadata
|
|
if err := json.Unmarshal(metadataBytes, &diskMetadata); err != nil {
|
|
continue
|
|
}
|
|
|
|
// Match by type and creation time
|
|
if diskMetadata.Type == metadata.Type && diskMetadata.CreatedAt.Equal(metadata.CreatedAt) {
|
|
// Create the appropriate unlocker instance
|
|
var unlocker secret.Unlocker
|
|
switch metadata.Type {
|
|
case "passphrase":
|
|
unlocker = secret.NewPassphraseUnlocker(fs, unlockerDir, diskMetadata)
|
|
case "keychain":
|
|
unlocker = secret.NewKeychainUnlocker(fs, unlockerDir, diskMetadata)
|
|
case "pgp":
|
|
unlocker = secret.NewPGPUnlocker(fs, unlockerDir, diskMetadata)
|
|
}
|
|
|
|
if unlocker != nil {
|
|
id := unlocker.GetID()
|
|
if strings.HasPrefix(id, toComplete) {
|
|
completions = append(completions, id)
|
|
}
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
// getVaultSecretCompletionFunc returns a completion function for 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) {
|
|
var completions []string
|
|
|
|
// Check if we're completing after a vault: prefix
|
|
if strings.Contains(toComplete, ":") {
|
|
// Complete secret names for the specified vault
|
|
const vaultSecretParts = 2
|
|
parts := strings.SplitN(toComplete, ":", vaultSecretParts)
|
|
vaultName := parts[0]
|
|
secretPrefix := parts[1]
|
|
|
|
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, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
|
|
// 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)
|
|
if currentVlt, err := vault.GetCurrentVault(fs, stateDir); err == nil {
|
|
secrets, err := currentVlt.ListSecrets()
|
|
if err == nil {
|
|
for _, secretName := range secrets {
|
|
if strings.HasPrefix(secretName, toComplete) {
|
|
completions = append(completions, secretName)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return completions, cobra.ShellCompDirectiveNoSpace
|
|
}
|
|
}
|