A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1835 lines
66 KiB
Markdown
1835 lines
66 KiB
Markdown
# Mail-RBAC Developer Specification
|
||
|
||
**Version:** 1.0.0
|
||
**Datum:** 2026-01-10
|
||
**Status:** Entwicklungsspezifikation
|
||
**Autor:** BreakPilot Development Team
|
||
|
||
---
|
||
|
||
## 1. Executive Summary
|
||
|
||
Dieses Dokument spezifiziert die technische Implementierung eines DSGVO-konformen Mail-Systems mit rollenbasierter Zugriffskontrolle (RBAC) und Mitarbeiter-Anonymisierungsfunktion.
|
||
|
||
### 1.1 Projektziele
|
||
|
||
| Ziel | Beschreibung | Priorität |
|
||
|------|--------------|-----------|
|
||
| **Rollenbasierte E-Mail** | Funktionale Mailboxen statt personengebundener Adressen | P0 |
|
||
| **DSGVO-Anonymisierung** | Vollständige Anonymisierung bei Mitarbeiter-Ausscheiden | P0 |
|
||
| **Audit-Trail** | Lückenlose Nachverfolgbarkeit aller E-Mail-Aktionen | P0 |
|
||
| **Kalender-Integration** | CalDAV mit Jitsi-Meeting-Links | P1 |
|
||
| **Groupware-UI** | Webmail-Interface für Mitarbeiter | P1 |
|
||
|
||
### 1.2 Nicht-Ziele (Out of Scope)
|
||
|
||
- Exchange/Outlook-Protokoll-Kompatibilität
|
||
- Mobile Push-Notifications (Phase 2)
|
||
- Externe E-Mail-Domain-Routing (Phase 2)
|
||
|
||
---
|
||
|
||
## 2. DSGVO-Konformität
|
||
|
||
### 2.1 Rechtliche Grundlagen
|
||
|
||
| Artikel | Anforderung | Umsetzung |
|
||
|---------|-------------|-----------|
|
||
| **Art. 5 DSGVO** | Datenminimierung | Nur notwendige Metadaten speichern |
|
||
| **Art. 17 DSGVO** | Recht auf Löschung | Anonymisierungs-Workflow |
|
||
| **Art. 20 DSGVO** | Datenportabilität | MBOX/EML Export |
|
||
| **Art. 30 DSGVO** | Verarbeitungsverzeichnis | Automatische Protokollierung |
|
||
| **Art. 32 DSGVO** | Sicherheit | Verschlüsselung at-rest & in-transit |
|
||
|
||
### 2.2 Datenschutz-Prinzipien
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ DATENSCHUTZ-BY-DESIGN │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ 1. TRENNUNG: Person ≠ Rolle ≠ Mailbox │
|
||
│ ├── Person: Max Mustermann (anonymisierbar) │
|
||
│ ├── Rolle: klassenlehrer (persistent) │
|
||
│ └── Mailbox: klassenlehrer.5a@... (rollengebunden) │
|
||
│ │
|
||
│ 2. MINIMIERUNG: Nur speichern was nötig │
|
||
│ ├── E-Mail-Inhalte: Verschlüsselt │
|
||
│ ├── Metadaten: Nur Subject-Hash, keine Namen │
|
||
│ └── Audit: Rollenbasiert, nicht personenbasiert │
|
||
│ │
|
||
│ 3. LÖSCHBARKEIT: Jederzeit anonymisierbar │
|
||
│ ├── Personendaten: Pseudonymisierung │
|
||
│ ├── E-Mail-Archive: Header-Anonymisierung │
|
||
│ └── Audit-Trail: Bleibt für Nachvollziehbarkeit │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 2.3 Aufbewahrungsfristen
|
||
|
||
| Datentyp | Frist | Rechtsgrundlage |
|
||
|----------|-------|-----------------|
|
||
| E-Mail-Inhalte | 10 Jahre | § 147 AO (Geschäftskorrespondenz) |
|
||
| Audit-Logs | 10 Jahre | § 257 HGB |
|
||
| Personenbezogene Daten | Bis Löschungsantrag | Art. 17 DSGVO |
|
||
| Anonymisierte Daten | Unbegrenzt | Keine personenbezogenen Daten |
|
||
|
||
---
|
||
|
||
## 3. Systemarchitektur
|
||
|
||
### 3.1 Komponenten-Übersicht
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────────┐
|
||
│ BREAKPILOT MAIL-RBAC │
|
||
├──────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||
│ │ PRESENTATION LAYER │ │
|
||
│ ├────────────────────────────────────────────────────────────┤ │
|
||
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
|
||
│ │ │ Admin UI │ │ Webmail UI │ │ Calendar UI │ │ │
|
||
│ │ │ (Next.js) │ │ (SOGo) │ │ (SOGo) │ │ │
|
||
│ │ │ :3000 │ │ :20000 │ │ :20000 │ │ │
|
||
│ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │
|
||
│ └─────────┼─────────────────┼─────────────────┼──────────────┘ │
|
||
│ │ │ │ │
|
||
│ ┌─────────┴─────────────────┴─────────────────┴──────────────┐ │
|
||
│ │ APPLICATION LAYER │ │
|
||
│ ├────────────────────────────────────────────────────────────┤ │
|
||
│ │ │ │
|
||
│ │ ┌─────────────────────────────────────────────────────┐ │ │
|
||
│ │ │ RBAC-MAIL-BRIDGE (Python) │ │ │
|
||
│ │ │ Port: 8087 │ │ │
|
||
│ │ ├─────────────────────────────────────────────────────┤ │ │
|
||
│ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │
|
||
│ │ │ │ Mailbox │ │ Anonymizer │ │ Audit │ │ │ │
|
||
│ │ │ │ Manager │ │ Service │ │ Logger │ │ │ │
|
||
│ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │
|
||
│ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │
|
||
│ │ │ │ RBAC │ │ Calendar │ │ Jitsi │ │ │ │
|
||
│ │ │ │ Sync │ │ Bridge │ │ Integrator │ │ │ │
|
||
│ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │
|
||
│ │ └─────────────────────────────────────────────────────┘ │ │
|
||
│ │ │ │ │
|
||
│ │ ┌────────────────────────┼────────────────────────────┐ │ │
|
||
│ │ │ │ │ │ │
|
||
│ │ │ ┌──────────────┐ ┌───┴────────┐ ┌─────────────┐ │ │ │
|
||
│ │ │ │ Stalwart │ │ Existing │ │ MinIO │ │ │ │
|
||
│ │ │ │ Mail Server │ │ RBAC API │ │ Storage │ │ │ │
|
||
│ │ │ │ :25/:143/:993│ │ :8000 │ │ :9000 │ │ │ │
|
||
│ │ │ └──────────────┘ └────────────┘ └─────────────┘ │ │ │
|
||
│ │ └────────────────────────────────────────────────────┘ │ │
|
||
│ └────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||
│ │ DATA LAYER │ │
|
||
│ ├────────────────────────────────────────────────────────────┤ │
|
||
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
|
||
│ │ │ PostgreSQL │ │ Stalwart │ │ MinIO │ │ │
|
||
│ │ │ (RBAC Data) │ │ (Mail Data) │ │ (Attachments)│ │ │
|
||
│ │ │ :5432 │ │ Internal │ │ :9000 │ │ │
|
||
│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │
|
||
│ └────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
└──────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 3.2 Datenfluss
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ E-MAIL SENDEN (OUTBOUND) │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ 1. Lehrer klickt "Senden" in Webmail │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ 2. SOGo → Stalwart SMTP (Port 25) │
|
||
│ │ ┌──────────────────────────────────────┐ │
|
||
│ │ │ From: klassenlehrer.5a@schule.bp.app │ │
|
||
│ │ │ (Rollenbasiert, nicht personenbasiert)│ │
|
||
│ │ └──────────────────────────────────────┘ │
|
||
│ ▼ │
|
||
│ 3. Stalwart → RBAC-Mail-Bridge (Milter Hook) │
|
||
│ │ ┌──────────────────────────────────────┐ │
|
||
│ │ │ - Validiere Absender-Berechtigung │ │
|
||
│ │ │ - Logge Audit-Event │ │
|
||
│ │ │ - Füge X-BP-Role Header hinzu │ │
|
||
│ │ └──────────────────────────────────────┘ │
|
||
│ ▼ │
|
||
│ 4. Stalwart → Internet (MX Lookup) │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ E-MAIL EMPFANGEN (INBOUND) │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ 1. Internet → Stalwart SMTP (Port 25) │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ 2. Stalwart → RBAC-Mail-Bridge (Milter Hook) │
|
||
│ │ ┌──────────────────────────────────────┐ │
|
||
│ │ │ - Identifiziere Ziel-Mailbox │ │
|
||
│ │ │ - Lookup aktuelle Rollenzuweisung │ │
|
||
│ │ │ - Logge Audit-Event │ │
|
||
│ │ └──────────────────────────────────────┘ │
|
||
│ ▼ │
|
||
│ 3. Stalwart → Mailbox Store (IMAP) │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ 4. SOGo zeigt E-Mail an (für zugewiesene Person) │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Datenmodell
|
||
|
||
### 4.1 Entity-Relationship-Diagramm
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────┐
|
||
│ MAIL-RBAC DATENMODELL │
|
||
├─────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌───────────────┐ ┌───────────────────┐ │
|
||
│ │ users │ │ functional_ │ │
|
||
│ │ (bestehend) │ │ mailboxes │ │
|
||
│ ├───────────────┤ ├───────────────────┤ │
|
||
│ │ id (PK) │ │ id (PK) │ │
|
||
│ │ email │ │ role_key (FK) │──────┐ │
|
||
│ │ name │ │ email_address │ │ │
|
||
│ │ is_active │ │ display_name │ │ │
|
||
│ │ anonymized_at │ │ tenant_id (FK) │ │ │
|
||
│ └───────┬───────┘ │ is_active │ │ │
|
||
│ │ └─────────┬─────────┘ │ │
|
||
│ │ │ │ │
|
||
│ │ ┌────────────────────┘ │ │
|
||
│ │ │ │ │
|
||
│ ▼ ▼ ▼ │
|
||
│ ┌───────────────────┐ ┌───────────────────┐ │
|
||
│ │ mailbox_ │ │ roles │ │
|
||
│ │ assignments │ │ (bestehend) │ │
|
||
│ ├───────────────────┤ ├───────────────────┤ │
|
||
│ │ id (PK) │ │ role_key (PK) │ │
|
||
│ │ mailbox_id (FK) │───────────────│ display_name │ │
|
||
│ │ user_id (FK) │ │ category │ │
|
||
│ │ valid_from │ └───────────────────┘ │
|
||
│ │ valid_to │ │
|
||
│ │ assigned_by (FK) │ │
|
||
│ │ revoked_at │ │
|
||
│ └─────────┬─────────┘ │
|
||
│ │ │
|
||
│ │ ┌────────────────────────────────────────────┐ │
|
||
│ │ │ │ │
|
||
│ ▼ ▼ │ │
|
||
│ ┌───────────────────┐ ┌───────────────────┐ │ │
|
||
│ │ email_audit_ │ │ anonymization_ │ │ │
|
||
│ │ trail │ │ log │ │ │
|
||
│ ├───────────────────┤ ├───────────────────┤ │ │
|
||
│ │ id (PK) │ │ id (PK) │ │ │
|
||
│ │ mailbox_id (FK) │ │ entity_type │ │ │
|
||
│ │ direction │ │ entity_id │ │ │
|
||
│ │ subject_hash │ │ anonymization_type│ │ │
|
||
│ │ timestamp │ │ fields_affected │ │ │
|
||
│ │ external_domain │ │ reason │ │ │
|
||
│ │ role_key │ │ performed_by (FK) │◄────────┘ │
|
||
│ └───────────────────┘ │ performed_at │ │
|
||
│ │ legal_basis │ │
|
||
│ └───────────────────┘ │
|
||
│ │
|
||
└───────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 4.2 SQL Schema
|
||
|
||
```sql
|
||
-- ============================================================
|
||
-- MAIL-RBAC SCHEMA
|
||
-- Version: 1.0.0
|
||
-- ============================================================
|
||
|
||
-- Erweiterung der bestehenden users-Tabelle
|
||
ALTER TABLE users ADD COLUMN IF NOT EXISTS
|
||
anonymized_at TIMESTAMP;
|
||
ALTER TABLE users ADD COLUMN IF NOT EXISTS
|
||
anonymization_token VARCHAR(64);
|
||
|
||
-- Funktionale Mailboxen (rollengebunden)
|
||
CREATE TABLE functional_mailboxes (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
||
-- Rollen-Verknüpfung
|
||
role_key VARCHAR(100) NOT NULL,
|
||
email_address VARCHAR(255) UNIQUE NOT NULL,
|
||
display_name VARCHAR(255) NOT NULL,
|
||
|
||
-- Tenant/Schule
|
||
tenant_id UUID NOT NULL REFERENCES tenants(id),
|
||
resource_type VARCHAR(50) DEFAULT 'class',
|
||
resource_id VARCHAR(100),
|
||
|
||
-- Stalwart Mailbox ID (nach Erstellung)
|
||
stalwart_mailbox_id VARCHAR(255),
|
||
|
||
-- Status
|
||
is_active BOOLEAN DEFAULT true,
|
||
created_at TIMESTAMP DEFAULT NOW(),
|
||
created_by UUID REFERENCES users(id),
|
||
|
||
-- Indizes
|
||
CONSTRAINT fk_tenant FOREIGN KEY (tenant_id)
|
||
REFERENCES tenants(id) ON DELETE CASCADE
|
||
);
|
||
|
||
CREATE INDEX idx_fm_role ON functional_mailboxes(role_key);
|
||
CREATE INDEX idx_fm_tenant ON functional_mailboxes(tenant_id);
|
||
CREATE INDEX idx_fm_email ON functional_mailboxes(email_address);
|
||
|
||
-- Mailbox-Zuweisungen (Person ↔ Funktionale Mailbox)
|
||
CREATE TABLE mailbox_assignments (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
||
-- Verknüpfungen
|
||
mailbox_id UUID NOT NULL REFERENCES functional_mailboxes(id),
|
||
user_id UUID NOT NULL REFERENCES users(id),
|
||
|
||
-- Gültigkeitszeitraum
|
||
valid_from TIMESTAMP DEFAULT NOW(),
|
||
valid_to TIMESTAMP,
|
||
|
||
-- Audit
|
||
assigned_by UUID REFERENCES users(id),
|
||
assigned_at TIMESTAMP DEFAULT NOW(),
|
||
revoked_by UUID REFERENCES users(id),
|
||
revoked_at TIMESTAMP,
|
||
revocation_reason VARCHAR(255),
|
||
|
||
-- Constraints
|
||
CONSTRAINT unique_active_mailbox_assignment
|
||
EXCLUDE USING gist (
|
||
mailbox_id WITH =,
|
||
tsrange(valid_from, COALESCE(valid_to, 'infinity'::timestamp)) WITH &&
|
||
) WHERE (revoked_at IS NULL)
|
||
);
|
||
|
||
CREATE INDEX idx_ma_mailbox ON mailbox_assignments(mailbox_id);
|
||
CREATE INDEX idx_ma_user ON mailbox_assignments(user_id);
|
||
CREATE INDEX idx_ma_active ON mailbox_assignments(revoked_at) WHERE revoked_at IS NULL;
|
||
|
||
-- E-Mail Audit Trail (DSGVO-konform, ohne personenbezogene Daten)
|
||
CREATE TABLE email_audit_trail (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
||
-- Mailbox-Referenz (nicht Person!)
|
||
mailbox_id UUID REFERENCES functional_mailboxes(id),
|
||
|
||
-- E-Mail-Metadaten (anonymisiert)
|
||
direction VARCHAR(10) NOT NULL CHECK (direction IN ('inbound', 'outbound')),
|
||
message_id_hash VARCHAR(64), -- SHA-256 der Message-ID
|
||
subject_hash VARCHAR(64), -- SHA-256 des Betreffs
|
||
timestamp TIMESTAMP NOT NULL,
|
||
|
||
-- Externe Partei (nur Domain, nicht volle Adresse)
|
||
external_party_domain VARCHAR(255),
|
||
|
||
-- Rolle zum Zeitpunkt (für Nachvollziehbarkeit)
|
||
role_key VARCHAR(100) NOT NULL,
|
||
|
||
-- Keine personenbezogenen Daten!
|
||
-- Die Person ist nur über mailbox_assignments nachvollziehbar
|
||
|
||
created_at TIMESTAMP DEFAULT NOW()
|
||
);
|
||
|
||
CREATE INDEX idx_eat_mailbox ON email_audit_trail(mailbox_id);
|
||
CREATE INDEX idx_eat_timestamp ON email_audit_trail(timestamp);
|
||
CREATE INDEX idx_eat_role ON email_audit_trail(role_key);
|
||
|
||
-- Anonymisierungsprotokoll
|
||
CREATE TABLE anonymization_log (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
||
-- Was wurde anonymisiert
|
||
entity_type VARCHAR(50) NOT NULL CHECK (entity_type IN (
|
||
'user', 'email_account', 'email_content', 'calendar_event'
|
||
)),
|
||
entity_id UUID NOT NULL,
|
||
|
||
-- Wie
|
||
anonymization_type VARCHAR(50) NOT NULL CHECK (anonymization_type IN (
|
||
'pseudonymization', 'deletion', 'header_anonymization'
|
||
)),
|
||
fields_affected JSONB NOT NULL,
|
||
|
||
-- Warum
|
||
reason VARCHAR(100) NOT NULL CHECK (reason IN (
|
||
'employee_departure', 'dsgvo_request', 'retention_expiry', 'manual'
|
||
)),
|
||
|
||
-- Audit
|
||
performed_by UUID NOT NULL REFERENCES users(id),
|
||
performed_at TIMESTAMP DEFAULT NOW(),
|
||
|
||
-- Rechtliche Dokumentation
|
||
legal_basis VARCHAR(255),
|
||
retention_period_days INTEGER,
|
||
confirmation_token VARCHAR(64)
|
||
);
|
||
|
||
CREATE INDEX idx_al_entity ON anonymization_log(entity_type, entity_id);
|
||
CREATE INDEX idx_al_performed ON anonymization_log(performed_at);
|
||
|
||
-- Kalender-Events mit Jitsi-Integration
|
||
CREATE TABLE calendar_events (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
||
-- Event-Daten
|
||
title VARCHAR(255) NOT NULL,
|
||
description TEXT,
|
||
start_time TIMESTAMP NOT NULL,
|
||
end_time TIMESTAMP NOT NULL,
|
||
location VARCHAR(255),
|
||
|
||
-- Jitsi-Integration
|
||
jitsi_room_id VARCHAR(100),
|
||
jitsi_url TEXT,
|
||
|
||
-- Organisator (Mailbox, nicht Person)
|
||
organizer_mailbox_id UUID REFERENCES functional_mailboxes(id),
|
||
|
||
-- CalDAV-Synchronisation
|
||
caldav_uid VARCHAR(255) UNIQUE,
|
||
caldav_etag VARCHAR(100),
|
||
|
||
-- Status
|
||
is_cancelled BOOLEAN DEFAULT false,
|
||
created_at TIMESTAMP DEFAULT NOW(),
|
||
updated_at TIMESTAMP DEFAULT NOW()
|
||
);
|
||
|
||
CREATE INDEX idx_ce_organizer ON calendar_events(organizer_mailbox_id);
|
||
CREATE INDEX idx_ce_time ON calendar_events(start_time, end_time);
|
||
|
||
-- Kalender-Teilnehmer
|
||
CREATE TABLE calendar_attendees (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
event_id UUID NOT NULL REFERENCES calendar_events(id) ON DELETE CASCADE,
|
||
mailbox_id UUID REFERENCES functional_mailboxes(id),
|
||
external_email VARCHAR(255),
|
||
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN (
|
||
'pending', 'accepted', 'declined', 'tentative'
|
||
)),
|
||
created_at TIMESTAMP DEFAULT NOW()
|
||
);
|
||
|
||
CREATE INDEX idx_ca_event ON calendar_attendees(event_id);
|
||
```
|
||
|
||
---
|
||
|
||
## 5. API-Spezifikation
|
||
|
||
### 5.1 REST API Endpoints
|
||
|
||
```yaml
|
||
# ============================================================
|
||
# MAIL-RBAC API SPECIFICATION
|
||
# OpenAPI 3.0
|
||
# ============================================================
|
||
|
||
openapi: 3.0.3
|
||
info:
|
||
title: BreakPilot Mail-RBAC API
|
||
version: 1.0.0
|
||
description: DSGVO-konformes Mail-System mit Rollen-Integration
|
||
|
||
servers:
|
||
- url: http://localhost:8087/api/v1
|
||
description: Development
|
||
|
||
paths:
|
||
# ==================== MAILBOXEN ====================
|
||
|
||
/mailboxes:
|
||
get:
|
||
summary: Liste aller funktionalen Mailboxen
|
||
tags: [Mailboxes]
|
||
parameters:
|
||
- name: tenant_id
|
||
in: query
|
||
schema:
|
||
type: string
|
||
format: uuid
|
||
- name: role_key
|
||
in: query
|
||
schema:
|
||
type: string
|
||
responses:
|
||
200:
|
||
description: Mailbox-Liste
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/FunctionalMailbox'
|
||
|
||
post:
|
||
summary: Erstellt eine neue funktionale Mailbox
|
||
tags: [Mailboxes]
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/CreateMailboxRequest'
|
||
responses:
|
||
201:
|
||
description: Mailbox erstellt
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/FunctionalMailbox'
|
||
|
||
/mailboxes/{mailbox_id}:
|
||
get:
|
||
summary: Details einer Mailbox
|
||
tags: [Mailboxes]
|
||
parameters:
|
||
- name: mailbox_id
|
||
in: path
|
||
required: true
|
||
schema:
|
||
type: string
|
||
format: uuid
|
||
responses:
|
||
200:
|
||
description: Mailbox-Details
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/FunctionalMailboxDetail'
|
||
|
||
/mailboxes/{mailbox_id}/assign:
|
||
post:
|
||
summary: Weist Mailbox einem Benutzer zu
|
||
tags: [Mailboxes]
|
||
parameters:
|
||
- name: mailbox_id
|
||
in: path
|
||
required: true
|
||
schema:
|
||
type: string
|
||
format: uuid
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/AssignMailboxRequest'
|
||
responses:
|
||
201:
|
||
description: Zuweisung erstellt
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/MailboxAssignment'
|
||
|
||
/mailboxes/{mailbox_id}/revoke:
|
||
post:
|
||
summary: Widerruft Mailbox-Zuweisung
|
||
tags: [Mailboxes]
|
||
parameters:
|
||
- name: mailbox_id
|
||
in: path
|
||
required: true
|
||
schema:
|
||
type: string
|
||
format: uuid
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
properties:
|
||
user_id:
|
||
type: string
|
||
format: uuid
|
||
reason:
|
||
type: string
|
||
responses:
|
||
200:
|
||
description: Zuweisung widerrufen
|
||
|
||
# ==================== ANONYMISIERUNG ====================
|
||
|
||
/users/{user_id}/anonymize:
|
||
post:
|
||
summary: Anonymisiert einen Benutzer vollständig
|
||
tags: [Anonymization]
|
||
description: |
|
||
Führt folgende Schritte aus:
|
||
1. Widerruft alle Mailbox-Zuweisungen
|
||
2. Anonymisiert Benutzerdaten
|
||
3. Anonymisiert E-Mail-Header
|
||
4. Erstellt Audit-Log
|
||
parameters:
|
||
- name: user_id
|
||
in: path
|
||
required: true
|
||
schema:
|
||
type: string
|
||
format: uuid
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/AnonymizationRequest'
|
||
responses:
|
||
200:
|
||
description: Anonymisierung erfolgreich
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/AnonymizationResult'
|
||
|
||
/users/{user_id}/anonymize/preview:
|
||
get:
|
||
summary: Vorschau der Anonymisierung
|
||
tags: [Anonymization]
|
||
description: Zeigt was anonymisiert werden würde
|
||
parameters:
|
||
- name: user_id
|
||
in: path
|
||
required: true
|
||
schema:
|
||
type: string
|
||
format: uuid
|
||
responses:
|
||
200:
|
||
description: Anonymisierungs-Vorschau
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/AnonymizationPreview'
|
||
|
||
# ==================== AUDIT ====================
|
||
|
||
/audit/logs:
|
||
get:
|
||
summary: Audit-Log abrufen
|
||
tags: [Audit]
|
||
parameters:
|
||
- name: mailbox_id
|
||
in: query
|
||
schema:
|
||
type: string
|
||
format: uuid
|
||
- name: from
|
||
in: query
|
||
schema:
|
||
type: string
|
||
format: date-time
|
||
- name: to
|
||
in: query
|
||
schema:
|
||
type: string
|
||
format: date-time
|
||
responses:
|
||
200:
|
||
description: Audit-Einträge
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/AuditLogEntry'
|
||
|
||
/audit/anonymizations:
|
||
get:
|
||
summary: Anonymisierungsprotokoll
|
||
tags: [Audit]
|
||
responses:
|
||
200:
|
||
description: Anonymisierungen
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/AnonymizationLogEntry'
|
||
|
||
/audit/export:
|
||
get:
|
||
summary: DSGVO-Export
|
||
tags: [Audit]
|
||
parameters:
|
||
- name: user_id
|
||
in: query
|
||
required: true
|
||
schema:
|
||
type: string
|
||
format: uuid
|
||
responses:
|
||
200:
|
||
description: DSGVO-Datenexport
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/GDPRExport'
|
||
|
||
# ==================== KALENDER ====================
|
||
|
||
/calendar/events:
|
||
post:
|
||
summary: Erstellt einen Kalender-Eintrag mit optionalem Jitsi-Meeting
|
||
tags: [Calendar]
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/CreateCalendarEventRequest'
|
||
responses:
|
||
201:
|
||
description: Event erstellt
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/CalendarEvent'
|
||
|
||
components:
|
||
schemas:
|
||
FunctionalMailbox:
|
||
type: object
|
||
properties:
|
||
id:
|
||
type: string
|
||
format: uuid
|
||
role_key:
|
||
type: string
|
||
example: "klassenlehrer"
|
||
email_address:
|
||
type: string
|
||
format: email
|
||
example: "klassenlehrer.5a@schule.breakpilot.app"
|
||
display_name:
|
||
type: string
|
||
example: "Klassenlehrer 5a"
|
||
is_active:
|
||
type: boolean
|
||
current_assignee:
|
||
$ref: '#/components/schemas/UserSummary'
|
||
|
||
CreateMailboxRequest:
|
||
type: object
|
||
required:
|
||
- role_key
|
||
- email_address
|
||
- display_name
|
||
- tenant_id
|
||
properties:
|
||
role_key:
|
||
type: string
|
||
email_address:
|
||
type: string
|
||
format: email
|
||
display_name:
|
||
type: string
|
||
tenant_id:
|
||
type: string
|
||
format: uuid
|
||
resource_type:
|
||
type: string
|
||
enum: [class, department, function]
|
||
resource_id:
|
||
type: string
|
||
|
||
AnonymizationRequest:
|
||
type: object
|
||
required:
|
||
- reason
|
||
- confirmed
|
||
properties:
|
||
reason:
|
||
type: string
|
||
enum: [employee_departure, dsgvo_request, manual]
|
||
confirmed:
|
||
type: boolean
|
||
description: Muss true sein für Durchführung
|
||
preserve_audit_trail:
|
||
type: boolean
|
||
default: true
|
||
delete_email_content:
|
||
type: boolean
|
||
default: false
|
||
|
||
AnonymizationResult:
|
||
type: object
|
||
properties:
|
||
success:
|
||
type: boolean
|
||
anonymization_id:
|
||
type: string
|
||
format: uuid
|
||
affected_mailboxes:
|
||
type: integer
|
||
affected_emails:
|
||
type: integer
|
||
audit_log_entry_id:
|
||
type: string
|
||
format: uuid
|
||
```
|
||
|
||
### 5.2 Internal Events (Message Queue)
|
||
|
||
```python
|
||
# Für zukünftige Skalierung: Event-basierte Kommunikation
|
||
|
||
class MailRBACEvent:
|
||
"""Base Event für Mail-RBAC System"""
|
||
event_type: str
|
||
timestamp: datetime
|
||
correlation_id: str
|
||
|
||
class MailboxAssignedEvent(MailRBACEvent):
|
||
"""Wird ausgelöst wenn eine Mailbox zugewiesen wird"""
|
||
event_type = "mailbox.assigned"
|
||
mailbox_id: str
|
||
user_id: str
|
||
role_key: str
|
||
|
||
class MailboxRevokedEvent(MailRBACEvent):
|
||
"""Wird ausgelöst wenn eine Mailbox-Zuweisung widerrufen wird"""
|
||
event_type = "mailbox.revoked"
|
||
mailbox_id: str
|
||
user_id: str
|
||
reason: str
|
||
|
||
class UserAnonymizedEvent(MailRBACEvent):
|
||
"""Wird ausgelöst wenn ein Benutzer anonymisiert wird"""
|
||
event_type = "user.anonymized"
|
||
user_id: str
|
||
anonymization_id: str
|
||
affected_mailboxes: List[str]
|
||
|
||
class EmailSentEvent(MailRBACEvent):
|
||
"""Wird ausgelöst wenn eine E-Mail gesendet wird"""
|
||
event_type = "email.sent"
|
||
mailbox_id: str
|
||
message_id_hash: str
|
||
external_domain: str
|
||
|
||
class CalendarEventCreatedEvent(MailRBACEvent):
|
||
"""Wird ausgelöst wenn ein Kalender-Event erstellt wird"""
|
||
event_type = "calendar.created"
|
||
event_id: str
|
||
organizer_mailbox_id: str
|
||
has_jitsi: bool
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Implementierungsdetails
|
||
|
||
### 6.1 Anonymisierungs-Service
|
||
|
||
```python
|
||
# rbac_mail_bridge/services/anonymizer.py
|
||
|
||
from dataclasses import dataclass
|
||
from datetime import datetime
|
||
from typing import List, Optional
|
||
import hashlib
|
||
import secrets
|
||
|
||
@dataclass
|
||
class AnonymizationResult:
|
||
success: bool
|
||
anonymization_id: str
|
||
affected_mailboxes: int
|
||
affected_emails: int
|
||
audit_log_id: str
|
||
errors: List[str]
|
||
|
||
class EmployeeAnonymizer:
|
||
"""
|
||
DSGVO-konforme Anonymisierung von Mitarbeiterdaten.
|
||
|
||
Prinzipien:
|
||
1. Keine personenbezogenen Daten in Audit-Logs
|
||
2. Rollenbasierte Historie bleibt erhalten
|
||
3. Verschlüsselung der Original-Daten für Auskunftsrechte
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
db: AsyncSession,
|
||
mail_server: StalwartClient,
|
||
encryption_service: EncryptionService
|
||
):
|
||
self.db = db
|
||
self.mail_server = mail_server
|
||
self.encryption = encryption_service
|
||
|
||
async def anonymize(
|
||
self,
|
||
user_id: str,
|
||
reason: str,
|
||
performed_by: str,
|
||
options: AnonymizationOptions
|
||
) -> AnonymizationResult:
|
||
"""
|
||
Führt vollständige Anonymisierung durch.
|
||
|
||
Steps:
|
||
1. Validation
|
||
2. Mailbox-Zuweisungen widerrufen
|
||
3. Benutzerdaten pseudonymisieren
|
||
4. E-Mail-Header anonymisieren (optional)
|
||
5. Audit-Log erstellen
|
||
6. Event publizieren
|
||
"""
|
||
anonymization_id = str(uuid.uuid4())
|
||
errors = []
|
||
|
||
async with self.db.begin():
|
||
# 1. Validierung
|
||
user = await self._get_user(user_id)
|
||
if not user:
|
||
raise UserNotFoundError(user_id)
|
||
|
||
if user.anonymized_at:
|
||
raise AlreadyAnonymizedError(user_id)
|
||
|
||
# 2. Mailbox-Zuweisungen widerrufen
|
||
assignments = await self._get_active_assignments(user_id)
|
||
for assignment in assignments:
|
||
await self._revoke_assignment(
|
||
assignment.id,
|
||
performed_by,
|
||
reason="employee_anonymization"
|
||
)
|
||
|
||
# 3. Benutzerdaten pseudonymisieren
|
||
pseudonym = self._generate_pseudonym()
|
||
|
||
# Original-Daten verschlüsseln (für DSGVO Art. 15 Auskunftsrecht)
|
||
encrypted_original = await self.encryption.encrypt({
|
||
"name": user.name,
|
||
"email": user.email,
|
||
"encrypted_at": datetime.utcnow().isoformat()
|
||
})
|
||
|
||
await self.db.execute("""
|
||
UPDATE users SET
|
||
name = :pseudonym,
|
||
email = :anon_email,
|
||
anonymized_at = NOW(),
|
||
anonymization_token = :token,
|
||
original_data_encrypted = :encrypted
|
||
WHERE id = :user_id
|
||
""", {
|
||
"pseudonym": f"Ehemaliger Mitarbeiter ({pseudonym})",
|
||
"anon_email": f"anon_{pseudonym}@deleted.local",
|
||
"token": secrets.token_hex(32),
|
||
"encrypted": encrypted_original,
|
||
"user_id": user_id
|
||
})
|
||
|
||
# 4. E-Mail-Header anonymisieren (falls gewünscht)
|
||
affected_emails = 0
|
||
if options.anonymize_email_headers:
|
||
affected_emails = await self._anonymize_email_headers(
|
||
user_id, pseudonym
|
||
)
|
||
|
||
# 5. Audit-Log
|
||
audit_log_id = await self._create_audit_log(
|
||
entity_type="user",
|
||
entity_id=user_id,
|
||
anonymization_type="pseudonymization",
|
||
fields_affected={
|
||
"name": True,
|
||
"email": True,
|
||
"mailbox_assignments": len(assignments),
|
||
"email_headers": affected_emails
|
||
},
|
||
reason=reason,
|
||
performed_by=performed_by,
|
||
legal_basis="Art. 17 DSGVO"
|
||
)
|
||
|
||
# 6. Event publizieren (für andere Services)
|
||
await self._publish_event(UserAnonymizedEvent(
|
||
user_id=user_id,
|
||
anonymization_id=anonymization_id,
|
||
affected_mailboxes=[a.mailbox_id for a in assignments]
|
||
))
|
||
|
||
return AnonymizationResult(
|
||
success=True,
|
||
anonymization_id=anonymization_id,
|
||
affected_mailboxes=len(assignments),
|
||
affected_emails=affected_emails,
|
||
audit_log_id=audit_log_id,
|
||
errors=errors
|
||
)
|
||
|
||
def _generate_pseudonym(self) -> str:
|
||
"""Generiert ein eindeutiges Pseudonym."""
|
||
return hashlib.sha256(
|
||
secrets.token_bytes(32)
|
||
).hexdigest()[:12]
|
||
|
||
async def _anonymize_email_headers(
|
||
self,
|
||
user_id: str,
|
||
pseudonym: str
|
||
) -> int:
|
||
"""
|
||
Anonymisiert E-Mail-Header in Stalwart.
|
||
|
||
Ersetzt:
|
||
- From: Max Mustermann → Ehemaliger Mitarbeiter
|
||
- Reply-To: max.mustermann@... → anon_xxx@deleted.local
|
||
|
||
Behält:
|
||
- Funktionale Absender-Adressen (klassenlehrer.5a@...)
|
||
"""
|
||
# Stalwart API Call für Header-Manipulation
|
||
return await self.mail_server.anonymize_headers(
|
||
user_id=user_id,
|
||
replacement_name=f"Ehemaliger Mitarbeiter ({pseudonym})"
|
||
)
|
||
```
|
||
|
||
### 6.2 Stalwart Integration
|
||
|
||
```python
|
||
# rbac_mail_bridge/integrations/stalwart.py
|
||
|
||
class StalwartClient:
|
||
"""
|
||
Client für Stalwart Mail Server API.
|
||
|
||
Dokumentation: https://stalw.art/docs/api/
|
||
"""
|
||
|
||
def __init__(self, base_url: str, api_key: str):
|
||
self.base_url = base_url
|
||
self.api_key = api_key
|
||
self.session = httpx.AsyncClient(
|
||
base_url=base_url,
|
||
headers={"Authorization": f"Bearer {api_key}"}
|
||
)
|
||
|
||
async def create_mailbox(
|
||
self,
|
||
email: str,
|
||
display_name: str,
|
||
quota_mb: int = 1024
|
||
) -> str:
|
||
"""Erstellt eine neue Mailbox in Stalwart."""
|
||
response = await self.session.post(
|
||
"/api/v1/accounts",
|
||
json={
|
||
"email": email,
|
||
"name": display_name,
|
||
"quota": quota_mb * 1024 * 1024,
|
||
"type": "individual"
|
||
}
|
||
)
|
||
response.raise_for_status()
|
||
return response.json()["id"]
|
||
|
||
async def update_mailbox_access(
|
||
self,
|
||
mailbox_id: str,
|
||
user_email: str,
|
||
access_type: str # "full", "send_as", "read_only"
|
||
):
|
||
"""
|
||
Aktualisiert Zugriffsrechte auf eine Mailbox.
|
||
|
||
Wird verwendet wenn:
|
||
- Neue Zuweisung erstellt wird
|
||
- Zuweisung widerrufen wird
|
||
"""
|
||
response = await self.session.put(
|
||
f"/api/v1/accounts/{mailbox_id}/access",
|
||
json={
|
||
"user": user_email,
|
||
"access": access_type
|
||
}
|
||
)
|
||
response.raise_for_status()
|
||
|
||
async def anonymize_headers(
|
||
self,
|
||
user_id: str,
|
||
replacement_name: str
|
||
) -> int:
|
||
"""
|
||
Anonymisiert E-Mail-Header für einen Benutzer.
|
||
|
||
Returns: Anzahl der betroffenen E-Mails
|
||
"""
|
||
# Stalwart unterstützt dies möglicherweise nicht nativ
|
||
# Alternative: Sieve-Filter oder Post-Processing
|
||
pass
|
||
```
|
||
|
||
### 6.3 SOGo Integration
|
||
|
||
```python
|
||
# rbac_mail_bridge/integrations/sogo.py
|
||
|
||
class SOGoClient:
|
||
"""
|
||
Client für SOGo Groupware API.
|
||
|
||
SOGo nutzt CalDAV/CardDAV und hat eine eigene REST API.
|
||
"""
|
||
|
||
def __init__(self, base_url: str, admin_user: str, admin_pass: str):
|
||
self.base_url = base_url
|
||
self.auth = (admin_user, admin_pass)
|
||
|
||
async def create_calendar_event(
|
||
self,
|
||
calendar_id: str,
|
||
event: CalendarEvent
|
||
) -> str:
|
||
"""Erstellt einen Kalender-Eintrag."""
|
||
ical_data = self._to_ical(event)
|
||
|
||
response = await httpx.put(
|
||
f"{self.base_url}/SOGo/dav/{calendar_id}/Calendar/{event.uid}.ics",
|
||
content=ical_data,
|
||
headers={"Content-Type": "text/calendar"},
|
||
auth=self.auth
|
||
)
|
||
response.raise_for_status()
|
||
return event.uid
|
||
|
||
def _to_ical(self, event: CalendarEvent) -> str:
|
||
"""Konvertiert Event zu iCalendar Format."""
|
||
lines = [
|
||
"BEGIN:VCALENDAR",
|
||
"VERSION:2.0",
|
||
"PRODID:-//BreakPilot//Mail-RBAC//DE",
|
||
"BEGIN:VEVENT",
|
||
f"UID:{event.uid}",
|
||
f"DTSTART:{event.start_time.strftime('%Y%m%dT%H%M%SZ')}",
|
||
f"DTEND:{event.end_time.strftime('%Y%m%dT%H%M%SZ')}",
|
||
f"SUMMARY:{event.title}",
|
||
]
|
||
|
||
if event.description:
|
||
lines.append(f"DESCRIPTION:{event.description}")
|
||
|
||
if event.jitsi_url:
|
||
lines.append(f"LOCATION:{event.jitsi_url}")
|
||
lines.append(f"X-JITSI-URL:{event.jitsi_url}")
|
||
|
||
lines.extend([
|
||
"END:VEVENT",
|
||
"END:VCALENDAR"
|
||
])
|
||
|
||
return "\r\n".join(lines)
|
||
```
|
||
|
||
---
|
||
|
||
## 7. Frontend-Integration
|
||
|
||
### 7.1 Admin-Seite: Mail-Management
|
||
|
||
```tsx
|
||
// website/app/admin/mail-management/page.tsx
|
||
|
||
'use client'
|
||
|
||
import { useState, useEffect } from 'react'
|
||
import AdminLayout from '@/components/admin/AdminLayout'
|
||
|
||
type TabType = 'mailboxes' | 'assignments' | 'anonymization' | 'audit'
|
||
|
||
interface FunctionalMailbox {
|
||
id: string
|
||
role_key: string
|
||
email_address: string
|
||
display_name: string
|
||
is_active: boolean
|
||
current_assignee?: {
|
||
id: string
|
||
name: string
|
||
email: string
|
||
}
|
||
}
|
||
|
||
export default function MailManagementPage() {
|
||
const [activeTab, setActiveTab] = useState<TabType>('mailboxes')
|
||
const [mailboxes, setMailboxes] = useState<FunctionalMailbox[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
|
||
useEffect(() => {
|
||
fetchMailboxes()
|
||
}, [])
|
||
|
||
const fetchMailboxes = async () => {
|
||
try {
|
||
const res = await fetch('/api/admin/mail-rbac/mailboxes')
|
||
const data = await res.json()
|
||
setMailboxes(data)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<AdminLayout
|
||
title="Mail-Management"
|
||
description="Rollenbasierte E-Mail-Verwaltung mit DSGVO-Anonymisierung"
|
||
>
|
||
{/* Tabs */}
|
||
<div className="border-b border-slate-200 mb-6">
|
||
<nav className="flex gap-6">
|
||
{[
|
||
{ id: 'mailboxes', label: 'Funktionale Mailboxen', icon: '📧' },
|
||
{ id: 'assignments', label: 'Zuweisungen', icon: '👤' },
|
||
{ id: 'anonymization', label: 'Anonymisierung', icon: '🔒' },
|
||
{ id: 'audit', label: 'Audit-Log', icon: '📋' },
|
||
].map((tab) => (
|
||
<button
|
||
key={tab.id}
|
||
onClick={() => setActiveTab(tab.id as TabType)}
|
||
className={`pb-3 px-1 text-sm font-medium border-b-2 transition-colors ${
|
||
activeTab === tab.id
|
||
? 'border-primary-600 text-primary-600'
|
||
: 'border-transparent text-slate-500 hover:text-slate-700'
|
||
}`}
|
||
>
|
||
{tab.icon} {tab.label}
|
||
</button>
|
||
))}
|
||
</nav>
|
||
</div>
|
||
|
||
{/* Mailboxes Tab */}
|
||
{activeTab === 'mailboxes' && (
|
||
<div className="space-y-6">
|
||
{/* Stats */}
|
||
<div className="grid grid-cols-4 gap-4">
|
||
<div className="bg-white rounded-lg shadow p-4">
|
||
<div className="text-3xl font-bold text-primary-600">
|
||
{mailboxes.length}
|
||
</div>
|
||
<div className="text-sm text-slate-500">Funktionale Mailboxen</div>
|
||
</div>
|
||
<div className="bg-white rounded-lg shadow p-4">
|
||
<div className="text-3xl font-bold text-green-600">
|
||
{mailboxes.filter(m => m.current_assignee).length}
|
||
</div>
|
||
<div className="text-sm text-slate-500">Zugewiesen</div>
|
||
</div>
|
||
<div className="bg-white rounded-lg shadow p-4">
|
||
<div className="text-3xl font-bold text-yellow-600">
|
||
{mailboxes.filter(m => !m.current_assignee).length}
|
||
</div>
|
||
<div className="text-sm text-slate-500">Nicht zugewiesen</div>
|
||
</div>
|
||
<div className="bg-white rounded-lg shadow p-4">
|
||
<div className="text-3xl font-bold text-slate-600">
|
||
{new Set(mailboxes.map(m => m.role_key)).size}
|
||
</div>
|
||
<div className="text-sm text-slate-500">Verschiedene Rollen</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Mailbox Liste */}
|
||
<div className="bg-white rounded-lg shadow">
|
||
<div className="p-4 border-b border-slate-200 flex justify-between items-center">
|
||
<h2 className="text-lg font-semibold">Funktionale Mailboxen</h2>
|
||
<button className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700">
|
||
+ Neue Mailbox
|
||
</button>
|
||
</div>
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full">
|
||
<thead className="bg-slate-50">
|
||
<tr>
|
||
<th className="text-left py-3 px-4 text-sm font-medium text-slate-600">
|
||
E-Mail-Adresse
|
||
</th>
|
||
<th className="text-left py-3 px-4 text-sm font-medium text-slate-600">
|
||
Rolle
|
||
</th>
|
||
<th className="text-left py-3 px-4 text-sm font-medium text-slate-600">
|
||
Aktuell zugewiesen
|
||
</th>
|
||
<th className="text-left py-3 px-4 text-sm font-medium text-slate-600">
|
||
Status
|
||
</th>
|
||
<th className="text-left py-3 px-4 text-sm font-medium text-slate-600">
|
||
Aktionen
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-slate-100">
|
||
{mailboxes.map((mailbox) => (
|
||
<tr key={mailbox.id} className="hover:bg-slate-50">
|
||
<td className="py-3 px-4">
|
||
<code className="text-sm bg-slate-100 px-2 py-1 rounded">
|
||
{mailbox.email_address}
|
||
</code>
|
||
</td>
|
||
<td className="py-3 px-4">
|
||
<span className="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full">
|
||
{mailbox.role_key}
|
||
</span>
|
||
</td>
|
||
<td className="py-3 px-4">
|
||
{mailbox.current_assignee ? (
|
||
<div>
|
||
<div className="font-medium">
|
||
{mailbox.current_assignee.name}
|
||
</div>
|
||
<div className="text-xs text-slate-500">
|
||
{mailbox.current_assignee.email}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<span className="text-slate-400 italic">
|
||
Nicht zugewiesen
|
||
</span>
|
||
)}
|
||
</td>
|
||
<td className="py-3 px-4">
|
||
<span className={`px-2 py-1 text-xs rounded-full ${
|
||
mailbox.is_active
|
||
? 'bg-green-100 text-green-800'
|
||
: 'bg-red-100 text-red-800'
|
||
}`}>
|
||
{mailbox.is_active ? 'Aktiv' : 'Inaktiv'}
|
||
</span>
|
||
</td>
|
||
<td className="py-3 px-4">
|
||
<div className="flex gap-2">
|
||
<button className="text-sm text-blue-600 hover:underline">
|
||
Bearbeiten
|
||
</button>
|
||
<button className="text-sm text-green-600 hover:underline">
|
||
Zuweisen
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Anonymization Tab */}
|
||
{activeTab === 'anonymization' && (
|
||
<div className="space-y-6">
|
||
{/* Warning Banner */}
|
||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
|
||
<div className="flex gap-3">
|
||
<span className="text-2xl">⚠️</span>
|
||
<div>
|
||
<h3 className="font-semibold text-amber-800">
|
||
Anonymisierung ist irreversibel
|
||
</h3>
|
||
<p className="text-sm text-amber-700">
|
||
Die Anonymisierung kann nicht rückgängig gemacht werden.
|
||
Stellen Sie sicher, dass alle Aufbewahrungsfristen eingehalten werden.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Anonymization Form */}
|
||
<div className="bg-white rounded-lg shadow p-6">
|
||
<h2 className="text-lg font-semibold mb-4">
|
||
Mitarbeiter anonymisieren
|
||
</h2>
|
||
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||
Mitarbeiter auswählen
|
||
</label>
|
||
<select className="w-full border border-slate-300 rounded-lg px-3 py-2">
|
||
<option>-- Bitte wählen --</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||
Grund der Anonymisierung
|
||
</label>
|
||
<select className="w-full border border-slate-300 rounded-lg px-3 py-2">
|
||
<option value="employee_departure">Mitarbeiter-Ausscheiden</option>
|
||
<option value="dsgvo_request">DSGVO-Löschungsantrag</option>
|
||
<option value="manual">Manuell (mit Begründung)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<label className="flex items-center gap-2">
|
||
<input type="checkbox" className="rounded" defaultChecked />
|
||
<span className="text-sm">Audit-Trail beibehalten</span>
|
||
</label>
|
||
<label className="flex items-center gap-2">
|
||
<input type="checkbox" className="rounded" />
|
||
<span className="text-sm">E-Mail-Inhalte löschen</span>
|
||
</label>
|
||
</div>
|
||
|
||
<div className="pt-4 border-t border-slate-200">
|
||
<button className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700">
|
||
Anonymisierung durchführen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</AdminLayout>
|
||
)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 8. Docker-Konfiguration
|
||
|
||
### 8.1 Docker Compose Erweiterung
|
||
|
||
```yaml
|
||
# docker-compose.mail.yml
|
||
# Verwendet mit: docker compose -f docker-compose.yml -f docker-compose.mail.yml up
|
||
|
||
version: '3.8'
|
||
|
||
services:
|
||
# Stalwart Mail Server
|
||
stalwart:
|
||
image: stalwartlabs/mail-server:v0.9
|
||
container_name: breakpilot-mail-stalwart
|
||
hostname: mail.breakpilot.local
|
||
ports:
|
||
- "25:25" # SMTP
|
||
- "143:143" # IMAP
|
||
- "465:465" # SMTPS
|
||
- "993:993" # IMAPS
|
||
- "4190:4190" # ManageSieve
|
||
- "8787:8080" # Admin API
|
||
volumes:
|
||
- stalwart-data:/opt/stalwart-mail/data
|
||
- ./config/stalwart/config.toml:/opt/stalwart-mail/etc/config.toml:ro
|
||
environment:
|
||
- STALWART_HOSTNAME=mail.breakpilot.local
|
||
networks:
|
||
- breakpilot-pwa-network
|
||
depends_on:
|
||
postgres:
|
||
condition: service_healthy
|
||
healthcheck:
|
||
test: ["CMD", "curl", "-f", "http://localhost:8080/healthz"]
|
||
interval: 30s
|
||
timeout: 10s
|
||
retries: 3
|
||
|
||
# SOGo Groupware
|
||
sogo:
|
||
image: sogo/sogo:5.10
|
||
container_name: breakpilot-mail-sogo
|
||
ports:
|
||
- "20000:20000"
|
||
volumes:
|
||
- ./config/sogo/sogo.conf:/etc/sogo/sogo.conf:ro
|
||
environment:
|
||
- MYSQL_HOST=postgres # SOGo unterstützt auch PostgreSQL
|
||
- SOGO_HOSTNAME=groupware.breakpilot.local
|
||
depends_on:
|
||
- stalwart
|
||
- postgres
|
||
networks:
|
||
- breakpilot-pwa-network
|
||
healthcheck:
|
||
test: ["CMD", "curl", "-f", "http://localhost:20000/SOGo/"]
|
||
interval: 30s
|
||
timeout: 10s
|
||
retries: 3
|
||
|
||
# RBAC-Mail-Bridge
|
||
rbac-mail-bridge:
|
||
build:
|
||
context: ./rbac-mail-bridge
|
||
dockerfile: Dockerfile
|
||
container_name: breakpilot-mail-rbac
|
||
ports:
|
||
- "8087:8087"
|
||
environment:
|
||
- DATABASE_URL=${DATABASE_URL}
|
||
- STALWART_API_URL=http://stalwart:8080
|
||
- STALWART_API_KEY=${STALWART_API_KEY}
|
||
- SOGO_URL=http://sogo:20000
|
||
- JITSI_URL=${JITSI_URL:-http://localhost:8443}
|
||
- ENCRYPTION_KEY=${MAIL_ENCRYPTION_KEY}
|
||
depends_on:
|
||
- postgres
|
||
- stalwart
|
||
- sogo
|
||
networks:
|
||
- breakpilot-pwa-network
|
||
healthcheck:
|
||
test: ["CMD", "curl", "-f", "http://localhost:8087/health"]
|
||
interval: 30s
|
||
timeout: 10s
|
||
retries: 3
|
||
|
||
volumes:
|
||
stalwart-data:
|
||
driver: local
|
||
```
|
||
|
||
### 8.2 Stalwart Konfiguration
|
||
|
||
```toml
|
||
# config/stalwart/config.toml
|
||
|
||
[server]
|
||
hostname = "mail.breakpilot.local"
|
||
|
||
[store]
|
||
db.type = "postgresql"
|
||
db.url = "postgresql://breakpilot:breakpilot123@postgres:5432/breakpilot_mail"
|
||
|
||
[authentication]
|
||
mechanisms = ["PLAIN", "LOGIN"]
|
||
directory.type = "internal"
|
||
|
||
[imap]
|
||
bind = ["0.0.0.0:143"]
|
||
|
||
[smtp]
|
||
bind = ["0.0.0.0:25"]
|
||
|
||
[api]
|
||
bind = ["0.0.0.0:8080"]
|
||
key = "${STALWART_API_KEY}"
|
||
```
|
||
|
||
---
|
||
|
||
## 9. Sicherheit
|
||
|
||
### 9.1 Verschlüsselung
|
||
|
||
```python
|
||
# rbac_mail_bridge/services/encryption.py
|
||
|
||
from cryptography.fernet import Fernet
|
||
from cryptography.hazmat.primitives import hashes
|
||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||
import base64
|
||
import os
|
||
|
||
class EncryptionService:
|
||
"""
|
||
AES-256 Verschlüsselung für Original-Daten.
|
||
|
||
Verwendet für:
|
||
- Verschlüsselung der Original-Benutzerdaten bei Anonymisierung
|
||
- DSGVO Art. 15 Auskunftsrecht (Entschlüsselung nur bei Berechtigung)
|
||
"""
|
||
|
||
def __init__(self, master_key: str):
|
||
self.master_key = master_key.encode()
|
||
|
||
def _derive_key(self, salt: bytes) -> bytes:
|
||
kdf = PBKDF2HMAC(
|
||
algorithm=hashes.SHA256(),
|
||
length=32,
|
||
salt=salt,
|
||
iterations=100000,
|
||
)
|
||
return base64.urlsafe_b64encode(kdf.derive(self.master_key))
|
||
|
||
async def encrypt(self, data: dict) -> bytes:
|
||
"""Verschlüsselt Daten mit AES-256."""
|
||
salt = os.urandom(16)
|
||
key = self._derive_key(salt)
|
||
f = Fernet(key)
|
||
|
||
plaintext = json.dumps(data).encode()
|
||
ciphertext = f.encrypt(plaintext)
|
||
|
||
# Salt + Ciphertext kombinieren
|
||
return salt + ciphertext
|
||
|
||
async def decrypt(self, encrypted: bytes, audit_reason: str) -> dict:
|
||
"""
|
||
Entschlüsselt Daten.
|
||
|
||
WICHTIG: Jede Entschlüsselung wird geloggt!
|
||
"""
|
||
salt = encrypted[:16]
|
||
ciphertext = encrypted[16:]
|
||
|
||
key = self._derive_key(salt)
|
||
f = Fernet(key)
|
||
|
||
plaintext = f.decrypt(ciphertext)
|
||
|
||
# Audit-Log
|
||
await self._log_decryption(audit_reason)
|
||
|
||
return json.loads(plaintext.decode())
|
||
```
|
||
|
||
### 9.2 Zugriffskontrolle
|
||
|
||
```python
|
||
# Berechtigungen für Mail-RBAC
|
||
|
||
MAIL_RBAC_PERMISSIONS = {
|
||
"mail:mailbox:create": "Kann funktionale Mailboxen erstellen",
|
||
"mail:mailbox:assign": "Kann Mailboxen Benutzern zuweisen",
|
||
"mail:mailbox:revoke": "Kann Mailbox-Zuweisungen widerrufen",
|
||
"mail:user:anonymize": "Kann Benutzer anonymisieren",
|
||
"mail:audit:view": "Kann Audit-Logs einsehen",
|
||
"mail:audit:export": "Kann DSGVO-Export durchführen",
|
||
}
|
||
|
||
# Rollen-Mapping
|
||
ROLE_PERMISSIONS = {
|
||
"schul_admin": [
|
||
"mail:mailbox:create",
|
||
"mail:mailbox:assign",
|
||
"mail:mailbox:revoke",
|
||
],
|
||
"data_protection_officer": [
|
||
"mail:user:anonymize",
|
||
"mail:audit:view",
|
||
"mail:audit:export",
|
||
],
|
||
"schulleitung": [
|
||
"mail:mailbox:assign",
|
||
"mail:mailbox:revoke",
|
||
"mail:audit:view",
|
||
],
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 10. Testing
|
||
|
||
### 10.1 Unit Tests
|
||
|
||
```python
|
||
# rbac_mail_bridge/tests/test_anonymizer.py
|
||
|
||
import pytest
|
||
from datetime import datetime
|
||
from unittest.mock import AsyncMock, MagicMock
|
||
|
||
from services.anonymizer import EmployeeAnonymizer, AnonymizationResult
|
||
|
||
@pytest.fixture
|
||
def anonymizer():
|
||
db = AsyncMock()
|
||
mail_server = AsyncMock()
|
||
encryption = AsyncMock()
|
||
return EmployeeAnonymizer(db, mail_server, encryption)
|
||
|
||
class TestAnonymizer:
|
||
@pytest.mark.asyncio
|
||
async def test_anonymize_user_success(self, anonymizer):
|
||
# Arrange
|
||
anonymizer._get_user = AsyncMock(return_value=MagicMock(
|
||
id="user-123",
|
||
name="Max Mustermann",
|
||
email="max@test.de",
|
||
anonymized_at=None
|
||
))
|
||
anonymizer._get_active_assignments = AsyncMock(return_value=[
|
||
MagicMock(id="assign-1", mailbox_id="mb-1"),
|
||
MagicMock(id="assign-2", mailbox_id="mb-2"),
|
||
])
|
||
anonymizer._revoke_assignment = AsyncMock()
|
||
anonymizer._create_audit_log = AsyncMock(return_value="audit-123")
|
||
anonymizer._publish_event = AsyncMock()
|
||
|
||
# Act
|
||
result = await anonymizer.anonymize(
|
||
user_id="user-123",
|
||
reason="employee_departure",
|
||
performed_by="admin-1",
|
||
options=MagicMock(anonymize_email_headers=False)
|
||
)
|
||
|
||
# Assert
|
||
assert result.success is True
|
||
assert result.affected_mailboxes == 2
|
||
assert anonymizer._revoke_assignment.call_count == 2
|
||
assert anonymizer._create_audit_log.called
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_anonymize_already_anonymized_raises(self, anonymizer):
|
||
# Arrange
|
||
anonymizer._get_user = AsyncMock(return_value=MagicMock(
|
||
anonymized_at=datetime.utcnow()
|
||
))
|
||
|
||
# Act & Assert
|
||
with pytest.raises(AlreadyAnonymizedError):
|
||
await anonymizer.anonymize(
|
||
user_id="user-123",
|
||
reason="employee_departure",
|
||
performed_by="admin-1",
|
||
options=MagicMock()
|
||
)
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_anonymize_user_not_found_raises(self, anonymizer):
|
||
# Arrange
|
||
anonymizer._get_user = AsyncMock(return_value=None)
|
||
|
||
# Act & Assert
|
||
with pytest.raises(UserNotFoundError):
|
||
await anonymizer.anonymize(
|
||
user_id="nonexistent",
|
||
reason="employee_departure",
|
||
performed_by="admin-1",
|
||
options=MagicMock()
|
||
)
|
||
```
|
||
|
||
### 10.2 Integration Tests
|
||
|
||
```python
|
||
# rbac_mail_bridge/tests/test_integration.py
|
||
|
||
import pytest
|
||
from httpx import AsyncClient
|
||
|
||
@pytest.mark.integration
|
||
class TestMailRBACIntegration:
|
||
@pytest.mark.asyncio
|
||
async def test_create_and_assign_mailbox(self, client: AsyncClient):
|
||
# 1. Mailbox erstellen
|
||
create_res = await client.post(
|
||
"/api/v1/mailboxes",
|
||
json={
|
||
"role_key": "klassenlehrer",
|
||
"email_address": "klassenlehrer.test@school.bp.app",
|
||
"display_name": "Klassenlehrer Test",
|
||
"tenant_id": "tenant-123"
|
||
}
|
||
)
|
||
assert create_res.status_code == 201
|
||
mailbox_id = create_res.json()["id"]
|
||
|
||
# 2. Mailbox zuweisen
|
||
assign_res = await client.post(
|
||
f"/api/v1/mailboxes/{mailbox_id}/assign",
|
||
json={
|
||
"user_id": "user-456",
|
||
"valid_from": "2026-01-10T00:00:00Z"
|
||
}
|
||
)
|
||
assert assign_res.status_code == 201
|
||
|
||
# 3. Verifizieren
|
||
get_res = await client.get(f"/api/v1/mailboxes/{mailbox_id}")
|
||
assert get_res.status_code == 200
|
||
assert get_res.json()["current_assignee"]["id"] == "user-456"
|
||
```
|
||
|
||
---
|
||
|
||
## 11. Deployment Checklist
|
||
|
||
### 11.1 Vor dem Deployment
|
||
|
||
- [ ] PostgreSQL Schema migriert
|
||
- [ ] Stalwart API Key generiert und in .env
|
||
- [ ] MAIL_ENCRYPTION_KEY generiert (32 Bytes, Base64)
|
||
- [ ] DNS Records konfiguriert (MX, SPF, DKIM, DMARC)
|
||
- [ ] SSL-Zertifikate für Mail-Domain
|
||
- [ ] Firewall-Regeln für Ports 25, 143, 465, 993
|
||
|
||
### 11.2 Nach dem Deployment
|
||
|
||
- [ ] Health-Checks für alle Services grün
|
||
- [ ] Test-E-Mail senden/empfangen
|
||
- [ ] Admin-UI erreichbar
|
||
- [ ] Audit-Logging funktioniert
|
||
- [ ] Backup konfiguriert
|
||
|
||
---
|
||
|
||
## 12. Roadmap
|
||
|
||
### Phase 1: MVP (4-6 Wochen)
|
||
- [x] Architektur-Dokumentation
|
||
- [ ] Datenbank-Schema
|
||
- [ ] RBAC-Mail-Bridge Backend
|
||
- [ ] Stalwart Integration
|
||
- [ ] Admin-UI (Basis)
|
||
|
||
### Phase 2: Groupware (2-3 Wochen)
|
||
- [ ] SOGo Integration
|
||
- [ ] CalDAV/CardDAV
|
||
- [ ] Jitsi-Meeting aus Kalender
|
||
|
||
### Phase 3: Anonymisierung (2-3 Wochen)
|
||
- [ ] Anonymisierungs-Service
|
||
- [ ] E-Mail-Header-Anonymisierung
|
||
- [ ] DSGVO-Export
|
||
|
||
### Phase 4: Polish (1-2 Wochen)
|
||
- [ ] Admin-UI vollständig
|
||
- [ ] Dokumentation
|
||
- [ ] Produktions-Hardening
|
||
|
||
---
|
||
|
||
## Anhang A: Glossar
|
||
|
||
| Begriff | Beschreibung |
|
||
|---------|--------------|
|
||
| **Funktionale Mailbox** | Rollengebundene E-Mail-Adresse (z.B. klassenlehrer.5a@...) |
|
||
| **Personenbezogene Mailbox** | An eine Person gebundene E-Mail-Adresse |
|
||
| **Anonymisierung** | Unwiderrufliche Entfernung personenbezogener Daten |
|
||
| **Pseudonymisierung** | Ersetzung durch Pseudonym (reversibel mit Schlüssel) |
|
||
| **Audit-Trail** | Lückenlose Protokollierung aller Aktionen |
|
||
| **RBAC** | Role-Based Access Control |
|
||
| **CalDAV** | Calendar Distributed Authoring and Versioning |
|
||
|
||
## Anhang B: Referenzen
|
||
|
||
- [Stalwart Mail Server Docs](https://stalw.art/docs/)
|
||
- [SOGo Installation Guide](https://www.sogo.nu/support/faq.html)
|
||
- [DSGVO Art. 17 - Recht auf Löschung](https://dsgvo-gesetz.de/art-17-dsgvo/)
|
||
- [RFC 5545 - iCalendar](https://tools.ietf.org/html/rfc5545)
|
||
- [RFC 4791 - CalDAV](https://tools.ietf.org/html/rfc4791)
|