Skip to content

Commit ad04c9b

Browse files
Bentechy660xAda
andauthored
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 <[email protected]>
1 parent c77c5bb commit ad04c9b

File tree

9 files changed

+74
-15
lines changed

9 files changed

+74
-15
lines changed

src/admin/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@ class AuditLogView(APIView):
3434
permission_classes = [IsAdminUser]
3535

3636
def get(self, request):
37-
serializer = AuditLogSerializer(data=AuditLogEntry.objects.all(), many=True)
37+
serializer = AuditLogSerializer(data=AuditLogEntry.objects.order_by("-id").all(), many=True)
3838
serializer.is_valid()
3939
return FormattedResponse(serializer.data)

src/announcements/views.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
from backend.viewsets import AuditLoggedViewSet
12
from rest_framework.viewsets import ModelViewSet
23

34
from announcements.models import Announcement
45
from announcements.serializers import AnnouncementSerializer
56
from backend.permissions import AdminOrReadOnly
67

78

8-
class AnnouncementViewSet(ModelViewSet):
9+
class AnnouncementViewSet(AuditLoggedViewSet, ModelViewSet):
910
queryset = Announcement.objects.all()
1011
permission_classes = (AdminOrReadOnly,)
1112
throttle_scope = "announcement"

src/authentication/views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
remove_2fa,
4444
verify_2fa,
4545
)
46-
from backend.viewsets import AdminListModelViewSet
46+
from backend.viewsets import AdminListModelViewSet, AuditLoggedViewSet
4747
from config import config
4848
from member.models import Member
4949
from plugins import providers
@@ -342,7 +342,7 @@ def post(self, request):
342342
return FormattedResponse({"invite_codes": codes})
343343

344344

345-
class InviteViewSet(AdminListModelViewSet):
345+
class InviteViewSet(AuditLoggedViewSet, AdminListModelViewSet):
346346
permission_classes = (permissions.IsAdminUser,)
347347
admin_serializer_class = InviteCodeSerializer
348348
list_admin_serializer_class = InviteCodeSerializer

src/backend/viewsets.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from admin.models import AuditLogEntry
12
from rest_framework import permissions
23
from rest_framework.viewsets import ModelViewSet
34

@@ -37,3 +38,59 @@ def get_serializer_class(self):
3738
if self.request.user.is_staff and not self.request.user.should_deny_admin():
3839
return self.admin_serializer_class
3940
return self.serializer_class
41+
42+
43+
class AuditLoggedViewSet(ModelViewSet):
44+
def create(self, request, *args, **kwargs):
45+
if request.user is not None and request.user.is_staff:
46+
ret = super().create(request, *args, **kwargs)
47+
48+
fields = {}
49+
fields["model_fields"] = ret.data
50+
fields["model_name"] = self.get_serializer().Meta.model.__name__
51+
AuditLogEntry.objects.create(user=request.user, username=request.user.username, action="create_model", extra=fields)
52+
53+
return ret
54+
return super().create(request, *args, **kwargs)
55+
56+
def destroy(self, request, *args, **kwargs):
57+
if request.user is not None and request.user.is_staff:
58+
instance = self.get_object()
59+
fields = {}
60+
fields["model_fields"] = self.get_serializer(instance).data
61+
fields["model_name"] = instance._meta.model.__name__
62+
fields["model_id"] = instance.id
63+
AuditLogEntry.objects.create(user=request.user, username=request.user.username, action="destroy_model", extra=fields)
64+
65+
ret = super().destroy(request, *args, **kwargs)
66+
67+
return ret
68+
return super().destroy(request, *args, **kwargs)
69+
70+
def update(self, request, *args, **kwargs):
71+
if request.user is not None and request.user.is_staff:
72+
old_instance = self.get_object() # Keep track of old data
73+
old_data = self.get_serializer(old_instance).data
74+
75+
ret = super().update(request, *args, **kwargs)
76+
77+
new_instance = self.get_object() # Get the new data
78+
new_data = self.get_serializer(new_instance).data
79+
80+
diffs = {}
81+
82+
for key, value in new_data.items():
83+
if old_data.get(key, None) != value:
84+
diffs[key] = {
85+
"old": old_data.get(key, None),
86+
"new": new_data.get(key, None)
87+
}
88+
89+
fields = {}
90+
fields["updated_fields"] = diffs
91+
fields["model_name"] = new_instance._meta.model.__name__
92+
fields["model_id"] = new_instance.id
93+
AuditLogEntry.objects.create(user=request.user, username=request.user.username, action="update_model", extra=fields)
94+
95+
return ret
96+
return super().update(request, *args, **kwargs)

