Skip to content

API reference

Core

starlette_permissions.base.BasePermission

Subclass this and override :meth:has_permission.

class IsBetaTester(BasePermission):
    message = "Beta programme members only"

    def has_permission(self, ctx):
        return "beta" in ctx.roles

Either method may be def or async def. Instances are shared across requests, so keep them immutable — all per-request state lives on the context.

has_permission

has_permission(
    ctx: PermissionContext,
) -> bool | Awaitable[bool]

Decide based on the request alone. Defaults to allowing everything.

has_object_permission

has_object_permission(
    ctx: PermissionContext, obj: Any
) -> bool | Awaitable[bool]

Decide based on a specific object, once you have loaded it.

Only consulted by :func:~starlette_permissions.checks.check_object_permissions and the @object_permission_required decorator — a rule that needs the object cannot run before the handler has fetched it.

denial

denial(
    ctx: PermissionContext | None = None,
) -> PermissionDenied

Build the exception describing this refusal.

Override for rules that need to vary the response — adding a Retry-After on a throttle, say.

starlette_permissions.context.PermissionContext

Everything a permission needs to decide, with user lookup cached.

Attributes:

Name Type Description
connection

The Request or WebSocket being guarded.

endpoint

The view function, when the check runs on one.

view_kwargs Mapping[str, Any]

The arguments the endpoint was called with. Useful for object-level rules that key off a path parameter.

request property

request: HTTPConnection

Alias for :attr:connection, for readability in HTTP-only code.

method property

method: str

The HTTP method, uppercased. "" for WebSocket connections.

user property

user: Any

The current user, or None. Resolved once, then cached.

is_authenticated property

is_authenticated: bool

Whether the request carries an identity.

A user object may say so itself via is_authenticated (Starlette's BaseUser and Django's user both do). Otherwise, merely having a non-None user counts.

starlette_permissions.checks

The evaluation entry points.

Everything else in the library — the decorator, the dependency, the mixin, the middleware — funnels into :func:check_permissions. You can also call these directly from a service layer, where there is no HTTP handler to decorate.

check_permissions async

check_permissions(
    permissions: PermissionLike | Sequence[PermissionLike],
    ctx: PermissionContext,
    *,
    mode: Mode = "all",
) -> None

Evaluate permissions and raise if the request is not allowed.

Raises:

Type Description
PermissionDenied

with the message and status code of the rule that refused. NotAuthenticated (401) is a subclass of it.

has_permissions async

has_permissions(
    permissions: PermissionLike | Sequence[PermissionLike],
    ctx: PermissionContext,
    *,
    mode: Mode = "all",
) -> bool

Non-raising variant of :func:check_permissions.

Handy for branching in a template or trimming a response, where a denial should hide a field rather than fail the request.

check_object_permissions async

check_object_permissions(
    permissions: PermissionLike | Sequence[PermissionLike],
    ctx: PermissionContext,
    obj: Any,
    *,
    mode: Mode = "all",
) -> None

Run object-level checks against obj, raising on refusal.

Call this once you have loaded the record — that is the earliest point at which a rule like "you may only edit your own posts" can be decided.

has_object_permissions async

has_object_permissions(
    permissions: PermissionLike | Sequence[PermissionLike],
    ctx: PermissionContext,
    obj: Any,
    *,
    mode: Mode = "all",
) -> bool

Non-raising variant of :func:check_object_permissions.

Enforcement

starlette_permissions.decorators

Decorator-based enforcement.

@router.get("/me")
@permission_required(IsAuthenticated)
async def get_me(request: Request): ...

The decorator must sit below the route decorator, so the router registers the guarded function rather than the bare one. Works on FastAPI and Starlette endpoints, sync and async.

permission_required

permission_required(
    *permissions: PermissionLike | Sequence[PermissionLike],
    mode: Mode = "all",
    message: str | None = None,
    status_code: int | None = None,
) -> Callable[[F], F]

Guard an endpoint with one or more permissions.

Parameters:

Name Type Description Default
*permissions PermissionLike | Sequence[PermissionLike]

