234 lines
7.5 KiB
Python
234 lines
7.5 KiB
Python
"""
|
|
Audit Log Events
|
|
"""
|
|
|
|
import logging
|
|
|
|
from sqlalchemy import event, inspect
|
|
from sqlalchemy.orm import Session
|
|
|
|
from api.v1.modules.a76.general_catalogs.company.models import Company
|
|
from api.v1.modules.a76.invoices.models import InvoiceHeader
|
|
from core.database import rls_company_var, rls_tenant_var
|
|
|
|
from .services.service import AuditService
|
|
from .utils.serialization import serialize_for_json
|
|
from core.context import get_user_context
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def register_audit_listeners(models_to_audit):
|
|
"""
|
|
Register SQLAlchemy listeners for given models
|
|
"""
|
|
for model in models_to_audit:
|
|
event.listen(model, "after_insert", after_insert_listener)
|
|
event.listen(model, "after_update", after_update_listener)
|
|
event.listen(model, "after_delete", after_delete_listener)
|
|
|
|
|
|
def _get_current_username():
|
|
try:
|
|
context = get_user_context()
|
|
if context:
|
|
# Token usually has 'preferred_username' or 'name' or 'sub'
|
|
return (
|
|
context.get("preferred_username")
|
|
or context.get("email")
|
|
or context.get("sub")
|
|
or "System"
|
|
)
|
|
except Exception:
|
|
pass
|
|
return "System"
|
|
|
|
|
|
def _resolve_audit_company_tenant(session: Session, target) -> tuple:
|
|
"""
|
|
company_id / tenant_id desde la fila ORM, ContextVars RLS (petición HTTP),
|
|
o ``Company.tenant_id`` por ``company_id``.
|
|
"""
|
|
resolution_source = "target"
|
|
company_id = getattr(target, "company_id", None)
|
|
if company_id is None and getattr(target, "__tablename__", None) == "company":
|
|
company_id = getattr(target, "id", None)
|
|
if company_id is not None:
|
|
resolution_source = "company_self_id"
|
|
if company_id is None:
|
|
company_id = rls_company_var.get()
|
|
if company_id is not None:
|
|
resolution_source = "rls_context"
|
|
|
|
tenant_id = getattr(target, "tenant_id", None)
|
|
if tenant_id is None:
|
|
tenant_id = rls_tenant_var.get()
|
|
if tenant_id is not None and resolution_source == "target":
|
|
resolution_source = "rls_context"
|
|
|
|
# Common fallback for invoice child tables where invoice_id points to header scope.
|
|
if (company_id is None or tenant_id is None) and hasattr(target, "invoice_id"):
|
|
invoice_id = getattr(target, "invoice_id", None)
|
|
if invoice_id is not None:
|
|
invoice_scope = (
|
|
session.query(InvoiceHeader.company_id, InvoiceHeader.tenant_id)
|
|
.filter(InvoiceHeader.id == invoice_id, InvoiceHeader.deleted_at.is_(None))
|
|
.first()
|
|
)
|
|
if invoice_scope:
|
|
if company_id is None:
|
|
company_id = invoice_scope[0]
|
|
if tenant_id is None:
|
|
tenant_id = invoice_scope[1]
|
|
resolution_source = "invoice_header_lookup"
|
|
|
|
if tenant_id is None and company_id is not None:
|
|
row = (
|
|
session.query(Company.tenant_id)
|
|
.filter(Company.id == company_id, Company.deleted_at.is_(None))
|
|
.first()
|
|
)
|
|
if row:
|
|
tenant_id = int(row[0])
|
|
resolution_source = "company_lookup"
|
|
|
|
return company_id, tenant_id, resolution_source
|
|
|
|
|
|
def after_insert_listener(mapper, connection, target):
|
|
"""
|
|
Listener for INSERT operations
|
|
"""
|
|
table_name = target.__tablename__
|
|
record_data = {c.name: getattr(target, c.name) for c in mapper.columns}
|
|
username = _get_current_username()
|
|
|
|
session = Session(bind=connection)
|
|
try:
|
|
company_id, tenant_id, resolution_source = _resolve_audit_company_tenant(
|
|
session, target
|
|
)
|
|
if company_id is None or tenant_id is None:
|
|
logger.warning(
|
|
"Audit skip INSERT table=%s record_id=%s company_id=%s tenant_id=%s source=%s",
|
|
table_name,
|
|
getattr(target, "id", None),
|
|
company_id,
|
|
tenant_id,
|
|
resolution_source,
|
|
)
|
|
return
|
|
|
|
AuditService.log_crud_operation(
|
|
db=session,
|
|
table_name=table_name,
|
|
operation_type="CREATE",
|
|
record_data=record_data,
|
|
username=username,
|
|
record_id=str(getattr(target, "id", "")),
|
|
company_id=company_id,
|
|
tenant_id=tenant_id,
|
|
)
|
|
except Exception as e:
|
|
logger.warning("Error logging insert for %s: %s", table_name, e)
|
|
finally:
|
|
session.close()
|
|
|
|
|
|
def after_update_listener(mapper, connection, target):
|
|
"""
|
|
Listener for UPDATE operations
|
|
"""
|
|
table_name = target.__tablename__
|
|
|
|
state = inspect(target)
|
|
changes = {}
|
|
old_values = {}
|
|
new_values = {}
|
|
|
|
for attr in state.attrs:
|
|
hist = attr.history
|
|
if hist.has_changes():
|
|
changes[attr.key] = hist.added[0] if hist.added else None
|
|
old_values[attr.key] = hist.deleted[0] if hist.deleted else None
|
|
new_values[attr.key] = hist.added[0] if hist.added else None
|
|
|
|
if not changes:
|
|
return
|
|
|
|
record_data = {c.name: getattr(target, c.name) for c in mapper.columns}
|
|
username = _get_current_username()
|
|
|
|
session = Session(bind=connection)
|
|
try:
|
|
company_id, tenant_id, resolution_source = _resolve_audit_company_tenant(
|
|
session, target
|
|
)
|
|
if company_id is None or tenant_id is None:
|
|
logger.warning(
|
|
"Audit skip UPDATE table=%s record_id=%s company_id=%s tenant_id=%s source=%s",
|
|
table_name,
|
|
getattr(target, "id", None),
|
|
company_id,
|
|
tenant_id,
|
|
resolution_source,
|
|
)
|
|
return
|
|
|
|
AuditService.log_crud_operation(
|
|
db=session,
|
|
table_name=table_name,
|
|
operation_type="UPDATE",
|
|
record_data=record_data,
|
|
username=username,
|
|
record_id=str(getattr(target, "id", "")),
|
|
old_values=serialize_for_json(old_values),
|
|
new_values=serialize_for_json(new_values),
|
|
company_id=company_id,
|
|
tenant_id=tenant_id,
|
|
)
|
|
except Exception as e:
|
|
logger.warning("Error logging update for %s: %s", table_name, e)
|
|
finally:
|
|
session.close()
|
|
|
|
|
|
def after_delete_listener(mapper, connection, target):
|
|
"""
|
|
Listener for DELETE operations
|
|
"""
|
|
table_name = target.__tablename__
|
|
record_data = {c.name: getattr(target, c.name) for c in mapper.columns}
|
|
username = _get_current_username()
|
|
|
|
session = Session(bind=connection)
|
|
try:
|
|
company_id, tenant_id, resolution_source = _resolve_audit_company_tenant(
|
|
session, target
|
|
)
|
|
if company_id is None or tenant_id is None:
|
|
logger.warning(
|
|
"Audit skip DELETE table=%s record_id=%s company_id=%s tenant_id=%s source=%s",
|
|
table_name,
|
|
getattr(target, "id", None),
|
|
company_id,
|
|
tenant_id,
|
|
resolution_source,
|
|
)
|
|
return
|
|
|
|
AuditService.log_crud_operation(
|
|
db=session,
|
|
table_name=table_name,
|
|
operation_type="DELETE",
|
|
record_data=record_data,
|
|
username=username,
|
|
record_id=str(getattr(target, "id", "")),
|
|
company_id=company_id,
|
|
tenant_id=tenant_id,
|
|
)
|
|
except Exception as e:
|
|
logger.warning("Error logging delete for %s: %s", table_name, e)
|
|
finally:
|
|
session.close()
|