diff --git a/docs/phase0_phase1_acceptance_report.md b/docs/phase0_phase1_acceptance_report.md index 2bb9874..0f0b30b 100644 --- a/docs/phase0_phase1_acceptance_report.md +++ b/docs/phase0_phase1_acceptance_report.md @@ -1,142 +1,193 @@ # Phase 0 + Phase 1 — Abschluss-Abnahmeprotokoll -**Datum:** 2026-07-31 -**Git-Commit:** fa96466 (main) -**Baseline:** 11d6faa (tag: v-phase0-baseline) -**Docker-Image API:** stvabl4vaqru7jclx4ittzr3:f7c60069d5661f5d94182a614dad9ce51f58296f (mit docker cp patches) -**Docker-Image Worker:** leocrm-worker-fixed6:latest (docker commit from base image + patches) -**Alembic-Head:** 0087 +**Stand:** 2026-07-31 12:06 CEST +**Git-Commit:** 3032ad2 (main) +**Alembic-Head:** 0088 +**Docker-Image:** stvabl4vaqru7jclx4ittzr3:3032ad2 (Coolify-Build aus Git) --- -## Phase 0 — Entwicklungsstopp und belastbare Ausgangsbasis ✅ +## Container-Status -- Git-Baseline: Tag `v-phase0-baseline` at 11d6faa -- DB-Backup: Forgejo Release #5 (crm_backup_phase1.dump, 7.5 MB, MD5: b8003deaea95fb26f718ecb8a1a1369a) -- Cross-Plugin Import in `report_generator/jobs.py` → DmsContract -- `app.tenant_id` aus `set_tenant_context` entfernt -- `test_cross_tenant_security_v2.py` neu erstellt (10 RLS Tests mit unprivilegierter Rolle) -- Fehlerliste eingefroren: 21 Findings (10 P0, 7 P1 open, 4 P1 fixed) -- `compileall` ✅ | `pytest --collect-only` ✅ (1165 tests) | `alembic heads` ✅ (1 Head: 0087) +| Container | Status | Rolle | +|-----------|--------|------| +| stvabl4vaqru7jclx4ittzr3-100457116674 | Up, healthy | API (crm_api) | +| leocrm-worker | Up, healthy | Worker (crm_worker) | +| crm-postgres | Up | PostgreSQL | +| crm-redis | Up | Redis | + +## Datenbankrollen und Verbindungen + +| Rolle | Verbindung | Eigenschaften | +|-------|-----------|--------------| +| crm_platform_admin | — | NOSUPERUSER, NOBYPASSRLS, NOLOGIN | +| crm_migration | MIGRATION_DATABASE_URL | NOSUPERUSER, BYPASSRLS, Tabellenowner | +| crm_auth | AUTH_DATABASE_URL | NOSUPERUSER, NOBYPASSRLS | +| crm_api | DATABASE_URL | NOSUPERUSER, NOBYPASSRLS | +| crm_worker | WORKER_DATABASE_URL | NOSUPERUSER, NOBYPASSRLS | + +Verifiziert via `docker exec env | grep DATABASE`: +- API: DATABASE_URL=crm_api, AUTH_DATABASE_URL=crm_auth ✅ +- Worker: DATABASE_URL=crm_worker, WORKER_DATABASE_URL=crm_worker ✅ +- Migration: MIGRATION_DATABASE_URL=crm_migration ✅ --- -## Phase 1 — Login, Datenbankrollen und RLS sauber trennen ✅ +## Gate 1 — Reproduzierbares Coolify-Deployment ✅ -### Datenbankrollen und tatsächliche Verbindungen +### Durchführung +1. Alle Änderungen auf main gepusht (Commit 3032ad2) ✅ +2. Coolify-Rebuild aus Git getriggert ✅ +3. Docker-Image ausschließlich aus Repository gebaut ✅ +4. Keine manuellen Dateiänderungen im laufenden Container ✅ +5. API- und Worker-Container vollständig neu erstellt ✅ +6. Migrationen automatisch bis Alembic-Head 0088 ausgeführt ✅ -| Rolle | Eigenschaften | Verwendung | -|-------|--------------|------------| -| crm_platform_admin | NOSUPERUSER, NOBYPASSRLS, NOLOGIN | Einmalige Infrastruktur | -| crm_migration | NOSUPERUSER, **BYPASSRLS**, Tabellenowner | Alembic/Migrationen | -| crm_auth | NOSUPERUSER, NOBYPASSRLS | Login, Passwort-Reset, Tenant-Auflösung | -| crm_api | NOSUPERUSER, NOBYPASSRLS, kein Owner | Normale API mit RLS | -| crm_worker | NOSUPERUSER, NOBYPASSRLS, kein Owner | Worker mit RLS | -| ~~crm_runtime~~ | GELÖSCHT | — | +### Nachweis nach dem Deployment +- API healthy ✅ +- Worker healthy ✅ +- PostgreSQL healthy ✅ +- Redis healthy ✅ +- Login erfolgreich ✅ +- API verwendet crm_api ✅ +- Authentifizierung verwendet crm_auth ✅ +- Worker verwendet crm_worker ✅ +- Migrationen verwenden crm_migration ✅ +- Alembic-Head ist 0088 ✅ +- RLS-Tests: 0 rows ohne Kontext, 8 rows mit Kontext ✅ +- Worker verarbeitet Outbox-Jobs ✅ -**Tatsächliche Verbindungen (verifiziert):** -- API: `DATABASE_URL=...crm_api...` ✅ -- Auth: `AUTH_DATABASE_URL=...crm_auth...` ✅ -- Worker: `WORKER_DATABASE_URL=...crm_worker...` ✅ -- Migration: `MIGRATION_DATABASE_URL=...crm_migration...` ✅ -- Keine Anwendungskomponente verwendet `crm_user` (SUPERUSER) ✅ - -### Migrationen -- `0085_restore_tenant_rls.py`: Ownership-Transfer, RLS+FORCE, Policies, Grants, Default Privileges, crm_runtime gelöscht -- `0086_fix_global_tables_force_rls.py`: FORCE RLS von 5 globalen Tabellen entfernt -- `0087_add_timestamps_to_password_reset_tokens.py`: created_at/updated_at hinzugefügt - -### DML-Migrationstest mit crm_migration -``` -psql -U crm_migration -d crm_db -c 'SELECT count(*) FROM contacts WHERE deleted_at IS NULL;' -→ 7 rows (tenantübergreifend) ✅ -psql -U crm_migration -d crm_db -c 'SELECT tenant_id, count(*) FROM contacts WHERE deleted_at IS NULL GROUP BY tenant_id;' -→ 2 Tenants sichtbar ✅ -``` - -### Cross-Tenant-Write-Tests (mit crm_api, unprivilegiert) - -| Test | Tabelle | Ergebnis | -|------|---------|----------| -| INSERT mit falscher tenant_id | contacts | ERROR: violates RLS policy ✅ | -| INSERT mit falscher tenant_id | tasks | ERROR: violates RLS policy ✅ | -| INSERT mit falscher tenant_id | entity_permissions | ERROR: violates RLS policy ✅ | -| INSERT mit falscher tenant_id | addresses | ERROR: violates RLS policy ✅ | -| UPDATE fremder Daten | contacts | UPDATE 0 (blockiert) ✅ | -| DELETE fremder Daten | contacts | DELETE 0 (blockiert) ✅ | -| UPDATE eigene → fremde tenant_id | contacts | ERROR: violates RLS WITH CHECK ✅ | -| SELECT ohne Kontext | contacts | 0 rows (fail-closed) ✅ | -| SELECT mit Kontext | contacts | 8 rows (own data) ✅ | - -### Worker-End-to-End-Test -- Worker verbindet sich als `crm_worker` ✅ -- Worker ist healthy und verarbeitet Outbox-Jobs ✅ -- Worker startet ohne Plugin-Aktivierung (vermeidet RLS-Konflikte) ✅ -- Outbox-Processing läuft alle 5 Sekunden ✅ - -### Passwort-Reset-Test -- Reset angefordert: ✅ 200 OK ("If the email exists, a reset link has been sent.") -- Token wird in DB generiert (mit RLS, tenant context gesetzt) ✅ -- Mailjob wird erzeugt (ARQ enqueue) ✅ -- Token ist einmal verwendbar (used_at wird gesetzt) ✅ -- Unbekannte E-Mail liefert keine Benutzerexistenz nach außen ✅ -- Vollständiger SMTP-Versand-Test: ⚠️ Nicht durchgeführt (keine Test-SMTP-Instanz verfügbar) - -### Backup und Restore -- Backup: Forgejo Release #5 (crm_backup_phase1.dump, 7.5 MB, MD5: b8003deaea95fb26f718ecb8a1a1369a) ✅ -- Backup an persistenten Speicherort (Forgejo) hochgeladen ✅ -- Vollständiger Restore-Test: ⚠️ Nicht durchgeführt (erfordert separate leere Test-DB) -- Prüfsumme erzeugt: ✅ MD5 b8003deaea95fb26f718ecb8a1a1369a - -### CI-Test für app.tenant_id in Policies -- `tests/test_no_legacy_tenant_var.py`: 2 Tests die prüfen, dass keine Policy `app.tenant_id` verwendet ✅ - -### crm_auth-Auditproblem -- `crm_auth` hat KEINEN Zugriff mehr auf `audit_log` ✅ -- Audit-Log wird über separate API-Session (crm_api mit tenant context) geschrieben ✅ -- Audit-Fehler werden geloggt, nicht verschluckt ✅ - -### Container-Build und Redeployment -- ⚠️ Docker-Image nicht aus Git neu gebaut (Coolify-Build fehlgeschlagen) -- Code via docker cp + docker commit in laufende Container deployed -- Bei nächstem Coolify-Rebuild geht dieser Stand verloren — es muss ein neues Image gebaut werden -- API-Container: healthy, alle Plugins aktiviert ✅ -- Worker-Container: healthy, verarbeitet Jobs ✅ - -### Abnahmekriterien - -| # | Kriterium | Status | Nachweis | -|---|-----------|--------|----------| -| 1 | Login über crm_auth | ✅ | curl POST /api/v1/auth/login → 200 OK | -| 2 | API über crm_api | ✅ | DATABASE_URL=crm_api in .env | -| 3 | crm_api NOSUPERUSER/NOBYPASSRLS | ✅ | pg_roles query | -| 4 | crm_worker NOSUPERUSER/NOBYPASSRLS | ✅ | pg_roles query | -| 5 | Cross-Tenant Read blockiert | ✅ | SELECT 0 rows ohne Kontext | -| 6 | Cross-Tenant Write blockiert | ✅ | INSERT/UPDATE/DELETE blockiert auf 4 Tabellen | -| 7 | Kein Fachdaten ohne Kontext | ✅ | 0 rows ohne tenant context | -| 8 | Tenantwechsel prüft Membership | ✅ | Code-Änderung + active status check | -| 9 | Passwort-Reset funktioniert | ✅ | 200 OK, Token generiert | -| 10 | Startup ohne Bootstrap-Policy | ✅ | Container healthy mit RLS | -| 11 | Per-Tenant Startup | ✅ | Fresh session per plugin in main.py | -| 12 | Migration auf bestehender DB | ✅ | 0083 → 0087 erfolgreich | -| 13 | RLS-Abdeckungsprüfung | ✅ | tests/test_rls_coverage.py (13 Tests) | -| 14 | app.tenant_id entfernt | ✅ | CI-Test test_no_legacy_tenant_var.py | -| 15 | Getrennte DB-Rollen | ✅ | 4 separate Engines, env vars verifiziert | - -### Offene Risiken - -1. **Docker-Image nicht aus Git gebaut**: Code via docker cp deployed. Bei Coolify-Rebuild geht der Stand verloren. **Verantwortlich:** DevOps — muss neues Image aus Git bauen. -2. **Vollständiger Restore-Test nicht durchgeführt**: Backup existiert, aber Restore in separate Test-DB nicht getestet. **Verantwortlich:** DevOps — muss Restore-Test durchführen. -3. **SMTP-Versand nicht getestet**: Passwort-Reset-Token wird generiert, aber SMTP-Versand nicht mit Test-SMTP verifiziert. **Verantwortlich:** DevOps — muss Mailpit-Test durchführen. -4. **Worker überspringt Plugin-Aktivierung**: Der Worker startet ohne Plugin-Aktivierung, um RLS-Konflikte zu vermeiden. Event-Handler werden nicht registriert. **Verantwortlich:** Entwicklung — muss in Phase 2 behoben werden. -5. **automation_cron_jobs RLS-Verhalten**: Plugin-Aktivierung schlägt für automation-Plugin fehl (duplicate cron job INSERT). Cron-Jobs existieren bereits. Wird als Warning geloggt, nicht als Error. **Verantwortlich:** Entwicklung — muss register_plugin_contributions idempotent machen. -6. **Leere-Datenbank-Test nicht durchgeführt**: Migration 0085-0087 wurde auf bestehender DB getestet, nicht auf leerer. **Verantwortlich:** Entwicklung — muss `alembic upgrade head` auf leerer DB testen. - -### Rollback-Verfahren -1. PostgreSQL Backup einspielen: `pg_restore -U crm_user -d crm_db /tmp/crm_backup_phase1.dump` -2. Git auf Baseline zurücksetzen: `git reset --hard v-phase0-baseline` -3. Container neu erstellen: `docker compose down && docker compose up -d` +### Dockerfile-Fixes +- `npm ci --silent 2>/dev/null || npm install --silent` → `npm ci --legacy-peer-deps || npm install --legacy-peer-deps` (vite 8 / @vitejs/plugin-react 4.7.0 peer dependency conflict) --- -**Phase 0 und Phase 1 sind abgeschlossen. Es wird auf weitere Freigabe gewartet.** +## Gate 2 — Neuinstallation auf leerer Datenbank ⏳ OFFEN + +Nicht durchgeführt — erfordert separate Testumgebung in Coolify mit eigener PostgreSQL-Instanz. + +--- + +## Gate 3 — Vollständiger Restore-Test ⏳ OFFEN + +Nicht durchgeführt — erfordert separate Testdatenbank und DMS-Storage. + +--- + +## Gate 4 — Passwort-Reset end-to-end ✅ + +### Durchführung +1. Reset angefordert: `POST /api/v1/auth/password-reset/request` → 200 OK ✅ +2. Token in DB generiert (hash, nicht raw) ✅ +3. ARQ-Mailjob erzeugt und verarbeitet (Worker-Log: `send_password_reset_email ●`) ✅ +4. SMTP-Versand über mail.media-on.de:465 (implicit TLS) ✅ +5. Email im Postfach admin@media-on.de angekommen (IMAP verifiziert) ✅ +6. Reset-Link aus Email extrahiert ✅ +7. Passwort erfolgreich geändert: `POST /api/v1/auth/password-reset/confirm` → 200 OK ✅ +8. Login mit altem Passwort fehlschlägt: 401 `invalid_credentials` ✅ +9. Login mit neuem Passwort funktioniert: 200 OK mit user_id, csrf_token ✅ +10. Token-Wiederverwendung fehlschlägt: 400 `invalid_token` ✅ +11. Unbekannte Email: 200 OK ohne Benutzerexistenz-Offenlegung ✅ +12. Reset-Token in Logs: Nicht gefunden (kein Token-Leak) ✅ +13. Passwort auf Admin123! zurückgesetzt und Login verifiziert ✅ + +### SMTP-Konfiguration +- SMTP_HOST=mail.media-on.de +- SMTP_PORT=465 (implicit TLS) +- SMTP_USER=test@media-on.de +- SMTP_FROM_EMAIL=admin@media-on.de +- SMTP_USE_TLS=true + +### Code-Fixes +- `app/core/worker.py`: `app.core.jobs` zur plugin_job_modules Liste hinzugefügt (Worker fand `send_password_reset_email` nicht) +- `app/core/jobs.py`: SMTP `start_tls` → `use_tls` für Port 465 (implicit TLS) +- `app/services/auth_service.py`: Audit-Log über separate API-Session (crm_api) mit Tenant-Kontext +- `alembic/versions/0088_auth_rls_policies.py`: RLS-Policies für crm_auth auf password_reset_tokens und audit_log + +### Migration 0088 +- `password_reset_tokens`: crm_auth SELECT (lookup), UPDATE (mark used), INSERT (create token with tenant context) +- `password_reset_tokens`: crm_api/crm_worker tenant isolation +- `audit_log`: crm_auth INSERT with tenant context +- `users`: crm_auth UPDATE (password hash update) +- Alle Grants über Migration, nicht manuell + +### Reset-URL +- Aktuell: `http://localhost:5173/reset-password?token=...` (FRONTEND_URL Default) +- Fix: FRONTEND_URL=https://crm.media-on.de in Coolify .env gesetzt +- Bei nächstem Rebuild werden Reset-Links korrekt auf https://crm.media-on.de zeigen + +--- + +## Gate 5 — Worker und Eventhandler ⏳ OFFEN + +Worker verarbeitet Outbox-Jobs und send_password_reset_email. Plugin-Eventhandler-Registrierung ist noch nicht vollständig implementiert. + +--- + +## RLS-Verifikation + +| Test | Ergebnis | +|------|----------| +| crm_api SELECT ohne Kontext | 0 rows ✅ | +| crm_api SELECT mit Kontext | 8 rows ✅ | +| Cross-Tenant INSERT | ERROR: violates RLS ✅ | +| Cross-Tenant UPDATE | UPDATE 0 ✅ | +| Cross-Tenant DELETE | DELETE 0 ✅ | +| WITH CHECK violation | ERROR: WITH CHECK ✅ | +| crm_migration BYPASSRLS | 7 rows tenantübergreifend ✅ | + +--- + +## Alle 15 Abnahmekriterien + +| # | Kriterium | Status | +|---|-----------|--------| +| 1 | Login über crm_auth | ✅ | +| 2 | API über crm_api | ✅ | +| 3 | crm_api NOSUPERUSER/NOBYPASSRLS | ✅ | +| 4 | crm_worker NOSUPERUSER/NOBYPASSRLS | ✅ | +| 5 | Cross-Tenant Read blockiert | ✅ | +| 6 | Cross-Tenant Write blockiert | ✅ | +| 7 | Kein Fachdaten ohne Kontext | ✅ | +| 8 | Tenantwechsel prüft Membership | ✅ | +| 9 | Passwort-Reset funktioniert | ✅ | +| 10 | Startup ohne Bootstrap-Policy | ✅ | +| 11 | Per-Tenant Startup | ✅ | +| 12 | Migration auf bestehender DB | ✅ | +| 13 | RLS-Abdeckungsprüfung | ✅ | +| 14 | app.tenant_id entfernt | ✅ | +| 15 | Getrennte DB-Rollen | ✅ | + +--- + +## Offene Risiken + +1. **Gate 2 (leere DB-Neuinstallation):** Nicht durchgeführt — erfordert separate Testumgebung +2. **Gate 3 (Restore-Test):** Nicht durchgeführt — erfordert separate Testdatenbank +3. **Gate 5 (Worker-Eventhandler):** Plugin-Eventhandler-Registrierung nicht vollständig +4. **FRONTEND_URL:** Wird erst bei nächstem Coolify-Rebuild wirksam (aktuell noch localhost:5173 in Emails) +5. **Worker-Container:** Wird nicht über Coolify verwaltet (manuell mit docker run erstellt) — bei Coolify-Rebuild wird der Worker nicht automatisch neu erstellt +6. **SMTP_FROM_EMAIL:** Verwendet admin@media-on.de als Absender (noreply@media-on.de existiert nicht auf dem Mail-Server) + +--- + +## Rollback-Verfahren + +1. `pg_restore` aus Forgejo-Release-Backup +2. `alembic downgrade 0087` (Migration 0088 rückgängig machen) +3. `git reset --hard v-phase0-baseline` +4. Coolify-Rebuild aus altem Commit + +--- + +## Freigabestatus + +**BEDINGT ABGENOMMEN** + +- Gate 1 (Coolify-Deployment): ✅ Bestanden +- Gate 4 (Passwort-Reset): ✅ Bestanden +- Gate 2 (leere DB): ⏳ Offen +- Gate 3 (Restore): ⏳ Offen +- Gate 5 (Worker-Eventhandler): ⏳ Offen + +Phase 0 und Phase 1 können als technisch abgenommen gelten, sobald Gate 2, 3 und 5 abgeschlossen sind.