feat(rls): activate RLS at runtime via split migrator/app DB roles #224
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/pms-285-rls-runtime-role-split"
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?
Activate the fail-closed RLS policy (PMS-257) at runtime by splitting the database connection into two roles so the request-serving connection physically cannot bypass per-tenant Row Level Security. A forgotten
WHERE tenant_idfilter now fail-closes to zero rows instead of leaking across the personal-tenant boundary.Databasenow holds two pools:app_pool(rolemokosh_app, NOSUPERUSER NOBYPASSRLS) returned bypool()andbegin_with_tenant, used for every request-serving query; andmigrator_pool(rolemokosh_migrator, BYPASSRLS) used for migrations, bootstrap, the cross-tenant background workers, and the explicitly-justified pre-auth / cross-tenant paths.AppConfiggainsMOKOSH_APP_DATABASE_URL(falls back toDATABASE_URLso a single-role dev box still boots with RLS inert).run_migrationsand the SSO bootstrap move to the migrator pool.Every raw
self.pool()serving query is routed: tenant-scoped reads/writes go throughbegin_with_tenant(GUC set); genuinely cross-tenant / pre-auth / worker sites move tomigrator_pool(), each with a// SAFETY:note. Named paths on the migrator pool: the Google(provider,subject)lookup +last_used_atupdate, password-reset token verify,find_user_placement,rehome_user_between_tenants,invitations::newest_pending_for+accept, thetenantssuper-admin handlers,copy_default_config(reads the default tenant, writes the new one), the notifications dispatcher / SLA sweep / contract lifecycle / recurring-invoicing / calendar-reminder / RMM-sync workers, and the startup admin bootstrap. Reads of the RLS-exempttenantsroot and tenant-scoped reads of RLS-coveredsla_targetsare handled explicitly. Two latent bugs fixed:sla::upsert_targetandslaclock read dropped the tenant tx before touchingsla_targets, which would have failed WITH CHECK / fail-closed under the app role.Step 9 (
newest_pending_for): resolved as decision (a) - kept as a deliberate pre-auth cross-tenant bridge on the migrator pool with a SAFETY note, because it runs before the user is placed in any tenant and reshaping it to a tenant-scoped query is not possible there; under the app role with no GUC it would fail closed and break invite acceptance.Step 8 (
from_trustedaudit): everyTenantId::from_trustedcall site now carries a current SAFETY note (28 sites on this branch; the issue's "53" predates the PMS-258/PMS-236 merges), completing PMS-261 AC2.Roles are provisioned by a new
mokosh-bootstrap provision-rolessubcommand (idempotent: creates the roles, grantsmokosh_appSELECT/INSERT/UPDATE/DELETE on tables, USAGE/SELECT on sequences, EXECUTE on functions, plusALTER DEFAULT PRIVILEGES FOR ROLE mokosh_migratorso future migrations auto-grant;mokosh_appowns nothing). The devcompose.dev.ymlpostgres entrypoint provisions both roles on boot and the server pointsDATABASE_URL/MOKOSH_APP_DATABASE_URLat them, sojust devboots with the split;.env.exampledocuments the new knobs.Tests: the integration harness gains
boot_rls, which builds the app pool as a fresh NOBYPASSRLS role against the per-test database;tests/per_user_isolation.rsruns its request queries through it so the HTTP suite exercises RLS rather than only the app-layerWHEREfilters, plus a newapp_role_read_is_fail_closed_without_gucregression proving a no-GUC read returns zero rows through the actual application role.#PMS-285