mirror of
https://github.com/hanzoai/ci.git
synced 2026-10-20 15:28:05 +00:00
This repo is public. A comment that recounts how a specific service broke — the date, the duration, the named blast radius, the steps that reproduce it — publishes a map of our failure modes to anyone who clones it, and the reader learns nothing they could not learn from the mechanism alone. Every such comment now states the engineering constraint in the present tense: why Hanzo Git resolves one `uses:` hop and not two, why a `type: boolean` workflow_call input is the constant `false`, why GOTOOLCHAIN=local turns a go.mod bump into a mid-build death, why an ignore rule never untracks what the index already holds, why an object store can answer 200 with zero body bytes. The facts that make each gate worth having are unchanged; the war stories are gone. All 15 gates in hanzo.yml pass.
96 lines
3.9 KiB
Go
96 lines
3.9 KiB
Go
package main
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// scope.go answers exactly one question: whose builds may THIS request see?
|
|
//
|
|
// It exists to keep a FILTER and a GATE apart. `?org=lux` narrows what is
|
|
// rendered and reads like tenancy, but it decides nothing about who may ask: a
|
|
// query parameter is a request for a view, never the authority for one. Conflate
|
|
// the two and every org's repo names, branches, commit SHAs and actor logins are
|
|
// one URL away from an unauthenticated caller.
|
|
//
|
|
// The authority is X-Org-Id, minted by admin-guard from the IAM-verified `owner`
|
|
// claim and written onto the request by the ingress middleware's
|
|
// authResponseHeaders. Traefik OVERWRITES any client-sent X-Org-Id with the
|
|
// guard's value, so on the wired path the header cannot be forged. This file
|
|
// still treats its ABSENCE as fatal rather than as "no filter", because absence
|
|
// is the signal that the request did not come through the guard at all.
|
|
|
|
// orgHeader is the identity the whole surface is scoped by. One name, one
|
|
// meaning, platform-wide (see the X-* header convention: X-Org-Id is the org
|
|
// slug from the JWT `owner` claim).
|
|
const orgHeader = "X-Org-Id"
|
|
|
|
// viewer is the resolved, trusted answer. Constructed only from headers the
|
|
// guard controls — never from the query string, never from a cookie.
|
|
type viewer struct {
|
|
// org is the caller's home org slug, from the verified `owner` claim.
|
|
org string
|
|
// sudo reports whether org is the platform admin org, which is the ONE
|
|
// identity that may see across tenants (the fleet view).
|
|
sudo bool
|
|
}
|
|
|
|
// resolveViewer lifts the guard-set header into a viewer. It fails closed: a
|
|
// missing or blank X-Org-Id yields ok=false and the caller MUST refuse the
|
|
// request.
|
|
//
|
|
// Defaulting an absent header to "no filter" is the specific bug this function
|
|
// exists to prevent — that default is what turns "reached ci without the guard"
|
|
// into "rendered every org's builds".
|
|
func resolveViewer(r *http.Request, adminOrg string) (viewer, bool) {
|
|
org := strings.TrimSpace(r.Header.Get(orgHeader))
|
|
if org == "" {
|
|
return viewer{}, false
|
|
}
|
|
return viewer{org: org, sudo: strings.EqualFold(org, strings.TrimSpace(adminOrg))}, true
|
|
}
|
|
|
|
// visible narrows runs to what v is permitted to see, then applies want (the
|
|
// optional `?org=` selection) WITHIN that permission.
|
|
//
|
|
// The ordering is the whole point: permission is applied first and `want` can
|
|
// only ever narrow the result. A lux viewer asking for `?org=hanzo` gets an
|
|
// empty list, not hanzo's builds — the parameter selects among what you may
|
|
// already see, it never reaches for more.
|
|
func (v viewer) visible(runs []Run, want string) []Run {
|
|
want = strings.TrimSpace(want)
|
|
if v.sudo {
|
|
// The fleet view: every org, narrowed by the requested one if given.
|
|
return filterByOrg(runs, want)
|
|
}
|
|
if want != "" && !strings.EqualFold(want, v.org) {
|
|
return nil
|
|
}
|
|
return filterByOrg(runs, v.org)
|
|
}
|
|
|
|
// orgs lists the org tabs this viewer may choose between. A tenant gets exactly
|
|
// its own org — rendering the full org list to a tenant would leak the set of
|
|
// orgs that build on the platform even though their runs are correctly hidden.
|
|
func (v viewer) orgs(runs []Run) []string {
|
|
if v.sudo {
|
|
return orgsOf(runs)
|
|
}
|
|
return []string{v.org}
|
|
}
|
|
|
|
// requireViewer resolves the viewer or writes the refusal. It returns ok=false
|
|
// when the request must not proceed.
|
|
func requireViewer(w http.ResponseWriter, r *http.Request, adminOrg string) (viewer, bool) {
|
|
v, ok := resolveViewer(r, adminOrg)
|
|
if ok {
|
|
return v, true
|
|
}
|
|
// 403, not 401: a 401 invites a credential retry, but there is nothing the
|
|
// CALLER can add to fix this. The header is set by infrastructure, so its
|
|
// absence is a routing fault (ci reached off-guard) and the honest answer is
|
|
// that this path is not authorized to serve, whoever is asking.
|
|
http.Error(w, "forbidden: no "+orgHeader+" (this service is only reachable through the IAM gate)", http.StatusForbidden)
|
|
return viewer{}, false
|
|
}
|