+
Skip to content

Add audit log #279

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

Merged
merged 5 commits into from
Aug 9, 2022
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
28 changes: 28 additions & 0 deletions src/admin/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 4.0.4 on 2022-06-09 22:43

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='AuditLogEntry',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('username', models.CharField(max_length=255)),
('time', models.DateTimeField(auto_now_add=True)),
('action', models.CharField(max_length=255)),
('extra', models.JSONField()),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
),
]
24 changes: 23 additions & 1 deletion src/admin/models.py
Original file line number Diff line number Diff line change
@@ -1 +1,23 @@
# Create your models here.
import os

from django.db import models
from django.db.models import SET_NULL

from member.models import Member


class AuditLogEntry(models.Model):
user = models.ForeignKey(Member, null=True, on_delete=SET_NULL)
username = models.CharField(max_length=255)
time = models.DateTimeField(auto_now_add=True)
action = models.CharField(max_length=255)
extra = models.JSONField()

@classmethod
def create_management_entry(cls, command: str, extra=None):
AuditLogEntry.objects.create(user=None, username=f"System ({os.getlogin()})",
action=f"management_{command}", extra=extra)

@classmethod
def create_entry(cls, user: Member, action: str, extra=None):
AuditLogEntry.objects.create(user=user, username=user.username, action=action, extra=extra)
9 changes: 9 additions & 0 deletions src/admin/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from rest_framework import serializers

from admin.models import AuditLogEntry


class AuditLogSerializer(serializers.ModelSerializer):
class Meta:
model = AuditLogEntry
fields = ["user", "username", "action", "time", "extra"]
21 changes: 21 additions & 0 deletions src/admin/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.urls import reverse
from django.test import TestCase
from django.core import mail
from rest_framework.status import HTTP_403_FORBIDDEN, HTTP_200_OK
from rest_framework.test import APITestCase

from challenge.models import Category, Challenge
Expand Down Expand Up @@ -90,6 +91,7 @@ def test_length(self):
response = self.client.get(reverse("self-check"))
self.assertEqual(len(response.data["d"]), 17)


