Files
mesh-controller/internal/inventory/rootfree_grants_test.go
T
jschoubben 983bf65141 Answer the controller's own verbs, root-free among them, from the serving controller alone (hq ADR 0259 §8)
The confirmation review asked who may answer root-free on the bus. Composed from the controller's own
manifest, the module principal of the machine running the controller and that machine's runtime were
granted the controller seat's tool subjects too: either could answer root-free, and the runtime's
credential is one an agent on that machine may hold. The controller's seat is now served by the
controller principal alone, in grants and memberships; TestOnlyTheServingControllerMayAnswerRootFree
failed before (3 answerers) and passes. And a machine waiting for its first setuid search is not
root-free, whatever ADR 0266's quiet window does to the self-check.
2026-10-09 13:51:18 +02:00

120 lines
4.8 KiB
Go

package inventory
import (
"os"
"strings"
"testing"
"github.com/novox/mesh-controller/internal/broker"
"github.com/novox/mesh-controller/internal/catalogue"
)
// grantMatches is a NATS subject pattern matching a subject: `*` one token, `>` the rest.
func grantMatches(pattern, subject string) bool {
p, s := strings.Split(pattern, "."), strings.Split(subject, ".")
for i, tok := range p {
if tok == ">" {
return len(s) > i
}
if i >= len(s) || (tok != "*" && tok != s[i]) {
return false
}
}
return len(p) == len(s)
}
func grantsAny(patterns []string, subject string) bool {
for _, p := range patterns {
if grantMatches(p, subject) {
return true
}
}
return false
}
// novox/hq ADR 0259 §8 (confirmation review of 2026-10-09): the router honours a verified sender only on the
// controller's `root-free` word, asked on the bus. So only the serving controller may answer that verb — be
// subscribed to its subject — and nobody may publish into the router's inbox but by answering a request it
// made (allow_responses). Composed here from the controller's own manifest, on a machine where the machine's
// runtime carries it beside an ordinary module, with the router, a channel, a person and an administrator: a
// runtime carrying the controller's module, a node-engine, a channel or anybody else answering `root-free` is
// an agent answering it.
func TestOnlyTheServingControllerMayAnswerRootFree(t *testing.T) {
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
controller, err := catalogue.ParseManifest(raw)
if err != nil {
t.Fatal(err)
}
parse := func(s string) catalogue.Manifest {
m, err := catalogue.ParseManifest([]byte(s))
if err != nil {
t.Fatal(err)
}
return m
}
dir := `"resources": [{"id": "state", "type": "directory", "mode": "0700", "place": "."}]`
router := parse(`{"module": "messenger", "version": "1", "runs-as": "messenger",
"seats": [{"name": "operator-channel", "scope": "mesh", "serves": ["open"], "accepts": ["ask"],
"emits": ["decided"], "by-caller": ["ask", "decided"]}],
"claims": [{"name": "operator-channel", "scope": "mesh", "serves": ["open"]}],
"invokes": ["seat:mesh-controller.root-free", "seat:mesh-controller.conditions"],
"own-secrets": {"broker": "${dir:state}/broker"}, "secrets-owner": "messenger",
"resources": [{"id": "account", "type": "user", "name": "messenger", "shell": "/usr/bin/nologin", "home": "/var/lib/messenger"},
{"id": "state", "type": "directory", "mode": "0700", "place": ".", "owner": "messenger"}]}`)
ordinary := parse(`{"module": "lab-bystander", "version": "1", "own-secrets": {"broker": "${dir:state}/broker"}, ` + dir + `}`)
runtime := catalogue.Manifest{Module: broker.RuntimeModule}
manifests := []catalogue.Manifest{controller, router, ordinary, runtime}
seats := map[string]catalogue.SeatDeclaration{}
declarers := map[string]string{}
for _, m := range manifests {
for _, s := range m.DefinesSeats {
seats[s.Name], declarers[s.Name] = s, m.Module
}
}
for _, own := range catalogue.SeatsWithAProtocol() {
seats[own.Name] = catalogue.SeatDeclaration{Name: own.Name, Scope: own.Scope, Accepts: own.Accepts,
Emits: own.Emits, Serves: own.Serves}
}
records := broker.Records{Nodes: []string{"anchor", "laptop"}, Assigned: map[string][]broker.Declared{},
People: map[string][]string{"operator": {"*"}, "guest": {"mesh-controller.status"}},
Interchangeable: map[string]bool{}}
for _, m := range manifests {
records.Assigned["anchor"] = append(records.Assigned["anchor"], declaredFor(m, seats, declarers))
}
// And a second machine whose runtime carries an ordinary module: where agents run as the operator.
records.Assigned["laptop"] = []broker.Declared{declaredFor(ordinary, seats, declarers), declaredFor(runtime, seats, declarers)}
users, err := broker.Users(records)
if err != nil {
t.Fatal(err)
}
const verb = "mesh.seat.mesh-controller.tool.root-free"
answerers := 0
for _, u := range users {
p, err := broker.PermissionsFor(u)
if err != nil {
t.Fatal(err)
}
answers := grantsAny(p.Subscribe, verb)
if answers != (u.Kind == broker.KindController) {
t.Errorf("%s (%s) %s subscribe to %s", u.Username(), u.Kind,
map[bool]string{true: "may", false: "may not"}[answers], verb)
}
if answers {
answerers++
}
// Nobody publishes into the router's inbox but as an answer to what it asked.
for _, inbox := range []string{"_INBOX.anchor.messenger.x1.y", "_INBOX.anchor.messenger.>"} {
if u.Username() != "anchor.messenger" && grantsAny(p.Publish, inbox) {
t.Errorf("%s may publish into the router's inbox (%s) without being asked", u.Username(), inbox)
}
}
}
if answerers != 1 {
t.Errorf("%d principals may answer root-free, want the controller alone", answerers)
}
}