Merge branch 'wip_MDL-47830_m29_pwrotation' of https://github.com/skodak/moodle

This commit is contained in:
Sam Hemelryk 2014-12-02 09:52:45 +13:00 committed by Dan Poltawski
commit 6c4e70a6d8
15 changed files with 342 additions and 5 deletions

View File

@ -70,6 +70,11 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
$temp->add(new admin_setting_configtext('minpasswordupper', new lang_string('minpasswordupper', 'admin'), new lang_string('configminpasswordupper', 'admin'), 1, PARAM_INT));
$temp->add(new admin_setting_configtext('minpasswordnonalphanum', new lang_string('minpasswordnonalphanum', 'admin'), new lang_string('configminpasswordnonalphanum', 'admin'), 1, PARAM_INT));
$temp->add(new admin_setting_configtext('maxconsecutiveidentchars', new lang_string('maxconsecutiveidentchars', 'admin'), new lang_string('configmaxconsecutiveidentchars', 'admin'), 0, PARAM_INT));
$temp->add(new admin_setting_configtext('passwordreuselimit',
new lang_string('passwordreuselimit', 'admin'),
new lang_string('passwordreuselimit_desc', 'admin'), 0, PARAM_INT));
$pwresetoptions = array(
300 => new lang_string('numminutes', '', 5),
900 => new lang_string('numminutes', '', 15),

View File

@ -89,6 +89,7 @@ class auth_plugin_email extends auth_plugin_base {
require_once($CFG->dirroot.'/user/profile/lib.php');
require_once($CFG->dirroot.'/user/lib.php');
$plainpassword = $user->password;
$user->password = hash_internal_user_password($user->password);
if (empty($user->calendartype)) {
$user->calendartype = $CFG->calendartype;
@ -96,6 +97,8 @@ class auth_plugin_email extends auth_plugin_base {
$user->id = user_create_user($user, false, false);
user_add_password_history($user->id, $plainpassword);
// Save any custom profile field information.
profile_save_data($user);

View File

@ -539,6 +539,7 @@ class auth_plugin_ldap extends auth_plugin_base {
global $CFG, $DB, $PAGE, $OUTPUT;
require_once($CFG->dirroot.'/user/profile/lib.php');
require_once($CFG->dirroot.'/user/lib.php');
if ($this->user_exists($user->username)) {
print_error('auth_ldap_user_exists', 'auth_ldap');
@ -553,6 +554,8 @@ class auth_plugin_ldap extends auth_plugin_base {
$user->id = user_create_user($user, false, false);
user_add_password_history($user->id, $plainslashedpassword);
// Save any custom profile field information
profile_save_data($user);

View File

@ -5,6 +5,8 @@ information provided here is intended especially for developers.
* Do not update user->firstaccess from any auth plugin, the complete_user_login() does it automatically.
* Add user_add_password_history() to user_signup() method.
=== 2.8 ===
* \core\session\manager::session_exists() now verifies the session is active

View File

@ -777,6 +777,8 @@ $string['passwordchangelogout'] = 'Log out after password change';
$string['passwordchangelogout_desc'] = 'If enabled, when a password is changed, all browser sessions are terminated, apart from the one in which the new password is specified. (This setting does not affect password changes via bulk user upload.)';
$string['passwordpolicy'] = 'Password policy';
$string['passwordresettime'] = 'Maximum time to validate password reset request';
$string['passwordreuselimit'] = 'Password rotation limit';
$string['passwordreuselimit_desc'] = 'Number of times a user must change their password before they are allowed to reuse a password. Hashes of previously used passwords are stored in local database table. This feature might not be compatible with some external authentication plugins.';
$string['pathtoclam'] = 'clam AV path';
$string['pathtodot'] = 'Path to dot';
$string['pathtodot_help'] = 'Path to dot. Probably something like /usr/bin/dot. To be able to generate graphics from DOT files, you must have installed the dot executable and point to it here. Note that, for now, this only used by the profiling features (Development->Profiling) built into Moodle.';

View File

@ -78,6 +78,7 @@ $string['errorminpassworddigits'] = 'Passwords must have at least {$a} digit(s).
$string['errorminpasswordlength'] = 'Passwords must be at least {$a} characters long.';
$string['errorminpasswordlower'] = 'Passwords must have at least {$a} lower case letter(s).';
$string['errorminpasswordnonalphanum'] = 'Passwords must have at least {$a} non-alphanumeric character(s).';
$string['errorpasswordreused'] = 'This password has been used before, and is not permitted to be reused';
$string['errorminpasswordupper'] = 'Passwords must have at least {$a} upper case letter(s).';
$string['errorpasswordupdate'] = 'Error updating password, password not changed';
$string['eventuserloggedin'] = 'User has logged in';
@ -102,6 +103,7 @@ $string['informminpassworddigits'] = 'at least {$a} digit(s)';
$string['informminpasswordlength'] = 'at least {$a} characters';
$string['informminpasswordlower'] = 'at least {$a} lower case letter(s)';
$string['informminpasswordnonalphanum'] = 'at least {$a} non-alphanumeric character(s)';
$string['informminpasswordreuselimit'] = 'Passwords can be reused after {$a} changes';
$string['informminpasswordupper'] = 'at least {$a} upper case letter(s)';
$string['informpasswordpolicy'] = 'The password must have {$a}';
$string['instructions'] = 'Instructions';

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="lib/db" VERSION="20141201" COMMENT="XMLDB file for core Moodle tables"
<XMLDB PATH="lib/db" VERSION="20141202" COMMENT="XMLDB file for core Moodle tables"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
>
@ -765,6 +765,18 @@
<INDEX NAME="courseid" UNIQUE="false" FIELDS="courseid"/>
</INDEXES>
</TABLE>
<TABLE NAME="user_password_history" COMMENT="A rotating log of hashes of previously used passwords for each user.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="hash" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="scale" COMMENT="Defines grading scales">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>

View File

@ -4058,6 +4058,7 @@ function xmldb_main_upgrade($oldversion) {
// Moodle v2.8.0 release upgrade line.
// Put any upgrade step following this.
if ($oldversion < 2014120100.00) {
// Define field sslverification to be added to mnet_host.
@ -4087,5 +4088,30 @@ function xmldb_main_upgrade($oldversion) {
// Main savepoint reached.
upgrade_main_savepoint(true, 2014120101.00);
}
if ($oldversion < 2014120102.00) {
// Define table user_password_history to be created.
$table = new xmldb_table('user_password_history');
// Adding fields to table user_password_history.
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('hash', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
$table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
// Adding keys to table user_password_history.
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
$table->add_key('userid', XMLDB_KEY_FOREIGN, array('userid'), 'user', array('id'));
// Conditionally launch create table for user_password_history.
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}
// Main savepoint reached.
upgrade_main_savepoint(true, 2014120102.00);
}
return true;
}

View File

@ -4283,6 +4283,9 @@ function delete_user(stdClass $user) {
// Purge user extra profile info.
$DB->delete_records('user_info_data', array('userid' => $user->id));
// Purge log of previous password hashes.
$DB->delete_records('user_password_history', array('userid' => $user->id));
// Last course access not necessary either.
$DB->delete_records('user_lastaccess', array('userid' => $user->id));
// Remove all user tokens.

View File

@ -25,6 +25,7 @@
*/
require('../config.php');
require_once($CFG->dirroot.'/user/lib.php');
require_once('change_password_form.php');
require_once($CFG->libdir.'/authlib.php');
@ -115,6 +116,8 @@ if ($mform->is_cancelled()) {
print_error('errorpasswordupdate', 'auth');
}
user_add_password_history($USER->id, $data->newpassword1);
if (!empty($CFG->passwordchangelogout)) {
\core\session\manager::kill_user_sessions($USER->id, session_id());
}

View File

@ -41,8 +41,15 @@ class login_change_password_form extends moodleform {
// visible elements
$mform->addElement('static', 'username', get_string('username'), $USER->username);
if (!empty($CFG->passwordpolicy)){
$mform->addElement('static', 'passwordpolicyinfo', '', print_password_policy());
$policies = array();
if (!empty($CFG->passwordpolicy)) {
$policies[] = print_password_policy();
}
if (!empty($CFG->passwordreuselimit) and $CFG->passwordreuselimit > 0) {
$policies[] = get_string('informminpasswordreuselimit', 'auth', $CFG->passwordreuselimit);
}
if ($policies) {
$mform->addElement('static', 'passwordpolicyinfo', '', implode('<br />', $policies));
}
$mform->addElement('password', 'password', get_string('oldpassword'));
$mform->addRule('password', get_string('required'), 'required', null, 'client');
@ -92,6 +99,11 @@ class login_change_password_form extends moodleform {
return $errors;
}
if (user_is_previously_used_password($USER->id, $data['newpassword1'])) {
$errors['newpassword1'] = get_string('errorpasswordreused', 'core_auth');
$errors['newpassword2'] = get_string('errorpasswordreused', 'core_auth');
}
$errmsg = '';//prevents eclipse warnings
if (!check_password_policy($data['newpassword1'], $errmsg)) {
$errors['newpassword1'] = $errmsg;

View File

@ -177,6 +177,8 @@ function core_login_process_password_reset_request() {
*/
function core_login_process_password_set($token) {
global $DB, $CFG, $OUTPUT, $PAGE, $SESSION;
require_once($CFG->dirroot.'/user/lib.php');
$pwresettime = isset($CFG->pwresettime) ? $CFG->pwresettime : 1800;
$sql = "SELECT u.*, upr.token, upr.timerequested, upr.id as tokenid
FROM {user} u
@ -239,6 +241,7 @@ function core_login_process_password_set($token) {
if (!$userauth->user_update_password($user, $data->password)) {
print_error('errorpasswordupdate', 'auth');
}
user_add_password_history($user->id, $data->password);
if (!empty($CFG->passwordchangelogout)) {
\core\session\manager::kill_user_sessions($user->id, session_id());
}

View File

@ -26,6 +26,7 @@
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/formslib.php');
require_once($CFG->dirroot.'/user/lib.php');
/**
* Set forgotten password form definition.
@ -64,8 +65,15 @@ class login_set_password_form extends moodleform {
// Visible elements.
$mform->addElement('static', 'username2', get_string('username'));
$policies = array();
if (!empty($CFG->passwordpolicy)) {
$mform->addElement('static', 'passwordpolicyinfo', '', print_password_policy());
$policies[] = print_password_policy();
}
if (!empty($CFG->passwordreuselimit) and $CFG->passwordreuselimit > 0) {
$policies[] = get_string('informminpasswordreuselimit', 'auth', $CFG->passwordreuselimit);
}
if ($policies) {
$mform->addElement('static', 'passwordpolicyinfo', '', implode('<br />', $policies));
}
$mform->addElement('password', 'password', get_string('newpassword'), $autocomplete);
$mform->addRule('password', get_string('required'), 'required', null, 'client');
@ -103,6 +111,11 @@ class login_set_password_form extends moodleform {
return $errors;
}
if (user_is_previously_used_password($USER->id, $data['password'])) {
$errors['password'] = get_string('errorpasswordreused', 'core_auth');
$errors['password2'] = get_string('passwordreused', 'core_auth');
}
return $errors;
}
}

View File

@ -878,4 +878,82 @@ function user_get_user_navigation_info($user, $page) {
}
return $returnobject;
}
}
/**
* Add password to the list of used hashes for this user.
*
* This is supposed to be used from:
* 1/ change own password form
* 2/ password reset process
* 3/ user signup in auth plugins if password changing supported
*
* @param int $userid user id
* @param string $password plaintext password
* @return void
*/
function user_add_password_history($userid, $password) {
global $CFG, $DB;
require_once($CFG->libdir.'/password_compat/lib/password.php');
if (empty($CFG->passwordreuselimit) or $CFG->passwordreuselimit < 0) {
return;
}
// Note: this is using separate code form normal password hashing because
// we need to have this under control in the future. Also the auth
// plugin might not store the passwords locally at all.
$record = new stdClass();
$record->userid = $userid;
$record->hash = password_hash($password, PASSWORD_DEFAULT);
$record->timecreated = time();
$DB->insert_record('user_password_history', $record);
$i = 0;
$records = $DB->get_records('user_password_history', array('userid' => $userid), 'timecreated DESC, id DESC');
foreach ($records as $record) {
$i++;
if ($i > $CFG->passwordreuselimit) {
$DB->delete_records('user_password_history', array('id' => $record->id));
}
}
}
/**
* Was this password used before on change or reset password page?
*
* The $CFG->passwordreuselimit setting determines
* how many times different password needs to be used
* before allowing previously used password again.
*
* @param int $userid user id
* @param string $password plaintext password
* @return bool true if password reused
*/
function user_is_previously_used_password($userid, $password) {
global $CFG, $DB;
require_once($CFG->libdir.'/password_compat/lib/password.php');
if (empty($CFG->passwordreuselimit) or $CFG->passwordreuselimit < 0) {
return false;
}
$reused = false;
$i = 0;
$records = $DB->get_records('user_password_history', array('userid' => $userid), 'timecreated DESC, id DESC');
foreach ($records as $record) {
$i++;
if ($i > $CFG->passwordreuselimit) {
$DB->delete_records('user_password_history', array('id' => $record->id));
continue;
}
// NOTE: this is slow but we cannot compare the hashes directly any more.
if (password_verify($password, $record->hash)) {
$reused = true;
}
}
return $reused;
}

View File

@ -177,4 +177,174 @@ class core_userliblib_testcase extends advanced_testcase {
$this->assertEquals(10, $count);
$this->assertEquals(10, get_user_preferences('login_failed_count_since_success', 0, $user));
}
/**
* Test function user_add_password_history().
*/
public function test_user_add_password_history() {
global $DB;
$this->resetAfterTest();
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$user3 = $this->getDataGenerator()->create_user();
$DB->delete_records('user_password_history', array());
set_config('passwordreuselimit', 0);
user_add_password_history($user1->id, 'pokus');
$this->assertEquals(0, $DB->count_records('user_password_history'));
// Test adding and discarding of old.
set_config('passwordreuselimit', 3);
user_add_password_history($user1->id, 'pokus');
$this->assertEquals(1, $DB->count_records('user_password_history'));
$this->assertEquals(1, $DB->count_records('user_password_history', array('userid' => $user1->id)));
user_add_password_history($user1->id, 'pokus2');
user_add_password_history($user1->id, 'pokus3');
user_add_password_history($user1->id, 'pokus4');
$this->assertEquals(3, $DB->count_records('user_password_history'));
$this->assertEquals(3, $DB->count_records('user_password_history', array('userid' => $user1->id)));
user_add_password_history($user2->id, 'pokus1');
$this->assertEquals(4, $DB->count_records('user_password_history'));
$this->assertEquals(3, $DB->count_records('user_password_history', array('userid' => $user1->id)));
$this->assertEquals(1, $DB->count_records('user_password_history', array('userid' => $user2->id)));
user_add_password_history($user2->id, 'pokus2');
user_add_password_history($user2->id, 'pokus3');
$this->assertEquals(3, $DB->count_records('user_password_history', array('userid' => $user2->id)));
$ids = array_keys($DB->get_records('user_password_history', array('userid' => $user2->id), 'timecreated ASC, id ASC'));
user_add_password_history($user2->id, 'pokus4');
$this->assertEquals(3, $DB->count_records('user_password_history', array('userid' => $user2->id)));
$newids = array_keys($DB->get_records('user_password_history', array('userid' => $user2->id), 'timecreated ASC, id ASC'));
$removed = array_shift($ids);
$added = array_pop($newids);
$this->assertSame($ids, $newids);
$this->assertGreaterThan($removed, $added);
// Test disabling prevents changes.
set_config('passwordreuselimit', 0);
$this->assertEquals(6, $DB->count_records('user_password_history'));
$ids = array_keys($DB->get_records('user_password_history', array('userid' => $user2->id), 'timecreated ASC, id ASC'));
user_add_password_history($user2->id, 'pokus5');
user_add_password_history($user3->id, 'pokus1');
$newids = array_keys($DB->get_records('user_password_history', array('userid' => $user2->id), 'timecreated ASC, id ASC'));
$this->assertSame($ids, $newids);
$this->assertEquals(6, $DB->count_records('user_password_history'));
set_config('passwordreuselimit', -1);
$ids = array_keys($DB->get_records('user_password_history', array('userid' => $user2->id), 'timecreated ASC, id ASC'));
user_add_password_history($user2->id, 'pokus6');
user_add_password_history($user3->id, 'pokus6');
$newids = array_keys($DB->get_records('user_password_history', array('userid' => $user2->id), 'timecreated ASC, id ASC'));
$this->assertSame($ids, $newids);
$this->assertEquals(6, $DB->count_records('user_password_history'));
}
/**
* Test function user_add_password_history().
*/
public function test_user_is_previously_used_password() {
global $DB;
$this->resetAfterTest();
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$DB->delete_records('user_password_history', array());
set_config('passwordreuselimit', 0);
user_add_password_history($user1->id, 'pokus');
$this->assertFalse(user_is_previously_used_password($user1->id, 'pokus'));
set_config('passwordreuselimit', 3);
user_add_password_history($user2->id, 'pokus1');
user_add_password_history($user2->id, 'pokus2');
user_add_password_history($user1->id, 'pokus1');
$this->assertTrue(user_is_previously_used_password($user1->id, 'pokus1'));
$this->assertFalse(user_is_previously_used_password($user1->id, 'pokus2'));
$this->assertFalse(user_is_previously_used_password($user1->id, 'pokus3'));
$this->assertFalse(user_is_previously_used_password($user1->id, 'pokus4'));
user_add_password_history($user1->id, 'pokus2');
$this->assertTrue(user_is_previously_used_password($user1->id, 'pokus1'));
$this->assertTrue(user_is_previously_used_password($user1->id, 'pokus2'));
$this->assertFalse(user_is_previously_used_password($user1->id, 'pokus3'));
$this->assertFalse(user_is_previously_used_password($user1->id, 'pokus4'));
user_add_password_history($user1->id, 'pokus3');
$this->assertTrue(user_is_previously_used_password($user1->id, 'pokus1'));
$this->assertTrue(user_is_previously_used_password($user1->id, 'pokus2'));
$this->assertTrue(user_is_previously_used_password($user1->id, 'pokus3'));
$this->assertFalse(user_is_previously_used_password($user1->id, 'pokus4'));
user_add_password_history($user1->id, 'pokus4');
$this->assertFalse(user_is_previously_used_password($user1->id, 'pokus1'));
$this->assertTrue(user_is_previously_used_password($user1->id, 'pokus2'));
$this->assertTrue(user_is_previously_used_password($user1->id, 'pokus3'));
$this->assertTrue(user_is_previously_used_password($user1->id, 'pokus4'));
set_config('passwordreuselimit', 2);
$this->assertFalse(user_is_previously_used_password($user1->id, 'pokus1'));
$this->assertFalse(user_is_previously_used_password($user1->id, 'pokus2'));
$this->assertTrue(user_is_previously_used_password($user1->id, 'pokus3'));
$this->assertTrue(user_is_previously_used_password($user1->id, 'pokus4'));
set_config('passwordreuselimit', 3);
$this->assertFalse(user_is_previously_used_password($user1->id, 'pokus1'));
$this->assertFalse(user_is_previously_used_password($user1->id, 'pokus2'));
$this->assertTrue(user_is_previously_used_password($user1->id, 'pokus3'));
$this->assertTrue(user_is_previously_used_password($user1->id, 'pokus4'));
set_config('passwordreuselimit', 0);
$this->assertFalse(user_is_previously_used_password($user1->id, 'pokus1'));
$this->assertFalse(user_is_previously_used_password($user1->id, 'pokus2'));
$this->assertFalse(user_is_previously_used_password($user1->id, 'pokus3'));
$this->assertFalse(user_is_previously_used_password($user1->id, 'pokus4'));
}
/**
* Test that password history is deleted together with user.
*/
public function test_delete_of_hashes_on_user_delete() {
global $DB;
$this->resetAfterTest();
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$DB->delete_records('user_password_history', array());
set_config('passwordreuselimit', 3);
user_add_password_history($user1->id, 'pokus');
user_add_password_history($user2->id, 'pokus1');
user_add_password_history($user2->id, 'pokus2');
$this->assertEquals(3, $DB->count_records('user_password_history'));
$this->assertEquals(1, $DB->count_records('user_password_history', array('userid' => $user1->id)));
$this->assertEquals(2, $DB->count_records('user_password_history', array('userid' => $user2->id)));
delete_user($user2);
$this->assertEquals(1, $DB->count_records('user_password_history'));
$this->assertEquals(1, $DB->count_records('user_password_history', array('userid' => $user1->id)));
$this->assertEquals(0, $DB->count_records('user_password_history', array('userid' => $user2->id)));
}
}