FastPermit
Composable, backend-agnostic authorization for FastAPI.
FastPermit keeps authentication and authorization separate. Your application authenticates a principal with JWT, OAuth2, Auth0, Keycloak, FastAPI Users, or a custom mechanism. FastPermit then decides whether that principal may perform an action.
The core is deliberately small:
- composable permissions with
&,|, and~; - request-level and object-level authorization;
- role-based access control (RBAC) primitives;
- pluggable permission backends;
- async-first execution;
- FastAPI dependency integration;
- no ORM or cache dependency in the core.
Status:
0.1.0— first public release.
Project status
FastPermit is in active early development. The 0.1.x line focuses on a small, typed,
backend-agnostic core before adding optional persistence and caching adapters.
Planned next steps:
- SQLAlchemy 2 / PostgreSQL adapter;
- Redis permission cache and invalidation hooks;
- tenant and resource scopes;
- audit and observability hooks.
Installation
Install from PyPI:
pip install fastpermit
For development:
git clone https://github.com/Vir2S/fastpermit.git
cd fastpermit
python -m venv .venv
source .venv/bin/activate
pip install -e '.[dev]'
make check
Quick start
from typing import Annotated
from fastapi import Depends, FastAPI
from fastpermit import BasicPrincipal, FastPermit, HasPermission, InMemoryBackend
app = FastAPI()
backend = InMemoryBackend(
{
"user-1": {"project:read", "project:update"},
}
)
async def get_current_principal() -> BasicPrincipal:
# Replace this with JWT, OAuth2, Auth0, Keycloak, or your own authentication.
return BasicPrincipal(id="user-1", roles=frozenset({"developer"}))
permit = FastPermit(
backend=backend,
principal_loader=get_current_principal,
)
@app.get("/projects")
async def list_projects(
principal: Annotated[
BasicPrincipal,
Depends(permit.require("project:read")),
],
) -> dict[str, str]:
return {"principal_id": str(principal.id)}
A string passed to require() is shorthand for HasPermission(...):
Depends(permit.require("project:read"))
is equivalent to:
Depends(permit.require(HasPermission("project:read")))
Permission algebra
Permissions can be combined without putting authorization branches in route handlers:
from fastpermit import HasPermission, HasRole, IsAuthenticated
permission = (
IsAuthenticated()
& HasPermission("project:update")
& (
HasRole("admin")
| HasPermission("project:update:any")
)
)
Supported operators:
A() & B() # AND
A() | B() # OR
~A() # NOT
The helpers all_of() and any_of() are available for larger expressions:
from fastpermit import all_of, any_of
permission = all_of(
IsAuthenticated(),
HasPermission("project:update"),
any_of(
HasRole("admin"),
HasRole("manager"),
),
)
Object-level permissions
Object rules are intentionally separate from loading the object. A custom rule only needs to
implement has_object_permission():
from typing import Any
from fastpermit import BasePermission, PermissionContext, Principal
class IsOwner(BasePermission):
async def has_object_permission(
self,
principal: Principal | None,
obj: Any,
context: PermissionContext,
) -> bool:
return principal is not None and obj.owner_id == principal.id
Then combine it with ordinary permissions:
edit_project = (
HasPermission("project:update:any")
| (
HasPermission("project:update")
& IsOwner()
)
)
Use it with a FastAPI loader dependency:
@app.patch("/projects/{project_id}")
async def update_project(
project=Depends(
permit.require_object(
edit_project,
loader=get_project,
)
),
):
return project
FastPermit evaluates both request-level and object-level branches as a single expression. Rules
that do not apply during a phase are neutral rather than implicitly allowing or denying it. This
keeps expressions such as HasRole("admin") | IsOwner() and ~IsOwner() logically correct.
Principals
FastPermit uses a small Principal protocol rather than a concrete user model. The included
BasicPrincipal is convenient for most applications:
from fastpermit import BasicPrincipal
principal = BasicPrincipal(
id="user-42",
roles=frozenset({"manager", "reviewer"}),
attributes={"organization_id": "org-1"},
)
You may return your own object from the authentication dependency as long as it exposes:
id
roles
attributes
is_authenticated
Backends
A backend answers one question: which permission codes are effective for this principal in this scope?
from collections.abc import Mapping
from typing import AbstractSet, Any
from fastpermit import PermissionBackend, Principal
class MyBackend(PermissionBackend):
async def get_permissions(
self,
principal: Principal,
*,
scope: Mapping[str, Any],
) -> AbstractSet[str]:
...
InMemoryBackend is included for tests, prototypes, and examples. SQLAlchemy/PostgreSQL and Redis
adapters are intentionally planned as optional integrations instead of core requirements.
Request context and scopes
A permission receives PermissionContext, which contains:
- the configured backend;
- a scope mapping;
- integration-specific attributes;
- a per-evaluation permission cache.
FastAPI integration exposes the current Request as context.attributes["request"] without
making the authorization core depend on FastAPI.
Static scope can be attached to a dependency:
Depends(
permit.require(
"billing:read",
scope={"tenant": "global"},
)
)
Dynamic tenant scopes are planned for the next integration iteration.
HTTP semantics
FastPermit does not authenticate requests. Authentication remains the responsibility of your principal loader.
When a FastPermit rule denies access:
- an absent or unauthenticated principal produces
401 Unauthorized; - an authenticated principal without sufficient authorization produces
403 Forbidden.
Design principles
- Authentication and authorization are separate concerns.
- Routes should describe required access, not implement role branches.
- Permission codes are stable capabilities such as
project:update. - Roles aggregate capabilities; application code should not be coupled to role names where a capability is the real requirement.
- Object-level rules belong in permissions, not route handlers.
- Storage and caching are adapters, not core concerns.
- Authorization expressions must preserve correct semantics across request and object phases.
Roadmap
0.1
- permission core;
-
AND,OR,NOTcomposition; -
all_of()/any_of(); -
IsAuthenticated; -
HasRole; -
HasPermission; - object-level permissions;
- backend protocol;
- in-memory backend;
- FastAPI integration;
- typed package;
- tests and CI.
0.2
- SQLAlchemy 2.x adapter;
- PostgreSQL RBAC reference models;
- Alembic examples;
- user-role and role-permission repositories.
0.3
- Redis cache adapter;
- cache invalidation primitives;
- cache versioning;
- configurable TTL policies.
0.4
- dynamic tenant scopes;
- attribute-based access control helpers;
- resource scopes;
- policy metadata.
0.5
- authorization audit events;
- observability hooks;
- OpenTelemetry integration.
License
MIT
Maintainer
Created and maintained by Vitaly Sem.
FastPermit is an independent open-source project developed with support from Born2CodeLab.
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 fastpermit-0.1.0.tar.gz.
File metadata
- Download URL: fastpermit-0.1.0.tar.gz
- Upload date:
- Size: 19.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a30b8d8cb135f44083d8b264d6254313f69c0f91ad7b47aceaa93b808f86d831
|
|
| MD5 |
8b51e0d01b511d0af441438b4c75a0dd
|
|
| BLAKE2b-256 |
7f8e66e9bf725e6a9becab74cef169fb4d929452cb62ffd5694a34bc2c8cc8fd
|
Provenance
The following attestation bundles were made for fastpermit-0.1.0.tar.gz:
Publisher:
release.yml on Vir2S/fastpermit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fastpermit-0.1.0.tar.gz -
Subject digest:
a30b8d8cb135f44083d8b264d6254313f69c0f91ad7b47aceaa93b808f86d831 - Sigstore transparency entry: 2617600234
- Sigstore integration time:
-
Permalink:
Vir2S/fastpermit@837a10d87d5dddc65cc0b92bb1a8e7cfe56e8304 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Vir2S
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@837a10d87d5dddc65cc0b92bb1a8e7cfe56e8304 -
Trigger Event:
release
-
Statement type:
File details
Details for the file fastpermit-0.1.0-py3-none-any.whl.
File metadata
- Download URL: fastpermit-0.1.0-py3-none-any.whl
- Upload date:
- Size: 13.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b7f82939bbb7b4510efd412887980d8a8f1d0077bc532fee1834d99efb44b082
|
|
| MD5 |
c0cecc276924f83b47360954df614f7d
|
|
| BLAKE2b-256 |
f83166c481185278e60ad7d325b1e6091cc9bffd821f26cd6784e93d484d9ef4
|
Provenance
The following attestation bundles were made for fastpermit-0.1.0-py3-none-any.whl:
Publisher:
release.yml on Vir2S/fastpermit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fastpermit-0.1.0-py3-none-any.whl -
Subject digest:
b7f82939bbb7b4510efd412887980d8a8f1d0077bc532fee1834d99efb44b082 - Sigstore transparency entry: 2617600240
- Sigstore integration time:
-
Permalink:
Vir2S/fastpermit@837a10d87d5dddc65cc0b92bb1a8e7cfe56e8304 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Vir2S
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@837a10d87d5dddc65cc0b92bb1a8e7cfe56e8304 -
Trigger Event:
release
-
Statement type: