14 KiB
Plan: Sprint 13 — Production Hardening
Date: 2026-06-18 Author: Patrick Plate / Lumen (Planner) Status: v2 (panel review incorporated) Basis: cannamanage-sprint13-analysis.md
Background
Sprint 13 addresses 3 remaining production-blocking security vulnerabilities (1 was already fixed), wires the existing test infrastructure into CI/CD as a quality gate, expands backend test coverage for security-critical paths, and performs repo cleanup. No new features — pure hardening.
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Security Hardening Layer │
├─────────────────────────────────────────────────────────────────┤
│ │
│ SecurityConfig ──► Role matchers for /api/v1/documents/** │
│ │
│ DocumentController ──► @PreAuthorize + tenant verification │
│ │
│ DocumentService ──► FilenameUtils.getName() sanitization │
│ ──► Explicit clubId check on download/delete │
│ │
├─────────────────────────────────────────────────────────────────┤
│ CI/CD Quality Gate │
├─────────────────────────────────────────────────────────────────┤
│ │
│ deploy.yml ──► [Checkout] → [Test Backend] → [Test Frontend] │
│ ──► [Build Images] → [Deploy] → [Health Check] │
│ │
├─────────────────────────────────────────────────────────────────┤
│ Operational Hardening │
├─────────────────────────────────────────────────────────────────┤
│ │
│ CORS ──► Externalized via application.properties │
│ Rate Limiting ──► Bucket4j on /api/v1/auth/login │
│ │
└─────────────────────────────────────────────────────────────────┘
Components
| # | Component | Module | Action |
|---|---|---|---|
| 1 | DocumentController | cannamanage-api | Add @PreAuthorize, tenant verification |
| 2 | DocumentService | cannamanage-service | Sanitize filename, add clubId check |
| 3 | SecurityConfig | cannamanage-api | Add document endpoint matchers |
| 4 | deploy.yml | .gitea/workflows | Add test steps before deployment |
| 5 | DocumentServiceTest | cannamanage-service | New — comprehensive test class |
| 6 | DocumentControllerTest | cannamanage-api | New — security-focused integration tests |
| 7 | AuthServiceTest | cannamanage-api | New — auth flow tests |
| 8 | SecurityConfig | cannamanage-api | Externalize CORS origins |
| 9 | RateLimitFilter | cannamanage-api | New — login rate limiting |
| 10 | package.json | cannamanage-frontend | Fix project name |
| 11 | README.md | root | New — project documentation |
Implementation Steps
Phase 1: Security Fixes (Priority: CRITICAL)
Step 1.1 — Fix IDOR in DocumentController
File: cannamanage-api/src/main/java/de/cannamanage/api/controller/DocumentController.java
- Add
@PreAuthorize("hasAnyRole('ADMIN', 'STAFF', 'MEMBER')")on class level - Inject
SecurityContextHolderto extract current user'sclubId - On
downloadDocument(UUID id): after fetching the document, verifydocument.getClubId().equals(currentUser.getClubId()) - On
deleteDocument(UUID id): same tenant check + requireADMINorSTAFFrole - Return
404 Not Foundif tenant mismatch — prevents object enumeration. An attacker should not be able to determine whether a document UUID exists in another tenant.
Prerequisite: Verify that portal JWT tokens include the clubId claim. If portal tokens lack clubId, portal document downloads will 403. Check PortalAuthService / JwtService token generation to confirm the claim is present before implementing tenant verification.
Step 1.2 — Fix Path Traversal in DocumentService
File: cannamanage-service/src/main/java/de/cannamanage/service/DocumentService.java
- Replace
file.getOriginalFilename()withFilenameUtils.getName(file.getOriginalFilename()) - Add null check: if result is blank, use
"document"as fallback - Add dependency on
commons-ioif not already present (it is — used by BankImportService) - Pattern: consistent with existing
BankImportServicewhich already does this correctly
Step 1.3 — Add Document Endpoint Matchers to SecurityConfig
File: cannamanage-api/src/main/java/de/cannamanage/api/security/SecurityConfig.java
Add explicit matchers in apiSecurityFilterChain():
.requestMatchers(HttpMethod.GET, "/api/v1/documents/**").hasAnyRole("ADMIN", "STAFF", "MEMBER")
.requestMatchers(HttpMethod.POST, "/api/v1/documents/**").hasAnyRole("ADMIN", "STAFF")
.requestMatchers(HttpMethod.DELETE, "/api/v1/documents/**").hasAnyRole("ADMIN", "STAFF")
Place these BEFORE the .anyRequest().authenticated() catch-all.
Phase 2: CI/CD Quality Gate (Priority: HIGH)
Step 2.1 — Add Backend Test Step to deploy.yml
File: .gitea/workflows/deploy.yml
Insert a new step after checkout, before Docker build:
- name: Run backend tests
run: |
set -euo pipefail
cd cannamanage-api
mvn test --batch-mode -f pom.xml
Note: Maven is available in the runner image. The Maven Wrapper (mvnw) is not committed to this repo.
This runs all backend tests. If any test fails, the deploy is aborted.
Step 2.2 — Add Frontend Lint/Type-Check Step
Prerequisite: Add "type-check": "tsc --noEmit" to cannamanage-frontend/package.json scripts.
Insert after backend tests:
- name: Frontend type check
run: |
set -euo pipefail
cd cannamanage-frontend
corepack enable
pnpm install --frozen-lockfile
pnpm run lint
pnpm run type-check
Note: Full Playwright integration tests are too heavy for every push (require Docker-in-Docker). Keep those for manual/nightly runs via docker-compose.test.yml.
Step 2.3 — Make Frontend Health Check Blocking
File: .gitea/workflows/deploy.yml
Change the frontend verify step to exit 1 on failure instead of just logging a warning.
Phase 3: Backend Test Expansion (Priority: HIGH)
Step 3.1 — DocumentServiceTest (new)
File: cannamanage-service/src/test/java/de/cannamanage/service/DocumentServiceTest.java
Test cases:
testUploadDocument_sanitizesFilename()— verify path traversal attempt is neutralizedtestUploadDocument_nullFilename_usesFallback()— null filename → "document"testUploadDocument_validFilename_preserved()— normal filename passes throughtestDownloadDocument_wrongTenant_throwsForbidden()— tenant isolationtestDownloadDocument_correctTenant_returnsContent()— happy pathtestDeleteDocument_wrongTenant_throwsForbidden()— tenant isolation on deletetestDeleteDocument_adminRole_succeeds()— admin can delete
Step 3.2 — DocumentControllerSecurityTest (new)
File: cannamanage-api/src/test/java/de/cannamanage/api/controller/DocumentControllerSecurityTest.java
Integration tests using @WebMvcTest + @WithMockUser:
testDownload_unauthenticated_returns401()testDownload_wrongTenant_returns403()testDownload_correctTenant_returns200()testDelete_memberRole_returns403()— MEMBER cannot deletetestDelete_staffRole_returns200()— STAFF can deletetestUpload_memberRole_returns403()— MEMBER cannot uploadtestUpload_staffRole_returns200()
Step 3.3 — AuthServiceTest (new)
File: cannamanage-api/src/test/java/de/cannamanage/api/service/AuthServiceTest.java
testLogin_validCredentials_returnsTokenPair()testLogin_invalidPassword_throws401()testLogin_nonExistentUser_throws401()testRefreshToken_validToken_returnsNewAccess()testRefreshToken_expired_throws401()testSha256_consistent()— hashing determinism
Step 3.4 — SecurityConfigTest (new)
File: cannamanage-api/src/test/java/de/cannamanage/api/security/SecurityConfigTest.java
Verify URL pattern matching:
testDocumentEndpoints_requireAuthentication()testAuthEndpoints_arePublic()testActuatorHealth_isPublic()
Phase 4: Operational Hardening (Priority: MEDIUM)
Step 4.1 — Externalize CORS Configuration
File: cannamanage-api/src/main/resources/application.properties
Add:
cannamanage.cors.allowed-origins=${CORS_ALLOWED_ORIGINS:http://localhost:3000}
File: cannamanage-api/src/main/java/de/cannamanage/api/security/SecurityConfig.java
Replace hardcoded origins with @Value("${cannamanage.cors.allowed-origins}") and split on comma.
Step 4.2 — Login Rate Limiting
Add Bucket4j dependency to cannamanage-api/pom.xml:
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j-core</artifactId>
<version>8.10.1</version>
</dependency>
Add Caffeine dependency to cannamanage-api/pom.xml:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
Create cannamanage-api/src/main/java/de/cannamanage/api/security/LoginRateLimitFilter.java:
- Use Caffeine cache (TTL-based eviction) instead of raw ConcurrentHashMap
- Key: IP address, Value: Bucket
- Max entries: 10,000
- TTL: 10 minutes (auto-evicts stale entries, prevents memory leak under DDoS)
- Limit: 5 attempts per minute per IP
- Applies only to
POST /api/v1/auth/login - Returns
429 Too Many RequestswithRetry-Afterheader when exceeded - Register in SecurityConfig filter chain
Phase 5: Repo Cleanup (Priority: LOW)
Step 5.1 — Fix package.json Project Name
File: cannamanage-frontend/package.json
Change "name": "shadboard-nextjs-starter-kit" → "name": "cannamanage-frontend"
Step 5.2 — Remove Dead .github Directory
rm -rf .github/
Only if .github/ contains GitHub-specific config (Actions, Dependabot) that doesn't apply to Gitea.
Step 5.3 — Create Root README
File: README.md
Minimal project README: what it is, how to run locally, how to deploy, architecture overview.
Step 5.4 — Clean Up Leftover Screenshot Scripts
Remove one-shot .mjs scripts from cannamanage-frontend/:
upload-dialog-screenshot.mjssprint12-final.mjssprint12-v2.mjs
These were development utilities, not part of the test suite.
Step 5.5 — Fix SonarQube Findings
Address 7 MAJOR/MINOR findings from the security review:
- Remove unused
auditServicefield in DocumentService (or wire it up for audit logging) - Replace generic
throw new Exception()in Camt053Parser with specific exception - Replace generic
RuntimeExceptionin AuthService.sha256() with custom exception - Extract duplicated
"Invalid credentials"string to constant - Fix static access via instance in Camt053Parser
Dependency Order
Step 1.1 ──┐
Step 1.2 ──┼──► Step 3.1 (tests verify the fixes)
Step 1.3 ──┘ │
▼
Step 3.2 (controller security tests)
│
▼
Step 2.1 (CI runs these tests)
Step 2.2
Step 2.3
│
▼
Step 4.1, 4.2 (operational hardening)
Step 5.1–5.5 (cleanup, parallel)
Acceptance Criteria
- ✅ No authenticated user can download/delete documents from another tenant
- ✅ Path traversal filenames are sanitized before storage
- ✅
/api/v1/documents/**has explicit role matchers in SecurityConfig - ✅ CI pipeline runs backend tests before deployment (fails on test failure)
- ✅ CI pipeline runs frontend lint + type-check before deployment
- ✅ At least 15 new backend tests covering security-critical paths
- ✅ CORS origins configurable via environment variable
- ✅ Login endpoint rate-limited (5 attempts/min/IP)
- ✅
package.jsonhas correct project name - ✅ Root README exists
Estimated Effort
| Phase | Effort | Cumulative |
|---|---|---|
| Phase 1: Security Fixes | ~2h | 2h |
| Phase 2: CI Quality Gate | ~1.5h | 3.5h |
| Phase 3: Backend Tests | ~3h | 6.5h |
| Phase 4: Operational Hardening | ~2h | 8.5h |
| Phase 5: Repo Cleanup | ~1h | 9.5h |
Total: ~9.5 hours (full sprint day)