feat: Phase 1 tenant isolation — add account_id to all tenant tables #133

Merged
chihlasm merged 37 commits from feat/tenant-isolation-phase-1 into main 2026-04-10 04:57:53 +00:00

37 Commits

Author SHA1 Message Date
chihlasm
b9da0e7107 chore: resolve merge conflicts with main
- deps.py: keep require_tenant_context + require_admin_db (RLS deps);
  drop unused get_tenant_context stub from Phase 0
- categories.py: keep both PLATFORM_ACCOUNT_ID and tenant_filter imports
  (body uses both)
- tenant-isolation spec: keep main's resolved TargetList/teams audit answers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 04:57:39 +00:00
chihlasm
8f044849d4 fix: get_tree returns 404 (not 403) for inaccessible trees — don't leak resource existence
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 04:17:31 +00:00
chihlasm
14304be383 fix: correct RLS test fixtures — tree_structure NOT NULL, tree_tags schema, session-scoped set_config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 04:15:41 +00:00
chihlasm
a5c5eb6cc3 fix: convert DATABASE_URL_SYNC from property to overridable field for Alembic superuser URL
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 04:03:32 +00:00
chihlasm
c4f919f3a5 feat: migration — enable RLS on trees, tags, categories, psa_connections, flow_proposals
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 04:02:10 +00:00
chihlasm
8de6ee7aa4 feat: migration — create resolutionflow_app and resolutionflow_admin DB roles
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 03:59:28 +00:00
chihlasm
83ad2e0661 feat: migrate admin endpoints to get_admin_db (BYPASSRLS) before RLS switch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 03:57:18 +00:00
chihlasm
ce4056c6b9 test: add failing RLS isolation tests (green after Task 10 migration + Task 11 URL switch)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 03:54:42 +00:00
chihlasm
9d60b9a244 feat: apply require_tenant_context to all user-facing routers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 03:52:52 +00:00
chihlasm
df9ecf2d29 feat: add require_tenant_context and require_admin_db dependencies
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 03:50:59 +00:00
chihlasm
b0e5f12897 feat: register RLS transaction-begin listener on app engine at startup
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 03:49:49 +00:00
chihlasm
b4f8694f6b feat: add tenant_context module — ContextVar, transaction listener, tenant_filter
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 03:48:34 +00:00
chihlasm
6f1becf21f feat: add admin_engine and get_admin_db for BYPASSRLS admin endpoints
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 03:46:29 +00:00
chihlasm
acbfb3fb37 feat: add ADMIN_DATABASE_URL setting with fallback to DATABASE_URL
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 03:45:52 +00:00
chihlasm
a394a1d464 fix: replace account_id=None with PLATFORM_ACCOUNT_ID for global content
After migration 174f442795b7 enforces NOT NULL on account_id, all
platform/global content must use the sentinel platform account instead
of NULL. Three categories of fixes:

1. trees.py: is_default trees now get PLATFORM_ACCOUNT_ID (not None)
2. admin_categories.py: global category CRUD now uses PLATFORM_ACCOUNT_ID
3. categories.py, tags.py, step_categories.py: creation endpoints coerce
   None → PLATFORM_ACCOUNT_ID; IS NULL filter queries updated to
   == PLATFORM_ACCOUNT_ID (IS NULL queries returned empty after migration
   backfilled all global rows to the platform account)

Defines PLATFORM_ACCOUNT_ID constant in app/core/service_account.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 18:35:52 +00:00
chihlasm
d2ebc4f182 fix: correct tree tags subquery in template_trees migration
The INSERT into template_trees incorrectly referenced `tags` as a column
on the `trees` table. Tags are a relationship via the `tree_tag_assignments`
join table — there is no direct column. Migration was failing with:

  UndefinedColumn: column "tags" does not exist ... FROM trees

Fixed by replacing COALESCE(tags, '[]') with a correlated subquery that
aggregates tag names from tree_tag_assignments → tree_tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 17:30:05 +00:00
chihlasm
8bcf08ae06 fix: persist account ownership for script templates and generations 2026-04-09 17:18:38 +00:00
chihlasm
478205c208 fix: platform account fallback for script_templates seeded without team/user
Migration 057 inserts 6 AD script templates with NULL team_id and NULL
created_by. Neither backfill path (created_by→users, team_id→team admin)
could attribute them to an account, causing the verify check to fail.

Fix: pre-create the platform sentinel account (ON CONFLICT DO NOTHING,
safe since 3a40fe11b427 also creates it idempotently) and add a final
fallback UPDATE assigning any remaining NULL script_templates to it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 06:41:00 +00:00
chihlasm
0f33feb6d6 fix: use correlated subquery in psa_post_log backfill to avoid invalid FROM-clause reference
PostgreSQL UPDATE...FROM does not allow the updated table to be
referenced inside the FROM clause's JOIN conditions. Replace the
LEFT JOIN psa_connections with a correlated subquery.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 06:31:17 +00:00
chihlasm
034b858fc9 fix: add depends_on 067 to cc214c63aa30 to fix fresh-DB migration order
session_resolution_outputs is created in migration 067 (sequential branch
from 064). On fresh databases, Alembic could run cc214c63aa30 before 067,
causing "table does not exist" errors. depends_on ensures 067 always runs
first regardless of branch traversal order.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 06:20:00 +00:00
chihlasm
b937cb41e4 fix: merge Phase 1 account_id chain with main head to resolve multiple-heads error
Combines the Phase 1 tenant isolation chain (064 → ... → 174f442795b7)
with the main sequential chain (064 → ... → 070) into a single Alembic
head (a9f3b2c1d4e5) so `alembic upgrade head` in the Dockerfile works
without ambiguity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 06:14:04 +00:00
chihlasm
0d475c71ed fix: correct Phase 1 down_revision — chain from 064 not b8d2f4a6c091
b8d2f4a6c091 was NOT the production head. The true head was 064
(064_normalize_script_builder_messages) via the chain:
b8d2f4a6c091 → f0aad74ea51b → 062 → 063 → 064

