docs(audit): Astra-Externaudit aufgenommen — 41 Findings verifiziert, PHASE S (4 Wellen) in Roadmap, Milestone 16, Issues #396-399
- docs/audits/astra-audit-2026-09-17.md: vollstaendiger Pruefbericht (2 P0, 29 P1, 10 P2), 10 Findings intern stichprobenartig verifiziert (alle korrekt) - PLATFORM_ROADMAP.md: PHASE S (S1 Sicherheitsgrenzen, S2 Ausfuehrung verbinden, S3 Fachliche Integritaet, S4 Betriebsfreigabe) mit je Finding Korrektur+Abnahme; Abnahmeszenarien quer (Kontakt->Outbox->Worker->Suchindex->KI; Mail->Freigabe->Versand) - Phase R: 8 Astra-Kritikpunkte eingearbeitet (externe Ueberwachung, Sollzustand-Vergleich, Heartbeat statt Queue, echte Prozesse, Modelldiscovery, E2E-Szenarien, Restore-Nachweis, 95%-Formulierung als Freigabekriterien) - PROGRESS.md: Phase S als NÄCHSTE PHASE, Wellen-Issues verlinkt
This commit is contained in:
@@ -0,0 +1,799 @@
|
||||
# Externaudit Astra — Vollständiger Prüfbericht (17.09.2026)
|
||||
|
||||
> **Geprüft:** leocrm-full-2026-09-16_ee5545d.zip (Stand ee5545d, Alembic-Head 0144)
|
||||
> **Prüfmodell:** Astra (neues Modell, 2026-09-17)
|
||||
> **Bearbeitung:** Interne Verifikation 2026-09-17 — 10 Findings stichprobenartig am Code bestätigt (F01, F02, F05, F08, F10, F12, F17, F24, F37, F41, alle korrekt). Sanierung: PHASE S in PLATFORM_ROADMAP.md, Milestone + Wellen-Issues in Forgejo.
|
||||
> **Volltext unverändert übernommen aus der Modellausgabe.**
|
||||
|
||||
---
|
||||
|
||||
## Gesamturteil
|
||||
|
||||
LeoCRM hat eine brauchbare modulare Grundarchitektur. Der geprüfte Stand rechtfertigt aber noch keine belastbare Produktionsfreigabe für mandantenfähige, durch KI gesteuerte Geschäftsprozesse.
|
||||
|
||||
Das Hauptproblem sind widersprüchliche oder unterbrochene Verbindungen zwischen vorhandenen Komponenten:
|
||||
|
||||
- Berechtigungen werden je nach Zugangsweg unterschiedlich geprüft.
|
||||
- API und Worker registrieren unterschiedliche Funktionen.
|
||||
- Mandantenkontext geht an Session- und Transaktionsgrenzen verloren.
|
||||
- Migrationen und Startup-Skripte verändern dieselben Zustände unterschiedlich.
|
||||
- Erfolgsanzeigen entsprechen teilweise nicht dem tatsächlichen Ergebnis.
|
||||
|
||||
Die Sanierung kann auf dem bestehenden System aufbauen. Vorrang haben gemeinsame Ausführungspfade, verlässliche Transaktionen und durchgängige Autorisierung.
|
||||
|
||||
Die Befunde verteilen sich auf:
|
||||
|
||||
| Priorität | Bedeutung | Anzahl |
|
||||
|---|---|---|
|
||||
| P0 | Grundlegende Sicherheitsgrenze verletzt; zuerst absichern | 2 |
|
||||
| P1 | Erheblicher Sicherheits-, Funktions-, Integritäts- oder Betriebsfehler | 29 |
|
||||
| P2 | Konkreter Integrations-, Konsistenz- oder Wartbarkeitsfehler | 10 |
|
||||
|
||||
Die Einstufungen sind Auditprioritäten, keine CVSS-Bewertungen.
|
||||
|
||||
## Was tatsächlich geprüft wurde
|
||||
|
||||
Das Archiv wurde inventarisiert; relevante Aufrufketten wurden gelesen und mit Build-, Test- und Laufzeitprüfungen ergänzt. 23 isolierte Proben untersuchten konkrete Fehlerbedingungen und Bestände mit echtem Anwendungscode und gezielt ersetztem I/O.
|
||||
|
||||
| Prüfung | Ergebnis |
|
||||
|---|---|
|
||||
| Python-Compile für Anwendung und Migrationen | Erfolgreich |
|
||||
| Backend-Testcollection | 2.362 Tests gefunden |
|
||||
| test_phase_f_agents.py | 42 Testkörper bestanden; 42 Teardown-Fehler wegen fehlendem Redis |
|
||||
| Ruff | 7 Meldungen |
|
||||
| TypeScript und Frontend-Produktionsbuild | Erfolgreich |
|
||||
| Vollständiger Vitest-Versuch | Kein abgeschlossener Ergebnisdatensatz |
|
||||
| BlockRenderer.test.tsx gezielt | 76 Tests: 68 bestanden, 8 fehlgeschlagen |
|
||||
| Generierte Plugin-Komponentenmap | Aktuell, 43 Komponenten |
|
||||
| Cross-Plugin-Checker | 497 Dateien geprüft, 0 gemeldete Verstöße |
|
||||
| Alembic-Head | Genau ein Head: 0144 |
|
||||
| Alembic-Metadatensortierung | Fehler reproduziert |
|
||||
| Geladener ORM-Bestand | 129 Tabellen, 120 mit tenant_id, 231 FK-Constraints |
|
||||
|
||||
Die acht Fehler der gezielten UI-Testdatei betreffen sieben fehlende QueryClient-Fixtures und eine überholte Erwartung an bereinigtes HTML. Daraus folgt keine nachgewiesene XSS-Lücke.
|
||||
|
||||
Prüfgrenze: Kein vollständiger PostgreSQL-/Redis-Integrationstest, kein separater produktiver Worker, kein vollständiger Browser-E2E-Lauf und kein tatsächlicher Restore. Die Produktionsinstanz wurde nicht verändert. Der Projektcode blieb unverändert.
|
||||
|
||||
## Findings
|
||||
|
||||
### F01 · P0 — KI führt nicht freigegebene Werkzeuge aus
|
||||
|
||||
Die ReAct-Schleife übernimmt Werkzeugnamen aus der Modellantwort und sucht diese im gesamten ToolRegistry. _execute_tool prüft weder die tatsächlich angebotene Werkzeugliste noch required_permission oder aktuelle Benutzerrechte.
|
||||
|
||||
Reproduziert: Angeboten wurde nur audit_allowed. Das Modell lieferte audit_restricted, registriert mit system:admin. Dessen Handler wurde trotzdem ausgeführt.
|
||||
|
||||
Die konkrete Schadenswirkung hängt davon ab, ob der jeweilige Handler zusätzliche eigene Rechteprüfungen besitzt.
|
||||
|
||||
Korrektur: Unmittelbar vor jedem Handleraufruf Werkzeugfreigabe, Argumente, Benutzerrechte, Verbote, Mandant, Plugin-Aktivierung und gegebenenfalls Genehmigung prüfen.
|
||||
|
||||
Abnahme: Ein registriertes, aber nicht freigegebenes Werkzeug wird abgewiesen; Handleraufrufe bleiben null. Rechteentzug während eines Agentenlaufs wirkt auf die nächste Aktion.
|
||||
|
||||
Fundstelle: app/ai/agent_loop.py, Zeilen 116 und 344.
|
||||
|
||||
### F02 · P0 — Mandantenverwaltung kann globale Anmeldeidentitäten verändern
|
||||
|
||||
Mit users:write kann die globale User.email eines Mitglieds des aktuellen Mandanten verändert werden.
|
||||
|
||||
User ist jedoch mandantenübergreifend. Derselbe Datensatz trägt Passwort und Systemadmin-Flag. Der Passwort-Reset verwendet die veränderbare E-Mail-Adresse.
|
||||
|
||||
Ein Benutzerverwalter aus Mandant A kann dadurch bei einem gemeinsamen Benutzer dessen globale Reset-Adresse verändern. Der vollständige Übernahmeablauf wurde nicht gegen ein reales Konto ausgeführt. Die Änderung des globalen Feldes wurde isoliert reproduziert.
|
||||
|
||||
Korrektur: Globale Identitätsänderungen von der Verwaltung einer Mandantenmitgliedschaft trennen. E-Mail-Wechsel nur über verifizierten Selbstservice oder ausdrücklich berechtigte globale Administration.
|
||||
|
||||
Abnahme: Ein Tenant-Verwalter kann weder die globale E-Mail-Adresse noch den globalen Aktivstatus eines gemeinsamen Benutzers verändern.
|
||||
|
||||
Fundstellen: app/routes/users.py, Zeile 176; app/services/user_service.py, Zeile 193; app/services/auth_service.py, Zeile 218.
|
||||
|
||||
### F03 · P1 — Sitzungswiderruf ist unvollständig
|
||||
|
||||
Mehrere Fälle sind inkonsistent:
|
||||
|
||||
- Der Cookie-Pfad prüft is_active aus der Redis-Session. Eine Deaktivierung invalidiert aber hauptsächlich den Berechtigungscache.
|
||||
- Eine fehlende UserTenant-Mitgliedschaft wird nicht zwingend abgewiesen.
|
||||
- Die Benutzerlöschung invalidiert weder Sessions noch Berechtigungscache zuverlässig.
|
||||
- Passwortwechsel löschen Redis-Sessions, lassen PostgreSQL-Fallback-Sessions bestehen.
|
||||
|
||||
Bei einem späteren Redis-Ausfall kann deshalb eine noch nicht abgelaufene alte DB-Session wieder akzeptiert werden. Bei gesundem Redis wird ein fehlender Session-Schlüssel dagegen korrekt abgewiesen.
|
||||
|
||||
Korrektur: Widerruf dauerhaft speichern und in beiden Sessionpfaden prüfen. Aktive Mitgliedschaft und Kontostatus zwingend durchsetzen.
|
||||
|
||||
Abnahme: Deaktivierung, Austritt und Passwortwechsel wirken unmittelbar – auch bei Redis-Ausfall.
|
||||
|
||||
Fundstellen: app/deps.py, Zeilen 101 und 131; app/core/auth.py, Zeilen 88 und 221; app/routes/users.py, Zeile 319.
|
||||
|
||||
### F04 · P1 — Suche umgeht Teile der Objekt- und Feldberechtigungen
|
||||
|
||||
Hauptsuche, Vorschläge, Facetten und Ähnlichkeitssuche verwenden unterschiedliche Schutzmechanismen.
|
||||
|
||||
autocomplete und find_similar_all_types filtern nach Mandant, berücksichtigen aber keine vollständige Benutzer-/Objektsichtbarkeit. Innerhalb desselben Mandanten reicht das nicht zum Schutz privater Daten.
|
||||
|
||||
Im Hauptpfad wird nur result.data feldweise gefiltert. title und snippet bleiben unverändert. Die KI-Zusammenfassung bekommt die Ergebnisse bereits vor dieser Feldfilterung.
|
||||
|
||||
Korrektur: Modulrechte, Plugin-Aktivierung, Objektsichtbarkeit und Feldfreigaben vor Snippet-Erzeugung und LLM-Übergabe anwenden. Alle Suchvarianten müssen denselben Schutzpfad nutzen.
|
||||
|
||||
Abnahme: Private Dateien oder Mails erscheinen weder in Vorschlägen noch in Ähnlichkeiten, Facetten oder KI-Zusammenfassungen.
|
||||
|
||||
Fundstellen: app/plugins/builtins/unified_search/search_engine.py, Zeilen 186 und 258; routes.py, Zeilen 197 und 209.
|
||||
|
||||
### F05 · P1 — Plugin-Sperre läuft vor der Authentisierung
|
||||
|
||||
require_active_plugin wird als Router-Dependency vor der Authentisierung der jeweiligen Route ausgeführt.
|
||||
|
||||
Sie liest den Mandanten aus dem DB-Kontext. Fehlt dieser, kehrt sie ohne Sperre zurück.
|
||||
|
||||
Reproduziert: Die Plugin-Prüfung lief mit leerem Mandantenkontext; erst danach setzte die Auth-Dependency den Mandanten. Der Endpunkt antwortete mit HTTP 200.
|
||||
|
||||
Korrektur: Die Plugin-Prüfung ausdrücklich vom bereits geprüften Benutzer-/Mandantenkontext abhängig machen. Fehlender Kontext muss zur Ablehnung führen.
|
||||
|
||||
Abnahme: Global aktives, im aktuellen Mandanten deaktiviertes Plugin liefert auch bei gültiger Sitzung und passenden Modulrechten 403.
|
||||
|
||||
Fundstellen: app/deps.py, Zeile 409; app/main.py, Zeile 656.
|
||||
|
||||
### F06 · P1 — Worker registriert keine der 44 deklarierten Plugin-Ereignisbehandlungen
|
||||
|
||||
Der Worker überspringt on_activate und ruft stattdessen register_event_handlers auf.
|
||||
|
||||
Diese Basismethode ist leer. Keines der 27 gefundenen Plugins überschreibt sie. Die eigentliche Registrierung findet in BasePlugin.on_activate statt.
|
||||
|
||||
Reproduziert: 27 Plugins, 44 Ereignisdeklarationen, 0 Registrierungen über den Worker-Aufruf.
|
||||
|
||||
API und Worker sind getrennte Prozesse; ihre Registries werden nicht geteilt. Gesonderte Core- oder Wildcard-Handler ändern diesen Befund nicht.
|
||||
|
||||
Korrektur: Prozessregistrierung und mandantenbezogenes Seeding trennen. API und Worker müssen dieselbe idempotente Registrierung ausführen.
|
||||
|
||||
Abnahme: Kontakt oder Datei anlegen → Outbox → separater Worker → zuständiger Plugin-Handler → persistierter Suchindex.
|
||||
|
||||
Fundstellen: app/core/worker.py, Zeile 150; app/plugins/base.py, Zeilen 51 und 155.
|
||||
|
||||
### F07 · P1 — Hintergrundjobs verlieren Mandantenkontext und Transaktionen
|
||||
|
||||
Mehrere Suchjobs und run_agent öffnen neue Sessions und lesen zunächst einen mandantenbezogenen Datensatz, um dessen Mandanten zu bestimmen. Unter der vorgesehenen RLS fehlt bereits für diese erste Abfrage der erforderliche Kontext.
|
||||
|
||||
Der Workflow-Timeoutjob setzt ebenfalls keinen Kontext und beendet Änderungen teilweise mit flush, ohne commit.
|
||||
|
||||
Zusätzlich verspricht TenantMixin automatische ORM-Filterung, implementiert im untersuchten DB-Modul aber lediglich die Spalten.
|
||||
|
||||
set_config(..., true) gilt nur für die aktuelle Transaktion. Nach Commit oder Rollback muss eine neue Transaktion ihren Kontext erneut erhalten.
|
||||
|
||||
Korrektur: Mandant und Auftraggeber im Job-Payload verpflichtend übergeben; Kontext vor dem ersten fachlichen SQL setzen; Änderungen, Audit und Outbox gemeinsam committen.
|
||||
|
||||
Abnahme: Indexierung, Agentenstart und Timeout-Verarbeitung funktionieren mit crm_worker, erzwungener RLS und zwei Mandanten.
|
||||
|
||||
Fundstellen: app/core/db/__init__.py, Zeilen 52 und 280; unified_search/jobs.py, Zeile 31; automation/agent_runner.py, Zeile 26; automation/workflow_timeout.py, Zeile 18.
|
||||
|
||||
### F08 · P1 — Externe Agenten-API funktioniert für reine Bearer-Clients nicht
|
||||
|
||||
Die externen Agentenrouten verlangten zunächst require_permission, das an die Cookie-Authentisierung gebunden ist.
|
||||
|
||||
Die Bearer-Verifikation im Handler wird deshalb bei einem normalen API-Client ohne Cookie nicht erreicht.
|
||||
|
||||
Reproduziert: Statusabfrage mit ausschließlich Bearer-Header liefert 401.
|
||||
|
||||
Im /run-Pfad steckt ein weiterer Fehler: `async with get_db() as stream_db:` — get_db() ist ein FastAPI-Asyncgenerator und kein solcher Contextmanager. Das erzeugt einen TypeError.
|
||||
|
||||
Korrektur: Cookie und API-Token über einen gemeinsamen geprüften Auth-Kontext auflösen. Für selbst verwaltete Sessions die vorhandene Sessionfactory beziehungsweise den richtigen Contextmanager verwenden.
|
||||
|
||||
Abnahme: Gültiger Bearer ohne Cookie funktioniert für Status, Run und Stream; ungültige Tokens werden abgelehnt.
|
||||
|
||||
Fundstellen: app/plugins/builtins/ai_assistant/external_api.py, Zeilen 44 und 126; app/deps.py, Zeile 299.
|
||||
|
||||
### F09 · P1 — Generische CRM-/MCP-Werkzeuge haben keinen funktionierenden Delegationspfad
|
||||
|
||||
Zwei Implementierungen senden X-Internal-Call, X-Tenant-Id, X-User-Id. Diese Header werden von der geschützten API nicht als gültige Authentisierung akzeptiert.
|
||||
|
||||
Reproduziert: Der generische CRM-Aufruf erhält „Not authenticated“.
|
||||
|
||||
Im separaten Worker zeigt der lokale Standardhost außerdem auf den Worker selbst. Der vorhandene Delegationstoken-Code ist nicht in diese Aufrufe eingebunden.
|
||||
|
||||
Korrektur: Fachservices und vorhandenen Delegationsmechanismus verbinden. Einen gemeinsamen geprüften Ausführungskontext verwenden und die beiden generischen Aufrufimplementierungen konsolidieren.
|
||||
|
||||
Abnahme: Dieselbe Fachaktion ist für denselben Benutzer über UI und Agent gleichermaßen erlaubt oder gesperrt; das Audit nennt Benutzer, Agent und Mandant.
|
||||
|
||||
Fundstellen: ai_assistant/crm_api_tool.py, Zeile 114; mcp_server/tool_definitions.py, Zeile 104; app/core/delegation_token.py.
|
||||
|
||||
### F10 · P1 — Token-Scopes vergeben Rechte, statt sie einzuschränken
|
||||
|
||||
require_permission akzeptiert einen passenden _token_scopes-Eintrag durch ein frühzeitiges return, auch wenn die aktuellen Benutzerberechtigungen leer sind.
|
||||
|
||||
Isoliert reproduziert: mail:write wird trotz fehlender Benutzerberechtigung erlaubt.
|
||||
|
||||
Der Cookie-Zwang aus F08 verdeckt diesen Fehler auf vielen HTTP-Pfaden. Eine alleinige Reparatur der Bearer-Unterstützung kann ihn erst erreichbar machen.
|
||||
|
||||
Korrektur: Effektive Rechte als Schnittmenge aus Benutzerrechten, Token-Scopes und Delegation berechnen. Explizite Verbote müssen Vorrang haben.
|
||||
|
||||
Abnahme: Token mit mail:write, Benutzer ohne mail:write → 403. Späterer Rechteentzug wirkt auf bestehende Tokens.
|
||||
|
||||
Fundstelle: app/deps.py, Zeile 314.
|
||||
|
||||
### F11 · P1 — Freigaben sind nicht sicher an Entscheider und Aktion gebunden
|
||||
|
||||
resolve_approval_request prüft Mandant und Status, aber nicht ausreichend: Ablaufdatum, vorgesehenen Genehmiger, Genehmigergruppe, konkurrierende Entscheidungen.
|
||||
|
||||
Die ursprüngliche approver_id wird mit dem tatsächlichen Entscheider überschrieben.
|
||||
|
||||
Reproduziert: Eine abgelaufene Anfrage konnte von einem anderen Entscheider genehmigt werden.
|
||||
|
||||
Für Agenten fehlt außerdem eine durchgängige, dauerhaft gespeicherte Fortsetzung genau des genehmigten Werkzeugaufrufs.
|
||||
|
||||
Korrektur: Zuordnung und Entscheider getrennt speichern; Ablauf und Berechtigung prüfen; Zustand atomar ändern. Genehmigung an Aktion, Argumente, Revision und Kontext binden.
|
||||
|
||||
Abnahme: Falscher Entscheider, abgelaufene Anfrage und doppelte Entscheidung werden abgewiesen. Genehmigung führt exakt die geprüfte Aktion einmal aus.
|
||||
|
||||
Fundstellen: app/core/approval.py, Zeile 106; app/ai/agent_loop.py, Zeile 384; app/routes/approvals.py, Zeile 236.
|
||||
|
||||
### F12 · P1 — Workflow-Genehmigung und -Ablehnung scheitern an falschen Aufrufen
|
||||
|
||||
Die Workflow-Routen behandeln die Rückgabe von create_approval_request als Dictionary: approval["id"]. Tatsächlich wird ein ORM-Objekt zurückgegeben.
|
||||
|
||||
Reproduziert: TypeError: 'ApprovalRequest' object is not subscriptable.
|
||||
|
||||
Danach wäre auch der Aufruf von resolve_approval_request falsch: tenant_id fehlt, und decided_by wird statt approver_id verwendet.
|
||||
|
||||
Korrektur: Beide Routen an den tatsächlichen zentralen Vertrag anpassen und die bestehende wartende Freigabe auflösen. Nicht bei jeder Entscheidung eine neue, sofort selbst genehmigte Anfrage erzeugen.
|
||||
|
||||
Abnahme: Beide tatsächlichen URLs funktionieren; Zustandswechsel, Audit und Freigabe stimmen. Wiederholung erzeugt keinen zweiten Fortschritt.
|
||||
|
||||
Fundstellen: app/routes/workflows.py, Zeilen 507 und 525; app/core/approval.py, Zeile 106.
|
||||
|
||||
### F13 · P1 — Dauerhafte Workflow-Ausführung ist nur teilweise verbunden
|
||||
|
||||
Die Engine dokumentiert Locks, Wiederaufnahme und Idempotenz. Im Anwendungscode hat acquire_lock jedoch keinen Aufrufer.
|
||||
|
||||
idempotency_key existiert als Modellfeld, wird aber von den Seiteneffekt-Handlern nicht durchgängig verwendet. resume verarbeitet Schritte ohne wirksame Sperre.
|
||||
|
||||
Der manuelle Trigger legt eine Instanz an; eine durchgängige automatische Ausführung und Wiederaufnahme fälliger Wait-Schritte ist nicht vollständig angeschlossen.
|
||||
|
||||
Korrektur: Die vorhandene Engine zum verbindlichen Zustandsübergang machen. Start und Wiederaufnahme über registrierte Jobs verbinden; Instanzen atomar übernehmen und stabile Schritt-/Idempotenzzustände persistieren.
|
||||
|
||||
Abnahme: Ein Ablauf übersteht Worker-Neustart. Gleichzeitige Resume-Aufrufe versenden eine Mail nicht doppelt. Unklarer Ausgang eines externen Aufrufs wird nachvollziehbar abgeglichen.
|
||||
|
||||
Fundstellen: app/workflows/engine.py, Zeilen 526 und 567; app/models/workflow.py, Zeile 72; app/routes/workflows.py, Zeile 338.
|
||||
|
||||
### F14 · P1 — KI-Datenrichtlinie schützt nicht den gesamten Nachrichtenstrom
|
||||
|
||||
enforce_data_policy filtert Dictionary-Inhalte, lässt Strings aber unverändert.
|
||||
|
||||
Reproduziert: Ein JSON-String mit einem synthetischen smtp_password passiert unverändert.
|
||||
|
||||
Der Agentenrunner ruft die Policy nur vor der ReAct-Schleife und mit db=None auf. Dadurch wird keine Providerkonfiguration geladen. Werkzeugantworten entstehen erst später innerhalb der Schleife.
|
||||
|
||||
Korrektur: Strukturierte Daten vor der Serialisierung filtern. Jede LLM-Anfrage einschließlich Toolantworten und RAG-Kontext muss denselben geprüften Pfad passieren. Verpflichtende, nicht ladbare Richtlinien müssen den Versand verhindern.
|
||||
|
||||
Abnahme: Gesperrte Felder fehlen am tatsächlichen Provider-Eingang sowohl im Startkontext als auch nach Werkzeugaufrufen.
|
||||
|
||||
Fundstellen: app/ai/data_policy.py, Zeile 35; automation/agent_runner.py, Zeile 242; app/ai/agent_loop.py, Zeile 346.
|
||||
|
||||
### F15 · P1 — Workflow-HTTP-Schutz lässt interne Hostnamen durch
|
||||
|
||||
_is_url_safe blockiert IP-Literale und einige fest eingetragene Hostnamen, lässt andere Hostnamen ungeprüft passieren.
|
||||
|
||||
Reproduziert: http://postgres:5432/ wird vom Validator akzeptiert.
|
||||
|
||||
Auch ein externer Domainname mit privater DNS-Auflösung kann passieren. Welche internen Dienste tatsächlich erreichbar wären, hängt vom Deployment ab. Die Probe sendete keine Netzwerkanfrage.
|
||||
|
||||
Korrektur: Aufgelöste IPv4-/IPv6-Ziele prüfen, die Verbindung an die geprüfte Auflösung binden und erlaubte Ziele beziehungsweise Ports begrenzen. Weiterleitungen ebenfalls prüfen oder sperren.
|
||||
|
||||
Abnahme: Interne Servicenamen, private DNS-Ziele und DNS-Wechsel werden abgefangen.
|
||||
|
||||
Fundstelle: app/workflows/step_handlers.py, Zeilen 169 und 197.
|
||||
|
||||
### F16 · P1 — Plugin-Lifecycle hat widersprüchliche Aktivierungswege
|
||||
|
||||
Mehrere Mechanismen verwalten denselben Zustand:
|
||||
|
||||
- prestart aktiviert absichtlich deaktivierte Plugins erneut.
|
||||
- main.py führt einen eigenen Aktivierungsablauf aus.
|
||||
- Seeding erfolgt im Kontext des ersten gefundenen Mandanten.
|
||||
- Aktivierungsfehler werden protokolliert, ohne einen bereits aktiven DB-Zustand zuverlässig zurückzunehmen.
|
||||
- Der Worker lädt Jobmodule vor seinem DB-Aktivierungsabgleich.
|
||||
|
||||
Global installiert, mandantenbezogen freigegeben und im Prozess betriebsbereit werden dadurch vermischt.
|
||||
|
||||
Korrektur: Registry-/PluginService-Lifecycle konsolidieren. Gewünschten Zustand, Installation, Mandantenfreigabe und Laufzeitgesundheit getrennt behandeln.
|
||||
|
||||
Abnahme: Absichtliche Deaktivierung bleibt nach Neustart erhalten. Ein Aktivierungsfehler erscheint nicht als gesund. Mandanten erhalten ihre Seeds unabhängig von der Ladefolge.
|
||||
|
||||
Fundstellen: prestart.sh, Zeile 148; app/main.py, Zeilen 309 und 315; app/core/worker.py, Zeile 221.
|
||||
|
||||
### F17 · P1 — Integrationen rufen eine nicht vorhandene Registry-Methode auf
|
||||
|
||||
ContractRegistry besitzt get_contract, aber kein get.
|
||||
|
||||
Mindestens sechs Produktionsstellen verwenden trotzdem get_contract_registry().get(...).
|
||||
|
||||
Betroffen sind unter anderem Agenten-/Workflow-Kommunikation, Miniapp-Werkzeuge, proaktive Hinweise und Berichtjobs. Teilweise wird nur geloggt, sodass erwartete Nachrichten oder Ergebnisse still fehlen.
|
||||
|
||||
Korrektur: Aufrufer auf den tatsächlichen Contract-Zugriff umstellen. Öffentliche Funktionen typisieren und ihre Verträge prüfen.
|
||||
|
||||
Abnahme: Betroffene Funktionen über ihre realen Einstiegspunkte ausführen; Nachrichten, Miniapps und Berichtablage müssen tatsächlich persistiert werden.
|
||||
|
||||
Fundstellen: app/plugins/builtins/contracts.py, Zeile 49; app/ai/miniapp_tools.py, Zeile 26; report_generator/jobs.py, Zeile 80; ai_proactive/services.py, Zeile 78.
|
||||
|
||||
### F18 · P1 — Drei Schema-Verfahren erzeugen Drift
|
||||
|
||||
Alembic, Plugin-SQL und sync_plugin_schema.py verändern dasselbe Datenbankschema.
|
||||
|
||||
Der Startup-Sync verliert bei Indizes wichtige Eigenschaften: UNIQUE, partielle WHERE-Bedingungen, PostgreSQL-Indexmethoden und Optionen.
|
||||
|
||||
Reproduziert: Aus dem partiellen eindeutigen Workspace-Default-Index erzeugt der Sync lediglich einen normalen Index auf tenant_id.
|
||||
|
||||
Abgefangene DDL-Fehler können außerdem die gemeinsame Transaktion unbrauchbar machen, während der Ablauf weiterläuft.
|
||||
|
||||
Korrektur: Pro Tabelle und Änderung einen verbindlichen Migrationsbesitzer bestimmen. Startup-Sync zunächst als lesenden Driftbericht verwenden; Reparaturen ausdrücklich migrieren.
|
||||
|
||||
Abnahme: Frischinstallation und Upgrade ergeben dieselben Spalten, Constraints, Indizes und Policies.
|
||||
|
||||
Fundstellen: prestart.sh, Zeile 135; scripts/sync_plugin_schema.py, Zeilen 177 und 188.
|
||||
|
||||
### F19 · P1 — Alembic kennt nicht alle benötigten Modelle
|
||||
|
||||
alembic/env.py importiert nur app.models.
|
||||
|
||||
In einem frischen Prozess umfasst das 46 Tabellen. Die Metadatensortierung scheitert bereits an einer Referenz von contact_merge_history auf contacts.
|
||||
|
||||
Reproduziert: NoReferencedTableError.
|
||||
|
||||
Nach Laden der Pluginmodelle sind 129 Tabellen bekannt. Nur den in der Roadmap genannten einzelnen Attachment-FK zu reparieren reicht daher nicht.
|
||||
|
||||
Korrektur: Deterministische, nebenwirkungsfreie Modelldiscovery für Alembic und Schema-Prüfungen einführen.
|
||||
|
||||
Abnahme: Metadatensortierung funktioniert im frischen Prozess; alembic check läuft nach Frischinstallation und Upgrade gegen PostgreSQL durch.
|
||||
|
||||
Fundstellen: alembic/env.py, Zeile 15; app/models/contact_merge.py, Zeile 28.
|
||||
|
||||
### F20 · P1 — Startup überschreibt beabsichtigte Datenbankrechte
|
||||
|
||||
Migration 0100 entzieht API und Worker Löschrechte für besonders geschützte Tabellen, darunter audit_log.
|
||||
|
||||
Danach erteilt prestart erneut: GRANT DELETE ON ALL TABLES IN SCHEMA public ...
|
||||
|
||||
Damit wird der beabsichtigte Löschschutz wieder aufgehoben.
|
||||
|
||||
Zusätzlich setzt der Start mehrere DB-Rollen auf dasselbe aus der privilegierten Verbindung entnommene Passwort. API und Worker erhalten die Migrationsverbindung im Environment.
|
||||
|
||||
Korrektur: Tabellenspezifische Rechte ausschließlich nachvollziehbar migrieren. Keine pauschale Rechteanhebung beim Start. Separate Zugangsdaten und ein abgegrenzter Migrationsschritt.
|
||||
|
||||
Abnahme: API und Worker können Auditdaten schreiben, aber nicht löschen. Laufzeitcontainer benötigen keinen frei verwendbaren Migrationszugang.
|
||||
|
||||
Fundstellen: prestart.sh, Zeilen 58 und 90; alembic/versions/0100_restrict_delete_grants.py, Zeile 39; docker-compose.yaml, Zeile 78.
|
||||
|
||||
### F21 · P1 — Migrationstest kann die falsche Datenbank verändern
|
||||
|
||||
test_migrations.sh überschreibt nur DATABASE_URL. Alembic bevorzugt jedoch MIGRATION_DATABASE_URL.
|
||||
|
||||
Wenn diese Variable auf eine echte Instanz zeigt und die vorherigen Skriptschritte erfolgreich laufen, können Upgrade und Downgrade dort statt in der Testdatenbank ausgeführt werden.
|
||||
|
||||
Zusätzlich wird eine SQLAlchemy-URL mit postgresql+psycopg2:// an psql übergeben. Das passt nicht zum erwarteten Verbindungsformat.
|
||||
|
||||
Das Skript wurde wegen dieses Befunds nicht ausgeführt.
|
||||
|
||||
Korrektur: Beide Alembic-Variablen auf das explizite temporäre Ziel setzen. Vor jeder DDL-Aktion Zielidentität prüfen und Aufräumen per trap sicherstellen.
|
||||
|
||||
Abnahme: Selbst bei anders gesetzter MIGRATION_DATABASE_URL verändert der Test ausschließlich die erzeugte Testdatenbank.
|
||||
|
||||
Fundstellen: scripts/test_migrations.sh, Zeilen 24 und 70; alembic/env.py, Zeile 27.
|
||||
|
||||
### F22 · P1 — Backup ist im ausgelieferten Container nicht vollständig betriebsfähig
|
||||
|
||||
Mehrere Probleme greifen zusammen:
|
||||
|
||||
- Das Runtime-Image installiert libpq5, aber nicht die benötigten Programme pg_dump und pg_restore.
|
||||
- API-Backups landen in /data/backups, CLI-Backups standardmäßig in /backups.
|
||||
- Compose persistiert diese Backupverzeichnisse nicht.
|
||||
- Backup-Konfiguration wird ohne erforderlichen Mandantenkontext gelesen.
|
||||
- Fehlerbenachrichtigung verwendet User.tenant_id, obwohl User global ist.
|
||||
- Die vorgesehene eingeschränkte Laufzeitrolle kann keinen vollständigen RLS-Dump garantieren.
|
||||
|
||||
Korrektur: Einen dokumentierten Backupablauf mit passenden Programmen, gesonderten Rechten, dauerhaftem Ziel, Ergebnisregistrierung und gemeinsamer DB-/Dateisicherung herstellen.
|
||||
|
||||
Abnahme: Sicherung im gebauten Image erstellen, Container ersetzen und anschließend Datenbank und Dateien auf isoliertem Ziel wiederherstellen.
|
||||
|
||||
Fundstellen: Dockerfile, Zeile 56; app/services/backup_service.py, Zeile 19; app/core/backup_job.py, Zeilen 26 und 208.
|
||||
|
||||
### F23 · P1 — Mandanten-Backup-API führt einen datenbankweiten Restore aus
|
||||
|
||||
Die Restore-Route ist über automation:admin eines Mandanten erreichbar. Sie wählt einen Backupdatensatz dieses Mandanten aus.
|
||||
|
||||
Der eigentliche Restore bearbeitet jedoch die gemeinsame Datenbank ohne Mandantenfilter.
|
||||
|
||||
Die Fehler aus F22 verhindern derzeit viele Aufrufe. Das ist aber keine Sicherheitsgrenze: Eine bloße Reparatur der Programme und Rechte kann den gefährlichen Wirkungsbereich erst freilegen.
|
||||
|
||||
Der Kommentar, pg_restore --clean sei atomar, ist ebenfalls falsch. --clean löscht zu restaurierende Objekte; Atomarität benötigt ausdrücklich passende Transaktionsoptionen.
|
||||
|
||||
Korrektur: Gesamtrestore als globale Betriebsoperation mit separater Berechtigung, Wartungszustand und isoliertem Ziel behandeln.
|
||||
|
||||
Abnahme: Ein Tenant-Admin kann keinen Gesamtrestore auslösen.
|
||||
|
||||
Fundstellen: app/routes/backups.py, Zeile 58; app/services/backup_service.py, Zeilen 175 und 224.
|
||||
|
||||
### F24 · P1 — Healthchecks melden Ausfälle als Erfolg
|
||||
|
||||
Reproduziert:
|
||||
|
||||
- /health/ready liefert bei not_ready weiterhin HTTP 200.
|
||||
- check_worker meldet up, wenn Redis erreichbar und die Queue leer ist – selbst ohne Worker.
|
||||
|
||||
Ein curl -f erkennt den ersten Fehler nicht. Der Compose-Workercheck prüft ebenfalls Redis statt die tatsächliche Verarbeitung.
|
||||
|
||||
Korrektur: Fehlerzustände mit passenden HTTP-Codes zurückgeben. Worker über zeitlich begrenzten Heartbeat und Verarbeitungsnachweis prüfen.
|
||||
|
||||
Abnahme: DB aus → Readiness 503. Worker aus bei gesundem Redis → Worker down. Festhängende Verarbeitung wird innerhalb einer definierten Zeit erkannt.
|
||||
|
||||
Fundstellen: app/routes/health.py, Zeile 39; app/core/monitoring.py, Zeile 219; docker-compose.yaml.
|
||||
|
||||
### F25 · P1 — CSV-Import meldet zurückgerollte Zeilen als erfolgreich
|
||||
|
||||
Mehrere Zeilen werden in derselben Transaktion geschrieben. Scheitert eine spätere Zeile, ruft der Import db.rollback() auf.
|
||||
|
||||
Dabei verschwinden auch frühere unbestätigte Zeilen. Die Zähler succeeded und created behalten diese aber bei.
|
||||
|
||||
Mit Transaktionsmodell reproduziert: partial_success mit einer erfolgreichen Zeile, obwohl anschließend keine Zeile bestätigt wird.
|
||||
|
||||
Der Rollback entfernt zudem den lokalen RLS-Kontext. Fehlerzeilen werden nach Vorvalidierung teilweise neu nummeriert.
|
||||
|
||||
Korrektur: Atomaren Import oder Teilimport ausdrücklich festlegen. Für Teilimport Savepoints pro Zeile verwenden und Originalzeilennummern erhalten.
|
||||
|
||||
Abnahme: Bei einem Fehler in der mittleren von drei Zeilen stimmen gespeicherte Daten, Erfolgszähler und Fehlernummern überein.
|
||||
|
||||
Fundstelle: app/services/import_export_service.py, Zeilen 131 und 138.
|
||||
|
||||
### F26 · P1 — DMS-Deduplizierung vermischt Dateiinhalt und Dateiidentität
|
||||
|
||||
Beim Upload wird nach gleichem Inhalt im ganzen Mandanten gesucht. Bei Treffer wird der bestehende DmsFile-Datensatz zurückgegeben.
|
||||
|
||||
Dabei werden Besitzer, Ordner, Dateiname und Sichtbarkeit nicht passend zum neuen Upload behandelt.
|
||||
|
||||
Reproduziert: Der Upload eines zweiten Benutzers erhält ID, Name und uploaded_by des vorhandenen fremden Datensatzes; es entsteht kein eigener Dateieintrag.
|
||||
|
||||
Korrektur: Binären Inhalt und fachliches Dateiobjekt trennen. Gleiche Bytes dürfen intern wiederverwendet werden; jeder fachliche Upload braucht aber seine eigene Identität und Rechtezuordnung.
|
||||
|
||||
Abnahme: Zwei Benutzer können dieselbe Vorlage in unterschiedliche Ordner hochladen, ohne fremde Metadaten zu erhalten. Löschen einer Datei beschädigt die andere nicht.
|
||||
|
||||
Fundstelle: app/plugins/builtins/dms/routes.py, Zeilen 146 und 159.
|
||||
|
||||
### F27 · P1 — Kalenderfilter entfernt Serientermine vor ihrer Berechnung
|
||||
|
||||
list_entries filtert zunächst CalendarEntry.start_at >= start_dt.
|
||||
|
||||
Eine wöchentliche Serie, die im Januar beginnt, wird dadurch für eine Februaransicht bereits aus der SQL-Ergebnismenge entfernt. Ihre Februar-Wiederholungen werden anschließend gar nicht berechnet.
|
||||
|
||||
Bei erzeugten Wiederholungen wird außerdem start_at verändert, während end_at auf dem Ausgangstermin bleiben kann. Auch mehrtägige Termine mit Beginn vor dem Sichtfenster fehlen.
|
||||
|
||||
Korrektur: Einzeltermine nach Überschneidung und Serien nach relevanter Laufzeit auswählen. Danach Wiederholungen im Sichtfenster erzeugen und Endzeit um dieselbe Dauer verschieben.
|
||||
|
||||
Abnahme: Januarserie erscheint im Februar; Dauer, mehrtägige Überschneidung und Sommerzeitwechsel stimmen. Ungültige Datumsparameter liefern einen Validierungsfehler.
|
||||
|
||||
Fundstelle: app/plugins/builtins/calendar/routes.py, Zeilen 369, 407 und 433.
|
||||
|
||||
### F28 · P1 — Import/Export besitzt einen abweichenden Berechtigungspfad
|
||||
|
||||
Die generischen Routen prüfen import_export:read/write.
|
||||
|
||||
Der Kontaktvertrag schreibt importierte Kontakte direkt als Modelle. Eine zusätzliche vollständige Prüfung von contacts:write und Feldschreibrechten ist dort nicht enthalten.
|
||||
|
||||
Beim Export werden Objektsichtbarkeit und eine feste Sensitivitätsliste berücksichtigt, aber individuelle Feldberechtigungen nicht vollständig übergeben.
|
||||
|
||||
Korrektur: Import-/Exportrechte als zusätzliche Rechte zu den Fach- und Feldrechten behandeln. Vorhandene Fachservices nutzen und Feldfilter vor Dateierzeugung anwenden.
|
||||
|
||||
Abnahme: Importrecht allein erlaubt keine Kontaktanlage. Ein verborgenes Feld fehlt gleichermaßen in API, CSV, XLSX und JSON.
|
||||
|
||||
Fundstellen: app/routes/import_export.py, Zeile 294; app/plugins/builtins/contacts/contracts.py, Zeilen 194 und 257.
|
||||
|
||||
### F29 · P1 — CI liefert keinen reproduzierbaren vollständigen Qualitätsnachweis
|
||||
|
||||
Der gezeigte Forgejo-Workflow:
|
||||
|
||||
- installiert requirements.txt, aber nicht ausdrücklich die Testabhängigkeiten,
|
||||
- definiert keine vollständige PostgreSQL-/Redis-Testumgebung,
|
||||
- verwendet Versionsbereiche, während das Containerimage den Lockfile verwendet.
|
||||
|
||||
Der Docker-Frontendbuild ruft Vite direkt auf und überspringt die im normalen npm-Build enthaltene TypeScript-Prüfung.
|
||||
|
||||
Ein optionaler Smokecheck fragt eine bestehende externe Instanz statt das gerade gebaute Image ab.
|
||||
|
||||
Korrektur: Eine reproduzierbare Pipeline mit identischen Locks, Testdiensten, echten DB-Rollen und Prüfungen des erzeugten Images herstellen.
|
||||
|
||||
Abnahme: Ein sauberer Runner besteht Backend-/Frontend-Suite, Typcheck, Migrationen und E2E gegen sein eigenes Artefakt.
|
||||
|
||||
Fundstellen: .forgejo/workflows/ci.yml, Zeile 21; Dockerfile, Zeile 19; scripts/ci_pipeline.sh, Zeile 113.
|
||||
|
||||
### F30 · P1 — Frischinstallation besitzt ein bekanntes Administrator-Standardpasswort
|
||||
|
||||
Compose und seed_admin.py enthalten einen festen Passwort-Fallback für das initiale Administratorkonto.
|
||||
|
||||
Wenn die Konfiguration fehlt, wird ein Konto mit bekanntem Zugang angelegt.
|
||||
|
||||
Der Befund betrifft den Installationspfad. Er beweist nicht, dass auf dem laufenden Server noch dieses Passwort gilt.
|
||||
|
||||
Korrektur: Initiales Geheimnis verpflichtend machen oder sicher einmalig erzeugen. Unkonfigurierter Produktivstart muss vor Benutzeranlage abbrechen.
|
||||
|
||||
Abnahme: Ein frisches Volume ohne gesetztes Geheimnis erzeugt kein nutzbares Konto mit festem Standardpasswort.
|
||||
|
||||
Fundstellen: docker-compose.yaml, Zeile 95; scripts/seed_admin.py, Zeile 79.
|
||||
|
||||
### F31 · P2 — Provider-Discovery und Reindexierung passen nicht zusammen
|
||||
|
||||
auto_register_providers erhält eine DB-Session, verwendet sie aber nicht zum Aktivierungsabgleich. Stattdessen wird eine feste Providerliste registriert.
|
||||
|
||||
Die Discovery registriert 13 effektive Suchtypen. Reindex- und Ähnlichkeitspfade verwenden daneben eine feste Liste aus vier Tabellen.
|
||||
|
||||
Ein neuer Provider bedeutet deshalb nicht automatisch vollständige Indexierung, Reindexierung und Ähnlichkeitssuche.
|
||||
|
||||
Korrektur: Beiträge aktiver Plugins zur gemeinsamen Quelle für Such- und Indexfähigkeiten machen. Jeder Provider beschreibt seine tatsächlich implementierten Fähigkeiten und Indexjobs.
|
||||
|
||||
Abnahme: Ein neuer Testprovider wird allein über seinen Pluginbeitrag gefunden und vollständig indiziert. Deaktivierung wirkt in API und Worker.
|
||||
|
||||
Fundstellen: unified_search/provider_registry.py, Zeile 118; search_engine.py, Zeile 19; jobs.py, Zeile 245.
|
||||
|
||||
### F32 · P2 — Suchscopes, Filter und Pagination liefern falsche Ergebnisse
|
||||
|
||||
Vier konkrete Probleme:
|
||||
|
||||
- entity_types=[] wird zu „alle Typen“.
|
||||
- Datum-/Tagfilter laufen erst nach der Begrenzung auf die besten Treffer.
|
||||
- Der Request-Offset wird nicht wirksam in die Ergebnisabfrage umgesetzt.
|
||||
- search.before_search läuft erst nach Erzeugung von Suchausdruck und Embedding.
|
||||
|
||||
Reproduziert: Trotz leerer Typenmenge wird ein Provider aufgerufen.
|
||||
|
||||
Korrektur: None und leere Menge unterscheiden. Fachfilter vor Limit und Offset anwenden; Hook vor Ableitung der Suchparameter ausführen.
|
||||
|
||||
Abnahme: Leere Workspace-Schnittmenge ergibt null Treffer. Seite zwei enthält die richtigen Folgetreffer; Filter finden auch Datensätze außerhalb der ursprünglichen Top-N.
|
||||
|
||||
Fundstellen: unified_search/search_engine.py, Zeilen 94 und 116; routes.py, Zeile 182.
|
||||
|
||||
### F33 · P2 — Workspace-Wechsel aktualisiert den Datenkontext nicht zuverlässig
|
||||
|
||||
Der Wechsel verändert Store und globalen HTTP-Header. Fachliche Querykeys wie für Aufgaben enthalten aber keinen Workspace und werden dabei nicht passend invalidiert.
|
||||
|
||||
Dadurch können Daten aus dem vorherigen Arbeitsbereich sichtbar bleiben.
|
||||
|
||||
Beim Wiederherstellen aus sessionStorage wird der API-Header nicht entsprechend synchronisiert; auch Reset behandelt beide Zustände nicht vollständig gemeinsam.
|
||||
|
||||
Korrektur: Workspace in die Queryidentität aufnehmen oder betroffene Queries kontrolliert verwerfen. Wiederherstellung und Reset mit dem API-Kontext synchronisieren.
|
||||
|
||||
Abnahme: Wechsel und Neuladen zwischen zwei unterschiedlich gefilterten Workspaces zeigen sofort passende Daten.
|
||||
|
||||
Einordnung: Das ist ein Arbeitskontextfehler; Workspaces sollen weiterhin keine Rechte vergeben.
|
||||
|
||||
Fundstellen: frontend/src/hooks/useWorkspace.ts, Zeile 56; frontend/src/store/workspaceStore.ts; frontend/src/api/tasks.ts, Zeile 118.
|
||||
|
||||
### F34 · P2 — Mandantenwechsel lässt alten Clientzustand bestehen
|
||||
|
||||
useSwitchTenant setzt den Mandanten und invalidiert Queries. Alte Daten, laufende Anfragen sowie Workspace-/Pluginzustand werden aber nicht vollständig entfernt beziehungsweise zurückgesetzt.
|
||||
|
||||
Viele fachliche Querykeys enthalten keinen Mandanten.
|
||||
|
||||
Dadurch können alte Mandantendaten bis zum Refetch sichtbar bleiben oder verspätete Antworten den neuen Zustand überlagern.
|
||||
|
||||
Korrektur: Mandantenwechsel als kontrollierten Kontextwechsel ausführen: Anfragen abbrechen, betroffene Caches und Stores leeren, Auth-/CSRF-Kontext aktualisieren und neue Plugin-/Workspacezustände laden.
|
||||
|
||||
Abnahme: Eine langsame Anfrage aus A liefert nach Wechsel zu B keine sichtbaren A-Daten.
|
||||
|
||||
Einordnung: Eine serverseitige Umgehung der Mandantentrennung wurde damit nicht nachgewiesen.
|
||||
|
||||
Fundstellen: frontend/src/api/auth.ts, Zeile 100; frontend/src/api/tasks.ts, Zeile 118; frontend/src/api/client.ts, Zeile 34.
|
||||
|
||||
### F35 · P2 — PWA-Anforderung wurde durch Abschaltung ersetzt
|
||||
|
||||
main.tsx deregistriert die Service Worker. Die Vite-Konfiguration aktiviert die PWA nicht.
|
||||
|
||||
Gleichzeitig verspricht der Offlinebanner: „Änderungen werden gespeichert, wenn die Verbindung wiederhergestellt ist.“
|
||||
|
||||
Ein entsprechender Offline-Schreibpfad ist dort nicht vorhanden.
|
||||
|
||||
Korrektur: Die vereinbarte PWA für Installation und kontrolliertes Caching statischer Assets wiederherstellen. API-/ERP-Daten und Schreiboperationen nicht pauschal offline cachen. Den Banner an die tatsächliche Funktion anpassen.
|
||||
|
||||
Abnahme: Installierbarkeit und Assetupdates funktionieren; Offline-Schreibaktionen werden nicht fälschlich als später gespeichert dargestellt.
|
||||
|
||||
Fundstellen: frontend/src/main.tsx, Zeile 7; frontend/src/App.tsx, Zeile 49; frontend/vite.config.ts.
|
||||
|
||||
### F36 · P2 — Generierte Plugin-Komponentenmap ist nicht verpflichtend geprüft
|
||||
|
||||
Die neue Map ist vorhanden und aktuell: 43 Komponenten, Check grün.
|
||||
|
||||
Der Generator beziehungsweise Aktualitätscheck wird aber nicht verbindlich von npm-Build, Dockerfile und der gezeigten CI aufgerufen.
|
||||
|
||||
Ein künftiger Manifestbeitrag kann deshalb fehlen, während die alte Map erfolgreich gebaut wird.
|
||||
|
||||
Korrektur: Generatorcheck an den normalen Build-/CI-Pfad hängen.
|
||||
|
||||
Abnahme: Ein neuer Manifestpfad ohne aktualisierte Map macht CI rot. Nach Generierung ist die Seite im Produktionsbundle enthalten.
|
||||
|
||||
Fundstellen: scripts/generate_component_map.py, Zeile 71; frontend/package.json, Zeile 8; Dockerfile, Zeile 19.
|
||||
|
||||
### F37 · P1 — SMTP-Variablen heißen in Compose anders als in Settings
|
||||
|
||||
Settings erwartet: SMTP_USERNAME, SMTP_FROM_EMAIL, SMTP_USE_TLS.
|
||||
|
||||
Compose setzt dagegen: SMTP_USER, SMTP_FROM, SMTP_TLS.
|
||||
|
||||
Reproduziert: Trotz gesetzter Compose-Namen bleibt der Benutzername leer; Standardabsender und Standard-TLS-Wert bleiben bestehen.
|
||||
|
||||
Betroffen sind insbesondere Systemmails wie Reset, Einladungen und geplante Alarmierung.
|
||||
|
||||
Korrektur: Environmentnamen zwischen Settings, Compose und Dokumentation angleichen.
|
||||
|
||||
Abnahme: Aus dem tatsächlichen Compose-Environment entstehen die erwarteten Settings; Reset- und Alarmmail funktionieren mit korrekter Anmeldung und Absender.
|
||||
|
||||
Fundstellen: app/config.py, Zeile 57; docker-compose.yaml, Zeile 89.
|
||||
|
||||
### F38 · P2 — Manifestfehler können einen endlosen Spinner erzeugen
|
||||
|
||||
pluginStore.setError setzt loading=false, lässt aber loaded=false.
|
||||
|
||||
PluginRouteRenderer betrachtet nur loaded und zeigt weiterhin den Spinner. Der Fehlerzustand wird dort nicht ausgewertet.
|
||||
|
||||
Korrektur: Laden, erfolgreich leeres Ergebnis und Fehler getrennt darstellen. Fehleranzeige und erneuten Versuch anbieten.
|
||||
|
||||
Abnahme: Manifest-API liefert 500 → verständliche Fehlermeldung statt Dauerspinner. Ein erneuter Versuch kann die Oberfläche wiederherstellen.
|
||||
|
||||
Fundstellen: frontend/src/store/pluginStore.ts, Zeile 118; PluginRouteRenderer.tsx, Zeile 74.
|
||||
|
||||
### F39 · P2 — DMS-Office-Editiersitzung verweist auf unpassende Endpunkte
|
||||
|
||||
create_edit_session akzeptiert Office-Dateien, gibt aber /preview als Dokument-URL zurück. Diese Route akzeptiert ausschließlich PDFs.
|
||||
|
||||
Die erzeugte Callback-URL /files/{id}/callback besitzt im DMS-Router keine Implementierung.
|
||||
|
||||
Damit ist die zurückgegebene Konfiguration noch kein funktionsfähiger Office-Bearbeitungsablauf.
|
||||
|
||||
Korrektur: Geeignete Download-/Speicher-Routen und einen begrenzten, geprüften Editorzugang anschließen. Bis dahin die Funktion nicht als betriebsbereit darstellen.
|
||||
|
||||
Abnahme: DOCX öffnen, ändern, speichern und erneut laden ergibt den aktualisierten Inhalt; fremde oder abgelaufene Editiersitzungen können nicht schreiben.
|
||||
|
||||
Fundstellen: app/plugins/builtins/dms/sharing_routes.py, Zeile 36; routes.py, Zeile 592.
|
||||
|
||||
### F40 · P2 — Plugin-Migrationen werden nur anhand des Dateinamens erkannt
|
||||
|
||||
Der Runner überspringt angewandte Dateien anhand von migration_file.
|
||||
|
||||
Wird das SQL nachträglich verändert, bleibt dies unbemerkt.
|
||||
|
||||
Die RLS-Einrichtung betrifft außerdem vor allem neu angelegte Tabellen. Der vollständige Sollzustand bereits vorhandener Plugin-Tabellen wird damit nicht garantiert.
|
||||
|
||||
Korrektur: Angewandte Migrationen unveränderlich machen und Hashes speichern. Zusätzlich tatsächliche Spalten, FKs, Indizes, Policies und Grants vergleichen.
|
||||
|
||||
Abnahme: Sowohl eine veränderte angewandte Migration als auch eine bestehende Tabelle mit falscher Policy werden erkannt.
|
||||
|
||||
Fundstelle: app/plugins/migration_runner.py, Zeilen 95 und 153.
|
||||
|
||||
### F41 · P2 — Stündliches Agentenlimit verwendet das falsche Zeitfenster
|
||||
|
||||
Der Code setzt: one_hour_ago = datetime.now(UTC).
|
||||
|
||||
Die Stunde wird nicht abgezogen.
|
||||
|
||||
Die anschließende Abfrage zählt deshalb nicht die Läufe der vergangenen Stunde, sondern nur Einträge ab dem aktuellen Zeitpunkt. Das konfigurierte Stundenlimit schützt so nicht zuverlässig vor wiederholten Starts.
|
||||
|
||||
F07 kann den Job bereits früher blockieren. Nach dessen Reparatur bleibt dieser weitere Fehler bestehen.
|
||||
|
||||
Korrektur: Zeitfenster korrekt berechnen und Kontingente atomar reservieren, damit parallele Starts die Begrenzung nicht umgehen.
|
||||
|
||||
Abnahme: Ausschöpfung des Stundenlimits blockiert weitere Starts; nach Ablauf des Fensters sind sie wieder erlaubt.
|
||||
|
||||
Fundstelle: app/plugins/builtins/automation/agent_runner.py, Zeile 68.
|
||||
|
||||
## Architektur- und Strukturbewertung
|
||||
|
||||
Die Zahl der Dateien oder ihre Länge ist nicht das Hauptproblem. Große Mail-/Kalender-Routen, PluginRegistry und Agenten-/Workflowmodule sind zwar Wartungsschwerpunkte. Ihr bloßes Aufteilen würde die beschriebenen Fehler jedoch nicht beheben.
|
||||
|
||||
Die grundlegenden Strukturprobleme sind:
|
||||
|
||||
1. Mehrere Stellen verwalten denselben Zustand. Pluginaktivität wird in Startup, Registry, API und Worker unterschiedlich interpretiert. Drei Mechanismen verändern das Schema.
|
||||
2. Contracts garantieren zu wenig Verhalten. Exportierte ORM-Modelle, Any und dynamische Funktionszugriffe erlauben, Fachlogik außerhalb ihres Moduls neu zusammenzusetzen.
|
||||
3. API- und Worker-Ausführung sind nicht gleichwertig. Kontext und Registrierungen werden stillschweigend als vorhanden angenommen, obwohl ein neuer Prozess oder eine neue Session beginnt.
|
||||
4. Berechtigungsprüfungen liegen zu weit vom Seiteneffekt entfernt. UI-Sperren und angebotene Toollisten schützen nicht automatisch die ausgeführte Fachoperation.
|
||||
5. Statusanzeigen sind teilweise von der tatsächlichen Funktion entkoppelt. „Aktiv“, „fertig“ oder „gesund“ kann trotz fehlender Registrierung, Persistenz oder Verarbeitung erscheinen.
|
||||
|
||||
Die bestehende Aufteilung sollte verbindlicher werden:
|
||||
|
||||
| Bereich | Verantwortung |
|
||||
|---|---|
|
||||
| Core | Identität, Mandant, Rechte, Transaktionen, Audit, Outbox, Jobs, gemeinsame Registries |
|
||||
| Fachplugin | Fachmodell, Regeln, Services, API, UI und Pluginbeiträge |
|
||||
| Pluginvertrag | Öffentliche, typisierte Fachoperationen und klar beschriebene Fähigkeiten |
|
||||
| Ausführungskontext | Mandant, Benutzer, Herkunft, Delegation, aktuelle Rechte und Nachverfolgbarkeit |
|
||||
| Workspace | Navigation, Ansichten, Defaults und einschränkende Abfragefilter |
|
||||
| Suche | Gemeinsamer Schutzpfad für Hauptsuche, Vorschläge, Facetten, Ähnlichkeit und RAG |
|
||||
| Betrieb | Migrationen, Sicherungen, Wiederherstellung und unabhängige Ausfallüberwachung |
|
||||
|
||||
UI, externe API, Agent und Worker sollten dieselben fachlichen Operationen mit demselben geprüften Kontext aufrufen. Fachänderung, Audit und Outbox gehören in eine gemeinsame Transaktion. Wiederholte Ereignisverarbeitung muss beherrscht werden.
|
||||
|
||||
## Datenmodell
|
||||
|
||||
Die gemessenen 129 Tabellen und 231 FK-Constraints sind der geladene ORM-Bestand. Daraus lässt sich nicht ableiten, welche Constraints auf dem Produktionsserver tatsächlich bestehen.
|
||||
|
||||
Für die Schemaabnahme müssen insbesondere nachgewiesen werden:
|
||||
|
||||
- Mandantenbezogene Referenzen können nicht auf fremde Mandanten zeigen.
|
||||
- Globale Benutzeridentität und Mandantenmitgliedschaft haben getrennte Änderungsrechte.
|
||||
- Softdelete und Eindeutigkeitsregeln passen zusammen.
|
||||
- Default-Objekte sind im vorgesehenen Umfang eindeutig.
|
||||
- Suchindex und fachliche Daten besitzen einen konsistenten Lebenszyklus.
|
||||
|
||||
Ein einfacher Fremdschlüssel auf eine UUID erzwingt nicht automatisch die Mandantengleichheit. Ob sämtliche notwendigen zusammengesetzten Fremdschlüssel bereits wirksam migriert sind, muss an einer vollständig aufgebauten PostgreSQL-Datenbank gemessen werden.
|
||||
|
||||
## Konzept: Business-KI-Plattform
|
||||
|
||||
Das Ziel bleibt sinnvoll: Fachmodule bringen ihre Domänenlogik selbst mit; die Plattform stellt einen verbindlichen technischen und sicherheitsbezogenen Rahmen bereit.
|
||||
|
||||
Was aktuell fehlt, ist vor allem der Nachweis vollständiger Abläufe über diese Grenzen hinweg.
|
||||
|
||||
Die ersten beiden Abnahmen sollten deshalb sein:
|
||||
|
||||
1. Kontaktanlage → Audit/Outbox → Worker → Suche → erlaubte KI-Abfrage.
|
||||
2. Mailentwurf → Freigabe → einmaliger Versand → nachvollziehbares Ergebnis.
|
||||
|
||||
Damit werden Fachservice, Rechte, Transaktionen, Pluginbeiträge, Worker und KI gemeinsam geprüft.
|
||||
|
||||
## Kalender und spätere LeoRental-Integration
|
||||
|
||||
Der Kalendervertrag exportiert derzeit vor allem Modelle und Workspacebeiträge. Ein verbindlicher CalendarPort-/CalendarProjection-Vertrag ist im gelieferten Stand noch nicht zu finden.
|
||||
|
||||
Vor der späteren Rental-Anbindung müssen feststehen:
|
||||
|
||||
- Welches Fachmodul besitzt den Termin?
|
||||
- Welche stabile Referenz verbindet Projekt und Kalendereintrag?
|
||||
- Wie werden Änderungen, Storno und Löschung übertragen?
|
||||
- Welches System ist führend?
|
||||
- Wie werden Zeitzonen und Konflikte behandelt?
|
||||
|
||||
LeoCRM Calendar soll dabei die führende Plattformfähigkeit bleiben. Zuerst muss jedoch der vorhandene Serien-/Zeitraumfehler F27 behoben werden. Das ist eine offene Integrationsaufgabe; ein bereits geliefertes Rental-Modul wurde hier nicht geprüft.
|
||||
|
||||
## Import/Export
|
||||
|
||||
Die bestehende modulare Contract-Anbindung ist die richtige Richtung. Sie benötigt vollständige Autorisierung, Transaktionsregeln und dieselben Fachvalidierungen wie die normale API.
|
||||
|
||||
Die gemeinsame Basis kann Parsing, Mapping, Vorschau und Fehlerberichte übernehmen. Fachliche Regeln und Persistenzverhalten bleiben beim zuständigen Modul.
|
||||
|
||||
## Fehler und Lücken im bestehenden Bauplan
|
||||
|
||||
Die geplante Phase R mit Betriebssicherheit, Teststabilität, Schemaabgleich, E2E und Restore ist richtig priorisiert. Ihr Entwurf muss aber korrigiert werden:
|
||||
|
||||
| Planung | Problem | Erforderliche Änderung |
|
||||
|---|---|---|
|
||||
| ARQ überwacht Worker und Redis | Wenn ARQ oder Redis ausfällt, läuft auch der Wächter nicht mehr | Ergänzende Überwachung außerhalb dieser Ausfallkette |
|
||||
| Alarm bei „installiert, aber inaktiv“ | Absichtliche Deaktivierung ist erlaubt | Gewünschten Zustand mit tatsächlicher Betriebsbereitschaft vergleichen |
|
||||
| Queue-Länge als Workerbeweis | Leere Queue kann auch fehlenden Worker bedeuten | Heartbeat-Alter und tatsächliche Verarbeitung messen |
|
||||
| Komplette Suite grün | Richtiger Ansatz, aber Mock-/Admin-Tests können reale Rechtefehler verdecken | Echte API-/Workerprozesse und tatsächliche Laufzeitrollen prüfen |
|
||||
| Ein FK-Fix plus Migrationshash | Behebt unvollständige Modelldiscovery und konkurrierende Schemawege nicht | Vollständige Metadaten und eindeutige Schema-Verantwortung herstellen |
|
||||
| E2E pro Kernfunktion | Erfolgreicher Normalfall prüft weder Rechteentzug noch Neustart | Mehrmandanten-, Rollen-, Fehler- und Wiederaufnahmeszenarien ergänzen |
|
||||
| Monatlicher Restorejob | Ein Jobname beweist keine sichere Zielwahl oder vollständige Sicherung | Isoliertes Ziel und tatsächliche DB-/Dateiwiederherstellung nachweisen |
|
||||
| „95 % Produktionsreife“ | Die fünf Kriterien ergeben keinen mathematischen Reifegrad | Konkrete Freigabekriterien und verbleibende Risiken dokumentieren |
|
||||
|
||||
Die genannten 9–14 Arbeitstage der Roadmap sind keine belastbare Aufwandsschätzung zur Behebung aller hier dokumentierten Fehler. Nach der ersten verifizierten Reparaturwelle sollte neu geschätzt werden.
|
||||
|
||||
Die Umsetzung gehört in die vorhandene PROGRESS.md-/Roadmap-Struktur. Es sollte kein zusätzlicher konkurrierender Status-Tracker entstehen.
|
||||
|
||||
## Empfohlene Reparaturreihenfolge
|
||||
|
||||
| Welle | Aufgaben | Erforderlicher Nachweis |
|
||||
|---|---|---|
|
||||
| 1: Sicherheitsgrenzen | F01–F05, F10–F11, F15, F20–F21, F23, F30 | Keine unberechtigten Aktionen oder globalen Identitätsänderungen; garantiert isolierte Tests |
|
||||
| 2: Ausführung verbinden | F06–F09, F12–F19, F31, F37, F40–F41 | API und separater Worker funktionieren mit echten Rollen, Kontexten und Pluginzuständen |
|
||||
| 3: Fachliche Integrität | F25–F28, F32–F35, F38–F39 | Importzählung, Dateiidentität, Termine, Feldrechte und Kontextwechsel stimmen |
|
||||
| 4: Betriebsfreigabe | F22, F24, F29, F36 und korrigierte Phase R | Frischinstallation, Upgrade, Vollsuite, E2E, unabhängiger Ausfallalarm und Restore bestehen |
|
||||
|
||||
Alarmierung und Testinfrastruktur können bereits während der ersten Welle vorbereitet werden.
|
||||
|
||||
## Was bereits sinnvoll gelöst ist
|
||||
|
||||
- Die Pluginrouten verwenden echte Routermechanismen für Parameter und Routenpriorität.
|
||||
- Die generierte Komponentenmap ist aktuell.
|
||||
- dms_file wird als Entityalias registriert; der Name ist für sich kein Fehler.
|
||||
- Workspaces sind ausdrücklich als Arbeitskontext ohne Rechtevergabe modelliert.
|
||||
- Einige Core-Workerjobs setzen den Mandantenkontext bereits korrekt.
|
||||
- Logout leert den QueryClient bereits.
|
||||
- Der Frontend-Produktionsbuild und TypeScript laufen erfolgreich.
|
||||
|
||||
Diese Verbesserungen sollten bei der Sanierung erhalten bleiben.
|
||||
|
||||
## Was für eine belastbare Freigabe noch nachgewiesen werden muss
|
||||
|
||||
1. PostgreSQL mit echten Rollen: Frischinstallation, Upgrade, RLS, Grants, Fremdschlüssel sowie Verhalten bei fehlendem Kontext.
|
||||
2. Separater Worker: Pluginregistrierung, Outbox, Retry, Fehlerablage, Deaktivierung und Wiederaufnahme nach Neustart.
|
||||
3. Browser mit mehreren Mandanten und Rollen: Suche, Kontextwechsel, Pluginfehler, Upload, Serientermine und KI-Freigaben.
|
||||
4. Reale Testintegrationen: SMTP/IMAP, Storage, Editor und LLM-Anbieter einschließlich Fehlerfällen.
|
||||
5. Wiederherstellung: Datenbank und Dateien auf isoliertem Ziel nach tatsächlichem Containerersatz.
|
||||
6. Weitere getrennte Prüfungen: Aktueller Abhängigkeits-/CVE- und Lizenzscan, Lasttests und vollständige Prüfung der produktiven Konfiguration. Diese wurden durch das vorliegende Codeaudit nicht ersetzt.
|
||||
Reference in New Issue
Block a user