85 lines
3.3 KiB
Go
85 lines
3.3 KiB
Go
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)
|
|
}
|
|
}
|