Files
cannamanage/docs/sprint-3/cannamanage-sprint3-plan-review.md
Patrick Plate 55d8434f35 feat(sprint-3): Phase 1 — staff permissions + token revocation
- StaffPermission enum (8 granular permissions)
- StaffAccount JPA entity with permissions collection
- RevokedToken entity for JWT blacklisting
- Flyway V3 migration (staff_accounts, staff_account_permissions, revoked_tokens)
- StaffAccountRepository + RevokedTokenRepository
- TokenRevocationService with Caffeine cache (60s TTL, 10k max)
- StaffPermissionChecker SpEL bean (@staffPermissions.has)
- PreventionOfficerChecker SpEL bean (@preventionOfficer.check)
- JwtService: added jti claim + generateStaffAccessToken + extractJti/extractPermissions
- JwtAuthFilter: token blacklist check via TokenRevocationService
- SecurityConfig: STAFF role added to endpoint matchers
- Controllers updated with @PreAuthorize for fine-grained access
- TokenCleanupScheduler (daily 03:00 cleanup of expired revoked tokens)
- Caffeine dependency added to cannamanage-service
- Unit tests: StaffPermissionCheckerTest (7), TokenRevocationServiceTest (9)
2026-06-11 16:45:21 +02:00

317 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Plan Review: CannaManage Sprint 3
**Date:** 2026-06-11
**Reviewer:** Roo (Plan Reviewer)
**Documents:** Sprint 3 Plan v2 (APPROVED by Planner)
**Verdict:** ✅ APPROVED
---
## Summary
The Sprint 3 plan is comprehensive, technically sound, and well-ordered. All deferred Sprint 2 features (staff, portal, reports, prevention officer, club settings) are covered. Architecture decisions are consistent with the existing codebase. The plan correctly builds on Sprint 2's shared-schema + TenantFilterAspect pattern rather than the architecture doc's theoretical schema-per-tenant model. 7 non-blocking findings identified — all are suggestions for improvement, none require plan revision.
---
## Review Checklist
### 1. Does the plan cover ALL deferred Sprint 2 items?
| Deferred Item | Plan Coverage | Status |
|---------------|--------------|--------|
| STAFF role | Phase 1 (StaffPermission enum, StaffAccount entity, SpEL checker) | ✅ |
| Member portal | Phase 5 (session-based SecurityFilterChain, PortalController) | ✅ |
| Reports | Phase 4 (ReportController, iText 7 PDF, Commons CSV) | ✅ |
| Prevention Officer | Phase 6 (configurable limit, assignment endpoint, under-21 gate) | ✅ |
| Club settings | Phase 2 (ClubController GET/PUT /clubs/me, stats) | ✅ |
| Integration tests | Phase 7 (Testcontainers PostgreSQL, 6 test classes) | ✅ |
**Result: ✅ PASS** — All deferred items fully addressed.
---
### 2. Architecture consistency with Sprint 2 patterns
| Pattern | Sprint 2 Implementation | Sprint 3 Plan | Consistent? |
|---------|------------------------|---------------|-------------|
| Tenant isolation | `TenantFilterAspect` (shared-schema, Hibernate @Filter) | Continues shared-schema; `staff_accounts` has `tenant_id` | ✅ |
| JWT auth | `JwtService` + `JwtAuthFilter` (stateless, JJWT) | Extends with `jti` + `permissions` claims | ✅ |
| SecurityConfig | `@Order(1)` API + `@Order(2)` Public | Inserts Portal `@Order(2)`, shifts Public to `@Order(3)` | ✅ |
| Role model | `UserRole` enum: `ROLE_ADMIN`, `ROLE_STAFF`, `ROLE_MEMBER` | Uses same enum; STAFF already present in code | ✅ |
| Entity base | `AbstractTenantEntity` with `tenant_id` | New entities (StaffAccount, InviteToken) follow same pattern | ✅ |
| Repositories | Spring Data JPA in `cannamanage-service/.../repository/` | New repos placed in same package | ✅ |
**Result: ✅ PASS** — Fully consistent with established patterns.
---
### 3. Staff permission model well-defined?
| Aspect | Assessment |
|--------|-----------|
| Enum values | 8 permissions covering all current features + 1 future (`MANAGE_GROW_CALENDAR`) | ✅ |
| Storage | JSONB on `staff_accounts` — correct for PostgreSQL, supports flexible permission sets | ✅ |
| Enforcement | SpEL `@PreAuthorize` + custom `StaffPermissionChecker` bean | ✅ |
| JWT embedding | Permissions in JWT for stateless checks; blacklist fallback for revoked tokens | ✅ |
| ADMIN bypass | `StaffPermissionChecker.has()` returns `true` for ADMIN role first | ✅ |
| Templates | 3 role templates (Ausgabe, Lager, Vorstand) — matches architecture doc | ✅ |
**Result: ✅ PASS** — Well-designed, DSGVO-compliant least-privilege model.
---
### 4. Member portal auth design clean?
| Aspect | Assessment |
|--------|-----------|
| Dual SecurityFilterChain | `@Order(2)` for `/portal/**` — correct isolation from API chain | ✅ |
| Session-based | `SessionCreationPolicy.IF_REQUIRED` + 30min timeout | ✅ |
| CSRF | Enabled via `CookieCsrfTokenRepository.withHttpOnlyFalse()` — React SPA can read cookie | ✅ |
| Read-only | All portal endpoints are GET — minimal attack surface | ✅ |
| Data isolation | Member can only see own data (enforced by `memberId` from session principal) | ✅ |
| `SameSite=Strict` | Correct for CSRF prevention | ✅ |
**Result: ✅ PASS** — Clean separation of concerns.
---
### 5. Dependencies pinned to specific versions?
| Library | Plan Version | Pinned? |
|---------|-------------|---------|
| iText 7 | `8.0.x` | ⚠️ Range |
| Apache Commons CSV | `1.11+` | ⚠️ Range |
| Caffeine | `3.1+` | ⚠️ Range |
| Spring Boot Starter Mail | Boot-managed | ✅ (BOM) |
| Testcontainers | `1.19+` | ⚠️ Range |
**Result: ⚠️ WARNING (non-blocking)** — Versions use ranges instead of exact pins. This is acceptable for a plan document (exact versions determined at implementation time by checking latest stable), but the implementor should pin exact versions in the POM.
---
### 6. Flyway V3 migration complete?
| Schema Change | Covered in V3? |
|---------------|---------------|
| `staff_accounts` table | ✅ Full DDL with JSONB, indexes, unique constraint |
| `revoked_tokens` table | ✅ With jti index + expires index |
| `invite_tokens` table | ✅ With token index, FK to users |
| `users.prevention_officer` column | ✅ `ALTER TABLE ADD COLUMN` |
| Club extension columns (9 columns) | ✅ All `ALTER TABLE ADD COLUMN IF NOT EXISTS` |
**Result: ✅ PASS** — All new tables and columns defined in a single idempotent migration.
---
### 7. Test plan comprehensive?
| Phase | Unit Tests | Integration Tests | Total |
|-------|-----------|-------------------|-------|
| P1 (Staff + Revocation) | T-01 to T-06 | — | 6 |
| P2 (Club) | T-07 to T-10 | — | 4 |
| P3 (Staff CRUD + Invite) | T-11 to T-14 | — | 4 |
| P4 (Reports) | T-15 to T-18 | — | 4 |
| P5 (Portal) | — | T-19, T-20 | 2 |
| P6 (Prevention Officer) | T-21, T-22 | — | 2 |
| P7 (Integration) | — | T-23 to T-26 | 4 |
| **Total** | **18** | **8** | **26** |
Coverage check:
- Every phase has at least one test: ✅
- Edge cases (expired token, invalid regex, permission denial): ✅
- E2E flows (invite → set-password → login → permission check): ✅
- Tenant isolation: ✅
- Token revocation lifecycle: ✅
**Result: ✅ PASS** — Comprehensive coverage matching all implementation phases.
---
### 8. Gaps between API spec and implementation plan?
| API Spec Endpoint | Sprint 3 Plan | Gap? |
|-------------------|---------------|------|
| `GET /clubs/me` (§6.1) | Phase 2 — `ClubController` | ✅ Covered |
| `PUT /clubs/me` (§6.2) | Phase 2 — `ClubController` | ✅ Covered |
| `GET /clubs/me/stats` (§6.3) | Phase 2 — `ClubStatsResponse` DTO | ✅ Covered |
| `POST /auth/logout` (§5.3) | Not explicitly addressed | ⚠️ See finding #3 |
| `GET /reports/monthly` (§10) | Phase 4 — `ReportController` | ✅ Covered |
| `GET /reports/members` (§10) | Phase 4 — member list export | ✅ Covered |
| `GET /reports/recall/{batchId}` (§10) | Phase 4 — recall report | ✅ Covered |
| Staff endpoints (not in spec) | Phase 3 — new endpoints | ️ Plan extends spec |
| Portal endpoints (not in spec) | Phase 5 — new `/portal/**` | ️ Plan extends spec |
| `POST /auth/set-password` (not in spec) | Phase 3 step 3.9 | ️ Plan extends spec |
**Result: ✅ PASS** — One minor gap (logout integration), otherwise plan both implements spec and correctly extends it for Sprint 3 scope.
---
### 9. Token revocation/blacklist approach sound?
| Aspect | Assessment |
|--------|-----------|
| DB-backed (no Redis) | Correct for MVP scale — simple, durable, no infrastructure dependency | ✅ |
| Caffeine cache (60s TTL, 10K max) | Appropriate tradeoff — worst case 60s window after revocation | ✅ |
| `jti` claim in JWT | Required for per-token revocation — currently missing, plan adds it | ✅ |
| `revokeAllForUser()` | Called on permission change + staff deactivation — correct triggers | ✅ |
| Cleanup scheduler (daily 3 AM) | Removes expired tokens to prevent table bloat | ✅ |
| Index on `jti` | Fast lookup for blacklist check | ✅ |
| Index on `expires_at` | Fast cleanup queries | ✅ |
**Result: ✅ PASS** — Sound, pragmatic approach for a club-scale application.
---
### 10. Risks not addressed?
| Potential Risk | Addressed? | Notes |
|----------------|-----------|-------|
| iText AGPL license | ✅ | Risk table mentions it, mitigation: switch to OpenPDF before go-live |
| Boot 4 `@EntityScan` issue | ✅ | Phase 7 step 7.1 explicitly addresses it |
| JSONB + Hibernate 6 | ✅ | `@JdbcTypeCode(SqlTypes.JSON)` mentioned |
| SMTP delivery | ✅ | Mailpit for dev, transactional email service for prod |
| Cache staleness | ✅ | Accepted as non-critical for club app |
| Portal CSRF + SPA | ✅ | `CookieCsrfTokenRepository.withHttpOnlyFalse()` |
| `JwtAuthFilter` portal path exclusion | ⚠️ | See finding #5 |
| Email domain regex DoS | ⚠️ | See finding #6 |
**Result: ✅ PASS** — All major risks addressed. Two minor items noted below.
---
## Findings
### ⚠️ Warnings (non-blocking — implement-time improvements)
#### 1. Dependency versions not pinned
**Plan §9** uses version ranges (`8.0.x`, `1.11+`, `3.1+`).
**Recommendation:** At implementation time, pin exact versions in the POM:
- iText 7: `8.0.5`
- Commons CSV: `1.12.0`
- Caffeine: `3.1.8`
- Testcontainers: `1.20.1`
---
#### 2. `revoked_tokens` table has no `tenant_id` column
The token blacklist is global across all tenants. This works correctly (tokens are unique by `jti` regardless of tenant), but it means:
- `revokeAllForUser(userId)` queries ALL tenants' revoked tokens
- No ability to purge one tenant's revoked tokens independently
**Recommendation:** Consider adding `tenant_id` for operational convenience. Not blocking — the current design is functionally correct.
---
#### 3. `POST /auth/logout` not integrated with token revocation
API spec §5.3 defines a logout endpoint that invalidates the refresh token. Sprint 3's `TokenRevocationService` adds access token revocation. The plan doesn't explicitly describe how these interact.
**Recommendation:** On `POST /auth/logout`:
1. Revoke refresh token (existing behavior)
2. Also add current access token's `jti` to `revoked_tokens` (new behavior)
This ensures immediate invalidation rather than waiting for natural token expiry. Implementor should handle this in Phase 1.
---
#### 4. Portal `formLogin` with JSON API may need `AuthenticationSuccessHandler`
The plan describes portal as "JSON API" (Decision D2: no Thymeleaf) but configures `formLogin()` with `defaultSuccessUrl`. Standard `formLogin` returns HTTP redirects (302), not JSON responses.
**Recommendation:** Implement a custom `AuthenticationSuccessHandler` that returns `200 OK` with session info as JSON, and a custom `AuthenticationFailureHandler` returning `401` JSON error. This aligns with the SPA architecture.
---
#### 5. `JwtAuthFilter.shouldNotFilter()` needs `/portal/**` exclusion
Current `shouldNotFilter()` skips `/api/v1/auth/`, `/swagger-ui`, `/v3/api-docs`. The plan adds a portal with session-based auth, but the JWT filter will still process `/portal/**` requests (finding no Bearer header → passes through).
**Recommendation:** Add `/portal/` to `shouldNotFilter()` for clarity and to avoid unnecessary filter processing:
```java
return path.startsWith("/api/v1/auth/")
|| path.startsWith("/portal/") // ← add this
|| path.startsWith("/swagger-ui")
|| path.startsWith("/v3/api-docs");
```
Not strictly blocking (filter already passes through when no Bearer header present), but cleaner.
---
#### 6. Regex pattern validation — potential ReDoS
`validateEmailDomain()` uses `email.matches(club.getAllowedEmailPattern())` with admin-supplied regex. Malicious or poorly written regex could cause catastrophic backtracking (ReDoS).
**Recommendation:** Add a timeout or use `Pattern.compile()` with a simple validation check:
```java
try {
Pattern.compile(pattern); // validates syntax
// Additionally: reject patterns with known dangerous constructs
// Or use a timeout: java.util.concurrent.CompletableFuture with timeout
} catch (PatternSyntaxException e) {
throw new InvalidRegexException(pattern);
}
```
The plan does validate invalid regex (step 2.7, test T-10) but doesn't mention ReDoS protection specifically.
---
#### 7. Architecture doc deviation should be noted
The architecture doc (03-Architecture.md) describes:
- Schema-per-tenant (Sprint 2 implemented shared-schema with `tenant_id`)
- `ROLE_CLUB_ADMIN` / `ROLE_PREVENTION_OFFICER` (code uses `ROLE_ADMIN` / boolean flag)
- 8-hour access token (code uses 1-hour)
These are expected evolutionary deviations — the architecture doc reflects initial design, and Sprint 2 made pragmatic choices. The architecture doc should be updated to reflect reality, but this doesn't block Sprint 3.
**Recommendation:** Add a backlog item to sync the wiki architecture doc with actual implementation post-Sprint 3.
---
## Traceability Matrix
| Requirement Source | Plan Step | Test Case | Status |
|-------------------|-----------|-----------|--------|
| Deferred: STAFF role | Phase 1 (1.11.15) | T-01 to T-06, T-25 | ✅ |
| Deferred: Club settings | Phase 2 (2.12.8) | T-07 to T-10 | ✅ |
| Deferred: Staff CRUD + invite | Phase 3 (3.13.15) | T-11 to T-14, T-25 | ✅ |
| Deferred: Reports (US-007, US-008) | Phase 4 (4.14.9) | T-15 to T-18 | ✅ |
| Deferred: Member portal | Phase 5 (5.15.7) | T-19, T-20 | ✅ |
| Deferred: Prevention Officer (US-010) | Phase 6 (6.16.8) | T-21, T-22 | ✅ |
| Deferred: Integration tests | Phase 7 (7.17.8) | T-23 to T-26 | ✅ |
| Decision D1: Token revocation | Phase 1 (1.5, 1.6, 1.10, 1.14) | T-05, T-06, T-26 | ✅ |
| Decision D2: Portal as JSON API | Phase 5 | T-19, T-20 | ✅ |
| Decision D3: Multiple prevention officers | Phase 6 (6.2, 6.7) | T-21 | ✅ |
| Decision D4: Minimal PDF branding | Phase 4 (4.4, 4.5) | T-16 | ✅ |
| Decision D5: Testcontainers | Phase 7 (7.2) | T-23 to T-26 | ✅ |
| Decision D6: Invite flow | Phase 3 (3.23.9) | T-11, T-12, T-25 | ✅ |
| Decision D7: Email domain whitelist | Phase 2 (2.7), Phase 3 (3.5) | T-09, T-10 | ✅ |
---
## Verdict
### ✅ APPROVED
The Sprint 3 plan is complete, technically sound, and ready for implementation. All 10 review checklist items pass. 7 non-blocking warnings are noted as implementation-time improvements — none require plan revision.
**Recommendation:** Proceed to implementation. The implementor should address warnings #3 (logout integration), #4 (portal auth handlers), and #5 (JwtAuthFilter exclusion) during Phase 1 and Phase 5 respectively.
---
## Review Metadata
| Field | Value |
|-------|-------|
| Review iteration | 1 of 3 (max) |
| Plan version reviewed | v2 |
| Time spent | ~15 minutes |
| Confidence | 92% |
| Blocking findings | 0 |
| Non-blocking findings | 7 |