feat: Implement comprehensive audit logging for authentication events, including login, logout, user agent, and IP address, and add a script for audit verification.
This commit is contained in:
@@ -88,7 +88,34 @@ class AuditMapper:
|
||||
|
||||
# General fallbacks
|
||||
"clients_and_providers": "CATALOGS",
|
||||
"clients_and_providers": "CATALOGS",
|
||||
"clients_and_providers": "CATALOGS",
|
||||
"items": "CATALOGS",
|
||||
"classes": "CATALOGS",
|
||||
"classification_concepts": "CATALOGS",
|
||||
"concepts": "CATALOGS",
|
||||
"customs_broker_concepts": "CATALOGS",
|
||||
"depreciation_catalog": "CATALOGS",
|
||||
"electronic_notices": "ELECTRONIC NOTICES",
|
||||
"equivalencies": "CATALOGS",
|
||||
"error_catalogs": "CATALOGS",
|
||||
"fda_catalog": "CATALOGS",
|
||||
"inpc": "CATALOGS",
|
||||
"legends": "CATALOGS",
|
||||
"multi_currency_types": "CATALOGS",
|
||||
"packages": "CATALOGS",
|
||||
"ports": "CATALOGS",
|
||||
"prevalidators": "CATALOGS",
|
||||
"seal": "CATALOGS",
|
||||
"signatures": "CATALOGS",
|
||||
"tariff_fractions": "CATALOGS",
|
||||
"unit_conversions": "CATALOGS",
|
||||
"us_tariff_fractions": "CATALOGS",
|
||||
|
||||
# DODA
|
||||
"doda": "DODA",
|
||||
"doda_containers": "DODA",
|
||||
"doda_pedimentos": "DODA",
|
||||
}
|
||||
|
||||
# Map (Table, Operation) to Legacy Movements (English)
|
||||
@@ -107,6 +134,14 @@ class AuditMapper:
|
||||
|
||||
("auth", "LOGIN"): "SYSTEM LOGIN",
|
||||
("auth", "LOGOUT"): "SYSTEM LOGOUT",
|
||||
|
||||
("classes", "CREATE"): "ADD CLASS",
|
||||
("classes", "UPDATE"): "EDIT CLASS",
|
||||
("classes", "DELETE"): "DELETE CLASS",
|
||||
|
||||
("doda", "CREATE"): "ADD DODA",
|
||||
("doda", "UPDATE"): "EDIT DODA",
|
||||
("doda", "DELETE"): "DELETE DODA",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -40,8 +40,8 @@ class AuditService:
|
||||
"""
|
||||
Low-level creation of an Audit Log entry
|
||||
"""
|
||||
# Timezone handling set to Mexico City as requested implicitly by legacy format example
|
||||
tz = pytz.timezone('America/Mexico_City')
|
||||
# Timezone handling set to Hermosillo (Sonora) to match user preference (-1h vs CDMX)
|
||||
tz = pytz.timezone('America/Hermosillo')
|
||||
now = datetime.now(tz)
|
||||
|
||||
log = AuditLog(
|
||||
@@ -120,6 +120,9 @@ class AuditService:
|
||||
|
||||
elif table_name == "companies":
|
||||
reference = record_data.get("rfc") or reference
|
||||
|
||||
elif table_name == "classes":
|
||||
reference = record_data.get("class_code") or reference
|
||||
|
||||
|
||||
# 3. Detect Changed Fields (for Update)
|
||||
@@ -150,7 +153,30 @@ class AuditService:
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def log_login(db: Session, username: str, ip_address: str = None):
|
||||
def log_login(db: Session, username: str, ip_address: str = None, user_agent: str = None):
|
||||
# Prevent duplicate login logs (debounce 5 seconds)
|
||||
# This handles cases where frontend might submit twice or redirects trigger re-auth
|
||||
try:
|
||||
# Timezone handling set to Hermosillo (Sonora) to match user preference (-1h vs CDMX)
|
||||
tz = pytz.timezone('America/Hermosillo')
|
||||
now = datetime.now(tz)
|
||||
five_seconds_ago = now - datetime.timedelta(seconds=5)
|
||||
|
||||
# Check for recent login from same user
|
||||
existing = db.query(AuditLog).filter(
|
||||
AuditLog.username == username,
|
||||
AuditLog.operation_type == "LOGIN",
|
||||
# Compare against timestamp (timezone aware)
|
||||
AuditLog.timestamp >= five_seconds_ago
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
print(f"[AUDIT DEBUG] Duplicate login skipped for {username} within 5s")
|
||||
return existing
|
||||
|
||||
except Exception as e:
|
||||
print(f"[AUDIT WARNING] Failed to check duplicate login: {e}")
|
||||
|
||||
return AuditService.create_audit_log(
|
||||
db=db,
|
||||
reference="LOGIN",
|
||||
@@ -158,5 +184,27 @@ class AuditService:
|
||||
movement="SYSTEM LOGIN",
|
||||
username=username,
|
||||
operation_type="LOGIN",
|
||||
ip_address=ip_address
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def log_logout(db: Session, username: str, ip_address: str = None, user_agent: str = None):
|
||||
"""
|
||||
Registra un evento de cierre de sesión
|
||||
"""
|
||||
try:
|
||||
# Reutilizamos create_audit_log para mantener consistencia
|
||||
AuditService.create_audit_log(
|
||||
db=db,
|
||||
reference="LOGOUT",
|
||||
procedure="SYSTEM AUTH",
|
||||
movement="SYSTEM LOGOUT",
|
||||
username=username,
|
||||
operation_type="LOGOUT",
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent
|
||||
)
|
||||
except Exception as e:
|
||||
# No re-lanzamos la excepción para no interrumpir el flujo de logout
|
||||
print(f"Error logging logout: {e}")
|
||||
|
||||
Reference in New Issue
Block a user