Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions netbox/users/models/tokens.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -86,6 +88,24 @@ 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:
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()
Expand Down
68 changes: 67 additions & 1 deletion netbox/users/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
Loading