Skip to content

feat(rls): Phase 1 #5 — Goal RLS + callsite fixes#133

Merged
webdevcom01-cell merged 1 commit into
mainfrom
feat/rls-phase1-goal
May 24, 2026
Merged

feat(rls): Phase 1 #5 — Goal RLS + callsite fixes#133
webdevcom01-cell merged 1 commit into
mainfrom
feat/rls-phase1-goal

Conversation

@webdevcom01-cell
Copy link
Copy Markdown
Owner

Summary

  • Migration 20260530000000_rls_phase1_goal: enables RLS + FORCE on Goal, adds composite index on (organizationId, id) (existing (organizationId, status) index is separate and untouched), SELECT/INSERT/UPDATE/DELETE policies for app_user, grants to app_user + admin_user
  • 9 callsites wrapped across 4 files:
    • goals/route.ts (GET findMany, POST create) → withOrgContext
    • goals/[goalId]/route.tsloadGoal() uses withAdminBypass (pre-auth lookup); GET/PATCH/DELETE use withOrgContext(goal.organizationId)
    • agents/[agentId]/goals/route.ts → goal existence check uses withAdminBypass (agent's orgId not in scope)
    • lib/templates/template-engine.tsgoal.create in importTemplate wrapped with withOrgContext(organizationId) — called from API routes only, not workers

RLS_ENFORCEMENT_ENABLED=false — all withOrgContext wrapping is a no-op until flag flip.

Cross-table JOIN analysis

GET /api/goals/[goalId] includes mission: { select: { id, statement } } (Goal → CompanyMission). Both tables now have RLS. This findUnique runs inside withOrgContext(prisma, goal.organizationId, ...) — both the Goal query and the CompanyMission sub-query execute in the same $transaction with the same app.current_org_id. Safe. ✓

goal-context.ts accesses Goal via prisma.agentGoalLink.findMany({ include: { goal } }) — not a prisma.goal.* call; addressed when AgentGoalLink is migrated.

Test plan

  • pnpm precheck — TS ✅ vitest ✅ ESLint ✅ (4119 tests, 307 files)
  • CI green on this PR
  • DB smoke test (post-merge): SELECT COUNT(*) FROM "Goal" as app_user without app.current_org_id — must return 0 rows
  • DB smoke test: set app.current_org_id to a valid org ID — must return only that org's goals
  • Verify GET/POST /api/goals, GET/PATCH/DELETE /api/goals/<id>, and template import work with flag off

🤖 Generated with Claude Code

- Migration 20260530000000: enable RLS + FORCE on Goal, composite index
  on (organizationId, id) — note: (organizationId, status) already exists;
  SELECT/INSERT/UPDATE/DELETE policies for app_user, grants to app_user + admin_user
- goals/route.ts: wrap findMany(GET) and create(POST) with withOrgContext
- goals/[goalId]/route.ts: loadGoal() uses withAdminBypass (pre-auth);
  GET findUnique (incl. CompanyMission JOIN), PATCH update, DELETE count +
  delete all wrapped with withOrgContext(goal.organizationId)
- agents/[agentId]/goals/route.ts: goal existence check uses withAdminBypass
  (agent's orgId not in scope for this check)
- lib/templates/template-engine.ts: goal.create in importTemplate wrapped
  with withOrgContext(organizationId) — called from API routes only

Cross-table JOIN: GET /goals/[goalId] includes mission (CompanyMission, already
RLS-enforced) — both sub-queries run in the same withOrgContext transaction,
same app.current_org_id. Safe.

RLS_ENFORCEMENT_ENABLED=false; wrapping is a no-op until flag flip.
@webdevcom01-cell webdevcom01-cell added the e2e Run E2E tests on this PR label May 23, 2026
@webdevcom01-cell webdevcom01-cell merged commit c91ad04 into main May 24, 2026
6 of 12 checks passed
@webdevcom01-cell webdevcom01-cell deleted the feat/rls-phase1-goal branch May 24, 2026 06:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

e2e Run E2E tests on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant