docs(rls): per-user isolation reference and decomposition for PMS-255 #185

Merged
David merged 2 commits from docs/pms-255-rls-isolation-plan into main 2026-06-13 12:57:14 +02:00
Owner

What

Adds dev-docs/rls-per-user-isolation.md: the implementation reference for PMS-255 (per-user data isolation), plus the decomposition into nine runner-sized child issues (PMS-256 through PMS-264). No code change.

Why

PMS-255 is an epic that the automated runner could not process as a single task. This doc supplies the ground-truth schema inventory and an ordered, explicit breakdown so each piece is independently executable.

Key findings (verified live against migrations/*.sql and src/ on 2026-06-12)

  • Personal-tenant provisioning is already largely built: place_bunyip_user provisions a kind='personal' tenant per user, and the PMS-243/245 backfill re-homes non-admin users off the shared default tenant Uuid::from_u128(1).
  • The real gap is enforcement: the 024 RLS policy is fail-open (matches all rows when app.current_tenant is unset), is set only in begin_with_tenant which most read paths bypass, and is USING-only (no WITH CHECK, so writes are unconstrained).
  • 76 tables; exactly 6 lack tenant_id: tenants (root, by design), user_oauth_identities (cross-tenant unique-constraint collision), kb_article_versions, invoice_lines, rate_card_items, sla_targets.

Resolved decisions

  • Isolation model: personal-tenant-per-user (kind='personal'), reusing the existing tenant boundary; org tenants + teams come later.
  • User-editable lookups are isolated and seeded per user; a system-shared read-only class is reserved for future non-editable global rows.

Open decisions flagged in the doc (to confirm before the affected child issue runs)

  • Ownership of legacy co-mingled default-tenant rows (re-home vs quarantine).
  • Whether portal contacts get per-user isolation or stay company-scoped for now.
  • The borderline lookup-vs-business classification of a few config tables.

Child issues

PMS-256 (GUC plumbing), PMS-257 (fail-closed + WITH CHECK), PMS-258 (6 no-tenant_id tables), PMS-259 (per-user lookup seeding), PMS-260 (known leak points), PMS-261 (worker scoping), PMS-262 (single-tenant feature removal), PMS-263 (backfill), PMS-264 (isolation test suite).

🤖 Generated with Claude Code

## What Adds `dev-docs/rls-per-user-isolation.md`: the implementation reference for PMS-255 (per-user data isolation), plus the decomposition into nine runner-sized child issues (PMS-256 through PMS-264). No code change. ## Why PMS-255 is an epic that the automated runner could not process as a single task. This doc supplies the ground-truth schema inventory and an ordered, explicit breakdown so each piece is independently executable. ## Key findings (verified live against `migrations/*.sql` and `src/` on 2026-06-12) - Personal-tenant provisioning is already largely built: `place_bunyip_user` provisions a `kind='personal'` tenant per user, and the PMS-243/245 backfill re-homes non-admin users off the shared default tenant `Uuid::from_u128(1)`. - The real gap is enforcement: the `024` RLS policy is fail-open (matches all rows when `app.current_tenant` is unset), is set only in `begin_with_tenant` which most read paths bypass, and is `USING`-only (no `WITH CHECK`, so writes are unconstrained). - 76 tables; exactly 6 lack `tenant_id`: `tenants` (root, by design), `user_oauth_identities` (cross-tenant unique-constraint collision), `kb_article_versions`, `invoice_lines`, `rate_card_items`, `sla_targets`. ## Resolved decisions - Isolation model: personal-tenant-per-user (`kind='personal'`), reusing the existing tenant boundary; org tenants + teams come later. - User-editable lookups are isolated and seeded per user; a system-shared read-only class is reserved for future non-editable global rows. ## Open decisions flagged in the doc (to confirm before the affected child issue runs) - Ownership of legacy co-mingled default-tenant rows (re-home vs quarantine). - Whether portal contacts get per-user isolation or stay company-scoped for now. - The borderline lookup-vs-business classification of a few config tables. ## Child issues PMS-256 (GUC plumbing), PMS-257 (fail-closed + WITH CHECK), PMS-258 (6 no-tenant_id tables), PMS-259 (per-user lookup seeding), PMS-260 (known leak points), PMS-261 (worker scoping), PMS-262 (single-tenant feature removal), PMS-263 (backfill), PMS-264 (isolation test suite). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
docs(rls): per-user isolation reference and decomposition for PMS-255
All checks were successful
E2E (staging) / Playwright against staging (pull_request) Successful in 1m27s
Check / fmt + clippy + compile + tests (pull_request) Successful in 2m19s
864a0d8409
Captures the ground-truth schema inventory (76 tables, 6 without tenant_id), the resolved personal-tenant-per-user model, the lookup-vs-business classification, and the nine-step decomposition into runner-sized child issues (PMS-256..PMS-264). Provisioning is already largely built (place_bunyip_user provisions personal tenants, PMS-243/245 backfill re-homes users off the shared default tenant); the outstanding work is enforcement (fail-closed RLS, GUC on every query, WITH CHECK), per-user lookup seeding, the six no-tenant_id tables, known leak points, worker scoping, single-tenant feature removal, backfill, and an isolation test suite.

#PMS-255

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge branch 'main' into docs/pms-255-rls-isolation-plan
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
E2E (staging) / Playwright against staging (pull_request) Successful in 48s
Check / fmt + clippy + compile + tests (pull_request) Successful in 3m25s
Build OCI container / Build and push mokosh-api image (push) Successful in 4m17s
18914ba1ae
David merged commit 5da88eb56a into main 2026-06-13 12:57:14 +02:00
David deleted branch docs/pms-255-rls-isolation-plan 2026-06-13 12:57:14 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-server!185
No description provided.