package vaultik //nolint:testpackage // calls the unexported ownership check import ( "bytes" "context" "os" "strings" "syscall" "testing" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sneak.berlin/go/vaultik/internal/database" "sneak.berlin/go/vaultik/internal/log" "sneak.berlin/go/vaultik/internal/ui" ) // These tests cover https://git.eeqj.de/sneak/vaultik/issues/316: a // restore by a normal user warns that ownership was not preserved only // when some restored entry has another owner or group on disk than the // snapshot records. const ownershipWarning = "did not preserve file ownership" // TestOwnershipWarningSilentWhenEntriesMatch checks that a restore whose // entries all end up with their recorded owner and group prints no // warning. func TestOwnershipWarningSilentWhenEntriesMatch(t *testing.T) { log.Initialize(log.Config{}) t.Parallel() targetDir := t.TempDir() owner := ownerOf(t, targetDir) out := restoreAsNormalUser(t, targetDir, []*database.File{ {Path: "/a", Mode: plainFileMode, UID: owner.Uid, GID: owner.Gid}, {Path: "/b", Mode: plainFileMode, UID: owner.Uid, GID: owner.Gid}, }) assert.Empty(t, out) } // TestOwnershipWarningWhenOwnerDiffers checks that the warning is printed // when an entry recorded with another owner is restored owned by the // restoring user, even though its group matches the recorded one. func TestOwnershipWarningWhenOwnerDiffers(t *testing.T) { log.Initialize(log.Config{}) t.Parallel() targetDir := t.TempDir() owner := ownerOf(t, targetDir) out := restoreAsNormalUser(t, targetDir, []*database.File{ {Path: "/a", Mode: plainFileMode, UID: otherOwnerID, GID: owner.Gid}, }) assert.Equal(t, 1, strings.Count(out, ownershipWarning), out) } // TestOwnershipWarningWhenDirectoryGivesItsGroup checks that the warning // is printed, once, when entries recorded with the restoring user's // owner and group take another group from the setgid directory they are // restored into, as every new entry takes its directory's group on // macOS. func TestOwnershipWarningWhenDirectoryGivesItsGroup(t *testing.T) { log.Initialize(log.Config{}) t.Parallel() if os.Geteuid() != 0 { t.Skip("giving a directory to another group needs root") } targetDir := t.TempDir() user := ownerOf(t, targetDir) require.NoError(t, os.Chown(targetDir, -1, int(otherOwnerID))) require.NoError(t, os.Chmod(targetDir, os.ModeSetgid|writableTestMode)) out := restoreAsNormalUser(t, targetDir, []*database.File{ {Path: "/a", Mode: plainFileMode, UID: user.Uid, GID: user.Gid}, {Path: "/b", Mode: plainFileMode, UID: user.Uid, GID: user.Gid}, }) assert.Equal(t, 1, strings.Count(out, ownershipWarning), out) } // ownerOf returns the owner and group path has on disk. func ownerOf(t *testing.T, path string) *syscall.Stat_t { t.Helper() info, err := os.Lstat(path) require.NoError(t, err) owner, ok := info.Sys().(*syscall.Stat_t) require.True(t, ok) return owner } // restoreAsNormalUser restores rows, all empty regular files, into // targetDir without changing any owner or group, as restore does for a // normal user, and returns what warnIfOwnershipNotPreserved prints. func restoreAsNormalUser( t *testing.T, targetDir string, rows []*database.File, ) string { t.Helper() ctx := context.Background() rows, repos := makeFiles(ctx, t, rows) var out bytes.Buffer v := newContainmentVaultik(ctx, afero.NewOsFs()) v.UI = ui.NewWithColor(&out, false) session := &restoreSession{ v: v, ctx: ctx, repos: repos, opts: &RestoreOptions{TargetDir: targetDir}, result: &RestoreResult{}, } for _, row := range rows { require.NoError(t, session.restoreFile(row)) } v.warnIfOwnershipNotPreserved(rows, targetDir) return out.String() }