Migrating from Oso¶
If your application uses Polar policies with the open-source sqlalchemy-oso
integration, this guide maps common patterns to Purview. The main change is moving
policy expressions into Python and SQLAlchemy. Purview focuses on row-level
authorization and a tenant-bound session lifecycle.
This is a conceptual migration guide, not a drop-in compatibility layer. Test the same actors, actions, resources, and tenant boundaries before switching enforcement.
Concept map¶
| Oso pattern | Purview equivalent |
|---|---|
Polar allow(actor, action, resource) |
@policy.rule(Model, action) returning SQLAlchemy predicates |
Multiple granting allow rules |
Multiple OR-combined rules and predicates |
authorized_sessionmaker list filtering |
A fresh context-bound session and ordinary ORM select() |
oso.authorize(...) |
await pv.authorize(...), which returns a boolean |
| Raising on denied authorization | authorize_or_403(...) or your own exception handling |
| Authorized resource query | Bound-session read or explicit authorized_select(...) |
| Roles attached to an actor | Context.roles, has_role(), and optional role implications |
| Resource relationships | SQLAlchemy relationship predicates such as .has() and .any() |
Translate a rule¶
A Polar ownership rule with an admin grant might look like:
allow(actor: User, "read", post: Post) if
post.created_by = actor;
allow(actor: User, "read", _post: Post) if
actor.role = "admin";
The corresponding Purview rule uses the model's foreign-key column:
from sqlalchemy import true
from purview import READ, Context, Policy
policy = Policy()
@policy.rule(Post, READ)
def read_post(ctx: Context):
predicates = []
if ctx.user_id is not None:
predicates.append(Post.created_by_id == ctx.user_id)
if ctx.has_role("admin"):
predicates.append(true())
return predicates
The predicates are OR-combined. The admin grant permits all rows within the bound tenant, not across tenants. The application's context resolver supplies verified roles for that tenant.
Migrate collection reads¶
from sqlalchemy import select
from purview.sqlalchemy import install
pv = install(Base, policy, tenant_column="tenant_id", strict=True)
async with sessions() as session:
pv.bind(session, Context(user_id=42, tenant_id=1, roles={"admin"}))
posts = (await session.scalars(select(Post))).all()
This assumes existing models and an async session factory. The quickstart provides a complete runnable setup.
strict=True makes scoped models without read rules deny access. Review this
explicitly during migration. Purview's default without strict mode is tenant-wide
visibility for models with no read rule.
Migrate object checks¶
Unlike APIs that raise on a denied check, pv.authorize() returns a boolean:
from purview import PurviewForbidden
if not await pv.authorize(session, "update", post):
raise PurviewForbidden("You cannot edit this post")
In FastAPI, await authorize_or_403(pv, session, "update", post) raises the Purview
exception for you. Install the adapter's error handlers to return HTTP 403.
Register action rules explicitly when your old policy distinguished read, update, delete, or a custom action. Purview falls back to read rules when an action has no rules of its own.
Account for creation and tenancy¶
- Create rules use
@policy.create_rule(Model)and return booleans from the context and proposed object. Callpv.validate_create(session, proposed)explicitly. - Multiple create rules are AND-combined, unlike granting row rules.
- Every discovered model needs a tenant field unless explicitly marked global.
- Global models are exempt from automatic read filtering, including read rules.
- Use a fresh session per actor/request and bind it before loading application data.
Check the scope of your old policy¶
Field-level permissions need input-validation or serialization logic. Non-SQLAlchemy resources and arbitrary Python object checks do not map directly to Purview's database-backed row checks. Raw SQL and bulk DML require separate controls.
See writing policies for action semantics and the security boundary for supported enforcement paths.