From 174789941936c401d07240eb1891a9fd38a107f0 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 26 Oct 2026 11:28:38 +0100 Subject: [PATCH 1/4] delivery: the bus node hands the loaded list's hashes to the vault, sealed, in the bus module's have (ADR 0547 clause 9, issue 1945) The vault holds no value for the controller's own account, the node accounts and the enrolled devices, so it cannot hash them; the one part that has their hashes is the node-engine of the bus node, in the list the bus loaded. The have of the bus module now carries that list's hashes sealed to the vault's key (UserListWritten.Handed), which every reader of the have bucket sees as a count, a list digest and a key fingerprint, and only the vault opens. HandedAccountsOf reads them from a composed list, HandAccounts seals, OpenHanded opens and refuses a box sealed to another key. --- go/delivery/delivery.go | 13 +++- go/delivery/delivery_test.go | 57 ++++++++++++++++ go/delivery/handed.go | 125 +++++++++++++++++++++++++++++++++++ 3 files changed, 192 insertions(+), 3 deletions(-) create mode 100644 go/delivery/handed.go diff --git a/go/delivery/delivery.go b/go/delivery/delivery.go index 8fccfaa..cea8059 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,9 @@ type UserListWritten struct { Users int `json:"users"` At time.Time `json:"at"` Error string `json:"error,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 +508,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..3ed5072 100644 --- a/go/delivery/delivery_test.go +++ b/go/delivery/delivery_test.go @@ -7,6 +7,8 @@ import ( "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 +169,47 @@ 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") + } }) t.Run("have node", func(t *testing.T) { in := HaveNode{Windows: json.RawMessage(`{"a":1}`), Busy: json.RawMessage(`true`)} @@ -306,6 +349,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) +} From 175a5567b8bd2b51bac61e1316d3ee4e2e8a8e0d Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 26 Oct 2026 11:53:11 +0100 Subject: [PATCH 2/4] The bus node follows the grants part's sealed user list, and only the grants part reads the vault's hashes (ADR 0547 clauses 7, 9; issue 1945) The grants part writes its list as an object of its own bucket, sealed to the bus node's key, and names it in user-list.sealed; the node-engine of the node holding mesh-broker is granted that one key and an object by name, the grants part its bucket (the controller's copy carries the same grants, byte for byte, golden regenerated there). A state may name its readers (announce, NamedReaders): mesh-vault.bus-accounts is read by mesh-grants alone. --- go/announce/announce.go | 30 ++- go/announce/announce_test.go | 11 +- go/buscompose/announced.go | 15 ++ go/buscompose/creates_streams_test.go | 45 +++- go/buscompose/nats.go | 10 + .../testdata/controller-user-lists.json | 4 +- go/buscompose/userlist_switch.go | 111 ++++++++++ .../userlist_switch_integration_test.go | 206 ++++++++++++++++++ go/buscompose/users.go | 11 +- go/buscompose/writers.go | 8 +- go/delivery/delivery.go | 6 + go/delivery/delivery_test.go | 64 ++++++ go/delivery/userlist.go | 149 +++++++++++++ 13 files changed, 659 insertions(+), 11 deletions(-) create mode 100644 go/buscompose/userlist_switch.go create mode 100644 go/buscompose/userlist_switch_integration_test.go create mode 100644 go/delivery/userlist.go diff --git a/go/announce/announce.go b/go/announce/announce.go index 37dfd21..b0d1a4a 100644 --- a/go/announce/announce.go +++ b/go/announce/announce.go @@ -46,6 +46,9 @@ 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. Naming readers makes it readable. + 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,11 +70,31 @@ 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 { if k.Name == state { - return k.Readable + return k.Readable || len(k.Readers) > 0 } } return false @@ -93,6 +116,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..075680e 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): the state is readable, by them alone. + 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..a2ec1d8 --- /dev/null +++ b/go/buscompose/userlist_switch_integration_test.go @@ -0,0 +1,206 @@ +//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) + } + kv, err := grants.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: GrantsUserListBucket}) + if err != nil { + t.Fatalf("the grants part's 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..eba8342 100644 --- a/go/delivery/delivery.go +++ b/go/delivery/delivery.go @@ -352,6 +352,12 @@ 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"` } // HaveNode is a node's own facts, written under HaveNodeKey. Each field is the node-engine's own JSON, carried whole: diff --git a/go/delivery/delivery_test.go b/go/delivery/delivery_test.go index a0aba8d..d202d7b 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. @@ -168,6 +171,67 @@ func TestRoundTrip(t *testing.T) { t.Fatalf("got %+v want %+v", out, in) } }) + // 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`)} b, err := EncodeHaveNode(in) 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 +} From 83ee7769d8ee4adc2658b26447cc9d0831a136b3 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 26 Oct 2026 12:00:29 +0100 Subject: [PATCH 3/4] Make the grants part's state in the switch's test as the part makes a declared state, by its stream (issue 1945) --- go/buscompose/userlist_switch_integration_test.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/go/buscompose/userlist_switch_integration_test.go b/go/buscompose/userlist_switch_integration_test.go index a2ec1d8..d8eb3fd 100644 --- a/go/buscompose/userlist_switch_integration_test.go +++ b/go/buscompose/userlist_switch_integration_test.go @@ -78,10 +78,17 @@ func TestTheBusNodeFollowsTheGrantsPartsSealedListUnderTheComposedGrants(t *test if err != nil { t.Fatal(err) } - kv, err := grants.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: GrantsUserListBucket}) - if err != nil { + // 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) From c2a410bb63765d4c1dca0da5cc74e24aca4debf5 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 26 Oct 2026 12:25:27 +0100 Subject: [PATCH 4/4] The have's user list says why the bus node does not watch the grants part's key (review of the switch, issue 1945) --- go/delivery/delivery.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/go/delivery/delivery.go b/go/delivery/delivery.go index 45ea1d0..5eecc31 100644 --- a/go/delivery/delivery.go +++ b/go/delivery/delivery.go @@ -361,6 +361,9 @@ type UserListWritten struct { // 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"`