Files
felhom-agent/internal/reconcile/storage_wipe_test.go
T

96 lines
4.6 KiB
Go

package reconcile
import "testing"
// The storage-authorization redesign: a USER-DATA data-bearing wipe is customer-confirmable
// (durable-id-bound, no operator signature); SYSTEM and BACKUP stay operator-signature ONLY, and a
// `confirmed:true` claim on them is REFUSED by role (a compromised controller can't relabel a
// protected device to walk the gate). These assert the gate's tiering — the non-hollow checks the
// spec calls for: "the gate refuses a `confirmed` wipe on system/backup (assert no exec); a user-data
// confirmed wipe binds to the durable id (a mismatched id is refused)".
const durA = "byid:wwn-0x5000c500a"
const durB = "byuuid:1111-2222"
// user-data + confirmed + matching durable id → allowed (customer-confirmable), audited with the id.
func TestStorageWipe_UserDataConfirmed_Allowed(t *testing.T) {
aud := &captureAudit{}
g := NewGate(nil, testHost, aud, nil) // NO verifier pinned — proves no operator signature is needed
d := g.AuthorizeStorageWipe(StorageWipeAuthz{
HostID: testHost, Role: StorageRoleUserData,
DeviceDurableID: durA, Confirmed: true, ConfirmDurableID: durA,
}, nil)
if !d.Allowed || d.Disposition != CustomerConfirmable || d.Reason != ReasonCustomerConfirmed {
t.Fatalf("user-data confirmed+matching: got allowed=%v disp=%s reason=%s", d.Allowed, d.Disposition, d.Reason)
}
if len(aud.recs) != 1 || !aud.recs[0].Allowed || aud.recs[0].DurableID != durA {
t.Fatalf("customer-confirmed wipe must be audited with the durable id: %+v", aud.recs)
}
}
// user-data + confirmed but the confirmation binds to a DIFFERENT disk's durable id → refused
// (binding_mismatch). A confirmation for one disk can't wipe another.
func TestStorageWipe_UserDataDurableMismatch_Refused(t *testing.T) {
g := NewGate(nil, testHost, &captureAudit{}, nil)
d := g.AuthorizeStorageWipe(StorageWipeAuthz{
HostID: testHost, Role: StorageRoleUserData,
DeviceDurableID: durA, Confirmed: true, ConfirmDurableID: durB, // confirms B, device is A
}, nil)
if d.Allowed || d.Reason != ReasonBindingMismatch {
t.Fatalf("durable-id mismatch: got allowed=%v reason=%s, want refused binding_mismatch", d.Allowed, d.Reason)
}
}
// user-data + an UNRESOLVABLE device durable id fails safe (refused) even when confirmed.
func TestStorageWipe_UserDataNoDurable_Refused(t *testing.T) {
g := NewGate(nil, testHost, &captureAudit{}, nil)
d := g.AuthorizeStorageWipe(StorageWipeAuthz{
HostID: testHost, Role: StorageRoleUserData,
DeviceDurableID: "", Confirmed: true, ConfirmDurableID: "",
}, nil)
if d.Allowed {
t.Fatal("a wipe with no resolvable durable id must not be allowed even when confirmed")
}
}
// user-data, NOT confirmed → pending_confirmation (ask the customer; not a signature).
func TestStorageWipe_UserDataUnconfirmed_PendingConfirmation(t *testing.T) {
g := NewGate(nil, testHost, &captureAudit{}, nil)
d := g.AuthorizeStorageWipe(StorageWipeAuthz{
HostID: testHost, Role: StorageRoleUserData, DeviceDurableID: durA,
}, nil)
if d.Allowed || d.Disposition != CustomerConfirmable || d.Reason != ReasonPendingConfirmation {
t.Fatalf("user-data unconfirmed: got allowed=%v disp=%s reason=%s", d.Allowed, d.Disposition, d.Reason)
}
}
// THE HEADLINE: a `confirmed:true` wipe on a SYSTEM device is REFUSED — it falls to the
// operator-signature path (no signer pinned → pending_signature), NOT customer-confirmable. The
// customer's confirmation is ignored BY ROLE. Same for BACKUP.
func TestStorageWipe_SystemConfirmedTrue_RefusedBySignature(t *testing.T) {
for _, role := range []string{StorageRoleSystem, StorageRoleBackup} {
g := NewGate(nil, testHost, &captureAudit{}, nil)
d := g.AuthorizeStorageWipe(StorageWipeAuthz{
HostID: testHost, Role: role,
DeviceDurableID: durA, Confirmed: true, ConfirmDurableID: durA, // a matching confirmation — must NOT help
}, nil)
if d.Allowed {
t.Fatalf("role=%s: a confirmed wipe of a protected device was ALLOWED — invariant violated", role)
}
if d.Disposition != Destructive || d.Reason != ReasonPendingSignature {
t.Fatalf("role=%s: got disp=%s reason=%s, want destructive/pending_signature", role, d.Disposition, d.Reason)
}
}
}
// An UNKNOWN/empty role fails safe to the protected (operator-signature) path, never user-data.
func TestStorageWipe_UnknownRole_FailsSafeDestructive(t *testing.T) {
g := NewGate(nil, testHost, &captureAudit{}, nil)
d := g.AuthorizeStorageWipe(StorageWipeAuthz{
HostID: testHost, Role: "", Confirmed: true, DeviceDurableID: durA, ConfirmDurableID: durA,
}, nil)
if d.Allowed || d.Disposition != Destructive {
t.Fatalf("unknown role must fail safe to destructive: got allowed=%v disp=%s", d.Allowed, d.Disposition)
}
}