Delivers the append-only audit trail system for all compliance-critical actions per the TRA-246 acceptance criteria. - tracking/models.py: AuditEvent model with ORM-level immutability guard (save raises on update, delete raises on direct call) - tracking/audit.py: single record() call point; never raises in production - tracking/admin.py: read-only Django admin for AuditEvent inspection - tracking/migrations/0001_initial.py: DB schema with composite indexes - tracking/serializers.py: PII metadata gating (oidc_sub stripped for non-admin callers) - tracking/views.py: read-only AuditEventViewSet (IsPrivileged + 60/min throttle) - tracking/urls.py: registers audit/events/ router - tracking/management/commands/prune_audit_log.py: retention enforcement command with --dry-run and --class filter; writes access.admin_action event on real prune runs - config/settings/base.py: AUDIT_RETENTION_DAYS per event class + audit throttle rate - api/exceptions.py: wires access.permission_denied audit event on every PermissionDenied exception (M1 integration point) - tests/test_audit.py: 26-event taxonomy coverage, immutability, retention, API permission, PII gating, and service helper unit tests Co-Authored-By: Paperclip <noreply@paperclip.ing>
49 lines
1.5 KiB
Python
49 lines
1.5 KiB
Python
from rest_framework import serializers
|
|
from .models import AuditEvent
|
|
|
|
# Fields that may contain PII and are gated behind the audit:read_pii capability.
|
|
_PII_METADATA_KEYS = {"oidc_sub", "oidc_sub_attempt", "email"}
|
|
|
|
|
|
class AuditEventSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = AuditEvent
|
|
fields = [
|
|
"id",
|
|
"occurred_at",
|
|
"recorded_at",
|
|
"event_class",
|
|
"event_type",
|
|
"actor_id",
|
|
"actor_type",
|
|
"actor_ip",
|
|
"object_type",
|
|
"object_id",
|
|
"metadata",
|
|
"retention_class",
|
|
]
|
|
read_only_fields = fields
|
|
|
|
def to_representation(self, instance):
|
|
data = super().to_representation(instance)
|
|
request = self.context.get("request")
|
|
if not _has_pii_capability(request):
|
|
data["metadata"] = {
|
|
k: v for k, v in data.get("metadata", {}).items()
|
|
if k not in _PII_METADATA_KEYS
|
|
}
|
|
return data
|
|
|
|
|
|
def _has_pii_capability(request) -> bool:
|
|
"""True when the requesting user holds the audit:read_pii capability."""
|
|
if request is None:
|
|
return False
|
|
user = getattr(request, "user", None)
|
|
if user is None or not user.is_authenticated:
|
|
return False
|
|
# Capability is held by admins. Extend this check when a formal capability
|
|
# registry is introduced in the accounts domain.
|
|
from api.permissions import ROLE_ADMIN, _user_roles
|
|
return ROLE_ADMIN in _user_roles(user)
|