Skip to main content

flask-rls

License Python SQLAlchemy

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 sql to 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.

Download files

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

Source Distribution

flask_rls-0.1.1.tar.gz (296.2 kB view details)

Uploaded Source

Built Distribution

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

flask_rls-0.1.1-py3-none-any.whl (17.9 kB view details)

Uploaded Python 3

File details

Details for the file flask_rls-0.1.1.tar.gz.

File metadata

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

File hashes

Hashes for flask_rls-0.1.1.tar.gz
Algorithm Hash digest
SHA256 f2d20a820c919a33f1c933b34d5d73b6274daae10810d8c981a541f9e5a71c09
MD5 0ba02cf82dcc4b47c2a07853f0ddbf21
BLAKE2b-256 abf3496fc63e7e932dccb76ec1cfed230d9cc90e4915dff1e053934ae82f386c

See more details on using hashes here.

Provenance

The following attestation bundles were made for flask_rls-0.1.1.tar.gz:

Publisher: publish.yml on kdpisda/flask-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 flask_rls-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: flask_rls-0.1.1-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

Hashes for flask_rls-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 8436d9cca427babc95a222fd9fa94372a82d1f74934e0c88f8d2c8af6eb56d36
MD5 cb462f95ddc7d0484a5914df7b899fa9
BLAKE2b-256 12ef844ca80a7eb372c0b3384c962b96879e99516c0f5d6eb8dea024bfef5b9a

See more details on using hashes here.

Provenance

The following attestation bundles were made for flask_rls-0.1.1-py3-none-any.whl:

Publisher: publish.yml on kdpisda/flask-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.1.1 This release

2 files

0.1.0

2 files

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