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 } // Only a container that mounts the access's path, or below it, is named: a path that begins with the same letters is // another path. A read-only mount of a read-write access is still named — the access is read-write, and the grant // follows what mounts it, not how. func TestOnlyAContainerMountingThePathIsNamedReadOnlyIncluded(t *testing.T) { m := mod("library", nil, nil, nil) m.Accesses = []Access{{ID: "media", Path: "/storage/media", Mode: AccessReadWrite}} m.Resources = []map[string]any{ {"id": "beside", "type": "container", "name": "beside", "image": anImage, "volumes": []any{"/storage/media2:/x"}}, {"id": "reader", "type": "container", "name": "reader", "image": anImage, "volumes": []any{"${access:media}:/media:ro"}}, } out, err := Resolution{Node: "ace", Modules: []Manifest{m}}.Declaration(Rendering{GrantsGroups: true}) if err != nil { t.Fatal(err) } grant, _ := byID(out)["library."+AccessID("/storage/media")][GrantField].(map[string]any) if fmt.Sprint(grant["containers"]) != "[library.reader]" { t.Errorf("the access grants %v; want the read-only mount and not /storage/media2", grant) } }