check / check (push) Failing after 1s
init and vault create put the mnemonic into the process environment for vault.CreateVault to read back, so every program they ran, gpg included, inherited it, and SB_SECRET_MNEMONIC and SB_UNLOCK_PASSPHRASE were read at 13 places and never unset. Each command that may need them now reads both once, in its RunE, into locked buffers on the CLI Instance, and unsets them at once. The buffers are passed down: vault.CreateVault takes the mnemonic, a Vault carries Mnemonic and UnlockPassphrase, and the PGP, keychain and Secure Enclave unlocker constructors take both. Nothing below the command reads the environment. README warns against both variables. Model: opus-5-5
236 lines
7.2 KiB
Go
236 lines
7.2 KiB
Go
package cli_test
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"git.eeqj.de/sneak/secret/internal/cli"
|
|
"git.eeqj.de/sneak/secret/internal/vault"
|
|
"github.com/awnumar/memguard"
|
|
"github.com/spf13/afero"
|
|
"github.com/spf13/cobra"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestRejectedMoveWithinVaultLeavesStateUnchanged is a regression test for
|
|
// https://git.eeqj.de/sneak/secret/issues/73, where a forced move of a secret
|
|
// onto itself deleted it, also when "work" was spelled two ways, and a failed
|
|
// move within "work" left "work" the current vault. "default" is the current
|
|
// vault in every case, and each case runs on its own copy of the state
|
|
// directory.
|
|
func TestRejectedMoveWithinVaultLeavesStateUnchanged(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
before := snapshotStateDir(t, newTwoVaultFs(t))
|
|
require.Equal(t, "default", before[testStateDir+"/currentvault"])
|
|
|
|
const (
|
|
ontoItself = "secret 'x' cannot be moved onto itself"
|
|
workX = "work:x"
|
|
)
|
|
|
|
tests := []struct {
|
|
command string
|
|
source, dest string
|
|
force bool
|
|
wantErr string
|
|
}{
|
|
{"mv x x", "x", "x", false, ontoItself},
|
|
{"mv --force x x", "x", "x", true, ontoItself},
|
|
{"mv --force work:x work:", workX, "work:", true, ontoItself},
|
|
// An empty destination name defaults to the source name.
|
|
{`mv --force work:x ""`, workX, "", true, ontoItself},
|
|
// "work" is a vault name, so the destination is work:x.
|
|
{"mv --force work:x work", workX, "work", true, ontoItself},
|
|
{
|
|
"mv work:nosuch work:y", "work:nosuch", "work:y", false,
|
|
"secret 'nosuch' not found",
|
|
},
|
|
// Only an existing vault is used.
|
|
{
|
|
"mv --force nosuch:x nosuch:y", "nosuch:x", "nosuch:y", true,
|
|
"vault 'nosuch' does not exist",
|
|
},
|
|
// Each of these spells "work" a second way. The spelling is not a
|
|
// valid vault name, so the move is not taken for a move between two
|
|
// vaults, which would delete the destination, here the source.
|
|
{
|
|
"mv --force work:x work/:x", workX, "work/:x", true,
|
|
vault.ValidateVaultName("work/").Error(),
|
|
},
|
|
{
|
|
"mv --force work/:x work:", "work/:x", "work:", true,
|
|
vault.ValidateVaultName("work/").Error(),
|
|
},
|
|
{
|
|
"mv --force work:x ./work:x", workX, "./work:x", true,
|
|
vault.ValidateVaultName("./work").Error(),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.command, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := newFsFromSnapshot(t, before)
|
|
c := cli.NewCLIInstanceWithStateDir(fs, testStateDir)
|
|
|
|
err := c.MoveSecret(&cobra.Command{}, tt.source, tt.dest, tt.force)
|
|
|
|
require.Equal(t, before, snapshotStateDir(t, fs))
|
|
require.EqualError(t, err, tt.wantErr)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestMoveWithinOtherVaultKeepsCurrentVault checks that `secret mv work:x
|
|
// work:y`, with "default" the current vault, renames "x" to "y" in "work" and
|
|
// leaves "default" the current vault.
|
|
func TestMoveWithinOtherVaultKeepsCurrentVault(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := newTwoVaultFs(t)
|
|
c := cli.NewCLIInstanceWithStateDir(fs, testStateDir)
|
|
|
|
err := c.MoveSecret(&cobra.Command{}, "work:x", "work:y", false)
|
|
require.NoError(t, err)
|
|
|
|
after := snapshotStateDir(t, fs)
|
|
workSecrets := testStateDir + "/vaults.d/work/secrets.d/"
|
|
|
|
require.Equal(t, "default", after[testStateDir+"/currentvault"])
|
|
require.Contains(t, after, workSecrets+"y/")
|
|
require.NotContains(t, after, workSecrets+"x/")
|
|
}
|
|
|
|
// TestMoveOntoSameSecretUnderAnotherNameIsRejected is a regression test for
|
|
// https://git.eeqj.de/sneak/secret/issues/78: on a case-insensitive
|
|
// filesystem "Foo" and "foo" are one secret, and `secret mv --force Foo foo`
|
|
// removed the destination, which was the source. Symbolic links on the real
|
|
// filesystem give one secret two names here: in "default", "y" is a link to
|
|
// the secret "x", and the secrets.d of "other" is a link to that of
|
|
// "default", so other:x is default:x. Each move must be rejected and leave
|
|
// the secret and the links as they were.
|
|
func TestMoveOntoSameSecretUnderAnotherNameIsRejected(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const isSame = "is the same secret on this filesystem"
|
|
|
|
tests := []struct {
|
|
command string
|
|
source, dest string
|
|
force bool
|
|
wantErr string
|
|
}{
|
|
{
|
|
"mv --force y x", "y", "x", true,
|
|
"secret 'y' cannot be moved onto itself: 'x' " + isSame,
|
|
},
|
|
{
|
|
"mv --force x y", "x", "y", true,
|
|
"secret 'x' cannot be moved onto itself: 'y' " + isSame,
|
|
},
|
|
{
|
|
"mv x y", "x", "y", false,
|
|
"secret 'x' cannot be moved onto itself: 'y' " + isSame,
|
|
},
|
|
{
|
|
"mv --force default:x other:x", "default:x", "other:x", true,
|
|
"secret 'default:x' cannot be moved onto itself: 'other:x' " +
|
|
isSame,
|
|
},
|
|
{
|
|
"mv default:x other", "default:x", "other", false,
|
|
"secret 'default:x' cannot be moved onto itself: 'other:x' " +
|
|
isSame,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.command, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewOsFs()
|
|
stateDir := t.TempDir()
|
|
vaultsDir := filepath.Join(stateDir, "vaults.d")
|
|
|
|
// "default" is created last, so it is the current vault.
|
|
_, err := vault.CreateVault(fs, stateDir, "other", testMnemonicBuffer(t))
|
|
require.NoError(t, err)
|
|
|
|
vlt, err := vault.CreateVault(fs, stateDir, "default", testMnemonicBuffer(t))
|
|
require.NoError(t, err)
|
|
|
|
err = vlt.AddSecret("x", memguard.NewBufferFromBytes([]byte("value")), false)
|
|
require.NoError(t, err)
|
|
|
|
defaultSecrets := filepath.Join(vaultsDir, "default", "secrets.d")
|
|
otherSecrets := filepath.Join(vaultsDir, "other", "secrets.d")
|
|
link := filepath.Join(defaultSecrets, "y")
|
|
|
|
require.NoError(t, os.Symlink("x", link))
|
|
require.NoError(t, os.Remove(otherSecrets))
|
|
require.NoError(t, os.Symlink(defaultSecrets, otherSecrets))
|
|
|
|
c := cli.NewCLIInstanceWithStateDir(fs, stateDir)
|
|
moveErr := c.MoveSecret(&cobra.Command{}, tt.source, tt.dest, tt.force)
|
|
|
|
value, err := vlt.GetSecret("x")
|
|
require.NoError(t, err)
|
|
|
|
defer value.Destroy()
|
|
|
|
require.Equal(t, []byte("value"), value.Bytes())
|
|
|
|
target, err := os.Readlink(link)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "x", target)
|
|
|
|
target, err = os.Readlink(otherSecrets)
|
|
require.NoError(t, err)
|
|
require.Equal(t, defaultSecrets, target)
|
|
|
|
require.EqualError(t, moveErr, tt.wantErr)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestForcedCaseOnlyMoveOnCaseSensitiveFilesystem checks that where "Foo"
|
|
// and "foo" are two secrets, `secret mv --force Foo foo` still replaces "foo"
|
|
// with "Foo".
|
|
func TestForcedCaseOnlyMoveOnCaseSensitiveFilesystem(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewOsFs()
|
|
stateDir := t.TempDir()
|
|
|
|
vlt, err := vault.CreateVault(fs, stateDir, "default", testMnemonicBuffer(t))
|
|
require.NoError(t, err)
|
|
|
|
err = vlt.AddSecret("Foo", memguard.NewBufferFromBytes([]byte("upper")), false)
|
|
require.NoError(t, err)
|
|
|
|
_, err = os.Stat(filepath.Join(stateDir, "vaults.d", "default", "secrets.d", "foo"))
|
|
if err == nil {
|
|
t.Skip("the temporary directory is on a case-insensitive filesystem")
|
|
}
|
|
|
|
err = vlt.AddSecret("foo", memguard.NewBufferFromBytes([]byte("lower")), false)
|
|
require.NoError(t, err)
|
|
|
|
c := cli.NewCLIInstanceWithStateDir(fs, stateDir)
|
|
err = c.MoveSecret(&cobra.Command{}, "Foo", "foo", true)
|
|
require.NoError(t, err)
|
|
|
|
value, err := vlt.GetSecret("foo")
|
|
require.NoError(t, err)
|
|
|
|
defer value.Destroy()
|
|
|
|
require.Equal(t, []byte("upper"), value.Bytes())
|
|
|
|
_, err = vlt.GetSecret("Foo")
|
|
require.ErrorIs(t, err, vault.ErrSecretNotFound)
|
|
}
|