Skip to content

Commit

Permalink
Merge branch 'master' into pick-smaller-changes-from-app-restructure
Browse files Browse the repository at this point in the history
  • Loading branch information
0xAda authored Jun 10, 2022
2 parents 5d4cfb9 + 45ee449 commit a580b03
Show file tree
Hide file tree
Showing 23 changed files with 450 additions and 48 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/integrate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@ jobs:
- name: Confirm Current is Healthy
run: curl -v --fail localhost:8000/api/v2/stats/stats/

- name: Clean Up Docker Compose
run: make clean-dev-server

- name: Get Core Logs
run: docker-compose logs backend
if: always()

- name: Get Sockets Logs
run: docker-compose logs sockets
if: always()

- name: Clean Up Docker Compose
run: make clean-dev-server
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,4 @@ $RECYCLE.BIN/
*.sqlite3
*.db
.vim
src/publicmedia
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,15 @@ clean-test:
clean-dev-server:
docker-compose rm -sfv
docker volume rm -f core_postgres

dev-logs:
docker-compose logs -f backend

dev-sql:
docker-compose exec database psql -U postgres postgres

dev-fake-bulk-data:
docker-compose exec backend python -m scripts.fake generate --teams 10000 --users 2 --categories 10 --challenges 100 --solves 10000

dev-shell:
docker-compose exec backend ./src/manage.py shell
27 changes: 14 additions & 13 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ django-zxcvbn-password-validator = "^1.4"
uvicorn = {extras = ["standard"], version = "^0.13.4"}
sentry-sdk = "^1.0.0"
coverage = {extras = ["toml"], version = "^5.5"}
Twisted = "22.1.0"
Twisted = "22.4.0"
channels-redis = "^3.2.0"
requests = "^2.26.0"
django-anymail = {extras = ["amazon_ses", "mailgun", "sendgrid", "console", "mailjet", "mandrill", "postal", "postmark", "sendinblue", "sparkpost"], version = "^8.5"}
Expand Down
2 changes: 1 addition & 1 deletion src/admin/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def test_length(self):
self.client.force_authenticate(user=self.user)

response = self.client.get(reverse("self-check"))
self.assertEqual(len(response.data["d"]), 14)
self.assertEqual(len(response.data["d"]), 17)

class DevMailEndpointTestCase(TestCase):
def test_endpoint_absent(self):
Expand Down
47 changes: 46 additions & 1 deletion src/andromeda/tests.py
Original file line number Diff line number Diff line change
@@ -1 +1,46 @@
# Create your tests here.
from django.urls import reverse
from rest_framework.test import APITestCase
from config import config

from team.tests import TeamSetupMixin
from requests.exceptions import ConnectionError


class AndromedaTestCase(TeamSetupMixin, APITestCase):
def test_get_instance_disabled_challenge_server(self):
with self.settings(
CHALLENGE_SERVER_ENABLED=False,
):
self.client.force_authenticate(user=self.user)
response = self.client.get(reverse("get-instance", args=["1"]))
self.assertContains(response, "challenge_server_disabled", status_code=403)

def test_request_new_instance_disabled_challenge_server(self):
with self.settings(
CHALLENGE_SERVER_ENABLED=False,
):
self.client.force_authenticate(user=self.user)
response = self.client.get(reverse("request-new-instance", args=["1"]))
self.assertContains(response, "challenge_server_disabled", status_code=403)

def test_get_instance_no_team(self):
with self.settings(
CHALLENGE_SERVER_ENABLED=True,
):
self.client.force_authenticate(user=self.user)
config.set("enable_team_leave", True)
self.client.post(reverse("team-leave"))
response = self.client.get(reverse("get-instance", args=["1"]))
self.assertContains(response, "challenge_server_team_required", status_code=403)

