mirror of
https://github.com/hanzoai/gateway.git
synced 2026-10-30 15:04:44 +00:00
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.
112 lines
4.0 KiB
Go
112 lines
4.0 KiB
Go
// Copyright © 2026 Hanzo AI. MIT License.
|
|
|
|
//go:build legacy
|
|
// +build legacy
|
|
|
|
// gin-driven: this suite drives the LEGACY Lura edge's transports
|
|
// (legacy_transports.go), which exist only under this tag. The policies it
|
|
// exercises are framework-free values shared with the zip edge, and
|
|
// transport_parity_test.go asserts the two edges answer alike.
|
|
|
|
package gateway
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// These tests pin the authed class: every /v1/* validates the IAM-JWT and WRITES
|
|
// identity FROM the JWT over a stripped slate — the gateway is the SOLE identity
|
|
// source.
|
|
// The global ingress strip is load-bearing (several identity headers are written only
|
|
// conditionally, so a forged copy would otherwise survive on the authed class).
|
|
|
|
type backendSaw struct {
|
|
reached bool
|
|
org string
|
|
user string
|
|
glob string // X-User-IsGlobalAdmin
|
|
owner string // X-User-Owner (home org)
|
|
}
|
|
|
|
func recordingBackend(r *gin.Engine, saw *backendSaw) {
|
|
r.Any("/*path", func(c *gin.Context) {
|
|
saw.reached = true
|
|
saw.org = c.Request.Header.Get("X-Org-Id")
|
|
saw.user = c.Request.Header.Get("X-User-Id")
|
|
saw.glob = c.Request.Header.Get("X-User-IsGlobalAdmin")
|
|
saw.owner = c.Request.Header.Get("X-User-Owner")
|
|
c.Status(http.StatusOK)
|
|
})
|
|
}
|
|
|
|
func send(r *gin.Engine, method, path, auth string, forged map[string]string) *httptest.ResponseRecorder {
|
|
req := httptest.NewRequest(method, path, nil)
|
|
req.Host = "api.hanzo.ai"
|
|
if auth != "" {
|
|
req.Header.Set("Authorization", "Bearer "+auth)
|
|
}
|
|
for k, v := range forged {
|
|
req.Header.Set(k, v)
|
|
}
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
return w
|
|
}
|
|
|
|
// Scenarios 1, 2, 6: an authed Sentry READ with no JWT (with or without a forged
|
|
// X-Org-Id) is 401 — the read routes to the AUTHED class, never the ingest class.
|
|
func TestIngestClass_AuthedReadRequiresJWT(t *testing.T) {
|
|
r, _, jwks := setupMiddlewareWithJWKS(t, nil)
|
|
defer jwks.Close()
|
|
var saw backendSaw
|
|
recordingBackend(r, &saw)
|
|
|
|
if w := send(r, http.MethodGet, "/v1/sentinel/issues?project=019f5339", "", nil); w.Code != http.StatusUnauthorized {
|
|
t.Errorf("read with no JWT: got %d, want 401", w.Code)
|
|
}
|
|
// A forged X-Org-Id must NOT buy a read (the gate rejects for lack of a valid JWT).
|
|
if w := send(r, http.MethodGet, "/v1/sentinel/issues", "", map[string]string{"X-Org-Id": "victim-org"}); w.Code != http.StatusUnauthorized {
|
|
t.Errorf("read with forged X-Org-Id + no JWT: got %d, want 401", w.Code)
|
|
}
|
|
if saw.reached {
|
|
t.Error("SECURITY: an unauthenticated read reached the backend")
|
|
}
|
|
}
|
|
|
|
// Scenario 3 (+ the load-bearing-strip proof): a valid JWT with a forged X-Org-Id
|
|
// AND a forged X-User-IsGlobalAdmin — the backend must see the JWT's org (hanzo),
|
|
// NOT the forged org, and must NOT see SuperAdmin (the JWT asserts neither, and
|
|
// the conditional write does not re-set it, so the strip is what defeats the forge).
|
|
func TestIngestClass_AuthedWriteOverwritesForgedIdentity(t *testing.T) {
|
|
r, tj, jwks := setupMiddlewareWithJWKS(t, nil)
|
|
defer jwks.Close()
|
|
var saw backendSaw
|
|
recordingBackend(r, &saw)
|
|
|
|
tok := tj.signToken(t, validClaims("https://hanzo.id", "https://api.hanzo.ai")) // a customer, not a SuperAdmin
|
|
w := send(r, http.MethodGet, "/v1/sentinel/issues", tok, map[string]string{
|
|
"X-Org-Id": "victim-org",
|
|
"X-User-IsGlobalAdmin": "true",
|
|
"X-User-Owner": "admin",
|
|
})
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("valid-JWT read: got %d, want 200", w.Code)
|
|
}
|
|
if !saw.reached {
|
|
t.Fatal("valid-JWT read did not reach the backend")
|
|
}
|
|
if saw.org != "acme" {
|
|
t.Errorf("SECURITY: backend saw X-Org-Id %q, want the token's home org \"acme\" (forge not overwritten)", saw.org)
|
|
}
|
|
if saw.owner != "acme" {
|
|
t.Errorf("SECURITY: backend saw X-User-Owner %q, want the token's home org \"acme\" (orgs[0]; forged \"admin\" overwritten)", saw.owner)
|
|
}
|
|
if saw.glob != "" {
|
|
t.Errorf("SECURITY: forged legacy X-User-IsGlobalAdmin survived (%q) — never written now (platform sudo = org==admin); the ingress strip must drop a forged copy", saw.glob)
|
|
}
|
|
}
|