Files
mesh-controller/internal/catalogue/grants_test.go
T
jschoubben 10d4712f82 Name who a read-write access grants its path's group to, so modules write shared data through the group the operator gave it (hq ADR 0312, issue 537)
Each read-write access is composed with a grant: the module's containers that
mount its path and the account its tools run as (the operator's, or the module's
own under ADR 0259 §8); that runtime restarts on the access, so it holds a group
the engine put its account in. A read access grants nothing. Sent only to an
engine whose statement says GrantContract (4), since an older one refuses the
field. Pins the node-engine at the branch that reads it.
2026-10-12 13:47:17 +02:00

155 lines
6.0 KiB
Go

package catalogue
import (
"encoding/json"
"fmt"
"testing"
"github.com/novox/mesh-host/validate"
)
// Defends novox/hq ADR 0312 and issue 537 on the controller's side: each read-write access reaches the machine
// naming who its path's group is granted to — the module's containers that mount it, and the account its tools
// run as — and the tool runner is restarted on it; a read access grants nothing; and none of it is sent to a
// node-engine older than the field, which would refuse the whole declaration.
const anImage = "x@sha256:1111111111111111111111111111111111111111111111111111111111111111"
// anImporter files into one library it writes and reads another: a container mounting both, a step mounting
// neither, and tools served by the machine's runtime.
func anImporter(t *testing.T) Manifest {
t.Helper()
m := aToolsModule(t, "importer", "tools/index.js")
m.Accesses = []Access{
{ID: "music", Path: "/storage/media/music", Mode: AccessReadWrite},
{ID: "movies", Path: "/storage/media/movies", Mode: AccessRead},
}
m.Resources = []map[string]any{
{"id": "app", "type": "container", "name": "importer", "image": anImage,
"volumes": []any{"${access:music}/incoming:/music", "${access:movies}:/movies:ro"}},
{"id": "sidecar", "type": "container", "name": "importer-sidecar", "image": anImage},
}
return m
}
func composeImporter(t *testing.T, r Resolution, with Rendering) map[string]map[string]any {
t.Helper()
with.Needed = map[string]map[string]string{RuntimeModule: {"broker": "sealed-credential"}}
with.ArtifactStore = "anchor.internal:5101"
out, err := r.Declaration(with)
if err != nil {
t.Fatal(err)
}
// What the node-engine this controller pins would make of it: nothing refused.
body, err := json.Marshal(map[string]any{"declaration": validate.Version, "resources": out})
if err != nil {
t.Fatal(err)
}
if problems := validate.Declaration(body); len(problems) > 0 {
t.Fatalf("composed into something the node-engine refuses: %v", problems)
}
return byID(out)
}
func TestAReadWriteAccessNamesTheContainersThatMountItAndTheToolsAccount(t *testing.T) {
r := Resolution{Node: "ace", Account: "jochen", Modules: []Manifest{anImporter(t), theRuntime(t)}}
got := composeImporter(t, r, Rendering{GrantsGroups: true})
music := got["importer."+AccessID("/storage/media/music")]
if music == nil {
t.Fatalf("no access resource for the library: %v", keysOf(got))
}
grant, _ := music[GrantField].(map[string]any)
if fmt.Sprint(grant["containers"]) != "[importer.app]" || grant["account"] != "jochen" {
t.Errorf("the read-write access grants %v; want the container that mounts it and the tools' account", grant)
}
// A read access grants nothing (§3).
if movies := got["importer."+AccessID("/storage/media/movies")]; movies[GrantField] != nil {
t.Errorf("a read access names a grant: %v", movies[GrantField])
}
// The tool runner is restarted when the access moved its account's groups (§6).
runtime := got[RuntimeModule+"."+RuntimeProcessID()]
found := false
for _, id := range runtime["restart-on"].([]any) {
if id == "importer."+AccessID("/storage/media/music") {
found = true
}
}
if !found {
t.Errorf("the tool runner is not restarted on the access that grants its account: %v", runtime["restart-on"])
}
}
func TestAModuleOfItsOwnAccountIsGrantedAsThatAccount(t *testing.T) {
m := anImporter(t)
m.RunsAs = "importer"
m.OwnSecrets = OwnSecrets{"broker": {Path: "/var/lib/mesh/importer/broker"}}
runtime := theRuntime(t)
runtime.Bundles[0].Binary = "node-tools"
r := Resolution{Node: "ace", Account: "jochen", Modules: []Manifest{m, runtime}}
with := Rendering{GrantsGroups: true}
with.Needed = map[string]map[string]string{RuntimeModule: {"broker": "sealed"}, "importer": {"broker": "sealed"}}
with.ArtifactStore = "anchor.internal:5101"
out, err := r.Declaration(with)
if err != nil {
t.Fatal(err)
}
got := byID(out)
grant, _ := got["importer."+AccessID("/storage/media/music")][GrantField].(map[string]any)
if grant["account"] != "importer" {
t.Errorf("the access grants %v; want the module's own account", grant)
}
own := got["importer."+OwnRuntimeID()]
if fmt.Sprint(own["restart-on"]) == "" || !restartsOnIt(own["restart-on"], "importer."+AccessID("/storage/media/music")) {
t.Errorf("the module's own runtime is not restarted on its access: %v", own["restart-on"])
}
if restartsOnIt(got[RuntimeModule+"."+RuntimeProcessID()]["restart-on"], "importer."+AccessID("/storage/media/music")) {
t.Error("the machine's runtime is restarted on an access of a module it does not serve")
}
}
func TestAModuleThatRunsNothingHereSendsAnEmptyGrant(t *testing.T) {
m := mod("spool", nil, nil, nil)
m.Accesses = []Access{{Path: "/storage/downloads", Mode: AccessReadWrite}}
out, err := Resolution{Node: "ace", Account: "jochen", Modules: []Manifest{m}}.Declaration(Rendering{GrantsGroups: true})
if err != nil {
t.Fatal(err)
}
grant, has := byID(out)["spool."+AccessID("/storage/downloads")][GrantField].(map[string]any)
if !has || len(grant) != 0 {
t.Errorf("a read-write access of a module running nothing here is sent %v (sent %v); want an empty grant, "+
"so its group is still judged", grant, has)
}
}
func TestNoGrantIsSentToAnEngineOlderThanTheField(t *testing.T) {
r := Resolution{Node: "ace", Account: "jochen", Modules: []Manifest{anImporter(t), theRuntime(t)}}
got := composeImporter(t, r, Rendering{})
for id, res := range got {
if _, has := res[GrantField]; has {
t.Errorf("%s names a grant to an engine that does not read it", id)
}
}
if restartsOnIt(got[RuntimeModule+"."+RuntimeProcessID()]["restart-on"], "importer."+AccessID("/storage/media/music")) {
t.Error("the tool runner is restarted on an access an older engine grants nothing for")
}
}
func restartsOnIt(list any, want string) bool {
items, _ := list.([]any)
for _, i := range items {
if fmt.Sprint(i) == want {
return true
}
}
return false
}
func keysOf(m map[string]map[string]any) []string {
var out []string
for k := range m {
out = append(out, k)
}
return out
}