Skip to content

Commit

Permalink
MDL-76656 webservice: generated token can only read once
Browse files Browse the repository at this point in the history
Generated tokens should only read once.
Therefore removing the token column at the table view of the manage tokens page and the user's page.
The token should not be able to search.
  • Loading branch information
meirzamoodle committed Jun 28, 2023
1 parent 3cd8474 commit f69a420
Show file tree
Hide file tree
Showing 19 changed files with 220 additions and 39 deletions.
47 changes: 47 additions & 0 deletions admin/templates/webservice_token_new.mustache
@@ -0,0 +1,47 @@
{{!
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/>.
}}
{{!
@template core_admin/webservice_token_new
This is the template for displaying the newly created webservice token.
Context variable required for this template:
* token - Token value
Example context (json):
{
"token": "ef9ee4d0c6eed5eab8453a63b93b5b8b"
}
}}

<div class="d-inline-block">
<div class="alert alert-warning">
<div class="lead">{{#str}}tokennewmessage, webservice{{/str}}</div>
</div>
<div class="alert alert-primary">
<div class="lead">{{tokenname}}</div>
<div class="d-flex justify-content-start align-middle">
<div class="lead text-break pt-1" id="copytoclipboardtoken">{{token}}</div>
<button class="btn btn-primary ml-2" data-action="copytoclipboard" data-clipboard-target="#copytoclipboardtoken" data-clipboard-success-message="{{#str}}tokencopied, webservice{{/str}}">
{{#pix}}t/copy, core {{/pix}}{{#str}}copytoclipboard{{/str}}</button>
</div>
</div>
</div>

{{#js}}
require(['core/copy_to_clipboard']);
{{/js}}
44 changes: 35 additions & 9 deletions admin/tests/behat/manage_tokens.feature
Expand Up @@ -19,19 +19,31 @@ Feature: Manage external services tokens
And I am on site homepage
And I navigate to "Server > Web services > Manage tokens" in site administration
And I press "Create token"
And I set the field "Name" to "Webservice1"
And I set the field "User" to "Firstname1 Lastname1"
And I set the field "Service" to "Moodle mobile web service"
And I set the field "IP restriction" to "127.0.0.1"
When I press "Save changes"
Then I should see "Moodle mobile web service" in the "Firstname1 Lastname1" "table_row"
And I should see "127.0.0.1" in the "Firstname1 Lastname1" "table_row"
And I click on "Delete" "link" in the "Firstname1 Lastname1" "table_row"
Then I should see "Firstname1 Lastname1" in the "Webservice1" "table_row"
And I should see "127.0.0.1" in the "Webservice1" "table_row"

# Verify the message and the "Copy to clipboard" button.
And I should see "Copy the token now. It won't be shown again once you leave this page."
And "Copy to clipboard" "button" should exist

# New token can only read once.
And I reload the page
And I should not see "Copy the token now. It won't be shown again once you leave this page."
And "Copy to clipboard" "button" should not exist

# Delete token.
And I click on "Delete" "link" in the "Webservice1" "table_row"
And I should see "Do you really want to delete this web service token for Firstname1 Lastname1 on the service Moodle mobile web service?"
And I press "Delete"
And "Firstname1 Lastname1" "table_row" should not exist
And "Webservice1" "table_row" should not exist

@javascript @skip_chrome_zerosize
Scenario: Tokens can be filtered by user and by service
Scenario: Tokens can be filtered by name (case-insensitive), by user and by service
Given the following "core_webservice > Service" exists:
| name | Site information |
| shortname | siteinfo |
Expand All @@ -40,10 +52,10 @@ Feature: Manage external services tokens
| service | siteinfo |
| functions | core_webservice_get_site_info |
And the following "core_webservice > Tokens" exist:
| user | service |
| user2 | siteinfo |
| user3 | moodle_mobile_app |
| user4 | siteinfo |
| user | service | name |
| user2 | siteinfo | WEBservice1 |
| user3 | moodle_mobile_app | webservicE3 |
| user4 | siteinfo | New service2 |
When I log in as "admin"
And I navigate to "Server > Web services > Manage tokens" in site administration

Expand All @@ -53,6 +65,20 @@ Feature: Manage external services tokens
And I should see "Moodle mobile web service" in the "Firstname3 Lastname3" "table_row"
And I should see "Site information" in the "Firstname4 Lastname4" "table_row"

# Filter tokens by by name (case-insensitive).
And I click on "Tokens filter" "link"
And I set the field "Name" to "webservice"
And I press "Show only matching tokens"
And I should see "Site information" in the "Firstname2 Lastname2" "table_row"
And I should see "Moodle mobile web service" in the "Firstname3 Lastname3" "table_row"
And "Firstname4 Lastname4" "table_row" should not exist

# Reset the filter.
And I press "Show all tokens"
And I should see "Site information" in the "Firstname2 Lastname2" "table_row"
And I should see "Moodle mobile web service" in the "Firstname3 Lastname3" "table_row"
And I should see "Site information" in the "Firstname4 Lastname4" "table_row"

# Filter tokens by user (note we can select the user by the identity field here).
When I click on "Tokens filter" "link"
And I set the field "User" to "user2@example.com"
Expand Down
25 changes: 21 additions & 4 deletions admin/webservice/tokens.php
Expand Up @@ -30,7 +30,7 @@
$action = optional_param('action', '', PARAM_ALPHANUMEXT);
$tokenid = optional_param('tokenid', '', PARAM_SAFEDIR);
$confirm = optional_param('confirm', 0, PARAM_BOOL);
$ftoken = optional_param('ftoken', '', PARAM_ALPHANUM);
$fname = optional_param('fname', '', PARAM_ALPHANUM);
$fusers = optional_param_array('fusers', [], PARAM_INT);
$fservices = optional_param_array('fservices', [], PARAM_INT);

Expand Down Expand Up @@ -74,7 +74,8 @@
$data->user,
context_system::instance(),
$data->validuntil,
$data->iprestriction
$data->iprestriction,
$data->name
);
redirect($PAGE->url);
}
Expand Down Expand Up @@ -127,7 +128,7 @@
// Pre-populate the form with the values that come as a part of the URL - typically when using the table_sql control
// links.
$filterdata = (object)[
'token' => $ftoken,
'name' => $fname,
'users' => $fusers,
'services' => $fservices,
];
Expand All @@ -150,12 +151,28 @@
echo html_writer::div($OUTPUT->render(new single_button(new moodle_url($PAGE->url, ['action' => 'create']),
get_string('createtoken', 'core_webservice'), 'get', single_button::BUTTON_PRIMARY)), 'my-3');

if (!empty($SESSION->webservicenewlycreatedtoken)) {
$webservicemanager = new webservice();
$newtoken = $webservicemanager->get_created_by_user_ws_token(
$USER->id,
$SESSION->webservicenewlycreatedtoken
);
if ($newtoken) {
// Unset the session variable.
unset($SESSION->webservicenewlycreatedtoken);
// Display the newly created token.
echo $OUTPUT->render_from_template(
'core_admin/webservice_token_new', ['token' => $newtoken->token, 'tokenname' => $newtoken->tokenname]
);
}
}

$filter->display();

$table = new \core_webservice\token_table('webservicetokens', $filterdata);

// In order to not lose the filter form values by clicking the table control links, make them part of the table's baseurl.
$baseurl = new moodle_url($PAGE->url, ['ftoken' => $filterdata->token]);
$baseurl = new moodle_url($PAGE->url, ['fname' => $filterdata->name]);

foreach ($filterdata->users as $i => $userid) {
$baseurl->param("fusers[{$i}]", $userid);
Expand Down
1 change: 1 addition & 0 deletions lang/en/external.php
Expand Up @@ -31,6 +31,7 @@
$string['privacy:metadata:tokens:creatorid'] = 'The ID of the user who created the token';
$string['privacy:metadata:tokens:iprestriction'] = 'IP restricted to use this token';
$string['privacy:metadata:tokens:lastaccess'] = 'The date when the token was last used';
$string['privacy:metadata:tokens:name'] = 'The token name';
$string['privacy:metadata:tokens:privatetoken'] = 'A more private token occasionally used to validate certain operations, such as SSO';
$string['privacy:metadata:tokens:timecreated'] = 'The date when the token was created';
$string['privacy:metadata:tokens:token'] = 'The user\'s token';
Expand Down
6 changes: 6 additions & 0 deletions lang/en/webservice.php
Expand Up @@ -191,11 +191,16 @@
$string['testwithtestclientdescription'] = 'Simulate external access to the service using the web service test client. Use an enabled protocol with token authentication. <strong>WARNING: The functions that you test WILL BE EXECUTED, so be careful what you choose to test!</strong>';
$string['token'] = 'Token';
$string['tokenauthlog'] = 'Token authentication';
$string['tokencopied'] = 'Text copied to clipboard.';
$string['tokencreatedbyadmin'] = 'Can only be reset by administrator (*)';
$string['tokencreator'] = 'Creator';
$string['tokenfilter'] = 'Tokens filter';
$string['tokenfiltersubmit'] = 'Show only matching tokens';
$string['tokenfilterreset'] = 'Show all tokens';
$string['tokenname'] = 'Name';
$string['tokennamehint'] = 'If you don\'t enter a name then a random name will be used.';
$string['tokennameprefix'] = 'Webservice-{$a}';
$string['tokennewmessage'] = 'Copy the token now. It won\'t be shown again once you leave this page.';
$string['unknownoptionkey'] = 'Unknown option key ({$a})';
$string['unnamedstringparam'] = 'A string parameter is unnamed.';
$string['updateusersettings'] = 'Update';
Expand All @@ -210,6 +215,7 @@
$string['userservices'] = 'User services: {$a}';
$string['usersettingssaved'] = 'User settings saved';
$string['validuntil'] = 'Valid until';
$string['validuntil_empty'] = 'This token has no expiration date.';
$string['validuntil_help'] = 'If set, the service will be inactivated after this date for this user.';
$string['webservice'] = 'Web service';
$string['webservices'] = 'Web services';
Expand Down
3 changes: 2 additions & 1 deletion lib/db/install.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="lib/db" VERSION="20230307" COMMENT="XMLDB file for core Moodle tables"
<XMLDB PATH="lib/db" VERSION="20230524" COMMENT="XMLDB file for core Moodle tables"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
>
Expand Down Expand Up @@ -2902,6 +2902,7 @@
<FIELD NAME="validuntil" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="timestampt - valid until data"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="created timestamp"/>
<FIELD NAME="lastaccess" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="last access timestamp"/>
<FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="token name, used to identify the token at the table view"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
Expand Down
19 changes: 19 additions & 0 deletions lib/db/upgrade.php
Expand Up @@ -3314,5 +3314,24 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2023062200.00);
}

if ($oldversion < 2023062300.01) {
// Define field name to be added to external_tokens.
$table = new xmldb_table('external_tokens');
$field = new xmldb_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null, 'lastaccess');
// Conditionally launch add field name.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Update the old external tokens.
$sql = 'UPDATE {external_tokens}
SET name = ' . $DB->sql_concat(
// We only need the prefix, so leave the third param with an empty string.
"'" . get_string('tokennameprefix', 'webservice', '') . "'",
"id");
$DB->execute($sql);
// Main savepoint reached.
upgrade_main_savepoint(true, 2023062300.01);
}

return true;
}
2 changes: 2 additions & 0 deletions lib/external/classes/privacy/provider.php
Expand Up @@ -56,6 +56,7 @@ public static function get_metadata(collection $collection) : collection {
'validuntil' => 'privacy:metadata:tokens:validuntil',
'timecreated' => 'privacy:metadata:tokens:timecreated',
'lastaccess' => 'privacy:metadata:tokens:lastaccess',
'name' => 'privacy:metadata:tokens:name',
], 'privacy:metadata:tokens');

