- Save cover email body as DMS document with new 'email' context type - Show email body separately from attachments in email detail view - Add per-category DMS document assignment in quarterly confirmation (Studiennachweis, Einkommenssituation, Vermögenssituation) - Add VERSION file and context processor for automatic version display - Add MCP server, agent system, import/export, and new migrations - Update compose files and production environment template Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
153 lines
4.0 KiB
Python
153 lines
4.0 KiB
Python
"""
|
||
PII-Maskierung für MCP-Ausgaben.
|
||
|
||
Bei readonly- und editor-Rollen werden folgende Felder maskiert:
|
||
- iban → "****" + letzte 4 Stellen
|
||
- email → "***@" + Domain
|
||
- telefon → "****" + letzte 4 Ziffern
|
||
- geburtsdatum → nur Jahreszahl
|
||
- jaehrliches_einkommen / monatliche_bezuege / vermoegen → Bereichsangabe
|
||
|
||
Admin-Rolle erhält ungemaskierte Daten.
|
||
"""
|
||
|
||
import re
|
||
from decimal import Decimal
|
||
|
||
|
||
def mask_iban(value: str | None) -> str | None:
|
||
if not value:
|
||
return value
|
||
clean = value.replace(" ", "")
|
||
if len(clean) > 4:
|
||
return "****" + clean[-4:]
|
||
return "****"
|
||
|
||
|
||
def mask_email(value: str | None) -> str | None:
|
||
if not value:
|
||
return value
|
||
parts = value.split("@", 1)
|
||
if len(parts) == 2:
|
||
return "***@" + parts[1]
|
||
return "***"
|
||
|
||
|
||
def mask_telefon(value: str | None) -> str | None:
|
||
if not value:
|
||
return value
|
||
digits = re.sub(r"\D", "", value)
|
||
if len(digits) > 4:
|
||
return "****" + digits[-4:]
|
||
return "****"
|
||
|
||
|
||
def mask_geburtsdatum(value) -> str | None:
|
||
"""Zeigt nur das Jahr des Geburtsdatums."""
|
||
if not value:
|
||
return None
|
||
try:
|
||
return str(value)[:4] # "YYYY-MM-DD" → "YYYY"
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
def mask_einkommen(value) -> str | None:
|
||
"""Gibt Einkommensbereich statt genauen Wert zurück."""
|
||
if value is None:
|
||
return None
|
||
try:
|
||
amount = float(value)
|
||
if amount < 10000:
|
||
return "< 10.000 €"
|
||
elif amount < 20000:
|
||
return "10.000–20.000 €"
|
||
elif amount < 30000:
|
||
return "20.000–30.000 €"
|
||
elif amount < 50000:
|
||
return "30.000–50.000 €"
|
||
elif amount < 75000:
|
||
return "50.000–75.000 €"
|
||
else:
|
||
return "> 75.000 €"
|
||
except (TypeError, ValueError):
|
||
return None
|
||
|
||
|
||
def mask_monatsbezuege(value) -> str | None:
|
||
"""Gibt Monatsbezüge-Bereich statt genauen Wert zurück."""
|
||
if value is None:
|
||
return None
|
||
try:
|
||
amount = float(value)
|
||
if amount < 500:
|
||
return "< 500 €/Mon."
|
||
elif amount < 1000:
|
||
return "500–1.000 €/Mon."
|
||
elif amount < 2000:
|
||
return "1.000–2.000 €/Mon."
|
||
elif amount < 3000:
|
||
return "2.000–3.000 €/Mon."
|
||
else:
|
||
return "> 3.000 €/Mon."
|
||
except (TypeError, ValueError):
|
||
return None
|
||
|
||
|
||
# PII-Felder nach Modell
|
||
PII_FIELDS: dict[str, dict] = {
|
||
"destinataer": {
|
||
"iban": mask_iban,
|
||
"email": mask_email,
|
||
"telefon": mask_telefon,
|
||
"geburtsdatum": mask_geburtsdatum,
|
||
"jaehrliches_einkommen": mask_einkommen,
|
||
"monatliche_bezuege": mask_monatsbezuege,
|
||
"vermoegen": mask_einkommen,
|
||
},
|
||
"paechter": {
|
||
"iban": mask_iban,
|
||
"email": mask_email,
|
||
"telefon": mask_telefon,
|
||
"geburtsdatum": mask_geburtsdatum,
|
||
},
|
||
"rentmeister": {
|
||
"iban": mask_iban,
|
||
"email": mask_email,
|
||
"telefon": mask_telefon,
|
||
},
|
||
}
|
||
|
||
|
||
def apply_privacy_filter(data: dict, model_type: str, role: str) -> dict:
|
||
"""
|
||
Maskiert PII-Felder in einem Daten-Dictionary basierend auf Rolle und Modelltyp.
|
||
|
||
Args:
|
||
data: Rohdaten-Dictionary
|
||
model_type: Modelltyp (z.B. "destinataer", "paechter")
|
||
role: Aktuelle Rolle ("readonly", "editor", "admin")
|
||
|
||
Returns:
|
||
Gefiltertes Dictionary (bei admin: unveränderter Input)
|
||
"""
|
||
from .auth import can_read_unmasked
|
||
|
||
if can_read_unmasked(role):
|
||
return data
|
||
|
||
maskers = PII_FIELDS.get(model_type, {})
|
||
if not maskers:
|
||
return data
|
||
|
||
result = dict(data)
|
||
for field, mask_fn in maskers.items():
|
||
if field in result:
|
||
result[field] = mask_fn(result[field])
|
||
return result
|
||
|
||
|
||
def apply_privacy_filter_list(items: list[dict], model_type: str, role: str) -> list[dict]:
|
||
"""Wendet apply_privacy_filter auf eine Liste von Dicts an."""
|
||
return [apply_privacy_filter(item, model_type, role) for item in items]
|