Grant the bus's own module the snapshot API and nothing else (hq ADR 0235)

The night's backup of the bus takes each stream through JetStream's snapshot
API, run by the nats module under its own account. The module holding
mesh-broker is composed that account: stream names and info, the snapshot
request, its flow-control acks, its own inbox — no write, which the writers
table checks. A bus module declaring anything else to say on the bus is
refused by module check rather than silently granted nothing. The genesis
user list is unchanged: the controller's grants are.
This commit is contained in:
jochen
2026-10-06 18:20:57 +02:00
parent 2b5060789f
commit 48581af35c
11 changed files with 463 additions and 2 deletions
+3
View File
@@ -141,6 +141,9 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio
if _, reads := m.OwnSecrets["broker"]; !reads {
d.NoAccount = true
}
// The bus's own module — the one holding mesh-broker — copies the bus's streams for the night's
// backup, and its account is granted that and nothing else (novox/hq ADR 0235).
d.SnapshotsTheBus = m.ClaimsSeat(catalogue.BrokerSeat)
for _, c := range m.Claims {
// Every seat with a protocol, the mesh's own included. One that says only who does a job is
// not here and grants nothing, which is most of them.
+55
View File
@@ -241,3 +241,58 @@ func TestIssuingATokenRecordsTheAccountItIsThePasswordOf(t *testing.T) {
}
}
}
// The module holding mesh-broker is the bus, and its user is the bus's own: granted the snapshot API
// for the night's backup and nothing else, whatever tools it declares (novox/hq ADR 0235). Another
// module on the same machine is granted none of it.
func TestTheBussOwnModuleBecomesTheSnapshotUser(t *testing.T) {
bus := catalogue.Manifest{
Module: "nats", Version: "1",
Provides: []catalogue.Offer{{Name: "mesh-bus", Scope: catalogue.ScopeMesh}},
Claims: []catalogue.Claim{{Name: catalogue.BrokerSeat, Scope: catalogue.ScopeMesh}},
Tools: []string{"nats_streams"},
OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/run/broker"}},
}
shop := catalogue.Manifest{Module: "shop", Version: "1", Emits: []string{"order.placed"},
OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/run/broker"}}}
inv, ctx := aMeshWith(t, bus, shop)
if _, err := inv.AddNode(ctx, "one"); err != nil {
t.Fatal(err)
}
for _, module := range []string{"nats", "shop"} {
if _, err := inv.Assign(ctx, "one", module); err != nil {
t.Fatal(err)
}
}
records, err := inv.BusRecords(ctx)
if err != nil {
t.Fatal(err)
}
users, err := broker.Users(records)
if err != nil {
t.Fatal(err)
}
seen := map[string]bool{}
for _, u := range users {
perms, err := broker.PermissionsFor(u)
if err != nil {
t.Fatal(err)
}
snapshots := granted(perms.Publish, "$JS.API.STREAM.SNAPSHOT.*")
switch u.Username() {
case "one.nats":
seen[u.Username()] = true
if !snapshots || len(perms.Publish) != len(broker.BusSnapshotGrants.Publish) {
t.Fatalf("the bus's own user is granted %v, not the snapshot API alone", perms.Publish)
}
case "one.shop":
seen[u.Username()] = true
if snapshots {
t.Fatal("a module that is not the bus may snapshot it")
}
}
}
if !seen["one.nats"] || !seen["one.shop"] {
t.Fatalf("users derived: %v", seen)
}
}