Real-World Usage Patterns¶
This page shows common patterns for using fastapi-has-permissions in production applications.
Pattern 1: Define a Permission Module¶
Create a permissions/ package in your application with reusable permission classes:
app/
permissions/
__init__.py
common.py
articles.py
api/
endpoints/
articles.py
auth/
dependencies.py
permissions/common.py¶
Define base permissions tied to your authentication system:
from dataclasses import field
from fastapi.exceptions import RequestValidationError
from fastapi_has_permissions import LazyPermission, Permission, PermissionWrapper
from fastapi_has_permissions.types import Exceptions
from app.auth.dependencies import get_current_user, User
# Type alias for your auth dependency
from typing import Annotated
from fastapi import Depends
CurrentUserDep = Annotated[User, Depends(get_current_user)]
class IsAuthenticated(Permission):
async def check_permissions(self, user: CurrentUserDep) -> bool:
return user is not None
class IsAdmin(Permission):
async def check_permissions(self, user: CurrentUserDep) -> bool:
return user is not None and user.role == "admin"
class IsEditor(Permission):
async def check_permissions(self, user: CurrentUserDep) -> bool:
return user is not None and user.role in ("admin", "editor")
class HasServiceToken(Permission):
async def check_permissions(self, request: Request) -> bool:
token = request.headers.get("x-service-token")
return token == settings.SERVICE_TOKEN
# Pre-composed permission: admin OR service token
class IsPrivilegedUser(PermissionWrapper):
permission: Permission = IsAdmin() | HasServiceToken()
# Base class for lazy permissions that skip on validation errors
class GracefulLazyPermission(LazyPermission):
skip_on_exc: Exceptions = field(default=(RequestValidationError,), kw_only=True)
permissions/articles.py¶
Define resource-specific permissions:
from typing import Annotated, Any
from uuid import UUID
from fastapi import Depends, Path
from fastapi_has_permissions import LazyPermission
from fastapi_has_permissions.types import Dep
from app.db import AsyncSessionDep
from app.models import Article
from .common import CurrentUserDep, GracefulLazyPermission
async def get_current_article(
db: AsyncSessionDep,
article_id: Annotated[UUID, Path()],
) -> Article:
return await db.get(Article, article_id)
ArticleDep = Annotated[Article, Depends(get_current_article)]
class IsArticleAuthor(GracefulLazyPermission, LazyPermission):
async def check_permissions(self, user: CurrentUserDep, article: ArticleDep) -> bool:
return article.created_by == user.email
class BelongsToSameWorkspace(GracefulLazyPermission):
resource_dep: Dep
async def check_permissions(self, resource: Any, /, user: CurrentUserDep) -> bool:
return resource.workspace_id == user.workspace_id
Pattern 2: Router-Level Permissions¶
Apply permissions at the router level so all endpoints under it are protected:
from fastapi import APIRouter, Depends
from app.permissions.common import IsAuthenticated, IsPrivilegedUser
# All endpoints require authentication
router = APIRouter(
prefix="/api/v1/users",
dependencies=[Depends(IsAuthenticated())],
)
# All endpoints require admin or service token
admin_router = APIRouter(
prefix="/api/v1/admin",
dependencies=[Depends(IsPrivilegedUser())],
)
Pattern 3: Two-Tier Router¶
Use a main router with basic auth and a sub-router with stricter permissions:
from fastapi import APIRouter, Depends
from app.permissions.common import IsAuthenticated, IsPrivilegedUser
# Public-ish endpoints (just need auth)
router = APIRouter(
prefix="/articles",
dependencies=[Depends(IsAuthenticated())],
)
@router.get("")
async def list_articles():
...
@router.get("/{article_id}")
async def get_article(article_id: UUID):
...
# Admin-only endpoints on the same prefix
admin_router = APIRouter(
prefix="",
dependencies=[Depends(IsPrivilegedUser())],
)
@admin_router.delete("/{article_id}")
async def delete_article(article_id: UUID):
...
@admin_router.post("/{article_id}/publish")
async def publish_article(article_id: UUID):
...
# Nest admin under main router
router.include_router(admin_router)
Pattern 4: Complex Composed Permissions¶
Use boolean operators and lazy permissions for resource-level access control:
from fastapi import APIRouter, Depends
from app.permissions.common import IsPrivilegedUser, IsEditor
from app.permissions.articles import IsArticleAuthor, BelongsToSameWorkspace, ArticleDep
router = APIRouter(
prefix="/articles",
dependencies=[
Depends(
IsPrivilegedUser()
| IsArticleAuthor()
| (IsEditor() & BelongsToSameWorkspace(ArticleDep))
),
],
)
This means access is granted if:
IsPrivilegedUser()-- user is an admin or has a service token, ORIsArticleAuthor()-- user is the author of the article, ORIsEditor() & BelongsToSameWorkspace(ArticleDep)-- user is an editor and the article belongs to their workspace
On list endpoints (GET /articles), the lazy permissions (IsArticleAuthor, BelongsToSameWorkspace) are
skipped because the article_id path parameter doesn't exist. Only IsPrivilegedUser() and IsEditor()
are evaluated.
Pattern 5: Endpoint-Level Overrides¶
Add extra permissions to specific endpoints beyond what the router requires:
from fastapi import APIRouter, Depends, status
from app.permissions.common import IsAuthenticated, HasServiceToken
router = APIRouter(
prefix="/billing",
dependencies=[Depends(IsAuthenticated())],
)
@router.get("/invoices")
async def list_invoices():
...
@router.post(
"/usage",
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(HasServiceToken())], # extra: must also have a service token
)
async def report_usage():
...
Summary¶
| Pattern | When to Use |
|---|---|
| Permission module | Always -- organizes permissions in one place |
| Router-level | Most endpoints share the same access rule |
| Two-tier router | Mix of public and admin endpoints on the same prefix |
| Complex composition | Resource ownership + workspace membership checks |
| Endpoint-level | Specific endpoint needs extra restrictions |