diff --git a/modules/mesh-grants/internal/buscompose/engine.go b/modules/mesh-grants/internal/buscompose/engine.go index 3dadd65b..ecb4366a 100644 --- a/modules/mesh-grants/internal/buscompose/engine.go +++ b/modules/mesh-grants/internal/buscompose/engine.go @@ -85,6 +85,17 @@ func engineGrants(p Principal) (pub, sub []string) { sub = append(sub, k.Filter) } } + // And the kept events of other seats it follows (the recovery's ended and restored): each stream read through an ordered + // consumer made for the read, with no durable of its own, and the stream's own info (design 47 G §554, A-108). + for _, h := range p.EngineHears { + if !safeSubject.MatchString(h.Stream) { + continue + } + pub = append(pub, "$JS.API.STREAM.INFO."+h.Stream, + "$JS.API.CONSUMER.CREATE."+h.Stream, "$JS.API.CONSUMER.CREATE."+h.Stream+".>", + "$JS.API.CONSUMER.MSG.NEXT."+h.Stream+".*", "$JS.API.CONSUMER.INFO."+h.Stream+".*", + "$JS.API.CONSUMER.DELETE."+h.Stream+".*") + } sub = append(sub, announcing(EngineSeat)...) pub = append(pub, discovering()...) tp, ts := seatTrafficGrants(SeatTrafficOf(occ.EngineAsker(p.Node), nil, p.Uses, nil)) diff --git a/modules/mesh-grants/internal/buscompose/nats.go b/modules/mesh-grants/internal/buscompose/nats.go index a3872693..926aaff8 100644 --- a/modules/mesh-grants/internal/buscompose/nats.go +++ b/modules/mesh-grants/internal/buscompose/nats.go @@ -124,6 +124,9 @@ type Principal struct { // (engineGrants). EngineEvents map[string]string EngineKept []KeptEvent + // EngineHears are the kept events of other seats its engine consumes (design 47 G §554, A-108: the events ended and + // restored of the seat mesh-recovery), each stream followed through an ordered consumer made for the read. + EngineHears []Heard // Ledgers are the local names of the ledgers its module asks for in `requires`, for a KindLedger principal alone // (issue 1966 D5): its whole authority is publishing on those ledgers' histories and snapshots (LedgerPublishGrants). Ledgers []string diff --git a/modules/mesh-grants/internal/buscompose/recovery_engine_test.go b/modules/mesh-grants/internal/buscompose/recovery_engine_test.go new file mode 100644 index 00000000..11f66d56 --- /dev/null +++ b/modules/mesh-grants/internal/buscompose/recovery_engine_test.go @@ -0,0 +1,49 @@ +package buscompose + +import ( + "testing" +) + +// Design 47 G §406, §554, A-108: the node-engine of a node is granted to hear the seat mesh-recovery's kept events ended and +// restored through an ordered consumer, to ask the verbs snapshot-kept, restore-kept and kept-points of the seat, and to call +// the catch-up tool of a module placed on its node, on its own node's tool subject and no other module's tool. +func TestTheNodeEngineIsGrantedWhatItNeedsOfTheRecoverySeat(t *testing.T) { + kept := []KeptEvent{ + {Seat: "mesh-recovery", Event: "ended", Stream: "SEAT_mesh-recovery_ended", Filter: "mesh.seat.mesh-recovery.event.ended.*.*"}, + {Seat: "mesh-recovery", Event: "restored", Stream: "SEAT_mesh-recovery_restored", Filter: "mesh.seat.mesh-recovery.event.restored.*.*"}, + } + records := Records{Nodes: []string{"one"}, Kept: kept, + Engine: &Declared{Module: EngineModule, Consumes: []string{"mesh-recovery.ended", "mesh-recovery.restored"}, + Invokes: []string{"seat:mesh-recovery.snapshot-kept", "seat:mesh-recovery.restore-kept", "seat:mesh-recovery.kept-points"}}, + Assigned: map[string][]Declared{"one": {{Module: "keeper", CatchUps: []string{"catch_up"}}, {Module: "plain"}}}} + users, err := Users(records) + if err != nil { + t.Fatal(err) + } + var node Principal + for _, u := range users { + if u.Kind == KindNode { + node = u + } + } + perms, err := PermissionsFor(node) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{ + "$JS.API.CONSUMER.CREATE.SEAT_mesh-recovery_ended", "$JS.API.CONSUMER.CREATE.SEAT_mesh-recovery_ended.>", + "$JS.API.CONSUMER.MSG.NEXT.SEAT_mesh-recovery_ended.*", "$JS.API.CONSUMER.DELETE.SEAT_mesh-recovery_restored.*", + "$JS.API.STREAM.INFO.SEAT_mesh-recovery_restored", + "mesh.seat.mesh-recovery.tool.snapshot-kept", "mesh.seat.mesh-recovery.tool.restore-kept", "mesh.seat.mesh-recovery.tool.kept-points", + "mesh.mod.keeper.tool.catch_up.one", + } { + if !MayPublish(perms, want) { + t.Errorf("the node-engine of one may not publish %s", want) + } + } + for _, bad := range []string{"mesh.mod.plain.tool.catch_up.one", "mesh.mod.keeper.tool.other.one", "$JS.API.CONSUMER.CREATE.SEAT_mesh-recovery_set-aside"} { + if MayPublish(perms, bad) { + t.Errorf("the node-engine of one may publish %s", bad) + } + } +} diff --git a/modules/mesh-grants/internal/buscompose/users.go b/modules/mesh-grants/internal/buscompose/users.go index c5d133c1..2f9c62a5 100644 --- a/modules/mesh-grants/internal/buscompose/users.go +++ b/modules/mesh-grants/internal/buscompose/users.go @@ -53,6 +53,9 @@ type Declared struct { Watches []Seat // Invokes are the tools it calls, `.` or `*` (novox/hq ADR 0152). Invokes []string + // CatchUps are the module's own tools that bring a kept data forward after a restore of it, as its manifest's kept data + // name them (design 47 G §406, §631, A-108): the node-engine on a node that places the module is granted to call each. + CatchUps []string // Waits are the event families a session may wait on through the tool runner, the tool runner's module's // alone (novox/hq ADR 0367 §2): its account subscribes them for a call, through no consumer. Waits []Wait @@ -223,9 +226,20 @@ func Users(r Records) ([]Principal, error) { if r.Engine != nil { engineHears, engineWatches, engineInvokes = r.Engine.Consumes, r.Engine.Watches, r.Engine.Invokes } + // The node-engine's hearing of kept events of other seats (the recovery's ended and restored) and its call of each + // catch-up tool of a module placed on its node (design 47 G §406, §554, A-108), granted as the seat's grants name them. + _, engineKeptHears := SplitKept(r.Kept, engineHears) + engineInvokes = slices.Clone(engineInvokes) + for _, d := range r.Assigned[node] { + for _, tool := range d.CatchUps { + if t := d.Module + "." + tool; !slices.Contains(engineInvokes, t) { + engineInvokes = append(engineInvokes, t) + } + } + } 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, EngineEvents: r.EngineEvents, EngineKept: keptOfSeat(r.Kept, EngineSeat), + EngineHears: engineKeptHears, Watches: engineWatches, Invokes: engineInvokes, EngineEvents: r.EngineEvents, EngineKept: keptOfSeat(r.Kept, EngineSeat), 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)}) diff --git a/modules/mesh-grants/internal/grants/catchups_test.go b/modules/mesh-grants/internal/grants/catchups_test.go new file mode 100644 index 00000000..94985bae --- /dev/null +++ b/modules/mesh-grants/internal/grants/catchups_test.go @@ -0,0 +1,21 @@ +package grants + +import ( + "slices" + "testing" + + "git.novox.be/novox/mesh-sdk/go/catalogue" +) + +// Design 47 G §406, §631, A-108: the catch-up tools a manifest's kept data name are the module's own, each once, whichever +// kind of kept data names them. +func TestCatchUpsAreTheToolsTheManifestsKeptDataName(t *testing.T) { + m, err := catalogue.Decode([]byte(`{"module":"keeper","version":"1.0","state":[{"name":"seen","form":2,"catch-up":{"tool":"seen_again","arguments":{"all":true}}},"plain"],` + + `"logs":[{"name":"lines","form":1,"catch-up":"seen_again"}],"streams":[{"name":"ev","events":["a"],"max-age-days":1,"max-mib":1,"form":1,"catch-up":{"tool":"ev_again"}}]}`)) + if err != nil { + t.Fatal(err) + } + if got := catchUpsOf(m); !slices.Equal(got, []string{"seen_again", "ev_again"}) && !slices.Equal(got, []string{"ev_again", "seen_again"}) { + t.Errorf("catchUpsOf = %v, want seen_again and ev_again once each", got) + } +} diff --git a/modules/mesh-grants/internal/grants/records.go b/modules/mesh-grants/internal/grants/records.go index 50e2a5f5..e195923f 100644 --- a/modules/mesh-grants/internal/grants/records.go +++ b/modules/mesh-grants/internal/grants/records.go @@ -156,6 +156,7 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio Ledgers: ledgersOf(m), } d.Loads = loadsABundle(m) + d.CatchUps = catchUpsOf(m) if _, reads := m.OwnSecrets["broker"]; !reads { d.NoAccount = true } @@ -608,3 +609,15 @@ func unionFields(dst, src reflect.Value) { } } } + +// catchUpsOf are the catch-up tools a manifest's kept data name, each once (design 47 G §406, §631): the module's own tools +// the node-engine calls after a restore of that data, which it is granted to call on its own node (A-108). +func catchUpsOf(m catalogue.Manifest) []string { + var tools []string + for _, k := range catalogue.KeptDeclarations(m) { + if k.CatchUp != nil && k.CatchUp.Tool != "" && !slices.Contains(tools, k.CatchUp.Tool) { + tools = append(tools, k.CatchUp.Tool) + } + } + return tools +}