diff --git a/common/djangoapps/student/management/commands/manage_user.py b/common/djangoapps/student/management/commands/manage_user.py index dcfdf79af4..58ee73ab27 100644 --- a/common/djangoapps/student/management/commands/manage_user.py +++ b/common/djangoapps/student/management/commands/manage_user.py @@ -4,6 +4,7 @@ Django users, set/unset permission bits, and associate groups by name. """ from django.contrib.auth import get_user_model +from django.contrib.auth.hashers import is_password_usable from django.contrib.auth.models import Group, BaseUserManager from django.core.management.base import BaseCommand, CommandError from django.db import transaction @@ -24,6 +25,7 @@ class Command(BaseCommand): parser.add_argument('--superuser', dest='is_superuser', action='store_true') parser.add_argument('--staff', dest='is_staff', action='store_true') parser.add_argument('--unusable-password', dest='unusable_password', action='store_true') + parser.add_argument('--initial-password-hash', dest='initial_password_hash') parser.add_argument('-g', '--groups', nargs='*', default=[]) def _maybe_update(self, user, attribute, new_value): @@ -69,7 +71,8 @@ class Command(BaseCommand): user.delete() @transaction.atomic - def handle(self, username, email, is_remove, is_staff, is_superuser, groups, unusable_password, *args, **options): + def handle(self, username, email, is_remove, is_staff, is_superuser, groups, + unusable_password, initial_password_hash, *args, **options): if is_remove: return self._handle_remove(username, email) @@ -81,10 +84,15 @@ class Command(BaseCommand): ) if created: - # Set the password to a random, unknown, but usable password - # allowing self-service password resetting. Cases where unusable - # passwords are required, should be explicit, and will be handled below. - user.set_password(BaseUserManager().make_random_password(25)) + if initial_password_hash: + if not is_password_usable(initial_password_hash): + raise CommandError('The password hash provided for user {} is invalid.'.format(username)) + user.password = initial_password_hash + else: + # Set the password to a random, unknown, but usable password + # allowing self-service password resetting. Cases where unusable + # passwords are required, should be explicit, and will be handled below. + user.set_password(BaseUserManager().make_random_password(25)) self.stderr.write(_('Created new user: "{}"').format(user)) else: # NOTE, we will not update the email address of an existing user. diff --git a/common/djangoapps/student/management/tests/test_manage_user.py b/common/djangoapps/student/management/tests/test_manage_user.py index 3b3186e250..53af1a3547 100644 --- a/common/djangoapps/student/management/tests/test_manage_user.py +++ b/common/djangoapps/student/management/tests/test_manage_user.py @@ -4,6 +4,7 @@ Unit tests for user_management management commands. import itertools import ddt +from django.contrib.auth.hashers import make_password from django.contrib.auth.models import Group, User from django.core.management import call_command, CommandError from django.test import TestCase @@ -71,6 +72,33 @@ class TestManageUserCommand(TestCase): call_command('manage_user', TEST_USERNAME, TEST_EMAIL, '--unusable-password') self.assertFalse(user.has_usable_password()) + def test_initial_password_hash(self): + """ + Ensure that a user's password hash is set correctly when the user is created, + and that it isn't touched for existing users. + """ + initial_hash = make_password('hunter2') + + # Make sure the command aborts if the provided hash isn't a valid Django password hash + with self.assertRaises(CommandError) as exc_context: + call_command('manage_user', TEST_USERNAME, TEST_EMAIL, '--initial-password-hash', 'invalid_hash') + self.assertIn('password hash', str(exc_context.exception).lower()) + + # Make sure the hash gets set correctly for a new user + call_command('manage_user', TEST_USERNAME, TEST_EMAIL, '--initial-password-hash', initial_hash) + user = User.objects.get(username=TEST_USERNAME) + self.assertEqual(user.password, initial_hash) + + # Change the password + new_hash = make_password('correct horse battery staple') + user.password = new_hash + user.save() + + # Verify that calling manage_user again leaves the password untouched + call_command('manage_user', TEST_USERNAME, TEST_EMAIL, '--initial-password-hash', initial_hash) + user = User.objects.get(username=TEST_USERNAME) + self.assertEqual(user.password, new_hash) + def test_wrong_email(self): """ Ensure that the operation is aborted if the username matches an