docs(01): create phase plan
Phase 01: Foundation - 5 plan(s) in 4 wave(s) - 2 parallel (wave 2: auth + tenant provisioning), 3 sequential - Ready for execution Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
250
.planning/phases/01-foundation/01-04-PLAN.md
Normal file
250
.planning/phases/01-foundation/01-04-PLAN.md
Normal file
@@ -0,0 +1,250 @@
|
||||
---
|
||||
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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</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/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
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: CASL permission definitions and ability factory</name>
|
||||
<files>
|
||||
package.json
|
||||
src/lib/casl/types.ts
|
||||
src/lib/casl/permissions.ts
|
||||
src/lib/casl/ability.ts
|
||||
</files>
|
||||
<action>
|
||||
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
|
||||
</action>
|
||||
<verify>
|
||||
TypeScript compiles without errors. Import defineAbilityFor and verify it returns an Ability instance for each role.
|
||||
</verify>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: API authorization middleware and RBAC tests</name>
|
||||
<files>
|
||||
src/lib/middleware/authorize.ts
|
||||
src/lib/__tests__/rbac.test.ts
|
||||
</files>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
Run `npx vitest run` — all RBAC tests pass. Verify that at minimum: Technician cannot access billing (critical requirement from phase success criteria).
|
||||
</verify>
|
||||
<done>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).</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
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
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-foundation/01-04-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user