""" Audit Endpoints - ServiceManagerWeb Endpoints para consulta de logs de auditoría. Solo accesible por roles: ADMIN, SUPPORT_MANAGER, AUDITOR """ from fastapi import APIRouter, Depends, HTTPException, status, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, and_, or_, desc from sqlalchemy.orm import selectinload from typing import Optional, List from datetime import datetime, timedelta import uuid import structlog from app.core.database import get_db from app.api.deps import get_current_user, get_current_tenant from app.models.user import User, UserRole from app.models.tenant import Tenant from app.models.audit import AuditLog from app.api.schemas.audit import ( AuditLogResponse, AuditLogListResponse, AuditLogFilters, AuditLogStats ) router = APIRouter() logger = structlog.get_logger(__name__) def require_auditor_role(current_user: User = Depends(get_current_user)) -> User: """ Dependency que verifica que el usuario tenga rol de auditor. Solo ADMIN, SUPPORT_MANAGER y AUDITOR pueden ver logs de auditoría. """ allowed_roles = [UserRole.ADMIN, UserRole.SUPPORT_MANAGER, UserRole.AUDITOR] if current_user.role not in allowed_roles: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Solo usuarios con rol ADMIN, SUPPORT_MANAGER o AUDITOR pueden acceder a logs de auditoría" ) return current_user @router.get("/", response_model=AuditLogListResponse) async def get_audit_logs( # Paginación page: int = Query(default=1, ge=1, description="Número de página"), per_page: int = Query(default=50, ge=1, le=100, description="Registros por página"), # Filtros user_id: Optional[uuid.UUID] = Query(None, description="Filtrar por usuario"), action: Optional[str] = Query(None, description="Filtrar por acción"), resource_type: Optional[str] = Query(None, description="Filtrar por tipo de recurso"), resource_id: Optional[uuid.UUID] = Query(None, description="Filtrar por ID de recurso"), date_from: Optional[datetime] = Query(None, description="Fecha desde"), date_to: Optional[datetime] = Query(None, description="Fecha hasta"), search: Optional[str] = Query(None, description="Búsqueda en acción o email"), # Dependencies current_user: User = Depends(require_auditor_role), current_tenant: Tenant = Depends(get_current_tenant), db: AsyncSession = Depends(get_db) ): """ Obtener logs de auditoría con filtros y paginación. **Permisos**: ADMIN, SUPPORT_MANAGER, AUDITOR **Filtros disponibles**: - `user_id`: Acciones de un usuario específico - `action`: Tipo de acción (ej: "ticket.create") - `resource_type`: Tipo de recurso (ej: "ticket") - `resource_id`: ID de recurso específico - `date_from`, `date_to`: Rango de fechas - `search`: Búsqueda en acciones **Retorna**: Lista paginada de audit logs """ logger.info( "Fetching audit logs", user_id=str(current_user.id), tenant_id=str(current_tenant.id), filters={ "user_id": str(user_id) if user_id else None, "action": action, "resource_type": resource_type, "page": page } ) # Query base - solo logs del tenant actual # Usar selectinload para cargar la relación user (eager loading para async) query = ( select(AuditLog) .where(AuditLog.tenant_id == current_tenant.id) .options(selectinload(AuditLog.user)) ) # Aplicar filtros if user_id: query = query.where(AuditLog.user_id == user_id) if action: query = query.where(AuditLog.action == action) if resource_type: query = query.where(AuditLog.resource_type == resource_type) if resource_id: query = query.where(AuditLog.resource_id == resource_id) if date_from: query = query.where(AuditLog.created_at >= date_from) if date_to: # Agregar 1 día para incluir todo el día date_to_end = date_to + timedelta(days=1) query = query.where(AuditLog.created_at < date_to_end) if search: # Búsqueda en action search_filter = AuditLog.action.ilike(f"%{search}%") query = query.where(search_filter) # Ordenar por fecha descendente (más recientes primero) query = query.order_by(desc(AuditLog.created_at)) # Contar total antes de paginar count_query = select(func.count()).select_from(query.subquery()) total_result = await db.execute(count_query) total = total_result.scalar() or 0 # Aplicar paginación offset = (page - 1) * per_page query = query.offset(offset).limit(per_page) # Ejecutar query result = await db.execute(query) logs = result.scalars().all() # Calcular total de páginas total_pages = (total + per_page - 1) // per_page # Convertir a response schema (agregar info del usuario) logs_response = [] for log in logs: log_dict = { "id": log.id, "tenant_id": log.tenant_id, "user_id": log.user_id, "action": log.action, "resource_type": log.resource_type, "resource_id": log.resource_id, "ip_address": str(log.ip_address) if log.ip_address else None, "user_agent": log.user_agent, "correlation_id": log.correlation_id, "old_values": log.old_values, "new_values": log.new_values, "metadata": log.extra_metadata, "created_at": log.created_at, "action_display": log.action_display, "user_email": None, "user_name": None } # Agregar info del usuario si existe if log.user: log_dict["user_email"] = log.user.email log_dict["user_name"] = log.user.full_name log_dict["user_role"] = log.user.role.value if hasattr(log.user.role, 'value') else str(log.user.role) logs_response.append(AuditLogResponse(**log_dict)) return AuditLogListResponse( logs=logs_response, total=total, page=page, per_page=per_page, total_pages=total_pages ) @router.get("/stats", response_model=AuditLogStats) async def get_audit_stats( current_user: User = Depends(require_auditor_role), current_tenant: Tenant = Depends(get_current_tenant), db: AsyncSession = Depends(get_db) ): """ Obtener estadísticas de auditoría del tenant. **Permisos**: ADMIN, SUPPORT_MANAGER, AUDITOR **Retorna**: Estadísticas de actividad """ logger.info( "Fetching audit stats", user_id=str(current_user.id), tenant_id=str(current_tenant.id) ) now = datetime.utcnow() # Total de acciones total_query = select(func.count()).select_from(AuditLog).where( AuditLog.tenant_id == current_tenant.id ) total_result = await db.execute(total_query) total_actions = total_result.scalar() or 0 # Acciones hoy (últimas 24 horas) today_start = now - timedelta(days=1) today_query = select(func.count()).select_from(AuditLog).where( and_( AuditLog.tenant_id == current_tenant.id, AuditLog.created_at >= today_start ) ) today_result = await db.execute(today_query) actions_today = today_result.scalar() or 0 # Acciones esta semana (últimos 7 días) week_start = now - timedelta(days=7) week_query = select(func.count()).select_from(AuditLog).where( and_( AuditLog.tenant_id == current_tenant.id, AuditLog.created_at >= week_start ) ) week_result = await db.execute(week_query) actions_this_week = week_result.scalar() or 0 # Top 5 acciones más frecuentes top_actions_query = select( AuditLog.action, func.count(AuditLog.id).label('count') ).where( AuditLog.tenant_id == current_tenant.id ).group_by( AuditLog.action ).order_by( desc('count') ).limit(5) top_actions_result = await db.execute(top_actions_query) top_actions = {row.action: row.count for row in top_actions_result} # Acciones por tipo de recurso by_resource_query = select( AuditLog.resource_type, func.count(AuditLog.id).label('count') ).where( AuditLog.tenant_id == current_tenant.id ).group_by( AuditLog.resource_type ).order_by( desc('count') ) by_resource_result = await db.execute(by_resource_query) by_resource_type = {row.resource_type: row.count for row in by_resource_result} # Top usuarios (con join a users para obtener nombres) top_users_query = select( User.email, func.count(AuditLog.id).label('count') ).join( User, AuditLog.user_id == User.id ).where( AuditLog.tenant_id == current_tenant.id ).group_by( User.email ).order_by( desc('count') ).limit(5) top_users_result = await db.execute(top_users_query) top_users = {row.email: row.count for row in top_users_result} # Acciones críticas hoy (delete, update sensibles, etc.) critical_actions_query = select(func.count()).select_from(AuditLog).where( and_( AuditLog.tenant_id == current_tenant.id, AuditLog.created_at >= today_start, or_( AuditLog.action.like('%.delete'), AuditLog.action.like('user.update'), AuditLog.action.like('%.assign'), AuditLog.action.in_(['user.login_failed', 'user.logout']) ) ) ) critical_result = await db.execute(critical_actions_query) critical_actions_today = critical_result.scalar() or 0 return AuditLogStats( total_actions=total_actions, actions_today=actions_today, actions_this_week=actions_this_week, critical_actions_today=critical_actions_today, top_actions=top_actions, top_users=top_users, by_resource_type=by_resource_type ) @router.get("/{log_id}", response_model=AuditLogResponse) async def get_audit_log_detail( log_id: uuid.UUID, current_user: User = Depends(require_auditor_role), current_tenant: Tenant = Depends(get_current_tenant), db: AsyncSession = Depends(get_db) ): """ Obtener detalle de un audit log específico. **Permisos**: ADMIN, SUPPORT_MANAGER, AUDITOR **Retorna**: Detalle completo del audit log """ # Buscar el log query = select(AuditLog).where( and_( AuditLog.id == log_id, AuditLog.tenant_id == current_tenant.id ) ) result = await db.execute(query) log = result.scalar_one_or_none() if not log: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Audit log {log_id} no encontrado" ) # Convertir a response log_dict = { "id": log.id, "tenant_id": log.tenant_id, "user_id": log.user_id, "action": log.action, "resource_type": log.resource_type, "resource_id": log.resource_id, "ip_address": str(log.ip_address) if log.ip_address else None, "user_agent": log.user_agent, "correlation_id": log.correlation_id, "old_values": log.old_values, "new_values": log.new_values, "metadata": log.extra_metadata, "created_at": log.created_at, "action_display": log.action_display, "user_email": None, "user_name": None } if log.user: log_dict["user_email"] = log.user.email log_dict["user_name"] = log.user.full_name return AuditLogResponse(**log_dict)