Permissions
Permissions are a fine-grained authorization mechanism that lets you control access to specific capabilities within your application. Unlike roles, which represent broad categories of users, permissions represent individual actions a user can perform — such as reading user records, exporting reports, or managing billing.
What Are Permissions?
Permissions are arbitrary string values stored as a list[str] on the User model. Like roles, they are embedded in JWT claims so that permission checks can be performed from the token without database lookups:
# The User model stores permissions as a list of strings
user.permissions # ["users:read", "billing:read", "reports:export"]
# The UserPrincipal deserializes permissions from the JWT
principal.permissions # ("users:read", "billing:read", "reports:export")
AllSafe Fast recommends the resource:action naming convention for permissions (e.g., users:read, billing:write). This makes it clear what resource and action each permission governs. You are free to use any string format, but consistency helps maintainability.
Permission Assignment
Permissions are assigned by storing them on the User model. When a user signs in, their permissions are read from the database and embedded in the JWT claims:
from allsafe_fast import Auth, AuthConfig
auth = Auth(AuthConfig(secret_key="your-secret-key"))
# Assign permissions when creating a user
user = await auth.create_user(
email="alice@example.com",
password="secure-password",
roles=["user"],
permissions=["users:read", "reports:view", "billing:read"],
)
# Permissions are embedded in the JWT on sign-in
# No database lookup needed for permission checks
Permission Checks
AllSafe Fast provides two ways to check permissions at the route level:
Single Permission Check
Use the permission parameter to require a single specific permission:
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 "reports:export" permission
@app.post("/api/reports/export")
async def export_report(
user: UserPrincipal = Depends(auth.user(permission="reports:export"))
):
return generate_report()
# Only users with the "users:read" permission
@app.get("/api/users")
async def list_users(
user: UserPrincipal = Depends(auth.user(permission="users:read"))
):
return get_all_users()
Multiple Permissions (All-Of Check)
Use the permissions parameter (a tuple) to require that the user has all of the specified permissions. This uses all-of semantics — the user must have every listed permission:
# User must have ALL of these permissions: billing:read AND invoices:view
@app.get("/api/billing/invoices")
async def view_invoices(
user: UserPrincipal = Depends(auth.user(permissions=("billing:read", "invoices:view")))
):
return get_invoices()
# User must have ALL: users:read AND users:create AND users:delete
@app.delete("/api/users/{user_id}")
async def delete_user(
user_id: str,
user: UserPrincipal = Depends(auth.user(permissions=("users:read", "users:create", "users:delete")))
):
return remove_user(user_id)
All-Of vs. Any-Of Semantics
The permissions parameter uses all-of semantics, meaning the user must have every listed permission. This is different from the roles parameter, which uses any-of semantics:
| Parameter | Semantics | User Needs | Example |
|---|---|---|---|
permission | Single check | That one permission | permission="users:read" |
permissions | All-of | Every listed permission | permissions=("users:read", "users:write") |
role | Single check | That one role | role="admin" |
roles | Any-of | At least one listed role | roles=("admin", "moderator") |
Roles are typically mutually exclusive categories — a user is usually one or the other, so any-of makes sense. Permissions are cumulative capabilities — a user needs all listed capabilities to perform a complex action, so all-of is the safer default. If you need any-of permission checking, use multiple AuthorizationService.check_permission() calls or check UserPrincipal.has_permission() individually.
Any-Of Permission Check (Custom)
If you need any-of semantics for permissions, implement it manually:
from allsafe_fast import UserPrincipal, AuthorizationError
def check_any_permission(user: UserPrincipal, perms: tuple) -> bool:
return any(user.has_permission(p) for p in perms)
# Usage in a route handler
@app.get("/api/dashboard")
async def dashboard(user: UserPrincipal = Depends(auth.user())):
if check_any_permission(user, ("reports:view", "billing:read")):
# Show combined dashboard
pass
else:
raise AuthorizationError("Insufficient permissions")
Route-Level vs. Service-Level Authorization
Permissions can be enforced at two levels:
Route-Level (via auth.user)
Route-level authorization is enforced before the handler runs. It is the most common pattern and protects entire endpoints:
# Route-level: checked before handler runs
@app.post("/api/users")
async def create_user(
user: UserPrincipal = Depends(auth.user(permission="users:create"))
):
# If we get here, the user has the permission
return create_new_user()
Service-Level (via AuthorizationService)
Service-level authorization is performed inside your business logic, allowing conditional checks and more complex authorization flows:
from allsafe_fast import AuthorizationService, UserPrincipal
class UserService:
@staticmethod
async def delete_user(target_id: str, actor: UserPrincipal):
# Check permission in service layer
AuthorizationService.check_permission(actor, "users:delete")
# Additional business logic checks
if str(actor.id) == target_id:
raise ValidationError("Cannot delete your own account")
return await remove_user_from_db(target_id)
# In the route handler
@app.delete("/api/users/{user_id}")
async def delete_user_route(
user_id: str,
user: UserPrincipal = Depends(auth.user(permission="users:delete"))
):
return await UserService.delete_user(user_id, user)
For sensitive operations, use both route-level and service-level checks. The route-level check rejects unauthorized requests early, while the service-level check protects against logic errors and internal calls that bypass the route layer.
Common Permission Examples
Here are typical permission patterns you might use in your application:
| Permission | Resource | Action | Typical Use |
|---|---|---|---|
users:read | users | read | View user lists and profiles |
users:create | users | create | Create new user accounts |
users:update | users | update | Modify user information |
users:delete | users | delete | Remove user accounts |
billing:read | billing | read | View billing information |
billing:write | billing | write | Modify billing settings |
reports:view | reports | view | View reports and analytics |
reports:export | reports | export | Download report data |
invoices:view | invoices | view | View invoices |
invoices:create | invoices | create | Generate new invoices |
content:publish | content | publish | Publish content to production |
settings:manage | settings | manage | Change system configuration |
Combining Roles and Permissions
You can combine role and permission checks in a single auth.user() call. All conditions must be satisfied:
# User must have the "admin" role AND the "users:delete" permission
@app.delete("/api/users/{user_id}")
async def delete_user(
user_id: str,
user: UserPrincipal = Depends(
auth.user(role="admin", permission="users:delete")
)
):
return remove_user(user_id)
# User must have "admin" or "billing_manager" role AND all listed permissions
@app.post("/api/billing/refund")
async def issue_refund(
user: UserPrincipal = Depends(
auth.user(
roles=("admin", "billing_manager"),
permissions=("billing:read", "billing:write")
)
)
):
return process_refund()