Просмотр исходного кода

fix(users): Deep copy default preferences on UserConfig creation (#23285)

Prevents shared mutable dictionary references when creating UserConfig
from DEFAULT_USER_PREFERENCES. Uses copy.deepcopy to ensure each user
gets an independent copy that can be modified without affecting defaults.

Fixes #23283
Martin Hauser 1 день назад
Родитель
Сommit
a3df6db89f
2 измененных файлов с 27 добавлено и 4 удалено
  1. 3 2
      netbox/account/views.py
  2. 24 2
      netbox/users/tests/test_preferences.py

+ 3 - 2
netbox/account/views.py

@@ -1,3 +1,4 @@
+import copy
 import logging
 
 from django.conf import settings
@@ -130,8 +131,8 @@ class LoginView(View):
             # Ensure the user has a UserConfig defined. (This should normally be handled by
             # create_userconfig() on user creation.)
             if not hasattr(request.user, 'config'):
-                request.user.config = get_config()
-                UserConfig(user=request.user, data=request.user.config.DEFAULT_USER_PREFERENCES).save()
+                config = get_config()
+                UserConfig(user=request.user, data=copy.deepcopy(config.DEFAULT_USER_PREFERENCES)).save()
 
             response = self.redirect_to_next(request, logger)
 

+ 24 - 2
netbox/users/tests/test_preferences.py

@@ -1,10 +1,12 @@
-from django.test import override_settings
+from django.conf import settings
+from django.test import override_settings, tag
 from django.test.client import RequestFactory
 from django.urls import reverse
 
 from dcim.models import Site
 from dcim.tables import SiteTable
-from users.models import User
+from netbox.config import get_config
+from users.models import User, UserConfig
 from users.preferences import UserPreference
 from utilities.testing import TestCase
 
@@ -43,6 +45,26 @@ class UserPreferencesTestCase(TestCase):
 
         self.assertEqual(userconfig.data, DEFAULT_USER_PREFERENCES)
 
+    @tag('regression')  # Ref: #23283
+    @override_settings(DEFAULT_USER_PREFERENCES={'locale': {'language': 'de'}, 'pagination': {'per_page': 25}})
+    def test_login_creates_missing_userconfig(self):
+        """A login without a UserConfig creates one from a copy of the default preferences."""
+        user = User.objects.create_user(username='User 1', password='pw')
+        UserConfig.objects.filter(user=user).delete()
+
+        self.client.logout()
+        response = self.client.post(reverse('login'), {'username': 'User 1', 'password': 'pw'})
+
+        defaults = {'locale': {'language': 'de'}, 'pagination': {'per_page': 25}}
+        self.assertEqual(response.status_code, 302)
+        self.assertEqual(UserConfig.objects.get(user=user).data, defaults)
+        self.assertEqual(response.cookies[settings.LANGUAGE_COOKIE_NAME].value, 'de')
+
+        userconfig = response.wsgi_request.user.config
+        userconfig.set('pagination.per_page', 50)
+        userconfig.clear('locale.language')
+        self.assertEqual(get_config().DEFAULT_USER_PREFERENCES, defaults)
+
     def test_table_ordering(self):
         url = reverse('dcim:site_list')
         response = self.client.get(f"{url}?sort=status")