package accounts import ( "context" "errors" "fmt" "io/fs" "os/exec" "strings" "testing" "github.com/novox/mesh-host/internal/declaration" ) // An account declared never to become root without a person (novox/hq ADR 0266, "how it is checked"): each // way to root is found and said, none is healthy whether or not anybody is logged in, an unanswered question // is unknown, and the judge only reads. // agentMachine is a fake machine for the escalation question: the account's uid, its groups by name and // number, what sudo lists, and the secrets' owners and modes. type agentMachine struct { uid string groups string gids string sudo string sudoErr error files map[string]FileMode failsID bool asked []string // texts are files' contents (doas's, polkit's); dirs a directory's files by name; acls a file's ACL; // setuid what find prints, and packaged the paths a package owns. texts map[string]string dirs map[string][]string acls map[string][]ACLEntry setuid string findErr error packaged map[string]bool } func (m *agentMachine) readFile(path string) ([]byte, error) { if t, ok := m.texts[path]; ok { return []byte(t), nil } return nil, fs.ErrNotExist } func (m *agentMachine) readDir(path string) ([]fs.DirEntry, error) { names, ok := m.dirs[path] if !ok { return nil, fs.ErrNotExist } var out []fs.DirEntry for _, n := range names { out = append(out, fakeEntry(n)) } return out, nil } type fakeEntry string func (f fakeEntry) Name() string { return string(f) } func (f fakeEntry) IsDir() bool { return false } func (f fakeEntry) Type() fs.FileMode { return 0 } func (f fakeEntry) Info() (fs.FileInfo, error) { return nil, fs.ErrNotExist } func (m *agentMachine) acl(path string) ([]ACLEntry, error) { if _, ok := m.files[path]; !ok { return nil, fs.ErrNotExist } return m.acls[path], nil } func (m *agentMachine) run(_ context.Context, name string, args ...string) (string, error) { line := name + " " + strings.Join(args, " ") m.asked = append(m.asked, line) switch { case m.failsID && name == "id": return "", errors.New("id exited 1: no such user") case line == "id -u agent": return m.uid + "\n", nil case line == "id -nG agent": return m.groups + "\n", nil case line == "id -G agent": return m.gids + "\n", nil case line == "sudo -l -U agent": return m.sudo, m.sudoErr case name == "find": return m.setuid, m.findErr case name == "pacman" && len(args) == 2 && args[0] == "-Qqo": if m.packaged[args[1]] { return "somepackage\n", nil } return "", errors.New("pacman exited 1: error: No package owns " + args[1]) } return "", errors.New("not a command the judge may run: " + line) } func (m *agentMachine) stat(path string) (FileMode, error) { if f, ok := m.files[path]; ok { return f, nil } return FileMode{}, fs.ErrNotExist } const notAllowed = "User agent is not allowed to run sudo on box.\n" var agent = Account{Module: "claude-code", ID: "claude-code.agent", Name: "agent", Root: true, Secrets: []string{"/var/lib/mesh/node-tools/broker"}} func clean() *agentMachine { return &agentMachine{uid: "1600", groups: "agent", gids: "1600", sudo: notAllowed, files: map[string]FileMode{"/var/lib/mesh/node-tools/broker": {UID: 1500, GID: 1500, Perm: 0o600}}} } func lookAgent(t *testing.T, m *agentMachine) Verdict { t.Helper() j := New(Exec{Run: m.run, Stat: m.stat, ReadFile: m.readFile, ReadDir: m.readDir, ACL: m.acl}) j.Set([]Account{agent}) st, _ := j.Look(t.Context()) if len(st.Accounts) != 1 { t.Fatalf("the statement: %+v", st) } for _, a := range m.asked { if !strings.HasPrefix(a, "id ") && a != "sudo -l -U agent" && !strings.HasPrefix(a, "find / -xdev") && !strings.HasPrefix(a, "pacman -Qqo ") { t.Errorf("the judge asked something that is not a read: %q", a) } } return st.Accounts[0] } func TestAnAgentAccountWithNoWayToRootIsHealthyWithNobodyLoggedIn(t *testing.T) { if v := lookAgent(t, clean()); v.State != Healthy || v.Reason != "" || !v.Root { t.Fatalf("no way to root: %+v", v) } } func TestEachWayToRootIsSaid(t *testing.T) { for _, c := range []struct { name string set func(*agentMachine) said string }{ {"uid 0", func(m *agentMachine) { m.uid = "0" }, "its uid is 0"}, {"docker", func(m *agentMachine) { m.groups = "agent docker" }, "in the group docker, which grants root"}, {"wheel", func(m *agentMachine) { m.groups = "agent wheel" }, "in the group wheel, which grants root"}, {"sudo", func(m *agentMachine) { m.sudo = "Matching Defaults entries for agent on box:\n env_reset\n\nUser agent may run the following commands on box:\n (ALL) NOPASSWD: ALL\n" }, "sudo grants it: (ALL) NOPASSWD: ALL"}, {"secret by others", func(m *agentMachine) { m.files["/var/lib/mesh/node-tools/broker"] = FileMode{UID: 1500, GID: 1500, Perm: 0o644} }, "it can read the secret /var/lib/mesh/node-tools/broker"}, {"secret by group", func(m *agentMachine) { m.gids = "1600 1500" m.files["/var/lib/mesh/node-tools/broker"] = FileMode{UID: 1500, GID: 1500, Perm: 0o640} }, "it can read the secret /var/lib/mesh/node-tools/broker"}, } { t.Run(c.name, func(t *testing.T) { m := clean() c.set(m) v := lookAgent(t, m) if v.State != Unhealthy || !strings.HasPrefix(v.Reason, ReasonRoot+": ") || !strings.Contains(v.Reason, c.said) { t.Fatalf("want %q said: %+v", c.said, v) } }) } } func TestEveryWayIsSaidAtOnce(t *testing.T) { m := clean() m.groups = "agent docker" m.sudo = "User agent may run the following commands on box:\n (ALL) ALL\n" v := lookAgent(t, m) if !strings.Contains(v.Reason, "docker") || !strings.Contains(v.Reason, "(ALL) ALL") { t.Fatalf("both ways: %+v", v) } } func TestAnUnansweredQuestionIsUnknownNeverHealthy(t *testing.T) { m := clean() m.failsID = true if v := lookAgent(t, m); v.State != Unknown { t.Fatalf("an unread database: %+v", v) } m = clean() m.sudo = "something sudo never says\n" if v := lookAgent(t, m); v.State != Unknown { t.Fatalf("an unread sudo: %+v", v) } } func TestASecretNotThereYetIsNoWay(t *testing.T) { m := clean() delete(m.files, "/var/lib/mesh/node-tools/broker") if v := lookAgent(t, m); v.State != Healthy { t.Fatalf("no secret placed: %+v", v) } } func TestSudoRules(t *testing.T) { none := []struct { out string err error }{ {notAllowed, nil}, {notAllowed, errors.New("sudo exited 1: ")}, {"", fmt.Errorf("sudo: %w", exec.ErrNotFound)}, } for _, c := range none { if rules, err := SudoRules(c.out, c.err); err != nil || len(rules) != 0 { t.Errorf("%q, %v: rules %v, err %v", c.out, c.err, rules, err) } } rules, err := SudoRules("Matching Defaults entries for agent on box:\n env_reset\n\nUser agent may run the following commands on box:\n (ALL) /usr/bin/systemctl\n (root) NOPASSWD: /usr/bin/true\n", nil) if err != nil || len(rules) != 2 || rules[0] != "(ALL) /usr/bin/systemctl" { t.Fatalf("two rules: %v, %v", rules, err) } if _, err := SudoRules("", errors.New("sudo exited 1: sudo: unable to resolve host")); err == nil { t.Error("a failing sudo that said neither was read as no rules") } } func TestReadable(t *testing.T) { m := FileMode{UID: 1500, GID: 1500, Perm: 0o600} if Readable(m, 1600, map[int]bool{1600: true}) || !Readable(m, 1500, nil) || !Readable(m, 0, nil) { t.Fatal("owner bits") } } func TestOfJudgesARootNeverAccountWithNoGroupsAndItsSecrets(t *testing.T) { d, err := declaration.ParseTrusted([]byte(`{"declaration":1,"resources":[ {"id":"claude-code.agent","type":"user","name":"agent","root":"never"}, {"id":"zsh.login","type":"user","name":"operator","shell":"/bin/zsh"}, {"id":"node-tools.need-broker","type":"file","path":"/var/lib/mesh/broker","sealed":"x","owner":"operator"}, {"id":"claude-code.own","type":"file","path":"/home/agent/own","sealed":"x","owner":"agent"}, {"id":"claude-code.plain","type":"file","path":"/etc/plain","content":"x"} ]}`)) if err != nil { t.Fatal(err) } got := Of(d, nil) if len(got) != 1 || got[0].ID != "claude-code.agent" || !got[0].Root || len(got[0].Secrets) != 1 || got[0].Secrets[0] != "/var/lib/mesh/broker" { t.Fatalf("judged: %+v", got) } }