fix(cloud): purge hanzoai/zip (forward zip-canonical-home) + complete F1 data-plane gates

Startup crash on v1.786.15/.16: `cloud: mount metrics: metrics.Mount: app is
*zip.App, want *zip.App`. Cloud's core migrated to github.com/zap-proto/zip
(v1.786.13→.15) so app is *zap-proto/zip.App, but eight hanzo modules cloud
imports still pinned the OLD github.com/hanzoai/zip and registered subsystems
that type-assert app.(*hanzoai/zip.App). Distinct import paths = distinct Go
types, so MountAll's first such subsystem ("metrics", hanzoai/metrics@v0.4.0)
failed the assert at runtime (build/vet/test stayed green — the mismatch is
runtime-only). authz/base/o11y were the same latent break behind it.

Forward fix (the canonical-home migration was already released upstream; cloud
merely lagged): bump every lagging module to its migrated tag —
  ai v1.789.1-… → v1.789.1        authz v1.10.1 → v1.10.3
  base v1.4.1 → v1.4.6            commerce v1.42.27 → v1.42.29
  licensing v0.1.0 → v0.1.1       metrics v0.4.0 → v0.4.1
  o11y v1.3.7 → v1.3.12           vfs v0.4.1 → v0.4.4
authz pinned to v1.10.3 specifically: v1.10.2/v1.10.4 carry an unrelated
GetPolicy 2-value change that breaks the pinned hanzoai/iam; v1.10.3 has the zip
migration AND the iam-compatible 1-value signature. commerce pinned to v1.42.29
(v1.43.0 regressed back to hanzoai/zip). clients/analytics (from the merged .16
work) is cloud's own code and is migrated in-place. Result: hanzoai/zip is gone
from go.mod/go.sum and the whole module graph; the compiled binary mounts
metrics+o11y+all subsystems and reaches "listening" with no panic.

Also folds in the COMPLETE F1 close (RED found the gate was partial — two
reverse-proxy paths still forwarded a forged X-Org-Id):
- clients/bot: gate proxy() on principal.Validated before forwarding X-Org-Id to
  bot-gateway (RED PoC red_forge_test.go now passes: no-principal forge → 403).
- clients/o11y: wrap the installed reverse-proxy handler in gate() — refuse any
  request with no X-User-Id before it reaches the o11y runtime (forge twin test).
- clients/crm: extend the forge guard to WRITE+DELETE verbs (belt-and-suspenders).
- middleware_identity: refresh the stale FAIL-MODE comment — post-F1 the DATA
  plane also fails secure on a cold-cache JWKS failure (bounded by stale-on-error).

