Decorator-based authorization for FastAPI with Casbin, without middleware
Project description
casbin-fastapi-decorator
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.
Why decorator, not middleware?
| 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 | ✅ | ⚠️ partial |
| 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.
Installation
pip install casbin-fastapi-decorator
Optional 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 OAuth2
Quick start
from 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.
API
PermissionGuard
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, error_factory=None) |
Decorator: permission check via enforcer.enforce(user, *args). Optional error_factory overrides the guard-level factory for this route only. |
AccessSubject
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).
Per-route error responses
Override the guard-level error_factory on specific routes to customize error handling:
def article_not_found_error(user, *resolved_args) -> HTTPException:
"""Return 404 instead of 403 for denied access."""
return HTTPException(status_code=404, detail="Article not found")
@app.get("/articles/draft")
@guard.require_permission(
"article", "write",
error_factory=article_not_found_error,
)
async def read_draft():
return {"title": "Draft Article"}
When a user without write permission accesses this route, they'll receive a 404 Not Found instead of the default 403 Forbidden, effectively hiding the resource's existence.
File provider
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.
JWT provider
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.
DB provider
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
yield
See packages/casbin-fastapi-decorator-db/README.md for full API and usage.
Casdoor provider
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.
Examples
| 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 |
Development
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:test
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file casbin_fastapi_decorator-1.2.0.tar.gz.
File metadata
- Download URL: casbin_fastapi_decorator-1.2.0.tar.gz
- Upload date:
- Size: 7.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
145dff95e27ca635b58a8a1588e4f5e5f6f45830848a3a4a1b8f0411de231449
|
|
| MD5 |
15168694982d53334c51c0afd2acdce2
|
|
| BLAKE2b-256 |
1e5a5a027809352de978e2413ee93e99ab6da5bc73bd4f5412bc134f6fd2c816
|
File details
Details for the file casbin_fastapi_decorator-1.2.0-py3-none-any.whl.
File metadata
- Download URL: casbin_fastapi_decorator-1.2.0-py3-none-any.whl
- Upload date:
- Size: 9.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e24ef63192ef45f4017260e47ea3ee741bbf1c251525242213b2059b9c655a1e
|
|
| MD5 |
51732aa9decac5238a4ab37195a116da
|
|
| BLAKE2b-256 |
e3de550944baa9962a518c54d4bfea11f50667ac1fbdd9429e6be039fb004886
|