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.
What it does
Point it at your database. rlsgrid reads the live schema and:
- Maps every
role × table × operationand labels itallow/deny/conditional/unrestricted. - 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.
- 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
pip install rlsgrid
export DATABASE_URL=postgresql://user:pw@host/db # use staging, never prod
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: check
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 CHECKexpression 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)): settenancy.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.
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 rlsgrid-0.5.2.tar.gz.
File metadata
- Download URL: rlsgrid-0.5.2.tar.gz
- Upload date:
- Size: 62.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f5ea762fda3a5b6acdbd5ed1a289b3a6ef86ba531a4a599e0163bd4ad330ec02
|
|
| MD5 |
68ed8b17cc0bf89afe28df4c9f5fcac2
|
|
| BLAKE2b-256 |
5911bb70fd35591544cf675f0551d4d273d92ce66b6579d80dff47387ca384f5
|
Provenance
The following attestation bundles were made for rlsgrid-0.5.2.tar.gz:
Publisher:
release.yml on matte97p/rlsgrid
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
rlsgrid-0.5.2.tar.gz -
Subject digest:
f5ea762fda3a5b6acdbd5ed1a289b3a6ef86ba531a4a599e0163bd4ad330ec02 - Sigstore transparency entry: 1643785609
- Sigstore integration time:
-
Permalink:
matte97p/rlsgrid@687e4703771e6cc0c2a6c90f782038ff2547dd4b -
Branch / Tag:
refs/tags/v0.5.2 - Owner: https://github.com/matte97p
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@687e4703771e6cc0c2a6c90f782038ff2547dd4b -
Trigger Event:
push
-
Statement type:
File details
Details for the file rlsgrid-0.5.2-py3-none-any.whl.
File metadata
- Download URL: rlsgrid-0.5.2-py3-none-any.whl
- Upload date:
- Size: 43.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f58ef5b066f14878bb6f8174cb3c9a975f519839da41ac63e1ae284991a794bf
|
|
| MD5 |
298bebd77db5fa28ff725e7ffb14c7b9
|
|
| BLAKE2b-256 |
18436163084335e1fd388eaebd9ba16e220e3ee15bf27391bb6158db2f2f4b48
|
Provenance
The following attestation bundles were made for rlsgrid-0.5.2-py3-none-any.whl:
Publisher:
release.yml on matte97p/rlsgrid
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
rlsgrid-0.5.2-py3-none-any.whl -
Subject digest:
f58ef5b066f14878bb6f8174cb3c9a975f519839da41ac63e1ae284991a794bf - Sigstore transparency entry: 1643785651
- Sigstore integration time:
-
Permalink:
matte97p/rlsgrid@687e4703771e6cc0c2a6c90f782038ff2547dd4b -
Branch / Tag:
refs/tags/v0.5.2 - Owner: https://github.com/matte97p
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@687e4703771e6cc0c2a6c90f782038ff2547dd4b -
Trigger Event:
push
-
Statement type: