""" Audit Models - DSGVO-compliant logging NO PII in audit logs - only references and metadata Erlaubt: ref_id (truncated), content_type, size_bytes, ttl_hours Verboten: user_name, content, transcript, email """ from datetime import datetime from enum import Enum from typing import Optional, Dict, Any from pydantic import BaseModel, Field import uuid class AuditAction(str, Enum): """Audit action types.""" # Session actions SESSION_CREATED = "session_created" SESSION_CONNECTED = "session_connected" SESSION_CLOSED = "session_closed" SESSION_EXPIRED = "session_expired" # Audio actions (no content logged) AUDIO_RECEIVED = "audio_received" AUDIO_PROCESSED = "audio_processed" # Task actions TASK_CREATED = "task_created" TASK_QUEUED = "task_queued" TASK_STARTED = "task_started" TASK_COMPLETED = "task_completed" TASK_FAILED = "task_failed" TASK_EXPIRED = "task_expired" # Encryption actions ENCRYPTION_KEY_VERIFIED = "encryption_key_verified" ENCRYPTION_KEY_INVALID = "encryption_key_invalid" # Integration actions BREAKPILOT_CALLED = "breakpilot_called" PERSONAPLEX_CALLED = "personaplex_called" OLLAMA_CALLED = "ollama_called" # Security actions RATE_LIMIT_EXCEEDED = "rate_limit_exceeded" UNAUTHORIZED_ACCESS = "unauthorized_access" class AuditEntry(BaseModel): """ Audit log entry - DSGVO compliant. NO PII is stored - only truncated references and metadata. """ id: str = Field(default_factory=lambda: str(uuid.uuid4())) timestamp: datetime = Field(default_factory=datetime.utcnow) # Action identification action: AuditAction namespace_id_truncated: str = Field( ..., description="First 8 chars of namespace ID", max_length=8, ) # Reference IDs (truncated for privacy) session_id_truncated: Optional[str] = Field( default=None, description="First 8 chars of session ID", max_length=8, ) task_id_truncated: Optional[str] = Field( default=None, description="First 8 chars of task ID", max_length=8, ) # Metadata (no PII) content_type: Optional[str] = Field(default=None, description="Type of content processed") size_bytes: Optional[int] = Field(default=None, description="Size in bytes") duration_ms: Optional[int] = Field(default=None, description="Duration in milliseconds") ttl_hours: Optional[int] = Field(default=None, description="TTL in hours") # Technical metadata success: bool = Field(default=True) error_code: Optional[str] = Field(default=None) latency_ms: Optional[int] = Field(default=None) # Context (no PII) device_type: Optional[str] = Field(default=None) client_version: Optional[str] = Field(default=None) backend_used: Optional[str] = Field(default=None, description="personaplex, ollama, etc.") @staticmethod def truncate_id(full_id: str, length: int = 8) -> str: """Truncate ID for privacy.""" if not full_id: return "" return full_id[:length] class Config: json_schema_extra = { "example": { "id": "audit-123", "timestamp": "2026-01-26T10:30:00Z", "action": "task_completed", "namespace_id_truncated": "teacher-", "session_id_truncated": "session-", "task_id_truncated": "task-xyz", "content_type": "student_observation", "size_bytes": 256, "ttl_hours": 168, "success": True, "latency_ms": 1250, "backend_used": "ollama", } } class AuditCreate(BaseModel): """Request to create an audit entry.""" action: AuditAction namespace_id: str = Field(..., description="Will be truncated before storage") session_id: Optional[str] = Field(default=None, description="Will be truncated") task_id: Optional[str] = Field(default=None, description="Will be truncated") content_type: Optional[str] = Field(default=None) size_bytes: Optional[int] = Field(default=None) duration_ms: Optional[int] = Field(default=None) success: bool = Field(default=True) error_code: Optional[str] = Field(default=None) latency_ms: Optional[int] = Field(default=None) device_type: Optional[str] = Field(default=None) backend_used: Optional[str] = Field(default=None) def to_audit_entry(self) -> AuditEntry: """Convert to AuditEntry with truncated IDs.""" return AuditEntry( action=self.action, namespace_id_truncated=AuditEntry.truncate_id(self.namespace_id), session_id_truncated=AuditEntry.truncate_id(self.session_id) if self.session_id else None, task_id_truncated=AuditEntry.truncate_id(self.task_id) if self.task_id else None, content_type=self.content_type, size_bytes=self.size_bytes, duration_ms=self.duration_ms, success=self.success, error_code=self.error_code, latency_ms=self.latency_ms, device_type=self.device_type, backend_used=self.backend_used, )