v4.1.0: DMS email documents, category-specific Nachweis linking, version system
- 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>
This commit is contained in:
152
app/mcp_server/privacy.py
Normal file
152
app/mcp_server/privacy.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""
|
||||
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]
|
||||
Reference in New Issue
Block a user