3f382bf762
From AUDIT-blast-radius-hostroot-localapi-2026-07-02.md. Each fix ships with a non-hollow test + a companion red-proof (shown failing on the pre-fix impl). Sudoers install-source grants became globs — deploy the sudoers drop-in with the binary. A1 (stale-lock pool-membership) deliberately excluded (spike). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
148 lines
6.3 KiB
Go
148 lines
6.3 KiB
Go
package localapi
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-agent/internal/storage"
|
|
)
|
|
|
|
// ---- Audit D3: the blank-format branch's anti-retarget guard ------------------------------
|
|
//
|
|
// AGENT-001 closed the classify→mkfs TOCTOU on the data-bearing branch; these tests pin the same
|
|
// guard onto the BLANK branch: the format must bind to a durable id, re-resolve it to the CURRENT
|
|
// device, and format THAT — never the mutable caller-supplied /dev path.
|
|
|
|
// Pure-function coverage of antiRetargetResolveBlank (the injected-deps sibling; no real /dev).
|
|
func TestFormatBlankPath_AntiRetarget_ReassignedDataBearingRefused(t *testing.T) {
|
|
const durable = "byid:wwn-0xBLANK"
|
|
dataBearing := storage.DeviceProbe{Probed: true, HasFilesystem: true, FSType: "ext4"}
|
|
got, err := antiRetargetResolveBlank(durable,
|
|
func(string) (string, error) { return "/dev/sdb", nil },
|
|
func(string) (string, error) { return durable, nil },
|
|
func(string) (storage.DeviceProbe, error) { return dataBearing, nil },
|
|
)
|
|
if err == nil {
|
|
t.Fatalf("re-inspect found a DATA-BEARING disk on the blank path — must refuse, got device %q", got)
|
|
}
|
|
if !strings.Contains(err.Error(), "now data-bearing") {
|
|
t.Errorf("error %q missing the now-data-bearing explanation", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestFormatBlankPath_AntiRetarget_ReassignedDifferentDiskRefused(t *testing.T) {
|
|
const durable = "byid:wwn-0xBLANK"
|
|
blank := storage.DeviceProbe{Probed: true}
|
|
got, err := antiRetargetResolveBlank(durable,
|
|
func(string) (string, error) { return "/dev/sdb", nil },
|
|
func(string) (string, error) { return "byid:wwn-0xDIFFERENT", nil }, // node reassigned → different id
|
|
func(string) (storage.DeviceProbe, error) { return blank, nil },
|
|
)
|
|
if err == nil || !strings.Contains(err.Error(), "durable-id mismatch") {
|
|
t.Fatalf("want durable-id mismatch refusal, got device %q err %v", got, err)
|
|
}
|
|
}
|
|
|
|
func TestFormatBlankPath_AntiRetarget_UnresolvableRefused(t *testing.T) {
|
|
blank := storage.DeviceProbe{Probed: true}
|
|
// Unresolvable durable id → refuse.
|
|
if got, err := antiRetargetResolveBlank("byid:wwn-0xGONE",
|
|
func(string) (string, error) { return "", errors.New("gone") },
|
|
func(string) (string, error) { return "byid:wwn-0xGONE", nil },
|
|
func(string) (storage.DeviceProbe, error) { return blank, nil },
|
|
); err == nil || !strings.Contains(err.Error(), "no longer resolves") {
|
|
t.Fatalf("want no-longer-resolves refusal, got device %q err %v", got, err)
|
|
}
|
|
// Empty durable id (nothing to bind) → refuse: a path-only blank format is what the guard prevents.
|
|
if got, err := antiRetargetResolveBlank("",
|
|
func(string) (string, error) { return "/dev/sdb", nil },
|
|
func(string) (string, error) { return "", nil },
|
|
func(string) (storage.DeviceProbe, error) { return blank, nil },
|
|
); err == nil || !strings.Contains(err.Error(), "path-only") {
|
|
t.Fatalf("want path-only refusal on empty durable id, got device %q err %v", got, err)
|
|
}
|
|
}
|
|
|
|
func TestFormatBlankPath_AntiRetarget_SameBlankProceeds(t *testing.T) {
|
|
const durable = "byid:wwn-0xBLANK"
|
|
blank := storage.DeviceProbe{Probed: true}
|
|
got, err := antiRetargetResolveBlank(durable,
|
|
func(string) (string, error) { return "/dev/sdb", nil },
|
|
func(string) (string, error) { return durable, nil },
|
|
func(string) (storage.DeviceProbe, error) { return blank, nil },
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("same still-blank disk must proceed, got refusal: %v", err)
|
|
}
|
|
if got != "/dev/sdb" {
|
|
t.Fatalf("device = %q, want the re-resolved /dev/sdb", got)
|
|
}
|
|
}
|
|
|
|
// ---- handler wiring: the blank branch formats the RE-RESOLVED device, never req.Device ----
|
|
|
|
// The red-proof companion vs the pre-fix handler: pre-fix, the blank branch called
|
|
// startFormatDetached(req.Device, ...) directly, so the format hit the caller path even when the
|
|
// re-resolve seam says the durable id now lives at a different node. Post-fix, mkfs must run on the
|
|
// re-resolved device.
|
|
func TestFormat_Blank_FormatsReresolvedDeviceNotCallerPath(t *testing.T) {
|
|
d := &fakeDiskOps{probe: storage.DeviceProbe{Probed: true}} // blank
|
|
srv := newDiskServerRaw(t, d, &fakeGate{}, nil, nil)
|
|
var boundID string
|
|
srv.reresolveBlank = func(_ context.Context, durableID string) (string, error) {
|
|
boundID = durableID
|
|
return "/dev/sdz", nil // the durable id's CURRENT node differs from the caller path
|
|
}
|
|
h := srv.Handler()
|
|
|
|
w := do(t, h, "POST", "/disks/format", "A", `{"device":"/dev/sdb","fstype":"ext4"}`)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("blank format: got %d want 200 (%s)", w.Code, w.Body.String())
|
|
}
|
|
if boundID != "byid:wwn-sdb" {
|
|
t.Errorf("blank format bound durable id %q, want the one derived from req.Device", boundID)
|
|
}
|
|
got := d.formatted()
|
|
if len(got) != 1 || got[0] != "/dev/sdz" {
|
|
t.Fatalf("mkfs ran on %v — must run on the RE-RESOLVED device /dev/sdz, never the caller path /dev/sdb", got)
|
|
}
|
|
}
|
|
|
|
// A blank-branch anti-retarget refusal (device changed in the window) must refuse 409 with ZERO mkfs.
|
|
func TestFormat_Blank_ReresolveRefusalNoMkfs(t *testing.T) {
|
|
d := &fakeDiskOps{probe: storage.DeviceProbe{Probed: true}} // blank
|
|
srv := newDiskServerRaw(t, d, &fakeGate{}, nil, nil)
|
|
srv.reresolveBlank = func(_ context.Context, durableID string) (string, error) {
|
|
return "", fmt.Errorf("%s is now data-bearing (target changed since blank inspection) — refusing", "/dev/sdb")
|
|
}
|
|
h := srv.Handler()
|
|
|
|
w := do(t, h, "POST", "/disks/format", "A", `{"device":"/dev/sdb","fstype":"ext4"}`)
|
|
if w.Code != http.StatusConflict {
|
|
t.Fatalf("refused blank format: got %d want 409 (%s)", w.Code, w.Body.String())
|
|
}
|
|
if got := d.formatted(); len(got) != 0 {
|
|
t.Fatalf("mkfs WAS called after an anti-retarget refusal: %v", got)
|
|
}
|
|
}
|
|
|
|
// A device with no durable id cannot be bound → the blank format is refused (no path-only formats).
|
|
func TestFormat_Blank_NoDurableIDRefused(t *testing.T) {
|
|
d := &fakeDiskOps{probe: storage.DeviceProbe{Probed: true}} // blank
|
|
srv := newDiskServerRaw(t, d, &fakeGate{}, nil, nil)
|
|
srv.deviceDurableID = func(string) (string, error) { return "", errors.New("no by-id/by-uuid entry") }
|
|
h := srv.Handler()
|
|
|
|
w := do(t, h, "POST", "/disks/format", "A", `{"device":"/dev/sdb","fstype":"ext4"}`)
|
|
if w.Code != http.StatusConflict {
|
|
t.Fatalf("unbindable blank format: got %d want 409 (%s)", w.Code, w.Body.String())
|
|
}
|
|
if got := d.formatted(); len(got) != 0 {
|
|
t.Fatalf("mkfs WAS called with no durable-id binding: %v", got)
|
|
}
|
|
}
|