PostgreSQL Row-Level Security for Flask and SQLAlchemy
Project description
flask-rls
PostgreSQL Row-Level Security for Flask and SQLAlchemy.
Security enforced by PostgreSQL, not by application code. Once a table has RLS enabled and
a tenant/user policy attached, the database itself returns only the rows the current
request is authorized to see — even for raw SQL or a forgotten .filter().
flask-rls is the Flask/SQLAlchemy sibling of
django-rls: the same concepts (TenantPolicy,
UserPolicy, CustomPolicy, Pythonic policies, RLS.user_id()), adapted to SQLAlchemy
Core and the Flask request lifecycle.
Features
- 🔒 Database-level Row-Level Security using PostgreSQL RLS
- 🏢 Tenant-based and user-based policies
- 🐍 Pythonic policies — compose predicates from SQLAlchemy Core expressions
- ⚡ Pool-safe — context is set per transaction via
set_config(..., is_local=true), so it cannot leak across pooled connections - 🧩 ORM-agnostic — works with bare SQLAlchemy or Flask-SQLAlchemy
- 🧱 Alembic migration operations (
op.enable_rls,op.create_policy, …) - 🛠️
flask rls sqlto dump policy DDL
Installation
pip install flask-rls # core
pip install "flask-rls[alembic]" # + Alembic migration operations
Requires Python 3.10+, SQLAlchemy 2.0+, Flask 2.2+, and PostgreSQL 12+.
Quick start
1. Initialize the extension
from flask import Flask, g
from flask_sqlalchemy import SQLAlchemy
from flask_rls import RLS
db = SQLAlchemy()
rls = RLS()
def create_app():
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "postgresql+psycopg://app@localhost/mydb"
db.init_app(app)
with app.app_context():
rls.init_app(app, engine=db.engine)
@app.before_request
def set_tenant():
g.tenant_id = current_tenant_id() # however your app resolves it
g.user_id = current_user_id()
return app
flask-rls reads g.tenant_id / g.user_id at each transaction begin and issues
SELECT set_config('rls.tenant_id', ..., true). No context set → the GUC is NULL →
policies match zero rows (fail closed).
2. Define and register policies
from flask_rls import TenantPolicy, UserPolicy, ExpressionPolicy, RLS
from sqlalchemy import column, true
rls.register("invoices", TenantPolicy("tenant_isolation", "tenant_id"))
# Pythonic policy: owner OR public
rls.register(
"projects",
ExpressionPolicy(
"project_access",
expr=(column("owner_id") == RLS.user_id()) | (column("is_public") == true()),
),
)
rls.register(...) feeds the flask rls sql dumper. Apply the DDL through Alembic (below)
or by piping flask rls sql into a migration.
3. Apply via Alembic
# migrations/env.py
from flask_rls.alembic import * # noqa: F401,F403 registers op.enable_rls, ...
# a migration
from flask_rls import TenantPolicy
def upgrade():
op.enable_rls("invoices")
op.force_rls("invoices")
op.create_policy("invoices", TenantPolicy("tenant_isolation", "tenant_id"))
def downgrade():
op.drop_policy("invoices", "tenant_isolation")
op.disable_rls("invoices")
Running privileged / background work
Outside a request there is no g, so context falls through to fail-closed. Use the scope
managers:
with rls.override(tenant_id=42): # privileged identity switch (jobs, CLI, tests)
generate_monthly_report()
with rls.bypass(): # emit no context — for non-RLS tables
create_new_tenant()
bypass() is not god-mode: on an RLS table it still sees zero rows. True cross-tenant
access requires a PostgreSQL role with BYPASSRLS (see the design doc).
Important: the owner-bypass gotcha
A table's owner (and superusers) bypass RLS entirely unless the table has
FORCE ROW LEVEL SECURITY. flask-rls emits FORCE for every registered table, and your
application should connect as a non-owner role regardless.
Configuration
| Flask config key | Default | Meaning |
|---|---|---|
RLS_GUC_PREFIX |
rls. |
GUC namespace prefix |
RLS_G_TENANT_KEY |
tenant_id |
flask.g attribute read for the tenant |
RLS_G_USER_KEY |
user_id |
flask.g attribute read for the user |
RLS_REQUIRE_CONTEXT |
False |
raise RLSContextRequiredError when context is missing |
RLS_DEBUG |
False |
debug-log each set_config call |
License
BSD 3-Clause — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file flask_rls-0.1.0.tar.gz.
File metadata
- Download URL: flask_rls-0.1.0.tar.gz
- Upload date:
- Size: 176.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4405584054422601890fc0b3ba8cd63ff92ba6387cf164426a312d2b0e06ad8f
|
|
| MD5 |
9bd1b7fbdd1104b6f1674f326d0f8800
|
|
| BLAKE2b-256 |
a59b16ce426b1fd34c4ef076cb77736cb03ef94d246fd9162d2f3c7c66c3b3d9
|
Provenance
The following attestation bundles were made for flask_rls-0.1.0.tar.gz:
Publisher:
publish.yml on kdpisda/flask-rls
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
flask_rls-0.1.0.tar.gz -
Subject digest:
4405584054422601890fc0b3ba8cd63ff92ba6387cf164426a312d2b0e06ad8f - Sigstore transparency entry: 2305689977
- Sigstore integration time:
-
Permalink:
kdpisda/flask-rls@35ef823f5dbc0a14fcfb6605cdac946f8d131f67 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/kdpisda
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@35ef823f5dbc0a14fcfb6605cdac946f8d131f67 -
Trigger Event:
release
-
Statement type:
File details
Details for the file flask_rls-0.1.0-py3-none-any.whl.
File metadata
- Download URL: flask_rls-0.1.0-py3-none-any.whl
- Upload date:
- Size: 17.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
81a08a865b936ce261e268ac824e2eb75cb62fbeef63aab4c4cfa59855417118
|
|
| MD5 |
d42bac18f92c04fb79697bdbd2385c98
|
|
| BLAKE2b-256 |
d3e16f92e2709f1ff7ea2cc15d5a0bea96d59a982f6851c0ef844bf7a66605b3
|
Provenance
The following attestation bundles were made for flask_rls-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on kdpisda/flask-rls
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
flask_rls-0.1.0-py3-none-any.whl -
Subject digest:
81a08a865b936ce261e268ac824e2eb75cb62fbeef63aab4c4cfa59855417118 - Sigstore transparency entry: 2305690096
- Sigstore integration time:
-
Permalink:
kdpisda/flask-rls@35ef823f5dbc0a14fcfb6605cdac946f8d131f67 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/kdpisda
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@35ef823f5dbc0a14fcfb6605cdac946f8d131f67 -
Trigger Event:
release
-
Statement type: