Phase 05: 2 gap closure plans in 1 wave - 05-06: P0 tenant scoping fix (6 models) + CASL subject correction - 05-07: E2E test coverage gaps + Phase 2 verification correction - Both plans are parallel (Wave 1, no dependencies) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
167 lines
8.8 KiB
Markdown
167 lines
8.8 KiB
Markdown
---
|
|
phase: 05-visibility-and-client-portal
|
|
plan: 06
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- src/lib/prisma-tenant.ts
|
|
- src/lib/casl/types.ts
|
|
- src/lib/casl/permissions.ts
|
|
- src/app/api/collections/route.ts
|
|
- src/app/api/collections/[id]/route.ts
|
|
- src/app/api/collections/[id]/void/route.ts
|
|
- src/app/api/remittances/route.ts
|
|
- src/app/api/remittances/[id]/verify/route.ts
|
|
- src/app/api/reports/collections/route.ts
|
|
autonomous: true
|
|
gap_closure: true
|
|
|
|
must_haves:
|
|
truths:
|
|
- "All 6 missing models (expense, expenseCategory, inventoryItem, stockMovement, vendor, ticketComment) are registered in TENANT_SCOPED_MODELS and have $extends query blocks"
|
|
- "Collection and remittance API routes use dedicated CASL subjects instead of borrowing Subscriber"
|
|
artifacts:
|
|
- path: "src/lib/prisma-tenant.ts"
|
|
provides: "Tenant scoping for all 28 tenant-scoped models"
|
|
contains: "expense.*expenseCategory.*inventoryItem.*stockMovement.*vendor.*ticketComment"
|
|
- path: "src/lib/casl/types.ts"
|
|
provides: "Collection and Remittance subjects in AppSubjects"
|
|
contains: "Collection.*Remittance"
|
|
- path: "src/lib/casl/permissions.ts"
|
|
provides: "Collection and Remittance permission rules per role"
|
|
contains: "Collection.*Remittance"
|
|
key_links:
|
|
- from: "src/lib/prisma-tenant.ts"
|
|
to: "withTenantContext query extensions"
|
|
via: "TENANT_SCOPED_MODELS array + $extends blocks"
|
|
pattern: "expense.*expenseCategory.*inventoryItem.*stockMovement.*vendor.*ticketComment"
|
|
- from: "src/app/api/collections/route.ts"
|
|
to: "src/lib/casl/permissions.ts"
|
|
via: "withPermission HOF"
|
|
pattern: "withPermission.*Collection"
|
|
---
|
|
|
|
<objective>
|
|
Fix the P0 tenant isolation security gap and correct CASL subject naming for collection/remittance routes.
|
|
|
|
Purpose: Close the critical multi-tenancy vulnerability where 6 models bypass application-layer tenant scoping, and improve permission precision by giving collection/remittance routes their own CASL subjects.
|
|
Output: Updated prisma-tenant.ts with all 6 missing models, updated CASL types and permissions, updated collection/remittance route handlers.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@C:\Users\KevinAsprec\.claude/get-shit-done/workflows/execute-plan.md
|
|
@C:\Users\KevinAsprec\.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/v1-MILESTONE-AUDIT.md
|
|
@src/lib/prisma-tenant.ts
|
|
@src/lib/casl/types.ts
|
|
@src/lib/casl/permissions.ts
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Add 6 missing models to tenant scoping</name>
|
|
<files>src/lib/prisma-tenant.ts</files>
|
|
<action>
|
|
1. Add the 6 missing model names to the TENANT_SCOPED_MODELS array on line 33:
|
|
- "expense"
|
|
- "expenseCategory"
|
|
- "inventoryItem"
|
|
- "stockMovement"
|
|
- "vendor"
|
|
- "ticketComment"
|
|
|
|
2. Add $extends query blocks for each of the 6 models inside withTenantContext(), following the EXACT same pattern used by existing models (e.g., the `ticket` or `collection` blocks). Each model needs these query methods:
|
|
- findMany: inject tenantId into args.where
|
|
- findFirst: inject tenantId into args.where
|
|
- findFirstOrThrow: inject tenantId into args.where
|
|
- findUnique: route through findFirst with tenant guard (same pattern as existing models — check if "id" in args.where without tenantId, then use prisma.{model}.findFirst with tenantId)
|
|
- findUniqueOrThrow: same findFirst routing with throw on null
|
|
- create: strip nested tenant relation, inject tenantId into args.data
|
|
- createMany: handle array and single data, strip tenant, inject tenantId
|
|
- update: inject tenantId into args.where
|
|
- updateMany: inject tenantId into args.where
|
|
- delete: inject tenantId into args.where
|
|
- deleteMany: inject tenantId into args.where
|
|
- upsert: inject tenantId into args.where, strip tenant from args.create, inject tenantId
|
|
- count: inject tenantId into args.where
|
|
- aggregate: inject tenantId into args.where
|
|
|
|
Place the 6 new model blocks AFTER the existing `jobTypeRate` block (last current model at line ~1804) and BEFORE the closing of the $extends object.
|
|
|
|
IMPORTANT: Use the exact same eslint-disable comments as existing blocks for the tenant destructuring pattern. Copy the pattern from an existing block like `ticket` or `collection` verbatim — do NOT invent a new pattern.
|
|
</action>
|
|
<verify>
|
|
1. Run: grep -c "expense\|expenseCategory\|inventoryItem\|stockMovement\|vendor\|ticketComment" src/lib/prisma-tenant.ts — should show multiple matches per model
|
|
2. Run: node -e "const t = require('./src/lib/prisma-tenant'); console.log(t.TENANT_SCOPED_MODELS)" — verify all 28 models listed (or use TypeScript compilation check)
|
|
3. Run: npx tsc --noEmit — no type errors
|
|
4. Run: npx jest --testPathPattern="tenant|rbac|e2e" --passWithNoTests — existing tests still pass
|
|
</verify>
|
|
<done>TENANT_SCOPED_MODELS contains all 28 models. Each of the 6 new models has a complete $extends query block matching the established pattern. TypeScript compiles. Existing tests pass.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Add Collection and Remittance CASL subjects and update routes</name>
|
|
<files>src/lib/casl/types.ts, src/lib/casl/permissions.ts, src/app/api/collections/route.ts, src/app/api/collections/[id]/route.ts, src/app/api/collections/[id]/void/route.ts, src/app/api/remittances/route.ts, src/app/api/remittances/[id]/verify/route.ts, src/app/api/reports/collections/route.ts</files>
|
|
<action>
|
|
1. In src/lib/casl/types.ts, add "Collection" and "Remittance" to the AppSubjects union type (after "Payment", before "Zone").
|
|
|
|
2. In src/lib/casl/permissions.ts, update permission rules:
|
|
- OFFICE_STAFF: Add `can("manage", "Collection")` and `can("manage", "Remittance")`
|
|
- COLLECTOR: Add `can("create", "Collection")`, `can("read", "Collection")`, `can("create", "Remittance")`, `can("read", "Remittance")`
|
|
- Do NOT change ADMIN (already has `can("manage", "all")`)
|
|
- Do NOT change TECHNICIAN or CLIENT (they should not access collections/remittances)
|
|
|
|
3. Update collection API routes to use "Collection" subject instead of "Subscriber":
|
|
- src/app/api/collections/route.ts: POST uses withPermission("create", "Collection"), GET uses withPermission("read", "Collection")
|
|
- src/app/api/collections/[id]/route.ts: GET uses withPermission("read", "Collection")
|
|
- src/app/api/collections/[id]/void/route.ts: uses withPermission("update", "Collection")
|
|
|
|
4. Update remittance API routes to use "Remittance" subject instead of "Subscriber":
|
|
- src/app/api/remittances/route.ts: POST uses withPermission("create", "Remittance"), GET uses withPermission("read", "Remittance")
|
|
- src/app/api/remittances/[id]/verify/route.ts: uses withPermission("update", "Remittance")
|
|
|
|
5. Update collection report route:
|
|
- src/app/api/reports/collections/route.ts: GET uses withPermission("read", "Collection") instead of withPermission("read", "Subscriber")
|
|
|
|
6. Update JSDoc comments in each route file to reflect the new subject name.
|
|
|
|
IMPORTANT: The RBAC integration tests in api-rbac.test.ts mock withPermission. After changing subjects, verify the RBAC tests still pass. If tests mock specific subjects, update them to match the new subjects.
|
|
</action>
|
|
<verify>
|
|
1. Run: grep -rn "withPermission.*Subscriber" src/app/api/collections/ src/app/api/remittances/ src/app/api/reports/collections/ — should return ZERO matches
|
|
2. Run: grep -rn "withPermission.*Collection\|withPermission.*Remittance" src/app/api/ — should show all collection/remittance routes using correct subjects
|
|
3. Run: npx tsc --noEmit — no type errors
|
|
4. Run: npx jest --testPathPattern="rbac|e2e" — all tests pass
|
|
</verify>
|
|
<done>Collection and Remittance are proper CASL subjects. All collection/remittance routes use their dedicated subjects. RBAC tests pass. No route still uses "Subscriber" for collection/remittance operations.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
1. TypeScript compilation passes: npx tsc --noEmit
|
|
2. All existing tests pass: npx jest
|
|
3. TENANT_SCOPED_MODELS has 28 entries (22 existing + 6 new)
|
|
4. Zero collection/remittance routes use "Subscriber" as CASL subject
|
|
5. grep for all 6 model names in prisma-tenant.ts $extends block confirms they exist
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- The P0 tenant isolation gap is closed: all tenant-scoped Prisma models have automatic tenantId injection
|
|
- Collection and remittance routes use semantically correct CASL subjects
|
|
- All existing tests continue to pass
|
|
- TypeScript compiles without errors
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/05-visibility-and-client-portal/05-06-SUMMARY.md`
|
|
</output>
|