$collection->add_database_table('external_services_users', [
Expand Down Expand Up @@ -293,6 +294,7 @@ protected static function transform_token($record) {
'valid_until' => $record->validuntil ? transform::datetime($record->validuntil) : null,
'created_on' => transform::datetime($record->timecreated),
'last_access' => $record->lastaccess ? transform::datetime($record->lastaccess) : null,
'name' => $record->name,
];
}

Expand Down
32 changes: 29 additions & 3 deletions lib/external/classes/util.php
Expand Up @@ -173,6 +173,7 @@ public static function get_area_files($contextid, $component, $filearea, $itemid
* @param context $context
* @param int $validuntil date when the token expired
* @param string $iprestriction allowed ip - if 0 or empty then all ips are allowed
* @param string $name token name as a note or token identity at the table view.
* @return string generated token
*/
public static function generate_token(
Expand All @@ -181,9 +182,10 @@ public static function generate_token(
int $userid,
context $context,
int $validuntil = 0,
string $iprestriction = ''
string $iprestriction = '',
string $name = ''
): string {
global $DB, $USER;
global $DB, $USER, $SESSION;

// Make sure the token doesn't exist (even if it should be almost impossible with the random generation).
$numtries = 0;
Expand Down Expand Up @@ -220,7 +222,17 @@ public static function generate_token(

// Generate the private token, it must be transmitted only via https.
$newtoken->privatetoken = random_string(64);
$DB->insert_record('external_tokens', $newtoken);

if (!$name) {
// Generate a token name.
$name = self::generate_token_name();
}
$newtoken->name = $name;

$tokenid = $DB->insert_record('external_tokens', $newtoken);
// Create new session to hold newly created token ID.
$SESSION->webservicenewlycreatedtoken = $tokenid;

return $newtoken->token;
}

Expand Down Expand Up @@ -399,6 +411,7 @@ public static function generate_token_for_current_user(stdClass $service) {
$token->iprestriction = null;
$token->sid = null;
$token->lastaccess = null;
$token->name = self::generate_token_name();
// Generate the private token, it must be transmitted only via https.
$token->privatetoken = random_string(64);
$token->id = $DB->insert_record('external_tokens', $token);
Expand Down Expand Up @@ -619,4 +632,17 @@ public static function delete_service_descriptions(string $component): void {
$DB->delete_records('external_services', ['component' => $component]);
$DB->delete_records('external_functions', ['component' => $component]);
}

/**
* Generate token name.
*
* @return string
*/
public static function generate_token_name(): string {
return get_string(
'tokennameprefix',
'webservice',
random_string(5)
);
}
}
3 changes: 3 additions & 0 deletions lib/upgrade.txt
Expand Up @@ -30,6 +30,9 @@ information provided here is intended especially for developers.
- core_useragent::get_device_type_cfg_var_name()
- theme_is_device_locked()
- theme_get_locked_theme_for_device()
* Addition of new 'name' field in the external_tokens table.
* \core_external\util::generate_token() has a new optional argument "name" used as a token name.
* Introduce a new public function \core_external\util::generate_token_name()

=== 4.2 ===

Expand Down
2 changes: 1 addition & 1 deletion version.php
Expand Up @@ -29,7 +29,7 @@

defined('MOODLE_INTERNAL') || die();

$version = 2023062300.00; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2023062300.01; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '4.3dev (Build: 20230623)'; // Human-friendly version name
Expand Down
6 changes: 3 additions & 3 deletions webservice/classes/token_filter.php
Expand Up @@ -51,9 +51,9 @@ public function definition() {
$mform->setExpanded('tokenfilter', true);
}

// Token.
$mform->addElement('text', 'token', get_string('token', 'core_webservice'), ['size' => 32]);
$mform->setType('token', PARAM_ALPHANUM);
// Token name.
$mform->addElement('text', 'name', get_string('tokenname', 'core_webservice'), ['size' => 32]);
$mform->setType('name', PARAM_TEXT);

// User selector.
$attributes = [
Expand Down
10 changes: 10 additions & 0 deletions webservice/classes/token_form.php
Expand Up @@ -26,6 +26,8 @@
namespace core_webservice;

use core_user;
use DateInterval;
use DateTime;

/**
* Form to create and edit a web service token.
Expand All @@ -49,6 +51,10 @@ public function definition() {

$mform->addElement('header', 'token', get_string('token', 'webservice'));

$mform->addElement('text', 'name', get_string('tokenname', 'webservice'));
$mform->setType('name', PARAM_TEXT);
$mform->addElement('static', 'tokennamehint', '', get_string('tokennamehint', 'webservice'));

// User selector.
$attributes = [
'multiple' => false,
Expand Down Expand Up @@ -90,6 +96,10 @@ public function definition() {

$mform->addElement('date_selector', 'validuntil',
get_string('validuntil', 'webservice'), array('optional' => true));
// Expires in 30 days.
$expires = new DateTime();
$expires->add(new DateInterval("P30D"));
$mform->setDefault('validuntil', $expires->getTimestamp());
$mform->setType('validuntil', PARAM_INT);

$mform->addElement('hidden', 'action');
Expand Down

0 comments on commit f69a420

Please sign in to comment.