v1.5.1.2: Integración completa del sistema de auditoría
- Agregado módulo de auditoría con AuditLog model - Implementado endpoint /api/v1/audit para consulta de logs - Integrado audit_service para registro automático de acciones - Agregado token_service para gestión de refresh tokens - Frontend: Vista de auditoría en panel interno - Actualizada versión en pyproject.toml y package.json - Sistema de auditoría completamente funcional y testeado
This commit is contained in:
311
backend/app/services/audit_service.py
Normal file
311
backend/app/services/audit_service.py
Normal file
@@ -0,0 +1,311 @@
|
||||
"""
|
||||
Audit Service - ServiceManagerWeb
|
||||
|
||||
Funciones helper para facilitar el registro de auditoría.
|
||||
Simplifica el proceso de logging en toda la aplicaci├│n.
|
||||
"""
|
||||
|
||||
from typing import Optional, Dict, Any
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from fastapi import Request
|
||||
import uuid
|
||||
import structlog
|
||||
|
||||
from app.models.audit import AuditLog
|
||||
from app.models.user import User
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class AuditService:
|
||||
"""
|
||||
Servicio centralizado para registro de auditoría.
|
||||
|
||||
Uso básico:
|
||||
await AuditService.log(
|
||||
db=db,
|
||||
tenant_id=tenant.id,
|
||||
user_id=current_user.id,
|
||||
action="ticket.create",
|
||||
resource_type="ticket",
|
||||
resource_id=new_ticket.id,
|
||||
new_values={"subject": "...", "status": "NEW"}
|
||||
)
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
async def log(
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
action: str,
|
||||
resource_type: str,
|
||||
resource_id: Optional[uuid.UUID] = None,
|
||||
user_id: Optional[uuid.UUID] = None,
|
||||
old_values: Optional[Dict[str, Any]] = None,
|
||||
new_values: Optional[Dict[str, Any]] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
request: Optional[Request] = None
|
||||
) -> AuditLog:
|
||||
"""
|
||||
Registra una acción en la bitácora de auditoría.
|
||||
|
||||
Args:
|
||||
db: Sesi├│n de base de datos
|
||||
tenant_id: ID del tenant
|
||||
action: Acci├│n realizada (formato: "recurso.verbo")
|
||||
Ejemplos: "user.login", "ticket.create", "ticket.assign"
|
||||
resource_type: Tipo de recurso ("user", "ticket", "comment", etc.)
|
||||
resource_id: ID del recurso afectado (opcional)
|
||||
user_id: ID del usuario que ejecut├│ la acci├│n (opcional = sistema)
|
||||
old_values: Valores antes del cambio (opcional)
|
||||
new_values: Valores despu├®s del cambio (opcional)
|
||||
metadata: Informaci├│n adicional (opcional)
|
||||
request: Request de FastAPI para extraer IP y user agent (opcional)
|
||||
|
||||
Returns:
|
||||
AuditLog creado
|
||||
"""
|
||||
# Extraer información del request si está disponible
|
||||
ip_address = None
|
||||
user_agent = None
|
||||
correlation_id = None
|
||||
|
||||
if request:
|
||||
# IP del cliente
|
||||
if request.client:
|
||||
ip_address = request.client.host
|
||||
|
||||
# User agent
|
||||
user_agent = request.headers.get("user-agent")
|
||||
|
||||
# Correlation ID (si existe en el request state)
|
||||
correlation_id = getattr(request.state, "correlation_id", None)
|
||||
|
||||
# Crear registro de auditoría
|
||||
audit_log = AuditLog(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
action=action,
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
correlation_id=correlation_id,
|
||||
old_values=old_values,
|
||||
new_values=new_values,
|
||||
extra_metadata=metadata # Mapeo metadata -> extra_metadata
|
||||
)
|
||||
|
||||
db.add(audit_log)
|
||||
await db.flush() # No commit, se hará con la transacción principal
|
||||
|
||||
# Log estructurado para debugging
|
||||
logger.info(
|
||||
"Audit log created",
|
||||
action=action,
|
||||
resource_type=resource_type,
|
||||
resource_id=str(resource_id) if resource_id else None,
|
||||
user_id=str(user_id) if user_id else "system",
|
||||
tenant_id=str(tenant_id)
|
||||
)
|
||||
|
||||
return audit_log
|
||||
|
||||
@staticmethod
|
||||
async def log_login(
|
||||
db: AsyncSession,
|
||||
user: User,
|
||||
request: Request,
|
||||
success: bool = True
|
||||
) -> AuditLog:
|
||||
"""
|
||||
Registra un intento de login.
|
||||
|
||||
Args:
|
||||
db: Sesi├│n de base de datos
|
||||
user: Usuario que intent├│ loguearse
|
||||
request: Request de FastAPI
|
||||
success: Si el login fue exitoso
|
||||
|
||||
Returns:
|
||||
AuditLog creado
|
||||
"""
|
||||
return await AuditService.log(
|
||||
db=db,
|
||||
tenant_id=user.tenant_id,
|
||||
user_id=user.id if success else None,
|
||||
action="user.login" if success else "user.login_failed",
|
||||
resource_type="user",
|
||||
resource_id=user.id,
|
||||
metadata={
|
||||
"success": success,
|
||||
"email": user.email
|
||||
},
|
||||
request=request
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def log_logout(
|
||||
db: AsyncSession,
|
||||
user: User,
|
||||
request: Request
|
||||
) -> AuditLog:
|
||||
"""
|
||||
Registra un logout.
|
||||
|
||||
Args:
|
||||
db: Sesi├│n de base de datos
|
||||
user: Usuario que cerr├│ sesi├│n
|
||||
request: Request de FastAPI
|
||||
|
||||
Returns:
|
||||
AuditLog creado
|
||||
"""
|
||||
return await AuditService.log(
|
||||
db=db,
|
||||
tenant_id=user.tenant_id,
|
||||
user_id=user.id,
|
||||
action="user.logout",
|
||||
resource_type="user",
|
||||
resource_id=user.id,
|
||||
request=request
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def log_create(
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
resource_type: str,
|
||||
resource_id: uuid.UUID,
|
||||
new_values: Dict[str, Any],
|
||||
request: Optional[Request] = None
|
||||
) -> AuditLog:
|
||||
"""
|
||||
Registra la creaci├│n de un recurso.
|
||||
|
||||
Args:
|
||||
db: Sesi├│n de base de datos
|
||||
tenant_id: ID del tenant
|
||||
user_id: ID del usuario que cre├│ el recurso
|
||||
resource_type: Tipo de recurso ("ticket", "user", etc.)
|
||||
resource_id: ID del recurso creado
|
||||
new_values: Valores del nuevo recurso
|
||||
request: Request de FastAPI (opcional)
|
||||
|
||||
Returns:
|
||||
AuditLog creado
|
||||
"""
|
||||
return await AuditService.log(
|
||||
db=db,
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
action=f"{resource_type}.create",
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id,
|
||||
new_values=new_values,
|
||||
request=request
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def log_update(
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
resource_type: str,
|
||||
resource_id: uuid.UUID,
|
||||
old_values: Dict[str, Any],
|
||||
new_values: Dict[str, Any],
|
||||
request: Optional[Request] = None
|
||||
) -> AuditLog:
|
||||
"""
|
||||
Registra la actualizaci├│n de un recurso.
|
||||
|
||||
Args:
|
||||
db: Sesi├│n de base de datos
|
||||
tenant_id: ID del tenant
|
||||
user_id: ID del usuario que actualiz├│
|
||||
resource_type: Tipo de recurso
|
||||
resource_id: ID del recurso
|
||||
old_values: Valores anteriores
|
||||
new_values: Valores nuevos
|
||||
request: Request de FastAPI (opcional)
|
||||
|
||||
Returns:
|
||||
AuditLog creado
|
||||
"""
|
||||
return await AuditService.log(
|
||||
db=db,
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
action=f"{resource_type}.update",
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id,
|
||||
old_values=old_values,
|
||||
new_values=new_values,
|
||||
request=request
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def log_delete(
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
resource_type: str,
|
||||
resource_id: uuid.UUID,
|
||||
old_values: Dict[str, Any],
|
||||
request: Optional[Request] = None
|
||||
) -> AuditLog:
|
||||
"""
|
||||
Registra la eliminaci├│n de un recurso.
|
||||
|
||||
Args:
|
||||
db: Sesi├│n de base de datos
|
||||
tenant_id: ID del tenant
|
||||
user_id: ID del usuario que elimin├│
|
||||
resource_type: Tipo de recurso
|
||||
resource_id: ID del recurso eliminado
|
||||
old_values: Valores del recurso antes de eliminar
|
||||
request: Request de FastAPI (opcional)
|
||||
|
||||
Returns:
|
||||
AuditLog creado
|
||||
"""
|
||||
return await AuditService.log(
|
||||
db=db,
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
action=f"{resource_type}.delete",
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id,
|
||||
old_values=old_values,
|
||||
request=request
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def sanitize_values(values: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Sanitiza valores sensibles antes de guardarlos en audit log.
|
||||
|
||||
Remueve campos como passwords, tokens, etc.
|
||||
|
||||
Args:
|
||||
values: Diccionario de valores
|
||||
|
||||
Returns:
|
||||
Diccionario sanitizado
|
||||
"""
|
||||
sensitive_fields = {
|
||||
'password',
|
||||
'password_hash',
|
||||
'totp_secret',
|
||||
'backup_codes',
|
||||
'token',
|
||||
'access_token',
|
||||
'refresh_token'
|
||||
}
|
||||
|
||||
return {
|
||||
key: '***REDACTED***' if key in sensitive_fields else value
|
||||
for key, value in values.items()
|
||||
}
|
||||
Reference in New Issue
Block a user