Permission classes, instances, or predicate functions. A single list is accepted too, so permission_required([A, B]) and permission_required(A, B) are the same thing.

()
mode Mode

"all" (default) requires every permission to pass; "any" requires at least one.

'all'
message str | None

Overrides the failing permission's message for this endpoint.

None
status_code int | None

Overrides the refusal status code for this endpoint.

None

Raises:

Type Description
MissingRequestError

at request time, if no Request reached the endpoint. This only happens when the decorator is applied above the route decorator, which stops the signature fix from taking effect.

object_permission_required

object_permission_required(
    *permissions: PermissionLike | Sequence[PermissionLike],
    mode: Mode = "all",
    getter: Callable[..., Any] | None = None,
    message: str | None = None,
    status_code: int | None = None,
) -> Callable[[F], F]

Guard an endpoint with object-level permissions.

Two shapes, depending on when the object becomes available:

  • With getter, the object is loaded and checked before the handler runs. The getter receives the endpoint's own keyword arguments and may be sync or async. Prefer this — nothing happens if the caller is refused.
  • Without getter, the handler's return value is the object, and it is checked after the handler runs. Convenient for a plain "fetch and return" route, but the fetch has already happened by then.
@router.get("/posts/{post_id}")
@object_permission_required(IsOwner("author_id"))
async def get_post(request: Request, post_id: int):
    return await posts.get(post_id)

get_permissions

get_permissions(
    endpoint: Callable[..., Any],
) -> tuple[BasePermission, ...]

Read back the permissions attached to an endpoint.

Useful in tests, and for generating an access matrix from a router.

starlette_permissions.dependencies

FastAPI dependency-based enforcement.

Preferred over the decorator on FastAPI: it needs no signature rewriting, it composes with the rest of the dependency graph, and it can be attached to a whole router in one place.

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

router = APIRouter(dependencies=[requires(IsService)])

Importing this module requires FastAPI. The rest of the library does not.

Note

This module deliberately does not use from __future__ import annotations. FastAPI reads these annotations at runtime to decide what to inject, resolving any string it finds against the callable's __globals__ — and a class instance such as :class:PermissionChecker has none. Stringified annotations therefore make older FastAPI treat request as a query parameter instead of injecting the request. Keeping real objects here avoids the whole class of problem.

PermissionChecker

A callable dependency that enforces permissions, or raises.

Instantiate it directly when you want the object itself — to reuse one checker in several places, or to call it from your own dependency.

requires

requires(
    *permissions: Permissions,
    mode: Mode = "all",
    message: str | None = None,
    status_code: int | None = None,
) -> Any

Build a Depends(...) that enforces permissions.

The dependency resolves to the :class:PermissionContext, so a handler that wants the user can take it as a value instead of re-deriving it:

@router.get("/me")
async def get_me(ctx: PermissionContext = requires(IsAuthenticated)):
    return ctx.user

requires_object

requires_object(
    *permissions: Permissions, mode: Mode = "all"
) -> Any

Build a dependency that resolves to an object-level checker.

The handler loads the record, then calls the checker with it:

@router.get("/posts/{post_id}")
async def get_post(post_id: int, check=requires_object(IsOwner("author_id"))):
    post = await posts.get(post_id)
    await check(post)
    return post

permission_responses

permission_responses(
    *, forbidden: bool = True, unauthorized: bool = True
) -> dict[int | str, dict[str, Any]]

OpenAPI responses entries for the denial cases.

A dependency cannot add response schemas to the generated schema on its own, so spread this into the route:

@router.get("/me", dependencies=[requires(IsAuthenticated)], responses=permission_responses())

current_context

current_context(request: Request) -> PermissionContext

A dependency resolving to the :class:PermissionContext, with no checks.

async def handler(ctx: PermissionContext = Depends(current_context)): ...

starlette_permissions.endpoints

Class-based enforcement for Starlette's HTTPEndpoint.

class PostEndpoint(PermissionMixin, HTTPEndpoint):
    permission_classes = [IsAuthenticated]

    async def get(self, request): ...
    async def delete(self, request): ...

