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:
2025-12-23 15:24:13 +07:00
parent 7264026d66
commit 128c53a11d
5 changed files with 559 additions and 30 deletions

View File

@@ -200,6 +200,12 @@ func TestSecretManagerIntegration(t *testing.T) {
// Expected: Secret moved to new location, old location removed
test12bMoveSecret(t, testMnemonic, runSecret, runSecretWithStdin)
// Test 12c: Cross-vault move
// Commands: secret move work:secret default, secret move work:secret default:newname
// Purpose: Test moving secrets between vaults with re-encryption
// Expected: Secret copied to destination vault with all versions, source deleted
test12cCrossVaultMove(t, testMnemonic, runSecretWithEnv, runSecretWithStdin)
// Test 13: Unlocker management
// Commands: secret unlocker list, secret unlocker add pgp
// Purpose: Test multiple unlocker types
@@ -1164,6 +1170,89 @@ func test12bMoveSecret(t *testing.T, testMnemonic string, runSecret func(...stri
assert.Equal(t, "original-value", getOutput, "source should still have original value")
}
func test12cCrossVaultMove(t *testing.T, testMnemonic string, runSecretWithEnv func(map[string]string, ...string) (string, error), runSecretWithStdin func(string, map[string]string, ...string) (string, error)) {
env := map[string]string{
"SB_SECRET_MNEMONIC": testMnemonic,
}
// Create a test secret in the work vault
_, err := runSecretWithEnv(env, "vault", "select", "work")
require.NoError(t, err, "select work vault should succeed")
// Add a secret with a version
_, err = runSecretWithStdin("cross-vault-value-v1", env, "add", "cross/move/test")
require.NoError(t, err, "add cross/move/test should succeed")
// Add another version
_, err = runSecretWithStdin("cross-vault-value-v2", env, "add", "--force", "cross/move/test")
require.NoError(t, err, "add cross/move/test v2 should succeed")
// Move to default vault using cross-vault syntax
output, err := runSecretWithEnv(env, "move", "work:cross/move/test", "default")
require.NoError(t, err, "cross-vault move should succeed")
assert.Contains(t, output, "Moved secret", "should show move confirmation")
assert.Contains(t, output, "2 version(s)", "should show version count")
// Verify secret exists in default vault
_, err = runSecretWithEnv(env, "vault", "select", "default")
require.NoError(t, err, "select default vault should succeed")
value, err := runSecretWithEnv(env, "get", "cross/move/test")
require.NoError(t, err, "get from default vault should succeed")
assert.Equal(t, "cross-vault-value-v2", value, "should have latest version value")
// Verify secret no longer exists in work vault
_, err = runSecretWithEnv(env, "vault", "select", "work")
require.NoError(t, err, "select work vault should succeed")
_, err = runSecretWithEnv(env, "get", "cross/move/test")
assert.Error(t, err, "get from work vault should fail after move")
// Test cross-vault move with rename
_, err = runSecretWithStdin("rename-test", env, "add", "rename/source")
require.NoError(t, err, "add rename/source should succeed")
output, err = runSecretWithEnv(env, "move", "work:rename/source", "default:renamed/dest")
require.NoError(t, err, "cross-vault move with rename should succeed")
assert.Contains(t, output, "Moved secret", "should show move confirmation")
// Verify renamed secret exists in default vault
_, err = runSecretWithEnv(env, "vault", "select", "default")
require.NoError(t, err, "select default vault should succeed")
value, err = runSecretWithEnv(env, "get", "renamed/dest")
require.NoError(t, err, "get renamed secret should succeed")
assert.Equal(t, "rename-test", value, "should have correct value")
// Test --force flag for overwriting
_, err = runSecretWithStdin("existing-secret", env, "add", "force/test")
require.NoError(t, err, "add force/test in default should succeed")
_, err = runSecretWithEnv(env, "vault", "select", "work")
require.NoError(t, err, "select work vault should succeed")
_, err = runSecretWithStdin("new-value", env, "add", "force/test")
require.NoError(t, err, "add force/test in work should succeed")
// Move without force should fail
output, err = runSecretWithEnv(env, "move", "work:force/test", "default")
assert.Error(t, err, "move without force should fail when dest exists")
assert.Contains(t, output, "already exists", "should indicate destination exists")
// Move with force should succeed
output, err = runSecretWithEnv(env, "move", "--force", "work:force/test", "default")
require.NoError(t, err, "move with force should succeed")
assert.Contains(t, output, "Moved secret", "should show move confirmation")
// Verify value was overwritten
_, err = runSecretWithEnv(env, "vault", "select", "default")
require.NoError(t, err, "select default vault should succeed")
value, err = runSecretWithEnv(env, "get", "force/test")
require.NoError(t, err, "get overwritten secret should succeed")
assert.Equal(t, "new-value", value, "should have new value after force move")
}
func test13UnlockerManagement(t *testing.T, tempDir, testMnemonic string, runSecret func(...string) (string, error), runSecretWithEnv func(map[string]string, ...string) (string, error)) {
// Make sure we're in default vault
_, err := runSecret("vault", "select", "default")