access

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Aug 14, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package access mirrors Illuminate\Auth\Access.

The files it answers to, in the clone at laravel_illuminate/auth/Access:

AuthorizationException.php
Gate.php
HandlesAuthorization.php
Response.php

It is the Laravel 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 has two authorization stories, and the weaker one wins whenever somebody is in a hurry (RULE 9). 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.

What is not ported, and why

Gate::setContainer sets the container the Gate resolves policies and the event dispatcher out of. Reason 2 -- a method that exists only to serve the container, which ADR 0001 rejected.

Gate::resolvePolicy is `$this->container->make($class)`, one line whose whole content is the container. Reason 2. Gate.Policy takes the policy itself.

Gate::guessPolicyNamesUsing replaces the convention that turns App\Models\Post into App\Policies\PostPolicy, and Gate::guessPolicyName is the convention itself. Both resolve a class by building its name as a string. Reasons 1 and 2: Go has no class lookup by name and no container to build one from. Register the policy with Gate.Policy.

Gate::getPolicyFromAttribute reads a #[UsePolicy] attribute off the model class. Reason 1 -- Go has no attributes. Same replacement.

The 'Class@method' and [Class, method] forms of Gate::define, and the $stringCallbacks they are remembered in, name a class to resolve by string. Reasons 1 and 2. Gate.Resource covers what they were for, taking the policy as a value.

Gate::canBeCalledWithUser, methodAllowsGuests, callbackAllowsGuests and parameterAllowsGuests reflect over a callback's first parameter to decide whether a guest may reach it: in PHP an unauthenticated user is null, and only a callback whose parameter is nullable is called with one. Reason 1. Nothing here is called with a null subject -- auth.Guest is a subject like any other, and an ability tells it apart with user.IsGuest, so the callback decides instead of its signature. A policy that says nothing about guests denies them.

Gate::dispatchGateEvaluatedEvent fires GateEvaluated through the dispatcher it resolves out of the container. Reason 2 for the resolution, and only for it: the event is written, in auth/access/events, and Gate.Observe is where the destination arrives -- as an argument, since there is no container to resolve one from. A Gate given none does no work.

HandlesAuthorization's allow() and deny() are protected trait methods. An unexported method promoted from an embedded struct is not callable by the package that embeds it, so they are Allow and Deny, the package functions. The two public ones are on HandlesAuthorization.

Illuminate\Contracts\Auth\Access\Gate, the interface, is not declared: Go satisfies an interface structurally, which is ADR 0045.

Everything in Response.php and AuthorizationException.php is here.

Index

Constants

View Source
const DefaultDenialMessage = "This action is unauthorized."

DefaultDenialMessage is the message the PHP constructor falls back to when it is given none: 'This action is unauthorized.'

Variables

This section is empty.

Functions

This section is empty.

Types

type Ability

type Ability func(ctx context.Context, user auth.Subject, arguments ...any) any

Ability is the callback Define registers, and the shape of a policy method.

It answers what the PHP callback answers -- bool|Response|null -- as one type, which is the third mechanical change of ADR 0044: PHP's mixed is Go's any. The four values it may hold are

nil            the PHP null: no opinion, carry on
true / false   the PHP bool
*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: $after($user, $ability, $result, $arguments).

It receives the result the check produced and can only replace it when it was nil. An after callback cannot overturn a decision -- that is the PHP's `$result ??= $afterResult`, and it 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 Illuminate\Auth\Access\AuthorizationException.

The name carries the second mechanical change of ADR 0044: PHP throws, Go returns an error, and a Go type that implements error is called an Error. The methods keep the PHP names.

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 is the constructor, __construct($message, $code, $previous).

An empty message becomes DefaultDenialMessage and a nil code becomes 0, which is what `$message ?? '...'` and `$code ?: 0` do in the PHP.

func (*AuthorizationError) AsNotFound

func (e *AuthorizationError) AsNotFound() *AuthorizationError

AsNotFound is AuthorizationException::asNotFound.

func (*AuthorizationError) Code

