Files
mesh-catalog/modules/mesh-files/cmd/mesh-files/files_test.go
T
jschoubben 5d98bcc0db
mesh/delivery-group group feat/900-node-files checking: composed check out of date: novox/mesh-controller@210f7cf5e64b was judged against trunk 611e2275cf7c, t…
mesh/merge-gate out of date: judged against trunk 3db85a0dde83, the trunk is now 8b53a3663244; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 3db85a0dde83, the trunk is now 8b53a3663244; judged again on a recheck or when it is merged
mesh/delivery superseded: a newer head of the same pull request
mesh-files: the holder of node-files, reads and writes with trash, owned paths and an act log, so file work on a node needs no shell (issue 900, ADR 0394)
2026-10-16 14:50:03 +02:00

820 lines
29 KiB
Go

package main
import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"syscall"
"testing"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// fakeRoot is the sudo -n runner of a test: it records each call, and answers a cat from a table. No test runs
// sudo (ADR 0391).
type fakeRoot struct {
mu sync.Mutex
calls []string
cat map[string]string
out map[string]string // the answer to a whole call, "bin arg arg"
err error
}
func (r *fakeRoot) Run(bin string, args ...string) ([]byte, error) {
r.mu.Lock()
defer r.mu.Unlock()
r.calls = append(r.calls, bin+" "+strings.Join(args, " "))
if o, ok := r.out[bin+" "+strings.Join(args, " ")]; ok {
return []byte(o), nil
}
if bin == "/usr/bin/cat" {
if s, ok := r.cat[args[len(args)-1]]; ok {
return []byte(s), nil
}
}
if r.err != nil {
return nil, r.err
}
// What root would do in a folder the account may not write, done in the test's tree.
switch bin {
case "/usr/bin/cp":
dest := args[len(args)-1]
os.Chmod(filepath.Dir(dest), 0o700)
b, err := os.ReadFile(args[len(args)-2])
if err != nil {
return nil, err
}
return nil, os.WriteFile(dest, b, 0o600)
case "/usr/bin/mkdir":
dest := args[len(args)-1]
for p := filepath.Dir(dest); p != "/"; p = filepath.Dir(p) {
if _, err := os.Stat(p); err == nil {
os.Chmod(p, 0o700)
break
}
}
return nil, os.MkdirAll(dest, 0o755)
}
return nil, nil
}
// reach makes the fake answer, for a path, what root's readlink -e and stat would: a regular file that leads to itself.
func (r *fakeRoot) reach(p string) {
r.out["/usr/bin/readlink -e -- "+p] = p + "\n"
r.out["/usr/bin/stat -c "+statFormat+" -- "+p] = "regular file|1000|1000|600|-rw-------|1\n"
}
func (r *fakeRoot) called(prefix string) bool {
for _, c := range r.calls {
if strings.HasPrefix(c, prefix) {
return true
}
}
return false
}
type rig struct {
*Files
root *fakeRoot
dir string
owners map[string]Ident
told []string
}
// testFiles is a holder over a temporary tree: a fake root runner, a clock the test moves, owners the test
// names (a test cannot make a file of another account), and a chown that refuses what is not its own.
func testFiles(t *testing.T) *rig {
t.Helper()
r, dir := testReader(t)
g := &rig{root: &fakeRoot{cat: map[string]string{}, out: map[string]string{}}, dir: dir, owners: map[string]Ident{}}
r.Root = g.root
r.Now = func() time.Time { return time.Date(2026, 10, 16, 12, 0, 0, 0, time.UTC) }
seq := 0
g.Files = &Files{Reader: r, State: filepath.Join(dir, "state"), Owned: map[string]string{}, Node: "shanks",
OwnerOf: func(p string) (Ident, error) {
if id, ok := g.owners[p]; ok {
return id, nil
}
return ownerOnDisk(p)
},
Lchown: func(p string, uid, gid int) error { return &fs.PathError{Op: "lchown", Path: p, Err: syscall.EPERM} },
NewID: func() string { seq++; return fmt.Sprintf("t%02d", seq) },
Tell: func(headline string, lines []string, subject string) error {
g.told = append(g.told, subject+" | "+headline+" | "+strings.Join(lines, " | "))
return nil
}}
if err := os.MkdirAll(g.State, 0o700); err != nil {
t.Fatal(err)
}
return g
}
var by = Act{Caller: "person.jochen", Reason: "a test"}
func (g *rig) path(name string) string { return filepath.Join(g.dir, name) }
func mustRead(t *testing.T, p string) string {
t.Helper()
b, err := os.ReadFile(p)
if err != nil {
t.Fatal(err)
}
return string(b)
}
func modeOf(t *testing.T, p string) fs.FileMode {
t.Helper()
i, err := os.Lstat(p)
if err != nil {
t.Fatal(err)
}
return i.Mode().Perm()
}
func actLines(t *testing.T, g *rig) []map[string]any {
t.Helper()
raw, err := os.ReadFile(filepath.Join(g.State, "acts.log"))
if err != nil {
return nil
}
var out []map[string]any
for _, l := range strings.Split(strings.TrimSpace(string(raw)), "\n") {
var m map[string]any
if err := json.Unmarshal([]byte(l), &m); err != nil {
t.Fatalf("a line of the act log is not JSON: %q", l)
}
out = append(out, m)
}
return out
}
// ADR 0394 §1: every write names why.
func TestAWriteWithoutAReasonIsRefused(t *testing.T) {
g := testFiles(t)
if _, err := g.Write(Act{Caller: "x"}, g.path("a"), "x", ""); err == nil || !strings.Contains(err.Error(), "reason") {
t.Errorf("no reason: %v", err)
}
if _, err := os.Stat(g.path("a")); err == nil {
t.Error("the file was written")
}
}
// ADR 0394 §3: a new file takes its folder's owner and group, and the mode asked.
func TestANewFileTakesItsFoldersOwner(t *testing.T) {
g := testFiles(t)
g.owners[g.dir] = Ident{4242, 4343}
if _, err := g.Write(by, g.path("new"), "hello\n", "0600"); err != nil {
t.Fatal(err)
}
if mustRead(t, g.path("new")) != "hello\n" || modeOf(t, g.path("new")) != 0o600 {
t.Errorf("content or mode wrong: %o", modeOf(t, g.path("new")))
}
if !g.root.called("/usr/bin/chown 4242:4343 -- " + g.path("new")) {
t.Errorf("the owner of the folder was not given: %v", g.root.calls)
}
}
// ADR 0394 §3, §4: an overwrite keeps the old file's owner and mode, and the old file goes to the trash.
func TestAnOverwriteKeepsOwnerAndModeAndTrashesTheOld(t *testing.T) {
g := testFiles(t)
write(t, g.path("f"), "old\n")
os.Chmod(g.path("f"), 0o640)
g.owners[g.path("f")] = Ident{4242, 4343}
if _, err := g.Write(by, g.path("f"), "new\n", ""); err != nil {
t.Fatal(err)
}
if mustRead(t, g.path("f")) != "new\n" || modeOf(t, g.path("f")) != 0o640 {
t.Errorf("content %q mode %o", mustRead(t, g.path("f")), modeOf(t, g.path("f")))
}
if !g.root.called("/usr/bin/chown 4242:4343 -- " + g.path("f")) {
t.Errorf("the old owner was not given back: %v", g.root.calls)
}
tr, err := g.Trash()
if err != nil || len(tr.Items) != 1 || tr.Items[0].Path != g.path("f") {
t.Fatalf("trash: %+v %v", tr, err)
}
if mustRead(t, filepath.Join(g.State, "trash", tr.Items[0].ID, "item")) != "old\n" {
t.Error("the old content is not in the trash")
}
}
// ADR 0394 §3: a copy keeps mode and owner, a directory with what it holds.
func TestACopyKeepsModeAndOwner(t *testing.T) {
g := testFiles(t)
write(t, g.path("src/a"), "A\n")
os.Chmod(g.path("src/a"), 0o750)
g.owners[g.path("src/a")] = Ident{4242, 4343}
if _, err := g.Copy(by, g.path("src"), g.path("dst")); err != nil {
t.Fatal(err)
}
if mustRead(t, g.path("dst/a")) != "A\n" || modeOf(t, g.path("dst/a")) != 0o750 {
t.Errorf("copy: %q %o", mustRead(t, g.path("dst/a")), modeOf(t, g.path("dst/a")))
}
if !g.root.called("/usr/bin/chown 4242:4343 -- " + g.path("dst/a")) {
t.Errorf("owner not kept: %v", g.root.calls)
}
if _, err := os.Stat(g.path("src/a")); err != nil {
t.Error("the source is gone")
}
}
// ADR 0394 §3: a move keeps mode and owner, and the source is gone.
func TestAMoveKeepsModeAndOwner(t *testing.T) {
g := testFiles(t)
write(t, g.path("a"), "A\n")
os.Chmod(g.path("a"), 0o700)
if _, err := g.Move(by, g.path("a"), g.path("b")); err != nil {
t.Fatal(err)
}
if mustRead(t, g.path("b")) != "A\n" || modeOf(t, g.path("b")) != 0o700 {
t.Error("the move lost content or mode")
}
if _, err := os.Stat(g.path("a")); err == nil {
t.Error("the source is still there")
}
}
// ADR 0394 §4: a delete goes to the trash, restore puts it back, and refuses when the path is taken now.
func TestADeleteGoesToTheTrashAndRestoreBringsItBack(t *testing.T) {
g := testFiles(t)
write(t, g.path("d/f"), "keep\n")
if _, err := g.Delete(by, g.path("d"), false); err == nil || !strings.Contains(err.Error(), "recursive") {
t.Errorf("a directory without recursive: %v", err)
}
if _, err := g.Delete(by, g.path("d"), true); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(g.path("d")); err == nil {
t.Fatal("not deleted")
}
tr, _ := g.Trash()
if len(tr.Items) != 1 {
t.Fatalf("trash %+v", tr)
}
id := tr.Items[0].ID
write(t, g.path("d/other"), "x")
if _, err := g.Restore(by, id); err == nil || !strings.Contains(err.Error(), "there now") {
t.Errorf("restore over a taken path: %v", err)
}
os.RemoveAll(g.path("d"))
if _, err := g.Restore(by, id); err != nil {
t.Fatal(err)
}
if mustRead(t, g.path("d/f")) != "keep\n" {
t.Error("not restored")
}
if tr, _ := g.Trash(); len(tr.Items) != 0 {
t.Errorf("the restored item is still in the trash: %+v", tr)
}
}
// ADR 0394 §4: an entry older than 7 days is emptied on a trash list and on a write call.
func TestATrashEntryOlderThanSevenDaysIsEmptied(t *testing.T) {
g := testFiles(t)
write(t, g.path("f"), "x")
if _, err := g.Delete(by, g.path("f"), false); err != nil {
t.Fatal(err)
}
now := g.Now()
g.Now = func() time.Time { return now.Add(6*24*time.Hour + 23*time.Hour) }
if tr, _ := g.Trash(); len(tr.Items) != 1 {
t.Fatalf("emptied too early: %+v", tr)
}
g.Now = func() time.Time { return now.Add(7*24*time.Hour + time.Minute) }
if tr, _ := g.Trash(); len(tr.Items) != 0 {
t.Fatalf("not emptied: %+v", tr)
}
entries, _ := os.ReadDir(filepath.Join(g.State, "trash"))
if len(entries) != 0 {
t.Errorf("the entry is still on disk: %v", entries)
}
// and on a write call
write(t, g.path("g"), "y")
g.Delete(by, g.path("g"), false)
g.Now = func() time.Time { return now.Add(30 * 24 * time.Hour) }
if _, err := g.Mkdir(by, g.path("m"), false); err != nil {
t.Fatal(err)
}
entries, _ = os.ReadDir(filepath.Join(g.State, "trash"))
if len(entries) != 0 {
t.Errorf("a write call left an old entry: %v", entries)
}
}
// ADR 0394 §5: an owned path is refused naming its module and tool; with an override it is changed, logged and
// told to the operator.
func TestAnOwnedPathIsRefusedNamingItsOwnerAndPassesWithAnOverride(t *testing.T) {
g := testFiles(t)
g.Owned[g.path("etc")] = "mesh-hosts (node-hostname)"
write(t, g.path("etc/hosts"), "old\n")
for name, run := range map[string]func() error{
"write": func() error { _, err := g.Write(by, g.path("etc/hosts"), "x", ""); return err },
"delete": func() error { _, err := g.Delete(by, g.path("etc/hosts"), false); return err },
"move": func() error { _, err := g.Move(by, g.path("etc/hosts"), g.path("elsewhere")); return err },
"into": func() error { _, err := g.Move(by, g.path("elsewhere"), g.path("etc/new")); return err },
"parent": func() error { _, err := g.Delete(by, g.dir, true); return err },
} {
err := run()
if err == nil || !strings.Contains(err.Error(), "mesh-hosts (node-hostname)") {
t.Errorf("%s: %v", name, err)
}
}
if mustRead(t, g.path("etc/hosts")) != "old\n" {
t.Fatal("an owned file was changed")
}
if len(actLines(t, g)) != 0 || len(g.told) != 0 {
t.Error("a refused act was logged or told")
}
ov := by
ov.Override = "the module is down and the node cannot resolve"
if _, err := g.Write(ov, g.path("etc/hosts"), "new\n", ""); err != nil {
t.Fatal(err)
}
if mustRead(t, g.path("etc/hosts")) != "new\n" {
t.Error("the override did not write")
}
lines := actLines(t, g)
if len(lines) != 1 || lines[0]["override"] != ov.Override {
t.Errorf("log: %v", lines)
}
if len(g.told) != 1 || !strings.HasPrefix(g.told[0], "mesh-files on shanks | ") ||
!strings.Contains(g.told[0], ov.Override) || !strings.Contains(g.told[0], "mesh-hosts (node-hostname)") ||
!strings.Contains(g.told[0], "person.jochen") {
t.Errorf("told: %v", g.told)
}
if strings.Contains(g.told[0], "/") && strings.Contains(g.told[0], g.dir) {
t.Errorf("the notice carries a path, which the operator channel refuses: %v", g.told)
}
if l := actLines(t, g); !strings.Contains(fmt.Sprint(l[0]["owned"]), g.path("etc")) {
t.Errorf("the path is in the act log: %v", l[0])
}
// an override on a path nobody owns says nothing
g.told = nil
if _, err := g.Write(ov, g.path("free"), "x", ""); err != nil || len(g.told) != 0 {
t.Errorf("an override that was not needed: %v %v", err, g.told)
}
}
// ADR 0394 §7: a credential file is refused before it is read, but may be copied and moved.
func TestACredentialFileIsRefusedToReadButMayBeCopied(t *testing.T) {
g := testFiles(t)
write(t, g.path("home/.ssh/id_ed25519"), "key\n")
if _, err := g.Read(g.path("home/.ssh/id_ed25519"), ReadAsk{}); err == nil || !strings.Contains(err.Error(), "refused by name") {
t.Errorf("read: %v", err)
}
if g.root.called("/usr/bin/cat") {
t.Error("it went for the content with root")
}
if _, err := g.Copy(by, g.path("home/.ssh/id_ed25519"), g.path("backup_key")); err != nil {
t.Errorf("copy of a secret place: %v", err)
}
if _, err := g.Move(by, g.path("backup_key"), g.path("backup_key2")); err != nil {
t.Errorf("move of a secret place: %v", err)
}
}
// ADR 0394 §1: a target on another node is refused until step 2.
func TestATargetOnAnotherNodeIsRefused(t *testing.T) {
g := testFiles(t)
write(t, g.path("a"), "x")
for _, to := range []string{"ace:/tmp/a", "novox:/etc/hosts"} {
for _, run := range []func() error{
func() error { _, err := g.Copy(by, g.path("a"), to); return err },
func() error { _, err := g.Move(by, g.path("a"), to); return err },
func() error { _, err := g.Copy(by, to, g.path("b")); return err },
} {
if err := run(); err == nil || !strings.Contains(err.Error(), "issue 903") {
t.Errorf("%s: %v", to, err)
}
}
}
if _, err := os.Stat(g.path("a")); err != nil {
t.Error("the source was touched")
}
}
// ADR 0394 §2: a permission error repeats the act through sudo -n with a fixed binary and argument list.
func TestAPermissionErrorFallsBackToRoot(t *testing.T) {
if os.Geteuid() == 0 {
t.Skip("run as root, nothing is refused")
}
g := testFiles(t)
if err := os.MkdirAll(g.path("locked"), 0o500); err != nil {
t.Fatal(err)
}
defer os.Chmod(g.path("locked"), 0o700)
if _, err := g.Write(by, g.path("locked/f"), "x\n", ""); err != nil {
t.Fatal(err)
}
if !g.root.called("/usr/bin/cp -- ") || !g.root.called("/usr/bin/cp -- "+filepath.Join(g.State, "tmp")) {
t.Errorf("write did not go through root: %v", g.root.calls)
}
g.root.calls = nil
os.Chmod(g.path("locked"), 0o500)
if _, err := g.Mkdir(by, g.path("locked/sub"), true); err != nil {
t.Fatal(err)
}
if !g.root.called("/usr/bin/mkdir -p -- " + g.path("locked/sub")) {
t.Errorf("mkdir: %v", g.root.calls)
}
// a root that fails is said, not swallowed
os.Chmod(g.path("locked"), 0o500)
g.root.err = errors.New("sudo: a password is required")
if _, err := g.Mkdir(by, g.path("locked/sub2"), false); err == nil || !strings.Contains(err.Error(), "password") {
t.Errorf("a failing root: %v", err)
}
// a read the account may not do is done by cat
write(t, g.path("secretish"), "line one\nline two\n")
os.Chmod(g.path("secretish"), 0)
defer os.Chmod(g.path("secretish"), 0o600)
g.root.cat[g.path("secretish")] = "line one\nline two\n"
g.root.reach(g.path("secretish"))
a, err := g.Read(g.path("secretish"), ReadAsk{})
if err != nil || a.Text != "line one\nline two\n" {
t.Errorf("read through root: %+v %v", a, err)
}
if !g.root.called("/usr/bin/cat -- " + g.path("secretish")) {
t.Errorf("calls: %v", g.root.calls)
}
}
// The root runner is a fixed list of binaries, never a shell.
func TestTheRootRunnerRunsOnlyAFixedList(t *testing.T) {
for _, bin := range []string{"/bin/sh", "/usr/bin/bash", "/usr/bin/env", "cat", "/usr/bin/sudo", "/usr/bin/sh", "/usr/bin/grep"} {
if _, err := (sudoRoot{}).Run(bin, "-c", "id"); err == nil || !strings.Contains(err.Error(), "not one of") {
t.Errorf("%s: %v", bin, err)
}
}
}
// ADR 0394 §6: every write act appends one JSON line, with the caller the runtime proved.
func TestEveryWriteActAppendsOneLogLine(t *testing.T) {
g := testFiles(t)
write(t, g.path("a"), "x")
acts := []struct {
verb string
run func() error
}{
{"write", func() error { _, err := g.Write(by, g.path("w"), "x", ""); return err }},
{"mkdir", func() error { _, err := g.Mkdir(by, g.path("m"), false); return err }},
{"chmod", func() error { _, err := g.Chmod(by, g.path("a"), "0600", false); return err }},
{"copy", func() error { _, err := g.Copy(by, g.path("a"), g.path("c")); return err }},
{"move", func() error { _, err := g.Move(by, g.path("c"), g.path("d")); return err }},
{"delete", func() error { _, err := g.Delete(by, g.path("d"), false); return err }},
{"chown", func() error { _, err := g.Chown(by, g.path("a"), "ops", "", false); return err }},
}
for i, a := range acts {
if err := a.run(); err != nil {
t.Fatalf("%s: %v", a.verb, err)
}
lines := actLines(t, g)
if len(lines) != i+1 {
t.Fatalf("after %s: %d lines", a.verb, len(lines))
}
l := lines[i]
if l["verb"] != a.verb || l["caller"] != "person.jochen" || l["reason"] != "a test" || l["time"] == nil || l["paths"] == nil {
t.Errorf("%s: %v", a.verb, l)
}
}
// a refused act adds none
g.Mkdir(by, g.path("a/b/c"), false)
if len(actLines(t, g)) != len(acts) {
t.Error("a failed act was logged")
}
}
// ADR 0159 / issue 365: the tool takes the caller from the runtime, never from the arguments.
func TestTheToolRecordsTheCallerTheRuntimeNamed(t *testing.T) {
g := testFiles(t)
var tool stdio.Tool
for _, x := range tools(g.Files) {
if x.Name == Seat+".mkdir" {
tool = x
}
}
args := map[string]any{"path": g.path("m"), "reason": "why", "caller": "person.mallory"}
if _, err := tool.Run(args); err != nil {
t.Fatal(err)
}
if l := actLines(t, g); len(l) != 1 || l[0]["caller"] != stdio.Unproven {
t.Errorf("log: %v", l)
}
}
func TestMkdirChmodAndChown(t *testing.T) {
g := testFiles(t)
if _, err := g.Mkdir(by, g.path("a/b"), false); err == nil {
t.Error("a missing parent was made without parents")
}
g.owners[g.dir] = Ident{4242, 4343}
if _, err := g.Mkdir(by, g.path("a/b"), true); err != nil {
t.Fatal(err)
}
if !g.root.called("/usr/bin/chown 4242:4343 -- " + g.path("a")) {
t.Errorf("a made parent takes the owner of its folder: %v", g.root.calls)
}
write(t, g.path("a/b/f"), "x")
os.Chmod(g.path("a/b/f"), 0o600)
if _, err := g.Chmod(by, g.path("a"), "u+x,g+r", true); err != nil {
t.Fatal(err)
}
if modeOf(t, g.path("a/b/f")) != 0o740 {
t.Errorf("recursive symbolic chmod: %o", modeOf(t, g.path("a/b/f")))
}
if _, err := g.Chmod(by, g.path("a/b/f"), "0644", false); err != nil || modeOf(t, g.path("a/b/f")) != 0o644 {
t.Errorf("octal: %v %o", err, modeOf(t, g.path("a/b/f")))
}
if _, err := g.Chmod(by, g.path("a/b/f"), "banana", false); err == nil {
t.Error("a bad mode")
}
g.root.calls = nil
if _, err := g.Chown(by, g.path("a/b/f"), "ops", "wheel", false); err != nil {
t.Fatal(err)
}
if !g.root.called("/usr/bin/chown 1000:998 -- " + g.path("a/b/f")) {
t.Errorf("chown by name: %v", g.root.calls)
}
if _, err := g.Chown(by, g.path("a/b/f"), "nobody-here", "", false); err == nil {
t.Error("an unknown account")
}
}
// ADR 0394 §5: without the word the module owns nothing and starts; a bad word is refused.
func TestWithoutOwnedPathsItOwnsNothing(t *testing.T) {
if o, err := ownedFrom(""); err != nil || len(o) != 0 {
t.Errorf("none: %v %v", o, err)
}
if o, err := ownedFrom(`{"/etc/hosts":"mesh-hosts (node-hostname)"}`); err != nil || o["/etc/hosts"] == "" {
t.Errorf("a word: %v %v", o, err)
}
if _, err := ownedFrom(`["/etc/hosts"]`); err == nil || !strings.Contains(err.Error(), OwnedWord) {
t.Errorf("a list: %v", err)
}
}
func TestItServesTheWriteVerbsToo(t *testing.T) {
g := testFiles(t)
got := map[string]bool{}
for _, tool := range tools(g.Files) {
got[tool.Name] = true
}
for _, v := range []string{"stat", "readlink", "list", "read", "search", "account", "write", "copy", "move",
"delete", "mkdir", "chmod", "chown", "trash", "restore"} {
if !got[Seat+"."+v] {
t.Errorf("no %s", v)
}
}
}
// ADR 0394 §3, §4: a copy or a move onto an existing target overwrites it: the old one goes to the trash, and
// the new one keeps the old one's owner and mode.
func TestACopyOrMoveOntoAnExistingTargetOverwritesIt(t *testing.T) {
g := testFiles(t)
write(t, g.path("new"), "new\n")
write(t, g.path("old"), "old\n")
os.Chmod(g.path("old"), 0o640)
g.owners[g.path("old")] = Ident{4242, 4343}
a, err := g.Copy(by, g.path("new"), g.path("old"))
if err != nil {
t.Fatal(err)
}
if mustRead(t, g.path("old")) != "new\n" || modeOf(t, g.path("old")) != 0o640 {
t.Errorf("copy: %q %o", mustRead(t, g.path("old")), modeOf(t, g.path("old")))
}
if !g.root.called("/usr/bin/chown 4242:4343 -- " + g.path("old")) {
t.Errorf("the old owner was not kept: %v", g.root.calls)
}
if a.Trashed == "" || mustRead(t, filepath.Join(g.State, "trash", a.Trashed, "item")) != "old\n" {
t.Errorf("the old target is not in the trash: %+v", a)
}
write(t, g.path("m"), "moved\n")
os.Chmod(g.path("old"), 0o600)
b, err := g.Move(by, g.path("m"), g.path("old"))
if err != nil || mustRead(t, g.path("old")) != "moved\n" || b.Trashed == "" {
t.Fatalf("move: %v %+v", err, b)
}
if _, err := os.Stat(g.path("m")); err == nil {
t.Error("the source is still there")
}
if modeOf(t, g.path("old")) != 0o600 {
t.Errorf("the overwritten file's mode is kept: %o", modeOf(t, g.path("old")))
}
// a target inside the source, or a source inside the target, is not an overwrite
write(t, g.path("d/f"), "x")
if _, err := g.Copy(by, g.path("d"), g.path("d/in")); err == nil {
t.Error("a copy into itself")
}
if _, err := g.Move(by, g.path("d/f"), g.path("d")); err == nil {
t.Error("a move over its own folder")
}
if mustRead(t, g.path("d/f")) != "x" {
t.Error("the file was lost")
}
}
// ADR 0394 §7: only read and search refuse a place a secret is kept; the writes do not.
func TestTheWritesDoNotRefuseASecretPlace(t *testing.T) {
g := testFiles(t)
k := g.path("home/.ssh/id_ed25519")
write(t, k, "key\n")
for name, run := range map[string]func() error{
"write": func() error { _, err := g.Write(by, k, "new\n", ""); return err },
"chmod": func() error { _, err := g.Chmod(by, k, "0600", false); return err },
"mkdir": func() error { _, err := g.Mkdir(by, g.path("home/.ssh/sub"), false); return err },
"delete": func() error { _, err := g.Delete(by, k, false); return err },
} {
if err := run(); err != nil {
t.Errorf("%s: %v", name, err)
}
}
}
// ADR 0394 §2: where the account's own rights refuse a read, stat, readlink, list and search repeat it through
// root with a fixed binary; a secret place is refused by name first, and root is not asked.
func TestTheReadsFallBackToRoot(t *testing.T) {
if os.Geteuid() == 0 {
t.Skip("run as root, nothing is refused")
}
g := testFiles(t)
locked := g.path("locked")
write(t, locked+"/inner/f", "x\n")
os.Chmod(locked, 0)
defer os.Chmod(locked, 0o700)
in := locked + "/inner"
g.root.out["/usr/bin/stat -c %F|%u|%g|%a|%A|%s -- "+in] = "directory|1000|1000|700|drwx------|4096\n"
g.root.out["/usr/bin/stat -c %F|%u|%g|%a|%A|%s -- "+in+"/f"] = "regular file|1000|998|4640|-rwSr-----|2\n"
st, err := g.Stat([]string{in + "/f"}, false)
if err != nil {
t.Fatal(err)
}
cs := st.Paths[0].Components
last := cs[len(cs)-1]
if st.Paths[0].Stopped != "" || last.Type != "file" || last.Mode != "4640" || last.Owner != "ops" ||
last.Group != "wheel" || last.Size == nil || *last.Size != 2 || cs[len(cs)-2].Type != "directory" {
t.Errorf("stat through root: %+v", st.Paths[0])
}
// readlink
g.root.out["/usr/bin/readlink -- "+in+"/l"] = "f\n"
g.root.out["/usr/bin/readlink -e -- "+in+"/l"] = in + "/f\n"
rl, err := g.Readlink(in + "/l")
if err != nil || !rl.IsLink || rl.Link != "f" || rl.LeadsTo != in+"/f" || !rl.Exists {
t.Errorf("readlink through root: %+v %v", rl, err)
}
// list
g.root.out["/usr/bin/find -H "+in+" -mindepth 1 -maxdepth 1 -printf %y\t%U\t%G\t%m\t%s\t%l\t%f\x00"] =
"f\t1000\t998\t640\t2\t\tf\x00d\t1000\t1000\t755\t4096\t\tsub dir\x00l\t1000\t1000\t777\t1\tf\tl\x00"
ls, err := g.List(in)
if err != nil || ls.Count != 3 {
t.Fatalf("list through root: %+v %v", ls, err)
}
byName := map[string]Entry{}
for _, e := range ls.Entries {
byName[e.Name] = e
}
if e := byName["f"]; e.Type != "file" || e.Mode != "0640" || e.Group != "wheel" || e.Size == nil || *e.Size != 2 {
t.Errorf("entry f: %+v", e)
}
if byName["sub dir"].Type != "directory" || byName["l"].Type != "link" || byName["l"].Link != "f" {
t.Errorf("entries: %+v", ls.Entries)
}
// search: the files are listed by find, the secret ones skipped by name, the rest read through cat
g.root.out["/usr/bin/find -H "+locked+" -maxdepth 5 -xdev -type f -printf %p\x00"] =
in + "/f\x00" + locked + "/.ssh/id_rsa\x00"
g.root.cat[in+"/f"] = "one\nneedle here\n"
g.root.reach(in + "/f")
sr, err := g.Search(SearchAsk{Text: "needle", Path: locked})
if err != nil || len(sr.Matches) != 1 || sr.Matches[0].File != in+"/f" || sr.Matches[0].Line != 2 {
t.Fatalf("search through root: %+v %v", sr, err)
}
if len(sr.Skipped) != 1 || !strings.HasSuffix(sr.Skipped[0], "id_rsa") {
t.Errorf("a credential file was not skipped by name: %+v", sr.Skipped)
}
if g.root.called("/usr/bin/cat -- " + locked + "/.ssh/id_rsa") {
t.Error("root read a credential file")
}
// a secret place is refused by name, and root is never asked
g.root.calls = nil
if _, err := g.List(g.path("home/.ssh")); err == nil {
t.Error("a secret place was listed")
}
if _, err := g.Read(locked+"/.ssh/id_rsa", ReadAsk{}); err == nil || len(g.root.calls) != 0 {
t.Errorf("a credential file was read: %v %v", err, g.root.calls)
}
}
// Review 1, finding 3: before root reads a file, root says where it leads and what it is. A link into a refused
// place, and a file that is not a regular file, are refused before any cat.
func TestRootNeverCatsAThroughALinkIntoASecretPlaceOrANonFile(t *testing.T) {
if os.Geteuid() == 0 {
t.Skip("run as root, nothing is refused")
}
g := testFiles(t)
locked := g.path("locked")
write(t, locked+"/x", "x\n")
os.Chmod(locked, 0)
defer os.Chmod(locked, 0o700)
g.root.out["/usr/bin/readlink -e -- "+locked+"/link"] = "/etc/ssl/private/k\n"
g.root.cat["/etc/ssl/private/k"] = "KEY\n"
g.root.cat[locked+"/link"] = "KEY\n"
if _, err := g.Read(locked+"/link", ReadAsk{}); err == nil || !strings.Contains(err.Error(), "refused by name") {
t.Errorf("a link into a secret place: %v", err)
}
g.root.out["/usr/bin/readlink -e -- "+locked+"/fifo"] = locked + "/fifo\n"
g.root.out["/usr/bin/stat -c "+statFormat+" -- "+locked+"/fifo"] = "fifo|1000|1000|600|prw-------|0\n"
g.root.cat[locked+"/fifo"] = "never\n"
if _, err := g.Read(locked+"/fifo", ReadAsk{}); err == nil || !strings.Contains(err.Error(), "not a file") {
t.Errorf("a fifo: %v", err)
}
// a search meets the same two
g.root.out["/usr/bin/find -H "+locked+" -maxdepth 5 -xdev -type f -printf %p\x00"] = locked + "/link\x00" + locked + "/fifo\x00"
sr, err := g.Search(SearchAsk{Text: "KEY", Path: locked})
if err != nil || len(sr.Matches) != 0 {
t.Errorf("search: %+v %v", sr, err)
}
for _, c := range g.root.calls {
if strings.HasPrefix(c, "/usr/bin/cat") {
t.Errorf("root read: %s", c)
}
}
}
// Review 1, finding 4: a directory target that holds an owned path is replaced only with an override.
func TestOverwritingAFolderThatHoldsAnOwnedPathIsRefused(t *testing.T) {
g := testFiles(t)
g.Owned[g.path("dst/etc")] = "mesh-hosts (node-hostname)"
write(t, g.path("dst/etc/hosts"), "old\n")
write(t, g.path("src/a"), "A\n")
if _, err := g.Copy(by, g.path("src"), g.path("dst")); err == nil || !strings.Contains(err.Error(), "mesh-hosts") {
t.Errorf("copy: %v", err)
}
write(t, g.path("src2/a"), "A\n")
if _, err := g.Move(by, g.path("src2"), g.path("dst")); err == nil || !strings.Contains(err.Error(), "mesh-hosts") {
t.Errorf("move: %v", err)
}
if mustRead(t, g.path("dst/etc/hosts")) != "old\n" {
t.Error("the owned file was replaced")
}
}
// Review 1, finding 5: a copy keeps setuid and setgid: the owner is set before the mode, as a chown clears them.
func TestACopyKeepsSetuid(t *testing.T) {
g := testFiles(t)
g.Lchown = func(p string, uid, gid int) error { return os.Lchown(p, os.Getuid(), os.Getgid()) }
write(t, g.path("tool"), "x\n")
if err := os.Chmod(g.path("tool"), 0o755|fs.ModeSetuid|fs.ModeSetgid); err != nil {
t.Fatal(err)
}
g.owners[g.path("tool")] = Ident{4242, 4343}
if _, err := g.Copy(by, g.path("tool"), g.path("tool2")); err != nil {
t.Fatal(err)
}
info, _ := os.Lstat(g.path("tool2"))
if info.Mode()&fs.ModeSetuid == 0 || info.Mode()&fs.ModeSetgid == 0 {
t.Errorf("the copy lost setuid or setgid: %v", info.Mode())
}
}
// Review 1, finding 1: every seat verb the code asks is declared in invokes.
func TestEverySeatVerbTheCodeAsksIsDeclared(t *testing.T) {
raw, _ := os.ReadFile("../../module.json")
var m struct{ Invokes []string }
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
declared := map[string]bool{}
for _, i := range m.Invokes {
declared[i] = true
}
files, _ := filepath.Glob("*.go")
asked := map[string]bool{}
for _, f := range files {
if strings.HasSuffix(f, "_test.go") {
continue
}
src := mustRead(t, f)
for _, hit := range regexp.MustCompile(`"(seat:[a-z-]+\.[a-z_-]+)"`).FindAllStringSubmatch(src, -1) {
asked[hit[1]] = true
}
}
if len(asked) == 0 {
t.Fatal("found no seat verb asked")
}
for a := range asked {
if !declared[a] {
t.Errorf("%s is asked but not in invokes %v", a, m.Invokes)
}
}
for d := range declared {
if !asked[d] {
t.Errorf("%s is declared but never asked", d)
}
}
}