Files
mesh-controller/internal/catalogue/terminal_settings_test.go
T
jochen 3275a16beb
mesh/merge-gate head 3275a16bebbb merged into trunk 584ee991961c — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head 3275a16bebbb merged into trunk 584ee991961c — pass: its merge-check.sh passed
mesh/delivery delivered
Let a trusted file that names its keys hold only those keys at the terminal (hq issue 586, ADR 0329 §5)
A module with a trusted mergeable file had its whole layer refused through the
settings verb, because such a file took any key. Once a module's files name the
keys they take (ADR 0326), each file takes only those, so only a trusted file's
named keys are the terminal's: betterbird's tags, in a file nothing trusts, is set
through the verb, while its accounts and identities stay the terminal's or the
operator's warrant. A module whose files name no keys keeps the whole-layer rule.
2026-10-13 05:22:35 +02:00

222 lines
13 KiB
Go

package catalogue
import (
"strings"
"testing"
)
// No placement and no access at the machine's own system or the mesh's state, however it is spelled (novox/hq
// issue 339); a module's own place elsewhere is taken.
func TestAPlacementOrAnAccessAtTheMachinesOwnIsRefused(t *testing.T) {
m := Manifest{Module: "notes", Resources: []map[string]any{{"id": "data", "type": "directory"}},
Accesses: []Access{{ID: "media"}}}
for _, path := range []string{"/", "/etc", "/etc/", "/etc/sudoers.d", "/usr/bin", "/root", "/var", "/var/lib",
"/home", "/home/", "/run", "/run/user/1000", "/var/lib/mesh/x", "/var/lib/mesh-host", "/var/lib/mesh-host/identity",
"/srv/../etc", "//etc", "/proc/1", "/sys", "/dev", "/boot/efi", "/bin", "/sbin", "/lib", "/lib64"} {
layers := []Layer{{From: "laptop", Values: map[string]any{PlacesSetting: map[string]any{"data": path}}}}
if _, err := Places(m, layers); err == nil || !strings.Contains(err.Error(), "issue 339") {
t.Errorf("a place at %s: %v", path, err)
}
layers = []Layer{{From: "laptop", Values: map[string]any{AccessesSetting: map[string]any{"media": path}}}}
if _, err := AccessPlaces(m, layers); err == nil || !strings.Contains(err.Error(), "issue 339") {
t.Errorf("an access at %s: %v", path, err)
}
}
for _, path := range []string{"/srv/notes", "/mnt/plex/data", "/storage/media", "/services/media/movies",
"/var/lib/notes/data", "/home/restic/repo", "/var/lib/mesh-store"} {
layers := []Layer{{From: "laptop", Values: map[string]any{PlacesSetting: map[string]any{"data": path}}}}
if got, err := Places(m, layers); err != nil || got["data"].Path != path {
t.Errorf("a place at %s: %v %v", path, got, err)
}
layers = []Layer{{From: "laptop", Values: map[string]any{AccessesSetting: map[string]any{"media": path}}}}
if got, err := AccessPlaces(m, layers); err != nil || got["media"] != path {
t.Errorf("an access at %s: %v %v", path, got, err)
}
}
}
// A line break, a carriage return or a NUL in any string of any setting is refused, at any depth, keys too —
// where a layer is judged, which is both where it is kept and where it is composed (novox/hq issue 339).
func TestASettingHoldsOneLine(t *testing.T) {
m := Manifest{Module: "mailu"}
for _, v := range []any{"a\nDEBUG=1", "a\rb", "a\x00b", []any{"ok", "x\ny"},
map[string]any{"k": []any{"ok", map[string]any{"deep": "x\ny"}}}, map[string]any{"k\nx": "v"}} {
if err := JudgeSettings(m, []Layer{{From: "home", Values: map[string]any{"v": v}}}, false); err == nil ||
!strings.Contains(err.Error(), "line break") {
t.Errorf("%q: %v", v, err)
}
}
if err := JudgeSettings(m, []Layer{{From: "home", Values: map[string]any{"v\nPATH": "x"}}}, false); err == nil {
t.Error("a line break in a key was taken")
}
if err := JudgeSettings(m, []Layer{{From: "home", Values: map[string]any{"v": "one line", "n": 3.0,
"l": []any{"a", "b"}}}}, false); err != nil {
t.Errorf("one line each: %v", err)
}
}
// Lines are allowed in one setting only: step-ca's `root`, as certificates that encoding/pem decodes and
// x509.ParseCertificate parses (novox/hq issue 339). Any other module or key, another label, or a block that is
// not a certificate is refused like any other line break.
func TestOnlyTheAuthoritysRootMayHoldLines(t *testing.T) {
ca := Manifest{Module: "step-ca"}
judge := func(m Manifest, key, v string) error {
return JudgeSettings(m, []Layer{{From: "anchor", Values: map[string]any{key: v}}}, false)
}
for _, ok := range []string{servedRoot, servedRoot + servedRoot, strings.TrimSuffix(servedRoot, "\n")} {
if err := judge(ca, "root", ok); err != nil {
t.Errorf("the authority's root: %v", err)
}
}
body := strings.TrimSuffix(strings.TrimPrefix(servedRoot, "-----BEGIN CERTIFICATE-----\n"), "-----END CERTIFICATE-----\n")
for name, bad := range map[string]string{
"a line after it": servedRoot + "PATH=/tmp\n",
"a line before it": "PATH=\n" + servedRoot,
"another label": "-----BEGIN PRIVATE KEY-----\n" + body + "-----END PRIVATE KEY-----\n",
"headers": "-----BEGIN CERTIFICATE-----\nProc-Type: 4,ENCRYPTED\n\n" + body + "-----END CERTIFICATE-----\n",
"base64 that is no cert": "-----BEGIN CERTIFICATE-----\nMIIBeDCCAR2gAwIBAgIQfake000000000000000000000000\n-----END CERTIFICATE-----\n",
"carriage returns": strings.ReplaceAll(servedRoot, "\n", "\r\n"),
} {
if err := judge(ca, "root", bad); err == nil {
t.Errorf("%s was taken", name)
}
}
if err := judge(ca, "other", servedRoot); err == nil {
t.Error("a certificate in another key of the authority was taken")
}
if err := judge(Manifest{Module: "mailu"}, "root", servedRoot); err == nil {
t.Error("a certificate in another module's setting was taken")
}
if err := JudgeSettings(ca, []Layer{{From: "anchor", Values: map[string]any{"root": []any{servedRoot}}}}, false); err == nil {
t.Error("a certificate below the top of the setting was taken")
}
}
// Every character a reader takes as the end of a line is refused, not only \n and \r: vertical tab, form feed,
// NEL and the Unicode line and paragraph separators (novox/hq issue 339).
func TestEveryLineEndIsRefused(t *testing.T) {
m := Manifest{Module: "mailu"}
for _, v := range []string{"a\vb", "a\fb", "a\u0085b", "a\u2028b", "a\u2029b"} {
if err := JudgeSettings(m, []Layer{{From: "home", Values: map[string]any{"v": v}}}, false); err == nil ||
!strings.Contains(err.Error(), "line break") {
t.Errorf("%q: %v", v, err)
}
}
}
// The review's additions: the spool, the container runtimes' data, /opt, and any home's .ssh.
func TestTheRuntimesDataAndAnyHomesSSHAreTheMachinesOwn(t *testing.T) {
m := Manifest{Module: "notes", Resources: []map[string]any{{"id": "data", "type": "directory"}},
Accesses: []Access{{ID: "media"}}}
for _, path := range []string{"/var/spool", "/var/spool/cron", "/var/lib/docker", "/var/lib/docker/volumes",
"/var/lib/containers/storage", "/var/lib/containerd", "/var/lib/containerd/io.containerd.snapshotter.v1", "/opt", "/opt/app", "/home/alice/.ssh", "/home/alice/.ssh/keys",
"/srv/backup/.ssh", "/root/.ssh"} {
layers := []Layer{{From: "laptop", Values: map[string]any{PlacesSetting: map[string]any{"data": path}}}}
if _, err := Places(m, layers); err == nil || !strings.Contains(err.Error(), "issue 339") {
t.Errorf("a place at %s: %v", path, err)
}
layers = []Layer{{From: "laptop", Values: map[string]any{AccessesSetting: map[string]any{"media": path}}}}
if _, err := AccessPlaces(m, layers); err == nil || !strings.Contains(err.Error(), "issue 339") {
t.Errorf("an access at %s: %v", path, err)
}
}
for _, path := range []string{"/var/lib/dockerish", "/home/alice/ssh", "/home/alice/.sshd-notes", "/storage/media"} {
layers := []Layer{{From: "laptop", Values: map[string]any{PlacesSetting: map[string]any{"data": path}}}}
if _, err := Places(m, layers); err != nil {
t.Errorf("a place at %s: %v", path, err)
}
}
}
// What a provider serves its consumers is the terminal's (novox/hq issue 339): any key under its `serves`, and any
// setting a served value asks for, is set at the terminal alone — a verb that could change a port could point every
// consumer at a listener of the caller's own. So is any setting a file marked `trusted` asks for.
func TestTerminalKeysAreDerived(t *testing.T) {
postgres := Manifest{Module: "postgres", Serves: map[string]map[string]any{"postgres-database": {"port": 5432.0}}}
keycloak := Manifest{Module: "keycloak", Serves: map[string]map[string]any{"oidc-client": {
"issuer": "${setting:issuer}", "token-path": "/protocol/openid-connect/token"}}}
power := Manifest{Module: "power", Resources: []map[string]any{
{"id": "logind", "type": "file", "path": "/etc/systemd/logind.conf.d/power.conf", "trusted": true,
"content": "HandleLidSwitch=${setting:handle-lid-switch}\n"},
{"id": "note", "type": "file", "path": "/var/lib/power/note", "trusted": false, "content": "${setting:greeting}\n"},
// Unmarked counts as trusted (fail closed): only `"trusted": false` lets a verb change what a file asks for.
{"id": "unmarked", "type": "file", "path": "/etc/power/unmarked", "content": "${setting:unmarked}\n"}}}
agent := Manifest{Module: "claude-code", Resources: []map[string]any{{"id": "settings", "type": "file",
"path": "/var/lib/agent/settings.json", "merge": MergeJSON,
"content": "{\n \"role\": \"\",\n \"mcp_servers\": {},\n \"managed_settings\": {}\n}\n"}}}
for _, c := range []struct {
m Manifest
want string
}{
{postgres, "places,accesses,port"},
{keycloak, "places,accesses,issuer,token-path"},
{power, "places,accesses,handle-lid-switch,unmarked"},
{Manifest{Module: "plain"}, "places,accesses"},
// A mergeable file asks for every key its own content names (novox/hq issue 340): the agent's module keeps
// what every Claude Code session on a node obeys in one.
{agent, "places,accesses,managed_settings,mcp_servers,role"},
{Manifest{Module: "notifier", Resources: []map[string]any{{"id": "look", "type": "file", "path": "/var/lib/n/look.json",
"merge": MergeJSON, "trusted": false, "content": `{"font": 13}`}}}, "places,accesses"},
{Manifest{Module: "empty", Resources: []map[string]any{{"id": "config", "type": "file", "path": "/var/lib/e/c.json",
"merge": MergeJSON, "content": `{}`}}}, "places,accesses"},
} {
if got := strings.Join(TerminalKeys(c.m), ","); got != c.want {
t.Errorf("%s: %s; want %s", c.m.Module, got, c.want)
}
}
}
// A module whose mergeable files name the keys they take (ADR 0326) has no whole layer at the terminal: a
// trusted file's named keys are terminal keys, and a file nothing trusts leaves its keys to the verb (novox/hq
// ADR 0329 §5, issue 586: betterbird's tags beside its trusted accounts, identities, calendars, address books
// and filters). A module whose files name no keys keeps the whole layer at the terminal (issue 340).
func TestOnlyANamedKeyOfATrustedFileIsTheTerminals(t *testing.T) {
betterbird := Manifest{Module: "betterbird", Resources: []map[string]any{
{"id": "prefs", "type": "file", "path": "/home/p/prefs.json", "merge": MergeJSON, "trusted": true,
TakesSettingsField: []any{"prefs"}, "content": `{"prefs": {}}`},
{"id": "accounts", "type": "file", "path": "/home/p/accounts.json", "merge": MergeJSON, "trusted": true,
TakesSettingsField: []any{"accounts"}, "content": `{"accounts": {}}`},
{"id": "configuration", "type": "file", "path": "/home/p/configuration.json", "merge": MergeJSON,
TakesSettingsField: []any{"identities", "calendars", "address-books", "filters"}, "content": `{}`},
{"id": "tags", "type": "file", "path": "/home/p/tags.json", "merge": MergeJSON, "trusted": false,
TakesSettingsField: []any{"tags"}, "content": `{"tags": {}}`}}}
if got := TrustedMergeable(betterbird); len(got) != 0 {
t.Fatalf("files that name their keys put no whole layer at the terminal: %v", got)
}
if got := strings.Join(TerminalKeys(betterbird), ","); got != "places,accesses,accounts,address-books,calendars,filters,identities,prefs" {
t.Fatalf("the named keys of the trusted files, unmarked counting as trusted, and not tags: %s", got)
}
silent := Manifest{Module: "keycloak", Resources: []map[string]any{{"id": "runtime", "type": "file",
"path": "/var/lib/kc/runtime.json", "merge": MergeJSON, "content": `{}`}}}
if got := TrustedMergeable(silent); len(got) != 1 {
t.Fatalf("a file that names no keys still takes any key, so its module's layer stays the terminal's: %v", got)
}
}
// A file that asks for a setting says whether what it asks is trusted (novox/hq issue 339): `trusted` is a
// boolean, on a file alone, and a file asking for a setting without it is named.
func TestAFileSaysWhetherItsSettingsAreTrusted(t *testing.T) {
m := Manifest{Module: "power", Resources: []map[string]any{
{"id": "said", "type": "file", "path": "/etc/a", "trusted": true, "content": "${setting:a}"},
{"id": "unsaid", "type": "file", "path": "/etc/b", "content": "${setting:b}"},
{"id": "no-setting", "type": "file", "path": "/etc/c", "content": "plain"}}}
if got := strings.Join(UnsaidTrust(m), ","); got != "unsaid" {
t.Errorf("unsaid: %s; want unsaid", got)
}
// A mergeable file that names keys asks for them, so it is named too; one that names none asks for nothing.
merged := Manifest{Module: "agent", Resources: []map[string]any{
{"id": "settings", "type": "file", "path": "/a", "merge": MergeJSON, "content": `{"managed_settings": {}}`},
{"id": "blank", "type": "file", "path": "/b", "merge": MergeJSON, "content": `{}`}}}
if got := strings.Join(UnsaidTrust(merged), ","); got != "settings" {
t.Errorf("unsaid: %s; want settings", got)
}
for _, bad := range []map[string]any{
{"id": "x", "type": "file", "path": "/etc/x", "trusted": "yes", "content": "${setting:a}"},
{"id": "y", "type": "directory", "trusted": true},
} {
if problems := TrustProblems(Manifest{Module: "power", Resources: []map[string]any{bad}}); len(problems) == 0 {
t.Errorf("%v was taken", bad)
}
}
}