authz package provides route-level and method-level authorization rules
with a first-match-wins, default-deny route rule DSL.
Import
Route Authorization
AnyRequest() is a required catch-all — omitting it causes Routes() to return an error
(unless AllowUnmatched() is set, see below).
Path matching
Method-scoped rules
Match takes the path pattern plus an optional list of HTTP methods:
Match can’t express, drop to MatchRequest, which
takes a single middleware.RequestMatcher built from the web/middleware
combinators:
Authorizers
HasRole / HasAnyRole
WithHierarchy is configured.
HasAuthority / HasAnyAuthority
Authenticated / Anonymous / PermitAll / DenyAll
Access (custom)
Access takes an Authorizer — func(ctx, auth, target) Decision — not a
plain boolean predicate, so it composes with hierarchy and audit like every
other verb:
Composing authorizers
AllOf, AnyOf, and Negate combine authorizers into larger boolean rules:
Role Hierarchy
Build aRoleHierarchy from a > string spec with ParseRoleHierarchy.
Segments separated by > mean “the left role includes the right role”;
chains ("A > B > C") and newline-separated edges are both accepted, and may
be mixed:
NewMapRoleHierarchy:
WithHierarchy — it is a Routes() option:
ROLE_SUPER automatically passes HasRole("ADMIN") and
HasRole("USER").
Audit hooks
WithAuthzPublisher is a Routes() option that emits a granted/denied event
for every decision, via event.AuthzPublisher. event.FuncAuthzPublisher
adapts plain functions so a one-off publisher doesn’t need its own named type:
Authorizer outside of Routes/Guard (e.g. before
Check), wrap it with NewAuditingAuthorizer:
Method-level guards
Guard wraps an Authorizer into a reusable closure for method-level checks —
an explicit function call instead of a reflection-based annotation. It
resolves the subject from ctx and returns nil on grant, or an error
wrapping core.ErrUnauthenticated / core.ErrAccessDenied on deny:
Check is the one-shot form when a reusable closure isn’t needed:
Guard accepts the same hierarchy and audit options as Routes, scoped to
this one guard via GuardWithHierarchy / GuardWithAuthzPublisher:
Exception translation (401 vs 403)
ExceptionTranslation is the single source of truth for the 401-vs-403
branch: an unauthenticated or anonymous caller gets the AuthenticationEntryPoint
(401), an authenticated caller gets the AccessDeniedHandler (403). The zero
value uses the built-in defaults (DefaultAuthenticationEntryPoint,
DefaultAccessDeniedHandler), so overriding is opt-in via the WithExceptionTranslation
Routes() option:
Default-deny escape hatches
BuildingRoutes() without an AnyRequest() rule fails by design
(default-deny completeness). AllowUnmatched() is the explicit, named opt-out
for a permit-by-default chain — prefer a declared AnyRequest() rule when you
can:
OnRuleShadowed is a best-effort build-time lint: it warns when an earlier
permissive path rule (PermitAll/Authenticated) prefixes a later, more
restrictive rule, which would otherwise silently shadow it:
Design notes
AnyRequest()is required orRoutes()errors — opt out withauthz.AllowUnmatched()- method-level checks are explicit calls:
authz.Guard(...)/authz.Check(ctx, ...) - role hierarchy via
authz.ParseRoleHierarchy("ADMIN > USER")/authz.NewMapRoleHierarchy(...) - authz events via
authz.WithAuthzPublisher(...)/authz.GuardWithAuthzPublisher(...) - error translation via
authz.WithExceptionTranslation(...)
