Skip to content

Commit cb59f6e

Browse files
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 <[email protected]>
1 parent 93cebae commit cb59f6e

File tree

4 files changed

+71
-3
lines changed

4 files changed

+71
-3
lines changed

netbox/users/api/serializers_/users.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from django.contrib.auth import get_user_model
1+
from django.contrib.auth import get_user_model, password_validation
22
from drf_spectacular.types import OpenApiTypes
33
from drf_spectacular.utils import extend_schema_field
44
from rest_framework import serializers
@@ -61,6 +61,14 @@ class Meta:
6161
'password': {'write_only': True}
6262
}
6363

64+
def validate(self, data):
65+
66+
# Enforce password validation rules (if configured)
67+
if not self.nested and data.get('password'):
68+
password_validation.validate_password(data['password'], self.instance)
69+
70+
return super().validate(data)
71+
6472
def create(self, validated_data):
6573
"""
6674
Extract the password from validated data and set it separately to ensure proper hash generation.

netbox/users/forms/model_forms.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from django import forms
22
from django.conf import settings
3-
from django.contrib.auth import get_user_model
3+
from django.contrib.auth import get_user_model, password_validation
44
from django.contrib.postgres.forms import SimpleArrayField
55
from django.core.exceptions import FieldError
66
from django.utils.safestring import mark_safe
@@ -227,6 +227,10 @@ def clean(self):
227227
if self.cleaned_data['password'] and self.cleaned_data['password'] != self.cleaned_data['confirm_password']:
228228
raise forms.ValidationError(_("Passwords do not match! Please check your input and try again."))
229229

230+
# Enforce password validation rules (if configured)
231+
if self.cleaned_data['password']:
232+
password_validation.validate_password(self.cleaned_data['password'], self.instance)
233+
230234

231235
class GroupForm(forms.ModelForm):
232236
users = DynamicModelMultipleChoiceField(

netbox/users/tests/test_api.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django.contrib.auth import get_user_model
2+
from django.test import override_settings
23
from django.urls import reverse
34

45
from core.models import ObjectType
@@ -93,6 +94,31 @@ def test_that_password_is_changed(self):
9394
user.refresh_from_db()
9495
self.assertTrue(user.check_password(data['password']))
9596

97+
@override_settings(AUTH_PASSWORD_VALIDATORS=[{
98+
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
99+
'OPTIONS': {'min_length': 8}
100+
}])
101+
def test_password_validation_enforced(self):
102+
"""
103+
Test that any configured password validation rules (AUTH_PASSWORD_VALIDATORS) are enforced.
104+
"""
105+
self.add_permissions('users.add_user')
106+
107+
data = {
108+
'username': 'new_user',
109+
'password': 'foo',
110+
}
111+
url = reverse('users-api:user-list')
112+
113+
# Password too short
114+
response = self.client.post(url, data, format='json', **self.header)
115+
self.assertEqual(response.status_code, 400)
116+
117+
# Password long enough
118+
data['password'] = 'foobar123'
119+
response = self.client.post(url, data, format='json', **self.header)
120+
self.assertEqual(response.status_code, 201)
121+
96122

97123
class GroupTest(APIViewTestCases.APIViewTestCase):
98124
model = Group

netbox/users/tests/test_views.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
from django.test import override_settings
2+
13
from core.models import ObjectType
24
from users.models import *
3-
from utilities.testing import ViewTestCases, create_test_user
5+
from utilities.testing import ViewTestCases, create_test_user, extract_form_failures
46

57

68
class UserTestCase(
@@ -58,6 +60,34 @@ def setUpTestData(cls):
5860
'last_name': 'newlastname',
5961
}
6062

63+
@override_settings(AUTH_PASSWORD_VALIDATORS=[{
64+
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
65+
'OPTIONS': {'min_length': 8}
66+
}])
67+
def test_password_validation_enforced(self):
68+
"""
69+
Test that any configured password validation rules (AUTH_PASSWORD_VALIDATORS) are enforced.
70+
"""
71+
self.add_permissions('users.add_user')
72+
data = {
73+
'username': 'new_user',
74+
'password': 'foo',
75+
'confirm_password': 'foo',
76+
}
77+
78+
# Password too short
79+
request = {
80+
'path': self._get_url('add'),
81+
'data': data,
82+
}
83+
response = self.client.post(**request)
84+
self.assertHttpStatus(response, 200)
85+
86+
# Password long enough
87+
data['password'] = 'foobar123'
88+
data['confirm_password'] = 'foobar123'
89+
self.assertHttpStatus(self.client.post(**request), 302)
90+
6191

6292
class GroupTestCase(
6393
ViewTestCases.GetObjectViewTestCase,

0 commit comments

Comments
 (0)