Purview¶
Row-level authorization and multi-tenancy for FastAPI + SQLAlchemy. Define a policy once as SQLAlchemy column expressions and get both yes/no checks and query filtering from the same rule — so the check and the filter can never disagree.
@policy.rule(Post, "read")
def read_post(ctx: Context) -> list[ColumnElement[bool]]:
rules = []
if ctx.has_role("author"):
rules.append(Post.author_id == ctx.user_id) # authors see their own
if ctx.has_role("org_admin"):
rules.append(true()) # admins see the whole tenant
return rules # OR-combined; empty = deny
That one rule powers a filtered select(Post) and an
authorize(session, "read", post) check.
Install¶
pip install purview-authz # core + SQLAlchemy
pip install "purview-authz[fastapi]" # plus the FastAPI adapter
The distribution is purview-authz; the import package is purview. Requires
Python 3.11+ and SQLAlchemy 2.0+.
Quickstart¶
from sqlalchemy import select, true
from purview import Context, Policy, READ
from purview.sqlalchemy import install
policy = Policy()
policy.global_model(Org) # opt the tenant root out of scoping
@policy.rule(Post, READ)
def read_post(ctx: Context):
return [Post.author_id == ctx.user_id] if ctx.has_role("author") else []
pv = install(Base, policy, tenant_column="org_id") # wires the guards; validates models
async with async_session() as session:
pv.bind(session, Context(user_id=42, tenant_id=1, roles={"author"}))
posts = await session.scalars(select(Post)) # filtered automatically
ok = await pv.authorize(session, "update", post) # yes/no for one object
Core ideas¶
- One definition, two forms. A boolean
ColumnElementfilters a collection (.where) and checks one object (EXISTS). The database evaluates both. - Tenancy is the session boundary. One session, one tenant; reads, writes, and attaches are all guarded. See the threat model.
- Secure by default. Every model is tenant-scoped;
install()refuses to start if a model lacks its tenant column.Purview.audit()flags any model left visible tenant-wide, andinstall(warn_on_unfiltered=True)warns on the documented unfiltered sharp edges. - Inspectable.
Purview.explain(session, "read", Post)shows the exact predicate the guard applies — compiled SQL, contributing rules, effective roles — with no database round-trip. - Composable rules. Role hierarchies (
Policy.role_implies) and predicate helpers (owned_by,in_values) keep policies declarative.
Learn more¶
- Design — the architecture and the keystone idea.
- Threat model — what is and isn't enforced, each guarantee mapped to its test.
- Migrating from Oso.
- API reference.
- Example app — a multi-tenant tracker (FastAPI + Alembic + Postgres) exercising every feature.