Skip to content

permissions

The permissions module decouples role-based and per-user permission grants from the framework's in-memory PermissionRegistry. It owns:

  • Two assignment tables (permissions_role_permission, permissions_user_permission).
  • An admin UI to edit them.
  • A RequiresPermission dependency that consults both roles and direct user grants.

The framework ships its own simpler RequiresPermission in simple_module_hosting.permissions that only checks roles; if you install the permissions module, prefer the one re-exported from permissions.deps because it also honours direct grants.

ModuleMeta

FieldValue
namePermissions
route_prefix/api/permissions
view_prefix/permissions
depends_on["Auth", "Users"]

Routes

API

All require authentication. Read endpoints need permissions.view; mutate endpoints need permissions.manage.

Method + pathBody / responsePermission
GET /api/permissions/list[PermissionGroupOut]permissions.view
GET /api/permissions/roles/{role_id}RolePermissionsOutpermissions.view
PUT /api/permissions/roles/{role_id}RolePermissionsUpdateRolePermissionsOutpermissions.manage
GET /api/permissions/users/{user_id}UserPermissionsOutpermissions.view
PUT /api/permissions/users/{user_id}UserPermissionsUpdateUserPermissionsOutpermissions.manage

View

Method + pathInertia component / behaviourPermission
GET /permissions/redirect to /users/adminlogin
GET /permissions/roles/{role_id}/editPermissions/RoleEditpermissions.manage
PUT /permissions/roles/{role_id}form action; redirectspermissions.manage
GET /permissions/users/{user_id}/editPermissions/UserEditpermissions.manage
PUT /permissions/users/{user_id}form action; redirectspermissions.manage

Using RequiresPermission

python
from fastapi import APIRouter, Depends
from permissions.deps import RequiresPermission

router = APIRouter()

@router.delete(
    "/orders/{order_id}",
    dependencies=[Depends(RequiresPermission("orders.delete"))],
)
async def delete_order(order_id: int) -> None:
    ...

RequiresPermission(permission) takes a single permission key and 403s unless the request's user holds it, considering:

  1. The keys assigned to any of the user's roles (read from the request-cached resolved_permissions).
  2. The keys assigned directly to the user (permissions_user_permission).
  3. The implicit WILDCARD grant — the admin role is synced to hold every permission key at startup, so admins pass any check.

For something tied to only role membership (no direct grants), use auth.deps.require_permission instead — it's a hair cheaper.

Public contracts

python
from permissions.contracts.schemas import (
    PermissionGroupOut, RoleOut, RolePermissionsOut, RolePermissionsUpdate,
    UserOut, UserPermissionsOut, UserPermissionsUpdate,
)
ClassPurpose
PermissionGroupOutA named group (e.g. Orders) with the list of permission keys it owns. Built from the framework's PermissionRegistry.
RoleOutid, name, description.
RolePermissionsOutA role plus its assigned permission keys.
RolePermissionsUpdate{ "permissions": ["orders.view", ...] } — replaces the full set.
UserOutid, email, full_name.
UserPermissionsOutA user, the keys granted directly, and the keys inherited from roles.
UserPermissionsUpdate{ "permissions": [...] } — replaces the user's direct grants only.

Models

RolePermission (table permissions_role_permission)

ColumnTypeNotes
role_namestrcomposite PK
permission_keystrcomposite PK; indexed for reverse lookups
assigned_atdatetime
assigned_bystr | Noneactor email

UserPermission (table permissions_user_permission)

ColumnTypeNotes
user_idUUIDcomposite PK; references users_user.id
permission_keystrcomposite PK
assigned_atdatetime
assigned_bystr | Noneactor email

The schema deliberately keys by string permission key, not a normalised permissions table. Permission keys are the source of truth (registered at boot from register_permissions); the rows here are just assignments. If a key disappears from the registry, its assignments become inert (no FK to break) and the admin UI flags them as orphans.

Permissions

CodeGranted toPurpose
permissions.viewadminread groups, roles, user grants
permissions.manageadminedit role + user grants

(none) — the editor is reachable from the users admin pages (each user/role row links to its permissions edit page).

Inertia pages

  • Permissions/RoleEdit.tsx — checkbox grid grouped by PermissionGroup; submits the full set on save.
  • Permissions/UserEdit.tsx — same grid, with badges showing which permissions are inherited from roles vs granted directly.

Locales

Top-level keys in permissions/locales/en.json: browse, table, edit (role editor), user_edit (user editor), toasts, errors.

Released under the MIT License.