Files
zip/caller.go
T
zeekay 5331b2e456 One GraphQL engine: a resolver is handed the whole selection
A graph served for something else (MountGraphFor) now gets what a
second engine had been written for:

- The request is gathered before anything resolves: fragments spread
  once per selection set, cycles and unknown fragments refused at
  parse, @skip and @include decided (on inline fragments too), fields
  under one key merged, variables put in place. A Resolver receives
  the field as a Selection with its whole subtree, so it can fetch
  only what is selected or refuse a request too deep to answer.
- An Object answer is asked for each selected field with that field's
  own arguments, so nesting can be traversal. Selection.Bind decodes
  arguments into a struct, refusing unknown ones.
- The schema is rendered per request with the caller's context, and a
  schema that fails is a 500 that says so.
- Bounds on the gathered tree refuse whole: 64 root fields, 10000
  fields in all, depth 64.
- A query's root fields resolve at once; a mutation's in order. A
  panic fails its field.
- Keys go out in the order selected. A given variable the operation
  does not declare is ignored, and a declared one given nothing leaves
  its argument out, both as the specification says.
- MountGraphFor's routes are declared, so a host routes to them and a
  document describes them; Headers(ctx) relays a request whole.
2026-10-18 20:08:59 -07:00

613 lines
25 KiB
Go

