An account could replace ~/.claude with a link to /etc and have the node-engine chown, chmod or write through it on the next apply. Below a person's or an agent's home every component is now opened without following a link, and a link refuses the resource in words. The root judge also reads doas and polkit rules, the container runtimes' sockets with their ACLs, ACLs on the secrets, and setuid-root programs no package owns, in the C locale; Judged and NotJudged write down exactly what it covers (hq ADR 0266 review).
206 lines
7.2 KiB
Go
206 lines
7.2 KiB
Go
//go:build linux
|
|
|
|
package apply
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-host/internal/store"
|
|
)
|
|
|
|
// Defends novox/hq ADR 0266 (the review of 2026-10-08): below a home, the node-engine — root — follows no
|
|
// symbolic link an account can put there: not to chown or chmod, not to write, not to make a directory.
|
|
|
|
// aLinkedHome is a home the test names as one, with `.claude` replaced by a link to a directory outside it,
|
|
// as an account would to have root change /etc.
|
|
func aLinkedHome(t *testing.T) (home, outside string) {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
home = filepath.Join(root, "home", "agent")
|
|
outside = filepath.Join(root, "etc")
|
|
for _, d := range []string{home, outside} {
|
|
if err := os.MkdirAll(d, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := os.WriteFile(filepath.Join(outside, "shadow"), []byte("root's"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
was := homes
|
|
homes = func() []string { return []string{home} }
|
|
t.Cleanup(func() { homes = was })
|
|
return home, outside
|
|
}
|
|
|
|
func link(t *testing.T, target, at string) {
|
|
t.Helper()
|
|
if err := os.Symlink(target, at); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func applyOneResource(t *testing.T, resource string) error {
|
|
t.Helper()
|
|
d := declare(t, resource)
|
|
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared, noServices, nil, nil)
|
|
return err
|
|
}
|
|
|
|
func mustBeLinkRefusal(t *testing.T, err error) {
|
|
t.Helper()
|
|
var refused *LinkUnderHomeError
|
|
if !errors.As(err, &refused) {
|
|
t.Fatalf("refused as a link under a home, got %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "never follows a link") {
|
|
t.Fatalf("said in words: %v", err)
|
|
}
|
|
}
|
|
|
|
func modeOfPath(t *testing.T, p string) os.FileMode {
|
|
t.Helper()
|
|
info, err := os.Stat(p)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return info.Mode().Perm()
|
|
}
|
|
|
|
func TestADirectoryUnderAHomeThatIsALinkIsRefusedAndItsTargetUntouched(t *testing.T) {
|
|
home, outside := aLinkedHome(t)
|
|
link(t, outside, filepath.Join(home, ".claude"))
|
|
err := applyOneResource(t, `{"id":"claude-code.agent-home","type":"directory","path":"`+
|
|
filepath.Join(home, ".claude")+`","mode":"0700"}`)
|
|
mustBeLinkRefusal(t, err)
|
|
if m := modeOfPath(t, outside); m != 0o755 {
|
|
t.Fatalf("the link's target was chmodded to %o", m)
|
|
}
|
|
}
|
|
|
|
func TestAFileThroughALinkedParentUnderAHomeIsRefused(t *testing.T) {
|
|
home, outside := aLinkedHome(t)
|
|
link(t, outside, filepath.Join(home, ".claude"))
|
|
err := applyOneResource(t, `{"id":"m.f","type":"file","path":"`+filepath.Join(home, ".claude", "shadow")+
|
|
`","content":"the mesh's\n","mode":"0644"}`)
|
|
mustBeLinkRefusal(t, err)
|
|
if got, _ := os.ReadFile(filepath.Join(outside, "shadow")); string(got) != "root's" {
|
|
t.Fatalf("written through the link: %q", got)
|
|
}
|
|
if m := modeOfPath(t, filepath.Join(outside, "shadow")); m != 0o600 {
|
|
t.Fatalf("chmodded through the link: %o", m)
|
|
}
|
|
}
|
|
|
|
func TestAFileThatIsItselfALinkUnderAHomeIsRefused(t *testing.T) {
|
|
home, outside := aLinkedHome(t)
|
|
link(t, filepath.Join(outside, "shadow"), filepath.Join(home, ".zshrc"))
|
|
err := applyOneResource(t, `{"id":"m.f","type":"file","path":"`+filepath.Join(home, ".zshrc")+
|
|
`","content":"x\n"}`)
|
|
mustBeLinkRefusal(t, err)
|
|
if got, _ := os.ReadFile(filepath.Join(outside, "shadow")); string(got) != "root's" {
|
|
t.Fatalf("written through the link: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestADirectoryAndAFileUnderAHomeAreMadeAsBefore(t *testing.T) {
|
|
home, _ := aLinkedHome(t)
|
|
dir := filepath.Join(home, ".claude")
|
|
if err := applyOneResource(t, `{"id":"m.d","type":"directory","path":"`+dir+`","mode":"0700"}`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if m := modeOfPath(t, dir); m != 0o700 {
|
|
t.Fatalf("mode %o", m)
|
|
}
|
|
file := filepath.Join(home, ".config", "mesh", "env.sh")
|
|
if err := applyOneResource(t, `{"id":"m.f","type":"file","path":"`+file+`","content":"A=1\n","mode":"0640"}`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, _ := os.ReadFile(file); string(got) != "A=1\n" || modeOfPath(t, file) != 0o640 {
|
|
t.Fatalf("written %q mode %o", got, modeOfPath(t, file))
|
|
}
|
|
}
|
|
|
|
// The descriptor half: a link put in place after any check is still not followed.
|
|
func TestTheDescriptorCallsFollowNoLinkBelowAHome(t *testing.T) {
|
|
home, outside := aLinkedHome(t)
|
|
link(t, outside, filepath.Join(home, ".claude"))
|
|
mustBeLinkRefusal(t, chmodPath(filepath.Join(home, ".claude"), 0o777))
|
|
mustBeLinkRefusal(t, chmodPath(filepath.Join(home, ".claude", "shadow"), 0o777))
|
|
if m := modeOfPath(t, filepath.Join(outside, "shadow")); m != 0o600 {
|
|
t.Fatalf("chmodded through the link: %o", m)
|
|
}
|
|
mustBeLinkRefusal(t, writeAtomically(filepath.Join(home, ".claude", "new"), []byte("x"), 0o644))
|
|
if _, err := os.Stat(filepath.Join(outside, "new")); !os.IsNotExist(err) {
|
|
t.Fatal("written through the link")
|
|
}
|
|
if _, err := makeDirsSaying(filepath.Join(home, ".claude", "a", "b"), 0o755, ""); err == nil {
|
|
t.Fatal("made directories through the link")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(outside, "a")); !os.IsNotExist(err) {
|
|
t.Fatal("made a directory through the link")
|
|
}
|
|
if _, err := readPath(filepath.Join(home, ".claude", "shadow")); err == nil {
|
|
t.Fatal("read through the link")
|
|
}
|
|
me := os.Getuid()
|
|
// A link itself is owned as a link, never its target.
|
|
if err := chownPath(filepath.Join(home, ".claude"), me, os.Getgid()); err != nil {
|
|
t.Fatalf("a link is owned as a link: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRemovalThroughALinkUnderAHomeIsRefused(t *testing.T) {
|
|
home, outside := aLinkedHome(t)
|
|
link(t, outside, filepath.Join(home, ".claude"))
|
|
_, _, err := remove(context.Background(), archHost(t), store.Applied{ID: "m.f", Type: "file",
|
|
Target: filepath.Join(home, ".claude", "shadow")}, nil, nil)
|
|
mustBeLinkRefusal(t, err)
|
|
if _, err := os.Stat(filepath.Join(outside, "shadow")); err != nil {
|
|
t.Fatal("removed through the link")
|
|
}
|
|
}
|
|
|
|
func TestAPathOutsideEveryHomeIsHandledAsBefore(t *testing.T) {
|
|
_, outside := aLinkedHome(t)
|
|
elsewhere := filepath.Join(filepath.Dir(outside), "srv")
|
|
if err := os.MkdirAll(elsewhere, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
link(t, outside, filepath.Join(elsewhere, "linked"))
|
|
if homeAbove(filepath.Join(elsewhere, "linked", "x")) != "" {
|
|
t.Fatal("not below a home")
|
|
}
|
|
if err := refuseLinksUnderHome(filepath.Join(elsewhere, "linked", "x")); err != nil {
|
|
t.Fatal("a system path may be a link the machine set up; only a home's are refused")
|
|
}
|
|
}
|
|
|
|
func TestHomesAreAPersonsOrAnAgentsNotAServicesOrRoots(t *testing.T) {
|
|
dir := t.TempDir()
|
|
passwd := filepath.Join(dir, "passwd")
|
|
if err := os.WriteFile(passwd, []byte("root:x:0:0::/root:/bin/bash\n"+
|
|
"postgres:x:968:968::/var/lib/postgres:/usr/bin/nologin\n"+
|
|
"nobody:x:65534:65534::/:/usr/bin/nologin\n"+
|
|
"operator:x:1000:1000::/home/operator:/bin/zsh\n"+
|
|
"agent:x:1001:1001::/home/agent:/bin/bash\n"+
|
|
"svc:x:990:990::/home/svc:/usr/bin/nologin\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
was := passwdFile
|
|
passwdFile = passwd
|
|
t.Cleanup(func() { passwdFile = was })
|
|
got := strings.Join(homes(), ",")
|
|
if got != "/home/operator,/home/agent,/home/svc" {
|
|
t.Fatalf("homes %s", got)
|
|
}
|
|
if homeAbove("/home/agent/.claude") != "/home/agent" || homeAbove("/home/agent") != "" ||
|
|
homeAbove("/var/lib/postgres/data") != "" || homeAbove("/root/.ssh") != "" {
|
|
t.Fatal("strictly below a person's or an agent's home, and nothing else")
|
|
}
|
|
}
|