from __future__ import annotations from collections.abc import Sequence from dataclasses import dataclass from datetime import datetime, timezone from sqlalchemy import or_ from sqlalchemy.orm import Session from govoplan_core.core.dsar import ( DsarErasureActionRef, DsarExecutionResultRef, DsarRecordRef, DsarSubjectRef, dsar_capability_name, ) from govoplan_encryption.backend.db.models import ( ContentProtectionRecord, EncryptionKeyOperation, EncryptionVault, ProtectionMigration, RecoveryApproval, RecoveryCeremony, ) ENCRYPTION_DSAR_CAPABILITY = dsar_capability_name("encryption") _MAX_RECORDS = 5_000 _CONFLICT = object() @dataclass(frozen=True, slots=True) class _SubjectSelectors: account_id: str vault_id: str | None key_operation_id: str | None envelope_id: str | None migration_id: str | None recovery_id: str | None @property def narrowed(self) -> bool: return any( ( self.vault_id, self.key_operation_id, self.envelope_id, self.migration_id, self.recovery_id, ) ) class EncryptionDsarProvider: provider_id = "encryption" module_id = "encryption" def search_subject( self, session: object, *, tenant_id: str, subject: DsarSubjectRef, ) -> Sequence[DsarRecordRef]: db = _session(session) selectors = _subject_selectors(subject) if selectors is None: return () records: list[DsarRecordRef] = [] if not selectors.narrowed or selectors.vault_id: query = db.query(EncryptionVault).filter( EncryptionVault.tenant_id == tenant_id, or_( EncryptionVault.created_by == selectors.account_id, EncryptionVault.updated_by == selectors.account_id, ), ) if selectors.vault_id: query = query.filter(EncryptionVault.vault_id == selectors.vault_id) records.extend( _vault_attribution(row, selectors.account_id) for row in _limited( query, EncryptionVault.created_at, EncryptionVault.id, label="vault attribution", ) ) if not selectors.narrowed or selectors.vault_id or selectors.key_operation_id: query = db.query(EncryptionKeyOperation).filter( EncryptionKeyOperation.tenant_id == tenant_id, EncryptionKeyOperation.requested_by == selectors.account_id, ) if selectors.vault_id: query = query.filter( EncryptionKeyOperation.vault_id == selectors.vault_id ) if selectors.key_operation_id: query = query.filter( EncryptionKeyOperation.id == selectors.key_operation_id ) records.extend( _key_operation_attribution(row) for row in _limited( query, EncryptionKeyOperation.created_at, EncryptionKeyOperation.id, label="key-operation attribution", ) ) if not selectors.narrowed or selectors.vault_id or selectors.envelope_id: query = db.query(ContentProtectionRecord).filter( ContentProtectionRecord.tenant_id == tenant_id, ContentProtectionRecord.registered_by == selectors.account_id, ) if selectors.vault_id: query = query.filter( ContentProtectionRecord.vault_id == selectors.vault_id ) if selectors.envelope_id: query = query.filter( ContentProtectionRecord.envelope_id == selectors.envelope_id ) records.extend( _protection_attribution(row) for row in _limited( query, ContentProtectionRecord.created_at, ContentProtectionRecord.id, label="content-protection attribution", ) ) if not selectors.narrowed or selectors.vault_id or selectors.migration_id: query = db.query(ProtectionMigration).filter( ProtectionMigration.tenant_id == tenant_id, ProtectionMigration.requested_by == selectors.account_id, ) if selectors.vault_id: query = query.filter( ProtectionMigration.target_vault_id == selectors.vault_id ) if selectors.migration_id: query = query.filter(ProtectionMigration.id == selectors.migration_id) records.extend( _migration_attribution(row) for row in _limited( query, ProtectionMigration.created_at, ProtectionMigration.id, label="migration attribution", ) ) if not selectors.narrowed or selectors.vault_id or selectors.recovery_id: ceremonies = db.query(RecoveryCeremony).filter( RecoveryCeremony.tenant_id == tenant_id, RecoveryCeremony.requester_account_id == selectors.account_id, ) if selectors.vault_id: ceremonies = ceremonies.filter( RecoveryCeremony.vault_id == selectors.vault_id ) if selectors.recovery_id: ceremonies = ceremonies.filter( RecoveryCeremony.id == selectors.recovery_id ) records.extend( _recovery_request_attribution(row) for row in _limited( ceremonies, RecoveryCeremony.created_at, RecoveryCeremony.id, label="recovery-request attribution", ) ) approvals = db.query(RecoveryApproval).filter( RecoveryApproval.tenant_id == tenant_id, RecoveryApproval.approver_account_id == selectors.account_id, ) if selectors.recovery_id: approvals = approvals.filter( RecoveryApproval.recovery_id == selectors.recovery_id ) elif selectors.vault_id: approvals = approvals.filter( RecoveryApproval.recovery_id.in_( db.query(RecoveryCeremony.id).filter( RecoveryCeremony.tenant_id == tenant_id, RecoveryCeremony.vault_id == selectors.vault_id, ) ) ) records.extend( _recovery_approval_attribution(row) for row in _limited( approvals, RecoveryApproval.created_at, RecoveryApproval.id, label="recovery-approval attribution", ) ) if len(records) > _MAX_RECORDS: raise ValueError("Encryption DSAR result limit exceeded; narrow selectors.") return tuple( sorted(records, key=lambda item: (item.resource_type, item.resource_id)) ) def plan_erasure( self, session: object, *, tenant_id: str, subject: DsarSubjectRef, records: Sequence[DsarRecordRef], ) -> Sequence[DsarErasureActionRef]: del tenant_id _session(session) if _subject_selectors(subject) is None: raise ValueError("Encryption DSAR subject selectors conflict.") actions: list[DsarErasureActionRef] = [] for record in records: _validate_record(record) actions.append( DsarErasureActionRef( action_id=( f"encryption:retain:{record.resource_type}:{record.resource_id}" ), provider_id=self.provider_id, module_id=self.module_id, kind="retain", resource_type=record.resource_type, resource_id=record.resource_id, title=f"Retain {record.title}", rationale=( record.retention_reason or "Cryptographic custody evidence remains immutable." ), executable=False, ) ) return tuple(actions) def execute_erasure( self, session: object, *, tenant_id: str, subject: DsarSubjectRef, actions: Sequence[DsarErasureActionRef], request_id: str, ) -> Sequence[DsarExecutionResultRef]: del tenant_id _session(session) if _subject_selectors(subject) is None: raise ValueError("Encryption DSAR subject selectors conflict.") results: list[DsarExecutionResultRef] = [] for action in actions: _validate_action(action) if action.executable or action.kind != "retain": raise ValueError("Encryption DSAR publishes retain actions only.") results.append( DsarExecutionResultRef( action_id=action.action_id, status="blocked", summary=( "Cryptographic lifecycle and custody attribution remains " "immutable security evidence." ), evidence={"request_id": request_id}, ) ) return tuple(results) def _subject_selectors(subject: DsarSubjectRef) -> _SubjectSelectors | None: references = subject.external_references account = _coalesce( subject.account_id, references.get("encryption.account"), references.get("access.account"), ) values = { "vault_id": _coalesce( references.get("encryption.vault"), references.get("encryption.vault_id"), ), "key_operation_id": _coalesce( references.get("encryption.key_operation"), references.get("encryption.key_operation_id"), ), "envelope_id": _coalesce( references.get("encryption.envelope"), references.get("encryption.envelope_id"), ), "migration_id": _coalesce( references.get("encryption.migration"), references.get("encryption.migration_id"), ), "recovery_id": _coalesce( references.get("encryption.recovery"), references.get("encryption.recovery_id"), ), } if account is _CONFLICT or any(value is _CONFLICT for value in values.values()): return None account_id = _optional_string(account) if not account_id: return None return _SubjectSelectors( account_id=account_id, vault_id=_optional_string(values["vault_id"]), key_operation_id=_optional_string(values["key_operation_id"]), envelope_id=_optional_string(values["envelope_id"]), migration_id=_optional_string(values["migration_id"]), recovery_id=_optional_string(values["recovery_id"]), ) def _vault_attribution(row: EncryptionVault, account_id: str) -> DsarRecordRef: activities = [] if row.created_by == account_id: activities.append("created_vault") if row.updated_by == account_id: activities.append("updated_vault") return _record( resource_type="vault_actor_attribution", resource_id=row.id, title="Encryption vault actor attribution", data={ "vault_id": row.vault_id, "purpose": row.purpose[:255], "profile_kind": row.profile_kind, "scope_type": row.scope_type, "state": row.state, "revision": row.revision, "current_key_version": row.current_key_version, "recovery_quorum": row.recovery_quorum, "activities": activities, "created_at": _iso(row.created_at), "updated_at": _iso(row.updated_at), }, observed_at=row.updated_at, ) def _key_operation_attribution(row: EncryptionKeyOperation) -> DsarRecordRef: return _record( resource_type="key_operation_actor_attribution", resource_id=row.id, title="Encryption key-operation actor attribution", data={ "key_operation_id": row.id, "vault_id": row.vault_id, "key_version": row.key_version, "operation": row.operation, "state": row.state, "activity": "requested_key_operation", "created_at": _iso(row.created_at), "completed_at": _iso(row.completed_at), }, observed_at=row.completed_at or row.created_at, ) def _protection_attribution(row: ContentProtectionRecord) -> DsarRecordRef: return _record( resource_type="content_protection_actor_attribution", resource_id=row.id, title="Content-protection registration attribution", data={ "protection_record_id": row.id, "owner_module": row.owner_module, "resource_type": row.resource_type, "profile_kind": row.profile_kind, "state": row.state, "activity": "registered_content_protection", "created_at": _iso(row.created_at), "updated_at": _iso(row.updated_at), }, observed_at=row.updated_at, ) def _migration_attribution(row: ProtectionMigration) -> DsarRecordRef: return _record( resource_type="protection_migration_actor_attribution", resource_id=row.id, title="Protection-migration actor attribution", data={ "migration_id": row.id, "mode": row.mode, "state": row.state, "activity": "requested_protection_migration", "created_at": _iso(row.created_at), "completed_at": _iso(row.completed_at), }, observed_at=row.completed_at or row.created_at, ) def _recovery_request_attribution(row: RecoveryCeremony) -> DsarRecordRef: return _record( resource_type="recovery_request_actor_attribution", resource_id=row.id, title="Encryption recovery-request attribution", data={ "recovery_id": row.id, "vault_id": row.vault_id, "state": row.state, "quorum": row.quorum, "revision": row.revision, "expires_at": _iso(row.expires_at), "activity": "requested_recovery_ceremony", "created_at": _iso(row.created_at), "updated_at": _iso(row.updated_at), }, observed_at=row.updated_at, ) def _recovery_approval_attribution(row: RecoveryApproval) -> DsarRecordRef: return _record( resource_type="recovery_approval_actor_attribution", resource_id=row.id, title="Encryption recovery-approval attribution", data={ "recovery_approval_id": row.id, "recovery_id": row.recovery_id, "decision": row.decision, "activity": "decided_recovery_approval", "created_at": _iso(row.created_at), }, observed_at=row.created_at, ) def _record( *, resource_type: str, resource_id: str, title: str, data: dict[str, object], observed_at: datetime | None, ) -> DsarRecordRef: return DsarRecordRef( provider_id="encryption", module_id="encryption", resource_type=resource_type, resource_id=resource_id, category="cryptographic_custody_attribution", title=title, data=data, observed_at=_aware(observed_at), immutable_evidence=True, retention_reason=( "Cryptographic lifecycle and custody attribution is retained as immutable " "security and accountability evidence." ), ) def _limited(query, first, second, *, label: str): rows = query.order_by(first, second).limit(_MAX_RECORDS + 1).all() if len(rows) > _MAX_RECORDS: raise ValueError(f"Encryption DSAR {label} limit exceeded; narrow selectors.") return rows def _coalesce(*values: str | None) -> str | None | object: normalized = {str(value).strip() for value in values if str(value or "").strip()} if len(normalized) > 1: return _CONFLICT return next(iter(normalized), None) def _optional_string(value: object) -> str | None: return value if isinstance(value, str) and value else None def _iso(value: datetime | None) -> str | None: aware = _aware(value) return aware.isoformat() if aware else None def _aware(value: datetime | None) -> datetime | None: if value is None or value.tzinfo is not None: return value return value.replace(tzinfo=timezone.utc) def _session(value: object) -> Session: if not isinstance(value, Session): raise TypeError("Encryption DSAR requires a SQLAlchemy Session.") return value _RESOURCE_TYPES = { "vault_actor_attribution", "key_operation_actor_attribution", "content_protection_actor_attribution", "protection_migration_actor_attribution", "recovery_request_actor_attribution", "recovery_approval_actor_attribution", } def _validate_record(record: DsarRecordRef) -> None: if record.provider_id != "encryption" or record.module_id != "encryption": raise ValueError("Encryption DSAR cannot plan a foreign provider record.") if record.resource_type not in _RESOURCE_TYPES or not record.resource_id: raise ValueError("Encryption DSAR record identity is invalid.") def _validate_action(action: DsarErasureActionRef) -> None: if action.provider_id != "encryption" or action.module_id != "encryption": raise ValueError("Encryption DSAR cannot execute a foreign provider action.") if not action.action_id.startswith("encryption:retain:"): raise ValueError("Encryption DSAR action identity is invalid.") __all__ = ["ENCRYPTION_DSAR_CAPABILITY", "EncryptionDsarProvider"]