package api import ( "net/http" "testing" "gitea.dooplex.hu/admin/felhom-hub/internal/store" ) // Security review 2026-10-12, Part 3 (R-955): every box-facing route that names a host or customer must refuse a // key that belongs to a different box. Most routes already had such a test (TestEscrowGet_CrossHostRefused, // TestGetDesiredState_SelfScoped, …); these five had none. Each asserts the refusal AND that the other box's // record is unchanged where the route writes. Red-proof (2026-10-12): with handleGetRestoreDirective's host check // disabled, TestCrossBox_RestoreDirectiveForOtherHostRefused failed (h1's key got 200); the other four were not // red-proved one by one (R-955). func newTwoBoxHandler(t *testing.T) (*Handler, *store.Store) { t.Helper() h, st := newTwoCustomerHandler(t) // customers c1/ckey and c2/c2key for _, hst := range []*store.Host{ {HostID: "h1", CustomerID: "c1", APIKey: "h1key"}, {HostID: "h2", CustomerID: "c2", APIKey: "h2key"}, } { if err := st.UpsertHost(hst); err != nil { t.Fatalf("UpsertHost %s: %v", hst.HostID, err) } } return h, st } func refused(code int) bool { return code == http.StatusUnauthorized || code == http.StatusForbidden } func TestCrossBox_ReportForOtherCustomerRefused(t *testing.T) { h, st := newTwoBoxHandler(t) rr := do(h, http.MethodPost, "/report", "c2key", `{"customer_id":"c1","customer_name":"written by c2"}`) if !refused(rr.Code) { t.Fatalf("c2's key reporting as c1: status = %d, want 401/403", rr.Code) } if c, _ := st.GetCustomer("c1"); c != nil { t.Fatalf("c1's report was stored from c2's key: %q", c.ReportJSON) } } func TestCrossBox_EventForOtherCustomerRefused(t *testing.T) { h, st := newTwoBoxHandler(t) rr := do(h, http.MethodPost, "/event", "c2key", eventBody("error", "test")) if !refused(rr.Code) { t.Fatalf("c2's key posting an event for c1: status = %d, want 401/403", rr.Code) } if evs, _ := st.GetRecentEvents("c1", 10); len(evs) != 0 { t.Fatalf("c1 got %d event(s) from c2's key", len(evs)) } } func TestCrossBox_EscrowPutForOtherHostRefused(t *testing.T) { h, _ := newTwoBoxHandler(t) rr := do(h, http.MethodPut, "/hosts/h2/escrow", "h1key", `{}`) if !refused(rr.Code) { t.Fatalf("h1's key writing h2's escrow: status = %d, want 401/403", rr.Code) } } func TestCrossBox_RestoreDirectiveForOtherHostRefused(t *testing.T) { h, _ := newTwoBoxHandler(t) // Arm h2's recovery mode, so the "not in recovery mode" 403 cannot mask a missing host check. if rr := do(h, http.MethodPut, "/admin/hosts/h2/recovery-mode", globalKey, `{"ttl_seconds":600}`); rr.Code != http.StatusOK { t.Fatalf("arm h2 recovery mode: status = %d", rr.Code) } if rr := do(h, http.MethodGet, "/hosts/h2/restore-directive", "h2key", ""); rr.Code != http.StatusOK { t.Fatalf("positive control: h2's own key: status = %d, want 200", rr.Code) } rr := do(h, http.MethodGet, "/hosts/h2/restore-directive", "h1key", "") if !refused(rr.Code) { t.Fatalf("h1's key reading h2's restore directive: status = %d, want 401/403", rr.Code) } } func TestCrossBox_OffsitePasswordForOtherCustomerRefused(t *testing.T) { h, _ := newTwoBoxHandler(t) rr := do(h, http.MethodPost, "/offsite/consume-password/c1", "c2key", `{}`) if !refused(rr.Code) { t.Fatalf("c2's key on c1's off-site password: status = %d, want 401/403", rr.Code) } }