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.
This commit is contained in:
@@ -113,6 +113,9 @@ func MembershipFor(node string, d Declared, where Placements) Membership {
|
||||
m.Serves = append(m.Serves, Served{Subject: own + ".tool.{tool}", Queue: "serve." + d.Module})
|
||||
}
|
||||
for _, s := range d.Holds {
|
||||
if servedOnlyByTheController(s) {
|
||||
continue // answered by the serving controller alone, never through a membership
|
||||
}
|
||||
for _, verb := range s.Serves {
|
||||
m.Seats = append(m.Seats, SeatServed{Seat: s.Name, Verb: verb, Subject: seatToolSubject(s, verb, node)})
|
||||
}
|
||||
|
||||
+15
-1
@@ -566,8 +566,12 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
"$JS.API.CONSUMER.INFO."+consumerStream(p)+"."+consumerDurable(p),
|
||||
"$JS.API.CONSUMER.MSG.NEXT."+consumerStream(p)+"."+consumerDurable(p))
|
||||
|
||||
// 3. Seats it holds: full participation.
|
||||
// 3. Seats it holds: full participation — but the controller's own seat, whose verbs only the serving
|
||||
// controller answers, on its own connection (servedOnlyByTheController).
|
||||
for _, s := range p.Holds {
|
||||
if servedOnlyByTheController(s) {
|
||||
continue
|
||||
}
|
||||
if s.isNewTraffic() {
|
||||
// Composed by SeatTrafficOf below, worker and all; only its tools are served here.
|
||||
for _, t := range s.Serves {
|
||||
@@ -661,6 +665,9 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
pub = append(pub, own+".event."+e)
|
||||
}
|
||||
for _, s := range d.Holds {
|
||||
if servedOnlyByTheController(s) {
|
||||
continue
|
||||
}
|
||||
for _, t := range s.Serves {
|
||||
sub = append(sub, seatToolSubject(s, t, p.Node))
|
||||
}
|
||||
@@ -795,6 +802,13 @@ func seatSubject(s Seat, kind, verb string) string {
|
||||
// seat carries the node it is asked of, because a flat subject would reach every machine's holder
|
||||
// and the queue group would silently pick a winner (novox/hq ADR 0132, design 33 §4). A holder
|
||||
// subscribes its own node's; a user publishes any node's (`*`) and names the machine in the subject.
|
||||
// servedOnlyByTheController says a seat's verbs are answered by the serving controller alone, on its own
|
||||
// connection (the KindController grant), never by a module claiming the seat or a runtime carrying it: the
|
||||
// controller's own seat. Its verbs decide what the mesh is — and `root-free` decides whether the router believes
|
||||
// a verified sender (novox/hq ADR 0259 §8) — so a machine's runtime, whose credential an agent on that machine
|
||||
// may hold, answering one would be an agent answering it (the confirmation review of 2026-10-09).
|
||||
func servedOnlyByTheController(s Seat) bool { return s.Name == ControllerSeat }
|
||||
|
||||
func seatToolSubject(s Seat, verb, node string) string {
|
||||
base := seatSubject(s, "tool", verb)
|
||||
if s.Scope == "node" && node != "" {
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user