Files
boc/landvex-admin-backend/app/services/audit_service.py
T
Bernt aee0f09db8 landvex: Fixar och tester klara för alla komponenter
- Datafabrik: Dockerfile fix, agentorkestrering fungerar
- Vision: Identify-modell, FAISS, OCR alla testade
- API: Alla 7 integrationstester passerade
- Upplösare: Entitetsupplösning verifierad
2026-07-05 06:41:32 +00:00

181 lines
6.4 KiB
Python

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