Skip to main content

Basic Example

A minimal example with manual Bearer authentication and file-based policies via CachedFileEnforcerProvider.

Project structure

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

Casbin files

casbin/model.conf

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = r.obj == p.obj && r.sub.role == p.sub && r.act == p.act

casbin/policy.csv

p, viewer, post, read
p, editor, post, read
p, editor, post, write
p, admin, post, read
p, admin, post, write
p, admin, post, delete

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"

class Resource(StrEnum):
POST = "post"

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

auth.py

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

HEADER_AUTH_SCHEME = HTTPBearer(auto_error=False)

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

In this example the token is simply the role string (viewer, editor, admin). In a real app you would validate a proper JWT here.

authz.py

from contextlib import asynccontextmanager
from collections.abc import AsyncIterator

from auth import get_current_user
from fastapi import FastAPI, HTTPException

from casbin_fastapi_decorator import PermissionGuard
from casbin_fastapi_decorator_file import CachedFileEnforcerProvider

enforcer_provider = CachedFileEnforcerProvider(
model_path="casbin/model.conf",
policy_path="casbin/policy.csv",
)


@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
async with enforcer_provider:
yield

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

main.py

from typing import Annotated, Any
from auth import get_current_user
from authz import guard, lifespan
from fastapi import Depends, FastAPI, Form, HTTPException
from model import Permission, PostCreateSchema, PostSchema, Resource, Role, UserSchema

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

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


def draft_not_found_error(user: Any, *resolved_args: Any) -> HTTPException:
"""Custom error for draft access — return 404 instead of 403."""
return HTTPException(status_code=404, detail="Article not found")


@app.post("/login")
async def login(role: Role) -> str:
"""Return the role as a plain-text token."""
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.get("/articles/draft")
@guard.require_permission(
Resource.POST,
Permission.WRITE,
error_factory=draft_not_found_error,
)
async def read_draft() -> dict[str, str]:
"""Requires write permission. Returns 404 on denial (route-level error_factory)."""
return {"title": "Draft Post"}

@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

Running

pip install "casbin-fastapi-decorator[file]" uvicorn python-multipart
uvicorn main:app --reload

Testing

# Login as admin (has write permission)
TOKEN=$(curl -s -X POST "http://localhost:8000/login?role=admin")
# → "admin"

# List posts (allowed for admin)
curl -H "Authorization: Bearer $TOKEN" http://localhost:8000/articles
# → [{"id": 1, "title": "First Post"}, ...]

# Access draft (allowed for admin, custom error_factory)
curl -H "Authorization: Bearer $TOKEN" http://localhost:8000/articles/draft
# → {"title": "Draft Post"}

# Create post (allowed for admin)
curl -X POST -H "Authorization: Bearer $TOKEN" \
-F "title=New Post" http://localhost:8000/articles
# → {"id": 3, "title": "New Post"}

# Login as viewer (has read-only permission)
TOKEN_VIEW=$(curl -s -X POST "http://localhost:8000/login?role=viewer")

# List posts (allowed for viewer)
curl -H "Authorization: Bearer $TOKEN_VIEW" http://localhost:8000/articles
# → [{"id": 1, "title": "First Post"}, ...]

# Access draft (denied for viewer — returns 404 due to custom error_factory)
curl -H "Authorization: Bearer $TOKEN_VIEW" http://localhost:8000/articles/draft
# → {"detail": "Article not found"} (404 status)

# Create post (denied for viewer — returns default 403)
curl -X POST -H "Authorization: Bearer $TOKEN_VIEW" \
-F "title=New Post" http://localhost:8000/articles
# → {"detail": "Forbidden"} (403 status)