Documentation
¶
Overview ¶
Package access is the ability-and-policy vocabulary for authorization: a Gate holding abilities and policies, a Response carrying the sentence behind an answer, and an AuthorizationError for the failure.
Define an ability, then ask:
gate := access.NewGate()
gate.Define("posts.update", func(ctx context.Context, user auth.Subject, arguments ...any) any {
post, ok := arguments[0].(Post)
return ok && post.AuthorID == user.ID
})
if gate.Allows(ctx, subject, "posts.update", post) {
// draw the button
}
The Gate is not a second way to authorize ¶
A framework with an auth.Grant and a Gate would have two authorization stories, and the weaker one wins whenever somebody is in a hurry. So there is one: the Gate does not decide, it delegates. Gate.Authorize wraps the ability as an auth.Policy and hands it to auth.Authorize, which is the only function that builds a Grant:
type abilityPolicy struct {
fn func(ctx context.Context, s auth.Subject, args []any) error
}
func (p abilityPolicy) Can(ctx context.Context, s auth.Subject, a auth.Action, args []any) error {
return p.fn(ctx, s, args)
}
func (g *Gate) Authorize(ctx context.Context, s auth.Subject, ability string, arguments ...any) (auth.Grant, error) {
// ...
return auth.Authorize[[]any](ctx, abilityPolicy{fn: check}, s, auth.Action(ability), arguments)
}
What that buys is the whole thesis of the framework. auth.Grant has only unexported fields, every repository signature demands one, and this package has no way to forge one either -- so an ability defined here reaches the database through the same door a hand-written policy does, and the anonymous subject auth.Authorize refuses is refused here too, before the ability is ever consulted. Not the developer guaranteeing the architecture: the compiler.
The Gate's own answers -- Gate.Allows, Gate.Check, Gate.Any -- issue nothing, and that is the point of them: they are for the view, which needs to know whether to draw a button and has no use for a Grant. A handler that acts on one still has to call Gate.Authorize to reach a repository.
A guest is a subject ¶
Nothing here is ever called with an absent user. auth.Guest is a subject like any other, and an ability tells it apart by asking user.IsGuest, so the callback decides rather than its signature. An ability that says nothing about guests denies them.
Allow and Deny are functions ¶
An unexported method promoted from an embedded struct is not callable by the package that embeds it, so the two shorthands that build a Response are the package functions Allow and Deny rather than methods. The two that are exported are on HandlesAuthorization.
Index ¶
- Constants
- type Ability
- type AfterCallback
- type AuthorizationError
- func (e *AuthorizationError) AsNotFound() *AuthorizationError
- func (e *AuthorizationError) Code() any
- func (e *AuthorizationError) Error() string
- func (e *AuthorizationError) HasStatus() bool
- func (e *AuthorizationError) Is(target error) bool
- func (e *AuthorizationError) Response() *Response
- func (e *AuthorizationError) SetResponse(response *Response) *AuthorizationError
- func (e *AuthorizationError) Status() *int
- func (e *AuthorizationError) ToResponse() *Response
- func (e *AuthorizationError) Unwrap() error
- func (e *AuthorizationError) WithStatus(status *int) *AuthorizationError
- type BeforeCallback
- type Condition
- type Gate
- func (g *Gate) Abilities() map[string]Ability
- func (g *Gate) After(callback AfterCallback) *Gate
- func (g *Gate) AllowIf(ctx context.Context, s auth.Subject, condition any, message string, code any) (*Response, error)
- func (g *Gate) Allows(ctx context.Context, s auth.Subject, ability string, arguments ...any) bool
- func (g *Gate) Any(ctx context.Context, s auth.Subject, abilities []string, arguments ...any) bool
- func (g *Gate) Authorize(ctx context.Context, s auth.Subject, ability string, arguments ...any) (auth.Grant, error)
- func (g *Gate) Before(callback BeforeCallback) *Gate
- func (g *Gate) Check(ctx context.Context, s auth.Subject, abilities []string, arguments ...any) bool
- func (g *Gate) DefaultDenialResponse(response *Response) *Gate
- func (g *Gate) Define(ability string, callback Ability) *Gate
- func (g *Gate) Denies(ctx context.Context, s auth.Subject, ability string, arguments ...any) bool
- func (g *Gate) DenyIf(ctx context.Context, s auth.Subject, condition any, message string, code any) (*Response, error)
- func (g *Gate) ForUser(s auth.Subject) *Gate
- func (g *Gate) GetPolicyFor(class any) any
- func (g *Gate) Has(abilities ...string) bool
- func (g *Gate) Inspect(ctx context.Context, s auth.Subject, ability string, arguments ...any) *Response
- func (g *Gate) None(ctx context.Context, s auth.Subject, abilities []string, arguments ...any) bool
- func (g *Gate) Observe(observer func(events.GateEvaluated)) *Gate
- func (g *Gate) Policies() map[reflect.Type]any
- func (g *Gate) Policy(model any, policy any) *Gate
- func (g *Gate) Raw(ctx context.Context, s auth.Subject, ability string, arguments ...any) any
- func (g *Gate) Resource(name string, policy any, abilities map[string]string) *Gate
- type HandlesAuthorization
- type Response
- func (r *Response) Allowed() bool
- func (r *Response) AsNotFound() *Response
- func (r *Response) Authorize() (*Response, error)
- func (r *Response) Code() any
- func (r *Response) Denied() bool
- func (r *Response) Message() string
- func (r *Response) Status() *int
- func (r *Response) String() string
- func (r *Response) ToArray() map[string]any
- func (r *Response) WithStatus(status *int) *Response
Constants ¶
const DefaultDenialMessage = "This action is unauthorized."
DefaultDenialMessage is what an error built with no message says.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Ability ¶
Ability is the callback Define registers, and the shape of a policy method.
It answers with one type, and the four values it may hold are
nil no opinion, carry on true / false allowed, or not *Response an answer with a sentence, a code and maybe a status error a refusal; an *AuthorizationError keeps its status and code
The user is a value and never nil, so an ability that has to tell a visitor from an account asks user.IsGuest -- which is the framework's declared anonymous reader, not an absent one.
type AfterCallback ¶
type AfterCallback func(ctx context.Context, user auth.Subject, ability string, result any, arguments []any) any
AfterCallback is what After registers: a callback that runs once the check has an answer.
It receives the result the check produced and can only replace it when it was nil. An after callback cannot overturn a decision, which is the difference between a hook for logging and a second authorization path.
type AuthorizationError ¶
type AuthorizationError struct {
// contains filtered or unexported fields
}
AuthorizationError is the failure a refused authorization answers with.
It answers auth.ErrForbidden. That is not decoration: exception.StatusOf reads errors.Is(err, auth.ErrForbidden) to choose 403, and an authorization failure that did not answer it would be rendered as a 500 -- the one denial in the framework that leaks a stack trace to whoever was denied.
func NewAuthorizationError ¶
func NewAuthorizationError(message string, code any, previous error) *AuthorizationError
NewAuthorizationError returns the failure.
An empty message becomes DefaultDenialMessage, and a nil code becomes 0.
func (*AuthorizationError) AsNotFound ¶
func (e *AuthorizationError) AsNotFound() *AuthorizationError
AsNotFound sets that status to 404.
func (*AuthorizationError) Code ¶
func (e *AuthorizationError) Code() any
Code is the response code or reason the failure carries.
func (*AuthorizationError) Error ¶
func (e *AuthorizationError) Error() string
Error is the sentence the failure answers with.
func (*AuthorizationError) HasStatus ¶
func (e *AuthorizationError) HasStatus() bool
HasStatus reports that a status was set at all.
func (*AuthorizationError) Is ¶
func (e *AuthorizationError) Is(target error) bool
Is reports that every AuthorizationError is auth.ErrForbidden.
There is one authorization error in this framework and handlers translate it to 403. This is the Gate's refusal joining it rather than opening a second one that every handler would have to learn about separately.
func (*AuthorizationError) Response ¶
func (e *AuthorizationError) Response() *Response
Response is the response the Gate produced, when the failure came from one. It is nil otherwise.
func (*AuthorizationError) SetResponse ¶
func (e *AuthorizationError) SetResponse(response *Response) *AuthorizationError
SetResponse attaches that response, and returns the failure.
func (*AuthorizationError) Status ¶
func (e *AuthorizationError) Status() *int
Status is that status, and nil when none was set.
func (*AuthorizationError) ToResponse ¶
func (e *AuthorizationError) ToResponse() *Response
ToResponse is the denial this failure stands for, built again from the message, the code and the status.
It is what Gate.Inspect calls when an ability answers with a failure instead of a Response.
func (*AuthorizationError) Unwrap ¶
func (e *AuthorizationError) Unwrap() error
Unwrap exposes the cause behind this failure to errors.Is and errors.As.
func (*AuthorizationError) WithStatus ¶
func (e *AuthorizationError) WithStatus(status *int) *AuthorizationError
WithStatus sets the HTTP status the failure carries. A nil pointer clears it.
type BeforeCallback ¶
type BeforeCallback func(ctx context.Context, user auth.Subject, ability string, arguments []any) any
BeforeCallback is what Before registers: a callback that runs ahead of every check.
A non-nil answer short-circuits the whole check, which is how an administrator override is written. A nil answer lets the check carry on.
type Condition ¶
Condition is the callback form of what AllowIf and DenyIf take.
Their condition parameter is any, so this is the shape a callback must have to be called rather than read as a value.
type Gate ¶
type Gate struct {
HandlesAuthorization
// contains filtered or unexported fields
}
Gate is the ability-name call site -- gate.Allows(ctx, subject, "update", post) -- over the one authorization path this framework has.
Gate.Authorize does not decide anything itself: it hands the ability to auth.Authorize as a policy, so the Grant that comes back is the same Grant a hand-written auth.Policy produces, and no repository is reachable without one. See the package documentation for the adapter that does it.
A Gate is built at boot and read afterwards. Define, Policy, Before, After and DefaultDenialResponse write to it and are not safe to call while another goroutine is checking: it is configuration, not request state.
func NewGate ¶
func NewGate() *Gate
NewGate returns an empty Gate: no abilities, no policies, no callbacks.
It takes nothing, because the subject is an argument to every check rather than something the Gate resolves for itself.
func (*Gate) Abilities ¶
Abilities is every ability the Gate has been given, by name.
It is a copy: reading what a Gate knows must not be a way to change it.
func (*Gate) After ¶
func (g *Gate) After(callback AfterCallback) *Gate
After registers a callback that runs after every check.
func (*Gate) AllowIf ¶
func (g *Gate) AllowIf(ctx context.Context, s auth.Subject, condition any, message string, code any) (*Response, error)
AllowIf is an on-demand check that fails when the condition is false.
The condition is a bool, a *Response or a Condition. A failure is returned as an error.
func (*Gate) Allows ¶
Allows reports whether the ability is granted.
It answers yes or no and issues nothing. A handler that acts on the answer still has to call Authorize to reach a repository, because that is the call that produces the Grant -- Allows is for the view, which needs to know whether to draw the button.
func (*Gate) Authorize ¶
func (g *Gate) Authorize(ctx context.Context, s auth.Subject, ability string, arguments ...any) (auth.Grant, error)
Authorize runs the check and issues the Grant, and it is the reason this package exists.
It answers with an auth.Grant rather than a Response, because a Response proves nothing: every repository in the framework demands a Grant, and a Gate that answered with anything else would be a second way to authorize -- one whose answer no repository can be reached with.
The Grant is not built here. The ability is wrapped as an auth.Policy and handed to auth.Authorize, which refuses an anonymous subject before the ability is ever consulted and issues the Grant when it is allowed. The decision goes through the same door a hand-written policy goes through.
The error is the AuthorizationError the denial produced -- it carries the Response, the code and the status the ability asked for, which the sentence auth.Authorize builds does not -- with that sentence kept as its cause. Either way errors.Is(err, auth.ErrForbidden) holds, so the exception handler answers 403 without knowing this package exists.
func (*Gate) Before ¶
func (g *Gate) Before(callback BeforeCallback) *Gate
Before registers a callback that runs before every check.
func (*Gate) Check ¶
func (g *Gate) Check(ctx context.Context, s auth.Subject, abilities []string, arguments ...any) bool
Check reports that every one of the abilities is granted.
An empty list is granted.
func (*Gate) DefaultDenialResponse ¶
DefaultDenialResponse sets the denial Gate.Inspect answers with when a check simply failed and nobody said why.
func (*Gate) Define ¶
Define registers the callback that decides an ability by name.
The callback is an Ability and nothing else, so a callback of the wrong shape does not compile and there is nothing left to report at run time. To define a whole group of abilities from one policy, use Gate.Resource.
func (*Gate) Denies ¶
Denies is the negation of Gate.Allows.
func (*Gate) DenyIf ¶
func (g *Gate) DenyIf(ctx context.Context, s auth.Subject, condition any, message string, code any) (*Response, error)
DenyIf is an on-demand check that fails when the condition is true.
func (*Gate) ForUser ¶
ForUser returns a Gate that already knows whose abilities it is answering about.
The subject is still an argument to every check, so the one given here is what a check falls back to when it is passed the zero Subject -- an argument naming somebody always wins. The copy is over a snapshot of the abilities, the policies and the callbacks.
The default denial response is not carried over.
func (*Gate) GetPolicyFor ¶
GetPolicyFor is the policy registered for a model, or nil.
It takes the model, a pointer to it, or its reflect.Type. It looks for the policy registered against that exact type, then against the type on the other side of a pointer, then against any interface the type implements. Nothing is resolved from a name in a string.
func (*Gate) Inspect ¶
func (g *Gate) Inspect(ctx context.Context, s auth.Subject, ability string, arguments ...any) *Response
Inspect is the check, as a Response.
An ability that answers with an error is a denial, and an *AuthorizationError keeps the status and the code it carried.
func (*Gate) Observe ¶ added in v0.2.0
func (g *Gate) Observe(observer func(events.GateEvaluated)) *Gate
Observe hands the Gate somewhere to send events.GateEvaluated, and answers a copy.
It answers a copy rather than mutating, so that a Gate handed to two places cannot have its audit trail redirected by one of them. It is the same shape as ForUser.
The destination is an argument, and a Gate given none does no work and allocates nothing.
func (*Gate) Policies ¶
Policies is every policy the Gate has been given, keyed by the model type. It is a copy, for the reason given on Abilities.
func (*Gate) Policy ¶
Policy says which policy decides for a given model.
The match is on the argument's reflect.Type, never on a name in a string. Register the model by value or by pointer; GetPolicyFor finds either from the other.
A policy is any value with a method named after the ability, in the shape of an Ability:
func (PostPolicy) Update(ctx context.Context, user auth.Subject, arguments ...any) any
It may also have a before method, which runs first and short-circuits the check when it answers anything but nil:
func (PostPolicy) Before(ctx context.Context, user auth.Subject, ability string, arguments ...any) any
An interface type may be registered too, and any model implementing it is then covered by that policy.
func (*Gate) Raw ¶
Raw is the untouched answer of the callback, before Inspect reads it as an allow or a denial: nil, a bool, a *Response or an error.
It fires events.GateEvaluated when an observer was given, after the answer is settled. See Gate.Observe.
func (*Gate) Resource ¶
Resource defines "<name>.<ability>" for each ability of a policy at once.
It takes the policy value itself, and the policy's own before method still runs first on every ability it registers.
A nil abilities map means viewAny, view, create, update and delete, each mapping to the method of the same name. Otherwise the keys are abilities and the values are the methods they call.
type HandlesAuthorization ¶
type HandlesAuthorization struct{}
HandlesAuthorization is the two denial shorthands a policy embeds to reach without importing them:
type PostPolicy struct {
access.HandlesAuthorization
}
func (p PostPolicy) Update(ctx context.Context, user auth.Subject, arguments ...any) any {
return p.DenyAsNotFound("no post with that id", nil)
}
Gate embeds it too.
The allow and deny shorthands are the package functions Allow and Deny instead: an unexported method promoted from an embedded struct is not callable by the package that embeds it.
func (HandlesAuthorization) DenyAsNotFound ¶
func (HandlesAuthorization) DenyAsNotFound(message string, code any) *Response
DenyAsNotFound is a denial that answers 404.
func (HandlesAuthorization) DenyWithStatus ¶
func (HandlesAuthorization) DenyWithStatus(status int, message string, code any) *Response
DenyWithStatus is a denial that answers with the given HTTP status.
type Response ¶
type Response struct {
// contains filtered or unexported fields
}
Response is one authorization answer, carrying the sentence and the code that explain it.
It is used through a pointer: WithStatus and AsNotFound set a field on the receiver and hand it back, and Response.Authorize reads that field again afterwards, so a value receiver would have dropped the status between the two calls.
func Allow ¶
Allow returns an answer that permits the action.
It is a function and not a method on HandlesAuthorization, because an unexported method promoted from an embedded struct is not callable by the package that embeds it.
func Deny ¶
Deny returns an answer that refuses the action. It is a function for the reason given on Allow.
func DenyAsNotFound ¶
DenyAsNotFound is a denial that answers 404 instead of 403, for a resource whose existence is itself private.
func DenyWithStatus ¶
DenyWithStatus is a refusal that answers with the given HTTP status.
func NewResponse ¶
NewResponse returns an answer with the given message and code.
An empty message and a nil code are both allowed, and mean that nothing was said beyond the answer itself.
func (*Response) AsNotFound ¶
AsNotFound sets the status to 404.
func (*Response) Authorize ¶
Authorize fails when the response was a denial, and hands the response back when it was not.
The error is an AuthorizationError carrying this response, its code and its status.
func (*Response) WithStatus ¶
WithStatus sets the HTTP status the answer carries. A nil pointer clears it.