class DevMailEndpointTestCase(TestCase):
def test_endpoint_absent(self):
with self.settings(
Expand Down Expand Up @@ -124,3 +126,22 @@ def test_endpoint_with_mail(self):
<p>From: noreply@ractf.co.uk</p>
<p>To: ['example@ractf.co.uk']</p>
<p>This is the body</p>""", html=True)


class AuditLogViewTestCase(APITestCase):
def setUp(self) -> None:
self.user = Member(username="audit_log_test", email="audit_log_test@bot.ractf")
self.user.save()
self.admin_user = Member(username="audit_log_test_admin", email="audit_log_test_admin@bot.ractf",
is_staff=True, is_superuser=True)
self.admin_user.save()

def test_audit_log_not_admin(self):
self.client.force_authenticate(user=self.user)
response = self.client.get(reverse("audit-log"))
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)

def test_audit_log_admin(self):
self.client.force_authenticate(user=self.admin_user)
response = self.client.get(reverse("audit-log"))
self.assertEqual(response.status_code, HTTP_200_OK)
1 change: 1 addition & 0 deletions src/admin/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
urlpatterns = [
path("self_check/", views.SelfCheckView.as_view(), name="self-check"),
path("list/", views.mail_list, name="mail-list"),
path("audit_log/", views.AuditLogView.as_view(), name="audit-log"),
]
12 changes: 12 additions & 0 deletions src/admin/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from rest_framework.permissions import IsAdminUser
from rest_framework.views import APIView

from admin.models import AuditLogEntry
from admin.serializers import AuditLogSerializer
from backend.response import FormattedResponse
from challenge.models import Challenge

Expand All @@ -20,8 +22,18 @@ def get(self, request):

return FormattedResponse(issues)


def mail_list(request):
if settings.EMAIL_BACKEND == "anymail.backends.test.EmailBackend":
return render(request, "mail_list.html", context={"emails": getattr(mail, 'outbox', [])})
else:
return HttpResponseNotFound()


class AuditLogView(APIView):
permission_classes = [IsAdminUser]

def get(self, request):
serializer = AuditLogSerializer(data=AuditLogEntry.objects.all(), many=True)
serializer.is_valid()
return FormattedResponse(serializer.data)
29 changes: 29 additions & 0 deletions src/config/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
)
from rest_framework.test import APITestCase

from admin.models import AuditLogEntry
from config import config
from member.models import Member

Expand Down Expand Up @@ -88,3 +89,31 @@ def test_update_patch_list(self):
config.set("testlist", ["test"])
self.client.patch(reverse("config-pk", kwargs={"name": "testlist"}), data={"value": "test"}, format="json")
self.assertEqual(config.get("testlist"), ["test", "test"])

def test_update_post_creates_audit_log(self):
self.client.force_authenticate(self.staff_user)
self.client.post(reverse("config-pk", kwargs={"name": "test"}), data={"value": "test"}, format="json")
self.client.post(reverse("config-pk", kwargs={"name": "test"}), data={"value": "test2"}, format="json")
entry = AuditLogEntry.objects.latest("pk")
self.assertEqual(entry.action, "set_config")

def test_update_post_creates_audit_log_with_correct_extra(self):
self.client.force_authenticate(self.staff_user)
self.client.post(reverse("config-pk", kwargs={"name": "test"}), data={"value": "test"}, format="json")
self.client.post(reverse("config-pk", kwargs={"name": "test"}), data={"value": "test2"}, format="json")
entry = AuditLogEntry.objects.latest("pk")
self.assertEqual(entry.extra, {"old_value": "test", "new_value": "test2", "key": "test"})

def test_update_patch_creates_audit_log(self):
self.client.force_authenticate(self.staff_user)
self.client.post(reverse("config-pk", kwargs={"name": "test"}), data={"value": "test"}, format="json")
self.client.patch(reverse("config-pk", kwargs={"name": "test"}), data={"value": "test2"}, format="json")
entry = AuditLogEntry.objects.latest("pk")
self.assertEqual(entry.action, "set_config")

def test_update_patch_creates_audit_log_with_correct_extra(self):
self.client.force_authenticate(self.staff_user)
self.client.post(reverse("config-pk", kwargs={"name": "test"}), data={"value": "test"}, format="json")
self.client.patch(reverse("config-pk", kwargs={"name": "test"}), data={"value": "test2"}, format="json")
entry = AuditLogEntry.objects.latest("pk")
self.assertEqual(entry.extra, {"old_value": "test", "new_value": "test2", "key": "test"})
17 changes: 17 additions & 0 deletions src/config/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
)
from rest_framework.views import APIView

from admin.models import AuditLogEntry
from backend.permissions import AdminOrAnonymousReadOnly
from backend.response import FormattedResponse
from config import config
Expand All @@ -27,6 +28,11 @@ def get(self, request, name=None):
def post(self, request, name):
if "value" not in request.data:
return FormattedResponse(status=HTTP_400_BAD_REQUEST)
AuditLogEntry.create_entry(request.user, "set_config", {
"old_value": config.get(name),
"new_value": request.data.get("value"),
"key": name,
})
config.set(name, request.data.get("value"))
return FormattedResponse(status=HTTP_201_CREATED)

Expand All @@ -36,7 +42,18 @@ def patch(self, request, name):
if config.get(name) is not None and isinstance(config.get(name), list):
value = config.get(name)
value.append(request.data["value"])
AuditLogEntry.create_entry(request.user, "set_config", {
"old_value": config.get(name),
"new_value": value,
"key": name,
})
config.set(name, value)
return FormattedResponse()

AuditLogEntry.create_entry(request.user, "set_config", {
"old_value": config.get(name),
"new_value": request.data.get("value"),
"key": name,
})
config.set(name, request.data.get("value"))
return FormattedResponse(status=HTTP_204_NO_CONTENT)
3 changes: 3 additions & 0 deletions src/ractf/management/commands/create_preevent_cache.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import time

from django.core.cache import caches
Expand All @@ -8,6 +9,7 @@
from django.utils import timezone
from rest_framework.request import Request

from admin.models import AuditLogEntry
from challenge import serializers
from challenge.models import Category, Challenge, File, Tag
from challenge.serializers import FastCategorySerializer
Expand Down Expand Up @@ -71,6 +73,7 @@ class Command(BaseCommand):
help = "Creates a cache to lessen the impact of the first 15 seconds of request spam"

def handle(self, *args, **options):
AuditLogEntry.create_management_entry("create_preevent_cache")
categories = get_queryset()
solve_counts = get_solve_counts()
positive_votes = get_positive_votes()
Expand Down
13 changes: 13 additions & 0 deletions src/ractf/management/commands/create_user.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import os

from django.core.management import BaseCommand
from django.core.management.base import CommandError, CommandParser
from django.db import IntegrityError

from admin.models import AuditLogEntry
from member.models import Member


Expand Down Expand Up @@ -41,6 +44,16 @@ def handle(self, *args, **options) -> None:
except IntegrityError:
raise CommandError("Username already in use")

AuditLogEntry.create_management_entry("create_user", extra={
"username": options["username"],
"email_verified": True,
"is_visible": options["visible"],
"is_staff": options["staff"],
"is_superuser": options["superuser"],
"is_bot": options["bot"],
"email": options["email"],
})

if member.is_bot:
print(member.issue_token())
else:
Expand Down
2 changes: 2 additions & 0 deletions src/ractf/management/commands/reset_scores.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.core.management import BaseCommand

from admin.models import AuditLogEntry
from challenge.models import Challenge, Score, Solve
from member.models import Member
from team.models import Team
Expand All @@ -9,6 +10,7 @@ class Command(BaseCommand):
help = "Removes all scores from the database"

def handle(self, *args, **options):
AuditLogEntry.create_management_entry("reset_scores")
Solve.objects.all().delete()
Score.objects.all().delete()
for team in Team.objects.all():
Expand Down
6 changes: 6 additions & 0 deletions src/ractf/management/commands/transfer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.core.management import BaseCommand

from member.models import Member
from admin.models import AuditLogEntry
from team.models import Team


Expand All @@ -13,6 +14,11 @@ def add_arguments(self, parser):

def handle(self, *args, **options):
user = Member.objects.get(pk=options["user_id"])
AuditLogEntry.create_management_entry("transfer", {
"team_id": options["team_id"],
"user_id": options["user_id"],
})

team = Team.objects.get(pk=options["team_id"])
team.owner = user
team.save()
3 changes: 3 additions & 0 deletions src/ractf/management/commands/unteam.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.core.management import BaseCommand

from member.models import Member
from admin.models import AuditLogEntry


class Command(BaseCommand):
Expand All @@ -16,10 +17,12 @@ def handle(self, *args, **options):
x = input("This will delete the team, are you sure?")
if x == "n":
return
AuditLogEntry.create_management_entry("unteam_delete", {"deleted_team": user.team.pk})
team = user.team
team.delete()
user.team = None
user.save()
return
AuditLogEntry.create_management_entry("unteam", {"user_id": user.pk})
user.team = None
user.save()
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载