Skip to main content

Schema-driven Row-Level Security test matrix generator and cross-tenant fuzzer for PostgreSQL and Supabase.

Project description

rlsgrid

Catch cross-tenant Row-Level Security leaks in Postgres and Supabase before your users do.

CI PyPI Downloads GitHub stars License: MIT Python

rlsgrid plan and fuzz output


What it does

Point it at your database. rlsgrid reads the live schema and:

  1. Maps every role × table × operation and labels it allow / deny / conditional / unrestricted.
  2. Fuzzes for real cross-tenant leaks — it seeds synthetic tenants and actively tries to read, insert, update, and delete one tenant's rows from another tenant's session.
  3. Emits a pgTAP suite you can run in CI.

Why

Postgres RLS is powerful and easy to get subtly wrong: a missing WITH CHECK, a FOR ALL where you meant FOR SELECT, a forgotten ENABLE ROW LEVEL SECURITY, a service_role bypass leaking client-side. Your application unit tests will not catch any of these — they test your code, not the policies. rlsgrid tests the policies, against a real database.

Use it

sudo apt install python3-venv -y
python3 -m venv ~/venvs/rlsgrid-env
source ~/venvs/rlsgrid-env/bin/activate
pip install rlsgrid
# if ipv6
export DATABASE_URL=postgresql://user:pw@host/db   # use staging, never prod
# if ipv4
getent ahosts db-host | grep STREAM | grep 4
export DATABASE_URL=postgresql://user:pw@<response>/db
rlsgrid init --from-db      # read the schema, write an annotated config
rlsgrid check --tenants 5   # seed → fuzz → teardown. exit 1 on any leak.

check is the whole loop: it leaves nothing behind and returns non-zero on a breach, so it drops straight into CI. A leak looks like:

✗ 1 cross-tenant breach detected
  LEAK role=authenticated actor_tenant=a1b2 → target_tenant=c3d4
       on public.documents UPDATE: target-owned row visible across tenants

In CI (GitHub Action)

- uses: matte97p/rlsgrid@v1
  with:
    command: fuzz   # seeds synthetic tenants, probes isolation, fails the build on any leak
    database-url: ${{ secrets.STAGING_DB_URL }}

Lower-level commands

rlsgrid introspect          # tables, RLS state, policies
rlsgrid plan --explain      # the full matrix, with a "why" column
rlsgrid gen pgtap --out tests/rls/generated.sql   # emit a pgTAP suite
rlsgrid fuzz --tenants 5    # fuzz only (auto-cleans up)
rlsgrid seed --dry-run      # show the seed plan without writing
rlsgrid check --sarif-out rls.sarif   # SARIF for GitHub code scanning

From pytest

Installing rlsgrid registers a rlsgrid fixture, so you can gate your existing suite:

def test_no_cross_tenant_leaks(rlsgrid):
    report = rlsgrid.check()
    assert report.ok, [b.detail for b in report.breaches]

Point it with --rlsgrid-config path/to/rlsgrid.toml.

Config for your stack — Supabase, Prisma, Drizzle, SQLAlchemy, Rails, function-based access checks — is in docs/RECIPES.md.

How it classifies a cell

  • allow — a permissive policy applies and gates nothing.
  • deny — RLS is on and no policy matches the role/op.
  • conditional — a policy applies but a USING / WITH CHECK expression gates which rows. This is where the fuzz earns its keep.
  • unrestricted — RLS is off, or the role has BYPASSRLS. Surfaced explicitly so you notice when you did not mean it.

Two enforcement models

  • RLS at the database (the Supabase default): the fuzz finds leaks directly. Set tenancy.mode = "jwt".
  • Access enforced by a SQL function (e.g. check_user_has_access_to_store(user_id, store_id)): set tenancy.mode = "function" and rlsgrid calls the helper with cross-tenant arguments, asserting it returns false.

How it compares

hand-written pgTAP static linters rlsgrid
New table lands without a test silent maybe shows up in plan
Cross-tenant write leaks only if you wrote that test no probed automatically
Function-based access no no first-class
Setup per-test low one config

It composes with supabase-test-helpers: keep your bespoke business-rule pgTAP, let rlsgrid watch the floor.

Safety

seed, fuzz, and check write to the database, so they refuse any URL matching [safety].forbid_url_patterns (default ["prod", "production"]). Point DATABASE_URL at staging or a disposable database.

Status

Alpha, but exercised end to end in CI against a rich multi-tenant schema and run through pg_prove. The config shape may still shift before 1.0. Issues and PRs welcome — see CONTRIBUTING.

Built by Matteo Perino while shipping GeoSuite, a multi-tenant Supabase app.

License

MIT — see LICENSE.

Related tools

Part of my open-source toolkit — github.com/matte97p:

  • pentest-framework — low-noise pentest orchestration, normalized to one schema and rendered to a PDF
  • demowright — record polished product-demo videos from a script that lives in your repo
  • GeoSuite CLIs — zero-dep Generative Engine Optimization toolkit

⭐ If rlsgrid caught a leak (or proved you safe), give it a star — it helps other Supabase teams find it.


🌐 Built by Matteo Perinomatteoperino.dev

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

rlsgrid-0.5.7.tar.gz (65.6 kB view details)

Uploaded Source

Built Distribution

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

rlsgrid-0.5.7-py3-none-any.whl (43.6 kB view details)

Uploaded Python 3

File details

Details for the file rlsgrid-0.5.7.tar.gz.

File metadata

  • Download URL: rlsgrid-0.5.7.tar.gz
  • Upload date:
  • Size: 65.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for rlsgrid-0.5.7.tar.gz
Algorithm Hash digest
SHA256 36b5226639167d8ba3526c05bbea1660ce0fc80f6f01176b7c20bb5c8ecbb407
MD5 ab0e9d8af714ac1186fce601c7db995b
BLAKE2b-256 46d56df1e561d8587b63975978271bf72fc03e65a3cfe3c3963dacdd756d5e48

See more details on using hashes here.

Provenance

The following attestation bundles were made for rlsgrid-0.5.7.tar.gz:

Publisher: release.yml on matte97p/rlsgrid

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

File details

Details for the file rlsgrid-0.5.7-py3-none-any.whl.

File metadata

  • Download URL: rlsgrid-0.5.7-py3-none-any.whl
  • Upload date:
  • Size: 43.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for rlsgrid-0.5.7-py3-none-any.whl
Algorithm Hash digest
SHA256 ccf5992c34bbf65377fde09f74b2396234d9cd521d40f86307f739ae38f9083d
MD5 1cd239d957b5555c2cab906b5d0bf6d4
BLAKE2b-256 95e5a8fcde842c61f01fb34a65eb1e1d5055e1e8227e185d858bcc567e8a7b0b

See more details on using hashes here.

Provenance

The following attestation bundles were made for rlsgrid-0.5.7-py3-none-any.whl:

Publisher: release.yml on matte97p/rlsgrid

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