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.
64 lines
2.1 KiB
Python
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
|