A signed-in user is not necessarily allowed to export a report or change
another customer’s records. Authentication establishes an identity;
authorization decides what that identity may do. Arkoren’s gate package
runs your permission checks before the protected action proceeds.
A rule is a Go function that receives a context and your authenticated user
type, then returns (bool, error). You supply the roles, ownership checks, and
other application decisions. The gate supplies ordered evaluation and the
missing-user and forbidden errors.
Identity Boundaries
Configure credential resolution
before using the gate. Rules must accept the exact type that authentication puts
in the context: User and *User are different types. A mismatch produces
auth.ErrUnauthenticated (401), even if credentials were valid for the other
type. The gate does not resolve credentials itself.
The examples use standard-library context, auth from
arkoren.dev/core/http/auth, and gate from
arkoren.dev/core/http/auth/gate. Assume your authenticator stores this
application-owned identity:
type User struct {
ID int
CustomerID int
CanExport bool
}
These fields come from your trusted identity loader, not request form fields. Focused snippets omit routine error propagation, but retain checks that prevent unauthorized work.
Route Admission
Use a route rule for an ability that does not depend on a particular record. For example, an export entitlement can admit users to the report-export route:
func canExport(_ context.Context, user User) (bool, error) {
return user.CanExport, nil
}
With your authenticated router r and application handler exportReport, apply
the rule before the handler runs:
r.With(gate.Require(canExport)).Post("/reports/{id}/export", exportReport)
No user of type User produces 401. A rule returning false, nil produces
gate.ErrForbidden (403); a non-nil error is returned unchanged, even if the
boolean is true. An infrastructure failure therefore does not become either
permission or an ordinary denial. Handling Denials explains how
these errors render.
If you pass several rules, every rule must allow. They run in argument order,
stopping at the first denial or error; put inexpensive checks before external
lookups. Express alternatives inside a rule with ordinary Go conditions.
gate.Require panics with no rules. For sign-in alone, use
auth.Require[User]() instead.
Resource Checks
Passing canExport does not authorize every report. Restrict which records the
caller can load through a scoped store lookup, then check any
additional action-specific permission against the stored record. Never build an
ownership decision from a request-supplied CustomerID or OwnerID.
For reports whose existence is already visible to the caller, an owner-only export rule can close over the report returned by your store:
type Report struct {
ID int
CustomerID int
OwnerID int
}
func canExportReport(report Report) func(context.Context, User) (bool, error) {
return func(_ context.Context, user User) (bool, error) {
return report.CustomerID == user.CustomerID && report.OwnerID == user.ID, nil
}
}
In the handler, with report already loaded through that scoped lookup, stop
before exporting if the resource check fails:
if err := gate.Authorize(r.Context(), canExport, canExportReport(report)); err != nil {
return err
}
Keep checks in the shared application operation when HTTP, tools, and jobs can perform the same action. A transport-only gate cannot protect a direct call; MCP security also separates tool availability from resource permission. A check followed by a database write can race with another request; use conditional writes and locking when permission or state can change between them.
Deferred Work
A queued export might run after membership or permissions change. Load the
current user and the report through your authorized store again when the job
runs, rather than trusting the queued permission snapshot. With those trusted
user and report values, attach the actor to the job context and reuse the
rules:
ctx = auth.NewContext(ctx, user)
if err := gate.Authorize(ctx, canExport, canExportReport(report)); err != nil {
return err
}
auth.NewContext attaches a value; it does not authenticate that value. The job
owns resolving the queued actor reference to a current identity. Do not attach
an arbitrary user decoded from the job payload and treat it as trusted.
As related checks grow, Writing Policies keeps them together
without introducing a separate policy language. For all functions and options,
see the gate API reference.