Files
mesh-controller/internal/broker/controller_buckets_test.go
T
mesh-adminandClaude Opus 5.5 234dea9d29 Promise the vault's intake verbs and let the controller ask them in its own name (issue 675)
The vault takes a value only from the controller, which it knows from the
subject that names its caller. The grant is composed from the records, as
the issued tools' is, so the installer's first user list is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:07:04 +00:00

143 lines
5.5 KiB
Go

package broker
import (
"context"
"slices"
"strings"
"testing"
"time"
"github.com/nats-io/nats.go/jetstream"
"github.com/novox/mesh-controller/internal/testbus"
)
// **The controller may write every bucket it writes** (novox/hq to-be 45 §1, issue 269). Writing a
// key is a publish to the bucket's own subject, which the management interface's grant does not
// cover: the cancelled sets' writes timed out for want of this, and the controller's own buckets
// would have.
func TestTheControllerMayWriteEveryBucketItWrites(t *testing.T) {
p, err := PermissionsFor(Principal{Kind: KindController, PasswordHash: "x"})
if err != nil {
t.Fatal(err)
}
want := []string{"$KV." + CallsBucket + ".>", "$KV." + HandActsBucket + ".>"}
for _, seat := range seatsTheControllerAsks {
if hasCancelledSet(seat) {
want = append(want, "$KV."+CancelledSetName(seat)+".>")
}
}
for _, subject := range want {
if !slices.Contains(p.Publish, subject) {
t.Errorf("the controller may not publish %s, so it cannot write that bucket", subject)
}
}
if slices.Contains(p.Publish, "$KV.>") {
t.Error("the controller may write any bucket, a module's state included")
}
}
// **A machine's node tools may say they are there, as that machine and no other** (novox/hq to-be 45
// S11), and the controller may hear the bus's advisories and ask who answers — read-only, named.
func TestTheWatchedSignalsMayBeSaidAndHeard(t *testing.T) {
tools, err := PermissionsFor(Principal{Kind: KindNodeTools, Node: "anchor", Module: RuntimeModule, PasswordHash: "x"})
if err != nil {
t.Fatal(err)
}
if !slices.Contains(tools.Publish, "mesh.control.anchor.tools-alive") {
t.Error("the node tools may not say they are there")
}
for _, s := range tools.Publish {
if strings.Contains(s, "tools-alive") && s != "mesh.control.anchor.tools-alive" {
t.Errorf("the node tools may say %s", s)
}
}
controller, err := PermissionsFor(Principal{Kind: KindController, PasswordHash: "x"})
if err != nil {
t.Fatal(err)
}
for _, s := range BusAdvisories {
if !slices.Contains(controller.Subscribe, s) {
t.Errorf("the controller may not hear %s", s)
}
}
if !slices.Contains(controller.Publish, "$SRV.INFO") || slices.Contains(controller.Subscribe, "$JS.EVENT.>") {
t.Error("the controller may not ask who answers, or hears every API call")
}
}
// The controller's record of what it asked the operator is bounded (correctness review of 2026-10-08): one
// value a key, a month's age, and a size it cannot outgrow.
func TestWhatTheControllerAskedIsBounded(t *testing.T) {
js, err := Dial(testbus.URL(t))
if err != nil {
t.Fatal(err)
}
defer js.Close()
if err := js.EnsureControllerBuckets(); err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
kv, err := jetstream.New(js.Conn())
if err != nil {
t.Fatal(err)
}
bucket, err := kv.KeyValue(ctx, AskedBucket)
if err != nil {
t.Fatal(err)
}
status, err := bucket.Status(ctx)
if err != nil {
t.Fatal(err)
}
info := status.(*jetstream.KeyValueBucketStatus).StreamInfo()
if status.History() != 1 || status.TTL() != AskedKeptFor || info.Config.MaxBytes <= 0 || info.Config.MaxBytes > 64<<20 {
t.Errorf("history %d, age %s, bytes %d", status.History(), status.TTL(), info.Config.MaxBytes)
}
}
// **The controller may ask the vault's intake verbs in its own name** (design 57's V2, issue 675): the vault
// takes a value only from the controller, which it knows from the subject that names its caller — so the
// controller is granted that subject for the verbs the vault's holder serves, and no more of the vault's seat.
// Composed from the records, as the issued tools are: the installer's first user list carries none.
func TestTheControllerMayAskTheVaultInItsOwnName(t *testing.T) {
vault := Declared{Module: "mesh-vault", Holds: []Seat{{Name: "mesh-vault",
Serves: []string{"get", "accept", "secret-ask", "issued", "rotate"}}}}
users, err := Users(Records{Nodes: []string{"novox"}, Assigned: map[string][]Declared{"novox": {vault}}})
if err != nil {
t.Fatal(err)
}
controller := users[0]
if strings.Join(controller.AsksTheVault, ",") != "accept,issued,rotate,secret-ask" {
t.Fatalf("composed %v", controller.AsksTheVault)
}
p, err := PermissionsFor(controller)
if err != nil {
t.Fatal(err)
}
for _, verb := range []string{"accept", "secret-ask", "issued", "rotate"} {
if !MayPublish(p, CalledSubject("mesh.seat.mesh-vault.tool."+verb, ControllerName)) {
t.Errorf("the controller may not ask the vault's %s in its own name", verb)
}
if MayPublish(p, CalledSubject("mesh.seat.mesh-vault.tool."+verb, "anchor.node-tools")) {
t.Errorf("the controller may ask the vault's %s in another's name", verb)
}
}
if MayPublish(p, CalledSubject("mesh.seat.mesh-vault.tool.get", ControllerName)) {
t.Error("the controller may ask the vault for a secret: it seals nothing the vault holds")
}
// A vault that serves none of them yet — the running one — composes no grant, nor does genesis.
older := Declared{Module: "mesh-vault", Holds: []Seat{{Name: "mesh-vault", Serves: []string{"get"}}}}
users, _ = Users(Records{Nodes: []string{"novox"}, Assigned: map[string][]Declared{"novox": {older}}})
if len(users[0].AsksTheVault) != 0 {
t.Errorf("a vault serving none composed %v", users[0].AsksTheVault)
}
genesis, _ := PermissionsFor(Principal{Kind: KindController, PasswordHash: "x"})
for _, s := range genesis.Publish {
if strings.Contains(s, "mesh-vault") {
t.Errorf("the installer's controller may publish %s", s)
}
}
}