Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
460
backend-compliance/gdpr_export_service.py
Normal file
460
backend-compliance/gdpr_export_service.py
Normal file
@@ -0,0 +1,460 @@
|
||||
"""
|
||||
GDPR Export Service für BreakPilot
|
||||
Generiert PDF-Datenauskunft gemäß DSGVO Art. 15
|
||||
|
||||
Datenkategorien mit Löschfristen:
|
||||
- Stammdaten: Account-Löschung + 30 Tage
|
||||
- Einwilligungen: 3 Jahre nach Widerruf/Ablauf
|
||||
- IP-Adressen: 4 Wochen
|
||||
- Session-Daten: Nach Sitzungsende
|
||||
- Audit-Log (personenbezogen): 3 Jahre
|
||||
- Analytics (Opt-in): 26 Monate
|
||||
- Marketing (Opt-in): 12 Monate
|
||||
"""
|
||||
|
||||
import os
|
||||
import io
|
||||
import uuid
|
||||
import httpx
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, Any, List
|
||||
from pathlib import Path
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
|
||||
# WeasyPrint für PDF-Generierung
|
||||
# WeasyPrint benötigt System-Libraries (GTK/Pango/Cairo)
|
||||
# Falls nicht verfügbar, wird nur HTML-Export unterstützt
|
||||
WEASYPRINT_AVAILABLE = False
|
||||
HTML = None
|
||||
CSS = None
|
||||
|
||||
try:
|
||||
from weasyprint import HTML, CSS
|
||||
WEASYPRINT_AVAILABLE = True
|
||||
except (ImportError, OSError) as e:
|
||||
# ImportError: weasyprint nicht installiert
|
||||
# OSError: System-Libraries fehlen (libgobject, etc.)
|
||||
print(f"WeasyPrint nicht verfügbar: {e}")
|
||||
print("PDF-Export deaktiviert. HTML-Export ist weiterhin möglich.")
|
||||
WEASYPRINT_AVAILABLE = False
|
||||
|
||||
# Consent Service URL
|
||||
CONSENT_SERVICE_URL = os.getenv("CONSENT_SERVICE_URL", "http://localhost:8081")
|
||||
|
||||
|
||||
class GDPRExportService:
|
||||
"""Service für DSGVO-konforme Datenexporte als PDF"""
|
||||
|
||||
def __init__(self, template_dir: str = None):
|
||||
"""
|
||||
Initialisiert den Export Service.
|
||||
|
||||
Args:
|
||||
template_dir: Pfad zum Templates-Verzeichnis
|
||||
"""
|
||||
if template_dir is None:
|
||||
template_dir = str(Path(__file__).parent / "templates" / "gdpr")
|
||||
|
||||
self.template_dir = template_dir
|
||||
self.jinja_env = Environment(
|
||||
loader=FileSystemLoader(template_dir),
|
||||
autoescape=select_autoescape(['html', 'xml'])
|
||||
)
|
||||
|
||||
# Custom Filter registrieren
|
||||
self.jinja_env.filters['format_datetime'] = self._format_datetime
|
||||
self.jinja_env.filters['translate_action'] = self._translate_action
|
||||
|
||||
@staticmethod
|
||||
def _format_datetime(value: Optional[str]) -> str:
|
||||
"""Formatiert ISO-Datetime für Anzeige"""
|
||||
if not value:
|
||||
return "-"
|
||||
try:
|
||||
if isinstance(value, str):
|
||||
# ISO Format: 2024-01-15T10:30:00Z
|
||||
dt = datetime.fromisoformat(value.replace('Z', '+00:00'))
|
||||
else:
|
||||
dt = value
|
||||
return dt.strftime("%d.%m.%Y %H:%M")
|
||||
except (ValueError, AttributeError):
|
||||
return str(value) if value else "-"
|
||||
|
||||
@staticmethod
|
||||
def _translate_action(action: str) -> str:
|
||||
"""Übersetzt Audit-Log Aktionen ins Deutsche"""
|
||||
translations = {
|
||||
"login": "Anmeldung",
|
||||
"logout": "Abmeldung",
|
||||
"register": "Registrierung",
|
||||
"consent_given": "Einwilligung erteilt",
|
||||
"consent_withdrawn": "Einwilligung widerrufen",
|
||||
"cookie_consent_updated": "Cookie-Präferenzen aktualisiert",
|
||||
"password_changed": "Passwort geändert",
|
||||
"password_reset_requested": "Passwort-Reset angefordert",
|
||||
"password_reset_completed": "Passwort zurückgesetzt",
|
||||
"email_verified": "E-Mail verifiziert",
|
||||
"profile_updated": "Profil aktualisiert",
|
||||
"data_export_requested": "Datenexport angefordert",
|
||||
"data_deletion_requested": "Datenlöschung angefordert",
|
||||
"session_created": "Sitzung gestartet",
|
||||
"session_revoked": "Sitzung beendet",
|
||||
"version_published": "Version veröffentlicht",
|
||||
"version_approved": "Version genehmigt",
|
||||
"version_rejected": "Version abgelehnt",
|
||||
}
|
||||
return translations.get(action, action)
|
||||
|
||||
async def get_user_data(self, token: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Holt alle Nutzerdaten vom Consent Service.
|
||||
|
||||
Args:
|
||||
token: JWT Token des Nutzers
|
||||
|
||||
Returns:
|
||||
Dictionary mit allen Nutzerdaten
|
||||
"""
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
user_data = {
|
||||
"user": {},
|
||||
"consents": [],
|
||||
"cookie_consents": [],
|
||||
"audit_logs": [],
|
||||
"sessions": []
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
# Profildaten
|
||||
try:
|
||||
profile_resp = await client.get(
|
||||
f"{CONSENT_SERVICE_URL}/api/v1/profile",
|
||||
headers=headers,
|
||||
timeout=10.0
|
||||
)
|
||||
if profile_resp.status_code == 200:
|
||||
user_data["user"] = profile_resp.json()
|
||||
except Exception as e:
|
||||
print(f"Error fetching profile: {e}")
|
||||
|
||||
# Einwilligungen
|
||||
try:
|
||||
consents_resp = await client.get(
|
||||
f"{CONSENT_SERVICE_URL}/api/v1/consent/my",
|
||||
headers=headers,
|
||||
timeout=10.0
|
||||
)
|
||||
if consents_resp.status_code == 200:
|
||||
data = consents_resp.json()
|
||||
user_data["consents"] = data.get("consents", data if isinstance(data, list) else [])
|
||||
except Exception as e:
|
||||
print(f"Error fetching consents: {e}")
|
||||
|
||||
# Cookie-Präferenzen
|
||||
try:
|
||||
cookies_resp = await client.get(
|
||||
f"{CONSENT_SERVICE_URL}/api/v1/cookies/consent/my",
|
||||
headers=headers,
|
||||
timeout=10.0
|
||||
)
|
||||
if cookies_resp.status_code == 200:
|
||||
data = cookies_resp.json()
|
||||
user_data["cookie_consents"] = data.get("consents", data if isinstance(data, list) else [])
|
||||
except Exception as e:
|
||||
print(f"Error fetching cookie consents: {e}")
|
||||
|
||||
# Meine Daten (GDPR endpoint)
|
||||
try:
|
||||
my_data_resp = await client.get(
|
||||
f"{CONSENT_SERVICE_URL}/api/v1/privacy/my-data",
|
||||
headers=headers,
|
||||
timeout=10.0
|
||||
)
|
||||
if my_data_resp.status_code == 200:
|
||||
my_data = my_data_resp.json()
|
||||
# Merge additional data
|
||||
if "audit_log" in my_data:
|
||||
user_data["audit_logs"] = my_data["audit_log"]
|
||||
if "sessions" in my_data:
|
||||
user_data["sessions"] = my_data["sessions"]
|
||||
if "user" in my_data and not user_data["user"]:
|
||||
user_data["user"] = my_data["user"]
|
||||
except Exception as e:
|
||||
print(f"Error fetching my-data: {e}")
|
||||
|
||||
# Aktive Sessions
|
||||
try:
|
||||
sessions_resp = await client.get(
|
||||
f"{CONSENT_SERVICE_URL}/api/v1/profile/sessions",
|
||||
headers=headers,
|
||||
timeout=10.0
|
||||
)
|
||||
if sessions_resp.status_code == 200:
|
||||
data = sessions_resp.json()
|
||||
if not user_data["sessions"]:
|
||||
user_data["sessions"] = data.get("sessions", data if isinstance(data, list) else [])
|
||||
except Exception as e:
|
||||
print(f"Error fetching sessions: {e}")
|
||||
|
||||
return user_data
|
||||
|
||||
def render_html(self, data: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Rendert das HTML-Template mit den Nutzerdaten.
|
||||
|
||||
Args:
|
||||
data: Nutzerdaten Dictionary
|
||||
|
||||
Returns:
|
||||
Gerendertes HTML
|
||||
"""
|
||||
template = self.jinja_env.get_template("gdpr_export.html")
|
||||
|
||||
# Kontext für das Template vorbereiten
|
||||
context = {
|
||||
"export_date": datetime.now().strftime("%d.%m.%Y %H:%M"),
|
||||
"document_id": f"GDPR-{uuid.uuid4().hex[:8].upper()}",
|
||||
"user": data.get("user", {}),
|
||||
"consents": data.get("consents", []),
|
||||
"cookie_consents": data.get("cookie_consents", []),
|
||||
"audit_logs": data.get("audit_logs", []),
|
||||
|
||||
# Company info (kann später aus Config kommen)
|
||||
"company_name": "BreakPilot GmbH",
|
||||
"company_address": "Musterstraße 1",
|
||||
"company_city": "12345 Musterstadt",
|
||||
"dpo_name": "Datenschutzbeauftragter",
|
||||
"dpo_email": "datenschutz@breakpilot.app"
|
||||
}
|
||||
|
||||
return template.render(**context)
|
||||
|
||||
def generate_pdf(self, html_content: str) -> bytes:
|
||||
"""
|
||||
Konvertiert HTML zu PDF mit WeasyPrint.
|
||||
|
||||
Args:
|
||||
html_content: Gerendertes HTML
|
||||
|
||||
Returns:
|
||||
PDF als Bytes
|
||||
|
||||
Raises:
|
||||
RuntimeError: Wenn WeasyPrint nicht verfügbar ist
|
||||
"""
|
||||
if not WEASYPRINT_AVAILABLE:
|
||||
raise RuntimeError(
|
||||
"WeasyPrint ist nicht installiert. "
|
||||
"Bitte installieren Sie: pip install weasyprint"
|
||||
)
|
||||
|
||||
# PDF generieren
|
||||
html = HTML(string=html_content, base_url=self.template_dir)
|
||||
pdf_buffer = io.BytesIO()
|
||||
html.write_pdf(pdf_buffer)
|
||||
|
||||
return pdf_buffer.getvalue()
|
||||
|
||||
async def generate_user_data_pdf(self, token: str) -> bytes:
|
||||
"""
|
||||
Komplette Pipeline: Daten holen, HTML rendern, PDF generieren.
|
||||
|
||||
Args:
|
||||
token: JWT Token des Nutzers
|
||||
|
||||
Returns:
|
||||
PDF als Bytes
|
||||
"""
|
||||
# 1. Nutzerdaten abrufen
|
||||
user_data = await self.get_user_data(token)
|
||||
|
||||
# 2. HTML rendern
|
||||
html_content = self.render_html(user_data)
|
||||
|
||||
# 3. PDF generieren
|
||||
pdf_bytes = self.generate_pdf(html_content)
|
||||
|
||||
return pdf_bytes
|
||||
|
||||
async def generate_user_data_html(self, token: str) -> str:
|
||||
"""
|
||||
Generiert nur HTML (für Preview oder wenn PDF nicht verfügbar).
|
||||
|
||||
Args:
|
||||
token: JWT Token des Nutzers
|
||||
|
||||
Returns:
|
||||
Gerendertes HTML
|
||||
"""
|
||||
user_data = await self.get_user_data(token)
|
||||
return self.render_html(user_data)
|
||||
|
||||
|
||||
# Datenkategorien und Löschfristen (für API-Response)
|
||||
DATA_RETENTION_POLICIES = [
|
||||
{
|
||||
"category": "stammdaten",
|
||||
"name_de": "Stammdaten",
|
||||
"name_en": "Master Data",
|
||||
"description_de": "Name, E-Mail-Adresse, Kontoinformationen",
|
||||
"description_en": "Name, email address, account information",
|
||||
"retention_period": "Account-Löschung + 30 Tage",
|
||||
"retention_days": None, # Abhängig von Account-Löschung
|
||||
"legal_basis": "Vertragserfüllung (Art. 6 Abs. 1 lit. b DSGVO)",
|
||||
"is_essential": True
|
||||
},
|
||||
{
|
||||
"category": "consent_records",
|
||||
"name_de": "Einwilligungen",
|
||||
"name_en": "Consent Records",
|
||||
"description_de": "Consent-Entscheidungen, Dokumentversionen",
|
||||
"description_en": "Consent decisions, document versions",
|
||||
"retention_period": "3 Jahre nach Widerruf/Ablauf",
|
||||
"retention_days": 1095, # 3 Jahre
|
||||
"legal_basis": "Gesetzliche Nachweispflicht (§ 7a UWG)",
|
||||
"is_essential": True
|
||||
},
|
||||
{
|
||||
"category": "ip_addresses",
|
||||
"name_de": "IP-Adressen",
|
||||
"name_en": "IP Addresses",
|
||||
"description_de": "Technische Protokollierung bei Aktionen",
|
||||
"description_en": "Technical logging during actions",
|
||||
"retention_period": "4 Wochen",
|
||||
"retention_days": 28,
|
||||
"legal_basis": "Berechtigtes Interesse (Art. 6 Abs. 1 lit. f DSGVO)",
|
||||
"is_essential": True
|
||||
},
|
||||
{
|
||||
"category": "session_data",
|
||||
"name_de": "Session-Daten",
|
||||
"name_en": "Session Data",
|
||||
"description_de": "Login-Tokens, Sitzungsinformationen",
|
||||
"description_en": "Login tokens, session information",
|
||||
"retention_period": "Nach Sitzungsende oder 24h Inaktivität",
|
||||
"retention_days": 1,
|
||||
"legal_basis": "Vertragserfüllung (Art. 6 Abs. 1 lit. b DSGVO)",
|
||||
"is_essential": True
|
||||
},
|
||||
{
|
||||
"category": "audit_log",
|
||||
"name_de": "Audit-Log",
|
||||
"name_en": "Audit Log",
|
||||
"description_de": "Protokoll aller datenschutzrelevanten Aktionen",
|
||||
"description_en": "Log of all privacy-relevant actions",
|
||||
"retention_period": "3 Jahre (personenbezogen)",
|
||||
"retention_days": 1095,
|
||||
"legal_basis": "Berechtigtes Interesse / Compliance",
|
||||
"is_essential": True
|
||||
},
|
||||
{
|
||||
"category": "password_reset_tokens",
|
||||
"name_de": "Passwort-Reset-Tokens",
|
||||
"name_en": "Password Reset Tokens",
|
||||
"description_de": "Temporäre Tokens für Passwort-Zurücksetzung",
|
||||
"description_en": "Temporary tokens for password reset",
|
||||
"retention_period": "24 Stunden oder nach Nutzung",
|
||||
"retention_days": 1,
|
||||
"legal_basis": "Vertragserfüllung",
|
||||
"is_essential": True
|
||||
},
|
||||
{
|
||||
"category": "email_verification_tokens",
|
||||
"name_de": "E-Mail-Verifikations-Tokens",
|
||||
"name_en": "Email Verification Tokens",
|
||||
"description_de": "Tokens für E-Mail-Bestätigung",
|
||||
"description_en": "Tokens for email confirmation",
|
||||
"retention_period": "7 Tage oder nach Nutzung",
|
||||
"retention_days": 7,
|
||||
"legal_basis": "Vertragserfüllung",
|
||||
"is_essential": True
|
||||
},
|
||||
{
|
||||
"category": "analytics",
|
||||
"name_de": "Analytics-Daten",
|
||||
"name_en": "Analytics Data",
|
||||
"description_de": "Nutzungsstatistiken (nur bei Zustimmung)",
|
||||
"description_en": "Usage statistics (only with consent)",
|
||||
"retention_period": "26 Monate",
|
||||
"retention_days": 790,
|
||||
"legal_basis": "Einwilligung (Art. 6 Abs. 1 lit. a DSGVO)",
|
||||
"is_essential": False,
|
||||
"cookie_category": "analytics"
|
||||
},
|
||||
{
|
||||
"category": "marketing",
|
||||
"name_de": "Marketing-Daten",
|
||||
"name_en": "Marketing Data",
|
||||
"description_de": "Werbe-Identifier (nur bei Zustimmung)",
|
||||
"description_en": "Advertising identifiers (only with consent)",
|
||||
"retention_period": "12 Monate",
|
||||
"retention_days": 365,
|
||||
"legal_basis": "Einwilligung (Art. 6 Abs. 1 lit. a DSGVO)",
|
||||
"is_essential": False,
|
||||
"cookie_category": "marketing"
|
||||
},
|
||||
{
|
||||
"category": "functional",
|
||||
"name_de": "Funktionale Daten",
|
||||
"name_en": "Functional Data",
|
||||
"description_de": "Personalisierung, Präferenzen (bei Zustimmung)",
|
||||
"description_en": "Personalization, preferences (with consent)",
|
||||
"retention_period": "6 Monate",
|
||||
"retention_days": 180,
|
||||
"legal_basis": "Einwilligung (Art. 6 Abs. 1 lit. a DSGVO)",
|
||||
"is_essential": False,
|
||||
"cookie_category": "functional"
|
||||
},
|
||||
{
|
||||
"category": "export_requests",
|
||||
"name_de": "Export-Anfragen",
|
||||
"name_en": "Export Requests",
|
||||
"description_de": "Anträge auf Datenauskunft",
|
||||
"description_en": "Data access requests",
|
||||
"retention_period": "30 Tage nach Abschluss",
|
||||
"retention_days": 30,
|
||||
"legal_basis": "Vertragserfüllung / DSGVO Art. 15",
|
||||
"is_essential": True
|
||||
},
|
||||
{
|
||||
"category": "deletion_requests",
|
||||
"name_de": "Lösch-Anfragen",
|
||||
"name_en": "Deletion Requests",
|
||||
"description_de": "Anträge auf Datenlöschung (anonymisiert)",
|
||||
"description_en": "Data deletion requests (anonymized)",
|
||||
"retention_period": "3 Jahre (anonymisiert)",
|
||||
"retention_days": 1095,
|
||||
"legal_basis": "Nachweis der Löschung",
|
||||
"is_essential": True
|
||||
},
|
||||
{
|
||||
"category": "notifications",
|
||||
"name_de": "Benachrichtigungen",
|
||||
"name_en": "Notifications",
|
||||
"description_de": "System-Benachrichtigungen an den Nutzer",
|
||||
"description_en": "System notifications to user",
|
||||
"retention_period": "90 Tage nach Lesen",
|
||||
"retention_days": 90,
|
||||
"legal_basis": "Vertragserfüllung",
|
||||
"is_essential": True
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def get_data_retention_policies() -> List[Dict[str, Any]]:
|
||||
"""Gibt alle Datenkategorien mit Löschfristen zurück"""
|
||||
return DATA_RETENTION_POLICIES
|
||||
|
||||
|
||||
def get_essential_data_categories() -> List[Dict[str, Any]]:
|
||||
"""Gibt nur essenzielle Datenkategorien zurück"""
|
||||
return [p for p in DATA_RETENTION_POLICIES if p.get("is_essential", True)]
|
||||
|
||||
|
||||
def get_optional_data_categories() -> List[Dict[str, Any]]:
|
||||
"""Gibt optionale (opt-in) Datenkategorien zurück"""
|
||||
return [p for p in DATA_RETENTION_POLICIES if not p.get("is_essential", True)]
|
||||
Reference in New Issue
Block a user