docs(01-04): complete CASL RBAC plan
Tasks completed: 2/2 - Task 1: CASL permission definitions and ability factory - Task 2: API authorization middleware and RBAC tests SUMMARY: .planning/phases/01-foundation/01-04-SUMMARY.md
This commit is contained in:
165
.planning/phases/01-foundation/01-04-SUMMARY.md
Normal file
165
.planning/phases/01-foundation/01-04-SUMMARY.md
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
phase: 01-foundation
|
||||
plan: "04"
|
||||
subsystem: auth
|
||||
tags: [casl, rbac, authorization, middleware, permissions, nextjs]
|
||||
|
||||
# Dependency graph
|
||||
requires:
|
||||
- phase: 01-02
|
||||
provides: NextAuth JWT session with tenantId and roles fields; getCurrentUser() helper
|
||||
- phase: 01-01
|
||||
provides: Prisma Role enum (ADMIN, OFFICE_STAFF, COLLECTOR, TECHNICIAN, CLIENT)
|
||||
provides:
|
||||
- CASL permission matrix for all 5 roles (Admin, Office Staff, Collector, Technician, Client)
|
||||
- defineAbilityFor() factory that builds ability from session user
|
||||
- API-layer authorization middleware returning 401/403
|
||||
- 66 unit tests validating every role boundary
|
||||
affects:
|
||||
- All future API routes that need authorization enforcement
|
||||
- Phase 2+ (Invoice, Subscriber, Payment routes use withPermission wrapper)
|
||||
- Phase 3+ (JobOrder routes use Technician permission conditions)
|
||||
|
||||
# Tech tracking
|
||||
tech-stack:
|
||||
added: ["@casl/ability (v6+) with createMongoAbility"]
|
||||
patterns:
|
||||
- "CASL MongoAbility with string-based subjects for forward-compatibility"
|
||||
- "withPermission() higher-order function wraps Next.js route handlers"
|
||||
- "Additive permission union for multi-role users (cannot() rules excluded in merge)"
|
||||
- "Condition casting via any for string subjects (tightened when Prisma models added)"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- src/lib/casl/types.ts
|
||||
- src/lib/casl/permissions.ts
|
||||
- src/lib/casl/ability.ts
|
||||
- src/lib/middleware/authorize.ts
|
||||
- src/lib/__tests__/rbac.test.ts
|
||||
modified:
|
||||
- package.json (added @casl/ability dependency)
|
||||
|
||||
key-decisions:
|
||||
- "createMongoAbility used throughout (not PureAbility) — string subjects require conditionsMatcher which createMongoAbility provides built-in"
|
||||
- "cannot() rules excluded when merging multi-role abilities — additive union means more roles = more (never less) access"
|
||||
- "Condition objects cast via as unknown as any for string subjects — CASL infers MongoQuery<never> for strings, type-safe tightening deferred to Phase 2 when Prisma models exist"
|
||||
- "Technician can('read', 'Subscriber') — coarse-grained access, data-layer enforces actual scope to assigned job contacts"
|
||||
- "Collector has no cannot() rules — absence of rule = no access (simpler than explicit denials)"
|
||||
|
||||
patterns-established:
|
||||
- "CASL ability factory pattern: defineAbilityFor(user) returns ability, used in middleware and handlers"
|
||||
- "withPermission('action', 'Subject')(handler) wraps route for coarse-grained guard"
|
||||
- "Fine-grained checks done inside handler using passed ability object"
|
||||
- "Single-role users get full ability including cannot() rules; multi-role gets positive-only union"
|
||||
|
||||
# Metrics
|
||||
duration: 7min
|
||||
completed: 2026-03-04
|
||||
---
|
||||
|
||||
# Phase 1 Plan 04: CASL RBAC Permission System Summary
|
||||
|
||||
**CASL MongoAbility permission system with 5-role matrix, withPermission() API middleware, and 66 unit tests enforcing all role boundaries including critical Technician billing block**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** 7 min
|
||||
- **Started:** 2026-03-04T10:50:45Z
|
||||
- **Completed:** 2026-03-04T10:57:37Z
|
||||
- **Tasks:** 2 completed
|
||||
- **Files modified:** 7 (5 created, 2 modified)
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- CASL permission matrix defined for all 5 roles (Admin, Office Staff, Collector, Technician, Client) plus super-admin bypass
|
||||
- API-layer authorization middleware (withPermission HOF) returns 401 for unauthenticated, 403 for unauthorized
|
||||
- 66 unit tests validating every role boundary — all pass, including critical Technician billing/subscriber management block
|
||||
- Multi-role additive union implemented (TECHNICIAN + COLLECTOR gets both roles' permissions)
|
||||
|
||||
## Task Commits
|
||||
|
||||
Each task was committed atomically:
|
||||
|
||||
1. **Task 1: CASL permission definitions and ability factory** - `67bb6cc` (feat)
|
||||
2. **Task 2: API authorization middleware and RBAC tests** - `1df2b2d` (feat)
|
||||
|
||||
**Plan metadata:** (included in this summary commit)
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `src/lib/casl/types.ts` - AppAbility type (MongoAbility), AppSubjects, AppActions, AppConditions
|
||||
- `src/lib/casl/permissions.ts` - definePermissionsFor() — permission matrix for all 5 roles
|
||||
- `src/lib/casl/ability.ts` - defineAbilityFor() — ability factory from session user; mergeAbilities() for multi-role union
|
||||
- `src/lib/middleware/authorize.ts` - withPermission() HOF and authorize() convenience wrapper
|
||||
- `src/lib/__tests__/rbac.test.ts` - 66 unit tests covering all role boundaries
|
||||
- `package.json` - Added @casl/ability dependency
|
||||
|
||||
## Decisions Made
|
||||
|
||||
**createMongoAbility over PureAbility:**
|
||||
PureAbility throws "You need to pass conditionsMatcher" at runtime when any rule has conditions. createMongoAbility includes the MongoDB conditions matcher built-in, which is required since Technician and Client rules use conditions ({ assignedToId: userId }, { subscriberId: userId }, etc.).
|
||||
|
||||
**cannot() rules excluded in multi-role merge:**
|
||||
When merging rules from multiple roles, cannot() (inverted) rules from a less-privileged role should not block permissions granted by a more-privileged role. The merge function only copies positive (can) rules. Single-role users still get the full ability including cannot() rules.
|
||||
|
||||
**Technician can("read", "Subscriber") — coarse-grained:**
|
||||
The Technician needs subscriber contact info to reach customers for assigned jobs. The CASL rule grants broad read capability; the data layer (Prisma query scoping) enforces that only subscribers related to assigned jobs are returned. This matches the plan's specification.
|
||||
|
||||
**Condition casting via any:**
|
||||
CASL's TypeScript types infer `MongoQuery<never>` for string-based subjects (no known model fields to validate against). Since Prisma models for Subscriber, Invoice, etc. don't exist yet, condition objects are cast through `as unknown as any`. When models are added in Phase 2+, subjects can be replaced with class types for fully type-safe conditions.
|
||||
|
||||
**No cannot() for Collector/Technician/Client:**
|
||||
Absence of a `can()` rule already blocks access — CASL's default is deny. Explicit `cannot()` rules are only needed to override a prior `can()` (like Office Staff's `can("read", "Account")` followed by `cannot("create", "Account")`). Using cannot() where unnecessary added noise.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 1 - Bug] PureAbility conditionsMatcher error at runtime**
|
||||
- **Found during:** Task 2 (running RBAC tests)
|
||||
- **Issue:** Tests using Technician and Client roles threw "You need to pass conditionsMatcher option in order to restrict access by conditions" because PureAbility doesn't include a conditions matcher by default
|
||||
- **Fix:** Replaced PureAbility with createMongoAbility throughout — this includes the MongoDB conditions matcher built-in
|
||||
- **Files modified:** src/lib/casl/types.ts, src/lib/casl/permissions.ts, src/lib/casl/ability.ts
|
||||
- **Verification:** All 66 tests pass after fix
|
||||
- **Committed in:** 1df2b2d (Task 2 commit)
|
||||
|
||||
**2. [Rule 1 - Bug] TypeScript compile errors with MongoAbility string subjects**
|
||||
- **Found during:** Task 2 (tsc --noEmit check after test fix)
|
||||
- **Issue:** CASL infers `MongoQuery<never>` for string-based subjects, making condition objects incompatible with the can() overloads
|
||||
- **Fix:** Added `cond()` helper that casts condition objects via `as unknown as any`, with explanatory comment about when this will be tightened
|
||||
- **Files modified:** src/lib/casl/permissions.ts
|
||||
- **Verification:** tsc --noEmit passes with --skipLibCheck
|
||||
- **Committed in:** 1df2b2d (Task 2 commit)
|
||||
|
||||
**3. [Rule 1 - Bug] cannot() rules in multi-role merge blocked valid permissions**
|
||||
- **Found during:** Task 2 (test failure for TECHNICIAN+COLLECTOR multi-role test)
|
||||
- **Issue:** The original ability.ts mergeAbilities() copied all rules including inverted (cannot) ones. This meant COLLECTOR's absent billing rules could conflict with other role abilities in edge cases
|
||||
- **Fix:** mergeAbilities() now only copies non-inverted (positive) rules. Single-role users still get full ability including cannot() semantics
|
||||
- **Files modified:** src/lib/casl/ability.ts
|
||||
- **Verification:** Multi-role tests pass; single-role tests (Office Staff COA block) still pass
|
||||
- **Committed in:** 1df2b2d (Task 2 commit)
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 3 auto-fixed (3x Rule 1 - Bug)
|
||||
**Impact on plan:** All auto-fixes required for correct operation. CASL's TypeScript types with string subjects have documented friction; fixes establish the pattern for future phases. No scope creep.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
- CASL's TypeScript types for string-based subjects are strict: `MongoQuery<never>` makes condition objects incompatible. Pattern: cast conditions and upgrade to class-based subjects when Prisma models arrive in Phase 2+.
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None - no external service configuration required.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
- RBAC system complete and tested — all API routes in Phase 2+ should use withPermission() wrapper
|
||||
- authorize() convenience alias available for handler-first style
|
||||
- Ability factory accepts session user shape directly from getCurrentUser() return value
|
||||
- Condition-based permissions (Technician, Client) will be tightened when Prisma models added in Phase 2
|
||||
- Phase 1 Foundation is now: DB + Schema (01), Auth (02), Multi-tenancy (03), RBAC (04) — ready for Phase 1-05 (final foundation task)
|
||||
|
||||
---
|
||||
*Phase: 01-foundation*
|
||||
*Completed: 2026-03-04*
|
||||
Reference in New Issue
Block a user