Security boundary¶
Purview filters supported ORM reads and guards tenant IDs during normal ORM object writes. Fine-grained create, update, and delete permissions require explicit application checks. This page describes the assumptions and exclusions behind those controls.
The session is the boundary¶
All automatic enforcement assumes:
- Your application authenticates the principal and verifies their tenant membership
and roles before constructing
Context. - Each actor/request receives a fresh session, bound before application data is
loaded or attached. Never share an
AsyncSessionbetween concurrent tasks. - Models and policy configuration are complete before calling
install(). - Queries and writes use the supported ORM paths described below.
- The application checks action permissions before mutation, and uses database constraints and transaction handling appropriate to its consistency needs.
Rebinding to a different tenant raises TenantMismatch. Rebinding within the same
tenant is allowed, but it does not clear loaded objects or retroactively revoke
access. SQLAlchemy's identity map can return an already-loaded object without a
query. Treat an actor or role change as a reason to open a new session.
Automatic enforcement¶
The test links below map the behavior to repository evidence. They describe the tested paths, rather than a claim about every possible SQLAlchemy operation.
| Behavior | Mechanism | Tests |
|---|---|---|
| Collection reads are narrowed to tenant and read policy | do_orm_execute with with_loader_criteria |
Read filtering |
| Supported relationship loads are filtered | Criteria applied to eager and lazy load statements | Relationship loads |
session.get() database loads obey read criteria |
Read guard on the database query | Get behavior |
| New objects with no tenant ID receive the bound tenant | before_flush guard |
Flush guards |
| Foreign tenant IDs are rejected on object attachment or insert | before_attach and before_flush guards |
Adversarial writes |
| Dirty objects cannot change their tenant away from the bound tenant | before_flush dirty-object check |
Write guard edges |
| Detached cross-tenant objects cannot be added through supported paths | Attach guard and merge/flush handling | Adversarial ORM paths |
| Rebinding to another tenant is rejected | TenantMismatch |
Rebinding tests |
| Missing tenant fields fail during installation | Mapper discovery and validation | Discovery validation |
With asynchronous sessions, use selectinload(...) or
AsyncAttrs.awaitable_attrs for relationships. Implicit lazy access that requires
I/O can raise SQLAlchemy's MissingGreenlet error.
Single-table and joined-table inheritance, composite keys, and UUID keys have dedicated coverage in polymorphic tests and key tests. Configure inherited policies on the base mapped model.
Checks your application must call¶
| Operation | Required call | What it checks |
|---|---|---|
| Update an existing object | await pv.authorize(session, "update", obj) |
Existing primary key, tenant, and governing action predicate |
| Delete an existing object | await pv.authorize(session, "delete", obj) |
Existing primary key, tenant, and governing action predicate |
| Validate a proposed object | pv.validate_create(session, obj) |
Proposed tenant and all registered create rules |
| Check multiple IDs | await pv.authorized_ids(session, action, Model, ids) |
Allowed subset using the same action predicate |
Call checks before modifying objects. Authorization queries can trigger autoflush, and checks evaluate stored row values rather than validating every proposed field change. Use database transactions, locks, constraints, or additional application validation where concurrent changes matter. Purview does not make a separate check and later write atomic by itself.
The ordinary flush guard handles tenant constraints, not the above fine-grained action rules. In particular, registering a create rule does not cause a flush to evaluate it.
Checks are covered by EXISTS tests, create-rule tests, and authorization regressions. The regression suite includes preserving flush guards and nested reads during authorization-query autoflush.
Outside automatic enforcement¶
| Path | Boundary |
|---|---|
Raw SQL, text(), SQLAlchemy Core tables, direct connections |
No automatic tenant or policy filtering |
Bulk INSERT, UPDATE, or DELETE, including session.execute(update(Model)) |
Bypasses object-level flush checks and is not automatically scoped |
| Unbound sessions | No automatic filtering or tenant checks |
Explicit bypass(reason=...) blocks |
Automatic guards are suspended |
| Models marked global | No automatic tenant or row filtering, even in strict mode |
| Objects already in memory or loaded before binding | No retroactive filtering or revocation |
| Database cascades, triggers, and external writers | Enforced by your database and application design |
| Field-level permissions | Enforced by input validation and serialization |
Opt-in warn_on_unfiltered=True emits advisory warnings for recognized unfiltered
operations. It does not block them and is not an exhaustive detector of unsupported
paths.
The bypass uses a ContextVar and resets on exit. Independently created request
tasks remain isolated, as exercised by
bypass isolation tests.
Child tasks created inside a bypass block inherit that context, so do not start
ordinary request work there. Use a dedicated maintenance session and do not reuse
its loaded objects in a request.
Default visibility¶
A scoped model with no read rule is tenant-wide by default. strict=True changes
that to default deny. A registered read rule that contributes no granting predicates
denies access in either mode. An action without its own rules falls back to read.
pv.audit() and install(audit="warn" | "raise") surface tenant-wide models.
Auditing does not prove that a registered rule is sufficiently restrictive. Review
policy logic and test realistic actors against representative data.
Database integrity¶
Purview is an application-layer control. Tenant-aware foreign keys, unique constraints, and database row-level security can provide additional protections. Purview does not validate arbitrary relationship assignments or automatically create tenant-aware schema constraints.
Report a vulnerability¶
See the security policy for private reporting. Include a minimal reproduction, the query or write path, session binding order, model definitions, and installed versions.