mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
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.
613 lines
25 KiB
Go
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())
|
|
}
|