Documentation
¶
Index ¶
- func Authenticate(a auth.Authenticator, logger *slog.Logger, opts ...AuthOption) func(http.Handler) http.Handler
- func RateLimitJSON() http.HandlerFunc
- func Recovery(logger *slog.Logger) func(next http.Handler) http.Handler
- func RequestContext(next http.Handler) http.Handler
- func RequireAccess(guard *access.Guard, resourceKind, action string, logger *slog.Logger, ...) func(http.Handler) http.Handler
- func RequirePermission(checker authz.Checker, resourceKind, action string, logger *slog.Logger, ...) func(http.Handler) http.Handler
- func SecurityHeaders(next http.Handler) http.Handler
- func StructuredLogger(logger *slog.Logger) func(next http.Handler) http.Handler
- func WriteAccessError(w http.ResponseWriter, r *http.Request, err error, logger *slog.Logger, ...) bool
- type AuthMessages
- type AuthOption
- type Metrics
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Authenticate ¶
func Authenticate(a auth.Authenticator, logger *slog.Logger, opts ...AuthOption) func(http.Handler) http.Handler
Authenticate returns a middleware that extracts a bearer token from the request's "Authorization" header, authenticates it via a, and stores the resulting Principal in the request context (auth.WithPrincipal), so a later handler or middleware (e.g. RequirePermission) can read it via auth.FromContext.
Extraction (see extractBearerToken) happens before a.Authenticate is ever called: a missing header, an empty header, a scheme other than "Bearer" (checked case-insensitively per RFC 6750), or a "Bearer" scheme with no token after it is rejected as auth.ErrUnauthenticated without invoking the Authenticator at all, since there is no credential yet worth asking an identity provider about.
Fail-closed: on any error from extraction or from a.Authenticate, this middleware writes an error response and returns without calling next.ServeHTTP.
Status mapping:
- auth.ErrServiceUnavailable -> 503. The identity provider is unreachable or failing; this must never collapse into 401 or 500. All three source systems this package was ported from made that mistake — go-crucible and go-licencias returned 403, and the base returned 403 too, for a Zitadel outage. A 503 tells the client the failure is transient and retryable and tells monitoring "infrastructure incident", where a 401 or 403 reads as "your credentials/permissions are the problem".
- auth.ErrForbidden -> 403
- anything else, including auth.ErrUnauthenticated -> 401
The underlying error is always logged server-side at error level; its detail is never included in the response body.
func RateLimitJSON ¶
func RateLimitJSON() http.HandlerFunc
RateLimitJSON returns an http.HandlerFunc that writes a JSON 429 response. Use with httprate.WithLimitHandler(middleware.RateLimitJSON()).
func RequestContext ¶
RequestContext populates vogel/reqctx with the three pieces of request-scoped metadata every downstream layer needs: the request ID, the client IP, and the User-Agent.
It is the single writer for this metadata (replacing what used to be two separate middlewares — one bridging chi's request ID into logger's own context key, one extracting IP/User-Agent into a middleware-local key). Consolidating into one middleware over one neutral package (reqctx) means:
- logger.Logger.WithContext and audit.Recorder.Record read the exact same request ID, from the exact same context key, so a log line and an audit_log row for the same request are provably linked.
- neither logger nor audit needs to import httpx or chi to get at it.
Mount chi's own middleware.RequestID (or equivalent) upstream of this middleware — RequestContext reads the upstream ID via chi's GetReqID rather than generating one itself.
func RequireAccess ¶ added in v0.3.0
func RequireAccess(guard *access.Guard, resourceKind, action string, logger *slog.Logger, opts ...AuthOption) func(http.Handler) http.Handler
RequireAccess returns a middleware that performs the same coarse authorization check as RequirePermission (principal + resource kind + an ID taken from the URL, with no entity-specific attributes), but through an access.Guard instead of a bare authz.Checker.
Use this instead of RequirePermission when the same Checker also backs a per-instance check made later in the handler (via guard.Check, after the entity is loaded, with its data folded into authz.Resource.Attr) -- see the access package doc for why that second check cannot happen here, in the middleware, instead. When guard was built with access.WithPrincipalAttributes, this middleware installs access.WithRequestScope on the request context BEFORE running its own check, so the principal-attribute resolver runs at most once for the request no matter how many of the middleware's own check and the handler's later guard.Check end up needing it. When guard has no resolver configured, the request is passed to next completely unchanged -- byte for byte the same behavior as before this middleware existed.
Fail-closed: on any error, this middleware writes a response (via WriteAccessError) and returns without calling next.ServeHTTP.
func RequirePermission ¶
func RequirePermission(checker authz.Checker, resourceKind, action string, logger *slog.Logger, opts ...AuthOption) func(http.Handler) http.Handler
RequirePermission returns a middleware that checks whether the Principal authenticated by an earlier Authenticate middleware is allowed to perform action on a resource of kind resourceKind.
Use this when the authorization decision depends only on the principal and the resource kind — i.e. no resource-level attributes are needed. The resource ID is taken from the URL parameter "id" if present, and falls back to the wildcard otherwise.
For attribute-based checks (e.g. status == "DRAFT"), or when the same Checker needs principal attributes resolved from another system (e.g. the user's current assignments), build an access.Guard and use RequireAccess here instead, then call access.Guard.Check + WriteAccessError in the handler after loading the entity.
Fail-closed: on any error, this middleware writes an error response and returns without calling next.ServeHTTP.
Status mapping — this deliberately supersedes go-licencias' DEC-08, which mapped a Cerbos check error to 403 "to avoid an oracle" (i.e. to keep a caller from telling a PDP outage apart from a real deny). That reasoning trades away more than it buys: a PDP outage reported as 403 tells the client "you lack permission" and tells monitoring "permissions bug" instead of "infrastructure incident", and unlike 403, 503 is retryable. go-crucible independently made the same mistake by returning 500 instead:
- checker.IsAllowed returns a non-nil error (PDP unreachable/failing) -> 503
- checker.IsAllowed returns (false, nil) (genuine denial) -> 403
- no Principal in the request context (missing/invalid credentials) -> 401
The underlying error is always logged server-side at error level; its detail is never included in the response body.
This is now a thin wrapper around RequireAccess, backed by an access.Guard with no PrincipalAttributes resolver configured — the status mapping and wildcard-ID logic live there once, instead of twice. One consequence of that: access.New panics on a nil checker, so a nil checker now surfaces immediately when RequirePermission is called (at router-construction time) rather than on the first request that hits the route it guards. That is a strictly earlier failure for what was already a wiring mistake, not a new way for this function to fail.
func SecurityHeaders ¶
SecurityHeaders adds common HTTP security headers to every response.
func StructuredLogger ¶
StructuredLogger returns a structured access-log middleware.
The request ID comes from reqctx, which RequestContext populates — mount RequestContext upstream of this middleware. Reading chi's GetReqID directly here instead would agree with what logger.Logger.WithContext and audit.Recorder.Record report only by coincidence, since both of those read reqctx: a request ID reaching the context by any other route would land in an audit_log row while this access log printed an empty one.
func WriteAccessError ¶ added in v0.3.0
func WriteAccessError(w http.ResponseWriter, r *http.Request, err error, logger *slog.Logger, opts ...AuthOption) bool
WriteAccessError maps an error returned by access.Guard.Check (or access.Guard.Principal) to the appropriate HTTP status and writes it, returning true. It writes nothing and returns false for nil or for any error that is not one of access.ErrUnauthenticated, access.ErrForbidden, or access.ErrUnavailable, so a handler can fall through to its own domain-specific error mapping (e.g. errors.Is against a not-found or conflict sentinel) instead of this helper claiming an error it does not recognize.
This is the counterpart, inside a handler, of what RequireAccess does in the router: a handler that calls guard.Check after loading an entity uses WriteAccessError to get the exact same 401/403/503 mapping and logging the middleware gives every route, without duplicating the switch itself.
Types ¶
type AuthMessages ¶
type AuthMessages struct {
// credentials.
Unauthorized string
// Forbidden is returned on HTTP 403: valid credentials, but the action is
// not permitted (insufficient role, or a policy denial).
Forbidden string
// policy decision point could not be reached.
ServiceUnavailable string
}
AuthMessages holds the user-facing strings returned by Authenticate and RequirePermission.
A library must not bake user-facing copy in one language: the source systems this package was ported from returned hardcoded Spanish literals (in one case, Rioplatense voseo inside a Chilean product — an existing inconsistency, not something worth reproducing). DefaultAuthMessages provides neutral English defaults; each consuming application overrides them via WithAuthMessages to localize.
func DefaultAuthMessages ¶
func DefaultAuthMessages() AuthMessages
DefaultAuthMessages returns neutral English defaults for AuthMessages.
type AuthOption ¶
type AuthOption func(*AuthMessages)
AuthOption customizes Authenticate or RequirePermission.
func WithAuthMessages ¶
func WithAuthMessages(m AuthMessages) AuthOption
WithAuthMessages overrides the default user-facing messages. Any field left as the empty string keeps its default.
type Metrics ¶
type Metrics struct {
// contains filtered or unexported fields
}
Metrics is an HTTP middleware that records Prometheus metrics (request count, latency, and response size) for every request.
Unlike a package-level prometheus.MustRegister in an init() function, Metrics is safe to construct more than once against the same Registerer: a prometheus.AlreadyRegisteredError is handled by reusing the already registered collector instead of panicking. This matters for a library — a consumer may build two server instances, or a test may construct a Metrics per test case.
func NewMetrics ¶
func NewMetrics(reg prometheus.Registerer) (*Metrics, error)
NewMetrics creates a Metrics middleware, registering its collectors on reg. If reg is nil, prometheus.DefaultRegisterer is used.