Commit 53d28f83 authored by Alex Becker's avatar Alex Becker Committed by Tim Graham
Browse files

Fixed #25089 -- Added password validation to createsuperuser/changepassword.

parent 264eeaf1
Loading
Loading
Loading
Loading
+14 −2
Original line number Diff line number Diff line
@@ -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))
+19 −1
Original line number Diff line number Diff line
@@ -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)
+56 −2
Original line number Diff line number Diff line
@@ -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')