The copied bus composer grants mesh-grants' principal $JS.API.STREAM.CREATE.<stream> and $JS.API.STREAM.INFO.<stream> for each bucket (KV_<module>_<state>), log (LOG_<module>_<log>) and module stream a placed module's manifest declares, each by its own name: NATS wildcards are whole tokens, so nothing narrower than an exact name exists and nothing wider is granted. The writers table lets the grants part create, never update or delete. The operator allowed this one grant on 2026-10-26 08:54, beside the memberships grant of 22:31 (ADR 0547 clause 1 and 5). The expected lists were written again by the controller's own internal/broker with the same change (mesh-control fix/1790-grants-create-buckets); a fixture now places mesh-grants beside the builder's state, log and stream. Issue: 1790
120 lines
4.7 KiB
Go
120 lines
4.7 KiB
Go
package buscompose
|
|
|
|
// The fixtures the copied bus composer is held to (issue 1377, ADR 0547 §5): the same source is compiled into the
|
|
// controller's internal/broker to write the expected lists, and into mesh-sdk's buscompose to compare against them.
|
|
|
|
// UserListFixture is one mesh's records, and the hashes its accounts carry.
|
|
type UserListFixture struct {
|
|
Name string
|
|
Records Records
|
|
// Hashless are users given no hash, so they are left out and named.
|
|
Hashless []string
|
|
}
|
|
|
|
func fixtureTelegramSeat() Seat {
|
|
return Seat{Name: "telegram-sender", Accepts: []string{"send"}, Emits: []string{"delivered", "failed"}}
|
|
}
|
|
|
|
// UserListFixtures are the meshes, from the controller's own tests' records outward.
|
|
func UserListFixtures() []UserListFixture {
|
|
return []UserListFixture{
|
|
{Name: "empty"},
|
|
{Name: "the controller's someRecords", Records: Records{
|
|
Nodes: []string{"two", "one"},
|
|
Assigned: map[string][]Declared{
|
|
"one": {{Module: "telegram", Serves: []string{"status"}}},
|
|
"two": {{Module: "shop", Emits: []string{"order.placed"}}},
|
|
},
|
|
Enrolling: []string{"three"},
|
|
People: map[string][]string{"ada": {"mesh-catalog.catalog_tools"}},
|
|
}},
|
|
{Name: "seats, consumers, people and a hashless user", Hashless: []string{"two.audit"}, Records: Records{
|
|
Nodes: []string{"one", "two"},
|
|
Assigned: map[string][]Declared{
|
|
"one": {
|
|
{Module: "telegram", Holds: []Seat{fixtureTelegramSeat()}, Serves: []string{"status"}},
|
|
{Module: "nats", SnapshotsTheBus: true, Serves: []string{"nats_streams"}},
|
|
{Module: "mesh-watchman"},
|
|
{Module: "mesh-grants", Emits: []string{"membership-changed"}},
|
|
},
|
|
"two": {
|
|
{Module: "shop", Uses: []Seat{fixtureTelegramSeat()}, Emits: []string{"order.placed"}},
|
|
{Module: "audit", Consumes: []string{"shop.order.placed"}},
|
|
},
|
|
},
|
|
People: map[string][]string{"ada": {"*"}, "bob": {"shop.status"}},
|
|
Interchangeable: map[string]bool{"shop": true},
|
|
}},
|
|
{Name: "state, logs, streams, queues and processes", Records: Records{
|
|
Nodes: []string{"ace", "novox"},
|
|
Assigned: map[string][]Declared{
|
|
"novox": {
|
|
{Module: "mesh-delivery-builder", Emits: []string{"built"},
|
|
State: []Bucket{{Module: "mesh-delivery-builder", Name: "steps", History: 5},
|
|
{Module: "mesh-delivery-builder", Name: "presence", PerMachine: true, KeyTTL: true, TTLSeconds: 60}},
|
|
Logs: []Log{{Module: "mesh-delivery-builder", Name: "builds", MaxMiB: 64}},
|
|
Streams: []ModuleStream{{Module: "mesh-delivery-builder", Name: "asks", Events: []string{"ask"}, MaxAgeDays: 7, MaxMiB: 32, WorkQueue: true}},
|
|
Queues: []QueueOwner{{Process: "worker", Stream: "asks", MaxDeliver: 5}},
|
|
Processes: []ProcessPrincipal{{Process: "worker", Emits: []string{"built"},
|
|
Queues: []QueueOwner{{Process: "worker", Stream: "asks", MaxDeliver: 5}},
|
|
State: []string{"steps"}, Invokes: []string{"gitea.gitea_commit"}}},
|
|
Invokes: []string{"gitea.gitea_commit"},
|
|
},
|
|
{Module: "mesh-delivery-view", Reads: []string{"mesh-delivery-builder.steps"},
|
|
KeyedReads: []KeyedRead{{Read: "mesh-delivery-builder.presence", Key: "novox"}}},
|
|
// The grants part, which may create every stream declared here (ADR 0547 §5, issue 1790).
|
|
{Module: "mesh-grants", Emits: []string{"membership-changed"},
|
|
State: []Bucket{{Module: "mesh-grants", Name: "assertions", History: 5}}},
|
|
},
|
|
"ace": {
|
|
{Module: "plex", Serves: []string{"plex_status"}, Checks: []string{"plex.plex_status"}},
|
|
{Module: "silent", NoAccount: true},
|
|
},
|
|
},
|
|
RootFree: map[string]bool{"ace": true},
|
|
}},
|
|
}
|
|
}
|
|
|
|
// HashOf is the stand-in hash a fixture's user carries.
|
|
func HashOf(user string) string {
|
|
h := "$2a$11$"
|
|
for len(h) < 29 {
|
|
h += user
|
|
}
|
|
return h[:29] + "fixturefixturefixturefixture00"
|
|
}
|
|
|
|
// ComposedFor is a fixture's user list as the composer writes it, and the users left out.
|
|
func ComposedFor(f UserListFixture) (string, []string, error) {
|
|
users, err := Users(f.Records)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
hashes := map[string]string{}
|
|
skip := map[string]bool{}
|
|
for _, u := range f.Hashless {
|
|
skip[u] = true
|
|
}
|
|
for _, u := range users {
|
|
if !skip[u.Username()] {
|
|
hashes[u.Username()] = HashOf(u.Username())
|
|
}
|
|
}
|
|
filled, missing := WithPasswords(users, hashes)
|
|
list, err := ComposeAccounts(filled)
|
|
return list, missing, err
|
|
}
|
|
|
|
// MembershipsFor is every membership a fixture's records issue, by `<node>.<module>`, as the composer makes them.
|
|
func MembershipsFor(f UserListFixture) map[string]Membership {
|
|
where := PlacementsOf(f.Records, f.Records.Interchangeable)
|
|
out := map[string]Membership{}
|
|
for node, ds := range f.Records.Assigned {
|
|
for _, d := range ds {
|
|
out[node+"."+d.Module] = MembershipFor(node, d, where)
|
|
}
|
|
}
|
|
return out
|
|
}
|