Per-method rules are supported too, which is usually what you want once read and write differ:

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

PermissionMixin

Checks permission_classes before dispatching to the handler.

Must come before HTTPEndpoint in the base list, so that its dispatch runs first.

get_permissions classmethod

get_permissions(method: str) -> tuple[BasePermission, ...]

Resolve the permissions applying to method.

A mapping combines the wildcard entry with the method-specific one, so {"*": IsAuthenticated, "DELETE": IsAdminUser} requires both on DELETE rather than replacing one with the other.

starlette_permissions.middleware

Apply permissions to a whole app, or to a subtree of routes.

app.add_middleware(
    PermissionMiddleware,
    permissions=IsAuthenticated,
    exempt=["/health", re.compile(r"/auth/.*")],
)

Use this for a blanket default ("everything needs a login except these"). For anything route-specific, the decorator or dependency stays clearer, because the rule lives next to the handler it guards.

PermissionMiddleware

ASGI middleware that enforces permissions before the app runs.

Parameters:

Name Type Description Default
app ASGIApp

The wrapped ASGI application.

required
permissions PermissionLike | Sequence[PermissionLike]

A permission, or any nesting of lists of them.

()
mode Mode

"all" (default) or "any".

'all'
exempt Collection[str | Pattern[str]]

Paths that skip the check. A str must match the path exactly; pass re.compile(...) for a pattern, which is matched against the whole path.

()
methods Collection[str] | None

Restrict the check to these HTTP methods. None checks all.

None
Note

Added via app.add_middleware, this sits outside Starlette's ExceptionMiddleware, so it renders its own JSON response rather than raising — a raised HTTPException would surface as a 500 there.

Note

Middleware runs before routing, so ctx.view_kwargs is empty here. Rules that need a path parameter belong on the route itself.

Built-in permissions

starlette_permissions.permissions.common

Permissions that depend only on the request, not on who is making it.

AllowAny

Bases: BasePermission

Allows everything. Useful as an explicit "this endpoint is public" marker.

DenyAll

Bases: BasePermission

Refuses everything. Handy for disabling a route without deleting it.

ReadOnly

Bases: BasePermission

Allows only GET, HEAD and OPTIONS.

Usually combined: ReadOnly | IsAdminUser gives everyone read access and admins write access.

IsMethod

Bases: BasePermission

Allows only the listed HTTP methods.

Predicate

Bases: BasePermission

Wraps a plain function as a permission.

The function takes the context and returns a bool; it may be async. This is what makes bare functions usable anywhere a permission is expected:

permission_required(lambda ctx: ctx.headers.get("x-tenant") == "acme")

HasHeader

Bases: BasePermission

Requires a header to be present, and optionally to hold a given value.

permission

permission(
    func: Callable[
        [PermissionContext], bool | Awaitable[bool]
    ]
    | None = None,
    *,
    message: str | None = None,
    status_code: int | None = None,
) -> Any

Turn a function into a permission, with or without arguments.

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

starlette_permissions.permissions.auth

Identity-based permissions.

IsAuthenticated

Bases: BasePermission

Requires a user on the request.

Refuses with 401 rather than 403, since the caller can fix this by authenticating. Set unauthenticated_status_code=403 in :func:~starlette_permissions.configure if you would rather not distinguish the two.

IsAnonymous

Bases: BasePermission

Requires that no user is attached. For login and signup routes.

IsAdminUser

Bases: BasePermission

Requires an authenticated user flagged as an administrator.

Checks each of settings.admin_attrs in turn — by default is_admin, is_staff, is_superuser — and allows if any is truthy.

IsAuthenticatedOrReadOnly

Bases: BasePermission

Anyone may read; only authenticated users may write.

starlette_permissions.permissions.roles

Role- and scope-based permissions.

Roles come from settings.role_getter, which by default reads roles, groups or role off the user object. Scopes come from settings.scope_getter, which by default reads Starlette's AuthenticationMiddleware credentials.

HasAnyRole

Bases: _RoleBase

