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.