package api import ( "bytes" "log" "path/filepath" "strings" "testing" "gitea.dooplex.hu/admin/felhom-hub/internal/store" _ "modernc.org/sqlite" ) func TestRecoveryCredential_VaultSelfScopedAndOperatorRetrieval(t *testing.T) { h, st, _ := newTestHandler(t) st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "HKEY"}) st.UpsertHost(&store.Host{HostID: "h2", CustomerID: "c1", APIKey: "HKEY2"}) body := `{"username":"root@pam","password":"Str0ng-Break-Glass"}` // self-scoped write: h1's key vaults h1 → 200 if rr := do(h, "PUT", "/hosts/h1/recovery-credential", "HKEY", body); rr.Code != 200 { t.Fatalf("self vault = %d: %s", rr.Code, rr.Body.String()) } // a host key CANNOT vault ANOTHER host → 403 if rr := do(h, "PUT", "/hosts/h1/recovery-credential", "HKEY2", body); rr.Code != 403 { t.Fatalf("cross-host vault must be 403, got %d", rr.Code) } // unauthenticated → 401 if rr := do(h, "PUT", "/hosts/h1/recovery-credential", "", body); rr.Code != 401 { t.Fatalf("unauth vault must be 401, got %d", rr.Code) } // operator retrieval requires the GLOBAL key; a host key is refused (401 — can't read its own back) if rr := do(h, "GET", "/admin/hosts/h1/recovery-credential", "HKEY", ""); rr.Code != 401 { t.Fatalf("host key reading recovery credential must be 401, got %d", rr.Code) } rr := do(h, "GET", "/admin/hosts/h1/recovery-credential", globalKey, "") if rr.Code != 200 { t.Fatalf("operator retrieval = %d: %s", rr.Code, rr.Body.String()) } if !strings.Contains(rr.Body.String(), "Str0ng-Break-Glass") || !strings.Contains(rr.Body.String(), "root@pam") { t.Fatalf("retrieval must return the vaulted credential, got %s", rr.Body.String()) } // a host with no credential → 404 if rr := do(h, "GET", "/admin/hosts/h2/recovery-credential", globalKey, ""); rr.Code != 404 { t.Fatalf("no-credential host must be 404, got %d", rr.Code) } } // SECRET DISCIPLINE (red-proof for trap 3): the password must NEVER appear in the hub log — on the // vault write NOR the operator retrieval. Companion: if a handler logged the password, this fails. func TestRecoveryCredential_PasswordNeverLogged(t *testing.T) { var buf bytes.Buffer path := filepath.Join(t.TempDir(), "test.db") st, err := store.New(path, log.New(&buf, "", 0)) if err != nil { t.Fatalf("store.New: %v", err) } defer st.Close() h := New(st, globalKey, "", "", nil, log.New(&buf, "", 0)) st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "HKEY"}) const secret = "SuperSecret-DoNotLog-42" if rr := do(h, "PUT", "/hosts/h1/recovery-credential", "HKEY", `{"username":"root@pam","password":"`+secret+`"}`); rr.Code != 200 { t.Fatalf("vault = %d", rr.Code) } if rr := do(h, "GET", "/admin/hosts/h1/recovery-credential", globalKey, ""); rr.Code != 200 { t.Fatalf("retrieve = %d", rr.Code) } if strings.Contains(buf.String(), secret) { t.Fatalf("the recovery password LEAKED into the hub log:\n%s", buf.String()) } }