Requires at least one of the given roles.

HasAllRoles

Bases: _RoleBase

Requires every one of the given roles.

HasAnyScope

Bases: _RoleBase

Requires at least one of the given OAuth-style scopes.

HasAllScopes

Bases: _RoleBase

Requires every one of the given OAuth-style scopes.

starlette_permissions.permissions.api_key

API-key permissions, for service-to-service calls.

This is the generalisation of a hand-rolled IsService check:

IsService = HasAPIKey(key=lambda: settings.service_api_token)

Passing a callable keeps the comparison against the current configured value, which matters when settings are loaded lazily or rotated at runtime.

HasAPIKey

Bases: BasePermission

Requires a matching API key in a header (or query parameter).

Parameters:

Name Type Description Default
key str | Collection[str] | Callable[[], str | Collection[str]]

The accepted key, a collection of accepted keys, or a callable returning either. A callable is re-invoked on every request.

required
header str

Header carrying the key. Defaults to X-API-Key.

'X-API-Key'
query_param str | None

Optional query parameter to accept the key from as well. Off by default — keys in URLs end up in access logs.

None
message str | None

Overrides the refusal message.

None

Comparison uses :func:secrets.compare_digest, so a wrong key takes the same time to reject regardless of how much of it was right.

starlette_permissions.permissions.objects

Object-level permissions — rules that need the record, not just the request.

These only take effect through :func:~starlette_permissions.check_object_permissions, @object_permission_required or requires_object. A request-level check cannot decide "you may edit your own post" before the post has been loaded, so has_permission on these classes deliberately allows everything.

ObjectPermission

Bases: BasePermission

Base for rules that apply only once an object is available.

IsOwner

Bases: ObjectPermission

Allows access only to the object's owner.

Parameters:

Name Type Description Default
owner_field str

Attribute (or dict key) on the object holding the owner's identifier. Defaults to user_id.

'user_id'
user_id Callable[[Any], Any]

How to get the comparable identifier from the current user. Defaults to the first of id, pk, uuid, user_id that exists, falling back to the user object itself.

_default_user_id
message str | None

Overrides the refusal message.

None
await check_object_permissions(IsOwner("author_id"), ctx, post)

IsOwnerOrReadOnly

Bases: IsOwner

Anyone may read the object; only its owner may modify it.

Composition

starlette_permissions.operators

Boolean combinators: &, |, ~ and the All/Any helpers.

Combinators are permissions themselves, so they nest freely::

IsAuthenticated & (IsAdminUser | IsOwner("user_id")) & ~IsBanned

AND

Bases: _Binary

Both sides must allow. Short-circuits on the first refusal.

OR

Bases: _Binary

Either side may allow.

When both refuse, the failure is reported as the left side's — the rule written first, which is normally the broader one. This keeps the message a caller sees stable no matter which branch was evaluated last.

NOT

Bases: BasePermission

Inverts a permission.

The wrapped rule's message would be misleading here (it explains why it allows), so the refusal is reported against the NOT itself.

All

All(*permissions: PermissionLike) -> BasePermission

Require every permission. The explicit form of chained &.

All() with no arguments allows everything, matching all([]).

Any_

Any_(*permissions: PermissionLike) -> BasePermission

Require at least one permission. The explicit form of chained |.

Exported as Any from the package root; the trailing underscore here only keeps it from shadowing typing.Any inside this module.

Any_() with no arguments denies everything, matching any([]).

Not

Not(
    permission: PermissionLike,
    *,
    message: str | None = None,
) -> BasePermission

Invert a permission. The explicit form of ~.

Configuration

starlette_permissions.settings

Global and per-app configuration.

The defaults work with Starlette's AuthenticationMiddleware out of the box. Anything else — a token on request.state, a custom user model, roles stored somewhere unusual — is a one-line :func:configure call at startup.

PermissionSettings dataclass

Immutable settings bundle. Use :func:configure to change the global one.

configure

configure(**kwargs: Any) -> PermissionSettings

Update the global settings in place and return the new bundle.

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

get_settings

