Quickstart¶
Run a complete example with an in-memory SQLite database. It creates three posts, binds an author to tenant 1, and shows that only their own post is returned.
1. Install¶
Prefer pip? Follow the virtual environment instructions and use
python quickstart.py below.
2. Add the example¶
Save this as quickstart.py, or copy it from
the repository.
This page includes that file directly, so the example and documentation stay in sync.
"""Run with Python after installing purview-authz[sqlite]."""
import asyncio
from sqlalchemy import ColumnElement, select
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from purview import READ, Context, Policy
from purview.sqlalchemy import install
class Base(DeclarativeBase):
pass
class Post(Base):
__tablename__ = "post"
id: Mapped[int] = mapped_column(primary_key=True)
tenant_id: Mapped[int]
author_id: Mapped[int]
title: Mapped[str]
policy = Policy()
@policy.rule(Post, READ)
def read_posts(ctx: Context[int, int]) -> list[ColumnElement[bool]]:
return [Post.author_id == ctx.user_id] if ctx.has_role("author") else []
async def main() -> None:
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
sessions = async_sessionmaker(engine, expire_on_commit=False)
pv = install(Base, policy, strict=True)
try:
async with engine.begin() as connection:
await connection.run_sync(Base.metadata.create_all)
# A separate, unbound session for trusted setup.
async with sessions() as session:
session.add_all(
[
Post(id=1, tenant_id=1, author_id=42, title="My draft"),
Post(id=2, tenant_id=1, author_id=7, title="Another author"),
Post(id=3, tenant_id=2, author_id=42, title="Another tenant"),
]
)
await session.commit()
async with sessions() as session:
pv.bind(session, Context(user_id=42, tenant_id=1, roles={"author"}))
posts = (await session.scalars(select(Post))).all()
print([post.title for post in posts]) # ['My draft']
print(await session.get(Post, 3)) # None
print(await pv.authorize(session, READ, posts[0])) # True
finally:
pv.uninstall()
await engine.dispose()
if __name__ == "__main__":
asyncio.run(main())
3. Run it¶
Expected output:
The other author's post fails the read policy. The third post belongs to another tenant, even though its author ID matches. Both are filtered out.
What happened¶
- Define a model.
Post.tenant_idis the tenant field. All mapped models underBasemust have this field unless explicitly marked global. - Register a rule. Authors contribute an ownership predicate. Other actors contribute no predicates, so the rule denies access.
- Install once.
install()validates the mapped models and registers SQLAlchemy session event handlers. Call it after importing your application's models. - Bind before querying. A fresh session receives the authenticated actor's
Context. The read guard adds both tenant and row predicates. - Check an object explicitly.
authorize()uses a databaseEXISTSquery for the object's primary key, tenant, and action predicate.
The separate seed session is intentionally unbound. Unbound sessions have no automatic filtering, which is useful for trusted setup work. Request handlers should always use a fresh bound session.
Choose your default¶
The example uses strict=True. This denies reads for scoped models that have no
read rule. In the default strict=False mode, a scoped model with no read rule is
visible to all actors within the bound tenant. In both modes, a registered rule
that returns no granting predicates denies access.
Add write permissions¶
authorize(session, "update", post) checks update permission. When there is no
update rule, Purview falls back to the read rule. Define an update rule when editing
should be more restrictive than reading, then check before changing the object:
from purview import PurviewForbidden
if not await pv.authorize(session, "update", post):
raise PurviewForbidden("You cannot edit this post")
post.title = "Revised draft"
await session.commit()
The tenant flush guard does not automatically call your update, delete, or create rules. Use explicit helpers for those permissions. See writing policies for create validation and FastAPI integration for complete request-handler patterns.