GR-chunks (A-60, §681): the chunk subjects of a chunks verb go to the principal that answers it, the reverse to an account naming it; the keeper's standing restore of restricted streams
The seat list's verbs saying `chunks: true` (ChunkVerbsOf, as KeptEventsOf) give the principal that answers the verb (a module's own account for a seat it holds, the tool runner's for a carried module's) the publish of the chunk subjects and the subscription to the acks, and an account naming the verb by name in `invokes` the reverse; a `*` entry none. The bus module's keeper holds a standing grant of the restore subjects of the restricted streams, and only those. Builds: design 47 G §681, A-45, A-60 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -21,6 +21,7 @@ import (
|
||||
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
"git.novox.be/novox/mesh-sdk/go/announce"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
)
|
||||
|
||||
@@ -58,6 +59,7 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) {
|
||||
t.Run("every node's compositions are read by their named readers alone and written by their node", everyNodesCompositions)
|
||||
t.Run("a module's ledger is written by its ledger account alone", aLedgerIsWrittenByItsLedgerAccountAlone)
|
||||
t.Run("the bus module's process alone keeps every module's bus state and serves the broker's verbs", theBusModulesProcessKeepsBusState)
|
||||
t.Run("chunk subjects go to the principal that answers the verb, and the reverse to an account naming it", chunkSubjectsFollowTheAnswerer)
|
||||
// ADR 0567 §3, §9: a node reads every key of the seat's assignments and its candidates (it composes with the whole
|
||||
// proposal laid over), and none of its open proposals; it writes neither.
|
||||
reads := strings.Join(NodeComposeReads("laptop"), " ")
|
||||
@@ -1380,3 +1382,58 @@ func TestASeatsEventsArePublishedByItsHolderAlone(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// chunkSubjectsFollowTheAnswerer is design 47 G §681 as A-60 has it: the chunk subjects of a `chunks: true` verb are granted to the
|
||||
// principal that answers the verb (the tool runner's for a carried module's tools bundle), the reverse to an account naming the
|
||||
// verb by name in `invokes`, none to a `*` entry; and the keeper holds a standing restore grant of the restricted streams alone.
|
||||
func chunkSubjectsFollowTheAnswerer(t *testing.T) {
|
||||
defs := map[string]seatdef.Definition{meshbroker.Seat: {Seat: meshbroker.Seat, Verbs: []seatdef.Verb{{Name: "list"}, {Name: "snapshot-get", Chunks: true}}}}
|
||||
chunked := ChunkVerbsOf(defs)
|
||||
if len(chunked) != 1 || chunked[0] != (ChunkVerb{Seat: meshbroker.Seat, Verb: "snapshot-get"}) {
|
||||
t.Fatalf("the seat definitions' chunked verbs = %v, want the broker's snapshot-get alone", chunked)
|
||||
}
|
||||
chunks, acks := "mesh.seat.mesh-broker.chunk.snapshot-get.>", "mesh.seat.mesh-broker.ack.snapshot-get.>"
|
||||
r := Records{Nodes: []string{"a"}, Chunked: chunked, Assigned: map[string][]Declared{"a": {
|
||||
{Module: "node-tools", ToolRunner: true},
|
||||
{Module: "nats", SnapshotsTheBus: true, Holds: []Seat{{Name: meshbroker.Seat, Serves: []string{"list", "snapshot-get"}}},
|
||||
Processes: []ProcessPrincipal{{Process: "keeper", Keeps: true}}},
|
||||
{Module: "recovery", Invokes: []string{"seat:mesh-broker.snapshot-get"}, Processes: []ProcessPrincipal{{Process: "recover", Invokes: []string{"seat:mesh-broker.snapshot-get"}}}},
|
||||
{Module: "admin-ish", Invokes: []string{"*"}},
|
||||
{Module: "mod-x", Streams: []ModuleStream{{Module: "mod-x", Name: "SECRETS", Events: []string{"sealed"}, Restricted: true},
|
||||
{Module: "mod-x", Name: "PLAIN", Events: []string{"plain"}}}},
|
||||
}}}
|
||||
users, err := Users(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
perms := map[string]Permissions{}
|
||||
for _, u := range users {
|
||||
p, err := PermissionsFor(u)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", u.Username(), err)
|
||||
}
|
||||
perms[u.Username()] = p
|
||||
}
|
||||
has := func(list []string, s string) bool { return slices.Contains(list, s) }
|
||||
if runner := perms["a.node-tools"]; !has(runner.Publish, chunks) || !has(runner.Subscribe, acks) {
|
||||
t.Errorf("the tool runner, answering the verb for the bus module's tools bundle, may not send chunks and hear acks: %v / %v", runner.Publish, runner.Subscribe)
|
||||
}
|
||||
if rec := perms["a.recovery"]; !has(rec.Subscribe, chunks) || !has(rec.Publish, acks) || has(rec.Publish, chunks) {
|
||||
t.Errorf("the account naming the verb may not hear chunks and send acks, or may send chunks: %v / %v", rec.Publish, rec.Subscribe)
|
||||
}
|
||||
if rec := perms["a.recovery.recover"]; !has(rec.Subscribe, chunks) || !has(rec.Publish, acks) {
|
||||
t.Errorf("the process naming the verb may not hear chunks and send acks")
|
||||
}
|
||||
for _, u := range []string{"a.admin-ish", "a.nats.keeper"} {
|
||||
if p := perms[u]; has(p.Subscribe, chunks) || has(p.Publish, chunks) || has(p.Publish, acks) {
|
||||
t.Errorf("%s, naming no chunked verb, is granted chunk subjects: %v / %v", u, p.Publish, p.Subscribe)
|
||||
}
|
||||
}
|
||||
keeper := perms["a.nats.keeper"]
|
||||
if !MayPublish(keeper, "$JS.API.STREAM.RESTORE.SECRETS") || !MayPublish(keeper, "$JS.SNAPSHOT.RESTORE.SECRETS.abc") {
|
||||
t.Errorf("the keeper holds no standing restore of the restricted stream")
|
||||
}
|
||||
if MayPublish(keeper, "$JS.API.STREAM.RESTORE.PLAIN") || MayPublish(keeper, "$JS.API.STREAM.RESTORE.KV_mod-x_records") {
|
||||
t.Errorf("the keeper's restore grant reaches a stream that is not restricted")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package buscompose
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
)
|
||||
|
||||
// **The chunk subjects of a verb that sends a file** (design 47 G §681, A-45, A-60): a verb whose seat definition says `chunks: true`
|
||||
// has its answer's file sent in chunks on `mesh.seat.<seat>.chunk.<verb>.<call>.<n>`, each acknowledged by the caller on
|
||||
// `mesh.seat.<seat>.ack.<verb>.<call>.<n>`. They are granted to the principal that answers the verb (publish the chunks, subscribe to
|
||||
// the acknowledgements) and, reversed, to an account that names the verb by name in `invokes`; a `*` entry grants none.
|
||||
|
||||
// ChunkVerb is one verb of a seat whose definition says it sends chunks.
|
||||
type ChunkVerb struct{ Seat, Verb string }
|
||||
|
||||
// ChunkVerbsOf is every verb the seat definitions say send chunks, sorted. Pure.
|
||||
func ChunkVerbsOf(defs map[string]seatdef.Definition) []ChunkVerb {
|
||||
var out []ChunkVerb
|
||||
for _, d := range defs {
|
||||
for _, v := range d.Verbs {
|
||||
if v.Chunks {
|
||||
out = append(out, ChunkVerb{Seat: d.Seat, Verb: v.Name})
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
if out[i].Seat != out[j].Seat {
|
||||
return out[i].Seat < out[j].Seat
|
||||
}
|
||||
return out[i].Verb < out[j].Verb
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func chunkSubject(c ChunkVerb) string { return "mesh.seat." + c.Seat + ".chunk." + c.Verb + ".>" }
|
||||
func ackSubject(c ChunkVerb) string { return "mesh.seat." + c.Seat + ".ack." + c.Verb + ".>" }
|
||||
|
||||
// chunkGrants are the chunk subjects p is granted: as the principal that answers a chunked verb, the publish of its chunks and
|
||||
// the subscription to their acknowledgements (a module's own account for a seat it holds, the tool runner's for each seat a
|
||||
// carried module holds; a process serves nothing, ADR 0436 §3), and as an account that names a chunked verb by name in `invokes`,
|
||||
// the subscription to the chunks and the publish of the acknowledgements.
|
||||
func chunkGrants(p Principal) (pub, sub []string) {
|
||||
if len(p.Chunked) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var served []Seat
|
||||
switch p.Kind {
|
||||
case KindModule:
|
||||
served = p.Holds
|
||||
case KindNodeTools:
|
||||
for _, d := range p.Carries {
|
||||
served = append(served, d.Holds...)
|
||||
}
|
||||
}
|
||||
for _, c := range p.Chunked {
|
||||
for _, s := range served {
|
||||
if s.Name == c.Seat && slices.Contains(s.Serves, c.Verb) {
|
||||
pub, sub = append(pub, chunkSubject(c)), append(sub, ackSubject(c))
|
||||
break
|
||||
}
|
||||
}
|
||||
if slices.Contains(p.Invokes, "seat:"+c.Seat+"."+c.Verb) {
|
||||
sub, pub = append(sub, chunkSubject(c)), append(pub, ackSubject(c))
|
||||
}
|
||||
}
|
||||
return pub, sub
|
||||
}
|
||||
|
||||
// restoreGrants are the keeper's standing grant of the bus's stream restore subjects and their chunk subjects, for the bus names
|
||||
// of the streams ADR 0436 §3 restricts and only those (design 47 G §681, A-60): the restore request of each and the chunks the
|
||||
// snapshot is sent to.
|
||||
func restoreGrants(names []string) []string {
|
||||
var out []string
|
||||
for _, n := range names {
|
||||
if !safeSubject.MatchString(n) || strings.Contains(n, ".") {
|
||||
continue
|
||||
}
|
||||
out = append(out, "$JS.API.STREAM.RESTORE."+n, "$JS.SNAPSHOT.RESTORE."+n+".>")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// restrictedStreamNames are the bus names of the streams every placed module declares restricted (ADR 0363 §4), sorted, once each.
|
||||
func restrictedStreamNames(r Records) []string {
|
||||
seen := map[string]bool{}
|
||||
for _, mods := range r.Assigned {
|
||||
for _, d := range mods {
|
||||
for _, s := range d.Streams {
|
||||
if s.Restricted {
|
||||
seen[s.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out := make([]string, 0, len(seen))
|
||||
for n := range seen {
|
||||
out = append(out, n)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
@@ -131,6 +131,10 @@ type Principal struct {
|
||||
// §4, §5), and KeepsBusState are the streams it defines, every module's bus state by its name.
|
||||
KeepsBus bool
|
||||
KeepsBusState []string
|
||||
// KeepsRestricted are the bus names of the streams ADR 0436 §3 restricts, the keeper's standing restore grant (chunks.go).
|
||||
KeepsRestricted []string
|
||||
// Chunked are the verbs whose answers are sent in chunks (Records.Chunked), the grants of which chunkGrants derives.
|
||||
Chunked []ChunkVerb
|
||||
// SaysSeatEvents are the subjects of the seat mesh-broker's events the bus module's keeper says them on (the lead's
|
||||
// ruling, registry milestone 13:0x): `mesh.seat.mesh-broker.event.<event>` for each event the seat's definition
|
||||
// names, as a holder's own account would be granted them; set on the keeper alone.
|
||||
@@ -633,6 +637,18 @@ var BusSnapshotGrants = struct{ Publish []string }{Publish: []string{
|
||||
// PermissionsFor derives a principal's authority. Pure, and the only place authority is decided:
|
||||
// a permission that cannot be derived from a declaration is a permission nobody can explain.
|
||||
func PermissionsFor(p Principal) (Permissions, error) {
|
||||
perms, err := permissionsFor(p)
|
||||
if err != nil || p.SnapshotsTheBus && p.Kind == KindModule {
|
||||
return perms, err
|
||||
}
|
||||
// The chunk subjects of a verb that sends a file (design 47 G §681, A-60), to the principal that answers it and to an account
|
||||
// naming it in `invokes`.
|
||||
pub, sub := chunkGrants(p)
|
||||
perms.Publish, perms.Subscribe = append(perms.Publish, pub...), append(perms.Subscribe, sub...)
|
||||
return perms, nil
|
||||
}
|
||||
|
||||
func permissionsFor(p Principal) (Permissions, error) {
|
||||
for _, part := range []struct{ what, value string }{
|
||||
{"node", p.Node}, {"module", p.Module}, {"process", p.Process},
|
||||
} {
|
||||
|
||||
@@ -57,6 +57,8 @@ func processGrants(p Principal) (pub, sub []string, err error) {
|
||||
// module uses (the operator channel's accept.condition in its module's name).
|
||||
if p.KeepsBus {
|
||||
pub = append(pub, busStateKeeperGrants(p.KeepsBusState)...)
|
||||
// And its standing restore of the streams ADR 0436 §3 restricts, used on a `snapshot-restore` call alone (design 47 G §681, A-60).
|
||||
pub = append(pub, restoreGrants(p.KeepsRestricted)...)
|
||||
if c, ok := keeperConsumer(p); ok {
|
||||
pub = append(pub, keeperConsumerGrants(c)...)
|
||||
}
|
||||
|
||||
@@ -149,6 +149,9 @@ type Records struct {
|
||||
// Kept is every seat event kept in a stream of its own (kept.go): the grants part makes each stream, so its principal is
|
||||
// granted the create of each by name.
|
||||
Kept []KeptEvent
|
||||
// Chunked is every verb the seat definitions say sends chunks (chunks.go, design 47 G §681): their subjects are granted to the
|
||||
// principal that answers the verb and to an account naming it in `invokes`.
|
||||
Chunked []ChunkVerb
|
||||
// EngineEvents are the node-engine seat's events that carry parts in their subject, by local name: where a node's own
|
||||
// name sits in each (machineEvent).
|
||||
EngineEvents map[string]string
|
||||
@@ -270,6 +273,7 @@ func Users(r Records) ([]Principal, error) {
|
||||
if d.SnapshotsTheBus && pr.Keeps && !pr.Tools {
|
||||
k := &out[len(out)-1]
|
||||
k.KeepsBus, k.KeepsBusState = true, BusStateNames(r.BusState, r.Bound)
|
||||
k.KeepsRestricted = restrictedStreamNames(r)
|
||||
k.Consumes, k.Streams = d.Consumes, d.Streams
|
||||
k.Watches = d.Watches
|
||||
k.SaysSeatEvents = brokerSeatEvents(d.Holds)
|
||||
@@ -386,6 +390,7 @@ func Users(r Records) ([]Principal, error) {
|
||||
if slices.Contains(reads, ComposedRead) {
|
||||
out[i].Nodes = sortedCopy(r.Nodes)
|
||||
}
|
||||
out[i].Chunked = r.Chunked
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -185,6 +185,8 @@ type Inputs struct {
|
||||
// Kept is every seat event the seat list says is kept in a stream of its own (buscompose.KeptEventsOf): the part makes
|
||||
// each stream and each hearing module's durable on it. Empty while the list is unread.
|
||||
Kept []buscompose.KeptEvent
|
||||
// Chunked is every verb the seat list says sends its answer's file in chunks (design 47 G §681): the part grants their subjects.
|
||||
Chunked []buscompose.ChunkVerb
|
||||
// EventSubjects are, by seat, the subjects of its events that carry parts, by the event's local name, as the definitions
|
||||
// give them: where a node-scoped seat's event names its machine (buscompose.Seat.EventSubjects).
|
||||
EventSubjects map[string]map[string]string
|
||||
@@ -366,6 +368,7 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
|
||||
// consumer only gains filters, so one made with such an event's subject on EVENTS would keep it).
|
||||
if d, ok := gate.(SeatDefinitions); ok {
|
||||
in.Kept = buscompose.KeptEventsOf(d.Definitions())
|
||||
in.Chunked = buscompose.ChunkVerbsOf(d.Definitions())
|
||||
in.EventSubjects = eventSubjectsOf(d.Definitions())
|
||||
in.Defined = definedEventsOf(d.Definitions())
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) {
|
||||
out := buscompose.Records{Assigned: map[string][]buscompose.Declared{}, People: map[string][]string{},
|
||||
Interchangeable: map[string]bool{}, Enrolling: slices.Clone(in.Enrolling),
|
||||
// And each node's logins (ADR 0583 §1): both while it rotates.
|
||||
NodeLogins: in.NodeLogins, Kept: in.Kept, EngineEvents: in.EventSubjects[buscompose.EngineSeat],
|
||||
NodeLogins: in.NodeLogins, Kept: in.Kept, Chunked: in.Chunked, EngineEvents: in.EventSubjects[buscompose.EngineSeat],
|
||||
// What the bus module's process may define: every manifest's bus state, and what it said it created (DLV-G2).
|
||||
BusState: map[string][]catalogue.BusStateEntry{}, Bound: in.Bound}
|
||||
for module, m := range in.Manifests {
|
||||
|
||||
Reference in New Issue
Block a user