Browse Source

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

Martin Hauser 2 tháng trước cách đây
mục cha
commit
b919868521
2 tập tin đã thay đổi với 87 bổ sung1 xóa
  1. 20 0
      netbox/users/models/tokens.py
  2. 67 1
      netbox/users/tests/test_models.py

+ 20 - 0
netbox/users/models/tokens.py

@@ -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()

+ 67 - 1
netbox/users/tests/test_models.py

@@ -1,6 +1,72 @@
+from datetime import timedelta
+
+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
+
 
-from users.models import 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):