Skip to content

Role-Based Access Control

CMS API implements RBAC through a many-to-many relationship between users, roles, and permissions.

Model

User ──── user_roles ──── Role ──── role_permissions ──── Permission
  • A user can have multiple roles
  • A role can have multiple permissions
  • A user's effective permissions are the union of all permissions across all their roles

Permission Naming

Permissions follow a resource:action pattern:

users:read
users:create
users:update
users:delete
users:assign_roles
roles:read
...

This makes it easy to reason about what each permission allows and to group permissions by resource.

How Authorization Works

When a user logs in, their permissions are collected and embedded in the access token:

# Collected at login time
permissions = []
for role in user.roles:
    for permission in role.permissions:
        permissions.append(permission.name)

# Embedded in JWT
payload = {
    "sub": user_id,
    "permissions": ["users:read", "users:update", ...],
    ...
}

On every request, the token is decoded and permissions are checked without hitting the database:

def require_permission(permission: str):
    def dependency(current_user = Depends(get_current_active_user)):
        if not current_user.has_permission(permission):
            raise ForbiddenException(f"Missing permission: {permission}")
        return current_user
    return dependency

Protecting Routes

By permission

@router.delete("/{id}", dependencies=[Depends(require_permission("users:delete"))])
def delete_user(id: UUID, service = Depends(get_user_service)):
    return service.delete(id)

By role

@router.get("/admin/stats", dependencies=[Depends(require_role("admin"))])
def admin_stats():
    ...

Just authenticated

@router.get("/me")
def get_me(current_user = Depends(get_current_active_user)):
    return current_user

Default Roles

Role Description
admin Full access to all resources
manager Read/create/update users, assign roles, read roles and permissions
viewer Read-only access to all resources

Permission Reference

Permission Description
users:read View users
users:create Create users
users:update Update users
users:delete Delete users
users:assign_roles Assign roles to users
roles:read View roles
roles:create Create roles
roles:update Update roles
roles:delete Delete roles
permissions:read View permissions
permissions:create Create permissions
permissions:update Update permissions
permissions:delete Delete permissions

Adding New Permissions

When you add a new resource to the API:

1. Create the permission via API or seed

POST /api/v1/permissions
{
  "name": "reports:read",
  "description": "View reports"
}

2. Assign it to the appropriate roles

POST /api/v1/roles/{admin_role_id}/permissions
{
  "permission_ids": ["new-permission-uuid"]
}

3. Protect your route

@router.get("/reports", dependencies=[Depends(require_permission("reports:read"))])
def get_reports():
    ...

Info

After adding permissions to roles, users must log in again to receive updated tokens with the new permissions.