Skip to content

Commit 92ff05f

Browse files
committed
feat(users): Validate token expiration date on creation
Add model-level validation to prevent creating tokens with a past expiration date. Updates to existing tokens are still allowed, ensuring flexibility for expired token modifications. Includes test cases to verify this behavior. Fixes #20823
1 parent 82171fc commit 92ff05f

File tree

2 files changed

+88
-1
lines changed

2 files changed

+88
-1
lines changed

netbox/users/models/tokens.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import binascii
22
import os
3+
import zoneinfo
34

45
from django.conf import settings
56
from django.contrib.postgres.fields import ArrayField
7+
from django.core.exceptions import ValidationError
68
from django.core.validators import MinLengthValidator
79
from django.db import models
810
from django.urls import reverse
@@ -86,6 +88,25 @@ def get_absolute_url(self):
8688
def partial(self):
8789
return f'**********************************{self.key[-6:]}' if self.key else ''
8890

91+
def clean(self):
92+
super().clean()
93+
94+
# Prevent creating a token with a past expiration date
95+
# while allowing updates to existing tokens.
96+
if self.pk is None and self.is_expired:
97+
current_tz = zoneinfo.ZoneInfo(settings.TIME_ZONE)
98+
now = timezone.now().astimezone(current_tz)
99+
current_time_str = f'{now.date().isoformat()} {now.time().isoformat(timespec="seconds")}'
100+
101+
# Translators: {current_time} is the current server date and time in ISO format,
102+
# {timezone} is the configured server time zone (for example, "UTC" or "Europe/Berlin").
103+
message = _(
104+
'Expiration time must be in the future. '
105+
'Current server time is {current_time} ({timezone}).'
106+
).format(current_time=current_time_str, timezone=current_tz.key)
107+
108+
raise ValidationError({'expires': message})
109+
89110
def save(self, *args, **kwargs):
90111
if not self.key:
91112
self.key = self.generate_key()

netbox/users/tests/test_models.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,72 @@
1+
from datetime import timedelta
2+
3+
from django.core.exceptions import ValidationError
14
from django.test import TestCase
5+
from django.utils import timezone
6+
7+
from users.models import User, Token
8+
from utilities.testing import create_test_user
9+
210

3-
from users.models import User
11+
class TokenTest(TestCase):
12+
"""
13+
Test class for testing the functionality of the Token model.
14+
"""
15+
16+
@classmethod
17+
def setUpTestData(cls):
18+
"""
19+
Set up test data for the Token model.
20+
"""
21+
cls.user = create_test_user('User 1')
22+
23+
def test_is_expired(self):
24+
"""
25+
Test the is_expired property.
26+
"""
27+
# Token with no expiration
28+
token = Token(user=self.user, expires=None)
29+
self.assertFalse(token.is_expired)
30+
31+
# Token with future expiration
32+
token.expires = timezone.now() + timedelta(days=1)
33+
self.assertFalse(token.is_expired)
34+
35+
# Token with past expiration
36+
token.expires = timezone.now() - timedelta(days=1)
37+
self.assertTrue(token.is_expired)
38+
39+
def test_cannot_create_token_with_past_expiration(self):
40+
"""
41+
Test that creating a token with an expiration date in the past raises a ValidationError.
42+
"""
43+
past_date = timezone.now() - timedelta(days=1)
44+
token = Token(user=self.user, expires=past_date)
45+
46+
with self.assertRaises(ValidationError) as cm:
47+
token.clean()
48+
self.assertIn('expires', cm.exception.error_dict)
49+
50+
def test_can_update_existing_expired_token(self):
51+
"""
52+
Test that updating an already expired token does NOT raise a ValidationError.
53+
"""
54+
# Create a valid token first with an expiration date in the past
55+
# bypasses the clean() method
56+
token = Token.objects.create(user=self.user)
57+
token.expires = timezone.now() - timedelta(days=1)
58+
token.save()
59+
60+
# Try to update the description
61+
token.description = 'New Description'
62+
try:
63+
token.clean()
64+
token.save()
65+
except ValidationError:
66+
self.fail('Updating an expired token should not raise ValidationError')
67+
68+
token.refresh_from_db()
69+
self.assertEqual(token.description, 'New Description')
470

571

672
class UserConfigTest(TestCase):

0 commit comments

Comments
 (0)