Grant the grants part creating the streams manifests declare, so a new module state no longer waits for a controller start
The operator's one more grant in the frozen controller's composer (2026-10-26 08:54), beside the memberships grant of 22:31 (ADR 0547 clause 1 and 5): mesh-grants' principal may publish $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, and nothing wider. The writers table lets it create, never update or delete. Byte-equal with mesh-sdk's buscompose (fix/1790-grants-create-buckets), whose expected lists this code wrote; the same test file holds both copies. Nothing else changes, and the mesh-sdk pin stays. Research 072: moves the grants piece (the grants part split out of the controller) one step, the creation of a module's declared bus objects; the controller's raise at start stays until it loses its lease. Issue: 1790
This commit is contained in:
@@ -0,0 +1,77 @@
|
|||||||
|
package broker
|
||||||
|
|
||||||
|
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.
|
||||||
|
func TestTheGrantsPartCreatesOnlyDeclaredStreams(t *testing.T) {
|
||||||
|
records := Records{
|
||||||
|
Nodes: []string{"novox"},
|
||||||
|
Assigned: map[string][]Declared{"novox": {
|
||||||
|
{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"}}},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
users, err := Users(records)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
perms := map[string]Permissions{}
|
||||||
|
for _, u := range users {
|
||||||
|
if u.Kind != KindModule {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p, err := PermissionsFor(u)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: %v", u.Username(), err)
|
||||||
|
}
|
||||||
|
perms[u.Module] = p
|
||||||
|
}
|
||||||
|
for _, row := range []struct {
|
||||||
|
who, subject string
|
||||||
|
may bool
|
||||||
|
}{
|
||||||
|
{GrantsModule, "$JS.API.STREAM.CREATE.KV_mesh-delivery-builder_steps", true},
|
||||||
|
{GrantsModule, "$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps", true},
|
||||||
|
{GrantsModule, "$JS.API.STREAM.CREATE.LOG_mesh-delivery-builder_builds", true},
|
||||||
|
{GrantsModule, "$JS.API.STREAM.CREATE.BUILDASKS", true},
|
||||||
|
{GrantsModule, "$JS.API.STREAM.CREATE.KV_mesh-grants_assertions", true},
|
||||||
|
{GrantsModule, "$JS.API.STREAM.UPDATE.KV_mesh-delivery-builder_steps", false},
|
||||||
|
{GrantsModule, "$JS.API.STREAM.DELETE.KV_mesh-delivery-builder_steps", false},
|
||||||
|
{GrantsModule, "$JS.API.STREAM.PURGE.KV_mesh-delivery-builder_steps", false},
|
||||||
|
{GrantsModule, "$JS.API.STREAM.CREATE.KV_mesh-delivery-builder_undeclared", false},
|
||||||
|
{GrantsModule, "$JS.API.STREAM.CREATE.EVENTS", false},
|
||||||
|
{GrantsModule, "$JS.API.STREAM.CREATE.ASSIGNMENTS", false},
|
||||||
|
{GrantsModule, "$JS.API.STREAM.INFO.EVENTS", false},
|
||||||
|
{"mesh-delivery-builder", "$JS.API.STREAM.CREATE.KV_mesh-delivery-builder_steps", false},
|
||||||
|
} {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A name that is no plain token is never granted, and a module other than the grants part is never granted one.
|
||||||
|
if got := CreateStreamGrants([]string{"*", ">", "KV_a.b", "KV_a_b"}); len(got) != 2 {
|
||||||
|
t.Errorf("grants for wildcard names: %v", got)
|
||||||
|
}
|
||||||
|
other, err := PermissionsFor(Principal{Kind: KindModule, Node: "novox", Module: "shop", CreatesStreams: []string{"KV_shop_x"}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if MayPublish(other, "$JS.API.STREAM.CREATE.KV_shop_x") {
|
||||||
|
t.Error("a module other than the grants part was granted a create")
|
||||||
|
}
|
||||||
|
// The writers table refuses the grants part an update, and a create of a stream it was not given.
|
||||||
|
g := Principal{Kind: KindModule, Node: "novox", Module: GrantsModule, CreatesStreams: []string{"KV_a_b"}}
|
||||||
|
if err := CheckWriters(g, []string{"$JS.API.STREAM.CREATE.KV_a_b"}); err != nil {
|
||||||
|
t.Errorf("the declared create refused: %v", err)
|
||||||
|
}
|
||||||
|
for _, refused := range []string{"$JS.API.STREAM.UPDATE.KV_a_b", "$JS.API.STREAM.CREATE.KV_a_c", "$JS.API.STREAM.CREATE.*"} {
|
||||||
|
if CheckWriters(g, []string{refused}) == nil {
|
||||||
|
t.Errorf("%s was not refused", refused)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -217,6 +217,11 @@ type Principal struct {
|
|||||||
// BoundBuckets are the providers' buckets of entries, `<module>_bound`, a provisioner client reads its own
|
// BoundBuckets are the providers' buckets of entries, `<module>_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.
|
// machine's keys of (mesh-sdk go/provider, issue 679): every bucket named bound of a module the mesh places.
|
||||||
BoundBuckets []string
|
BoundBuckets []string
|
||||||
|
// CreatesStreams are the streams the grants part's principal alone may create (novox/hq ADR 0547 §5; the operator,
|
||||||
|
// 2026-10-26 08:54): the stream of every bucket, log and stream a manifest of a module placed anywhere declares,
|
||||||
|
// by name. Created only, never updated or deleted: a new module state is on the bus when the grants part hears its
|
||||||
|
// manifest, not at the controller's next start (issue 1790).
|
||||||
|
CreatesStreams []string
|
||||||
|
|
||||||
// PasswordHash is the bcrypt hash the mesh minted. The plaintext is sealed to the principal
|
// PasswordHash is the bcrypt hash the mesh minted. The plaintext is sealed to the principal
|
||||||
// and never appears here: this file is written to a node's disk and read by a server, and a
|
// and never appears here: this file is written to a node's disk and read by a server, and a
|
||||||
@@ -920,6 +925,9 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
|||||||
// first so it publishes only what changed, and no stream it defines.
|
// first so it publishes only what changed, and no stream it defines.
|
||||||
if p.Module == GrantsModule {
|
if p.Module == GrantsModule {
|
||||||
pub = append(pub, "mesh.assignment.*.*", "$JS.API.DIRECT.GET."+AssignmentsStream+".mesh.assignment.*.*")
|
pub = append(pub, "mesh.assignment.*.*", "$JS.API.DIRECT.GET."+AssignmentsStream+".mesh.assignment.*.*")
|
||||||
|
// 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)...)
|
||||||
}
|
}
|
||||||
// The view alone reads the build seats' workers for the backlog of asks (issue 1887 A8a).
|
// The view alone reads the build seats' workers for the backlog of asks (issue 1887 A8a).
|
||||||
if p.Module == ViewModule {
|
if p.Module == ViewModule {
|
||||||
@@ -1587,6 +1595,20 @@ func ComposeAccounts(principals []Principal) (string, error) {
|
|||||||
// from them. No other principal is granted the subjects below, and nothing is a wildcard over them.
|
// from them. No other principal is granted the subjects below, and nothing is a wildcard over them.
|
||||||
const WatchmanModule = "mesh-delivery-watchman"
|
const WatchmanModule = "mesh-delivery-watchman"
|
||||||
|
|
||||||
|
// CreateStreamGrants is what creating each named stream needs, and nothing wider (novox/hq ADR 0547 §5; the operator,
|
||||||
|
// 2026-10-26 08:54): its create, and its information to read back what it holds. Each by its own name: a name that
|
||||||
|
// is not one plain subject token is left out, since a wildcard there would be every stream.
|
||||||
|
func CreateStreamGrants(streams []string) []string {
|
||||||
|
var out []string
|
||||||
|
for _, s := range streams {
|
||||||
|
if !safeSubject.MatchString(s) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, "$JS.API.STREAM.CREATE."+s, "$JS.API.STREAM.INFO."+s)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// GrantsModule is the one module whose principal publishes the modules' memberships (novox/hq ADR 0547 §5): the grants
|
// GrantsModule is the one module whose principal publishes the modules' memberships (novox/hq ADR 0547 §5): the grants
|
||||||
// part, which composes them from the manifests and placement. It defines no stream and is granted no other subject here.
|
// part, which composes them from the manifests and placement. It defines no stream and is granted no other subject here.
|
||||||
const GrantsModule = "mesh-grants"
|
const GrantsModule = "mesh-grants"
|
||||||
|
|||||||
@@ -207,6 +207,11 @@ func Users(r Records) ([]Principal, error) {
|
|||||||
PerMachine: perMachineNames(d.State), KeyedReads: d.KeyedReads,
|
PerMachine: perMachineNames(d.State), KeyedReads: d.KeyedReads,
|
||||||
Logs: logNames(d.Logs), Streams: d.Streams, Hears: ComposedHears(d), Queues: d.Queues,
|
Logs: logNames(d.Logs), Streams: d.Streams, Hears: ComposedHears(d), Queues: d.Queues,
|
||||||
})
|
})
|
||||||
|
// **The grants part creates what a manifest declares** (novox/hq ADR 0547 §5; the operator, 2026-10-26
|
||||||
|
// 08:54): every placed module's buckets, logs and streams, each by its stream's name.
|
||||||
|
if d.Module == GrantsModule {
|
||||||
|
out[len(out)-1].CreatesStreams = declaredStreamsOf(r)
|
||||||
|
}
|
||||||
// **The vault's holder asks the desks and the issuing providers itself** (issue 723), under the
|
// **The vault's holder asks the desks and the issuing providers itself** (issue 723), under the
|
||||||
// account its own runtime runs on (ADR 0259 §8's runs-as): the one module granted it.
|
// account its own runtime runs on (ADR 0259 §8's runs-as): the one module granted it.
|
||||||
if holdsSeat(d, VaultSeat) {
|
if holdsSeat(d, VaultSeat) {
|
||||||
@@ -418,3 +423,29 @@ func boundBucketsOf(r Records) []string {
|
|||||||
func runnerServes(d Declared) bool {
|
func runnerServes(d Declared) bool {
|
||||||
return d.Module == RuntimeModule || d.NoAccount || d.Loads || len(d.Serves) > 0 || len(d.Holds) > 0 || len(d.Waits) > 0 || len(d.Checks) > 0
|
return d.Module == RuntimeModule || d.NoAccount || d.Loads || len(d.Serves) > 0 || len(d.Holds) > 0 || len(d.Waits) > 0 || len(d.Checks) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// declaredStreamsOf is the stream of every bucket, log and stream a module placed anywhere declares, once each, in name
|
||||||
|
// order (novox/hq ADR 0547 §5, issue 1790): `KV_<module>_<state>`, `LOG_<module>_<log>` and each module stream's own
|
||||||
|
// name. What the grants part may create, and nothing beside.
|
||||||
|
func declaredStreamsOf(r Records) []string {
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, mods := range r.Assigned {
|
||||||
|
for _, d := range mods {
|
||||||
|
for _, b := range d.State {
|
||||||
|
seen["KV_"+b.Bucket()] = true
|
||||||
|
}
|
||||||
|
for _, l := range d.Logs {
|
||||||
|
seen[l.Stream()] = true
|
||||||
|
}
|
||||||
|
for _, s := range d.Streams {
|
||||||
|
seen[s.Name] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out := make([]string, 0, len(seen))
|
||||||
|
for s := range seen {
|
||||||
|
out = append(out, s)
|
||||||
|
}
|
||||||
|
sort.Strings(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,6 +37,17 @@ type WriterRow struct {
|
|||||||
// isController, and the other writers' tests, are who a row's writer is as a principal.
|
// isController, and the other writers' tests, are who a row's writer is as a principal.
|
||||||
func isController(p Principal, _ string) bool { return p.Kind == KindController }
|
func isController(p Principal, _ string) bool { return p.Kind == KindController }
|
||||||
|
|
||||||
|
// createsDeclared is the controller, or the grants part creating one stream a manifest declares, by its own name: the
|
||||||
|
// create alone, never an update or a delete (novox/hq ADR 0547 §5; the operator, 2026-10-26 08:54).
|
||||||
|
func createsDeclared(p Principal, pattern string) bool {
|
||||||
|
if isController(p, pattern) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
name, ok := strings.CutPrefix(pattern, "$JS.API.STREAM.CREATE.")
|
||||||
|
return ok && p.Kind == KindModule && p.Module == GrantsModule && slices.Contains(p.CreatesStreams, name) &&
|
||||||
|
safeSubject.MatchString(name)
|
||||||
|
}
|
||||||
|
|
||||||
// ownMachine is a node writing a subject whose third token is its own name, and no wildcard there.
|
// ownMachine is a node writing a subject whose third token is its own name, and no wildcard there.
|
||||||
func ownMachine(p Principal, pattern string) bool {
|
func ownMachine(p Principal, pattern string) bool {
|
||||||
tokens := strings.Split(pattern, ".")
|
tokens := strings.Split(pattern, ".")
|
||||||
@@ -170,7 +181,8 @@ var WritersTable = []WriterRow{
|
|||||||
// bucket's stream (ADR 0201), which defines nothing the mesh keeps.
|
// bucket's stream (ADR 0201), which defines nothing the mesh keeps.
|
||||||
Subjects: []string{"$JS.API.STREAM.CREATE.>", "$JS.API.STREAM.UPDATE.>", "$JS.API.STREAM.DELETE.>",
|
Subjects: []string{"$JS.API.STREAM.CREATE.>", "$JS.API.STREAM.UPDATE.>", "$JS.API.STREAM.DELETE.>",
|
||||||
"$JS.API.CONSUMER.DURABLE.CREATE.>"},
|
"$JS.API.CONSUMER.DURABLE.CREATE.>"},
|
||||||
Writes: isController},
|
Writes: createsDeclared,
|
||||||
|
Shared: "the grants part creates a stream a manifest declares, by its name, and never updates or deletes one (novox/hq ADR 0547 §5; the operator, 2026-10-26 08:54)"},
|
||||||
{State: "builds and their outcomes", Writer: "the build seat's holder", KeptIn: "its own state",
|
{State: "builds and their outcomes", Writer: "the build seat's holder", KeptIn: "its own state",
|
||||||
Others: "the controller asks",
|
Others: "the controller asks",
|
||||||
Subjects: []string{"mesh.seat.node-build-agent.event.built", "mesh.seat.mesh-build-machine.event.built"},
|
Subjects: []string{"mesh.seat.node-build-agent.event.built", "mesh.seat.mesh-build-machine.event.built"},
|
||||||
|
|||||||
Reference in New Issue
Block a user