package catalogue import ( "encoding/json" "fmt" "sort" "strings" "testing" "github.com/novox/mesh-host/validate" ) // Defends novox/hq ADR 0325 on the controller's side, row by row of its "How it is checked" table: the module check // (rows 1, 2), the identity composed (row 3), the tool runner's entry (row 4), no identity root (row 11) — and none // of it sent to a node-engine older than the contract, which would refuse the whole declaration. const identityImage = "x@sha256:1111111111111111111111111111111111111111111111111111111111111111" // aLibrarian files films: a container told its identity by PUID and PGID that mounts its config and the movies it // writes; a sidecar that declares none and reads the music; a directory only its tools write; and tools served by the // machine's runtime. func aLibrarian(t *testing.T) Manifest { t.Helper() m := aToolsModule(t, "librarian", "tools/index.js") m.Accesses = []Access{ {ID: "movies", Path: "/storage/media/movies", Mode: AccessReadWrite}, {ID: "music", Path: "/storage/media/music", Mode: AccessRead}, } m.Resources = []map[string]any{ {"id": "config", "type": "directory", "mode": "0750"}, {"id": "notes", "type": "directory", "mode": "0700"}, {"id": "app", "type": "container", "name": "librarian", "image": identityImage, "identity": map[string]any{"user": "env:PUID", "group": "env:PGID"}, "volumes": []any{"${dir:config}:/config", "${access:movies}:/movies"}}, {"id": "sidecar", "type": "container", "name": "librarian-sidecar", "image": identityImage, "volumes": []any{"${access:music}:/music:ro"}}, } return m } // aPlayer reads the movies the librarian writes, as its own identity through the runtime's user option, and has // tools of its own that read them too. func aPlayer(t *testing.T) Manifest { t.Helper() m := aToolsModule(t, "player", "tools/index.js") m.Accesses = []Access{{ID: "movies", Path: "/storage/media/movies", Mode: AccessRead}} m.Resources = []map[string]any{ {"id": "app", "type": "container", "name": "player", "image": identityImage, "identity": map[string]any{"user": "run"}, "volumes": []any{"${access:movies}:/movies:ro"}}, } return m } func keepingFolders(ids map[string]ModuleIdentity, kept ...string) Rendering { with := Rendering{KeepsFolders: true, Identities: ids, KeptFolders: map[string]bool{}} for _, k := range kept { with.KeptFolders[k] = true } return with } func composeFolders(t *testing.T, r Resolution, with Rendering) Composed { t.Helper() with.Needed = map[string]map[string]string{RuntimeModule: {"broker": "sealed-credential"}} with.ArtifactStore = "anchor.internal:5101" composed, err := r.Compose(with) if err != nil { t.Fatal(err) } return composed } var identities = map[string]ModuleIdentity{"librarian": {UID: 50001, GID: 50001}, "player": {UID: 50002, GID: 50002}} // Row 3: the identity composed — the same on every machine, the container told it, the module's directories its // containers mount owned by it, and the declaration's identities naming the account. func TestAModuleRunsAsItsIdentityTheSameOnEveryMachine(t *testing.T) { var seen []string for _, node := range []string{"ace", "novox"} { r := Resolution{Node: node, Account: "jochen", Modules: []Manifest{aLibrarian(t), aPlayer(t), theRuntime(t)}} composed := composeFolders(t, r, keepingFolders(identities)) got := byID(composed.Resources) app := got["librarian.app"] ident, _ := app[IdentityField].(map[string]any) want := map[string]any{"mode": "env", "user_env": "PUID", "group_env": "PGID", "uid": 50001, "gid": 50001} if fmt.Sprint(ident) != fmt.Sprint(want) { t.Errorf("%s: the container is told %v; want %v", node, ident, want) } player, _ := got["player.app"][IdentityField].(map[string]any) if fmt.Sprint(player) != fmt.Sprint(map[string]any{"mode": "run", "uid": 50002, "gid": 50002}) { t.Errorf("%s: the player is told %v", node, player) } if _, has := got["librarian.sidecar"][IdentityField]; has { t.Errorf("%s: a container declaring no identity was given one", node) } if owner := got["librarian.config"]["owner"]; owner != "50001:50001" { t.Errorf("%s: the directory its identity's container mounts is owned by %v", node, owner) } if owner, has := got["librarian.notes"]["owner"]; has { t.Errorf("%s: a directory no identity container mounts was given the owner %v: what writes it is not "+ "the identity", node, owner) } raw, _ := json.Marshal(composed.Identities) seen = append(seen, string(raw)) if !strings.Contains(string(raw), `"name":"mesh-librarian"`) || !strings.Contains(string(raw), `"uid":50001`) { t.Errorf("%s: the declaration's identities are %s", node, raw) } } // What the node-engine this controller pins (the ADR 0325 half) makes of it: nothing refused. r := Resolution{Node: "ace", Account: "jochen", Modules: []Manifest{aLibrarian(t), aPlayer(t), theRuntime(t)}} with := keepingFolders(identities, "/storage/media/movies") composed := composeFolders(t, r, with) body, err := json.Marshal(map[string]any{"declaration": validate.Version, "resources": composed.Resources, "identities": composed.Identities}) if err != nil { t.Fatal(err) } if problems := validate.Declaration(body); len(problems) > 0 { t.Fatalf("the pinned node-engine refuses it: %v", problems) } if seen[0] != seen[1] { t.Fatalf("the identities differ between machines: %s and %s", seen[0], seen[1]) } } // Rows 2, 4 and 8's composed part: each access names its users — the module's identity where a container declaring // identity mounts it, never a container declaring none — and the tool runner's account by name, with no number, // each at the access's own use (the node-engine keeps one entry per user at the widest use any access gives it, so // the runner's is the widest among the modules whose tools use the folder); and whether the folder is taken. func TestAnAccessNamesItsUsers(t *testing.T) { r := Resolution{Node: "ace", Account: "jochen", Modules: []Manifest{aLibrarian(t), aPlayer(t), theRuntime(t)}} with := keepingFolders(identities, "/storage/media/movies") got := byID(composeFolders(t, r, with).Resources) users := func(id string) string { raw, _ := json.Marshal(got[id]["users"]) return string(raw) } movies := "librarian." + AccessID("/storage/media/movies") if want := `[{"identity":"mesh-librarian","uid":50001,"use":"read-write","from":"container"},` + `{"identity":"jochen","use":"read-write","from":"tool-runner"}]`; users(movies) != want { t.Errorf("the librarian's movies name %s; want %s", users(movies), want) } played := "player." + AccessID("/storage/media/movies") if want := `[{"identity":"mesh-player","uid":50002,"use":"read","from":"container"},` + `{"identity":"jochen","use":"read","from":"tool-runner"}]`; users(played) != want { t.Errorf("the player's movies name %s; want %s", users(played), want) } // The sidecar declares no identity: no entry for it (§2); the runner reads the music for the librarian's tools. music := "librarian." + AccessID("/storage/media/music") if want := `[{"identity":"jochen","use":"read","from":"tool-runner"}]`; users(music) != want { t.Errorf("the music names %s; want only the tool runner's read", users(music)) } if got[movies]["taken"] != true || got[music]["taken"] != false { t.Errorf("taken is %v for the movies (taken) and %v for the music (not)", got[movies]["taken"], got[music]["taken"]) } // The runner is named with no number: the engine resolves it by name, never as uid 0 sent from here. if strings.Contains(users(music), `"uid"`) { t.Errorf("the tool runner's entry carries a number: %s", users(music)) } } // The contract: to an engine that does not state link.IdentityContract nothing of ADR 0325 is sent — no identities, no // container identity, no users, no taken, no owner from an identity — and what is sent is what the pinned node-engine // accepts, as before. func TestNothingOfTheIdentityIsSentToAnOlderEngine(t *testing.T) { r := Resolution{Node: "ace", Account: "jochen", Modules: []Manifest{aLibrarian(t), aPlayer(t), theRuntime(t)}} composed := composeFolders(t, r, Rendering{Identities: identities}) if len(composed.Identities) > 0 { t.Fatalf("identities were composed for an older engine: %v", composed.Identities) } for _, res := range composed.Resources { for _, field := range []string{IdentityField, "users", "taken"} { if _, has := res[field]; has { t.Errorf("%v carries %s for an older engine", res["id"], field) } } if res["id"] == "librarian.config" && res["owner"] != nil { t.Errorf("the directory was given the owner %v for an older engine", res["owner"]) } } body, err := json.Marshal(map[string]any{"declaration": validate.Version, "resources": composed.Resources}) if err != nil { t.Fatal(err) } if problems := validate.Declaration(body); len(problems) > 0 { t.Fatalf("the pinned node-engine refuses it: %v", problems) } // An engine that keeps folders, with no allocation yet (a question asked before the first send): composed // without an identity, never with a made-up one. none := composeFolders(t, r, keepingFolders(nil)) if len(none.Identities) > 0 || byID(none.Resources)["librarian.app"][IdentityField] != nil { t.Fatal("an identity was composed with no allocation") } } // Row 11: no identity outside the range is ever sent — 0 above all — and the composition says why. func TestNoModuleIdentityIsRoot(t *testing.T) { for _, n := range []int{0, 4, 10, 1000, IdentityLow - 1, IdentityHigh + 1} { if AllocatableID(n) { t.Errorf("%d is allocatable", n) } r := Resolution{Node: "ace", Account: "jochen", Modules: []Manifest{aLibrarian(t), theRuntime(t)}} with := keepingFolders(map[string]ModuleIdentity{"librarian": {UID: n, GID: n}}) with.Needed = map[string]map[string]string{RuntimeModule: {"broker": "sealed-credential"}} with.ArtifactStore = "anchor.internal:5101" if _, err := r.Compose(with); err == nil || !strings.Contains(err.Error(), "§11") { t.Errorf("an identity of %d composed: %v", n, err) } } for _, n := range []int{IdentityLow, IdentityHigh} { if !AllocatableID(n) { t.Errorf("%d is not allocatable", n) } } } func manifestJSON(t *testing.T, m map[string]any) []byte { t.Helper() raw, err := json.Marshal(m) if err != nil { t.Fatal(err) } return raw } func aContainerModule(name string, resources ...map[string]any) map[string]any { return map[string]any{"module": name, "version": "1", "capabilities": []any{"container-runtime"}, "accesses": []any{map[string]any{"id": "movies", "path": "/storage/media/movies", "mode": "read-write"}}, "resources": resources} } // Rows 1 and 2: the module check accepts identity as env: or run and refuses every other shape and an owner in // it; a module that declares identity is refused a directory naming its owner and a container declaring none that // mounts its own directory or a read-write access; a directory's mode is accepted. func TestTheModuleCheckHoldsAnIdentityModuleToTheDecision(t *testing.T) { ok := func(identity any) map[string]any { return map[string]any{"id": "app", "type": "container", "name": "app", "image": identityImage, "identity": identity, "volumes": []any{"${dir:config}:/config", "${access:movies}:/movies"}} } config := map[string]any{"id": "config", "type": "directory", "mode": "0750"} for _, identity := range []any{ map[string]any{"user": "env:PUID", "group": "env:PGID"}, map[string]any{"user": "run"}, } { if _, err := ParseManifest(manifestJSON(t, aContainerModule("app", config, ok(identity)))); err != nil { t.Errorf("identity %v refused: %v", identity, err) } } for _, c := range []struct { identity any want string }{ {map[string]any{"user": "1000", "group": "1000"}, `"env:" or "run"`}, {map[string]any{"user": "env:PUID"}, "told its group the same way"}, {map[string]any{"user": "run", "group": "env:PGID"}, "takes no group"}, {map[string]any{"user": "env:puid", "group": "env:PGID"}, "environment variable's name"}, {map[string]any{"user": "env:PUID", "group": "env:PGID", "owner": "1000:1000"}, "never an owner or a number"}, {"run", "identity is"}, } { _, err := ParseManifest(manifestJSON(t, aContainerModule("app", config, ok(c.identity)))) if err == nil || !strings.Contains(err.Error(), c.want) || !strings.Contains(err.Error(), "ADR 0325") { t.Errorf("identity %v: %v, want it refused saying %q", c.identity, err, c.want) } } // An identity on anything but a container. onAFile := map[string]any{"id": "f", "type": "directory", "mode": "0750", "identity": map[string]any{"user": "run"}} if _, err := ParseManifest(manifestJSON(t, aContainerModule("app", onAFile))); err == nil { t.Error("an identity on a directory was accepted") } // Held to the decision whole once it declares identity (§1, §2, §3). owned := map[string]any{"id": "config", "type": "directory", "mode": "0750", "owner": "1000:1000"} bare := map[string]any{"id": "helper", "type": "container", "name": "helper", "image": identityImage, "volumes": []any{"${dir:config}:/config", "${access:movies}:/movies"}} _, err := ParseManifest(manifestJSON(t, aContainerModule("app", owned, ok(map[string]any{"user": "run"}), bare))) for _, want := range []string{"names its owner 1000:1000", "declares no identity and mounts the module's own directory config", "declares no identity and mounts the read-write access movies"} { if err == nil || !strings.Contains(err.Error(), want) { t.Errorf("an identity module: %v, want it refused saying %q", err, want) } } // A setting's owner too. settingOwned := map[string]any{"id": "config", "type": "directory", "mode": "0750", "owner": "${setting:puid}:${setting:pgid}"} identityModule := aContainerModule("app", settingOwned, ok(map[string]any{"user": "run"})) identityModule["settings"] = map[string]any{"puid": map[string]any{"default": "1000"}, "pgid": map[string]any{"default": "1000"}} if _, err := ParseManifest(manifestJSON(t, identityModule)); err == nil || !strings.Contains(err.Error(), "names its owner") { t.Errorf("a setting's owner in an identity module: %v", err) } } // Rows 1 and 2 for what is registered today: a module that declares no identity is warned, never refused — a // directory naming its owner, a container mounting its own directory or a read-write access — and each warning // names ADR 0325. A placement's owner is refused for an identity module and warned of for any other. func TestAModuleWithoutIdentityIsWarnedNotRefused(t *testing.T) { owned := map[string]any{"id": "config", "type": "directory", "mode": "0750", "owner": "1000:1000"} bare := map[string]any{"id": "app", "type": "container", "name": "app", "image": identityImage, "volumes": []any{"${dir:config}:/config", "${access:movies}:/movies"}} m, err := ParseManifest(manifestJSON(t, aContainerModule("legacy", owned, bare))) if err != nil { t.Fatalf("a registered shape was refused: %v", err) } warnings := IdentityWarnings(m) sort.Strings(warnings) if len(warnings) != 3 { t.Fatalf("warned %d times: %q", len(warnings), warnings) } for _, w := range warnings { if !strings.Contains(w, "ADR 0325") || !strings.Contains(w, "refused once they have moved") { t.Errorf("a warning that does not name ADR 0325 or say refusal comes later: %q", w) } } placed := []Layer{{From: "ace", Values: map[string]any{PlacesSetting: map[string]any{ "config": map[string]any{"path": "/srv/legacy", "owner": "1000:1000"}}}}} if err := PlacesOwnerProblem(m, placed); err != nil { t.Errorf("a placement's owner refused for a module without identity: %v", err) } if w := PlacesOwnerWarnings(m, placed); len(w) != 1 || !strings.Contains(w[0], "places.config.owner") { t.Errorf("a placement's owner warned %q", w) } withIdentity := aLibrarian(t) placedLibrarian := []Layer{{From: "ace", Values: map[string]any{PlacesSetting: map[string]any{ "config": map[string]any{"path": "/srv/librarian", "owner": "1000:1000"}}}}} if err := PlacesOwnerProblem(withIdentity, placedLibrarian); err == nil || !strings.Contains(err.Error(), "ADR 0325") { t.Errorf("a placement's owner for an identity module: %v", err) } if len(IdentityWarnings(withIdentity)) != 0 { t.Error("an identity module was warned rather than refused") } }