Skip to content

Authorization

Introduction

In addition to authentication, Arvel provides a way to authorize user actions against a given resource. Authorization is built on two primitives: gates (closures that decide a single ability) and policies (classes that group authorization logic around a model).

Note

Authorization in Arvel is deliberately minimal — a Gate class, a Policy base, and a CanMiddleware. There is no Gate facade and no Auth.user()-style implicit user; you pass the user explicitly. Roles and permissions are not in core (see Roles & Permissions).

Registering the Provider

There's no separate authorization provider — the Gate singleton is bound by AuthServiceProvider, the same provider that powers authentication. Add it to bootstrap/providers.py:

# bootstrap/providers.py
from arvel.auth.provider import AuthServiceProvider

providers = [
    # ...other providers...
    AuthServiceProvider,
]

Without it, self.container.make(Gate) raises because nothing has registered the binding.

Gates

The Gate is registered as a container singleton. Resolve it from the container (or inject it) to define and check abilities.

Defining Abilities

Define an ability with a name and a callback. The callback receives the user and any resource arguments, and returns a boolean. It can be sync or async. Resolve the Gate singleton from the container — typically in a service provider's boot(), where self.container is available:

from arvel.auth.gate import Gate
from arvel.providers import ServiceProvider


class AuthorizationServiceProvider(ServiceProvider):
    def boot(self) -> None:
        gate = self.container.make(Gate)
        gate.define("edit-post", lambda user, post: user.id == post["owner_id"])

The snippets below assume gate is that resolved instance.

Authorizing Actions

Three async methods check an ability:

from arvel.auth.exceptions import AuthorizationException

if await gate.allows("edit-post", user, post):
    ...

if await gate.denies("edit-post", user, post):
    raise AuthorizationException()

# raises AuthorizationException when denied
await gate.authorize("edit-post", user, post)

Warning

Gates are fail-closed: checking an ability that was never defined (and has no matching policy) raises AuthorizationException rather than silently returning False. Define every ability you check.

Before & After Hooks

Register a before hook to short-circuit checks (for example, to grant superadmins everything). A non-None return value wins. An after hook observes the result:

gate.before(lambda user, ability: True if user.is_admin else None)
gate.after(lambda user, ability, result: log_decision(user, ability, result))

Note

before hooks receive only (user, ability) — they can't inspect the resource arguments. Use a full ability or policy when the decision depends on the resource.

Policies

When authorization logic for a model grows beyond a single closure, group it into a policy class.

Writing a Policy

Subclass Policy[T]. Each ability maps to a method of the same name, receiving the user and (optionally) the resource. Methods can be sync or async:

from typing import Any
from arvel.auth.policy import Policy
from app.models.post import Post


class PostPolicy(Policy[Post]):
    def view(self, user: Any, post: Post) -> bool:
        return post.published or post.user_id == user.id

    async def update(self, user: Any, post: Post) -> bool:
        return post.user_id == user.id

Registering a Policy

Map a model type to a policy instance on the gate. The gate dispatches to the policy automatically when the first resource argument's type is registered:

gate.policy(Post, PostPolicy())

await gate.allows("update", user, post)   # routed to PostPolicy.update

Note

There is no policy auto-discovery — register each policy explicitly. The ability name must exactly match the policy method name.

Enforcing Authorization in Routes

Attach CanMiddleware to require an ability. It checks the gate for the authenticated user and raises an unauthenticated error (401) when no user is present, or an authorization error (403) when the check fails:

from arvel import Route
from arvel.http.middleware import Authenticate
from arvel.auth.middleware.can import CanMiddleware


@Route.get(
    "/admin",
    middleware=[Authenticate("web"), CanMiddleware("admin-only")],
)
async def admin(request: Request) -> dict[str, str]:
    return {"area": "admin"}

Pass model_param= to inject a route path parameter as the gate's resource argument.

Roles & Permissions

Core Arvel ships gates, policies, and CanMiddleware — but no roles or permissions tables. Role-based access control lives in the separate arvel-permission package, which adds Role and Permission models, HasRoles / HasPermissions traits, route middleware (RoleMiddleware, PermissionMiddleware), and a gate integration that grants abilities based on a user's permissions.

See arvel-permission.