Skip to content

feat(rls): Phase 1 #10 — ApprovalPolicy RLS + callsite fixes#138

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

feat(rls): Phase 1 #10 — ApprovalPolicy RLS + callsite fixes#138
webdevcom01-cell merged 1 commit into
mainfrom
feat/rls-phase1-approvalpolicy

Conversation

@webdevcom01-cell
Copy link
Copy Markdown
Owner

Summary

  • Migration 20260604000000_rls_phase1_approvalpolicy: TENANT_DIRECT RLS on ApprovalPolicy (SELECT/INSERT/UPDATE/DELETE policies + composite organizationId, id index)
  • Pre-auth lookups (loadPolicy in [policyId]/route.ts, pre-auth findUnique in [policyId]/decisions/route.ts): use withAdminBypass — fetch org ID to perform the auth check itself; same pattern as loadDepartment/loadGoal from migrations chore(deps): bump actions/deploy-pages from 4.0.5 to 5.0.0 #4/chore(deps): bump pnpm/action-setup from f40ffcd9367d9f12939873eb1018b921a783ffaa to b906affcce14559ad1aafd4ab0e942779e9f58b1 #5
  • CRUD routes (policies/route.ts GET/POST, [policyId]/route.ts GET/PATCH/DELETE): use withOrgContext with org ID already in scope from query param / body / loadPolicy result
  • checkPolicies in approval-engine.ts: gains optional organizationId?: string | null param; fail-open catch still handles missing org; no production callers yet so signature change is safe
  • requestApproval in approval-engine.ts: wraps approvalPolicy.findUnique in withOrgContext; organizationId was already in its signature
  • processTimeouts in approval-engine.ts: NO CHANGE — cross-org cron that processes expired decisions across all tenants; relies on DATABASE_URL having BYPASSRLS (Phase 0b); comment added to document intent

Admin-path classification

Callsite Classification Reason
policies/route.ts GET/POST TENANT withOrgContext requireOrgMember auth, org ID in scope
[policyId]/route.ts loadPolicy PRE-AUTH withAdminBypass fetches org ID for the auth check itself
[policyId]/route.ts GET/PATCH/DELETE TENANT withOrgContext org ID from loadPolicy, auth passed
[policyId]/decisions/route.ts pre-auth PRE-AUTH withAdminBypass same pre-auth lookup pattern
approval-engine checkPolicies TENANT withOrgContext add optional orgId; fail-open catch
approval-engine requestApproval TENANT withOrgContext orgId already in signature
approval-engine processTimeouts NO CHANGE cross-org cron, DATABASE_URL BYPASSRLS

Test plan

  • pnpm precheck — 4/4 pass ✅
  • CI green
  • E2E: existing JWTSessionError flake — ignore
  • After merge: apply migration to Railway prod DB
  • Flag-on smoke: create a policy via POST /api/policies, list via GET /api/policies?orgId=…, confirm no 42501; trigger governance-timeout cron, confirm processTimeouts still resolves expired decisions across all orgs

RLS state after this PR

Phase 1: 10/14 live — OrganizationMember, Invitation, CompanyMission, Department, Goal, AgentPermissionGrant, HeartbeatConfig, HeartbeatContext, HeartbeatRun, ApprovalPolicy

🤖 Generated with Claude Code

Migration 20260604000000: TENANT_DIRECT RLS on ApprovalPolicy.
Pre-auth lookups (loadPolicy, decisions pre-auth) use withAdminBypass —
same pattern as loadDepartment/loadGoal from migrations #4/#5.
CRUD routes use withOrgContext with org ID from loadPolicy result.
checkPolicies gains optional organizationId param; requestApproval wraps
the policy findUnique in withOrgContext.
processTimeouts unchanged — cross-org cron relies on DATABASE_URL BYPASSRLS.
@webdevcom01-cell webdevcom01-cell added the e2e Run E2E tests on this PR label May 24, 2026
@webdevcom01-cell webdevcom01-cell merged commit 9b27c21 into main May 24, 2026
6 of 12 checks passed
@webdevcom01-cell webdevcom01-cell deleted the feat/rls-phase1-approvalpolicy branch May 24, 2026 08:55
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