func (e *AuthorizationError) Code() any

Code is Exception::getCode, which the PHP constructor fills from its $code argument and toResponse reads back.

func (*AuthorizationError) Error

func (e *AuthorizationError) Error() string

Error implements error with the exception's message, which is Exception::getMessage.

func (*AuthorizationError) HasStatus

func (e *AuthorizationError) HasStatus() bool

HasStatus is AuthorizationException::hasStatus.

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 (RULE 9). 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 AuthorizationException::response: 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 is AuthorizationException::setResponse.

func (*AuthorizationError) Status

func (e *AuthorizationError) Status() *int

Status is AuthorizationException::status. A nil result is the PHP null.

func (*AuthorizationError) ToResponse

func (e *AuthorizationError) ToResponse() *Response

ToResponse is AuthorizationException::toResponse: 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, which is where the PHP catches the exception.

func (*AuthorizationError) Unwrap

func (e *AuthorizationError) Unwrap() error

Unwrap exposes the $previous of the constructor to errors.Is and errors.As.

func (*AuthorizationError) WithStatus

func (e *AuthorizationError) WithStatus(status *int) *AuthorizationError

WithStatus is AuthorizationException::withStatus. The status is int|null in PHP, so it is a pointer here; nil clears it.

type BeforeCallback

type BeforeCallback func(ctx context.Context, user auth.Subject, ability string, arguments []any) any

BeforeCallback is what Before registers: $before($user, $ability, $arguments).

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

type Condition func(ctx context.Context, user auth.Subject) any

Condition is the closure form of what AllowIf and DenyIf take.

The PHP accepts Response|Closure|bool there, so the parameter is any and this is the shape a closure must have to be called rather than read as a value.

type Gate

type Gate struct {
	HandlesAuthorization
	// contains filtered or unexported fields
}

Gate is Illuminate\Auth\Access\Gate.

It is the Laravel 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; the PHP has the same shape, for the same reason -- it is configuration, not request state.

func NewGate

func NewGate() *Gate

NewGate is the Gate constructor.

The PHP takes the container, a user resolver and the seven pieces of state a forUser() clone has to carry. None of them survive here: there is no container (ADR 0001), and the subject is an argument to every check rather than something the Gate resolves.

func (*Gate) Abilities

func (g *Gate) Abilities() map[string]Ability

Abilities is Gate::abilities.

The PHP hands back the array, and a PHP array is copied when it is handed back. This copies for the same reason: reading what a Gate knows must not be a way to change it.

func (*Gate) After

func (g *Gate) After(callback AfterCallback) *Gate

After is Gate::after: 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 Gate::allowIf: an on-demand check that fails when the condition is false.

The condition is a bool, a *Response or a Condition. The PHP throws, so this returns (T, error).

func (*Gate) Allows

func (g *Gate) Allows(ctx context.Context, s auth.Subject, ability string, arguments ...any) bool

Allows is Gate::allows.

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) Any

func (g *Gate) Any(ctx context.Context, s auth.Subject, abilities []string, arguments ...any) bool

Any is Gate::any: at least one of the abilities is granted.

An empty list is not, which is what Collection::contains answers for an empty collection.

func (*Gate) Authorize

func (g *Gate) Authorize(ctx context.Context, s auth.Subject, ability string, arguments ...any) (auth.Grant, error)

Authorize is Gate::authorize, and it is the reason this package exists.

The PHP returns the Response and throws when the check fails. This returns the auth.Grant, 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 (RULE 9).

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 is Gate::before: 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 is Gate::check: every one of the abilities is granted.

An empty list is granted, which is what Collection::every answers for an empty collection.

func (*Gate) DefaultDenialResponse

func (g *Gate) DefaultDenialResponse(response *Response) *Gate

DefaultDenialResponse is Gate::defaultDenialResponse: the denial Inspect answers with when a check simply failed and nobody said why.

func (*Gate) Define

func (g *Gate) Define(ability string, callback Ability) *Gate

Define is Gate::define.

