Skip to main content

starlette-permissions

Django-style permission classes for Starlette and FastAPI.

If you have written Django REST Framework permissions, this will look familiar: declare a rule as a class, combine rules with &, | and ~, and attach them to a route. If you haven't, the whole idea is that authorization rules are objects — testable on their own, reusable across endpoints, and readable at the call site.

from starlette_permissions import IsAuthenticated, IsAdminUser, permission_required


@router.delete("/posts/{post_id}")
@permission_required(IsAuthenticated & IsAdminUser)
async def delete_post(request: Request, post_id: int): ...
  • Starlette-first. The only required dependency is starlette. FastAPI is an optional extra, and nothing in the core imports it.
  • Three ways in. A decorator, a FastAPI dependency, and a mixin for HTTPEndpoint — all driven by the same permission objects.
  • Composable. IsAuthenticated & (IsAdminUser | IsOwner("author_id")) means what it looks like it means.
  • Object-level rules. "You may edit your own posts" is a first-class case, not something you hand-roll in every handler.
  • Typed. Ships py.typed; the public API is fully annotated.

Install

pip install starlette-permissions

For FastAPI's requires(...) dependency:

pip install "starlette-permissions[fastapi]"

Supported versions

Minimum Newest tested
Python 3.10 3.14
Starlette 0.35 1.6.0
FastAPI (extra) 0.110 0.141.1

Both ends are pinned and exercised by CI, not merely declared — see compatibility. The package metadata deliberately sets no upper bound, so newer releases install freely; the ceiling records what has actually been tested. FastAPI below 0.110 pins Starlette under 0.28 and cannot be combined with a supported Starlette.

Quick start

Tell the library how to find your user, once, at startup:

from starlette_permissions import configure

configure(user_getter=lambda conn: getattr(conn.state, "user", None))

If you use Starlette's AuthenticationMiddleware, you can skip that — the default reads scope["user"] and request.state.user already.

FastAPI

The dependency form is the idiomatic one. It needs no Request parameter, it shows up in the dependency graph, and it can be attached to a whole router:

from fastapi import APIRouter
from starlette_permissions import IsAuthenticated, PermissionContext
from starlette_permissions.dependencies import requires

router = APIRouter()


@router.get("/me", dependencies=[requires(IsAuthenticated)])
async def get_me(): ...


# Or take the context as a value, and get the user for free:
@router.get("/profile")
async def profile(ctx: PermissionContext = requires(IsAuthenticated)):
    return ctx.user


# Or guard everything under one router:
admin = APIRouter(dependencies=[requires(IsAdminUser)])

Starlette

from starlette.applications import Starlette
from starlette.routing import Route
from starlette_permissions import IsAuthenticated, permission_required, install_exception_handlers


@permission_required(IsAuthenticated)
async def me(request):
    return JSONResponse({"user": request.state.user.name})


app = Starlette(routes=[Route("/me", me)])
install_exception_handlers(app)  # renders denials as JSON instead of plain text

Class-based endpoints get a mixin:

class PostEndpoint(PermissionMixin, HTTPEndpoint):
    permission_classes = {"*": IsAuthenticated, "DELETE": IsAdminUser}

Writing a permission

Subclass BasePermission and override has_permission. It receives a PermissionContext — the connection, the resolved user, roles, scopes, and the endpoint's own arguments — and returns a bool. Sync or async, your choice.

from starlette_permissions import BasePermission


class HasActiveSubscription(BasePermission):
    message = "An active subscription is required"

    async def has_permission(self, ctx):
        return await billing.is_active(ctx.user.id)

For one-off rules, a function is enough:

from starlette_permissions import permission


@permission(message="Requests must come from the office network")
def from_office(ctx):
    return ctx.connection.client.host.startswith("10.")

Object-level rules

Some rules can't be decided until the record is loaded. Those go in has_object_permission, and run through check_object_permissions:

from starlette_permissions import IsOwner, check_object_permissions
from starlette_permissions.dependencies import requires_object


@router.patch("/posts/{post_id}")
async def edit_post(post_id: int, check=requires_object(IsOwner("author_id"))):
    post = await posts.get(post_id)
    await check(post)  # raises 403 unless ctx.user owns it
    return await posts.update(post, ...)

Built-in permissions

Permission Allows when
AllowAny always
DenyAll never
IsAuthenticated a user is attached (401 otherwise)
IsAnonymous no user is attached
IsAdminUser user has is_admin / is_staff / is_superuser
IsAuthenticatedOrReadOnly always for GET/HEAD/OPTIONS, else authenticated
ReadOnly method is GET/HEAD/OPTIONS
IsMethod("POST", ...) method is listed
HasRole("admin") user has any of the given roles
HasAllRoles("a", "b") user has every given role
HasScope("posts:write") credentials carry any of the given scopes
HasAPIKey(key=...) request carries a matching API key
HasHeader("X-Tenant") header is present (and matches, if a value is given)
IsOwner("user_id") (object-level) the object belongs to the user
IsOwnerOrReadOnly() (object-level) anyone reads, only the owner writes

Combining

&, | and ~ work on classes and instances alike:

permission_required(IsAuthenticated & ~IsBanned)
permission_required(ReadOnly | IsAdminUser)
permission_required(All(IsAuthenticated, HasRole("editor"), ~IsSuspended))

Multiple permissions default to all must pass, as in Django and DRF:

permission_required(IsAuthenticated, HasRole("editor"))  # both
permission_required(IsAuthenticated, HasRole("editor"), mode="any")  # either

Documentation

Full docs: https://korolenkowork.github.io/starlette-permissions/

Contributing

poetry install --with dev
poetry run pytest -q && poetry run ruff check . && poetry run mypy

AGENTS.md documents the conventions and the handful of rules that are load-bearing rather than cosmetic — why dependencies are never capped, why dependencies.py cannot use from __future__ import annotations, and what the four CI environments each prove. Written for AI agents, useful for anyone.

Releases publish to PyPI only from a pushed tag matching v[0-9]+.[0-9]+.[0-9]+*; see the release steps in AGENTS.md.

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

starlette_permissions-0.1.0.tar.gz (50.3 kB view details)

Uploaded Source

Built Distribution

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

starlette_permissions-0.1.0-py3-none-any.whl (41.3 kB view details)

Uploaded Python 3

File details

Details for the file starlette_permissions-0.1.0.tar.gz.

File metadata

  • Download URL: starlette_permissions-0.1.0.tar.gz
  • Upload date:
  • Size: 50.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for starlette_permissions-0.1.0.tar.gz
Algorithm Hash digest
SHA256 af0255ffbd0f7c228c5e51bbc25f77888b02c8763a5f57a27342299870264f81
MD5 8f95d6472812ee7a969e24513b59ded6
BLAKE2b-256 eeb12fe8eb1f930df8b999270487178f286d50a869fc374184104ca858932c81

See more details on using hashes here.

Provenance

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

Publisher: publish.yml on korolenkowork/starlette-permissions

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

File details

Details for the file starlette_permissions-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for starlette_permissions-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3f4fb08df9f950f13c7967797ec91969b2fe4734303480186fa291ac9a10d922
MD5 6cde3a31f0eb48b9c9479c62f37de090
BLAKE2b-256 563c646741f738f9fadd220554f5884343302ef755e79ddee8a6401c38debb0b

See more details on using hashes here.

Provenance

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

Publisher: publish.yml on korolenkowork/starlette-permissions

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

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page