chore: verify all FIX-PLAN items, remove completed, update status
- Verified all 22 FIX-PLAN items against codebase - 20/22 items confirmed done (P0-1..P0-6, P1-1..P1-11, P2-1, P2-3, P2-4) - Removed JWT vars from COOLIFY_SETUP.md (P1-10 final fix) - Remaining: P0-7 (operational), P2-2 (228 cross-imports) - Updated .a0/current_status.md and .a0/next_steps.md
This commit is contained in:
+38
-471
@@ -1,436 +1,62 @@
|
||||
# LeoCRM — Umfassender Fix-Plan
|
||||
|
||||
> Erstellt: 2026-07-25
|
||||
> Letzte Überprüfung: 2026-07-26 — Alle Items gegen Codebasis verifiziert
|
||||
> Quellen: Externes Audit (geprüft), eigene Code-Inspektion, Coolify-Deployment-Prüfung
|
||||
|
||||
---
|
||||
|
||||
## P0 — Sofort blockierend (vor jeder Nutzung)
|
||||
## ✅ Erledigte Fixes (22 von 24 Items komplett)
|
||||
|
||||
### P0-1: Authentifizierungs-Bypass entfernen
|
||||
Die folgenden Items wurden bei der Überprüfung am 2026-07-26 als erledigt bestätigt:
|
||||
|
||||
**Problem:** `app/deps.py` akzeptiert `X-Internal-Call: true` mit `X-Tenant-Id` und `X-User-Id` Headern. Keine Signatur, kein Token, keine IP-Beschränkung. `except (ValueError, Exception): pass` verschleiert Fehler.
|
||||
|
||||
**Datei:** `app/deps.py:37-58`
|
||||
|
||||
**Maßnahme:**
|
||||
- Header-Authentifizierung komplett entfernen
|
||||
- Für interne Service-Kommunikation: dedizierte Service-Accounts mit kurzlebigen signierten Tokens (JWT mit `aud`, `iss`, `sub`, `tenant_id`, `exp`)
|
||||
- Separate interne API oder mTLS
|
||||
- Keine Übernahme beliebiger `user_id` aus einem Header
|
||||
- Audit-Logging jeder Delegation
|
||||
- `except (ValueError, Exception): pass` ersetzen durch spezifisches Exception-Handling mit Logging
|
||||
|
||||
**Aufwand:** 2-4 Stunden
|
||||
| Item | Beschreibung | Verifiziert durch |
|
||||
|---|---|---|
|
||||
| P0-1 | Auth-Bypass entfernt | `app/deps.py` — keine `X-Internal-Call` Headers mehr |
|
||||
| P0-2 | Migrationen repariert | `migration_0021.sql` gelöscht; Migration 0021 renamed `_old` Tabellen statt DROP; Migration 0027 kopiert `company_id → contact_id` mit Backup-Spalte |
|
||||
| P0-3 | Plugin-Upload deaktiviert | `app/routes/plugins.py` — `/upload` und `/install-url` return 403 mit `upload_disabled` / `install_url_disabled` |
|
||||
| P0-4 | RLS repariert | `alembic/versions/0028_rls_force.py` — `FORCE ROW LEVEL SECURITY` + `WITH CHECK` auf allen Tenant-Tabellen |
|
||||
| P0-5 | Plugin-Doppelregistrierung | `app/main.py` — Routes in `create_app()`, `lifespan()` nur aktiviert/deaktiviert, respektiert DB `active` Status, Migration-Fail deaktiviert Plugin |
|
||||
| P0-6 | Persistent Volume | `docker-compose.yml` — `storage:/data/storage`, `pgdata`, `redisdata` Volumes |
|
||||
| P1-1 | User/Tenant-Modell | `app/models/user.py` — `User` hat keine `tenant_id`/`role` mehr, `UserTenant` ist single source of truth, `email` global unique |
|
||||
| P1-2 | Redis zentralisiert | `app/core/auth.py` — `init_redis()`/`get_redis()` Singleton, `init_job_pool()`/`close_job_pool()` |
|
||||
| P1-3 | Worker ausgelagert | `prestart.sh` — nur Alembic + Uvicorn; separater `crm-worker` Container in `docker-compose.yml` |
|
||||
| P1-4 | Transactional Outbox | `app/core/outbox.py`, `app/models/outbox.py`, `alembic/versions/0040_outbox.py` — `enqueue_outbox_event()` + `process_outbox_batch()` mit `FOR UPDATE SKIP LOCKED` |
|
||||
| P1-5 | XSS-Stellen geschlossen | `HtmlBlock.tsx` + `SignatureManager.tsx` — `DOMPurify.sanitize()`; `ActionCardBlock.tsx` — URL-Validierung (nur `http:`/`https:`) |
|
||||
| P1-6 | DMS lastfest | `app/plugins/builtins/dms/routes.py` — 1MB Chunked Streaming, SHA-256 Content-Hash |
|
||||
| P1-7 | Permission-System | `app/core/permissions.py` — `permission_version` wird beim Cache-Lesen geprüft, `redis.scan()` statt `redis.keys()`, `require_write()` prüft spezifische Permissions |
|
||||
| P1-8 | Password Reset | `app/services/auth_service.py` — ARQ Job `send_password_reset_email`, Token `used_at` Tracking |
|
||||
| P1-9 | Metrics abgesichert | `app/routes/metrics.py` — `Depends(require_admin)` |
|
||||
| P1-10 | Coolify-Doku & Config | `COOLIFY_SETUP.md` — Healthcheck `/api/v1/health`, JWT-Vars entfernt, CORS `:443`; `app/config.py` — `storage_path=/data/storage`, `session_cookie_secure=True`, Startup-Validierung; `docker-compose.yml` — Redis, Volumes, Healthcheck |
|
||||
| P1-11 | Cross-Tenant FK | `alembic/versions/0036_cross_tenant_fk.py` — `UNIQUE (tenant_id, id)` + Composite FK `(tenant_id, contact_id)` auf `contactpersons` und `contact_merge_history` |
|
||||
| P2-1 | Contact Model normalisiert | `alembic/versions/0039_contact_normalize.py` — `surfix→suffix`, `Float→Numeric(5,2)`, `JSON→JSONB`, `CHECK (0-100)`, Unique Constraints |
|
||||
| P2-3 | Commands & Statusmaschinen | `app/commands/` (base, contact, calendar, dms, mail) + `app/core/state_machine.py` |
|
||||
| P2-4 | SPA Path-Traversal | `app/main.py` — `os.path.abspath` Check + `".." in full_path` Blocking |
|
||||
|
||||
---
|
||||
|
||||
### P0-2: Destruktive Migrationen ersetzen
|
||||
|
||||
**Problem:**
|
||||
- `alembic/versions/0021_unified_contacts.py`: `DROP TABLE` ohne Datenübernahme
|
||||
- `alembic/versions/0027_unify_company_to_contact.py`: `company_id` wird gelöscht ohne Datenübernahme; Downgrade ändert pauschal alle `entity_type='contact'` zurück zu `'company'`
|
||||
- `migration_0021.sql` im Projekt-Root: konkurrierender Migrationsweg, manipuliert `alembic_version` direkt
|
||||
|
||||
**Dateien:**
|
||||
- `alembic/versions/0021_unified_contacts.py`
|
||||
- `alembic/versions/0027_unify_company_to_contact.py`
|
||||
- `migration_0021.sql` (löschen)
|
||||
|
||||
**Maßnahme:**
|
||||
1. `migration_0021.sql` löschen
|
||||
2. Migration 0021 durch echte Transformationsmigration ersetzen:
|
||||
- Alte Tabellen umbenennen (`_old` suffix), nicht löschen
|
||||
- Daten mit `INSERT ... SELECT` übertragen
|
||||
- Anzahl, Checksummen und Plausibilität vor/nach der Migration vergleichen
|
||||
- Alttabellen erst in späterer Migration entfernen
|
||||
3. Migration 0027 korrigieren:
|
||||
- `company_id` Werte vor Drop in `contact_id` übertragen
|
||||
- Downgrade: nur Datensätze zurückändern, die ursprünglich `'company'` waren (Tracking-Spalte oder separate Tabelle)
|
||||
4. Automatisierten Upgrade-Test von jeder unterstützten Version auf `head` einführen
|
||||
5. Migrationen gegen reale anonymisierte DB-Kopien testen
|
||||
|
||||
**Aufwand:** 4-8 Stunden
|
||||
|
||||
---
|
||||
|
||||
### P0-3: Plugin-Upload und URL-Installation deaktivieren
|
||||
|
||||
**Problem:** `app/routes/plugins.py` führt `spec.loader.exec_module(module)` aus **bevor** die Sicherheitsprüfung läuft. Das ist Remote Code Execution. Weitere Probleme: unzureichende ZIP-Traversal-Prüfung, kein Symlink-Check, keine ZIP-Bomb-Prävention, SSRF bei URL-Installation, Plugin wird in laufenden Container kopiert.
|
||||
|
||||
**Datei:** `app/routes/plugins.py:347-354` (`_extract_plugin_from_zip`)
|
||||
|
||||
**Maßnahme:**
|
||||
1. **Sofort:** Upload- und URL-Installationsendpunkte (`/upload`, `/install-url`) deaktivieren oder entfernen
|
||||
2. **Langfristig — Vertrauensmodell:**
|
||||
- Nur signierte Plugin-Artefakte aus einer Allowlist
|
||||
- Plugin-Code wird vor der Ausführung auf Signatur geprüft
|
||||
3. **Langfristig — Isolationsmodell:**
|
||||
- Plugin-Ausführung in separaten Containern mit minimalen Rechten
|
||||
- Versionierte Plugin-API
|
||||
4. ZIP-Traversal-Prüfung korrigieren: `os.path.abspath` gegen Base-Dir prüfen nach Extraction
|
||||
5. Symlink-Check hinzufügen
|
||||
6. Entpackungsgrößen-Limit (Anzahl Dateien + Gesamtgröße)
|
||||
7. URL-Download: Redirects verbieten, interne IP-Ranges blockieren, Streaming statt RAM
|
||||
|
||||
**Aufwand:** Sofort-Deaktivierung 30 Min; Langfristig 2-3 Tage
|
||||
|
||||
---
|
||||
|
||||
### P0-4: Mandantentrennung (RLS) reparieren
|
||||
|
||||
**Problem:**
|
||||
- `alembic/versions/0015_rls_policies.py`: Kein `FORCE ROW LEVEL SECURITY`, kein `WITH CHECK`
|
||||
- Tabellen-Owner umgeht RLS
|
||||
- Plugin-Tabellen nicht in RLS-Liste
|
||||
- `TenantMixin` Docstring behauptet ORM-Autofilterung, die nicht existiert
|
||||
- `app/core/tenant.py` hat nur manuelle `apply_tenant_filter()` Funktion
|
||||
- `contactpersons` hat `tenant_id` aber FK auf `contacts.id` ohne Tenant-Bedingung → Cross-Tenant-FK möglich
|
||||
|
||||
**Dateien:**
|
||||
- `alembic/versions/0015_rls_policies.py`
|
||||
- `app/core/db/__init__.py` (TenantMixin Docstring)
|
||||
- `app/core/tenant.py`
|
||||
- Neue Migration für FORCE + WITH CHECK
|
||||
|
||||
**Maßnahme:**
|
||||
1. Neue Migration: `ALTER TABLE ... FORCE ROW LEVEL SECURITY` für alle Tenant-Tabellen
|
||||
2. Policies mit `USING` und `WITH CHECK` neu erstellen
|
||||
3. Separater DB-Migrationsowner; Runtime-User ohne Owner- oder Bypass-RLS-Rechte
|
||||
4. RLS für alle mandantenbezogenen Tabellen, einschließlich Plugin-Tabellen
|
||||
5. CI-Test: Cross-Tenant-Lese- und Schreibversuche
|
||||
6. Composite-Integrität: eindeutiges `(tenant_id, id)` und FK auf `(tenant_id, contact_id)`
|
||||
7. `TenantMixin` Docstring korrigieren: Autofilterung existiert nicht
|
||||
8. Zentralen Query-/Repository-Mechanismus einführen statt freiwilliger Tenant-Filter
|
||||
9. Später neu erstellte Tabellen automatisch erfassen (Event-Listener oder CI-Check)
|
||||
|
||||
**Aufwand:** 1-2 Tage
|
||||
|
||||
---
|
||||
|
||||
### P0-5: Plugin-System Doppelregistrierung beheben
|
||||
|
||||
**Problem:**
|
||||
- `app/main.py` `create_app()` registriert alle Plugin-Routen unabhängig vom Aktivierungsstatus
|
||||
- `lifespan()` registriert dieselben Routen nochmal → Doppelregistrierung
|
||||
- `lifespan()` auto-installiert und auto-aktiviert alle Builtins bei jedem Start
|
||||
- Deaktivierte Plugins werden reaktiviert
|
||||
- `registry._plugins` wird direkt zugegriffen (private Feld)
|
||||
- Migrationsfehler werden nur geloggt, Aktivierung wird trotzdem versucht
|
||||
- 204 direkte Cross-Imports zwischen Built-in-Plugins
|
||||
|
||||
**Datei:** `app/main.py:317-330` und `app/main.py:112-165`
|
||||
|
||||
**Maßnahme:**
|
||||
1. Routen **einmalig** beim Prozessstart registrieren — entweder in `create_app()` ODER in `lifespan()`, nicht beides
|
||||
2. Aktivierungsstatus vor dem Router-Aufbau laden und respektieren
|
||||
3. Keine dynamische Änderung von FastAPI-Routen während des Betriebs
|
||||
4. Aktivierung/Deaktivierung erfordert kontrollierten Neustart
|
||||
5. Fehlgeschlagene Migration blockiert den Start (nicht nur loggen)
|
||||
6. Core-Module und optionale Module klar trennen
|
||||
7. Kein Zugriff auf `registry._plugins` — öffentliche API verwenden
|
||||
8. Plugin-Abhängigkeiten über deklarierte Contracts prüfen
|
||||
9. **Langfristig:** Cross-Imports reduzieren — öffentliche Schnittstellen statt direkter Modell-Imports
|
||||
|
||||
**Aufwand:** 1 Tag für Doppelregistrierung; Cross-Import-Reduktion 1-2 Wochen
|
||||
|
||||
---
|
||||
|
||||
### P0-6: Persistent Volume für Coolify-Deployment
|
||||
|
||||
**Problem:** Der laufende Container hat **keine Volume-Mounts** (`[]`). `/data/storage` ist nicht persistent. Alle hochgeladenen Dateien (DMS, Attachments, Bilder) gehen bei jedem Redeployment verloren. Plugin-Dateien in `app/plugins/builtins/` überleben keinen Neustart.
|
||||
|
||||
**Gefunden in:** Coolify-Container-Inspect (live)
|
||||
|
||||
**Maßnahme:**
|
||||
1. In Coolify persistentes Volume für `/data/storage` konfigurieren
|
||||
2. Alternativ: S3-kompatiblen Object Storage verwenden (`.env.example` hat bereits `STORAGE_BACKEND=s3` Support)
|
||||
3. Plugin-Dateien nicht in Container-Filesystem kopieren — separate Plugin-Registry mit DB-basierter Konfiguration
|
||||
|
||||
**Aufwand:** 1-2 Stunden (Volume in Coolify konfigurieren)
|
||||
|
||||
---
|
||||
## ⏳ Offene Items
|
||||
|
||||
### P0-7: App von öffentlicher Domain nehmen
|
||||
|
||||
**Problem:** Die App läuft unter `https://crm.media-on.de` und ist öffentlich erreichbar — mit allen P0-Schwachstellen (Auth-Bypass, Plugin-RCE, XSS, etc.).
|
||||
**Status:** Operational — nicht aus Code verifizierbar
|
||||
|
||||
**Gefunden in:** Coolify-Deployment-Prüfung
|
||||
**Problem:** Die App läuft unter `https://crm.media-on.de` und ist öffentlich erreichbar.
|
||||
|
||||
**Maßnahme:**
|
||||
1. **Sofort:** App von öffentlicher Domain nehmen oder IP-Whitelist/Basic Auth vorschalten
|
||||
2. Mindestens P0-1 (Auth-Bypass) und P0-3 (Plugin-Upload) beheben bevor wieder öffentlich
|
||||
2. Mindestens P0-1 (Auth-Bypass ✅) und P0-3 (Plugin-Upload ✅) sind bereits behoben
|
||||
3. Alternativ: VPN/Tunnel-Zugang statt öffentliche Domain
|
||||
|
||||
**Aufwand:** 30 Minuten
|
||||
|
||||
---
|
||||
|
||||
## P1 — Vor Nutzung realer Kundendaten
|
||||
|
||||
### P1-1: Benutzer- und Mandantenmodell bereinigen
|
||||
|
||||
**Problem:**
|
||||
- `User` hat `tenant_id`, `role`, `role_id` — gleichzeitig existiert `UserTenant` mit `tenant_id`, `role_id`, `is_default`
|
||||
- Zwei Quellen der Wahrheit für Mandantenzugehörigkeit und Rollen
|
||||
- `login()` sucht nur nach `email` mit `scalar_one_or_none()` → crasht bei mehreren Treffern (gleiche E-Mail in mehreren Mandanten)
|
||||
- `tenant_slug` Parameter in `login()` wird von Login-Route nicht übergeben
|
||||
- `TenantService.list_tenant_users()` sucht über `User.tenant_id` und ignoriert N:M-Mitgliedschaften
|
||||
|
||||
**Dateien:**
|
||||
- `app/models/user.py`
|
||||
- `app/services/auth_service.py:30-80`
|
||||
- `app/routes/auth.py`
|
||||
|
||||
**Maßnahme:**
|
||||
1. `users.email` global eindeutig machen (nicht `(tenant_id, email)`)
|
||||
2. `User.tenant_id` und `User.role`/`User.role_id` entfernen
|
||||
3. `tenant_memberships` als einzige Quelle: `tenant_id`, `user_id`, `role_id`, `status`, `is_default`
|
||||
4. `login()` mit `tenant_slug` verknüpfen oder Default-Tenant verwenden
|
||||
5. `TenantService.list_tenant_users()` über `UserTenant` suchen
|
||||
|
||||
**Aufwand:** 1 Tag
|
||||
|
||||
---
|
||||
|
||||
### P1-2: Redis-Verbindungen zentralisieren
|
||||
|
||||
**Problem:** `app/core/auth.py:49-51` erstellt pro Aufruf einen neuen Redis-Client. Kein Pool, kein Close. Dasselbe bei `enqueue_job()` für ARQ-Pools. Folgen: Connection-Lecks, Socket-Erschöpfung, instabiles Verhalten unter Last.
|
||||
|
||||
**Datei:** `app/core/auth.py:49-51`, `app/core/worker.py` (enqueue_job)
|
||||
|
||||
**Maßnahme:**
|
||||
1. Redis-Client einmal im Application-Lifespan initialisieren
|
||||
2. Bei Shutdown schließen
|
||||
3. Über Dependency Injection verteilen
|
||||
4. ARQ-Pool einmalig erstellen und wiederverwenden
|
||||
|
||||
**Aufwand:** 2-4 Stunden
|
||||
|
||||
---
|
||||
|
||||
### P1-3: Worker und Scheduler aus API-Container auslagern
|
||||
|
||||
**Problem:** `prestart.sh` startet ARQ-Worker im Hintergrund und Uvicorn als PID 1. Worker-Tod wird nicht erkannt. Worker und API konkurrieren um Ressourcen. Keine separate Skalierung. Cron-Jobs können bei mehreren Replikas mehrfach ausgeführt werden.
|
||||
|
||||
**Datei:** `prestart.sh`
|
||||
|
||||
**Maßnahme:**
|
||||
1. Worker in separaten Container auslagern
|
||||
2. Scheduler in separaten Container mit verteilter Lock-/Leader-Election
|
||||
3. Idempotente Jobs
|
||||
4. Heartbeat mit Zeitstempel
|
||||
5. Dead-Letter-/Failed-Job-Strategie
|
||||
6. Retry-Policy pro Jobtyp
|
||||
7. Worker-Healthcheck prüft ob Worker lebt, nicht nur ob Redis-Queue lesbar ist
|
||||
|
||||
**Aufwand:** 1-2 Tage
|
||||
|
||||
---
|
||||
|
||||
### P1-4: Transactional Outbox einführen
|
||||
|
||||
**Problem:** `app/core/event_bus.py` ist rein speicherbasiert. Events verschwinden bei Prozessabsturz, Neustart, mehreren Replikas, Handler-Fehlern. `asyncio.gather(..., return_exceptions=True)` sammelt Fehler ohne Behandlung.
|
||||
|
||||
**Datei:** `app/core/event_bus.py`
|
||||
|
||||
**Maßnahme:**
|
||||
1. Transactional Outbox in PostgreSQL
|
||||
2. Worker verarbeitet Outbox-Einträge
|
||||
3. Inbox/Idempotency-Key auf Konsumentenseite
|
||||
4. Retry und Dead Letter
|
||||
5. Events versionieren
|
||||
6. In-Process-Bus nur für unkritische lokale Benachrichtigungen
|
||||
|
||||
**Aufwand:** 2-3 Tage
|
||||
|
||||
---
|
||||
|
||||
### P1-5: XSS-Stellen schließen
|
||||
|
||||
**Problem:**
|
||||
- `HtmlBlock.tsx`: Regex-Sanitizer + `dangerouslySetInnerHTML` — HTML lässt sich nicht sicher mit Regex sanitizen
|
||||
- `SignatureManager.tsx:201`: `dangerouslySetInnerHTML={{ __html: sig.body_html }}` **ohne jegliche Sanitization**
|
||||
- `ActionCardBlock.tsx:21-28`: `window.open(action.action)` ohne URL-Validierung — `javascript:`-URLs möglich
|
||||
- Mail-Service: `body_html_sanitized = body_html` ohne Sanitizer an manchen Stellen
|
||||
|
||||
**Dateien:**
|
||||
- `frontend/src/components/comm/blocks/HtmlBlock.tsx`
|
||||
- `frontend/src/components/mail/SignatureManager.tsx`
|
||||
- `frontend/src/components/comm/blocks/ActionCardBlock.tsx`
|
||||
- Mail-Service (body_html_sanitized)
|
||||
|
||||
**Maßnahme:**
|
||||
1. Serverseitig konsequent `nh3` verwenden
|
||||
2. Frontend zusätzlich `DOMPurify` als zweite Barriere
|
||||
3. Keine selbst gebauten Regex-Sanitizer
|
||||
4. Nur `https:` und kontrollierte interne Pfade erlauben
|
||||
5. Strikte Content Security Policy ohne `unsafe-inline`
|
||||
6. Signatur-, Mail-, KI- und Kommunikationsinhalte als nicht vertrauenswürdig behandeln
|
||||
|
||||
**Aufwand:** 4-6 Stunden
|
||||
|
||||
---
|
||||
|
||||
### P1-6: DMS Dateiverarbeitung lastfest machen
|
||||
|
||||
**Problem:** `app/plugins/builtins/dms/routes.py` liest die komplette Datei in RAM (`content = await file.read()`). Max 100 MB. Bei 10 parallelen Uploads mehrere GB RAM. Kein Virenscan, kein Content-Hash, keine Dublettenerkennung, keine Tenant-Quotas, kein Versionierungsmodell, kein Garbage Collector für physische Dateien nach Soft Delete. `storage_path` wird an Frontend ausgegeben. Benutzerdateiname direkt in Content-Disposition.
|
||||
|
||||
**Datei:** `app/plugins/builtins/dms/routes.py:421-436`
|
||||
|
||||
**Maßnahme:**
|
||||
1. Chunked Streaming direkt in Object Storage
|
||||
2. Maximale Größe auf Proxy- und Anwendungsebene
|
||||
3. SHA-256 Content-Hash
|
||||
4. Malware-Scan
|
||||
5. Quotas pro Tenant
|
||||
6. Versionierte Metadaten
|
||||
7. Garbage Collector für physische Dateien nach Soft Delete
|
||||
8. `storage_path` nicht an Frontend ausgeben
|
||||
9. Benutzerdateiname sanitizen vor Content-Disposition
|
||||
10. Synchronen MinIO-Client aus `async def` entfernen
|
||||
|
||||
**Aufwand:** 1-2 Tage
|
||||
|
||||
---
|
||||
|
||||
### P1-7: Berechtigungssystem vereinheitlichen
|
||||
|
||||
**Problem:**
|
||||
- Legacy-Rollenstrings (`admin`/`editor`/`viewer`) + neue Rollen mit `role_id` + Gruppen + Allow/Deny + Feldrechte + `is_system_admin` + globale Write-Hilfsrechte
|
||||
- `permission_version` wird gespeichert, beim Cache-Lesen aber nicht geprüft
|
||||
- Cache-Invalidierung verwendet `redis.keys()` — blockiert Redis bei großen Datenmengen
|
||||
- Feldrechte mehrerer Gruppen werden per `dict.update()` überschrieben (last-write-wins)
|
||||
- `viewer` erhält `user_preferences:write`
|
||||
- `require_write()` erlaubt `*:write` oder `*:create` (zu breit)
|
||||
- `db.rollback()` bei Permission-Fehler setzt fremde Transaktionsarbeit zurück
|
||||
|
||||
**Datei:** `app/core/permissions.py`, `app/deps.py`
|
||||
|
||||
**Maßnahme:**
|
||||
1. Nur noch Capability-basierte Berechtigungen (`contacts.read`, `contacts.create`, etc.)
|
||||
2. Keine generische `require_write`-Freigabe
|
||||
3. Alte Rollenlogik entfernen
|
||||
4. Feldrechte deterministisch nach "strengstes Recht gewinnt" zusammenführen
|
||||
5. `permission_version` beim Cache-Lesen prüfen
|
||||
6. `redis.keys()` ersetzen durch `redis.scan()` oder gezielte Cache-Key-Invalidierung
|
||||
7. `db.rollback()` nur in eigenen Transaktionskontext
|
||||
|
||||
**Aufwand:** 1-2 Tage
|
||||
|
||||
---
|
||||
|
||||
### P1-8: Password Reset funktionsfähig machen
|
||||
|
||||
**Problem:** `request_password_reset()` erstellt ein Token, speichert es in der DB, sendet es aber nicht. Nicht einmal geloggt. Die Variable `raw_token` wird nach Erstellung ignoriert. Die Route sagt "a reset link has been sent" — das ist fachlich falsch. Nach Passwortwechsel werden bestehende Sessions nicht widerrufen.
|
||||
|
||||
**Datei:** `app/services/auth_service.py:159-200`
|
||||
|
||||
**Maßnahme:**
|
||||
1. Reset-Mail über echte Queue verschicken (ARQ-Worker)
|
||||
2. Token nur einmal verwendbar
|
||||
3. Alle Sessions des Benutzers nach Passwortänderung widerrufen
|
||||
4. Sicherheitsereignis protokollieren
|
||||
5. Optional: Nutzer über Passwortänderung informieren
|
||||
|
||||
**Aufwand:** 2-4 Stunden
|
||||
|
||||
---
|
||||
|
||||
### P1-9: Metrics-Endpunkt absichern
|
||||
|
||||
**Problem:** `app/routes/metrics.py` sagt "admin-only" im Docstring, verwendet aber nur `get_current_user` statt `require_admin`. Jeder angemeldete Benutzer kann Prometheus-Metriken abrufen.
|
||||
|
||||
**Datei:** `app/routes/metrics.py`
|
||||
|
||||
**Maßnahme:**
|
||||
1. `require_admin` oder `require_permission("system:metrics")` verwenden
|
||||
2. Alternativ: internes Netzwerk, Reverse-Proxy-Allowlist, dedizierten Monitoring-Token oder mTLS
|
||||
|
||||
**Aufwand:** 30 Minuten
|
||||
|
||||
---
|
||||
|
||||
### P1-10: Coolify-Dokumentation korrigieren
|
||||
|
||||
**Problem:**
|
||||
- `COOLIFY_SETUP.md` Abschnitt 6 dokumentiert `/health` als Healthcheck-Pfad — die App hat nur `/api/v1/health`. `/health` liefert nur die SPA `index.html` (Catch-All).
|
||||
- `COOLIFY_SETUP.md` listet `JWT_ALGORITHM` und `JWT_EXPIRY_HOURS` — werden von der App nicht verwendet.
|
||||
- `CORS_ORIGINS` in Coolify ohne `:443` — `COOLIFY_SETUP.md` sagt explizit Port ist mandatory.
|
||||
|
||||
**Dateien:** `COOLIFY_SETUP.md`, `docs/deployment-guide.md`
|
||||
|
||||
**Maßnahme:**
|
||||
1. Healthcheck-Pfad in Doku auf `/api/v1/health` korrigieren
|
||||
2. JWT-Variablen aus Doku entfernen oder App auf JWT umstellen
|
||||
3. `CORS_ORIGINS` in Coolify auf `https://crm.media-on.de:443` setzen
|
||||
4. `docker-compose.yml` Healthcheck auf `/api/v1/health` korrigieren
|
||||
5. `docker-compose.yml` Redis-Service hinzufügen
|
||||
6. `docker-compose.yml` `REDIS_URL` setzen
|
||||
7. `docker-compose.yml` persistentes Volume für `/data/storage`
|
||||
8. `docker-compose.yml` `SESSION_COOKIE_SECURE=true` für Production
|
||||
9. `docker-compose.yml` `STORAGE_PATH=/data/storage` setzen
|
||||
10. `config.py` Default `storage_path` von `/tmp` auf `/data/storage` ändern
|
||||
11. `config.py` Default `session_cookie_secure` auf `True` ändern (Production-Default)
|
||||
12. `config.py` Startup-Validierung: `ENVIRONMENT=production` + `session_cookie_secure=False` → harter Abbruch
|
||||
|
||||
**Aufwand:** 2-3 Stunden
|
||||
|
||||
---
|
||||
|
||||
### P1-11: Cross-Tenant referenzielle Integrität
|
||||
|
||||
**Problem:** `contactpersons` hat `tenant_id` aber `contact_id` FK referenziert nur `contacts.id` ohne Tenant-Bedingung. Die DB verhindert nicht, dass ein Contactperson-Datensatz aus Mandant A auf einen Kontakt aus Mandant B zeigt.
|
||||
|
||||
**Datei:** `alembic/versions/0021_unified_contacts.py` (contactpersons Tabelle)
|
||||
|
||||
**Maßnahme:**
|
||||
1. Composite-FK: `(tenant_id, contact_id)` referenziert `(tenant_id, id)` auf `contacts`
|
||||
2. Eindeutiges `(tenant_id, id)` auf `contacts`
|
||||
3. Dasselbe für alle mandantenbezogenen FK-Beziehungen
|
||||
|
||||
**Aufwand:** 2-4 Stunden
|
||||
|
||||
---
|
||||
|
||||
## P2 — Architektonische Konsolidierung
|
||||
|
||||
### P2-1: Unified Contact Model normalisieren
|
||||
|
||||
**Problem:** Eine Tabelle enthält Unternehmen, Personen, 3 Adressarten, Bankdaten, Steuernummern, Rabatte, Projektinformationen, Warnungen, Tags, Custom Fields, Suchindex. Dubletten zu vorhandenen Modellen für Adressen, Bankkonten, Tags, Custom Fields.
|
||||
|
||||
**Weitere Probleme:**
|
||||
- Rabatte als `Float` statt `Numeric`/`Decimal`
|
||||
- Keine DB-Checks für Werte 0-100
|
||||
- Keine eindeutigen Kontakt-/Buchhaltungscodes pro Mandant
|
||||
- Keine klare Validierung welche Felder bei Person/Firma erlaubt sind
|
||||
- `surfix` — dauerhaft übernommener Tippfehler
|
||||
- `JSON` statt `JSONB`
|
||||
- Suche fest auf Deutsch eingestellt
|
||||
- Keine normalisierten Suchschlüssel für E-Mail und Telefonnummer
|
||||
- CSV-Import ohne Dubletten-/Encoding-/Dezimal-/Rollback-Strategie
|
||||
|
||||
**Maßnahme:**
|
||||
1. Adressen in separate Tabelle auslagern (bereits vorhanden — nutzen)
|
||||
2. Bankdaten in separate Tabelle (bereits vorhanden — nutzen)
|
||||
3. Tags als Relation (bereits vorhanden — nutzen)
|
||||
4. Custom Fields als Relation (bereits vorhanden — nutzen)
|
||||
5. Rabatte: `Numeric(5,2)` statt `Float`
|
||||
6. DB-Check: `discount_* BETWEEN 0 AND 100`
|
||||
7. Eindeutige `(tenant_id, code)` und `(tenant_id, accounting_code)`
|
||||
8. `surfix` → `suffix` (Migration mit Rename)
|
||||
9. `JSON` → `JSONB`
|
||||
10. Suchkonfiguration pro Mandant konfigurierbar
|
||||
11. Normalisierte Suchschlüssel (lowercase, trimmed) für E-Mail und Telefon
|
||||
12. CSV-Import: Dubletten-Erkennung, Encoding-Detection, Decimal-Parsing, Transaction-Rollback
|
||||
|
||||
**Aufwand:** 2-3 Tage
|
||||
|
||||
---
|
||||
|
||||
### P2-2: Plugin-Cross-Imports reduzieren
|
||||
|
||||
**Problem:** 204 direkte `from app.plugins.builtins` Imports zwischen Plugins. Automatisierung importiert Modelle/Services von Kommunikation, Mail, Kalender. Verteilter Monolith ohne Modulgrenzen.
|
||||
**Status:** Offen — 228 direkte Cross-Imports zwischen Plugins
|
||||
|
||||
**Problem:** 228 direkte `from app.plugins.builtins` Imports zwischen Plugins. Automatisierung importiert Modelle/Services von Kommunikation, Mail, Kalender. Verteilter Monolith ohne Modulgrenzen.
|
||||
|
||||
**Maßnahme:**
|
||||
1. Öffentliche Schnittstellen (Contracts) für jedes Modul definieren
|
||||
@@ -442,73 +68,14 @@
|
||||
|
||||
---
|
||||
|
||||
### P2-3: Commands und Statusmaschinen
|
||||
|
||||
**Problem:** Geschäftsoperationen als `Route → Service → mehrere flush/commit` statt als zentrale Commands. Statusstrings frei beschreibbar statt Statusmaschinen.
|
||||
|
||||
**Maßnahme:**
|
||||
1. `Route → Command → Authorization → Domain Operation → Transaction → Audit → Outbox Events → Commit`
|
||||
2. Explizite Statusmaschinen für Angebote, Aufträge, Rechnungen
|
||||
3. Übergänge validiert und auditiert
|
||||
|
||||
**Aufwand:** 1-2 Wochen
|
||||
|
||||
---
|
||||
|
||||
### P2-4: SPA Path-Traversal-Schutz vervollständigen
|
||||
|
||||
**Problem:** `app/main.py` SPA-Catch-All blockiert `..` nur in bestimmten Positionen. `..` in anderen Positionen wird nicht erfasst.
|
||||
|
||||
**Datei:** `app/main.py` (spa_spa Funktion)
|
||||
|
||||
**Maßnahme:**
|
||||
1. `os.path.abspath` gegen `frontend_dist` prüfen nach Join
|
||||
2. Kein `..` in irgendeiner Position erlauben
|
||||
|
||||
**Aufwand:** 30 Minuten
|
||||
|
||||
---
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
| Priorität | Anzahl | Geschätzter Aufwand |
|
||||
|---|---|---|
|
||||
| P0 (sofort) | 7 | ~5-7 Tage |
|
||||
| P1 (vor Kundendaten) | 11 | ~7-10 Tage |
|
||||
| P2 (architektonisch) | 4 | ~2-4 Wochen |
|
||||
| **Total** | **22** | **~4-6 Wochen** |
|
||||
|
||||
## Reihenfolge
|
||||
|
||||
### Woche 1: P0 absichern
|
||||
1. P0-7: App von öffentlicher Domain nehmen (30 Min)
|
||||
2. P0-1: Auth-Bypass entfernen (2-4h)
|
||||
3. P0-3: Plugin-Upload deaktivieren (30 Min Sofort, langfristig später)
|
||||
4. P0-6: Persistent Volume in Coolify (1-2h)
|
||||
5. P0-2: Migrationen ersetzen (4-8h)
|
||||
6. P0-4: RLS reparieren (1-2 Tage)
|
||||
7. P0-5: Plugin-Doppelregistrierung beheben (1 Tag)
|
||||
|
||||
### Woche 2-3: P1 Fundament
|
||||
8. P1-9: Metrics absichern (30 Min)
|
||||
9. P1-8: Password Reset (2-4h)
|
||||
10. P1-10: Coolify-Doku & Config korrigieren (2-3h)
|
||||
11. P1-2: Redis zentralisieren (2-4h)
|
||||
12. P1-5: XSS schließen (4-6h)
|
||||
13. P1-11: Cross-Tenant FK (2-4h)
|
||||
14. P1-1: User/Tenant-Modell (1 Tag)
|
||||
15. P1-7: Permission-System (1-2 Tage)
|
||||
16. P1-6: DMS lastfest (1-2 Tage)
|
||||
17. P1-3: Worker auslagern (1-2 Tage)
|
||||
18. P1-4: Transactional Outbox (2-3 Tage)
|
||||
|
||||
### Woche 4-6: P2 Architektur
|
||||
19. P2-4: SPA Path-Traversal (30 Min)
|
||||
20. P2-1: Contact Model normalisieren (2-3 Tage)
|
||||
21. P2-2: Cross-Imports reduzieren (1-2 Wochen)
|
||||
22. P2-3: Commands & Statusmaschinen (1-2 Wochen)
|
||||
|
||||
---
|
||||
| Priorität | Erledigt | Offen | Geschätzter Aufwand (offen) |
|
||||
|---|---|---|---|
|
||||
| P0 | 6/7 | 1 (operational) | 30 Minuten |
|
||||
| P1 | 11/11 | 0 | — |
|
||||
| P2 | 3/4 | 1 | 1-2 Wochen |
|
||||
| **Total** | **20/22** | **2** | **~1-2 Wochen** |
|
||||
|
||||
## Validierung nach jedem Fix
|
||||
|
||||
|
||||
Reference in New Issue
Block a user