src/challenge/views.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from backend.permissions import AdminOrReadOnly, IsBot, ReadOnlyBot
2323
from backend.response import FormattedResponse
2424
from backend.signals import flag_reject, flag_score, flag_submit
25-
from backend.viewsets import AdminCreateModelViewSet
25+
from backend.viewsets import AdminCreateModelViewSet, AuditLoggedViewSet
2626
from challenge.models import (
2727
Category,
2828
Challenge,
@@ -64,7 +64,7 @@ def get_cache_key(user):
6464
return str(caches["default"].get("challenge_mod_index", 0)) + "categoryvs_team_" + str(user.team.pk)
6565

6666

67-
class CategoryViewset(AdminCreateModelViewSet):
67+
class CategoryViewset(AuditLoggedViewSet, AdminCreateModelViewSet):
6868
queryset = Category.objects.all()
6969
permission_classes = (CompetitionOpen & AdminOrReadOnly,)
7070
throttle_scope = "challenges"
@@ -147,7 +147,7 @@ def list(self, request, *args, **kwargs):
147147
return FormattedResponse(categories)
148148

149149

150-
class ChallengeViewset(AdminCreateModelViewSet):
150+
class ChallengeViewset(AuditLoggedViewSet, AdminCreateModelViewSet):
151151
queryset = Challenge.objects.all()
152152
permission_classes = (CompetitionOpen & AdminOrReadOnly,)
153153
throttle_scope = "challenges"

src/hint/views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from backend.permissions import IsBot
88
from backend.response import FormattedResponse
99
from backend.signals import use_hint
10-
from backend.viewsets import AdminCreateModelViewSet
10+
from backend.viewsets import AdminCreateModelViewSet, AuditLoggedViewSet
1111
from challenge.permissions import CompetitionOpen
1212
from challenge.views import get_cache_key
1313
from hint.models import Hint, HintUse
@@ -21,7 +21,7 @@
2121
from team.permissions import HasTeam
2222

2323

24-
class HintViewSet(AdminCreateModelViewSet):
24+
class HintViewSet(AuditLoggedViewSet, AdminCreateModelViewSet):
2525
queryset = Hint.objects.all()
2626
permission_classes = (HasUsedHint,)
2727
throttle_scope = "hint"

src/member/views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from rest_framework.viewsets import ModelViewSet
55

66
from backend.permissions import AdminOrReadOnlyVisible, ReadOnlyBot
7-
from backend.viewsets import AdminListModelViewSet
7+
from backend.viewsets import AdminListModelViewSet, AuditLoggedViewSet
88
from member.models import UserIP, Member
99
from member.serializers import (
1010
AdminMemberSerializer,
@@ -43,7 +43,7 @@ def get_object(self):
4343
)
4444

4545

46-
class MemberViewSet(AdminListModelViewSet):
46+
class MemberViewSet(AuditLoggedViewSet, AdminListModelViewSet):
4747
permission_classes = (AdminOrReadOnlyVisible,)
4848
throttle_scope = "member"
4949
serializer_class = MemberSerializer

src/pages/views.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
from backend.viewsets import AuditLoggedViewSet
12
from rest_framework.viewsets import ModelViewSet
23

34
from backend.permissions import AdminOrAnonymousReadOnly
45
from pages.models import Page
56
from pages.serializers import PageSerializer
67

78

8-
class TagViewSet(ModelViewSet):
9+
class TagViewSet(AuditLoggedViewSet, ModelViewSet):
910
queryset = Page.objects.all()
1011
permission_classes = (AdminOrAnonymousReadOnly,)
1112
throttle_scope = "pages"

src/team/views.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from backend.permissions import AdminOrReadOnlyVisible, ReadOnlyBot, AdminOrReadOnly
1919
from backend.response import FormattedResponse
2020
from backend.signals import team_join, team_join_attempt, team_join_reject
21-
from backend.viewsets import AdminListModelViewSet
21+
from backend.viewsets import AdminListModelViewSet, AuditLoggedViewSet
2222
from challenge.models import Solve
2323
from config import config
2424
from member.models import Member
@@ -57,7 +57,7 @@ def get_object(self):
5757
)
5858

5959

60-
class LeaderboardGroupViewSet(AdminListModelViewSet):
60+
class LeaderboardGroupViewSet(AuditLoggedViewSet, AdminListModelViewSet):
6161
permission_classes = (AdminOrReadOnly,)
6262
serializer_class = LeaderboardGroupSerializer
6363
admin_serializer_class = LeaderboardGroupSerializer
@@ -66,7 +66,7 @@ class LeaderboardGroupViewSet(AdminListModelViewSet):
6666
queryset = LeaderboardGroup.objects.all()
6767

6868

69-
class TeamViewSet(AdminListModelViewSet):
69+
class TeamViewSet(AuditLoggedViewSet, AdminListModelViewSet):
7070
permission_classes = (AdminOrReadOnlyVisible,)
7171
throttle_scope = "team"
7272
serializer_class = TeamSerializer

0 commit comments

Comments
 (0)