Roles
Roles are a coarse-grained mechanism for grouping users by their level of access within an application. In AllSafe Fast, roles are simple string values stored on the User model and embedded in JWT claims, making them fast to check without database lookups on every request.
What Are Roles?
A role is a string label assigned to a user that represents their category or level of access. Roles are stored as a list[str] on the User model:
# The User model stores roles as a list of strings
user.roles # ["admin", "user"]
# Roles are embedded in the JWT claims for fast access
# The UserPrincipal deserializes them from the token
principal.roles # ("admin", "user") — tuple on the principal
Because roles are embedded in the JWT, role checks happen entirely from the token — no database query is needed on each request. This makes role-based authorization extremely fast.
Roles are coarse-grained (admin, user, moderator). Permissions are fine-grained (users:read, billing:write). Use roles for broad access categories and permissions for specific capabilities. See the Permissions page for more details.
Role Assignment
Roles are assigned by storing them on the User model. When a user signs in, their roles are read from the database and embedded in the JWT claims. The UserPrincipal then carries these roles for the lifetime of the session:
from allsafe_fast import Auth, AuthConfig
auth = Auth(AuthConfig(secret_key="your-secret-key"))
# When creating a user, assign roles
user = await auth.create_user(
email="alice@example.com",
password="secure-password",
roles=["admin", "user"],
)
# When the user signs in, roles are included in the JWT
# The UserPrincipal deserializes them from the token
# No database lookup needed for role checks
Role Checking
AllSafe Fast provides two ways to check roles at the route level:
Single Role Check
Use the role parameter to require a single specific role:
from allsafe_fast import Auth, AuthConfig, UserPrincipal
from fastapi import FastAPI, Depends
app = FastAPI()
auth = Auth(AuthConfig(secret_key="your-secret-key"))
app.include_router(auth.router)
# Only users with the "admin" role can access this endpoint
@app.get("/api/admin/dashboard")
async def admin_dashboard(user: UserPrincipal = Depends(auth.user(role="admin"))):
return {"message": "Admin dashboard", "user": user.email}
# Only users with the "support_agent" role
@app.get("/api/support/tickets")
async def support_tickets(user: UserPrincipal = Depends(auth.user(role="support_agent"))):
return get_tickets()
Multiple Roles (Any-Of Check)
Use the roles parameter (a tuple) to require that the user has any of the specified roles. This uses any-of semantics — the user only needs one of the listed roles:
# User must have ANY of these roles: admin OR moderator
@app.get("/api/moderation/queue")
async def moderation_queue(
user: UserPrincipal = Depends(auth.user(roles=("admin", "moderator")))
):
return get_moderation_queue()
# User must have ANY of these roles: admin OR billing_manager
@app.get("/api/billing/overview")
async def billing_overview(
user: UserPrincipal = Depends(auth.user(roles=("admin", "billing_manager")))
):
return get_billing_overview()
The roles parameter uses any-of semantics — the user needs only one of the listed roles. This is different from the permissions parameter, which uses all-of semantics (the user must have every listed permission). This distinction is intentional: roles are typically mutually exclusive categories, while permissions are cumulative capabilities.
Checking Roles in Code
Beyond route-level guards, you can check roles programmatically using the UserPrincipal or AuthorizationService:
from allsafe_fast import UserPrincipal, AuthorizationService
# Using UserPrincipal.has_role() — returns bool, no exception
if user.has_role("admin"):
# Show admin controls
pass
# Using AuthorizationService.check_role() — raises AuthorizationError on failure
AuthorizationService.check_role(user, "admin")
# If we get here, the user has the admin role
# Using AuthorizationService.check_any_role() — any-of check
AuthorizationService.check_any_role(user, ("admin", "moderator"))
# If we get here, the user has at least one of the specified roles
Role Hierarchy
AllSafe Fast does not implement role hierarchy. There is no automatic inheritance between roles — having the moderator role does not grant user role permissions, and having admin does not automatically grant moderator.
Role hierarchy (where admin implies moderator implies user) is a conceptual pattern that AllSafe Fast does not implement. If you need hierarchical roles, you have two options:
1. Assign multiple roles explicitly (e.g., give an admin both admin and user roles).
2. Implement custom logic using UserPrincipal.has_role() to check for any role in a hierarchy you define.
# Conceptual role hierarchy — you implement this yourself
ROLE_HIERARCHY = {
"admin": ["moderator", "user"],
"moderator": ["user"],
"user": [],
}
def has_role_or_inherit(user: UserPrincipal, role: str) -> bool:
if user.has_role(role):
return True
for user_role in user.roles:
inherited = ROLE_HIERARCHY.get(user_role, [])
if role in inherited:
return True
return False
Custom Roles
Roles in AllSafe Fast are arbitrary strings. There is no predefined set of roles — you can use any string value that makes sense for your application. This gives you complete flexibility to model your authorization scheme:
| Example Role | Typical Use Case |
|---|---|
admin | Full system access, user management, configuration |
user | Standard authenticated user with basic access |
moderator | Content moderation, review queues, comment management |
support_agent | Customer support, ticket handling, user assistance |
billing_manager | Billing overview, invoice management, payment review |
content_editor | Content creation and editing, publishing workflows |
read_only | View-only access, no write permissions |
# You can use any string as a role — no registration required
user = await auth.create_user(
email="bob@example.com",
password="secure-password",
roles=["support_agent", "user"],
)
# Then protect routes with your custom roles
@app.get("/api/support/cases")
async def support_cases(
user: UserPrincipal = Depends(auth.user(role="support_agent"))
):
return get_open_cases()
Updating User Roles
When you update a user's roles in the database, the change takes effect on the user's next sign-in (when a new JWT is issued). Existing tokens will still carry the old roles until they expire or are refreshed:
# Update roles in the database
await auth.update_user(user_id, roles=["admin", "user", "moderator"])
# The change takes effect on next sign-in or token refresh
# Existing tokens still carry the old roles until expiry
# To take effect immediately, revoke the user's sessions
await auth.revoke_all_sessions(user_id)
Use snake_case for role names (e.g., support_agent, billing_manager). Keep role names lowercase and descriptive. Avoid spaces and special characters.
Roles in JWT Claims
When a JWT is issued, roles are included in the claims. This means the token is self-contained — the server can check roles without querying the database:
# JWT payload structure (simplified)
{
"sub": "550e8400-e29b-41d4-a716-446655440000", # user ID
"email": "alice@example.com",
"email_verified": True,
"roles": ["admin", "user"], # embedded in token
"permissions": ["users:read"], # embedded in token
"iss": "allsafe_fast",
"aud": "application",
"exp": 1700000000,
}