Files
SysAdmin Agent e0b377014c
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy (push) Has been cancelled
Code Quality / quality (push) Has been cancelled
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>
2026-03-15 18:48:52 +00:00

153 lines
4.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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.00020.000 €"
elif amount < 30000:
return "20.00030.000 €"
elif amount < 50000:
return "30.00050.000 €"
elif amount < 75000:
return "50.00075.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 "5001.000 €/Mon."
elif amount < 2000:
return "1.0002.000 €/Mon."
elif amount < 3000:
return "2.0003.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]