From b08d113e0cb23d027f066dbc20c1d0c924f8d303 Mon Sep 17 00:00:00 2001 From: Andrew Hancox Date: Fri, 31 Mar 2017 11:41:33 +0100 Subject: [PATCH] Add user provisioning. --- README.md | 5 +- auth.php | 107 +++++++++++++++++-- lang/en/auth_userkey.php | 5 +- settings.html | 22 ++-- tests/auth_plugin_test.php | 208 +++++++++++++++++++++++++++++++++++++ version.php | 2 +- 6 files changed, 330 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 24165fe..31f0980 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,5 @@ E.g. http://yourmoodle.com/login/index.php?enrolkey_skipsso=1 TODO: ----- -1. Add users provisioning. -2. Implement logout webservice to be able to call it from external application. -3. Add a test client code to README. +1. Implement logout webservice to be able to call it from external application. +2. Add a test client code to README. diff --git a/auth.php b/auth.php index c9ddf11..b1a0afb 100644 --- a/auth.php +++ b/auth.php @@ -29,6 +29,7 @@ use auth_userkey\userkey_manager_interface; require_once($CFG->libdir . "/externallib.php"); require_once($CFG->libdir.'/authlib.php'); +require_once($CFG->dirroot . '/user/lib.php'); /** * User key authentication plugin. @@ -58,7 +59,8 @@ class auth_plugin_userkey extends auth_plugin_base { 'iprestriction' => 0, 'redirecturl' => '', 'ssourl' => '', - // TODO: use this field when implementing user creation. 'createuser' => 0. + 'createuser' => false, + 'updateuser' => false, ); /** @@ -302,6 +304,19 @@ class auth_plugin_userkey extends auth_plugin_base { return false; } + /** + * Check if we need to update users. + * + * @return bool + */ + protected function should_update_user() { + if (isset($this->config->updateuser) && $this->config->updateuser == true) { + return true; + } + + return false; + } + /** * Check if restriction by IP is enabled. * @@ -323,10 +338,77 @@ class auth_plugin_userkey extends auth_plugin_base { * @return object User object. */ protected function create_user(array $data) { - // TODO: - // 1. Validate user - // 2. Create user. - // 3. Throw exception if something went wrong. + global $DB, $CFG; + + $user = $data; + unset($user['ip']); + $user['auth'] = 'userkey'; + $user['mnethostid'] = $CFG->mnet_localhost_id; + + if ($DB->record_exists('user', array('username' => $user['username'], 'mnethostid' => $CFG->mnet_localhost_id))) { + throw new invalid_parameter_exception('Username already exists: '.$user['username']); + } + if (!validate_email($user['email'])) { + throw new invalid_parameter_exception('Email address is invalid: '.$user['email']); + } else if (empty($CFG->allowaccountssameemail) && + $DB->record_exists('user', array('email' => $user['email'], 'mnethostid' => $user['mnethostid']))) { + throw new invalid_parameter_exception('Email address already exists: '.$user['email']); + } + + $userid = user_create_user($user); + return $DB->get_record('user', ['id' => $userid]); + } + + /** + * Update an existing user. + * + * @param stdClass $user Existing user record. + * @param array $data Validated user data from web service. + * + * @return object User object. + */ + protected function update_user(\stdClass $user, array $data) { + global $DB, $CFG; + + $userdata = $data; + unset($userdata['ip']); + $userdata['auth'] = 'userkey'; + + $changed = false; + foreach ($userdata as $key => $value) { + if ($user->$key != $value) { + $changed = true; + break; + } + } + + if (!$changed) { + return $user; + } + + if ( + $user->username != $userdata['username'] + && + $DB->record_exists('user', array('username' => $userdata['username'], 'mnethostid' => $CFG->mnet_localhost_id)) + ) { + throw new invalid_parameter_exception('Username already exists: '.$userdata['username']); + } + if (!validate_email($userdata['email'])) { + throw new invalid_parameter_exception('Email address is invalid: '.$userdata['email']); + } else if ( + empty($CFG->allowaccountssameemail) + && + $user->email != $userdata['email'] + && + $DB->record_exists('user', array('email' => $userdata['email'], 'mnethostid' => $CFG->mnet_localhost_id)) + ) { + throw new invalid_parameter_exception('Email address already exists: '.$userdata['email']); + } + $userdata['id'] = $user->id; + + $userdata = (object) $userdata; + user_update_user($userdata, false); + return $DB->get_record('user', ['id' => $user->id]); } /** @@ -381,6 +463,8 @@ class auth_plugin_userkey extends auth_plugin_base { } else { throw new invalid_parameter_exception('User is not exist'); } + } else if ($this->should_update_user()) { + $user = $this->update_user($user, $data); } return $user; @@ -506,7 +590,18 @@ class auth_plugin_userkey extends auth_plugin_base { ); } - // TODO: add more fields here when we implement user creation. + $mappingfield = $this->get_mapping_field(); + if ($this->should_create_user() || $this->should_update_user()) { + $parameters['firstname'] = new external_value(core_user::get_property_type('firstname'), 'The first name(s) of the user'); + $parameters['lastname'] = new external_value(core_user::get_property_type('lastname'), 'The family name of the user'); + + if ($mappingfield != 'email') { + $parameters['email'] = new external_value(core_user::get_property_type('email'), 'A valid and unique email address'); + } + if ($mappingfield != 'username') { + $parameters['username'] = new external_value(core_user::get_property_type('username'), 'A valid and unique username'); + } + } return $parameters; } diff --git a/lang/en/auth_userkey.php b/lang/en/auth_userkey.php index e83bb8c..cf3e55b 100644 --- a/lang/en/auth_userkey.php +++ b/lang/en/auth_userkey.php @@ -32,8 +32,10 @@ A user has to have provided IP to be able to use a key to login to LMS.'; $string['keylifetime'] = 'User key life time'; $string['keylifetime_desc'] = 'Life time in seconds of the each user login key.'; $string['incorrectkeylifetime'] = 'User key life time should be a number'; -$string['createuser'] = 'Crete user?'; +$string['createuser'] = 'Create user?'; $string['createuser_desc'] = 'If enabled, a new user will be created if fail to find one in LMS.'; +$string['updateuser'] = 'Update user?'; +$string['updateuser_desc'] = 'If enabled, users will be updated with the properties supplied when the webservice is called.'; $string['redirecturl'] = 'Logout redirect URL'; $string['redirecturl_desc'] = 'Optionally you can redirect users to this URL after they logged out from LMS.'; $string['incorrectredirecturl'] = 'You should provide valid URL'; @@ -43,3 +45,4 @@ $string['pluginisdisabled'] = 'The userkey authentication plugin is disabled.'; $string['ssourl'] = 'URL of SSO host'; $string['ssourl_desc'] = 'URL of the SSO host to redirect users to. If defined users will be redirected here on login instead of the Moodle Login page'; $string['redirecterrordetected'] = 'Unsupported redirect to {$a} detected, execution terminated.'; +$string['noip'] = 'Unable to fetch IP address of client.'; diff --git a/settings.html b/settings.html index 9473a5b..8e95618 100644 --- a/settings.html +++ b/settings.html @@ -63,13 +63,19 @@ $fields = get_auth_plugin('userkey')->get_allowed_mapping_fields(); notification($err[$field], 'notifyfailure'); } ?> - - - - - - - - + + + + $field, false) ?> + notification($err[$field], 'notifyfailure'); } ?> + + + + + + $field, false) ?> + notification($err[$field], 'notifyfailure'); } ?> + + diff --git a/tests/auth_plugin_test.php b/tests/auth_plugin_test.php index b9c8159..b841988 100644 --- a/tests/auth_plugin_test.php +++ b/tests/auth_plugin_test.php @@ -277,6 +277,185 @@ class auth_plugin_userkey_testcase extends advanced_testcase { $this->assertEquals($expected, $actual); } + /** + * Test that we can request a key for a new user. + */ + public function test_return_correct_login_url_and_create_new_user() { + global $CFG, $DB; + + set_config('createuser', true, 'auth_userkey'); + $this->auth = new auth_plugin_userkey(); + + $userkeymanager = new \auth_userkey\fake_userkey_manager(); + $this->auth->set_userkey_manager($userkeymanager); + + $user = new stdClass(); + $user->username = 'username'; + $user->email = 'username@test.com'; + $user->firstname = 'user'; + $user->lastname = 'name'; + $user->ip = '192.168.1.1'; + + $expected = $CFG->wwwroot . '/auth/userkey/login.php?key=FaKeKeyFoRtEsTiNg'; + $actual = $this->auth->get_login_url($user); + + $this->assertEquals($expected, $actual); + + $userrecord = $DB->get_record('user', ['username' => 'username']); + $this->assertEquals($user->email, $userrecord->email); + $this->assertEquals($user->firstname, $userrecord->firstname); + $this->assertEquals($user->lastname, $userrecord->lastname); + $this->assertEquals('userkey', $userrecord->auth); + } + + /** + * Test that when we attempt to create a new user duplicate usernames are caught. + */ + public function test_create_refuse_duplicate_username() { + set_config('createuser', true, 'auth_userkey'); + $this->auth = new auth_plugin_userkey(); + + $userkeymanager = new \auth_userkey\fake_userkey_manager(); + $this->auth->set_userkey_manager($userkeymanager); + + $originaluser = new stdClass(); + $originaluser->username = 'username'; + $originaluser->email = 'username@test.com'; + $originaluser->firstname = 'user'; + $originaluser->lastname = 'name'; + $originaluser->city = 'brighton'; + $originaluser->ip = '192.168.1.1'; + + self::getDataGenerator()->create_user($originaluser); + + $duplicateuser = clone($originaluser); + $duplicateuser->email = 'duplicateuser@test.com'; + + $this->setExpectedException('invalid_parameter_exception', 'Username already exists: username'); + $this->auth->get_login_url($duplicateuser); + } + + /** + * Test that when we attempt to create a new user duplicate emails are caught. + */ + public function test_create_refuse_duplicate_email() { + set_config('createuser', true, 'auth_userkey'); + set_config('mappingfield', 'username', 'auth_userkey'); + $this->auth = new auth_plugin_userkey(); + + $userkeymanager = new \auth_userkey\fake_userkey_manager(); + $this->auth->set_userkey_manager($userkeymanager); + + $originaluser = new stdClass(); + $originaluser->username = 'username'; + $originaluser->email = 'username@test.com'; + $originaluser->firstname = 'user'; + $originaluser->lastname = 'name'; + $originaluser->city = 'brighton'; + $originaluser->ip = '192.168.1.1'; + + self::getDataGenerator()->create_user($originaluser); + + $duplicateuser = clone($originaluser); + $duplicateuser->username = 'duplicateuser'; + + $this->setExpectedException('invalid_parameter_exception', 'Email address already exists: username@test.com'); + $this->auth->get_login_url($duplicateuser); + } + + /** + * Test that we can request a key for an existing user and update their details. + */ + public function test_return_correct_login_url_and_update_user() { + global $CFG, $DB; + + set_config('updateuser', true, 'auth_userkey'); + $this->auth = new auth_plugin_userkey(); + + $userkeymanager = new \auth_userkey\fake_userkey_manager(); + $this->auth->set_userkey_manager($userkeymanager); + + $originaluser = new stdClass(); + $originaluser->username = 'username'; + $originaluser->email = 'username@test.com'; + $originaluser->firstname = 'user'; + $originaluser->lastname = 'name'; + $originaluser->city = 'brighton'; + $originaluser->ip = '192.168.1.1'; + + self::getDataGenerator()->create_user($originaluser); + + $user = new stdClass(); + $user->username = 'usernamechanged'; + $user->email = 'username@test.com'; + $user->firstname = 'userchanged'; + $user->lastname = 'namechanged'; + $user->ip = '192.168.1.1'; + + $expected = $CFG->wwwroot . '/auth/userkey/login.php?key=FaKeKeyFoRtEsTiNg'; + $actual = $this->auth->get_login_url($user); + + $this->assertEquals($expected, $actual); + + $userrecord = $DB->get_record('user', ['email' => $user->email]); + $this->assertEquals($user->username, $userrecord->username); + $this->assertEquals($user->firstname, $userrecord->firstname); + $this->assertEquals($user->lastname, $userrecord->lastname); + $this->assertEquals($originaluser->city, $userrecord->city); + $this->assertEquals('userkey', $userrecord->auth); + } + + /** + * Test that when we attempt to update a user duplicate emails are caught. + */ + public function test_update_refuse_duplicate_email() { + set_config('updateuser', true, 'auth_userkey'); + set_config('mappingfield', 'username', 'auth_userkey'); + $this->auth = new auth_plugin_userkey(); + + $userkeymanager = new \auth_userkey\fake_userkey_manager(); + $this->auth->set_userkey_manager($userkeymanager); + + self::getDataGenerator()->create_user(['email' => 'trytoduplicate@test.com']); + self::getDataGenerator()->create_user(['username' => 'username']); + + $originaluser = new stdClass(); + $originaluser->username = 'username'; + $originaluser->email = 'trytoduplicate@test.com'; + $originaluser->firstname = 'user'; + $originaluser->lastname = 'name'; + $originaluser->city = 'brighton'; + $originaluser->ip = '192.168.1.1'; + + $this->setExpectedException('invalid_parameter_exception', 'Email address already exists: trytoduplicate@test.com'); + $this->auth->get_login_url($originaluser); + } + + /** + * Test that when we attempt to update a user duplicate usernames are caught. + */ + public function test_update_refuse_duplicate_username() { + set_config('updateuser', true, 'auth_userkey'); + $this->auth = new auth_plugin_userkey(); + + $userkeymanager = new \auth_userkey\fake_userkey_manager(); + $this->auth->set_userkey_manager($userkeymanager); + + self::getDataGenerator()->create_user(['username' => 'trytoduplicate']); + self::getDataGenerator()->create_user(['email' => 'username@test.com']); + + $originaluser = new stdClass(); + $originaluser->username = 'trytoduplicate'; + $originaluser->email = 'username@test.com'; + $originaluser->firstname = 'user'; + $originaluser->lastname = 'name'; + $originaluser->city = 'brighton'; + $originaluser->ip = '192.168.1.1'; + + $this->setExpectedException('invalid_parameter_exception', 'Username already exists: trytoduplicate'); + $this->auth->get_login_url($originaluser); + } + /** * Test that we can get login url if we do not use fake keymanager. */ @@ -394,6 +573,33 @@ class auth_plugin_userkey_testcase extends advanced_testcase { $actual = $this->auth->get_request_login_url_user_parameters(); $this->assertEquals($expected, $actual); + // Check IP if createuser enabled. + set_config('createuser', true, 'auth_userkey'); + $this->auth = new auth_plugin_userkey(); + $expected = array( + 'ip' => new external_value(PARAM_HOST, 'User IP address'), + 'firstname' => new external_value(core_user::get_property_type('firstname'), 'The first name(s) of the user'), + 'lastname' => new external_value(core_user::get_property_type('lastname'), 'The family name of the user'), + 'email' => new external_value(core_user::get_property_type('email'), 'A valid and unique email address'), + 'username' => new external_value(core_user::get_property_type('username'), 'A valid and unique username'), + ); + $actual = $this->auth->get_request_login_url_user_parameters(); + $this->assertEquals($expected, $actual); + set_config('createuser', false, 'auth_userkey'); + + // Check IP if updateuser enabled. + set_config('updateuser', true, 'auth_userkey'); + $this->auth = new auth_plugin_userkey(); + $expected = array( + 'ip' => new external_value(PARAM_HOST, 'User IP address'), + 'firstname' => new external_value(core_user::get_property_type('firstname'), 'The first name(s) of the user'), + 'lastname' => new external_value(core_user::get_property_type('lastname'), 'The family name of the user'), + 'email' => new external_value(core_user::get_property_type('email'), 'A valid and unique email address'), + 'username' => new external_value(core_user::get_property_type('username'), 'A valid and unique username'), + ); + $actual = $this->auth->get_request_login_url_user_parameters(); + $this->assertEquals($expected, $actual); + set_config('updateuser', false, 'auth_userkey'); } /** @@ -531,6 +737,8 @@ class auth_plugin_userkey_testcase extends advanced_testcase { $formconfig->iprestriction = 0; $formconfig->redirecturl = 'http://google.com/'; $formconfig->ssourl = 'http://google.com/'; + $formconfig->createuser = false; + $formconfig->updateuser = false; $this->auth->process_config($formconfig); diff --git a/version.php b/version.php index f41fe0d..9262dd1 100644 --- a/version.php +++ b/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die; -$plugin->version = 2016092600; // The current plugin version (Date: YYYYMMDDXX) +$plugin->version = 2016092601; // The current plugin version (Date: YYYYMMDDXX) $plugin->release = 2016092600; // Match release exactly to version. $plugin->requires = 2015051100; // Requires this Moodle version. $plugin->component = 'auth_userkey'; // Full name of the plugin (used for diagnostics).