The PHP also accepts a [Class, method] array or a 'Class@method' string and throws InvalidArgumentException for anything else. Neither form exists here -- both name a class to be resolved out of the container by string -- and the throw goes with them: a callback that is not an Ability does not compile, so there is nothing left to report at runtime. Resource is the callback array's replacement.

func (*Gate) Denies

func (g *Gate) Denies(ctx context.Context, s auth.Subject, ability string, arguments ...any) bool

Denies is Gate::denies.

func (*Gate) DenyIf

func (g *Gate) DenyIf(ctx context.Context, s auth.Subject, condition any, message string, code any) (*Response, error)

DenyIf is Gate::denyIf: an on-demand check that fails when the condition is true.

func (*Gate) ForUser

func (g *Gate) ForUser(s auth.Subject) *Gate

ForUser is Gate::forUser: a Gate that already knows whose abilities it is answering about.

The PHP swaps the user resolver, because there every check reads the subject off the Gate. Here the subject is an argument, so the one given to ForUser is what a check falls back to when it is passed the zero Subject -- an argument naming somebody always wins. It is the same clone the PHP makes, over a snapshot of the abilities, the policies and the callbacks.

The default denial response is not carried over, which is the PHP's own behaviour: forUser() does not pass it to the new instance.

func (*Gate) GetPolicyFor

func (g *Gate) GetPolicyFor(class any) any

GetPolicyFor is Gate::getPolicyFor.

It takes the model, a pointer to it, or its reflect.Type -- the last one being what the PHP means by passing a class name. It finds the policy registered for that exact type, then for the type on the other side of a pointer, then for any interface the type implements, which is where the PHP walks is_subclass_of.

The two lookups between those in the PHP are gone: getPolicyFromAttribute reads a #[UsePolicy] attribute off the class and guessPolicyName looks for a class named after it. Go has neither attributes nor class lookup by string.

func (*Gate) Has

func (g *Gate) Has(abilities ...string) bool

Has is Gate::has: every named ability has been defined.

The PHP takes an array or a list of arguments; the variadic covers both.

func (*Gate) Inspect

func (g *Gate) Inspect(ctx context.Context, s auth.Subject, ability string, arguments ...any) *Response

Inspect is Gate::inspect: the check, as a Response.

The PHP catches AuthorizationException here and turns it into a denial; nothing is thrown in Go, so an ability that answers with an error is the same case, and an *AuthorizationError keeps the status and the code it carried.

func (*Gate) None

func (g *Gate) None(ctx context.Context, s auth.Subject, abilities []string, arguments ...any) bool

None is Gate::none: not one of the abilities is granted.

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, which the PHP has for the same reason.

Illuminate resolves the dispatcher out of the container and fires unconditionally. There is no container (ADR 0001), so the destination is an argument, and a Gate given none does no work and allocates nothing.

func (*Gate) Policies

func (g *Gate) Policies() map[reflect.Type]any

Policies is Gate::policies, keyed by the model type rather than by class name. It is a copy, for the reason given on Abilities.

func (*Gate) Policy

func (g *Gate) Policy(model any, policy any) *Gate

Policy is Gate::policy: it says which policy decides for a given model.

The PHP matches on class name, because that is what a PHP value carries. Here the match is on the argument's reflect.Type -- the first porting reason, a language feature Go does not have. 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 covered. That is where the PHP walks is_subclass_of.

func (*Gate) Raw

func (g *Gate) Raw(ctx context.Context, s auth.Subject, ability string, arguments ...any) any

Raw is Gate::raw: the untouched answer of the callback, before Inspect reads it as an allow or a denial.

The PHP returns mixed and so does this: 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

func (g *Gate) Resource(name string, policy any, abilities map[string]string) *Gate

Resource is Gate::resource: it defines "<name>.<ability>" for each ability of a policy at once.

The PHP takes the policy's class name and resolves it out of the container per check; this takes the policy itself, because there is no container to resolve it from (ADR 0001). Everything else is the same, including the policy's own before method running first.

