Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47e64f0284 | ||
|
|
6927b02d4d | ||
|
|
fda4859a95 | ||
|
|
a81717e53c | ||
|
|
d01839c29c | ||
|
|
f67d15536b |
@@ -111,6 +111,11 @@ func TestTheControlPlaneIsToldWhereTheNodePutTheStoreAndTheBroker(t *testing.T)
|
|||||||
Kind: inventory.BusController, Node: "anchor", Module: "mesh-controller"}, "bootstrap"); err != nil {
|
Kind: inventory.BusController, Node: "anchor", Module: "mesh-controller"}, "bootstrap"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
// Seeded with the credential the controller already uses, so it is sealed: an account counts as issued only once
|
||||||
|
// sealed (ADR 0543 §2, PR #467), and genesis's seed is the credential in use.
|
||||||
|
if err := open.inventory.MarkBusAccountSealed(ctx, "anchor.mesh-controller"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
if _, err := assign(ctx, open, "anchor", "mesh-controller"); err != nil {
|
if _, err := assign(ctx, open, "anchor", "mesh-controller"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,6 +217,11 @@ func TestTheAdaptersInputCarriesTheOwnSecretsTheBoxesTheLicencesAndTheBus(t *tes
|
|||||||
Module: "objects"}, "a-password"); err != nil {
|
Module: "objects"}, "a-password"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
// Minted and sealed to its node: an account counts as issued, and is named among the minted, only once sealed
|
||||||
|
// (ADR 0543 §2, PR #467).
|
||||||
|
if err := inv.MarkBusAccountSealed(ctx, "anchor.objects"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
if err := inv.PutBusMembership(ctx, "laptop", "sealed-membership"); err != nil {
|
if err := inv.PutBusMembership(ctx, "laptop", "sealed-membership"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,8 +88,6 @@ func composeModulesByNode(ctx context.Context, open *stores, nodes []string) (ma
|
|||||||
}
|
}
|
||||||
return u.Policy(), u.From, u.Why
|
return u.Policy(), u.From, u.Why
|
||||||
}
|
}
|
||||||
// The memberships of each node's modules are issued by its composition (memberships.go): read once for the pass.
|
|
||||||
issuing, issuingErr := membershipsFor(ctx, open, link.OverNATS{Conn: bus.Conn(), JS: bus.Context()})
|
|
||||||
sort.Strings(nodes)
|
sort.Strings(nodes)
|
||||||
out := map[string][]string{}
|
out := map[string][]string{}
|
||||||
errsOf := map[string][]error{}
|
errsOf := map[string][]error{}
|
||||||
@@ -117,13 +115,6 @@ func composeModulesByNode(ctx context.Context, open *stores, nodes []string) (ma
|
|||||||
errsOf[node] = append(errsOf[node], fmt.Errorf("%s: %w", node, err))
|
errsOf[node] = append(errsOf[node], fmt.Errorf("%s: %w", node, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Its modules' memberships, before its compositions (issue 249): a failure is the node's, and its compositions
|
|
||||||
// are still written; the take-in that asked tries the node again.
|
|
||||||
if issuingErr != nil {
|
|
||||||
errsOf[node] = append(errsOf[node], fmt.Errorf("%s: %w", node, issuingErr))
|
|
||||||
} else if err := issuing.issue(ctx, node, declared); err != nil {
|
|
||||||
errsOf[node] = append(errsOf[node], fmt.Errorf("%s: %w", node, err))
|
|
||||||
}
|
|
||||||
// The provisioner's plans of the modules the node's composition hands to it, for the assignments mirror.
|
// The provisioner's plans of the modules the node's composition hands to it, for the assignments mirror.
|
||||||
if err := open.inventory.RecordComposedPlans(ctx, node, plansKept(declared)); err != nil {
|
if err := open.inventory.RecordComposedPlans(ctx, node, plansKept(declared)); err != nil {
|
||||||
errsOf[node] = append(errsOf[node], fmt.Errorf("%s: its provisioner's plans: %w", node, err))
|
errsOf[node] = append(errsOf[node], fmt.Errorf("%s: its provisioner's plans: %w", node, err))
|
||||||
|
|||||||
@@ -1,186 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"slices"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
||||||
"github.com/nats-io/nats.go"
|
|
||||||
|
|
||||||
"github.com/novox/mesh-controller/internal/broker"
|
|
||||||
"github.com/novox/mesh-controller/internal/link"
|
|
||||||
)
|
|
||||||
|
|
||||||
// **A node's composition issues its modules' memberships** (novox/hq ADR 0160, ADR 0201; issue 249 met again under
|
|
||||||
// issue 1945).
|
|
||||||
//
|
|
||||||
// A membership is what an assignment serves and reaches: its subjects, its seats, and the state, logs, streams and waits
|
|
||||||
// its manifest declares, published last-per-subject on the AssignmentsStream, where the tool runner and every bus process
|
|
||||||
// read and follow their own. A push issued them before it sent a declaration; release 6 removed the push and nothing
|
|
||||||
// issued them after, so a module taken in with a new state, read or stream was granted it on the bus and refused it by its
|
|
||||||
// runtime, which keeps a module to the membership last issued (2026-10-25: mesh-delivery-gate's `conditions`, on novox
|
|
||||||
// from 20:33). Composing a node is now where its memberships are issued: from the same records the bus's grants are
|
|
||||||
// composed from (so what a runtime serves and what its account may are one composition), with the private network's
|
|
||||||
// addresses and what each module receives from the node's composition, before its compositions are written (the code
|
|
||||||
// arriving first was refused its own state, issue 249).
|
|
||||||
//
|
|
||||||
// Only a membership that differs from the one last issued is published, and only over that one, so a pass that changes
|
|
||||||
// nothing re-serves nothing and a newer one issued meanwhile is not overwritten (issues 930, 934).
|
|
||||||
|
|
||||||
// membershipPass is what one composer pass issues every node's memberships from, read once for the pass.
|
|
||||||
type membershipPass struct {
|
|
||||||
records broker.Records
|
|
||||||
where broker.Placements
|
|
||||||
bus link.OverNATS
|
|
||||||
}
|
|
||||||
|
|
||||||
// membershipsFor reads the records, asserts the memberships' stream and every declared bucket and log (a membership
|
|
||||||
// names them, and a module whose state does not exist fails its first read), and judges which machines are root-free
|
|
||||||
// (ADR 0259 §8: a channel's verified sender is composed only from one) when a seat promises one, as a push did.
|
|
||||||
func membershipsFor(ctx context.Context, open *stores, bus link.OverNATS) (*membershipPass, error) {
|
|
||||||
records, err := open.inventory.BusRecords(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("the memberships are not issued: %w", err)
|
|
||||||
}
|
|
||||||
raiser := broker.OnConn(bus.Conn)
|
|
||||||
for _, s := range broker.MeshStreams() {
|
|
||||||
if s.Name != broker.AssignmentsStream {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := raiser.EnsureStream(s); err != nil {
|
|
||||||
return nil, fmt.Errorf("the memberships are not issued: their stream: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buckets, err := open.inventory.DeclaredBuckets(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("the memberships are not issued: the modules' state: %w", err)
|
|
||||||
}
|
|
||||||
if _, err := broker.RaiseBuckets(raiser, buckets); err != nil {
|
|
||||||
return nil, fmt.Errorf("the memberships are not issued: the modules' state could not be asserted: %w", err)
|
|
||||||
}
|
|
||||||
logs, err := open.inventory.DeclaredLogs(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("the memberships are not issued: the modules' logs: %w", err)
|
|
||||||
}
|
|
||||||
if _, err := broker.RaiseLogs(raiser, logs); err != nil {
|
|
||||||
return nil, fmt.Errorf("the memberships are not issued: the modules' logs could not be asserted: %w", err)
|
|
||||||
}
|
|
||||||
var whys map[string]string
|
|
||||||
if promisesVerifiedSender(records) {
|
|
||||||
records.RootFree, whys = rootVerdictsOf(judgeRootFree(ctx, newRootReader(ctx, open.inventory, bus.Conn),
|
|
||||||
records.Nodes, time.Now()))
|
|
||||||
}
|
|
||||||
where := broker.PlacementsOf(records, records.Interchangeable)
|
|
||||||
for _, line := range droppedVerifiedSenders(records, where, whys) {
|
|
||||||
fmt.Println(line)
|
|
||||||
}
|
|
||||||
return &membershipPass{records: records, where: where, bus: bus}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// promisesVerifiedSender says a seat some module holds promises a verified sender: only then is the root-free judgement,
|
|
||||||
// a discovery on the bus, worth its seconds. Pure.
|
|
||||||
func promisesVerifiedSender(records broker.Records) bool {
|
|
||||||
for _, declared := range records.Assigned {
|
|
||||||
for _, d := range declared {
|
|
||||||
for _, s := range d.Holds {
|
|
||||||
if slices.Contains(s.Capabilities, "verified-sender") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// issue issues node's memberships on the bus, from its composition's addresses and received contributions.
|
|
||||||
func (p *membershipPass) issue(ctx context.Context, node string, declared sendable) error {
|
|
||||||
issued, err := issueComposedMemberships(node, p.records, p.where, declared.Mesh, declared.Received,
|
|
||||||
func(node, module string) ([]byte, uint64, error) {
|
|
||||||
m, err := p.bus.JS.GetLastMsg(broker.AssignmentsStream, broker.MembershipSubject(node, module), nats.Context(ctx))
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
return m.Data, m.Sequence, nil
|
|
||||||
},
|
|
||||||
func(node, module string, body []byte, seq uint64) error {
|
|
||||||
return p.bus.PublishMembershipAfter(ctx, node, module, body, seq)
|
|
||||||
})
|
|
||||||
if len(issued) > 0 {
|
|
||||||
fmt.Printf("memberships: %s: issued %s\n", node, strings.Join(issued, ", "))
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// issueComposedMemberships composes the membership of every module assigned to node and publishes each one that differs
|
|
||||||
// from the one last issued, over that one (seq 0 when none was: only if none was issued meanwhile). It answers the
|
|
||||||
// modules it issued; every failure is tried past and joined in the error.
|
|
||||||
func issueComposedMemberships(node string, records broker.Records, where broker.Placements, mesh []string,
|
|
||||||
received map[string]map[string][]catalogue.Contribution,
|
|
||||||
last func(node, module string) ([]byte, uint64, error),
|
|
||||||
publish func(node, module string, body []byte, seq uint64) error) ([]string, error) {
|
|
||||||
var issued []string
|
|
||||||
var errs []error
|
|
||||||
for _, d := range records.Assigned[node] {
|
|
||||||
body, err := composedMembership(node, d, where, mesh, received[d.Module])
|
|
||||||
if err != nil {
|
|
||||||
errs = append(errs, fmt.Errorf("%s on %s: its membership: %w", d.Module, node, err))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
was, seq, err := last(node, d.Module)
|
|
||||||
switch {
|
|
||||||
case errors.Is(err, nats.ErrMsgNotFound):
|
|
||||||
was, seq = nil, 0
|
|
||||||
case err != nil:
|
|
||||||
errs = append(errs, fmt.Errorf("%s on %s: its membership as last issued could not be read: %w", d.Module, node, err))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if sameMembership(was, body) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := publish(node, d.Module, body, seq); err != nil {
|
|
||||||
errs = append(errs, fmt.Errorf("%s on %s: its membership could not be issued: %w", d.Module, node, err))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
issued = append(issued, d.Module)
|
|
||||||
}
|
|
||||||
sort.Strings(issued)
|
|
||||||
return issued, errors.Join(errs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// composedMembership is one assignment's membership as a node's composition issues it: MembershipFor, with the private
|
|
||||||
// network's addresses and what the module receives, by requirement (ADR 0167).
|
|
||||||
func composedMembership(node string, d broker.Declared, where broker.Placements, mesh []string,
|
|
||||||
received map[string][]catalogue.Contribution) ([]byte, error) {
|
|
||||||
m := broker.MembershipFor(node, d, where)
|
|
||||||
m.Mesh = mesh
|
|
||||||
for requirement, given := range received {
|
|
||||||
raw, err := json.Marshal(given)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if m.Receives == nil {
|
|
||||||
m.Receives = map[string]json.RawMessage{}
|
|
||||||
}
|
|
||||||
m.Receives[requirement] = raw
|
|
||||||
}
|
|
||||||
return json.Marshal(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
// sameMembership says two memberships say the same, whatever the order of their fields. Pure.
|
|
||||||
func sameMembership(was, now []byte) bool {
|
|
||||||
if was == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
var a, b any
|
|
||||||
if json.Unmarshal(was, &a) != nil || json.Unmarshal(now, &b) != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return reflect.DeepEqual(a, b)
|
|
||||||
}
|
|
||||||
@@ -108,83 +108,14 @@ func TestADroppedVerifiedSenderIsSaidWithItsReason(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// **A node's composition issues every membership that changed** (issue 249 met again under issue 1945): release 6 removed
|
func TestAMembershipNeverIssuedIsLeftToItsOwnSend(t *testing.T) {
|
||||||
// the push that issued memberships, and mesh-delivery-gate taken in with a new state `conditions` was granted its bucket and
|
records, _ := verifiedSenderRecords(map[string]bool{"novox": true})
|
||||||
// refused it by the tool runner, which reads the membership last issued (2026-10-25, novox). The self-check's reissue
|
bus := &fakeBus{last: map[string][]byte{}, published: map[string][]byte{}, seq: map[string]uint64{}}
|
||||||
// leaves one never issued alone: the composition issues it.
|
var said []string
|
||||||
func TestAComposedNodeIssuesEveryMembershipThatChanged(t *testing.T) {
|
n := reissueVerifiedSenders(records, broker.PlacementsOf(records, nil), bus.get, bus.put,
|
||||||
gate := func(states ...string) broker.Declared {
|
func(f string, a ...any) { said = append(said, f) })
|
||||||
d := broker.Declared{Module: "mesh-delivery-gate"}
|
if n != 0 || len(said) != 0 {
|
||||||
for _, s := range states {
|
t.Fatalf("issued %d, said %v", n, said)
|
||||||
d.State = append(d.State, broker.Bucket{Module: "mesh-delivery-gate", Name: s})
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
records := func(d broker.Declared) broker.Records {
|
|
||||||
return broker.Records{Nodes: []string{"novox"}, Assigned: map[string][]broker.Declared{"novox": {d}}}
|
|
||||||
}
|
|
||||||
issuedAs := func(d broker.Declared, mesh []string) []byte {
|
|
||||||
body, err := composedMembership("novox", d, broker.PlacementsOf(records(d), nil), mesh, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
return body
|
|
||||||
}
|
|
||||||
newer := []byte(`{"node":"novox","module":"mesh-delivery-gate","newer":true}`)
|
|
||||||
for _, c := range []struct {
|
|
||||||
name string
|
|
||||||
before []byte // the membership last issued, nil for none
|
|
||||||
between bool // another issued meanwhile, after the read
|
|
||||||
issued []string
|
|
||||||
states []string // what the bus holds after, by state name; nil when it must hold newer
|
|
||||||
err string
|
|
||||||
}{
|
|
||||||
{name: "never issued is issued", issued: []string{"mesh-delivery-gate"}, states: []string{"asks", "conditions"}},
|
|
||||||
{name: "a new state is issued", before: issuedAs(gate("asks"), []string{"10.0.0.1"}),
|
|
||||||
issued: []string{"mesh-delivery-gate"}, states: []string{"asks", "conditions"}},
|
|
||||||
{name: "unchanged is not issued again", before: issuedAs(gate("asks", "conditions"), []string{"10.0.0.1"}),
|
|
||||||
states: []string{"asks", "conditions"}},
|
|
||||||
{name: "a newer one meanwhile is not overwritten", before: issuedAs(gate("asks"), []string{"10.0.0.1"}),
|
|
||||||
between: true, err: "wrong last sequence"},
|
|
||||||
} {
|
|
||||||
t.Run(c.name, func(t *testing.T) {
|
|
||||||
r := records(gate("asks", "conditions"))
|
|
||||||
where := broker.PlacementsOf(r, nil)
|
|
||||||
bus := &fakeBus{last: map[string][]byte{}, published: map[string][]byte{}, seq: map[string]uint64{}}
|
|
||||||
if c.before != nil {
|
|
||||||
bus.last["novox/mesh-delivery-gate"], bus.seq["novox/mesh-delivery-gate"] = c.before, 1
|
|
||||||
}
|
|
||||||
if c.between {
|
|
||||||
bus.between = func() { bus.last["novox/mesh-delivery-gate"], bus.seq["novox/mesh-delivery-gate"] = newer, 2 }
|
|
||||||
}
|
|
||||||
if n := reissueVerifiedSenders(r, where, bus.get, bus.put, func(string, ...any) {}); n != 0 {
|
|
||||||
t.Fatalf("the self-check's reissue issued %d", n)
|
|
||||||
}
|
|
||||||
issued, err := issueComposedMemberships("novox", r, where, []string{"10.0.0.1"}, nil, bus.get, bus.put)
|
|
||||||
if c.err == "" && err != nil || c.err != "" && (err == nil || !strings.Contains(err.Error(), c.err)) {
|
|
||||||
t.Fatalf("error %v, want %q", err, c.err)
|
|
||||||
}
|
|
||||||
if strings.Join(issued, ",") != strings.Join(c.issued, ",") {
|
|
||||||
t.Fatalf("issued %v, want %v", issued, c.issued)
|
|
||||||
}
|
|
||||||
if c.states == nil {
|
|
||||||
if string(bus.last["novox/mesh-delivery-gate"]) != string(newer) {
|
|
||||||
t.Fatalf("the newer membership was overwritten: %s", bus.last["novox/mesh-delivery-gate"])
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var m broker.Membership
|
|
||||||
if err := json.Unmarshal(bus.last["novox/mesh-delivery-gate"], &m); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
var names []string
|
|
||||||
for _, s := range m.State {
|
|
||||||
names = append(names, s.Name)
|
|
||||||
}
|
|
||||||
if strings.Join(names, ",") != strings.Join(c.states, ",") || strings.Join(m.Mesh, ",") != "10.0.0.1" {
|
|
||||||
t.Fatalf("the bus holds states %v and mesh %v, want %v", names, m.Mesh, c.states)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package broker
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// Defends issue 1790 and novox/hq ADR 0547 §5 (the operator, 2026-10-26 08:54): the grants part may create the stream
|
||||||
|
// of each bucket, log and stream a placed module's manifest declares, by its own name, and read it back; nothing
|
||||||
|
// wider, nothing updated or deleted, and no other principal so. Held byte-equal in the controller's copy.
|
||||||
|
func TestTheGrantsPartCreatesOnlyDeclaredStreams(t *testing.T) {
|
||||||
|
records := Records{
|
||||||
|
Nodes: []string{"novox"},
|
||||||
|
Assigned: map[string][]Declared{"novox": {
|
||||||
|
{Module: "mesh-delivery-builder",
|
||||||
|
State: []Bucket{{Module: "mesh-delivery-builder", Name: "steps", History: 5}},
|
||||||
|
Logs: []Log{{Module: "mesh-delivery-builder", Name: "builds"}},
|
||||||
|
Streams: []ModuleStream{{Module: "mesh-delivery-builder", Name: "BUILDASKS", Events: []string{"ask"}}}},
|
||||||
|
{Module: GrantsModule, State: []Bucket{{Module: GrantsModule, Name: "assertions"}}},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
users, err := Users(records)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
perms := map[string]Permissions{}
|
||||||
|
for _, u := range users {
|
||||||
|
if u.Kind != KindModule {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p, err := PermissionsFor(u)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: %v", u.Username(), err)
|
||||||
|
}
|
||||||
|
perms[u.Module] = p
|
||||||
|
}
|
||||||
|
for _, row := range []struct {
|
||||||
|
who, subject string
|
||||||
|
may bool
|
||||||
|
}{
|
||||||
|
{GrantsModule, "$JS.API.STREAM.CREATE.KV_mesh-delivery-builder_steps", true},
|
||||||
|
{GrantsModule, "$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps", true},
|
||||||
|
{GrantsModule, "$JS.API.STREAM.CREATE.LOG_mesh-delivery-builder_builds", true},
|
||||||
|
{GrantsModule, "$JS.API.STREAM.CREATE.BUILDASKS", true},
|
||||||
|
{GrantsModule, "$JS.API.STREAM.CREATE.KV_mesh-grants_assertions", true},
|
||||||
|
{GrantsModule, "$JS.API.STREAM.UPDATE.KV_mesh-delivery-builder_steps", false},
|
||||||
|
{GrantsModule, "$JS.API.STREAM.DELETE.KV_mesh-delivery-builder_steps", false},
|
||||||
|
{GrantsModule, "$JS.API.STREAM.PURGE.KV_mesh-delivery-builder_steps", false},
|
||||||
|
{GrantsModule, "$JS.API.STREAM.CREATE.KV_mesh-delivery-builder_undeclared", false},
|
||||||
|
{GrantsModule, "$JS.API.STREAM.CREATE.EVENTS", false},
|
||||||
|
{GrantsModule, "$JS.API.STREAM.CREATE.ASSIGNMENTS", false},
|
||||||
|
{GrantsModule, "$JS.API.STREAM.INFO.EVENTS", false},
|
||||||
|
{"mesh-delivery-builder", "$JS.API.STREAM.CREATE.KV_mesh-delivery-builder_steps", false},
|
||||||
|
} {
|
||||||
|
if got := MayPublish(perms[row.who], row.subject); got != row.may {
|
||||||
|
t.Errorf("%s may publish %s: %v, want %v", row.who, row.subject, got, row.may)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A name that is no plain token is never granted, and a module other than the grants part is never granted one.
|
||||||
|
if got := CreateStreamGrants([]string{"*", ">", "KV_a.b", "KV_a_b"}); len(got) != 2 {
|
||||||
|
t.Errorf("grants for wildcard names: %v", got)
|
||||||
|
}
|
||||||
|
other, err := PermissionsFor(Principal{Kind: KindModule, Node: "novox", Module: "shop", CreatesStreams: []string{"KV_shop_x"}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if MayPublish(other, "$JS.API.STREAM.CREATE.KV_shop_x") {
|
||||||
|
t.Error("a module other than the grants part was granted a create")
|
||||||
|
}
|
||||||
|
// The writers table refuses the grants part an update, and a create of a stream it was not given.
|
||||||
|
g := Principal{Kind: KindModule, Node: "novox", Module: GrantsModule, CreatesStreams: []string{"KV_a_b"}}
|
||||||
|
if err := CheckWriters(g, []string{"$JS.API.STREAM.CREATE.KV_a_b"}); err != nil {
|
||||||
|
t.Errorf("the declared create refused: %v", err)
|
||||||
|
}
|
||||||
|
for _, refused := range []string{"$JS.API.STREAM.UPDATE.KV_a_b", "$JS.API.STREAM.CREATE.KV_a_c", "$JS.API.STREAM.CREATE.*"} {
|
||||||
|
if CheckWriters(g, []string{refused}) == nil {
|
||||||
|
t.Errorf("%s was not refused", refused)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,6 +72,17 @@ func TestAnAccountMayReadItsOwnMembershipAndNoOthers(t *testing.T) {
|
|||||||
has(t, perms.Subscribe, "mesh.assignment.anchor.postgres")
|
has(t, perms.Subscribe, "mesh.assignment.anchor.postgres")
|
||||||
has(t, perms.Publish, "$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.anchor.postgres")
|
has(t, perms.Publish, "$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.anchor.postgres")
|
||||||
hasNot(t, perms.Subscribe, "mesh.assignment.>")
|
hasNot(t, perms.Subscribe, "mesh.assignment.>")
|
||||||
|
hasNot(t, perms.Publish, "mesh.assignment.*.*")
|
||||||
|
// The grants part alone publishes memberships (ADR 0547 §5), and defines no stream.
|
||||||
|
grants, err := PermissionsFor(Principal{Kind: KindModule, Node: "anchor", Module: GrantsModule, PasswordHash: "x"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
has(t, grants.Publish, "mesh.assignment.*.*")
|
||||||
|
has(t, grants.Publish, "$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.*.*")
|
||||||
|
hasNot(t, perms.Publish, "$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.*.*")
|
||||||
|
hasNot(t, grants.Publish, "$JS.API.STREAM.CREATE.>")
|
||||||
|
hasNot(t, grants.Publish, "mesh.assignment.>")
|
||||||
}
|
}
|
||||||
|
|
||||||
// The runtime arriving on a machine changes nothing about what each module is issued (to-be 38 WP2):
|
// The runtime arriving on a machine changes nothing about what each module is issued (to-be 38 WP2):
|
||||||
|
|||||||
@@ -217,6 +217,11 @@ type Principal struct {
|
|||||||
// BoundBuckets are the providers' buckets of entries, `<module>_bound`, a provisioner client reads its own
|
// BoundBuckets are the providers' buckets of entries, `<module>_bound`, a provisioner client reads its own
|
||||||
// machine's keys of (mesh-sdk go/provider, issue 679): every bucket named bound of a module the mesh places.
|
// machine's keys of (mesh-sdk go/provider, issue 679): every bucket named bound of a module the mesh places.
|
||||||
BoundBuckets []string
|
BoundBuckets []string
|
||||||
|
// CreatesStreams are the streams the grants part's principal alone may create (novox/hq ADR 0547 §5; the operator,
|
||||||
|
// 2026-10-26 08:54): the stream of every bucket, log and stream a manifest of a module placed anywhere declares,
|
||||||
|
// by name. Created only, never updated or deleted: a new module state is on the bus when the grants part hears its
|
||||||
|
// manifest, not at the controller's next start (issue 1790).
|
||||||
|
CreatesStreams []string
|
||||||
|
|
||||||
// PasswordHash is the bcrypt hash the mesh minted. The plaintext is sealed to the principal
|
// PasswordHash is the bcrypt hash the mesh minted. The plaintext is sealed to the principal
|
||||||
// and never appears here: this file is written to a node's disk and read by a server, and a
|
// and never appears here: this file is written to a node's disk and read by a server, and a
|
||||||
@@ -915,6 +920,15 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
|||||||
sub = append(sub, BusConnectSubject, BusDisconnectSubject)
|
sub = append(sub, BusConnectSubject, BusDisconnectSubject)
|
||||||
pub = append(pub, BusConnzSubject)
|
pub = append(pub, BusConnzSubject)
|
||||||
}
|
}
|
||||||
|
// The grants part alone publishes the modules' memberships into the assignments stream (novox/hq ADR 0547 §5;
|
||||||
|
// the operator, 2026-10-25 22:31): one per subject, `mesh.assignment.<node>.<module>`, reading the last one of each
|
||||||
|
// first so it publishes only what changed, and no stream it defines.
|
||||||
|
if p.Module == GrantsModule {
|
||||||
|
pub = append(pub, "mesh.assignment.*.*", "$JS.API.DIRECT.GET."+AssignmentsStream+".mesh.assignment.*.*")
|
||||||
|
// And creating each stream a manifest declares, by its own name (the operator, 2026-10-26 08:54; issue
|
||||||
|
// 1790): the create and the read of that one stream, never an update, a delete or a stream not declared.
|
||||||
|
pub = append(pub, CreateStreamGrants(p.CreatesStreams)...)
|
||||||
|
}
|
||||||
// The view alone reads the build seats' workers for the backlog of asks (issue 1887 A8a).
|
// The view alone reads the build seats' workers for the backlog of asks (issue 1887 A8a).
|
||||||
if p.Module == ViewModule {
|
if p.Module == ViewModule {
|
||||||
pub = append(pub, BuildBacklogReads()...)
|
pub = append(pub, BuildBacklogReads()...)
|
||||||
@@ -1581,6 +1595,24 @@ func ComposeAccounts(principals []Principal) (string, error) {
|
|||||||
// from them. No other principal is granted the subjects below, and nothing is a wildcard over them.
|
// from them. No other principal is granted the subjects below, and nothing is a wildcard over them.
|
||||||
const WatchmanModule = "mesh-delivery-watchman"
|
const WatchmanModule = "mesh-delivery-watchman"
|
||||||
|
|
||||||
|
// CreateStreamGrants is what creating each named stream needs, and nothing wider (novox/hq ADR 0547 §5; the operator,
|
||||||
|
// 2026-10-26 08:54): its create, and its information to read back what it holds. Each by its own name: a name that
|
||||||
|
// is not one plain subject token is left out, since a wildcard there would be every stream.
|
||||||
|
func CreateStreamGrants(streams []string) []string {
|
||||||
|
var out []string
|
||||||
|
for _, s := range streams {
|
||||||
|
if !safeSubject.MatchString(s) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, "$JS.API.STREAM.CREATE."+s, "$JS.API.STREAM.INFO."+s)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// GrantsModule is the one module whose principal publishes the modules' memberships (novox/hq ADR 0547 §5): the grants
|
||||||
|
// part, which composes them from the manifests and placement. It defines no stream and is granted no other subject here.
|
||||||
|
const GrantsModule = "mesh-grants"
|
||||||
|
|
||||||
// The bus's own events, imported into MESH from the default system account `$SYS` under subjects of MESH's own
|
// The bus's own events, imported into MESH from the default system account `$SYS` under subjects of MESH's own
|
||||||
// (novox/hq ADR 0431 §1, issue 1147): the server says an account's connections opening and closing only to the system
|
// (novox/hq ADR 0431 §1, issue 1147): the server says an account's connections opening and closing only to the system
|
||||||
// account, and the list of an account's connections only to a request made there. `$SYS` holds no user, so nobody
|
// account, and the list of an account's connections only to a request made there. `$SYS` holds no user, so nobody
|
||||||
|
|||||||
@@ -207,6 +207,11 @@ func Users(r Records) ([]Principal, error) {
|
|||||||
PerMachine: perMachineNames(d.State), KeyedReads: d.KeyedReads,
|
PerMachine: perMachineNames(d.State), KeyedReads: d.KeyedReads,
|
||||||
Logs: logNames(d.Logs), Streams: d.Streams, Hears: ComposedHears(d), Queues: d.Queues,
|
Logs: logNames(d.Logs), Streams: d.Streams, Hears: ComposedHears(d), Queues: d.Queues,
|
||||||
})
|
})
|
||||||
|
// **The grants part creates what a manifest declares** (novox/hq ADR 0547 §5; the operator, 2026-10-26
|
||||||
|
// 08:54): every placed module's buckets, logs and streams, each by its stream's name.
|
||||||
|
if d.Module == GrantsModule {
|
||||||
|
out[len(out)-1].CreatesStreams = declaredStreamsOf(r)
|
||||||
|
}
|
||||||
// **The vault's holder asks the desks and the issuing providers itself** (issue 723), under the
|
// **The vault's holder asks the desks and the issuing providers itself** (issue 723), under the
|
||||||
// account its own runtime runs on (ADR 0259 §8's runs-as): the one module granted it.
|
// account its own runtime runs on (ADR 0259 §8's runs-as): the one module granted it.
|
||||||
if holdsSeat(d, VaultSeat) {
|
if holdsSeat(d, VaultSeat) {
|
||||||
@@ -418,3 +423,29 @@ func boundBucketsOf(r Records) []string {
|
|||||||
func runnerServes(d Declared) bool {
|
func runnerServes(d Declared) bool {
|
||||||
return d.Module == RuntimeModule || d.NoAccount || d.Loads || len(d.Serves) > 0 || len(d.Holds) > 0 || len(d.Waits) > 0 || len(d.Checks) > 0
|
return d.Module == RuntimeModule || d.NoAccount || d.Loads || len(d.Serves) > 0 || len(d.Holds) > 0 || len(d.Waits) > 0 || len(d.Checks) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// declaredStreamsOf is the stream of every bucket, log and stream a module placed anywhere declares, once each, in name
|
||||||
|
// order (novox/hq ADR 0547 §5, issue 1790): `KV_<module>_<state>`, `LOG_<module>_<log>` and each module stream's own
|
||||||
|
// name. What the grants part may create, and nothing beside.
|
||||||
|
func declaredStreamsOf(r Records) []string {
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, mods := range r.Assigned {
|
||||||
|
for _, d := range mods {
|
||||||
|
for _, b := range d.State {
|
||||||
|
seen["KV_"+b.Bucket()] = true
|
||||||
|
}
|
||||||
|
for _, l := range d.Logs {
|
||||||
|
seen[l.Stream()] = true
|
||||||
|
}
|
||||||
|
for _, s := range d.Streams {
|
||||||
|
seen[s.Name] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out := make([]string, 0, len(seen))
|
||||||
|
for s := range seen {
|
||||||
|
out = append(out, s)
|
||||||
|
}
|
||||||
|
sort.Strings(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,6 +37,17 @@ type WriterRow struct {
|
|||||||
// isController, and the other writers' tests, are who a row's writer is as a principal.
|
// isController, and the other writers' tests, are who a row's writer is as a principal.
|
||||||
func isController(p Principal, _ string) bool { return p.Kind == KindController }
|
func isController(p Principal, _ string) bool { return p.Kind == KindController }
|
||||||
|
|
||||||
|
// createsDeclared is the controller, or the grants part creating one stream a manifest declares, by its own name: the
|
||||||
|
// create alone, never an update or a delete (novox/hq ADR 0547 §5; the operator, 2026-10-26 08:54).
|
||||||
|
func createsDeclared(p Principal, pattern string) bool {
|
||||||
|
if isController(p, pattern) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
name, ok := strings.CutPrefix(pattern, "$JS.API.STREAM.CREATE.")
|
||||||
|
return ok && p.Kind == KindModule && p.Module == GrantsModule && slices.Contains(p.CreatesStreams, name) &&
|
||||||
|
safeSubject.MatchString(name)
|
||||||
|
}
|
||||||
|
|
||||||
// ownMachine is a node writing a subject whose third token is its own name, and no wildcard there.
|
// ownMachine is a node writing a subject whose third token is its own name, and no wildcard there.
|
||||||
func ownMachine(p Principal, pattern string) bool {
|
func ownMachine(p Principal, pattern string) bool {
|
||||||
tokens := strings.Split(pattern, ".")
|
tokens := strings.Split(pattern, ".")
|
||||||
@@ -170,7 +181,8 @@ var WritersTable = []WriterRow{
|
|||||||
// bucket's stream (ADR 0201), which defines nothing the mesh keeps.
|
// bucket's stream (ADR 0201), which defines nothing the mesh keeps.
|
||||||
Subjects: []string{"$JS.API.STREAM.CREATE.>", "$JS.API.STREAM.UPDATE.>", "$JS.API.STREAM.DELETE.>",
|
Subjects: []string{"$JS.API.STREAM.CREATE.>", "$JS.API.STREAM.UPDATE.>", "$JS.API.STREAM.DELETE.>",
|
||||||
"$JS.API.CONSUMER.DURABLE.CREATE.>"},
|
"$JS.API.CONSUMER.DURABLE.CREATE.>"},
|
||||||
Writes: isController},
|
Writes: createsDeclared,
|
||||||
|
Shared: "the grants part creates a stream a manifest declares, by its name, and never updates or deletes one (novox/hq ADR 0547 §5; the operator, 2026-10-26 08:54)"},
|
||||||
{State: "builds and their outcomes", Writer: "the build seat's holder", KeptIn: "its own state",
|
{State: "builds and their outcomes", Writer: "the build seat's holder", KeptIn: "its own state",
|
||||||
Others: "the controller asks",
|
Others: "the controller asks",
|
||||||
Subjects: []string{"mesh.seat.node-build-agent.event.built", "mesh.seat.mesh-build-machine.event.built"},
|
Subjects: []string{"mesh.seat.node-build-agent.event.built", "mesh.seat.mesh-build-machine.event.built"},
|
||||||
|
|||||||
Reference in New Issue
Block a user