feat: Art. 11 DSGVO — reject DSR when data subject not identifiable
- New DSRArt11Service: handles rejection with proper legal basis,
automated email notification to requester explaining Art. 11
- POST /dsr/{id}/reject-art11 endpoint
- ActionButtons.tsx: "Nicht identifizierbar (Art. 11)" button
shown when identity is not yet verified
- Also fixes: DSR export type-cast rollback handling
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,8 @@ export function ActionButtons({
|
|||||||
onExtendDeadline,
|
onExtendDeadline,
|
||||||
onComplete,
|
onComplete,
|
||||||
onReject,
|
onReject,
|
||||||
onAssign
|
onAssign,
|
||||||
|
onRejectArt11,
|
||||||
}: {
|
}: {
|
||||||
request: DSRRequest
|
request: DSRRequest
|
||||||
onVerifyIdentity: () => void
|
onVerifyIdentity: () => void
|
||||||
@@ -17,6 +18,7 @@ export function ActionButtons({
|
|||||||
onComplete: () => void
|
onComplete: () => void
|
||||||
onReject: () => void
|
onReject: () => void
|
||||||
onAssign: () => void
|
onAssign: () => void
|
||||||
|
onRejectArt11?: () => void
|
||||||
}) {
|
}) {
|
||||||
const isTerminal = request.status === 'completed' || request.status === 'rejected' || request.status === 'cancelled'
|
const isTerminal = request.status === 'completed' || request.status === 'rejected' || request.status === 'cancelled'
|
||||||
|
|
||||||
@@ -48,12 +50,23 @@ export function ActionButtons({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{!request.identityVerification.verified && (
|
{!request.identityVerification.verified && (
|
||||||
<button
|
<>
|
||||||
onClick={onVerifyIdentity}
|
<button
|
||||||
className="w-full px-4 py-2 bg-yellow-500 text-white hover:bg-yellow-600 rounded-lg transition-colors text-sm font-medium"
|
onClick={onVerifyIdentity}
|
||||||
>
|
className="w-full px-4 py-2 bg-yellow-500 text-white hover:bg-yellow-600 rounded-lg transition-colors text-sm font-medium"
|
||||||
Identitaet verifizieren
|
>
|
||||||
</button>
|
Identitaet verifizieren
|
||||||
|
</button>
|
||||||
|
{onRejectArt11 && (
|
||||||
|
<button
|
||||||
|
onClick={onRejectArt11}
|
||||||
|
className="w-full px-4 py-2 text-gray-600 bg-gray-50 hover:bg-gray-100 border border-gray-200 rounded-lg transition-colors text-sm"
|
||||||
|
title="Person kann anhand der gespeicherten Daten nicht identifiziert werden (Art. 11 DSGVO)"
|
||||||
|
>
|
||||||
|
Nicht identifizierbar (Art. 11)
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -243,6 +243,19 @@ async def change_status(
|
|||||||
return svc.change_status(dsr_id, body, tenant_id)
|
return svc.change_status(dsr_id, body, tenant_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{dsr_id}/reject-art11")
|
||||||
|
async def reject_art11(
|
||||||
|
dsr_id: str,
|
||||||
|
notes: str = Query(""),
|
||||||
|
tenant_id: str = Depends(_get_tenant),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""Reject DSR under Art. 11 DSGVO — data subject not identifiable."""
|
||||||
|
from compliance.services.dsr_art11_service import DSRArt11Service
|
||||||
|
with translate_domain_errors():
|
||||||
|
return DSRArt11Service(db).reject_not_identifiable(dsr_id, tenant_id, notes)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{dsr_id}/verify-identity")
|
@router.post("/{dsr_id}/verify-identity")
|
||||||
async def verify_identity(
|
async def verify_identity(
|
||||||
dsr_id: str,
|
dsr_id: str,
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
"""
|
||||||
|
DSR Art. 11 Service — handles "data subject not identifiable" rejections.
|
||||||
|
|
||||||
|
Art. 11 Abs. 1 DSGVO: If the controller is unable to identify the data
|
||||||
|
subject, it is not obligated to obtain additional information solely to
|
||||||
|
comply with Art. 15-20 requests.
|
||||||
|
|
||||||
|
Common scenario: Website visitor requests access, but only anonymous
|
||||||
|
cookies/IP-hashes are stored — no way to link to a person.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from compliance.domain import ValidationError
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class DSRArt11Service:
|
||||||
|
"""Handles Art. 11 DSGVO rejections for non-identifiable data subjects."""
|
||||||
|
|
||||||
|
def __init__(self, db: Session) -> None:
|
||||||
|
self._db = db
|
||||||
|
|
||||||
|
def reject_not_identifiable(
|
||||||
|
self, dsr_id: str, tenant_id: str, notes: str = "",
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Reject DSR because data subject cannot be identified."""
|
||||||
|
from compliance.db.dsr_models import DSRRequestDB
|
||||||
|
from compliance.services.dsr_workflow_service import _dsr_to_dict, _record_history
|
||||||
|
|
||||||
|
dsr = (
|
||||||
|
self._db.query(DSRRequestDB)
|
||||||
|
.filter(DSRRequestDB.id == dsr_id, DSRRequestDB.tenant_id == tenant_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not dsr:
|
||||||
|
raise ValidationError("DSR not found")
|
||||||
|
if dsr.status in ("completed", "rejected", "cancelled"):
|
||||||
|
raise ValidationError("DSR already closed")
|
||||||
|
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
reason = (
|
||||||
|
"Die bei uns gespeicherten Daten (anonymisierte Cookies, IP-Hashes, "
|
||||||
|
"Device-Fingerprints) erlauben keine Identifikation der betroffenen Person. "
|
||||||
|
"Gemaess Art. 11 Abs. 1 DSGVO sind wir nicht verpflichtet, zusaetzliche "
|
||||||
|
"Informationen zu erheben, um die betroffene Person zu identifizieren."
|
||||||
|
)
|
||||||
|
if notes:
|
||||||
|
reason += f" Ergaenzung: {notes}"
|
||||||
|
|
||||||
|
_record_history(self._db, dsr, "rejected",
|
||||||
|
comment="Art. 11 DSGVO — Identifikation nicht moeglich")
|
||||||
|
dsr.status = "rejected"
|
||||||
|
dsr.rejection_reason = reason
|
||||||
|
dsr.rejection_legal_basis = "Art. 11 Abs. 1 DSGVO"
|
||||||
|
dsr.identity_verified = False
|
||||||
|
dsr.verification_method = "art11_not_identifiable"
|
||||||
|
dsr.verification_notes = "Daten erlauben keine Identifikation der betroffenen Person"
|
||||||
|
dsr.completed_at = now
|
||||||
|
dsr.updated_at = now
|
||||||
|
self._db.commit()
|
||||||
|
self._db.refresh(dsr)
|
||||||
|
|
||||||
|
# Send rejection notification
|
||||||
|
self._send_art11_notification(dsr)
|
||||||
|
|
||||||
|
return _dsr_to_dict(dsr)
|
||||||
|
|
||||||
|
def _send_art11_notification(self, dsr: Any) -> None:
|
||||||
|
if not dsr.requester_email:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
from compliance.services.smtp_sender import send_email
|
||||||
|
send_email(
|
||||||
|
recipient=dsr.requester_email,
|
||||||
|
subject=f"Zu Ihrer Anfrage {dsr.request_number} — Art. 11 DSGVO",
|
||||||
|
body_html=f"""
|
||||||
|
<div style="font-family:system-ui,sans-serif;max-width:600px;margin:0 auto;">
|
||||||
|
<div style="background:#6b7280;color:white;padding:20px 24px;border-radius:12px 12px 0 0;">
|
||||||
|
<h1 style="margin:0;font-size:20px;">Mitteilung zu Ihrer Anfrage</h1>
|
||||||
|
</div>
|
||||||
|
<div style="background:white;padding:24px;border:1px solid #e5e7eb;border-top:none;border-radius:0 0 12px 12px;">
|
||||||
|
<p>Sehr geehrte/r Antragsteller/in,</p>
|
||||||
|
<p>wir haben Ihre Anfrage ({dsr.request_number}) gemaess Art. 15 DSGVO
|
||||||
|
erhalten und geprueft.</p>
|
||||||
|
<p>Leider koennen wir die bei uns gespeicherten Daten (anonymisierte
|
||||||
|
Cookies, IP-Hashes) <strong>keiner identifizierbaren Person zuordnen</strong>.</p>
|
||||||
|
<p>Gemaess <strong>Art. 11 Abs. 1 DSGVO</strong> sind wir nicht verpflichtet,
|
||||||
|
zusaetzliche Informationen zu erheben, um Sie zu identifizieren.
|
||||||
|
Eine Auskunftserteilung ist daher nicht moeglich.</p>
|
||||||
|
<p>Sollten Sie ueber ein Kundenkonto bei uns verfuegen, koennen Sie
|
||||||
|
die Anfrage unter Angabe Ihrer Kundennummer erneut einreichen.</p>
|
||||||
|
<p style="margin-top:24px;color:#6b7280;font-size:12px;">
|
||||||
|
Mit freundlichen Gruessen<br>
|
||||||
|
<strong>Datenschutzbeauftragter</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Art. 11 notification failed: %s", e)
|
||||||
Reference in New Issue
Block a user