feat: Cookie-Banner ↔ Backend Integration (DSR, Retention, Consent Proof)

Phase 1: Vendor sync from service registry (82+ services → banner vendors)
Phase 2: Category-based retention (marketing=90d, statistics=790d, not hardcoded 365d)
Phase 3: DSR ↔ Banner email linking (link-email, by-email, Art.17 erasure, Art.15/20 export)
Phase 4: Consent sync (Banner → Einwilligungen bridge)
Phase 6: Consent proof (SHA256 config hash + config_version in audit log, Art. 7(1) DSGVO)

New files:
- banner_dsr_service.py — email linking + DSR integration
- vendor_banner_sync.py — service registry → vendor configs
- migration 106 — linked_email, banner_config_hash, consent_version columns

Tests: 20+ new backend tests + 2 Playwright E2E test suites (API + UI)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-02 19:41:22 +02:00
parent c3f8e19e92
commit 44acd68c96
12 changed files with 1522 additions and 5 deletions
@@ -0,0 +1,127 @@
"""
Vendor-Banner Sync — maps the 82-service registry to banner vendor configs.
Automatically creates vendor entries in the cookie banner with correct
category assignment and legally required retention periods.
"""
import logging
import os
import uuid
logger = logging.getLogger(__name__)
# Service category → Banner category mapping
CATEGORY_MAP = {
"tracking": "statistics",
"heatmap": "statistics",
"tag_manager": "statistics",
"marketing": "marketing",
"social": "marketing",
"push": "marketing",
"crm": "marketing",
"chatbot": "functional",
"support": "functional",
"video": "functional",
"testing": "functional",
"cdn": "necessary",
"payment": "necessary",
"error_tracking": "necessary",
"accessibility": "necessary",
"cmp": "necessary",
"other": "functional",
}
# Legally required max retention per category (in days)
# Based on: DSGVO Art. 5(1)(e), CNIL guidelines, EDPB recommendations
RETENTION_DEFAULTS = {
"necessary": 365, # Session + functional = max 12 months
"statistics": 790, # Max 26 months (Google Analytics default)
"marketing": 90, # Max 90 days for retargeting
"functional": 365, # Max 12 months
}
# Specific service retention overrides
SERVICE_RETENTION = {
"google_analytics": 790, # 26 months (GA4 default)
"matomo": 790, # 26 months
"hotjar": 365, # 12 months
"facebook_pixel": 90, # 90 days (Meta default)
"google_ads": 90, # 90 days
"stripe": 0, # Session only (payment)
"paypal": 0, # Session only
"klarna": 0, # Session only
}
def get_banner_vendors_from_registry() -> list[dict]:
"""Convert service registry entries to banner vendor configs."""
from compliance.services.service_registry import SERVICE_REGISTRY
vendors = []
for pattern, meta in SERVICE_REGISTRY.items():
service_id = meta.get("id", "")
category = meta.get("category", "other")
banner_category = CATEGORY_MAP.get(category, "functional")
# Skip CMP — consent managers are not vendor entries
if service_id == "cmp":
continue
retention = SERVICE_RETENTION.get(service_id, RETENTION_DEFAULTS.get(banner_category, 365))
vendors.append({
"vendor_name": meta["name"],
"vendor_url": "", # Would need manual entry
"category_key": banner_category,
"description_de": f"{meta['name']} ({meta.get('provider', '')})",
"description_en": f"{meta['name']} ({meta.get('provider', '')})",
"cookie_names": [], # Service-specific, populated later
"retention_days": retention,
"is_active": True,
"country": meta.get("country", ""),
"eu_adequate": meta.get("eu_adequate", False),
"requires_consent": meta.get("requires_consent", True),
"legal_ref": meta.get("legal_ref", ""),
"service_id": service_id,
})
logger.info("Generated %d banner vendors from service registry", len(vendors))
return vendors
async def sync_vendors_to_site(pool, site_config_id: str, tenant_id: str) -> dict:
"""Sync service registry vendors to a site's banner vendor configs."""
vendors = get_banner_vendors_from_registry()
created = 0
updated = 0
async with pool.acquire() as conn:
for v in vendors:
# Check if vendor already exists for this site
existing = await conn.fetchrow("""
SELECT id FROM compliance_banner_vendor_configs
WHERE site_config_id = $1 AND vendor_name = $2
""", uuid.UUID(site_config_id), v["vendor_name"])
if existing:
await conn.execute("""
UPDATE compliance_banner_vendor_configs
SET category_key = $1, retention_days = $2, is_active = $3
WHERE id = $4
""", v["category_key"], v["retention_days"], v["is_active"], existing["id"])
updated += 1
else:
import json
await conn.execute("""
INSERT INTO compliance_banner_vendor_configs
(site_config_id, vendor_name, category_key, description_de,
description_en, cookie_names, retention_days, is_active)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
""", uuid.UUID(site_config_id), v["vendor_name"], v["category_key"],
v["description_de"], v["description_en"],
json.dumps(v["cookie_names"]), v["retention_days"], v["is_active"])
created += 1
logger.info("Synced vendors to site %s: %d created, %d updated", site_config_id, created, updated)
return {"created": created, "updated": updated, "total": len(vendors)}