MDL-47830 auth: Add pw rotation restrictions
authorPetr Skoda <petr.skoda@totaralms.com>
Thu, 20 Nov 2014 22:26:32 +0000 (11:26 +1300)
committerPetr Skoda <petr.skoda@totaralms.com>
Sun, 30 Nov 2014 19:53:52 +0000 (08:53 +1300)
16 files changed:
admin/settings/security.php
auth/email/auth.php
auth/ldap/auth.php
auth/upgrade.txt
lang/en/admin.php
lang/en/auth.php
lib/db/install.xml
lib/db/upgrade.php
lib/moodlelib.php
login/change_password.php
login/change_password_form.php
login/lib.php
login/set_password_form.php
user/lib.php
user/tests/userlib_test.php
version.php

index b455946..af1e6c8 100644 (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),
index 8ab97ae..f244c45 100644 (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);
 
index f80ab19..03d483b 100644 (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);
 
index 0c6d453..a2acc9f 100644 (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
index 63e1d81..1baed1a 100644 (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.';
index 0bfe51b..e62338e 100644 (file)
@@ -79,6 +79,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';
@@ -103,6 +104,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';
index 6a6c9e4..b7b9634 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20141017" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20141121" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <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"/>
index c9728de..8195714 100644 (file)
@@ -4058,5 +4058,29 @@ function xmldb_main_upgrade($oldversion) {
     // Moodle v2.8.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2014112800.01) {
+
+        // 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, 2014112800.01);
+    }
+
     return true;
 }
index 5b31e13..6dced9e 100644 (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.
index eedfddf..0dfa342 100644 (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());
     }
index 953539f..589b45b 100644 (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;
index ac2a1cf..e4f0dc4 100644 (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());
         }
index 62e8b5c..21b399b 100644 (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;
     }
 }
index 5fac196..dacb5ae 100644 (file)
@@ -878,4 +878,82 @@ function user_get_user_navigation_info($user, $page) {
     }
 
     return $returnobject;
-}
\ No newline at end of file
+}
+
+/**
+ * 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;
+}
index 10eea01..002d7dc 100644 (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)));
+    }
 }
index 5d006cb..ab8a087 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2014112800.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2014112800.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.