Identity management for FastAPI.
Project description
fastapi-principal
English | 中文说明
fastapi-principal is a small authorization toolkit for FastAPI. It keeps the
core permission model of
flask-principal.
The request lifecycle, loader/saver hooks, and persistence APIs are adapted for
FastAPI rather than being a drop-in Flask-Principal port.
It does not authenticate users by itself. Bring your own Session, Cookie,
JWT, OAuth, API Key, or database lookup, then use fastapi-principal to
keep the current request identity in an async-safe context and check whether
that identity has the required permissions.
Highlights
flask-principalstyleNeed,Permission, andDenialobjects, with a FastAPI-friendlyIdentitymodel.- Async-safe request identity storage with
contextvars. FastAPImiddleware integration throughPrincipal(app).- Route protection with
Depends(permission.require(403)). - Sync and async identity loaders and savers.
- Permission composition with
|,&, and~. - Context manager and decorator support for fine-grained checks and migrations.
- Lightweight signal API compatible with
(sender, identity)handlers.
Install
pip install fastapi-principal
Quick Start
from fastapi import Depends, FastAPI, Request
from fastapi_principal import Identity, Permission, Principal, RoleNeed
from fastapi_principal import get_identity, identity_loaded
app = FastAPI()
principal = Principal(app)
admin = Permission(RoleNeed("admin"))
@principal.identity_loader
async def load_identity(request: Request):
user_id = request.headers.get("X-User-Id")
if user_id is None:
return None
return Identity(user_id, auth_type="header")
@identity_loaded.connect
def add_roles(sender, identity: Identity):
# Load roles, actions, and resource permissions from your own storage.
if identity.id == "alice":
identity.provides.add(RoleNeed("admin"))
@app.get("/admin", dependencies=[Depends(admin.require(403))])
async def admin_view():
return {"message": "Hello, admin"}
@app.get("/me")
async def me():
identity = get_identity()
return {"id": identity.id, "auth_type": identity.auth_type}
Request examples:
curl -i http://localhost:8000/admin
curl -i -H "X-User-Id: alice" http://localhost:8000/admin
Mental Model
The library has four moving parts:
Needis one capability, such asRoleNeed("admin").Identityis the current user or actor and owns a set of provided needs.Permissiondescribes the needs required by a resource.Principalloads one identity per request and stores it in a context variable.
The request lifecycle looks like this:
FastAPIreceives a request.Principalmiddleware calls registered identity loaders, newest first.- The first loader returning an
Identitywins. - If no loader returns an identity,
AnonymousIdentity()is used. identity_loadedis fired so the app can add roles and permissions.- Route dependencies or endpoint code call
permission.require(...). - The identity context is reset after the response is produced.
request.state.identity is also populated for code that prefers request-local
state over get_identity().
Needs
Needs are hashable named tuples. They can represent users, roles, actions, or resource-level permissions.
from fastapi_principal import ActionNeed, ItemNeed, RoleNeed, TypeNeed, UserNeed
UserNeed(42) # Need(method="id", value=42)
RoleNeed("admin") # Need(method="role", value="admin")
TypeNeed("service-account") # Need(method="type", value="service-account")
ActionNeed("publish") # Need(method="action", value="publish")
ItemNeed("edit", 7, "post") # ItemNeed(method="edit", value=7, type="post")
An authenticated Identity(id) automatically provides UserNeed(id).
AnonymousIdentity() provides no needs.
Permissions
Permission(*needs) grants access when at least one required need is present.
editor_or_admin = Permission(RoleNeed("editor"), RoleNeed("admin"))
Denial(*needs) grants access unless one of those needs is present.
from fastapi_principal import Denial
not_banned = Denial(RoleNeed("banned"))
Permissions also keep flask-principal style set operations:
admin = Permission(RoleNeed("admin"))
editor = Permission(RoleNeed("editor"))
not_banned = Denial(RoleNeed("banned"))
admin_or_editor = admin.union(editor)
admin_only = admin_or_editor - editor
required_banned_role = not_banned.reverse()
is_subset = admin in admin_or_editor
Composition
Use Python operators to build richer rules:
admin = Permission(RoleNeed("admin"))
editor = Permission(RoleNeed("editor"))
manager = Permission(RoleNeed("manager"))
banned = Permission(RoleNeed("banned"))
admin_or_editor = admin | editor
editor_manager = editor & manager
not_banned = ~banned
policy = admin | (editor & manager & ~banned)
Permission(a, b) means "a or b". Use Permission(a) & Permission(b) when both
needs are required.
FastAPI Usage
Protect a Route
@app.get("/admin", dependencies=[Depends(admin.require(403))])
async def admin_view():
return {"ok": True}
permission.require(status_code) returns an IdentityContext that FastAPI can
call directly. The explicit dependency property is also available:
Depends(admin.require(403).dependency)
If no status code is provided and access is denied, PermissionDenied is raised.
Use Inside an Endpoint
@app.post("/posts/{post_id}")
async def update_post(post_id: int):
permission = Permission(ItemNeed("edit", post_id, "post"))
with permission.require(403):
return {"updated": post_id}
Use as a Decorator
@admin.require(403)
async def rebuild_index():
return {"status": "queued"}
Both sync and async functions are supported.
Check Manually
identity = get_identity()
if identity.can(admin):
...
if admin.can():
...
admin.test(403)
Loading Identities
Register loaders with @principal.identity_loader. Loaders receive the current
Request and may be sync or async.
@principal.identity_loader
def load_from_header(request: Request):
user_id = request.headers.get("X-User-Id")
return Identity(user_id) if user_id else None
@principal.identity_loader
async def load_from_session(request: Request):
user_id = request.session.get("user_id")
return Identity(user_id, auth_type="session") if user_id else None
The newest loader runs first. The first non-None identity wins. Loader
exceptions are logged and the next loader is tried.
Enriching Identities
Use identity_loaded to add roles, actions, or item-level needs after an
identity is loaded.
@identity_loaded.connect
def add_needs(sender, identity: Identity):
if identity.id is None:
return
user = get_user_from_db(identity.id)
for role in user.roles:
identity.provides.add(RoleNeed(role.name))
For FastAPI-first code, a single-argument handler is also accepted:
@identity_loaded.connect
def add_default_need(identity: Identity):
identity.provides.add(RoleNeed("member"))
Sender filtering is supported:
@identity_loaded.connect(sender=app)
def add_app_needs(sender, identity: Identity):
...
Persisting Identity Changes
Use Principal.set_identity() when login/logout code must persist a new
identity for future requests.
@principal.identity_saver
async def save_identity(request: Request, identity: Identity):
request.session["user_id"] = identity.id
request.session["auth_type"] = identity.auth_type
@app.post("/login")
async def login(request: Request):
identity = Identity("alice", auth_type="password")
await principal.set_identity(request, identity)
return {"status": "ok"}
Identity savers are called newest first and may be sync or async.
For flask-principal style in-request changes, use identity_changed:
from fastapi_principal import identity_changed
identity_changed.send(app, identity=Identity("alice"))
identity_changed updates the active context identity and then notifies its own
receivers. It does not run identity savers because it does not receive the
request object.
App Factory Pattern
principal = Principal()
def create_app():
app = FastAPI()
principal.init_app(app)
return app
flask-principal Compatibility Notes
fastapi-principal follows Flask-Principal's authorization model, but its
FastAPI integration is intentionally different from Flask's session and signal
pipeline.
Need,UserNeed,RoleNeed,TypeNeed,ActionNeed, andItemNeedfollow the same tuple-based model.Identity(id)automatically providesUserNeed(id). In Flask-Principal, applications often add that need explicitly inidentity_loaded.Permission,Denial,union,difference,reverse, and subset checks followflask-principalstyle semantics.identity_loaded.connect(handler)supports(sender, identity).identity_changed.send(sender, identity=...)is available for in-request identity changes, but it does not run identity savers because it has noRequest.principal.identity_loaderreceivesrequest, andprincipal.identity_saverreceives(request, identity).FastAPI-specific persistence should useawait principal.set_identity(request, identity).- Flask-Principal's
use_sessions,skip_static, and full Blinker signal API are not provided.
API Reference
Needs
| Symbol | Description |
|---|---|
Need |
namedtuple("Need", ["method", "value"]). |
ItemNeed |
namedtuple("ItemNeed", ["method", "value", "type"]) for resource-level permissions. |
UserNeed(value) |
Shortcut for Need("id", value). |
RoleNeed(value) |
Shortcut for Need("role", value). |
TypeNeed(value) |
Shortcut for Need("type", value). |
ActionNeed(value) |
Shortcut for Need("action", value). |
Identities
| Symbol | Description |
|---|---|
Identity(id, auth_type=None) |
Active user or actor. Non-anonymous identities automatically provide UserNeed(id). |
AnonymousIdentity() |
Identity with id=None and no provided needs. |
identity.can(permission) |
Return whether the identity satisfies a permission. |
get_identity() |
Return the active request identity, or AnonymousIdentity() outside a request context. |
set_identity(identity, *, sender=None) |
Change the active context identity and fire identity_loaded. |
Permissions
| Symbol | Description |
|---|---|
BasePermission |
Base class for custom permission types. Supports OR, AND, and NOT composition. |
Permission(*needs) |
Grants when the identity provides at least one required need and no excluded need. |
Denial(*needs) |
Grants unless the identity provides one of the excluded needs. |
OrPermission |
Composite permission returned by OR composition. |
AndPermission |
Composite permission returned by p1 & p2. |
NotPermission |
Inverted permission returned by ~p. |
permission.require(http_exception=None) |
Return an IdentityContext usable as a dependency, decorator, or context manager. |
permission.test(http_exception=None) |
Raise immediately when the active identity does not satisfy the permission. |
PermissionDenied |
Raised when permission is denied and no HTTP status code is configured. |
IdentityContext |
Runtime permission check returned by permission.require(). |
Principal and Signals
| Symbol | Description |
|---|---|
Principal(app=None) |
FastAPI middleware and identity hook manager. Pass app or call init_app(app). |
principal.identity_loader(func) |
Register a sync or async identity loader. The most recently registered loader runs first. |
principal.identity_saver(func) |
Register a sync or async identity saver used by principal.set_identity(). |
principal.set_identity(request, identity) |
Change the active identity, update request.state.identity, and run savers. |
identity_loaded |
Signal fired after identity loading or context identity changes. |
identity_changed |
Flask-Principal style signal that sets the active identity, then notifies receivers. |
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 fastapi_principal-0.1.1.tar.gz.
File metadata
- Download URL: fastapi_principal-0.1.1.tar.gz
- Upload date:
- Size: 16.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c69f40de19418c45a91e3a0d61388a42f8710d0923624ca30e869741b716505a
|
|
| MD5 |
24c4691196b0c4ceab90a208e98eea50
|
|
| BLAKE2b-256 |
95efdba70bcad55b4f53ffa92ad65489a4ac43ebcc4087f58436e0da31537719
|
File details
Details for the file fastapi_principal-0.1.1-py3-none-any.whl.
File metadata
- Download URL: fastapi_principal-0.1.1-py3-none-any.whl
- Upload date:
- Size: 11.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8bbf8d77373bb36523f83f27ea40bfad6f58bbd0ecbbf6571b361e0185235791
|
|
| MD5 |
b2d14e369c789f9e41b363493fcce6e3
|
|
| BLAKE2b-256 |
3652bc5c19e7f56a22733cf8abe24e01c7352e33a4c556419ac819f6cd1f4121
|