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>
46 lines
1.5 KiB
Python
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
|