# 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 --- ## Phase 0 — Entwicklungsstopp und belastbare Ausgangsbasis ✅ - 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) --- ## Phase 1 — Login, Datenbankrollen und RLS sauber trennen ✅ ### Datenbankrollen und tatsächliche Verbindungen | 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 | — | **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` --- **Phase 0 und Phase 1 sind abgeschlossen. Es wird auf weitere Freigabe gewartet.**