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")
ℹ Permission Naming Convention

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:

ParameterSemanticsUser NeedsExample
permissionSingle checkThat one permissionpermission="users:read"
permissionsAll-ofEvery listed permissionpermissions=("users:read", "users:write")
roleSingle checkThat one rolerole="admin"
rolesAny-ofAt least one listed roleroles=("admin", "moderator")
⚠ Why the Difference?

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)
💡 Defense in Depth

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:

PermissionResourceActionTypical Use
users:readusersreadView user lists and profiles
users:createuserscreateCreate new user accounts
users:updateusersupdateModify user information
users:deleteusersdeleteRemove user accounts
billing:readbillingreadView billing information
billing:writebillingwriteModify billing settings
reports:viewreportsviewView reports and analytics
reports:exportreportsexportDownload report data
invoices:viewinvoicesviewView invoices
invoices:createinvoicescreateGenerate new invoices
content:publishcontentpublishPublish content to production
settings:managesettingsmanageChange 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()