diff --git a/admin/tests/behat/manage_tokens.feature b/admin/tests/behat/manage_tokens.feature new file mode 100644 index 00000000000..d30e2300bd1 --- /dev/null +++ b/admin/tests/behat/manage_tokens.feature @@ -0,0 +1,26 @@ +@core @core_admin +Feature: Manage tokens + In order to manage webservice usage + As an admin + I need to be able to create and delete tokens + + Background: + Given the following "users" exist: + | username | password | firstname | lastname | + | testuser | testuser | Joe | Bloggs | + | testuser2 | testuser2 | TestFirstname | TestLastname | + And I log in as "admin" + And I am on site homepage + + @javascript + Scenario: Add & delete a token + Given I navigate to "Plugins > Web services > Manage tokens" in site administration + And I follow "Add" + And I set the field "User" to "Joe Bloggs" + And I set the field "IP restriction" to "127.0.0.1" + When I press "Save changes" + Then I should see "Joe Bloggs" + And I should see "127.0.0.1" + And I follow "Delete" + And I press "Delete" + And I should not see "Joe Bloggs" diff --git a/admin/webservice/tokens.php b/admin/webservice/tokens.php index 60ea271005f..f15f7c20578 100644 --- a/admin/webservice/tokens.php +++ b/admin/webservice/tokens.php @@ -101,7 +101,11 @@ switch ($action) { break; case 'delete': - $token = $webservicemanager->get_created_by_user_ws_token($USER->id, $tokenid); + $token = $webservicemanager->get_token_by_id_with_details($tokenid); + + if ($token->creatorid != $USER->id) { + require_capability("moodle/webservice:managealltokens", context_system::instance()); + } //Delete the token if ($confirm and confirm_sesskey()) { diff --git a/lang/en/role.php b/lang/en/role.php index 45870a2f0ba..2bfc5df9fdd 100644 --- a/lang/en/role.php +++ b/lang/en/role.php @@ -455,9 +455,9 @@ $string['useshowadvancedtochange'] = 'Use \'Show advanced\' to change'; $string['viewingdefinitionofrolex'] = 'Viewing the definition of role \'{$a}\''; $string['viewrole'] = 'View role details'; $string['webservice:createtoken'] = 'Create a web service token'; +$string['webservice:managealltokens'] = 'Manage all users\' web services'; $string['webservice:createmobiletoken'] = 'Create a web service token for mobile access'; $string['whydoesuserhavecap'] = 'Why does {$a->fullname} have capability {$a->capability} in context {$a->context}?'; $string['whydoesusernothavecap'] = 'Why does {$a->fullname} not have capability {$a->capability} in context {$a->context}?'; $string['xroleassignments'] = '{$a}\'s role assignments'; $string['xuserswiththerole'] = 'Users with the role "{$a->role}"'; - diff --git a/lang/en/webservice.php b/lang/en/webservice.php index ebfe9384082..057f9f609b1 100644 --- a/lang/en/webservice.php +++ b/lang/en/webservice.php @@ -131,6 +131,7 @@ $string['norequiredcapability'] = 'No required capability'; $string['notoken'] = 'The token list is empty.'; $string['onesystemcontrolling'] = 'Allow an external system to control Moodle'; $string['onesystemcontrollingdescription'] = 'The following steps help you to set up the Moodle web services to allow an external system to interact with Moodle. This includes setting up a token (security key) authentication method.'; +$string['onlyseecreatedtokens'] = 'Only tokens you own or created can be seen. You can still delete other tokens.'; $string['operation'] = 'Operation'; $string['optional'] = 'Optional'; $string['passwordisexpired'] = 'Password is expired.'; diff --git a/lib/adminlib.php b/lib/adminlib.php index 53866698d66..410130ec38a 100644 --- a/lib/adminlib.php +++ b/lib/adminlib.php @@ -9524,84 +9524,29 @@ class admin_setting_managewebservicetokens extends admin_setting { * @return string */ public function output_html($data, $query='') { - global $CFG, $OUTPUT, $DB, $USER; + global $CFG, $OUTPUT; - // display strings - $stroperation = get_string('operation', 'webservice'); - $strtoken = get_string('token', 'webservice'); - $strservice = get_string('service', 'webservice'); - $struser = get_string('user'); - $strcontext = get_string('context', 'webservice'); - $strvaliduntil = get_string('validuntil', 'webservice'); - $striprestriction = get_string('iprestriction', 'webservice'); + require_once($CFG->dirroot . '/webservice/classes/token_table.php'); + $baseurl = new moodle_url('/' . $CFG->admin . '/settings.php?section=webservicetokens'); $return = $OUTPUT->box_start('generalbox webservicestokenui'); - $table = new html_table(); - $table->head = array($strtoken, $struser, $strservice, $striprestriction, $strvaliduntil, $stroperation); - $table->colclasses = array('leftalign', 'leftalign', 'leftalign', 'centeralign', 'centeralign', 'centeralign'); - $table->id = 'webservicetokens'; - $table->attributes['class'] = 'admintable generaltable'; + if (has_capability('moodle/webservice:managealltokens', context_system::instance())) { + $return .= \html_writer::div(get_string('onlyseecreatedtokens', 'webservice')); + } + + $table = new \webservice\token_table('webservicetokens'); + $table->define_baseurl($baseurl); + $table->attributes['class'] = 'admintable generaltable'; // Any need changing? $table->data = array(); + ob_start(); + $table->out(10, false); + $tablehtml = ob_get_contents(); + ob_end_clean(); + $return .= $tablehtml; $tokenpageurl = "$CFG->wwwroot/$CFG->admin/webservice/tokens.php?sesskey=" . sesskey(); - //TODO: in order to let the administrator delete obsolete token, split this request in multiple request or use LEFT JOIN - - //here retrieve token list (including linked users firstname/lastname and linked services name) - $sql = "SELECT t.id, t.token, u.id AS userid, u.firstname, u.lastname, s.name, t.iprestriction, t.validuntil, s.id AS serviceid - FROM {external_tokens} t, {user} u, {external_services} s - WHERE t.creatorid=? AND t.tokentype = ? AND s.id = t.externalserviceid AND t.userid = u.id"; - $tokens = $DB->get_records_sql($sql, array($USER->id, EXTERNAL_TOKEN_PERMANENT)); - if (!empty($tokens)) { - foreach ($tokens as $token) { - //TODO: retrieve context - - $delete = "id."\">"; - $delete .= get_string('delete').""; - - $validuntil = ''; - if (!empty($token->validuntil)) { - $validuntil = userdate($token->validuntil, get_string('strftimedatetime', 'langconfig')); - } - - $iprestriction = ''; - if (!empty($token->iprestriction)) { - $iprestriction = $token->iprestriction; - } - - $userprofilurl = new moodle_url('/user/profile.php?id='.$token->userid); - $useratag = html_writer::start_tag('a', array('href' => $userprofilurl)); - $useratag .= $token->firstname." ".$token->lastname; - $useratag .= html_writer::end_tag('a'); - - //check user missing capabilities - require_once($CFG->dirroot . '/webservice/lib.php'); - $webservicemanager = new webservice(); - $usermissingcaps = $webservicemanager->get_missing_capabilities_by_users( - array(array('id' => $token->userid)), $token->serviceid); - - if (!is_siteadmin($token->userid) and - array_key_exists($token->userid, $usermissingcaps)) { - $missingcapabilities = implode(', ', - $usermissingcaps[$token->userid]); - if (!empty($missingcapabilities)) { - $useratag .= html_writer::tag('div', - get_string('usermissingcaps', 'webservice', - $missingcapabilities) - . ' ' . $OUTPUT->help_icon('missingcaps', 'webservice'), - array('class' => 'missingcaps')); - } - } - - $table->data[] = array($token->token, $useratag, $token->name, $iprestriction, $validuntil, $delete); - } - - $return .= html_writer::table($table); - } else { - $return .= get_string('notoken', 'webservice'); - } - $return .= $OUTPUT->box_end(); // add a token to the table $return .= ""; diff --git a/lib/db/access.php b/lib/db/access.php index 808d0b9c1ea..86c450ca279 100644 --- a/lib/db/access.php +++ b/lib/db/access.php @@ -1845,6 +1845,13 @@ $capabilities = array( 'manager' => CAP_ALLOW ) ), + 'moodle/webservice:managealltokens' => array( + + 'riskbitmask' => RISK_CONFIG | RISK_DATALOSS | RISK_PERSONAL, + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array() + ), 'moodle/webservice:createmobiletoken' => array( 'riskbitmask' => RISK_SPAM | RISK_PERSONAL, diff --git a/webservice/classes/token_table.php b/webservice/classes/token_table.php new file mode 100644 index 00000000000..059fc049d8f --- /dev/null +++ b/webservice/classes/token_table.php @@ -0,0 +1,260 @@ +. + +/** + * Contains the class used for the displaying the tokens table. + * + * @package core_webservice + * @copyright 2017 John Okely + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace webservice; + +defined('MOODLE_INTERNAL') || die; + +require_once($CFG->libdir . '/tablelib.php'); +require_once($CFG->dirroot . '/webservice/lib.php'); +require_once($CFG->dirroot . '/user/lib.php'); + +/** + * Class for the displaying the participants table. + * + * @package core_webservice + * @copyright 2017 John Okely + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class token_table extends \table_sql { + + /** + * @var bool $showalltokens Whether or not the user is able to see all tokens. + */ + protected $showalltokens; + + /** + * Sets up the table. + * @param int $id The id of the table + */ + public function __construct($id) { + parent::__construct($id); + + // Get the context. + $context = \context_system::instance(); + + // Can we see tokens created by all users? + $this->showalltokens = has_capability('moodle/webservice:managealltokens', $context); + + // Define the headers and columns. + $headers = []; + $columns = []; + + $headers[] = get_string('token', 'webservice'); + $columns[] = 'token'; + $headers[] = get_string('user'); + $columns[] = 'fullname'; + $headers[] = get_string('service', 'webservice'); + $columns[] = 'name'; + $headers[] = get_string('iprestriction', 'webservice'); + $columns[] = 'iprestriction'; + $headers[] = get_string('validuntil', 'webservice'); + $columns[] = 'validuntil'; + if ($this->showalltokens) { + // Only need to show creator if you can see tokens created by other people. + $headers[] = get_string('tokencreator', 'webservice'); + $columns[] = 'creatorlastname'; // So we can have semi-useful sorting. Table SQL doesn't two fullname collumns. + } + $headers[] = get_string('operation', 'webservice'); + $columns[] = 'operation'; + + $this->define_columns($columns); + $this->define_headers($headers); + + $this->no_sorting('operation'); + $this->no_sorting('token'); + $this->no_sorting('iprestriction'); + + $this->set_attribute('id', $id); + } + + /** + * Generate the operation column. + * + * @param \stdClass $data Data for the current row + * @return string Content for the column + */ + public function col_operation($data) { + $tokenpageurl = new \moodle_url( + "/admin/webservice/tokens.php", + [ + "sesskey" => sesskey(), + "action" => "delete", + "tokenid" => $data->id + ] + ); + return \html_writer::link($tokenpageurl, get_string("delete")); + } + + /** + * Generate the validuntil column. + * + * @param \stdClass $data Data for the current row + * @return string Content for the column + */ + public function col_validuntil($data) { + if (empty($data->validuntil)) { + return ''; + } else { + return userdate($data->validuntil, get_string('strftimedatetime', 'langconfig')); + } + } + + /** + * Generate the fullname column. Also includes capabilities the user is missing for the webservice (if any) + * + * @param \stdClass $data Data for the current row + * @return string Content for the column + */ + public function col_fullname($data) { + global $OUTPUT; + + $userprofilurl = new \moodle_url('/user/profile.php', ['id' => $data->userid]); + $content = \html_writer::link($userprofilurl, fullname($data)); + + // Make up list of capabilities that the user is missing for the given webservice. + $webservicemanager = new \webservice(); + $usermissingcaps = $webservicemanager->get_missing_capabilities_by_users([['id' => $data->userid]], $data->serviceid); + + if (!is_siteadmin($data->userid) && array_key_exists($data->userid, $usermissingcaps)) { + $missingcapabilities = implode(', ', $usermissingcaps[$data->userid]); + if (!empty($missingcapabilities)) { + $capabilitiesstring = get_string('usermissingcaps', 'webservice', $missingcapabilities) . ' ' . + $OUTPUT->help_icon('missingcaps', 'webservice'); + $content .= \html_writer::div($capabilitiesstring, 'missingcaps'); + } + } + + return $content; + } + + /** + * Generate the token column. + * + * @param \stdClass $data Data for the current row + * @return string Content for the column + */ + public function col_token($data) { + global $USER; + // Hide the token if it wasn't created by the current user. + if ($data->creatorid != $USER->id) { + return '-'; + } + + return $data->token; + } + + /** + * Generate the creator column. + * + * @param \stdClass $data + * @return string + */ + public function col_creatorlastname($data) { + // We have loaded all the name fields for the creator, with the 'creator' prefix. + // So just remove the prefix and make up a user object. + $user = []; + foreach ($data as $key => $value) { + if (strpos($key, 'creator') !== false) { + $newkey = str_replace('creator', '', $key); + $user[$newkey] = $value; + } + } + + $creatorprofileurl = new \moodle_url('/user/profile.php', ['id' => $data->creatorid]); + return \html_writer::link($creatorprofileurl, fullname((object)$user)); + } + + /** + * This function is used for the extra user fields. + * + * These are being dynamically added to the table so there are no functions 'col_' as + * the list has the potential to increase in the future and we don't want to have to remember to add + * a new method to this class. We also don't want to pollute this class with unnecessary methods. + * + * @param string $colname The column name + * @param \stdClass $data + * @return string + */ + public function other_cols($colname, $data) { + return s($data->{$colname}); + } + + /** + * Query the database for results to display in the table. + * + * Note: Initial bars are not implemented for this table because it includes user details twice and the initial bars do not work + * when the user table is included more than once. + * + * @param int $pagesize size of page for paginated displayed table. + * @param bool $useinitialsbar Not implemented. Please pass false. + */ + public function query_db($pagesize, $useinitialsbar = false) { + global $DB, $USER; + + if ($useinitialsbar) { + debugging('Initial bar not implemented yet. Call out($pagesize, false)'); + } + + $usernamefields = get_all_user_name_fields(true, 'u'); + $creatorfields = get_all_user_name_fields(true, 'c', null, 'creator'); + + $params = ["tokenmode" => EXTERNAL_TOKEN_PERMANENT]; + + // TODO: in order to let the administrator delete obsolete token, split the request in multiple request or use LEFT JOIN. + + if ($this->showalltokens) { + // Show all tokens. + $sql = "SELECT t.id, t.token, u.id AS userid, $usernamefields, s.name, t.iprestriction, t.validuntil, s.id AS serviceid, + t.creatorid, $creatorfields + FROM {external_tokens} t, {user} u, {external_services} s, {user} c + WHERE t.tokentype = :tokenmode AND s.id = t.externalserviceid AND t.userid = u.id AND c.id = t.creatorid"; + $countsql = "SELECT COUNT(t.id) + FROM {external_tokens} t, {user} u, {external_services} s, {user} c + WHERE t.tokentype = :tokenmode AND s.id = t.externalserviceid AND t.userid = u.id AND c.id = t.creatorid"; + } else { + // Only show tokens created by the current user. + $sql = "SELECT t.id, t.token, u.id AS userid, $usernamefields, s.name, t.iprestriction, t.validuntil, s.id AS serviceid, + t.creatorid, $creatorfields + FROM {external_tokens} t, {user} u, {external_services} s, {user} c + WHERE t.creatorid=:userid AND t.tokentype = :tokenmode AND s.id = t.externalserviceid AND t.userid = u.id AND + c.id = t.creatorid"; + $countsql = "SELECT COUNT(t.id) + FROM {external_tokens} t, {user} u, {external_services} s, {user} c + WHERE t.creatorid=:userid AND t.tokentype = :tokenmode AND s.id = t.externalserviceid AND + t.userid = u.id AND c.id = t.creatorid"; + $params["userid"] = $USER->id; + } + + $sort = $this->get_sql_sort(); + if ($sort) { + $sql = $sql . ' ORDER BY ' . $sort; + } + + $total = $DB->count_records_sql($countsql, $params); + $this->pagesize($pagesize, $total); + + $this->rawdata = $DB->get_recordset_sql($sql, $params, $this->get_page_start(), $this->get_page_size()); + } +} diff --git a/webservice/lib.php b/webservice/lib.php index 0101ccacded..2117e8bc28e 100644 --- a/webservice/lib.php +++ b/webservice/lib.php @@ -402,6 +402,29 @@ class webservice { return $token; } + /** + * Return a token of an arbitrary user by tokenid, including details of the associated user and the service name. + * If no tokens exist an exception is thrown + * + * The returned value is a stdClass: + * ->id token id + * ->token + * ->firstname user firstname + * ->lastname + * ->name service name + * + * @param int $tokenid token id + * @return stdClass + */ + public function get_token_by_id_with_details($tokenid) { + global $DB; + $sql = "SELECT t.id, t.token, u.id AS userid, u.firstname, u.lastname, s.name, t.creatorid + FROM {external_tokens} t, {user} u, {external_services} s + WHERE t.id=? AND t.tokentype = ? AND s.id = t.externalserviceid AND t.userid = u.id"; + $token = $DB->get_record_sql($sql, array($tokenid, EXTERNAL_TOKEN_PERMANENT), MUST_EXIST); + return $token; + } + /** * Return a database token record for a token id *