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.1— authorization hardening release.
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.
Install from PyPI:
pip install fastpermitFor development:
git clone https://github.com/Vir2S/fastpermit.git
cd fastpermit
python -m venv .venv
source .venv/bin/activate
pip install -e '.[dev]'
make checkfrom 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")))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() # NOTThe 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 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.idThen 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 projectFastPermit 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.
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
A backend answers one question: which permission codes are effective for this principal in this scope?
from collections.abc import Mapping, Set
from typing import Any
from fastpermit import PermissionBackend, Principal
class MyBackend(PermissionBackend):
async def get_permissions(
self,
principal: Principal,
*,
scope: Mapping[str, Any],
) -> Set[str]:
...InMemoryBackend is included for tests, prototypes, and examples. SQLAlchemy/PostgreSQL and Redis
adapters are intentionally planned as optional integrations instead of core requirements.
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.
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.
A custom exception factory can override the integration response, including masking object-level
denials as 404 Not Found:
from fastapi import HTTPException
def access_exception(principal, permission, phase):
if phase == "object":
return HTTPException(status_code=404, detail="Not found.")
return HTTPException(status_code=403, detail=permission.message)
permit = FastPermit(
backend=backend,
principal_loader=get_current_principal,
exception_factory=access_exception,
)PermissionEvaluator.check() and check_object() are strict: only an explicit True decision
allows access. A neutral None decision remains available through decision() and
object_decision() for composition and pre-check workflows.
- 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.
- 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.
- SQLAlchemy 2.x adapter;
- PostgreSQL RBAC reference models;
- Alembic examples;
- user-role and role-permission repositories.
- Redis cache adapter;
- cache invalidation primitives;
- cache versioning;
- configurable TTL policies.
- dynamic tenant scopes;
- attribute-based access control helpers;
- resource scopes;
- policy metadata.
- authorization audit events;
- observability hooks;
- OpenTelemetry integration.
MIT
Created and maintained by Vitalii Semotiuk.
FastPermit is an independent open-source project developed with support from Born2CodeLab.