Database-enforced authorization policies written in pure Python. For SQLAlchemy 2.0.
Project description
Database-enforced authorization policies written in pure Python.
For SQLAlchemy 2.0.
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 query — show 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
SessionandAsyncSession - 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 default —
authorize_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
SessionandAsyncSession. 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
- Oso / sqlalchemy-oso — the original inspiration; deprecated Dec 2023
- SQLAlchemy — the foundation this library builds on
- Best-README-Template — this README's structure
Project details
Release history Release notifications | RSS feed
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 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e2896948e066f708617db0f2db78fdc7c705afc967b95595433195c8f507ff39
|
|
| MD5 |
3f4a7948ae39ee378eb441bc2fdfaf70
|
|
| BLAKE2b-256 |
11950789a4c20c600350c78e91a77d11e23dddaaa6c9543ea3aec6f7d4f85d43
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f5e1eacc84db960de032e876a2f6d4529358eadac53810395a051b1b3ed0072b
|
|
| MD5 |
23066e25f70f50f65797c4111ad0fd7a
|
|
| BLAKE2b-256 |
ae5878ab29166d61c2d0acb4d1af4e23d7bfe43b7c84e5d9e325948146732dbd
|