Skip to main content

Tenant isolation as a Postgres guarantee — force-RLS + least-privilege grants, as reusable SQL.

Project description

pantheon-rls

tests PyPI Python License

Tenant isolation as a Postgres guarantee — force-RLS + least-privilege grants, as reusable SQL. Zero required dependencies.

The guarantee: tenant A cannot read or write tenant B's rows — enforced by the database, not by a WHERE tenant_id = ? you have to remember on every query. Forget one query and you leak; with this, the boundary is in the engine, below the app.

Extracted from PANTHEON, a multi-tenant AI substrate, where it's the isolation boundary under every scoped table. Companion to pantheon-credit-ledger (which uses exactly this).

What it does

Three statements per scoped table make isolation real:

from pantheon_rls import enable_rls_sql, grant_crud_sql, bind_tenant

# 1. Once, in your migration, as the privileged (table-owning) role:
for stmt in enable_rls_sql("invoices"):
    conn.execute(stmt)
conn.execute(grant_crud_sql("invoices", "app_role"))   # your NON-superuser runtime role

# 2. Per request, on your app connection:
bind_tenant(session, tenant_id)   # every statement after this sees only this tenant's rows

enable_rls_sql emits ENABLE + FORCE row-level security + a fail-closed policy:

ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
ALTER TABLE invoices FORCE ROW LEVEL SECURITY;
CREATE POLICY invoices_tenant_isolation ON invoices
  USING      (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid)
  WITH CHECK (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid);

The two things people get wrong

  • FORCE, not just ENABLE. Without FORCE, the table owner bypasses the policy — so your migration/maintenance connection (and often your app, if it owns the tables) silently sees everything. FORCE subjects the owner too.
  • A superuser / BYPASSRLS role ignores RLS entirely. So the app must connect at runtime as a NOSUPERUSER NOBYPASSRLS role; migrations use the privileged one. Miss this and every dev box (usually superuser) looks perfectly isolated while production leaks. grant_crud_sql exists to point the four CRUD verbs at that non-privileged role.

And it's fail-closed: current_setting(name, true) is NULL when unset, NULLIF maps ''NULL, and col = NULL is never true — so no tenant context means zero rows, never "all rows".

Safe to call with non-constant identifiers

Table / column / role / GUC names are validated to be plain [schema.]identifiers (anything else raises ValueError), and the tenant id at bind time is passed as a bind parameter, never interpolated — so these helpers don't become a SQL-injection vector even if a name comes from config.

Install

pip install pantheon-rls                 # SQL helpers only, zero dependencies
pip install "pantheon-rls[sqlalchemy]"   # + bind_tenant() convenience (else run set_config with your own driver)
# or copy the single pantheon_rls.py file

Scope

This is the isolation primitive — the RLS DDL + grant helpers + the per-transaction tenant bind. It is not an ORM, a migration tool, or a full multi-tenancy framework; it's the ~60 lines that make Postgres itself refuse cross-tenant access, done right (FORCE + non-privileged role + fail-closed).

License

Apache-2.0. See LICENSE.

Project details


Download files

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

Source Distribution

pantheon_rls-0.1.0.tar.gz (9.1 kB view details)

Uploaded Source

Built Distribution

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

pantheon_rls-0.1.0-py3-none-any.whl (9.6 kB view details)

Uploaded Python 3

File details

Details for the file pantheon_rls-0.1.0.tar.gz.

File metadata

  • Download URL: pantheon_rls-0.1.0.tar.gz
  • Upload date:
  • Size: 9.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pantheon_rls-0.1.0.tar.gz
Algorithm Hash digest
SHA256 616197bb8d645edcd417e41027e1eac16bfd71242a2d1e9abd0dc09d35e990e1
MD5 64369fcde9b78f817f2506f7ac814103
BLAKE2b-256 d456001ed9b64365684529b58959770427020e71768653f8ba3abca82fe851bc

See more details on using hashes here.

Provenance

The following attestation bundles were made for pantheon_rls-0.1.0.tar.gz:

Publisher: publish.yml on Igfray/pantheon-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 pantheon_rls-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: pantheon_rls-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 9.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pantheon_rls-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0ce537b100344baa27fb1eeeb29fc6c79e91d71937b58ba245ff86627eeeaf9e
MD5 64a09ffcaa3aac8ad83e97c92979875f
BLAKE2b-256 931cdd90e42e4026669a4776ca2b59801e5b12722f26eb178f23911b4dad0e58

See more details on using hashes here.

Provenance

The following attestation bundles were made for pantheon_rls-0.1.0-py3-none-any.whl:

Publisher: publish.yml on Igfray/pantheon-rls

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

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page