Skip to content

Commit 2d492a6

Browse files
jchristgitthebeanogamerDavid Cooke
authored
Drop CIText & update Django (#237)
* Update to Django 4.0 `django-clacks` was removed temporarily until we figure out why poetry won't lock the dependencies with clacks included. * Drop CI text fields in favour of charfields A unique constraint is created on the database in order to prevent duplicate signups using a name that only differs in casing for both users as well as teams. Two test cases are added to verify this behaviour. * Re-add django-clacks * Truncate data in migration Co-authored-by: Daniel Milnes <[email protected]> * Reject usernames with a length > 36 Co-authored-by: Daniel Milnes <[email protected]> Co-authored-by: David Cooke <[email protected]>
1 parent 3dd84ca commit 2d492a6

File tree

12 files changed

+797
-581
lines changed

12 files changed

+797
-581
lines changed

poetry.lock

Lines changed: 615 additions & 532 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,31 @@ authors = ["RACTF Admins <[email protected]>"]
66

77
[tool.poetry.dependencies]
88
python = "^3.9"
9-
Django = "^3.1"
10-
django-cors-headers = "3.2.1"
11-
django-redis = "4.11.0"
12-
django-redis-cache = "2.1.1"
13-
django-storages = "1.9.1"
14-
djangorestframework = "3.11.1"
9+
Django = "^4.0"
10+
django-cors-headers = "^3.11"
11+
django-redis = "^5.2"
12+
django-redis-cache = "^3.0"
13+
django-storages = "~1.12"
14+
djangorestframework = "^3.13"
1515
pyotp = "2.3.0"
1616
gunicorn = "^20.0.4"
1717
boto3 = "^1.14.33"
1818
psycopg2-binary = "^2.8.5"
1919
django-filter = "^2.3.0"
2020
newrelic = "^5.22.1"
21-
django-prometheus = "^2.1.0"
22-
django-cachalot = "^2.3.5"
23-
django-silk = "^4.1.0"
21+
django-prometheus = "^2.2"
22+
django-clacks = "^0.3"
23+
django-cachalot = "^2.5"
24+
django-silk = "^4.2"
2425
serpy = "^0.3.1"
25-
django-zxcvbn-password-validator = "^1.3.2"
26+
django-zxcvbn-password-validator = "^1.4"
2627
uvicorn = {extras = ["standard"], version = "^0.13.4"}
2728
sentry-sdk = "^1.0.0"
2829
coverage = {extras = ["toml"], version = "^5.5"}
2930
Twisted = "22.1.0"
3031
channels-redis = "^3.2.0"
31-
django-clacks = "^0.2.0"
3232
requests = "^2.26.0"
33-
django-anymail = {extras = ["amazon_ses", "mailgun", "sendgrid", "console", "mailjet", "mandrill", "postal", "postmark", "sendinblue", "sparkpost"], version = "^8.4"}
33+
django-anymail = {extras = ["amazon_ses", "mailgun", "sendgrid", "console", "mailjet", "mandrill", "postal", "postmark", "sendinblue", "sparkpost"], version = "^8.5"}
3434

3535
[tool.poetry.dev-dependencies]
3636
ipython = "^7.31.1"
@@ -55,10 +55,6 @@ docopt = "^0.6.2"
5555

5656
[tool.pytest.ini_options]
5757
python_files = "tests.py test_*.py *_tests.py"
58-
filterwarnings = """
59-
ignore::django.utils.deprecation.RemovedInDjango40Warning
60-
ignore::django.utils.deprecation.RemovedInDjango41Warning
61-
"""
6258

6359
[tool.coverage.run]
6460
source = ["src"]

src/authentication/basic_auth.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ def validate(self, data):
1717

1818
self.validate_email(data["email"])
1919
self.check_email_or_username_in_use(email=data["email"], username=data["username"])
20+
if len(data["username"]) > 36:
21+
raise ValidationError("username_too_long")
2022

2123
return {key: data[key] for key in self.required_fields}
2224

src/authentication/tests.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,15 @@ def test_register_with_mail_failing_domain(self):
235235
config.set("email_domain", None)
236236
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
237237

238+
def test_register_long_username(self):
239+
data = {
240+
"username": "a" * 37,
241+
"password": "uO7*$E@0ngqL",
242+
"email": "[email protected]",
243+
}
244+
response = self.client.post(reverse("register"), data)
245+
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
246+
238247

239248
class EmailResendTestCase(APITestCase):
240249
def test_email_resend(self):

src/backend/signals.py

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
from django.dispatch import Signal
22

3-
flag_score = Signal(providing_args=["user", "team", "challenge", "flag", "solve"])
4-
flag_reject = Signal(providing_args=["user", "team", "challenge", "flag", "reason"])
5-
flag_submit = Signal(providing_args=["user", "team", "challenge", "flag"])
6-
team_join = Signal(providing_args=["user", "team"])
7-
team_join_attempt = Signal(providing_args=["user", "team_name"])
8-
team_join_reject = Signal(providing_args=["user", "team_name"])
9-
team_create = Signal(providing_args=["team"])
10-
use_hint = Signal(providing_args=["user", "team", "hint"])
11-
logout = Signal(providing_args=["user"])
12-
add_2fa = Signal(providing_args=["user"])
13-
verify_2fa = Signal(providing_args=["user"])
14-
remove_2fa = Signal(providing_args=["user"])
15-
password_reset = Signal(providing_args=["user"])
16-
password_reset_start = Signal(providing_args=["user"])
17-
password_reset_start_reject = Signal(providing_args=["email"])
18-
email_verified = Signal(providing_args=["user"])
19-
change_password = Signal(providing_args=["user"])
20-
login = Signal(providing_args=["user"])
21-
login_reject = Signal(providing_args=["username", "reason"])
22-
register = Signal(providing_args=["user"])
23-
register_reject = Signal(providing_args=["username", "email"])
24-
websocket_connect = Signal(providing_args=["channel_layer"])
25-
websocket_disconnect = Signal(providing_args=["channel_layer"])
3+
flag_score = Signal()
4+
flag_reject = Signal()
5+
flag_submit = Signal()
6+
team_join = Signal()
7+
team_join_attempt = Signal()
8+
team_join_reject = Signal()
9+
team_create = Signal()
10+
use_hint = Signal()
11+
logout = Signal()
12+
add_2fa = Signal()
13+
verify_2fa = Signal()
14+
remove_2fa = Signal()
15+
password_reset = Signal()
16+
password_reset_start = Signal()
17+
password_reset_start_reject = Signal()
18+
email_verified = Signal()
19+
change_password = Signal()
20+
login = Signal()
21+
login_reject = Signal()
22+
register = Signal()
23+
register_reject = Signal()
24+
websocket_connect = Signal()
25+
websocket_disconnect = Signal()
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Generated by Django 4.0.2 on 2022-02-11 21:43
2+
3+
import backend.validators
4+
from django.db import migrations, models
5+
import django.db.models.functions.text
6+
7+
8+
def truncate_usernames(apps, schema_editor):
9+
Member = apps.get_model('member', 'member')
10+
db_alias = schema_editor.connection.alias
11+
for member in Member.objects.using(db_alias).all():
12+
if len(member.username) > 36:
13+
member.username = member.username[:36]
14+
member.save()
15+
16+
17+
class Migration(migrations.Migration):
18+
19+
dependencies = [
20+
('member', '0008_remove_member_password_reset_token'),
21+
]
22+
23+
operations = [
24+
migrations.AlterModelOptions(
25+
name='member',
26+
options={},
27+
),
28+
migrations.RunPython(
29+
truncate_usernames,
30+
# Marked as elidable since when we squash we will have the 36 characters
31+
# in by default
32+
elidable=True,
33+
),
34+
migrations.AlterField(
35+
model_name='member',
36+
name='username',
37+
field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 36 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=36, unique=True, validators=[backend.validators.printable_name], verbose_name='username'),
38+
),
39+
migrations.AddConstraint(
40+
model_name='member',
41+
constraint=models.UniqueConstraint(django.db.models.functions.text.Lower('username'), name='member_member_username_uniq_idx'),
42+
),
43+
]

