diff --git a/netbox/users/api/serializers_/users.py b/netbox/users/api/serializers_/users.py index 2273b2d5a..eafa27a75 100644 --- a/netbox/users/api/serializers_/users.py +++ b/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. diff --git a/netbox/users/forms/model_forms.py b/netbox/users/forms/model_forms.py index 7a9f63ea7..0c28621e1 100644 --- a/netbox/users/forms/model_forms.py +++ b/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( diff --git a/netbox/users/tests/test_api.py b/netbox/users/tests/test_api.py index 4ebe64b32..bf1d93a8f 100644 --- a/netbox/users/tests/test_api.py +++ b/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 diff --git a/netbox/users/tests/test_views.py b/netbox/users/tests/test_views.py index 8711e2b44..3dabc9dae 100644 --- a/netbox/users/tests/test_views.py +++ b/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,