The fourth review (hq issue 339): the safe reading of a file that asks for a setting and does not say is that root or a consumer trusts it, so its settings are the terminal's; `"trusted": false` is the opt-out. With that, nothing unsafe is left to refuse: `module check` lists and counts the unmarked files and never refuses them.
214 lines
9.8 KiB
Go
214 lines
9.8 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
)
|
|
|
|
// Where root creates and owns a module's directories, and which of the machine's paths reach its container,
|
|
// are said at the controller's terminal alone (novox/hq issue 339): through a verb, a caller who set `places`
|
|
// to /etc with an owner of its own would have the next push hand it /etc, and one who set `accesses` to /
|
|
// would have the machine's root mounted into a container.
|
|
|
|
// throughVerb runs a verb's call the way the serving controller does: the command line argvFor composes, in
|
|
// a process that carries the verb in its environment (runVerb), through this binary's own dispatch.
|
|
func throughVerb(t *testing.T, verb string, args map[string]any) error {
|
|
t.Helper()
|
|
argv, err := argvFor(verb, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
t.Setenv(verbVar, verb)
|
|
defer os.Unsetenv(verbVar)
|
|
return runLine(t, argv)
|
|
}
|
|
|
|
// atTheTerminal runs a command line the way the operator does at the controller's terminal: no verb.
|
|
func atTheTerminal(t *testing.T, argv ...string) error {
|
|
t.Helper()
|
|
t.Setenv(verbVar, "")
|
|
os.Unsetenv(verbVar)
|
|
return runLine(t, argv)
|
|
}
|
|
|
|
func runLine(t *testing.T, argv []string) error {
|
|
t.Helper()
|
|
before := os.Args
|
|
defer func() { os.Args = before }()
|
|
os.Args = append([]string{"mesh-controller"}, argv...)
|
|
return run()
|
|
}
|
|
|
|
func TestPlacesAndAccessesAreRefusedThroughEveryVerb(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
register(t, open, catalogue.Manifest{Module: "notes", Version: "1",
|
|
Accesses: []catalogue.Access{{ID: "media", Path: "/storage/media", Mode: "read"}},
|
|
Resources: []map[string]any{{"id": "data", "type": "directory", "mode": "0755"},
|
|
// Nothing trusts this file: said, so a verb may change what it asks for (an unmarked one counts as
|
|
// trusted, and only the terminal could).
|
|
{"id": "rc", "type": "file", "path": "/etc/notes.conf", "mode": "0644", "trusted": false,
|
|
"content": "x = ${setting:x}\n"}}})
|
|
if _, err := assign(ctx, open, "laptop", "notes"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
layer := func() string {
|
|
t.Helper()
|
|
values, _, err := open.inventory.Layer(ctx, "laptop", "notes")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, _ := json.Marshal(values)
|
|
return string(raw)
|
|
}
|
|
refused := func(what string, err error) {
|
|
t.Helper()
|
|
if err == nil || !strings.Contains(err.Error(), "controller's terminal") ||
|
|
!strings.Contains(err.Error(), "issue 339") {
|
|
t.Fatalf("%s: %v", what, err)
|
|
}
|
|
}
|
|
|
|
// The attack the issue reports, through each verb route: nothing is kept.
|
|
attacks := []map[string]any{
|
|
{"places": map[string]any{"data": map[string]any{"path": "/srv/notes", "owner": "1001:1001"}}, "x": 1},
|
|
{"accesses": map[string]any{"media": "/srv/elsewhere"}, "x": 1},
|
|
}
|
|
for _, values := range attacks {
|
|
raw, _ := json.Marshal(values)
|
|
refused("settings verb, one machine", throughVerb(t, "settings",
|
|
map[string]any{"module": "notes", "node": "laptop", "values": string(raw)}))
|
|
refused("settings verb, the whole mesh", throughVerb(t, "settings",
|
|
map[string]any{"module": "notes", "values": string(raw)}))
|
|
for _, line := range []string{
|
|
"settings set notes '" + string(raw) + "' --node laptop",
|
|
"settings set --node laptop notes '" + string(raw) + "'",
|
|
"settings set notes '" + string(raw) + "'",
|
|
} {
|
|
if err := throughVerb(t, "command", map[string]any{"command": line}); err == nil {
|
|
t.Fatalf("the command verb ran %q", line)
|
|
}
|
|
}
|
|
if got := layer(); got != "null" && got != "{}" {
|
|
t.Fatalf("a refused call kept a layer: %s", got)
|
|
}
|
|
}
|
|
|
|
// At the terminal the same placement is taken.
|
|
if err := atTheTerminal(t, "settings", "set", "notes",
|
|
`{"places":{"data":{"path":"/srv/notes","owner":"1001:1001"}},"x":0}`, "--node", "laptop"); err != nil {
|
|
t.Fatalf("places at the terminal: %v", err)
|
|
}
|
|
// A verb may change another key and keep the placement as it is.
|
|
if err := throughVerb(t, "settings", map[string]any{"module": "notes", "node": "laptop",
|
|
"values": `{"places":{"data":{"path":"/srv/notes","owner":"1001:1001"}},"x":1}`}); err != nil {
|
|
t.Fatalf("another key through the verb: %v", err)
|
|
}
|
|
kept := layer()
|
|
// But not move it, drop it, or clear the layer that holds it.
|
|
refused("moved through the verb", throughVerb(t, "settings", map[string]any{"module": "notes", "node": "laptop",
|
|
"values": `{"places":{"data":{"path":"/srv/other","owner":"1001:1001"}},"x":1}`}))
|
|
refused("dropped through the verb", throughVerb(t, "settings", map[string]any{"module": "notes", "node": "laptop",
|
|
"values": `{"x":1}`, "replace": "true"}))
|
|
refused("cleared through the verb", throughVerb(t, "settings",
|
|
map[string]any{"module": "notes", "node": "laptop", "clear": "true"}))
|
|
if err := throughVerb(t, "command", map[string]any{"command": "settings clear notes --node laptop"}); err == nil {
|
|
t.Fatal("the command verb cleared a layer")
|
|
}
|
|
if got := layer(); got != kept {
|
|
t.Fatalf("a refused call changed the layer: %s, was %s", got, kept)
|
|
}
|
|
// Reading through a verb still answers.
|
|
if err := throughVerb(t, "settings", map[string]any{"module": "notes", "node": "laptop"}); err != nil {
|
|
t.Fatalf("reading through the verb: %v", err)
|
|
}
|
|
|
|
// The machine's own trees are refused from anywhere, the terminal too.
|
|
for _, path := range []string{"/etc", "/etc/sudoers.d", "/", "/home", "/var/lib", "/var/lib/mesh-host/x", "/srv/../etc"} {
|
|
err := atTheTerminal(t, "settings", "set", "notes",
|
|
`{"places":{"data":{"path":"`+path+`","owner":"1001:1001"}},"x":1}`, "--node", "laptop")
|
|
if err == nil || !strings.Contains(err.Error(), "issue 339") {
|
|
t.Fatalf("a place at %s at the terminal: %v", path, err)
|
|
}
|
|
err = atTheTerminal(t, "settings", "set", "notes",
|
|
`{"places":{"data":{"path":"/srv/notes","owner":"1001:1001"}},"accesses":{"media":"`+path+`"},"x":1}`,
|
|
"--node", "laptop")
|
|
if err == nil || !strings.Contains(err.Error(), "issue 339") {
|
|
t.Fatalf("an access at %s at the terminal: %v", path, err)
|
|
}
|
|
}
|
|
// A line break in any setting is refused where it is kept, through a verb or at the terminal.
|
|
for _, x := range []string{`"a\nPATH=/tmp"`, `"a\rb"`, `"a\u0000b"`, `["ok","x\ny"]`, `{"k":"x\ny"}`} {
|
|
err := throughVerb(t, "settings", map[string]any{"module": "notes", "node": "laptop", "replace": "true",
|
|
"values": `{"places":{"data":{"path":"/srv/notes","owner":"1001:1001"}},"x":` + x + `}`})
|
|
if err == nil || !strings.Contains(err.Error(), "line break") {
|
|
t.Fatalf("a line break in %s: %v", x, err)
|
|
}
|
|
}
|
|
if got := layer(); got != kept {
|
|
t.Fatalf("a refused call changed the layer: %s, was %s", got, kept)
|
|
}
|
|
}
|
|
|
|
// What a provider serves is set at the terminal alone (novox/hq issue 339): through the settings verb, a caller
|
|
// could move a database's port to a listener of its own and collect every consumer's credentials, or point every
|
|
// login at an issuer of its own.
|
|
func TestAServedKeyIsRefusedThroughAVerb(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
register(t, open, catalogue.Manifest{Module: "store", Version: "1",
|
|
Provides: catalogue.FromAnywhere("database"),
|
|
Serves: map[string]map[string]any{"database": {"port": 5432.0}},
|
|
Resources: []map[string]any{{"id": "rc", "type": "file", "path": "/etc/store.conf", "mode": "0644",
|
|
"trusted": false, "content": "x = ${setting:x}\n"}}})
|
|
register(t, open, catalogue.Manifest{Module: "keycloak", Version: "1",
|
|
Provides: catalogue.FromAnywhere("oidc-client"),
|
|
Serves: map[string]map[string]any{"oidc-client": {"issuer": "${setting:issuer}"}},
|
|
Resources: []map[string]any{{"id": "rc", "type": "file", "path": "/etc/kc.conf", "mode": "0644",
|
|
"trusted": false, "content": "x = ${setting:x}\n"}}})
|
|
register(t, open, catalogue.Manifest{Module: "power", Version: "1",
|
|
Resources: []map[string]any{{"id": "logind", "type": "file", "path": "/etc/systemd/logind.conf.d/power.conf",
|
|
"mode": "0644", "trusted": true, "content": "HandleLidSwitch=${setting:lid}\nx=${setting:x}\n"}}})
|
|
if err := atTheTerminal(t, "settings", "set", "keycloak", `{"issuer":"https://id.example/realms/mesh","x":0}`,
|
|
"--node", "anchor"); err != nil {
|
|
t.Fatalf("the issuer at the terminal: %v", err)
|
|
}
|
|
if err := atTheTerminal(t, "settings", "set", "power", `{"lid":"suspend","x":0}`, "--node", "anchor"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, m := range []string{"store", "keycloak", "power"} {
|
|
if _, err := assign(ctx, open, "anchor", m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
refused := func(what string, err error) {
|
|
t.Helper()
|
|
if err == nil || !strings.Contains(err.Error(), "controller's terminal") {
|
|
t.Fatalf("%s: %v", what, err)
|
|
}
|
|
}
|
|
refused("a served port through the verb", throughVerb(t, "settings", map[string]any{"module": "store",
|
|
"node": "anchor", "values": `{"port":6543,"x":0}`}))
|
|
refused("a served port, mesh-wide, through the verb", throughVerb(t, "settings",
|
|
map[string]any{"module": "store", "values": `{"port":6543}`}))
|
|
refused("the issuer through the verb", throughVerb(t, "settings", map[string]any{"module": "keycloak",
|
|
"node": "anchor", "values": `{"issuer":"https://evil.example/realms/mesh","x":0}`}))
|
|
refused("clearing the issuer through the verb", throughVerb(t, "settings", map[string]any{"module": "keycloak",
|
|
"node": "anchor", "clear": "true"}))
|
|
if err := throughVerb(t, "settings", map[string]any{"module": "keycloak", "node": "anchor",
|
|
"values": `{"issuer":"https://id.example/realms/mesh","x":1}`}); err != nil {
|
|
t.Fatalf("another key through the verb, the issuer kept: %v", err)
|
|
}
|
|
refused("a setting a trusted file asks for, through the verb", throughVerb(t, "settings", map[string]any{
|
|
"module": "power", "node": "anchor", "values": `{"lid":"ignore","x":0}`}))
|
|
// And `trusted` is the catalogue's word: it never reaches the machine, whose engine parses strictly.
|
|
plan := printed(t, func() error { return atTheTerminal(t, "plan", "anchor", "--json") })
|
|
if strings.Contains(plan, `"trusted"`) {
|
|
t.Fatal("the declaration carries the catalogue's `trusted`")
|
|
}
|
|
}
|