Files
zeekay 5fd1e59baf SuperAdmin is a person whose home org is admin, on every door the gateway has
The relay gate and the money bits asked authz's PlatformSudo, which granted
platform authority to an admin membership held at any position, so a brand
org's member added to admin got X-User-IsAdmin and the Admin bit here while
cloud and IAM said no. authz v1.10.47 has no PlatformSudo; both transports
now ask Claims.Sudo.

cmd/admin-api asked the owner CLAIM, the org of the application that minted
the token, behind an IAM_ADMIN_ORG knob. A bearer is now asked Sudo, and a
session the server's isSuperAdmin verdict. The knob and the row-level
is_global_admin column, a copy of the predicate nothing read, are gone.

authz v1.10.47 brings zip v1.37.3, whose untyped routes are Raw, and brand
orgs that open only to the people who run them, so the fixtures' plain
member now lives in a customer org.
2026-10-18 21:17:58 -07:00

178 lines
6.3 KiB
Go

package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/hanzoai/gateway/v2/iam"
"github.com/hanzoai/gateway/v2/token"
)
// fakeCloudAPI stands in for cloud's GET /v1/ai/account. It echoes a
// fixed account so the data-layer gate resolves a browser session exactly as
// it does in production.
func fakeCloudAPI(t *testing.T, account map[string]any) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/ai/account" {
http.NotFound(w, r)
return
}
_ = json.NewEncoder(w).Encode(map[string]any{"status": "ok", "data": account})
}))
}
// fakeIAM stands in for the in-cluster IAM the aggregator fans out to.
//
// It answers the RESOURCE addresses and the shape they really carry: the body is
// the list itself, under a key named for the resource, and nothing is wrapped.
// Any other address is a test failure rather than a 404 — a fixture that quietly
// served whatever it was asked is how a client kept agreeing with a fixture while
// production answered 410.
func fakeIAM(t *testing.T) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case iam.Applications:
_ = json.NewEncoder(w).Encode(map[string]any{
"applications": []map[string]any{{"owner": "admin", "name": "hanzo-cloud", "clientId": "abc123"}},
})
case iam.Roles:
_ = json.NewEncoder(w).Encode(map[string]any{
"roles": []map[string]any{{"owner": "admin", "name": "operators"}},
"total": 1,
})
case iam.AuditLogs:
_ = json.NewEncoder(w).Encode(map[string]any{
"auditLogs": []map[string]any{{"owner": "admin", "action": "login"}},
"total": 1,
})
default:
t.Errorf("aggregator asked IAM for %s, which IAM does not serve", r.URL.Path)
http.NotFound(w, r)
}
}))
}
func newTestServer(cloudURL, iamURL string) *server {
cfg := &config{
cloudURL: cloudURL,
iamInternal: iamURL,
validator: token.NewValidator(token.Config{}), // no token in tests → ErrNoToken → session path
}
return &server{cfg: cfg, agg: &aggregator{cfg: cfg}}
}
// request through the real gate, carrying a browser session cookie so
// sessionIdentity (the path dave's browser uses) runs.
func gatedGet(srv *server, h http.HandlerFunc, path string) *httptest.ResponseRecorder {
req := httptest.NewRequest(http.MethodGet, path, nil)
req.Header.Set("Cookie", "hanzo_session=opaque")
rec := httptest.NewRecorder()
srv.gate(h)(rec, req)
return rec
}
// The core privilege boundary: a tenant org-admin must be denied god-mode
// data at the data layer, independent of the SPA gate. This is the backend
// half of the "dave" fix.
func TestGate_DeniesTenantOrgAdmin(t *testing.T) {
cloud := fakeCloudAPI(t, map[string]any{"owner": "acme", "name": "dave", "isAdmin": true, "type": "normal-user"})
defer cloud.Close()
idp := fakeIAM(t)
defer idp.Close()
srv := newTestServer(cloud.URL, idp.URL)
for _, path := range []string{"/v1/admin/applications", "/v1/admin/roles", "/v1/admin/overview"} {
rec := gatedGet(srv, srv.handleApplications, path)
if rec.Code != http.StatusForbidden {
t.Fatalf("%s: tenant org-admin should be 403, got %d (%s)", path, rec.Code, rec.Body.String())
}
}
}
// The known misconfig angle: a `hanzo` TENANT org-admin must STILL be denied
// here — an org's own admin is never a SuperAdmin, whatever a deployment value
// once said about which orgs were "admin" ones.
func TestGate_DeniesHanzoTenantOrg(t *testing.T) {
cloud := fakeCloudAPI(t, map[string]any{"owner": "hanzo", "name": "dave", "isAdmin": true, "type": "normal-user"})
defer cloud.Close()
idp := fakeIAM(t)
defer idp.Close()
srv := newTestServer(cloud.URL, idp.URL)
rec := gatedGet(srv, srv.handleApplications, "/v1/admin/applications")
if rec.Code != http.StatusForbidden {
t.Fatalf("hanzo tenant org-admin should be 403, got %d", rec.Code)
}
}
func TestGate_DeniesAnonymous(t *testing.T) {
cloud := fakeCloudAPI(t, map[string]any{"owner": "admin", "type": "anonymous-user"})
defer cloud.Close()
idp := fakeIAM(t)
defer idp.Close()
srv := newTestServer(cloud.URL, idp.URL)
rec := gatedGet(srv, srv.handleApplications, "/v1/admin/applications")
if rec.Code != http.StatusForbidden {
t.Fatalf("anonymous session should be 403, got %d", rec.Code)
}
}
func TestGate_DeniesNoCookie(t *testing.T) {
cloud := fakeCloudAPI(t, map[string]any{"owner": "admin", "isAdmin": true})
defer cloud.Close()
idp := fakeIAM(t)
defer idp.Close()
srv := newTestServer(cloud.URL, idp.URL)
req := httptest.NewRequest(http.MethodGet, "/v1/admin/applications", nil)
rec := httptest.NewRecorder()
srv.gate(srv.handleApplications)(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf("no-cookie request should be 403, got %d", rec.Code)
}
}
// A SuperAdmin — the server's verdict on the session — passes the gate and gets
// the live IAM applications via the unified /v1/admin/* surface.
func TestGate_AllowsSuperAdmin_Applications(t *testing.T) {
cloud := fakeCloudAPI(t, map[string]any{"owner": "admin", "name": "root", "isAdmin": true, "type": "normal-user", "isSuperAdmin": true})
defer cloud.Close()
idp := fakeIAM(t)
defer idp.Close()
srv := newTestServer(cloud.URL, idp.URL)
rec := gatedGet(srv, srv.handleApplications, "/v1/admin/applications")
if rec.Code != http.StatusOK {
t.Fatalf("SuperAdmin should be 200, got %d (%s)", rec.Code, rec.Body.String())
}
var env struct {
Status string `json:"status"`
Data []map[string]any `json:"data"`
Total int `json:"total"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &env); err != nil {
t.Fatalf("decode: %v", err)
}
if env.Status != "ok" || len(env.Data) != 1 || env.Data[0]["name"] != "hanzo-cloud" || env.Total != 1 {
t.Fatalf("unexpected body: %s", rec.Body.String())
}
}
func TestGate_AllowsSuperAdmin_Roles(t *testing.T) {
cloud := fakeCloudAPI(t, map[string]any{"owner": "admin", "name": "root", "isAdmin": true, "type": "normal-user", "isSuperAdmin": true})
defer cloud.Close()
idp := fakeIAM(t)
defer idp.Close()
srv := newTestServer(cloud.URL, idp.URL)
rec := gatedGet(srv, srv.handleRoles, "/v1/admin/roles")
if rec.Code != http.StatusOK {
t.Fatalf("SuperAdmin should be 200, got %d (%s)", rec.Code, rec.Body.String())
}
}