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/
- Getting started
- Writing permissions
- Composition
- Object-level permissions
- FastAPI guide · Starlette guide · SQLAlchemy guide
- Settings · Testing
- Migrating from DRF
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
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 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
af0255ffbd0f7c228c5e51bbc25f77888b02c8763a5f57a27342299870264f81
|
|
| MD5 |
8f95d6472812ee7a969e24513b59ded6
|
|
| BLAKE2b-256 |
eeb12fe8eb1f930df8b999270487178f286d50a869fc374184104ca858932c81
|
Provenance
The following attestation bundles were made for starlette_permissions-0.1.0.tar.gz:
Publisher:
publish.yml on korolenkowork/starlette-permissions
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
starlette_permissions-0.1.0.tar.gz -
Subject digest:
af0255ffbd0f7c228c5e51bbc25f77888b02c8763a5f57a27342299870264f81 - Sigstore transparency entry: 2426901844
- Sigstore integration time:
-
Permalink:
korolenkowork/starlette-permissions@4ab96a28a82e87b42038617b8dd1742cdd312967 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/korolenkowork
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@4ab96a28a82e87b42038617b8dd1742cdd312967 -
Trigger Event:
push
-
Statement type:
File details
Details for the file starlette_permissions-0.1.0-py3-none-any.whl.
File metadata
- Download URL: starlette_permissions-0.1.0-py3-none-any.whl
- Upload date:
- Size: 41.3 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 |
3f4fb08df9f950f13c7967797ec91969b2fe4734303480186fa291ac9a10d922
|
|
| MD5 |
6cde3a31f0eb48b9c9479c62f37de090
|
|
| BLAKE2b-256 |
563c646741f738f9fadd220554f5884343302ef755e79ddee8a6401c38debb0b
|
Provenance
The following attestation bundles were made for starlette_permissions-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on korolenkowork/starlette-permissions
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
starlette_permissions-0.1.0-py3-none-any.whl -
Subject digest:
3f4fb08df9f950f13c7967797ec91969b2fe4734303480186fa291ac9a10d922 - Sigstore transparency entry: 2426902325
- Sigstore integration time:
-
Permalink:
korolenkowork/starlette-permissions@4ab96a28a82e87b42038617b8dd1742cdd312967 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/korolenkowork
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@4ab96a28a82e87b42038617b8dd1742cdd312967 -
Trigger Event:
push
-
Statement type: