diff --git a/go/announce/announce.go b/go/announce/announce.go index 37dfd21..c4cbd2b 100644 --- a/go/announce/announce.go +++ b/go/announce/announce.go @@ -46,6 +46,10 @@ func Key(module string) (string, error) { type State struct { Name string `json:"name"` Readable bool `json:"readable,omitempty"` + // Readers, when named, are the only modules that may read the state (novox/hq ADR 0547 clause 9, issue 1945): a + // state readable by some and not all, such as the vault's bus account hashes. Only ReadableBy grants them; Readable, + // which a composer that knows no readers asks, stays false for it, so such a composer grants nobody. + Readers []string `json:"readers,omitempty"` } // States is an announcement: the module, the machine whose instance said it, and every state the module keeps, sorted @@ -67,6 +71,26 @@ func (s States) Keeps(state string) bool { return false } +// ReadableBy says whether the module announced the state readable by reader: readable, and reader among the readers +// when it names any. +func (s States) ReadableBy(state, reader string) bool { + for _, k := range s.States { + if k.Name != state { + continue + } + if len(k.Readers) > 0 { + for _, r := range k.Readers { + if r == reader { + return true + } + } + return false + } + return k.Readable + } + return false +} + // Readable says whether the module announced the state readable by others. func (s States) Readable(state string) bool { for _, k := range s.States { @@ -93,6 +117,11 @@ func (s *States) check() error { return fmt.Errorf("announce: %s announces the state %q twice", s.Module, k.Name) } seen[k.Name] = true + for _, r := range k.Readers { + if !name.MatchString(r) { + return fmt.Errorf("announce: %s names %q as a reader of %s, and a reader is a module: lower-case letters, digits and hyphens", s.Module, r, k.Name) + } + } } return nil } diff --git a/go/announce/announce_test.go b/go/announce/announce_test.go index 63bc795..7156aea 100644 --- a/go/announce/announce_test.go +++ b/go/announce/announce_test.go @@ -9,7 +9,8 @@ import ( // own event subject, in a versioned value the controller keeps by module. func TestAnAnnouncementRoundTripsSortedAndSaysWhatIsReadable(t *testing.T) { data, err := Encode(States{Module: "mesh-controller", Node: "novox", States: []State{ - {Name: "plans-building", Readable: true}, {Name: "assignments", Readable: true}, {Name: "own"}}}) + {Name: "plans-building", Readable: true}, {Name: "assignments", Readable: true}, {Name: "own"}, + {Name: "hashes", Readers: []string{"mesh-grants"}}}}) if err != nil { t.Fatal(err) } @@ -24,12 +25,17 @@ func TestAnAnnouncementRoundTripsSortedAndSaysWhatIsReadable(t *testing.T) { for _, s := range back.States { names = append(names, s.Name) } - if strings.Join(names, ",") != "assignments,own,plans-building" { + if strings.Join(names, ",") != "assignments,hashes,own,plans-building" { t.Fatalf("the states are not sorted: %v", names) } if !back.Readable("plans-building") || back.Readable("own") || back.Readable("absent") { t.Fatal("Readable does not say what was announced readable") } + // Named readers (ADR 0547 clause 9): readable by them alone, and not readable to a composer that knows no readers. + if back.Readable("hashes") || !back.ReadableBy("hashes", "mesh-grants") || back.ReadableBy("hashes", "spy") || + !back.ReadableBy("plans-building", "spy") || back.ReadableBy("own", "spy") { + t.Fatal("ReadableBy does not say who was named") + } if !back.Keeps("own") || back.Keeps("absent") { t.Fatal("Keeps does not say what was announced") } @@ -57,6 +63,7 @@ func TestAnAnnouncementRefusesWhatNoBucketCouldBe(t *testing.T) { {States{Module: "a", States: []State{{Name: "x_y"}}}, `"x_y"`}, {States{Module: "a", States: []State{{Name: ""}}}, `""`}, {States{Module: "a", States: []State{{Name: "x"}, {Name: "x", Readable: true}}}, "twice"}, + {States{Module: "a", States: []State{{Name: "x", Readers: []string{"b.c"}}}}, `"b.c"`}, } { if _, err := Encode(c.s); err == nil || !strings.Contains(err.Error(), c.says) { t.Errorf("%+v: %v, wanted a refusal saying %s", c.s, err, c.says) diff --git a/go/buscompose/announced.go b/go/buscompose/announced.go index 9145249..c03e705 100644 --- a/go/buscompose/announced.go +++ b/go/buscompose/announced.go @@ -45,10 +45,25 @@ func ControllerAnnouncement(node string) announce.States { // announced the state readable, or announced nothing yet (the transition); refused when the owner announced and did // not announce that state readable. func ReadsAnnounced(reads []string, announced map[string]announce.States) (granted, refused []string) { + return ReadsAnnouncedBy("", reads, announced) +} + +// ReadsAnnouncedBy is ReadsAnnounced for one reader module, which also honours the readers an owner named for a state +// (announce.State.Readers) and the composer's named readers (NamedReaders, ADR 0547 clause 9): a read named for other +// modules is refused. An empty reader is judged by the readable mark alone. +func ReadsAnnouncedBy(reader string, reads []string, announced map[string]announce.States) (granted, refused []string) { for _, r := range reads { owner, name, ok := strings.Cut(r, ".") s, said := announced[owner] switch { + case reader != "" && !ReaderMay(reader, r): + refused = append(refused, r) + case reader != "" && ok && said && s.Keeps(name): + if s.ReadableBy(name, reader) { + granted = append(granted, r) + } else { + refused = append(refused, r) + } case !ok || !said || s.Readable(name): granted = append(granted, r) default: diff --git a/go/buscompose/creates_streams_test.go b/go/buscompose/creates_streams_test.go index 0b77eec..0463e68 100644 --- a/go/buscompose/creates_streams_test.go +++ b/go/buscompose/creates_streams_test.go @@ -4,16 +4,24 @@ 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. +// 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"}, - Assigned: map[string][]Declared{"novox": { + 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"}}}, + {Module: GrantsModule, State: []Bucket{{Module: GrantsModule, Name: "assertions"}}, Reads: []string{VaultBusAccounts}}, + {Module: RuntimeModule}, }}, } users, err := Users(records) @@ -22,6 +30,14 @@ func TestTheGrantsPartCreatesOnlyDeclaredStreams(t *testing.T) { } 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 } @@ -48,6 +64,27 @@ func TestTheGrantsPartCreatesOnlyDeclaredStreams(t *testing.T) { {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) diff --git a/go/buscompose/nats.go b/go/buscompose/nats.go index 2ccc5d2..40be216 100644 --- a/go/buscompose/nats.go +++ b/go/buscompose/nats.go @@ -213,6 +213,9 @@ type Principal struct { // StatesItsKeyToTheVault is a machine principal whose node-engine may ask the vault's `node-key` in its own name // (issue 786, novox/hq ADR 0372): set while the vault's holder serves it. That one verb, and nothing else new. StatesItsKeyToTheVault bool + // FollowsGrantsUserList is the machine principal of the node holding mesh-broker: its node-engine reads the grants + // part's sealed user list, one key and an object by name (novox/hq ADR 0547 clause 7, GrantsUserListFollows). + FollowsGrantsUserList bool // BoundBuckets are the providers' buckets of entries, `_bound`, a provisioner client reads its own // machine's keys of (mesh-sdk go/provider, issue 679): every bucket named bound of a module the mesh places. BoundBuckets []string @@ -912,6 +915,11 @@ func PermissionsFor(p Principal) (Permissions, error) { if p.StatesItsKeyToTheVault { pub = append(pub, "mesh.seat."+VaultNodeKey.Seat+"."+CallKind+"."+VaultNodeKey.Verb+"."+CallerToken(p.Username())) } + // And, on the node holding mesh-broker, the grants part's sealed user list it follows from take-over (novox/hq ADR + // 0547 clause 7): one key, and an object by its name. + if p.FollowsGrantsUserList { + pub = append(pub, GrantsUserListFollows()...) + } case KindModule: // 0. The watchman alone hears who connects and disconnects, and asks who is connected (ADR 0431 §2). @@ -927,6 +935,8 @@ func PermissionsFor(p Principal) (Permissions, error) { // And creating each stream a manifest declares, by its own name (the operator, 2026-10-26 08:54; issue // 1790): the create and the read of that one stream, never an update, a delete or a stream not declared. pub = append(pub, CreateStreamGrants(p.CreatesStreams)...) + // And its own object bucket, where it writes the user list sealed to the bus node (ADR 0547 clause 7). + pub = append(pub, GrantsUserListWrites()...) } // The view alone reads the build seats' workers for the backlog of asks (issue 1887 A8a). if p.Module == ViewModule { diff --git a/go/buscompose/testdata/controller-user-lists.json b/go/buscompose/testdata/controller-user-lists.json index 4bfe19f..a4da9e9 100644 --- a/go/buscompose/testdata/controller-user-lists.json +++ b/go/buscompose/testdata/controller-user-lists.json @@ -46,7 +46,7 @@ }, { "Name": "seats, consumers, people and a hashless user", - "List": "# The mesh's users, composed by the controller. Do not edit: the next\n# composition overwrites it. Permissions are derived from what each module\n# declares and nothing else (novox/hq ADR 0043, design 29 §2).\n\naccounts {\n MESH {\n jetstream: enabled\n imports = [\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.CONNECT\" }, to: \"mesh.bus.connect\" }\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.DISCONNECT\" }, to: \"mesh.bus.disconnect\" }\n { service: { account: \"$SYS\", subject: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\" }, to: \"mesh.bus.connz\" }\n ]\n users = [\n { user: \"controller\", password: \"$2a$11$controllercontrollercofixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.CONTROL.controller.\u003e\", \"$JS.ACK.DEAD_LETTER_NOTICES.controller.\u003e\", \"$JS.ACK.EVENTS.controller-merges.\u003e\", \"$JS.ACK.EVENTS.controller.\u003e\", \"$JS.ACK.KV_mesh-delivery-builder_builds.mesh-controller-take-in.\u003e\", \"$JS.ACK.KV_mesh-delivery-indexer_modules.mesh-controller-take-in.\u003e\", \"$JS.API.\u003e\", \"$KV.composed-one.\u003e\", \"$KV.composed-two.\u003e\", \"$KV.mesh-controller_asked.\u003e\", \"$KV.mesh-controller_assignments.\u003e\", \"$KV.mesh-controller_builds.\u003e\", \"$KV.mesh-controller_calls.\u003e\", \"$KV.mesh-controller_composed.\u003e\", \"$KV.mesh-controller_condition-history.\u003e\", \"$KV.mesh-controller_conditions.\u003e\", \"$KV.mesh-controller_declared-config.\u003e\", \"$KV.mesh-controller_declared-packages.\u003e\", \"$KV.mesh-controller_delivery-merges.\u003e\", \"$KV.mesh-controller_grants.\u003e\", \"$KV.mesh-controller_hand-acts.\u003e\", \"$KV.mesh-controller_lease.\u003e\", \"$KV.mesh-controller_predefined.\u003e\", \"$KV.mesh-controller_rollouts.\u003e\", \"$KV.mesh-controller_states-announced.\u003e\", \"$O.declaration-files-one.\u003e\", \"$O.declaration-files-two.\u003e\", \"$SRV.INFO\", \"_INBOX.enrol.\u003e\", \"mesh.again.\u003e\", \"mesh.assignment.\u003e\", \"mesh.events.dead.\u003e\", \"mesh.mod.*.tool.\u003e\", \"mesh.mod.mesh-controller.event.states\", \"mesh.node.\u003e\", \"mesh.seat.mesh-controller.event.applied\", \"mesh.seat.mesh-controller.event.artifacts-gone\", \"mesh.seat.mesh-controller.event.assignment-changed\", \"mesh.seat.mesh-controller.event.bucket-deleted\", \"mesh.seat.mesh-controller.event.built-before\", \"mesh.seat.mesh-controller.event.checked\", \"mesh.seat.mesh-controller.event.condition-changed\", \"mesh.seat.mesh-controller.event.condition-cleared\", \"mesh.seat.mesh-controller.event.condition-raised\", \"mesh.seat.mesh-controller.event.declared-config-changed\", \"mesh.seat.mesh-controller.event.doctor-heartbeat\", \"mesh.seat.mesh-controller.event.healer-acted\", \"mesh.seat.mesh-controller.event.predefined-changed\", \"mesh.seat.mesh-controller.event.refused\", \"mesh.seat.mesh-controller.event.rolled-back\", \"mesh.seat.mesh-controller.event.secret-replaced\", \"mesh.seat.mesh-controller.tool.plans\", \"mesh.seat.mesh-controller.tool.push\", \"mesh.seat.mesh-controller.tool.upgrade\", \"mesh.seat.mesh-delivery.tool.close\", \"mesh.seat.mesh-delivery.tool.groups\", \"mesh.seat.mesh-delivery.tool.release\", \"mesh.seat.mesh-delivery.tool.stalled\", \"mesh.seat.mesh-delivery.tool.stop\", \"mesh.seat.mesh-delivery.tool.times\", \"mesh.seat.node-backup.tool.backed-up.*\", \"mesh.seat.node-backup.tool.now.*\", \"mesh.seat.node-intrusion-prevention.tool.banned.*\", \"mesh.seat.node-launcher.tool.secret.*\", \"mesh.seat.node-service-manager.tool.restart.*\", \"mesh.seat.operator-channel.tool.tell\"] }\n subscribe: { allow: [\"$JS.API.\u003e\", \"$JS.EVENT.ADVISORY.CONSUMER.DELETED.\u003e\", \"$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.mesh-controller\", \"$SRV.INFO.mesh-controller.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-controller\", \"$SRV.PING.mesh-controller.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-controller\", \"$SRV.STATS.mesh-controller.\u003e\", \"_DELIVER.controller\", \"_DELIVER.controller.\u003e\", \"_INBOX.controller.\u003e\", \"mesh.control.\u003e\", \"mesh.mod.*.event.provisioner.failing\", \"mesh.mod.*.event.provisioner.recovered\", \"mesh.mod.*.event.provisioner.retirement\", \"mesh.mod.gitea.event.pull.merged\", \"mesh.mod.gitea.event.pull.updated\", \"mesh.mod.mesh-catalog.event.catching-up\", \"mesh.mod.mesh-catalog.event.upgraded\", \"mesh.seat.mesh-build-machine.event.built\", \"mesh.seat.mesh-controller.tool.\u003e\", \"mesh.seat.node-build-agent.event.built\", \"mesh.seat.operator-channel.event.decided.mesh-controller\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.one\", password: \"$2a$11$node.onenode.onenode.ofixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.one.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-one.*.$O.declaration-files-one.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-one.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-one.*\", \"$JS.API.CONSUMER.INFO.NODES.one\", \"$JS.API.DIRECT.GET.KV_composed-one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.one.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.one.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-one.$O.declaration-files-one.M.*\", \"$JS.API.STREAM.INFO.KV_composed-one\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-one.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-one.\u003e\", \"$KV.node-engine_have.one.\u003e\", \"$SRV.PING.node-tools.one\", \"mesh.control.one.\u003e\", \"mesh.seat.node-backup.call.backed-up.one.node~one\", \"mesh.seat.node-backup.call.now.one.node~one\", \"mesh.seat.node-backup.tool.backed-up.one\", \"mesh.seat.node-backup.tool.now.one\"] }\n subscribe: { allow: [\"_DELIVER.one\", \"_DELIVER.one.\u003e\", \"_INBOX.node.one.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.one.ask.access-act\", \"mesh.node.one.ask.hand-over\", \"mesh.node.one.ask.judge-directories\", \"mesh.node.one.ask.move-secret\", \"mesh.node.one.ask.report\", \"mesh.node.one.ask.setuid-search\", \"mesh.node.one.declare\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.two\", password: \"$2a$11$node.twonode.twonode.tfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.two.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-two.*.$O.declaration-files-two.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-two.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-two.*\", \"$JS.API.CONSUMER.INFO.NODES.two\", \"$JS.API.DIRECT.GET.KV_composed-two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.two.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.two.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-two.$O.declaration-files-two.M.*\", \"$JS.API.STREAM.INFO.KV_composed-two\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-two.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-two.\u003e\", \"$KV.node-engine_have.two.\u003e\", \"$SRV.PING.node-tools.two\", \"mesh.control.two.\u003e\", \"mesh.seat.node-backup.call.backed-up.two.node~two\", \"mesh.seat.node-backup.call.now.two.node~two\", \"mesh.seat.node-backup.tool.backed-up.two\", \"mesh.seat.node-backup.tool.now.two\"] }\n subscribe: { allow: [\"_DELIVER.two\", \"_DELIVER.two.\u003e\", \"_INBOX.node.two.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.two.ask.access-act\", \"mesh.node.two.ask.hand-over\", \"mesh.node.two.ask.judge-directories\", \"mesh.node.two.ask.move-secret\", \"mesh.node.two.ask.report\", \"mesh.node.two.ask.setuid-search\", \"mesh.node.two.declare\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"one.mesh-grants\", password: \"$2a$11$one.mesh-grantsone.mesfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.one_mesh-grants.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.one_mesh-grants\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_mesh-grants\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.*.*\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.one.mesh-grants\", \"mesh.assignment.*.*\", \"mesh.mod.mesh-grants.event.membership-changed\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.mesh-grants\", \"$SRV.INFO.mesh-grants.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-grants\", \"$SRV.PING.mesh-grants.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-grants\", \"$SRV.STATS.mesh-grants.\u003e\", \"_INBOX.one.mesh-grants.\u003e\", \"mesh.assignment.one.mesh-grants\", \"mesh.mod.mesh-grants.call.\u003e\", \"mesh.mod.mesh-grants.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"one.mesh-watchman\", password: \"$2a$11$one.mesh-watchmanone.mfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.one_mesh-watchman.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.one_mesh-watchman\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_mesh-watchman\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.one.mesh-watchman\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.mesh-watchman\", \"$SRV.INFO.mesh-watchman.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-watchman\", \"$SRV.PING.mesh-watchman.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-watchman\", \"$SRV.STATS.mesh-watchman.\u003e\", \"_INBOX.one.mesh-watchman.\u003e\", \"mesh.assignment.one.mesh-watchman\", \"mesh.mod.mesh-watchman.call.\u003e\", \"mesh.mod.mesh-watchman.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"one.nats\", password: \"$2a$11$one.natsone.natsone.nafixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.API.STREAM.INFO.*\", \"$JS.API.STREAM.NAMES\", \"$JS.API.STREAM.SNAPSHOT.*\", \"$JS.SNAPSHOT.ACK.\u003e\"] }\n subscribe: { allow: [\"_INBOX.one.nats.\u003e\"] }\n } }\n { user: \"one.telegram\", password: \"$2a$11$one.telegramone.telegrfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.one_telegram.\u003e\", \"$JS.ACK.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.one_telegram\", \"$JS.API.CONSUMER.INFO.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_telegram\", \"$JS.API.CONSUMER.MSG.NEXT.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.one.telegram\", \"mesh.seat.telegram-sender.event.delivered\", \"mesh.seat.telegram-sender.event.failed\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.telegram\", \"$SRV.INFO.telegram.\u003e\", \"$SRV.PING\", \"$SRV.PING.telegram\", \"$SRV.PING.telegram.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.telegram\", \"$SRV.STATS.telegram.\u003e\", \"_INBOX.one.telegram.\u003e\", \"mesh.assignment.one.telegram\", \"mesh.mod.telegram.call.\u003e\", \"mesh.mod.telegram.tool.\u003e\", \"mesh.seat.telegram-sender.accept.send\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"person.ada\", password: \"$2a$11$person.adaperson.adapefixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.API.CONSUMER.CREATE.KV_composed-one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-controller_composed.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_composed-one.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_composed-two.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-controller_composed.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.DIRECT.GET.KV_composed-one.\u003e\", \"$JS.API.DIRECT.GET.KV_composed-two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-controller_composed.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-one.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-two.\u003e\", \"$JS.API.STREAM.INFO.KV_composed-one\", \"$JS.API.STREAM.INFO.KV_composed-two\", \"$JS.API.STREAM.INFO.KV_mesh-controller_composed\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.OBJ_declaration-files-one\", \"$JS.API.STREAM.INFO.OBJ_declaration-files-two\", \"$JS.API.STREAM.MSG.GET.OBJ_declaration-files-one\", \"$JS.API.STREAM.MSG.GET.OBJ_declaration-files-two\", \"$JS.API.STREAM.PURGE.OBJ_declaration-files-one\", \"$JS.API.STREAM.PURGE.OBJ_declaration-files-two\", \"$JS.FC.KV_composed-one.\u003e\", \"$JS.FC.KV_composed-two.\u003e\", \"$JS.FC.KV_mesh-controller_composed.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$KV.composed-one.\u003e\", \"$KV.composed-two.\u003e\", \"$KV.mesh-controller_composed.\u003e\", \"$KV.mesh-delivery-assigner_wants.want.\u003e\", \"$O.declaration-files-one.\u003e\", \"$O.declaration-files-two.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.\u003e\", \"mesh.mod.*.call.*.*.person~ada\", \"mesh.mod.*.call.*.person~ada\", \"mesh.mod.*.tool.\u003e\", \"mesh.seat.*.call.*.*.person~ada\", \"mesh.seat.*.call.*.person~ada\", \"mesh.seat.*.tool.\u003e\"], deny: [\"mesh.seat.node-launcher.tool.secret\", \"mesh.seat.node-launcher.tool.secret.*\", \"mesh.mod.*.tool.node-launcher.secret\", \"mesh.mod.*.tool.node-launcher.secret.*\", \"mesh.seat.node-launcher.call.secret.\u003e\", \"mesh.mod.*.call.node-launcher.secret.\u003e\"] }\n subscribe: { allow: [\"_INBOX.person.ada.\u003e\"] }\n } }\n { user: \"person.bob\", password: \"$2a$11$person.bobperson.bobpefixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.\u003e\", \"mesh.mod.shop.call.status.*.person~bob\", \"mesh.mod.shop.call.status.person~bob\", \"mesh.mod.shop.tool.status\", \"mesh.mod.shop.tool.status.*\"] }\n subscribe: { allow: [\"_INBOX.person.bob.\u003e\"] }\n } }\n { user: \"two.shop\", password: \"$2a$11$two.shoptwo.shoptwo.shfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.two_shop.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.two_shop\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.two_shop\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.two.shop\", \"mesh.mod.shop.event.order.placed\", \"mesh.seat.telegram-sender.accept.send\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.shop\", \"$SRV.INFO.shop.\u003e\", \"$SRV.PING\", \"$SRV.PING.shop\", \"$SRV.PING.shop.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.shop\", \"$SRV.STATS.shop.\u003e\", \"_INBOX.two.shop.\u003e\", \"mesh.assignment.two.shop\", \"mesh.mod.shop.call.\u003e\", \"mesh.mod.shop.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n ]\n }\n \"$SYS\" {\n exports = [\n { stream: \"$SYS.ACCOUNT.MESH.CONNECT\", accounts: [\"MESH\"] }\n { stream: \"$SYS.ACCOUNT.MESH.DISCONNECT\", accounts: [\"MESH\"] }\n { service: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\", accounts: [\"MESH\"] }\n ]\n }\n}\n", + "List": "# The mesh's users, composed by the controller. Do not edit: the next\n# composition overwrites it. Permissions are derived from what each module\n# declares and nothing else (novox/hq ADR 0043, design 29 §2).\n\naccounts {\n MESH {\n jetstream: enabled\n imports = [\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.CONNECT\" }, to: \"mesh.bus.connect\" }\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.DISCONNECT\" }, to: \"mesh.bus.disconnect\" }\n { service: { account: \"$SYS\", subject: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\" }, to: \"mesh.bus.connz\" }\n ]\n users = [\n { user: \"controller\", password: \"$2a$11$controllercontrollercofixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.CONTROL.controller.\u003e\", \"$JS.ACK.DEAD_LETTER_NOTICES.controller.\u003e\", \"$JS.ACK.EVENTS.controller-merges.\u003e\", \"$JS.ACK.EVENTS.controller.\u003e\", \"$JS.ACK.KV_mesh-delivery-builder_builds.mesh-controller-take-in.\u003e\", \"$JS.ACK.KV_mesh-delivery-indexer_modules.mesh-controller-take-in.\u003e\", \"$JS.API.\u003e\", \"$KV.composed-one.\u003e\", \"$KV.composed-two.\u003e\", \"$KV.mesh-controller_asked.\u003e\", \"$KV.mesh-controller_assignments.\u003e\", \"$KV.mesh-controller_builds.\u003e\", \"$KV.mesh-controller_calls.\u003e\", \"$KV.mesh-controller_composed.\u003e\", \"$KV.mesh-controller_condition-history.\u003e\", \"$KV.mesh-controller_conditions.\u003e\", \"$KV.mesh-controller_declared-config.\u003e\", \"$KV.mesh-controller_declared-packages.\u003e\", \"$KV.mesh-controller_delivery-merges.\u003e\", \"$KV.mesh-controller_grants.\u003e\", \"$KV.mesh-controller_hand-acts.\u003e\", \"$KV.mesh-controller_lease.\u003e\", \"$KV.mesh-controller_predefined.\u003e\", \"$KV.mesh-controller_rollouts.\u003e\", \"$KV.mesh-controller_states-announced.\u003e\", \"$O.declaration-files-one.\u003e\", \"$O.declaration-files-two.\u003e\", \"$SRV.INFO\", \"_INBOX.enrol.\u003e\", \"mesh.again.\u003e\", \"mesh.assignment.\u003e\", \"mesh.events.dead.\u003e\", \"mesh.mod.*.tool.\u003e\", \"mesh.mod.mesh-controller.event.states\", \"mesh.node.\u003e\", \"mesh.seat.mesh-controller.event.applied\", \"mesh.seat.mesh-controller.event.artifacts-gone\", \"mesh.seat.mesh-controller.event.assignment-changed\", \"mesh.seat.mesh-controller.event.bucket-deleted\", \"mesh.seat.mesh-controller.event.built-before\", \"mesh.seat.mesh-controller.event.checked\", \"mesh.seat.mesh-controller.event.condition-changed\", \"mesh.seat.mesh-controller.event.condition-cleared\", \"mesh.seat.mesh-controller.event.condition-raised\", \"mesh.seat.mesh-controller.event.declared-config-changed\", \"mesh.seat.mesh-controller.event.doctor-heartbeat\", \"mesh.seat.mesh-controller.event.healer-acted\", \"mesh.seat.mesh-controller.event.predefined-changed\", \"mesh.seat.mesh-controller.event.refused\", \"mesh.seat.mesh-controller.event.rolled-back\", \"mesh.seat.mesh-controller.event.secret-replaced\", \"mesh.seat.mesh-controller.tool.plans\", \"mesh.seat.mesh-controller.tool.push\", \"mesh.seat.mesh-controller.tool.upgrade\", \"mesh.seat.mesh-delivery.tool.close\", \"mesh.seat.mesh-delivery.tool.groups\", \"mesh.seat.mesh-delivery.tool.release\", \"mesh.seat.mesh-delivery.tool.stalled\", \"mesh.seat.mesh-delivery.tool.stop\", \"mesh.seat.mesh-delivery.tool.times\", \"mesh.seat.node-backup.tool.backed-up.*\", \"mesh.seat.node-backup.tool.now.*\", \"mesh.seat.node-intrusion-prevention.tool.banned.*\", \"mesh.seat.node-launcher.tool.secret.*\", \"mesh.seat.node-service-manager.tool.restart.*\", \"mesh.seat.operator-channel.tool.tell\"] }\n subscribe: { allow: [\"$JS.API.\u003e\", \"$JS.EVENT.ADVISORY.CONSUMER.DELETED.\u003e\", \"$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.mesh-controller\", \"$SRV.INFO.mesh-controller.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-controller\", \"$SRV.PING.mesh-controller.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-controller\", \"$SRV.STATS.mesh-controller.\u003e\", \"_DELIVER.controller\", \"_DELIVER.controller.\u003e\", \"_INBOX.controller.\u003e\", \"mesh.control.\u003e\", \"mesh.mod.*.event.provisioner.failing\", \"mesh.mod.*.event.provisioner.recovered\", \"mesh.mod.*.event.provisioner.retirement\", \"mesh.mod.gitea.event.pull.merged\", \"mesh.mod.gitea.event.pull.updated\", \"mesh.mod.mesh-catalog.event.catching-up\", \"mesh.mod.mesh-catalog.event.upgraded\", \"mesh.seat.mesh-build-machine.event.built\", \"mesh.seat.mesh-controller.tool.\u003e\", \"mesh.seat.node-build-agent.event.built\", \"mesh.seat.operator-channel.event.decided.mesh-controller\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.one\", password: \"$2a$11$node.onenode.onenode.ofixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_user-list.*.$KV.mesh-grants_user-list.sealed\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.one.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-one.*.$O.declaration-files-one.C.*\", \"$JS.API.CONSUMER.CREATE.OBJ_mesh-grants-user-list.*.$O.mesh-grants-user-list.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-one.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_user-list.*\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-one.*\", \"$JS.API.CONSUMER.DELETE.OBJ_mesh-grants-user-list.*\", \"$JS.API.CONSUMER.INFO.NODES.one\", \"$JS.API.DIRECT.GET.KV_composed-one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-grants_user-list.$KV.mesh-grants_user-list.sealed\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.one.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-one.$O.declaration-files-one.M.*\", \"$JS.API.DIRECT.GET.OBJ_mesh-grants-user-list.$O.mesh-grants-user-list.M.*\", \"$JS.API.STREAM.INFO.KV_composed-one\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-grants_user-list\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-one.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-grants_user-list.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-one.\u003e\", \"$JS.FC.OBJ_mesh-grants-user-list.\u003e\", \"$KV.node-engine_have.one.\u003e\", \"$SRV.PING.node-tools.one\", \"mesh.control.one.\u003e\", \"mesh.seat.node-backup.call.backed-up.one.node~one\", \"mesh.seat.node-backup.call.now.one.node~one\", \"mesh.seat.node-backup.tool.backed-up.one\", \"mesh.seat.node-backup.tool.now.one\"] }\n subscribe: { allow: [\"_DELIVER.one\", \"_DELIVER.one.\u003e\", \"_INBOX.node.one.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.one.ask.access-act\", \"mesh.node.one.ask.hand-over\", \"mesh.node.one.ask.judge-directories\", \"mesh.node.one.ask.move-secret\", \"mesh.node.one.ask.report\", \"mesh.node.one.ask.setuid-search\", \"mesh.node.one.declare\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.two\", password: \"$2a$11$node.twonode.twonode.tfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.two.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-two.*.$O.declaration-files-two.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-two.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-two.*\", \"$JS.API.CONSUMER.INFO.NODES.two\", \"$JS.API.DIRECT.GET.KV_composed-two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.two.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.two.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-two.$O.declaration-files-two.M.*\", \"$JS.API.STREAM.INFO.KV_composed-two\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-two.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-two.\u003e\", \"$KV.node-engine_have.two.\u003e\", \"$SRV.PING.node-tools.two\", \"mesh.control.two.\u003e\", \"mesh.seat.node-backup.call.backed-up.two.node~two\", \"mesh.seat.node-backup.call.now.two.node~two\", \"mesh.seat.node-backup.tool.backed-up.two\", \"mesh.seat.node-backup.tool.now.two\"] }\n subscribe: { allow: [\"_DELIVER.two\", \"_DELIVER.two.\u003e\", \"_INBOX.node.two.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.two.ask.access-act\", \"mesh.node.two.ask.hand-over\", \"mesh.node.two.ask.judge-directories\", \"mesh.node.two.ask.move-secret\", \"mesh.node.two.ask.report\", \"mesh.node.two.ask.setuid-search\", \"mesh.node.two.declare\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"one.mesh-grants\", password: \"$2a$11$one.mesh-grantsone.mesfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.one_mesh-grants.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.one_mesh-grants\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_mesh-grants\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.*.*\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.one.mesh-grants\", \"$JS.API.DIRECT.GET.OBJ_mesh-grants-user-list.\u003e\", \"$JS.API.STREAM.CREATE.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.INFO.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.MSG.GET.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.PURGE.OBJ_mesh-grants-user-list\", \"$O.mesh-grants-user-list.\u003e\", \"mesh.assignment.*.*\", \"mesh.mod.mesh-grants.event.membership-changed\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.mesh-grants\", \"$SRV.INFO.mesh-grants.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-grants\", \"$SRV.PING.mesh-grants.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-grants\", \"$SRV.STATS.mesh-grants.\u003e\", \"_INBOX.one.mesh-grants.\u003e\", \"mesh.assignment.one.mesh-grants\", \"mesh.mod.mesh-grants.call.\u003e\", \"mesh.mod.mesh-grants.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"one.mesh-watchman\", password: \"$2a$11$one.mesh-watchmanone.mfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.one_mesh-watchman.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.one_mesh-watchman\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_mesh-watchman\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.one.mesh-watchman\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.mesh-watchman\", \"$SRV.INFO.mesh-watchman.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-watchman\", \"$SRV.PING.mesh-watchman.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-watchman\", \"$SRV.STATS.mesh-watchman.\u003e\", \"_INBOX.one.mesh-watchman.\u003e\", \"mesh.assignment.one.mesh-watchman\", \"mesh.mod.mesh-watchman.call.\u003e\", \"mesh.mod.mesh-watchman.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"one.nats\", password: \"$2a$11$one.natsone.natsone.nafixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.API.STREAM.INFO.*\", \"$JS.API.STREAM.NAMES\", \"$JS.API.STREAM.SNAPSHOT.*\", \"$JS.SNAPSHOT.ACK.\u003e\"] }\n subscribe: { allow: [\"_INBOX.one.nats.\u003e\"] }\n } }\n { user: \"one.telegram\", password: \"$2a$11$one.telegramone.telegrfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.one_telegram.\u003e\", \"$JS.ACK.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.one_telegram\", \"$JS.API.CONSUMER.INFO.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_telegram\", \"$JS.API.CONSUMER.MSG.NEXT.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.one.telegram\", \"mesh.seat.telegram-sender.event.delivered\", \"mesh.seat.telegram-sender.event.failed\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.telegram\", \"$SRV.INFO.telegram.\u003e\", \"$SRV.PING\", \"$SRV.PING.telegram\", \"$SRV.PING.telegram.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.telegram\", \"$SRV.STATS.telegram.\u003e\", \"_INBOX.one.telegram.\u003e\", \"mesh.assignment.one.telegram\", \"mesh.mod.telegram.call.\u003e\", \"mesh.mod.telegram.tool.\u003e\", \"mesh.seat.telegram-sender.accept.send\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"person.ada\", password: \"$2a$11$person.adaperson.adapefixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.API.CONSUMER.CREATE.KV_composed-one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-controller_composed.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_composed-one.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_composed-two.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-controller_composed.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.DIRECT.GET.KV_composed-one.\u003e\", \"$JS.API.DIRECT.GET.KV_composed-two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-controller_composed.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-one.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-two.\u003e\", \"$JS.API.STREAM.INFO.KV_composed-one\", \"$JS.API.STREAM.INFO.KV_composed-two\", \"$JS.API.STREAM.INFO.KV_mesh-controller_composed\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.OBJ_declaration-files-one\", \"$JS.API.STREAM.INFO.OBJ_declaration-files-two\", \"$JS.API.STREAM.MSG.GET.OBJ_declaration-files-one\", \"$JS.API.STREAM.MSG.GET.OBJ_declaration-files-two\", \"$JS.API.STREAM.PURGE.OBJ_declaration-files-one\", \"$JS.API.STREAM.PURGE.OBJ_declaration-files-two\", \"$JS.FC.KV_composed-one.\u003e\", \"$JS.FC.KV_composed-two.\u003e\", \"$JS.FC.KV_mesh-controller_composed.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$KV.composed-one.\u003e\", \"$KV.composed-two.\u003e\", \"$KV.mesh-controller_composed.\u003e\", \"$KV.mesh-delivery-assigner_wants.want.\u003e\", \"$O.declaration-files-one.\u003e\", \"$O.declaration-files-two.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.\u003e\", \"mesh.mod.*.call.*.*.person~ada\", \"mesh.mod.*.call.*.person~ada\", \"mesh.mod.*.tool.\u003e\", \"mesh.seat.*.call.*.*.person~ada\", \"mesh.seat.*.call.*.person~ada\", \"mesh.seat.*.tool.\u003e\"], deny: [\"mesh.seat.node-launcher.tool.secret\", \"mesh.seat.node-launcher.tool.secret.*\", \"mesh.mod.*.tool.node-launcher.secret\", \"mesh.mod.*.tool.node-launcher.secret.*\", \"mesh.seat.node-launcher.call.secret.\u003e\", \"mesh.mod.*.call.node-launcher.secret.\u003e\"] }\n subscribe: { allow: [\"_INBOX.person.ada.\u003e\"] }\n } }\n { user: \"person.bob\", password: \"$2a$11$person.bobperson.bobpefixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.\u003e\", \"mesh.mod.shop.call.status.*.person~bob\", \"mesh.mod.shop.call.status.person~bob\", \"mesh.mod.shop.tool.status\", \"mesh.mod.shop.tool.status.*\"] }\n subscribe: { allow: [\"_INBOX.person.bob.\u003e\"] }\n } }\n { user: \"two.shop\", password: \"$2a$11$two.shoptwo.shoptwo.shfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.two_shop.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.two_shop\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.two_shop\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.two.shop\", \"mesh.mod.shop.event.order.placed\", \"mesh.seat.telegram-sender.accept.send\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.shop\", \"$SRV.INFO.shop.\u003e\", \"$SRV.PING\", \"$SRV.PING.shop\", \"$SRV.PING.shop.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.shop\", \"$SRV.STATS.shop.\u003e\", \"_INBOX.two.shop.\u003e\", \"mesh.assignment.two.shop\", \"mesh.mod.shop.call.\u003e\", \"mesh.mod.shop.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n ]\n }\n \"$SYS\" {\n exports = [\n { stream: \"$SYS.ACCOUNT.MESH.CONNECT\", accounts: [\"MESH\"] }\n { stream: \"$SYS.ACCOUNT.MESH.DISCONNECT\", accounts: [\"MESH\"] }\n { service: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\", accounts: [\"MESH\"] }\n ]\n }\n}\n", "Missing": [ "two.audit" ], @@ -146,7 +146,7 @@ }, { "Name": "state, logs, streams, queues and processes", - "List": "# The mesh's users, composed by the controller. Do not edit: the next\n# composition overwrites it. Permissions are derived from what each module\n# declares and nothing else (novox/hq ADR 0043, design 29 §2).\n\naccounts {\n MESH {\n jetstream: enabled\n imports = [\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.CONNECT\" }, to: \"mesh.bus.connect\" }\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.DISCONNECT\" }, to: \"mesh.bus.disconnect\" }\n { service: { account: \"$SYS\", subject: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\" }, to: \"mesh.bus.connz\" }\n ]\n users = [\n { user: \"ace.plex\", password: \"$2a$11$ace.plexace.plexace.plfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.ace_plex.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.ace_plex\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.ace_plex\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.ace.plex\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.plex\", \"$SRV.INFO.plex.\u003e\", \"$SRV.PING\", \"$SRV.PING.plex\", \"$SRV.PING.plex.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.plex\", \"$SRV.STATS.plex.\u003e\", \"_INBOX.ace.plex.\u003e\", \"mesh.assignment.ace.plex\", \"mesh.mod.plex.call.\u003e\", \"mesh.mod.plex.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"controller\", password: \"$2a$11$controllercontrollercofixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.CONTROL.controller.\u003e\", \"$JS.ACK.DEAD_LETTER_NOTICES.controller.\u003e\", \"$JS.ACK.EVENTS.controller-merges.\u003e\", \"$JS.ACK.EVENTS.controller.\u003e\", \"$JS.ACK.KV_mesh-delivery-builder_builds.mesh-controller-take-in.\u003e\", \"$JS.ACK.KV_mesh-delivery-indexer_modules.mesh-controller-take-in.\u003e\", \"$JS.API.\u003e\", \"$KV.composed-ace.\u003e\", \"$KV.composed-novox.\u003e\", \"$KV.mesh-controller_asked.\u003e\", \"$KV.mesh-controller_assignments.\u003e\", \"$KV.mesh-controller_builds.\u003e\", \"$KV.mesh-controller_calls.\u003e\", \"$KV.mesh-controller_composed.\u003e\", \"$KV.mesh-controller_condition-history.\u003e\", \"$KV.mesh-controller_conditions.\u003e\", \"$KV.mesh-controller_declared-config.\u003e\", \"$KV.mesh-controller_declared-packages.\u003e\", \"$KV.mesh-controller_delivery-merges.\u003e\", \"$KV.mesh-controller_grants.\u003e\", \"$KV.mesh-controller_hand-acts.\u003e\", \"$KV.mesh-controller_lease.\u003e\", \"$KV.mesh-controller_predefined.\u003e\", \"$KV.mesh-controller_rollouts.\u003e\", \"$KV.mesh-controller_states-announced.\u003e\", \"$O.declaration-files-ace.\u003e\", \"$O.declaration-files-novox.\u003e\", \"$SRV.INFO\", \"_INBOX.enrol.\u003e\", \"mesh.again.\u003e\", \"mesh.assignment.\u003e\", \"mesh.events.dead.\u003e\", \"mesh.mod.*.tool.\u003e\", \"mesh.mod.mesh-controller.event.states\", \"mesh.node.\u003e\", \"mesh.seat.mesh-controller.event.applied\", \"mesh.seat.mesh-controller.event.artifacts-gone\", \"mesh.seat.mesh-controller.event.assignment-changed\", \"mesh.seat.mesh-controller.event.bucket-deleted\", \"mesh.seat.mesh-controller.event.built-before\", \"mesh.seat.mesh-controller.event.checked\", \"mesh.seat.mesh-controller.event.condition-changed\", \"mesh.seat.mesh-controller.event.condition-cleared\", \"mesh.seat.mesh-controller.event.condition-raised\", \"mesh.seat.mesh-controller.event.declared-config-changed\", \"mesh.seat.mesh-controller.event.doctor-heartbeat\", \"mesh.seat.mesh-controller.event.healer-acted\", \"mesh.seat.mesh-controller.event.predefined-changed\", \"mesh.seat.mesh-controller.event.refused\", \"mesh.seat.mesh-controller.event.rolled-back\", \"mesh.seat.mesh-controller.event.secret-replaced\", \"mesh.seat.mesh-controller.tool.plans\", \"mesh.seat.mesh-controller.tool.push\", \"mesh.seat.mesh-controller.tool.upgrade\", \"mesh.seat.mesh-delivery.tool.close\", \"mesh.seat.mesh-delivery.tool.groups\", \"mesh.seat.mesh-delivery.tool.release\", \"mesh.seat.mesh-delivery.tool.stalled\", \"mesh.seat.mesh-delivery.tool.stop\", \"mesh.seat.mesh-delivery.tool.times\", \"mesh.seat.node-backup.tool.backed-up.*\", \"mesh.seat.node-backup.tool.now.*\", \"mesh.seat.node-intrusion-prevention.tool.banned.*\", \"mesh.seat.node-launcher.tool.secret.*\", \"mesh.seat.node-service-manager.tool.restart.*\", \"mesh.seat.operator-channel.tool.tell\"] }\n subscribe: { allow: [\"$JS.API.\u003e\", \"$JS.EVENT.ADVISORY.CONSUMER.DELETED.\u003e\", \"$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.mesh-controller\", \"$SRV.INFO.mesh-controller.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-controller\", \"$SRV.PING.mesh-controller.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-controller\", \"$SRV.STATS.mesh-controller.\u003e\", \"_DELIVER.controller\", \"_DELIVER.controller.\u003e\", \"_INBOX.controller.\u003e\", \"mesh.control.\u003e\", \"mesh.mod.*.event.provisioner.failing\", \"mesh.mod.*.event.provisioner.recovered\", \"mesh.mod.*.event.provisioner.retirement\", \"mesh.mod.gitea.event.pull.merged\", \"mesh.mod.gitea.event.pull.updated\", \"mesh.mod.mesh-catalog.event.catching-up\", \"mesh.mod.mesh-catalog.event.upgraded\", \"mesh.seat.mesh-build-machine.event.built\", \"mesh.seat.mesh-controller.tool.\u003e\", \"mesh.seat.node-build-agent.event.built\", \"mesh.seat.operator-channel.event.decided.mesh-controller\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.ace\", password: \"$2a$11$node.acenode.acenode.afixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-ace.*.$O.declaration-files-ace.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-ace.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-ace.*\", \"$JS.API.CONSUMER.INFO.NODES.ace\", \"$JS.API.DIRECT.GET.KV_composed-ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.ace.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-ace.$O.declaration-files-ace.M.*\", \"$JS.API.STREAM.INFO.KV_composed-ace\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-ace.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-ace.\u003e\", \"$KV.node-engine_have.ace.\u003e\", \"$SRV.PING.node-tools.ace\", \"mesh.control.ace.\u003e\", \"mesh.mod.plex.call.plex_status.ace.node~ace\", \"mesh.mod.plex.tool.plex_status.ace\", \"mesh.seat.node-backup.call.backed-up.ace.node~ace\", \"mesh.seat.node-backup.call.now.ace.node~ace\", \"mesh.seat.node-backup.tool.backed-up.ace\", \"mesh.seat.node-backup.tool.now.ace\"] }\n subscribe: { allow: [\"_DELIVER.ace\", \"_DELIVER.ace.\u003e\", \"_INBOX.node.ace.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.ace.ask.access-act\", \"mesh.node.ace.ask.hand-over\", \"mesh.node.ace.ask.judge-directories\", \"mesh.node.ace.ask.move-secret\", \"mesh.node.ace.ask.report\", \"mesh.node.ace.ask.setuid-search\", \"mesh.node.ace.declare\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.novox\", password: \"$2a$11$node.novoxnode.novoxnofixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-novox.*.$O.declaration-files-novox.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-novox.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-novox.*\", \"$JS.API.CONSUMER.INFO.NODES.novox\", \"$JS.API.DIRECT.GET.KV_composed-novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.novox.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-novox.$O.declaration-files-novox.M.*\", \"$JS.API.STREAM.INFO.KV_composed-novox\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-novox.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-novox.\u003e\", \"$KV.node-engine_have.novox.\u003e\", \"$SRV.PING.node-tools.novox\", \"mesh.control.novox.\u003e\", \"mesh.seat.node-backup.call.backed-up.novox.node~novox\", \"mesh.seat.node-backup.call.now.novox.node~novox\", \"mesh.seat.node-backup.tool.backed-up.novox\", \"mesh.seat.node-backup.tool.now.novox\"] }\n subscribe: { allow: [\"_DELIVER.novox\", \"_DELIVER.novox.\u003e\", \"_INBOX.node.novox.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.novox.ask.access-act\", \"mesh.node.novox.ask.hand-over\", \"mesh.node.novox.ask.judge-directories\", \"mesh.node.novox.ask.move-secret\", \"mesh.node.novox.ask.report\", \"mesh.node.novox.ask.setuid-search\", \"mesh.node.novox.declare\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"novox.mesh-delivery-builder\", password: \"$2a$11$novox.mesh-delivery-bufixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.novox_mesh-delivery-builder.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_presence.*.$KV.mesh-delivery-builder_presence.novox\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_presence.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.novox_mesh-delivery-builder\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.novox_mesh-delivery-builder\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.novox.mesh-delivery-builder\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_presence.$KV.mesh-delivery-builder_presence.novox\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.DIRECT.GET.LOG_mesh-delivery-builder_builds\", \"$JS.API.DIRECT.GET.LOG_mesh-delivery-builder_builds.\u003e\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_presence\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps\", \"$JS.API.STREAM.INFO.LOG_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_GO\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_IMAGE\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_NODE\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_GO\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_IMAGE\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_NODE\", \"$JS.FC.KV_mesh-delivery-builder_presence.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_steps.\u003e\", \"$KV.mesh-delivery-builder_presence.novox\", \"$KV.mesh-delivery-builder_steps.\u003e\", \"mesh.log.mesh-delivery-builder.builds.\u003e\", \"mesh.mod.gitea.call.gitea_commit.*.novox~mesh-delivery-builder\", \"mesh.mod.gitea.call.gitea_commit.novox~mesh-delivery-builder\", \"mesh.mod.gitea.tool.gitea_commit\", \"mesh.mod.gitea.tool.gitea_commit.*\", \"mesh.mod.mesh-delivery-builder.event.built\", \"mesh.mod.mesh-delivery-builder.event.states\", \"mesh.stream.mesh-delivery-builder.event.ask\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.mesh-delivery-builder\", \"$SRV.INFO.mesh-delivery-builder.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-delivery-builder\", \"$SRV.PING.mesh-delivery-builder.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-delivery-builder\", \"$SRV.STATS.mesh-delivery-builder.\u003e\", \"_INBOX.novox.mesh-delivery-builder.\u003e\", \"mesh.assignment.novox.mesh-delivery-builder\", \"mesh.mod.mesh-delivery-builder.call.\u003e\", \"mesh.mod.mesh-delivery-builder.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"novox.mesh-delivery-builder.worker\", password: \"$2a$11$novox.mesh-delivery-bufixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.asks.novox_mesh-delivery-builder_worker.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.INFO.asks.novox_mesh-delivery-builder_worker\", \"$JS.API.CONSUMER.MSG.NEXT.asks.novox_mesh-delivery-builder_worker\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps\", \"$JS.FC.KV_mesh-delivery-builder_steps.\u003e\", \"$KV.mesh-delivery-builder_steps.\u003e\", \"mesh.mod.gitea.call.gitea_commit.*.novox~mesh-delivery-builder~worker\", \"mesh.mod.gitea.call.gitea_commit.novox~mesh-delivery-builder~worker\", \"mesh.mod.gitea.tool.gitea_commit\", \"mesh.mod.gitea.tool.gitea_commit.*\", \"mesh.mod.mesh-delivery-builder.event.built\", \"mesh.mod.mesh-delivery-builder.event.states\"] }\n subscribe: { allow: [\"_INBOX.novox.mesh-delivery-builder.worker.\u003e\"] }\n } }\n { user: \"novox.mesh-delivery-view\", password: \"$2a$11$novox.mesh-delivery-vifixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.novox_mesh-delivery-view.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_presence.*.$KV.mesh-delivery-builder_presence.novox\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_presence.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.novox_mesh-delivery-view\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_GO.SEAT_NODE_BUILD_GO_CHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_GO.SEAT_NODE_BUILD_GO_DELIVERY_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_GO.SEAT_NODE_BUILD_GO_RECHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_IMAGE.SEAT_NODE_BUILD_IMAGE_CHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_IMAGE.SEAT_NODE_BUILD_IMAGE_DELIVERY_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_IMAGE.SEAT_NODE_BUILD_IMAGE_RECHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_NODE.SEAT_NODE_BUILD_NODE_CHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_NODE.SEAT_NODE_BUILD_NODE_DELIVERY_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_NODE.SEAT_NODE_BUILD_NODE_RECHECK_worker\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.novox_mesh-delivery-view\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.novox.mesh-delivery-view\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_presence.$KV.mesh-delivery-builder_presence.novox\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_presence\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_GO\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_IMAGE\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_NODE\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_GO\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_IMAGE\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_NODE\", \"$JS.FC.KV_mesh-delivery-builder_presence.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_steps.\u003e\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.mesh-delivery-view\", \"$SRV.INFO.mesh-delivery-view.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-delivery-view\", \"$SRV.PING.mesh-delivery-view.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-delivery-view\", \"$SRV.STATS.mesh-delivery-view.\u003e\", \"_INBOX.novox.mesh-delivery-view.\u003e\", \"mesh.assignment.novox.mesh-delivery-view\", \"mesh.mod.mesh-delivery-view.call.\u003e\", \"mesh.mod.mesh-delivery-view.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"novox.mesh-grants\", password: \"$2a$11$novox.mesh-grantsnovoxfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.novox_mesh-grants.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_assertions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_assertions.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.novox_mesh-grants\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.novox_mesh-grants\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.*.*\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.novox.mesh-grants\", \"$JS.API.DIRECT.GET.KV_mesh-grants_assertions.\u003e\", \"$JS.API.STREAM.CREATE.KV_mesh-delivery-builder_presence\", \"$JS.API.STREAM.CREATE.KV_mesh-delivery-builder_steps\", \"$JS.API.STREAM.CREATE.KV_mesh-grants_assertions\", \"$JS.API.STREAM.CREATE.LOG_mesh-delivery-builder_builds\", \"$JS.API.STREAM.CREATE.asks\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_presence\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps\", \"$JS.API.STREAM.INFO.KV_mesh-grants_assertions\", \"$JS.API.STREAM.INFO.LOG_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.asks\", \"$JS.FC.KV_mesh-grants_assertions.\u003e\", \"$KV.mesh-grants_assertions.\u003e\", \"mesh.assignment.*.*\", \"mesh.mod.mesh-grants.event.membership-changed\", \"mesh.mod.mesh-grants.event.states\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.mesh-grants\", \"$SRV.INFO.mesh-grants.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-grants\", \"$SRV.PING.mesh-grants.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-grants\", \"$SRV.STATS.mesh-grants.\u003e\", \"_INBOX.novox.mesh-grants.\u003e\", \"mesh.assignment.novox.mesh-grants\", \"mesh.mod.mesh-grants.call.\u003e\", \"mesh.mod.mesh-grants.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n ]\n }\n \"$SYS\" {\n exports = [\n { stream: \"$SYS.ACCOUNT.MESH.CONNECT\", accounts: [\"MESH\"] }\n { stream: \"$SYS.ACCOUNT.MESH.DISCONNECT\", accounts: [\"MESH\"] }\n { service: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\", accounts: [\"MESH\"] }\n ]\n }\n}\n", + "List": "# The mesh's users, composed by the controller. Do not edit: the next\n# composition overwrites it. Permissions are derived from what each module\n# declares and nothing else (novox/hq ADR 0043, design 29 §2).\n\naccounts {\n MESH {\n jetstream: enabled\n imports = [\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.CONNECT\" }, to: \"mesh.bus.connect\" }\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.DISCONNECT\" }, to: \"mesh.bus.disconnect\" }\n { service: { account: \"$SYS\", subject: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\" }, to: \"mesh.bus.connz\" }\n ]\n users = [\n { user: \"ace.plex\", password: \"$2a$11$ace.plexace.plexace.plfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.ace_plex.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.ace_plex\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.ace_plex\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.ace.plex\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.plex\", \"$SRV.INFO.plex.\u003e\", \"$SRV.PING\", \"$SRV.PING.plex\", \"$SRV.PING.plex.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.plex\", \"$SRV.STATS.plex.\u003e\", \"_INBOX.ace.plex.\u003e\", \"mesh.assignment.ace.plex\", \"mesh.mod.plex.call.\u003e\", \"mesh.mod.plex.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"controller\", password: \"$2a$11$controllercontrollercofixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.CONTROL.controller.\u003e\", \"$JS.ACK.DEAD_LETTER_NOTICES.controller.\u003e\", \"$JS.ACK.EVENTS.controller-merges.\u003e\", \"$JS.ACK.EVENTS.controller.\u003e\", \"$JS.ACK.KV_mesh-delivery-builder_builds.mesh-controller-take-in.\u003e\", \"$JS.ACK.KV_mesh-delivery-indexer_modules.mesh-controller-take-in.\u003e\", \"$JS.API.\u003e\", \"$KV.composed-ace.\u003e\", \"$KV.composed-novox.\u003e\", \"$KV.mesh-controller_asked.\u003e\", \"$KV.mesh-controller_assignments.\u003e\", \"$KV.mesh-controller_builds.\u003e\", \"$KV.mesh-controller_calls.\u003e\", \"$KV.mesh-controller_composed.\u003e\", \"$KV.mesh-controller_condition-history.\u003e\", \"$KV.mesh-controller_conditions.\u003e\", \"$KV.mesh-controller_declared-config.\u003e\", \"$KV.mesh-controller_declared-packages.\u003e\", \"$KV.mesh-controller_delivery-merges.\u003e\", \"$KV.mesh-controller_grants.\u003e\", \"$KV.mesh-controller_hand-acts.\u003e\", \"$KV.mesh-controller_lease.\u003e\", \"$KV.mesh-controller_predefined.\u003e\", \"$KV.mesh-controller_rollouts.\u003e\", \"$KV.mesh-controller_states-announced.\u003e\", \"$O.declaration-files-ace.\u003e\", \"$O.declaration-files-novox.\u003e\", \"$SRV.INFO\", \"_INBOX.enrol.\u003e\", \"mesh.again.\u003e\", \"mesh.assignment.\u003e\", \"mesh.events.dead.\u003e\", \"mesh.mod.*.tool.\u003e\", \"mesh.mod.mesh-controller.event.states\", \"mesh.node.\u003e\", \"mesh.seat.mesh-controller.event.applied\", \"mesh.seat.mesh-controller.event.artifacts-gone\", \"mesh.seat.mesh-controller.event.assignment-changed\", \"mesh.seat.mesh-controller.event.bucket-deleted\", \"mesh.seat.mesh-controller.event.built-before\", \"mesh.seat.mesh-controller.event.checked\", \"mesh.seat.mesh-controller.event.condition-changed\", \"mesh.seat.mesh-controller.event.condition-cleared\", \"mesh.seat.mesh-controller.event.condition-raised\", \"mesh.seat.mesh-controller.event.declared-config-changed\", \"mesh.seat.mesh-controller.event.doctor-heartbeat\", \"mesh.seat.mesh-controller.event.healer-acted\", \"mesh.seat.mesh-controller.event.predefined-changed\", \"mesh.seat.mesh-controller.event.refused\", \"mesh.seat.mesh-controller.event.rolled-back\", \"mesh.seat.mesh-controller.event.secret-replaced\", \"mesh.seat.mesh-controller.tool.plans\", \"mesh.seat.mesh-controller.tool.push\", \"mesh.seat.mesh-controller.tool.upgrade\", \"mesh.seat.mesh-delivery.tool.close\", \"mesh.seat.mesh-delivery.tool.groups\", \"mesh.seat.mesh-delivery.tool.release\", \"mesh.seat.mesh-delivery.tool.stalled\", \"mesh.seat.mesh-delivery.tool.stop\", \"mesh.seat.mesh-delivery.tool.times\", \"mesh.seat.node-backup.tool.backed-up.*\", \"mesh.seat.node-backup.tool.now.*\", \"mesh.seat.node-intrusion-prevention.tool.banned.*\", \"mesh.seat.node-launcher.tool.secret.*\", \"mesh.seat.node-service-manager.tool.restart.*\", \"mesh.seat.operator-channel.tool.tell\"] }\n subscribe: { allow: [\"$JS.API.\u003e\", \"$JS.EVENT.ADVISORY.CONSUMER.DELETED.\u003e\", \"$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.mesh-controller\", \"$SRV.INFO.mesh-controller.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-controller\", \"$SRV.PING.mesh-controller.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-controller\", \"$SRV.STATS.mesh-controller.\u003e\", \"_DELIVER.controller\", \"_DELIVER.controller.\u003e\", \"_INBOX.controller.\u003e\", \"mesh.control.\u003e\", \"mesh.mod.*.event.provisioner.failing\", \"mesh.mod.*.event.provisioner.recovered\", \"mesh.mod.*.event.provisioner.retirement\", \"mesh.mod.gitea.event.pull.merged\", \"mesh.mod.gitea.event.pull.updated\", \"mesh.mod.mesh-catalog.event.catching-up\", \"mesh.mod.mesh-catalog.event.upgraded\", \"mesh.seat.mesh-build-machine.event.built\", \"mesh.seat.mesh-controller.tool.\u003e\", \"mesh.seat.node-build-agent.event.built\", \"mesh.seat.operator-channel.event.decided.mesh-controller\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.ace\", password: \"$2a$11$node.acenode.acenode.afixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-ace.*.$O.declaration-files-ace.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-ace.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-ace.*\", \"$JS.API.CONSUMER.INFO.NODES.ace\", \"$JS.API.DIRECT.GET.KV_composed-ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.ace.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-ace.$O.declaration-files-ace.M.*\", \"$JS.API.STREAM.INFO.KV_composed-ace\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-ace.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-ace.\u003e\", \"$KV.node-engine_have.ace.\u003e\", \"$SRV.PING.node-tools.ace\", \"mesh.control.ace.\u003e\", \"mesh.mod.plex.call.plex_status.ace.node~ace\", \"mesh.mod.plex.tool.plex_status.ace\", \"mesh.seat.node-backup.call.backed-up.ace.node~ace\", \"mesh.seat.node-backup.call.now.ace.node~ace\", \"mesh.seat.node-backup.tool.backed-up.ace\", \"mesh.seat.node-backup.tool.now.ace\"] }\n subscribe: { allow: [\"_DELIVER.ace\", \"_DELIVER.ace.\u003e\", \"_INBOX.node.ace.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.ace.ask.access-act\", \"mesh.node.ace.ask.hand-over\", \"mesh.node.ace.ask.judge-directories\", \"mesh.node.ace.ask.move-secret\", \"mesh.node.ace.ask.report\", \"mesh.node.ace.ask.setuid-search\", \"mesh.node.ace.declare\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.novox\", password: \"$2a$11$node.novoxnode.novoxnofixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-novox.*.$O.declaration-files-novox.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-novox.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-novox.*\", \"$JS.API.CONSUMER.INFO.NODES.novox\", \"$JS.API.DIRECT.GET.KV_composed-novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.novox.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-novox.$O.declaration-files-novox.M.*\", \"$JS.API.STREAM.INFO.KV_composed-novox\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-novox.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-novox.\u003e\", \"$KV.node-engine_have.novox.\u003e\", \"$SRV.PING.node-tools.novox\", \"mesh.control.novox.\u003e\", \"mesh.seat.node-backup.call.backed-up.novox.node~novox\", \"mesh.seat.node-backup.call.now.novox.node~novox\", \"mesh.seat.node-backup.tool.backed-up.novox\", \"mesh.seat.node-backup.tool.now.novox\"] }\n subscribe: { allow: [\"_DELIVER.novox\", \"_DELIVER.novox.\u003e\", \"_INBOX.node.novox.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.novox.ask.access-act\", \"mesh.node.novox.ask.hand-over\", \"mesh.node.novox.ask.judge-directories\", \"mesh.node.novox.ask.move-secret\", \"mesh.node.novox.ask.report\", \"mesh.node.novox.ask.setuid-search\", \"mesh.node.novox.declare\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"novox.mesh-delivery-builder\", password: \"$2a$11$novox.mesh-delivery-bufixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.novox_mesh-delivery-builder.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_presence.*.$KV.mesh-delivery-builder_presence.novox\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_presence.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.novox_mesh-delivery-builder\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.novox_mesh-delivery-builder\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.novox.mesh-delivery-builder\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_presence.$KV.mesh-delivery-builder_presence.novox\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.DIRECT.GET.LOG_mesh-delivery-builder_builds\", \"$JS.API.DIRECT.GET.LOG_mesh-delivery-builder_builds.\u003e\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_presence\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps\", \"$JS.API.STREAM.INFO.LOG_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_GO\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_IMAGE\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_NODE\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_GO\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_IMAGE\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_NODE\", \"$JS.FC.KV_mesh-delivery-builder_presence.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_steps.\u003e\", \"$KV.mesh-delivery-builder_presence.novox\", \"$KV.mesh-delivery-builder_steps.\u003e\", \"mesh.log.mesh-delivery-builder.builds.\u003e\", \"mesh.mod.gitea.call.gitea_commit.*.novox~mesh-delivery-builder\", \"mesh.mod.gitea.call.gitea_commit.novox~mesh-delivery-builder\", \"mesh.mod.gitea.tool.gitea_commit\", \"mesh.mod.gitea.tool.gitea_commit.*\", \"mesh.mod.mesh-delivery-builder.event.built\", \"mesh.mod.mesh-delivery-builder.event.states\", \"mesh.stream.mesh-delivery-builder.event.ask\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.mesh-delivery-builder\", \"$SRV.INFO.mesh-delivery-builder.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-delivery-builder\", \"$SRV.PING.mesh-delivery-builder.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-delivery-builder\", \"$SRV.STATS.mesh-delivery-builder.\u003e\", \"_INBOX.novox.mesh-delivery-builder.\u003e\", \"mesh.assignment.novox.mesh-delivery-builder\", \"mesh.mod.mesh-delivery-builder.call.\u003e\", \"mesh.mod.mesh-delivery-builder.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"novox.mesh-delivery-builder.worker\", password: \"$2a$11$novox.mesh-delivery-bufixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.asks.novox_mesh-delivery-builder_worker.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.INFO.asks.novox_mesh-delivery-builder_worker\", \"$JS.API.CONSUMER.MSG.NEXT.asks.novox_mesh-delivery-builder_worker\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps\", \"$JS.FC.KV_mesh-delivery-builder_steps.\u003e\", \"$KV.mesh-delivery-builder_steps.\u003e\", \"mesh.mod.gitea.call.gitea_commit.*.novox~mesh-delivery-builder~worker\", \"mesh.mod.gitea.call.gitea_commit.novox~mesh-delivery-builder~worker\", \"mesh.mod.gitea.tool.gitea_commit\", \"mesh.mod.gitea.tool.gitea_commit.*\", \"mesh.mod.mesh-delivery-builder.event.built\", \"mesh.mod.mesh-delivery-builder.event.states\"] }\n subscribe: { allow: [\"_INBOX.novox.mesh-delivery-builder.worker.\u003e\"] }\n } }\n { user: \"novox.mesh-delivery-view\", password: \"$2a$11$novox.mesh-delivery-vifixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.novox_mesh-delivery-view.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_presence.*.$KV.mesh-delivery-builder_presence.novox\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_presence.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.novox_mesh-delivery-view\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_GO.SEAT_NODE_BUILD_GO_CHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_GO.SEAT_NODE_BUILD_GO_DELIVERY_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_GO.SEAT_NODE_BUILD_GO_RECHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_IMAGE.SEAT_NODE_BUILD_IMAGE_CHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_IMAGE.SEAT_NODE_BUILD_IMAGE_DELIVERY_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_IMAGE.SEAT_NODE_BUILD_IMAGE_RECHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_NODE.SEAT_NODE_BUILD_NODE_CHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_NODE.SEAT_NODE_BUILD_NODE_DELIVERY_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_NODE.SEAT_NODE_BUILD_NODE_RECHECK_worker\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.novox_mesh-delivery-view\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.novox.mesh-delivery-view\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_presence.$KV.mesh-delivery-builder_presence.novox\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_presence\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_GO\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_IMAGE\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_NODE\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_GO\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_IMAGE\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_NODE\", \"$JS.FC.KV_mesh-delivery-builder_presence.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_steps.\u003e\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.mesh-delivery-view\", \"$SRV.INFO.mesh-delivery-view.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-delivery-view\", \"$SRV.PING.mesh-delivery-view.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-delivery-view\", \"$SRV.STATS.mesh-delivery-view.\u003e\", \"_INBOX.novox.mesh-delivery-view.\u003e\", \"mesh.assignment.novox.mesh-delivery-view\", \"mesh.mod.mesh-delivery-view.call.\u003e\", \"mesh.mod.mesh-delivery-view.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"novox.mesh-grants\", password: \"$2a$11$novox.mesh-grantsnovoxfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.novox_mesh-grants.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_assertions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_assertions.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.novox_mesh-grants\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.novox_mesh-grants\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.*.*\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.novox.mesh-grants\", \"$JS.API.DIRECT.GET.KV_mesh-grants_assertions.\u003e\", \"$JS.API.DIRECT.GET.OBJ_mesh-grants-user-list.\u003e\", \"$JS.API.STREAM.CREATE.KV_mesh-delivery-builder_presence\", \"$JS.API.STREAM.CREATE.KV_mesh-delivery-builder_steps\", \"$JS.API.STREAM.CREATE.KV_mesh-grants_assertions\", \"$JS.API.STREAM.CREATE.LOG_mesh-delivery-builder_builds\", \"$JS.API.STREAM.CREATE.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.CREATE.asks\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_presence\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps\", \"$JS.API.STREAM.INFO.KV_mesh-grants_assertions\", \"$JS.API.STREAM.INFO.LOG_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.INFO.asks\", \"$JS.API.STREAM.MSG.GET.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.PURGE.OBJ_mesh-grants-user-list\", \"$JS.FC.KV_mesh-grants_assertions.\u003e\", \"$KV.mesh-grants_assertions.\u003e\", \"$O.mesh-grants-user-list.\u003e\", \"mesh.assignment.*.*\", \"mesh.mod.mesh-grants.event.membership-changed\", \"mesh.mod.mesh-grants.event.states\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.mesh-grants\", \"$SRV.INFO.mesh-grants.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-grants\", \"$SRV.PING.mesh-grants.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-grants\", \"$SRV.STATS.mesh-grants.\u003e\", \"_INBOX.novox.mesh-grants.\u003e\", \"mesh.assignment.novox.mesh-grants\", \"mesh.mod.mesh-grants.call.\u003e\", \"mesh.mod.mesh-grants.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n ]\n }\n \"$SYS\" {\n exports = [\n { stream: \"$SYS.ACCOUNT.MESH.CONNECT\", accounts: [\"MESH\"] }\n { stream: \"$SYS.ACCOUNT.MESH.DISCONNECT\", accounts: [\"MESH\"] }\n { service: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\", accounts: [\"MESH\"] }\n ]\n }\n}\n", "Missing": null, "Err": "", "Memberships": { diff --git a/go/buscompose/userlist_switch.go b/go/buscompose/userlist_switch.go new file mode 100644 index 0000000..a827741 --- /dev/null +++ b/go/buscompose/userlist_switch.go @@ -0,0 +1,111 @@ +package buscompose + +import ( + "slices" + "strings" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// **The grants part's user list, sealed to the bus node** (novox/hq ADR 0547 clause 7, issue 1945). The grants part writes +// the list it composed as one object of its own object bucket, sealed to the key of the node holding mesh-broker, and +// names it in one key of its own state `user-list`. The node-engine of that node follows the key when the switch says +// so (clause 8), opens the object and writes the bus's user-list file. +// +// Who may do what, and nothing more: +// +// - the grants part writes its object bucket (create, put, purge of an object's old chunks, delete) and its own state +// as before; +// - the node-engine of the node holding mesh-broker reads that one key of the state, watched, and an object of the +// bucket by its name: never the bucket's list of names, never a write, never another key of the grants part's state; +// - no other principal touches either. + +// GrantsUserListBucket, GrantsUserListKey, GrantsUserListObjects and GrantsUserListObjectStream are delivery's names, +// here where the grants are composed. +const ( + GrantsUserListBucket = delivery.GrantsUserListBucket + GrantsUserListKey = delivery.GrantsUserListKey + GrantsUserListObjects = delivery.GrantsUserListObjects + GrantsUserListObjectStream = delivery.GrantsUserListObjectStream +) + +// GrantsUserListFollows is what the node-engine of the node holding mesh-broker may publish to follow the grants part's +// list: the one key `user-list.sealed` read and watched (its own ordered consumer, filtered on that key alone), and an +// object of the bucket by its name, as a node reads its declaration's files (ADR 0486 §2: never STREAM.INFO of the object +// stream, which lists every name, and never STREAM.MSG.GET). +func GrantsUserListFollows() []string { + kv := "KV_" + GrantsUserListBucket + key := "$KV." + GrantsUserListBucket + "." + GrantsUserListKey + obj, s := GrantsUserListObjects, GrantsUserListObjectStream + return []string{ + "$JS.API.DIRECT.GET." + kv + "." + key, + "$JS.API.CONSUMER.CREATE." + kv + ".*." + key, + "$JS.API.CONSUMER.DELETE." + kv + ".*", + "$JS.API.STREAM.INFO." + kv, + "$JS.FC." + kv + ".>", + "$JS.API.DIRECT.GET." + s + ".$O." + obj + ".M.*", + "$JS.API.CONSUMER.CREATE." + s + ".*.$O." + obj + ".C.*", + "$JS.API.CONSUMER.DELETE." + s + ".*", + "$JS.FC." + s + ".>", + } +} + +// GrantsUserListWrites is what the grants part may publish to keep its object bucket: create it and read its info back, +// put an object (its chunks and its metadata, the old chunks of a name purged), read an object's metadata, and delete +// one. Its own state `user-list` it writes already, as every module writes its own. +func GrantsUserListWrites() []string { + obj, s := GrantsUserListObjects, GrantsUserListObjectStream + return []string{ + "$JS.API.STREAM.CREATE." + s, + "$JS.API.STREAM.INFO." + s, + "$JS.API.STREAM.PURGE." + s, + "$JS.API.DIRECT.GET." + s + ".>", + "$JS.API.STREAM.MSG.GET." + s, + "$O." + obj + ".>", + } +} + +// grantsOwnObjects is the grants part writing its own object bucket, and nothing of another. +func grantsOwnObjects(p Principal, pattern string) bool { + return p.Kind == KindModule && p.Module == GrantsModule && + (pattern == "$O."+GrantsUserListObjects+".>" || pattern == "$JS.API.STREAM.CREATE."+GrantsUserListObjectStream) +} + +// **Named readers** (novox/hq ADR 0547 clause 9, progressive insight of 2026-10-26; issue 1945). A state named here is +// read by the modules named and by no other, whatever another module's `reads` says: the vault's `bus-accounts` holds +// every bus account's bcrypt hash, and from take-over the grants part alone may read it. A module's announcement may name +// readers too (announce.State.Readers); both are honoured. +const VaultBusAccounts = "mesh-vault.bus-accounts" + +// NamedReaders are the states only the modules named may read, by `.`. +var NamedReaders = map[string][]string{VaultBusAccounts: {GrantsModule}} + +// ReaderMay says whether module may read the state read (`.`) by the named readers: always, for a state +// that names none. +func ReaderMay(module, read string) bool { + readers, named := NamedReaders[read] + return !named || slices.Contains(readers, module) +} + +// readsNamed splits a module's reads by the named readers: those it may read, and those named for others. +func readsNamed(module string, reads []string) (granted, refused []string) { + for _, r := range reads { + if ReaderMay(module, strings.TrimSpace(r)) { + granted = append(granted, r) + } else { + refused = append(refused, r) + } + } + return granted, refused +} + +// withNamedReaders is d with the reads named for other modules moved to its refused reads. +func withNamedReaders(d Declared) Declared { + granted, refused := readsNamed(d.Module, d.Reads) + if len(refused) == 0 { + return d + } + d.Reads = granted + d.ReadsRefused = append(append([]string(nil), d.ReadsRefused...), refused...) + return d +} diff --git a/go/buscompose/userlist_switch_integration_test.go b/go/buscompose/userlist_switch_integration_test.go new file mode 100644 index 0000000..d8eb3fd --- /dev/null +++ b/go/buscompose/userlist_switch_integration_test.go @@ -0,0 +1,213 @@ +//go:build integration + +package buscompose + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "os" + "path/filepath" + "testing" + "time" + + "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/seal" + "github.com/nats-io/nats-server/v2/server" + "github.com/nats-io/nats.go" + "github.com/nats-io/nats.go/jetstream" +) + +// Defends novox/hq ADR 0547 clause 7 on a real server (issue 1945): with the list this composer writes, the grants part +// creates its object bucket and its state, puts the sealed list and names it in `user-list.sealed`; the node-engine of +// the node holding mesh-broker watches that one key, reads the object by its name and opens it with its key, and may +// neither list the bucket, nor read another key of the state, nor write; another node reads nothing of either. +func TestTheBusNodeFollowsTheGrantsPartsSealedListUnderTheComposedGrants(t *testing.T) { + records := Records{ + Nodes: []string{"novox", "ace"}, + Assigned: map[string][]Declared{ + "novox": {{Module: "nats", SnapshotsTheBus: true}, + {Module: GrantsModule, State: []Bucket{{Module: GrantsModule, Name: "user-list"}}}}, + "ace": {{Module: "plex"}}, + }, + } + users, err := Users(records) + if err != nil { + t.Fatal(err) + } + for i := range users { + users[i].PasswordHash = "pw-" + users[i].Username() + } + accounts, err := ComposeAccounts(users) + if err != nil { + t.Fatal(err) + } + dir := t.TempDir() + conf := filepath.Join(dir, "server.conf") + if err := os.WriteFile(conf, []byte("listen: 127.0.0.1:-1\njetstream { store_dir: \""+dir+"\" }\n"+accounts), 0o600); err != nil { + t.Fatal(err) + } + opts, err := server.ProcessConfigFile(conf) + if err != nil { + t.Fatalf("the composed list is not the server's: %v", err) + } + opts.NoLog, opts.NoSigs = true, true + s, err := server.NewServer(opts) + if err != nil { + t.Fatal(err) + } + go s.Start() + if !s.ReadyForConnections(30 * time.Second) { + t.Fatal("the bus did not come up") + } + t.Cleanup(s.Shutdown) + dial := func(user string) *nats.Conn { + nc, err := nats.Connect(s.ClientURL(), nats.UserInfo(user, "pw-"+user), nats.CustomInboxPrefix("_INBOX."+user)) + if err != nil { + t.Fatalf("%s: %v", user, err) + } + t.Cleanup(nc.Close) + return nc + } + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // The grants part: its state and its object bucket, the sealed list put and named. + grants, err := jetstream.New(dial("novox." + GrantsModule)) + if err != nil { + t.Fatal(err) + } + // Its state is made as the grants part makes a declared state, by its stream's name (CreateKeyValue would ask the + // account's info, which no module is granted). + if _, err := grants.CreateStream(ctx, jetstream.StreamConfig{Name: "KV_" + GrantsUserListBucket, + Subjects: []string{"$KV." + GrantsUserListBucket + ".>"}, MaxMsgsPerSubject: 1, AllowDirect: true, + AllowRollup: true, DenyDelete: true, Discard: jetstream.DiscardNew}); err != nil { + t.Fatalf("the grants part's state: %v", err) + } + kv, err := grants.KeyValue(ctx, GrantsUserListBucket) + if err != nil { + t.Fatalf("the grants part binds its state: %v", err) + } + objects, err := grants.CreateObjectStore(ctx, jetstream.ObjectStoreConfig{Bucket: GrantsUserListObjects}) + if err != nil { + t.Fatalf("the grants part's object bucket: %v", err) + } + node, err := seal.GenerateKeyPair() + if err != nil { + t.Fatal(err) + } + list := []byte(accounts) + digest := delivery.UserListDigest(list) + object, err := delivery.SealUserList(list, node.PublicKey) + if err != nil { + t.Fatal(err) + } + name := delivery.UserListObjectName(digest) + for range 2 { // put twice: a second put of a name purges its old chunks + if _, err := objects.PutBytes(ctx, name, object); err != nil { + t.Fatalf("the grants part put its object: %v", err) + } + } + value, err := delivery.EncodeGrantsUserList(delivery.GrantsUserList{Follow: delivery.FollowGrants, Digest: digest, + Object: name, SealedTo: delivery.SealingKeyID(node.PublicKey), Node: "novox", Revision: 1, Users: len(users)}) + if err != nil { + t.Fatal(err) + } + if _, err := kv.Put(ctx, GrantsUserListKey, value); err != nil { + t.Fatal(err) + } + if _, err := kv.Put(ctx, "list", []byte(`{"v":1}`)); err != nil { + t.Fatal(err) + } + + // The bus node's engine: the key watched, the object by name (as mesh-host's link reads a declaration's file). + busNode := dial("node.novox") + js, err := busNode.JetStream(nats.MaxWait(5 * time.Second)) + if err != nil { + t.Fatal(err) + } + keys, err := js.KeyValue(GrantsUserListBucket) + if err != nil { + t.Fatalf("the bus node binds the grants part's state: %v", err) + } + w, err := keys.Watch(GrantsUserListKey) + if err != nil { + t.Fatalf("the bus node watches the key: %v", err) + } + defer w.Stop() + var got delivery.GrantsUserList + select { + case e := <-w.Updates(): + if e == nil { + t.Fatal("the watch found no value") + } + if got, err = delivery.DecodeGrantsUserList(e.Value()); err != nil { + t.Fatal(err) + } + case <-ctx.Done(): + t.Fatal("the watch said nothing") + } + read := func(js nats.JetStreamContext, name string) ([]byte, error) { + meta, err := js.GetLastMsg(GrantsUserListObjectStream, "$O."+GrantsUserListObjects+".M."+ + base64.URLEncoding.EncodeToString([]byte(name)), nats.DirectGet()) + if err != nil { + return nil, err + } + var info struct { + NUID string `json:"nuid"` + Size int `json:"size"` + } + if err := json.Unmarshal(meta.Data, &info); err != nil { + return nil, err + } + sub, err := js.SubscribeSync("$O."+GrantsUserListObjects+".C."+info.NUID, nats.OrderedConsumer(), + nats.BindStream(GrantsUserListObjectStream)) + if err != nil { + return nil, err + } + defer sub.Unsubscribe() + var body []byte + for len(body) < info.Size { + m, err := sub.NextMsg(5 * time.Second) + if err != nil { + return nil, err + } + body = append(body, m.Data...) + } + return body, nil + } + body, err := read(js, got.Object) + if err != nil { + t.Fatalf("the bus node reads the object by its name: %v", err) + } + opened, err := delivery.OpenUserList(body, got.Digest, func(b seal.Box) ([]byte, error) { + s, err := seal.Open(b, node.PrivateKey) + return []byte(s), err + }) + if err != nil || !bytes.Equal(opened, list) { + t.Fatalf("the bus node opened %d bytes, %v", len(opened), err) + } + + // And nothing beyond: no other key, no list of the bucket, no write. + if _, err := keys.Get("list"); err == nil { + t.Error("the bus node read another key of the grants part's state") + } + if _, err := js.StreamInfo(GrantsUserListObjectStream); err == nil { + t.Error("the bus node read the object stream's info, which lists every name") + } + if _, err := js.Publish("$O."+GrantsUserListObjects+".M.x", []byte("x")); err == nil { + t.Error("the bus node wrote the grants part's bucket") + } + // Another node reads nothing of it. + other, err := dial("node.ace").JetStream(nats.MaxWait(2 * time.Second)) + if err != nil { + t.Fatal(err) + } + if _, err := read(other, got.Object); err == nil { + t.Error("another node read the object") + } + if _, err := other.GetLastMsg("KV_"+GrantsUserListBucket, "$KV."+GrantsUserListBucket+"."+GrantsUserListKey, nats.DirectGet()); err == nil { + t.Errorf("another node read the key: %v", err) + } +} diff --git a/go/buscompose/users.go b/go/buscompose/users.go index e8b2747..472a0c4 100644 --- a/go/buscompose/users.go +++ b/go/buscompose/users.go @@ -147,14 +147,19 @@ func Users(r Records) ([]Principal, error) { for _, node := range sortedCopy(r.Nodes) { witness := false var checks []string + bus := false for _, d := range r.Assigned[node] { if d.Module == controllerModule { witness = true } + if d.SnapshotsTheBus { + bus = true + } checks = append(checks, d.Checks...) } out = append(out, Principal{Kind: KindNode, Node: node, WitnessesController: witness, Checks: checks, - MovesIntoTheVault: servesMoveIn(r), StatesItsKeyToTheVault: servesVerb(r, VaultNodeKey)}) + MovesIntoTheVault: servesMoveIn(r), StatesItsKeyToTheVault: servesVerb(r, VaultNodeKey), + FollowsGrantsUserList: bus}) // **And the machine's provisioner client, on an account of its own** (novox/hq ADR 0349 clause 11): one // per machine that runs it, delivered to the module running it. for _, d := range r.Assigned[node] { @@ -176,6 +181,9 @@ func Users(r Records) ([]Principal, error) { } } for _, d := range r.Assigned[node] { + // A state with named readers is read by them alone (ADR 0547 clause 9): a read named for another module is + // granted nothing, here and in the runtime carrying the module. + d = withNamedReaders(d) // **A process account is its own, delivered in its own secret** (broker-, novox/hq // ADR 0436 §1, §2), never the module's `broker` — so it is composed whether or not the module // itself has an account, and is never carried by the machine's runtime, which runs as the @@ -223,6 +231,7 @@ func Users(r Records) ([]Principal, error) { var carried []Declared for _, d := range r.Assigned[node] { if d.RunsAs == "" && runnerServes(d) { + d = withNamedReaders(d) // Never a restricted stream for what the tool runner carries (novox/hq ADR 0376 §1). d.Hears = ComposedHears(d) carried = append(carried, d) diff --git a/go/buscompose/writers.go b/go/buscompose/writers.go index 4978be4..fff55d1 100644 --- a/go/buscompose/writers.go +++ b/go/buscompose/writers.go @@ -44,6 +44,9 @@ func createsDeclared(p Principal, pattern string) bool { return true } name, ok := strings.CutPrefix(pattern, "$JS.API.STREAM.CREATE.") + if ok && p.Kind == KindModule && p.Module == GrantsModule && name == GrantsUserListObjectStream { + return true + } return ok && p.Kind == KindModule && p.Module == GrantsModule && slices.Contains(p.CreatesStreams, name) && safeSubject.MatchString(name) } @@ -249,7 +252,10 @@ var WritersTable = []WriterRow{ KeptIn: "the object bucket " + DeclarationFilesBucket + "- of each node, an object removed a day after the " + "node's latest declaration no longer names it", Others: "a node-engine reads an object of its own bucket by its name and may neither list the bucket nor write it", - Subjects: []string{"$O.*.>"}, Writes: isControllerOrTheHandPath}, + Subjects: []string{"$O.*.>"}, Writes: func(p Principal, pattern string) bool { + return isControllerOrTheHandPath(p, pattern) || grantsOwnObjects(p, pattern) + }, + Shared: "the grants part writes its own object bucket " + GrantsUserListObjects + ", the user list sealed to the bus node, and no other (novox/hq ADR 0547 clause 7)"}, {State: "the declared-config mirror", Writer: "controller (lease holder), from its store, only on a change", KeptIn: "key-value " + DeclaredConfigBucket, Others: "read through `reads` as mesh-controller." + MirrorDeclaredConfig, Subjects: kvOf(DeclaredConfigBucket), Writes: isController}, diff --git a/go/delivery/delivery.go b/go/delivery/delivery.go index 8fccfaa..5eecc31 100644 --- a/go/delivery/delivery.go +++ b/go/delivery/delivery.go @@ -34,9 +34,12 @@ // **The release 4 layouts** (issues 1356 to 1359; ADR 0453 clauses 4, 6 and 16): the new fingerprint form (fingerprint2.go), // the indexer's manifests by digest and the new fields of its modules and candidates (manifests.go, source.go), the // builder's build keys (builder.go) and the checker's verdicts and kept verdicts (checker.go). A manifests value is the -// one exception to the size above: a manifest is up to about 21 KB, whole, byte for byte. +// one exception to the size above: a manifest is up to about 21 KB, whole, byte for byte. The bus module's have on the +// node holding mesh-broker is the other: its user list's handing carries a sealed box of every loaded user's hash, some +// kilobytes (handed.go). // -// Standard library only, and this module's own asks and declaration packages (a composition holds declaration resources). +// Standard library only, and this module's own asks, declaration and seal packages (a composition holds declaration +// resources; the adoption handing is a sealed box, handed.go). package delivery import ( @@ -352,6 +355,18 @@ type UserListWritten struct { Users int `json:"users"` At time.Time `json:"at"` Error string `json:"error,omitempty"` + // Follows is which list the node follows now (FollowController or FollowGrants, novox/hq ADR 0547 clause 8), empty + // from a node-engine before the switch. + Follows string `json:"follows,omitempty"` + // Key is the bus node's sealing key, its public half as SPKI PEM: the key the grants part seals its list to + // (clause 7). Public, as the vault holds it. + Key string `json:"key,omitempty"` + // Unwatched says why the bus node does not watch the grants part's key now (its watch refused or ended, and tried + // again), empty while it does or where it need not (ADR 0547 clause 8): it then keeps the last list it followed. + Unwatched string `json:"unwatched,omitempty"` + // Handed is the hashes of the users of the list on the machine, sealed to the vault, for its adoption of the + // accounts it holds no value for (ADR 0547 clause 9, handed.go); nil while the vault's key is not known. + Handed *AccountsHanded `json:"handed,omitempty"` } // HaveNode is a node's own facts, written under HaveNodeKey. Each field is the node-engine's own JSON, carried whole: @@ -502,7 +517,8 @@ func SameHave(a, b Have) bool { } x, y := *ua, *ub x.At, y.At = time.Time{}, time.Time{} - return x == y && ua.At.Equal(ub.At) + x.Handed, y.Handed = nil, nil + return x == y && ua.At.Equal(ub.At) && sameHanded(ua.Handed, ub.Handed) } // SameHaveNode says whether two node facts are the same value, comparing each carried JSON without its spacing. diff --git a/go/delivery/delivery_test.go b/go/delivery/delivery_test.go index a0aba8d..564e6ad 100644 --- a/go/delivery/delivery_test.go +++ b/go/delivery/delivery_test.go @@ -1,12 +1,15 @@ package delivery import ( + "bytes" "encoding/json" "errors" "reflect" "strings" "testing" "time" + + "git.novox.be/novox/mesh-sdk/go/seal" ) // 1131/AC1: the have key forms and parses back; bad names are refused, naming them. @@ -167,6 +170,108 @@ func TestRoundTrip(t *testing.T) { if !reflect.DeepEqual(out, in) || !out.UserList.At.Equal(at) { t.Fatalf("got %+v want %+v", out, in) } + // And with the hashes handed to the vault (ADR 0547 clause 9): sealed to its key, opened by it alone, every + // listed user with a bcrypt hash once, and refused to another key. + hash := "$2a$11$" + strings.Repeat("a", 53) + list := []byte("{ user: \"controller\", password: \"" + hash + "\", permissions: {\n" + + "{ user: \"node.ace\", password: \"" + hash + "\", permissions: {\n" + + "{ user: \"controller\", password: \"" + hash + "\" }\n{ user: \"nkey\", password: \"plain\" }\n") + accounts := HandedAccountsOf(list) + if len(accounts) != 2 || accounts[0].User != "controller" || accounts[1].User != "node.ace" || accounts[1].Hash != hash { + t.Fatalf("handed accounts: %+v", accounts) + } + vault, err := seal.GenerateKeyPair() + if err != nil { + t.Fatal(err) + } + other, _ := seal.GenerateKeyPair() + handed, err := HandAccounts(accounts, vault.PublicKey, "sha256:ab", at) + if err != nil { + t.Fatal(err) + } + in.UserList.Handed = &handed + b, err = EncodeHave(in) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(b), "node.ace") || strings.Contains(string(b), hash) { + t.Fatalf("a user or a hash is in the clear: %s", b) + } + out, err = DecodeHave(b) + if err != nil || !SameHave(out, in) { + t.Fatalf("handed round trip: %v %+v", err, out.UserList) + } + opened, err := OpenHanded(*out.UserList.Handed, vault.PublicKey, vault.PrivateKey) + if err != nil || !reflect.DeepEqual(opened, accounts) { + t.Fatalf("opened %+v, %v", opened, err) + } + if _, err := OpenHanded(*out.UserList.Handed, other.PublicKey, other.PrivateKey); err == nil { + t.Fatal("a handing opened for another key") + } + if _, err := HandAccounts(accounts, "", "sha256:ab", at); err == nil { + t.Fatal("handed with no vault key") + } + }) + // The grants part's list sealed to the bus node (novox/hq ADR 0547 clause 7): the key's value round-trips and refuses + // what the bus node could not follow, and the object opens with the node's key alone, to the digest the key names. + t.Run("the grants part's sealed user list", func(t *testing.T) { + in := Have{FP: "f1", Phase: PhaseRunning, Health: HealthOK, UserList: &UserListWritten{Source: GrantsUserListSource, + Revision: 3, Wanted: "sha256:ab", Digest: "sha256:ab", Users: 2, Follows: FollowGrants, Key: "PEM"}} + b, err := EncodeHave(in) + if err != nil { + t.Fatal(err) + } + if out, err := DecodeHave(b); err != nil || out.UserList.Follows != FollowGrants || out.UserList.Key != "PEM" { + t.Fatalf("got %+v, %v", out.UserList, err) + } + list := []byte("accounts { MESH { users = [ { user: \"controller\" } ] } }\n") + digest := UserListDigest(list) + node, err := seal.GenerateKeyPair() + if err != nil { + t.Fatal(err) + } + other, _ := seal.GenerateKeyPair() + g := GrantsUserList{Follow: FollowGrants, Digest: digest, Object: UserListObjectName(digest), + SealedTo: SealingKeyID(node.PublicKey), Node: "novox", Revision: 1, Users: 1} + raw, err := EncodeGrantsUserList(g) + if err != nil { + t.Fatal(err) + } + back, err := DecodeGrantsUserList(raw) + if err != nil || back.Digest != digest || back.LeftOut == nil || back.Follow != FollowGrants { + t.Fatalf("decoded %+v, %v", back, err) + } + for _, bad := range []GrantsUserList{{Follow: "both", Digest: digest, Object: g.Object}, + {Follow: FollowGrants, Digest: digest, Object: "other"}, {Follow: FollowGrants, Digest: "md5:x", Object: "md5:x"}} { + if _, err := EncodeGrantsUserList(bad); err == nil { + t.Errorf("%+v was written", bad) + } + } + if _, err := EncodeGrantsUserList(GrantsUserList{Follow: FollowController}); err != nil { + t.Errorf("a switch with no list yet refused: %v", err) + } + object, err := SealUserList(list, node.PublicKey) + if err != nil { + t.Fatal(err) + } + if bytes.Contains(object, []byte("controller")) { + t.Fatal("the object carries the list in the clear") + } + opener := func(priv string) func(seal.Box) ([]byte, error) { + return func(b seal.Box) ([]byte, error) { s, err := seal.Open(b, priv); return []byte(s), err } + } + if got, err := OpenUserList(object, digest, opener(node.PrivateKey)); err != nil || !bytes.Equal(got, list) { + t.Fatalf("opened %q, %v", got, err) + } + if _, err := OpenUserList(object, digest, opener(other.PrivateKey)); err == nil { + t.Error("another key opened the list") + } + if _, err := OpenUserList(object, "sha256:00", opener(node.PrivateKey)); err == nil { + t.Error("a list of another digest was taken") + } + if _, err := SealUserList(list, "not a key"); err == nil { + t.Error("a list was sealed to no key") + } }) t.Run("have node", func(t *testing.T) { in := HaveNode{Windows: json.RawMessage(`{"a":1}`), Busy: json.RawMessage(`true`)} @@ -306,6 +411,20 @@ func TestSame(t *testing.T) { if SameHave(ua, ub) { t.Error("different user lists same") } + // A handing is compared by what it says: one said and one not differ, as do two to other keys. + ub.UserList.Revision = 1 + ua.UserList.Handed = &AccountsHanded{From: "sha256:a", To: "sha256:k", Users: 2, At: at} + if SameHave(ua, ub) { + t.Error("a handing said and one not are the same") + } + ub.UserList.Handed = &AccountsHanded{From: "sha256:a", To: "sha256:k", Users: 2, At: at.In(time.Local)} + if !SameHave(ua, ub) { + t.Error("equal handings differ") + } + ub.UserList.Handed.To = "sha256:j" + if SameHave(ua, ub) { + t.Error("handings to other keys same") + } n := HaveNode{V: 1, Busy: json.RawMessage(`true`)} if !SameHaveNode(n, HaveNode{V: 1, Busy: json.RawMessage(`true`)}) || SameHaveNode(n, HaveNode{V: 1}) { t.Error("have node") diff --git a/go/delivery/handed.go b/go/delivery/handed.go new file mode 100644 index 0000000..af0bc3c --- /dev/null +++ b/go/delivery/handed.go @@ -0,0 +1,125 @@ +package delivery + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "regexp" + "strings" + "time" + + "git.novox.be/novox/mesh-sdk/go/seal" +) + +// The adoption handing of novox/hq ADR 0547 clause 9 (issue 1945): the vault adopts, once and before `take-over`, the +// bcrypt hashes of the bus accounts it holds no value for (the controller's own, the node accounts, the enrolled +// devices) from the list the bus has loaded. The node-engine of the node holding mesh-broker is the one part that has +// that list, so it hands the hashes in the bus module's have, beside what it says of the list it wrote: sealed to the +// vault's own key, which it learns from the vault's answer to `node-key`, so that of every reader of the have bucket +// the vault alone can open them. The have says in the clear only how many users it hands, from which list and to which +// key; the users' names stay sealed, as the list itself is (ADR 0547 clause 7: it names every account). +// +// It hands every user of the loaded list; the vault takes from it only the users it holds no value for, and never +// replaces a hash it made from a value it holds, so the node-engine never needs to read the vault's state (which the +// grants part alone is to read, clause 9). + +// AccountsHanded is the hashes of the loaded list's users, sealed to the vault, in UserListWritten. +type AccountsHanded struct { + // From is the digest of the list on the machine the hashes are taken from, `sha256:` (UserListWritten.Digest). + From string `json:"from"` + // To is the fingerprint of the vault's key the box is sealed to (KeyFingerprint). + To string `json:"to"` + // Users is how many users the box holds. + Users int `json:"users"` + // Box is the JSON of []HandedAccount, sealed to the vault's key (ADR 0206). + Box seal.Box `json:"box"` + At time.Time `json:"at"` +} + +// HandedAccount is one user of an AccountsHanded box, opened: the bus user and the bcrypt hash the loaded list +// carries for it. +type HandedAccount struct { + User string `json:"user"` + Hash string `json:"hash"` +} + +// KeyFingerprint is a public key's fingerprint as a handing names it: `sha256:` of its PEM, without the space +// around it. +func KeyFingerprint(publicPEM string) string { + sum := sha256.Sum256([]byte(strings.TrimSpace(publicPEM))) + return "sha256:" + hex.EncodeToString(sum[:]) +} + +// listedAccount is one user of a bus user list as the composer writes it (buscompose), with its password's hash. +var listedAccount = regexp.MustCompile(`\{\s*user:\s*"([^"]+)",\s*password:\s*"([^"]+)"`) + +// HandedAccountsOf is every user of a bus user list that carries a bcrypt hash, in the list's order. A user listed +// twice is handed once, as the list first names it. +func HandedAccountsOf(list []byte) []HandedAccount { + var out []HandedAccount + seen := map[string]bool{} + for _, m := range listedAccount.FindAllSubmatch(list, -1) { + user, hash := string(m[1]), string(m[2]) + if seen[user] || !IsBcrypt(hash) { + continue + } + seen[user] = true + out = append(out, HandedAccount{User: user, Hash: hash}) + } + return out +} + +// bcryptHash is the form of a bcrypt hash: version, cost, and 53 characters of salt and digest. +var bcryptHash = regexp.MustCompile(`^\$2[abxy]?\$[0-9]{2}\$[./A-Za-z0-9]{53}$`) + +// IsBcrypt says whether s has the form of a bcrypt hash. +func IsBcrypt(s string) bool { return bcryptHash.MatchString(s) } + +// HandAccounts seals the accounts to the vault's public key (SPKI PEM) as the handing of the list whose digest is from. +func HandAccounts(accounts []HandedAccount, vaultPEM, from string, at time.Time) (AccountsHanded, error) { + if strings.TrimSpace(vaultPEM) == "" { + return AccountsHanded{}, errors.New("delivery: no vault key to seal the handed accounts to") + } + body, err := json.Marshal(accounts) + if err != nil { + return AccountsHanded{}, err + } + box, err := seal.Seal(string(body), vaultPEM) + if err != nil { + return AccountsHanded{}, fmt.Errorf("delivery: the handed accounts could not be sealed to the vault's key: %w", err) + } + return AccountsHanded{From: from, To: KeyFingerprint(vaultPEM), Users: len(accounts), Box: box, At: at.UTC()}, nil +} + +// OpenHanded opens a handing with the vault's private key (PEM), refusing one sealed to another key than publicPEM's +// and a box that does not hold as many users as it says. +func OpenHanded(h AccountsHanded, publicPEM, privatePEM string) ([]HandedAccount, error) { + if h.To != KeyFingerprint(publicPEM) { + return nil, fmt.Errorf("delivery: the handing is sealed to the key %s, not to this one (%s)", h.To, + KeyFingerprint(publicPEM)) + } + plain, err := seal.Open(h.Box, privatePEM) + if err != nil { + return nil, fmt.Errorf("delivery: the handing does not open: %w", err) + } + var out []HandedAccount + if err := json.Unmarshal([]byte(plain), &out); err != nil { + return nil, fmt.Errorf("delivery: the handing holds no accounts: %w", err) + } + if len(out) != h.Users { + return nil, fmt.Errorf("delivery: the handing says %d users and holds %d", h.Users, len(out)) + } + return out, nil +} + +// sameHanded says whether two handings say the same: the same list, key, count and box, whatever zone their time is in. +func sameHanded(a, b *AccountsHanded) bool { + if a == nil || b == nil { + return a == b + } + x, y := *a, *b + x.At, y.At = time.Time{}, time.Time{} + return x == y && a.At.Equal(b.At) +} diff --git a/go/delivery/userlist.go b/go/delivery/userlist.go new file mode 100644 index 0000000..0860350 --- /dev/null +++ b/go/delivery/userlist.go @@ -0,0 +1,149 @@ +package delivery + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "strings" + "time" + + "git.novox.be/novox/mesh-sdk/go/seal" +) + +// **The grants part's user list, sealed to the bus node** (novox/hq ADR 0547 clauses 7 and 8, issue 1945): the contract +// between the grants part (mesh-delivery modules/mesh-grants), which writes it, and the node-engine of the node holding +// mesh-broker (mesh-host), which follows it once the operator's take-over says so. +// +// - The grants part composes the bus's user list (the accounts block, buscompose.ComposeAccounts, with the vault's +// hashes), seals it to the bus node's sealing key (seal.Seal, the key the node says in its have of the bus module, +// UserListWritten.Key), and puts the box as one object of its object bucket GrantsUserListObjects, named by the +// list's digest (UserListObjectName). +// - It writes the key GrantsUserListKey of its state `user-list` (bucket GrantsUserListBucket): the GrantsUserList +// below, which names the object, the list's digest, its revision, its user count and the users left out, and which +// source the bus node follows (Follow: FollowGrants from take-over, FollowController in shadow and after hand-back). +// - The node-engine of the bus node watches that key alone. While it says FollowGrants it opens the object, checks +// the digest, writes the bus's user-list file (mode 0600) only when the file's digest differs, and says what it +// loaded in its have of the bus module (UserListWritten, Source GrantsUserListSource); otherwise it follows the +// controller's compositions as before (clause 8). Never both. + +// The names of the grants part's user list on the bus. +const ( + // GrantsUserListBucket is the grants part's state `user-list`. + GrantsUserListBucket = "mesh-grants_user-list" + // GrantsUserListKey is the one key of it the bus node reads. + GrantsUserListKey = "sealed" + // GrantsUserListObjects is the grants part's object bucket, where each sealed list is one object. + GrantsUserListObjects = "mesh-grants-user-list" + // GrantsUserListObjectStream is the bucket's stream. + GrantsUserListObjectStream = "OBJ_" + GrantsUserListObjects + // GrantsUserListSource is the source a have names for a list taken from the grants part. + GrantsUserListSource = GrantsUserListBucket + "." + GrantsUserListKey +) + +// Which list the bus node follows. +const ( + FollowController = "controller" + FollowGrants = "grants" +) + +// GrantsUserList is the value of the grants part's key `user-list.sealed`. +type GrantsUserList struct { + V int `json:"v"` + // Follow is which list the bus node follows: FollowGrants from take-over, FollowController otherwise. + Follow string `json:"follow"` + // Digest is the list's digest, `sha256:` of the file's bytes as the bus loads them. + Digest string `json:"digest"` + // Object is the object's name in GrantsUserListObjects (UserListObjectName of Digest). + Object string `json:"object"` + // SealedTo is the fingerprint of the key the object is sealed to (SealingKeyID), and Node the node it holds. + SealedTo string `json:"sealed_to"` + Node string `json:"node"` + // Revision counts the lists the grants part wrote: one more for each new digest. + Revision uint64 `json:"revision"` + Users int `json:"users"` + // LeftOut are the users the list leaves out for want of a hash in the vault. + LeftOut []string `json:"left_out"` + At time.Time `json:"at"` +} + +// EncodeGrantsUserList is g as JSON at Version; it refuses a value the bus node could not follow. +func EncodeGrantsUserList(g GrantsUserList) ([]byte, error) { + g.V = Version + if g.LeftOut == nil { + g.LeftOut = []string{} + } + if err := g.check(); err != nil { + return nil, err + } + return json.Marshal(g) +} + +// DecodeGrantsUserList reads the key's value; it refuses another version and what Encode refuses. +func DecodeGrantsUserList(data []byte) (GrantsUserList, error) { + var g GrantsUserList + if err := decode(data, &g); err != nil { + return GrantsUserList{}, err + } + if err := g.check(); err != nil { + return GrantsUserList{}, err + } + return g, nil +} + +func (g GrantsUserList) check() error { + if g.Follow != FollowGrants && g.Follow != FollowController { + return fmt.Errorf("delivery: the user list's follow %q is not %s or %s", g.Follow, FollowGrants, FollowController) + } + if g.Digest == "" { + return nil // a switch with no list written yet: the bus node keeps what it follows + } + if !strings.HasPrefix(g.Digest, "sha256:") || g.Object != UserListObjectName(g.Digest) { + return fmt.Errorf("delivery: the user list names object %q for digest %q; the object is the digest's hex", g.Object, g.Digest) + } + return nil +} + +// UserListDigest is a list's digest as the have and the key say it, `sha256:` of its bytes. +func UserListDigest(content []byte) string { + sum := sha256.Sum256(content) + return "sha256:" + hex.EncodeToString(sum[:]) +} + +// UserListObjectName is the object a list of that digest is put under: the digest's hex. +func UserListObjectName(digest string) string { return strings.TrimPrefix(digest, "sha256:") } + +// SealingKeyID is a short fingerprint of a public key in PEM, `sha256:<16 hex>`, to say which key a box is sealed to +// without carrying the key. +func SealingKeyID(publicPEM string) string { + sum := sha256.Sum256([]byte(strings.TrimSpace(publicPEM))) + return "sha256:" + hex.EncodeToString(sum[:8]) +} + +// SealUserList is the object a list is put as: the sealed box (seal.Seal) of its bytes to the bus node's key, as JSON. +func SealUserList(list []byte, nodePEM string) ([]byte, error) { + if !strings.Contains(nodePEM, "PUBLIC KEY") { + return nil, fmt.Errorf("delivery: the bus node's key is not a public key in PEM") + } + box, err := seal.Seal(string(list), nodePEM) + if err != nil { + return nil, fmt.Errorf("delivery: the user list could not be sealed: %w", err) + } + return json.Marshal(box) +} + +// OpenUserList opens an object with open (the node's own key) and checks it is the list of digest. +func OpenUserList(object []byte, digest string, open func(seal.Box) ([]byte, error)) ([]byte, error) { + var box seal.Box + if err := json.Unmarshal(object, &box); err != nil { + return nil, fmt.Errorf("delivery: the object is no sealed box: %w", err) + } + list, err := open(box) + if err != nil { + return nil, err + } + if got := UserListDigest(list); got != digest { + return nil, fmt.Errorf("delivery: the opened list's digest is %s, and the key names %s", got, digest) + } + return list, nil +}