eeg_portal/docs/compose/specs/code-review-critical-fixes.md
Bernhard Müller cfc32b3a7d chore: cleanup test data and add missing test files
- Add TariffInviteServiceTest, GlobalExceptionHandlerTest, setup-test.ts
- Add compose plans/specs docs
- Extend .gitignore with *.xlsx, *.ps1, *.http, *.py, *.docx
- Remove stale test data files and scripts from working tree
2026-07-24 11:48:56 +02:00

7.2 KiB

Spec: Code-Review-Fixes — Kritische und hohe Befunde

Zusammenfassung

Behebung von 3 kritischen und 5 hohen Code-Review-Findings im EEG Portal Backend. Die kritischen Finds betreffen Sicherheitslücken (fehlende Authorization, IDOR) und inkonsistente Fehlerbehandlung. Die hohen Finds betreffen Code-Qualität, Konsistenz und ein Produktionsrisiko.

Ausgangslage

Ein Code-Review hat 8 Befunde identifiziert:

# Schwere Befund
1 KRITISCH EnergyCommunityAdminController fehlt @PreAuthorize — Admin-Endpunkte ohne Rollenprüfung
2 KRITISCH NotificationService.markAsRead hat IDOR — jede User kann beliebige Notifications lesen
3 KRITISCH GlobalExceptionHandler fehlt MethodArgumentNotValidException-Handler — @Valid-Fehler → 500 statt 400
4 HOCH Inkonsistente Exception-Strategy — ResponseStatusException bypassed GlobalExceptionHandler
5 HOCH TariffService Code-Duplizierung — ~30 Zeilen validierende Logik doppelt
6 HOCH AtNumberAlreadyExistsException-Handler verwirft Exception-Message
7 HOCH MeteringPoint.membershipsCascadeType.ALL ohne orphanRemoval (latentes Risiko)
8 HOCH ddl-auto: update als Prod-Default — kann Schema still ändern

Befund-Detail

1. Fehlende @PreAuthorize auf EnergyCommunityAdminController (KRITISCH)

Datei: eeg_backend/.../community/api/EnergyCommunityAdminController.java:18-21

Ist-Zustand: Kein @PreAuthorize auf Klassenebene. Endpunkte unter /api/admin/energy-communities sind nur durch URL-Muster gesichert (SecurityFilterChain), nicht durch annotation-basierte Authorization. Im Vergleich: AdminIamController hat @PreAuthorize("hasRole('ADMIN')") auf Klassenebene.

Soll-Zustand: @PreAuthorize("hasRole('ADMIN')") auf Klassenebene, konsistent mit AdminIamController.

Risiko: Jeder authentifizierte MEMBER kann Admin-Endpoints aufrufen (Community erstellen, bearbeiten, löschen), sofern der SecurityFilterChain keine explizite URL-Prüfung hat.

2. IDOR in NotificationService.markAsRead (KRITISCH)

Dateien:

  • eeg_backend/.../common/api/NotificationController.java:33-38
  • eeg_backend/.../common/service/NotificationService.java:39-45

Ist-Zustand: markAsRead(UUID id) nimmt nur die Notification-ID, kein @CurrentUserId. Der Service macht keinen Ownership-Check. Andere Methoden im selben Controller (getNotifications, getUnreadCount, markAllAsRead) verwenden korrekt @CurrentUserId.

Soll-Zustand: Controller fügt @CurrentUserId String userId Parameter hinzu. Service-Signatur wird markAsRead(UUID id, UUID userId). Ownership-Check: notification.getUserId().equals(userId), sonst AccessDeniedException.

3. Fehlender MethodArgumentNotValidException-Handler (KRITISCH)

Datei: eeg_backend/.../common/exception/GlobalExceptionHandler.java

Ist-Zustand: Kein @ExceptionHandler(MethodArgumentNotValidException.class). Spring's Default-Handler liefert ein eigenes JSON-Format (field errors, global errors, object name) statt des projektspezifischen ErrorResponse(message, status). @Valid-Fehler produzieren inkonsistente Responses.

Soll-Zustand: Handler extrahiert BindingResult-Fehler, formatiert sie als komma-separierte Nachricht, gibt 400 BAD_REQUEST mit ErrorResponse zurück.

4. Inkonsistente Exception-Strategy (HOCH)

Dateien:

  • eeg_backend/.../iam/service/AdminIamService.java:53-66 (rejectUser)
  • eeg_backend/.../community/service/EnergyCommunityService.java:53-73 (update, delete)

