--- phase: 01-foundation plan: 04 type: execute wave: 3 depends_on: ["01-02", "01-03"] files_modified: - package.json - src/lib/casl/ability.ts - src/lib/casl/permissions.ts - src/lib/casl/types.ts - src/lib/middleware/authorize.ts - src/app/api/test-rbac/route.ts - src/lib/__tests__/rbac.test.ts autonomous: true must_haves: truths: - "A Technician cannot access billing or subscriber management routes and receives a 403" - "A Collector can view subscribers but cannot modify subscribers outside their zone" - "A Client can only see their own account data" - "An Admin has full access within their tenant" - "A user with multiple roles gets the union of all permissions" artifacts: - path: "src/lib/casl/ability.ts" provides: "CASL ability factory that builds permissions from user roles" exports: ["defineAbilityFor"] - path: "src/lib/casl/permissions.ts" provides: "Permission matrix for all five roles" contains: "ADMIN" - path: "src/lib/middleware/authorize.ts" provides: "API route authorization wrapper" exports: ["authorize", "withPermission"] - path: "src/lib/__tests__/rbac.test.ts" provides: "RBAC permission tests for all roles" min_lines: 80 key_links: - from: "src/lib/casl/ability.ts" to: "src/lib/casl/permissions.ts" via: "Reads role permission definitions" pattern: "defineAbilityFor" - from: "src/lib/middleware/authorize.ts" to: "src/lib/casl/ability.ts" via: "Builds ability from session user and checks permission" pattern: "defineAbilityFor.*can\\(" - from: "src/lib/middleware/authorize.ts" to: "src/lib/auth.ts" via: "Gets current user session for authorization" pattern: "getCurrentUser|getServerSession" --- Implement role-based access control using CASL.js with a permission matrix for all five roles (Admin, Office Staff, Collector, Technician, Client), an API-layer authorization middleware that returns 403 for unauthorized access, and comprehensive tests proving each role's boundaries. Purpose: Without RBAC, any authenticated user can access everything. This plan enforces the principle of least privilege — a Technician sees only their jobs, a Client sees only their account. Output: Working CASL permission system, API authorization middleware, and tests for all role boundaries. @C:\Users\KevinAsprec\.claude/get-shit-done/workflows/execute-plan.md @C:\Users\KevinAsprec\.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/01-foundation/01-CONTEXT.md @.planning/phases/01-foundation/01-02-SUMMARY.md @.planning/phases/01-foundation/01-03-SUMMARY.md @src/lib/auth-options.ts @src/types/next-auth.d.ts @prisma/schema.prisma Task 1: CASL permission definitions and ability factory package.json src/lib/casl/types.ts src/lib/casl/permissions.ts src/lib/casl/ability.ts Install CASL: `npm install @casl/ability`. Create `src/lib/casl/types.ts`: - Define AppAbility type using CASL's PureAbility - Define subjects: "Tenant", "User", "Subscriber", "Invoice", "Payment", "Ticket", "JobOrder", "Inventory", "Expense", "Account", "Report", "all" - Define actions: "create", "read", "update", "delete", "manage" (manage = all actions) - Export AppAbility and AppSubjects types - Note: Most subjects (Subscriber, Invoice, etc.) don't have models yet — we're defining the permission structure now so it's ready when those models are created in later phases Create `src/lib/casl/permissions.ts`: - Export a function `definePermissionsFor(role: Role, userId: string, tenantId: string)` that returns an array of CASL permission rules - Permission matrix based on CONTEXT.md decisions: **ADMIN**: can("manage", "all") — full access within tenant **OFFICE_STAFF**: - can("manage", "User") — create/manage users, assign roles - can("manage", "Subscriber") — full subscriber management - can("manage", "Invoice") — billing management - can("manage", "Payment") — record payments - can("manage", "Ticket") — ticketing - can("manage", "JobOrder") — job order management - can("read", "Report") — view financial reports - can("read", "Account") — view accounting (not modify) - cannot("create", "Account") — cannot modify Chart of Accounts - cannot("update", "Account") - cannot("delete", "Account") **COLLECTOR**: - can("read", "Subscriber") — can view all subscribers (context for work) - can("create", "Payment") — can record payments (zone filtering done at data layer, not CASL) - can("read", "Payment") — view payment history - cannot("manage", "Invoice") — no billing access - cannot("manage", "User") — no user management - cannot("manage", "Report") — no report access **TECHNICIAN**: - can("read", "JobOrder", { assignedToId: userId }) — only their assigned jobs - can("update", "JobOrder", { assignedToId: userId }) — update their own jobs - can("read", "Subscriber") — limited: only contact info for assigned job subscribers (enforced at data layer) - can("read", "Inventory", { assignedToId: userId }) — only inventory checked out to them - cannot("manage", "Invoice") - cannot("manage", "Payment") - cannot("manage", "Subscriber") — no subscriber management - cannot("manage", "Report") **CLIENT**: - can("read", "Invoice", { subscriberId: userId }) — only their own invoices - can("read", "Payment", { subscriberId: userId }) — only their own payments - can("read", "Subscriber", { id: userId }) — only their own profile - can("create", "Ticket") — submit tickets - can("read", "Ticket", { submittedById: userId }) — only their own tickets - cannot("manage", "User") - cannot("manage", "Report") Create `src/lib/casl/ability.ts`: - Export `defineAbilityFor(user: { id: string, roles: Role[], tenantId: string | null, isSuperAdmin: boolean })` - If isSuperAdmin: can("manage", "all") — unrestricted - Otherwise: iterate over user.roles, call definePermissionsFor for each role, merge all rules (union of permissions for multi-role users) - Return the built CASL Ability instance - Export the Ability type for use in components/routes TypeScript compiles without errors. Import defineAbilityFor and verify it returns an Ability instance for each role. CASL permission matrix defined for all 5 roles plus super-admin. Multi-role users get union of permissions. Ability factory builds correct permissions from user session data. Task 2: API authorization middleware and RBAC tests src/lib/middleware/authorize.ts src/lib/__tests__/rbac.test.ts Create `src/lib/middleware/authorize.ts`: - Export `withPermission(action: string, subject: string)` — a higher-order function that wraps a Next.js API route handler - Flow: 1. Get current user session via getCurrentUser() from auth.ts 2. If no session: return 401 { error: "Unauthorized" } 3. Build CASL ability using defineAbilityFor(session.user) 4. Check ability.can(action, subject) 5. If cannot: return 403 { error: "Forbidden" } 6. If can: call the wrapped handler, passing the ability and user in context - Export `authorize(handler, action, subject)` as an alternative API for convenience - The wrapped handler receives (req, { user, ability }) so handlers can do fine-grained checks internally Example usage (add as JSDoc comment): ```typescript // In route.ts: export const GET = withPermission("read", "Subscriber")(async (req, { user, ability }) => { // ability is available for fine-grained checks within the handler const subscribers = await getSubscribers(user.tenantId); return NextResponse.json(subscribers); }); ``` Create `src/lib/__tests__/rbac.test.ts` — comprehensive unit tests: **Admin tests:** - Admin can manage Subscribers (returns true) - Admin can manage Users (returns true) - Admin can manage Reports (returns true) **Office Staff tests:** - Office Staff can manage Subscribers (true) - Office Staff can read Reports (true) - Office Staff cannot create Account (false — cannot modify COA) - Office Staff cannot delete Account (false) **Collector tests:** - Collector can read Subscribers (true) - Collector can create Payment (true) - Collector cannot manage Invoice (false) - Collector cannot manage User (false) - Collector cannot read Reports (false) **Technician tests:** - Technician cannot manage Invoice (false) - Technician cannot manage Payment (false) - Technician cannot manage Subscriber (false) - Technician cannot read Reports (false) **Client tests:** - Client can create Ticket (true) - Client cannot manage User (false) - Client cannot manage Subscriber (false — not even their own, they can only read) - Client cannot read Reports (false) **Multi-role tests:** - User with [TECHNICIAN, COLLECTOR] roles can read Subscribers (from Collector) AND update JobOrder (from Technician) - Union of permissions is additive **Super-admin tests:** - Super-admin can manage all (true for any subject) All tests use defineAbilityFor directly — no HTTP requests needed. These are pure unit tests. Run `npx vitest run` — all RBAC tests pass. Verify that at minimum: Technician cannot access billing (critical requirement from phase success criteria). API authorization middleware returns 403 for unauthorized access. All 5 roles have correct permission boundaries verified by unit tests. Multi-role union works. Super-admin has full access (AUTH-02, AUTH-03). 1. `npx vitest run` passes all RBAC tests 2. Admin has full access, Office Staff can't modify COA, Collector can't manage billing, Technician can't see billing/subscriber management, Client can only see own data 3. Multi-role user gets union of permissions 4. Super-admin bypasses all permission checks 5. withPermission middleware returns 401 for unauthenticated, 403 for unauthorized - All 5 roles have correctly scoped permissions (AUTH-02, AUTH-03) - API-layer enforcement returns 403 (not just UI hiding) for unauthorized access - Technician cannot access billing or subscriber management routes - Multi-role users get union of all assigned role permissions - Comprehensive unit tests validate every role boundary After completion, create `.planning/phases/01-foundation/01-04-SUMMARY.md`