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

Fixes #16964: Ensure configured password validators are enforced (#16990)

* Closes #16964: Validate password when creating a new user or updating password for an existing user

* Add serializer validation & tests

---------

Co-authored-by: Nishant Gaglani <nishantgaglani@gmail.com>
Jeremy Stretch 1 год назад
Родитель
Сommit
cb59f6e6f7

+ 9 - 1
netbox/users/api/serializers_/users.py

@@ -1,4 +1,4 @@
-from django.contrib.auth import get_user_model
+from django.contrib.auth import get_user_model, password_validation
 from drf_spectacular.types import OpenApiTypes
 from drf_spectacular.utils import extend_schema_field
 from rest_framework import serializers
@@ -61,6 +61,14 @@ class UserSerializer(ValidatedModelSerializer):
             'password': {'write_only': True}
         }
 
+    def validate(self, data):
+
+        # Enforce password validation rules (if configured)
+        if not self.nested and data.get('password'):
+            password_validation.validate_password(data['password'], self.instance)
+
+        return super().validate(data)
+
     def create(self, validated_data):
         """
         Extract the password from validated data and set it separately to ensure proper hash generation.

+ 5 - 1
netbox/users/forms/model_forms.py

@@ -1,6 +1,6 @@
 from django import forms
 from django.conf import settings
-from django.contrib.auth import get_user_model
+from django.contrib.auth import get_user_model, password_validation
 from django.contrib.postgres.forms import SimpleArrayField
 from django.core.exceptions import FieldError
 from django.utils.safestring import mark_safe
@@ -227,6 +227,10 @@ class UserForm(forms.ModelForm):
         if self.cleaned_data['password'] and self.cleaned_data['password'] != self.cleaned_data['confirm_password']:
             raise forms.ValidationError(_("Passwords do not match! Please check your input and try again."))
 
+        # Enforce password validation rules (if configured)
+        if self.cleaned_data['password']:
+            password_validation.validate_password(self.cleaned_data['password'], self.instance)
+
 
 class GroupForm(forms.ModelForm):
     users = DynamicModelMultipleChoiceField(

+ 26 - 0
netbox/users/tests/test_api.py

@@ -1,4 +1,5 @@
 from django.contrib.auth import get_user_model
+from django.test import override_settings
 from django.urls import reverse
 
 from core.models import ObjectType
@@ -93,6 +94,31 @@ class UserTest(APIViewTestCases.APIViewTestCase):
         user.refresh_from_db()
         self.assertTrue(user.check_password(data['password']))
 
+    @override_settings(AUTH_PASSWORD_VALIDATORS=[{
+        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+        'OPTIONS': {'min_length': 8}
+    }])
+    def test_password_validation_enforced(self):
+        """
+        Test that any configured password validation rules (AUTH_PASSWORD_VALIDATORS) are enforced.
+        """
+        self.add_permissions('users.add_user')
+
+        data = {
+            'username': 'new_user',
+            'password': 'foo',
+        }
+        url = reverse('users-api:user-list')
+
+        # Password too short
+        response = self.client.post(url, data, format='json', **self.header)
+        self.assertEqual(response.status_code, 400)
+
+        # Password long enough
+        data['password'] = 'foobar123'
+        response = self.client.post(url, data, format='json', **self.header)
+        self.assertEqual(response.status_code, 201)
+
 
 class GroupTest(APIViewTestCases.APIViewTestCase):
     model = Group

+ 31 - 1
netbox/users/tests/test_views.py

@@ -1,6 +1,8 @@
+from django.test import override_settings
+
 from core.models import ObjectType
 from users.models import *
-from utilities.testing import ViewTestCases, create_test_user
+from utilities.testing import ViewTestCases, create_test_user, extract_form_failures
 
 
 class UserTestCase(
@@ -58,6 +60,34 @@ class UserTestCase(
             'last_name': 'newlastname',
         }
 
+    @override_settings(AUTH_PASSWORD_VALIDATORS=[{
+        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+        'OPTIONS': {'min_length': 8}
+    }])
+    def test_password_validation_enforced(self):
+        """
+        Test that any configured password validation rules (AUTH_PASSWORD_VALIDATORS) are enforced.
+        """
+        self.add_permissions('users.add_user')
+        data = {
+            'username': 'new_user',
+            'password': 'foo',
+            'confirm_password': 'foo',
+        }
+
+        # Password too short
+        request = {
+            'path': self._get_url('add'),
+            'data': data,
+        }
+        response = self.client.post(**request)
+        self.assertHttpStatus(response, 200)
+
+        # Password long enough
+        data['password'] = 'foobar123'
+        data['confirm_password'] = 'foobar123'
+        self.assertHttpStatus(self.client.post(**request), 302)
+
 
 class GroupTestCase(
     ViewTestCases.GetObjectViewTestCase,