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
9.1 KiB
phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, duration, completed
| phase | plan | subsystem | tags | requires | provides | affects | tech-stack | key-files | key-decisions | patterns-established | duration | completed | |||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-foundation | 04 | auth |
|
|
|
|
|
|
|
|
7min | 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:
- Task 1: CASL permission definitions and ability factory -
67bb6cc(feat) - 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, AppConditionssrc/lib/casl/permissions.ts- definePermissionsFor() — permission matrix for all 5 rolessrc/lib/casl/ability.ts- defineAbilityFor() — ability factory from session user; mergeAbilities() for multi-role unionsrc/lib/middleware/authorize.ts- withPermission() HOF and authorize() convenience wrappersrc/lib/__tests__/rbac.test.ts- 66 unit tests covering all role boundariespackage.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 viaas 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