diff --git a/django/contrib/auth/management/commands/changepassword.py b/django/contrib/auth/management/commands/changepassword.py index 647b7b1728..600911f759 100644 --- a/django/contrib/auth/management/commands/changepassword.py +++ b/django/contrib/auth/management/commands/changepassword.py @@ -3,6 +3,8 @@ from __future__ import unicode_literals import getpass from django.contrib.auth import get_user_model +from django.contrib.auth.password_validation import validate_password +from django.core.exceptions import ValidationError from django.core.management.base import BaseCommand, CommandError from django.db import DEFAULT_DB_ALIAS from django.utils.encoding import force_str @@ -46,12 +48,22 @@ class Command(BaseCommand): MAX_TRIES = 3 count = 0 p1, p2 = 1, 2 # To make them initially mismatch. - while p1 != p2 and count < MAX_TRIES: + password_validated = False + while (p1 != p2 or not password_validated) and count < MAX_TRIES: p1 = self._get_pass() p2 = self._get_pass("Password (again): ") if p1 != p2: self.stdout.write("Passwords do not match. Please try again.\n") - count = count + 1 + count += 1 + # Don't validate passwords that don't match. + continue + try: + validate_password(p2, u) + except ValidationError as err: + self.stdout.write(', '.join(err.messages)) + count += 1 + else: + password_validated = True if count == MAX_TRIES: raise CommandError("Aborting password change for user '%s' after %s attempts" % (u, count)) diff --git a/django/contrib/auth/management/commands/createsuperuser.py b/django/contrib/auth/management/commands/createsuperuser.py index ae6ea0e48a..558ee64d9f 100644 --- a/django/contrib/auth/management/commands/createsuperuser.py +++ b/django/contrib/auth/management/commands/createsuperuser.py @@ -8,6 +8,7 @@ import sys from django.contrib.auth import get_user_model from django.contrib.auth.management import get_default_username +from django.contrib.auth.password_validation import validate_password from django.core import exceptions from django.core.management.base import BaseCommand, CommandError from django.db import DEFAULT_DB_ALIAS @@ -56,6 +57,9 @@ class Command(BaseCommand): # If not provided, create the user with an unusable password password = None user_data = {} + # Same as user_data but with foreign keys as fake model instances + # instead of raw IDs. + fake_user_data = {} # Do quick and dirty validation if --noinput if not options['interactive']: @@ -121,7 +125,13 @@ class Command(BaseCommand): field.remote_field.field_name, ) if field.remote_field else '', )) - user_data[field_name] = self.get_input_data(field, message) + input_value = self.get_input_data(field, message) + user_data[field_name] = input_value + fake_user_data[field_name] = input_value + + # Wrap any foreign keys in fake model instances + if field.remote_field: + fake_user_data[field_name] = field.remote_field.model(input_value) # Get a password while password is None: @@ -130,13 +140,21 @@ class Command(BaseCommand): if password != password2: self.stderr.write("Error: Your passwords didn't match.") password = None + # Don't validate passwords that don't match. continue if password.strip() == '': self.stderr.write("Error: Blank passwords aren't allowed.") password = None + # Don't validate blank passwords. continue + try: + validate_password(password2, self.UserModel(**fake_user_data)) + except exceptions.ValidationError as err: + self.stderr.write(', '.join(err.messages)) + password = None + except KeyboardInterrupt: self.stderr.write("\nOperation cancelled.") sys.exit(1) diff --git a/tests/auth_tests/test_management.py b/tests/auth_tests/test_management.py index f45703fcfd..891cfda50c 100644 --- a/tests/auth_tests/test_management.py +++ b/tests/auth_tests/test_management.py @@ -43,6 +43,8 @@ def mock_inputs(inputs): if six.PY2: # getpass on Windows only supports prompt as bytestring (#19807) assert isinstance(prompt, six.binary_type) + if callable(inputs['password']): + return inputs['password']() return inputs['password'] def mock_input(prompt): @@ -107,6 +109,9 @@ class GetDefaultUsernameTestCase(TestCase): self.assertEqual(management.get_default_username(), 'julia') +@override_settings(AUTH_PASSWORD_VALIDATORS=[ + {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, +]) class ChangepasswordManagementCommandTestCase(TestCase): def setUp(self): @@ -139,11 +144,24 @@ class ChangepasswordManagementCommandTestCase(TestCase): mismatched passwords three times. """ command = changepassword.Command() - command._get_pass = lambda *args: args or 'foo' + command._get_pass = lambda *args: str(args) or 'foo' with self.assertRaises(CommandError): command.execute(username="joe", stdout=self.stdout, stderr=self.stderr) + def test_password_validation(self): + """ + A CommandError should be raised if the user enters in passwords which + fail validation three times. + """ + command = changepassword.Command() + command._get_pass = lambda *args: '1234567890' + + abort_msg = "Aborting password change for user 'joe' after 3 attempts" + with self.assertRaisesMessage(CommandError, abort_msg): + command.execute(username="joe", stdout=self.stdout, stderr=self.stderr) + self.assertIn('This password is entirely numeric.', self.stdout.getvalue()) + def test_that_changepassword_command_works_with_nonascii_output(self): """ #21627 -- Executing the changepassword management command should allow @@ -158,7 +176,10 @@ class ChangepasswordManagementCommandTestCase(TestCase): command.execute(username="J\xfalia", stdout=self.stdout) -@override_settings(SILENCED_SYSTEM_CHECKS=['fields.W342']) # ForeignKey(unique=True) +@override_settings( + SILENCED_SYSTEM_CHECKS=['fields.W342'], # ForeignKey(unique=True) + AUTH_PASSWORD_VALIDATORS=[{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}], +) class CreatesuperuserManagementCommandTestCase(TestCase): def test_basic_usage(self): @@ -443,6 +464,39 @@ class CreatesuperuserManagementCommandTestCase(TestCase): test(self) + def test_password_validation(self): + """ + Creation should fail if the password fails validation. + """ + new_io = six.StringIO() + # Returns '1234567890' the first two times it is called, then + # 'password' subsequently. + def bad_then_good_password(index=[0]): + index[0] += 1 + if index[0] <= 2: + return '1234567890' + return 'password' + + @mock_inputs({ + 'password': bad_then_good_password, + 'username': 'joe1234567890', + }) + def test(self): + call_command( + "createsuperuser", + interactive=True, + stdin=MockTTY(), + stdout=new_io, + stderr=new_io, + ) + self.assertEqual( + new_io.getvalue().strip(), + "This password is entirely numeric.\n" + "Superuser created successfully." + ) + + test(self) + class CustomUserModelValidationTestCase(SimpleTestCase): @override_settings(AUTH_USER_MODEL='auth.CustomUserNonListRequiredFields')