Each node's account calls the engine's invokes and hears its seat events on the seat (issue 1966)
The grants part built the node-engine's declaration with its consumes alone, so a node's account was granted none of the engine manifest's invokes: ace's engine could not publish mesh.seat.mesh-catalog.call.seat-definitions.node~ace, read no seat list and composed nothing new. And its consumes of mesh-catalog's seat-definition events were granted on the emitter's subject only, where the engine subscribes the seat's (ADR 0636 §4). The engine's declaration now carries its invokes and the seats it watches, split by the same consumedOf a placed module's consumes are; the node's account is granted the invokes as a module's are (in its own caller token by callerNamed) and the watched events on each seat's subject by the same watchedEvents a module's use. Its consumes stay granted on the emitter's subjects too, so nothing granted before is taken away. Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5
This commit is contained in:
@@ -2037,7 +2037,8 @@ func TestGrants(t *testing.T) {
|
||||
|
||||
t.Run("assertions: the node-engine's declared state is created though no node is assigned it", func(t *testing.T) {
|
||||
b := fixture()
|
||||
b.manifestOf(buscompose.EngineModule, `{"module":"mesh-host","version":"1","state":[{"name":"acts"}]}`)
|
||||
b.manifestOf(buscompose.EngineModule, `{"module":"mesh-host","version":"1","state":[{"name":"acts"}],`+
|
||||
`"invokes":["seat:mesh-catalog.seat-definitions"],"consumes":["mesh-catalog.seat-definition-changed","restic.backed-up"]}`)
|
||||
b.loaded = shadowOf(t, b)
|
||||
p := partOn(b)
|
||||
p.Create = b
|
||||
@@ -2068,6 +2069,39 @@ func TestGrants(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Issue 1966: each node's account calls what the engine's manifest invokes in its own name, as a module's account
|
||||
// does (G1), and hears a consumed seat's event on the seat's own subject, where the engine subscribes it, and still
|
||||
// on the emitter's (G2); a consume naming no seat stays on its emitter's alone.
|
||||
users, err := buscompose.Users(recs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
nodes := 0
|
||||
for _, u := range users {
|
||||
if u.Kind != buscompose.KindNode {
|
||||
continue
|
||||
}
|
||||
nodes++
|
||||
perms, err := buscompose.PermissionsFor(u)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
token := buscompose.CallerToken(u.Username())
|
||||
if !buscompose.MayPublish(perms, "mesh.seat.mesh-catalog.call.seat-definitions."+token) ||
|
||||
buscompose.MayPublish(perms, "mesh.seat.mesh-catalog.call.seat-definitions.other~one") ||
|
||||
buscompose.MayPublish(perms, "mesh.seat.mesh-catalog.call.modules."+token) {
|
||||
t.Errorf("%s: the engine's invoke of seat-definitions is not granted in its own name alone", u.Username())
|
||||
}
|
||||
if !buscompose.MaySubscribe(perms, "mesh.seat.mesh-catalog.event.seat-definition-changed") ||
|
||||
!buscompose.MaySubscribe(perms, "mesh.mod.mesh-catalog.event.seat-definition-changed") ||
|
||||
buscompose.MaySubscribe(perms, "mesh.seat.mesh-catalog.event.manifest-accepted") ||
|
||||
!buscompose.MaySubscribe(perms, "mesh.mod.restic.event.backed-up") {
|
||||
t.Errorf("%s: the engine's consumes are not heard on the seat's subject and the emitter's", u.Username())
|
||||
}
|
||||
}
|
||||
if nodes == 0 {
|
||||
t.Fatal("no node account composed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("assertions: a refused create is said, never stops it, and is tried at the next event", func(t *testing.T) {
|
||||
|
||||
@@ -779,6 +779,16 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
}
|
||||
sub = append(sub, subject)
|
||||
}
|
||||
// And the seats' events the node-engine's manifest consumes, on each seat's own subject, and the verbs it invokes,
|
||||
// in the node's own name (callerNamed, below): as a module's are (issue 1966: the seat list's verb and events).
|
||||
sub = append(sub, watchedEvents(p.Watches)...)
|
||||
tp, ts := seatTrafficGrants(SeatTrafficOf(p.Username(), nil, nil, p.Watches))
|
||||
pub, sub = append(pub, tp...), append(sub, ts...)
|
||||
invoked, err := invokedSubjects(p.Invokes)
|
||||
if err != nil {
|
||||
return Permissions{}, err
|
||||
}
|
||||
pub = append(pub, invoked...)
|
||||
// And the creation of the seat mesh-module-assigner's two states it composes from (ADR 0567 §9), so a node agent started
|
||||
// before they exist binds each when the bus says it was made, with no timer; no other bucket's.
|
||||
sub = append(sub, NodeComposeAdvisories()...)
|
||||
@@ -918,19 +928,7 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
|
||||
// 2b. Events of a role it watches, under the seat's own namespace. Subscribe only: watching a
|
||||
// role is hearing what it announced, not taking part in it.
|
||||
for _, w := range p.Watches {
|
||||
if w.Kinded {
|
||||
continue // composed by SeatTrafficOf below
|
||||
}
|
||||
for _, e := range w.Emits {
|
||||
// An event that names its holder's machine is heard from every machine (machineEvent): a flat subject would
|
||||
// match no event the holder says.
|
||||
if pattern, ok := machineEvent(w, e); ok {
|
||||
e = pattern
|
||||
}
|
||||
sub = append(sub, seatSubject(w, "event", e))
|
||||
}
|
||||
}
|
||||
sub = append(sub, watchedEvents(p.Watches)...)
|
||||
|
||||
// 2c. Its own consumer, which it **pulls**: the runtime asks for the next message and is
|
||||
// answered on its own inbox, so what it needs is to ask about the consumer and to ask it
|
||||
@@ -1609,6 +1607,25 @@ func quoted(values []string) string {
|
||||
return strings.Join(out, ", ")
|
||||
}
|
||||
|
||||
// watchedEvents are the subjects a principal subscribes to hear the seats it watches (a `consumes` naming a seat's event):
|
||||
// each on the seat's own namespace, an event naming its holder's machine heard from every machine (machineEvent), since a
|
||||
// flat subject would match no event the holder says. A kinded seat's are SeatTrafficOf's.
|
||||
func watchedEvents(watches []Seat) []string {
|
||||
var out []string
|
||||
for _, w := range watches {
|
||||
if w.Kinded {
|
||||
continue
|
||||
}
|
||||
for _, e := range w.Emits {
|
||||
if pattern, ok := machineEvent(w, e); ok {
|
||||
e = pattern
|
||||
}
|
||||
out = append(out, seatSubject(w, "event", e))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// invokedSubjects is the publish side of a grant to call tools: one subject per `<module>.<tool>`,
|
||||
// or the whole tool namespace for `*`. A person's authority and a module's `invokes` are both this
|
||||
// (novox/hq ADR 0152), so a malformed entry is refused in one place, before it could be widened into
|
||||
|
||||
@@ -200,12 +200,16 @@ func Users(r Records) ([]Principal, error) {
|
||||
// each with the node's permissions.
|
||||
// What the node-engine consumes is what its account hears (a `consumes` line of its own manifest; Engine): the
|
||||
// backup holder's word and the status view's `settled`, with no module named in this code.
|
||||
var engineHears []string
|
||||
// And the seats' events it consumes, on each seat's own subject, and the verbs it invokes, in the node's own name,
|
||||
// granted as a module's are (issue 1966: the seat list's verb seat-definitions and its two events).
|
||||
var engineHears, engineInvokes []string
|
||||
var engineWatches []Seat
|
||||
if r.Engine != nil {
|
||||
engineHears = r.Engine.Consumes
|
||||
engineHears, engineWatches, engineInvokes = r.Engine.Consumes, r.Engine.Watches, r.Engine.Invokes
|
||||
}
|
||||
for _, login := range nodeLoginsOf(r, node) {
|
||||
out = append(out, Principal{Kind: KindNode, Node: node, Login: login, Checks: checks, Runner: runner, Consumes: engineHears,
|
||||
Watches: engineWatches, Invokes: engineInvokes,
|
||||
MovesIntoTheVault: servesMoveIn(r), StatesItsKeyToTheVault: servesVerb(r, VaultNodeKey),
|
||||
AsksItsOwnSecrets: servesVerb(r, VaultOwn), AsksTheVaultToSign: servesVerb(r, VaultSign), Uses: asksSeatOf(r),
|
||||
FollowsGrantsUserList: bus, ServedReads: servedOn(served, node), RotatesItsLogin: servesVerb(r, VaultRotateNode)})
|
||||
|
||||
@@ -111,7 +111,12 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) {
|
||||
// The node-engine's own manifest, placed on every node (ADR 0563): its state, logs and streams alone, created by this
|
||||
// part; it adds no principal, since each node's engine speaks as the node's account.
|
||||
if m, ok := declared[buscompose.EngineModule]; ok {
|
||||
out.Engine = &buscompose.Declared{Module: m.Module, Consumes: m.Consumes, State: bucketsFor(m, in.Nodes), Logs: logsOf(m), Streams: streamsOf(m)}
|
||||
// What it consumes and invokes are its node account's: a seat's event also on the seat's own subject, as a module's
|
||||
// consumes are split (consumedOf), every consume still on its emitter's as before, and each verb it invokes in the
|
||||
// node's own name (issue 1966: the seat list's verb and events).
|
||||
_, watches := consumedOf(m, seats, declarers)
|
||||
out.Engine = &buscompose.Declared{Module: m.Module, Consumes: m.Consumes, Watches: watches, Invokes: m.Invokes,
|
||||
State: bucketsFor(m, in.Nodes), Logs: logsOf(m), Streams: streamsOf(m)}
|
||||
}
|
||||
return out, gaps
|
||||
}
|
||||
@@ -119,29 +124,7 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) {
|
||||
// declaredFor is mesh-controller's declaredFor (internal/inventory/busrecords.go), copied: one module's manifest as the
|
||||
// composer needs it.
|
||||
func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaration, declarers map[string]string) buscompose.Declared {
|
||||
var fromModules []string
|
||||
var watches []buscompose.Seat
|
||||
for _, c := range m.Consumes {
|
||||
emitter, event, named := strings.Cut(c, ".")
|
||||
if named {
|
||||
if s, isASeat := seats[emitter]; isASeat && s.Kinded {
|
||||
ev := strings.TrimSuffix(event, ".*")
|
||||
if catalogue.SeatSays(s.Emits, ev) {
|
||||
watches = append(watches, buscompose.Seat{Name: s.Name, Scope: s.Scope, Emits: []string{ev},
|
||||
Kinded: true, Proofs: s.Proofs, DeclaredBy: declarers[s.Name]})
|
||||
continue
|
||||
}
|
||||
}
|
||||
if s, isASeat := seats[emitter]; isASeat && catalogue.SeatSays(s.Emits, event) {
|
||||
// Its scope, and whether the node-engine holds it, place an event that names its holder's machine (ADR 0618 §4).
|
||||
compiled, _ := catalogue.SeatNamed(s.Name)
|
||||
watches = append(watches, buscompose.Seat{Name: s.Name, Scope: s.Scope, Emits: []string{event},
|
||||
HeldByTheEngine: compiled.HeldByTheEngine})
|
||||
continue
|
||||
}
|
||||
}
|
||||
fromModules = append(fromModules, c)
|
||||
}
|
||||
fromModules, watches := consumedOf(m, seats, declarers)
|
||||
d := buscompose.Declared{
|
||||
Module: m.Module, ToolRunner: catalogue.IsToolRunner(m), Emits: m.EmitsAll(), Consumes: fromModules, Watches: watches, Serves: m.Tools,
|
||||
Invokes: m.Invokes, Waits: waitsOf(m, seats), State: bucketsOf(m), Reads: m.Reads, Logs: logsOf(m),
|
||||
@@ -178,6 +161,36 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio
|
||||
return d
|
||||
}
|
||||
|
||||
// consumedOf splits what a manifest consumes as the composer grants it: a seat's event on the seat's own subject (its
|
||||
// watches), every other by its emitter's (novox/hq ADR 0118). A placed module and the node-engine's own manifest are split
|
||||
// alike.
|
||||
func consumedOf(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaration, declarers map[string]string) ([]string, []buscompose.Seat) {
|
||||
var fromModules []string
|
||||
var watches []buscompose.Seat
|
||||
for _, c := range m.Consumes {
|
||||
emitter, event, named := strings.Cut(c, ".")
|
||||
if named {
|
||||
if s, isASeat := seats[emitter]; isASeat && s.Kinded {
|
||||
ev := strings.TrimSuffix(event, ".*")
|
||||
if catalogue.SeatSays(s.Emits, ev) {
|
||||
watches = append(watches, buscompose.Seat{Name: s.Name, Scope: s.Scope, Emits: []string{ev},
|
||||
Kinded: true, Proofs: s.Proofs, DeclaredBy: declarers[s.Name]})
|
||||
continue
|
||||
}
|
||||
}
|
||||
if s, isASeat := seats[emitter]; isASeat && catalogue.SeatSays(s.Emits, event) {
|
||||
// Its scope, and whether the node-engine holds it, place an event that names its holder's machine (ADR 0618 §4).
|
||||
compiled, _ := catalogue.SeatNamed(s.Name)
|
||||
watches = append(watches, buscompose.Seat{Name: s.Name, Scope: s.Scope, Emits: []string{event},
|
||||
HeldByTheEngine: compiled.HeldByTheEngine})
|
||||
continue
|
||||
}
|
||||
}
|
||||
fromModules = append(fromModules, c)
|
||||
}
|
||||
return fromModules, watches
|
||||
}
|
||||
|
||||
// loadsABundle says the node's tool runtime loads a bundle of the module. The controller reads it from the resolved
|
||||
// manifest's Bundles, which a build derives and the indexer's manifest does not carry (the builder's build record names
|
||||
// the repository's manifest digest), so the same answer is derived here from `build.artifacts` by mesh-sdk's rule
|
||||
|
||||
Reference in New Issue
Block a user