# Security Kernel — Verantwortungstabelle ## Architektur-Prinzip ```text Authentifizierung → Tenant Membership → Capability-Prüfung → Objektfilter/ACL → RLS als letzte Barriere ``` ## Was was prüft | Schicht | Verantwortung | Was geprüft wird | Wo implementiert | |---------|--------------|------------------|------------------| | **Authentifizierung** | User identifizieren | Session-Cookie, CSRF-Token | `deps.py:get_current_user()` | | **Tenant Membership** | User gehört zu Tenant | `UserTenant.status == 'active'` | `deps.py:get_current_user()` | | **Capability (RBAC)** | Darf User grundsätzlich Modul nutzen? | `contacts:read`, `contacts:write`, etc. | `require_permission()` Decorator | | **Objekt-ACL** | Darf User DIESEN Datensatz sehen? | `owner_id == user_id` OR shared via `entity_permissions` | `visibility.py:apply_visibility_filter()` | | **ABAC** | Darf User Datensatz mit bestimmten Attributen sehen? | Policy conditions (status, custom fields) | `policy_service.py:apply_policy_filter()` | | **RLS** | Ist User im richtigen Tenant? | `tenant_id == current_setting('app.current_tenant_id')` | PostgreSQL RLS Policies | ## Was RLS NICHT mehr prüft (seit Migration 0069) - ❌ `owner_id` — das macht `visibility.py` - ❌ `entity_permissions` (sharing) — das macht `visibility.py` - ❌ `is_system_admin` — das macht `visibility.py` (überspringt Filter) - ❌ Business-Autorisierung — das macht die Application Layer ## Was RLS nur noch prüft - ✅ `tenant_id == current_setting('app.current_tenant_id')` — Tenant-Isolation - ✅ `WITH CHECK` für INSERT/UPDATE — verhindert cross-tenant writes ## Defense-in-Depth ```text RLS (PostgreSQL) → tenant_id Isolation (Safety Belt) visibility.py (App) → tenant_id + owner_id + sharing (Vehicle Control) ``` Beide Schichten filtern `tenant_id` unabhängig voneinander. Selbst wenn eine Schicht versagt, blockt die andere cross-tenant Zugriff. ## RLS Policies (nach Migration 0069) Alle RLS-enabled Tabellen haben genau eine Policy: ```sql CREATE POLICY {table}_tenant_isolation ON {table} FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid) WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid) ``` Keine Business-Logic in RLS. Keine owner_id, keine sharing, keine permissions. ## Session-Variablen | Variable | Wert | Wo gesetzt | |----------|------|------------| | `app.current_tenant_id` | UUID des aktuellen Tenants | `deps.py:set_tenant_context()` | | `app.tenant_id` | UUID des aktuellen Tenants (Alias) | `deps.py:set_tenant_context()` | | `app.current_user_id` | UUID des aktuellen Users | `deps.py:set_user_context()` | | `app.is_system_admin` | `'true'` oder `'false'` | `deps.py:set_user_context()` | | `app.current_user_groups` | Komma-getrennte Group-IDs | `deps.py:set_user_context()` | ## Test-Verifikation 8/8 Cross-Tenant Security Tests grün: - ✅ visibility_filter_blocks_cross_tenant - ✅ check_single_entity_access_cross_tenant - ✅ get_visible_ids_tenant_scoped - ✅ entity_permissions_tenant_scoped - ✅ rls_tenant_isolation_policy_exists - ✅ rls_enabled_on_tenant_tables - ✅ rls_disabled_on_system_tables - ✅ tenant_context_variable_consistency --- ## Phase D — Undo/Restore Security ### Restore-Registry: Explizite Registrierung Nur explizit registrierte Entity-Typen können restored werden (`RestoreRegistry`). Kein dynamisches ORM-Laden, kein blindes Snapshot-Zurückschreiben. | Entity Type | Restore Permission | Excluded Fields | |-------------|-------------------|----------------| | contact | contacts:write | search_tsv, embedding, default_person_id, admin_contactperson_id | | task | tasks:write | created_by, assigned_to, contact_id | | calendar_entry | calendar:write | calendar_id, created_by, assigned_to, source_mail_id | | dms_file | dms:write | storage_path, content_hash, size_bytes, uploaded_by, folder_id | | mail | mail:write | message_id, rfc822_size, raw_path, account_id, folder_id | ### Sensitive Fields - `id`, `tenant_id`, `created_at`, `updated_at`, `deleted_at` werden **nie** restored - `search_tsv`, `embedding` werden **nie** restored (computed/derived fields) - Entity-spezifische Exclusions verhindern Restore von relationship IDs, storage paths, IMAP metadata ### Mail Restore: IMAP-Semantik - Delete → Move in serverseitigen Trash (IMAP MOVE) - Restore → Move zurück in Original-Ordner (falls noch vorhanden) - Serverfehler erzeugen keinen falschen lokalen Status (MailSyncQueue für Retry) - Kein „Undo Send" für bereits zugestellte externe Mails ### Bulk Restore: Partial-Failure-Semantik - Bei Teilausfällen: `partial_success` Flag + per-item Fehler-Report - Kein stummes Versagen — jeder Erfolg und jeder Fehlschlag wird gemeldet ### Retention: GDPR-Hard-Delete - EntityHistory älter als 90 Tage wird hard-deleted (`archive_old_history`) - Erfordert `system:admin` Permission - Snapshots enthalten keine Passwörter oder Secrets (excluded fields)