|
|
|
@@ -4,10 +4,12 @@ import (
|
|
|
|
|
"errors"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"filippo.io/age"
|
|
|
|
|
"git.eeqj.de/sneak/secret/internal/secret"
|
|
|
|
|
"git.eeqj.de/sneak/secret/internal/vault"
|
|
|
|
|
"github.com/awnumar/memguard"
|
|
|
|
|
"github.com/spf13/afero"
|
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
@@ -16,6 +18,19 @@ import (
|
|
|
|
|
|
|
|
|
|
var errInjected = errors.New("injected failure")
|
|
|
|
|
|
|
|
|
|
// The kinds of change hookFs passes to before.
|
|
|
|
|
const (
|
|
|
|
|
opCreate = "create"
|
|
|
|
|
opOpen = "open"
|
|
|
|
|
opMkdir = "mkdir"
|
|
|
|
|
opRemove = "remove"
|
|
|
|
|
opRename = "rename"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// currentFile is the file in a secret's directory that names its current
|
|
|
|
|
// version.
|
|
|
|
|
const currentFile = "current"
|
|
|
|
|
|
|
|
|
|
// hookFs passes every call through to Fs, but first calls before for each
|
|
|
|
|
// call that changes the filesystem, with the path it changes (the new path,
|
|
|
|
|
// for Rename). A test uses before to inspect the tree at every point where
|
|
|
|
@@ -29,7 +44,7 @@ type hookFs struct {
|
|
|
|
|
|
|
|
|
|
//nolint:ireturn // implements afero.Fs
|
|
|
|
|
func (h hookFs) Create(name string) (afero.File, error) {
|
|
|
|
|
err := h.before("create", name)
|
|
|
|
|
err := h.before(opCreate, name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
@@ -41,7 +56,7 @@ func (h hookFs) Create(name string) (afero.File, error) {
|
|
|
|
|
func (h hookFs) OpenFile(
|
|
|
|
|
name string, flag int, perm os.FileMode,
|
|
|
|
|
) (afero.File, error) {
|
|
|
|
|
err := h.before("open", name)
|
|
|
|
|
err := h.before(opOpen, name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
@@ -50,7 +65,7 @@ func (h hookFs) OpenFile(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h hookFs) Mkdir(name string, perm os.FileMode) error {
|
|
|
|
|
err := h.before("mkdir", name)
|
|
|
|
|
err := h.before(opMkdir, name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
@@ -59,7 +74,7 @@ func (h hookFs) Mkdir(name string, perm os.FileMode) error {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h hookFs) MkdirAll(path string, perm os.FileMode) error {
|
|
|
|
|
err := h.before("mkdir", path)
|
|
|
|
|
err := h.before(opMkdir, path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
@@ -68,7 +83,7 @@ func (h hookFs) MkdirAll(path string, perm os.FileMode) error {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h hookFs) Remove(name string) error {
|
|
|
|
|
err := h.before("remove", name)
|
|
|
|
|
err := h.before(opRemove, name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
@@ -77,7 +92,7 @@ func (h hookFs) Remove(name string) error {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h hookFs) RemoveAll(path string) error {
|
|
|
|
|
err := h.before("remove", path)
|
|
|
|
|
err := h.before(opRemove, path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
@@ -86,7 +101,7 @@ func (h hookFs) RemoveAll(path string) error {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h hookFs) Rename(oldname, newname string) error {
|
|
|
|
|
err := h.before("rename", newname)
|
|
|
|
|
err := h.before(opRename, newname)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
@@ -152,6 +167,24 @@ func writeLongTermKey(
|
|
|
|
|
return ltIdentity
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// newVaultWithSecret creates the vault name under stateDir from the test
|
|
|
|
|
// mnemonic, with a secret "shared" in it that holds value.
|
|
|
|
|
func newVaultWithSecret(
|
|
|
|
|
t *testing.T, fs afero.Fs, stateDir, name, value string,
|
|
|
|
|
) *vault.Vault {
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
|
|
vlt, err := vault.CreateVault(fs, stateDir, name)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
buffer := memguard.NewBufferFromBytes([]byte(value))
|
|
|
|
|
defer buffer.Destroy()
|
|
|
|
|
|
|
|
|
|
require.NoError(t, vlt.AddSecret("shared", buffer, false))
|
|
|
|
|
|
|
|
|
|
return vlt
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestWriteFileAtomicReplacesFile(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
@@ -160,7 +193,7 @@ func TestWriteFileAtomicReplacesFile(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
fs, dir := tfs.open(t)
|
|
|
|
|
path := filepath.Join(dir, "current")
|
|
|
|
|
path := filepath.Join(dir, currentFile)
|
|
|
|
|
|
|
|
|
|
require.NoError(t, secret.WriteFileAtomic(fs, path, []byte("old")))
|
|
|
|
|
require.NoError(t, secret.WriteFileAtomic(fs, path, []byte("new")))
|
|
|
|
@@ -174,7 +207,7 @@ func TestWriteFileAtomicReplacesFile(t *testing.T) {
|
|
|
|
|
assert.Equal(t, secret.FilePerms, info.Mode().Perm())
|
|
|
|
|
|
|
|
|
|
// No temporary file is left next to it
|
|
|
|
|
assert.Equal(t, []string{"current"}, dirNames(t, fs, dir))
|
|
|
|
|
assert.Equal(t, []string{currentFile}, dirNames(t, fs, dir))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -187,11 +220,11 @@ func TestWriteFileAtomicFailureKeepsOldFile(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
base, dir := tfs.open(t)
|
|
|
|
|
path := filepath.Join(dir, "current")
|
|
|
|
|
path := filepath.Join(dir, currentFile)
|
|
|
|
|
require.NoError(t, secret.WriteFileAtomic(base, path, []byte("old")))
|
|
|
|
|
|
|
|
|
|
fs := hookFs{Fs: base, before: func(op, _ string) error {
|
|
|
|
|
if op == "rename" {
|
|
|
|
|
if op == opRename {
|
|
|
|
|
return errInjected
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -206,11 +239,14 @@ func TestWriteFileAtomicFailureKeepsOldFile(t *testing.T) {
|
|
|
|
|
assert.Equal(t, "old", string(data))
|
|
|
|
|
|
|
|
|
|
// The temporary file is removed again
|
|
|
|
|
assert.Equal(t, []string{"current"}, dirNames(t, base, dir))
|
|
|
|
|
assert.Equal(t, []string{currentFile}, dirNames(t, base, dir))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestRemoveDirAtomic checks that RemoveDirAtomic deletes nothing where the
|
|
|
|
|
// directory stands, which a crash could stop half-way, and that it leaves
|
|
|
|
|
// nothing behind.
|
|
|
|
|
func TestRemoveDirAtomic(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
@@ -218,20 +254,132 @@ func TestRemoveDirAtomic(t *testing.T) {
|
|
|
|
|
t.Run(tfs.name, func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
fs, dir := tfs.open(t)
|
|
|
|
|
base, dir := tfs.open(t)
|
|
|
|
|
listed := filepath.Join(dir, "secrets.d")
|
|
|
|
|
target := filepath.Join(listed, "doomed")
|
|
|
|
|
|
|
|
|
|
require.NoError(t, fs.MkdirAll(filepath.Join(target, "versions"), 0o700))
|
|
|
|
|
require.NoError(t, secret.WriteFileAtomic(fs,
|
|
|
|
|
filepath.Join(target, "current"), []byte("20231216.001")))
|
|
|
|
|
require.NoError(t, base.MkdirAll(filepath.Join(target, "versions"), 0o700))
|
|
|
|
|
require.NoError(t, secret.WriteFileAtomic(base,
|
|
|
|
|
filepath.Join(target, currentFile), []byte("20231216.001")))
|
|
|
|
|
|
|
|
|
|
fs := hookFs{Fs: base, before: func(op, path string) error {
|
|
|
|
|
if op == opRemove && strings.HasPrefix(path, target) {
|
|
|
|
|
t.Errorf("deleted %s where it stands", path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
require.NoError(t, secret.RemoveDirAtomic(fs, target))
|
|
|
|
|
|
|
|
|
|
// Gone, and no temporary directory is left in the directory
|
|
|
|
|
// that is listed or in the one above it
|
|
|
|
|
assert.Empty(t, dirNames(t, fs, listed))
|
|
|
|
|
assert.Equal(t, []string{"secrets.d"}, dirNames(t, fs, dir))
|
|
|
|
|
assert.Empty(t, dirNames(t, base, listed))
|
|
|
|
|
assert.Equal(t, []string{"secrets.d"}, dirNames(t, base, dir))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestLongestNames adds a secret to a vault and removes the vault, both
|
|
|
|
|
// named with 255 bytes, the most a file name may have, on the real
|
|
|
|
|
// filesystem: the temporary directories they use must fit that limit too.
|
|
|
|
|
func TestLongestNames(t *testing.T) {
|
|
|
|
|
t.Setenv(secret.EnvMnemonic, testMnemonic)
|
|
|
|
|
|
|
|
|
|
const longestName = 255
|
|
|
|
|
|
|
|
|
|
fs := afero.NewOsFs()
|
|
|
|
|
name := strings.Repeat("a", longestName)
|
|
|
|
|
|
|
|
|
|
vlt, err := vault.CreateVault(fs, t.TempDir(), name)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
value := memguard.NewBufferFromBytes([]byte("long"))
|
|
|
|
|
defer value.Destroy()
|
|
|
|
|
|
|
|
|
|
require.NoError(t, vlt.AddSecret(name, value, false))
|
|
|
|
|
|
|
|
|
|
got, err := vlt.GetSecret(name)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
assert.Equal(t, "long", string(got))
|
|
|
|
|
|
|
|
|
|
vaultDir, err := vlt.GetDirectory()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.NoError(t, secret.RemoveDirAtomic(fs, vaultDir))
|
|
|
|
|
assert.NoDirExists(t, vaultDir)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestForcedCopyKeepsDestinationUntilReplaced copies a secret over one in
|
|
|
|
|
// another vault, as a forced move between vaults does, and makes the last
|
|
|
|
|
// step that completes the copy fail. The secret it was to replace must
|
|
|
|
|
// still be there unchanged: it may go only once its replacement is whole.
|
|
|
|
|
//
|
|
|
|
|
//nolint:paralleltest // t.Setenv forbids t.Parallel
|
|
|
|
|
func TestForcedCopyKeepsDestinationUntilReplaced(t *testing.T) {
|
|
|
|
|
t.Setenv(secret.EnvMnemonic, testMnemonic)
|
|
|
|
|
|
|
|
|
|
for _, tfs := range testFilesystems {
|
|
|
|
|
t.Run(tfs.name, func(t *testing.T) {
|
|
|
|
|
base, stateDir := tfs.open(t)
|
|
|
|
|
src := newVaultWithSecret(t, base, stateDir, "source", "new")
|
|
|
|
|
dest := newVaultWithSecret(t, base, stateDir, "dest", "old")
|
|
|
|
|
|
|
|
|
|
// The copy is complete once its current file is written
|
|
|
|
|
fs := hookFs{Fs: base, before: func(op, path string) error {
|
|
|
|
|
if op == opRename && filepath.Base(path) == currentFile {
|
|
|
|
|
return errInjected
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
err := vault.NewVault(fs, stateDir, "dest").
|
|
|
|
|
CopySecretAllVersions(src, "shared", "shared", true)
|
|
|
|
|
require.ErrorIs(t, err, errInjected)
|
|
|
|
|
|
|
|
|
|
value, err := dest.GetSecret("shared")
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
assert.Equal(t, "old", string(value))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestTempDirsStayOutOfListings adds a version, adds a secret, copies a
|
|
|
|
|
// secret over another and removes one, and checks that none of them makes a
|
|
|
|
|
// directory directly in secrets.d or in a versions directory. Those are
|
|
|
|
|
// listed to find secrets and versions, so a temporary directory made there
|
|
|
|
|
// would be listed while half-built, and one left by a crash would stay.
|
|
|
|
|
//
|
|
|
|
|
//nolint:paralleltest // t.Setenv forbids t.Parallel
|
|
|
|
|
func TestTempDirsStayOutOfListings(t *testing.T) {
|
|
|
|
|
t.Setenv(secret.EnvMnemonic, testMnemonic)
|
|
|
|
|
|
|
|
|
|
for _, tfs := range testFilesystems {
|
|
|
|
|
t.Run(tfs.name, func(t *testing.T) {
|
|
|
|
|
base, stateDir := tfs.open(t)
|
|
|
|
|
newVaultWithSecret(t, base, stateDir, "default", "first")
|
|
|
|
|
|
|
|
|
|
fs := hookFs{Fs: base, before: func(op, path string) error {
|
|
|
|
|
parent := filepath.Base(filepath.Dir(path))
|
|
|
|
|
if op == opMkdir && (parent == "secrets.d" || parent == "versions") {
|
|
|
|
|
t.Errorf("made %s where it is listed", path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}}
|
|
|
|
|
vlt := vault.NewVault(fs, stateDir, "default")
|
|
|
|
|
|
|
|
|
|
value := memguard.NewBufferFromBytes([]byte("second"))
|
|
|
|
|
defer value.Destroy()
|
|
|
|
|
|
|
|
|
|
require.NoError(t, vlt.AddSecret("shared", value, true))
|
|
|
|
|
require.NoError(t, vlt.AddSecret("other", value, false))
|
|
|
|
|
require.NoError(t, vlt.CopySecretAllVersions(vlt, "shared", "other", true))
|
|
|
|
|
|
|
|
|
|
vaultDir, err := vlt.GetDirectory()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.NoError(t, secret.RemoveDirAtomic(fs,
|
|
|
|
|
filepath.Join(vaultDir, "secrets.d", "shared")))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -301,7 +449,7 @@ func TestVersionSaveFailureLeavesNothing(t *testing.T) {
|
|
|
|
|
writeLongTermKey(t, base, stateDir)
|
|
|
|
|
|
|
|
|
|
fs := hookFs{Fs: base, before: func(op, path string) error {
|
|
|
|
|
if op == "rename" && filepath.Base(path) == "priv.age" {
|
|
|
|
|
if op == opRename && filepath.Base(path) == "priv.age" {
|
|
|
|
|
return errInjected
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -339,7 +487,7 @@ func TestSetCurrentVersionNeverMissing(t *testing.T) {
|
|
|
|
|
require.NoError(t, base.MkdirAll(secretDir, 0o700))
|
|
|
|
|
require.NoError(t, secret.SetCurrentVersion(base, secretDir, "20231216.001"))
|
|
|
|
|
|
|
|
|
|
currentPath := filepath.Join(secretDir, "current")
|
|
|
|
|
currentPath := filepath.Join(secretDir, currentFile)
|
|
|
|
|
fs := hookFs{Fs: base, before: func(string, string) error {
|
|
|
|
|
exists, err := afero.Exists(base, currentPath)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|