mirror of
https://github.com/hanzoai/gateway.git
synced 2026-11-02 23:52:23 +00:00
The DSN wire moved to /v1/event (o11y v1.5.66, cloud c6227da79) and this side
still matched /v1/sentry. The gateway decides which requests pass with NO JWT,
so the moment cloud starts serving ingest at the door, every error beacon would
have met a 401 here — at the edge, before cloud could DSN-authenticate it. The
comment already stated the invariant: the two allowlists must agree, or the
tokenless ingest one lets through is refused by the other.
isErrorIngestPath and isSentryIngestPath are now ONE predicate over ONE list of
roots. They were byte-identical in shape and differed only by prefix, which is
two places for that agreement to rot; ingestRoots names the same pair cloud's
module.IngestWire does — the door a minted DSN spells, and the /api/ suffix a
stock SDK appends to whatever DSN path it is given.
THE SUFFIX ANCHOR WAS UNTESTED, and it is the whole boundary. Replacing the
{envelope,store} check with `return true` — which puts every error-plane READ
into the unauthenticated class — passed the entire suite, cache bypassed. The
new test pins both directions: the six wire spellings enter, and the reads that
sit UNDER an ingest root (issues, discover, health) stay gated, along with the
door's own root, the face, and every non-POST verb. It fails on that mutation
and names each path it would have admitted.
65 lines
2.3 KiB
Go
65 lines
2.3 KiB
Go
package gateway
|
|
|
|
import (
|
|
"net/http"
|
|
"testing"
|
|
)
|
|
|
|
// The tokenless class is anchored on a SUFFIX, and this is what proves it.
|
|
//
|
|
// isIngestPath selects the requests the gateway lets through with no JWT, because
|
|
// a reporting SDK holds a DSN key and no session. Everything that decides who may
|
|
// enter that class lives in one predicate: POST, under an ingest root, ending in
|
|
// the wire's own {envelope,store}. Drop any one of those and a READ walks in
|
|
// unauthenticated — and the whole suite stayed green while exactly that was true,
|
|
// which is why this file exists.
|
|
//
|
|
// The reads below are the error plane's own: issues, discover, projects, logs,
|
|
// traces, stats. They sit UNDER an ingest root, so a prefix check alone admits
|
|
// them. They must never be in this class; they carry a session and are gated.
|
|
func TestIngestClassIsAnchoredOnTheWireSuffix(t *testing.T) {
|
|
admitted := []string{
|
|
"/v1/event/019f5339/envelope/",
|
|
"/v1/event/019f5339/envelope",
|
|
"/v1/event/019f5339/store/",
|
|
"/v1/event/019f5339/store",
|
|
// A stock SDK appends its own /api/<project>/envelope/ to the DSN path.
|
|
"/v1/o11y/api/019f5339/envelope/",
|
|
"/v1/o11y/api/019f5339/store/",
|
|
}
|
|
for _, p := range admitted {
|
|
if !isIngestPath(http.MethodPost, p) {
|
|
t.Errorf("POST %s is the DSN wire and must enter the tokenless class", p)
|
|
}
|
|
}
|
|
|
|
refused := []string{
|
|
// Reads under an ingest root — a prefix-only check would admit every one.
|
|
"/v1/event/issues",
|
|
"/v1/event/019f5339/issues",
|
|
"/v1/event/discover",
|
|
"/v1/o11y/api/v1/health",
|
|
"/v1/o11y/api/019f5339/issues",
|
|
// The door's own root is the PRODUCT event stream, which authenticates.
|
|
"/v1/event",
|
|
"/v1/event/",
|
|
// The face is a face. No spelling of it is ingest.
|
|
"/v1/sentinel/issues",
|
|
"/v1/sentinel/019f5339/envelope/",
|
|
// Suffix present, wrong root.
|
|
"/v1/admin/019f5339/envelope/",
|
|
}
|
|
for _, p := range refused {
|
|
if isIngestPath(http.MethodPost, p) {
|
|
t.Errorf("POST %s is NOT the DSN wire and must stay JWT-gated", p)
|
|
}
|
|
}
|
|
|
|
// Ingest is POST. A read verb never enters the class, whatever it addresses.
|
|
for _, m := range []string{http.MethodGet, http.MethodPut, http.MethodPatch, http.MethodDelete} {
|
|
if isIngestPath(m, "/v1/event/019f5339/envelope/") {
|
|
t.Errorf("%s on the wire path must stay JWT-gated; ingest is POST", m)
|
|
}
|
|
}
|
|
}
|