Files
MABEA/backend/app/services/mindermenge.py
T
patrickandClaude Sonnet 5 ceddeb1bf4
CI / backend-tests (push) Failing after 19s
CI / frontend-build (push) Successful in 25s
Multi-Agent-Review: Race Conditions, DB-Indizes, TLS/Security-Härtung
Konsolidierte Funde aus postgres-/sql-/jwt-/owasp-top10-expert-Review:

- Race Conditions gefixt: doppelte aktive Kontrolle (SAVEPOINT + partieller
  Unique-Index), doppelte Mindermengen-Genehmigung (FOR UPDATE + Unique-Index),
  Lost-Update bei Nachfüllung (FOR UPDATE auf Fehlbestand/Objektposition).
- Migration 0007: partielle Unique-Indizes als DB-Sicherheitsnetz + fehlende
  FK-Indizes (fehlbestand.material_id, kontrolle(objekt_id,status),
  zustaendigkeit, benutzer_rolle.rolle, objektposition.ablaufdatum u.a.).
- Connection-Pool explizit begrenzt (pool_size=5, max_overflow=5) - ohne das
  könnte jeder uvicorn-Worker den Postgres max_connections-Wert sprengen.
- Timing-Angriff bei Login-Enumeration gefixt (konstante Antwortzeit über
  Dummy-Hash), JWT-Decode verlangt jetzt exp/sub-Claims.
- App-seitiges Rate-Limiting (slowapi, 5/min) auf /auth/login als Verteidigung
  in der Tiefe zusätzlich zum nginx-Limit.
- nginx: TLS mit selbstsigniertem Zertifikat (LAN-Betrieb, keine Domain),
  HSTS, Content-Security-Policy, Permissions-Policy ergänzt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
2026-09-04 18:47:06 +02:00

78 lines
3.0 KiB
Python

from datetime import datetime, timezone
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.fehlbestand import Fehlbestand, FehlbestandStatus
from app.models.mindermenge import MindermengeStatus, MindermengenGenehmigung
from app.services import historie as historie_service
class FehlbestandBereitsErledigtError(Exception):
"""Ein bereits erledigter Fehlbestand braucht keine Mindermengen-Bewertung mehr."""
class BereitsGenehmigtError(Exception):
"""Für diesen Fehlbestand existiert bereits eine aktive Genehmigung."""
async def genehmigen(
db: AsyncSession,
*,
fehlbestand: Fehlbestand,
begruendung: str,
genehmigt_von: int,
zustaendiger_server_id: int,
) -> MindermengenGenehmigung:
"""Prompt 04: reine fachliche Bewertung - ändert NIEMALS Ist-Menge, Fehlmenge
oder den Fehlbestand-Status selbst (Leitplanke, U2). Verknüpft mit der Kontrolle,
die den Fehlbestand ausgelöst hat ("aktuelle Kontrolle des Objekts", Prompt 04.3.4)."""
if fehlbestand.status == FehlbestandStatus.erledigt:
raise FehlbestandBereitsErledigtError()
if fehlbestand.kontrolle_id is None:
# Sollte praktisch nicht vorkommen (jeder Fehlbestand entsteht aus einer
# Kontrolle, Sprint 3), aber das Schema erlaubt kontrolle_id=NULL.
raise ValueError("Fehlbestand ohne auslösende Kontrolle kann nicht genehmigt werden")
# Row-Lock auf den Fehlbestand serialisiert parallele Genehmigungsversuche
# (sql-expert-Review-Fund: TOCTOU zwischen Check und Insert ohne Lock).
await db.execute(select(Fehlbestand).where(Fehlbestand.id == fehlbestand.id).with_for_update())
result = await db.execute(
select(MindermengenGenehmigung).where(
MindermengenGenehmigung.fehlbestand_id == fehlbestand.id,
MindermengenGenehmigung.status == MindermengeStatus.aktiv,
)
)
if result.scalar_one_or_none() is not None:
raise BereitsGenehmigtError()
genehmigung = MindermengenGenehmigung(
fehlbestand_id=fehlbestand.id,
genehmigt_von=genehmigt_von,
begruendung=begruendung,
genehmigt_am=datetime.now(timezone.utc),
ausloesende_kontrolle_id=fehlbestand.kontrolle_id,
status=MindermengeStatus.aktiv,
)
db.add(genehmigung)
try:
await db.flush()
except IntegrityError as exc:
# Sicherheitsnetz gegen den partiellen Unique-Index (Migration 0007),
# falls der Lock oben je umgangen würde (z. B. anderer Isolation-Level).
raise BereitsGenehmigtError() from exc
await historie_service.log(
db,
zustaendiger_server_id=zustaendiger_server_id,
benutzer_id=genehmigt_von,
ereignistyp="mindermenge_genehmigt",
entitaet_typ="mindermengen_genehmigung",
entitaet_id=genehmigung.id,
neuer_wert={"fehlbestand_id": str(fehlbestand.id), "status": "aktiv"},
begruendung=begruendung,
)
return genehmigung