Skip to main content

rlsalchemy

Declarative PostgreSQL and CockroachDB row level security for SQLAlchemy 2.1 and Alembic 1.18.

Models own policy expressions. A typed context model derives every setting name, SQL cast, and prefix from one class. SQLAlchemy table metadata carries the compiled declaration. Alembic compares that declaration with the active database and writes one reversible operation for the complete table state. Sessions bind request context through the standard Session.info mapping and SessionEvents.after_begin.

Context

Declare the transaction-local settings once as a typed model. Field names become setting names, annotations derive the casts, and the prefix snake-cases from the class name unless passed explicitly. Class access projects a field to its policy-side expression, so the predicate and the bound value can never drift apart.

import uuid

import rls
from patos import FrozenModel


class ScopeTable(FrozenModel):
    read: frozenset[uuid.UUID] = frozenset()
    write: frozenset[uuid.UUID] = frozenset()


class User(rls.Context, prefix="app"):
    scopes: ScopeTable = ScopeTable()

Models

Declare __rls__ on a mapped class and build a Catalog after all models import. An inherited declaration protects every concrete subclass, which keeps shared tenant rules in one mixin. Every mapped table must declare policies or opt out with the singleton rls.Open(), so an unprotected table is a decision, never an accident.

import rls
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column


class Base(DeclarativeBase):
    pass


class Item(Base):
    __tablename__ = "item"

    id: Mapped[int] = mapped_column(primary_key=True)
    scopes: Mapped[list[uuid.UUID]] = mapped_column(ARRAY(sa.Uuid()))

    @classmethod
    def __rls__(cls) -> tuple[rls.Policy, ...]:
        readable = cls.scopes.op("<@")(User.setting("scopes.read"))
        writable = cls.scopes.op("<@")(User.setting("scopes.write"))
        return rls.crud(readable, write=writable)


catalog = rls.Catalog(Base.registry)

Catalog is a closed set of mapped tables and policy declarations. It is deliberately not the self-registering implementation pattern provided by patos.Registry.

rls.crud produces separate select, insert, update, and delete policies. Their table-local names derive from those commands, so ordinary declarations contain no identifier strings. Individual policy constructors (Policy.select, Policy.insert, Policy.update, Policy.delete, Policy.for_all) cover tables that need a different shape, multiple roles, or restrictive composition. Pass name= only when a table deliberately declares more than one policy for the same command.

Sessions

No custom session class is required. Pass the context instance's info() as standard session information.

from sqlalchemy.ext.asyncio import async_sessionmaker


sessions = async_sessionmaker(engine)

user = User(
    scopes=ScopeTable(
        read=frozenset({account}),
        write=frozenset({account}),
    )
)
async with sessions(info=user.info()) as session:
    async with session.begin():
        rows = await session.scalars(sa.select(Item))

The package writes every value with SQLAlchemy set_config expressions and transaction-local scope, serialized once per context instance. Nested context models flatten into dotted settings, which keeps arrays native and avoids JSON subqueries inside policies. A pooled connection cannot retain context after commit or rollback. Scalars, dates, UUIDs, collections, JSON values, and None are supported. Applications that do not use Pydantic can pass pre-serialized pairs through rls.SessionContext(...).info() and use the same transaction hook.

Alembic

The installed package exposes an Alembic 1.18 plugin. Enable it beside the built-in plugins.

context.configure(
    connection=connection,
    target_metadata=Base.metadata,
    autogenerate_plugins=["alembic.autogenerate.*", "rls"],
)

Autogenerate reads PostgreSQL policies in one joined catalog query. On CockroachDB it reads the shared table flags once and uses the database's structured policy command for each table. Drift produces one typed AlterRLSOp carrying complete before and after rls.RLSState values, so downgrade is the same operation with the states reversed. The snapshot includes enable and force flags as well as policies, so a partially configured live table also reverses exactly. The renderer stores each state through Pydantic structured data. As with every Alembic autogenerate candidate, applications may replace a large compiled snapshot with equivalent migration-local SQLAlchemy expressions before accepting the revision.

Verification

Applications can verify the live database without generating a migration.

violations = catalog.verify(connection)
assert not violations

Verification checks enable and force flags, every declared policy, commands, permissive or restrictive mode, target roles, predicates, and undeclared live policies. Policy comparison uses a PostgreSQL AST and preserves casts that can change behavior, folding deparser noise through SQLGlot's leaves-first tree replacement. CompiledPolicy then uses frozen value equality over the canonical result. Managed tables with undeclared live row security are reported too.

PostgreSQL reflection reads pg_catalog.pg_policies in one query. CockroachDB reflection uses its structured SHOW POLICIES command because its PostgreSQL compatibility view is empty. The declaration, migration, session context, and verification APIs stay the same on both databases.

CockroachDB does not allow SQL subqueries inside policy predicates. Express those relationships through a database function or a direct predicate. Its documented ON CONFLICT DO NOTHING behavior can also skip row policy checks for candidate rows, so security-sensitive inserts should not rely on that form.

Applications without Alembic can call catalog.create_all(connection) inside their own transaction. Every emitted schema statement is a typed SQLAlchemy ExecutableDDLElement with dialect-managed identifier quoting.

Auditing

rlsalchemy owns declaration, installation, and drift detection. For posture reports, lint rules, isolation proofs, and CI gating over the live database, pair it with pgrls, which reads the same catalog state this package writes.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rlsalchemy-0.6.0.tar.gz (17.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

rlsalchemy-0.6.0-py3-none-any.whl (26.8 kB view details)

Uploaded Python 3

File details

Details for the file rlsalchemy-0.6.0.tar.gz.

File metadata

  • Download URL: rlsalchemy-0.6.0.tar.gz
  • Upload date:
  • Size: 17.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for rlsalchemy-0.6.0.tar.gz
Algorithm Hash digest
SHA256 6f84984c4acaf06c59c21c24dc9d9e97ab40bd54d186c188ae695b72d1999293
MD5 3060c8dea1afbe095a65feb7739ceda1
BLAKE2b-256 56ce16228657d847cf9f6342d61cfd0d2c5664c981e9b769433af161854e9f1d

See more details on using hashes here.

Provenance

The following attestation bundles were made for rlsalchemy-0.6.0.tar.gz:

Publisher: publish.yml on phvv-me/rls

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file rlsalchemy-0.6.0-py3-none-any.whl.

File metadata

  • Download URL: rlsalchemy-0.6.0-py3-none-any.whl
  • Upload date:
  • Size: 26.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for rlsalchemy-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 095bbe035c7cc8677d411a851adb2a17fbfbcde9ecba3cef53ed42090bb857b9
MD5 369a69b2ba714fed22814e60915c46b2
BLAKE2b-256 92672c03f435898d2a82cb314e939fa51f80e656e73d6c0ebb8f436d169f3128

See more details on using hashes here.

Provenance

The following attestation bundles were made for rlsalchemy-0.6.0-py3-none-any.whl:

Publisher: publish.yml on phvv-me/rls

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.6.0 This release

2 files

0.5.1

1 file

0.4.1

1 file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page