A nil abilities map means the PHP default, which is viewAny, view, create, update and delete, each mapping to the method of the same name. The keys are abilities and the values are the methods they call.

type HandlesAuthorization

type HandlesAuthorization struct{}

HandlesAuthorization is the Illuminate\Auth\Access\HandlesAuthorization trait.

A PHP trait is a set of methods copied into a class; the Go form of that is an embedded struct, so a policy embeds this one and gains the same two methods:

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, exactly as the PHP class does with `use HandlesAuthorization`.

The trait's other two methods, allow() and deny(), are protected. An unexported method promoted from an embedded struct is not callable by the package that embeds it, so they are the package functions Allow and Deny.

func (HandlesAuthorization) DenyAsNotFound

func (HandlesAuthorization) DenyAsNotFound(message string, code any) *Response

DenyAsNotFound is HandlesAuthorization::denyAsNotFound.

func (HandlesAuthorization) DenyWithStatus

func (HandlesAuthorization) DenyWithStatus(status int, message string, code any) *Response

DenyWithStatus is HandlesAuthorization::denyWithStatus.

type Response

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

Response is Illuminate\Auth\Access\Response: one authorization answer, carrying the sentence and the code that explain it.

It is used through a pointer because the PHP object is. 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.

The PHP class declares Arrayable and Stringable. Those are ToArray and String here, and nothing is declared: Go satisfies an interface structurally.

func Allow

func Allow(message string, code any) *Response

Allow is Response::allow.

It is also the allow() of the HandlesAuthorization trait: that method is protected and forwards here, and an unexported method promoted from an embedded struct is not callable by the package that embeds it, so the function is the only form of it that works.

func Deny

func Deny(message string, code any) *Response

Deny is Response::deny. It is also the trait's protected deny(), for the reason given on Allow.

func DenyAsNotFound

func DenyAsNotFound(message string, code any) *Response

DenyAsNotFound is Response::denyAsNotFound: a denial that answers 404 instead of 403, for a resource whose existence is itself private.

func DenyWithStatus

func DenyWithStatus(status int, message string, code any) *Response

DenyWithStatus is Response::denyWithStatus.

func NewResponse

func NewResponse(allowed bool, message string, code any) *Response

NewResponse is the Response constructor, __construct($allowed, $message, $code).

The PHP defaults the message to the empty string and the code to null; Go has no default arguments, so both are always passed. An empty message and a nil code mean the same thing they mean in PHP.

func (*Response) Allowed

func (r *Response) Allowed() bool

Allowed is Response::allowed.

func (*Response) AsNotFound

func (r *Response) AsNotFound() *Response

AsNotFound is Response::asNotFound.

func (*Response) Authorize

func (r *Response) Authorize() (*Response, error)

Authorize is Response::authorize: it fails when the response was a denial, and hands the response back when it was not.

The PHP throws, so this returns (T, error) -- the second mechanical change of ADR 0044. The error is an AuthorizationError carrying this response, its code and its status, exactly as the PHP exception does.

func (*Response) Code

func (r *Response) Code() any

Code is Response::code: the response code or reason, which PHP types as mixed.

func (*Response) Denied

func (r *Response) Denied() bool

Denied is Response::denied.

func (*Response) Message

func (r *Response) Message() string

Message is Response::message.

func (*Response) Status

func (r *Response) Status() *int

Status is Response::status. A nil result is the PHP null: no status was set.

func (*Response) String

func (r *Response) String() string

String is Response::__toString: the message on its own. It makes a Response a fmt.Stringer, which is what the PHP Stringable interface bought.

func (*Response) ToArray

func (r *Response) ToArray() map[string]any

ToArray is Response::toArray.

func (*Response) WithStatus

func (r *Response) WithStatus(status *int) *Response

WithStatus is Response::withStatus. The status is int|null in PHP, so it is a pointer here; nil clears it.

Directories

Path Synopsis
Package events mirrors Illuminate\Auth\Access\Events.
Package events mirrors Illuminate\Auth\Access\Events.

Jump to

Keyboard shortcuts

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