This caused 'multiple head revisions' on Railway deployment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 06:04:10 +00:00
chihlasm
417fa562ce fix: Task 9 migration — include tags in template_trees INSERT
The tags column was accidentally omitted from the is_default tree copy.
Now uses COALESCE(tags, '[]'::jsonb) to preserve source tree tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 05:34:59 +00:00
chihlasm
42937b24a4 feat: Phase 1 Group 9 — enforce NOT NULL on all account_id columns
All previously-nullable account_id columns are now NOT NULL.
tree_embeddings and feedback backfilled before constraint applied.
Global content assigned to platform sentinel account (00000000-...-0001)
in preceding migration.

Tables updated: users, trees, tree_categories, tree_tags,
step_categories, step_library, tree_embeddings, feedback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 05:34:32 +00:00
chihlasm
b4b8c67d3b feat: Phase 1 Group 10 — create global content tables and platform account
Creates template_trees and platform_steps (no account_id, no RLS).
Migrates is_default=TRUE trees and public steps into them.
Creates sentinel platform account (00000000-...-0001) for global
tree_categories, tree_tags, step_categories, step_library, and
is_default trees — clearing all NULL account_id rows in those tables
as prerequisite for Group 9 SET NOT NULL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 05:31:33 +00:00
chihlasm
d24da77604 feat: Phase 1 Group 8 — add account_id to target_lists (keep team_id)
Zero rows in production — this is a schema-only migration in practice.
team_id kept for app code compatibility. Drop deferred to later cleanup.
Backfill: team_id → team admin user → account_id; fallback: created_by.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 05:25:24 +00:00
chihlasm
857e782d14 feat: Phase 1 Group 7 — add account_id to script tables (keep team_id)
team_id is kept in all three tables — drop deferred until app code
is fully migrated off team_id references.

Tables: script_builder_sessions, script_templates, script_generations
Backfill: user_id/created_by → users.account_id

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 05:23:35 +00:00
chihlasm
086c4580f1 feat: Phase 1 Group 6 — add account_id to maintenance_schedules
Primary backfill: tree_id → trees.account_id
Fallback: created_by → users.account_id (for is_default tree rows)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 05:20:56 +00:00
chihlasm
0d69474128 feat: Phase 1 Group 5 — add account_id to PSA and notification tables
psa_post_log: backfill via psa_connection, fallback to posted_by user
psa_member_mappings: backfill via psa_connection
notification_logs: backfill via notification_config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 05:19:12 +00:00
chihlasm
b5fdb488b3 feat: Phase 1 Group 4 — add account_id to user_folders and user_pinned_trees
Backfill: user_id → users.account_id

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 05:16:50 +00:00
chihlasm
de5ecf4fb2 feat: Phase 1 Group 3 — add account_id to step_ratings and step_usage_log
Backfill from rater/user's account_id (not the step's account_id).
This is an explicit design decision — step rating data is attributed
to the account that performed the rating.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 05:15:10 +00:00
chihlasm
2779a41b94 feat: Phase 1 Group 2 — add account_id to AI branching tables
Tables: session_branches, session_handoffs, fork_points,
        ai_session_steps, ai_suggestions
Backfill: session_id → ai_sessions.account_id (all except
ai_suggestions which uses user_id → users.account_id)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 05:12:18 +00:00
chihlasm
4666c4f6d2 feat: Phase 1 Group 1 — add account_id to core session tables
Migration sequence: add nullable → backfill via user_id/ai_session chain
→ verify zero NULLs → SET NOT NULL → CREATE INDEX.

Tables: sessions, attachments, session_supporting_data,
        session_resolution_outputs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 05:09:14 +00:00
chihlasm
2837c6e4cf docs: add Phase 1 tenant isolation schema migrations implementation plan
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 04:58:24 +00:00
chihlasm
56775eca04 docs: add tenant isolation Phase 0 implementation plan
8 tasks covering: CRITICAL copilot hotfix, tenant_filter() helper,
get_tenant_context dependency, analytics/category/AI session gap fixes,
full UUID endpoint audit, TargetList dead code audit, teams orphan
check, and CI grep check for missing tenant filters.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 03:02:19 +00:00
chihlasm
82bb7967d8 docs: add background job isolation policy to tenant isolation spec
Documents policy for all 5 existing background jobs:
- Knowledge Flywheel and PSA Retry flagged for account_id threading
- Chat Retention already follows correct pattern (model for others)
- Maintenance Schedule Firing needs account_id in queries + Session creation
- AI Conversation Expiry approved as cross-tenant with justification

Adds approved cross-tenant query registry and Phase 2 checklist items.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 02:38:55 +00:00
chihlasm
a7dff9e143 docs: add tenant data isolation design spec
Complete architecture plan for multi-tenant data isolation across
all layers (PostgreSQL RLS, application-layer filtering, schema
migration, testing strategy, and phased rollout checklist).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 02:24:38 +00:00