mirror of
https://github.com/hanzoai/gateway.git
synced 2026-10-30 15:04:44 +00:00
Canon collapsed the ingest matchers to /v1/event/ and /v1/o11y/api/ and dropped /v1/sentinel. Three assertions still described the old shape and failed at both canon and head: the path test expected /v1/o11y/api to be a "sibling matcher" it no longer is, and the parity test still posted /v1/sentry. They are repointed to the two roots the selector actually reads — both roots exercised, and the non-widening property tested with a read UNDER an ingest root rather than an unrelated path. The route-class comment matches the code again. No config declares /v1/sentry, /v1/event, or /v1/sentinel; this is a stale-assertion fix, not a behaviour change. Co-authored-by: Hanzo Dev <[email protected]>
105 lines
4.7 KiB
Go
105 lines
4.7 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"
|
|
)
|
|
|
|
// TestIsIngestPath pins the byte-tight boundary of the tokenless DSN-ingest
|
|
// allow-rule: POST + an ingest-root prefix + an envelope|store suffix ONLY. The
|
|
// two roots — /v1/event/ (a minted DSN) and /v1/o11y/api/ (a stock Sentry SDK's
|
|
// appended suffix) — go through one selector and must behave identically. It
|
|
// MUST NOT widen to a bare root prefix (that would expose the error-plane read
|
|
// APIs — issues/discover/projects — to tokenless callers).
|
|
func TestIsIngestPath(t *testing.T) {
|
|
cases := []struct {
|
|
method string
|
|
path string
|
|
want bool
|
|
}{
|
|
{http.MethodPost, "/v1/event/019f5339-28a4-78d1/envelope/", true},
|
|
{http.MethodPost, "/v1/event/019f5339-28a4-78d1/store/", true},
|
|
{http.MethodPost, "/v1/event/acme/envelope", true}, // slash-less tolerated
|
|
{http.MethodPost, "/v1/event/acme/store", true},
|
|
{http.MethodPost, "/v1/o11y/api/acme/envelope/", true}, // the second root, same rule
|
|
{http.MethodPost, "/v1/o11y/api/acme/store", true},
|
|
{http.MethodGet, "/v1/event/acme/envelope/", false}, // ingest is POST-only
|
|
{http.MethodGet, "/v1/o11y/api/acme/envelope/", false}, // POST-only, both roots
|
|
{http.MethodPost, "/v1/event/acme/issues", false}, // a READ path — must stay gated
|
|
{http.MethodPost, "/v1/o11y/api/acme/issues", false}, // a READ path — must stay gated
|
|
{http.MethodPost, "/v1/event/", false}, // bare root — not ingest
|
|
{http.MethodPost, "/v1/event", false}, // bare, no trailing slash
|
|
{http.MethodPost, "/v1/o11y/api/", false}, // bare root — not ingest
|
|
{http.MethodPost, "/v1/eventX/acme/envelope/", false}, // prefix boundary: needs the '/'
|
|
{http.MethodPost, "/v1/sentinel/acme/envelope/", false}, // not an ingest root
|
|
{http.MethodPost, "/v1/event/acme/envelope/../issues", false}, // suffix anchor defeats traversal
|
|
}
|
|
for _, c := range cases {
|
|
if got := isIngestPath(c.method, c.path); got != c.want {
|
|
t.Errorf("isIngestPath(%q, %q) = %v, want %v", c.method, c.path, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The security boundary end-to-end through the middleware: the ingest verbs are
|
|
// reachable WITHOUT a token (DSN-authed downstream at cloud), but a read on the
|
|
// same root still 401s without identity — the exact non-widening property RED
|
|
// requires on a public tokenless edge.
|
|
func TestAuthMiddleware_IngestBypassButReadsRequireAuth(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
mw := NewAuthMiddleware(AuthConfig{Enabled: true, RequireAuth: true, JWKSURL: "http://127.0.0.1:0/jwks"})
|
|
|
|
engine := gin.New()
|
|
engine.Use(mw)
|
|
engine.Any("/*path", func(c *gin.Context) { c.String(http.StatusOK, "ok") })
|
|
|
|
do := func(method, path string) int {
|
|
req := httptest.NewRequest(method, path, nil)
|
|
rec := httptest.NewRecorder()
|
|
engine.ServeHTTP(rec, req)
|
|
return rec.Code
|
|
}
|
|
|
|
// Tokenless ingest passes through (not 401) -> reaches DSN auth at cloud.
|
|
// Both roots, both verbs.
|
|
for _, path := range []string{
|
|
"/v1/event/019f5339/envelope/",
|
|
"/v1/event/019f5339/store/",
|
|
"/v1/o11y/api/019f5339/envelope/",
|
|
} {
|
|
if code := do(http.MethodPost, path); code == http.StatusUnauthorized {
|
|
t.Errorf("tokenless ingest %q was blocked (401); it must pass through to DSN auth", path)
|
|
}
|
|
}
|
|
|
|
// A read on the SAME root is not an ingest verb and stays JWT-gated — this is
|
|
// the property that must not widen.
|
|
if code := do(http.MethodPost, "/v1/event/019f5339/issues"); code != http.StatusUnauthorized {
|
|
t.Errorf("tokenless read under an ingest root returned %d, want 401 — reads must stay JWT-gated", code)
|
|
}
|
|
if code := do(http.MethodGet, "/v1/o11y/api/019f5339/issues"); code != http.StatusUnauthorized {
|
|
t.Errorf("tokenless read under an ingest root returned %d, want 401 — reads must stay JWT-gated", code)
|
|
}
|
|
// A GET to the ingest path is not the ingest verb -> still gated.
|
|
if code := do(http.MethodGet, "/v1/event/019f5339/envelope/"); code != http.StatusUnauthorized {
|
|
t.Errorf("GET on the ingest path returned %d, want 401 (ingest is POST-only)", code)
|
|
}
|
|
// The bare root is not ingest -> gated.
|
|
if code := do(http.MethodPost, "/v1/event/"); code != http.StatusUnauthorized {
|
|
t.Errorf("POST to the bare /v1/event/ root returned %d, want 401 (not an ingest verb)", code)
|
|
}
|
|
}
|