Merge branch 'MDL-45639-master' of git://github.com/jleyva/moodle

This commit is contained in:
David Monllao
2016-10-18 08:26:25 +08:00
16 changed files with 469 additions and 20 deletions
+70
View File
@@ -0,0 +1,70 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Auto-login end-point, a user can be fully authenticated in the site providing a valid key.
*
* @package tool_mobile
* @copyright 2016 Juan Leyva
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../../config.php');
require_once($CFG->libdir . '/externallib.php');
$userid = required_param('userid', PARAM_INT); // The user id the key belongs to (for double-checking).
$key = required_param('key', PARAM_ALPHANUMEXT); // The key generated by the tool_mobile_external::get_autologin_key() external function.
$urltogo = optional_param('urltogo', $CFG->wwwroot, PARAM_URL); // URL to redirect.
$context = context_system::instance();
$PAGE->set_context($context);
// Force https.
$PAGE->https_required();
// Check if the user is already logged-in.
if (isloggedin() and !isguestuser()) {
delete_user_key('tool_mobile', $userid);
if ($USER->id == $userid) {
redirect($urltogo);
} else {
throw new moodle_exception('alreadyloggedin', 'error', '', format_string(fullname($USER)));
}
}
tool_mobile\api::check_autologin_prerequisites($userid);
// Validate and delete the key.
$key = validate_user_key($key, 'tool_mobile', null);
delete_user_key('tool_mobile', $userid);
// Double check key belong to user.
if ($key->userid != $userid) {
throw new moodle_exception('invalidkey');
}
// Key validated, now require an active user: not guest, not suspended.
$user = core_user::get_user($key->userid, '*', MUST_EXIST);
core_user::require_active_user($user, true, true);
// Do the user log-in.
if (!$user = get_complete_user_data('id', $user->id)) {
throw new moodle_exception('cannotfinduser', '', '', $user->id);
}
complete_user_login($user);
\core\session\manager::apply_concurrent_login_limit($user->id, session_id());
redirect($urltogo);
+42
View File
@@ -28,6 +28,7 @@ use core_component;
use core_plugin_manager;
use context_system;
use moodle_url;
use moodle_exception;
/**
* API exposed by tool_mobile
@@ -44,6 +45,8 @@ class api {
const LOGIN_VIA_BROWSER = 2;
/** @var int to identify the login via an embedded browser. */
const LOGIN_VIA_EMBEDDED_BROWSER = 3;
/** @var int seconds an auto-login key will expire. */
const LOGIN_KEY_TTL = 60;
/**
* Returns a list of Moodle plugins supporting the mobile app.
@@ -180,4 +183,43 @@ class api {
return $settings;
}
/*
* Check if all the required conditions are met to allow the auto-login process continue.
*
* @param int $userid current user id
* @since Moodle 3.2
* @throws moodle_exception
*/
public static function check_autologin_prerequisites($userid) {
global $CFG;
if (!$CFG->enablewebservices or !$CFG->enablemobilewebservice) {
throw new moodle_exception('enablewsdescription', 'webservice');
}
if (!is_https()) {
throw new moodle_exception('httpsrequired', 'tool_mobile');
}
if (has_capability('moodle/site:config', context_system::instance(), $userid) or is_siteadmin($userid)) {
throw new moodle_exception('autologinnotallowedtoadmins', 'tool_mobile');
}
}
/**
* Creates an auto-login key for the current user, this key is restricted by time and ip address.
*
* @return string the key
* @since Moodle 3.2
*/
public static function get_autologin_key() {
global $USER;
// Delete previous keys.
delete_user_key('tool_mobile', $USER->id);
// Create a new key.
$iprestriction = getremoteaddr();
$validuntil = time() + self::LOGIN_KEY_TTL;
return create_user_key('tool_mobile', $USER->id, null, $iprestriction, $validuntil);
}
}
+87 -1
View File
@@ -32,6 +32,10 @@ use external_value;
use external_single_structure;
use external_multiple_structure;
use external_warnings;
use context_system;
use moodle_exception;
use moodle_url;
use core_text;
/**
* This is the external API for this tool.
@@ -207,4 +211,86 @@ class external extends external_api {
)
);
}
}
/**
* Returns description of get_autologin_key() parameters.
*
* @return external_function_parameters
* @since Moodle 3.2
*/
public static function get_autologin_key_parameters() {
return new external_function_parameters (
array(
'privatetoken' => new external_value(PARAM_ALPHANUM, 'Private token, usually generated by login/token.php'),
)
);
}
/**
* Creates an auto-login key for the current user. Is created only in https sites and is restricted by time and ip address.
*
* @param string $privatetoken the user private token for validating the request
* @return array with the settings and warnings
* @since Moodle 3.2
*/
public static function get_autologin_key($privatetoken) {
global $CFG, $DB, $USER;
$params = self::validate_parameters(self::get_autologin_key_parameters(), array('privatetoken' => $privatetoken));
$privatetoken = $params['privatetoken'];
$context = context_system::instance();
self::validate_context($context);
api::check_autologin_prerequisites($USER->id);
if (isset($_GET['privatetoken']) or empty($privatetoken)) {
throw new moodle_exception('invalidprivatetoken', 'tool_mobile');
}
// Check the request counter, we must limit the number of times the privatetoken is sent.
// Between each request 6 minutes are required.
$last = get_user_preferences('tool_mobile_autologin_request_last', 0, $USER);
// Check if we must reset the count.
$timenow = time();
if ($timenow - $last < 6 * MINSECS) {
throw new moodle_exception('autologinkeygenerationlockout', 'tool_mobile');
}
set_user_preference('tool_mobile_autologin_request_last', $timenow, $USER);
// We are expecting a privatetoken linked to the current token being used.
// This WS is only valid when using mobile services via REST (this is intended).
$currenttoken = required_param('wstoken', PARAM_ALPHANUM);
$conditions = array(
'userid' => $USER->id,
'token' => $currenttoken,
'privatetoken' => $privatetoken,
);
if (!$token = $DB->get_record('external_tokens', $conditions)) {
throw new moodle_exception('invalidprivatetoken', 'tool_mobile');
}
$result = array();
$result['key'] = api::get_autologin_key();
$autologinurl = new moodle_url("/$CFG->admin/tool/mobile/autologin.php");
$result['autologinurl'] = $autologinurl->out(false);
$result['warnings'] = array();
return $result;
}
/**
* Returns description of get_autologin_key() result value.
*
* @return external_description
* @since Moodle 3.2
*/
public static function get_autologin_key_returns() {
return new external_single_structure(
array(
'key' => new external_value(PARAM_ALPHANUMEXT, 'Auto-login key for a single usage with time expiration.'),
'autologinurl' => new external_value(PARAM_URL, 'Auto-login URL.'),
'warnings' => new external_warnings(),
)
);
}
}
+9 -1
View File
@@ -49,7 +49,15 @@ $functions = array(
'description' => 'Returns a list of the site configurations, filtering by section.',
'type' => 'read',
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
)
),
'tool_mobile_get_autologin_key' => array(
'classname' => 'tool_mobile\external',
'methodname' => 'get_autologin_key',
'description' => 'Creates an auto-login key for the current user.
Is created only in https sites and is restricted by time and ip address.',
'type' => 'write',
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
)
);
@@ -22,11 +22,15 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['autologinkeygenerationlockout'] = 'Auto-login key generation is locked out, too much requests in an hour.';
$string['autologinnotallowedtoadmins'] = 'Auto-login is not allowed to site admins';
$string['clickheretolaunchtheapp'] = 'Click here if the app does not open automatically.';
$string['enablesmartappbanners'] = 'Enable Smart App Banners';
$string['enablesmartappbanners_desc'] = 'This will display a banner promoting the Moodle Mobile app when visiting the site in Mobile Safari.';
$string['forcedurlscheme'] = 'The URL scheme allows to open the mobile app from other apps like the browser. Use this setting if you want to allow only your custom branded app to be opened by the browser.';
$string['forcedurlscheme_key'] = 'URL scheme';
$string['httpsrequired'] = 'HTTPS required';
$string['invalidprivatetoken'] = 'Invalid private token. Token should not be empty or passed via GET parameter.';
$string['iosappid'] = 'App\'s unique identifier';
$string['iosappid_desc'] = 'You only need to change this value if you have a custom iOS app';
$string['loginintheapp'] = 'Via the app';
+66
View File
@@ -0,0 +1,66 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Moodle Mobile admin tool api tests.
*
* @package tool_mobile
* @category external
* @copyright 2016 Juan Leyva
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 3.1
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
use tool_mobile\api;
/**
* Moodle Mobile admin tool api tests.
*
* @package tool_mobile
* @copyright 2016 Juan Leyva
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 3.1
*/
class tool_mobile_api_testcase extends externallib_advanced_testcase {
/**
* Test get_autologin_key.
*/
public function test_get_autologin_key() {
global $USER, $DB;
$this->resetAfterTest(true);
$this->setAdminUser();
// Set server timezone for test.
$this->setTimezone('UTC');
// SEt user to GMT+5.
$USER->timezone = 5;
$timenow = time();
$key = api::get_autologin_key();
$key = $DB->get_record('user_private_key', array('value' => $key), '*', MUST_EXIST);
$this->assertEquals($timenow + api::LOGIN_KEY_TTL, $key->validuntil);
$this->assertEquals('0.0.0.0', $key->iprestriction);
}
}
+120 -1
View File
@@ -34,7 +34,7 @@ use tool_mobile\external;
use tool_mobile\api;
/**
* External learning plans webservice API tests.
* Moodle Mobile admin tool external functions tests.
*
* @package tool_mobile
* @copyright 2016 Juan Leyva
@@ -142,4 +142,123 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
$this->assertEquals($expected, $result['settings']);
}
/*
* Test get_autologin_key.
*/
public function test_get_autologin_key() {
global $DB, $CFG, $USER;
$this->resetAfterTest(true);
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$service = $DB->get_record('external_services', array('shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE));
$token = external_generate_token_for_current_user($service);
$this->assertDebuggingCalled(); // MDL-55992.
// Check we got the private token.
$this->assertTrue(isset($token->privatetoken));
// Enable requeriments.
$CFG->httpswwwroot = str_replace('http:', 'https:', $CFG->httpswwwroot); // Mock https.
$CFG->enablewebservices = 1;
$CFG->enablemobilewebservice = 1;
$_GET['wstoken'] = $token->token; // Mock parameters.
$this->setCurrentTimeStart();
$result = external::get_autologin_key($token->privatetoken);
$result = external_api::clean_returnvalue(external::get_autologin_key_returns(), $result);
// Validate the key.
$this->assertEquals(32, core_text::strlen($result['key']));
$key = $DB->get_record('user_private_key', array('value' => $result['key']));
$this->assertEquals($USER->id, $key->userid);
$this->assertTimeCurrent($key->validuntil - api::LOGIN_KEY_TTL);
// Now, try with an invalid private token.
set_user_preference('tool_mobile_autologin_request_last', time() - HOURSECS, $USER);
$this->expectException('moodle_exception');
$this->expectExceptionMessage(get_string('invalidprivatetoken', 'tool_mobile'));
$result = external::get_autologin_key(random_string('64'));
}
/**
* Test get_autologin_key missing ws.
*/
public function test_get_autologin_key_missing_ws() {
$this->resetAfterTest(true);
$this->setAdminUser();
$this->expectException('moodle_exception');
$this->expectExceptionMessage(get_string('enablewsdescription', 'webservice'));
$result = external::get_autologin_key('');
}
/**
* Test get_autologin_key missing https.
*/
public function test_get_autologin_key_missing_https() {
global $CFG;
$this->resetAfterTest(true);
$this->setAdminUser();
$CFG->enablewebservices = 1;
$CFG->enablemobilewebservice = 1;
$this->expectException('moodle_exception');
$this->expectExceptionMessage(get_string('httpsrequired', 'tool_mobile'));
$result = external::get_autologin_key('');
}
/**
* Test get_autologin_key missing admin.
*/
public function test_get_autologin_key_missing_admin() {
global $CFG;
$this->resetAfterTest(true);
$this->setAdminUser();
$CFG->enablewebservices = 1;
$CFG->enablemobilewebservice = 1;
$CFG->httpswwwroot = str_replace('http:', 'https:', $CFG->httpswwwroot);
$this->expectException('moodle_exception');
$this->expectExceptionMessage(get_string('autologinnotallowedtoadmins', 'tool_mobile'));
$result = external::get_autologin_key('');
}
/**
* Test get_autologin_key locked.
*/
public function test_get_autologin_key_missing_locked() {
global $CFG, $DB, $USER;
$this->resetAfterTest(true);
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$CFG->enablewebservices = 1;
$CFG->enablemobilewebservice = 1;
$CFG->httpswwwroot = str_replace('http:', 'https:', $CFG->httpswwwroot);
$service = $DB->get_record('external_services', array('shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE));
$token = external_generate_token_for_current_user($service);
$this->assertDebuggingCalled(); // MDL-55992.
$_GET['wstoken'] = $token->token; // Mock parameters.
$result = external::get_autologin_key($token->privatetoken);
$result = external_api::clean_returnvalue(external::get_autologin_key_returns(), $result);
// Mock last time request.
$mocktime = time() - 7 * MINSECS;
set_user_preference('tool_mobile_autologin_request_last', $mocktime, $USER);
$result = external::get_autologin_key($token->privatetoken);
$result = external_api::clean_returnvalue(external::get_autologin_key_returns(), $result);
// We just requested one token, we must wait.
$this->expectException('moodle_exception');
$this->expectExceptionMessage(get_string('autologinkeygenerationlockout', 'tool_mobile'));
$result = external::get_autologin_key($token->privatetoken);
}
}
+1 -1
View File
@@ -23,6 +23,6 @@
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2016052304; // The current plugin version (Date: YYYYMMDDXX).
$plugin->version = 2016052305; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2016051900; // Requires this Moodle version.
$plugin->component = 'tool_mobile'; // Full name of the plugin (used for diagnostics).
+1
View File
@@ -2577,6 +2577,7 @@
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="token" TYPE="char" LENGTH="128" NOTNULL="true" SEQUENCE="false" COMMENT="security token, aka private access key"/>
<FIELD NAME="privatetoken" TYPE="char" LENGTH="64" NOTNULL="false" SEQUENCE="false" COMMENT="private token, generated at the same time that the token, must be stored safely by the ws client, to be transmitted only via https"/>
<FIELD NAME="tokentype" TYPE="int" LENGTH="4" NOTNULL="true" SEQUENCE="false" COMMENT="type of token: 0=permanent, no session; 1=linked to current browser session via sid; 2=permanent, with emulated session"/>
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="owner of the token"/>
<FIELD NAME="externalserviceid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+14
View File
@@ -2250,6 +2250,7 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2016101100.00);
}
if ($oldversion < 2016101101.00) {
// Define field component to be added to message_read.
$table = new xmldb_table('message_read');
@@ -2285,5 +2286,18 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2016101401.00);
}
if ($oldversion < 2016101401.02) {
$table = new xmldb_table('external_tokens');
$field = new xmldb_field('privatetoken', XMLDB_TYPE_CHAR, '64', null, null, null, null);
// Conditionally add privatetoken field to the external_tokens table.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Main savepoint reached.
upgrade_main_savepoint(true, 2016101401.02);
}
return true;
}
+3
View File
@@ -724,6 +724,7 @@ function external_generate_token($tokentype, $serviceorid, $userid, $contextorid
if (!empty($iprestriction)) {
$newtoken->iprestriction = $iprestriction;
}
$newtoken->privatetoken = null;
$DB->insert_record('external_tokens', $newtoken);
return $newtoken->token;
}
@@ -1053,6 +1054,8 @@ function external_generate_token_for_current_user($service) {
$token->externalserviceid = $service->id;
// MDL-43119 Token valid for 3 months (12 weeks).
$token->validuntil = $token->timecreated + 12 * WEEKSECS;
// Generate the private token, it must be transmitted only via https.
$token->privatetoken = random_string(64);
$token->id = $DB->insert_record('external_tokens', $token);
$params = array(
+31 -14
View File
@@ -3009,6 +3009,36 @@ function require_course_login($courseorid, $autologinguest = true, $cm = null, $
}
}
/**
* Validates a user key, checking if the key exists, is not expired and the remote ip is correct.
*
* @param string $keyvalue the key value
* @param string $script unique script identifier
* @param int $instance instance id
* @return stdClass the key entry in the user_private_key table
* @since Moodle 3.2
* @throws moodle_exception
*/
function validate_user_key($keyvalue, $script, $instance) {
global $DB;
if (!$key = $DB->get_record('user_private_key', array('script' => $script, 'value' => $keyvalue, 'instance' => $instance))) {
print_error('invalidkey');
}
if (!empty($key->validuntil) and $key->validuntil < time()) {
print_error('expiredkey');
}
if ($key->iprestriction) {
$remoteaddr = getremoteaddr(null);
if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) {
print_error('ipmismatch');
}
}
return $key;
}
/**
* Require key login. Function terminates with error if key not found or incorrect.
*
@@ -3030,20 +3060,7 @@ function require_user_key_login($script, $instance=null) {
$keyvalue = required_param('key', PARAM_ALPHANUM);
if (!$key = $DB->get_record('user_private_key', array('script' => $script, 'value' => $keyvalue, 'instance' => $instance))) {
print_error('invalidkey');
}
if (!empty($key->validuntil) and $key->validuntil < time()) {
print_error('expiredkey');
}
if ($key->iprestriction) {
$remoteaddr = getremoteaddr(null);
if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) {
print_error('ipmismatch');
}
}
$key = validate_user_key($keyvalue, $script, $instance);
if (!$user = $DB->get_record('user', array('id' => $key->userid))) {
print_error('invaliduserid');
+14 -1
View File
@@ -44,11 +44,14 @@ $username = trim(core_text::strtolower($username));
if (is_restored_user($username)) {
throw new moodle_exception('restoredaccountresetpassword', 'webservice');
}
$systemcontext = context_system::instance();
$user = authenticate_user_login($username, $password);
if (!empty($user)) {
// Cannot authenticate unless maintenance access is granted.
$hasmaintenanceaccess = has_capability('moodle/site:maintenanceaccess', context_system::instance(), $user);
$hasmaintenanceaccess = has_capability('moodle/site:maintenanceaccess', $systemcontext, $user);
if (!empty($CFG->maintenance_enabled) and !$hasmaintenanceaccess) {
throw new moodle_exception('sitemaintenance', 'admin');
}
@@ -92,6 +95,8 @@ if (!empty($user)) {
// Get an existing token or create a new one.
$token = external_generate_token_for_current_user($service);
$privatetoken = $token->privatetoken;
$token->privatetoken = null;
// log token access
$DB->set_field('external_tokens', 'lastaccess', time(), array('id'=>$token->id));
@@ -103,8 +108,16 @@ if (!empty($user)) {
$event->add_record_snapshot('external_tokens', $token);
$event->trigger();
$siteadmin = has_capability('moodle/site:config', $systemcontext, $USER->id) || is_siteadmin($USER->id);
$usertoken = new stdClass;
$usertoken->token = $token->token;
// Private token, only transmitted to https sites and non-admin users.
if (is_https() and !$siteadmin) {
$usertoken->privatetoken = $privatetoken;
} else {
$usertoken->privatetoken = null;
}
echo json_encode($usertoken);
} else {
throw new moodle_exception('invalidlogin');
+1 -1
View File
@@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2016101401.01; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2016101401.02; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
+1
View File
@@ -340,6 +340,7 @@ class webservice {
$newtoken->contextid = context_system::instance()->id;
$newtoken->creatorid = $userid;
$newtoken->timecreated = time();
$newtoken->privatetoken = null;
$DB->insert_record('external_tokens', $newtoken);
}
+5
View File
@@ -12,6 +12,11 @@ This information is intended for authors of webservices, not people writing webs
In some contexts those parameteres are not necessary because is not required to do a file rewrite via
file_rewrite_pluginfile_urls.
* External function get_site_info now returns the site course ID. This new field is marked as VALUE_OPTIONAL for backwards compatibility.
* A new field "privatetoken" has been added to the "external_tokens" table.
This private token must be safely stored (or not stored at all) by the client because it will be used in places where a request
must be double-checked.
This token should not be passed via GET paramaters and it must be transmitted only via https.
This token is generated only in login/token.php after the user credential has been confirmed. It can't be generated by admins.
=== 3.1 ===