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:
@@ -10,28 +10,28 @@ See: .planning/PROJECT.md (updated 2026-03-04)
|
|||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Phase: 1 of 5 (Foundation)
|
Phase: 1 of 5 (Foundation)
|
||||||
Plan: 3 of 5 in current phase
|
Plan: 4 of 5 in current phase
|
||||||
Status: In progress
|
Status: In progress
|
||||||
Last activity: 2026-03-04 — Completed 01-03-PLAN.md (tenant provisioning, Prisma middleware, RLS, isolation tests)
|
Last activity: 2026-03-04 — Completed 01-04-PLAN.md (CASL RBAC, 5-role permission matrix, API middleware, 66 tests)
|
||||||
|
|
||||||
Progress: [███░░░░░░░] 15% (3/20 plans across all phases)
|
Progress: [████░░░░░░] 20% (4/20 plans across all phases)
|
||||||
|
|
||||||
## Performance Metrics
|
## Performance Metrics
|
||||||
|
|
||||||
**Velocity:**
|
**Velocity:**
|
||||||
- Total plans completed: 3
|
- Total plans completed: 4
|
||||||
- Average duration: 9.3 min
|
- Average duration: 8.75 min
|
||||||
- Total execution time: 28 min
|
- Total execution time: 35 min
|
||||||
|
|
||||||
**By Phase:**
|
**By Phase:**
|
||||||
|
|
||||||
| Phase | Plans | Total | Avg/Plan |
|
| Phase | Plans | Total | Avg/Plan |
|
||||||
|-------|-------|-------|----------|
|
|-------|-------|-------|----------|
|
||||||
| 01-foundation | 3/5 complete | 28 min | 9.3 min |
|
| 01-foundation | 4/5 complete | 35 min | 8.75 min |
|
||||||
|
|
||||||
**Recent Trend:**
|
**Recent Trend:**
|
||||||
- Last 5 plans: 01-01 (11 min), 01-02 (8 min), 01-03 (9 min)
|
- Last 5 plans: 01-01 (11 min), 01-02 (8 min), 01-03 (9 min), 01-04 (7 min)
|
||||||
- Trend: stable at ~9 min/plan
|
- Trend: stable at ~8-9 min/plan
|
||||||
|
|
||||||
*Updated after each plan completion*
|
*Updated after each plan completion*
|
||||||
|
|
||||||
@@ -59,6 +59,11 @@ Recent decisions affecting current work:
|
|||||||
- [01-03]: findUnique cross-tenant protection routes through findFirst internally — Prisma unique key cannot have tenantId injected without changing where shape
|
- [01-03]: findUnique cross-tenant protection routes through findFirst internally — Prisma unique key cannot have tenantId injected without changing where shape
|
||||||
- [01-03]: RLS USING allows null app.current_tenant_id — super-admin mode (no tenant context) sees all rows
|
- [01-03]: RLS USING allows null app.current_tenant_id — super-admin mode (no tenant context) sees all rows
|
||||||
- [01-03]: Initial migration baselined with migrate resolve --applied (schema was created via db push in 01-01)
|
- [01-03]: Initial migration baselined with migrate resolve --applied (schema was created via db push in 01-01)
|
||||||
|
- [01-04]: createMongoAbility used throughout (not PureAbility) — string subjects require conditionsMatcher which createMongoAbility provides built-in
|
||||||
|
- [01-04]: cannot() rules excluded when merging multi-role abilities — additive union means more roles = more (never less) access
|
||||||
|
- [01-04]: Condition objects cast via any for string subjects — CASL infers MongoQuery<never> for strings; tighten when Prisma models defined in Phase 2+
|
||||||
|
- [01-04]: Technician can("read", "Subscriber") coarse-grained — data layer enforces actual scope to assigned job contacts only
|
||||||
|
- [01-04]: withPermission() HOF wraps Next.js route handlers; authorize() as convenience alias
|
||||||
|
|
||||||
### Pending Todos
|
### Pending Todos
|
||||||
|
|
||||||
@@ -68,10 +73,10 @@ None.
|
|||||||
|
|
||||||
- [Phase 1 research flag]: MikroTik RouterOS Node.js client library maintenance status is LOW confidence — verify `node-routeros` vs `mikronode` before implementing router integration (MikroTik integration is v2, but adapter interface should be planned)
|
- [Phase 1 research flag]: MikroTik RouterOS Node.js client library maintenance status is LOW confidence — verify `node-routeros` vs `mikronode` before implementing router integration (MikroTik integration is v2, but adapter interface should be planned)
|
||||||
- [Phase 3 research flag]: Semaphore SMS API pricing/stability for 2026 is MEDIUM confidence — verify before any SMS work (SMS is v2, but abstraction layer design is relevant)
|
- [Phase 3 research flag]: Semaphore SMS API pricing/stability for 2026 is MEDIUM confidence — verify before any SMS work (SMS is v2, but abstraction layer design is relevant)
|
||||||
- [01-03 resolved]: prisma-tenant.ts TypeScript errors fixed — Prisma $extends create/upsert required destructuring tenant relation before spreading tenantId
|
- [01-04 note]: CASL condition types for string subjects use any cast — upgrade to class-based subjects when Phase 2 Prisma models (Subscriber, Invoice, etc.) are defined
|
||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-03-04T10:45:22Z
|
Last session: 2026-03-04T10:57:37Z
|
||||||
Stopped at: Completed 01-03-PLAN.md (tenant provisioning + Prisma middleware + RLS + isolation tests)
|
Stopped at: Completed 01-04-PLAN.md (CASL RBAC + 5-role permission matrix + API middleware + 66 tests)
|
||||||
Resume file: None
|
Resume file: None
|
||||||
|
|||||||
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