package catalogue import ( "encoding/json" "strings" "testing" ) // novox/hq ADR 0310 §1, §2: node-reads is a node seat, and its verbs say the commands they replace, so // mesh_search finds them by `cat`, `grep` and `getent passwd` (ADR 0245 §3). func TestNodeReadsIsANodeSeatWhoseVerbsSayWhatTheyReplace(t *testing.T) { s, ok := SeatNamed(ReadsSeat) if !ok { t.Fatalf("the mesh defines no seat %s", ReadsSeat) } if s.Scope != ScopeNode || s.Delivers != "" || !strings.Contains(s.Decision, "0310") { t.Errorf("%s: scope %q, delivers %q, decision %q", ReadsSeat, s.Scope, s.Delivers, s.Decision) } replaced := map[string]string{} for _, v := range s.Serves { if v.Optional { t.Errorf("%s.%s is optional: a new seat's holder serves every verb", ReadsSeat, v.Name) } if len(v.Replaces) == 0 { t.Errorf("%s.%s says nothing it replaces", ReadsSeat, v.Name) } for _, c := range v.Replaces { replaced[c] = v.Name } } for command, verb := range map[string]string{"cat": "read", "grep": "search", "getent passwd": "account", "getent group": "account", "id": "account", "stat": "stat", "readlink": "readlink", "ls": "list"} { if replaced[command] != verb { t.Errorf("%q is replaced by %q, want %s.%s", command, replaced[command], ReadsSeat, verb) } } } // novox/hq ADR 0310 §3: a holder of node-reads reads as an account of its own; one the machine's runtime would // carry — as the operator's account — is refused at parse. func TestAHolderOfNodeReadsWithoutAnAccountOfItsOwnIsRefused(t *testing.T) { _, err := ParseManifest([]byte(`{"module": "reads", "version": "1", "claims": [{"name": "node-reads", "scope": "node"}]}`)) if err == nil || !strings.Contains(err.Error(), "runs-as") || !strings.Contains(err.Error(), "0310") { t.Errorf("a holder of node-reads without runs-as was not refused for it: %v", err) } if p := ReadsHolderProblems(Manifest{Module: "reads", RunsAs: "mesh-reads", Claims: []Claim{{Name: ReadsSeat}}}); len(p) != 0 { t.Errorf("a holder running as its own account was refused: %v", p) } } // novox/hq ADR 0310 §5: the holder is told, by the composition, where its node keeps a secret — the directory // of every module's own secret, every credential file and grant directory, the vault's directories and the // place the operator-sealed secrets are kept — and no other module is told. func TestTheHolderOfNodeReadsIsToldWhereItsNodeKeepsASecret(t *testing.T) { with := Rendering{ArtifactStore: "anchor.internal:5101", Needed: map[string]map[string]string{RuntimeModule: {"broker": "sealed-credential"}, "reads": {"broker": "own"}, "desk": {"broker": "own"}}} goRuntime := Manifest{Module: RuntimeModule, Version: "1", OwnSecrets: OwnSecrets{"broker": {Path: "/var/lib/mesh/" + RuntimeModule + "/broker"}}, Build: &Build{Artifacts: []Artifact{{Name: "runtime", Kind: ArtifactBundle, Language: "go", System: "arch", From: "cmd/node-tools"}}}} goRuntime, err := goRuntime.Resolve([]Built{{Name: "runtime", Kind: ArtifactBundle, Reference: ArtifactStoreScheme + RuntimeModule + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}}) if err != nil { t.Fatal(err) } reads := aToolsModule(t, "reads", "tools/index.js") reads.RunsAs, reads.SecretsOwner = "mesh-reads", "mesh-reads" reads.Claims = []Claim{{Name: ReadsSeat}} reads.OwnSecrets = OwnSecrets{"broker": {Path: "/var/lib/mesh-reads/broker"}} desk := aToolsModule(t, "desk", "tools/index.js") desk.RunsAs, desk.SecretsOwner = "desk", "desk" desk.OwnSecrets = OwnSecrets{"broker": {Path: "/var/lib/desk/broker"}} // The vault as the catalogue declares it: its directories have no path of their own, and the node places them. vault, err := placedManifest(catalogueManifest(t, "mesh-vault"), with) if err != nil { t.Fatal(err) } app := Manifest{Module: "app", Version: "1", Secrets: map[string]string{"postgres-database": "/etc/app/db.secret"}, OwnSecrets: OwnSecrets{"token": {Path: "/home/ops/.config/app/token"}}} r := Resolution{Node: "anchor", Account: "ops", Modules: []Manifest{reads, desk, vault, app, goRuntime}} owns, err := r.ownRuntimes(with) if err != nil { t.Fatal(err) } var given map[string]map[string]string for _, p := range owns { if p["id"] == "reads."+OwnRuntimeID() { if err := json.Unmarshal([]byte(p["env"].(map[string]string)[RuntimeToolEnv]), &given); err != nil { t.Fatal(err) } } if p["id"] == "desk."+OwnRuntimeID() && strings.Contains(p["env"].(map[string]string)[RuntimeToolEnv], ReadsSecretPlaces) { t.Error("a module not holding node-reads is told the node's secret places") } } var places []string if err := json.Unmarshal([]byte(given["reads"][ReadsSecretPlaces]), &places); err != nil { t.Fatalf("the holder is not told its node's secret places: %v (%v)", err, given) } want := []string{"/etc/app/db.secret", "/home/ops/.config/app", "/var/lib/desk", "/var/lib/mesh-reads", "/var/lib/mesh-vault", "/var/lib/mesh/" + RuntimeModule} if strings.Join(places, " ") != strings.Join(want, " ") { t.Errorf("secret places:\n got %v\nwant %v", places, want) } } // The review of #257: a secret place the controller cannot resolve fails the composition, naming the module, // rather than leaving the list one place shorter. func TestASecretPlaceThatCannotBeResolvedFailsTheComposition(t *testing.T) { r := Resolution{Node: "anchor", Modules: []Manifest{ {Module: "app", Version: "1", Keeps: "${dir:keep}/sealed"}, }} if _, err := r.secretPlaces(Rendering{}); err == nil || !strings.Contains(err.Error(), "app") { t.Errorf("an unresolved place was dropped: %v", err) } r.Modules[0].Keeps = "relative/place" if _, err := r.secretPlaces(Rendering{}); err == nil { t.Error("a relative place was dropped") } } // ADR 0310 §3: the account that reads is in no group beside its own; one put in a group is refused. func TestTheReadsAccountIsInNoGroup(t *testing.T) { m := Manifest{Module: "reads", RunsAs: "mesh-reads", Claims: []Claim{{Name: ReadsSeat}}, Resources: []map[string]any{{"id": "account", "type": "user", "name": "mesh-reads", "groups": []any{"docker"}}}} if p := ReadsHolderProblems(m); len(p) != 1 || !strings.Contains(p[0], "docker") { t.Errorf("an account in docker was not refused: %v", p) } m.Resources[0]["groups"] = []any{} if p := ReadsHolderProblems(m); len(p) != 0 { t.Errorf("an account in no group was refused: %v", p) } }