mirror of
https://github.com/django/django.git
synced 2025-06-01 09:39:12 +00:00
Fixed #25089 -- Added password validation to createsuperuser/changepassword.
This commit is contained in:
parent
264eeaf14a
commit
53d28f8339
@ -3,6 +3,8 @@ from __future__ import unicode_literals
|
|||||||
import getpass
|
import getpass
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
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.core.management.base import BaseCommand, CommandError
|
||||||
from django.db import DEFAULT_DB_ALIAS
|
from django.db import DEFAULT_DB_ALIAS
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
@ -46,12 +48,22 @@ class Command(BaseCommand):
|
|||||||
MAX_TRIES = 3
|
MAX_TRIES = 3
|
||||||
count = 0
|
count = 0
|
||||||
p1, p2 = 1, 2 # To make them initially mismatch.
|
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()
|
p1 = self._get_pass()
|
||||||
p2 = self._get_pass("Password (again): ")
|
p2 = self._get_pass("Password (again): ")
|
||||||
if p1 != p2:
|
if p1 != p2:
|
||||||
self.stdout.write("Passwords do not match. Please try again.\n")
|
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:
|
if count == MAX_TRIES:
|
||||||
raise CommandError("Aborting password change for user '%s' after %s attempts" % (u, count))
|
raise CommandError("Aborting password change for user '%s' after %s attempts" % (u, count))
|
||||||
|
@ -8,6 +8,7 @@ import sys
|
|||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.management import get_default_username
|
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 import exceptions
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
from django.db import DEFAULT_DB_ALIAS
|
from django.db import DEFAULT_DB_ALIAS
|
||||||
@ -56,6 +57,9 @@ class Command(BaseCommand):
|
|||||||
# If not provided, create the user with an unusable password
|
# If not provided, create the user with an unusable password
|
||||||
password = None
|
password = None
|
||||||
user_data = {}
|
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
|
# Do quick and dirty validation if --noinput
|
||||||
if not options['interactive']:
|
if not options['interactive']:
|
||||||
@ -121,7 +125,13 @@ class Command(BaseCommand):
|
|||||||
field.remote_field.field_name,
|
field.remote_field.field_name,
|
||||||
) if field.remote_field else '',
|
) 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
|
# Get a password
|
||||||
while password is None:
|
while password is None:
|
||||||
@ -130,13 +140,21 @@ class Command(BaseCommand):
|
|||||||
if password != password2:
|
if password != password2:
|
||||||
self.stderr.write("Error: Your passwords didn't match.")
|
self.stderr.write("Error: Your passwords didn't match.")
|
||||||
password = None
|
password = None
|
||||||
|
# Don't validate passwords that don't match.
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if password.strip() == '':
|
if password.strip() == '':
|
||||||
self.stderr.write("Error: Blank passwords aren't allowed.")
|
self.stderr.write("Error: Blank passwords aren't allowed.")
|
||||||
password = None
|
password = None
|
||||||
|
# Don't validate blank passwords.
|
||||||
continue
|
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:
|
except KeyboardInterrupt:
|
||||||
self.stderr.write("\nOperation cancelled.")
|
self.stderr.write("\nOperation cancelled.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
@ -43,6 +43,8 @@ def mock_inputs(inputs):
|
|||||||
if six.PY2:
|
if six.PY2:
|
||||||
# getpass on Windows only supports prompt as bytestring (#19807)
|
# getpass on Windows only supports prompt as bytestring (#19807)
|
||||||
assert isinstance(prompt, six.binary_type)
|
assert isinstance(prompt, six.binary_type)
|
||||||
|
if callable(inputs['password']):
|
||||||
|
return inputs['password']()
|
||||||
return inputs['password']
|
return inputs['password']
|
||||||
|
|
||||||
def mock_input(prompt):
|
def mock_input(prompt):
|
||||||
@ -107,6 +109,9 @@ class GetDefaultUsernameTestCase(TestCase):
|
|||||||
self.assertEqual(management.get_default_username(), 'julia')
|
self.assertEqual(management.get_default_username(), 'julia')
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(AUTH_PASSWORD_VALIDATORS=[
|
||||||
|
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
|
||||||
|
])
|
||||||
class ChangepasswordManagementCommandTestCase(TestCase):
|
class ChangepasswordManagementCommandTestCase(TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -139,11 +144,24 @@ class ChangepasswordManagementCommandTestCase(TestCase):
|
|||||||
mismatched passwords three times.
|
mismatched passwords three times.
|
||||||
"""
|
"""
|
||||||
command = changepassword.Command()
|
command = changepassword.Command()
|
||||||
command._get_pass = lambda *args: args or 'foo'
|
command._get_pass = lambda *args: str(args) or 'foo'
|
||||||
|
|
||||||
with self.assertRaises(CommandError):
|
with self.assertRaises(CommandError):
|
||||||
command.execute(username="joe", stdout=self.stdout, stderr=self.stderr)
|
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):
|
def test_that_changepassword_command_works_with_nonascii_output(self):
|
||||||
"""
|
"""
|
||||||
#21627 -- Executing the changepassword management command should allow
|
#21627 -- Executing the changepassword management command should allow
|
||||||
@ -158,7 +176,10 @@ class ChangepasswordManagementCommandTestCase(TestCase):
|
|||||||
command.execute(username="J\xfalia", stdout=self.stdout)
|
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):
|
class CreatesuperuserManagementCommandTestCase(TestCase):
|
||||||
|
|
||||||
def test_basic_usage(self):
|
def test_basic_usage(self):
|
||||||
@ -443,6 +464,39 @@ class CreatesuperuserManagementCommandTestCase(TestCase):
|
|||||||
|
|
||||||
test(self)
|
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):
|
class CustomUserModelValidationTestCase(SimpleTestCase):
|
||||||
@override_settings(AUTH_USER_MODEL='auth.CustomUserNonListRequiredFields')
|
@override_settings(AUTH_USER_MODEL='auth.CustomUserNonListRequiredFields')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user