fix: billing tenant isolation, invoice status validation, SMTP password masking

H2: list_invoices now passes tenant_id (from current_user) to get_invoices;
    get_invoices applies WHERE tenant_id filter for non-global-admin users;
    get_invoice_endpoint returns 404 when tenant mismatch for non-admins.

H3: InvoiceStatusUpdate.status changed to Literal["draft","sent","paid","cancelled"]
    for schema-level validation; guard also added in update_invoice_status service.

H5: _settings_to_out masks smtp_password as "***" when set, "" when empty;
    update_settings skips writing when value is the "***" sentinel.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 13:40:59 +02:00
co-authored by Claude Sonnet 4.6
parent a11d2fb1e7
commit b69190dd86
5 changed files with 25 additions and 5 deletions
+9
View File
@@ -534,5 +534,14 @@ Der Admin-Settings-Endpunkt (`GET /api/admin/settings`) erfordert `global_admin`
### 2026-07-22 | Workflow-Editor | _legacy_dispatch umging cancelled/rejected Pre-Check
`_legacy_dispatch` in `dispatch_service.py` rief `render_order_line_task.delay()` direkt auf und umging damit den Pre-Check in `dispatch_order_line_render` (der cancelled/rejected Lines überspringt). Alle Legacy-Dispatch-Pfade (auch im Graph-Fallback) liefen so durch, auch für bereits gecancelte Jobs. **Lösung:** `_legacy_dispatch` ruft jetzt `dispatch_order_line_render.delay()` auf statt `render_order_line_task.delay()` direkt.
### 2026-07-22 | Security | Billing-Rechnungen ohne Tenant-Isolation
`list_invoices` in `billing/router.py` übergab `tenant_id` nicht an `get_invoices`. `get_invoices` im Service ignorierte den Parameter — kein WHERE-Filter vorhanden. Jeder `admin_or_pm`-User konnte alle Rechnungen aller Tenants sehen. **Lösung:** Router extrahiert `tenant_id` aus `current_user` (nur wenn nicht `global_admin`), Service filtert mit `.where(Invoice.tenant_id == tenant_id)`. `get_invoice_endpoint` prüft zusätzlich `inv.tenant_id == current_user.tenant_id` für Nicht-Admins.
### 2026-07-22 | Security | Invoice-Status nicht validiert — beliebige Strings schreibbar
`InvoiceStatusUpdate.status` war `str` ohne Einschränkung. `VALID_STATUSES` in `billing/service.py` war definiert aber nie referenziert. **Lösung:** `InvoiceStatusUpdate.status` auf `Literal["draft","sent","paid","cancelled"]` geändert (Pydantic-Validierung auf Schemaebene). Zusätzlich guard in `update_invoice_status` als defence-in-depth.
### 2026-07-22 | Security | SMTP-Passwort im Klartext in GET /api/admin/settings
`SettingsOut.smtp_password` gab den gespeicherten Wert an jeden `global_admin` zurück. **Lösung:** `_settings_to_out` maskiert den Wert — `"***"` wenn gesetzt, `""` wenn leer. `update_settings` überspringt das Schreiben wenn `body.smtp_password == "***"` (Sentinel für "unverändert lassen").
### 2026-07-22 | Refactoring | Turntable-Branch aus render_order_line_task extrahiert
`render_order_line_task` war ein 427-Zeilen-Monolith. Der Turntable-Branch (~55 Zeilen) wurde in `_render_turntable(*, render_invocation, step_path, output_path, template, order_line_id, emit, pl)` ausgelagert — resolved objects als Parameter (Option B), damit Session und PipelineLogger im Main Task verbleiben und kein doppeltes DB-Lookup entsteht. Der 68-Zeilen Exception-Handler (Mark-as-failed bei max_retries) wurde in `_handle_render_task_exhausted(order_line_id, exc, tenant_id)` extrahiert; die Retry-Logik (`self.retry`) bleibt im Main Task, da sie den Celery `self`-Context benötigt. Beide Helper stehen in `render_order_line.py` vor den Task-Definitionen.