|
|
|
@@ -4,12 +4,10 @@ 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"
|
|
|
|
@@ -18,19 +16,6 @@ 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
|
|
|
|
@@ -44,7 +29,7 @@ type hookFs struct {
|
|
|
|
|
|
|
|
|
|
//nolint:ireturn // implements afero.Fs
|
|
|
|
|
func (h hookFs) Create(name string) (afero.File, error) {
|
|
|
|
|
err := h.before(opCreate, name)
|
|
|
|
|
err := h.before("create", name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
@@ -56,7 +41,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(opOpen, name)
|
|
|
|
|
err := h.before("open", name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
@@ -65,7 +50,7 @@ func (h hookFs) OpenFile(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h hookFs) Mkdir(name string, perm os.FileMode) error {
|
|
|
|
|
err := h.before(opMkdir, name)
|
|
|
|
|
err := h.before("mkdir", name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
@@ -74,7 +59,7 @@ func (h hookFs) Mkdir(name string, perm os.FileMode) error {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h hookFs) MkdirAll(path string, perm os.FileMode) error {
|
|
|
|
|
err := h.before(opMkdir, path)
|
|
|
|
|
err := h.before("mkdir", path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
@@ -83,7 +68,7 @@ func (h hookFs) MkdirAll(path string, perm os.FileMode) error {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h hookFs) Remove(name string) error {
|
|
|
|
|
err := h.before(opRemove, name)
|
|
|
|
|
err := h.before("remove", name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
@@ -92,7 +77,7 @@ func (h hookFs) Remove(name string) error {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h hookFs) RemoveAll(path string) error {
|
|
|
|
|
err := h.before(opRemove, path)
|
|
|
|
|
err := h.before("remove", path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
@@ -101,7 +86,7 @@ func (h hookFs) RemoveAll(path string) error {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h hookFs) Rename(oldname, newname string) error {
|
|
|
|
|
err := h.before(opRename, newname)
|
|
|
|
|
err := h.before("rename", newname)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
@@ -167,24 +152,6 @@ 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()
|
|
|
|
|
|
|
|
|
@@ -193,7 +160,7 @@ func TestWriteFileAtomicReplacesFile(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
fs, dir := tfs.open(t)
|
|
|
|
|
path := filepath.Join(dir, currentFile)
|
|
|
|
|
path := filepath.Join(dir, "current")
|
|
|
|
|
|
|
|
|
|
require.NoError(t, secret.WriteFileAtomic(fs, path, []byte("old")))
|
|
|
|
|
require.NoError(t, secret.WriteFileAtomic(fs, path, []byte("new")))
|
|
|
|
@@ -207,7 +174,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{currentFile}, dirNames(t, fs, dir))
|
|
|
|
|
assert.Equal(t, []string{"current"}, dirNames(t, fs, dir))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -220,11 +187,11 @@ func TestWriteFileAtomicFailureKeepsOldFile(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
base, dir := tfs.open(t)
|
|
|
|
|
path := filepath.Join(dir, currentFile)
|
|
|
|
|
path := filepath.Join(dir, "current")
|
|
|
|
|
require.NoError(t, secret.WriteFileAtomic(base, path, []byte("old")))
|
|
|
|
|
|
|
|
|
|
fs := hookFs{Fs: base, before: func(op, _ string) error {
|
|
|
|
|
if op == opRename {
|
|
|
|
|
if op == "rename" {
|
|
|
|
|
return errInjected
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -239,14 +206,11 @@ func TestWriteFileAtomicFailureKeepsOldFile(t *testing.T) {
|
|
|
|
|
assert.Equal(t, "old", string(data))
|
|
|
|
|
|
|
|
|
|
// The temporary file is removed again
|
|
|
|
|
assert.Equal(t, []string{currentFile}, dirNames(t, base, dir))
|
|
|
|
|
assert.Equal(t, []string{"current"}, 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()
|
|
|
|
|
|
|
|
|
@@ -254,132 +218,20 @@ func TestRemoveDirAtomic(t *testing.T) {
|
|
|
|
|
t.Run(tfs.name, func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
base, dir := tfs.open(t)
|
|
|
|
|
fs, dir := tfs.open(t)
|
|
|
|
|
listed := filepath.Join(dir, "secrets.d")
|
|
|
|
|
target := filepath.Join(listed, "doomed")
|
|
|
|
|
|
|
|
|
|
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, fs.MkdirAll(filepath.Join(target, "versions"), 0o700))
|
|
|
|
|
require.NoError(t, secret.WriteFileAtomic(fs,
|
|
|
|
|
filepath.Join(target, "current"), []byte("20231216.001")))
|
|
|
|
|
|
|
|
|
|
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, 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")))
|
|
|
|
|
assert.Empty(t, dirNames(t, fs, listed))
|
|
|
|
|
assert.Equal(t, []string{"secrets.d"}, dirNames(t, fs, dir))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -449,7 +301,7 @@ func TestVersionSaveFailureLeavesNothing(t *testing.T) {
|
|
|
|
|
writeLongTermKey(t, base, stateDir)
|
|
|
|
|
|
|
|
|
|
fs := hookFs{Fs: base, before: func(op, path string) error {
|
|
|
|
|
if op == opRename && filepath.Base(path) == "priv.age" {
|
|
|
|
|
if op == "rename" && filepath.Base(path) == "priv.age" {
|
|
|
|
|
return errInjected
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -487,7 +339,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, currentFile)
|
|
|
|
|
currentPath := filepath.Join(secretDir, "current")
|
|
|
|
|
fs := hookFs{Fs: base, before: func(string, string) error {
|
|
|
|
|
exists, err := afero.Exists(base, currentPath)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|