The Flask/SQLAlchemy sibling of django-rls. Same
idea — PostgreSQL row-level security instead of .filter() discipline —
rebuilt on SQLAlchemy Core and the Flask request lifecycle. The port was
not a find-and-replace: most of the design work went into the connection
pool, where a stale session variable on a recycled connection is exactly
the kind of leak RLS exists to prevent.
How it works
Policies register against tables and compile to real CREATE POLICY
DDL. Tenant- and user-scoped policies come ready-made, and
ExpressionPolicy composes predicates from plain SQLAlchemy Core
expressions:
from flask_rls import TenantPolicy, ExpressionPolicy, RLS
from sqlalchemy import column, true
rls.register("invoices", TenantPolicy("tenant_isolation", "tenant_id"))
rls.register(
"projects",
ExpressionPolicy(
"project_access",
expr=(column("owner_id") == RLS.user_id()) | (column("is_public") == true()),
),
)
At each transaction begin, the extension reads g.tenant_id /
g.user_id and issues SELECT set_config('rls.tenant_id', ..., true).
No context set → the variable is NULL → policies match zero rows:
fail closed, not fail open. Outside a request (jobs, CLI, tests),
rls.override(tenant_id=42) switches identity explicitly and
rls.bypass() emits no context at all — and is deliberately not
god-mode: true cross-tenant access requires a PostgreSQL BYPASSRLS
role.
Under the hood
- Context is set with
is_local=true, so it dies with the transaction — it cannot survive onto a pooled connection and leak into the next request. This is a deliberate divergence from django-rls’s session-scoped context. FORCE ROW LEVEL SECURITYis emitted for every registered table, so the table owner is constrained too — the classic owner-bypass gotcha.- Alembic operations (
op.enable_rls,op.force_rls,op.create_policy, …) ship in an optional extra, andflask rls sqldumps the full policy DDL for review or manual application. - ORM-agnostic: works with bare SQLAlchemy or Flask-SQLAlchemy; config
keys (
RLS_GUC_PREFIX,RLS_REQUIRE_CONTEXT,RLS_DEBUG, …) tune the strictness.
Runs on Python 3.10+, SQLAlchemy 2.0+, Flask 2.2+, and PostgreSQL 12+.
Published on PyPI as flask-rls (BSD-3-Clause), with docs at
flask-rls.com. Full story in
the deep dive.