Documentation
¶
Overview ¶
Package http enforces authorization on HTTP routes.
Requirements are declared where the route is registered:
authz, err := authzhttp.NewEnforcer(extractGrants,
authzhttp.WithLogger(logger),
authzhttp.WithMetricsProvider(metricsProvider),
)
// ...
routing.Get(router, "/recipes/{id}", readRecipe,
routing.WithMiddleware(authz.Require(ReadRecipesPermission)))
Why not a central table ¶
The gRPC half of this package keys a table by grpc.UnaryServerInfo.FullMethod, which is known before dispatch, and denies anything absent from it. The equivalent here would key by route pattern — and a route pattern is not known until the mux has matched. Global middleware installed via routing.Backend.Use runs before matching, where chi's RoutePattern is still empty.
Making it work would mean either re-implementing path matching inside platform-go, producing a second router that must agree with the real one, or adding a per-backend hook to routing.Backend for every supported mux. Both cost more than they buy for a seam whose requirement is one call at the registration site — which also puts the requirement next to the handler it guards, where a reader is most likely to notice its absence.
The consequence, stated plainly ¶
HTTP cannot fail closed on an undeclared route the way gRPC does. A route registered without Require middleware is unguarded, and nothing here can detect that.
What this package does guarantee is that a route which *is* guarded cannot be reached without grants: Require denies when the extractor returns false, and denies rather than vacuously allowing when handed an empty permission list.
Cover the rest with a test over your own registrations — assert every registered route either carries authorization middleware or appears on an explicit public-routes list. The durable fix is an option inside routing, which sees every registration and can refuse to start; that belongs there rather than here.
Denials ¶
The default response is the platform envelope: errors/http code E110 at HTTP 403, with the trace ID in details, identical to what the router emits for a handler that returned ErrPermissionDenied. A service with its own envelope replaces it with WithDenyHandler.
The body says "permission denied" and nothing else. Which permission was missing goes to the span and the log; putting it in the response would tell an unauthorized caller what to go looking for.
Watching it ¶
authorization_http_checks, authorization_http_denials, and authorization_http_missing_grants. They are unlabeled totals.
The route is deliberately not a metric dimension. This middleware runs after routing has matched the request but has no portable way to recover the pattern that matched it — routing.Router exposes none — so the only label available is the raw URL path, and a route with an identifier in it would produce one time series per identifier.
The path is on the span instead, under authorization.method, where it costs one attribute on one trace rather than a series that never stops growing. To investigate a specific route, use the spans or the denial logs, which carry the full request; use these counters for the rate and for alerting.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type DenyHandler ¶
type DenyHandler func(res http.ResponseWriter, req *http.Request, err error)
DenyHandler writes the response for a denied request. Replace the default when a service has its own error envelope.
type Enforcer ¶
type Enforcer struct {
// contains filtered or unexported fields
}
Enforcer builds authorization middleware for HTTP routes.
Unlike the gRPC Enforcer, this one holds no table of requirements. A route's permissions are declared at its registration site instead:
routing.Get(r, "/things/{id}", handler,
routing.WithMiddleware(authz.Require(ReadThingsPermission)))
That is not a stylistic preference. Global middleware runs before the mux matches, so the route pattern is not yet known there — a central table keyed by pattern would require platform-go to re-implement path matching, or routing.Backend to grow a per-backend hook. Declaring at registration keeps the requirement next to the handler it guards, which is also where a reader is most likely to notice a missing one.
The consequence is that HTTP cannot fail closed on an undeclared route the way gRPC does: a route registered with no Require middleware is simply unguarded. Assert coverage with a test over your registered routes. The eventual home for boot-time enforcement is an option inside routing, which sees every registration and can refuse to start.
func NewEnforcer ¶
func NewEnforcer(extract authorization.GrantsExtractor, opts ...Option) (*Enforcer, error)
NewEnforcer builds an HTTP authorization Enforcer.
func (*Enforcer) Require ¶
func (e *Enforcer) Require(perms ...authorization.Permission) routing.Middleware
Require returns middleware admitting only requests whose grants include every permission in perms.
Require with no permissions denies everything rather than allowing it. The set-algebra answer would be a vacuous allow, but a middleware installed with an empty list is far more likely to be a bug — a slice that came back empty from configuration — than an intent to admit everyone, and a route that needs no authorization simply omits the middleware.
type Option ¶
type Option func(*Enforcer)
Option configures an Enforcer.
func WithAuditOnly ¶
func WithAuditOnly() Option
WithAuditOnly evaluates and records every decision but denies nothing. See the gRPC package's WithAuditOnly for why this exists and how to use it.
func WithDenyHandler ¶
func WithDenyHandler(h DenyHandler) Option
WithDenyHandler replaces the response written on denial. The default encodes the platform's APIResponse envelope with code E110 at HTTP 403.
func WithLogger ¶
WithLogger attaches a logger. Denials are logged; allows are not.
func WithMetricsProvider ¶
WithMetricsProvider attaches a metrics provider, enabling the authorization counters.