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>
This commit is contained in:
Jeremy Stretch 2024-07-26 07:58:14 -04:00
parent 93cebae55c
commit cb59f6e6f7
4 changed files with 71 additions and 3 deletions

View File

@ -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.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers from rest_framework import serializers
@ -61,6 +61,14 @@ class UserSerializer(ValidatedModelSerializer):
'password': {'write_only': True} '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): def create(self, validated_data):
""" """
Extract the password from validated data and set it separately to ensure proper hash generation. Extract the password from validated data and set it separately to ensure proper hash generation.

View File

@ -1,6 +1,6 @@
from django import forms from django import forms
from django.conf import settings 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.contrib.postgres.forms import SimpleArrayField
from django.core.exceptions import FieldError from django.core.exceptions import FieldError
from django.utils.safestring import mark_safe 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']: 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.")) 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): class GroupForm(forms.ModelForm):
users = DynamicModelMultipleChoiceField( users = DynamicModelMultipleChoiceField(

View File

@ -1,4 +1,5 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.test import override_settings
from django.urls import reverse from django.urls import reverse
from core.models import ObjectType from core.models import ObjectType
@ -93,6 +94,31 @@ class UserTest(APIViewTestCases.APIViewTestCase):
user.refresh_from_db() user.refresh_from_db()
self.assertTrue(user.check_password(data['password'])) 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): class GroupTest(APIViewTestCases.APIViewTestCase):
model = Group model = Group

View File

@ -1,6 +1,8 @@
from django.test import override_settings
from core.models import ObjectType from core.models import ObjectType
from users.models import * 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( class UserTestCase(
@ -58,6 +60,34 @@ class UserTestCase(
'last_name': 'newlastname', '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( class GroupTestCase(
ViewTestCases.GetObjectViewTestCase, ViewTestCases.GetObjectViewTestCase,