Browse Source

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 4 ngày trước cách đây
mục cha
commit
a3df6db89f

+ 3 - 2
netbox/account/views.py

@@ -1,3 +1,4 @@
+import copy
 import logging
 import logging
 
 
 from django.conf import settings
 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
             # Ensure the user has a UserConfig defined. (This should normally be handled by
             # create_userconfig() on user creation.)
             # create_userconfig() on user creation.)
             if not hasattr(request.user, 'config'):
             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)
             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.test.client import RequestFactory
 from django.urls import reverse
 from django.urls import reverse
 
 
 from dcim.models import Site
 from dcim.models import Site
 from dcim.tables import SiteTable
 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 users.preferences import UserPreference
 from utilities.testing import TestCase
 from utilities.testing import TestCase
 
 
@@ -43,6 +45,26 @@ class UserPreferencesTestCase(TestCase):
 
 
         self.assertEqual(userconfig.data, DEFAULT_USER_PREFERENCES)
         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):
     def test_table_ordering(self):
         url = reverse('dcim:site_list')
         url = reverse('dcim:site_list')
         response = self.client.get(f"{url}?sort=status")
         response = self.client.get(f"{url}?sort=status")