Add cross-vault move command for secrets
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
This commit is contained in:
@@ -483,3 +483,158 @@ func (v *Vault) GetSecretObject(name string) (*secret.Secret, error) {
|
||||
|
||||
return secretObj, nil
|
||||
}
|
||||
|
||||
// CopySecretVersion copies a single version from source to this vault
|
||||
// It decrypts the value using srcIdentity and re-encrypts for this vault
|
||||
func (v *Vault) CopySecretVersion(
|
||||
srcVersion *secret.Version,
|
||||
srcIdentity *age.X25519Identity,
|
||||
destSecretName string,
|
||||
destVersionName string,
|
||||
) error {
|
||||
secret.DebugWith("Copying secret version to vault",
|
||||
slog.String("src_secret", srcVersion.SecretName),
|
||||
slog.String("src_version", srcVersion.Version),
|
||||
slog.String("dest_vault", v.Name),
|
||||
slog.String("dest_secret", destSecretName),
|
||||
slog.String("dest_version", destVersionName),
|
||||
)
|
||||
|
||||
// Get the decrypted value from source
|
||||
valueBuffer, err := srcVersion.GetValue(srcIdentity)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decrypt source version: %w", err)
|
||||
}
|
||||
defer valueBuffer.Destroy()
|
||||
|
||||
// Load source metadata
|
||||
if err := srcVersion.LoadMetadata(srcIdentity); err != nil {
|
||||
return fmt.Errorf("failed to load source metadata: %w", err)
|
||||
}
|
||||
|
||||
// Create destination version with same name
|
||||
destVersion := secret.NewVersion(v, destSecretName, destVersionName)
|
||||
|
||||
// Copy metadata (preserve original timestamps)
|
||||
destVersion.Metadata = srcVersion.Metadata
|
||||
|
||||
// Save the version (encrypts to this vault's LT key)
|
||||
if err := destVersion.Save(valueBuffer); err != nil {
|
||||
return fmt.Errorf("failed to save destination version: %w", err)
|
||||
}
|
||||
|
||||
secret.Debug("Successfully copied secret version",
|
||||
"src_version", srcVersion.Version,
|
||||
"dest_version", destVersionName,
|
||||
"dest_vault", v.Name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CopySecretAllVersions copies all versions of a secret from source vault to this vault
|
||||
// It re-encrypts each version with this vault's long-term key
|
||||
func (v *Vault) CopySecretAllVersions(
|
||||
srcVault *Vault,
|
||||
srcSecretName string,
|
||||
destSecretName string,
|
||||
force bool,
|
||||
) error {
|
||||
secret.DebugWith("Copying all secret versions between vaults",
|
||||
slog.String("src_vault", srcVault.Name),
|
||||
slog.String("src_secret", srcSecretName),
|
||||
slog.String("dest_vault", v.Name),
|
||||
slog.String("dest_secret", destSecretName),
|
||||
slog.Bool("force", force),
|
||||
)
|
||||
|
||||
// Get destination vault directory
|
||||
destVaultDir, err := v.GetDirectory()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get destination vault directory: %w", err)
|
||||
}
|
||||
|
||||
// Check if destination secret already exists
|
||||
destStorageName := strings.ReplaceAll(destSecretName, "/", "%")
|
||||
destSecretDir := filepath.Join(destVaultDir, "secrets.d", destStorageName)
|
||||
|
||||
exists, err := afero.DirExists(v.fs, destSecretDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check destination: %w", err)
|
||||
}
|
||||
|
||||
if exists && !force {
|
||||
return fmt.Errorf("secret '%s' already exists in vault '%s' (use --force to overwrite)",
|
||||
destSecretName, v.Name)
|
||||
}
|
||||
|
||||
if exists && force {
|
||||
// Remove existing secret
|
||||
secret.Debug("Removing existing destination secret", "path", destSecretDir)
|
||||
if err := v.fs.RemoveAll(destSecretDir); err != nil {
|
||||
return fmt.Errorf("failed to remove existing destination secret: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Get source vault's long-term key
|
||||
srcIdentity, err := srcVault.GetOrDeriveLongTermKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unlock source vault '%s': %w", srcVault.Name, err)
|
||||
}
|
||||
|
||||
// Get source secret directory
|
||||
srcVaultDir, err := srcVault.GetDirectory()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get source vault directory: %w", err)
|
||||
}
|
||||
|
||||
srcStorageName := strings.ReplaceAll(srcSecretName, "/", "%")
|
||||
srcSecretDir := filepath.Join(srcVaultDir, "secrets.d", srcStorageName)
|
||||
|
||||
// List all versions
|
||||
versions, err := secret.ListVersions(srcVault.fs, srcSecretDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list source versions: %w", err)
|
||||
}
|
||||
|
||||
if len(versions) == 0 {
|
||||
return fmt.Errorf("source secret '%s' has no versions", srcSecretName)
|
||||
}
|
||||
|
||||
// Get current version name
|
||||
currentVersion, err := secret.GetCurrentVersion(srcVault.fs, srcSecretDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current version: %w", err)
|
||||
}
|
||||
|
||||
// Create destination secret directory
|
||||
if err := v.fs.MkdirAll(destSecretDir, secret.DirPerms); err != nil {
|
||||
return fmt.Errorf("failed to create destination secret directory: %w", err)
|
||||
}
|
||||
|
||||
// Copy each version
|
||||
for _, versionName := range versions {
|
||||
srcVersion := secret.NewVersion(srcVault, srcSecretName, versionName)
|
||||
if err := v.CopySecretVersion(srcVersion, srcIdentity, destSecretName, versionName); err != nil {
|
||||
// Rollback: remove partial copy
|
||||
secret.Debug("Rolling back partial copy due to error", "error", err)
|
||||
_ = v.fs.RemoveAll(destSecretDir)
|
||||
|
||||
return fmt.Errorf("failed to copy version %s: %w", versionName, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set current version
|
||||
if err := secret.SetCurrentVersion(v.fs, destSecretDir, currentVersion); err != nil {
|
||||
_ = v.fs.RemoveAll(destSecretDir)
|
||||
|
||||
return fmt.Errorf("failed to set current version: %w", err)
|
||||
}
|
||||
|
||||
secret.DebugWith("Successfully copied all secret versions",
|
||||
slog.String("src_vault", srcVault.Name),
|
||||
slog.String("dest_vault", v.Name),
|
||||
slog.Int("version_count", len(versions)),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user