Closes #20823: Validate token expiration date on creation (#20862)

This commit is contained in:
Martin Hauser 2025-11-24 22:05:59 +01:00 committed by GitHub
parent d9aab6bbe2
commit b919868521
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 88 additions and 2 deletions

View File

@ -1,8 +1,10 @@
import binascii
import os
import zoneinfo
from django.conf import settings
from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError
from django.core.validators import MinLengthValidator
from django.db import models
from django.urls import reverse
@ -86,6 +88,24 @@ class Token(models.Model):
def partial(self):
return f'**********************************{self.key[-6:]}' if self.key else ''
def clean(self):
super().clean()
# Prevent creating a token with a past expiration date
# while allowing updates to existing tokens.
if self.pk is None and self.is_expired:
current_tz = zoneinfo.ZoneInfo(settings.TIME_ZONE)
now = timezone.now().astimezone(current_tz)
current_time_str = f'{now.date().isoformat()} {now.time().isoformat(timespec="seconds")}'
# Translators: {current_time} is the current server date and time in ISO format,
# {timezone} is the configured server time zone (for example, "UTC" or "Europe/Berlin").
message = _('Expiration time must be in the future. '
'Current server time is {current_time} ({timezone}).'
).format(current_time=current_time_str, timezone=current_tz.key)
raise ValidationError({'expires': message})
def save(self, *args, **kwargs):
if not self.key:
self.key = self.generate_key()

View File

@ -1,6 +1,72 @@
from django.test import TestCase
from datetime import timedelta
from users.models import User
from django.core.exceptions import ValidationError
from django.test import TestCase
from django.utils import timezone
from users.models import User, Token
from utilities.testing import create_test_user
class TokenTest(TestCase):
"""
Test class for testing the functionality of the Token model.
"""
@classmethod
def setUpTestData(cls):
"""
Set up test data for the Token model.
"""
cls.user = create_test_user('User 1')
def test_is_expired(self):
"""
Test the is_expired property.
"""
# Token with no expiration
token = Token(user=self.user, expires=None)
self.assertFalse(token.is_expired)
# Token with future expiration
token.expires = timezone.now() + timedelta(days=1)
self.assertFalse(token.is_expired)
# Token with past expiration
token.expires = timezone.now() - timedelta(days=1)
self.assertTrue(token.is_expired)
def test_cannot_create_token_with_past_expiration(self):
"""
Test that creating a token with an expiration date in the past raises a ValidationError.
"""
past_date = timezone.now() - timedelta(days=1)
token = Token(user=self.user, expires=past_date)
with self.assertRaises(ValidationError) as cm:
token.clean()
self.assertIn('expires', cm.exception.error_dict)
def test_can_update_existing_expired_token(self):
"""
Test that updating an already expired token does NOT raise a ValidationError.
"""
# Create a valid token first with an expiration date in the past
# bypasses the clean() method
token = Token.objects.create(user=self.user)
token.expires = timezone.now() - timedelta(days=1)
token.save()
# Try to update the description
token.description = 'New Description'
try:
token.clean()
token.save()
except ValidationError:
self.fail('Updating an expired token should not raise ValidationError')
token.refresh_from_db()
self.assertEqual(token.description, 'New Description')
class UserConfigTest(TestCase):