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:
363
backend-compliance/gdpr_api.py
Normal file
363
backend-compliance/gdpr_api.py
Normal file
@@ -0,0 +1,363 @@
|
||||
"""
|
||||
GDPR API Endpoints für BreakPilot
|
||||
Stellt DSGVO-konforme Funktionen für Datenauskunft und -löschung bereit
|
||||
|
||||
Endpoints:
|
||||
- POST /privacy/export-pdf - PDF-Datenauskunft herunterladen
|
||||
- GET /privacy/export-html - HTML-Preview der Datenauskunft
|
||||
- GET /privacy/data-categories - Datenkategorien mit Löschfristen
|
||||
- POST /privacy/request-deletion - Löschanfrage einreichen
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Header, Query
|
||||
from fastapi.responses import Response, HTMLResponse
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
import os
|
||||
|
||||
from gdpr_export_service import (
|
||||
GDPRExportService,
|
||||
get_data_retention_policies,
|
||||
get_essential_data_categories,
|
||||
get_optional_data_categories,
|
||||
WEASYPRINT_AVAILABLE
|
||||
)
|
||||
from consent_client import generate_jwt_token
|
||||
|
||||
# Consent Service URL
|
||||
CONSENT_SERVICE_URL = os.getenv("CONSENT_SERVICE_URL", "http://localhost:8081")
|
||||
|
||||
# Admin User UUID
|
||||
ADMIN_USER_UUID = "a0000000-0000-0000-0000-000000000001"
|
||||
|
||||
router = APIRouter(prefix="/consent/privacy", tags=["gdpr-privacy"])
|
||||
|
||||
# Service-Instanz
|
||||
gdpr_service = GDPRExportService()
|
||||
|
||||
|
||||
# Request Models
|
||||
class DeletionRequest(BaseModel):
|
||||
reason: Optional[str] = None
|
||||
confirm: bool = False
|
||||
|
||||
|
||||
# Helper für Token
|
||||
def get_user_token(authorization: Optional[str]) -> str:
|
||||
"""
|
||||
Extrahiert Token aus Authorization Header oder generiert einen für Dev.
|
||||
"""
|
||||
if authorization:
|
||||
parts = authorization.split(" ")
|
||||
if len(parts) == 2 and parts[0] == "Bearer":
|
||||
return parts[1]
|
||||
|
||||
# Für Entwicklung: Generiere einen Demo-Token
|
||||
return generate_jwt_token(
|
||||
user_id="demo-user-12345",
|
||||
email="demo@breakpilot.app",
|
||||
role="user"
|
||||
)
|
||||
|
||||
|
||||
# ==========================================
|
||||
# DSGVO Art. 15: Auskunftsrecht
|
||||
# ==========================================
|
||||
|
||||
@router.post("/export-pdf", response_class=Response)
|
||||
async def export_user_data_pdf(authorization: Optional[str] = Header(None)):
|
||||
"""
|
||||
Generiert eine PDF-Datenauskunft gemäß DSGVO Art. 15.
|
||||
|
||||
Returns:
|
||||
PDF-Dokument mit allen gespeicherten Nutzerdaten
|
||||
"""
|
||||
if not WEASYPRINT_AVAILABLE:
|
||||
raise HTTPException(
|
||||
status_code=501,
|
||||
detail={
|
||||
"error": "PDF-Export nicht verfügbar",
|
||||
"message": "WeasyPrint ist nicht installiert. Bitte nutzen Sie den HTML-Export.",
|
||||
"alternative": "/consent/privacy/export-html"
|
||||
}
|
||||
)
|
||||
|
||||
token = get_user_token(authorization)
|
||||
|
||||
try:
|
||||
pdf_bytes = await gdpr_service.generate_user_data_pdf(token)
|
||||
|
||||
return Response(
|
||||
content=pdf_bytes,
|
||||
media_type="application/pdf",
|
||||
headers={
|
||||
"Content-Disposition": "attachment; filename=breakpilot_datenauskunft.pdf",
|
||||
"Cache-Control": "no-store, no-cache, must-revalidate",
|
||||
"Pragma": "no-cache"
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail={
|
||||
"error": "PDF-Generierung fehlgeschlagen",
|
||||
"message": str(e)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/export-html", response_class=HTMLResponse)
|
||||
async def export_user_data_html(authorization: Optional[str] = Header(None)):
|
||||
"""
|
||||
Generiert eine HTML-Datenauskunft (Preview oder Alternative zu PDF).
|
||||
|
||||
Returns:
|
||||
HTML-Dokument mit allen gespeicherten Nutzerdaten
|
||||
"""
|
||||
token = get_user_token(authorization)
|
||||
|
||||
try:
|
||||
html_content = await gdpr_service.generate_user_data_html(token)
|
||||
return HTMLResponse(
|
||||
content=html_content,
|
||||
headers={
|
||||
"Cache-Control": "no-store, no-cache, must-revalidate",
|
||||
"Pragma": "no-cache"
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail={
|
||||
"error": "HTML-Generierung fehlgeschlagen",
|
||||
"message": str(e)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ==========================================
|
||||
# Datenkategorien & Löschfristen
|
||||
# ==========================================
|
||||
|
||||
@router.get("/data-categories")
|
||||
async def get_data_categories(
|
||||
filter: Optional[str] = Query(None, description="Filter: 'essential', 'optional', oder leer für alle")
|
||||
):
|
||||
"""
|
||||
Gibt alle Datenkategorien mit ihren Löschfristen zurück.
|
||||
|
||||
Diese Information wird auch im PDF-Export angezeigt und gibt Nutzern
|
||||
Transparenz darüber, welche Daten wie lange gespeichert werden.
|
||||
|
||||
Query Parameters:
|
||||
filter: 'essential' für Pflicht-Daten, 'optional' für Opt-in Daten
|
||||
"""
|
||||
if filter == "essential":
|
||||
categories = get_essential_data_categories()
|
||||
elif filter == "optional":
|
||||
categories = get_optional_data_categories()
|
||||
else:
|
||||
categories = get_data_retention_policies()
|
||||
|
||||
return {
|
||||
"categories": categories,
|
||||
"total_count": len(categories),
|
||||
"filter_applied": filter,
|
||||
"info": {
|
||||
"essential": "Diese Daten sind für den Betrieb des Dienstes erforderlich",
|
||||
"optional": "Diese Daten werden nur bei expliziter Zustimmung erhoben"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/data-categories/{category}")
|
||||
async def get_data_category_details(category: str):
|
||||
"""
|
||||
Gibt Details zu einer spezifischen Datenkategorie zurück.
|
||||
"""
|
||||
all_categories = get_data_retention_policies()
|
||||
for cat in all_categories:
|
||||
if cat["category"] == category:
|
||||
return cat
|
||||
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Datenkategorie '{category}' nicht gefunden"
|
||||
)
|
||||
|
||||
|
||||
# ==========================================
|
||||
# DSGVO Art. 17: Recht auf Löschung
|
||||
# ==========================================
|
||||
|
||||
@router.post("/request-deletion")
|
||||
async def request_data_deletion(
|
||||
request: DeletionRequest,
|
||||
authorization: Optional[str] = Header(None)
|
||||
):
|
||||
"""
|
||||
Reicht einen Antrag auf Datenlöschung ein (DSGVO Art. 17).
|
||||
|
||||
Der Antrag wird protokolliert und innerhalb von 30 Tagen bearbeitet.
|
||||
Bestimmte Daten müssen aufgrund gesetzlicher Aufbewahrungsfristen
|
||||
möglicherweise länger gespeichert werden.
|
||||
|
||||
Body:
|
||||
reason: Optionaler Grund für die Löschung
|
||||
confirm: Muss true sein zur Bestätigung
|
||||
"""
|
||||
if not request.confirm:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": "Bestätigung erforderlich",
|
||||
"message": "Bitte bestätigen Sie den Löschantrag mit 'confirm: true'",
|
||||
"warning": "Die Löschung Ihrer Daten kann nicht rückgängig gemacht werden!"
|
||||
}
|
||||
)
|
||||
|
||||
token = get_user_token(authorization)
|
||||
|
||||
# TODO: Hier sollte der Löschantrag an den Go-Service weitergeleitet werden
|
||||
# Für jetzt: Platzhalter-Response
|
||||
|
||||
return {
|
||||
"status": "pending",
|
||||
"message": "Ihr Löschantrag wurde eingereicht",
|
||||
"details": {
|
||||
"request_date": "2024-01-15T10:30:00Z",
|
||||
"expected_completion": "Innerhalb von 30 Tagen",
|
||||
"reason_provided": request.reason is not None,
|
||||
"exceptions": [
|
||||
"Consent-Nachweise (3 Jahre Aufbewahrungspflicht)",
|
||||
"Anonymisierte Audit-Logs (Compliance)"
|
||||
]
|
||||
},
|
||||
"next_steps": [
|
||||
"Sie erhalten eine Bestätigungs-E-Mail",
|
||||
"Die Löschung wird nach Prüfung durchgeführt",
|
||||
"Bei vollständiger Löschung wird Ihr Account deaktiviert"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# ==========================================
|
||||
# Admin Endpoints für GDPR
|
||||
# ==========================================
|
||||
|
||||
admin_router = APIRouter(prefix="/consent/admin/privacy", tags=["gdpr-admin"])
|
||||
|
||||
|
||||
def get_admin_token(authorization: Optional[str]) -> str:
|
||||
"""Extrahiert Admin-Token aus Authorization Header oder generiert einen für Dev."""
|
||||
if authorization:
|
||||
parts = authorization.split(" ")
|
||||
if len(parts) == 2 and parts[0] == "Bearer":
|
||||
return parts[1]
|
||||
|
||||
return generate_jwt_token(
|
||||
user_id=ADMIN_USER_UUID,
|
||||
email="admin@breakpilot.app",
|
||||
role="admin"
|
||||
)
|
||||
|
||||
|
||||
@admin_router.get("/export-pdf/{user_id}", response_class=Response)
|
||||
async def admin_export_user_data_pdf(
|
||||
user_id: str,
|
||||
authorization: Optional[str] = Header(None)
|
||||
):
|
||||
"""
|
||||
[Admin] Generiert PDF-Datenauskunft für einen beliebigen Nutzer.
|
||||
|
||||
Nur für Admins: Ermöglicht Export von Nutzerdaten für Support-Anfragen
|
||||
oder Behördenanfragen.
|
||||
"""
|
||||
if not WEASYPRINT_AVAILABLE:
|
||||
raise HTTPException(
|
||||
status_code=501,
|
||||
detail="PDF-Export nicht verfügbar. WeasyPrint fehlt."
|
||||
)
|
||||
|
||||
# Für Admin-Export: Generiere Token für den angegebenen Nutzer
|
||||
# In Produktion sollte hier eine echte Nutzer-Abfrage stattfinden
|
||||
token = generate_jwt_token(
|
||||
user_id=user_id,
|
||||
email=f"user-{user_id[:8]}@breakpilot.app",
|
||||
role="user"
|
||||
)
|
||||
|
||||
try:
|
||||
pdf_bytes = await gdpr_service.generate_user_data_pdf(token)
|
||||
|
||||
return Response(
|
||||
content=pdf_bytes,
|
||||
media_type="application/pdf",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename=datenauskunft_{user_id[:8]}.pdf",
|
||||
"Cache-Control": "no-store"
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@admin_router.get("/deletion-requests")
|
||||
async def admin_get_deletion_requests(
|
||||
status: Optional[str] = Query(None, description="Filter: pending, processing, completed"),
|
||||
page: int = Query(1, ge=1),
|
||||
per_page: int = Query(20, ge=1, le=100),
|
||||
authorization: Optional[str] = Header(None)
|
||||
):
|
||||
"""
|
||||
[Admin] Gibt alle Löschanträge zurück.
|
||||
"""
|
||||
# TODO: Implementierung mit echtem Backend
|
||||
return {
|
||||
"requests": [],
|
||||
"total": 0,
|
||||
"page": page,
|
||||
"per_page": per_page,
|
||||
"status_filter": status,
|
||||
"message": "Löschanträge-Verwaltung noch nicht implementiert"
|
||||
}
|
||||
|
||||
|
||||
@admin_router.post("/deletion-requests/{request_id}/process")
|
||||
async def admin_process_deletion_request(
|
||||
request_id: str,
|
||||
authorization: Optional[str] = Header(None)
|
||||
):
|
||||
"""
|
||||
[Admin] Bearbeitet einen Löschantrag.
|
||||
"""
|
||||
# TODO: Implementierung mit echtem Backend
|
||||
return {
|
||||
"request_id": request_id,
|
||||
"status": "processing",
|
||||
"message": "Löschantrag wird bearbeitet"
|
||||
}
|
||||
|
||||
|
||||
@admin_router.get("/retention-stats")
|
||||
async def admin_get_retention_stats(authorization: Optional[str] = Header(None)):
|
||||
"""
|
||||
[Admin] Gibt Statistiken über Daten und Löschfristen zurück.
|
||||
"""
|
||||
# TODO: Implementierung mit echtem Backend
|
||||
categories = get_data_retention_policies()
|
||||
|
||||
return {
|
||||
"total_categories": len(categories),
|
||||
"essential_categories": len(get_essential_data_categories()),
|
||||
"optional_categories": len(get_optional_data_categories()),
|
||||
"categories": [
|
||||
{
|
||||
"name": cat["name_de"],
|
||||
"retention_days": cat.get("retention_days"),
|
||||
"is_essential": cat.get("is_essential", True)
|
||||
}
|
||||
for cat in categories
|
||||
],
|
||||
"message": "Detaillierte Statistiken noch nicht implementiert"
|
||||
}
|
||||
Reference in New Issue
Block a user