mesh/delivery-group group fix/418-node-reads rejected: a member's own check failed
mesh/merge-gate pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without …
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Reading a file, searching one, listing a directory, the owner and mode of each component of a path and looking up an account had no mesh tool, so they went through the login shell's execute as the operator's account. node-reads is a node seat with six verbs, each saying the commands it replaces. Its holder must run as an account of its own (ADR 0310 §3), and its own runtime is given MESH_SECRET_PLACES: the directory of every module's own secret, every credential file and grant directory, the vault's directories and the sealed-secret keep, so it refuses them by name before any read (§5).
110 lines
5.2 KiB
Go
110 lines
5.2 KiB
Go
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"}}
|
|
vault := Manifest{Module: "vault", Version: "1", Provides: []Offer{{Name: "secret"}},
|
|
Keeps: "/var/lib/vault/root", Grants: map[string]string{"secret": "/var/lib/vault/grants"},
|
|
Resources: []map[string]any{
|
|
{"id": "ledger", "type": "directory", "path": "/var/lib/vault/ledger"},
|
|
{"id": "grants", "type": "directory", "path": "/var/lib/vault/grants"},
|
|
}}
|
|
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/" + RuntimeModule, "/var/lib/vault/grants", "/var/lib/vault/ledger", "/var/lib/vault/root"}
|
|
if strings.Join(places, " ") != strings.Join(want, " ") {
|
|
t.Errorf("secret places:\n got %v\nwant %v", places, want)
|
|
}
|
|
}
|