Files
breakpilot-lehrer/voice-service/models/audit.py
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

150 lines
5.1 KiB
Python

"""
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,
)