From 53d28f83390aed98fab4eed187eaf1edaa92c6d0 Mon Sep 17 00:00:00 2001
From: Alex Becker <acbecker@uchicago.edu>
Date: Thu, 9 Jul 2015 01:15:05 -0500
Subject: [PATCH] Fixed #25089 -- Added password validation to
 createsuperuser/changepassword.

---
 .../management/commands/changepassword.py     | 16 ++++-
 .../management/commands/createsuperuser.py    | 20 ++++++-
 tests/auth_tests/test_management.py           | 58 ++++++++++++++++++-
 3 files changed, 89 insertions(+), 5 deletions(-)

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')