arvel-permission¶
Introduction¶
arvel-permission adds roles and permissions, modeled after Spatie's Laravel Permission. It gives you Role and Permission models, HasRoles / HasPermissions mixins for your user model, route middleware, and a bridge into the authorization Gate.
Installation¶
Register the provider and publish the migration:
# bootstrap/providers.py
from arvel_permission import PermissionServiceProvider
providers = [PermissionServiceProvider]
The migration creates five tables: roles, permissions, model_has_roles, model_has_permissions, role_has_permissions.
Making a Model Rolable¶
Mix HasRoles and HasPermissions into your user model and declare the polymorphic pivots:
from typing import ClassVar
from arvel.database import Model, id_
from arvel.database.orm import MorphToMany
from arvel_permission import (
HasRoles, HasPermissions, Role, Permission,
model_has_roles, model_has_permissions,
)
class User(Model, HasRoles, HasPermissions):
__tablename__ = "users"
id: int = id_(init=False)
default_guard_name: ClassVar[str] = "web"
roles: ClassVar[MorphToMany[Role]] = MorphToMany(
Role, table=model_has_roles, name="model", related_key="role_id"
)
permissions: ClassVar[MorphToMany[Permission]] = MorphToMany(
Permission, table=model_has_permissions, name="model", related_key="permission_id"
)
Note
All trait methods are async and need an active DB session in scope (the framework's session context). In a request that's already set up for you; in scripts and tests, wrap the call in the session context.
Assigning & Checking¶
await user.assign_role("editor")
await user.give_permission_to("posts.publish")
await user.has_role("editor") # -> bool
await user.has_any_role("editor", "admin")
await user.has_permission_to("posts.publish")
names = await user.get_role_names()
Other methods: remove_role, sync_roles, has_all_roles, has_level, revoke_permission_to, sync_permissions, get_all_permissions, get_direct_permissions, get_permissions_via_roles.
Permissions resolve through roles automatically — has_permission_to is true if the user has the permission directly or via any assigned role.
Wildcard Permissions¶
With wildcard_enabled (the default), a held permission like posts.* satisfies a check for posts.edit.
Route Middleware¶
The package provides three middleware classes. Register them yourself — the provider does not wire them:
from arvel_permission import RoleMiddleware, PermissionMiddleware, RoleOrPermissionMiddleware
RoleMiddleware("admin")
PermissionMiddleware("posts.publish")
RoleOrPermissionMiddleware("admin|posts.publish") # pipe = OR
A failed check raises UnauthorizedException.
Gate Integration¶
When PermissionServiceProvider boots and a Gate is bound, it registers a before hook so await gate.allows("posts.edit", user) resolves through the user's permissions. If no Gate is bound, this is skipped silently.
Configuration¶
PermissionConfig is a plain (frozen) model — there are no environment variables. Override defaults by setting the provider's config before boot:
from arvel_permission import PermissionConfig, PermissionServiceProvider
class AppPermissionProvider(PermissionServiceProvider):
config = PermissionConfig(default_guard_name="api", wildcard_enabled=True)
Notable fields: default_guard_name (web), cache_enabled (true), wildcard_enabled (true), events_enabled (false), cache_ttl (86400).
Gotchas¶
- The provider binds only
PermissionConfig.PermissionRegistraris not container-bound — instantiate it directly if you need programmatic role/permission registration. - Middleware and the user-model mixins are wired by you, not by the provider.
- Checking a role/permission that belongs to a different guard raises
GuardMismatchError. - The events module is in-process only and off by default (
events_enabled).