G-delivery 5 (grants half): a read-only state is written by the process naming it alone
The bus module's spend record must not be writable by the tool runner. A state declared read-only gets read on the module's own account and the tool runner carrying it (direct get, a watch's consumer) and no publish on its $KV subjects; the process whose bus state names it writes it. The judge rule and the manifest field follow with mesh-sdk's read-only. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
@@ -1097,7 +1097,8 @@ func theBusModulesProcessKeepsBusState(t *testing.T) {
|
||||
Emits: []string{"created", "refused"}}},
|
||||
Consumes: []string{"mesh-delivery-indexer.manifest-accepted"}, Uses: []Seat{{Name: "operator-channel", Accepts: []string{"condition"}, Emits: []string{"decided"}, ByCaller: []string{"condition", "decided"}}},
|
||||
Streams: []ModuleStream{{Module: "nats", Name: "BROKER_EVENTS", Events: []string{"created", "refused"}, MaxAgeDays: 30, MaxMiB: 256}},
|
||||
Processes: []ProcessPrincipal{{Process: "keeper", State: []string{"asked"}, Keeps: true}, {Process: "other"},
|
||||
State: []Bucket{{Module: "nats", Name: "spent", ReadOnly: true}},
|
||||
Processes: []ProcessPrincipal{{Process: "keeper", State: []string{"asked", "spent"}, Keeps: true}, {Process: "other"},
|
||||
{Process: "nats-tools", Tools: true, Removes: []string{catalogue.RemovesEngineActs}}}},
|
||||
{Module: "mod-x", Processes: []ProcessPrincipal{{Process: "sneak", Keeps: true}},
|
||||
Streams: []ModuleStream{{Module: "mod-x", Name: "SECRETS", Events: []string{"sealed"}, Restricted: true}}},
|
||||
@@ -1142,6 +1143,23 @@ func theBusModulesProcessKeepsBusState(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Review round 2 (security F2): nats' read-only state spent is written by the process naming it in its bus state
|
||||
// alone; the tool runner carrying nats, and nats' own account, read it (a direct get, a watch's consumer) and publish
|
||||
// nothing on its $KV subjects, so neither writes nor deletes a spend record.
|
||||
for _, sub := range []string{"$KV.nats_spent.ask-1"} {
|
||||
if !MayPublish(keeper, sub) {
|
||||
t.Errorf("the process naming spent may not write %s", sub)
|
||||
}
|
||||
}
|
||||
for _, u := range []string{"a.node-tools"} {
|
||||
p := perms[u]
|
||||
if MayPublish(p, "$KV.nats_spent.ask-1") {
|
||||
t.Errorf("%s may write or delete nats' read-only spent", u)
|
||||
}
|
||||
if u == "a.node-tools" && (!MayPublish(p, "$JS.API.DIRECT.GET.KV_nats_spent.$KV.nats_spent.ask-1") || !MayPublish(p, "$JS.API.CONSUMER.CREATE.KV_nats_spent.x")) {
|
||||
t.Errorf("the tool runner may not read nats' spent")
|
||||
}
|
||||
}
|
||||
for _, s := range []string{"EVENTS", "KV_mesh-grants_memberships", "KV_mod-x_other"} {
|
||||
if MayPublish(keeper, "$JS.API.STREAM.CREATE."+s) || MayPublish(keeper, "$JS.API.STREAM.DELETE."+s) {
|
||||
t.Errorf("the bus module's process may define %s", s)
|
||||
|
||||
@@ -189,6 +189,8 @@ type Principal struct {
|
||||
// PerMachine is the local names of its state keyed by machine, reaching the key named for Node and
|
||||
// no other; KeyedReads the keys of others' state it reads one key at a time (novox/hq ADR 0260).
|
||||
PerMachine []string
|
||||
// ReadOnly is the local names of its state only its module's processes write: read, never written (review round 2).
|
||||
ReadOnly []string
|
||||
KeyedReads []KeyedRead
|
||||
// Logs is the local names of the logs this principal's module keeps (novox/hq ADR 0297).
|
||||
Logs []string
|
||||
@@ -987,7 +989,7 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
|
||||
// 5. Its state, and the state of others it reads (novox/hq ADR 0201): every one read and
|
||||
// watched, its own written too.
|
||||
kept := stateAccess{Module: p.Module, Node: p.Node, Keeps: p.State, PerMachine: p.PerMachine, Reads: p.Reads,
|
||||
kept := stateAccess{Module: p.Module, Node: p.Node, Keeps: p.State, PerMachine: p.PerMachine, ReadOnly: p.ReadOnly, Reads: p.Reads,
|
||||
KeyedReads: p.KeyedReads, Nodes: p.Nodes}
|
||||
pub = append(pub, stateGrants(kept)...)
|
||||
// And it hears each of those buckets created, so one not on the bus at its start is bound when it appears
|
||||
@@ -1095,7 +1097,7 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
// each assignment is issued, as it keeps each module's events under that module's own name.
|
||||
for _, d := range p.Carries {
|
||||
pub = append(pub, stateGrants(stateAccess{Module: d.Module, Node: p.Node, Keeps: stateNames(d.State),
|
||||
PerMachine: perMachineNames(d.State), Reads: d.Reads, KeyedReads: d.KeyedReads, Nodes: p.Nodes})...)
|
||||
PerMachine: perMachineNames(d.State), ReadOnly: readOnlyNames(d.State), Reads: d.Reads, KeyedReads: d.KeyedReads, Nodes: p.Nodes})...)
|
||||
pub = append(pub, objectGrants(objectNames(d.Objects), d.ReadsObjects)...)
|
||||
// A bundle reads every node's compositions on the runtime's own connection (ADR 0593 §6), which hears a node's
|
||||
// bucket made later by any stream's creation.
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-sdk/go/announce"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -40,6 +41,9 @@ type Bucket struct {
|
||||
// KeyTTL says the bucket allows a time to live per key (novox/hq ADR 0433 §1): its owner writes each key with its
|
||||
// own, and the server writes a delete marker, kept KeyTTLMarkerTTL, when it ends.
|
||||
KeyTTL bool
|
||||
// ReadOnly says only the module's processes that name it in their bus state write it (review round 2): the module's
|
||||
// own account and the tool runner carrying it read it, and publish nothing on its subjects.
|
||||
ReadOnly bool
|
||||
// Whole is the bucket's name where it is not BucketName(Module, Name): one node's bucket of a state kept one bucket per
|
||||
// node, the node-engine's compositions `composed-<node>` (ADR 0593 §6, EngineComposedBuckets).
|
||||
Whole string
|
||||
@@ -154,7 +158,9 @@ func stateGrants(a stateAccess) []string {
|
||||
continue
|
||||
}
|
||||
read(bucket, "")
|
||||
out = append(out, "$KV."+bucket+".>")
|
||||
if !slices.Contains(a.ReadOnly, name) {
|
||||
out = append(out, "$KV."+bucket+".>")
|
||||
}
|
||||
}
|
||||
// **And it says what it keeps** (novox/hq ADR 0495 clause 2, issue 1790): a module keeping state announces it on its
|
||||
// own event subject, from its code; refused, its readers are read as before.
|
||||
@@ -255,12 +261,25 @@ type KeyedRead struct {
|
||||
type stateAccess struct {
|
||||
Module, Node string
|
||||
Keeps, PerMachine []string
|
||||
Reads []string
|
||||
KeyedReads []KeyedRead
|
||||
// ReadOnly are the names of Keeps only a process writes: read here, never written.
|
||||
ReadOnly []string
|
||||
Reads []string
|
||||
KeyedReads []KeyedRead
|
||||
// Nodes are every node, whose bucket of a per-node state (delivery.ComposedRead) a read of it reaches.
|
||||
Nodes []string
|
||||
}
|
||||
|
||||
// readOnlyNames is the local names of a module's buckets only its processes write.
|
||||
func readOnlyNames(buckets []Bucket) []string {
|
||||
var out []string
|
||||
for _, b := range buckets {
|
||||
if b.ReadOnly {
|
||||
out = append(out, b.Name)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// perMachineNames is the local names of a module's buckets keyed by machine.
|
||||
func perMachineNames(buckets []Bucket) []string {
|
||||
var out []string
|
||||
|
||||
@@ -281,7 +281,7 @@ func Users(r Records) ([]Principal, error) {
|
||||
Emits: d.Emits, Consumes: d.Consumes, Serves: d.Serves,
|
||||
Holds: d.Holds, Uses: d.Uses, Watches: d.Watches, Invokes: d.Invokes,
|
||||
State: stateNames(d.State), Reads: d.Reads, ReadLogs: d.ReadLogs, SnapshotsTheBus: d.SnapshotsTheBus,
|
||||
PerMachine: perMachineNames(d.State), KeyedReads: d.KeyedReads,
|
||||
PerMachine: perMachineNames(d.State), ReadOnly: readOnlyNames(d.State), KeyedReads: d.KeyedReads,
|
||||
Logs: logNames(d.Logs), Streams: d.Streams, Hears: ComposedHears(d), Queues: d.Queues,
|
||||
Objects: objectNames(d.Objects), ReadsObjects: d.ReadsObjects,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user