aegislm / audit.py
ACA050's picture
Upload folder using huggingface_hub
7343c19 verified
Raw
History Blame Contribute Delete
13.7 kB
"""
Audit Logging Service
Provides audit logging functionality for security and compliance.
Immutable entries - append only, no updates allowed.
"""
import uuid
from datetime import datetime
from typing import Any, Dict, Optional
from enum import Enum
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from backend.db.models import AuditLog, User, Tenant
from backend.db.session import get_db_session
class AuditAction(str, Enum):
"""Standard audit action types."""
# Job actions
JOB_CREATED = "JOB_CREATED"
JOB_STARTED = "JOB_STARTED"
JOB_COMPLETED = "JOB_COMPLETED"
JOB_FAILED = "JOB_FAILED"
JOB_CANCELLED = "JOB_CANCELLED"
JOB_DELETED = "JOB_DELETED"
# User actions
USER_CREATED = "USER_CREATED"
USER_UPDATED = "USER_UPDATED"
USER_DELETED = "USER_DELETED"
USER_LOGIN = "USER_LOGIN"
USER_LOGOUT = "USER_LOGOUT"
ROLE_CHANGED = "ROLE_CHANGED"
# API Key actions
API_KEY_CREATED = "API_KEY_CREATED"
API_KEY_DELETED = "API_KEY_DELETED"
API_KEY_ROTATED = "API_KEY_ROTATED"
# Release actions
RELEASE_APPROVED = "RELEASE_APPROVED"
RELEASE_REJECTED = "RELEASE_REJECTED"
RELEASE_BLOCKED = "RELEASE_BLOCKED"
# Report actions
REPORT_CREATED = "REPORT_CREATED"
REPORT_EXPORTED = "REPORT_EXPORTED"
REPORT_DELETED = "REPORT_DELETED"
# Monitoring actions
ALERT_CREATED = "ALERT_CREATED"
ALERT_RESOLVED = "ALERT_RESOLVED"
BASELINE_UPDATED = "BASELINE_UPDATED"
# Tenant actions
TENANT_CREATED = "TENANT_CREATED"
TENANT_UPDATED = "TENANT_UPDATED"
TENANT_DEACTIVATED = "TENANT_DEACTIVATED"
# Model/Dataset actions
MODEL_REGISTERED = "MODEL_REGISTERED"
DATASET_REGISTERED = "DATASET_REGISTERED"
class ResourceType(str, Enum):
"""Resource types for audit logging."""
JOB = "job"
USER = "user"
API_KEY = "api_key"
RELEASE = "release"
REPORT = "report"
ALERT = "alert"
TENANT = "tenant"
MODEL = "model"
DATASET = "dataset"
class AuditLogger:
"""
Audit logging service for tracking user actions.
All entries are immutable - append only, no updates allowed.
"""
def __init__(self, db_session: AsyncSession):
self.db = db_session
async def log(
self,
tenant_id: uuid.UUID,
user_id: uuid.UUID,
action: AuditAction,
resource_type: ResourceType,
resource_id: Optional[uuid.UUID] = None,
metadata: Optional[Dict[str, Any]] = None,
request_id: Optional[str] = None,
ip_address: Optional[str] = None,
status: str = "success",
error_message: Optional[str] = None,
) -> AuditLog:
"""
Create an audit log entry.
Args:
tenant_id: Tenant ID
user_id: User who performed the action
action: Action performed
resource_type: Type of resource affected
resource_id: ID of the affected resource
metadata: Additional action metadata
request_id: Request ID for tracing
ip_address: Client IP address
status: Action status (success, failure, pending)
error_message: Error message if action failed
Returns:
Created AuditLog entry
"""
audit_log = AuditLog(
tenant_id=tenant_id,
user_id=user_id,
action=action.value,
resource_type=resource_type.value,
resource_id=resource_id,
metadata=metadata,
request_id=request_id,
ip_address=ip_address,
status=status,
error_message=error_message,
timestamp=datetime.utcnow(),
)
self.db.add(audit_log)
await self.db.commit()
await self.db.refresh(audit_log)
return audit_log
async def log_job_created(
self,
tenant_id: uuid.UUID,
user_id: uuid.UUID,
job_id: uuid.UUID,
metadata: Optional[Dict[str, Any]] = None,
**kwargs,
) -> AuditLog:
"""Log job creation."""
return await self.log(
tenant_id=tenant_id,
user_id=user_id,
action=AuditAction.JOB_CREATED,
resource_type=ResourceType.JOB,
resource_id=job_id,
metadata=metadata,
**kwargs,
)
async def log_job_completed(
self,
tenant_id: uuid.UUID,
user_id: uuid.UUID,
job_id: uuid.UUID,
metadata: Optional[Dict[str, Any]] = None,
**kwargs,
) -> AuditLog:
"""Log job completion."""
return await self.log(
tenant_id=tenant_id,
user_id=user_id,
action=AuditAction.JOB_COMPLETED,
resource_type=ResourceType.JOB,
resource_id=job_id,
metadata=metadata,
**kwargs,
)
async def log_job_failed(
self,
tenant_id: uuid.UUID,
user_id: uuid.UUID,
job_id: uuid.UUID,
error_message: str,
metadata: Optional[Dict[str, Any]] = None,
**kwargs,
) -> AuditLog:
"""Log job failure."""
return await self.log(
tenant_id=tenant_id,
user_id=user_id,
action=AuditAction.JOB_FAILED,
resource_type=ResourceType.JOB,
resource_id=job_id,
metadata=metadata,
status="failure",
error_message=error_message,
**kwargs,
)
async def log_api_key_created(
self,
tenant_id: uuid.UUID,
user_id: uuid.UUID,
api_key_id: uuid.UUID,
metadata: Optional[Dict[str, Any]] = None,
**kwargs,
) -> AuditLog:
"""Log API key creation."""
return await self.log(
tenant_id=tenant_id,
user_id=user_id,
action=AuditAction.API_KEY_CREATED,
resource_type=ResourceType.API_KEY,
resource_id=api_key_id,
metadata=metadata,
**kwargs,
)
async def log_release_approved(
self,
tenant_id: uuid.UUID,
user_id: uuid.UUID,
release_id: uuid.UUID,
metadata: Optional[Dict[str, Any]] = None,
**kwargs,
) -> AuditLog:
"""Log release approval."""
return await self.log(
tenant_id=tenant_id,
user_id=user_id,
action=AuditAction.RELEASE_APPROVED,
resource_type=ResourceType.RELEASE,
resource_id=release_id,
metadata=metadata,
**kwargs,
)
async def log_user_login(
self,
tenant_id: uuid.UUID,
user_id: uuid.UUID,
metadata: Optional[Dict[str, Any]] = None,
**kwargs,
) -> AuditLog:
"""Log user login."""
return await self.log(
tenant_id=tenant_id,
user_id=user_id,
action=AuditAction.USER_LOGIN,
resource_type=ResourceType.USER,
resource_id=user_id,
metadata=metadata,
**kwargs,
)
async def log_role_changed(
self,
tenant_id: uuid.UUID,
user_id: uuid.UUID,
target_user_id: uuid.UUID,
old_role: str,
new_role: str,
**kwargs,
) -> AuditLog:
"""Log role change."""
return await self.log(
tenant_id=tenant_id,
user_id=user_id,
action=AuditAction.ROLE_CHANGED,
resource_type=ResourceType.USER,
resource_id=target_user_id,
metadata={"old_role": old_role, "new_role": new_role},
**kwargs,
)
async def log_report_exported(
self,
tenant_id: uuid.UUID,
user_id: uuid.UUID,
report_id: uuid.UUID,
metadata: Optional[Dict[str, Any]] = None,
**kwargs,
) -> AuditLog:
"""Log report export."""
return await self.log(
tenant_id=tenant_id,
user_id=user_id,
action=AuditAction.REPORT_EXPORTED,
resource_type=ResourceType.REPORT,
resource_id=report_id,
metadata=metadata,
**kwargs,
)
async def get_tenant_audit_logs(
self,
tenant_id: uuid.UUID,
limit: int = 100,
offset: int = 0,
action: Optional[AuditAction] = None,
user_id: Optional[uuid.UUID] = None,
resource_type: Optional[ResourceType] = None,
) -> list[AuditLog]:
"""
Get audit logs for a tenant.
Args:
tenant_id: Tenant ID
limit: Maximum number of entries to return
offset: Number of entries to skip
action: Filter by action type
user_id: Filter by user
resource_type: Filter by resource type
Returns:
List of audit log entries
"""
query = select(AuditLog).where(
AuditLog.tenant_id == tenant_id
)
if action:
query = query.where(AuditLog.action == action.value)
if user_id:
query = query.where(AuditLog.user_id == user_id)
if resource_type:
query = query.where(AuditLog.resource_type == resource_type.value)
query = (
query
.order_by(AuditLog.timestamp.desc())
.limit(limit)
.offset(offset)
)
result = await self.db.execute(query)
return list(result.scalars().all())
async def create_audit_log(
tenant_id: uuid.UUID,
user_id: uuid.UUID,
action: AuditAction,
resource_type: ResourceType,
resource_id: Optional[uuid.UUID] = None,
metadata: Optional[Dict[str, Any]] = None,
request_id: Optional[str] = None,
ip_address: Optional[str] = None,
status: str = "success",
error_message: Optional[str] = None,
) -> AuditLog:
"""
Convenience function to create an audit log entry.
Creates a new database session for the operation.
"""
async with get_db_session() as session:
logger = AuditLogger(session)
return await logger.log(
tenant_id=tenant_id,
user_id=user_id,
action=action,
resource_type=resource_type,
resource_id=resource_id,
metadata=metadata,
request_id=request_id,
ip_address=ip_address,
status=status,
error_message=error_message,
)
# =============================================================================
# Audit context manager for automatic logging
# =============================================================================
class AuditContext:
"""
Context manager for automatic audit logging.
Usage:
async with AuditContext(
tenant_id=tenant_id,
user_id=user_id,
action=AuditAction.JOB_CREATED,
resource_type=ResourceType.JOB,
resource_id=job_id,
) as audit:
# Do work
await process_job(job_id)
# Success automatically logged on exit
"""
def __init__(
self,
tenant_id: uuid.UUID,
user_id: uuid.UUID,
action: AuditAction,
resource_type: ResourceType,
resource_id: Optional[uuid.UUID] = None,
metadata: Optional[Dict[str, Any]] = None,
request_id: Optional[str] = None,
ip_address: Optional[str] = None,
):
self.tenant_id = tenant_id
self.user_id = user_id
self.action = action
self.resource_type = resource_type
self.resource_id = resource_id
self.metadata = metadata or {}
self.request_id = request_id
self.ip_address = ip_address
self.status = "success"
self.error_message = None
self._audit_log: Optional[AuditLog] = None
async def __aenter__(self) -> "AuditContext":
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
self.status = "failure"
self.error_message = str(exc_val) if exc_val else "Unknown error"
# Log the action
async with get_db_session() as session:
logger = AuditLogger(session)
self._audit_log = await logger.log(
tenant_id=self.tenant_id,
user_id=self.user_id,
action=self.action,
resource_type=self.resource_type,
resource_id=self.resource_id,
metadata=self.metadata,
request_id=self.request_id,
ip_address=self.ip_address,
status=self.status,
error_message=self.error_message,
)
return False # Don't suppress exceptions
@property
def audit_log(self) -> Optional[AuditLog]:
"""Get the created audit log entry."""
return self._audit_log