Merge branch 'MDL-78511-master2' of https://github.com/raortegar/moodle

This commit is contained in:
Ilya Tregubov 2023-12-14 08:25:33 +08:00
commit d1024fae70
2393 changed files with 85375 additions and 15 deletions

View file

@ -29,7 +29,7 @@ use tool_mfa\local\form\revoke_factor_form;
require_login(null, false);
if (isguestuser()) {
throw new require_login_exception('Guests are not allowed here.');
throw new require_login_exception('error:isguestuser', 'tool_mfa');
}
$action = optional_param('action', '', PARAM_ALPHANUMEXT);
@ -85,6 +85,7 @@ switch ($action) {
$form->is_validated();
if ($form->is_cancelled()) {
$factorobject->setup_factor_form_is_cancelled($factorid);
redirect($returnurl);
}

View file

@ -65,7 +65,7 @@ interface object_factor {
* Defines setup_factor form definition page for particular factor.
*
* @param \MoodleQuickForm $mform
* @return object $mform
* @return \MoodleQuickForm $mform
* @throws \coding_exception
*/
public function setup_factor_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm;
@ -74,7 +74,7 @@ interface object_factor {
* Defines setup_factor form definition page after form data has been set.
*
* @param \MoodleQuickForm $mform
* @return object $mform
* @return \MoodleQuickForm $mform
* @throws \coding_exception
*/
public function setup_factor_form_definition_after_data(\MoodleQuickForm $mform): \MoodleQuickForm;
@ -92,7 +92,7 @@ interface object_factor {
* Defines login form definition page for particular factor.
*
* @param \MoodleQuickForm $mform
* @return object $mform
* @return \MoodleQuickForm $mform
* @throws \coding_exception
*/
public function login_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm;
@ -101,7 +101,7 @@ interface object_factor {
* Defines login form definition page after form data has been set.
*
* @param \MoodleQuickForm $mform
* @return object $mform
* @return \MoodleQuickForm $mform
* @throws \coding_exception
*/
public function login_form_definition_after_data(\MoodleQuickForm $mform): \MoodleQuickForm;
@ -115,6 +115,21 @@ interface object_factor {
*/
public function login_form_validation(array $data): array;
/**
* Setups in given factor when the form is cancelled
*
* @param int $factorid
* @return void
*/
public function setup_factor_form_is_cancelled(int $factorid): void;
/**
* Setup submit button string in given factor
*
* @return string|null
*/
public function setup_factor_form_submit_button_string(): ?string;
/**
* Setups given factor and adds it to user's active factors list.
* Returns true if factor has been successfully added, otherwise false.

View file

@ -152,7 +152,7 @@ abstract class object_factor_base implements object_factor {
* Dummy implementation. Should be overridden in child class.
*
* @param \MoodleQuickForm $mform
* @return object $mform
* @return \MoodleQuickForm $mform
*/
public function setup_factor_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm {
return $mform;
@ -164,7 +164,7 @@ abstract class object_factor_base implements object_factor {
* Dummy implementation. Should be overridden in child class.
*
* @param \MoodleQuickForm $mform
* @return object $mform
* @return \MoodleQuickForm $mform
*/
public function setup_factor_form_definition_after_data(\MoodleQuickForm $mform): \MoodleQuickForm {
return $mform;
@ -183,6 +183,28 @@ abstract class object_factor_base implements object_factor {
return [];
}
/**
* Setups in given factor when the form is cancelled
*
* Dummy implementation. Should be overridden in child class.
*
* @param int $factorid
* @return void
*/
public function setup_factor_form_is_cancelled(int $factorid): void {
}
/**
* Setup submit button string in given factor
*
* Dummy implementation. Should be overridden in child class.
*
* @return string|null
*/
public function setup_factor_form_submit_button_string(): ?string {
return null;
}
/**
* Setups given factor and adds it to user's active factors list.
* Returns true if factor has been successfully added, otherwise false.
@ -232,7 +254,7 @@ abstract class object_factor_base implements object_factor {
* Dummy implementation. Should be overridden in child class.
*
* @param \MoodleQuickForm $mform
* @return object $mform
* @return \MoodleQuickForm $mform
*/
public function login_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm {
return $mform;
@ -244,7 +266,7 @@ abstract class object_factor_base implements object_factor {
* Dummy implementation. Should be overridden in child class.
*
* @param \MoodleQuickForm $mform
* @return object $mform
* @return \MoodleQuickForm $mform
*/
public function login_form_definition_after_data(\MoodleQuickForm $mform): \MoodleQuickForm {
return $mform;

View file

@ -71,7 +71,7 @@ class setup_factor_form extends \moodleform {
$factor = \tool_mfa\plugininfo\factor::get_factor($factorname);
$mform = $factor->setup_factor_form_definition_after_data($mform);
$this->xss_whitelist_static_form_elements($mform);
$this->add_action_buttons();
$this->add_action_buttons(true, $factor->setup_factor_form_submit_button_string());
}
/**

View file

@ -49,7 +49,7 @@ class factor extends object_factor_base {
* E-Mail Factor implementation.
*
* @param \MoodleQuickForm $mform Form to inject global elements into.
* @return object $mform
* @return \MoodleQuickForm $mform
*/
public function login_form_definition_after_data(\MoodleQuickForm $mform): \MoodleQuickForm {
$this->generate_and_email_code();

View file

@ -0,0 +1,75 @@
<?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/>.
/**
* Admin setting for AWS regions.
*
* @package factor_sms
* @author Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace factor_sms;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/lib/adminlib.php');
/**
* Admin setting for a list of AWS regions.
*
* @package factor_sms
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class admin_settings_aws_region extends \admin_setting_configtext {
/**
* Return part of form with setting.
*
* @param mixed $data array or string depending on setting
* @param string $query
* @return string
*/
public function output_html($data, $query='') {
global $CFG, $OUTPUT;
$default = $this->get_defaultsetting();
$options = [];
$all = require_once($CFG->dirroot . '/lib/aws-sdk/src/data/endpoints.json.php');
$ends = $all['partitions'][0]['regions'];
if ($ends) {
foreach ($ends as $key => $value) {
$options[] = [
'value' => $key,
'label' => $key . ' - ' . $value['description'],
];
}
}
$context = [
'list' => $this->get_full_name(),
'name' => $this->get_full_name(),
'id' => $this->get_id(),
'value' => $data,
'size' => $this->size,
'options' => $options,
];
$element = $OUTPUT->render_from_template('factor_sms/setting_aws_region', $context);
return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
}
}

View file

@ -0,0 +1,62 @@
<?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/>.
namespace factor_sms\event;
/**
* Event for a sent SMS
*
* @package factor_sms
* @author Alex Morris <alex.morris@catalyst.net.nz>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class sms_sent extends \core\event\base {
/**
* Init sms sent event
*/
protected function init() {
$this->data['crud'] = 'r';
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Returns non-localised event description with id's for admin use only.
*
* @return string
*/
public function get_description(): string {
$content = [
'userid' => $this->other['userid'],
'debuginfo' => is_array($this->other['debug']) ? json_encode($this->other['debug']) : $this->other['debug'],
];
return get_string('event:smssentdescription', 'factor_sms', $content);
}
/**
* Returns localised general event name.
*
* Override in subclass, we can not make it static and abstract at the same time.
*
* @return string
*/
public static function get_name(): string {
return get_string('event:smssent', 'factor_sms');
}
}

View file

@ -0,0 +1,457 @@
<?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/>.
namespace factor_sms;
use moodle_url;
use stdClass;
use tool_mfa\local\factor\object_factor_base;
/**
* SMS Factor implementation.
*
* @package factor_sms
* @subpackage tool_mfa
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor extends object_factor_base {
/** @var string Factor icon */
protected $icon = 'fa-commenting-o';
/**
* Defines login form definition page for SMS Factor.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
*/
public function login_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm {
$mform->addElement(new \tool_mfa\local\form\verification_field());
$mform->setType('verificationcode', PARAM_ALPHANUM);
return $mform;
}
/**
* Defines login form definition page after form data has been set.
*
* @param \MoodleQuickForm $mform Form to inject global elements into.
* @return \MoodleQuickForm $mform
*/
public function login_form_definition_after_data(\MoodleQuickForm $mform): \MoodleQuickForm {
$this->generate_and_sms_code();
// Disable the form check prompt.
$mform->disable_form_change_checker();
return $mform;
}
/**
* Implements login form validation for SMS Factor.
*
* @param array $data
* @return array
*/
public function login_form_validation(array $data): array {
$return = [];
if (!$this->check_verification_code($data['verificationcode'])) {
$return['verificationcode'] = get_string('error:wrongverification', 'factor_sms');
}
return $return;
}
/**
* Gets the string for setup button on preferences page.
*
* @return string
*/
public function get_setup_string(): string {
return get_string('setupfactorbutton', 'factor_sms');
}
/**
* Defines setup_factor form definition page for SMS Factor.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
*/
public function setup_factor_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm {
global $OUTPUT, $USER, $DB;
if (!empty(
$phonenumber = $DB->get_field('tool_mfa', 'label', ['factor' => $this->name, 'userid' => $USER->id, 'revoked' => 0])
)) {
redirect(
new \moodle_url('/admin/tool/mfa/user_preferences.php'),
get_string('factorsetup', 'tool_mfa', $phonenumber),
null,
\core\output\notification::NOTIFY_SUCCESS);
}
$mform->addElement('html', $OUTPUT->heading(get_string('setupfactor', 'factor_sms'), 2));
if (empty($this->get_phonenumber())) {
$mform->addElement('hidden', 'verificationcode', 0);
$mform->setType('verificationcode', PARAM_ALPHANUM);
// Add field for phone number setup.
$mform->addElement('text', 'phonenumber', get_string('addnumber', 'factor_sms'),
[
'autocomplete' => 'tel',
'inputmode' => 'tel',
]);
$mform->setType('phonenumber', PARAM_TEXT);
// HTML to display a message about the phone number.
$message = \html_writer::tag('div', '', ['class' => 'col-md-3']);
$message .= \html_writer::tag(
'div', \html_writer::tag('p', get_string('phonehelp', 'factor_sms')), ['class' => 'col-md-9']);
$mform->addElement('html', \html_writer::tag('div', $message, ['class' => 'row']));
}
return $mform;
}
/**
* Defines setup_factor form definition page after form data has been set.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
*/
public function setup_factor_form_definition_after_data(\MoodleQuickForm $mform): \MoodleQuickForm {
global $OUTPUT;
$phonenumber = $this->get_phonenumber();
if (empty($phonenumber)) {
return $mform;
}
$duration = get_config('factor_sms', 'duration');
$code = $this->secretmanager->create_secret($duration, true);
if (!empty($code)) {
$this->sms_verification_code($code, $phonenumber);
}
$message = get_string('logindesc', 'factor_sms', '<b>' . $phonenumber . '</b><br/>');
$message .= get_string('editphonenumberinfo', 'factor_sms');
$mform->addElement('html', \html_writer::tag('p', $OUTPUT->notification($message, 'success')));
$mform->addElement(new \tool_mfa\local\form\verification_field());
$mform->setType('verificationcode', PARAM_ALPHANUM);
$editphonenumber = \html_writer::link(
new \moodle_url('/admin/tool/mfa/factor/sms/editphonenumber.php', ['sesskey' => sesskey()]),
get_string('editphonenumber', 'factor_sms'),
['class' => 'btn btn-secondary', 'type' => 'button']);
$mform->addElement('html', \html_writer::tag('div', $editphonenumber, ['class' => 'float-sm-left col-md-4']));
// Disable the form check prompt.
$mform->disable_form_change_checker();
return $mform;
}
/**
* Returns the phone number from the current session or from the user profile data.
* @return string|null
*/
private function get_phonenumber(): ?string {
global $SESSION, $USER, $DB;
if (!empty($SESSION->tool_mfa_sms_number)) {
return $SESSION->tool_mfa_sms_number;
}
$phonenumber = $DB->get_field('tool_mfa', 'label', ['factor' => $this->name, 'userid' => $USER->id, 'revoked' => 0]);
if (!empty($phonenumber)) {
return $phonenumber;
}
return null;
}
/**
* Returns an array of errors, where array key = field id and array value = error text.
*
* @param array $data
* @return array
*/
public function setup_factor_form_validation(array $data): array {
$errors = [];
// Phone number validation.
if (!empty($data["phonenumber"]) && empty(helper::is_valid_phonenumber($data["phonenumber"]))) {
$errors['phonenumber'] = get_string('error:wrongphonenumber', 'factor_sms');
} else if (!empty($this->get_phonenumber())) {
// Code validation.
if (empty($data["verificationcode"])) {
$errors['verificationcode'] = get_string('error:emptyverification', 'factor_sms');
} else if ($this->secretmanager->validate_secret($data['verificationcode']) !== $this->secretmanager::VALID) {
$errors['verificationcode'] = get_string('error:wrongverification', 'factor_sms');
}
}
return $errors;
}
/**
* Reset values of the session data of the given factor.
*
* @param int $factorid
* @return void
*/
public function setup_factor_form_is_cancelled(int $factorid): void {
global $SESSION;
if (!empty($SESSION->tool_mfa_sms_number)) {
unset($SESSION->tool_mfa_sms_number);
}
// Clean temp secrets code.
$secretmanager = new \tool_mfa\local\secret_manager('sms');
$secretmanager->cleanup_temp_secrets();
}
/**
* Setup submit button string in given factor
*
* @return string|null
*/
public function setup_factor_form_submit_button_string(): ?string {
global $SESSION;
if (!empty($SESSION->tool_mfa_sms_number)) {
return get_string('setupsubmitcode', 'factor_sms');
}
return get_string('setupsubmitphone', 'factor_sms');
}
/**
* Adds an instance of the factor for a user, from form data.
*
* @param stdClass $data
* @return stdClass|null the factor record, or null.
*/
public function setup_user_factor(stdClass $data): ?stdClass {
global $DB, $SESSION, $USER;
// Handle phone number submission.
if (empty($SESSION->tool_mfa_sms_number)) {
$SESSION->tool_mfa_sms_number = !empty($data->phonenumber) ? $data->phonenumber : '';
$addurl = new \moodle_url('/admin/tool/mfa/action.php', [
'action' => 'setup',
'factor' => 'sms',
]);
redirect($addurl);
}
// If the user somehow gets here through form resubmission.
// We dont want two phones active.
if ($DB->record_exists('tool_mfa', ['userid' => $USER->id, 'factor' => $this->name, 'revoked' => 0])) {
return null;
}
$time = time();
$label = $this->get_phonenumber();
$row = new \stdClass();
$row->userid = $USER->id;
$row->factor = $this->name;
$row->secret = '';
$row->label = $label;
$row->timecreated = $time;
$row->createdfromip = $USER->lastip;
$row->timemodified = $time;
$row->lastverified = $time;
$row->revoked = 0;
$id = $DB->insert_record('tool_mfa', $row);
$record = $DB->get_record('tool_mfa', ['id' => $id]);
$this->create_event_after_factor_setup($USER);
// Remove session phone number.
unset($SESSION->tool_mfa_sms_number);
return $record;
}
/**
* Returns an array of all user factors of given type.
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_all_user_factors(stdClass $user): array {
global $DB;
$sql = 'SELECT *
FROM {tool_mfa}
WHERE userid = ?
AND factor = ?
AND label IS NOT NULL
AND revoked = 0';
return $DB->get_records_sql($sql, [$user->id, $this->name]);
}
/**
* Returns the information about factor availability.
*
* @return bool
*/
public function is_enabled(): bool {
if (empty(get_config('factor_sms', 'gateway'))) {
return false;
}
$class = '\factor_sms\local\smsgateway\\' . get_config('factor_sms', 'gateway');
if (!call_user_func($class . '::is_gateway_enabled')) {
return false;
}
return parent::is_enabled();
}
/**
* Decides if a factor requires input from the user to verify.
*
* @return bool
*/
public function has_input(): bool {
return true;
}
/**
* Decides if factor needs to be setup by user and has setup_form.
*
* @return bool
*/
public function has_setup(): bool {
return true;
}
/**
* Decides if the setup buttons should be shown on the preferences page.
*
* @return bool
*/
public function show_setup_buttons(): bool {
global $DB, $USER;
// If there is already a factor setup, don't allow multiple (for now).
$record = $DB->get_record('tool_mfa',
['userid' => $USER->id, 'factor' => $this->name, 'secret' => '', 'revoked' => 0]);
return empty($record);
}
/**
* Returns true if factor class has factor records that might be revoked.
* It means that user can revoke factor record from their profile.
*
* @return bool
*/
public function has_revoke(): bool {
return true;
}
/**
* Generates and sms' the code for login to the user, stores codes in DB.
*
* @return int|null the instance ID being used.
*/
private function generate_and_sms_code(): ?int {
global $DB, $USER;
$duration = get_config('factor_sms', 'duration');
$instance = $DB->get_record('tool_mfa', ['factor' => $this->name, 'userid' => $USER->id, 'revoked' => 0]);
if (empty($instance)) {
return null;
}
$secret = $this->secretmanager->create_secret($duration, false);
// There is a new code that needs to be sent.
if (!empty($secret)) {
// Grab the singleton SMS record.
$this->sms_verification_code($secret, $instance->label);
}
return $instance->id;
}
/**
* This function sends an SMS code to the user based on the phonenumber provided.
*
* @param int $secret the secret to send.
* @param string|null $phonenumber the phonenumber to send the verification code to.
* @return void
*/
private function sms_verification_code(int $secret, ?string $phonenumber): void {
global $CFG, $SITE;
// Here we should get the information, then construct the message.
$url = new moodle_url($CFG->wwwroot);
$content = [
'fullname' => $SITE->fullname,
'url' => $url->get_host(),
'code' => $secret,
];
$message = get_string('smsstring', 'factor_sms', $content);
$class = '\factor_sms\local\smsgateway\\' . get_config('factor_sms', 'gateway');
$gateway = new $class();
$gateway->send_sms_message($message, $phonenumber);
}
/**
* Verifies entered code against stored DB record.
*
* @param string $enteredcode
* @return bool
*/
private function check_verification_code(string $enteredcode): bool {
return ($this->secretmanager->validate_secret($enteredcode) === \tool_mfa\local\secret_manager::VALID) ? true : false;
}
/**
* Returns all possible states for a user.
*
* @param \stdClass $user
*/
public function possible_states(\stdClass $user): array {
return [
\tool_mfa\plugininfo\factor::STATE_PASS,
\tool_mfa\plugininfo\factor::STATE_NEUTRAL,
\tool_mfa\plugininfo\factor::STATE_FAIL,
\tool_mfa\plugininfo\factor::STATE_UNKNOWN,
];
}
/**
* Get the login description associated with this factor.
* Override for factors that have a user input.
*
* @return string The login option.
*/
public function get_login_desc(): string {
$phonenumber = $this->get_phonenumber();
if (empty($phonenumber)) {
return get_string('errorsmssent', 'factor_sms');
} else {
return get_string('logindesc', 'factor_' . $this->name, $phonenumber);
}
}
}

View file

@ -0,0 +1,67 @@
<?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/>.
namespace factor_sms;
/**
* Helper class for shared sms gateway functions
*
* @package factor_sms
* @author Alex Morris <alex.morris@catalyst.net.nz>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class helper {
/**
* This function internationalises a number to E.164 standard.
* https://46elks.com/kb/e164
*
* @param string $phonenumber the phone number to format.
* @return string the formatted phone number.
*/
public static function format_number(string $phonenumber): string {
// Remove all whitespace, dashes and brackets.
$phonenumber = preg_replace('/[ \(\)-]/', '', $phonenumber);
// Number is already in international format. Do nothing.
if (str_starts_with ($phonenumber, '+')) {
return $phonenumber;
}
// Strip leading 0 if found.
if (str_starts_with ($phonenumber, '0')) {
$phonenumber = substr($phonenumber, 1);
}
// Prepend country code.
$countrycode = get_config('factor_sms', 'countrycode');
$phonenumber = !empty($countrycode) ? '+' . $countrycode . $phonenumber : $phonenumber;
return $phonenumber;
}
/**
* Validate phone number with E.164 format. https://en.wikipedia.org/wiki/E.164
*
* @param string $phonenumber from the given user input
* @return bool
*/
public static function is_valid_phonenumber(string $phonenumber) : bool {
$phonenumber = self::format_number($phonenumber);
return (preg_match("/^\+[1-9]\d{1,14}$/", $phonenumber)) ? true : false;
}
}

View file

@ -0,0 +1,100 @@
<?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/>.
/**
* AWS helper class. Contains useful functions when interacting with the SDK.
*
* @package factor_sms
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace factor_sms\local;
use Aws\CommandInterface;
use Aws\AwsClient;
use Psr\Http\Message\RequestInterface;
/**
* This class contains functions that help plugins to interact with the AWS SDK.
*
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class aws_helper {
/**
* This creates a proxy string suitable for use with the AWS SDK.
*
* @return string the string to use for proxy settings.
*/
public static function get_proxy_string(): string {
global $CFG;
$proxy = '';
if (empty($CFG->proxytype)) {
return $proxy;
}
if ($CFG->proxytype === 'SOCKS5') {
// If it is a SOCKS proxy, append the protocol info.
$protocol = 'socks5://';
} else {
$protocol = '';
}
if (!empty($CFG->proxyhost)) {
$proxy = $CFG->proxyhost;
if (!empty($CFG->proxyport)) {
$proxy .= ':'. $CFG->proxyport;
}
if (!empty($CFG->proxyuser) && !empty($CFG->proxypassword)) {
$proxy = $protocol . $CFG->proxyuser . ':' . $CFG->proxypassword . '@' . $proxy;
}
}
return $proxy;
}
/**
* Configure the provided AWS client to route traffic via the moodle proxy for any hosts not excluded.
*
* @param AwsClient $client
* @return AwsClient
*/
public static function configure_client_proxy(AwsClient $client): AwsClient {
$client->getHandlerList()->appendBuild(self::add_proxy_when_required(), 'proxy_bypass');
return $client;
}
/**
* Generate a middleware higher order function to wrap the handler and append proxy configuration based on target.
*
* @return callable Middleware high order callable.
*/
protected static function add_proxy_when_required(): callable {
return function (callable $fn) {
return function (CommandInterface $command, ?RequestInterface $request = null) use ($fn) {
if (isset($request)) {
$target = (string) $request->getUri();
if (!is_proxybypass($target)) {
$command['@http']['proxy'] = self::get_proxy_string();
}
}
$promise = $fn($command, $request);
return $promise;
};
};
}
}

View file

@ -0,0 +1,62 @@
<?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/>.
/**
* AWS Client factory. Retrieves a client with moodle specific HTTP configuration.
*
* @package factor_sms
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright 2022 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace factor_sms\local;
use Aws\AwsClient;
/**
* AWS Client factory. Retrieves a client with moodle specific HTTP configuration.
*
* @copyright 2022 Catalyst IT
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class client_factory {
/**
* Get an AWS client with moodle specific HTTP configuration.
*
* @param string $class Fully qualified AWS classname e.g. \Aws\S3\S3Client
* @param array $opts array of constructor options for AWS Client.
* @return AwsClient
*/
public static function get_client(string $class, array $opts): AwsClient {
// Modify the opts to add HTTP timeouts.
if (empty($opts['http'])) {
$opts['http'] = ['connect_timeout' => HOURSECS];
} else if (!array_key_exists('connect_timeout', $opts['http'])) {
// Try not to override existing settings.
$opts['http']['connect_timeout'] = HOURSECS;
}
// Blindly trust the call here. If it exceptions, the raw message is the most useful.
$client = new $class($opts);
if (!$client instanceof \Aws\AwsClient) {
throw new \moodle_exception('clientnotfound', 'factor_sms');
}
// Now we can configure the proxy with the routing aware middleware.
return aws_helper::configure_client_proxy($client);
}
}

View file

@ -0,0 +1,156 @@
<?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/>.
namespace factor_sms\local\smsgateway;
use factor_sms\admin_settings_aws_region;
use factor_sms\event\sms_sent;
use factor_sms\local\aws_helper;
/**
* AWS SNS SMS Gateway class
*
* @package factor_sms
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class aws_sns implements gateway_interface {
/**
* Create an instance of this class.
*/
public function __construct() {
global $CFG;
require_once($CFG->libdir . '/aws-sdk/src/functions.php');
require_once($CFG->libdir . '/guzzlehttp/guzzle/src/functions_include.php');
require_once($CFG->libdir . '/guzzlehttp/promises/src/functions_include.php');
}
/**
* Sends a message using the AWS SNS API
*
* @param string $messagecontent the content to send in the SMS message.
* @param string $phonenumber the destination for the message.
* @return bool true on message send success
*/
public function send_sms_message(string $messagecontent, string $phonenumber): bool {
global $SITE, $USER;
$config = get_config('factor_sms');
// Setup client params and instantiate client.
$params = [
'version' => 'latest',
'region' => $config->api_region,
'http' => ['proxy' => aws_helper::get_proxy_string()],
];
if (!$config->usecredchain) {
$params['credentials'] = [
'key' => $config->api_key,
'secret' => $config->api_secret,
];
}
$client = new \Aws\Sns\SnsClient($params);
// Transform the phone number to international standard.
$phonenumber = \factor_sms\helper::format_number($phonenumber);
// Setup the sender information.
$senderid = $SITE->shortname;
// Remove spaces and non-alphanumeric characters from ID.
$senderid = preg_replace("/[^A-Za-z0-9]/", '', trim($senderid));
// We have to truncate the senderID to 11 chars.
$senderid = substr($senderid, 0, 11);
if (defined('BEHAT_SITE_RUNNING')) {
// Fake SMS sending in behat.
return true;
}
try {
// These messages need to be transactional.
$client->SetSMSAttributes([
'attributes' => [
'DefaultSMSType' => 'Transactional',
'DefaultSenderID' => $senderid,
],
]);
// Actually send the message.
$result = $client->publish([
'Message' => $messagecontent,
'PhoneNumber' => $phonenumber,
]);
$data = [
'relateduserid' => null,
'context' => \context_user::instance($USER->id),
'other' => [
'userid' => $USER->id,
'debug' => [
'messageid' => $result->get('MessageId'),
],
],
];
$event = sms_sent::create($data);
$event->trigger();
return true;
} catch (\Aws\Exception\AwsException $e) {
throw new \moodle_exception('errorawsconection', 'factor_sms', '', $e->getAwsErrorMessage());
}
}
/**
* Add gateway specific settings to the SMS factor settings page.
*
* @param \admin_settingpage $settings
* @return void
*/
public static function add_settings(\admin_settingpage $settings): void {
global $CFG;
require_once($CFG->dirroot . '/admin/tool/mfa/factor/sms/classes/admin_settings_aws_region.php');
$settings->add(new \admin_setting_configcheckbox('factor_sms/usecredchain',
get_string('settings:aws:usecredchain', 'factor_sms'), '', 0));
if (!get_config('factor_sms', 'usecredchain')) {
// AWS Settings.
$settings->add(new \admin_setting_configtext('factor_sms/api_key',
get_string('settings:aws:key', 'factor_sms'),
get_string('settings:aws:key_help', 'factor_sms'), ''));
$settings->add(new \admin_setting_configpasswordunmask('factor_sms/api_secret',
get_string('settings:aws:secret', 'factor_sms'),
get_string('settings:aws:secret_help', 'factor_sms'), ''));
}
$settings->add(new admin_settings_aws_region('factor_sms/api_region',
get_string('settings:aws:region', 'factor_sms'),
get_string('settings:aws:region_help', 'factor_sms'),
'ap-southeast-2'));
}
/**
* Returns whether or not the gateway is enabled
*
* @return bool
*/
public static function is_gateway_enabled(): bool {
return true;
}
}

View file

@ -0,0 +1,53 @@
<?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/>.
/**
* SMS Gateway interface
*
* @package factor_sms
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace factor_sms\local\smsgateway;
interface gateway_interface {
/**
* Sends an SMS message
*
* @param string $messagecontent the content to send in the SMS message.
* @param string $phonenumber the destination for the message.
* @return bool true on message send success
*/
public function send_sms_message(string $messagecontent, string $phonenumber): bool;
/**
* Add gateway specific settings to the SMS factor settings page.
*
* @param \admin_settingpage $settings
* @return void
*/
public static function add_settings(\admin_settingpage $settings): void;
/**
* Returns whether or not the gateway is enabled
*
* @return bool
*/
public static function is_gateway_enabled(): bool;
}

View file

@ -0,0 +1,40 @@
<?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/>.
namespace factor_sms\privacy;
use core_privacy\local\metadata\null_provider;
/**
* Privacy provider.
*
* @package factor_sms
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}

View file

@ -0,0 +1,44 @@
<?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/>.
/**
* Edit phonenumber redirect
*
* @package factor_sms
* @copyright 2023 Raquel Ortega <raquel.ortega@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '../../../../../../config.php');
require_login(null, false);
if (isguestuser()) {
throw new require_login_exception('error:isguestuser', 'tool_mfa');
}
$sesskey = optional_param('sesskey', false, PARAM_TEXT);
require_sesskey();
// Remove session phone number.
unset($SESSION->tool_mfa_sms_number);
// Clean temp secrets code.
$secretmanager = new \tool_mfa\local\secret_manager('sms');
$secretmanager->cleanup_temp_secrets();
redirect(new \moodle_url('/admin/tool/mfa/action.php', [
'action' => 'setup',
'factor' => 'sms',
]));

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/>.
/**
* Language strings.
*
* @package factor_sms
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['action:revoke'] = 'Revoke mobile phone number';
$string['addnumber'] = 'Mobile number';
$string['clientnotfound'] = 'AWS Service client not found. Client must be fully qualified classname e.g. \Aws\S3\S3Client';
$string['editphonenumber'] = 'Edit phone number';
$string['editphonenumberinfo'] = "If you didn't get the code or entered the wrong number, please edit number and try again.";
$string['errorawsconection'] = 'Error connecting to AWS server: {$a}';
$string['errorsmssent'] = 'Error sending a SMS message containing your verification code.';
$string['error:emptyverification'] = 'Empty code. Try again.';
$string['error:wrongphonenumber'] = 'The phone number you provided is not in a valid format.';
$string['error:wrongverification'] = 'Wrong code. Try again.';
$string['event:smssent'] = 'SMS Message sent';
$string['event:smssentdescription'] = 'The user with id {$a->userid} had a verification code sent to them via SMS <br> Information: {$a->debuginfo}';
$string['info'] = '<p>Setup Mobile phone to receive authentication code.</p>';
$string['logindesc'] = 'We\'ve just sent an SMS containing a 6-digit code to your mobile number: {$a}';
$string['loginoption'] = 'Have a code sent to you mobile phone';
$string['loginskip'] = "I didn't receive a code";
$string['loginsubmit'] = 'Continue';
$string['logintitle'] = 'Enter the verification code sent to your mobile';
$string['phonehelp'] = 'Enter your mobile number (including country code) to receive a verification code.';
$string['pluginname'] = 'SMS Mobile phone';
$string['privacy:metadata'] = 'The mobile phone SMS factor plugin does not store any personal data';
$string['settings:aws'] = 'AWS SNS';
$string['settings:aws:key'] = 'Key';
$string['settings:aws:key_help'] = 'Amazon API key credential.';
$string['settings:aws:region'] = 'Region';
$string['settings:aws:region_help'] = 'Amazon API gateway region.';
$string['settings:aws:secret'] = 'Secret';
$string['settings:aws:secret_help'] = 'Amazon API secret credential.';
$string['settings:aws:usecredchain'] = 'Use the default credential provider chain to find AWS credentials';
$string['settings:countrycode'] = 'Country number code';
$string['settings:countrycode_help'] = 'The calling code without the leading + as a default if users do not enter an international number with a + prefix.
See this link for a list of calling codes: {$a}';
$string['settings:duration'] = 'Validity duration';
$string['settings:duration_help'] = 'The period of time that the code is valid.';
$string['settings:gateway'] = 'SMS Gateway';
$string['settings:gateway_help'] = 'The SMS provider you wish to send messages via';
$string['setupfactor'] = 'SMS Setup';
$string['setupfactorbutton'] = 'Setup SMS';
$string['setupsubmitcode'] = 'Save';
$string['setupsubmitphone'] = 'Send code';
$string['smsstring'] = '{$a->code} is your {$a->fullname} one-time security code.
@{$a->url} #{$a->code}';
$string['summarycondition'] = 'Using an SMS one-time security code';

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/>.
/**
* Settings
*
* @package factor_sms
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG, $OUTPUT;
$enabled = new admin_setting_configcheckbox('factor_sms/enabled',
new lang_string('settings:enablefactor', 'tool_mfa'),
new lang_string('settings:enablefactor_help', 'tool_mfa'), 0);
$enabled->set_updatedcallback(function () {
\tool_mfa\manager::do_factor_action('sms', get_config('factor_sms', 'enabled') ? 'enable' : 'disable');
});
$settings->add($enabled);
$settings->add(new admin_setting_configtext('factor_sms/weight',
new lang_string('settings:weight', 'tool_mfa'),
new lang_string('settings:weight_help', 'tool_mfa'), 100, PARAM_INT));
$settings->add(new admin_setting_configduration('factor_sms/duration',
get_string('settings:duration', 'tool_mfa'),
get_string('settings:duration_help', 'tool_mfa'), 30 * MINSECS, MINSECS));
$codeslink = 'https://en.wikipedia.org/wiki/List_of_country_calling_codes';
$link = \html_writer::link($codeslink, $codeslink);
$settings->add(new admin_setting_configtext('factor_sms/countrycode',
get_string('settings:countrycode', 'factor_sms'),
get_string('settings:countrycode_help', 'factor_sms', $link), '', PARAM_INT));
$gateways = [
'aws_sns' => get_string('settings:aws', 'factor_sms'),
];
$settings->add(new admin_setting_configselect('factor_sms/gateway',
get_string('settings:gateway', 'factor_sms'),
get_string('settings:gateway_help', 'factor_sms'),
'aws_sns', $gateways));
if (empty(get_config('factor_sms', 'gateway'))) {
return;
}
$class = '\factor_sms\local\smsgateway\\' . get_config('factor_sms', 'gateway');
call_user_func($class . '::add_settings', $settings);

View file

@ -0,0 +1,50 @@
{{!
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 factor_sms/setting_aws_region
Admin aws region setting template.
Context variables required for this template:
* list - form list name
* name - form element name
* id - element id
* value - element value
* size - element size
* options - list of data list options: label, value.
Example context (json):
{
"list": "test",
"name": "test",
"id": "test0",
"value": "A tall, dark stranger will have more fun than you.",
"size": "21",
"options": [ { "label": "eu-north-1 - Europe (Stockholm)", "value": "eu-north-1" } ]
}
}}
{{!
Setting config aws region
}}
<div class="form-text defaultsnext">
<input type="text" list="{{list}}" name="{{name}}" value="{{value}}" size="{{size}}" id="{{id}}" class="form-control text-ltr" {{#readonly}}disabled{{/readonly}}>
<datalist id="{{list}}">
{{#options}}
<option value="{{value}}" label="{{label}}"></option>
{{/options}}
</datalist>
</div>

View file

@ -0,0 +1,64 @@
<?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/>.
/**
* factor_sms unit tests.
*
* @package factor_sms
* @author Mikhail Golenkov <mikhailgolenkov@catalyst-au.net>
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace factor_sms;
/**
* Testcase for the list of AWS regions admin setting.
*
* @package factor_sms
* @author Mikhail Golenkov <mikhailgolenkov@catalyst-au.net>
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \admin_settings_aws_region_test
*/
class admin_settings_aws_region_test extends \advanced_testcase {
/**
* Cleanup after all tests are executed.
*
* @return void
*/
public function tearDown(): void {
$admin = admin_get_root();
$admin->purge_children(true);
}
/**
* Test that output_html() method works and returns HTML string with expected content.
*/
public function test_output_html(): void {
$this->resetAfterTest();
$setting = new admin_settings_aws_region('test_aws_region',
'Test visible name', 'Test description', 'Test default setting');
$html = $setting->output_html('');
$this->assertTrue(str_contains($html, 'Test visible name'));
$this->assertTrue(str_contains($html, 'Test description'));
$this->assertTrue(str_contains($html, 'Default: Test default setting'));
$this->assertTrue(str_contains($html,
'<input type="text" list="s__test_aws_region" name="s__test_aws_region" value=""'));
$this->assertTrue(str_contains($html, '<datalist id="s__test_aws_region">'));
$this->assertTrue(str_contains($html, '<option value="'));
}
}

View file

@ -0,0 +1,61 @@
<?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/>.
/**
* factor_sms unit tests.
*
* @package factor_sms
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \factor_sms\local\aws_helper
*/
namespace factor_sms;
use factor_sms\local\aws_helper;
/**
* Testcase for the AWS helper.
*
* @package factor_sms
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \factor_sms\classes\local\aws_helper
*/
class aws_helper_test extends \advanced_testcase {
public function test_get_proxy_string():void {
global $CFG;
$this->resetAfterTest();
// Confirm with no config an empty string is returned.
$CFG->proxyhost = '';
$this->assertEquals('', aws_helper::get_proxy_string());
// Now set some configs.
$CFG->proxyhost = '127.0.0.1';
$CFG->proxyuser = 'user';
$CFG->proxypassword = 'password';
$CFG->proxyport = '1337';
$this->assertEquals('user:password@127.0.0.1:1337', aws_helper::get_proxy_string());
// Now change to SOCKS proxy.
$CFG->proxytype = 'SOCKS5';
$this->assertEquals('socks5://user:password@127.0.0.1:1337', aws_helper::get_proxy_string());
}
}

View file

@ -0,0 +1,55 @@
<?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/>.
/**
* Behat custom steps and configuration for factor_sms.
*
* @package factor_sms
* @category test
* @copyright 2023 <raquel.ortega@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../../../../../../lib/behat/behat_base.php');
require_once(__DIR__ . '/../../../../../../../lib/behat/behat_field_manager.php');
/**
* Behat custom steps and configuration for factor_sms.
*
* @package factor_sms
* @category test
* @copyright 2023 <raquel.ortega@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_factor_sms extends behat_base {
/**
* Sets the given field with a valid code created in tool_mfa_secrets table
*
* @Given /^I set the field "(?P<field_string>(?:[^"]|\\")*)" with valid code$/
*
* @param string $field
*/
public function i_set_the_field_with_valid_code(string $field): void {
global $DB, $USER;
$record = $DB->get_record('tool_mfa_secrets',
['userid' => $USER->id, 'revoked' => '0']
);
$field = behat_field_manager::get_form_field_from_label($field, $this);
$field->set_value($record->secret);
}
}

View file

@ -0,0 +1,56 @@
@tool_admin_mfa
Feature: Login user with sms authentication factor
In order to login using SMS factor authentication
As an user
I need to be able to login
Background:
Given I log in as "admin"
And the following config values are set as admin:
| enabled | 1 | tool_mfa |
| lockout | 3 | tool_mfa |
And the following config values are set as admin:
| enabled | 1 | factor_sms |
# Set up user SMS factor in user preferences.
When I follow "Preferences" in the user menu
And I click on "Multi-factor authentication preferences" "link"
And I click on "Setup SMS" "button"
And I set the field "Mobile number" to "+34649709233"
And I press "Send code"
And I set the field "Enter code" with valid code
Then I press "Save"
Scenario: Revoke factor
Given I click on "Revoke" "link"
And I should see "Are you sure you want to revoke factor?"
And I press "Revoke"
And I should see "successfully revoked"
When I log out
And I log in as "admin"
Then I should see "Unable to authenticate"
Scenario: Login user successfully with sms verification
Given I log out
And I log in as "admin"
And I should see "2-step verification"
And I should see "Enter code"
When I set the field "Enter code" with valid code
And I click on "Continue" "button"
Then I am logged in as "admin"
Scenario: Wrong code number end of possible attempts
Given I log out
And I log in as "admin"
And I should see "2-step verification"
And I should see "Enter code"
When I set the field "Enter code" to "555556"
And I click on "Continue" "button"
And I should see "Wrong code."
And I should see "You have 2 attempts left."
And I set the field "Enter code" to "555553"
And I click on "Continue" "button"
And I should see "Wrong code."
And I should see "1 attempts left."
And I set the field "Enter code" to "555553"
And I click on "Continue" "button"
Then I should see "Unable to authenticate"

View file

@ -0,0 +1,49 @@
@tool_admin_mfa
Feature: Setup SMS factor in user preferences
In order check the setup SMS factor verification
As an admin
I want to setup and enable the SMS factor for the current user
Background:
Given I log in as "admin"
And the following config values are set as admin:
| enabled | 1 | tool_mfa |
And the following config values are set as admin:
| enabled | 1 | factor_sms |
When I follow "Preferences" in the user menu
And I click on "Multi-factor authentication preferences" "link"
And I click on "Setup SMS" "button"
Scenario: Phone number setup form validation
Given I set the field "Mobile number" to "++5555sss"
And I press "Send code"
And I should see "The phone number you provided is not in a valid format."
And I set the field "Mobile number" to "0123456789"
And I press "Send code"
And I should see "The phone number you provided is not in a valid format."
And I set the field "Mobile number" to "786-307-3615"
And I press "Send code"
And I should see "The phone number you provided is not in a valid format."
When I set the field "Mobile number" to "649709233"
And I press "Send code"
Then I should see "The phone number you provided is not in a valid format."
Scenario: Edit phone number
Given I set the field "Mobile number" to "+34649709233"
And I press "Send code"
And I click on "Edit phone number" "link"
And I should see "Mobile number"
When I set the field "Mobile number" to "+34649709232"
And I press "Send code"
Then I should see "Enter code"
Scenario: Code setup form validation
Given I set the field "Mobile number" to "+34649709233"
And I press "Send code"
And I should see "Enter code"
When I set the field "Enter code" to "555556"
And I click on "Save" "button"
And I should see "Wrong code. Try again"
And I set the field "Enter code" to "ddddd5"
And I click on "Save" "button"
Then I should see "Wrong code. Try again"

View file

@ -0,0 +1,166 @@
<?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/>.
namespace factor_sms;
/**
* Tests for sms factor.
*
* @covers \factor_sms\factor
* @package factor_sms
* @copyright 2023 Raquel Ortega <raquel.ortega@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor_test extends \advanced_testcase {
/**
* Data provider for test_format_number().
*
* @return array of different country codes and phone numbers.
*/
public function format_number_provider(): array {
return [
'Phone number with local format' => [
'phonenumber' => '0123456789',
'expected' => '+34123456789',
'countrycode' => '34',
],
'Phone number without international format' => [
'phonenumber' => '123456789',
'expected' => '+34123456789',
'countrycode' => '34',
],
'Phone number with international format' => [
'phonenumber' => '+39123456789',
'expected' => '+39123456789',
],
'Phone number with spaces using international format' => [
'phonenumber' => '+34 123 456 789',
'expected' => '+34123456789',
],
'Phone number with spaces using local format with country code' => [
'phonenumber' => '0 123 456 789',
'expected' => '+34123456789',
'countrycode' => '34',
],
'Phone number with spaces using local format without country code' => [
'phonenumber' => '0 123 456 789',
'expected' => '123456789',
],
];
}
/**
* Test format number with different phones and different country codes
* @covers \factor_sms\helper::format_number
* @dataProvider format_number_provider
*
* @param string $phonenumber Phone number.
* @param string $expected Expected value.
* @param string|null $countrycode Country code.
*/
public function test_format_number(string $phonenumber, string $expected, ?string $countrycode = null): void {
$this->resetAfterTest(true);
set_config('countrycode', $countrycode, 'factor_sms');
$this->assertEquals($expected, \factor_sms\helper::format_number($phonenumber));
}
/**
* Data provider for test_is_valid__phonenumber().
*
* @return array with different phone numebr tests
*/
public function is_valid_phonenumber_provider(): array {
return [
['+919367788755', true],
['8989829304', false],
['+16308520397', true],
['786-307-3615', false],
['+14155552671', true],
['+551155256325', true],
['649709233', false],
['+34649709233', true],
['+aaasss', false],
];
}
/**
* Test is valid phone number in E.164 format (https://en.wikipedia.org/wiki/E.164)
* @covers \factor_sms\helper::is_valid_phonenumber
* @dataProvider is_valid_phonenumber_provider
*
* @param string $phonenumber
* @param bool $valid True if the given phone number is valid, false if is invalid
*/
public function test_is_valid_phonenumber(string $phonenumber, bool $valid): void {
$this->resetAfterTest(true);
if ($valid) {
$this->assertTrue(\factor_sms\helper::is_valid_phonenumber($phonenumber));
} else {
$this->assertFalse(\factor_sms\helper::is_valid_phonenumber($phonenumber));
}
}
/**
* Test set up user factor and verification code with a random phone number
* @covers ::setup_user_factor
* @covers ::check_verification_code
* @covers ::revoke_user_factor
*/
public function test_check_verification_code(): void {
global $SESSION;
$this->resetAfterTest(true);
// Create and login a user and set up the phone number.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
// Generate a fake phone number and save it in session.
$phonenumber = '+34' . (string)random_int(100000000, 999999999);
$SESSION->tool_mfa_sms_number = $phonenumber;
$smsfactor = \tool_mfa\plugininfo\factor::get_factor('sms');
$rc = new \ReflectionClass($smsfactor::class);
$smsdata = [];
$factorinstance = $smsfactor->setup_user_factor((object) $smsdata);
// Check if user factor was created successful.
$this->assertNotEmpty($factorinstance);
$this->assertEquals(1, count($smsfactor->get_active_user_factors($user)));
// Create the secret code.
$secretmanager = new \tool_mfa\local\secret_manager('sms');
$secretcode = $secretmanager->create_secret(1800, true);
// Check verification code.
$rcm = $rc->getMethod('check_verification_code');
$rcm->setAccessible(true);
$this->assertTrue($rcm->invoke($smsfactor, $secretcode));
// Test that calling the revoke on the generic type revokes all.
$smsfactor->revoke_user_factor($factorinstance->id);
$this->assertEquals(0, count($smsfactor->get_active_user_factors($user)));
unset($SESSION->tool_mfa_sms_number);
}
}

View file

@ -0,0 +1,32 @@
<?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/>.
/**
* Plugin version and other meta-data are defined here.
*
* @package factor_sms
* @subpackage tool_mfa
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2023080300; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2023042400.00; // Requires this Moodle version.
$plugin->component = 'factor_sms'; // Full name of the plugin (used for diagnostics).
$plugin->maturity = MATURITY_STABLE;

View file

@ -341,7 +341,7 @@ class factor extends object_factor_base {
* TOTP Factor implementation.
*
* @param stdClass $data
* @return stdClass the factor record, or null.
* @return stdClass|null the factor record, or null.
*/
public function setup_user_factor(stdClass $data): stdClass|null {
global $DB, $USER;

View file

@ -242,7 +242,7 @@ class factor extends object_factor_base {
* WebAuthn Factor implementation.
*
* @param \MoodleQuickForm $mform
* @return object $mform
* @return \MoodleQuickForm $mform
*/
public function setup_factor_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm {
global $PAGE, $USER, $SESSION;

View file

@ -41,6 +41,7 @@ $string['error:actionnotfound'] = 'Action \'{$a}\' not supported';
$string['error:directaccess'] = 'This page shouldn\'t be accessed directly';
$string['error:factornotenabled'] = 'Multi-factor authentication factor \'{$a}\' not enabled';
$string['error:factornotfound'] = 'Multi-factor authentication factor \'{$a}\' not found';
$string['error:isguestuser'] = 'Guests are not allowed here.';
$string['error:notenoughfactors'] = 'Unable to authenticate';
$string['error:reauth'] = 'We couldn\'t confirm your identity sufficiently to meet the site authentication security policy.<br>This may be due to: <br> 1) Steps being locked - please wait a few minutes and try again.
<br> 2) Steps being failed - please double check the details for each step. <br> 3) Steps were skipped - please reload this page or try logging in again.';

View file

@ -262,4 +262,29 @@ class secret_manager_test extends \advanced_testcase {
$reflectedsessionid->setValue($secman, 'diffsession');
$this->assertFalse($reflectedmethod->invoke($secman, true));
}
/**
* Tests with cleanup temporal secrets
*
* @covers ::cleanup_temp_secrets
*/
public function test_cleanup_temp_secrets(): void {
global $DB;
$this->resetAfterTest(true);
$secman = new \tool_mfa\local\secret_manager('mock');
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
// Create secrets.
$secman->create_secret(1800, true);
$secman->create_secret(1800, true);
// Cleanup current user secrets.
$secman->cleanup_temp_secrets();
// Check there are no secrets of the current user.
$records = $DB->get_records('tool_mfa_secrets', ['userid' => $user->id]);
$this->assertEmpty($records);
}
}

View file

@ -26,7 +26,7 @@ require_once(__DIR__ . '/../../../config.php');
require_login(null, false);
if (isguestuser()) {
throw new require_login_exception('Guests are not allowed here.');
throw new require_login_exception('error:isguestuser', 'tool_mfa');
}
$action = optional_param('action', '', PARAM_TEXT);