fix(rls): complete the fail-closed backstop on 11 deferred tables (PMS-683) #467
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/PMS-683-rls-tail"
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?
What
Complete the fail-closed RLS backstop that PR #464 began: migrate the 6 feature services behind the 11 deferred tenant-scoped tables onto
Database::begin_with_tenant, enable RLS on all 11 tables in migration095_rls_deferred_tables.sql, and shrinktests/rls_coverage.rs'sALLOWED_WITHOUT_RLSallowlist to empty. Every tenant-scopedpublictable now has RLS enabled, forced, and atenant_isolationpolicy.Why
The request-serving pool (
mokosh_app) is NOBYPASSRLS, so enabling RLS on a table actively constrains every app query against it: a query that does not set theapp.current_tenantGUC viabegin_with_tenantfail-closes to zero rows. PR #464 could only safely coverquotes/quote_sequencesbecause the other 11 tables' services still queried the raw pool. This change migrates those services so the tables are GUC-safe, then turns RLS on for them, restoring the tenant-isolation invariant the allowlist was tracking as debt.Changes
pool: PgPool->db: Database; every tenant-scoped statement now runs insidebegin_with_tenant, mirroringquotes/contacts):dashboards(saved_dashboards, scheduled_dashboards),approvals(ticket_approvals, change_requests),email_intake(tenant_intake_tokens, email_intake_log),saved_reports(saved_reports, scheduled_reports),workflows(workflow_rules, workflow_rule_runs),ticket_templates(ticket_templates).tenant_intake_tokensspecial case: the cross-tenantresolve_tokenSHA-256 lookup (and itslast_used_atbump) run on the BYPASSRLSmigrator_poolwith an explicit// SAFETY:note, since resolve-by-hash has no tenant context to set as the GUC; every other access is tenant-scoped.migrations/095_rls_deferred_tables.sql: ENABLE + FORCE ROW LEVEL SECURITY and a fail-closedtenant_isolationpolicy on all 11 tables, mirroring the 038/090/091/094 shape exactly.tests/rls_coverage.rs:ALLOWED_WITHOUT_RLSis now empty; the test asserts (both directions) that every tenant table has RLS and nothing stale remains.src/api/router.rs,src/main.rsworkers,tests/scheduled_reports.rs,tests/scheduled_dashboards.rs) to passdb.clone().approvalscreate()ticket-existence check, theapprovalsroutes parent-existence check (time_entries/change_requests/quotes, now via a newApprovalsService::db()accessor), andsaved_reportsexecute()running its compiled entity query (tickets etc.) - all now insidebegin_with_tenant.Tests
just pre-commit(mirrors CI check.yml): PASSED, exit 0. fmt + clippy--all-targets -D warnings+ check--all-targetsclean; unit tests 293 passed / 0 failed; doc tests 2 passed / 0 failed / 3 ignored.--test-threads=4,--no-fail-fast): all 18 targeted binaries PASSED, 0 failed, exit 0. Key results:rls_coverageok (confirms all 11 tables now have RLS enabled + forced + atenant_isolationpolicy and the allowlist is empty and consistent in both directions);per_user_isolation4 ok andrls_isolation1 ok (the NOBYPASSRLS app-role path still isolates correctly); plusdashboards_crud,scheduled_dashboards(3),ticket_approvals,approvals_polymorphic(5),email_intake,email_intake_attachments,email_intake_phase2(3),intake_token_admin,saved_reports,saved_reports_execute,scheduled_reports(3),workflow_rules,workflow_rules_phase2,workflow_rules_phase3,ticket_templatesall ok.#[sqlx::test]'sboot_rlspath creates an unprivileged probe role, which requires CREATEROLE. The dev container's defaultDATABASE_URLrole (mokosh_migrator) lacks it, soper_user_isolation/rls_isolationonly run whenDATABASE_URLis the superuser role - exactly as CI runs them (integration.yml uses thepostgressuperuser). The suite above was run withDATABASE_URLset to the container'sMOKOSH_ADMIN_DATABASE_URL(the postgres superuser) to mirror CI; this is a pre-existing local-harness characteristic, unrelated to this change.begin_with_tenant(GUC set) or, for the one documented cross-tenantresolve_tokenpath, the BYPASSRLSmigrator_pool. No bareself.poolremains in any migrated service; the residualself.db.pool()calls inemail_intaketouch only non-target tables (users / contacts / tickets / ticket_notes / tenant_settings), whose RLS posture is unchanged by this migration.