a4d0f0c35d
Check Cross-Plugin Imports / check (push) Has been cancelled
- D-GEN: RestoreRegistry mit RestoreConfig (model_class, restore_permission, excluded_fields, special_handler) - D-HOOK: history_hooks.py mit register_history_hooks() für after_create/update/delete - D-CORE: Company create+update record_history in companies.py - D-PLUG: Task/Calendar/DMS record_history in services/routes - D-SOFT: Alle registrierten Entitäten haben deleted_at + un-delete via Registry - D-MAIL: Mail special_handler (IMAP Trash-Move, Folder-Verify) + record_history in delete/move - D-TRASH: GET /entity-history/trash (filterbar, paginiert) + Frontend Trash.tsx - D-TOAST: UndoToast.tsx (5s Auto-Dismiss, useUndoToast Hook) - D-HIST-UI: HistoryPanel.tsx (Timeline, Diff-View, Restore-Button) - D-BULK: POST /entity-history/bulk-restore mit partial_success Semantik - D-RET: POST /entity-history/retention/archive (GDPR hard-delete >90 Tage) - D-TEST: 26 Tests in test_restore_registry.py, alle grün - D-DOC: test-strategy.md + security_kernel.md aktualisiert Backend: 10 Dateien, Frontend: 7 Dateien, Tests: 1 Datei, Docs: 3 Dateien 26/26 Tests passed, TSC 0 errors, App import 492 routes
4.9 KiB
4.9 KiB
Security Kernel — Verantwortungstabelle
Architektur-Prinzip
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 machtvisibility.py - ❌
entity_permissions(sharing) — das machtvisibility.py - ❌
is_system_admin— das machtvisibility.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 CHECKfür INSERT/UPDATE — verhindert cross-tenant writes
Defense-in-Depth
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:
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:write | message_id, rfc822_size, raw_path, account_id, folder_id |
Sensitive Fields
id,tenant_id,created_at,updated_at,deleted_atwerden nie restoredsearch_tsv,embeddingwerden 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_successFlag + 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:adminPermission - Snapshots enthalten keine Passwörter oder Secrets (excluded fields)