Skip to main content

FastPermit

CI Python License PyPI

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.

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, 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.

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.

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.

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 Vitalii Semotiuk.

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.1.tar.gz (23.1 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.1-py3-none-any.whl (14.3 kB view details)

Uploaded Python 3

File details

Details for the file fastpermit-0.1.1.tar.gz.

File metadata

  • Download URL: fastpermit-0.1.1.tar.gz
  • Upload date:
  • Size: 23.1 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.1.tar.gz
Algorithm Hash digest
SHA256 1eaecb60f6f0e23d8ac04cf4730949d46cd4b0c2faa194482f8d765c1c2b6945
MD5 759f7921d5e3439302e4aed81c188f37
BLAKE2b-256 0829b9685c3e6c28c98753278c70a704e57ffdb34a5cae2079e1341166aa4188

See more details on using hashes here.

Provenance

The following attestation bundles were made for fastpermit-0.1.1.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.1-py3-none-any.whl.

File metadata

  • Download URL: fastpermit-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 14.3 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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 40d6dd8cea47c17b42f4d7a5ffd401bc4a348e4d5661d200fc2c41e0881d6e85
MD5 0dc090565e1a4a239e50a796ea6ef211
BLAKE2b-256 8572a34b532cb270879d7182af0b0eafef7efbfaf8ae72bf1f4b90bfc222c6aa

See more details on using hashes here.

Provenance

The following attestation bundles were made for fastpermit-0.1.1-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

This release

0.1.1 This release

2 files

0.1.0

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