src/member/models.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
from django.contrib.auth import get_user_model
66
from django.contrib.auth.models import AbstractUser
7-
from django.contrib.postgres.fields import CICharField
87
from django.db import models
98
from django.db.models import SET_NULL
9+
from django.db.models.functions import Lower
1010
from django.utils import timezone
1111
from django.utils.translation import gettext_lazy as _
1212
from django_prometheus.models import ExportModelOperationsMixin
@@ -24,7 +24,7 @@ class TOTPStatus(IntEnum):
2424
class Member(ExportModelOperationsMixin("member"), AbstractUser):
2525
username_validator = printable_name
2626

27-
username = CICharField(
27+
username = models.CharField(
2828
_("username"),
2929
max_length=36,
3030
unique=True,
@@ -49,6 +49,14 @@ class Member(ExportModelOperationsMixin("member"), AbstractUser):
4949
leaderboard_points = models.IntegerField(default=0)
5050
last_score = models.DateTimeField(default=timezone.now)
5151

52+
class Meta:
53+
constraints = [
54+
models.UniqueConstraint(
55+
Lower('username'),
56+
name='member_member_username_uniq_idx',
57+
),
58+
]
59+
5260
def __str__(self):
5361
return self.username
5462

src/member/tests.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from django.contrib.auth import get_user_model
22
from django.contrib.auth.models import AnonymousUser
3+
from django.db.utils import IntegrityError
34
from django.http import HttpRequest
45
from rest_framework.request import Request
56
from rest_framework.reverse import reverse
@@ -148,6 +149,12 @@ def test_patch_member(self):
148149
)
149150
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
150151

152+
def test_disallows_differently_cased_but_same_username(self):
153+
"""A differently-cased but otherwise same username should not be allowed registration."""
154+
155+
self.user.username = self.admin_user.username.upper()
156+
self.assertRaises(IntegrityError, self.user.save)
157+
151158
def test_patch_member_admin(self):
152159
self.client.force_authenticate(self.admin_user)
153160
response = self.client.patch(
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Generated by Django 4.0.2 on 2022-02-11 21:43
2+
3+
import backend.validators
4+
from django.db import migrations, models
5+
import django.db.models.functions.text
6+
7+
8+
def truncate_usernames(apps, schema_editor):
9+
Team = apps.get_model('team', 'team')
10+
db_alias = schema_editor.connection.alias
11+
for team in Team.objects.using(db_alias).all():
12+
if len(team.name) > 36:
13+
team.name = team.name[:36]
14+
team.save()
15+
16+
17+
class Migration(migrations.Migration):
18+
19+
dependencies = [
20+
('team', '0003_team_size_limit_exempt'),
21+
]
22+
23+
operations = [
24+
migrations.RunPython(
25+
truncate_usernames,
26+
# Marked as elidable since when we squash we will have the 36 characters
27+
# in by default
28+
elidable=True,
29+
),
30+
migrations.AlterField(
31+
model_name='team',
32+
name='name',
33+
field=models.CharField(max_length=36, unique=True, validators=[backend.validators.printable_name]),
34+
),
35+
migrations.AddConstraint(
36+
model_name='team',
37+
constraint=models.UniqueConstraint(django.db.models.functions.text.Lower('name'), name='team_team_username_uniq_idx'),
38+
),
39+
]

src/team/models.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
from django.contrib.postgres.fields import CICharField
21
from django.db import models
32
from django.db.models import CASCADE, Prefetch
3+
from django.db.models.functions import Lower
44
from django.utils import timezone
55
from django_prometheus.models import ExportModelOperationsMixin
66

@@ -32,7 +32,7 @@ def prefetch_solves(self) -> "models.QuerySet[Team]":
3232
class Team(ExportModelOperationsMixin("team"), models.Model):
3333
"""Represents a team of one or more Members."""
3434

35-
name = CICharField(max_length=36, unique=True, validators=[printable_name])
35+
name = models.CharField(max_length=36, unique=True, validators=[printable_name])
3636
is_visible = models.BooleanField(default=True)
3737
password = models.CharField(max_length=64)
3838
owner = models.ForeignKey(Member, on_delete=CASCADE, related_name="owned_team")
@@ -43,3 +43,11 @@ class Team(ExportModelOperationsMixin("team"), models.Model):
4343
size_limit_exempt = models.BooleanField(default=False)
4444

4545
objects = TeamQuerySet.as_manager()
46+
47+
class Meta:
48+
constraints = [
49+
models.UniqueConstraint(
50+
Lower('name'),
51+
name='team_team_username_uniq_idx',
52+
),
53+
]

0 commit comments

Comments
 (0)