Ist-Zustand:

  • AdminIamService.approveUser: wirft IllegalArgumentException/IllegalStateException → GlobalExceptionHandler → 400/409
  • AdminIamService.rejectUser: wirft ResponseStatusException → bypassed GlobalExceptionHandler → Spring-Default
  • EnergyCommunityService.getCommunityById: IllegalArgumentException → 400
  • EnergyCommunityService.update: ResponseStatusException(NOT_FOUND) → bypassed
  • EnergyCommunityService.delete: IllegalArgumentException + ResponseStatusException(CONFLICT) → gemischt

Soll-Zustand: Einheitlich IllegalArgumentException (400) und IllegalStateException (409) in Services. ResponseStatusException nur in Controllern (wie AdminIamController.approveUser already macht).

Mapping: IAE → 400 (GlobalExceptionHandler:39-45), ISE → 409 (GlobalExceptionHandler:47-53).

5. TariffService Code-Duplizierung (HOCH)

Datei: eeg_backend/.../tariff/service/TariffService.java:74-197

Ist-Zustand: createUserTariff (Zeilen 76-126) und updateUserTariff (Zeilen 160-197) teilen ~30 Zeilen identische Validierung:

  • source != target MeteringPoint
  • Beide Points vorhanden (findByById)
  • Beide Points ACTIVE (MakoState)
  • Community-Tarif vorhanden
  • Preis <= Maximalpreis
  • Beide User aktive Members der Community

Soll-Zustand: Private Methode validateUserTariffRequest(UUID communityId, UserTariffRequest request) extrahieren. createUserTariff ruft sie plus Duplicate-Tarif-Check und Invite-Check auf. updateUserTariff ruft sie plus Ownership-Check auf.

6. AtNumberAlreadyExistsException-Handler verwirft Message (HOCH)

Datei: eeg_backend/.../common/exception/GlobalExceptionHandler.java:55-60

Ist-Zustand: Handler gibt hardcoded "Zählerpunkt existiert bereits" zurück, obwohl AtNumberAlreadyExistsException eine Message mit der AT-Nummer enthält.

Soll-Zustand: ex.getMessage() verwenden, wie bei allen anderen Handlern.

7. MeteringPoint.memberships ohne orphanRemoval (HOCH)

Datei: eeg_backend/.../community/domain/MeteringPoint.java:51-52

Ist-Zustand: @OneToMany(mappedBy = "meteringPoint", cascade = CascadeType.ALL) — kein orphanRemoval. Wenn ein Membership aus der Liste entfernt wird, wird es nicht automatisch aus der DB gelöscht.

Soll-Zustand: orphanRemoval = true hinzufügen. Defensive Absicherung, auch wenn aktuell kein Delete-Endpoint existiert.

8. ddl-auto: update als Prod-Default (HOCH)

Datei: eeg_backend/src/main/resources/application-prod.yml:9

Ist-Zustand: ddl-auto: ${JPA_DDL_AUTO:update} — Default update kann in Produktion Spalten still hinzufügen/ändern, aber nie löschen.

Soll-Zustand: Default auf validate ändern. Schema-Änderungen nur über Migrationen (Flyway/Liquibase) oder manuell.

Technische Abhängigkeiten

Finding 3 (Handler-Ergänzung) ─┐
Finding 6 (AtNumber-Handler)  ─┤── Finding 4 (Exception-Unification)
Finding 1 (PreAuthorize)      ─┤
Finding 2 (IDOR-Fix)          ─┤
Finding 5 (Refactoring)       ─┤
Finding 7 (orphanRemoval)     ─┤
Finding 8 (ddl-auto)          ─┘

Finding 3 und 6 müssen VOR Finding 4 kommen, da die Exception-Unification auf den bestehenden Handlern aufbaut. Alle anderen Findings sind unabhängig voneinander.

Scope

Im Scope

  • Nur Backend-Änderungen (Java, YAML)
  • Bestehende Tests aktualisieren (nicht neue Test-Suiten erstellen, wo Tests existieren)
  • Conventional Commits

Nicht im Scope

  • Frontend-Änderungen
  • Flyway/Liquibase-Migration (nur ddl-auto Default ändern)
  • MeteringPoint-Lösch-Endpoint erstellen (nur orphanRemoval als Absicherung)
  • Audit-Logging
  • Rollenbasierte SecurityFilterChain-Änderungen