""" Audit Service för LandveX Admin Backend. Hanterar audit-logging för alla systemändringar. """ from datetime import datetime, timezone from typing import Optional, Dict, Any, List from sqlalchemy import select, desc, func from sqlalchemy.ext.asyncio import AsyncSession from app.config import get_settings from app.models import AuditLog, AuditAction settings = get_settings() class AuditService: """Service för att hantera audit logs.""" def __init__(self, session: AsyncSession): self.session = session async def log( self, action: AuditAction, entity_type: str, entity_id: str, actor_id: Optional[str] = None, actor_type: str = "user", actor_ip: Optional[str] = None, actor_user_agent: Optional[str] = None, previous_values: Optional[Dict[str, Any]] = None, new_values: Optional[Dict[str, Any]] = None, changed_fields: Optional[List[str]] = None, request_id: Optional[str] = None, request_method: Optional[str] = None, request_path: Optional[str] = None, request_body: Optional[str] = None, success: bool = True, error_message: Optional[str] = None, tenant_id: Optional[str] = None, tenant_schema: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, ) -> AuditLog: """Skapa en audit log entry.""" sanitized_body = None if request_body: sanitized_body = self._sanitize_request_body(request_body) audit_entry = AuditLog( action=action, entity_type=entity_type, entity_id=entity_id, actor_id=actor_id or "system", actor_type=actor_type, actor_ip=actor_ip, actor_user_agent=actor_user_agent, previous_values=previous_values, new_values=new_values, changed_fields=changed_fields, request_id=request_id, request_method=request_method, request_path=request_path, request_body=sanitized_body, success="Y" if success else "N", error_message=error_message, tenant_id=tenant_id, tenant_schema=tenant_schema, metadata_json=metadata, ) self.session.add(audit_entry) await self.session.flush() return audit_entry async def get_by_entity( self, entity_type: str, entity_id: str, limit: int = 100, offset: int = 0 ) -> tuple[List[AuditLog], int]: query = ( select(AuditLog) .where(AuditLog.entity_type == entity_type) .where(AuditLog.entity_id == entity_id) .order_by(desc(AuditLog.timestamp)) .offset(offset) .limit(limit) ) count_query = ( select(func.count(AuditLog.id)) .where(AuditLog.entity_type == entity_type) .where(AuditLog.entity_id == entity_id) ) result = await self.session.execute(query) count_result = await self.session.execute(count_query) return list(result.scalars().all()), count_result.scalar() async def get_by_tenant( self, tenant_id: str, limit: int = 100, offset: int = 0 ) -> tuple[List[AuditLog], int]: query = ( select(AuditLog) .where(AuditLog.tenant_id == tenant_id) .order_by(desc(AuditLog.timestamp)) .offset(offset) .limit(limit) ) count_query = ( select(func.count(AuditLog.id)) .where(AuditLog.tenant_id == tenant_id) ) result = await self.session.execute(query) count_result = await self.session.execute(count_query) return list(result.scalars().all()), count_result.scalar() async def get_by_actor( self, actor_id: str, limit: int = 100, offset: int = 0 ) -> tuple[List[AuditLog], int]: query = ( select(AuditLog) .where(AuditLog.actor_id == actor_id) .order_by(desc(AuditLog.timestamp)) .offset(offset) .limit(limit) ) count_query = ( select(func.count(AuditLog.id)) .where(AuditLog.actor_id == actor_id) ) result = await self.session.execute(query) count_result = await self.session.execute(count_query) return list(result.scalars().all()), count_result.scalar() async def get_recent(self, limit: int = 50, action: Optional[AuditAction] = None) -> List[AuditLog]: query = select(AuditLog).order_by(desc(AuditLog.timestamp)) if action: query = query.where(AuditLog.action == action) query = query.limit(limit) result = await self.session.execute(query) return list(result.scalars().all()) async def cleanup_old_logs(self, retention_days: Optional[int] = None) -> int: retention = retention_days or settings.AUDIT_RETENTION_DAYS cutoff = datetime.now(timezone.utc) - __import__('datetime').timedelta(days=retention) count_query = select(func.count(AuditLog.id)).where(AuditLog.timestamp < cutoff) result = await self.session.execute(count_query) return result.scalar() def _sanitize_request_body(self, body: str) -> Optional[str]: import json sensitive = { "password", "secret", "token", "api_key", "apikey", "authorization", "credit_card", "ssn", "personal_number" } try: data = json.loads(body) if isinstance(data, dict): sanitized = self._redact_sensitive(data, sensitive) return json.dumps(sanitized) return body[:1000] except json.JSONDecodeError: return body[:1000] def _redact_sensitive(self, data: Dict[str, Any], sensitive: set) -> Dict[str, Any]: result = {} for key, value in data.items(): if any(field in key.lower() for field in sensitive): result[key] = "***REDACTED***" elif isinstance(value, dict): result[key] = self._redact_sensitive(value, sensitive) elif isinstance(value, list): result[key] = [ self._redact_sensitive(item, sensitive) if isinstance(item, dict) else item for item in value ] else: result[key] = value return result