fix(rls): cover tenant-less tables; tenant-scope user_oauth_identities #207
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/pms-258-rls-cover-tenantless-tables"
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
Brings the five tables the dynamic RLS loop never covered (because they lack a
tenant_idcolumn, or carried a global unique key) under fail-closed row level security. This is PMS-258 (Step 3 of the PMS-255 epic), whose migration was authored locally during the original run but never merged - its file number was taken by the PMS-257 fail-closed flip, so these gaps were still open onmain.Why it matters
user_oauth_identitieshad a GLOBALUNIQUE (provider, subject). A single upstream identity (one Googlesub) could therefore exist only once across the whole database, so under personal-tenant isolation one user's identity row blocks every other tenant's and the link leaks across the per-user boundary. This was flagged CRITICAL in the epic.Changes
migrations/041_rls_cover_tenantless_tables.sql:user_oauth_identities: add denormalizedtenant_id(backfilled from the owning user, thenNOT NULL), replace the global unique key withUNIQUE (tenant_id, provider, subject), add an index, and attach the standard fail-closedtenant_isolationpolicy (USING + WITH CHECK + FORCE).kb_article_versions,invoice_lines,rate_card_items,sla_targets: fail-closed PARENT-JOIN policy (EXISTS over the parent'stenant_id). Chosen over a denormalized column to avoid backfill and INSERT-site drift; these rows are always reached through their parent. The expression mirrors 038 so behaviour matches the directly-scoped tables.src/modules/auth/service.rs: both Google-link INSERT sites populatetenant_id; the link-to-existing path now runs insidebegin_with_tenant(existing.tenant_id)so it satisfies WITH CHECK under a NOBYPASSRLS connection. A SAFETY note marks the pre-auth(provider, subject)lookup as a path PMS-285 must route onto the privileged pool once the app drops BYPASSRLS.tests/tenantless_table_rls.rs: same subject insertable in two tenants (no cross-tenant collision) while a same-tenant duplicate is rejected; parent-join policy is fail-closed on read and enforces WITH CHECK on write (asserted under a dedicatedNOSUPERUSER NOBYPASSRLSrole, likerls_isolation.rs).Testing
just check(compile + clippy + fmt): green.tests/tenantless_table_rls.rs: 2 passed.auth(17),bunyip_login(6),knowledge_base(10),contracts(6),recurring_invoicing(5),sla(2),rls_isolation(1),per_user_isolation(3): all pass.Notes
🤖 Generated with Claude Code