aee0f09db8
- Datafabrik: Dockerfile fix, agentorkestrering fungerar - Vision: Identify-modell, FAISS, OCR alla testade - API: Alla 7 integrationstester passerade - Upplösare: Entitetsupplösning verifierad
181 lines
6.4 KiB
Python
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
|