diff --git a/modules/mesh-grants/internal/buscompose/named_asks_test.go b/modules/mesh-grants/internal/buscompose/named_asks_test.go index 066edd62..9361ac03 100644 --- a/modules/mesh-grants/internal/buscompose/named_asks_test.go +++ b/modules/mesh-grants/internal/buscompose/named_asks_test.go @@ -67,3 +67,47 @@ func TestNamedAsks(t *testing.T) { t.Errorf("the work queue's consumers: %d, want 4 (a main and a named worker for each of two queues)", len(consumers)) } } + +// Design 47 G §610, A-179 (test 60's case of §271): a user of a seat is granted a verb's call subject only where the verb's +// effective callers name it, so a kept-artifact call is composed for the module that invokes it and for no other user of +// the build seat; `module:` and `seat:` entries name theirs, and a seat whose definition is unread grants its users no verb. +func TestAVerbIsGrantedToItsEffectiveCallersAlone(t *testing.T) { + build := Seat{Name: "node-build-go", Scope: "node", Serves: []string{"kept-artifact", "freed", "run"}, + VerbCallers: map[string][]string{ + "kept-artifact": {"invokers", "persons", "sessions"}, + "freed": {"module:a-checker"}, + "run": {"seat:mesh-pipeline-runner"}, + }} + user := func(module string, invokes []string, held ...string) Principal { + p := Principal{Kind: KindModule, Node: "one", Module: module, Uses: []Seat{build}, Invokes: invokes} + for _, h := range held { + p.Holds = append(p.Holds, Seat{Name: h}) + } + return p + } + unread := build + unread.VerbCallers = nil + const base = "mesh.seat.node-build-go.tool." + for _, c := range []struct { + who Principal + want map[string]bool + }{ + {user("an-invoker", []string{"seat:node-build-go.kept-artifact"}), map[string]bool{ + base + "kept-artifact.ace": true, base + "freed.ace": false, base + "run.ace": false}}, + {user("a-runner", nil, "mesh-pipeline-runner"), map[string]bool{ + base + "kept-artifact.ace": false, base + "freed.ace": false, base + "run.ace": true}}, + {user("a-checker", nil), map[string]bool{base + "kept-artifact.ace": false, base + "freed.ace": true}}, + {Principal{Kind: KindModule, Node: "one", Module: "a-checker", Uses: []Seat{unread}}, map[string]bool{ + base + "freed.ace": false, base + "kept-artifact.ace": false}}, + } { + perms, err := PermissionsFor(c.who) + if err != nil { + t.Fatal(err) + } + for subject, want := range c.want { + if got := MayPublish(perms, subject); got != want { + t.Errorf("%s may publish %s: %v, want %v", c.who.Module, subject, got, want) + } + } + } +} diff --git a/modules/mesh-grants/internal/buscompose/nats.go b/modules/mesh-grants/internal/buscompose/nats.go index ba7035e7..42d11be8 100644 --- a/modules/mesh-grants/internal/buscompose/nats.go +++ b/modules/mesh-grants/internal/buscompose/nats.go @@ -28,6 +28,7 @@ import ( sdkbc "git.novox.be/novox/mesh-sdk/go/buscompose" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/membership" + "git.novox.be/novox/mesh-sdk/go/seatdef" ) // A Kind is what a principal is, which decides the shape of its authority rather than its @@ -107,6 +108,11 @@ type Seat struct { // entries of the callers of the accepted kinds the definition puts on that queue (design 47 G §543, §600). The asker // in a named ask's subject is the asking seat's name (A-163). Empty where the seat list is unread. AcceptCallers map[string][]string + // VerbCallers are, by verb, the callers each verb the seat serves admits: its definition's effective list + // (seatdef.Verb.EffectiveCallers, the class defaults of design 47 G §610). A user of the seat is granted a verb's call + // subject only where that list names it (A-179), so the bus holds every other caller out. Empty where the seat list is + // unread, and then no verb of the seat is granted to its users. + VerbCallers map[string][]string // HeldByTheEngine says every node's engine holds the seat on its own account (catalogue.Seat.HeldByTheEngine, novox/hq // ADR 0563 §1): each of its events names the engine's machine as its last token (machineEvent). HeldByTheEngine bool @@ -1061,7 +1067,10 @@ func permissionsFor(p Principal) (Permissions, error) { pub = append(pub, seatSubject(s, "accept", a)) } for _, t := range s.Serves { - pub = append(pub, seatToolSubject(s, t, "*")) + // Only a verb whose effective callers name this module (design 47 G A-179, §610). + if callerOf(p, s, t) { + pub = append(pub, seatToolSubject(s, t, "*")) + } } } @@ -1358,6 +1367,27 @@ func seatSubject(s Seat, kind, verb string) string { return "mesh.seat." + s.Name + "." + kind + "." + verb } +// callerOf says whether a verb's effective callers (Seat.VerbCallers) name the module principal p: `invokers` where its +// manifest invokes the verb (`seat:.`, or every tool by `*`), `module:` naming it, or `seat:` +// naming a seat it holds (design 47 G §336, §610, A-179). `operator`, `persons` and `sessions` name no module. +func callerOf(p Principal, s Seat, verb string) bool { + for _, c := range s.VerbCallers[verb] { + switch { + case c == seatdef.CallerInvokers: + if slices.Contains(p.Invokes, "*") || slices.Contains(p.Invokes, "seat:"+s.Name+"."+verb) { + return true + } + case c == seatdef.CallerModulePrefix+p.Module: + return true + } + if seat, ok := strings.CutPrefix(c, seatdef.CallerSeatPrefix); ok && + slices.ContainsFunc(p.Holds, func(h Seat) bool { return h.Name == seat }) { + return true + } + } + return false +} + // seatToolSubject is where a role's tool is asked. Mesh-wide for a mesh-scoped seat; a node-scoped // seat carries the node it is asked of, because a flat subject would reach every machine's holder // and the queue group would silently pick a winner (novox/hq ADR 0132, design 33 §4). A holder diff --git a/modules/mesh-grants/internal/grants/kept.go b/modules/mesh-grants/internal/grants/kept.go index 3994d901..b87f86ec 100644 --- a/modules/mesh-grants/internal/grants/kept.go +++ b/modules/mesh-grants/internal/grants/kept.go @@ -177,3 +177,13 @@ func withAcceptCallers(askers map[string]map[string][]string, d *buscompose.Decl } } } + +// withVerbCallers gives each seat a declaration holds, uses or watches the effective callers of its verbs +// (Seat.VerbCallers, design 47 G §610, A-179), as withAcceptCallers gives the askers of its queues. +func withVerbCallers(callers map[string]map[string][]string, d *buscompose.Declared) { + for _, list := range []*[]buscompose.Seat{&d.Holds, &d.Uses, &d.Watches} { + for i := range *list { + (*list)[i].VerbCallers = callers[(*list)[i].Name] + } + } +} diff --git a/modules/mesh-grants/internal/grants/records.go b/modules/mesh-grants/internal/grants/records.go index cdf31fb4..f2c32ade 100644 --- a/modules/mesh-grants/internal/grants/records.go +++ b/modules/mesh-grants/internal/grants/records.go @@ -110,6 +110,7 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) { d.Bindings = in.Bindings[m.Module] withEventSubjects(in.EventSubjects, &d) withAcceptCallers(in.AcceptCallers, &d) + withVerbCallers(in.VerbCallers, &d) return d } d := declare(m) @@ -142,6 +143,7 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) { State: bucketsFor(m, in.Nodes), Logs: logsOf(m), Streams: streamsOf(m)} withEventSubjects(in.EventSubjects, out.Engine) withAcceptCallers(in.AcceptCallers, out.Engine) + withVerbCallers(in.VerbCallers, out.Engine) } return out, gaps } diff --git a/modules/mesh-grants/internal/grants/sentobjects_test.go b/modules/mesh-grants/internal/grants/sentobjects_test.go index 28e7c6cd..efce0b8b 100644 --- a/modules/mesh-grants/internal/grants/sentobjects_test.go +++ b/modules/mesh-grants/internal/grants/sentobjects_test.go @@ -6,6 +6,8 @@ import ( "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/seatdef" + + "mesh-grants/internal/buscompose" ) // The sent stores a module reads follow the catalog graph (design 47 G §59, §124): the consumers of a module's events and the @@ -34,5 +36,11 @@ func TestSentStoresRead(t *testing.T) { if got := sentStoresRead(c.m, all, callers); !slices.Equal(got, c.want) { t.Errorf("%s: %v, want %v", c.name, got, c.want) } + } // The same callers reach each seat a declaration holds, uses or watches, from which its users' call subjects are + // composed (design 47 G A-179). + d := buscompose.Declared{Module: "bank", Uses: []buscompose.Seat{{Name: "orders-seat"}}, Holds: []buscompose.Seat{{Name: "billing"}}} + withVerbCallers(callers, &d) + if !slices.Equal(d.Uses[0].VerbCallers["place"], callers["orders-seat"]["place"]) || d.Holds[0].VerbCallers != nil { + t.Errorf("a used seat's verbs carry %v, a held seat no definition names %v", d.Uses[0].VerbCallers, d.Holds[0].VerbCallers) } }