Skip to main content

Database-enforced authorization policies written in pure Python. For SQLAlchemy 2.0.

Project description

sqla-authz logo

Database-enforced authorization policies written in pure Python.
For SQLAlchemy 2.0.

Explore the docs »

Why sqla-authz?

Most authorization libraries answer a yes/no question: can this user do this action? That works for protecting endpoints, but not when you need to filter a queryshow me only the rows this user is allowed to see.

sqla-authz turns your policies into database-level filters, so the database does the enforcement. No post-query Python loops, no N+1 permission checks.

  • Database-enforced — policies compile to filter expressions; the database does the filtering
  • SQLAlchemy-native kernel — strongest for ORM-heavy Python apps that want authorization close to the query layer
  • Pure Python — no DSL, no config files; full IDE autocomplete, type checking, and debugging
  • Fail-closed — missing policy = zero rows, not a data leak
  • Async-equal — same synchronous policy code works with both Session and AsyncSession
  • Type-safe — passes Pyright strict mode with zero errors

sqla-authz is a focused authorization kernel, not a general cross-service policy platform. Its sweet spot is query filtering, scopes, point checks, and ORM write guardrails inside SQLAlchemy applications.

Usage

Define a Policy

Policies are plain Python functions decorated with @policy. They receive an actor and return a SQLAlchemy filter expression:

from sqlalchemy import ColumnElement, or_, true
from sqla_authz import policy, READ

@policy(Post, READ)
def post_read_policy(actor: User) -> ColumnElement[bool]:
    if actor.role == "admin":
        return true()
    return or_(Post.is_published == True, Post.author_id == actor.id)

Apply to Queries

Use authorize_query() to apply the policy to a query:

from sqlalchemy import select
from sqla_authz import authorize_query, READ

stmt = select(Post).order_by(Post.created_at.desc())
stmt = authorize_query(stmt, actor=current_user, action=READ)
result = session.execute(stmt)
# -> SELECT ... FROM post WHERE (is_published = true OR author_id = :id)

No policy registered for a model? The query returns zero rows — authorization is deny-by-default.

Scopes

Scopes are cross-cutting filters (like tenant isolation) that are AND'd with all policies — define them once instead of repeating in every policy:

from sqla_authz import scope

@scope(applies_to=[Post, Comment, Document])
def tenant(actor: User, Model: type) -> ColumnElement[bool]:
    return Model.org_id == actor.org_id

Composition: final_filter = (policy_1 OR policy_2) AND scope_1 AND scope_2. Use verify_scopes(Base, field="org_id") at startup to catch models missing a scope.

Point Checks

Check a single resource without hitting the database:

from sqla_authz import can, authorize

if can(current_user, "read", some_post):
    ...

# Or raise AuthorizationDenied if denied
authorize(current_user, "read", some_post)

Create Authorization

Check a pending ORM object before it is flushed:

from sqla_authz import authorize_create

draft = Post(title="Roadmap", author_id=current_user.id, is_published=False)
authorize_create(current_user, draft)
session.add(draft)
session.commit()

You can also enable automatic create interception for ORM session.add() / flush flows with AuthzConfig(intercept_creates=True).

Automatic Session Interception

Opt-in to authorize every SELECT automatically:

from sqla_authz.session import authorized_sessionmaker

Session = authorized_sessionmaker(
    bind=engine,
    actor_provider=get_current_user,
    action="read",
)

FastAPI Integration

Inject authorized query results directly into your endpoints:

from sqla_authz.integrations.fastapi import AuthzDep, get_actor, get_session, install_error_handlers

app.dependency_overrides[get_actor] = get_current_user
app.dependency_overrides[get_session] = get_db
install_error_handlers(app)  # maps AuthorizationDenied → 403, NoPolicyError → 500

@app.get("/posts")
async def list_posts(posts: list[Post] = AuthzDep(Post, "read")):
    return posts

@app.get("/posts/{post_id}")
async def get_post(post: Post = AuthzDep(Post, "read", id_param="post_id")):
    return post

For more examples and advanced patterns, see the Documentation.

Getting Started

pip install sqla-authz

Optional extras:

pip install "sqla-authz[fastapi]"
pip install "sqla-authz[testing]"
pip install "sqla-authz[asyncio]"

For development:

git clone https://github.com/colbyjoines/sqla-authz.git
cd sqla-authz
uv sync --dev

Architecture

  • Explicit by defaultauthorize_query() is visible and greppable. Automatic session interception is opt-in.
  • Database-enforced — policies compile to filter expressions. The database does the filtering, not Python.
  • Async-equal — same code for Session and AsyncSession. Filter construction is pure Python with no I/O.
  • Fail-closed — missing policy = zero rows, not a data leak.

References

Colby Joines - colby.j.joines@gmail.com

Project Repo: https://github.com/colbyjoines/sqla-authz

Project Docs: https://colbyjoines.github.io/sqla-authz/

Acknowledgments

Project details


Download files

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

Source Distribution

sqla_authz-0.1.0b4.tar.gz (42.3 kB view details)

Uploaded Source

Built Distribution

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

sqla_authz-0.1.0b4-py3-none-any.whl (62.5 kB view details)

Uploaded Python 3

File details

Details for the file sqla_authz-0.1.0b4.tar.gz.

File metadata

  • Download URL: sqla_authz-0.1.0b4.tar.gz
  • Upload date:
  • Size: 42.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for sqla_authz-0.1.0b4.tar.gz
Algorithm Hash digest
SHA256 e2896948e066f708617db0f2db78fdc7c705afc967b95595433195c8f507ff39
MD5 3f4a7948ae39ee378eb441bc2fdfaf70
BLAKE2b-256 11950789a4c20c600350c78e91a77d11e23dddaaa6c9543ea3aec6f7d4f85d43

See more details on using hashes here.

File details

Details for the file sqla_authz-0.1.0b4-py3-none-any.whl.

File metadata

  • Download URL: sqla_authz-0.1.0b4-py3-none-any.whl
  • Upload date:
  • Size: 62.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for sqla_authz-0.1.0b4-py3-none-any.whl
Algorithm Hash digest
SHA256 f5e1eacc84db960de032e876a2f6d4529358eadac53810395a051b1b3ed0072b
MD5 23066e25f70f50f65797c4111ad0fd7a
BLAKE2b-256 ae5878ab29166d61c2d0acb4d1af4e23d7bfe43b7c84e5d9e325948146732dbd

See more details on using hashes here.

Supported by

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