Skip to main content

Database Example

A complete example using casbin-fastapi-decorator with the db extra. Policies are stored in a SQLite database and can be queried at runtime.

Install

pip install "casbin-fastapi-decorator[db]" uvicorn aiosqlite

Project structure

my-app/
├── casbin/
│ └── model.conf
├── auth.py
├── authz.py
├── db.py
├── model.py
└── main.py

No policy.csv needed — policies live in the database.

casbin/model.conf

Same as the basic example.

model.py

from enum import StrEnum
from pydantic import BaseModel

class UserSchema(BaseModel):
role: str

class PostCreateSchema(BaseModel):
title: str

class PostSchema(PostCreateSchema):
id: int

class Permission(StrEnum):
READ = "read"
WRITE = "write"
DELETE = "delete"

class Resource(StrEnum):
POST = "post"

class Role(StrEnum):
ADMIN = "admin"
EDITOR = "editor"
VIEWER = "viewer"

db.py

from collections.abc import AsyncIterator
from contextlib import asynccontextmanager

from fastapi import FastAPI
from model import Permission, Resource, Role
from sqlalchemy import String, select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

engine = create_async_engine("sqlite+aiosqlite:///./example.db")
async_session = async_sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)

class Base(DeclarativeBase):
pass

class Policy(Base):
__tablename__ = "policies"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
sub: Mapped[str] = mapped_column(String(100))
obj: Mapped[str] = mapped_column(String(100))
act: Mapped[str] = mapped_column(String(100))

async def seed_policies() -> None:
async with async_session() as session:
result = await session.execute(select(Policy))
if result.scalars().first() is not None:
return
session.add_all([
Policy(sub=Role.ADMIN, obj=Resource.POST, act=Permission.READ),
Policy(sub=Role.ADMIN, obj=Resource.POST, act=Permission.WRITE),
Policy(sub=Role.ADMIN, obj=Resource.POST, act=Permission.DELETE),
Policy(sub=Role.EDITOR, obj=Resource.POST, act=Permission.READ),
Policy(sub=Role.EDITOR, obj=Resource.POST, act=Permission.WRITE),
Policy(sub=Role.VIEWER, obj=Resource.POST, act=Permission.READ),
])
await session.commit()

@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
await seed_policies()
yield
await engine.dispose()

auth.py

from typing import Annotated
from fastapi import HTTPException, Security
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from model import UserSchema

async def get_current_user(
header_auth: Annotated[
HTTPAuthorizationCredentials | None,
Security(HTTPBearer(auto_error=False))
],
) -> UserSchema:
if not header_auth:
raise HTTPException(401, "Unauthorized")
return UserSchema(role=header_auth.credentials)

authz.py

from auth import get_current_user
from casbin_fastapi_decorator_db import DatabaseEnforcerProvider
from db import Policy, async_session
from fastapi import HTTPException

from casbin_fastapi_decorator import PermissionGuard

enforcer_provider = DatabaseEnforcerProvider(
model_path="casbin/model.conf",
session_factory=async_session,
policy_model=Policy,
policy_mapper=lambda p: (p.sub, p.obj, p.act),
)

guard = PermissionGuard(
user_provider=get_current_user,
enforcer_provider=enforcer_provider,
error_factory=lambda *_: HTTPException(403, "Forbidden"),
)

main.py

from typing import Annotated
from auth import get_current_user
from authz import guard
from db import Policy, async_session, lifespan
from fastapi import Depends, FastAPI, Form
from model import Permission, PostCreateSchema, PostSchema, Resource, Role, UserSchema
from sqlalchemy import select

app = FastAPI(title="Core + DB Example", lifespan=lifespan)

MOCK_DB = [
PostSchema(id=1, title="First Post"),
PostSchema(id=2, title="Second Post"),
]

@app.post("/login")
async def login(role: Role) -> str:
return role

@app.get("/me")
@guard.auth_required()
async def me(user: Annotated[UserSchema, Depends(get_current_user)]) -> UserSchema:
return user

@app.get("/articles")
@guard.require_permission(Resource.POST, Permission.READ)
async def list_posts() -> list[PostSchema]:
return MOCK_DB

@app.post("/articles")
@guard.require_permission(Resource.POST, Permission.WRITE)
async def create_post(data: Annotated[PostCreateSchema, Form]) -> PostSchema:
pk = sorted(MOCK_DB, key=lambda p: p.id)[-1].id + 1
post = PostSchema(id=pk, title=data.title)
MOCK_DB.append(post)
return post

@app.delete("/articles/{post_id}")
@guard.require_permission(Resource.POST, Permission.DELETE)
async def delete_post(post_id: int) -> dict:
return {"id": post_id, "deleted": True}

@app.get("/policies")
async def list_policies() -> list[dict]:
"""Inspect current policies from the database."""
async with async_session() as session:
result = await session.execute(select(Policy))
policies = result.scalars().all()
return [{"sub": p.sub, "obj": p.obj, "act": p.act} for p in policies]

Running

uvicorn main:app --reload

Testing

# View current policies
curl http://localhost:8000/policies

# Login as admin
curl -H "Authorization: Bearer admin" http://localhost:8000/articles

# Delete (only admin has this permission)
curl -X DELETE -H "Authorization: Bearer admin" http://localhost:8000/articles/1

# Viewer cannot delete
curl -X DELETE -H "Authorization: Bearer viewer" http://localhost:8000/articles/1
# → {"detail": "Forbidden"}