Authorization decorator factory for FastAPI based on Casbin and fastapi-decorators.
Decorators are applied directly to routes — no middleware, no extra parameters in your function signatures.
| Feature | casbin-fastapi-decorator | fastapi-authz / fastapi-casbin-auth |
|---|---|---|
| Approach | Decorator per route | Global middleware |
| Per-route permission config | ✅ | ❌ |
| Dynamic objects from request | ✅ AccessSubject |
❌ |
| No extra params in endpoint signature | ✅ | ❌ |
| Native FastAPI DI integration | ✅ | |
| JWT extras | ✅ | ❌ |
| DB-backed policies (SQLAlchemy async) | ✅ | ❌ |
| File policies with hot-reload | ✅ | ❌ |
| Casdoor OAuth2 integration | ✅ | ❌ |
Works with APIRouter |
✅ | ✅ |
Middleware-based authorization checks every incoming request globally. With a decorator, you configure permissions exactly where the route is defined — no hidden side effects, no boilerplate dependencies in every function signature.
pip install casbin-fastapi-decoratorOptional extras — install only what you need:
pip install "casbin-fastapi-decorator[file]" # File policies with hot-reload (recommended)
pip install "casbin-fastapi-decorator[jwt]" # JWT authentication
pip install "casbin-fastapi-decorator[db]" # Policies from DB (SQLAlchemy) with hot-reload
pip install "casbin-fastapi-decorator[casdoor]" # Casdoor OAuth2from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from casbin_fastapi_decorator import AccessSubject, PermissionGuard
from casbin_fastapi_decorator_file import CachedFileEnforcerProvider
# 1. Providers — regular FastAPI dependencies
async def get_current_user() -> dict:
return {"sub": "alice", "role": "admin"}
# CachedFileEnforcerProvider loads the enforcer once and hot-reloads
# automatically when model.conf or policy.csv changes on disk.
enforcer_provider = CachedFileEnforcerProvider(
model_path="model.conf",
policy_path="policy.csv",
)
# 2. Decorator factory
guard = PermissionGuard(
user_provider=get_current_user,
enforcer_provider=enforcer_provider,
error_factory=lambda user, *rv: HTTPException(403, "Forbidden"),
)
# 3. Wire lifespan to start/stop the file watcher
@asynccontextmanager
async def lifespan(app: FastAPI):
async with enforcer_provider:
yield
app = FastAPI(lifespan=lifespan)
# 4. Authentication only
@app.get("/me")
@guard.auth_required()
async def me():
return {"ok": True}
# 5. Static permission check
@app.get("/articles")
@guard.require_permission("articles", "read")
async def list_articles():
return []
# 6. Dynamic check — object resolved from request
async def get_article(article_id: int) -> dict:
return {"id": article_id, "owner": "alice"}
@app.get("/articles/{article_id}")
@guard.require_permission(
AccessSubject(val=get_article, selector=lambda a: a["owner"]),
"read",
)
async def read_article(article_id: int):
return {"article_id": article_id}Arguments of require_permission are passed to enforcer.enforce(user, *args) in the same order. AccessSubject is resolved via FastAPI DI, then transformed by the selector.
PermissionGuard(
user_provider=..., # FastAPI dependency that returns the current user
enforcer_provider=..., # FastAPI dependency that returns a casbin.Enforcer
error_factory=..., # callable(user, *rvals) -> Exception
)| Method | Description |
|---|---|
auth_required() |
Decorator: authentication only (user_provider must not raise) |
require_permission(*args) |
Decorator: permission check via enforcer.enforce(user, *args) |
AccessSubject(
val=get_item, # FastAPI dependency
selector=lambda item: item["name"], # transformation before enforce
)Wraps a dependency whose value is resolved from the request and passed to the enforcer. By default, selector is identity (lambda x: x).
casbin-fastapi-decorator-file — loads the Casbin enforcer once from model.conf + policy.csv and hot-reloads automatically when either file changes on disk (via watchdog).
pip install "casbin-fastapi-decorator[file]"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) -> AsyncGenerator[None, None]:
async with enforcer_provider: # starts watchdog
yield # stops watchdog on shutdown
guard = PermissionGuard(
user_provider=get_current_user,
enforcer_provider=enforcer_provider,
error_factory=lambda *_: HTTPException(403, "Forbidden"),
)Edit policy.csv while the app is running — the enforcer reloads on the next request with zero downtime. The same applies to model.conf changes.
Recommended for all file-based setups. Compared to a plain
async def get_enforcer()that returnscasbin.Enforcer(...), this provider avoids re-reading files on every request.
See packages/casbin-fastapi-decorator-file/README.md for full API and usage.
casbin-fastapi-decorator-jwt — extracts and validates a JWT from the Bearer header and/or a cookie.
pip install "casbin-fastapi-decorator[jwt]"See packages/casbin-fastapi-decorator-jwt/README.md for full API and usage.
casbin-fastapi-decorator-db — loads Casbin policies from a SQLAlchemy async session with caching and hot-reload.
pip install "casbin-fastapi-decorator[db]"The enforcer is cached and reloaded automatically when:
model.confchanges on disk (watchdog)- DB policy rows change — detected by SHA-256 hash, polled every
poll_intervalseconds (default 30 s)
from casbin_fastapi_decorator_db import DatabaseEnforcerProvider
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),
poll_interval=30.0, # seconds between DB hash checks
)
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
async with enforcer_provider: # starts watchdog + polling task
yieldSee packages/casbin-fastapi-decorator-db/README.md for full API and usage.
casbin-fastapi-decorator-casdoor — Casdoor OAuth2 authentication and remote Casbin policy enforcement.
pip install "casbin-fastapi-decorator[casdoor]"from casbin_fastapi_decorator_casdoor import CasdoorEnforceTarget, CasdoorIntegration
casdoor = CasdoorIntegration(
endpoint="http://localhost:8000",
client_id="...", client_secret="...", certificate=cert,
org_name="my_org", application_name="my_app",
target=CasdoorEnforceTarget(
enforce_id=lambda parsed: f"{parsed['owner']}/my_enforcer",
),
)
app.include_router(casdoor.router) # GET /callback, POST /logout
guard = casdoor.create_guard()CasdoorEnforceTarget selects the Casdoor enforce mode — by enforcer, permission, model, resource, or owner. Values can be static strings or callables resolved from the JWT payload at request time.
See packages/casbin-fastapi-decorator-casdoor/README.md for full API, compose pattern, and usage.
| Example | Description |
|---|---|
examples/core |
Bearer token auth, plain file-based policies |
examples/core-file |
Bearer token auth, file policies with hot-reload via CachedFileEnforcerProvider |
examples/core-jwt |
JWT auth via JWTUserProvider, file-based policies |
examples/core-db |
Bearer token auth, DB policies with hot-reload via DatabaseEnforcerProvider |
examples/core-casdoor |
Casdoor OAuth2 auth + remote enforcement, facade and compose patterns |
Requires Python 3.10+, uv, task.
task install # uv sync --all-groups + install all packages
task lint # ruff + ty + bandit for all packages
task tests # all tests (core + jwt + db + casdoor + file)Individual package tasks:
task core:lint task core:test
task jwt:lint task jwt:test
task db:lint task db:test # requires Docker (testcontainers)
task casdoor:lint task casdoor:test
task file:lint task file:testMIT