mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
Hanzo CI/CD / test (linux-amd64) (push) Skipped
Hanzo CI/CD / test (linux-arm64) (push) Skipped
Hanzo CI/CD / test-advisory (push) Failing after 5m15s
Hanzo CI/CD / test-amd64 (push) Failing after 17m40s
Hanzo CI/CD / cicd (push) Skipped
CI/CD / cicd (push) Failing after 17m53s
The event plane folded every occurrence into event.fact, and the Sentry module still read and wrote event.error. Its reads now select the error signal of event.fact, mapping issue, env, origin and the severity band. Errors enter through /v1/event only. The Sentry envelope and store routes, DSN keys and their rotation leave the module; the consumer of event.error files each fact through Ingest(org, product), which keeps the issue lifecycle and returns the issues a batch created.
121 lines
5.8 KiB
Go
121 lines
5.8 KiB
Go
package o11y
|
|
|
|
// anonymous.go answers ONE question about this package's route table: does the
|
|
// op at method+path answer a caller who holds no Hanzo principal?
|
|
//
|
|
// WHY THE ANSWER LIVES HERE. It is a fact about these routes, so it belongs
|
|
// beside them. It used to live in the embedding host instead — hanzoai/cloud's
|
|
// apps/o11y kept its own list of "the o11y paths that need no principal" — and
|
|
// that list named the probes under an INTERNAL versioned namespace this module
|
|
// used to rewrite onto before PATH UNTOUCHED (mount.go) removed the rewrite. The paths it named stopped
|
|
// existing and the paths that exist were never named, so the host's exemption
|
|
// matched nothing at all and every public op — /version, /health, the three
|
|
// probes, sign-in, the shared-dashboard reads — was refused 403 at the unified
|
|
// binary while the standalone one served them 200. A copy of a route fact, kept
|
|
// one repo away from the routes, drifted the moment the routes moved. There is
|
|
// one copy now and it is this one.
|
|
//
|
|
// WHAT THE HOST'S GATE IS FOR, stated so the exemption can be argued rather
|
|
// than enumerated. The runtime trusts X-Org-Id as gateway-minted: whoever sets
|
|
// that header names the tenant whose telemetry is read. Off-gateway — a direct
|
|
// pod call, a bearer-less request — the identity middleware restores a client's
|
|
// X-Org-Id but mints no X-User-Id, so the host refuses a request that carries a
|
|
// tenant and no principal before the runtime can believe it.
|
|
//
|
|
// THE RULE, therefore, in one sentence: an op whose own gate reads no tenant
|
|
// from the request has nothing for a forged tenant to reach, so gating it can
|
|
// only remove an answer. Anonymous is exactly that set — the runtime's
|
|
// OpenAccess routes plus the two public-dashboard reads it gates with
|
|
// CheckWithoutClaims — and nothing else. Every other op stays gated at the host
|
|
// AND at the runtime, which is one rule enforced twice, not two rules.
|
|
//
|
|
// Anonymous does NOT authorize. It says a principal is not the ADMISSION test;
|
|
// the op's own credential still is — a service-account key on
|
|
// service_accounts/me, the share's scope on a public
|
|
// dashboard, a session cookie on /users/me (which answers 401 without one, and
|
|
// that is the honest answer rather than the host's 403 for a route the runtime
|
|
// would have served).
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// Anonymous reports whether the op at method+path is served to a caller with no
|
|
// Hanzo principal. Path is the FULL public path as it arrives on the wire —
|
|
// this module never rewrites one (mount.go), so the route literal and the
|
|
// argument are the same spelling.
|
|
//
|
|
// anonymous_test.go proves every static entry below is a route Mount actually
|
|
// registers, which is the drift that broke the unified binary: a path in an
|
|
// exemption list that no route serves.
|
|
func Anonymous(method, path string) bool {
|
|
if openOps[method+" "+path] {
|
|
return true
|
|
}
|
|
return publicDashboardRead(method, path)
|
|
}
|
|
|
|
// openOps is every FIXED-path op the runtime serves without a principal. Each
|
|
// line is one of its OpenAccess registrations; the grouping is the reason.
|
|
var openOps = map[string]bool{
|
|
// The process describing ITSELF. No tenant is read and none could be: a
|
|
// kubelet probe and an external status check hold no principal, and a gate
|
|
// that hides whether the process is up makes an incident invisible at
|
|
// exactly the moment it matters. /version and /health are also the two the
|
|
// console reads before it has a session, to know what it is talking to.
|
|
"GET /v1/o11y/livez": true,
|
|
"GET /v1/o11y/healthz": true,
|
|
"GET /v1/o11y/readyz": true,
|
|
"GET /v1/o11y/version": true,
|
|
"GET /v1/o11y/health": true,
|
|
|
|
// The console's pre-session configuration — branding and the ingest URL.
|
|
// Read before the console knows whether it has a session, so requiring one
|
|
// to read it is circular.
|
|
"GET /v1/o11y/global/config": true,
|
|
|
|
// THE PATH TO HAVING A PRINCIPAL used to be eleven entries long — sign-in,
|
|
// registration, session rotation, sign-out, three SSO callbacks and three
|
|
// password-reset routes — because o11y authenticated people itself. It
|
|
// authenticates nobody now: Hanzo IAM does, at hanzo.id, and the edge hands
|
|
// this process an already-validated identity. There is no unauthenticated
|
|
// path to a principal here because there is no principal minted here, and
|
|
// the entries are DELETED rather than left standing, since an exemption is
|
|
// only ever a hole waiting for a route to be re-added under it.
|
|
//
|
|
// SELF-ADDRESSED: the subject is the caller's own identity, never a tenant
|
|
// named in the request. There is no X-Org-Id to forge into these — "me" is
|
|
// read from the claims the runtime resolved — so the host has nothing to
|
|
// protect and a caller with no claims gets the runtime's own 401.
|
|
"GET /v1/o11y/users/me": true,
|
|
"GET /v1/o11y/service_accounts/me": true,
|
|
"PUT /v1/o11y/service_accounts/me": true,
|
|
}
|
|
|
|
// publicDashboardRead reports whether path is one of the two reads a SHARED
|
|
// dashboard's public page makes: the sanitized dashboard and one widget's data.
|
|
// Anonymous is the entire point of a share link, and the runtime gates both with
|
|
// CheckWithoutClaims scoped to that dashboard's own read scope — so the tenant
|
|
// comes from the share, not from a header a caller could set.
|
|
//
|
|
// GET /v1/o11y/public/dashboards/{id}
|
|
// GET /v1/o11y/public/dashboards/{id}/widgets/{idx}/query_range
|
|
func publicDashboardRead(method, path string) bool {
|
|
if method != http.MethodGet {
|
|
return false
|
|
}
|
|
rest, ok := strings.CutPrefix(path, o11yRoot+"/public/dashboards/")
|
|
if !ok {
|
|
return false
|
|
}
|
|
seg := strings.Split(rest, "/")
|
|
switch len(seg) {
|
|
case 1: // {id}
|
|
return seg[0] != ""
|
|
case 4: // {id}/widgets/{idx}/query_range
|
|
return seg[0] != "" && seg[1] == "widgets" && seg[2] != "" && seg[3] == "query_range"
|
|
}
|
|
return false
|
|
}
|