PurePolyglot / src /consent_policy.py
github-actions[bot]
Automated deployment to Hugging Face
946166e
Raw History Blame Contribute Delete
4.85 kB
"""Canonical, versioned policy registry for crowdsourcing consent receipts."""
from __future__ import annotations
import hashlib
import json
from functools import lru_cache
from pathlib import Path
from typing import Any
POLICY_ROOT = Path(__file__).resolve().parents[1] / "consent_policies"
MANIFEST_PATH = POLICY_ROOT / "manifest.json"
@lru_cache(maxsize=1)
def _load_manifest() -> dict[str, Any]:
with MANIFEST_PATH.open(encoding="utf-8") as manifest_file:
return json.load(manifest_file)
def get_consent_policy(
consent_version: str | None = None,
consent_locale: str = "en",
) -> dict[str, Any]:
"""Return one verified policy plus the exact statement shown to participants."""
manifest = _load_manifest()
version = consent_version or str(manifest["current_version"])
try:
policy = manifest["policies"][version]
locale_policy = policy["locales"][consent_locale]
except KeyError as exc:
raise ValueError("unknown consent policy version or locale") from exc
canonical_text = str(locale_policy["canonical_text"])
digest = hashlib.sha256(canonical_text.encode("utf-8")).hexdigest()
expected_digest = str(locale_policy["consent_text_sha256"])
if digest != expected_digest:
raise RuntimeError(
f"consent policy hash mismatch for {version}/{consent_locale}"
)
canonical_payload = json.loads(canonical_text)
expected_payload = {
"consent_version": version,
"privacy_notice_version": policy["privacy_notice_version"],
"retention_policy_version": policy["retention_policy_version"],
"withdrawal_policy_version": policy["withdrawal_policy_version"],
"dataset_license": policy["dataset_license"],
"consent_surface": policy["consent_surface"],
"research_purposes": policy["research_purposes"],
"data_recipient_categories": policy["data_recipient_categories"],
"notice": policy["notice"],
}
if canonical_payload != expected_payload:
raise RuntimeError(
f"canonical consent text differs from manifest fields for "
f"{version}/{consent_locale}"
)
return {
"consent_version": version,
"consent_locale": consent_locale,
"consent_text_sha256": digest,
"canonical_text": canonical_text,
**{
key: value
for key, value in policy.items()
if key not in {"locales", "allowed_modalities"}
},
"allowed_modalities": list(policy["allowed_modalities"]),
}
def get_current_consent_policy(consent_locale: str = "en") -> dict[str, Any]:
return get_consent_policy(None, consent_locale)
def validate_consent_policy_payload(payload: dict[str, Any]) -> None:
"""Reject receipts whose disclosed policy differs from the registry."""
policy = get_consent_policy(
str(payload.get("consent_version", "")),
str(payload.get("consent_locale", "")),
)
scalar_fields = (
"consent_surface",
"privacy_notice_version",
"dataset_license",
"retention_policy_version",
"withdrawal_policy_version",
"data_controller_name",
"data_controller_contact",
"consent_text_sha256",
)
for field in scalar_fields:
if payload.get(field) != policy[field]:
raise ValueError(f"{field} does not match the registered consent policy")
for field in ("research_purposes", "data_recipient_categories"):
supplied = payload.get(field, [])
if not isinstance(supplied, list) or not supplied or not all(
isinstance(item, str) and item.strip() for item in supplied
):
raise ValueError(f"{field} must contain known non-empty strings")
if sorted(supplied) != sorted(policy[field]):
raise ValueError(f"{field} does not match the registered consent policy")
supplied_modalities = payload.get("permitted_modalities", [])
if not isinstance(supplied_modalities, list) or not supplied_modalities or not all(
isinstance(item, str) and item.strip() for item in supplied_modalities
):
raise ValueError("permitted_modalities must contain known non-empty strings")
permitted_modalities = set(supplied_modalities)
if not permitted_modalities.issubset(set(policy["allowed_modalities"])):
raise ValueError("permitted_modalities contains a modality not in the policy")
if payload.get("raw_audio_consent"):
if not policy.get("raw_audio_collection_allowed", False):
raise ValueError("this consent policy does not authorize raw audio")
if not permitted_modalities.intersection({"speech", "multimodal"}):
raise ValueError(
"raw_audio_consent requires speech or multimodal consent"
)