access

package
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 30, 2026 License: MIT Imports: 7 Imported by: 0

Documentation

Overview

Package access is the per-instance authorization check callable from the application layer (the hexagonal use cases, not the transport layer) once an entity has already been loaded.

It sits between two packages that must each stay narrow for opposite reasons. authz stays dependency-free: it declares only the Checker port and the Principal/Resource shapes, so a consumer that needs nothing but the contract does not pull in this package, auth, or a PDP client. httpx/middleware, on the other hand, can only ever perform a COARSE check: RequirePermission (and RequireAccess, built on this package) run before the handler has read anything from storage, so all they have to check against is the resource kind and, at best, an ID taken straight off the URL. A decision that depends on the entity's own data -- its owner, its status, whether it has already been submitted -- is simply not knowable at that point. The two ways around that both cost more than this package does: teaching the middleware to load the entity itself means every protected route pays for a second read of the same row the handler is about to load anyway (once for the check, once for the actual work), and skipping the middleware check entirely just to defer to the handler loses the fail-closed 401/403/503 status mapping that RequirePermission already gives every route for free. access.Guard is what a handler calls AFTER its own load, with the loaded entity's data folded into authz.Resource.Attr, so the same authz.Checker port backs both the coarse, pre-load check in httpx/middleware and the fine-grained, post-load check here -- without httpx/middleware, this package, or authz importing one another beyond what is declared above. Like vogel/audit, this package must never import chi, httpx, or net/http: `go list -deps ./access` in access/deps_test.go enforces that a Guard is callable from a use case that has never heard of HTTP at all -- a worker, a CLI, a queue consumer.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrUnauthenticated indicates ctx carries no authenticated principal.
	ErrUnauthenticated = errors.New("access: unauthenticated")

	// ErrForbidden indicates the checker evaluated the request and denied
	// it: a genuine policy decision, not an infrastructure failure.
	ErrForbidden = errors.New("access: forbidden")

	// ErrUnavailable indicates the authorization decision could not be made
	// at all -- the PDP is unreachable or failing, or a configured
	// PrincipalAttributes resolver returned an error -- as opposed to the
	// request being genuinely denied.
	ErrUnavailable = errors.New("access: authorization decision unavailable")
)

Sentinel errors returned (optionally wrapped) by Guard.Principal and Guard.Check.

These are this package's own sentinels rather than a reuse of auth's ErrServiceUnavailable or authz's own errors: auth.ErrServiceUnavailable's name and doc comment both specifically name the identity provider -- it is the contract of auth.Authenticator, which this package does not implement. What can fail here is different: the PDP (authz.Checker) or a consumer's own attribute resolver (PrincipalAttributes), neither of which is an identity provider. authz itself declares no sentinels at all -- its Checker contract only distinguishes an error from (false, nil), leaving the mapping to status codes to whoever consumes it, which is exactly the job these three sentinels do for this package's callers.

Functions

func WithRequestScope

func WithRequestScope(ctx context.Context) context.Context

WithRequestScope returns a copy of ctx carrying a memoization scope for resolved principal attributes. Idempotent: if ctx already carries a scope, it is returned unchanged rather than nesting a second one, so calling this more than once on the same ctx tree (or its descendants) is always safe.

httpx/middleware.RequireAccess installs this automatically, once, when its Guard has a PrincipalAttributes resolver configured -- before its own coarse check runs -- so that the handler's later per-instance Check reuses the same resolved attributes instead of resolving them again.

Without a scope in ctx, Guard.Principal resolves attributes fresh on every call: that is the correct behavior for a call site that is not part of an HTTP request at all (a worker, a CLI, a scheduled job), which has no natural "request" over which to amortize the resolver.

Types

type Guard

type Guard struct {
	// contains filtered or unexported fields
}

Guard performs per-instance authorization checks against a authz.Checker, optionally resolving principal attributes first. Build one with New.

func New

func New(checker authz.Checker, opts ...Option) *Guard

New builds a Guard backed by checker, applying every opt in order.

checker must not be nil: a Guard with no way to reach a PDP is a wiring mistake, not a runtime condition a caller could sensibly recover from, so New panics immediately instead of deferring the failure to the first Check call (where it would otherwise surface as a confusing nil pointer dereference deep inside an unrelated request).

func (*Guard) Check

func (g *Guard) Check(ctx context.Context, resource authz.Resource, action string) error

Check resolves the caller's authz.Principal (see Principal) and asks g.checker whether it may perform action on resource, returning:

  • nil, when the checker allows the action;
  • an error wrapping ErrUnauthenticated, when ctx carries no principal;
  • ErrForbidden, when the checker returns (false, nil) -- a genuine policy denial;
  • an error wrapping ErrUnavailable (and the underlying cause), when either the principal-attribute resolver or the checker itself fails -- an infrastructure problem, not a denial.

resource, Attr included, is passed through to the checker exactly as given: Check does not substitute the collection wildcard for an empty resource ID the way httpx/middleware's coarse check does, because that substitution only makes sense before an entity exists to have an ID at all -- a route-shape concern the middleware owns, not this package.

func (*Guard) HasPrincipalAttributes

func (g *Guard) HasPrincipalAttributes() bool

HasPrincipalAttributes reports whether a PrincipalAttributes resolver was configured via WithPrincipalAttributes.

httpx/middleware.RequireAccess uses this to decide whether it needs to install a per-request memoization scope (WithRequestScope) before calling Check: when no resolver is configured, Principal never does any extra work, so installing a scope would add bookkeeping to a hot path with nothing for it to memoize. Exported (rather than kept package-private) because that decision has to be made from httpx/middleware, a different package, and Go has no way to expose a capability across a package boundary except through an exported name.

func (*Guard) Principal

func (g *Guard) Principal(ctx context.Context) (authz.Principal, error)

Principal resolves the authz.Principal for the caller stored in ctx by an earlier auth middleware: ID and Roles come straight from auth.Principal, and Attr is populated by the configured PrincipalAttributes resolver, if any.

Two failure modes are distinguished, because they map to different HTTP statuses one layer up: no auth.Principal in ctx means the caller was never authenticated at all and wraps ErrUnauthenticated, while a resolver error means the caller IS authenticated but this Guard could not finish deciding and wraps ErrUnavailable together with the resolver's own error (via a second %w, so both errors.Is(err, ErrUnavailable) and errors.Is(err, cause) hold).

type Option

type Option func(*Guard)

Option configures a Guard built by New.

func WithPrincipalAttributes

func WithPrincipalAttributes(fn PrincipalAttributes) Option

WithPrincipalAttributes configures the resolver Guard.Principal calls to populate authz.Principal.Attr. A nil fn is the same as not passing this option at all: the Guard behaves exactly as it did before this package supported attribute resolution, and Attr is always left nil.

type PrincipalAttributes

type PrincipalAttributes func(ctx context.Context, p *auth.Principal) (map[string]any, error)

PrincipalAttributes resolves consumer-owned attributes for an authenticated principal -- e.g. the set of resource IDs the user currently owns or is assigned to, looked up in whatever system tracks that assignment. The returned map becomes authz.Principal.Attr for every check made against that principal.

This is deliberately a function type, not an interface: the lookup is almost always a single call into an already-wired repository or client (see examples/api for one built from a store query), and a one-method interface would only add a name to implement without buying anything a closure does not already give the caller.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL