diff --git a/cmd/mesh-host/kept_test.go b/cmd/mesh-host/kept_test.go index 6c9c5dc..209ce37 100644 --- a/cmd/mesh-host/kept_test.go +++ b/cmd/mesh-host/kept_test.go @@ -33,6 +33,14 @@ func TestAnAccessTheMeshMayNotKeepYetIsSaidAsAWaitOfItsOwnKind(t *testing.T) { if err := os.MkdirAll(filepath.Join(lib, "a film"), 0o755); err != nil { t.Fatal(err) } + // Run as root (the build seat's merge-check), the test's folders stand for an operator's, never root's. + if os.Geteuid() == 0 { + for _, p := range []string{dir, lib, filepath.Join(lib, "a film")} { + if err := os.Lchown(p, 65534, 65534); err != nil { + t.Fatal(err) + } + } + } v := judgeAccess(judgedAccess{module: "radarr", id: "radarr.movies", name: "movies", path: lib, users: []store.KeptUser{{Identity: "mesh-radarr", UID: 61001, Use: "read-write", From: "container"}}}) if v.state != link.StateWaiting || len(v.waits) != 1 { diff --git a/internal/apply/kept_test.go b/internal/apply/kept_test.go index 326be71..107f8ad 100644 --- a/internal/apply/kept_test.go +++ b/internal/apply/kept_test.go @@ -32,7 +32,7 @@ const ( // aclFolder is a folder of the test's own on a filesystem that carries ACLs, or the test is skipped saying why. func aclFolder(t *testing.T) string { t.Helper() - dir := t.TempDir() + dir := userDir(t) fd, err := unix.Open(dir, unix.O_RDONLY|unix.O_DIRECTORY, 0) if err != nil { t.Fatal(err) @@ -110,7 +110,9 @@ func effectiveOn(t *testing.T, path string, uid uint32) uint16 { func applyAccesses(t *testing.T, known store.State, resources ...string) (Report, store.State, error) { t.Helper() + toUser(t) d := declare(t, strings.Join(resources, ",")) + toUser(t) return Apply(context.Background(), archHost(t), d, known, store.OriginDeclared, noServices, nil, nil) } @@ -135,6 +137,7 @@ func TestAKeptFolderCarriesAnEntryPerUserAndNothingElseOfItChanges(t *testing.T) t.Fatal(err) } unix.Close(fd) + toUser(t) _, _, before := readACLs(t, lib) report, _, err := applyAccesses(t, store.State{}, @@ -253,6 +256,7 @@ func TestWhatIsMadeInAKeptFolderIsGivenItsUsersRights(t *testing.T) { if err := os.Chmod(usual, 0o644); err != nil { t.Fatal(err) } + toUser(t) c, err := JudgeKept(lib, rec.Want, ActLimits) if err != nil { t.Fatal(err) @@ -269,6 +273,7 @@ func TestWhatIsMadeInAKeptFolderIsGivenItsUsersRights(t *testing.T) { if r, _ := keptRecord(lib); !r.OutOfWatches { t.Error("watches that ran out are not recorded") } + toUser(t) look := LookKept(lib, rec.Want, SweepLimits) if look.Repaired == 0 { t.Errorf("the sweep put nothing back: %+v", look) @@ -276,6 +281,7 @@ func TestWhatIsMadeInAKeptFolderIsGivenItsUsersRights(t *testing.T) { if got := effectiveOn(t, usual, radarr); got&6 != 6 { t.Fatalf("after the sweep a read-write identity may %s", permWords(got)) } + toUser(t) if c, _ := JudgeKept(lib, rec.Want, ActLimits); c.Narrowed != 0 { t.Fatalf("after the sweep %d entries still give a user less than its right", c.Narrowed) } @@ -329,6 +335,7 @@ func TestAFolderThatHoldsDataIsTakenOnlyOnAWarrantAndLetGoEntryByEntry(t *testin } d := declare(t, a) + toUser(t) done, said, err := TakeFolder(context.Background(), d, "lidarr.music", lib, "warrant-7", "the operator", noServices) if err != nil { t.Fatalf("the take act was refused: %v", err) @@ -357,6 +364,7 @@ func TestAFolderThatHoldsDataIsTakenOnlyOnAWarrantAndLetGoEntryByEntry(t *testin t.Fatal(err) } beforeMask, _ := func() (uint16, bool) { a, _, _ := readACLs(t, song); return a.perm(aclMask, 0) }() + toUser(t) ReconcileKept(context.Background()) for _, p := range []string{lib, song} { a, def, _ := readACLs(t, p) @@ -397,6 +405,7 @@ func TestAWalkEntersNoOtherFilesystem(t *testing.T) { }, fd: unix.Fstat, } + toUser(t) w, err := walkKept(dir, ActLimits, stats, nil, keepVisit(map[uint32]bool{radarr: true})) if err != nil { t.Fatal(err) @@ -456,10 +465,12 @@ func TestAMissingFolderIsMadeWhereItsDataLivesAndWaitsWhereItsDiskIsNot(t *testi if _, err := os.Stat(books); !errors.Is(err, os.ErrNotExist) { t.Fatal("a folder was made where its disk is not") } + toUser(t) if v := AccessVerdict(context.Background(), books, false, true); v.Kind != VerdictWaitDisk { t.Errorf("the look judges it %s, not waiting for its disk", v.Kind) } // And the folder made before, its dataset gone: its disk is not there, whatever is at its path now. + toUser(t) if v := AccessVerdict(context.Background(), movies, false, true); v.Kind != VerdictWaitDisk { t.Errorf("a kept folder whose dataset is not mounted is judged %s", v.Kind) } @@ -492,12 +503,14 @@ func TestTheSweepPutsBackWhatAnAppNarrowedAndCoversTheFolderOverLooks(t *testing if err := os.Chmod(lib, 0o755); err != nil { t.Fatal(err) } + toUser(t) if short, _ := judgeTop(lib, rec.Want); len(short) == 0 { t.Fatal("a folder whose mask an app narrowed is not said short") } if _, _, err := applyAccesses(t, store.State{}, a); err != nil { t.Fatal(err) } + toUser(t) if short, _ := judgeTop(lib, rec.Want); len(short) != 0 { t.Fatalf("the apply did not put the folder back: %v", short) } @@ -506,6 +519,7 @@ func TestTheSweepPutsBackWhatAnAppNarrowedAndCoversTheFolderOverLooks(t *testing looks, repaired := 0, 0 for { looks++ + toUser(t) look := LookKept(lib, rec.Want, limits) repaired += look.Repaired if look.Failed != "" { @@ -527,6 +541,7 @@ func TestTheSweepPutsBackWhatAnAppNarrowedAndCoversTheFolderOverLooks(t *testing if repaired != 25 { t.Errorf("the sweep put back %d of 25 narrowed entries", repaired) } + toUser(t) if c, _ := JudgeKept(lib, rec.Want, ActLimits); c.Narrowed != 0 { t.Errorf("after the looks %d entries still give the user less than its right", c.Narrowed) } @@ -562,6 +577,7 @@ func TestRootsDataOnItsPlacedFilesystemIsNeverKept(t *testing.T) { if o := outcomeOf(report, "m.data"); o.Action == "waiting" { t.Errorf("root's data waits, and is never asked about: %+v", o) } + toUser(t) if v := AccessVerdict(context.Background(), path, false, true); v.Kind != VerdictRoot { t.Errorf("the look judges root's data at %s %s", path, v.Kind) } @@ -625,6 +641,7 @@ func TestNoWalkRunsWhereTheDiskIsNotThere(t *testing.T) { t.Fatal(err) } MountPoints = func() []string { return []string{"/"} } + toUser(t) ReconcileKept(context.Background()) rec, had := keptRecord(lib) if !had || len(rec.Users) != 2 { @@ -638,6 +655,7 @@ func TestNoWalkRunsWhereTheDiskIsNotThere(t *testing.T) { } // The disk back: the drop runs. MountPoints = func() []string { return []string{"/", dataset} } + toUser(t) ReconcileKept(context.Background()) if _, still := func() (uint16, bool) { a, _, _ := readACLs(t, lib); return a.perm(aclUser, plex) }(); still { t.Fatal("the drop did not run once the disk was there") @@ -672,6 +690,7 @@ func TestADiskMountedOverAFolderKeptEmptyIsNotKeptWithoutAWarrant(t *testing.T) t.Fatal(err) } MountPoints = func() []string { return []string{"/", pool} } + toUser(t) if v := AccessVerdict(context.Background(), lib, false, true); v.Kind != VerdictWaitKeep { t.Fatalf("the look judges it %s, and the data on the new disk was never taken", v.Kind) } @@ -720,6 +739,7 @@ func TestWideningTheMaskGivesTheOwningGroupNothingMore(t *testing.T) { } before := groupRight() rec, _ := keptRecord(lib) + toUser(t) LookKept(lib, rec.Want, SweepLimits) if got := effectiveOn(t, ep, radarr); got&6 != 6 { t.Fatalf("the sweep did not widen the mask: %s", permWords(got)) @@ -825,13 +845,14 @@ func TestTheWatchEndsWithItsContext(t *testing.T) { // Review L6: a broken folder's words carry no path. func TestABrokenFoldersWordsCarryNoPath(t *testing.T) { - dir := t.TempDir() + dir := userDir(t) keeping(t, nil, nil) file := filepath.Join(dir, "notafolder") if err := os.WriteFile(file, nil, 0o644); err != nil { t.Fatal(err) } for _, path := range []string{file, filepath.Join(file, "below")} { + toUser(t) v := AccessVerdict(context.Background(), path, false, true) if v.Kind != VerdictBroken || strings.Contains(v.Why, "/") { t.Errorf("%s: %+v", path, v) @@ -896,6 +917,7 @@ func TestAContainerWaitsWhileItsAccessesDiskIsNotThere(t *testing.T) { } d := declare(t, accessJSON("radarr.movies", lib, "read-write", false, "mesh-radarr:61001:read-write:container")+ `,{"id":"radarr.app","type":"container","name":"radarr","image":"`+pinned+`","volumes":["`+lib+`:/movies"]}`) + toUser(t) report, _, _ := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared, run, nil, nil) if o := outcomeOf(report, "radarr.app"); o.Action != "waiting" { t.Fatalf("a container mounting a folder whose disk is not there was not held: %+v", o) @@ -919,6 +941,7 @@ func TestEntriesThatCouldNotBeDroppedAreSaidUntilTheyAre(t *testing.T) { t.Fatal(err) } MountPoints = func() []string { return []string{"/"} } + toUser(t) ReconcileKept(context.Background()) left := Leftovers() if len(left) != 1 || left[0].Access != "radarr.movies" || !strings.Contains(left[0].Why, "could not be removed") || @@ -926,6 +949,7 @@ func TestEntriesThatCouldNotBeDroppedAreSaidUntilTheyAre(t *testing.T) { t.Fatalf("entries that could not be dropped are not said, path-free: %+v", left) } MountPoints = func() []string { return []string{"/", dataset} } + toUser(t) ReconcileKept(context.Background()) if left := Leftovers(); len(left) != 0 { t.Fatalf("said after the drop ran: %+v", left) @@ -952,6 +976,7 @@ func TestEntriesLeftOnAFilesystemNoLongerAtTheFolderAreDroppedWhenItIsBack(t *te if _, _, err := applyAccesses(t, store.State{}, a); err != nil { t.Fatal(err) } + toUser(t) done, _, err := TakeFolder(context.Background(), declare(t, a), "radarr.media", lib, "w-1", "op", noServices) if err != nil { t.Fatal(err) @@ -971,6 +996,7 @@ func TestEntriesLeftOnAFilesystemNoLongerAtTheFolderAreDroppedWhenItIsBack(t *te } // The pool goes: the filesystem below holds the folder again, and the entries left there are taken out. MountPoints = func() []string { return []string{"/"} } + toUser(t) ReconcileKept(context.Background()) if _, still := func() (uint16, bool) { a, _, _ := readACLs(t, filepath.Join(lib, "old")) @@ -1058,6 +1084,7 @@ func placedPool(t *testing.T, source, fsid *string) (pool, lib, taken string) { if _, _, err := applyAccesses(t, store.State{}, notTaken); err != nil { t.Fatal(err) } + toUser(t) done, _, err := TakeFolder(context.Background(), declare(t, notTaken), "radarr.media", lib, "w-1", "op", noServices) if err != nil { t.Fatal(err) @@ -1082,6 +1109,7 @@ func TestAnotherFilesystemAtTheSameMountPointIsNotTaken(t *testing.T) { if o := outcomeOf(report, "radarr.media"); o.Action != "waiting" { t.Fatalf("another filesystem at the same mount point was kept on the old warrant: %+v", o) } + toUser(t) if v := AccessVerdict(context.Background(), lib, true, true); v.Kind != VerdictWaitKeep { t.Fatalf("the look judges it %s", v.Kind) } @@ -1100,6 +1128,7 @@ func TestADeviceRenamedIsTheSameFilesystem(t *testing.T) { if o := outcomeOf(report, "radarr.media"); o.Action == "waiting" { t.Fatalf("a device renamed reads as another filesystem: %+v", o) } + toUser(t) ReconcileKept(context.Background()) if got := effectiveOn(t, filepath.Join(lib, "old"), radarr); got != 7 { t.Fatalf("the library's entries were walked away: the identity may %s", permWords(got)) @@ -1123,11 +1152,13 @@ func TestAMovedFolderIsNotTakenOnTheOldWarrant(t *testing.T) { if _, _, err := applyAccesses(t, store.State{}, notTaken); err != nil { t.Fatal(err) } + toUser(t) done, _, err := TakeFolder(context.Background(), declare(t, notTaken), "radarr.media", lib, "w-1", "op", noServices) if err != nil { t.Fatal(err) } <-done + toUser(t) before := AccessVerdict(context.Background(), lib, true, true) film := filepath.Join(lib, "film") if err := os.WriteFile(film, []byte("data"), 0o644); err != nil { @@ -1148,10 +1179,12 @@ func TestAMovedFolderIsNotTakenOnTheOldWarrant(t *testing.T) { if o := outcomeOf(report, "radarr.media"); o.Action != "waiting" { t.Fatalf("the second apply kept it on the old warrant: %+v", o) } + toUser(t) v := AccessVerdict(context.Background(), lib, true, true) if v.Kind != VerdictWaitKeep || v.Placed == "" || v.Placed == before.Placed || strings.Contains(v.Placed, "/") { t.Fatalf("the look does not ask again naming the new filesystem: %+v (before %+v)", v, before) } + toUser(t) ReconcileKept(context.Background()) if got := effectiveOn(t, film, radarr); got != 0 { t.Fatal("the data on the new filesystem was walked") @@ -1199,6 +1232,7 @@ func TestEntriesLeftOnTheFilesystemTheFolderIsOnAreNeverWalkedAway(t *testing.T) f.Left = []store.KeptLeft{{Mount: pool, Placed: f.Placed, Users: users}} return true }) + toUser(t) ReconcileKept(context.Background()) if got := effectiveOn(t, filepath.Join(lib, "old"), radarr); got != 7 { t.Fatalf("kept=%v: entries on the folder's own filesystem were walked away", kept) @@ -1226,6 +1260,7 @@ func TestATakenOnOfTheOldFormIsNotTaken(t *testing.T) { f.Mount, f.TakenOn = "/", "/" return true }) + toUser(t) if v := AccessVerdict(context.Background(), lib, true, true); v.Kind != VerdictWaitKeep { t.Fatalf("an old-form record is read as taken: %s", v.Kind) } @@ -1263,6 +1298,7 @@ func takeIt(t *testing.T, lib, access string) { if _, _, err := applyAccesses(t, store.State{}, access); err != nil { t.Fatal(err) } + toUser(t) done, _, err := TakeFolder(context.Background(), declare(t, access), "radarr.media", lib, "w-1", "op", noServices) if err != nil { t.Fatal(err) @@ -1307,6 +1343,7 @@ func TestAFilesystemThatCannotBeNamedIsNeverKept(t *testing.T) { if got := effectiveOn(t, lib, radarr); got != 0 { t.Fatal("an entry was added on a filesystem that cannot be named") } + toUser(t) if _, _, err := TakeFolder(context.Background(), declare(t, access), "radarr.media", lib, "w", "op", noServices); err == nil { t.Fatal("the take act ran on a filesystem that cannot be named") } @@ -1327,6 +1364,7 @@ func TestAFilesystemsUUIDNamesItBeforeItsFsid(t *testing.T) { if o := outcomeOf(report, "radarr.media"); o.Action == "waiting" { t.Fatalf("a renumbered device with one UUID reads as another filesystem: %+v", o) } + toUser(t) ReconcileKept(context.Background()) if got := effectiveOn(t, filepath.Join(lib, "old"), radarr); got != 7 { t.Fatalf("the library's entries were walked away: %s", permWords(got)) @@ -1348,6 +1386,7 @@ func TestEntriesLeftOnAFilesystemThatNeverReturnsAreForgotten(t *testing.T) { if len(Leftovers()) != 1 { t.Fatal("the leftover is not said before it expires") } + toUser(t) said := ReconcileKept(context.Background()) if left := Leftovers(); len(left) != 0 { t.Fatalf("a leftover on a filesystem gone for longer than %s is still said: %+v", LeftFor, left) @@ -1446,3 +1485,39 @@ func flightsDone(t *testing.T) { return len(inFlight.reads) == 0 }) } + +// userDirs are the folders the running test made, which toUser gives an unprivileged owner when it runs as root. +var userDirs []string + +// userDir is a t.TempDir that toUser gives an unprivileged owner when the test runs as root (the build seat runs +// merge-check as root): root's data is never kept (ADR 0325 ยง12), and a test's own folders stand for an operator's. +func userDir(t *testing.T) string { + t.Helper() + dir := t.TempDir() + userDirs = append(userDirs, dir) + t.Cleanup(func() { userDirs = nil }) + return dir +} + +// unprivileged is the owner toUser gives a test's folders: nobody. +const unprivileged = 65534 + +// toUser gives everything under the running test's own folders to an unprivileged owner when the test runs as +// root, so they are judged as an operator's data is; as anyone else it changes nothing. +func toUser(t *testing.T) { + t.Helper() + if os.Geteuid() != 0 { + return + } + for _, dir := range userDirs { + err := filepath.WalkDir(dir, func(p string, _ os.DirEntry, err error) error { + if err != nil { + return err + } + return os.Lchown(p, unprivileged, unprivileged) + }) + if err != nil { + t.Fatalf("giving the test's folders to an unprivileged owner: %v", err) + } + } +} diff --git a/internal/apply/oldshape_test.go b/internal/apply/oldshape_test.go index a9d23da..a4eb521 100644 --- a/internal/apply/oldshape_test.go +++ b/internal/apply/oldshape_test.go @@ -22,7 +22,7 @@ import ( // made, no container held, nothing judged. These are origin/main's outcomes, word for word. func TestAnOlderControllersAccessesAreAppliedExactlyAsBefore(t *testing.T) { keeping(t, nil, nil) - dir := t.TempDir() + dir := userDir(t) unwritable := filepath.Join(dir, "library") // its group may not write it empty := filepath.Join(dir, "spool") absent := filepath.Join(dir, "media", "movies") @@ -66,6 +66,7 @@ func TestAnOlderControllersAccessesAreAppliedExactlyAsBefore(t *testing.T) { } return "", nil } + toUser(t) report, _, err := Apply(context.Background(), archHost(t), declare(t, strings.Join(resources, ",")), store.State{}, store.OriginDeclared, run, nil, nil) @@ -126,6 +127,7 @@ func TestALeftoverSaysWhyItsWalkCouldNotRun(t *testing.T) { t.Fatal(err) } uuid, fsid, source = "", "0000000000000000", "" // nothing names it now + toUser(t) ReconcileKept(context.Background()) left := Leftovers() if len(left) != 1 || strings.Contains(left[0].Why, "disk") || !strings.Contains(left[0].Why, "could not be named") {