get_settings(
    conn: HTTPConnection | None = None,
) -> PermissionSettings

Return the settings for this connection.

An app can carry its own bundle on app.state.permission_settings, which wins over the global one. That keeps mounted sub-applications independent.

override_settings

override_settings(
    **kwargs: Any,
) -> Iterator[PermissionSettings]

Temporarily replace global settings. Intended for tests.

Exceptions

starlette_permissions.exceptions

Exceptions raised when a permission check fails.

Both exceptions derive from Starlette's HTTPException, so FastAPI's default handler renders them as {"detail": ...} JSON with no extra setup. Plain Starlette renders HTTPException as plain text; call :func:install_exception_handlers if you want JSON there too.

PermissionDenied

Bases: HTTPException

Raised when one or more permission checks fail. Renders as 403.

NotAuthenticated

Bases: PermissionDenied

Raised when the request carries no identity at all. Renders as 401.

ConfigurationError

Bases: RuntimeError

The library was handed something it cannot use as a permission.

MissingRequestError

Bases: ConfigurationError

No Request/WebSocket could be found for the endpoint being guarded.

SyncCheckError

Bases: ConfigurationError

An async permission was evaluated from a context that cannot await it.

install_exception_handlers

install_exception_handlers(app: Starlette) -> None

Render permission failures as JSON on a plain Starlette app.

FastAPI already does this for every HTTPException, so calling it there is a no-op in practice. On Starlette the built-in handler returns PlainTextResponse, which is rarely what an API wants.

Testing helpers

starlette_permissions.testing

Helpers for unit-testing permissions without standing up an app.

ctx = make_context(user=User(id=1, roles=["admin"]), method="DELETE")
assert await has_permissions(HasRole("admin"), ctx)

make_request

make_request(
    *,
    method: str = "GET",
    path: str = "/",
    headers: Mapping[str, str] | None = None,
    query_string: str = "",
    user: Any = _UNSET,
    auth: Any = None,
    path_params: Mapping[str, Any] | None = None,
    app: Any = None,
) -> Request

Build a Request with just enough ASGI scope to check permissions.

user is placed on the scope where Starlette's AuthenticationMiddleware would put it, so the default user_getter finds it.

make_context

make_context(
    *,
    settings: PermissionSettings | None = None,
    view_kwargs: Mapping[str, Any] | None = None,
    endpoint: Any = None,
    **request_kwargs: Any,
) -> PermissionContext

Build a :class:PermissionContext directly. Arguments as :func:make_request.

Compatibility

starlette_permissions.compat

Drop-in replacement for a hand-rolled permission_required.

This exists so an existing codebase can adopt the library with an import swap and migrate afterwards, one module at a time. It reproduces the older behaviour exactly, including the parts the main API deliberately changed:

  • OR semantics — a list passes if any permission passes. The main permission_required requires all of them, matching Django and DRF.
  • A returned response rather than a raised exception, so exception handlers and error-logging middleware never see the denial.
  • Permissions defined as is_permitted(request, *args, **kwargs).

Every use emits a DeprecationWarning. To migrate a call site:

# before
from starlette_permissions.compat import permission_required
@permission_required([IsAuthenticated])

# after
from starlette_permissions import permission_required
@permission_required(IsAuthenticated)

With a single permission in the list the two are equivalent, so most call sites can move without any behaviour change at all.

BasePermission

The legacy base class: a static is_permitted taking the request.

New code should subclass :class:starlette_permissions.BasePermission, whose has_permission receives a :class:~starlette_permissions.PermissionContext instead.

LegacyPermission

Bases: BasePermission

Adapts an is_permitted-style permission to the current interface.

Produced automatically by :func:~starlette_permissions.base.resolve_permission, so legacy permissions can be mixed into &/| expressions and passed to the modern decorator without being rewritten first.

permission_required

permission_required(
    permissions: Sequence[Any],
) -> Callable[..., Any]

Legacy decorator: OR semantics, returns a 403 response instead of raising.

Deprecated since 0.1. Use :func:starlette_permissions.permission_required.