44acd68c96
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>
128 lines
4.8 KiB
Python
128 lines
4.8 KiB
Python
"""
|
|
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)}
|