The authz package provides route-level and method-level authorization rules with a first-match-wins, default-deny route rule DSL.

Import

Route Authorization

Rules are evaluated top-to-bottom. The first matching rule decides the outcome. 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:
For a matching predicate Match can’t express, drop to MatchRequest, which takes a single middleware.RequestMatcher built from the web/middleware combinators:

Authorizers

HasRole / HasAnyRole

Role checks are hierarchy-aware when WithHierarchy is configured.

HasAuthority / HasAnyAuthority

Exact string match; no prefix is added.

Authenticated / Anonymous / PermitAll / DenyAll

Access (custom)

Access takes an Authorizerfunc(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 a RoleHierarchy 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:
…or programmatically from an adjacency map with NewMapRoleHierarchy:
Both return an error on a cycle. Apply the hierarchy when starting the route set with WithHierarchy — it is a Routes() option:
A user with 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:
To audit a single 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

Building Routes() 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 or Routes() errors — opt out with authz.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(...)