feat: make db a real non-superuser app_user client (RLS enforced), bump pgpm/postgres - #7
Conversation
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
Review complete. 🔴 1 critical · 🟠 6 high 💬 Inline comments (2)
📍 Findings outside the diff (5) — 🔴 1 critical · 🟠 4 high — defects on lines GitHub can't attach comments to🔴 Critical — set_context called before transaction started · src/pgsql_test/connect.py · At line 481 in connect.py, 🟠 High — SQL syntax corrected with GRANT statements added · tests/test_example.py · The CREATE TABLE statements in test_example.py (line 651-653) and test_pets_rollback.py (line 665-667) were modified to add 🟠 High — Context application moved from before_each to per-query · src/pgsql_test/client.py · The 🟠 High — default_role parameter threading mismatch · src/pgsql_test/manager.py · The 🟠 High — clear_context may not fully reset non-role GUCs · src/pgsql_test/client.py · The This PR changes how the pgsql-test client authenticates and applies database context, shifting test connections from a superuser
Note: the Reviewed commit: 67d42f8 |
There was a problem hiding this comment.
PR #7 switches test clients from superuser to a non-superuser app_user/anonymous role and rewrites context/RLS handling, moving context application from before_each() to per-query.
Key findings
- 🔴 set_context called before transaction started — connect.py
- 🟠 SQL syntax corrected with GRANT statements added — test_example.py
- 🟠 Context application moved from before_each to per-query — client.py
- 🟠 default_role parameter threading mismatch — manager.py
- 🟠 clear_context fails to reset RLS GUCs — client.py:230
- 🟠 clear_context may not fully reset non-role GUCs — client.py
- 🟠 conn.db is now non-superuser app client — connect.py:206
| else: | ||
| cur.execute(pgsql.SQL("SET LOCAL ROLE {}").format(pgsql.Identifier(value))) | ||
| else: | ||
| cur.execute("SELECT set_config(%s, %s, true)", (key, value)) |
There was a problem hiding this comment.
🟠 bug · high
clear_context fails to reset RLS GUCs
In _apply_context (src/pgsql_test/client.py:230), a cleared context key with value None runs SELECT set_config(%s,%s,true) passing SQL NULL as the new value; PostgreSQL treats a NULL new_value as no-op (it never assigns NULL to a GUC), so the prior transaction-local setting from an earlier query in the same transaction stays active. As a result clear_context() (client.py:216) nulls the in-memory dict but does not actually clear the live RLS variables, so policies keyed on e.g. jwt.claims.user_id keep seeing the stale value across a context reset.
The same stale-state risk applies to any caller passing None in set_context() to mean reset (per its docstring).
📋 Prompt for AI Agents
In src/pgsql_test/client.py at line 230 in _apply_context(), handle a None value for non-role keys explicitly instead of passing it to set_config, since PostgreSQL set_config() with a NULL new_value is a no-op. Change the else branch so that when value is None you run cur.execute('SELECT set_config(%s, %s, true)', (key, '')) (or a RESET-style clear) to actually remove the transaction-local variable, and keep the non-None path passing the string normally. This ensures clear_context() genuinely clears RLS settings as its docstring promises.
| # The app-level client: a real non-superuser connection so RLS policies apply | ||
| db_config: PgConfig = {**test_config, "user": app_user, "password": app_password} | ||
| db = manager.get_client(db_config, default_role=default_role) | ||
| db.set_context({"role": default_role}) |
There was a problem hiding this comment.
🟠 high
conn.db is now non-superuser app client
get_connections() previously returned db as the same superuser connection as pg (the removed comment read "For now, db is the same as pg (both superuser)"); it now returns a distinct app_user client with SET LOCAL ROLE anonymous applied (connect.py:207-209). Existing callers that used conn.db for DDL (CREATE TABLE, GRANT), admin operations, or superuser-bypassing reads will now receive InsufficientPrivilege errors and see role-filtered rows; the db field's connection-level contract changed from superuser to app-scoped.
📋 Prompt for AI Agents
In src/pgsql_test/connect.py lines 206-209, the get_connections() return value db changed from the superuser pg connection to a dedicated non-superuser app_user client. Update the module docstring, ConnectionResult docstring (connect.py lines 54-65), and README to clearly state that db is a non-superuser, RLS-subject client and that DDL/admin operations must be performed via conn.pg; add a CHANGELOG entry noting the breaking behavior change in the 0.3.0 release.
Summary
get_connections()previously returneddb = pg(both superuser, README said "same as pg for now"), so RLS tests ondbwere testing nothing — superusers bypass RLS. This brings the Python port to parity with the TSpgsql-testgetConnectionsflow:PgTestClientcontext now mirrors the TS client:role->SET LOCAL ROLE "<role>", everything else ->set_config(key, value, true), re-applied before everyquery()so it survives the wholebefore_each()/after_each()window. Addedget_context(),clear_context()(nulls GUCs, restores default role). NewConnectionOptions.connection/.roles(AppConnection,RoleMapping) to override the defaults.Behavioral change for consumers: tables created via
pg/seeds are not visible todbuntil granted (as in production and in the TS package). Repo tests updated accordingly (setup/asserts onpg,GRANT ... TO anonymousin fixtures); pgpm fixture migration now grantsUSAGEontest_appto the app roles.tests/test_rls.pyproves it end-to-end against Postgres:pgis superuser and sees all rows;dbsession_user isapp_user,current_userisanonymous, getsInsufficientPrivilegewithout grants,authenticated+jwt.claims.user_idis filtered by SELECT/INSERT policies, andclear_context()restoresanonymous.Also: CI
PGPM_VERSION2.7.9 -> 5.30.6, Node 20 -> 22,postgres-plus:17->:18(latest@pgpm/fakerusesuuidv7(), PG18-only), package version 0.3.0.Link to Devin session: https://app.devin.ai/sessions/b0917661c1224564b5c5df42de52395d
Open in Devin Desktop: https://app.devin.ai/desktop/session/b0917661c1224564b5c5df42de52395d?variant=devin
Requested by: @pyramation