// Package authctx carries the authenticated identity of a request. // // It is the successor to the development identity that used to be injected by // httpserver.devOrgMiddleware. The difference is not the shape — both put a // value on the request context — but the provenance: everything here was read // out of a server-side session row, and nothing in it can be influenced by the // request that carries it. // // That is the whole point of the package existing separately from the handlers. // A handler that wants to know who is calling has exactly one place to ask, and // that place cannot be reached from a request body, a query string or a header. // There is deliberately no setter that takes a user id from a client. package authctx import ( "context" "errors" "time" ) type key struct{} // ErrNoIdentity means a protected operation was reached without an // authenticated identity. That is a routing or middleware bug rather than a // client error: an unauthenticated request should have been refused before it // got this far. var ErrNoIdentity = errors.New("no authenticated identity in context") // Identity is who the request is, as resolved from the session row. // // Role is read once per request by the middleware, out of the user row, so an // authorization check never has to re-query. It is the authorization authority: // httpserver.Server.authorize gates operations on it, the repository's ownership // predicate narrows a talent caller's rows by it, and service/definitions.go // checks it on every definition write. AccountType is NOT an authority — a user // can change their own through PATCH /me. type Identity struct { UserID string OrgID string Email string FullName string Role string AccountType string Status string // SessionID is the row this identity came from, so logout and per-session // diagnostics do not have to re-hash the cookie. SessionID string // ExpiresAt is the session's sliding deadline as of this request. ExpiresAt time.Time } // With returns a context carrying the authenticated identity. func With(ctx context.Context, id Identity) context.Context { return context.WithValue(ctx, key{}, id) } // From reads the identity, reporting whether one was present. func From(ctx context.Context) (Identity, bool) { v, ok := ctx.Value(key{}).(Identity) return v, ok && v.UserID != "" } // MustFrom reads the identity or returns ErrNoIdentity. func MustFrom(ctx context.Context) (Identity, error) { if v, ok := From(ctx); ok { return v, nil } return Identity{}, ErrNoIdentity }