package zip
import (
"cmp"
"context"
"net/http"
"strconv"
"strings"
"sync"
"github.com/valyala/fasthttp"
"github.com/zap-proto/fiber/v3"
)
// Who is calling — two different questions, answered by two different
// authorities, and never by the caller itself.
//
// WHO the request is for — the gateway's assertion, carried in the identity
// headers below. It was minted upstream, at the
// edge, against an IdP; a service forwards it
// unchanged (see Ctx.Forward) but cannot mint one.
// WHAT is calling — the kernel's attestation, read off the unix
// socket as the peer credential (see Peer). It
// cannot be set, spoofed or forwarded by the peer,
// because the peer never sends it: the kernel
// answers on its behalf.
//
// The second is why a service-to-service call over a unix socket needs no
// credential of its own. An argv flag, an environment variable or a shared
// secret would all be things the caller states about itself; SO_PEERCRED is
// the one thing about a caller that the caller does not get to say.
// The gateway-injected identity headers, named once. Ctx.Org and friends read
// these, Ctx.Forward propagates exactly these, and nothing else in zip spells
// them.
//
// The set is what a gateway can assert and a callee can act on, which is more
// than a subject: WHOSE tenant (org, project), WHICH person (id, name, email),
// WHICH account pays (the billing-account claim), and the two admin scopes —
// which are distinct authorities and must never collapse into one another.
// Owner names the org a principal belongs to, and a deployment that reserves one
// org for platform operators reads platform sudo off THAT and never off
// IsOrgAdmin, which says only "administers their own org". A plane that
// forwarded one and dropped the other would let a callee read "holds an org" as
// "administers it", or worse, as "administers the fleet".
const (
HeaderOrg = "X-Org-Id"
HeaderProject = "X-Project-Id"
HeaderUser = "X-User-Id"
HeaderUserName = "X-User-Name"
HeaderUserEmail = "X-User-Email"
HeaderUserOwner = "X-User-Owner"
// HeaderActedBy names the principal ACTING when a call is made on another
// org's behalf. Present only for an impersonation ([ActingAs]); its absence
// is the ordinary case and means the caller is acting as itself.
HeaderActedBy = "X-Acted-By"
HeaderUserAdmin = "X-User-IsAdmin"
HeaderUserOrgAdmin = "X-User-IsOrgAdmin"
HeaderRequestID = "X-Request-Id"
// HeaderAccount carries the principal's signed billing-account claim: the
// account inside its org that a charge is billed to. zip carries it as the
// gateway minted it and never interprets it; a callee resolving who pays reads
// it through [Caller.Account].
HeaderAccount = "X-Billing-Account-Id"
)
// identityHeaders is the set Call forwards — the caller's identity and the
// request id that ties the two hops together in the logs. It is exactly the
// list above and nothing beyond it: anything else a callee needs is an
// argument, and a header that is not identity has no business travelling
// implicitly.
//
// Forwarding a SUBSET is the failure this list exists to prevent. A callee
// resolving a billing subject prefers the minted name over the opaque id, and
// one gating a privileged surface reads the owner; drop either from the plane
// and the same handler decides differently depending on whether it was reached
// over REST or over a call — silently, and in the direction that bills or
// admits the wrong principal.
var identityHeaders = [...]string{
HeaderOrg, HeaderProject,
HeaderUser, HeaderUserName, HeaderUserEmail, HeaderUserOwner,
HeaderUserAdmin, HeaderUserOrgAdmin,
HeaderRequestID,
// The billing account travels with the identity it belongs to. A callee that
// resolves the payer without it falls back to a rule that ignores the claim,
// and bills a different account than the same request resolves over REST.
HeaderAccount,
// The impersonation travels WITH the identity it re-points, or the next hop
// sees a clean call from the target org and the audit trail ends at this
// process. That is precisely the failure this exists to close.
HeaderActedBy,
}
// callerKey carries the live request into a handler's context. The REQUEST is
// attached rather than a copy of what a handler might want from it, because
// copying five headers onto every typed request to serve the few that forward
// them is the wrong trade — this way the cost is one small value and the reads
// are lazy.
type callerKey struct{}
// callerContext binds the in-flight request to the context typed handlers
// receive, so the two callers of an op — the REST route and the op-call plane —
// both hand the handler a context that can answer who is calling.
//
// Both transports reuse the underlying RequestCtx for the next request: fasthttp
// pools it, and the ZAP server keeps one per connection. So the request is held
// through an [inflight] the request itself owns, which the server closes when it
// resets the request for the next one. A context kept past its request then
// reaches no request at all, and answers as a call whose request has ended,
// instead of reading whichever request the RequestCtx is serving now.
func callerContext(fc fiber.Ctx) context.Context {
rc := fc.RequestCtx()
if rc == nil {
return fc.Context()
}
in, _ := rc.UserValue(inflightKey{}).(*inflight)
if in == nil {
// ONE value carrying both facts, because this is the only place that holds
// the fiber Ctx and therefore the only place that can resolve the caller's
// IP under the app's trust configuration.
in = &inflight{rc: rc, ip: fc.IP()}
rc.SetUserValue(inflightKey{}, in)
}
return context.WithValue(fc.Context(), callerKey{}, in)
}
// inflight is what a handler's context carries about the call in progress: the
// request itself, and the caller's IP as fiber resolved it — which honours a
// proxy header only where the app trusts one. See [Caller.IP].
//
// It lives in the request's user values, so the server Closes it when it resets
// the request (fasthttp closes every io.Closer there, before the headers are
// reset). rc is read and cleared under mu, so no reader is inside the headers
// when the server reuses them.
type inflight struct {
mu sync.Mutex
rc *fasthttp.RequestCtx // nil once the request has ended
ip string
}
// inflightKey is the request user value holding its inflight.
type inflightKey struct{}
// Close ends the context's reach into the request. The server calls it.
func (in *inflight) Close() error {
in.mu.Lock()
in.rc = nil
in.mu.Unlock()
return nil
}
// request runs fn on the request behind ctx, holding it so the server cannot
// reset it underneath. behind reports whether a request was ever behind ctx;
// ended, whether that request is over, in which case fn does not run.
func request(ctx context.Context, fn func(rc *fasthttp.RequestCtx)) (behind, ended bool) {
in, _ := ctx.Value(callerKey{}).(*inflight)
if in == nil {
return false, false
}
in.mu.Lock()
defer in.mu.Unlock()
if in.rc == nil {
return true, true
}
fn(in.rc)
return true, false
}
// Header reads one header of the request behind a handler's context, and "" when
// there is no request behind it — a frame that arrived over ZAP, a command, a
// background job.
//
// It exists for the caller that has to REACH another service over HTTP on this
// caller's behalf and therefore needs the credential the caller presented, which
// [CallerOf] deliberately does not carry: Caller is the identity a request was
// resolved TO, and handing that back would let a service mint its own answer to
// the question it was supposed to ask.
func Header(ctx context.Context, name string) string {
get := headerOf(ctx)
if get == nil {
return ""
}
return get(name)
}
// Headers is every header of the request behind a handler's context, copied, and
// nil when there is no request behind it.
//
// It is [Header] for the caller that relays the request whole: a host that
// answers one address by asking the process that owns the operation, and must
// present the caller to that process exactly as the caller presented itself —
// the credential, the cookies, and whatever the edge in front of it set.
func Headers(ctx context.Context) http.Header {
var h http.Header
request(ctx, func(rc *fasthttp.RequestCtx) {
h = http.Header{}
for k, v := range rc.Request.Header.All() {
h.Add(string(k), string(v))
}
})
return h
}
// headerOf is the declared-header reader an op's invoke seam takes, resolved
// from the context rather than from a transport's own request object.
//
// It is a function rather than a map because the transports answer differently
// and all of them answer honestly: where a request is behind the context it
// reads that request's headers; where none is — a frame that arrived over ZAP,
// a command, a background job — it is nil and a `header:` field keeps whatever
// the arguments supplied. Reading it from the ctx is what lets the MCP door take
// a frame: the door no longer needs to hold an HTTP request to say what it knows
// about headers, it needs only to say honestly that it knows nothing.
func headerOf(ctx context.Context) func(string) string {
if behind, _ := request(ctx, func(*fasthttp.RequestCtx) {}); !behind {
return nil
}
return func(k string) (v string) {
request(ctx, func(rc *fasthttp.RequestCtx) { v = string(rc.Request.Header.Peek(k)) })
return v
}
}
// Forward returns a context that carries this request's identity onward, so a
// [Call] made with it reaches the next service as the same caller. Use it when
// an untyped handler calls another app; a typed handler's ctx already carries
// it.
//
// out, err := zip.Call[In, Out](c.Forward(), conn, "flags_bool", &in)
//
// This is propagation, not minting: the headers are the gateway's assertion,
// passed along exactly as received.
func (c *Ctx) Forward() context.Context { return callerContext(c.fc) }
// forwardIdentity copies the caller's identity onto an outbound request. A ctx
// with no request behind it (a background job, a test) forwards nothing, which
// is the honest answer — an unattributed call should look unattributed rather
// than borrow the identity of whatever ran last. A background caller that
// legitimately acts FOR someone says so with [WithCaller], and an inbound
// request always wins over what it said.
//
// A caller derived with [ActingAs] wins over both, exactly as it does in
// [CallerOf]: the next hop receives the org being acted for, the actor under
// [HeaderActedBy], and no project, so the callee reads the same caller this
// process reads. It is written whole from the derived caller rather than merged
// onto the request's headers, so no field of the original request rides beside it.
func forwardIdentity(ctx context.Context, req *fasthttp.Request) {
// The request's headers are copied while it is held. A request that has ended
// forwards no identity at all: it is not this call's to state, and stating
// nothing is what an unattributed call should look like.
var trace []byte
var ids [][2][]byte
behind, ended := request(ctx, func(rc *fasthttp.RequestCtx) {
if v := rc.Request.Header.Peek(HeaderTrace); len(v) > 0 {
trace = append([]byte(nil), v...)
}
for _, h := range identityHeaders {
if v := rc.Request.Header.Peek(h); len(v) > 0 {
ids = append(ids, [2][]byte{[]byte(h), append([]byte(nil), v...)})
}
}
})
live := behind && !ended
// Trace context travels too, and it is listed apart from the identity headers
// because it is not a claim about anyone. Identity is the gateway's assertion,
// forwarded because a callee has to know who it is acting for; [HeaderTrace] is
// this hop's position in a trace, forwarded so the next hop can say it came from
// here. Folding it into the identity list would make "what may a caller assert
// about itself" a question with two different answers.
if len(trace) > 0 {
req.Header.SetBytesV(HeaderTrace, trace)
}
if Traceparent != nil {
if v := Traceparent(ctx); v != "" {
req.Header.Set(HeaderTrace, v)
}
}
if acting, ok := ctx.Value(actingKey{}).(Caller); ok {
for h, v := range acting.headers() {
req.Header.Set(h, v)
}
return
}
if live {
for _, kv := range ids {
req.Header.SetBytesKV(kv[0], kv[1])
}
return
}
if behind {
return
}
stated, ok := ctx.Value(statedKey{}).(Caller)
if !ok {
return
}
for h, v := range stated.headers() {
req.Header.Set(h, v)
}
}
// statedKey carries an explicitly stated caller — see [WithCaller].
type statedKey struct{}
// actingKey carries a caller DERIVED from an authenticated one — see [ActingAs].
// Unexported and written in exactly one place, which is what keeps the
// derivation the only way to obtain one.
type actingKey struct{}
// WithCaller states who a [Call] made with this context acts for, for a caller
// that has no inbound request to propagate.
//
// The background work a service does is not all unattributed. A grant issued
// when an org opens, a meter that debits after the response has already gone
// out, a reactor draining a queue — each acts FOR a tenant, and the callee has
// to know which one to write to the right books. Without this the only place
// left to put the org is the argument, and an org in the argument is an org the
// caller chose: any caller could then name any tenant and be believed.
//
// ctx := zip.WithCaller(context.Background(), zip.Caller{Org: org})
// _, err := zip.Call[GrantIn, GrantOut](ctx, conn, "finance_grant", &in)
//
// An inbound request always wins. [forwardIdentity] prefers the gateway's
// assertion and reads this only when there is none, so this can supply an
// identity where none exists but can never override or launder one — which is
// what keeps [Ctx.Forward]'s guarantee intact. It is a statement by one of our
// own processes, trusted exactly as far as the socket's peer credential makes
// it trustworthy (see [Peer]), and never as far as a gateway's assertion.
func WithCaller(ctx context.Context, c Caller) context.Context {
return context.WithValue(ctx, statedKey{}, c)
}
// Detach returns a context for work that outlives the request behind ctx.
//
// go react(zip.Detach(ctx), ev)
//
// The server recycles a request once its handler returns, so a goroutine still
// holding the handler's context reads whatever request the server put there next
// — its headers, and so its caller. Detach reads the caller while the request is
// still this one, states it on a context that no longer reaches the request, and
// drops the request's cancellation. A context with no request behind it keeps the
// caller it already stated.
//
// It launders nothing: the caller is the one this context already answered with,
// and a stated caller is read only where no request is behind the context.
func Detach(ctx context.Context) context.Context {
who := CallerOf(ctx)
who = Caller{
Org: strings.Clone(who.Org), Project: strings.Clone(who.Project),
User: strings.Clone(who.User), Name: strings.Clone(who.Name),
Email: strings.Clone(who.Email), Owner: strings.Clone(who.Owner),
Admin: who.Admin, OrgAdmin: who.OrgAdmin,
RequestID: strings.Clone(who.RequestID), ActedBy: strings.Clone(who.ActedBy),
Account: strings.Clone(who.Account), IP: strings.Clone(who.IP),
}
ctx = context.WithValue(context.WithoutCancel(ctx), callerKey{}, (*inflight)(nil))
return WithCaller(ctx, who)
}
// ActingAs derives a caller acting on ANOTHER org's behalf, and is the one way
// to re-point the principal on a context that has a request behind it.
//
// ctx, err := zip.ActingAs(ctx, targetOrg)
//
// # Why this is not the laundering hole
//
// [WithCaller] is read only where there is no request, deliberately, so that
// stating an identity can never override an authenticated one. ActingAs does not
// weaken that: it cannot INVENT a caller, only re-point one that is already
// there. Called on a context with no authenticated principal it refuses, so the
// floor is unchanged — you can still only act with authority you arrived with.
//
// What it adds is that the derived caller carries BOTH facts. Org becomes the
// org being acted upon; [Caller.ActedBy] records who is acting, and travels on
// the wire with the rest of the identity. An impersonation is therefore
// distinguishable from an identity at every hop and in every audit row, which
// is the property that makes it safe to allow at all.
//
// # What it does NOT do
//
// It does not authorise. Whether this principal may act for that org is a policy
// question about roles and tenancy that belongs to the application, and zip has
// no view on it — an [Authorizer] is where that decision goes. ActingAs supplies
// the vocabulary and the audit trail; it does not grant anything.
func ActingAs(ctx context.Context, org string) (context.Context, error) {
if org == "" {
return ctx, ErrBadRequest("zip: ActingAs needs the org being acted for")
}
base := CallerOf(ctx)
if base.User == "" && base.Org == "" {
// Nothing authenticated to derive from. Refusing here is what keeps this
// from becoming a way to state an identity out of nothing.
return ctx, ErrUnauthorized("zip: ActingAs needs an authenticated caller to derive from")
}
acting := base
acting.Org = org
acting.Project = "" // a project is scoped to the original org, never to the target
// Administering one's own org says nothing about the org acted for, so the derived
// caller does not carry it there. Platform authority (Owner, Admin) is the
// principal's own and stays.
acting.OrgAdmin = false
if base.ActedBy != "" {
// Already an impersonation: the ORIGINAL actor stays the actor, so a
// chain of hops cannot quietly reassign who is responsible.
acting.ActedBy = base.ActedBy
} else {
acting.ActedBy = cmp.Or(base.User, base.Org)
}
return context.WithValue(ctx, actingKey{}, acting), nil
}
// Caller is the gateway's assertion about who a request is for, read as one
// value. It is what [Ctx.Forward] propagates and what [Call] carries.
//
// Empty fields mean the gateway said nothing — local dev, a direct ingress, or
// a call with no request behind it. Treat empty as unauthenticated, never as
// permitted.
// The two admin fields are separate authorities and reading one for the other
// is a privilege escalation: OrgAdmin says a person administers THEIR OWN org,
// Owner says which org that is. Admin is platform authority, which the gateway
// mints only for a person whose Owner is the org a deployment reserves for its
// operators, never for a membership of it.
type Caller struct {
Org string
Project string
User string
Name string
Email string
Owner string
Admin bool
OrgAdmin bool
RequestID string
// ActedBy is the principal ACTING when this call is made on another org's
// behalf, and empty in the ordinary case where the caller acts as itself.
//
// It is what makes an impersonation distinguishable from an identity: Org
// says whose data this call is for, ActedBy says who chose to touch it. A
// gate that must not be reachable by impersonation reads this; an audit row
// that omits it records the wrong actor.
ActedBy string
// Account is the principal's signed billing-account claim, as the gateway
// minted it: which account inside its org pays. Empty when the credential
// carried no claim. A callee accepts it only where it names an account in the
// ledger being charged, because the claim belongs to the principal, not to
// whichever org the call acts for.
Account string
// IP is where this call came FROM, and it is the one field here that is not
// a header the caller stated about itself — it is what the connection says.
//
// By default it is the socket peer, which cannot be spoofed and, behind a
// load balancer, is the load balancer. zip does not silently believe
// X-Forwarded-For: an IP that quietly reports the proxy is worse than none,
// because someone will rate-limit or audit on it, and an XFF that anyone can
// set is attacker-controlled input wearing the costume of a fact.
//
// A deployment that IS behind a proxy opts in explicitly, and the opt-in is
// allowlist-gated: set [Config.TrustProxy] with [Config.TrustedProxies] and
// [Config.ProxyHeader], and the forwarded value is honoured only when the
// peer is one of the named proxies. Anywhere else it falls back to the peer.
//
// Empty when there is no connection behind the context — a background
// caller, or a command — for the same reason every other field here is:
// nothing is known, so nothing is claimed.
IP string
}
// headers renders a stated caller onto the wire. Empty fields are omitted
// rather than sent blank, so "said nothing" and "said empty" stay the same
// thing on both sides of the call.
func (c Caller) headers() map[string]string {
h := make(map[string]string, len(identityHeaders))
for k, v := range map[string]string{
HeaderOrg: c.Org,
HeaderProject: c.Project,
HeaderUser: c.User,
HeaderUserName: c.Name,
HeaderUserEmail: c.Email,
HeaderUserOwner: c.Owner,
HeaderRequestID: c.RequestID,
HeaderActedBy: c.ActedBy,
HeaderAccount: c.Account,
} {
if v != "" {
h[k] = v
}
}
if c.Admin {
h[HeaderUserAdmin] = "true"
}
if c.OrgAdmin {
h[HeaderUserOrgAdmin] = "true"
}
return h
}
// CallerOf returns the identity forwarded with this call. It is the
// context-shaped read of the identity an untyped handler gets field by field
// off its [Ctx] — one accessor per surface, over the same headers:
//
// func listFlags(ctx context.Context, in *ListIn) (*ListOut, error) {
// org := zip.CallerOf(ctx).Org
// if org == "" {
// return nil, zip.ErrUnauthorized("no org")
// }
// ...
// }
//
// A context with no request behind it reads back whatever [WithCaller] stated
// on it, so what a background caller says it acts for is what the code running
// under that context sees — one value, written and read the same way, rather
// than a statement that only becomes visible one hop later.
func CallerOf(ctx context.Context) Caller {
// A DERIVED caller wins over the request, and nothing else does. It can only
// exist where [ActingAs] made one, and ActingAs can only re-point an
// identity that was already authenticated — so this is not a door a stated
// caller can walk through. See the laundering note on [WithCaller].
if acting, ok := ctx.Value(actingKey{}).(Caller); ok {
return acting
}
// A request that has ended has no caller to give: the RequestCtx is serving
// someone else by now, and borrowing that identity is the one wrong answer.
var c Caller
behind, ended := request(ctx, func(rc *fasthttp.RequestCtx) {
h := &rc.Request.Header
c = Caller{
Org: string(h.Peek(HeaderOrg)),
Project: string(h.Peek(HeaderProject)),
User: string(h.Peek(HeaderUser)),
Name: string(h.Peek(HeaderUserName)),
Email: string(h.Peek(HeaderUserEmail)),
Owner: string(h.Peek(HeaderUserOwner)),
Admin: string(h.Peek(HeaderUserAdmin)) == "true",
OrgAdmin: string(h.Peek(HeaderUserOrgAdmin)) == "true",
RequestID: string(h.Peek(HeaderRequestID)),
ActedBy: string(h.Peek(HeaderActedBy)),
Account: string(h.Peek(HeaderAccount)),
}
})
switch {
case !behind:
stated, _ := ctx.Value(statedKey{}).(Caller)
return stated
case ended:
return Caller{}
}
c.IP = callerIP(ctx)
return c
}
// callerIP is what the connection said, resolved once at the seam. Empty when
// no connection is behind the context — a background caller, or a command.
func callerIP(ctx context.Context) string {
in, _ := ctx.Value(callerKey{}).(*inflight)
if in == nil {
return ""
}
return in.ip
}
// Peer is the kernel's attestation of the process at the other end of a unix
// socket: what it is, not what it claims. Read it to decide whether a caller
// may reach an op at all — a coarse, infrastructure-level gate under the
// per-user authorization that [Authorizer] performs on the decoded input.
type Peer struct {
PID int
UID int
GID int
}
// String renders a peer for a log line.
func (p *Peer) String() string {
if p == nil {
return "peer(unknown)"
}
return "pid=" + strconv.Itoa(p.PID) + " uid=" + strconv.Itoa(p.UID) + " gid=" + strconv.Itoa(p.GID)
}
// Local reports a call with no transport behind it: this process asking
// itself, through [Here]. A server that scopes by the peer's credential asks
// this first — there is no peer to read, and there is no one to refuse.
func Local(ctx context.Context) bool {
behind, _ := request(ctx, func(*fasthttp.RequestCtx) {})
return !behind
}
// PeerOf returns the credential of the process that made this call, or nil
// when there is nothing to attest — the request arrived over tcp, or the host
// OS does not report peer credentials (see peerOf).
//
// It is the typed-handler counterpart of [Ctx.Peer]: a typed op reads it off
// the ctx it was handed.
//
// if p := zip.PeerOf(ctx); p == nil || p.UID != wantUID {
// return nil, zip.ErrForbidden("not a fleet peer")
// }
func PeerOf(ctx context.Context) (p *Peer) {
request(ctx, func(rc *fasthttp.RequestCtx) { p = peerOf(rc.Conn()) })
return p
}
// Peer returns the credential of the process that made this request, or nil.
// The untyped-handler counterpart of [PeerOf].
func (c *Ctx) Peer() *Peer {
rc := c.fc.RequestCtx()
if rc == nil {
return nil
}
return peerOf(rc.Conn())
}