Skip to main content

FastPermit

CI Python License

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

  1. Authentication and authorization are separate concerns.
  2. Routes should describe required access, not implement role branches.
  3. Permission codes are stable capabilities such as project:update.
  4. Roles aggregate capabilities; application code should not be coupled to role names where a capability is the real requirement.
  5. Object-level rules belong in permissions, not route handlers.
  6. Storage and caching are adapters, not core concerns.
  7. Authorization expressions must preserve correct semantics across request and object phases.

Roadmap

0.1

  • permission core;
  • AND, OR, NOT composition;
  • 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

fastpermit-0.1.0.tar.gz (19.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fastpermit-0.1.0-py3-none-any.whl (13.7 kB view details)

Uploaded Python 3

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

Hashes for fastpermit-0.1.0.tar.gz
Algorithm Hash digest
SHA256 a30b8d8cb135f44083d8b264d6254313f69c0f91ad7b47aceaa93b808f86d831
MD5 8b51e0d01b511d0af441438b4c75a0dd
BLAKE2b-256 7f8e66e9bf725e6a9becab74cef169fb4d929452cb62ffd5694a34bc2c8cc8fd

See more details on using hashes here.

Provenance

The following attestation bundles were made for fastpermit-0.1.0.tar.gz:

Publisher: release.yml on Vir2S/fastpermit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for fastpermit-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b7f82939bbb7b4510efd412887980d8a8f1d0077bc532fee1834d99efb44b082
MD5 c0cecc276924f83b47360954df614f7d
BLAKE2b-256 f83166c481185278e60ad7d325b1e6091cc9bffd821f26cd6784e93d484d9ef4

See more details on using hashes here.

Provenance

The following attestation bundles were made for fastpermit-0.1.0-py3-none-any.whl:

Publisher: release.yml on Vir2S/fastpermit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.1.1

2 files

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page