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
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user