Files
postgrest/test/io/test_rls.py
jimmy 7989108b0b feat: expose row-level can_edit/can_delete on select *
Compute per-row editability and deletability from a table's row-level
security policies and return them as synthetic columns so clients can
hide edit/delete affordances for rows the user cannot change.

- Introspect pg_policies and relrowsecurity at schema-cache load and
  combine the UPDATE/DELETE USING qualifiers per table (permissive OR,
  restrictive AND).
- Store the combined qualifiers on Table and inject can_edit/can_delete
  as computed select fields when expanding `select *`, only for
  RLS-enabled tables with a matching policy (COALESCE'd to a boolean).
- Keep the computed columns out of the OpenAPI spec so they are not
  rendered as regular fields.
- Add a cfExpression field to CoercibleField to carry raw SQL
  expressions through the planner to SqlFragment.
2026-08-29 11:30:21 +02:00

64 lines
2.1 KiB
Python

from config import SECRET
from postgrest import run
from util import jwtauthheader
def author_headers(account_id):
"Authorization header for postgrest_test_author with the given account id."
return jwtauthheader(
{"role": "postgrest_test_author", "account_id": account_id}, SECRET
)
def test_rls_can_edit_can_delete(defaultenv):
"select * on an RLS table exposes can_edit/can_delete computed from the policies"
env = {**defaultenv, "PGRST_JWT_SECRET": SECRET}
with run(env=env) as postgrest:
response = postgrest.session.get("/rls_items", headers=author_headers(1))
assert response.status_code == 200
rows = {r["id"]: r for r in response.json()}
# rows visible to account_id=1: own row and the public row
assert set(rows) == {1, 2}
# the own row can be edited and deleted
assert rows[1]["can_edit"] is True
assert rows[1]["can_delete"] is True
# the public row is visible but not editable or deletable
assert rows[2]["can_edit"] is False
assert rows[2]["can_delete"] is False
def test_no_rls_omits_can_edit_can_delete(defaultenv):
"select * on a table without RLS omits the computed columns"
env = {**defaultenv, "PGRST_JWT_SECRET": SECRET}
with run(env=env) as postgrest:
response = postgrest.session.get("/no_rls_items", headers=author_headers(1))
assert response.status_code == 200
rows = response.json()
assert len(rows) == 2
for row in rows:
assert "can_edit" not in row
assert "can_delete" not in row
def test_rls_columns_not_in_openapi(defaultenv):
"The OpenAPI spec must not advertise the computed columns"
env = {**defaultenv, "PGRST_JWT_SECRET": SECRET}
with run(env=env) as postgrest:
response = postgrest.session.get("/", headers=author_headers(1))
assert response.status_code == 200
spec = response.json()
properties = spec["definitions"]["rls_items"]["properties"]
assert "can_edit" not in properties
assert "can_delete" not in properties