fix(rls): set the tenant GUC on serving reads of RLS-covered tables (PMS-692) #488
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/PMS-692-tenant-guc-rls-reads"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
PMS-692: set the tenant GUC on serving reads of RLS-covered tables
With the PMS-285 role split active (the dev stack already wires it),
Database::pool()returns the unprivilegedmokosh_app(NOBYPASSRLS) connection. Migration 038 put a fail-closedtenant_isolationpolicy on everytenant_idtable, so any serving query that runs on the bare pool WITHOUT setting theapp.current_tenantGUC (viabegin_with_tenant) matchestenant_id = NULL- zero rows on reads,WITH CHECKrejection on writes. This fixes every cited call site.The concrete breakages fixed
contact_id, tenant resolved from the row) moves tomigrator_pool()with a SAFETY note. It fail-closed on the app pool, so no customer could ever set a portal password.SearchServiceheld a barePgPool;GET /searchreturned an empty 200. Now takesDatabaseand runs all ten scans in onebegin_with_tenanttransaction.begin_with_tenant(converted by earlier PMS-256/285 work); covered by a regression assertion.AttachmentServiceheld a barePgPool; the INSERT'sWITH CHECKwas rejected. Now takesDatabase(reads use an inline tenant tx, writes commit explicitly).is_user_tombstoned(tenant-unscoped by design) moves tomigrator_pool()with a SAFETY note - on the app pool it always read "not tombstoned", making the MAPPS-348 410 branch dead code.Also swept
begin_with_tenant;settings::read_email_intake_default_companytakes the tenant-GUC connection like itsread_ci_impact_max_depthsibling. The bunyip account-deleted webhook (pre-auth, cross-tenant) moves tomigrator_pool().ticket_sequencesprobe), sla-worker accessor renamedpool()->migrator_pool(), and the unusedAuthService::pool()accessor removed..pool()call (RLS-exempttenantsreads, pre-auth paths) carries an explicit// SAFETY (PMS-285)note.Guardrail + regression
scripts/check-pool-safety.nu(wired into check.yml) fails any new serving.pool()call lacking an adjacent// SAFETY (PMS-285note.tests/rls_serving_reads.rsuses theboot_rls/build_app_role_poolharness (request pool as a realNOSUPERUSER NOBYPASSRLSrole) to assertGET /searchreturns the seeded row andis_user_tombstonedreadstruefor a soft-deleted user - both of which fail-closed before this change.Acceptance
grep -rn '.pool()' src/ | grep -v migrator_poolreturns only SAFETY-noted sites, each naming an RLS-exempt (tenants) or pre-auth read.**/service.rs/routes.rsholds a barePgPoolfield for tenant-scoped queries (Search + Attachment converted).from_pools+NOBYPASSRLSasserts non-emptyGET /searchandis_user_tombstoned == true(saved-reports/portal set-password covered by existing suites).db.pool()without the SAFETY note.Verification
clippy --all-targets -D warnings,fmt, all four nu gates, and the auth / RLS / portal / email_intake / tickets / saved-reports suites (13 suites incl. the new one) pass against a throwaway Postgres. Not run against live staging.