55d8434f35
- 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)
317 lines
14 KiB
Markdown
317 lines
14 KiB
Markdown
# 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.1–1.15) | T-01 to T-06, T-25 | ✅ |
|
||
| Deferred: Club settings | Phase 2 (2.1–2.8) | T-07 to T-10 | ✅ |
|
||
| Deferred: Staff CRUD + invite | Phase 3 (3.1–3.15) | T-11 to T-14, T-25 | ✅ |
|
||
| Deferred: Reports (US-007, US-008) | Phase 4 (4.1–4.9) | T-15 to T-18 | ✅ |
|
||
| Deferred: Member portal | Phase 5 (5.1–5.7) | T-19, T-20 | ✅ |
|
||
| Deferred: Prevention Officer (US-010) | Phase 6 (6.1–6.8) | T-21, T-22 | ✅ |
|
||
| Deferred: Integration tests | Phase 7 (7.1–7.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.2–3.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 |
|