Base = F1 (fix/cloud-data-plane-principal-gate, 81854518) + origin/main (analytics
.16, e1fcfdd5). One healthy image: F1 + analytics + zip-fix + bot/o11y gates.
This commit is contained in:
2026-07-02 04:37:38 -07:00
parent b5f8d01faf
commit c57cf70fd7
10 changed files with 212 additions and 37 deletions
+1 -1
View File
@@ -61,8 +61,8 @@ import (
aiobject "github.com/hanzoai/ai/object"
"github.com/hanzoai/cloud"
"github.com/hanzoai/zip"
luxlog "github.com/luxfi/log"
"github.com/zap-proto/zip"
)
const (
+1 -1
View File
@@ -15,8 +15,8 @@ import (
"testing"
"github.com/hanzoai/cloud"
"github.com/hanzoai/zip"
luxlog "github.com/luxfi/log"
"github.com/zap-proto/zip"
)
func mountApp(t *testing.T) *zip.App {
+10 -1
View File
@@ -21,8 +21,9 @@ import (
"time"
"github.com/hanzoai/cloud"
"github.com/zap-proto/zip"
"github.com/hanzoai/cloud/clients/principal"
luxlog "github.com/luxfi/log"
"github.com/zap-proto/zip"
)
// identityHeaders are forwarded so bot-gateway sees the gateway-minted tenant
@@ -60,6 +61,14 @@ func Mount(app *zip.App, deps cloud.Deps) error {
}
func (s *service) proxy(c *zip.Ctx) error {
// Gate on a validated principal before forwarding X-Org-Id to bot-gateway,
// which trusts these headers as the gateway-minted tenant context. Off-gateway,
// the identity middleware restores a forged X-Org-Id but leaves X-User-Id empty;
// a no-principal request must be refused before it hands bot-gateway a victim
// tenant — the same gate every data-plane resolver (crm/kms/ml/…) already ships.
if !principal.Validated(c) {
return zip.ErrForbidden("no validated principal")
}
// Strip the /v1/bot prefix — bot-gateway serves bare paths.
rest := strings.TrimPrefix(c.Fiber().Params("*"), "/")
target := s.target + "/" + rest
+57
View File
@@ -0,0 +1,57 @@
package bot
import (
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"github.com/hanzoai/cloud"
luxlog "github.com/luxfi/log"
"github.com/zap-proto/zip"
)
// TestRed_BotProxyForwardsForgedOrgNoPrincipal proves the /v1/bot/* proxy is NOT
// gated on a validated principal. It replays the exact state SanitizeIdentity
// leaves on the off-gateway forge path: X-Org-Id RESTORED to the client's forged
// value, X-User-Id EMPTY (no validated principal). A gated data-plane resolver
// (crm/kms/ml/...) answers 403 in this state. The bot proxy instead forwards the
// forged tenant to bot-gateway — which the package doc says trusts these headers
// as "the gateway-minted tenant context" — reopening the cross-tenant hole for
// every /v1/bot/* surface (billable chat, per-tenant channels/skills/agents).
//
// SECURE behavior (asserted here, FAILS today): a no-principal request must not
// hand bot-gateway a victim tenant — the proxy should 403, or at minimum strip
// the unvalidated identity so bot-gateway sees no forged org.
func TestRed_BotProxyForwardsForgedOrgNoPrincipal(t *testing.T) {
var gotOrg atomic.Value
gotOrg.Store("")
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotOrg.Store(r.Header.Get("X-Org-Id"))
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"ok":true}`))
}))
defer upstream.Close()
t.Setenv("BOT_GATEWAY_URL", upstream.URL)
app := zip.New(zip.Config{Logger: luxlog.New("test")})
if err := Mount(app, cloud.Deps{Logger: luxlog.New("test")}); err != nil {
t.Fatalf("Mount: %v", err)
}
// The off-gateway forge, post-SanitizeIdentity: forged org, NO validated user.
req := httptest.NewRequest(http.MethodGet, "/v1/bot/v1/models", nil)
req.Header.Set("X-Org-Id", "victim") // forged; no X-User-Id → no validated principal
resp, err := app.Fiber().Test(req)
if err != nil {
t.Fatalf("forged bot request: %v", err)
}
_ = resp.Body.Close()
if resp.StatusCode != http.StatusForbidden {
t.Errorf("no-principal forged /v1/bot/* = HTTP %d, want 403 (proxy must gate like every data-plane resolver)", resp.StatusCode)
}
if org := gotOrg.Load().(string); org != "" {
t.Errorf("bot-gateway received X-Org-Id=%q from a NO-PRINCIPAL forge — cross-tenant hole forwarded through cloud's /v1/bot proxy", org)
}
}
+31 -1
View File
@@ -9,8 +9,8 @@ import (
"testing"
"github.com/hanzoai/cloud"
"github.com/zap-proto/zip"
luxlog "github.com/luxfi/log"
"github.com/zap-proto/zip"
)
func mountApp(t *testing.T) *zip.App {
@@ -178,4 +178,34 @@ func TestRed_NoPrincipalForgedOrgRefused(t *testing.T) {
}
_ = resp.Body.Close()
}
// Belt-and-suspenders: WRITE + DELETE verbs route through the SAME principal
// gate. A no-principal forge must never create, mutate, or delete another
// tenant's data — assert 403 before any store access on every mutating verb.
forged := func(method, path string, body io.Reader) int {
req := httptest.NewRequest(method, path, body)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Org-Id", "victim") // forged; deliberately NO X-User-Id
resp, err := app.Fiber().Test(req)
if err != nil {
t.Fatalf("forged %s %s: %v", method, path, err)
}
defer func() { _ = resp.Body.Close() }()
return resp.StatusCode
}
writes := []struct {
method, path string
body io.Reader
}{
{http.MethodPost, "/v1/crm/companies", bytes.NewReader([]byte(`{"name":"Pwned Inc"}`))},
{http.MethodPost, "/v1/crm/contacts", bytes.NewReader([]byte(`{"email":"x@evil.example"}`))},
{http.MethodPost, "/v1/crm/opportunities", bytes.NewReader([]byte(`{"name":"Steal"}`))},
{http.MethodDelete, "/v1/crm/companies/comp_whatever", nil},
{http.MethodDelete, "/v1/crm/contacts/cont_whatever", nil},
}
for _, w := range writes {
if code := forged(w.method, w.path, w.body); code != http.StatusForbidden {
t.Fatalf("forged %s %s want 403 (no validated principal), got %d", w.method, w.path, code)
}
}
}
+21 -1
View File
@@ -59,6 +59,26 @@ func newHandler(rawURL string) (http.Handler, error) {
return proxy, nil
}
// gate refuses any request that carries no validated principal before it reaches
// the o11y runtime. The bare reverse proxy forwards ALL inbound headers upstream,
// including a client-forged X-Org-Id restored on the bearer-less path; without
// this an off-gateway caller reads another tenant's telemetry/logs. X-User-Id is
// set ONLY by the identity middleware from a verified credential (the same signal
// principal.Validated uses), so its presence is the authoritative principal gate.
// Every legitimate /v1/o11y/* caller arrives through the console BFF with a
// user-bound bearer, so this refuses only the anonymous-forge path.
func gate(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.TrimSpace(r.Header.Get("X-User-Id")) == "" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte(`{"status":"error","msg":"no validated principal"}`))
return
}
next.ServeHTTP(w, r)
})
}
func init() {
// Order 71: after o11y.Mount (70) installs the route surface. Ordering is not
// strictly required (the handler is resolved per-request) but keeps the
@@ -68,7 +88,7 @@ func init() {
if err != nil {
return err
}
o11y.SetHandler(h)
o11y.SetHandler(gate(h))
if deps.Logger != nil {
deps.Logger.New("subsystem", "o11y-runtime").
Info("o11y runtime handler installed (reverse proxy)", "upstream", upstream())
+58
View File
@@ -0,0 +1,58 @@
package o11y
import (
"io"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
)
// TestRed_O11yProxyGatesForgedOrgNoPrincipal is the twin of the bot/crm forge
// guards, for the /v1/o11y/* reverse proxy. An off-gateway caller forges X-Org-Id
// with NO validated principal (no X-User-Id — the state the identity middleware
// leaves on the bearer-less path). The bare reverse proxy would forward that
// forged tenant to the o11y runtime (cross-tenant telemetry/logs); gate() must
// refuse it 403 before the upstream is ever reached, and must still pass a
// request that carries a validated principal.
func TestRed_O11yProxyGatesForgedOrgNoPrincipal(t *testing.T) {
var reached atomic.Bool
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
reached.Store(true)
w.WriteHeader(http.StatusOK)
_, _ = io.WriteString(w, `{"status":"ok"}`)
}))
defer upstream.Close()
h, err := newHandler(upstream.URL)
if err != nil {
t.Fatalf("newHandler: %v", err)
}
gated := gate(h)
// Forged org, NO X-User-Id → must 403, upstream never reached.
req := httptest.NewRequest(http.MethodGet, "http://api.hanzo.ai/v1/o11y/v3/query_range", nil)
req.Header.Set("X-Org-Id", "victim") // forged; no X-User-Id → no validated principal
rec := httptest.NewRecorder()
gated.ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf("no-principal forged /v1/o11y/* = HTTP %d, want 403 (proxy must gate like every data-plane resolver)", rec.Code)
}
if reached.Load() {
t.Fatal("o11y runtime received a NO-PRINCIPAL forged request — cross-tenant telemetry hole forwarded through cloud's /v1/o11y proxy")
}
// A validated principal (X-User-Id set by the identity middleware) passes through.
req2 := httptest.NewRequest(http.MethodGet, "http://api.hanzo.ai/v1/o11y/v3/query_range", nil)
req2.Header.Set("X-Org-Id", "acme")
req2.Header.Set("X-User-Id", "u_acme") // validated principal
rec2 := httptest.NewRecorder()
gated.ServeHTTP(rec2, req2)
if rec2.Code != http.StatusOK {
t.Fatalf("validated /v1/o11y/* = HTTP %d, want 200 (gate must pass real principals)", rec2.Code)
}
if !reached.Load() {
t.Fatal("validated request did not reach the o11y runtime — gate is over-blocking legitimate callers")
}
}
+9 -10
View File
@@ -31,8 +31,8 @@ require (
)
require (
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
github.com/google/flatbuffers v25.12.19+incompatible // indirect
github.com/hanzoai/zip v0.2.0 // indirect
github.com/zap-proto/http v0.2.0 // indirect
)
@@ -63,7 +63,6 @@ require (
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
github.com/Machiel/slugify v1.0.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
@@ -647,14 +646,14 @@ require (
github.com/gofiber/schema v1.7.1 // indirect
github.com/gofiber/utils/v2 v2.0.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hanzoai/ai v1.789.1-0.20260701221616-296a9e9becb4
github.com/hanzoai/authz v1.10.1
github.com/hanzoai/base v1.4.1
github.com/hanzoai/commerce v1.42.27
github.com/hanzoai/licensing v0.1.0
github.com/hanzoai/metrics v0.4.0
github.com/hanzoai/o11y v1.3.7
github.com/hanzoai/vfs v0.4.1
github.com/hanzoai/ai v1.789.1
github.com/hanzoai/authz v1.10.3
github.com/hanzoai/base v1.4.6
github.com/hanzoai/commerce v1.42.29
github.com/hanzoai/licensing v0.1.1
github.com/hanzoai/metrics v0.4.1
github.com/hanzoai/o11y v1.3.12
github.com/hanzoai/vfs v0.4.4
github.com/klauspost/compress v1.18.6 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
+16 -18
View File
@@ -1113,22 +1113,22 @@ github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/hanzoai/ai v1.789.1-0.20260701221616-296a9e9becb4 h1:Q5YERS0gyhheKaujymzxWjwY27dnRKVqtJeJdbYJdCg=
github.com/hanzoai/ai v1.789.1-0.20260701221616-296a9e9becb4/go.mod h1:TXC1HZzgh2/QKDUvmEg36qNycPQKgHI0qFIxNsf6X4o=
github.com/hanzoai/ai v1.789.1 h1:OiU1LRwP4LaiJKUr8iwi5JZgSiQoUQhLYFyCL2I9Pzg=
github.com/hanzoai/ai v1.789.1/go.mod h1:eAkyHkoCRxQFUnxHRRj8oTi9jtNbitIm+GNAGjzrXKo=
github.com/hanzoai/alertmanager v0.28.2 h1:40fm5ZDq4MZ7CJwPb7AHVa1DU9kHK5lgMuJUJK7frT4=
github.com/hanzoai/alertmanager v0.28.2/go.mod h1:SOwWSWO7p9CaBKr+bp1IrPfTPjftxp+cnyBK9GhgJFs=
github.com/hanzoai/authz v1.10.1 h1:BXKRUS6vkW57zbzX04gBMGLFW+wdHG5qHKb3OcLHuCo=
github.com/hanzoai/authz v1.10.1/go.mod h1:rFasfZo9RpQH6di96nBmjXsOAw0R07Z1mi3dnAmV6dE=
github.com/hanzoai/authz v1.10.3 h1:ikBLkpwrgssXv6SPviIS28YQEmwcLG+hdRPioSGuRRw=
github.com/hanzoai/authz v1.10.3/go.mod h1:EGzev/cAcAQsP4w2R2PvhTuvru8o/XHTRw4I9KrftfQ=
github.com/hanzoai/authzstore v0.1.1 h1:4GsvB+bKs+gFtfKDMoYq/C7KxJAHrXLwtwdSutTakbo=
github.com/hanzoai/authzstore v0.1.1/go.mod h1:ZpiA4r7yoCC/4WO9CcsDezcYVVv40AmH8qoQSrBNtLo=
github.com/hanzoai/base v1.4.1 h1:d3/0nSSAlMhBDesGz8HTZ3dYVc6btYjkD39zMrMatk0=
github.com/hanzoai/base v1.4.1/go.mod h1:l+8byuizKirHJRJRhvV/j5wVke34c5KIdY0zf4W4jFg=
github.com/hanzoai/base v1.4.6 h1:u52xLTpLnJTzIc6tUdFRnI1KMYKchDfTVaWAfNe8uSo=
github.com/hanzoai/base v1.4.6/go.mod h1:5XgwqUuMt3MkBLn/zA1J5XED8j7CpY3LCv9lpe2pBmA=
github.com/hanzoai/beego/v2 v2.3.10 h1:WtLq1e/YD2SsFmlVds8JLyD/SQHJMqRXKixn1Xumz+0=
github.com/hanzoai/beego/v2 v2.3.10/go.mod h1:eDGRL/NJSqjoYHtWa5eMb2ze0gnlEabs1UO/naIfuX0=
github.com/hanzoai/builder v0.3.13 h1:tAOJ+0Q0xrrovk7lkvaZxuKZ4lqENIB6tE0Rr9+6Bo8=
github.com/hanzoai/builder v0.3.13/go.mod h1:TWZaiP0Y9tCMwtLH2EvQqBAeT1f3aJI5Y0XPM8S0wcE=
github.com/hanzoai/commerce v1.42.27 h1:2fdo1WdfM+Dl4O28p0DQVQ39vsBAFvvKJ1quH3XLXjY=
github.com/hanzoai/commerce v1.42.27/go.mod h1:wFhWJJu79pKWblX0dS3W+p1Vb019cZ4D6tN0j4FetEU=
github.com/hanzoai/commerce v1.42.29 h1:7uvorI2/o8OO5QwL+D9TxAELprBvfj4j07dmsPO+xwE=
github.com/hanzoai/commerce v1.42.29/go.mod h1:M/a2EobHgkTWcaWd69z7YOg5xccSyj1oSmyA0N48C2w=
github.com/hanzoai/commerce/metering v0.1.0 h1:qKK5eqHbZwiWVZLzxvQh+rWI/h+yIt6BJlCWTEE7feI=
github.com/hanzoai/commerce/metering v0.1.0/go.mod h1:LLFOtgJM5OczHb0D/4GRntL0a/jl80VkmyvTYCZN3yE=
github.com/hanzoai/common v0.67.7 h1:6LAzDF4MOPVE6TvtA7gtYRvWESCTI5g0Hsqhe00XTd4=
@@ -1165,14 +1165,14 @@ github.com/hanzoai/kv-go/v9 v9.18.0 h1:vO2SD8dV0+H9WWCVKV9KHaWZq4yeMsZruohrsZN94
github.com/hanzoai/kv-go/v9 v9.18.0/go.mod h1:S+Li20E6Bskpw6r+c8WWhfi4hCr8SVV32qPXO0wdl+E=
github.com/hanzoai/ldapserver v1.2.1 h1:H+AFuntREWo1n96cCUBnrzNeb07npTEUyvqXZRH4x20=
github.com/hanzoai/ldapserver v1.2.1/go.mod h1:mPwAbPBw9YeXfC2hfg1QSAw6CiGt6o2oF2pi9VR+j3U=
github.com/hanzoai/licensing v0.1.0 h1:YMX27DFsB4kQBrwZC5HxVDxUBAI5apCt9DZrpUMJ1VE=
github.com/hanzoai/licensing v0.1.0/go.mod h1:Rwk13J+hg7bNrS6TAUn/3IWBTHeGhTjyz1goubKK37M=
github.com/hanzoai/metrics v0.4.0 h1:2gFJlEsbvtWOaD9dU1mC3U8PKzYD4HKVepGQMFJ9xuA=
github.com/hanzoai/metrics v0.4.0/go.mod h1:unWBFRus8cWhSPaeQtMabVdXiI7FAIauVTLpG1WMAdw=
github.com/hanzoai/licensing v0.1.1 h1:roV1Eltz2hpFQbXBYFqRVdHC2HZONVzL5pvlCp96hOU=
github.com/hanzoai/licensing v0.1.1/go.mod h1:0SWQBAvGuhL7IRM5bTIPRhhOebLmfhjK+ppx/83uWPo=
github.com/hanzoai/metrics v0.4.1 h1:xF0E4cOhqfK6iKNGMfOYYkouNOTWb7bnUVUYLpN1dgE=
github.com/hanzoai/metrics v0.4.1/go.mod h1:BltpQMr8YTINvtwijkhxf7zRKKCAIiS90mRwAe7o2uw=
github.com/hanzoai/notify2 v1.6.3 h1:E0yE3mhwpCPLYAY7AAKJIzf+XL2yXcyxXD7/tK9giLs=
github.com/hanzoai/notify2 v1.6.3/go.mod h1:tF4lEPIr2J42/gpHjyWnpJey5545bLfRei6pxbVuFzc=
github.com/hanzoai/o11y v1.3.7 h1:D1Y5iv3j/ua7gUzOtA0uiIoDNG5nAuCvIS+uAu6We50=
github.com/hanzoai/o11y v1.3.7/go.mod h1:MdnXACvI3Vdg3OL9gnVWiaTDWAXz0u4m2cDpEsyPbcI=
github.com/hanzoai/o11y v1.3.12 h1:EUn3gFY8Koq4QIicRQ2gczMSgUShQ4ZQjMfplwbVpZs=
github.com/hanzoai/o11y v1.3.12/go.mod h1:8E/Agfil5pJyxO0oj3tty7TbuANSx8iinvKBvbGt1Uc=
github.com/hanzoai/orm v0.5.2 h1:0jbIGUDPENI1n47sT/IiXy9KBWdSHmiYuc5kV2lLgKo=
github.com/hanzoai/orm v0.5.2/go.mod h1:LL21Snpds3jQeoFxE5VMsnEcq8jZ4hOzhI7CkjXJX7A=
github.com/hanzoai/oss v1.8.5 h1:ukFSUKDuZV9bxorOeT3kliJ2EXi/4nHLEV//ZinqDxI=
@@ -1195,12 +1195,10 @@ github.com/hanzoai/storage-go v1.0.0 h1:c5EO04oGQY67VzZYaiA2Dtc/swTc5k5x8Ts/fpw5
github.com/hanzoai/storage-go v1.0.0/go.mod h1:IPXouQWTki5LKa9Y3qfOr6o8Qwlmc4sGObJnGMNKCOM=
github.com/hanzoai/tasks v1.40.0 h1:1Y0uXHmdGsU3QSv5So7NH55DhWqw/UPV3kgFeBaaWls=
github.com/hanzoai/tasks v1.40.0/go.mod h1:VwQ9ljfO0zYW694NPr4eIdc7WBPpPIs4zC8dbrNALZ0=
github.com/hanzoai/vfs v0.4.1 h1:erXat8LUZRI+fuw+8XNpslWdAaIrkGPXNGbP3PGQUBU=
github.com/hanzoai/vfs v0.4.1/go.mod h1:8AzyntfZgjK6iiB4A+xPestHgSAGUEHMthTkhACRIjM=
github.com/hanzoai/vfs v0.4.4 h1:XYkNPmpKcTiarTw/nG5p7aI38AmmYRNZkucwAj0GQBg=
github.com/hanzoai/vfs v0.4.4/go.mod h1:efMt/hQsAl2U2MboFrvpSPQ50DaPjO5eAy5xU+/kXfs=
github.com/hanzoai/xorm v1.1.6 h1:z8B3alP39taTGLA4BonBa+7RHIOMmjYjxF3N9gpzh84=
github.com/hanzoai/xorm v1.1.6/go.mod h1:wso0cQDDuzgjJwNOj3AXCftkKF3uOoUowKp5A7ol0zc=
github.com/hanzoai/zip v0.2.0 h1:2GI1OoUv4QdCxmfiwoiyYsSHpiPAxrp0v+Gu4BLtTVI=
github.com/hanzoai/zip v0.2.0/go.mod h1:3G+k2wy5bQ1wld66m0OPH8/LJ0kgIeNi1KPymMmlbf4=
github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0=
github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ=
+8 -4
View File
@@ -94,10 +94,14 @@ var authorityHeaders = []string{
// X-User-IsAdmin is NEVER restored from client input.
//
// FAIL MODE. If the validator can't verify a token (JWKS unreachable on a cold
// cache, issuer/audience misconfigured), the request resolves anonymous: admin
// fails SECURE (legit admins get 403 until config is corrected — a bounded
// availability cost), while org scoping is unaffected (the gateway-minted
// X-Org-Id is restored on the no-principal path). Never fails OPEN to admin.
// cache, issuer/audience misconfigured), the request resolves anonymous and BOTH
// planes fail SECURE. Admin fails closed (X-User-IsAdmin is never restored from
// client input), and — since F1 — the DATA plane fails closed too: it gates on a
// validated principal (clients/principal.Validated) and the anonymous request
// carries no X-User-Id, so the restored X-Org-Id is refused, not served. Never
// fails OPEN. The availability cost is bounded to COLD caches: the jwksCache is
// stale-on-error (a warm cache keeps validating through a transient JWKS outage),
// so only a from-cold JWKS failure degrades to anonymous-403.
func SanitizeIdentity(v *identityValidator, adminOrg string) zip.Handler {
adminOrg = strings.TrimSpace(adminOrg)
return func(c *zip.Ctx) error {