Files
mesh-delivery/modules/mesh-grants/internal/buscompose/objects.go
T
jschoubben a78c5700fd
mesh/delivery merged: source.main moved to novox/mesh-delivery@fb69c8dee691
mesh/delivery-group group fix/1966-requires-state merged: every member is merged
mesh/check checking a78c5700fdf4 on fb69c8dee691 | novox/mesh-delivery#188 in group fix/1966-requires-state
Grant a module's bus state asked in requires to the accounts that run its code
The grants part wrote a binding into a membership on the bus module's
created, but granted its subjects to no account but the ledger's, so
distribution's program and the tool runner carrying it were refused their
own key-value, object store and stream entries (issue 1966, ADR 0595 §1,
design 25 § 12.2). The kept bindings now reach Declared in the same pass that
writes them into the membership, and one function, boundOf, grants each to
the module's own account and the tool runner, as its old section is granted;
a process's bus.state names a key-value entry as an old state. The composed
membership carries the bindings, so grants_membership's two sides agree.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-09 13:34:26 +01:00

118 lines
4.3 KiB
Go

package buscompose
import (
"fmt"
"slices"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// A module's own object bucket (novox/hq ADR 0592 §5, §9). The grants part creates it once, create-only, kept with no
// age limit and refusing a new object when full; only its module writes it; another module reads it only through a
// `reads` entry naming it and the owner's trusted config value of the bucket's grant naming that module, read-only, as a
// restricted stream is heard (ADR 0363 §4, §5). The condition when it fills is the bus module's (ADR 0595 §6).
// ObjectBucket is one object bucket a module owns, as the bus holds it.
type ObjectBucket struct {
Module string
Name string
// MaxMiB is its cap, catalogue.ObjectsLeastMiB to catalogue.ObjectsMostMiB.
MaxMiB int
// Restricted says it names a grant, so a module the owner's trusted config value names reads it.
Restricted bool
}
// Bucket is its name on the bus, `<module>_<name>` (catalogue.ObjectsBucket).
func (o ObjectBucket) Bucket() string { return catalogue.ObjectsBucket(o.Module, o.Name) }
// Stream is the stream the bus keeps it in.
func (o ObjectBucket) Stream() string { return "OBJ_" + o.Bucket() }
// ObjectsStreamConfig is a module's object bucket as the stream the client library's object store makes of it, with
// the mesh's rule: no age limit, its own cap, and a new object refused when full rather than an old one dropped (ADR
// 0592 §5); file storage and one replica, as a state's.
func ObjectsStreamConfig(o ObjectBucket) jetstream.StreamConfig {
b := o.Bucket()
return jetstream.StreamConfig{
Name: o.Stream(),
Description: fmt.Sprintf("%s's object bucket %q (novox/hq ADR 0592 §5): written by %s alone, read by the modules its "+
"grant names, kept with no age limit and at most %d MiB, a new object refused when full", o.Module, o.Name, o.Module, o.MaxMiB),
Subjects: []string{"$O." + b + ".C.>", "$O." + b + ".M.>"},
MaxBytes: int64(o.MaxMiB) * 1024 * 1024,
Storage: jetstream.FileStorage,
Replicas: 1,
Discard: jetstream.DiscardNew,
AllowRollup: true,
AllowDirect: true,
MaxMsgs: -1,
MaxConsumers: -1,
}
}
// objectReadGrants is what reading one object bucket needs and nothing more: binding it, a direct get of an object's
// metadata, an ordered consumer on its chunks or its names (a list or a watch), that consumer's deletion and flow
// control. No publish under `$O.`, so no write; no purge.
func objectReadGrants(bucket string) []string {
stream := "OBJ_" + bucket
return []string{
"$JS.API.STREAM.INFO." + stream,
"$JS.API.DIRECT.GET." + stream + ".>",
"$JS.API.CONSUMER.CREATE." + stream + ".>",
"$JS.API.CONSUMER.DELETE." + stream + ".>",
"$JS.FC." + stream + ".>",
}
}
// objectGrants is what a principal publishes for its module's own object buckets (written, and a replaced or deleted
// object's chunks purged) and for those it reads (read-only), never their creation, which is the grants part's.
func objectGrants(own, reads []string) []string {
var out []string
for _, b := range own {
if !safeSubject.MatchString(b) {
continue
}
out = append(out, "$O."+b+".>", "$JS.API.STREAM.PURGE.OBJ_"+b)
out = append(out, objectReadGrants(b)...)
}
for _, b := range reads {
if safeSubject.MatchString(b) {
out = append(out, objectReadGrants(b)...)
}
}
return out
}
// objectNames is the bus names of a module's own object buckets.
func objectNames(objects []ObjectBucket) []string {
out := make([]string, 0, len(objects))
for _, o := range objects {
out = append(out, o.Bucket())
}
return out
}
// writesOwnObjects is a module writing one of its own object buckets (ADR 0592 §5), itself or through the runtime that
// carries it, and nothing of another's.
func writesOwnObjects(p Principal, pattern string) bool {
switch p.Kind {
case KindModule:
// Its own, from the old section or bound in `requires` (boundOf, issue 1966).
for _, b := range append(slices.Clone(p.Objects), boundOf(p.Module, p.Bound).Objects...) {
if pattern == "$O."+b+".>" {
return true
}
}
case KindNodeTools:
for _, d := range p.Carries {
for _, b := range append(objectNames(d.Objects), boundOf(d.Module, d.Bindings).Objects...) {
if pattern == "$O."+b+".>" {
return true
}
}
}
}
return false
}