From ad04c9b3f13ab3b3bdf35f4b43d97b185f54bd2a Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 16 Oct 2023 23:19:18 +0100 Subject: [PATCH] Audit Log Model Changes (#323) * Pin to python3.10 and fix pyyaml * backend work for leaderboard groups * lol * return names * add uritemplate * Upgrade uvicorn to use a version of websockets from this decade * Add audit logging --------- Co-authored-by: Ada Cooke --- src/admin/views.py | 2 +- src/announcements/views.py | 3 +- src/authentication/views.py | 4 +-- src/backend/viewsets.py | 57 +++++++++++++++++++++++++++++++++++++ src/challenge/views.py | 6 ++-- src/hint/views.py | 4 +-- src/member/views.py | 4 +-- src/pages/views.py | 3 +- src/team/views.py | 6 ++-- 9 files changed, 74 insertions(+), 15 deletions(-) diff --git a/src/admin/views.py b/src/admin/views.py index 49d88644..9345f87e 100644 --- a/src/admin/views.py +++ b/src/admin/views.py @@ -34,6 +34,6 @@ class AuditLogView(APIView): permission_classes = [IsAdminUser] def get(self, request): - serializer = AuditLogSerializer(data=AuditLogEntry.objects.all(), many=True) + serializer = AuditLogSerializer(data=AuditLogEntry.objects.order_by("-id").all(), many=True) serializer.is_valid() return FormattedResponse(serializer.data) diff --git a/src/announcements/views.py b/src/announcements/views.py index f2174bf8..5435bd51 100644 --- a/src/announcements/views.py +++ b/src/announcements/views.py @@ -1,3 +1,4 @@ +from backend.viewsets import AuditLoggedViewSet from rest_framework.viewsets import ModelViewSet from announcements.models import Announcement @@ -5,7 +6,7 @@ from backend.permissions import AdminOrReadOnly -class AnnouncementViewSet(ModelViewSet): +class AnnouncementViewSet(AuditLoggedViewSet, ModelViewSet): queryset = Announcement.objects.all() permission_classes = (AdminOrReadOnly,) throttle_scope = "announcement" diff --git a/src/authentication/views.py b/src/authentication/views.py index 985c4570..dd650681 100644 --- a/src/authentication/views.py +++ b/src/authentication/views.py @@ -43,7 +43,7 @@ remove_2fa, verify_2fa, ) -from backend.viewsets import AdminListModelViewSet +from backend.viewsets import AdminListModelViewSet, AuditLoggedViewSet from config import config from member.models import Member from plugins import providers @@ -342,7 +342,7 @@ def post(self, request): return FormattedResponse({"invite_codes": codes}) -class InviteViewSet(AdminListModelViewSet): +class InviteViewSet(AuditLoggedViewSet, AdminListModelViewSet): permission_classes = (permissions.IsAdminUser,) admin_serializer_class = InviteCodeSerializer list_admin_serializer_class = InviteCodeSerializer diff --git a/src/backend/viewsets.py b/src/backend/viewsets.py index 722ea748..558fdf25 100644 --- a/src/backend/viewsets.py +++ b/src/backend/viewsets.py @@ -1,3 +1,4 @@ +from admin.models import AuditLogEntry from rest_framework import permissions from rest_framework.viewsets import ModelViewSet @@ -37,3 +38,59 @@ def get_serializer_class(self): if self.request.user.is_staff and not self.request.user.should_deny_admin(): return self.admin_serializer_class return self.serializer_class + + +class AuditLoggedViewSet(ModelViewSet): + def create(self, request, *args, **kwargs): + if request.user is not None and request.user.is_staff: + ret = super().create(request, *args, **kwargs) + + fields = {} + fields["model_fields"] = ret.data + fields["model_name"] = self.get_serializer().Meta.model.__name__ + AuditLogEntry.objects.create(user=request.user, username=request.user.username, action="create_model", extra=fields) + + return ret + return super().create(request, *args, **kwargs) + + def destroy(self, request, *args, **kwargs): + if request.user is not None and request.user.is_staff: + instance = self.get_object() + fields = {} + fields["model_fields"] = self.get_serializer(instance).data + fields["model_name"] = instance._meta.model.__name__ + fields["model_id"] = instance.id + AuditLogEntry.objects.create(user=request.user, username=request.user.username, action="destroy_model", extra=fields) + + ret = super().destroy(request, *args, **kwargs) + + return ret + return super().destroy(request, *args, **kwargs) + + def update(self, request, *args, **kwargs): + if request.user is not None and request.user.is_staff: + old_instance = self.get_object() # Keep track of old data + old_data = self.get_serializer(old_instance).data + + ret = super().update(request, *args, **kwargs) + + new_instance = self.get_object() # Get the new data + new_data = self.get_serializer(new_instance).data + + diffs = {} + + for key, value in new_data.items(): + if old_data.get(key, None) != value: + diffs[key] = { + "old": old_data.get(key, None), + "new": new_data.get(key, None) + } + + fields = {} + fields["updated_fields"] = diffs + fields["model_name"] = new_instance._meta.model.__name__ + fields["model_id"] = new_instance.id + AuditLogEntry.objects.create(user=request.user, username=request.user.username, action="update_model", extra=fields) + + return ret + return super().update(request, *args, **kwargs) \ No newline at end of file diff --git a/src/challenge/views.py b/src/challenge/views.py index 502a44ce..1069b9e8 100644 --- a/src/challenge/views.py +++ b/src/challenge/views.py @@ -22,7 +22,7 @@ from backend.permissions import AdminOrReadOnly, IsBot, ReadOnlyBot from backend.response import FormattedResponse from backend.signals import flag_reject, flag_score, flag_submit -from backend.viewsets import AdminCreateModelViewSet +from backend.viewsets import AdminCreateModelViewSet, AuditLoggedViewSet from challenge.models import ( Category, Challenge, @@ -64,7 +64,7 @@ def get_cache_key(user): return str(caches["default"].get("challenge_mod_index", 0)) + "categoryvs_team_" + str(user.team.pk) -class CategoryViewset(AdminCreateModelViewSet): +class CategoryViewset(AuditLoggedViewSet, AdminCreateModelViewSet): queryset = Category.objects.all() permission_classes = (CompetitionOpen & AdminOrReadOnly,) throttle_scope = "challenges" @@ -147,7 +147,7 @@ def list(self, request, *args, **kwargs): return FormattedResponse(categories) -class ChallengeViewset(AdminCreateModelViewSet): +class ChallengeViewset(AuditLoggedViewSet, AdminCreateModelViewSet): queryset = Challenge.objects.all() permission_classes = (CompetitionOpen & AdminOrReadOnly,) throttle_scope = "challenges" diff --git a/src/hint/views.py b/src/hint/views.py index 0e9db09a..e8ae9069 100644 --- a/src/hint/views.py +++ b/src/hint/views.py @@ -7,7 +7,7 @@ from backend.permissions import IsBot from backend.response import FormattedResponse from backend.signals import use_hint -from backend.viewsets import AdminCreateModelViewSet +from backend.viewsets import AdminCreateModelViewSet, AuditLoggedViewSet from challenge.permissions import CompetitionOpen from challenge.views import get_cache_key from hint.models import Hint, HintUse @@ -21,7 +21,7 @@ from team.permissions import HasTeam -class HintViewSet(AdminCreateModelViewSet): +class HintViewSet(AuditLoggedViewSet, AdminCreateModelViewSet): queryset = Hint.objects.all() permission_classes = (HasUsedHint,) throttle_scope = "hint" diff --git a/src/member/views.py b/src/member/views.py index 3ce3e153..d81185c8 100644 --- a/src/member/views.py +++ b/src/member/views.py @@ -4,7 +4,7 @@ from rest_framework.viewsets import ModelViewSet from backend.permissions import AdminOrReadOnlyVisible, ReadOnlyBot -from backend.viewsets import AdminListModelViewSet +from backend.viewsets import AdminListModelViewSet, AuditLoggedViewSet from member.models import UserIP, Member from member.serializers import ( AdminMemberSerializer, @@ -43,7 +43,7 @@ def get_object(self): ) -class MemberViewSet(AdminListModelViewSet): +class MemberViewSet(AuditLoggedViewSet, AdminListModelViewSet): permission_classes = (AdminOrReadOnlyVisible,) throttle_scope = "member" serializer_class = MemberSerializer diff --git a/src/pages/views.py b/src/pages/views.py index 70832fbc..2dcbc418 100644 --- a/src/pages/views.py +++ b/src/pages/views.py @@ -1,3 +1,4 @@ +from backend.viewsets import AuditLoggedViewSet from rest_framework.viewsets import ModelViewSet from backend.permissions import AdminOrAnonymousReadOnly @@ -5,7 +6,7 @@ from pages.serializers import PageSerializer -class TagViewSet(ModelViewSet): +class TagViewSet(AuditLoggedViewSet, ModelViewSet): queryset = Page.objects.all() permission_classes = (AdminOrAnonymousReadOnly,) throttle_scope = "pages" diff --git a/src/team/views.py b/src/team/views.py index aebebddb..b4cf089a 100644 --- a/src/team/views.py +++ b/src/team/views.py @@ -18,7 +18,7 @@ from backend.permissions import AdminOrReadOnlyVisible, ReadOnlyBot, AdminOrReadOnly from backend.response import FormattedResponse from backend.signals import team_join, team_join_attempt, team_join_reject -from backend.viewsets import AdminListModelViewSet +from backend.viewsets import AdminListModelViewSet, AuditLoggedViewSet from challenge.models import Solve from config import config from member.models import Member @@ -57,7 +57,7 @@ def get_object(self): ) -class LeaderboardGroupViewSet(AdminListModelViewSet): +class LeaderboardGroupViewSet(AuditLoggedViewSet, AdminListModelViewSet): permission_classes = (AdminOrReadOnly,) serializer_class = LeaderboardGroupSerializer admin_serializer_class = LeaderboardGroupSerializer @@ -66,7 +66,7 @@ class LeaderboardGroupViewSet(AdminListModelViewSet): queryset = LeaderboardGroup.objects.all() -class TeamViewSet(AdminListModelViewSet): +class TeamViewSet(AuditLoggedViewSet, AdminListModelViewSet): permission_classes = (AdminOrReadOnlyVisible,) throttle_scope = "team" serializer_class = TeamSerializer