GR8: a seat's verb is granted to its users only where its effective callers name them
Step 4 of a module's permissions granted every user of a seat every verb the seat serves, so a kept-artifact call was composed for any module using a build seat. The compiled Seat now carries each verb's effective callers (seatdef EffectiveCallers, the class defaults), filled where the askers of its queues are, and a user is granted a verb's call subject only where those callers name it: invokers that invoke it, its module, or a seat it holds. A seat whose definitions are unread grants its users no verb. Builds: design 47 G §610, §336, §271 (test 60), A-165, A-179 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:<seat>.<verb>`, or every tool by `*`), `module:<module>` naming it, or `seat:<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
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user