Documentation
¶
Overview ¶
Package authz is a small, application-agnostic authorization layer built on the classic subject–action–object model of access control: "may this subject perform this action on this object?".
All three sides of the question are type parameters chosen by the application. The subject type S is typically its user or principal struct; the resource type R and the action type A are comparable identifier types — plain strings work, opaque struct types make forged literals impossible (see the README, "Choosing R and A"). The package never inspects any of them: it only carries them from the call site to the policies, which receive them already typed.
Basic usage, with strings for both identifier types:
engine := authz.NewEngine[string, string, *User]()
engine.RegisterFunc("document", func(ctx context.Context, req authz.Request[string, *User]) (bool, error) {
return req.Subject.Role == "admin", nil
})
err := engine.Require(ctx, user, "document", "read", "doc-123")
Type-safe policies with ResourceChecker and an ObjectLoader that fetches the object behind a Request's ObjectID:
type DocumentPolicy struct{}
func (DocumentPolicy) Can(ctx context.Context, user *User, action string, doc Document) (bool, error) {
switch action {
case "read":
return doc.IsPublic || doc.OwnerID == user.ID, nil
case "update", "delete":
return doc.OwnerID == user.ID, nil
}
return false, nil
}
engine.Register("document", authz.Wrap(DocumentPolicy{}, documentLoader))
For resources that also have collection-level actions (create, list, ...) use WrapHybrid; for resources that only have collection-level actions use WrapCollectionOnly.
When the caller has already loaded the object (the common case inside a service layer), Engine.CheckObject and Engine.RequireObject authorize it directly, skipping the ObjectLoader round-trip. Every checker built with Wrap or WrapHybrid supports this.
Denials returned by Require and RequireObject match errors.Is(err, ErrUnauthorized) and carry the request context as an *UnauthorizedError. Pass a *slog.Logger with WithLogger to have denials logged; applications that log with zerolog can bridge it through the zerologadapter subpackage.
Throughout the package, generic types share one parameter order: identifier types first (R, then A), then the subject S, then the object type T.
Index ¶
- Variables
- type CollectionChecker
- type CollectionCheckerFunc
- type ConfigModifier
- type Engine
- func (e *Engine[R, A, S]) Check(ctx context.Context, subject S, resource R, action A, objectID string) (bool, error)
- func (e *Engine[R, A, S]) CheckObject(ctx context.Context, subject S, resource R, action A, obj any) (bool, error)
- func (e *Engine[R, A, S]) HasChecker(resource R) bool
- func (e *Engine[R, A, S]) Register(resource R, checker PolicyChecker[A, S])
- func (e *Engine[R, A, S]) RegisterFunc(resource R, fn func(ctx context.Context, req Request[A, S]) (bool, error))
- func (e *Engine[R, A, S]) Require(ctx context.Context, subject S, resource R, action A, objectID string) error
- func (e *Engine[R, A, S]) RequireObject(ctx context.Context, subject S, resource R, action A, obj any) error
- func (e *Engine[R, A, S]) Resources() []R
- type ObjectChecker
- type ObjectLoader
- type ObjectLoaderFunc
- type PolicyChecker
- func Wrap[A comparable, S, T any](checker ResourceChecker[A, S, T], loader ObjectLoader[T]) PolicyChecker[A, S]
- func WrapCollectionOnly[A comparable, S any](collection CollectionChecker[A, S]) PolicyChecker[A, S]
- func WrapFunc[A comparable, S, T any](checker func(ctx context.Context, subject S, action A, obj T) (bool, error), ...) PolicyChecker[A, S]
- func WrapHybrid[A comparable, S, T any](resource ResourceChecker[A, S, T], collection CollectionChecker[A, S], ...) PolicyChecker[A, S]
- type PolicyCheckerFunc
- type Request
- type ResourceChecker
- type ResourceCheckerFunc
- type UnauthorizedError
Constants ¶
This section is empty.
Variables ¶
var ( // ErrNoChecker is returned by Check* when no checker is registered for // the requested resource kind (unless WithDenyUnknownResources is set). ErrNoChecker = errors.New("authz: no policy checker registered for resource") // Require and RequireObject. ErrUnauthorized = errors.New("authz: unauthorized") // ErrMissingObjectID is returned when an object-level checker receives a // request without an ObjectID. ErrMissingObjectID = errors.New("authz: action requires an object ID") // ErrUnexpectedObjectID is returned when a collection-only checker // receives a request with an ObjectID. ErrUnexpectedObjectID = errors.New("authz: collection-only checker received an object ID") // ErrNoObjectChecker is returned by CheckObject when the registered // checker cannot authorize already-loaded objects. ErrNoObjectChecker = errors.New("authz: checker cannot authorize already-loaded objects") // ErrObjectType is returned by CheckObject when the object is not of the // type the registered checker handles. ErrObjectType = errors.New("authz: object type does not match registered checker") )
Functions ¶
This section is empty.
Types ¶
type CollectionChecker ¶
type CollectionChecker[A comparable, S any] interface { CanCollection(ctx context.Context, subject S, action A) (bool, error) }
CollectionChecker decides collection-level actions (create, list, ...), which have no object.
type CollectionCheckerFunc ¶
type CollectionCheckerFunc[A comparable, S any] func(ctx context.Context, subject S, action A) (bool, error)
CollectionCheckerFunc adapts a function to CollectionChecker.
func (CollectionCheckerFunc[A, S]) CanCollection ¶
func (f CollectionCheckerFunc[A, S]) CanCollection(ctx context.Context, subject S, action A) (bool, error)
type ConfigModifier ¶
type ConfigModifier func(*engineConfig)
A ConfigModifier mutates one setting of an engineConfig. Each With* constructor returns one; NewEngine folds them, in order, over the default configuration.
func WithDenyUnknownResources ¶
func WithDenyUnknownResources() ConfigModifier
WithDenyUnknownResources makes checks on resource kinds with no registered checker return a plain denial (fail closed) instead of ErrNoChecker.
func WithLogger ¶
func WithLogger(l *slog.Logger) ConfigModifier
WithLogger enables Warn-level logging of the denials returned by Require and RequireObject. The subject is never logged.
type Engine ¶
type Engine[R, A comparable, S any] struct { // contains filtered or unexported fields }
Engine dispatches authorization checks to the PolicyChecker registered for each resource kind. The resource type R and the action type A are comparable identifier types chosen by the application: plain strings work, named string types document intent, opaque struct types make forged literals impossible (see the README). The subject type S is the application's user or principal type. The engine never inspects any of them; R and A are rendered with fmt.Sprint only in denial errors and logs, so give them a String method for readable output.
func NewEngine ¶
func NewEngine[R, A comparable, S any](modifiers ...ConfigModifier) *Engine[R, A, S]
func (*Engine[R, A, S]) Check ¶
func (e *Engine[R, A, S]) Check(ctx context.Context, subject S, resource R, action A, objectID string) (bool, error)
Check reports whether subject may perform action on the object of the given resource kind identified by objectID (empty for collection-level actions).
func (*Engine[R, A, S]) CheckObject ¶
func (e *Engine[R, A, S]) CheckObject(ctx context.Context, subject S, resource R, action A, obj any) (bool, error)
CheckObject is Check for an object the caller has already loaded, skipping the ObjectLoader round-trip. The registered checker must implement ObjectChecker (checkers built with Wrap and WrapHybrid do).
func (*Engine[R, A, S]) HasChecker ¶
HasChecker reports whether a checker is registered for the resource kind, e.g. for boot-time wiring assertions.
func (*Engine[R, A, S]) Register ¶
func (e *Engine[R, A, S]) Register(resource R, checker PolicyChecker[A, S])
Register binds a checker to a resource kind, replacing any previous one.
func (*Engine[R, A, S]) RegisterFunc ¶
func (e *Engine[R, A, S]) RegisterFunc(resource R, fn func(ctx context.Context, req Request[A, S]) (bool, error))
RegisterFunc is Register for a plain function.
func (*Engine[R, A, S]) Require ¶
func (e *Engine[R, A, S]) Require(ctx context.Context, subject S, resource R, action A, objectID string) error
Require is Check returning *UnauthorizedError on denial instead of a bool.
type ObjectChecker ¶
type ObjectChecker[A comparable, S any] interface { CanObject(ctx context.Context, subject S, action A, obj any) (bool, error) }
ObjectChecker is implemented by checkers that can also decide on an already-loaded object, letting Engine.CheckObject skip the ObjectLoader round-trip. Checkers built with Wrap and WrapHybrid implement it.
type ObjectLoader ¶
ObjectLoader fetches the object a Request's ObjectID refers to, converting the boundary-level string ID to whatever the application uses internally.
type ObjectLoaderFunc ¶
ObjectLoaderFunc adapts a function to ObjectLoader.
type PolicyChecker ¶
type PolicyChecker[A comparable, S any] interface { Can(ctx context.Context, req Request[A, S]) (bool, error) }
PolicyChecker decides authorization requests for one resource kind.
func Wrap ¶
func Wrap[A comparable, S, T any](checker ResourceChecker[A, S, T], loader ObjectLoader[T]) PolicyChecker[A, S]
Wrap builds a PolicyChecker for a resource that only has object-level actions. Loader errors are returned as-is, so callers can tell "object not found" apart from a denial.
func WrapCollectionOnly ¶
func WrapCollectionOnly[A comparable, S any](collection CollectionChecker[A, S]) PolicyChecker[A, S]
WrapCollectionOnly builds a PolicyChecker for a resource that only has collection-level actions.
func WrapFunc ¶
func WrapFunc[A comparable, S, T any]( checker func(ctx context.Context, subject S, action A, obj T) (bool, error), loader func(ctx context.Context, id string) (T, error), ) PolicyChecker[A, S]
WrapFunc is Wrap for plain functions.
func WrapHybrid ¶
func WrapHybrid[A comparable, S, T any](resource ResourceChecker[A, S, T], collection CollectionChecker[A, S], loader ObjectLoader[T]) PolicyChecker[A, S]
WrapHybrid builds a PolicyChecker for a resource with both object-level and collection-level actions: requests without an ObjectID go to collection, the others load the object and go to resource. It panics on a nil collection so that a miswired resource fails at startup rather than silently denying; use Wrap for object-only resources.
type PolicyCheckerFunc ¶
type PolicyCheckerFunc[A comparable, S any] func(ctx context.Context, req Request[A, S]) (bool, error)
PolicyCheckerFunc adapts a function to PolicyChecker.
type Request ¶
type Request[A comparable, S any] struct { Subject S Action A ObjectID string }
Request is a single authorization question: may Subject perform Action on the object identified by ObjectID? The resource kind is not part of the request: a policy is registered for exactly one resource kind, so it already knows which one it is deciding for.
ObjectID is a string because that is how object identifiers arrive at an application's boundary (URL path segments, message fields); the ObjectLoader given to Wrap converts it to whatever the application uses internally. It is empty for collection-level actions such as create or list.
type ResourceChecker ¶
type ResourceChecker[A comparable, S, T any] interface { Can(ctx context.Context, subject S, action A, obj T) (bool, error) }
ResourceChecker decides object-level actions on objects of type T.
type ResourceCheckerFunc ¶
type ResourceCheckerFunc[A comparable, S, T any] func(ctx context.Context, subject S, action A, obj T) (bool, error)
ResourceCheckerFunc adapts a function to ResourceChecker.
type UnauthorizedError ¶
type UnauthorizedError struct {
}
UnauthorizedError is the denial returned by Require and RequireObject. It matches errors.Is(err, ErrUnauthorized) and carries the request context for callers that map denials to transport responses (e.g. HTTP 403 bodies). Resource and Action hold the fmt.Sprint rendering of the typed values the engine was called with; give R and A a String method to control it.
func (*UnauthorizedError) Error ¶
func (e *UnauthorizedError) Error() string
func (*UnauthorizedError) Is ¶
func (e *UnauthorizedError) Is(target error) bool
Directories
¶
| Path | Synopsis |
|---|---|
|
Package zerologadapter bridges zerolog into log/slog, so applications that log with zerolog can plug their logger into slog-based libraries (such as authz) without a second logging stack.
|
Package zerologadapter bridges zerolog into log/slog, so applications that log with zerolog can plug their logger into slog-based libraries (such as authz) without a second logging stack. |