From 2d35b7d3d8388d71b80694fd53aa3631411f434c Mon Sep 17 00:00:00 2001 From: Gilles-Philippe Leblanc Date: Mon, 2 Dec 2013 16:32:57 -0500 Subject: [PATCH] MDL-42816 auth_manual: Add password expiry for manual auth method --- auth/manual/auth.php | 53 ++++++++++- auth/manual/config.html | 77 +++++++++++++++- auth/manual/lang/en/auth_manual.php | 7 ++ auth/manual/tests/manual_test.php | 108 +++++++++++++++++++++++ lib/classes/user.php | 25 ++++++ lib/testing/generator/data_generator.php | 5 +- lib/tests/user_test.php | 40 ++++++++- 7 files changed, 309 insertions(+), 6 deletions(-) create mode 100644 auth/manual/tests/manual_test.php diff --git a/auth/manual/auth.php b/auth/manual/auth.php index 1c7ff9eaa9f..2803711cc0c 100644 --- a/auth/manual/auth.php +++ b/auth/manual/auth.php @@ -37,12 +37,17 @@ require_once($CFG->libdir.'/authlib.php'); */ class auth_plugin_manual extends auth_plugin_base { + /** + * The name of the component. Used by the configuration. + */ + const COMPONENT_NAME = 'auth_manual'; + /** * Constructor. */ function auth_plugin_manual() { $this->authtype = 'manual'; - $this->config = get_config('auth/manual'); + $this->config = get_config(self::COMPONENT_NAME); } /** @@ -81,6 +86,7 @@ class auth_plugin_manual extends auth_plugin_base { */ function user_update_password($user, $newpassword) { $user = get_complete_user_data('id', $user->id); + set_user_preference('auth_manual_passwordupdatetime', time(), $user->id); // This will also update the stored hash to the latest algorithm // if the existing hash is using an out-of-date algorithm (or the // legacy md5 algorithm). @@ -153,13 +159,56 @@ class auth_plugin_manual extends auth_plugin_base { include 'config.html'; } + /** + * Return number of days to user password expires. + * + * If user password does not expire, it should return 0 or a positive value. + * If user password is already expired, it should return negative value. + * + * @param mixed $username username (with system magic quotes) + * @return integer + */ + public function password_expire($username) { + $result = 0; + + if (!empty($this->config->expirationtime)) { + $user = core_user::get_user_by_username($username, 'id,timecreated'); + $lastpasswordupdatetime = get_user_preferences('auth_manual_passwordupdatetime', $user->timecreated, $user->id); + $expiretime = $lastpasswordupdatetime + $this->config->expirationtime * DAYSECS; + $now = time(); + $result = ($expiretime - $now) / DAYSECS; + if ($expiretime > $now) { + $result = ceil($result); + } else { + $result = floor($result); + } + } + + return $result; + } + /** * Processes and stores configuration data for this authentication plugin. * - * @param array $config + * @param stdClass $config * @return void */ function process_config($config) { + // Set to defaults if undefined. + if (!isset($config->expiration)) { + $config->expiration = ''; + } + if (!isset($config->expiration_warning)) { + $config->expiration_warning = ''; + } + if (!isset($config->expirationtime)) { + $config->expirationtime = ''; + } + + // Save settings. + set_config('expiration', $config->expiration, self::COMPONENT_NAME); + set_config('expiration_warning', $config->expiration_warning, self::COMPONENT_NAME); + set_config('expirationtime', $config->expirationtime, self::COMPONENT_NAME); return true; } diff --git a/auth/manual/config.html b/auth/manual/config.html index 8c03ce00a8e..f622ab19996 100644 --- a/auth/manual/config.html +++ b/auth/manual/config.html @@ -1,5 +1,78 @@ -authtype, $user_fields, get_string('auth_fieldlocks_help', 'auth'), false, false); + // Set to defaults if undefined. + if (!isset($config->expiration)) { + $config->expiration = ''; + } + if (!isset($config->expiration_warning)) { + $config->expiration_warning = ''; + } + if (!isset($config->expirationtime)) { + $config->expirationtime = ''; + } + $expirationoptions = array( + new lang_string('no'), + new lang_string('yes'), + ); + $expirationtimeoptions = array( + '30' => new lang_string('numdays', '', 30), + '60' => new lang_string('numdays', '', 60), + '90' => new lang_string('numdays', '', 90), + '120' => new lang_string('numdays', '', 120), + '150' => new lang_string('numdays', '', 150), + '180' => new lang_string('numdays', '', 180), + '365' => new lang_string('numdays', '', 365), + ); + $expirationwarningoptions = array( + '0' => new lang_string('never'), + '1' => new lang_string('numdays', '', 1), + '2' => new lang_string('numdays', '', 2), + '3' => new lang_string('numdays', '', 3), + '4' => new lang_string('numdays', '', 4), + '5' => new lang_string('numdays', '', 5), + '6' => new lang_string('numdays', '', 6), + '7' => new lang_string('numdays', '', 7), + '10' => new lang_string('numdays', '', 10), + '14' => new lang_string('numdays', '', 14), + ); ?> +
+ + + + + + + + + + + + + + + + + + + authtype, $user_fields, get_string('auth_fieldlocks_help', 'auth'), false, false) ?>
+

+
+ + + expiration, false) ?> +
+ + + expirationtime, false) ?> +
+ + + expiration_warning, false) ?> +
diff --git a/auth/manual/lang/en/auth_manual.php b/auth/manual/lang/en/auth_manual.php index 53d89098a42..a9197978bc6 100644 --- a/auth/manual/lang/en/auth_manual.php +++ b/auth/manual/lang/en/auth_manual.php @@ -23,4 +23,11 @@ */ $string['auth_manualdescription'] = 'This method removes any way for users to create their own accounts. All accounts must be manually created by the admin user.'; +$string['expiration'] = 'Enable password expiry'; +$string['expiration_desc'] = 'Allow passwords to expire after a specified time.'; +$string['expiration_warning'] = 'Notification threshold'; +$string['expiration_warning_desc'] = 'Number of days before password expiry that a notification is issued.'; +$string['passwdexpiretime'] = 'Password duration'; +$string['passwdexpiretime_desc'] = 'Length of time for which a password is valid.'; $string['pluginname'] = 'Manual accounts'; +$string['passwdexpire_settings'] = 'Password expiry settings'; diff --git a/auth/manual/tests/manual_test.php b/auth/manual/tests/manual_test.php new file mode 100644 index 00000000000..087721acd41 --- /dev/null +++ b/auth/manual/tests/manual_test.php @@ -0,0 +1,108 @@ +. + +/** + * Manual authentication tests. + * + * @package auth_manual + * @category test + * @copyright 2014 Gilles-Philippe Leblanc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot.'/auth/manual/auth.php'); + +/** + * Manual authentication tests class. + * + * @package auth_manual + * @category test + * @copyright 2014 Gilles-Philippe Leblanc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class auth_manual_testcase extends advanced_testcase { + + /** @var auth_plugin_manual Keeps the authentication plugin. */ + protected $authplugin; + + /** @var stdClass Keeps authentication plugin config */ + protected $config; + + /** + * Setup test data. + */ + protected function setUp() { + $this->resetAfterTest(true); + $this->authplugin = new auth_plugin_manual(); + $this->config = new stdClass(); + $this->config->expiration = '1'; + $this->config->expiration_warning = '2'; + $this->config->expirationtime = '30'; + $this->authplugin->process_config($this->config); + $this->authplugin->config = get_config(auth_plugin_manual::COMPONENT_NAME); + } + + /** + * Test user_update_password method. + */ + public function test_user_update_password() { + $user = $this->getDataGenerator()->create_user(); + $expectedtime = time(); + $passwordisupdated = $this->authplugin->user_update_password($user, 'MyNewPassword*'); + + // Assert that the actual time should be equal or a little greater than the expected time. + $this->assertGreaterThanOrEqual($expectedtime, get_user_preferences('auth_manual_passwordupdatetime', 0, $user->id)); + + // Assert that the password was successfully updated. + $this->assertTrue($passwordisupdated); + } + + /** + * Test test_password_expire method. + */ + public function test_password_expire() { + $userrecord = array(); + $expirationtime = 31 * DAYSECS; + $userrecord['timecreated'] = time() - $expirationtime; + $user1 = $this->getDataGenerator()->create_user($userrecord); + $user2 = $this->getDataGenerator()->create_user(); + + // The user 1 was created 31 days ago and has not changed his password yet, so the password has expirated. + $this->assertLessThanOrEqual(-1, $this->authplugin->password_expire($user1->username)); + + // The user 2 just came to be created and has not changed his password yet, so the password has not expirated. + $this->assertEquals(30, $this->authplugin->password_expire($user2->username)); + + $this->authplugin->user_update_password($user1, 'MyNewPassword*'); + + // The user 1 just updated his password so the password has not expirated. + $this->assertEquals(30, $this->authplugin->password_expire($user1->username)); + } + + /** + * Test test_process_config method. + */ + public function test_process_config() { + $this->assertTrue($this->authplugin->process_config($this->config)); + $config = get_config(auth_plugin_manual::COMPONENT_NAME); + $this->assertEquals($this->config->expiration, $config->expiration); + $this->assertEquals($this->config->expiration_warning, $config->expiration_warning); + $this->assertEquals($this->config->expirationtime, $config->expirationtime); + } +} diff --git a/lib/classes/user.php b/lib/classes/user.php index 7bcee3415af..bad53f530f0 100644 --- a/lib/classes/user.php +++ b/lib/classes/user.php @@ -79,6 +79,31 @@ class core_user { } } + + /** + * Return user object from db based on their username. + * + * @param string $username The username of the user searched. + * @param string $fields A comma separated list of user fields to be returned, support and noreply user. + * @param int $mnethostid The id of the remote host. + * @param int $strictness IGNORE_MISSING means compatible mode, false returned if user not found, debug message if more found; + * IGNORE_MULTIPLE means return first user, ignore multiple user records found(not recommended); + * MUST_EXIST means throw an exception if no user record or multiple records found. + * @return stdClass|bool user record if found, else false. + * @throws dml_exception if user record not found and respective $strictness is set. + */ + public static function get_user_by_username($username, $fields = '*', $mnethostid = null, $strictness = IGNORE_MISSING) { + global $DB, $CFG; + + // Because we use the username as the search criteria, we must also restrict our search based on mnet host. + if (empty($mnethostid)) { + // If empty, we restrict to local users. + $mnethostid = $CFG->mnet_localhost_id; + } + + return $DB->get_record('user', array('username' => $username, 'mnethostid' => $mnethostid), $fields, $strictness); + } + /** * Helper function to return dummy noreply user record. * diff --git a/lib/testing/generator/data_generator.php b/lib/testing/generator/data_generator.php index b0efa09f72e..ea316a7b3cc 100644 --- a/lib/testing/generator/data_generator.php +++ b/lib/testing/generator/data_generator.php @@ -226,7 +226,10 @@ EOD; $record['deleted'] = 0; } - $record['timecreated'] = time(); + if (!isset($record['timecreated'])) { + $record['timecreated'] = time(); + } + $record['timemodified'] = $record['timecreated']; $record['lastip'] = '0.0.0.0'; diff --git a/lib/tests/user_test.php b/lib/tests/user_test.php index ea379e84f59..a0d2e68bf74 100644 --- a/lib/tests/user_test.php +++ b/lib/tests/user_test.php @@ -31,10 +31,16 @@ */ class core_user_testcase extends advanced_testcase { + /** + * Setup test data. + */ + protected function setUp() { + $this->resetAfterTest(true); + } + public function test_get_user() { global $CFG; - $this->resetAfterTest(true); // Create user and try fetach it with api. $user = $this->getDataGenerator()->create_user(); @@ -78,4 +84,36 @@ class core_user_testcase extends advanced_testcase { $this->assertEquals($user, $supportuser); $this->assertTrue(core_user::is_real_user($supportuser->id)); } + + /** + * Test get_user_by_username method. + */ + public function test_get_user_by_username() { + $record = array(); + $record['username'] = 'johndoe'; + $record['email'] = 'johndoe@example.com'; + $record['timecreated'] = time(); + + // Create a default user for the test. + $userexpected = $this->getDataGenerator()->create_user($record); + + // Assert that the returned user is the espected one. + $this->assertEquals($userexpected, core_user::get_user_by_username('johndoe')); + + // Assert that a subset of fields is correctly returned. + $this->assertEquals((object) $record, core_user::get_user_by_username('johndoe', 'username,email,timecreated')); + + // Assert that a user with a different mnethostid will no be returned. + $this->assertFalse(core_user::get_user_by_username('johndoe', 'username,email,timecreated', 2)); + + // Create a new user from a different host. + $record['mnethostid'] = 2; + $userexpected2 = $this->getDataGenerator()->create_user($record); + + // Assert that the new user is returned when specified the correct mnethostid. + $this->assertEquals($userexpected2, core_user::get_user_by_username('johndoe', '*', 2)); + + // Assert that a user not in the db return false. + $this->assertFalse(core_user::get_user_by_username('janedoe')); + } }