103 lines
2.7 KiB
Python
103 lines
2.7 KiB
Python
"""
|
|
Single call point for all audit event writes.
|
|
|
|
Usage:
|
|
from tracking.audit import record
|
|
|
|
record(
|
|
"auth.login_success",
|
|
actor=request.user,
|
|
actor_ip=client_ip,
|
|
metadata={"oidc_sub": sub},
|
|
)
|
|
|
|
Never raises in production so that compliance events cannot break request processing.
|
|
In DEBUG mode the write error is re-raised for test visibility.
|
|
"""
|
|
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
|
|
from django.conf import settings
|
|
from tracking.models import AuditEvent
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Valid event_class prefixes derived from the taxonomy
|
|
_VALID_CLASSES = {
|
|
"auth",
|
|
"progress",
|
|
"assessment",
|
|
"trainer",
|
|
"certificate",
|
|
"access",
|
|
}
|
|
|
|
|
|
def _derive_class(event_type: str) -> str:
|
|
prefix = event_type.split(".")[0]
|
|
return prefix if prefix in _VALID_CLASSES else "_unknown"
|
|
|
|
|
|
def _retention_class(event_class: str) -> str:
|
|
"""Return the retention bucket key for this event class."""
|
|
cfg = getattr(settings, "AUDIT_RETENTION_DAYS", {})
|
|
if event_class in cfg:
|
|
return event_class
|
|
return "_default"
|
|
|
|
|
|
def record(
|
|
event_type: str,
|
|
*,
|
|
actor=None,
|
|
actor_type: str = "user",
|
|
actor_ip: str | None = None,
|
|
object_type: str | None = None,
|
|
object_id: str | None = None,
|
|
occurred_at: datetime | None = None,
|
|
metadata: dict | None = None,
|
|
):
|
|
"""
|
|
Persist a compliance audit event.
|
|
|
|
Returns the saved AuditEvent instance, or None if the write failed and
|
|
DEBUG is False (the error is logged instead of propagated).
|
|
"""
|
|
event_class = _derive_class(event_type)
|
|
ret_class = _retention_class(event_class)
|
|
ts = occurred_at or datetime.now(tz=timezone.utc)
|
|
|
|
actor_id = None
|
|
if actor is not None:
|
|
pk = getattr(actor, "pk", None) or getattr(actor, "id", None)
|
|
if pk is not None:
|
|
try:
|
|
import uuid as _uuid
|
|
actor_id = _uuid.UUID(str(pk))
|
|
except (ValueError, AttributeError):
|
|
actor_id = None
|
|
|
|
obj_id = str(object_id) if object_id is not None else None
|
|
|
|
try:
|
|
event = AuditEvent(
|
|
event_class=event_class,
|
|
event_type=event_type,
|
|
actor_id=actor_id,
|
|
actor_type=actor_type,
|
|
actor_ip=actor_ip,
|
|
object_type=object_type,
|
|
object_id=obj_id,
|
|
occurred_at=ts,
|
|
metadata=metadata or {},
|
|
retention_class=ret_class,
|
|
)
|
|
event.save()
|
|
return event
|
|
except Exception as exc:
|
|
logger.exception("audit.record failed for event_type=%s: %s", event_type, exc)
|
|
if getattr(settings, "DEBUG", False):
|
|
raise
|
|
return None
|