Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/audit log #323

Merged
merged 9 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/admin/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
3 changes: 2 additions & 1 deletion src/announcements/views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from backend.viewsets import AuditLoggedViewSet
from rest_framework.viewsets import ModelViewSet

from announcements.models import Announcement
from announcements.serializers import AnnouncementSerializer
from backend.permissions import AdminOrReadOnly


class AnnouncementViewSet(ModelViewSet):
class AnnouncementViewSet(AuditLoggedViewSet, ModelViewSet):
queryset = Announcement.objects.all()
permission_classes = (AdminOrReadOnly,)
throttle_scope = "announcement"
Expand Down
4 changes: 2 additions & 2 deletions src/authentication/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
57 changes: 57 additions & 0 deletions src/backend/viewsets.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from admin.models import AuditLogEntry
from rest_framework import permissions
from rest_framework.viewsets import ModelViewSet

Expand Down Expand Up @@ -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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this be better as a Mixin style class?

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)
6 changes: 3 additions & 3 deletions src/challenge/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions src/hint/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions src/member/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -43,7 +43,7 @@ def get_object(self):
)


class MemberViewSet(AdminListModelViewSet):
class MemberViewSet(AuditLoggedViewSet, AdminListModelViewSet):
permission_classes = (AdminOrReadOnlyVisible,)
throttle_scope = "member"
serializer_class = MemberSerializer
Expand Down
3 changes: 2 additions & 1 deletion src/pages/views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from backend.viewsets import AuditLoggedViewSet
from rest_framework.viewsets import ModelViewSet

from backend.permissions import AdminOrAnonymousReadOnly
from pages.models import Page
from pages.serializers import PageSerializer


class TagViewSet(ModelViewSet):
class TagViewSet(AuditLoggedViewSet, ModelViewSet):
queryset = Page.objects.all()
permission_classes = (AdminOrAnonymousReadOnly,)
throttle_scope = "pages"
Expand Down
6 changes: 3 additions & 3 deletions src/team/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading