This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/docs/klausur-modul/MAIL-RBAC-DEVELOPER-SPECIFICATION.md
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
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>
2026-02-09 09:51:32 +01:00

1835 lines
66 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)