mirror of
https://github.com/hanzoai/ci.git
synced 2026-10-20 15:28:05 +00:00
Hanzo CI/CD / cicd (push) Waiting to run
CI/CD / gate (push) Waiting to run
These types are published through the composed cloud document, whose
schema namespace is FLAT and global: `Service` appears in fifty-three
other app documents, `Run` and `Head` in ten each, and two names that
differ only in case are worse still — every generator PascalCases, so an
SDK binds whichever it read last or refuses to compile.
Three of the five are better names besides:
Version -> Artifact a tag and a digest identifying built bytes is
not a version; nothing here is semver.
Head -> Tip its own comment already said "the tip of the
default branch".
Service -> Pipeline the row is not a workload, it is the chain
behind one: head, built, declared, running.
Run -> Execution } free names for the two that only collided
Build -> Check }
The wire does not move: JSON comes from the tags and those are
untouched, which the shape test asserts. What changes is the name a
generated client gives the class.
Co-authored-by: Hanzo Dev <[email protected]>
96 lines
3.9 KiB
Go
96 lines
3.9 KiB
Go
package ci
|
|
|
|
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 []Execution, want string) []Execution {
|
|
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 []Execution) []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
|
|
}
|