Files
mesh-controller/internal/broker/declaration_files_test.go
T
jschoubben da364c2730 Give each node a bucket of its own for the files that travel by digest, and keep the sweep from removing an object a send relies on (issues 1696, 1697; review of ADR 0486 step 3)
A literal * in a published subject matches the grant's *, so in one shared bucket a node could read every node's files. Each node now
has declaration-files-<node>, which its account reads by name and the controller writes; the sweep judges each bucket against that
node's latest declaration, under the node's lock with the send, and an object the sweep marked is put again by the next send. Putting
that fails send after send raises a condition.
2026-10-21 15:43:36 +02:00

110 lines
4.4 KiB
Go

package broker
import (
"slices"
"strings"
"testing"
)
// 1697/AC1: the composed accounts give a node-engine exactly the four reads of one object by its name, on its own node's bucket and
// no other's; the controller the write of every node's bucket; and no other principal anything of any bucket.
func TestEachNodesBucketIsWrittenByTheControllerAndReadByNameByThatNodesEngine(t *testing.T) { // 1697/AC1
node, err := PermissionsFor(Principal{Kind: KindNode, Node: "alpha", PasswordHash: "x"})
if err != nil {
t.Fatal(err)
}
var mine []string
for _, s := range node.Publish {
if strings.Contains(s, "declaration-files") {
mine = append(mine, s)
}
}
want := []string{
"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-alpha.*.$O.declaration-files-alpha.C.*",
"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-alpha.*",
"$JS.API.DIRECT.GET.OBJ_declaration-files-alpha.$O.declaration-files-alpha.M.*",
"$JS.FC.OBJ_declaration-files-alpha.>",
}
if !slices.Equal(mine, want) {
t.Fatalf("a node-engine may publish %q of the buckets, want exactly %q", mine, want)
}
// What would make a list, a write, or a read of another node's files: none of it, in any pattern a node holds. (The real
// server's judgement of the wildcard forms is the live test's.)
for _, refused := range []string{
"$JS.API.STREAM.INFO.OBJ_declaration-files-alpha",
"$JS.API.STREAM.MSG.GET.OBJ_declaration-files-alpha",
"$JS.API.DIRECT.GET.OBJ_declaration-files-alpha",
"$JS.API.DIRECT.GET.OBJ_declaration-files-alpha.$O.declaration-files-alpha.M.abc.def",
"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-alpha.x.$O.declaration-files-alpha.M.YWJj",
"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-alpha.x.$O.declaration-files-alpha.M.YWJj.more",
"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-alpha",
"$O.declaration-files-alpha.M.abc",
"$O.declaration-files-alpha.C.abc",
// another node's bucket, however it is named
"$JS.API.DIRECT.GET.OBJ_declaration-files-beta.$O.declaration-files-beta.M.YWJj",
"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-beta.h4sh.$O.declaration-files-beta.C.NUID",
"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-beta.h4sh",
"$JS.FC.OBJ_declaration-files-beta.x",
// and the old shared bucket
"$JS.API.DIRECT.GET.OBJ_declaration-files.$O.declaration-files.M.YWJj",
} {
if MayPublish(node, refused) {
t.Errorf("a node-engine may publish %s, which lists or writes a bucket or reads another node's", refused)
}
}
for _, kept := range []string{
"$JS.API.DIRECT.GET.OBJ_declaration-files-alpha.$O.declaration-files-alpha.M.YWJj",
"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-alpha.h4sh.$O.declaration-files-alpha.C.NUID",
} {
if !MayPublish(node, kept) {
t.Errorf("a node-engine may not publish %s, which reads one object of its own bucket by its name", kept)
}
}
controller, err := PermissionsFor(Principal{Kind: KindController, PasswordHash: "x", FilesFor: []string{"alpha", "beta"}})
if err != nil {
t.Fatal(err)
}
for _, s := range []string{"$O.declaration-files-alpha.M.abc", "$O.declaration-files-beta.C.nuid"} {
if !MayPublish(controller, s) {
t.Errorf("the controller may not write %s", s)
}
}
if MayPublish(controller, "$O.declaration-files-gamma.M.abc") {
t.Error("the controller may write the bucket of a node it was not given")
}
for _, p := range []Principal{
{Kind: KindModule, Module: "mesh-vault", PasswordHash: "x"},
{Kind: KindNodeTools, Node: "alpha", PasswordHash: "x"},
{Kind: KindPerson, Module: "jochen", Invokes: []string{"*"}, PasswordHash: "x"},
} {
perms, err := PermissionsFor(p)
if err != nil {
t.Fatal(err)
}
for _, s := range append(append([]string(nil), perms.Publish...), perms.Subscribe...) {
if strings.Contains(s, "declaration-files") {
t.Errorf("%s holds %s, and no account but the controller's and the node-engines' has any access", p.Username(), s)
}
}
}
}
// The users the controller composes carry each node's bucket to the controller and to that node alone.
func TestTheComposedUsersGiveEachNodeItsOwnBucketAndTheControllerAll(t *testing.T) {
users, err := Users(Records{Nodes: []string{"alpha", "beta"}})
if err != nil {
t.Fatal(err)
}
var controller Principal
for _, u := range users {
if u.Kind == KindController {
controller = u
}
}
if !slices.Equal(controller.FilesFor, []string{"alpha", "beta"}) {
t.Fatalf("the controller writes the buckets of %v", controller.FilesFor)
}
}