Test that the forge bridge's, indexer's and teller's credentials grant exactly their manifests (issue 1243)

ADR 0437 §4's check: the three manifests, copied from mesh-delivery's main, are composed through Users and
PermissionsFor; each may do what it declares and is refused another module's bucket, another module's name and a seat's
verb subject.
This commit is contained in:
2026-10-19 21:47:47 +00:00
parent 4083755ca1
commit d8c13d97ca
@@ -0,0 +1,490 @@
package broker
import (
"encoding/json"
"slices"
"sort"
"strings"
"testing"
)
// The delivery's forge bridge, indexer and teller are processes on their module's own broker credential, granted from
// the manifest alone (novox/hq ADR 0437 §4, issue 1243): `state` grants writing and watching the module's own buckets
// and streams, `reads` watching another module's bucket and never writing it, `consumes` hearing the named events,
// `invokes` calling the named tools, `uses` the seat operator-channel's by-caller subjects under the module's own name.
//
// The three manifests are copied from mesh-delivery's main at 6a23627426c6 (modules/<module>/module.json, read
// 2026-10-19), whole, so a change to one of them there that changes its grant is a change to read here.
const bridgeManifest = `{
"module": "mesh-delivery-forge-bridge",
"version": "1",
"slug": "forge-bridge",
"consumes": [
"gitea.pull.updated",
"gitea.pull.merged",
"gitea.pull.closed"
],
"streams": [
{ "name": "forge", "events": ["forge.**"] }
],
"state": [
{ "name": "heads", "history": 1 }
],
"reads": [
"mesh-delivery-gate.merges",
"mesh-delivery-gate.owed"
],
"invokes": [
"gitea.gitea_merge_pull_request",
"gitea.gitea_commit_status",
"gitea.gitea_delivery_view",
"gitea.gitea_note_append",
"gitea.gitea_add_comment",
"gitea.gitea_open_pulls",
"gitea.gitea_commit_trees"
],
"own-secrets": {
"broker": "${dir:state}/broker"
},
"secrets-owner": "mesh-delivery-forge-bridge",
"resources": [
{
"id": "account",
"type": "user",
"name": "mesh-delivery-forge-bridge",
"shell": "/usr/bin/nologin",
"home": "/var/lib/mesh-delivery-forge-bridge"
},
{
"id": "state",
"type": "directory",
"mode": "0700",
"owner": "mesh-delivery-forge-bridge",
"place": ".",
"unknown": "program"
},
{
"id": "bridge",
"type": "process",
"name": "mesh-delivery-forge-bridge",
"artifact": "program",
"run": [
"./mesh-delivery-forge-bridge"
],
"env": {
"MESH_BROKER_FILE": "${dir:state}/broker"
},
"user": "mesh-delivery-forge-bridge",
"health": {
"kind": "unit"
}
}
],
"build": {
"artifacts": [
{
"name": "program",
"kind": "bundle",
"language": "go",
"system": "arch",
"from": "cmd/mesh-delivery-forge-bridge",
"binary": "mesh-delivery-forge-bridge"
}
]
}
}`
const indexerManifest = `{
"module": "mesh-delivery-indexer",
"version": "1",
"slug": "indexer",
"consumes": [
"mesh-delivery-forge-bridge.forge.**"
],
"state": [
{ "name": "candidates", "history": 10 },
{ "name": "modules", "history": 20 }
],
"reads": [
"mesh-controller.builds",
"mesh-delivery-gate.source"
],
"own-secrets": {
"broker": "${dir:state}/broker"
},
"secrets-owner": "mesh-delivery-indexer",
"resources": [
{
"id": "account",
"type": "user",
"name": "mesh-delivery-indexer",
"shell": "/usr/bin/nologin",
"home": "/var/lib/mesh-delivery-indexer"
},
{
"id": "state",
"type": "directory",
"mode": "0700",
"owner": "mesh-delivery-indexer",
"place": ".",
"unknown": "program"
},
{
"id": "indexer",
"type": "process",
"name": "mesh-delivery-indexer",
"artifact": "program",
"run": [
"./mesh-delivery-indexer"
],
"env": {
"MESH_BROKER_FILE": "${dir:state}/broker"
},
"user": "mesh-delivery-indexer",
"health": {
"kind": "unit"
}
}
],
"build": {
"artifacts": [
{
"name": "program",
"kind": "bundle",
"language": "go",
"system": "arch",
"from": "cmd/mesh-delivery-indexer",
"binary": "mesh-delivery-indexer"
}
]
}
}`
const tellerManifest = `{
"module": "mesh-delivery-teller",
"version": "1",
"slug": "teller",
"state": [
{ "name": "answers", "history": 5 }
],
"reads": [
"mesh-delivery-gate.asks"
],
"uses": [
"operator-channel"
],
"own-secrets": {
"broker": "${dir:state}/broker"
},
"secrets-owner": "mesh-delivery-teller",
"resources": [
{
"id": "account",
"type": "user",
"name": "mesh-delivery-teller",
"shell": "/usr/bin/nologin",
"home": "/var/lib/mesh-delivery-teller"
},
{
"id": "state",
"type": "directory",
"mode": "0700",
"owner": "mesh-delivery-teller",
"place": ".",
"unknown": "program"
},
{
"id": "teller",
"type": "process",
"name": "mesh-delivery-teller",
"artifact": "program",
"run": [
"./mesh-delivery-teller"
],
"env": {
"MESH_BROKER_FILE": "${dir:state}/broker"
},
"user": "mesh-delivery-teller",
"health": {
"kind": "unit"
}
}
],
"build": {
"artifacts": [
{
"name": "program",
"kind": "bundle",
"language": "go",
"system": "arch",
"from": "cmd/mesh-delivery-teller",
"binary": "mesh-delivery-teller"
}
]
}
}`
// manifestOf is the part of a module's manifest that composes a bus account, as the controller reads it.
type manifestOf struct {
Module string `json:"module"`
Consumes []string `json:"consumes"`
Streams []struct {
Name string `json:"name"`
Events []string `json:"events"`
} `json:"streams"`
State []struct {
Name string `json:"name"`
History int `json:"history"`
} `json:"state"`
Reads []string `json:"reads"`
Invokes []string `json:"invokes"`
Uses []string `json:"uses"`
OwnSecrets map[string]json.RawMessage `json:"own-secrets"`
Claims []struct {
Name string `json:"name"`
Scope string `json:"scope"`
Serves []string `json:"serves"`
} `json:"claims"`
}
// declaredOf is what the controller declares of a manifest to the broker (inventory's busrecords): its state as
// buckets, its streams under the module's name in the bus's spelling, its claims as held seats, the module streams
// its consumes name as heard, and no account when it declares no own secret `broker`. streamsOf are the module
// streams the other manifests of the test own, by module.
func declaredOf(t *testing.T, raw string, streamsOf map[string]ModuleStream) Declared {
t.Helper()
var m manifestOf
if err := json.Unmarshal([]byte(raw), &m); err != nil {
t.Fatal(err)
}
d := Declared{Module: m.Module, Consumes: m.Consumes, Reads: m.Reads, Invokes: m.Invokes}
if _, reads := m.OwnSecrets["broker"]; !reads {
d.NoAccount = true
}
for _, s := range m.State {
d.State = append(d.State, Bucket{Module: m.Module, Name: s.Name, History: s.History})
}
for _, s := range m.Streams {
var events []string
for _, e := range s.Events {
events = append(events, WirePattern(e))
}
d.Streams = append(d.Streams, ModuleStream{Module: m.Module, Name: strings.ToUpper(s.Name), Events: events,
MaxAgeDays: 7, MaxMiB: 64})
}
for _, c := range m.Claims {
d.Holds = append(d.Holds, Seat{Name: c.Name, Scope: c.Scope, Serves: c.Serves})
}
for _, name := range m.Uses {
if name == "operator-channel" {
d.Uses = append(d.Uses, operatorChannelWithCondition())
}
}
for _, c := range m.Consumes {
emitter, event, _ := strings.Cut(c, ".")
if s, owned := streamsOf[emitter]; owned && strings.HasPrefix(event, strings.ToLower(s.Name)+".") {
d.Hears = append(d.Hears, Heard{Stream: s.Name, Emitter: emitter,
Filters: []string{ModuleStreamSubject(emitter, WirePattern(event))}})
d.HearsAStream = true
}
}
return d
}
// canWrite says a principal may publish to a bucket's key; canRead says it may get one directly.
func canWrite(p Permissions, bucket, key string) bool {
return allowed(p.Publish, "$KV."+bucket+"."+key)
}
func canRead(p Permissions, bucket, key string) bool {
return allowed(p.Publish, "$JS.API.DIRECT.GET.KV_"+bucket+".$KV."+bucket+"."+key)
}
// composed is every module's and every machine's tool runner's grants the broker composes for these records,
// as `<module>@<node>` and `runner@<node>`.
func composed(t *testing.T, r Records) map[string]Permissions {
t.Helper()
users, err := Users(r)
if err != nil {
t.Fatal(err)
}
out := map[string]Permissions{}
for _, u := range users {
switch u.Kind {
case KindNodeTools:
out["runner@"+u.Node] = perms(t, u)
case KindModule:
out[u.Module+"@"+u.Node] = perms(t, u)
}
}
return out
}
// withPrefix is the patterns that start with any of the prefixes, sorted.
func withPrefix(patterns []string, prefixes ...string) []string {
var out []string
for _, p := range patterns {
for _, prefix := range prefixes {
if strings.HasPrefix(p, prefix) {
out = append(out, p)
break
}
}
}
sort.Strings(out)
return out
}
// eventsSaid is the events a principal may publish, under a module's name or on a module's stream.
func eventsSaid(p Permissions) []string {
var out []string
for _, s := range withPrefix(p.Publish, "mesh.mod.", "mesh.stream.") {
if strings.Contains(s, ".event.") {
out = append(out, s)
}
}
return out
}
// toolsCalled is the tools of one module a principal may call, by name.
func toolsCalled(p Permissions, module string) []string {
var out []string
for _, s := range withPrefix(p.Publish, "mesh.mod."+module+".tool.") {
if name := strings.TrimPrefix(s, "mesh.mod."+module+".tool."); !strings.Contains(name, ".") {
out = append(out, name)
}
}
return out
}
// partsRecords is the three parts on one machine, beside the machine's tool runner.
func partsRecords(t *testing.T) Records {
t.Helper()
streams := map[string]ModuleStream{"mesh-delivery-forge-bridge": {Module: "mesh-delivery-forge-bridge", Name: "FORGE"}}
return Records{Nodes: []string{"novox"}, Assigned: map[string][]Declared{"novox": {
{Module: RuntimeModule},
declaredOf(t, bridgeManifest, streams), declaredOf(t, indexerManifest, streams), declaredOf(t, tellerManifest, streams),
}}}
}
// 1243/AC1: each part's credential, composed from its manifest, may do exactly what its declaration names: it writes
// its own buckets and stream, reads the buckets it reads (and cannot write them), hears what it consumes, calls what it
// invokes, and says what the seat it uses lets it say under its own name.
func TestEachPartsCredentialGrantsExactlyItsDeclaration(t *testing.T) {
all := composed(t, partsRecords(t))
bridge, indexer, teller := all["mesh-delivery-forge-bridge@novox"], all["mesh-delivery-indexer@novox"], all["mesh-delivery-teller@novox"]
// The forge bridge: state heads, the stream FORGE, reads merges and owed, invokes seven tools of the forge.
if got, want := withPrefix(bridge.Publish, "$KV."), []string{"$KV.mesh-delivery-forge-bridge_heads.>"}; !slices.Equal(got, want) {
t.Errorf("the bridge writes %v; want exactly %v", got, want)
}
if got, want := eventsSaid(bridge), []string{"mesh.stream.mesh-delivery-forge-bridge.event.forge.>"}; !slices.Equal(got, want) {
t.Errorf("the bridge says %v; want exactly %v", got, want)
}
for _, b := range []string{"merges", "owed"} {
if !canRead(bridge, "mesh-delivery-gate_"+b, "g.1") || canWrite(bridge, "mesh-delivery-gate_"+b, "g.1") {
t.Errorf("the bridge must read and not write the gate's %s", b)
}
}
for _, event := range []string{"pull.updated", "pull.merged", "pull.closed"} {
if !allowed(bridge.Subscribe, "mesh.mod.gitea.event."+event) {
t.Errorf("the bridge does not hear gitea's %s", event)
}
}
if allowed(bridge.Subscribe, "mesh.mod.gitea.event.pull.opened") {
t.Error("the bridge hears an event of gitea's it does not consume")
}
called := []string{"gitea_add_comment", "gitea_commit_status", "gitea_commit_trees", "gitea_delivery_view",
"gitea_merge_pull_request", "gitea_note_append", "gitea_open_pulls"}
if got := toolsCalled(bridge, "gitea"); !slices.Equal(got, called) {
t.Errorf("the bridge calls %v; want exactly %v", got, called)
}
if allowed(bridge.Publish, "mesh.mod.gitea.tool.gitea_delete_repo") {
t.Error("the bridge may call a tool of gitea's it does not invoke")
}
// The indexer: state candidates and modules, reads the controller's builds and the gate's source, hears FORGE.
if got, want := withPrefix(indexer.Publish, "$KV."), []string{"$KV.mesh-delivery-indexer_candidates.>", "$KV.mesh-delivery-indexer_modules.>"}; !slices.Equal(got, want) {
t.Errorf("the indexer writes %v; want exactly %v", got, want)
}
if got := eventsSaid(indexer); len(got) != 0 {
t.Errorf("the indexer says %v; it declares no event", got)
}
for _, read := range [][2]string{{"mesh-controller_builds", "b.1"}, {"mesh-delivery-gate_source", "main"}} {
if !canRead(indexer, read[0], read[1]) || canWrite(indexer, read[0], read[1]) {
t.Errorf("the indexer must read and not write %s", read[0])
}
}
if !allowed(indexer.Subscribe, "mesh.mod.mesh-delivery-forge-bridge.event.forge.merged") ||
!allowed(indexer.Publish, "$JS.API.CONSUMER.MSG.NEXT.FORGE.novox_mesh-delivery-indexer") {
t.Errorf("the indexer does not hear FORGE through its own consumer: %v / %v", indexer.Subscribe, indexer.Publish)
}
if got := withPrefix(indexer.Publish, "mesh.seat."); len(got) != 0 {
t.Errorf("the indexer reaches the seats %v; it uses none", got)
}
// The teller: state answers, reads the gate's asks, uses operator-channel under its own name.
if got, want := withPrefix(teller.Publish, "$KV."), []string{"$KV.mesh-delivery-teller_answers.>"}; !slices.Equal(got, want) {
t.Errorf("the teller writes %v; want exactly %v", got, want)
}
if !canRead(teller, "mesh-delivery-gate_asks", "merge.g.h") || canWrite(teller, "mesh-delivery-gate_asks", "merge.g.h") {
t.Error("the teller must read and not write the gate's asks")
}
for _, verb := range []string{"ask", "cancel", "condition"} {
if !allowed(teller.Publish, "mesh.seat.operator-channel.accept."+verb+".mesh-delivery-teller") {
t.Errorf("the teller may not say %s as itself", verb)
}
}
if !allowed(teller.Subscribe, "mesh.seat.operator-channel.event.decided.mesh-delivery-teller") {
t.Errorf("the teller does not hear its own decided events: %v", teller.Subscribe)
}
}
// 1243/AC2: each part's credential is refused writing another module's bucket, publishing under another module's name
// and answering a verb subject of a seat.
func TestNoPartsCredentialReachesBeyondItsDeclaration(t *testing.T) {
all := composed(t, partsRecords(t))
parts := []string{"mesh-delivery-forge-bridge", "mesh-delivery-indexer", "mesh-delivery-teller"}
buckets := map[string][]string{
"mesh-delivery-forge-bridge": {"heads"}, "mesh-delivery-indexer": {"candidates", "modules"},
"mesh-delivery-teller": {"answers"}, "mesh-delivery-gate": {"merges", "owed", "source", "asks", "deliveries"},
"mesh-controller": {"builds", "plans"},
}
for _, part := range parts {
p := all[part+"@novox"]
for owner, names := range buckets {
if owner == part {
continue
}
for _, name := range names {
if canWrite(p, owner+"_"+name, "some.key") {
t.Errorf("%s may write %s_%s", part, owner, name)
}
}
}
for _, other := range append(append([]string(nil), parts...), "mesh-delivery-gate", "mesh-controller", "gitea", "mesh-delivery") {
if other == part {
continue
}
for _, subject := range []string{"mesh.mod." + other + ".event.merged", "mesh.stream." + other + ".event.forge.merged"} {
if allowed(p.Publish, subject) {
t.Errorf("%s may publish %s, under %s's name", part, subject, other)
}
}
}
for _, verb := range []string{"mesh.seat.mesh-delivery.tool.deliveries", "mesh.seat.mesh-delivery.tool.release",
"mesh.seat.mesh-delivery.tool.stop", "mesh.seat.operator-channel.tool.tell"} {
if allowed(p.Subscribe, verb) {
t.Errorf("%s answers the verb subject %s", part, verb)
}
}
}
// The teller says what it says as itself alone.
teller := all["mesh-delivery-teller@novox"]
for _, subject := range []string{"mesh.seat.operator-channel.accept.ask.mesh-delivery-gate",
"mesh.seat.operator-channel.accept.condition.mesh-delivery-indexer", "mesh.seat.operator-channel.accept.ask.*",
"mesh.seat.operator-channel.accept.ask.>"} {
if allowed(teller.Publish, subject) {
t.Errorf("the teller may publish %s", subject)
}
}
}