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.
155 lines
6.0 KiB
Go
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
|
|
}
|