package buscompose import "testing" // Defends issue 1790 and novox/hq ADR 0547 ยง5 (the operator, 2026-10-26 08:54): the grants part may create the stream // of each bucket, log and stream a placed module's manifest declares, by its own name, and read it back; nothing // wider, nothing updated or deleted, and no other principal so. Held byte-equal in the controller's copy. And ADR 0547 // clause 7 (issue 1945, the operator's second exception): the grants part writes its own object bucket of sealed user // lists, and the node-engine of the node holding mesh-broker alone reads its one key and an object of it by name. func TestTheGrantsPartCreatesOnlyDeclaredStreams(t *testing.T) { records := Records{ Nodes: []string{"novox", "ace"}, Assigned: map[string][]Declared{"ace": {{Module: "plex"}}, "novox": { {Module: "nats", SnapshotsTheBus: true}, // mesh-sdk's copy alone (ADR 0547 clause 9, named readers): the vault's hashes are read by the grants part // alone, whatever another module's reads say; the controller's copy grants any module that declares it. {Module: "mesh-vault", State: []Bucket{{Module: "mesh-vault", Name: "bus-accounts"}}}, {Module: "spy", Serves: []string{"spy_x"}, Reads: []string{VaultBusAccounts}}, {Module: "mesh-delivery-builder", State: []Bucket{{Module: "mesh-delivery-builder", Name: "steps", History: 5}}, Logs: []Log{{Module: "mesh-delivery-builder", Name: "builds"}}, Streams: []ModuleStream{{Module: "mesh-delivery-builder", Name: "BUILDASKS", Events: []string{"ask"}}}}, {Module: GrantsModule, State: []Bucket{{Module: GrantsModule, Name: "assertions"}}, Reads: []string{VaultBusAccounts}}, {Module: RuntimeModule}, }}, } users, err := Users(records) if err != nil { t.Fatal(err) } perms := map[string]Permissions{} for _, u := range users { if u.Kind == KindNode || u.Kind == KindNodeTools { p, err := PermissionsFor(u) if err != nil { t.Fatalf("%s: %v", u.Username(), err) } perms[u.Username()] = p continue } if u.Kind != KindModule { continue } p, err := PermissionsFor(u) if err != nil { t.Fatalf("%s: %v", u.Username(), err) } perms[u.Module] = p } for _, row := range []struct { who, subject string may bool }{ {GrantsModule, "$JS.API.STREAM.CREATE.KV_mesh-delivery-builder_steps", true}, {GrantsModule, "$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps", true}, {GrantsModule, "$JS.API.STREAM.CREATE.LOG_mesh-delivery-builder_builds", true}, {GrantsModule, "$JS.API.STREAM.CREATE.BUILDASKS", true}, {GrantsModule, "$JS.API.STREAM.CREATE.KV_mesh-grants_assertions", true}, {GrantsModule, "$JS.API.STREAM.UPDATE.KV_mesh-delivery-builder_steps", false}, {GrantsModule, "$JS.API.STREAM.DELETE.KV_mesh-delivery-builder_steps", false}, {GrantsModule, "$JS.API.STREAM.PURGE.KV_mesh-delivery-builder_steps", false}, {GrantsModule, "$JS.API.STREAM.CREATE.KV_mesh-delivery-builder_undeclared", false}, {GrantsModule, "$JS.API.STREAM.CREATE.EVENTS", false}, {GrantsModule, "$JS.API.STREAM.CREATE.ASSIGNMENTS", false}, {GrantsModule, "$JS.API.STREAM.INFO.EVENTS", false}, {"mesh-delivery-builder", "$JS.API.STREAM.CREATE.KV_mesh-delivery-builder_steps", false}, // The sealed user list (ADR 0547 clause 7): the grants part writes its own bucket and no other. {GrantsModule, "$JS.API.STREAM.CREATE.OBJ_mesh-grants-user-list", true}, {GrantsModule, "$O.mesh-grants-user-list.C.abc", true}, {GrantsModule, "$JS.API.STREAM.PURGE.OBJ_mesh-grants-user-list", true}, {GrantsModule, "$O.declaration-files-novox.C.abc", false}, {GrantsModule, "$JS.API.STREAM.DELETE.OBJ_mesh-grants-user-list", false}, {"mesh-delivery-builder", "$O.mesh-grants-user-list.C.abc", false}, // The bus node's engine reads the one key and an object by name; nothing of the bucket's list, no write. {"node.novox", "$JS.API.DIRECT.GET.KV_mesh-grants_user-list.$KV.mesh-grants_user-list.sealed", true}, {"node.novox", "$JS.API.CONSUMER.CREATE.KV_mesh-grants_user-list.abc.$KV.mesh-grants_user-list.sealed", true}, {"node.novox", "$JS.API.DIRECT.GET.OBJ_mesh-grants-user-list.$O.mesh-grants-user-list.M.abc", true}, {"node.novox", "$JS.API.CONSUMER.CREATE.OBJ_mesh-grants-user-list.abc.$O.mesh-grants-user-list.C.abc", true}, {"node.novox", "$JS.API.DIRECT.GET.KV_mesh-grants_user-list.$KV.mesh-grants_user-list.list", false}, {"node.novox", "$JS.API.STREAM.INFO.OBJ_mesh-grants-user-list", false}, {"node.novox", "$JS.API.STREAM.MSG.GET.OBJ_mesh-grants-user-list", false}, {"node.novox", "$O.mesh-grants-user-list.C.abc", false}, {"node.ace", "$JS.API.DIRECT.GET.KV_mesh-grants_user-list.$KV.mesh-grants_user-list.sealed", false}, {"node.ace", "$JS.API.DIRECT.GET.OBJ_mesh-grants-user-list.$O.mesh-grants-user-list.M.abc", false}, {GrantsModule, "$JS.API.DIRECT.GET.KV_mesh-vault_bus-accounts.$KV.mesh-vault_bus-accounts.x", true}, {"spy", "$JS.API.DIRECT.GET.KV_mesh-vault_bus-accounts.$KV.mesh-vault_bus-accounts.x", false}, {"novox.node-tools", "$JS.API.DIRECT.GET.KV_mesh-vault_bus-accounts.$KV.mesh-vault_bus-accounts.x", false}, } { if got := MayPublish(perms[row.who], row.subject); got != row.may { t.Errorf("%s may publish %s: %v, want %v", row.who, row.subject, got, row.may) } } // Issue 1790 (2026-10-26): a module hears the bus create each bucket it may bind, its own and those it reads, so a // state missing at its start is bound when it appears; no other bucket's, and no other advisory. for _, row := range []struct { who, subject string may bool }{ {"mesh-delivery-builder", "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_steps", true}, {GrantsModule, "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-vault_bus-accounts", true}, {"mesh-delivery-builder", "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-grants_assertions", false}, {"mesh-delivery-builder", "$JS.EVENT.ADVISORY.STREAM.CREATED.LOG_mesh-delivery-builder_builds", false}, {"mesh-delivery-builder", "$JS.EVENT.ADVISORY.STREAM.DELETED.KV_mesh-delivery-builder_steps", false}, } { if got := MaySubscribe(perms[row.who], row.subject); got != row.may { t.Errorf("%s may hear %s: %v, want %v", row.who, row.subject, got, row.may) } } // A name that is no plain token is never granted, and a module other than the grants part is never granted one. if got := CreateStreamGrants([]string{"*", ">", "KV_a.b", "KV_a_b"}); len(got) != 2 { t.Errorf("grants for wildcard names: %v", got) } other, err := PermissionsFor(Principal{Kind: KindModule, Node: "novox", Module: "shop", CreatesStreams: []string{"KV_shop_x"}}) if err != nil { t.Fatal(err) } if MayPublish(other, "$JS.API.STREAM.CREATE.KV_shop_x") { t.Error("a module other than the grants part was granted a create") } // The writers table refuses the grants part an update, and a create of a stream it was not given. g := Principal{Kind: KindModule, Node: "novox", Module: GrantsModule, CreatesStreams: []string{"KV_a_b"}} if err := CheckWriters(g, []string{"$JS.API.STREAM.CREATE.KV_a_b"}); err != nil { t.Errorf("the declared create refused: %v", err) } for _, refused := range []string{"$JS.API.STREAM.UPDATE.KV_a_b", "$JS.API.STREAM.CREATE.KV_a_c", "$JS.API.STREAM.CREATE.*"} { if CheckWriters(g, []string{refused}) == nil { t.Errorf("%s was not refused", refused) } } }