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:
@@ -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)}
|
||||
Reference in New Issue
Block a user