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 vs. Permissions

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()
⚠ Any-Of vs. All-Of

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.

❗ Conceptual Only

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 RoleTypical Use Case
adminFull system access, user management, configuration
userStandard authenticated user with basic access
moderatorContent moderation, review queues, comment management
support_agentCustomer support, ticket handling, user assistance
billing_managerBilling overview, invoice management, payment review
content_editorContent creation and editing, publishing workflows
read_onlyView-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)
💡 Role Naming Convention

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,
}