Two holes the review proved. The ask's subject proved nothing — a responder can reach it through a reply — so the engine now verifies the mesh's signature over a fixed context with the key it verifies declarations with, before reading anything, and refuses an ask for another node, expired, good too long, without a nonce, or replayed. And the apply followed a link the hand-over had refused: a component swapped for a link after the record carried root's chmod and chown to its target. A handed-over directory must now still resolve to itself, and is changed only through one descriptor opened from / with no link followed (fchmod, fchown). Tests for the reply guard, the run loop's hook, the fallback renewal on proof and a non-regular fallback; 27 checks each removed in turn, each failing a test.
178 lines
7.5 KiB
Go
178 lines
7.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-host/internal/apply"
|
|
"github.com/novox/mesh-host/internal/link"
|
|
"github.com/novox/mesh-host/internal/store"
|
|
)
|
|
|
|
// Every directory used as found is its module's resource of kind directory, unhealthy, naming its owner, on
|
|
// every statement (novox/hq issue 339): the controller raises that as the module's condition. The way out it
|
|
// names is the operator's line, with this node's name (novox/hq ADR 0272, issue 356).
|
|
func TestAFoundDirectoryIsStatedUnhealthy(t *testing.T) {
|
|
dir := t.TempDir()
|
|
known := store.State{Resources: []store.Applied{
|
|
{ID: "notes.data", Type: "directory", Target: dir, AsFound: true, Declared: "root, mode 755", AppliedAt: time.Now()},
|
|
{ID: "notes.cache", Type: "directory", Target: dir},
|
|
}}
|
|
foundNode.Store("laptop")
|
|
t.Cleanup(func() { foundNode.Store("") })
|
|
h := withFoundDirectories(&link.Health{}, foundDirectories(known))
|
|
if len(h.Resources) != 1 {
|
|
t.Fatalf("stated: %+v", h.Resources)
|
|
}
|
|
r := h.Resources[0]
|
|
if r.Module != "notes" || r.Kind != link.KindDirectory || r.State != link.StateUnhealthy || r.Target != dir ||
|
|
!strings.HasPrefix(r.Reason, link.ReasonUsedAsFound) || !strings.Contains(r.Reason, "owned by ") ||
|
|
!strings.Contains(r.Reason, "root, mode 755") {
|
|
t.Fatalf("stated as: %+v", r)
|
|
}
|
|
// The path is the target, kept as evidence, and never in the reason: the operator's channel withholds a path
|
|
// (novox/hq ADR 0234 §6), and the controller says the reason there.
|
|
if !strings.Contains(r.Reason, "`nox node hand-over laptop <directory>` on the control-node") ||
|
|
strings.Contains(r.Reason, "mesh-host") || strings.Contains(r.Reason, dir) {
|
|
t.Fatalf("the way out is not the operator's line, or carries the path: %q", r.Reason)
|
|
}
|
|
foundNode.Store("")
|
|
if r := withFoundDirectories(&link.Health{}, foundDirectories(known)).Resources[0]; !strings.Contains(r.Reason,
|
|
"`nox node hand-over <node> <directory>`") {
|
|
t.Fatalf("before enrolment the line names a placeholder: %q", r.Reason)
|
|
}
|
|
}
|
|
|
|
// realDir is a temporary directory with every link in its path resolved, so a judgement of links is about the
|
|
// links the test makes.
|
|
func realDir(t *testing.T) string {
|
|
t.Helper()
|
|
dir, err := filepath.EvalSymlinks(t.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return dir
|
|
}
|
|
|
|
// A hand-over records a directory used as found, and refuses one that is not — through the engine's own
|
|
// subcommand and through the controller's ask alike, which are one judgement.
|
|
func TestHandOverRecordsOnlyAFoundDirectory(t *testing.T) {
|
|
dir := realDir(t)
|
|
statePath := filepath.Join(dir, "state.json")
|
|
found := filepath.Join(dir, "found")
|
|
if err := os.Mkdir(found, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Mkdir(filepath.Join(dir, "mine"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := store.Save(statePath, store.State{Resources: []store.Applied{
|
|
{ID: "notes.data", Type: "directory", Target: found, AsFound: true, Declared: "root, mode 755"},
|
|
{ID: "notes.mine", Type: "directory", Target: filepath.Join(dir, "mine")},
|
|
}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var out bytes.Buffer
|
|
if err := handOverCommand(options{state: statePath, file: filepath.Join(dir, "mine"), out: &out}); err == nil {
|
|
t.Fatal("a directory the mesh made was handed over")
|
|
}
|
|
if err := handOverCommand(options{state: statePath, file: found + "/", out: &out}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
all, err := store.LoadHandedOver(statePath)
|
|
if err != nil || len(all) != 1 || all[0].Path != found || !strings.HasSuffix(all[0].By, " at the machine") {
|
|
t.Fatalf("recorded: %+v %v", all, err)
|
|
}
|
|
handedOverFrom(statePath)
|
|
if !apply.HandedOver()[apply.HandOverKey("notes.data", found)] {
|
|
t.Fatal("the apply does not see the hand-over")
|
|
}
|
|
if !strings.Contains(out.String(), "root, mode 755") {
|
|
t.Fatalf("said: %s", out.String())
|
|
}
|
|
|
|
// The controller's ask is the same judgement, recording who asked in the controller's words.
|
|
_ = os.Remove(store.HandedOverPath(statePath))
|
|
said, err := handOverFrom(statePath)(found, "jo through mesh-cli on anchor")
|
|
if err != nil || !strings.Contains(said, "by jo through mesh-cli on anchor") || !strings.Contains(said, "root, mode 755") {
|
|
t.Fatalf("the ask answered %q, %v", said, err)
|
|
}
|
|
all, _ = store.LoadHandedOver(statePath)
|
|
if len(all) != 1 || all[0].By != "jo through mesh-cli on anchor" || all[0].ID != "notes.data" {
|
|
t.Fatalf("recorded: %+v", all)
|
|
}
|
|
}
|
|
|
|
// Every value of a hand-over is judged before anything is recorded, and each refusal records nothing: a relative
|
|
// path, a path that is not the engine's statement of the directory (`..`, a doubled or trailing separator), one
|
|
// not used as found, a link at the path, a link in the path, a file, a directory that is gone.
|
|
func TestAHandOverIsRefusedForEveryPathThatIsNotTheFoundDirectory(t *testing.T) {
|
|
dir := realDir(t)
|
|
statePath := filepath.Join(dir, "state.json")
|
|
found := filepath.Join(dir, "found")
|
|
if err := os.Mkdir(found, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// A link at the path of a second found directory, and a found directory below a linked directory.
|
|
linked := filepath.Join(dir, "linked")
|
|
if err := os.Symlink(found, linked); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
through := filepath.Join(dir, "via", "data")
|
|
if err := os.Symlink(dir, filepath.Join(dir, "via")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Mkdir(filepath.Join(dir, "data"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
file := filepath.Join(dir, "file")
|
|
if err := os.WriteFile(file, nil, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gone := filepath.Join(dir, "gone")
|
|
if err := store.Save(statePath, store.State{Resources: []store.Applied{
|
|
{ID: "notes.data", Type: "directory", Target: found, AsFound: true},
|
|
{ID: "notes.linked", Type: "directory", Target: linked, AsFound: true},
|
|
{ID: "notes.through", Type: "directory", Target: through, AsFound: true},
|
|
{ID: "notes.file", Type: "directory", Target: file, AsFound: true},
|
|
{ID: "notes.gone", Type: "directory", Target: gone, AsFound: true},
|
|
}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Each refused by its own check, which the reason names.
|
|
refused := map[string][2]string{
|
|
"a relative path": {"found", "not one"},
|
|
"a path with ..": {dir + "/x/../found", "as the engine states it"},
|
|
"a doubled separator": {dir + "//found", "as the engine states it"},
|
|
"a trailing separator": {found + "/", "as the engine states it"},
|
|
"a directory not used as found": {filepath.Join(dir, "other"), "not a directory this machine uses as found"},
|
|
"a link at the path": {linked, "is a link"},
|
|
"a link in the path": {through, "reached through a link"},
|
|
"a file": {file, "is not a directory on this machine"},
|
|
"a directory that is gone": {gone, "cannot be read on this machine"},
|
|
}
|
|
for name, c := range refused {
|
|
said, err := handOverFound(statePath, c[0], "jo")
|
|
if err == nil || said != "" {
|
|
t.Fatalf("%s (%s) was handed over: %q", name, c[0], said)
|
|
}
|
|
if !strings.Contains(err.Error(), "nothing was handed over") || !strings.Contains(err.Error(), c[1]) {
|
|
t.Fatalf("%s's refusal does not say %q and that nothing was handed over: %v", name, c[1], err)
|
|
}
|
|
}
|
|
if all, err := store.LoadHandedOver(statePath); err != nil || len(all) != 0 {
|
|
t.Fatalf("a refusal recorded something: %+v %v", all, err)
|
|
}
|
|
// The found directory itself, stated exactly, is taken.
|
|
if _, err := handOverFound(statePath, found, "jo"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if all, _ := store.LoadHandedOver(statePath); len(all) != 1 || all[0].Path != found {
|
|
t.Fatalf("recorded: %+v", all)
|
|
}
|
|
}
|