Files
training-software/tracking/views.py
Paperclip CTO 686acf259a feat(TRA-246): implement audit logging and compliance controls (M5)
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>
2026-05-07 09:11:23 +02:00

46 lines
1.5 KiB
Python

from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters, mixins, viewsets
from rest_framework.throttling import UserRateThrottle
from api.permissions import IsPrivileged
from .models import AuditEvent
from .serializers import AuditEventSerializer
class AuditEventThrottle(UserRateThrottle):
scope = "audit"
class AuditEventViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
"""
Read-only audit event stream.
Requires Manager or Admin role. Supports filtering by event_class,
event_type, actor_id, object_type, object_id, and date range.
Ordered by occurred_at descending.
"""
serializer_class = AuditEventSerializer
permission_classes = [IsPrivileged]
throttle_classes = [AuditEventThrottle]
filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
filterset_fields = ["event_class", "event_type", "actor_id", "object_type", "object_id"]
ordering_fields = ["occurred_at", "recorded_at"]
ordering = ["-occurred_at"]
def get_queryset(self):
qs = AuditEvent.objects.all()
params = self.request.query_params
after = params.get("occurred_after")
before = params.get("occurred_before")
if after:
qs = qs.filter(occurred_at__gte=after)
if before:
qs = qs.filter(occurred_at__lte=before)
return qs
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx["request"] = self.request
return ctx