def test_get_instance(self):
# This is a bad test but we need a stub service
with self.settings(
CHALLENGE_SERVER_ENABLED=True,
ANDROMEDA_URL="http://andromeda",
ANDROMEDA_API_KEY="andromeda",
ANDROMEDA_TIMEOUT=0.1,
):
with self.assertRaises(ConnectionError):
self.client.force_authenticate(user=self.user)
self.client.get(reverse("get-instance", args=["1"]))
2 changes: 1 addition & 1 deletion src/andromeda/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""API endpoints for the andromeda integration."""

from django.conf import settings
from rest_framework.generics import get_object_or_404
from rest_framework.permissions import IsAdminUser, IsAuthenticated
from rest_framework.status import HTTP_403_FORBIDDEN
Expand All @@ -10,7 +11,6 @@
from backend.response import FormattedResponse
from challenge.models import Challenge
from challenge.permissions import CompetitionOpen
from config import config


class GetInstanceView(APIView):
Expand Down
9 changes: 6 additions & 3 deletions src/backend/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@
"token_provider": "basic_auth",
"enable_bot_users": True,
"enable_caching": True,
"enable_challenge_server": True,
"enable_ctftime": True,
"enable_flag_submission": True,
"enable_flag_submission_after_competition": True,
Expand Down Expand Up @@ -283,15 +282,19 @@
"NUM_PROXIES": int(os.getenv("NUM_PROXIES", 0)),
}

if os.getenv("CHALLENGE_SERVER_TYPE") == "POLARIS":
if os.getenv("CHALLENGE_SERVER_TYPE") == "POLARIS": # pragma: no cover
CHALLENGE_SERVER_ENABLED = True
POLARIS_URL = os.getenv("POLARIS_URL")
POLARIS_USERNAME = os.getenv("POLARIS_USERNAME")
POLARIS_PASSWORD = os.getenv("POLARIS_PASSWORD")
else:
elif os.getenv("ANDROMEDA_URL") or os.getenv("CHALLENGE_SERVER_TYPE") == "ANDROMEDA": # Backwards compatability with old Docker Compose files
CHALLENGE_SERVER_ENABLED = True
ANDROMEDA_URL = os.getenv("ANDROMEDA_URL")
ANDROMEDA_API_KEY = os.getenv("ANDROMEDA_API_KEY")
ANDROMEDA_SERVER_IP = os.getenv("ANDROMEDA_IP") # shown to participants
ANDROMEDA_TIMEOUT = float(os.getenv("ANDROMEDA_TIMEOUT", 5))
else:
CHALLENGE_SERVER_ENABLED = False

INSTALLED_PLUGINS = [
"plugins.flag.hashed",
Expand Down
18 changes: 18 additions & 0 deletions src/challenge/migrations/0022_challenge_current_score.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.0.2 on 2022-02-26 20:13

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('challenge', '0021_challenge_maintenance'),
]

operations = [
migrations.AddField(
model_name='challenge',
name='current_score',
field=models.IntegerField(help_text='The dynamically updated score for this challenge, null if the challenge is statically scored.', null=True),
),
]
30 changes: 26 additions & 4 deletions src/challenge/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,24 +62,31 @@ class Challenge(ExportModelOperationsMixin("challenge"), models.Model):
points_type = models.CharField(max_length=64, default="basic")
release_time = models.DateTimeField(default=timezone.now)
tiebreaker = models.BooleanField(default=True, help_text="Should the challenge be able to break ties?")
current_score = models.IntegerField(
null=True,
help_text="The dynamically updated score for this challenge, null if the challenge is statically scored."
)

def self_check(self):
"""Check the challenge doesn't have any configuration issues."""
issues = []

if not self.score:
issues.append({"issue": "missing_points", "challenge": self.id})
issues.append({"issue": "missing_points", "challenge": self.pk})

if not self.flag_type:
issues.append({"issue": "missing_flag_type", "challenge": self.id})
issues.append({"issue": "missing_flag_type", "challenge": self.pk})
elif type(self.flag_metadata) != dict:
issues.append({"issue": "invalid_flag_data_type", "challenge": self.id})
issues.append({"issue": "invalid_flag_data_type", "challenge": self.pk})
else:
issues += [
{"issue": "invalid_flag_data", "extra": issue, "challenge": self.id}
{"issue": "invalid_flag_data", "extra": issue, "challenge": self.pk}
for issue in self.flag_plugin.self_check()
]

if (self.flag_type == "lenient") ^ (self.challenge_type == "freeform"):
issues.append({"issue": "lenient_freeform_mismatch", "challenge": self.pk})

return issues

