Second meeting of the engine-state-in-runner kind (mesh-host_acts this morning, mesh-host_health now); thorough: the runner never carries the engine module. One predicate, Composed, keeps the engine out of the placed loop (no user, no process, nothing carried); CarriedByRunner is the one test of what the runner carries. The engine's grants stay the node account's, its state created through Records.Engine.
154 lines
8.9 KiB
Go
154 lines
8.9 KiB
Go
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},
|
|
// The node-engine's manifest as a placement names it (issue 774, met twice on 2026-10-30): no account of its
|
|
// own, so the runner would carry it, and its state is the node's to write alone.
|
|
{Module: EngineModule, NoAccount: true, Consumes: []string{"restic.backed-up"},
|
|
State: []Bucket{{Module: EngineModule, Name: "health"}, {Module: EngineModule, Name: EngineActsState}}},
|
|
}},
|
|
// The node-engine's own manifest, placed on every node though no node is assigned it (novox/hq ADR 0563 §2, §3).
|
|
Engine: &Declared{Module: EngineModule, State: []Bucket{{Module: EngineModule, Name: EngineActsState}}},
|
|
}
|
|
users, err := Users(records)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
perms := map[string]Permissions{}
|
|
for _, u := range users {
|
|
if u.Module == EngineModule {
|
|
t.Errorf("the node-engine's manifest composed a user of its own: %s", u.Username())
|
|
}
|
|
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 node-engine's acts: created by the grants part, written by each node's engine under its own keys alone.
|
|
{GrantsModule, "$JS.API.STREAM.CREATE.KV_mesh-host_acts", true},
|
|
{"node.ace", "$JS.API.STREAM.CREATE.KV_mesh-host_acts", false},
|
|
{"node.ace", "$KV.mesh-host_acts.ace.act.hand-over-1", true},
|
|
{"node.ace", "$KV.mesh-host_acts.ace.open.hand-over.abc", true},
|
|
{"node.ace", "$KV.mesh-host_acts.novox.act.hand-over-1", false},
|
|
{"node.ace", "$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.ace.ask.hand-over-1", true},
|
|
{"node.ace", "$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.novox.ask.hand-over-1", false},
|
|
{"novox.node-tools", "$KV.mesh-host_acts.novox.act.hand-over-1", false},
|
|
// The runner never carries the engine (issue 774): its health report is the node's own key alone.
|
|
{"novox.node-tools", "$KV.mesh-host_health.novox", false},
|
|
{"novox.node-tools", "$KV.mesh-host_acts.novox.ask.x", false},
|
|
{"node.novox", "$KV.mesh-host_health.novox", true},
|
|
{"node.novox", "$KV.mesh-host_health.ace", 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)
|
|
}
|
|
}
|
|
}
|