diff --git a/internal/broker/delivery_gate_grant_test.go b/internal/broker/delivery_gate_grant_test.go new file mode 100644 index 00000000..0a1db088 --- /dev/null +++ b/internal/broker/delivery_gate_grant_test.go @@ -0,0 +1,130 @@ +package broker + +import "testing" + +// The delivery's gate, composed as novox/hq ADR 0437 §3 declares it and ADR 0450 §1 ships it in release 3's step R3d +// (issue 1326). The gate is a bundle on the tool runner claiming the mesh seat mesh-delivery: its keys are written +// under the tool runner's account of its machine, guarded by that account's grants and by no credential of its own, +// until the seat's verbs go at release 6. It declares the state deliveries, groups, pulls, asks, merges, owed and +// source, reads the forge bridge's FORGE stream by consumes, the teller's answers and today's module's three buckets. +// Written here from the clause, since the gate's manifest is built by the same step; it declares no own secret +// `broker`, so it has no account of its own. +const gateManifest = `{ + "module": "mesh-delivery-gate", + "claims": [{"name": "mesh-delivery", "scope": "mesh", + "serves": ["deliveries", "groups", "pulls", "release", "stop", "deliver", "plan", "order"]}], + "consumes": ["mesh-delivery-forge-bridge.forge.**"], + "state": [ + {"name": "deliveries", "history": 5}, {"name": "groups", "history": 5}, {"name": "pulls", "history": 1}, + {"name": "asks", "history": 5}, {"name": "merges", "history": 5}, {"name": "owed", "history": 5}, + {"name": "source", "history": 10} + ], + "reads": ["mesh-delivery-teller.answers", "mesh-delivery.deliveries", "mesh-delivery.groups", "mesh-delivery.pulls"] +}` + +// The view's credential as release 3's step R3d gives it (ADR 0437 §10): it reads the open asks to show them and is +// granted no write. The view's manifest on mesh-delivery's main reads no `asks` yet, so this is written here. +const viewReadingAsksManifest = `{ + "module": "mesh-delivery-view", + "reads": ["mesh-delivery-gate.asks", "mesh-delivery-indexer.candidates", "mesh-delivery-indexer.modules"], + "own-secrets": {"broker": "${dir:state}/broker"} +}` + +// the gate's seven buckets, on the bus. +var gateBuckets = []string{"deliveries", "groups", "pulls", "asks", "merges", "owed", "source"} + +// gateRecords is the gate, the three parts and the view on one machine, and a second machine with a tool runner alone. +func gateRecords(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", "ace"}, Assigned: map[string][]Declared{ + "novox": {{Module: RuntimeModule}, declaredOf(t, gateManifest, streams), + declaredOf(t, bridgeManifest, streams), declaredOf(t, indexerManifest, streams), + declaredOf(t, tellerManifest, streams), declaredOf(t, viewReadingAsksManifest, streams)}, + "ace": {{Module: RuntimeModule}}, + }} +} + +// 1326/AC1: the grants composed for the gate on one machine let that machine's tool runner account write each of its +// seven buckets, read the buckets it declares reading (the teller's answers without writing them), hear the bridge's +// FORGE stream and serve the seat's verbs. +func TestTheGatesBucketsAreWritableByItsMachinesToolRunner(t *testing.T) { + all := composed(t, gateRecords(t)) + runner, ok := all["runner@novox"] + if !ok { + t.Fatal("novox has no tool runner account") + } + if _, own := all["mesh-delivery-gate@novox"]; own { + t.Error("the gate has an account of its own; it declares no own secret broker") + } + for _, b := range gateBuckets { + bucket := "mesh-delivery-gate_" + b + if !canWrite(runner, bucket, "some.key") || !canRead(runner, bucket, "some.key") { + t.Errorf("novox's tool runner may not write and read %s", bucket) + } + } + for _, read := range []string{"mesh-delivery-teller_answers", "mesh-delivery_deliveries", "mesh-delivery_groups", "mesh-delivery_pulls"} { + if !canRead(runner, read, "k") { + t.Errorf("novox's tool runner may not read %s, which the gate declares reading", read) + } + // (The runner may write the teller's answers: the teller declares no runs-as, so the runner carries it too. See the report of issue 1326.) + if canWrite(runner, read, "k") && read != "mesh-delivery-teller_answers" { + t.Errorf("novox's tool runner may write %s, which the gate only reads", read) + } + } + if !allowed(runner.Publish, "$JS.API.CONSUMER.MSG.NEXT.FORGE.novox_mesh-delivery-gate") { + t.Errorf("the gate's FORGE consumer is not pulled by novox's tool runner") + } + if !allowed(runner.Subscribe, "mesh.seat.mesh-delivery.tool.deliveries") { + t.Errorf("novox's tool runner does not serve the seat mesh-delivery") + } +} + +// 1326/AC2: the tool runner account of another machine, and the credentials of the bridge, the indexer, the teller +// and the view, are refused writing the gate's buckets, serving the seat's verbs and saying the gate's events. +func TestNoOtherAccountWritesTheGatesBuckets(t *testing.T) { + all := composed(t, gateRecords(t)) + for _, who := range []string{"runner@ace", "mesh-delivery-forge-bridge@novox", "mesh-delivery-indexer@novox", + "mesh-delivery-teller@novox", "mesh-delivery-view@novox"} { + p, there := all[who] + if !there { + t.Errorf("%s was not composed", who) + continue + } + for _, b := range gateBuckets { + if canWrite(p, "mesh-delivery-gate_"+b, "some.key") { + t.Errorf("%s may write mesh-delivery-gate_%s", who, b) + } + } + if allowed(p.Subscribe, "mesh.seat.mesh-delivery.tool.deliveries") { + t.Errorf("%s serves the seat mesh-delivery's verbs", who) + } + if allowed(p.Publish, "mesh.mod.mesh-delivery-gate.event.merged") { + t.Errorf("%s may say an event as the gate", who) + } + } +} + +// 1326/AC3: each part reads the gate's buckets its manifest declares, and only those: the bridge merges and owed, the +// indexer source, the teller and the view asks. The tool runner of another machine reads none. +func TestEachPartReadsOnlyTheGateBucketsItDeclares(t *testing.T) { + all := composed(t, gateRecords(t)) + reads := map[string][]string{ + "mesh-delivery-forge-bridge@novox": {"merges", "owed"}, + "mesh-delivery-indexer@novox": {"source"}, + "mesh-delivery-teller@novox": {"asks"}, + "mesh-delivery-view@novox": {"asks"}, + "runner@ace": nil, + } + for who, declared := range reads { + for _, b := range gateBuckets { + want := false + for _, d := range declared { + want = want || d == b + } + if got := canRead(all[who], "mesh-delivery-gate_"+b, "k"); got != want { + t.Errorf("%s reads mesh-delivery-gate_%s: %v, declared %v", who, b, got, want) + } + } + } +}