@cached_property
Expand Down Expand Up @@ -185,6 +192,21 @@ def get_unlocked_annotated_queryset(cls, user):
)
return x

@property
def needs_recalculate(self):
return self.points_plugin.recalculate_type != "none"

def recalculate_score(self, solve_set):
from team.models import Team

new_score = self.points_plugin.recalculate(
teams=Team.objects.filter(solves__challenge=self),
users=get_user_model().objects.filter(solves__challenge=self),
solves=solve_set.filter(correct=True),
)
self.current_score = new_score
self.save(update_fields=["current_score"])


class ChallengeVote(ExportModelOperationsMixin("challenge_vote"), models.Model):
challenge = models.ForeignKey(Challenge, on_delete=CASCADE, related_name="votes")
Expand Down
8 changes: 6 additions & 2 deletions src/challenge/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ class FastChallengeSerializer(ChallengeSerializerMixin, serpy.Serializer):
challenge_type = serpy.StrField()
challenge_metadata = serpy.Field()
flag_type = serpy.StrField()
points_type = serpy.StrField()
author = serpy.StrField()
score = serpy.IntField()
unlock_requirements = serpy.StrField()
Expand All @@ -147,6 +148,7 @@ class FastChallengeSerializer(ChallengeSerializerMixin, serpy.Serializer):
unlock_time_surpassed = serpy.MethodField()
post_score_explanation = serpy.MethodField()
tiebreaker = serpy.BoolField()
current_score = serpy.IntField(required=False)

def __init__(self, *args, **kwargs) -> None:
"""Add the 'context' attribute to the serializer."""
Expand Down Expand Up @@ -213,6 +215,7 @@ class FastAdminChallengeSerializer(ChallengeSerializerMixin, serpy.Serializer):
challenge_metadata = serpy.Field()
flag_type = serpy.StrField()
flag_metadata = serpy.Field()
points_type = serpy.StrField()
author = serpy.StrField()
score = serpy.IntField()
unlock_requirements = serpy.StrField()
Expand All @@ -229,6 +232,7 @@ class FastAdminChallengeSerializer(ChallengeSerializerMixin, serpy.Serializer):
unlock_time_surpassed = serpy.MethodField()
post_score_explanation = serpy.StrField()
tiebreaker = serpy.BoolField()
current_score = serpy.IntField(required=False)

def __init__(self, *args, **kwargs):
super(FastAdminChallengeSerializer, self).__init__(*args, **kwargs)
Expand All @@ -249,7 +253,7 @@ def to_value(self, instance):


class CreateChallengeSerializer(serializers.ModelSerializer):
tags = serializers.ListField(write_only=True)
tags = serializers.ListField(child=serializers.DictField(), write_only=True)

class Meta:
model = Challenge
Expand All @@ -261,6 +265,7 @@ class Meta:
"challenge_type",
"challenge_metadata",
"flag_type",
"points_type",
"author",
"score",
"unlock_requirements",
Expand All @@ -283,7 +288,6 @@ def create(self, validated_data):

def update(self, instance, validated_data):
tags = validated_data.pop("tags", None)
# FIXME: this is a list of strings somehow
if tags:
Tag.objects.filter(challenge=instance).delete()
for tag_data in tags:
Expand Down
12 changes: 9 additions & 3 deletions src/challenge/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,15 @@ def challenge_cache_invalidate(sender, instance, **kwargs):
def challenge_recalculate(sender, instance, **kwargs):
with transaction.atomic():
correct_solves = instance.solves.filter(correct=True)
Score.objects.filter(id__in=correct_solves.values_list("score", flat=True)).update(points=instance.score)
for solve in correct_solves:
recalculate_team(solve.team)
if instance.current_score is None:
Score.objects.filter(id__in=correct_solves.values_list("score", flat=True)).update(points=instance.score)
for solve in correct_solves:
recalculate_team(solve.team)
else:
Score.objects.filter(id__in=correct_solves.values_list("score", flat=True))\
.update(points=instance.current_score)
for solve in correct_solves:
recalculate_team(solve.team)


@receiver([post_save, post_delete], sender=HintUse)
Expand Down
Loading

0 comments on commit a580b03

Please sign in to comment.