The grants part composes a ledger account per assignment of a module asking for a ledger, publishing only on its ledger histories and snapshots (issue 1966 D5)
This commit is contained in:
@@ -20,3 +20,5 @@ require (
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
)
|
||||
|
||||
replace git.novox.be/novox/mesh-sdk/go => ../../../mesh-sdk/go
|
||||
|
||||
@@ -49,6 +49,7 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) {
|
||||
t.Run("only declared streams are created", theGrantsPartCreatesOnlyDeclaredStreams)
|
||||
t.Run("a declared stream is created as the controller asserted it", declaredStreamsAreCreatedAsTheControllerAssertsThem)
|
||||
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)
|
||||
// ADR 0567 §3, §9: a node reads every key of the seat's placement 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"), " ")
|
||||
@@ -942,3 +943,75 @@ func everyNodesCompositions(t *testing.T) {
|
||||
t.Errorf("the view's membership issues %+v", m.State)
|
||||
}
|
||||
}
|
||||
|
||||
// 1966/AC20, AC21 (issue 1966 D5, ADR 0597 §9): a module asking for a ledger, assigned on two machines, gets one ledger account
|
||||
// on each, publishing only on that module's ledger histories and snapshots; the account is gone from a machine the module
|
||||
// leaves; and the module's own account, its process and the machine's tool runner hold no publish on its ledger.
|
||||
func aLedgerIsWrittenByItsLedgerAccountAlone(t *testing.T) {
|
||||
assigned := func(nodes ...string) []Principal {
|
||||
r := Records{Nodes: nodes, Assigned: map[string][]Declared{}}
|
||||
for _, n := range nodes {
|
||||
r.Assigned[n] = []Declared{
|
||||
{Module: RuntimeModule},
|
||||
{Module: "m", Ledgers: []string{"records", "audit"}, Processes: []ProcessPrincipal{{Process: "w"}}},
|
||||
{Module: "plain"},
|
||||
}
|
||||
}
|
||||
users, err := Users(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return users
|
||||
}
|
||||
ledgers := func(users []Principal) []string {
|
||||
var out []string
|
||||
for _, p := range users {
|
||||
if p.Kind == KindLedger {
|
||||
out = append(out, p.Username())
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
both := assigned("a", "b")
|
||||
if got := ledgers(both); !slices.Equal(got, []string{"ledger.a.m", "ledger.b.m"}) {
|
||||
t.Fatalf("ledger accounts %v, want ledger.a.m and ledger.b.m", got)
|
||||
}
|
||||
if got := ledgers(assigned("a")); !slices.Equal(got, []string{"ledger.a.m"}) {
|
||||
t.Fatalf("after m left b: ledger accounts %v, want ledger.a.m alone", got)
|
||||
}
|
||||
want := []string{"mesh.ledger.m.audit.history.>", "mesh.ledger.m.audit.snapshot.>",
|
||||
"mesh.ledger.m.records.history.>", "mesh.ledger.m.records.snapshot.>"}
|
||||
raw := []string{"mesh.ledger.m.records.history.decision/0290", "mesh.ledger.m.records.snapshot.decision/0290"}
|
||||
for _, p := range both {
|
||||
perms, err := PermissionsFor(p)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", p.Username(), err)
|
||||
}
|
||||
if p.Kind == KindLedger {
|
||||
if !slices.Equal(perms.Publish, want) || len(perms.PublishDeny) != 0 {
|
||||
t.Errorf("%s may publish %v, want %v alone", p.Username(), perms.Publish, want)
|
||||
}
|
||||
continue
|
||||
}
|
||||
for _, pattern := range perms.Publish {
|
||||
for _, subject := range raw {
|
||||
if SubjectsOverlap(pattern, subject) {
|
||||
t.Errorf("%s may publish %s, which reaches the ledger subject %s", p.Username(), pattern, subject)
|
||||
}
|
||||
}
|
||||
}
|
||||
if p.Kind == KindModule || p.Kind == KindNodeTools || p.Kind == KindProcess {
|
||||
if err := CheckWriters(p, want); err == nil {
|
||||
t.Errorf("%s was granted a raw publish on a ledger's history", p.Username())
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := CheckWriters(Principal{Kind: KindLedger, Node: "a", Module: "other", Ledgers: []string{"records"}}, want); err == nil {
|
||||
t.Error("another module's ledger account was granted m's ledger histories")
|
||||
}
|
||||
for _, bad := range []Principal{{Kind: KindLedger, Node: "a", Module: "m"}, {Kind: KindLedger, Node: "a", Module: "m", Ledgers: []string{"a.b"}}} {
|
||||
if _, err := PermissionsFor(bad); err == nil {
|
||||
t.Errorf("%+v was composed", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package buscompose
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A module's ledger and the one account that writes it (issue 1966 D5; novox/hq ADR 0597 §9, design 25 § 12.4).
|
||||
//
|
||||
// **Only the tool runner's ledger credential publishes on a ledger's history** (ADR 0597 §9): one per module assignment,
|
||||
// named ledger.<machine>.<module> (KindLedger), scoped to the subjects of that module's ledger histories and snapshots, and
|
||||
// gone when the module is unassigned from the machine. The module's own account, its processes and the machine's tool
|
||||
// runner hold no publish there, so a raw publish on a history is refused by the bus. The subjects are `mesh.ledger.<module>.
|
||||
// <local name>.history.<instance>` and `mesh.ledger.<module>.<local name>.snapshot.<instance>`, one subject per instance
|
||||
// (design 25 § 12.4); the bus module that creates the ledger's history stream (B2) takes them from here.
|
||||
|
||||
// LedgerHistorySubject is the subject a ledger's history is on, without the instance: an operation on instance I is on
|
||||
// `<LedgerHistorySubject>.<I>`.
|
||||
func LedgerHistorySubject(module, local string) string {
|
||||
return "mesh.ledger." + module + "." + local + ".history"
|
||||
}
|
||||
|
||||
// LedgerSnapshotSubject is the subject a ledger's instances' snapshots are on, without the instance.
|
||||
func LedgerSnapshotSubject(module, local string) string {
|
||||
return "mesh.ledger." + module + "." + local + ".snapshot"
|
||||
}
|
||||
|
||||
// LedgerPublishGrants are the publish patterns a module's ledger account is granted: the histories and the snapshots of the
|
||||
// ledgers named, every instance of each, and nothing else. Sorted.
|
||||
func LedgerPublishGrants(module string, locals []string) []string {
|
||||
var out []string
|
||||
for _, local := range locals {
|
||||
out = append(out, LedgerHistorySubject(module, local)+".>", LedgerSnapshotSubject(module, local)+".>")
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// writesLedger is a module's ledger account publishing on that module's own ledgers' histories and snapshots, and no other
|
||||
// principal (ADR 0597 §9).
|
||||
func writesLedger(p Principal, pattern string) bool {
|
||||
return p.Kind == KindLedger && slices.Contains(LedgerPublishGrants(p.Module, p.Ledgers), pattern)
|
||||
}
|
||||
|
||||
// ledgerSubjects are the subjects no principal but a ledger account may publish on, for the writers table.
|
||||
var ledgerSubjects = []string{"mesh.ledger.*.*.history.>", "mesh.ledger.*.*.snapshot.>"}
|
||||
|
||||
// ledgerPermissions are a ledger account's whole authority: publishing on its module's ledgers, and its own reply space for
|
||||
// the stream's acknowledgements. Refused when it names no ledger or a name that is no subject token.
|
||||
func ledgerPermissions(p Principal) (Permissions, error) {
|
||||
pub := LedgerPublishGrants(p.Module, p.Ledgers)
|
||||
if len(p.Ledgers) == 0 {
|
||||
return Permissions{}, fmt.Errorf("%s asks for no ledger, so its account would publish nowhere", p.Username())
|
||||
}
|
||||
for _, l := range p.Ledgers {
|
||||
if !safeSubject.MatchString(l) || strings.Contains(l, "_") {
|
||||
return Permissions{}, fmt.Errorf("%s: %q cannot be part of a subject: a permission is a subject pattern, and this would widen it", p.Username(), l)
|
||||
}
|
||||
}
|
||||
if err := CheckWriters(p, pub); err != nil {
|
||||
return Permissions{}, err
|
||||
}
|
||||
return Permissions{Publish: pub, Subscribe: []string{p.inbox()}}, nil
|
||||
}
|
||||
@@ -52,6 +52,10 @@ const (
|
||||
// exactly what its bus says — never root, never the operator account, and never able to answer a tool
|
||||
// call or a seat's verb (§3). One per process and machine.
|
||||
KindProcess Kind = "process"
|
||||
// KindLedger is one module assignment's ledger credential (novox/hq ADR 0597 §9, design 25 § 12.4): held by that
|
||||
// machine's tool runner, never by the module's own code, and the only account that publishes on that module's ledger
|
||||
// histories and snapshots. One per module assignment, whose module asks for a ledger in `requires`.
|
||||
KindLedger Kind = "ledger"
|
||||
)
|
||||
|
||||
// RuntimeModule is the module that IS the node's tool runtime (novox/hq ADR 0175). Where it is
|
||||
@@ -102,6 +106,9 @@ type Principal struct {
|
||||
Kind Kind
|
||||
Node string
|
||||
Module string
|
||||
// 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
|
||||
// Process is the process resource's id, for a KindProcess principal alone (novox/hq ADR 0436 §2):
|
||||
// Username and inbox are <node>.<module>.<process id>.
|
||||
Process string
|
||||
@@ -425,6 +432,10 @@ func (p Principal) Username() string {
|
||||
return p.Login
|
||||
}
|
||||
return "node." + p.Node
|
||||
case KindLedger:
|
||||
// `ledger.<machine>.<module>`: never `<node>.<module>`, which is the module's own account or the tool runner's,
|
||||
// so the module's code never holds it.
|
||||
return "ledger." + p.Node + "." + p.Module
|
||||
case KindProvisioner:
|
||||
// Named for the machine, as a node is: one client per machine, whichever module runs it, and never
|
||||
// `<node>.<module>`, which is the module's own account or the tool runner's.
|
||||
@@ -567,6 +578,11 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
return Permissions{Publish: pub, Subscribe: []string{p.inbox()}}, nil
|
||||
}
|
||||
|
||||
// A ledger account publishes on its module's ledger histories and snapshots and nothing else (issue 1966 D5).
|
||||
if p.Kind == KindLedger {
|
||||
return ledgerPermissions(p)
|
||||
}
|
||||
|
||||
if p.Kind == KindProcess && len(p.Removes) > 0 {
|
||||
pub, err := BusRemovalGrants(p.Removes, p.FilesFor)
|
||||
if err != nil {
|
||||
|
||||
@@ -103,6 +103,9 @@ type Declared struct {
|
||||
// composed whether or not the module itself has an account (NoAccount), because a process account is
|
||||
// delivered in its own secret, broker-<process id>, never the module's `broker`.
|
||||
Processes []ProcessPrincipal
|
||||
// Ledgers are the local names of the ledgers it asks for in `requires` (catalogue.StorageLedger); each assignment of a
|
||||
// module with any gets a ledger principal of its own (issue 1966 D5, ADR 0597 §9).
|
||||
Ledgers []string
|
||||
}
|
||||
|
||||
// ProcessPrincipal is one process of a module that declares bus (novox/hq ADR 0436 §1, §2), as the broker
|
||||
@@ -229,6 +232,11 @@ func Users(r Records) ([]Principal, error) {
|
||||
out[len(out)-1].FilesFor = sortedCopy(r.Nodes)
|
||||
}
|
||||
}
|
||||
// **A ledger account per assignment of a module that asks for a ledger** (issue 1966 D5, ADR 0597 §9): held by the
|
||||
// machine's tool runner, so it is composed whether or not the module has an account of its own.
|
||||
if len(d.Ledgers) > 0 {
|
||||
out = append(out, Principal{Kind: KindLedger, Node: node, Module: d.Module, Ledgers: sortedCopy(d.Ledgers)})
|
||||
}
|
||||
if runtimeHere && d.Module == RuntimeModule {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -236,6 +236,12 @@ var WritersTable = []WriterRow{
|
||||
ProvisionerAppliedBucket,
|
||||
Others: "the watchman reads it; a failure is that machine's failed apply",
|
||||
Subjects: []string{"prov.applied.*.>"}, Writes: ownProvisioner},
|
||||
// A ledger's history and its snapshots (novox/hq ADR 0597 §9, issue 1966 D5): the tool runner's ledger credential of
|
||||
// the owning module alone, so the module's own code, its processes and the runner's own account cannot write one raw.
|
||||
{State: "a ledger's history and snapshots", Writer: "the tool runner's ledger credential of the owning module",
|
||||
KeptIn: "the bus, the ledger's history stream and its snapshot subject",
|
||||
Others: "the owner reads them through the tool runner's calls; nobody else reads them (ADR 0596 §2)",
|
||||
Subjects: ledgerSubjects, Writes: writesLedger},
|
||||
}
|
||||
|
||||
// CheckWriters refuses a grant that lets a principal publish on a subject the writers table gives
|
||||
|
||||
@@ -139,6 +139,8 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio
|
||||
Served: catalogue.ServedSettings(m),
|
||||
// Its own object buckets (ADR 0592 §5).
|
||||
Objects: objectsOf(m),
|
||||
// The ledgers it asks for in `requires` (issue 1966 D5): each assignment gets a ledger account.
|
||||
Ledgers: ledgersOf(m),
|
||||
}
|
||||
d.Loads = loadsABundle(m)
|
||||
if _, reads := m.OwnSecrets["broker"]; !reads {
|
||||
@@ -282,6 +284,17 @@ func readsOfObjects(module, node string, reads, refused []string, manifests map[
|
||||
return states, objects, refusedOut
|
||||
}
|
||||
|
||||
// ledgersOf are the local names of the ledgers a manifest asks for in `requires`.
|
||||
func ledgersOf(m catalogue.Manifest) []string {
|
||||
var out []string
|
||||
for _, e := range m.BusState {
|
||||
if e.Storage == catalogue.StorageLedger {
|
||||
out = append(out, e.Name)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func logsOf(m catalogue.Manifest) []buscompose.Log {
|
||||
var out []buscompose.Log
|
||||
for _, l := range m.Logs {
|
||||
|
||||
Reference in New Issue
Block a user