Skip to main content

Quick Start

A complete working example in under 5 minutes.

1. Install

pip install "casbin-fastapi-decorator[file]" uvicorn

2. Create the Casbin model

Create 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

3. Create the policy file

Create 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

4. Create the app

from contextlib import asynccontextmanager
from typing import Annotated

from fastapi import Depends, FastAPI, HTTPException, Security
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from pydantic import BaseModel

from casbin_fastapi_decorator import PermissionGuard
from casbin_fastapi_decorator_file import CachedFileEnforcerProvider

bearer = HTTPBearer(auto_error=False)


# --- User model ---

class User(BaseModel):
role: str


# --- Providers ---

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


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


@asynccontextmanager
async def lifespan(_app: FastAPI):
async with enforcer_provider:
yield


# --- Guard ---

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

app = FastAPI(lifespan=lifespan)


# --- Routes ---

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


@app.get("/posts")
@guard.require_permission("post", "read")
async def list_posts() -> list[dict]:
return [{"id": 1, "title": "Hello World"}]


@app.post("/posts")
@guard.require_permission("post", "write")
async def create_post() -> dict:
return {"id": 2, "title": "New Post"}

5. Test it

uvicorn main:app --reload

Send a request as viewer (can read but not write):

# This works
curl -H "Authorization: Bearer viewer" http://localhost:8000/posts

# This returns 403
curl -X POST -H "Authorization: Bearer viewer" http://localhost:8000/posts

Send as editor (can read and write):

# Both work
curl -H "Authorization: Bearer editor" http://localhost:8000/posts
curl -X POST -H "Authorization: Bearer editor" http://localhost:8000/posts

What's happening

  1. The request arrives at GET /posts
  2. guard.require_permission("post", "read") intercepts it
  3. get_current_user resolves the user from the Bearer token — returns User(role="viewer")
  4. CachedFileEnforcerProvider returns the cached enforcer loaded from model.conf and policy.csv
  5. The matcher checks user.role == p.sub, so enforce(user, "post", "read") returns True
  6. The route handler runs and returns the posts

If you edit casbin/policy.csv while the app is running, the next request will pick up the change automatically.

Next steps

  • PermissionGuard — learn all configuration options
  • AccessSubject — dynamic permissions based on request data
  • File extra — cached file-based enforcer with hot-reload
  • JWT extra — replace manual token parsing with JWTUserProvider