diff --git a/netbox/users/models/tokens.py b/netbox/users/models/tokens.py index 3c1284bc9ce..77b0f71eee6 100644 --- a/netbox/users/models/tokens.py +++ b/netbox/users/models/tokens.py @@ -3,6 +3,7 @@ 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 +87,14 @@ def get_absolute_url(self): 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: + raise ValidationError({'expires': _('Expiration time must be in the future.')}) + def save(self, *args, **kwargs): if not self.key: self.key = self.generate_key() diff --git a/netbox/users/tests/test_models.py b/netbox/users/tests/test_models.py index 7bb6f31af74..d37ad671111 100644 --- a/netbox/users/tests/test_models.py +++ b/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):