Make another unlocker current when the current one is removed (closes #137)
check / check (push) Canceled after 0s

Removing the vault's current unlocker left current-unlocker naming it,
so every command that opened the vault failed although another
unlocker remained, and the error pointed at the mnemonic.
RemoveUnlocker now makes the first other unlocker, in the order
`secret unlocker list` shows, current before the removal and returns
its ID, which `secret unlocker remove` prints; removing the last
unlocker also removes current-unlocker. When current-unlocker names an
unlocker that does not exist and the vault has others, the error wraps
ErrUnlockerNotFound and says to run `secret unlocker select`, without
the mnemonic advice.

Unverified: no test covers that advice for a vault other than the
current one, which gets `secret vault select` first.

Model: opus-5-5
This commit is contained in:
2026-10-14 18:16:59 +00:00
parent fd6c5df1a8
commit b9c552c3cc
8 changed files with 253 additions and 25 deletions
+32 -3
View File
@@ -4,9 +4,10 @@
// file the unlocker needs is missing or the passphrase is wrong, the error
// keeps its cause and ends by saying that the mnemonic still opens that
// vault, but only for a vault that the mnemonic does open, and not when the
// passphrase could not be read at all. When a secret's current file is
// missing, the error says how to make a version current again. Each test
// that pins such advice also follows it.
// passphrase could not be read at all. When the current unlocker does not
// exist and the vault has another, the error says to select one instead.
// When a secret's current file is missing, the error says how to make a
// version current again. Each test that pins such advice also follows it.
package cli_test
@@ -222,6 +223,34 @@ func TestMoveUnlockFailureNamesVault(t *testing.T) {
assert.Equal(t, "value", getSecret(t, c, "y"))
}
// TestMissingCurrentUnlockerNamesSelect checks the error of `secret get`
// when current-unlocker names an unlocker that does not exist and the vault
// has another: it names the missing unlocker and says to select another
// one, without the advice to use the mnemonic. The test then follows that
// advice.
func TestMissingCurrentUnlockerNamesSelect(t *testing.T) {
t.Parallel()
fs := newTwoVaultFs(t)
unlockerDir, c := newUnlockTestCLI(t, fs)
require.NoError(t, afero.WriteFile(fs,
filepath.Join(unlockTestVaultDir, "current-unlocker"),
[]byte("missing"), secret.FilePerms))
err := c.GetSecret(discardCmd(), "x")
require.ErrorIs(t, err, vault.ErrUnlockerNotFound)
require.EqualError(t, err, "failed to unlock vault: "+
"failed to get long-term key: failed to get current unlocker: "+
"unlocker with ID missing not found; the vault 'default' has other "+
"unlockers: run 'secret unlocker select' with the ID of one of them "+
"to make it the current unlocker")
require.NoError(t, c.UnlockerSelect(filepath.Base(unlockerDir)))
assert.Equal(t, "value", getSecret(t, c, "x"))
}
// TestPassphraseNotReadNamesNoMnemonic runs `secret get x` on the built
// binary without SB_UNLOCK_PASSPHRASE and with a stdin that is not a
// terminal, so the passphrase cannot be read. The unlocker was not tried,