first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
+181
View File
@@ -0,0 +1,181 @@
<?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/>.
/**
* Configure user factor page
*
* @package tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../../config.php');
use tool_mfa\local\form\setup_factor_form;
require_login(null, false);
if (isguestuser()) {
throw new require_login_exception('error:isguestuser', 'tool_mfa');
}
$action = optional_param('action', '', PARAM_ALPHANUMEXT);
$factor = optional_param('factor', '', PARAM_ALPHANUMEXT);
$factorid = optional_param('factorid', '', PARAM_INT);
$params = ['action' => $action, 'factor' => $factor, 'factorid' => $factorid];
$currenturl = new moodle_url('/admin/tool/mfa/action.php', $params);
$returnurl = new moodle_url('/admin/tool/mfa/user_preferences.php');
if (empty($factor) || empty($action)) {
throw new moodle_exception('error:directaccess', 'tool_mfa', $returnurl);
}
if (!\tool_mfa\plugininfo\factor::factor_exists($factor)) {
throw new moodle_exception('error:factornotfound', 'tool_mfa', $returnurl, $factor);
}
if (!in_array($action, \tool_mfa\plugininfo\factor::get_factor_actions())) {
throw new moodle_exception('error:actionnotfound', 'tool_mfa', $returnurl, $action);
}
if (!empty($factorid) && !\tool_mfa\manager::is_factorid_valid($factorid, $USER)) {
throw new moodle_exception('error:incorrectfactorid', 'tool_mfa', $returnurl, $factorid);
}
$factorobject = \tool_mfa\plugininfo\factor::get_factor($factor);
$context = context_user::instance($USER->id);
$PAGE->set_context($context);
$PAGE->set_url('/admin/tool/mfa/action.php');
$PAGE->set_pagelayout('standard');
$PAGE->set_cacheable(false);
if ($node = $PAGE->settingsnav->find('usercurrentsettings', null)) {
$PAGE->navbar->add($node->get_content(), $node->action());
}
$PAGE->navbar->add(get_string('preferences:header', 'tool_mfa'), new \moodle_url('/admin/tool/mfa/user_preferences.php'));
switch ($action) {
case 'setup':
if (!$factorobject || !$factorobject->has_setup()) {
redirect($returnurl);
}
$PAGE->set_title(get_string('setupfactor', 'tool_mfa'));
$PAGE->navbar->add($factorobject->get_setup_string());
$OUTPUT = $PAGE->get_renderer('tool_mfa');
$form = new setup_factor_form($currenturl, ['factorname' => $factor]);
if ($form->is_submitted()) {
$form->is_validated();
if ($form->is_cancelled()) {
$factorobject->setup_factor_form_is_cancelled($factorid);
redirect($returnurl);
}
if ($data = $form->get_data()) {
$record = $factorobject->setup_user_factor($data);
if (!empty($record)) {
$factorobject->set_state(\tool_mfa\plugininfo\factor::STATE_PASS);
$finalurl = new moodle_url($returnurl, ['action' => 'setup', 'factorid' => $record->id]);
redirect($finalurl);
}
throw new moodle_exception('error:setupfactor', 'tool_mfa', $returnurl);
}
}
echo $OUTPUT->header();
$form->display();
break;
case 'replace':
// Replace works much the same as setup.
if (!$factorobject || !$factorobject->has_replace()) {
redirect($returnurl);
}
$PAGE->set_title(get_string('replacefactor', 'tool_mfa'));
$PAGE->navbar->add($factorobject->get_setup_string());
$OUTPUT = $PAGE->get_renderer('tool_mfa');
// Use setup factor form, but pass in additional id for replacement.
$form = new setup_factor_form($currenturl, ['factorname' => $factor, 'replaceid' => $factorid]);
if ($form->is_submitted()) {
$form->is_validated();
if ($form->is_cancelled()) {
$factorobject->setup_factor_form_is_cancelled($factorid);
redirect($returnurl);
}
if ($data = $form->get_data()) {
$record = $factorobject->replace_user_factor($data, $factorid);
if (!empty($record)) {
$factorobject->set_state(\tool_mfa\plugininfo\factor::STATE_PASS);
$finalurl = new moodle_url($returnurl, ['action' => 'setup', 'factorid' => $record->id]);
redirect($finalurl);
}
throw new moodle_exception('error:setupfactor', 'tool_mfa', $returnurl);
}
}
echo $OUTPUT->header();
$form->display();
break;
case 'revoke':
// Ensure sesskey is valid.
require_sesskey();
$PAGE->set_title(get_string('revokefactor', 'tool_mfa'));
if (!$factorobject || !$factorobject->has_revoke()) {
throw new moodle_exception('error:revoke', 'tool_mfa', $returnurl);
}
if ($factorobject->revoke_user_factor($factorid)) {
$finalurl = new moodle_url($returnurl, ['action' => 'revoked', 'factorid' => $factorid]);
redirect($finalurl);
}
throw new moodle_exception('error:revoke', 'tool_mfa', $returnurl);
break;
case 'manage':
$PAGE->set_title(get_string('managefactor', 'tool_mfa'));
$PAGE->navbar->add(get_string('action:manage', 'factor_'.$factor));
$OUTPUT = $PAGE->get_renderer('tool_mfa');
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('managefactor', 'factor_' . $factorobject->name));
echo $OUTPUT->active_factors($factor);
echo $OUTPUT->single_button($returnurl, get_string('back'));
// JS for modal confirming replace and revoke actions.
$PAGE->requires->js_call_amd('tool_mfa/confirmation_modal', 'init', [$context->id]);
break;
default:
break;
}
echo $OUTPUT->footer();
@@ -0,0 +1,3 @@
define("tool_mfa/autosubmit_verification_code",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;_exports.init=()=>{const codeInput=document.querySelector("#id_verificationcode"),codeForm=codeInput.closest("form"),submitButton=codeForm.querySelector("#id_submitbutton");codeInput.addEventListener("keyup",(function(){this.value.length>=6&&codeForm.submit()})),codeInput.disabled&&(submitButton.disabled=!0)}}));
//# sourceMappingURL=autosubmit_verification_code.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"autosubmit_verification_code.min.js","sources":["../src/autosubmit_verification_code.js"],"sourcesContent":["\n// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Module to autosubmit the verification code element when it reaches 6 characters.\n *\n * @module tool_mfa/autosubmit_verification_code\n * @copyright 2020 Peter Burnett <peterburnett@catalyst-au.net>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport const init = () => {\n const codeInput = document.querySelector(\"#id_verificationcode\");\n const codeForm = codeInput.closest(\"form\");\n const submitButton = codeForm.querySelector(\"#id_submitbutton\");\n\n // Event listener for code input field.\n codeInput.addEventListener('keyup', function() {\n if (this.value.length >= 6) {\n // Submits the closes form (parent).\n codeForm.submit();\n }\n });\n\n // Disable the submit button if the input field is disabled.\n // This occurs if there are no more attempts left for the factor.\n if (codeInput.disabled) {\n submitButton.disabled = true;\n }\n};\n"],"names":["codeInput","document","querySelector","codeForm","closest","submitButton","addEventListener","this","value","length","submit","disabled"],"mappings":"0KAwBoB,WACVA,UAAYC,SAASC,cAAc,wBACnCC,SAAWH,UAAUI,QAAQ,QAC7BC,aAAeF,SAASD,cAAc,oBAG5CF,UAAUM,iBAAiB,SAAS,WAC5BC,KAAKC,MAAMC,QAAU,GAErBN,SAASO,YAMbV,UAAUW,WACVN,aAAaM,UAAW"}
+10
View File
@@ -0,0 +1,10 @@
define("tool_mfa/confirmation_modal",["exports","core/modal_events","core/modal_save_cancel","core/notification","core/str","core/url","core/fragment","core/prefetch"],(function(_exports,_modal_events,_modal_save_cancel,_notification,_str,_url,_fragment,Prefetch){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Modal for confirming factor actions.
*
* @module tool_mfa/confirmation_modal
* @copyright 2023 David Woloszyn <david.woloszyn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_modal_events=_interopRequireDefault(_modal_events),_modal_save_cancel=_interopRequireDefault(_modal_save_cancel),_notification=_interopRequireDefault(_notification),_url=_interopRequireDefault(_url),_fragment=_interopRequireDefault(_fragment),Prefetch=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Prefetch);const SELECTORS_ACTION=".mfa-action-button";_exports.init=contextId=>{Prefetch.prefetchStrings("tool_mfa",["yesremove","yesreplace"]),registerEventListeners(contextId)};const registerEventListeners=contextId=>{document.addEventListener("click",(e=>{const action=e.target.closest(SELECTORS_ACTION);action&&buildModal(action,contextId).catch(_notification.default.exception)}))},buildModal=async(element,contextId)=>{const data={action:element.getAttribute("data-action"),factor:element.getAttribute("data-factor"),factorid:element.getAttribute("data-factorid"),devicename:element.getAttribute("data-devicename"),actionurl:_url.default.relativeUrl("/admin/tool/mfa/action.php")};"revoke"===data.action?(data.title=await(0,_str.getString)("revokefactorconfirmation","factor_"+data.factor,data.devicename),data.buttontext=await(0,_str.getString)("yesremove","tool_mfa")):"replace"===data.action&&(data.title=await(0,_str.getString)("replacefactorconfirmation","factor_"+data.factor,data.devicename),data.buttontext=await(0,_str.getString)("yesreplace","tool_mfa"));const modal=await _modal_save_cancel.default.create({title:data.title,body:_fragment.default.loadFragment("tool_mfa","factor_action_confirmation_form",contextId,data),show:!0,buttons:{save:data.buttontext,cancel:(0,_str.getString)("cancel","moodle")}});modal.getRoot().on(_modal_events.default.save,(()=>{modal.getRoot().find("form").submit()}))}}));
//# sourceMappingURL=confirmation_modal.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"confirmation_modal.min.js","sources":["../src/confirmation_modal.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Modal for confirming factor actions.\n *\n * @module tool_mfa/confirmation_modal\n * @copyright 2023 David Woloszyn <david.woloszyn@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ModalEvents from 'core/modal_events';\nimport ModalSaveCancel from 'core/modal_save_cancel';\nimport Notification from 'core/notification';\nimport {getString} from 'core/str';\nimport Url from 'core/url';\nimport Fragment from 'core/fragment';\nimport * as Prefetch from 'core/prefetch';\n\nconst SELECTORS = {\n ACTION: '.mfa-action-button',\n};\n\n/**\n * Entrypoint of the js.\n *\n * @method init\n * @param {Integer} contextId Context ID of the user.\n */\nexport const init = (contextId) => {\n // Prefetch the language strings.\n Prefetch.prefetchStrings('tool_mfa', [\n 'yesremove',\n 'yesreplace',\n ]);\n registerEventListeners(contextId);\n};\n\n/**\n * Register event listeners.\n *\n * @method registerEventListeners\n * @param {Integer} contextId Context ID of the user.\n */\nconst registerEventListeners = (contextId) => {\n document.addEventListener('click', (e) => {\n const action = e.target.closest(SELECTORS.ACTION);\n if (action) {\n buildModal(action, contextId).catch(Notification.exception);\n }\n });\n};\n\n/**\n * Build the modal with the provided data.\n *\n * @method buildModal\n * @param {HTMLElement} element The button element.\n * @param {Number} contextId Context ID of the user.\n */\nconst buildModal = async(element, contextId) => {\n\n // Prepare data for modal.\n const data = {\n action: element.getAttribute('data-action'),\n factor: element.getAttribute('data-factor'),\n factorid: element.getAttribute('data-factorid'),\n devicename: element.getAttribute('data-devicename'),\n actionurl: Url.relativeUrl('/admin/tool/mfa/action.php'),\n };\n\n // Customise modal depending on action being performed.\n if (data.action === 'revoke') {\n data.title = await getString('revokefactorconfirmation', 'factor_' + data.factor, data.devicename);\n data.buttontext = await getString('yesremove', 'tool_mfa');\n\n } else if (data.action === 'replace') {\n data.title = await getString('replacefactorconfirmation', 'factor_' + data.factor, data.devicename);\n data.buttontext = await getString('yesreplace', 'tool_mfa');\n }\n\n const modal = await ModalSaveCancel.create({\n title: data.title,\n body: Fragment.loadFragment('tool_mfa', 'factor_action_confirmation_form', contextId, data),\n show: true,\n buttons: {\n 'save': data.buttontext,\n 'cancel': getString('cancel', 'moodle'),\n },\n });\n\n modal.getRoot().on(ModalEvents.save, () => {\n modal.getRoot().find('form').submit();\n });\n\n};\n"],"names":["SELECTORS","contextId","Prefetch","prefetchStrings","registerEventListeners","document","addEventListener","e","action","target","closest","buildModal","catch","Notification","exception","async","element","data","getAttribute","factor","factorid","devicename","actionurl","Url","relativeUrl","title","buttontext","modal","ModalSaveCancel","create","body","Fragment","loadFragment","show","buttons","getRoot","on","ModalEvents","save","find","submit"],"mappings":";;;;;;;4+BA+BMA,iBACM,mCASSC,YAEjBC,SAASC,gBAAgB,WAAY,CACjC,YACA,eAEJC,uBAAuBH,kBASrBG,uBAA0BH,YAC5BI,SAASC,iBAAiB,SAAUC,UAC1BC,OAASD,EAAEE,OAAOC,QAAQV,kBAC5BQ,QACAG,WAAWH,OAAQP,WAAWW,MAAMC,sBAAaC,eAYvDH,WAAaI,MAAMC,QAASf,mBAGxBgB,KAAO,CACTT,OAAQQ,QAAQE,aAAa,eAC7BC,OAAQH,QAAQE,aAAa,eAC7BE,SAAUJ,QAAQE,aAAa,iBAC/BG,WAAYL,QAAQE,aAAa,mBACjCI,UAAWC,aAAIC,YAAY,+BAIX,WAAhBP,KAAKT,QACLS,KAAKQ,YAAc,kBAAU,2BAA4B,UAAYR,KAAKE,OAAQF,KAAKI,YACvFJ,KAAKS,iBAAmB,kBAAU,YAAa,aAExB,YAAhBT,KAAKT,SACZS,KAAKQ,YAAc,kBAAU,4BAA6B,UAAYR,KAAKE,OAAQF,KAAKI,YACxFJ,KAAKS,iBAAmB,kBAAU,aAAc,mBAG9CC,YAAcC,2BAAgBC,OAAO,CACvCJ,MAAOR,KAAKQ,MACZK,KAAMC,kBAASC,aAAa,WAAY,kCAAmC/B,UAAWgB,MACtFgB,MAAM,EACNC,QAAS,MACGjB,KAAKS,mBACH,kBAAU,SAAU,aAItCC,MAAMQ,UAAUC,GAAGC,sBAAYC,MAAM,KACjCX,MAAMQ,UAAUI,KAAK,QAAQC"}
@@ -0,0 +1,43 @@
// 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/>.
/**
* Module to autosubmit the verification code element when it reaches 6 characters.
*
* @module tool_mfa/autosubmit_verification_code
* @copyright 2020 Peter Burnett <peterburnett@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
export const init = () => {
const codeInput = document.querySelector("#id_verificationcode");
const codeForm = codeInput.closest("form");
const submitButton = codeForm.querySelector("#id_submitbutton");
// Event listener for code input field.
codeInput.addEventListener('keyup', function() {
if (this.value.length >= 6) {
// Submits the closes form (parent).
codeForm.submit();
}
});
// Disable the submit button if the input field is disabled.
// This occurs if there are no more attempts left for the factor.
if (codeInput.disabled) {
submitButton.disabled = true;
}
};
@@ -0,0 +1,108 @@
// 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/>.
/**
* Modal for confirming factor actions.
*
* @module tool_mfa/confirmation_modal
* @copyright 2023 David Woloszyn <david.woloszyn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import ModalEvents from 'core/modal_events';
import ModalSaveCancel from 'core/modal_save_cancel';
import Notification from 'core/notification';
import {getString} from 'core/str';
import Url from 'core/url';
import Fragment from 'core/fragment';
import * as Prefetch from 'core/prefetch';
const SELECTORS = {
ACTION: '.mfa-action-button',
};
/**
* Entrypoint of the js.
*
* @method init
* @param {Integer} contextId Context ID of the user.
*/
export const init = (contextId) => {
// Prefetch the language strings.
Prefetch.prefetchStrings('tool_mfa', [
'yesremove',
'yesreplace',
]);
registerEventListeners(contextId);
};
/**
* Register event listeners.
*
* @method registerEventListeners
* @param {Integer} contextId Context ID of the user.
*/
const registerEventListeners = (contextId) => {
document.addEventListener('click', (e) => {
const action = e.target.closest(SELECTORS.ACTION);
if (action) {
buildModal(action, contextId).catch(Notification.exception);
}
});
};
/**
* Build the modal with the provided data.
*
* @method buildModal
* @param {HTMLElement} element The button element.
* @param {Number} contextId Context ID of the user.
*/
const buildModal = async(element, contextId) => {
// Prepare data for modal.
const data = {
action: element.getAttribute('data-action'),
factor: element.getAttribute('data-factor'),
factorid: element.getAttribute('data-factorid'),
devicename: element.getAttribute('data-devicename'),
actionurl: Url.relativeUrl('/admin/tool/mfa/action.php'),
};
// Customise modal depending on action being performed.
if (data.action === 'revoke') {
data.title = await getString('revokefactorconfirmation', 'factor_' + data.factor, data.devicename);
data.buttontext = await getString('yesremove', 'tool_mfa');
} else if (data.action === 'replace') {
data.title = await getString('replacefactorconfirmation', 'factor_' + data.factor, data.devicename);
data.buttontext = await getString('yesreplace', 'tool_mfa');
}
const modal = await ModalSaveCancel.create({
title: data.title,
body: Fragment.loadFragment('tool_mfa', 'factor_action_confirmation_form', contextId, data),
show: true,
buttons: {
'save': data.buttontext,
'cancel': getString('cancel', 'moodle'),
},
});
modal.getRoot().on(ModalEvents.save, () => {
modal.getRoot().find('form').submit();
});
};
+130
View File
@@ -0,0 +1,130 @@
<?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/>.
/**
* MFA page
*
* @package tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../../config.php');
require_once($CFG->dirroot . '/admin/tool/mfa/lib.php');
require_once($CFG->libdir.'/adminlib.php');
use tool_mfa\local\form\login_form;
use tool_mfa\manager;
use tool_mfa\plugininfo\factor;
require_login(null, false);
$context = context_user::instance($USER->id);
$PAGE->set_context($context);
$PAGE->set_url('/admin/tool/mfa/auth.php');
$PAGE->set_pagelayout('login');
$PAGE->blocks->show_only_fake_blocks();
$pagetitle = $SITE->shortname.': '.get_string('mfa', 'tool_mfa');
$PAGE->set_title($pagetitle);
// Logout if it was requested.
$logout = optional_param('logout', false, PARAM_BOOL);
$sesskey = optional_param('sesskey', '_none_', PARAM_RAW);
if ($logout) {
if (!confirm_sesskey($sesskey)) {
echo $OUTPUT->header();
echo $OUTPUT->confirm(
get_string('logoutconfirm'),
new moodle_url($PAGE->url, ['logout' => 1, 'sesskey' => sesskey()]),
new moodle_url('/'),
);
echo $OUTPUT->footer();
die;
}
if (!empty($SESSION->wantsurl)) {
// If we have the wantsurl, we should redirect there, to keep it intact.
$wantsurl = $SESSION->wantsurl;
} else {
// Else redirect home.
$wantsurl = new \moodle_url($CFG->wwwroot);
}
manager::mfa_logout();
redirect($wantsurl);
}
$currenturl = new moodle_url('/admin/tool/mfa/auth.php');
// Perform state check.
manager::resolve_mfa_status();
// We have a valid landing here, before doing any actions, clear any redir loop progress.
manager::clear_redirect_counter();
// If a specific factor was requested, use it.
$pickedname = optional_param('factorname', false, PARAM_ALPHA);
$pickedfactor = factor::get_factor($pickedname);
$formfactor = optional_param('factor', false, PARAM_ALPHA);
if ($pickedfactor && $pickedfactor->has_input() && $pickedfactor->get_state() == factor::STATE_UNKNOWN) {
$factor = $pickedfactor;
} else if ($formfactor) {
// Check if a factor was supplied by the form, such as for a form submission.
$factor = factor::get_factor($formfactor);
} else {
// Else, get the next factor that requires input.
$factor = factor::get_next_user_login_factor();
}
// If ok, perform form actions for input factor.
$form = new login_form($currenturl, ['factor' => $factor], 'post', '', ['class' => 'ignoredirty']);
if ($form->is_submitted()) {
if (!$form->is_validated() && !$form->is_cancelled()) {
// Increment the fail counter for the factor,
// And let the factor handle locking logic.
$factor->increment_lock_counter();
manager::resolve_mfa_status(false);
} else {
// Set state from user actions.
if ($form->is_cancelled()) {
$factor->process_cancel_action();
// Move to next factor.
manager::resolve_mfa_status(true);
} else {
if ($data = $form->get_data()) {
// Validation has passed, so before processing, lets action the global form submissions as well.
$form->globalmanager->submit($data);
// Did user submit something that causes a fail state?
if ($factor->get_state() == factor::STATE_FAIL) {
manager::resolve_mfa_status(true);
}
$factor->set_state(factor::STATE_PASS);
// Move to next factor.
manager::resolve_mfa_status(true);
}
}
}
}
$renderer = $PAGE->get_renderer('tool_mfa');
echo $OUTPUT->header();
manager::display_debug_notification();
echo $renderer->verification_form($factor, $form);
echo $OUTPUT->footer();
@@ -0,0 +1,97 @@
<?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 tool_mfa\event;
use stdClass;
/**
* Event for when user factor is deleted.
*
* @property-read array $other {
* Extra information about event.
* }
*
* @package 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 user_deleted_factor extends \core\event\base {
/**
* Create instance of event.
*
* @param stdClass $user the User object of the User who had the factor deleted.
* @param stdClass $deleteuser the user who performed the factor delete.
* @param string $factorname deleted factor
*
* @return \core\event\base the user_factor_deleted event
*
* @throws \coding_exception
*/
public static function user_deleted_factor_event(stdClass $user, $deleteuser, $factorname): \core\event\base {
$data = [
'relateduserid' => $user->id,
'context' => \context_user::instance($user->id),
'other' => [
'userid' => $user->id,
'factorname' => $factorname,
'delete' => $deleteuser->id,
],
];
return self::create($data);
}
/**
* Init method.
*
* @return void
*/
protected function init(): void {
$this->data['crud'] = 'd';
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description(): string {
// The log message changed from logging the deleter user object to the ID. This must be kept for backwards compat
// With old log events.
if (is_object($this->other['delete'])) {
return "The user with id '{$this->other['delete']->id}' successfully deleted
{$this->other['factorname']} factor for user with id '{$this->other['userid']}'";
} else {
return "The user with id '{$this->other['delete']}' successfully deleted
{$this->other['factorname']} factor for user with id '{$this->other['userid']}'";
}
}
/**
* Return localised event name.
*
* @return string
* @throws \coding_exception
*/
public static function get_name(): string {
return get_string('event:userdeletedfactor', 'tool_mfa');
}
}
@@ -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/>.
namespace tool_mfa\event;
use stdClass;
/**
* Event for when user successfully passed all MFA factor checks.
*
* @property-read array $other {
* Extra information about event.
* }
*
* @package 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 user_failed_mfa extends \core\event\base {
/**
* Create instance of event.
*
* @param stdClass $user the User object of the User who failed MFA authentication.
*
* @return user_failed_mfa the user_passed_mfa event
*
* @throws \coding_exception
*/
public static function user_failed_mfa_event(stdClass $user): user_failed_mfa {
// Build debug info string.
$factors = \tool_mfa\plugininfo\factor::get_active_user_factor_types();
$debug = '';
$failurereason = get_string('event:failnotenoughfactors', 'tool_mfa');
foreach ($factors as $factor) {
$debug .= "<br> Factor {$factor->name} status: {$factor->get_state()}";
if ($factor->get_state() === \tool_mfa\plugininfo\factor::STATE_FAIL) {
$failurereason = get_string('event:failfactor', 'tool_mfa');
} else if ($factor->get_state() === \tool_mfa\plugininfo\factor::STATE_LOCKED) {
$failurereason = get_string('event:faillockout', 'tool_mfa');
}
}
$data = [
'relateduserid' => null,
'context' => \context_user::instance($user->id),
'other' => [
'userid' => $user->id,
'debug' => $debug,
'failurereason' => $failurereason,
],
];
return self::create($data);
}
/**
* Init method.
*
* @return void
*/
protected function init(): void {
$this->data['crud'] = 'r';
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description(): string {
return "The user with id '{$this->other['userid']}' failed authenticating with MFA.
<br> Information: {$this->other['failurereason']}{$this->other['debug']}";
}
/**
* Return localised event name.
*
* @return string
* @throws \coding_exception
*/
public static function get_name(): string {
return get_string('event:userfailedmfa', 'tool_mfa');
}
}
@@ -0,0 +1,93 @@
<?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 tool_mfa\event;
use stdClass;
/**
* Event for when user successfully passed all MFA factor checks.
*
* @property-read array $other {
* Extra information about event.
* }
*
* @package tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user_passed_mfa extends \core\event\base {
/**
* Create instance of event.
*
* @param stdClass $user the User object of the User who passed all MFA factor checks.
*
* @return user_passed_mfa the user_passed_mfa event
*
* @throws \coding_exception
*/
public static function user_passed_mfa_event(stdClass $user): user_passed_mfa {
// Build debug info string.
$factors = \tool_mfa\plugininfo\factor::get_active_user_factor_types();
$debug = '';
foreach ($factors as $factor) {
$debug .= "<br> Factor {$factor->name} status: {$factor->get_state()}";
}
$data = [
'relateduserid' => null,
'context' => \context_user::instance($user->id),
'other' => [
'userid' => $user->id,
'debug' => $debug,
],
];
return self::create($data);
}
/**
* Init method.
*
* @return void
*/
protected function init(): void {
$this->data['crud'] = 'r';
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description(): string {
return "The user with id '{$this->other['userid']}' successfully passed MFA. <br> Information: {$this->other['debug']}";
}
/**
* Return localised event name.
*
* @return string
* @throws \coding_exception
*/
public static function get_name(): string {
return get_string('event:userpassedmfa', 'tool_mfa');
}
}
@@ -0,0 +1,87 @@
<?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 tool_mfa\event;
use stdClass;
/**
* Event for when user successfully revoked MFA Factor.
*
* @property-read array $other {
* Extra information about event.
* }
*
* @package tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user_revoked_factor extends \core\event\base {
/**
* Create instance of event.
*
* @param stdClass $user the User object of the User who has revoked new factor
* @param string $factorname revoked factor
*
* @return self the related event
*
* @throws \coding_exception
*/
public static function user_revoked_factor_event(stdClass $user, $factorname): self {
$data = [
'relateduserid' => null,
'context' => \context_user::instance($user->id),
'other' => [
'userid' => $user->id,
'factorname' => $factorname,
],
];
return self::create($data);
}
/**
* Init method.
*
* @return void
*/
protected function init(): void {
$this->data['crud'] = 'd';
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description(): string {
return "The user with id '{$this->other['userid']}' successfully revoked {$this->other['factorname']}";
}
/**
* Return localised event name.
*
* @return string
* @throws \coding_exception
*/
public static function get_name(): string {
return get_string('event:userrevokedfactor', 'tool_mfa');
}
}
@@ -0,0 +1,87 @@
<?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 tool_mfa\event;
use stdClass;
/**
* Event for when user successfully setup new MFA Factor.
*
* @property-read array $other {
* Extra information about event.
* }
*
* @package tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user_setup_factor extends \core\event\base {
/**
* Create instance of event.
*
* @param stdClass $user the User object of the User who has setup new factor
* @param string $factorname setup factor
*
* @return self the related event
*
* @throws \coding_exception
*/
public static function user_setup_factor_event(stdClass $user, $factorname): self {
$data = [
'relateduserid' => null,
'context' => \context_user::instance($user->id),
'other' => [
'userid' => $user->id,
'factorname' => $factorname,
],
];
return self::create($data);
}
/**
* Init method.
*
* @return void
*/
protected function init(): void {
$this->data['crud'] = 'c';
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description(): string {
return "The user with id '{$this->other['userid']}' successfully setup {$this->other['factorname']}";
}
/**
* Return localised event name.
*
* @return string
* @throws \coding_exception
*/
public static function get_name(): string {
return get_string('event:usersetupfactor', 'tool_mfa');
}
}
@@ -0,0 +1,34 @@
<?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 tool_mfa\hook;
use core\hook\stoppable_trait;
/**
* Allow plugins to callback as soon possible after user has passed MFA.
*
* @package tool_mfa
* @copyright 2024 Juan Leyva
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\core\attribute\label('Allow plugins to callback as soon possible after user has passed MFA.')]
#[\core\attribute\tags('user', 'login')]
class after_user_passed_mfa implements
\Psr\EventDispatcher\StoppableEventInterface
{
use stoppable_trait;
}
@@ -0,0 +1,271 @@
<?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 tool_mfa\local;
use tool_mfa\local\factor\object_factor_base;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/ddllib.php');
require_once($CFG->libdir.'/xmlize.php');
require_once($CFG->libdir.'/messagelib.php');
/**
* Admin setting for MFA.
*
* @package tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class admin_setting_managemfa extends \admin_setting {
/**
* Calls parent::__construct with specific arguments
*/
public function __construct() {
$this->nosave = true;
parent::__construct('mfaui', get_string('mfasettings', 'tool_mfa'), '', '');
}
/**
* Always returns true
*
* @return bool
*/
public function get_setting(): bool {
return true;
}
/**
* Always returns '' and doesn't write anything
*
* @param mixed $data
* @return string Always returns ''
*/
public function write_setting($data): string {
return '';
}
/**
* Returns XHTML to display Manage MFA admin page.
*
* @param mixed $data Unused
* @param string $query
*
* @return string highlight
* @throws \coding_exception
* @throws \moodle_exception
*/
public function output_html($data, $query=''): string {
global $OUTPUT;
$return = $OUTPUT->box_start('generalbox');
$return .= $this->define_manage_mfa_table();
$return .= $OUTPUT->box_end();
$return .= $OUTPUT->heading(get_string('settings:combinations', 'tool_mfa'), 3);
$return .= $OUTPUT->box_start('generalbox');
$return .= $this->define_factor_combinations_table();
$return .= $OUTPUT->box_end();
return highlight($query, $return);
}
/**
* Defines main table with configurable factors.
*
* @return string HTML code
* @throws \coding_exception
* @throws \moodle_exception
*/
public function define_manage_mfa_table() {
global $OUTPUT;
$sesskey = sesskey();
$txt = get_strings(['enable', 'disable', 'moveup', 'movedown', 'order', 'settings']);
$txt->factor = get_string('factor', 'tool_mfa');
$txt->weight = get_string('weight', 'tool_mfa');
$txt->setup = get_string('setuprequired', 'tool_mfa');
$txt->input = get_string('inputrequired', 'tool_mfa');
$table = new \html_table();
$table->id = 'managemfatable';
$table->attributes['class'] = 'admintable generaltable';
$table->head = [
$txt->factor,
$txt->enable,
$txt->order,
$txt->weight,
$txt->settings,
$txt->setup,
$txt->input,
];
$table->colclasses = ['leftalign', 'centeralign', 'centeralign', 'centeralign', 'centeralign'];
$table->data = [];
$factors = \tool_mfa\plugininfo\factor::get_factors();
$enabledfactors = \tool_mfa\plugininfo\factor::get_enabled_factors();
$order = 1;
foreach ($factors as $factor) {
$settingsparams = ['section' => 'factor_'.$factor->name];
$settingsurl = new \moodle_url('settings.php', $settingsparams);
$settingslink = \html_writer::link($settingsurl, $txt->settings);
if ($factor->is_enabled()) {
$hideshowparams = ['action' => 'disable', 'factor' => $factor->name, 'sesskey' => $sesskey];
$hideshowurl = new \moodle_url('tool/mfa/index.php', $hideshowparams);
$hideshowlink = \html_writer::link($hideshowurl, $OUTPUT->pix_icon('t/hide', $txt->disable));
$class = '';
if ($order > 1) {
$upparams = ['action' => 'up', 'factor' => $factor->name, 'sesskey' => $sesskey];
$upurl = new \moodle_url('tool/mfa/index.php', $upparams);
$uplink = \html_writer::link($upurl, $OUTPUT->pix_icon('t/up', $txt->moveup));
} else {
$uplink = \html_writer::link('', $uplink = $OUTPUT->spacer(['style' => 'margin-right: .5rem']));
}
if ($order < count($enabledfactors)) {
$downparams = ['action' => 'down', 'factor' => $factor->name, 'sesskey' => $sesskey];
$downurl = new \moodle_url('tool/mfa/index.php', $downparams);
$downlink = \html_writer::link($downurl, $OUTPUT->pix_icon('t/down', $txt->movedown));
} else {
$downlink = '';
}
$updownlink = $uplink.$downlink;
$order++;
} else {
$hideshowparams = ['action' => 'enable', 'factor' => $factor->name, 'sesskey' => $sesskey];
$hideshowurl = new \moodle_url('tool/mfa/index.php', $hideshowparams);
$hideshowlink = \html_writer::link($hideshowurl, $OUTPUT->pix_icon('t/show', $txt->enable));
$class = 'dimmed_text';
$updownlink = '';
}
$hassetup = $factor->has_setup() ? get_string('yes') : get_string('no');
$hasinput = $factor->has_input() ? get_string('yes') : get_string('no');
$rowarray = [
$factor->get_display_name(),
$hideshowlink,
$updownlink,
$factor->get_weight(),
$settingslink,
$hassetup,
$hasinput,
];
$row = new \html_table_row($rowarray);
$row->attributes['class'] = $class;
$table->data[] = $row;
}
return \html_writer::table($table);
}
/**
* Defines supplementary table that shows available combinations of factors enough for successful authentication.
*
* @return string HTML code
*/
public function define_factor_combinations_table() {
global $OUTPUT;
$factors = \tool_mfa\plugininfo\factor::get_enabled_factors();
$combinations = $this->get_factor_combinations($factors, 0, count($factors) - 1);
if (empty($combinations)) {
return $OUTPUT->notification(get_string('error:notenoughfactors', 'tool_mfa'), 'notifyproblem');
}
$txt = get_strings(['combination', 'totalweight'], 'tool_mfa');
$table = new \html_table();
$table->id = 'managemfatable';
$table->attributes['class'] = 'admintable generaltable table table-bordered';
$table->head = [$txt->combination, $txt->totalweight];
$table->colclasses = ['leftalign', 'centeralign'];
$table->data = [];
$factorstringconnector = get_string('connector', 'tool_mfa');
foreach ($combinations as $combination) {
$factorstrings = array_map(static function(object_factor_base $factor): string {
return $factor->get_summary_condition() . ' <sup>' . $factor->get_weight() . '</sup>';
}, $combination['combination']);
$string = implode(" {$factorstringconnector} ", $factorstrings);
$table->data[] = new \html_table_row([$string, $combination['totalweight']]);
}
return \html_writer::table($table);
}
/**
* Recursive method to get all possible combinations of given factors.
* Output is filtered by combination total weight (should be greater than 100).
*
* @param array $allfactors initial array of factor objects
* @param int $start start position in initial array
* @param int $end end position in initial array
* @param int $totalweight total weight of combination
* @param array $combination combination candidate
* @param array $result array that includes combination total weight and subarray of factors combination
*
* @return array
*/
public function get_factor_combinations($allfactors, $start = 0, $end = 0,
$totalweight = 0, $combination = [], $result = []) {
if ($totalweight >= 100) {
// Ensure this is a valid combination before appending result.
$valid = true;
foreach ($combination as $factor) {
if (!$factor->check_combination($combination)) {
$valid = false;
}
}
if ($valid) {
$result[] = ['totalweight' => $totalweight, 'combination' => $combination];
}
return $result;
} else if ($start > $end) {
return $result;
}
$combinationnext = $combination;
$combinationnext[] = $allfactors[$start];
$result = $this->get_factor_combinations(
$allfactors,
$start + 1,
$end,
$totalweight + $allfactors[$start]->get_weight(),
$combinationnext,
$result);
$result = $this->get_factor_combinations(
$allfactors,
$start + 1,
$end,
$totalweight,
$combination,
$result);
return $result;
}
}
@@ -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 tool_mfa\local\factor;
/**
* Fallback factor class.
*
* @package 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 fallback extends object_factor_base {
/**
* Overridden constructor. Name is hard set to 'fallback'.
*/
public function __construct() {
$this->name = 'fallback';
}
/**
* {@inheritDoc}
*/
public function get_display_name(): string {
return get_string('fallback', 'tool_mfa');
}
/**
* {@inheritDoc}
*/
public function get_info(): string {
return get_string('fallback_info', 'tool_mfa');
}
/**
* {@inheritDoc}
*/
public function get_state(): string {
return \tool_mfa\plugininfo\factor::STATE_FAIL;
}
/**
* Sets the state of the factor check into the session.
* Returns whether storing the var was successful.
*
* @param string $state
* @return bool
*/
public function set_state(string $state): bool {
return false;
}
}
@@ -0,0 +1,331 @@
<?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/>.
/**
* MFA factor interface.
*
* @package tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_mfa\local\factor;
use stdClass;
interface object_factor {
/**
* Returns true if factor is enabled, otherwise false.
*
* @return bool
* @throws \dml_exception
*/
public function is_enabled(): bool;
/**
* Returns configured factor weight.
*
* @return int
* @throws \dml_exception
*/
public function get_weight(): int;
/**
* Returns factor name from language string.
*
* @return string
* @throws \coding_exception
*/
public function get_display_name(): string;
/**
* Returns factor info from language string.
*
* @return string
* @throws \coding_exception
*/
public function get_info(): string;
/**
* Defines setup_factor form definition page for particular factor.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
* @throws \coding_exception
*/
public function setup_factor_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm;
/**
* Defines setup_factor form definition page after form data has been set.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
* @throws \coding_exception
*/
public function setup_factor_form_definition_after_data(\MoodleQuickForm $mform): \MoodleQuickForm;
/**
* Implements setup_factor form validation for particular factor.
* 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;
/**
* Defines login form definition page for particular factor.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
* @throws \coding_exception
*/
public function login_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm;
/**
* Defines login form definition page after form data has been set.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
* @throws \coding_exception
*/
public function login_form_definition_after_data(\MoodleQuickForm $mform): \MoodleQuickForm;
/**
* Implements login form validation for particular factor.
* Returns an array of errors, where array key = field id and array value = error text.
*
* @param array $data
* @return array
*/
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.
*
* @param stdClass $data
* @return stdClass|null the factor record, or null.
*/
public function setup_user_factor(stdClass $data): stdClass|null;
/**
* Returns an array of all user factors of given type (both active and revoked).
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_all_user_factors(stdClass $user): array;
/**
* Returns an array of active user factor records.
* Filters get_all_user_factors() output.
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_active_user_factors(stdClass $user): array;
/**
* 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;
/**
* Marks factor record as revoked.
* If factorid is not provided, revoke all instances of factor.
*
* @param int|null $factorid
* @return bool
*/
public function revoke_user_factor(?int $factorid = null): bool;
/**
* When validation code is correct - update lastverified field for given factor.
* If factor id is not provided, update all factor entries for user.
*
* @param int|null $factorid
* @return bool|\dml_exception
*/
public function update_lastverified(?int $factorid = null): bool|\dml_exception;
/**
* Gets lastverified timestamp.
*
* @param int $factorid
* @return int|bool
*/
public function get_lastverified(int $factorid): int|bool;
/**
* Returns true if factor needs to be setup by user and has setup_form.
*
* @return bool
*/
public function has_setup(): bool;
/**
* If has_setup returns true, decides if the setup buttons should be shown on the preferences page.
*
* @return bool
*/
public function show_setup_buttons(): bool;
/**
* Returns true if factor requires user input for success or failure during login.
*
* @return bool
*/
public function has_input(): bool;
/**
* Returns the state of the factor check
*
* @return string
*/
public function get_state(): string;
/**
* Sets the state of the factor check into the session.
* Returns whether storing the var was successful.
*
* @param string $state
* @return bool
*/
public function set_state(string $state): bool;
/**
* Fires any additional actions required by the factor once the user reaches the pass state.
*
* @return void
*/
public function post_pass_state(): void;
/**
* Retrieves label for a factorid.
*
* @param int $factorid
* @return string|\dml_exception
*/
public function get_label(int $factorid): string|\dml_exception;
/**
* Returns a list of urls to not redirect from.
*
* @return array
*/
public function get_no_redirect_urls(): array;
/**
* Returns all possible states for a user.
*
* @param stdClass $user
* @return array
*/
public function possible_states(stdClass $user): array;
/**
* Return summary condition for passing factor.
*
* @return string
*/
public function get_summary_condition(): string;
/**
* Checks whether the factor combination is valid based on factor behaviour.
* E.g. a combination with nosetup and another factor is not valid,
* as you cannot pass nosetup with another factor.
*
* @param array $combination array of factors that make up the combination
* @return bool
*/
public function check_combination(array $combination): bool;
/**
* Gets the string for setup button on preferences page.
*
* @return string the string to display on the button.
*/
public function get_setup_string(): string;
/**
* Deletes all instances of a factor for user.
*
* @param stdClass $user the user to delete for.
* @return void
*/
public function delete_factor_for_user(stdClass $user): void;
/**
* Process a cancel action from a user.
*
* @return void
*/
public function process_cancel_action(): void;
/**
* Hook point for global auth form action hooks.
*
* @param \MoodleQuickForm $mform Form to inject global elements into.
* @return void
*/
public function global_definition(\MoodleQuickForm $mform): void;
/**
* Hook point for global auth form action hooks.
*
* @param \MoodleQuickForm $mform Form to inject global elements into.
* @return void
*/
public function global_definition_after_data(\MoodleQuickForm $mform): void;
/**
* Hook point for global auth form action hooks.
*
* @param array $data Data from the form.
* @param array $files Files form the form.
* @return array of errors from validation.
*/
public function global_validation(array $data, array $files): array;
/**
* Hook point for global auth form action hooks.
*
* @param object $data Data from the form.
* @return void
*/
public function global_submit(object $data): void;
}
@@ -0,0 +1,715 @@
<?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 tool_mfa\local\factor;
use stdClass;
/**
* MFA factor abstract class.
*
* @package tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class object_factor_base implements object_factor {
/** @var string Factor name */
public $name;
/** @var int Lock counter */
private $lockcounter;
/**
* Secret manager
*
* @var \tool_mfa\local\secret_manager
*/
protected $secretmanager;
/** @var string Factor icon */
protected $icon = 'fa-lock';
/**
* Class constructor
*
* @param string $name factor name
*/
public function __construct($name) {
global $DB, $USER;
$this->name = $name;
// Setup secret manager.
$this->secretmanager = new \tool_mfa\local\secret_manager($this->name);
}
/**
* This loads the locked state from the DB
* Base class implementation.
*
* @return void
*/
public function load_locked_state(): void {
global $DB, $USER;
// Check if lockcounter column exists (incase upgrade hasnt run yet).
// Only 'input factors' are lockable.
if ($this->is_enabled() && $this->is_lockable()) {
try {
// Setup the lock counter.
$sql = "SELECT MAX(lockcounter) FROM {tool_mfa} WHERE userid = ? AND factor = ? AND revoked = ?";
@$this->lockcounter = $DB->get_field_sql($sql, [$USER->id, $this->name, 0]);
if (empty($this->lockcounter)) {
$this->lockcounter = 0;
}
// Now lock this factor if over the counter.
$lockthreshold = get_config('tool_mfa', 'lockout');
if ($this->lockcounter >= $lockthreshold) {
$this->set_state(\tool_mfa\plugininfo\factor::STATE_LOCKED);
}
} catch (\dml_exception $e) {
// Set counter to -1.
$this->lockcounter = -1;
}
}
}
/**
* Returns true if factor is enabled, otherwise false.
*
* Base class implementation.
*
* @return bool
* @throws \dml_exception
*/
public function is_enabled(): bool {
$status = get_config('factor_'.$this->name, 'enabled');
if ($status == 1) {
return true;
}
return false;
}
/**
* Returns configured factor weight.
*
* Base class implementation.
*
* @return int
* @throws \dml_exception
*/
public function get_weight(): int {
$weight = get_config('factor_'.$this->name, 'weight');
if ($weight) {
return (int) $weight;
}
return 0;
}
/**
* Returns factor name from language string.
*
* Base class implementation.
*
* @return string
* @throws \coding_exception
*/
public function get_display_name(): string {
return get_string('pluginname', 'factor_'.$this->name);
}
/**
* Returns factor help from language string.
*
* Base class implementation.
*
* @return string
* @throws \coding_exception
*/
public function get_info(): string {
return get_string('info', 'factor_'.$this->name);
}
/**
* Returns factor help from language string when there is factor management available.
*
* Base class implementation.
*
* @param int $factorid The factor we want manage info for.
* @return string
* @throws \coding_exception
*/
public function get_manage_info(int $factorid): string {
return get_string('manageinfo', 'factor_'.$this->name, $this->get_label($factorid));
}
/**
* Defines setup_factor form definition page for particular factor.
*
* Dummy implementation. Should be overridden in child class.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
*/
public function setup_factor_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm {
return $mform;
}
/**
* Defines setup_factor form definition page after form data has been set.
*
* Dummy implementation. Should be overridden in child class.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
*/
public function setup_factor_form_definition_after_data(\MoodleQuickForm $mform): \MoodleQuickForm {
return $mform;
}
/**
* Implements setup_factor form validation for particular factor.
* Returns an array of errors, where array key = field id and array value = error text.
*
* Dummy implementation. Should be overridden in child class.
*
* @param array $data
* @return array
*/
public function setup_factor_form_validation(array $data): array {
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.
*
* Dummy implementation. Should be overridden in child class.
*
* @param stdClass $data
* @return stdClass|null the record if created, or null.
*/
public function setup_user_factor(stdClass $data): stdClass|null {
return null;
}
/**
* Replaces a given factor and adds it to user's active factors list.
* Returns the new factor if it has been successfully replaced.
*
* Dummy implementation. Should be overridden in child class.
*
* @param stdClass $data The new factor data.
* @param int $id The id of the factor to replace.
* @return stdClass|null the record if created, or null.
*/
public function replace_user_factor(stdClass $data, int $id): stdClass|null {
return null;
}
/**
* Returns an array of all user factors of given type (both active and revoked).
*
* Dummy implementation. Should be overridden in child class.
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_all_user_factors(stdClass $user): array {
return [];
}
/**
* Returns an array of active user factor records.
* Filters get_all_user_factors() output.
*
* @param stdClass $user object to check against.
* @return array
*/
public function get_active_user_factors(stdClass $user): array {
$return = [];
$factors = $this->get_all_user_factors($user);
foreach ($factors as $factor) {
if ($factor->revoked == 0) {
$return[] = $factor;
}
}
return $return;
}
/**
* Defines login form definition page for particular factor.
*
* Dummy implementation. Should be overridden in child class.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
*/
public function login_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm {
return $mform;
}
/**
* Defines login form definition page after form data has been set.
*
* Dummy implementation. Should be overridden in child class.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
*/
public function login_form_definition_after_data(\MoodleQuickForm $mform): \MoodleQuickForm {
return $mform;
}
/**
* Implements login form validation for particular factor.
* Returns an array of errors, where array key = field id and array value = error text.
*
* Dummy implementation. Should be overridden in child class.
*
* @param array $data
* @return array
*/
public function login_form_validation(array $data): array {
return [];
}
/**
* Returns true if factor class has factor records that might be revoked.
* It means that user can revoke factor record from their profile.
*
* Override in child class if necessary.
*
* @return bool
*/
public function has_revoke(): bool {
return false;
}
/**
* Marks factor record as revoked.
* If factorid is not provided, revoke all instances of factor.
*
* @param int|null $factorid
* @return bool
* @throws \coding_exception
* @throws \dml_exception
*/
public function revoke_user_factor(?int $factorid = null): bool {
global $DB, $USER;
if (!empty($factorid)) {
// If we have an explicit factor id, this means we need to be careful about the user.
$params = ['id' => $factorid];
$existing = $DB->get_record('tool_mfa', $params);
if (empty($existing)) {
return false;
}
$matchinguser = $existing->userid == $USER->id;
if (!is_siteadmin() && !$matchinguser) {
// We aren't admin, and this isn't our factor.
return false;
}
} else {
$params = ['userid' => $USER->id, 'factor' => $this->name];
}
$DB->set_field('tool_mfa', 'revoked', 1, $params);
$event = \tool_mfa\event\user_revoked_factor::user_revoked_factor_event($USER, $this->get_display_name());
$event->trigger();
return true;
}
/**
* Returns true if factor class has factor records that can be replaced.
*
* Override in child class if necessary.
*
* @return bool
*/
public function has_replace(): bool {
return false;
}
/**
* When validation code is correct - update lastverified field for given factor.
* If factor id is not provided, update all factor entries for user.
*
* @param int|null $factorid
* @return bool|\dml_exception
* @throws \dml_exception
*/
public function update_lastverified(?int $factorid = null): bool|\dml_exception {
global $DB, $USER;
if (!empty($factorid)) {
$params = ['id' => $factorid];
} else {
$params = ['factor' => $this->name, 'userid' => $USER->id];
}
return $DB->set_field('tool_mfa', 'lastverified', time(), $params);
}
/**
* Gets lastverified timestamp.
*
* @param int $factorid
* @return int|bool the lastverified timestamp, or false if not found.
*/
public function get_lastverified(int $factorid): int|bool {
global $DB;
$record = $DB->get_record('tool_mfa', ['id' => $factorid]);
return $record->lastverified;
}
/**
* Returns true if factor needs to be setup by user and has setup_form.
* Override in child class if necessary.
*
* @return bool
*/
public function has_setup(): bool {
return false;
}
/**
* If has_setup returns true, decides if the setup buttons should be shown on the preferences page.
*
* @return bool
*/
public function show_setup_buttons(): bool {
return $this->has_setup();
}
/**
* Returns true if a factor requires input from the user to verify.
*
* Override in child class if necessary
*
* @return bool
*/
public function has_input(): bool {
return true;
}
/**
* Returns true if a factor is able to be locked if it fails.
*
* Generally only input factors are lockable.
* Override in child class if necessary
*
* @return bool
*/
public function is_lockable(): bool {
return $this->has_input();
}
/**
* Returns the state of the factor from session information.
*
* Implementation for factors that require input.
* Should be overridden in child classes with no input.
*
* @return mixed
*/
public function get_state(): string {
global $SESSION;
$property = 'factor_'.$this->name;
if (property_exists($SESSION, $property)) {
return $SESSION->$property;
} else {
return \tool_mfa\plugininfo\factor::STATE_UNKNOWN;
}
}
/**
* Sets the state of the factor into the session.
*
* Implementation for factors that require input.
* Should be overridden in child classes with no input.
*
* @param string $state the state constant to set.
* @return bool
*/
public function set_state(string $state): bool {
global $SESSION;
// Do not allow overwriting fail states.
if ($this->get_state() == \tool_mfa\plugininfo\factor::STATE_FAIL) {
return false;
}
$property = 'factor_'.$this->name;
$SESSION->$property = $state;
return true;
}
/**
* Creates an event when user successfully setup a factor
*
* @param object $user
* @return void
*/
public function create_event_after_factor_setup(object $user): void {
$event = \tool_mfa\event\user_setup_factor::user_setup_factor_event($user, $this->get_display_name());
$event->trigger();
}
/**
* Function for factor actions in the pass state.
* Override in child class if necessary.
*
* @return void
*/
public function post_pass_state(): void {
// Update lastverified for factor.
if ($this->get_state() == \tool_mfa\plugininfo\factor::STATE_PASS) {
$this->update_lastverified();
}
// Now clean temp secrets for factor.
$this->secretmanager->cleanup_temp_secrets();
}
/**
* Function to retrieve the label for a factorid.
*
* @param int $factorid
* @return string|\dml_exception
*/
public function get_label(int $factorid): string|\dml_exception {
global $DB;
return $DB->get_field('tool_mfa', 'label', ['id' => $factorid]);
}
/**
* Function to get urls that should not be redirected from.
*
* @return array
*/
public function get_no_redirect_urls(): array {
return [];
}
/**
* Function to get possible states for a user from factor.
* Implementation where state is based on deterministic user data.
* This should be overridden in factors where state is non-deterministic.
* E.g. IP changes based on whether a user is using a VPN.
*
* @param stdClass $user
* @return array
*/
public function possible_states(stdClass $user): array {
return [$this->get_state()];
}
/**
* Returns condition for passing factor.
* Implementation for basic conditions.
* Override for complex conditions such as auth type.
*
* @return string
*/
public function get_summary_condition(): string {
return get_string('summarycondition', 'factor_'.$this->name);
}
/**
* Checks whether the factor combination is valid based on factor behaviour.
* E.g. a combination with nosetup and another factor is not valid,
* as you cannot pass nosetup with another factor.
*
* @param array $combination array of factors that make up the combination
* @return bool
*/
public function check_combination(array $combination): bool {
return true;
}
/**
* Gets the string for setup button on preferences page.
*
* @return string
*/
public function get_setup_string(): string {
return get_string('setupfactor', 'tool_mfa');
}
/**
* Gets the string for manage button on preferences page.
*
* @return string
*/
public function get_manage_string(): string {
return get_string('managefactor', 'tool_mfa');
}
/**
* Deletes all instances of factor for a user.
*
* @param stdClass $user the user to delete for.
* @return void
*/
public function delete_factor_for_user(stdClass $user): void {
global $DB, $USER;
$DB->delete_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]);
// Emit event for deletion.
$event = \tool_mfa\event\user_deleted_factor::user_deleted_factor_event($user, $USER, $this->name);
$event->trigger();
}
/**
* Increments the lock counter for a factor.
*
* @return void
*/
public function increment_lock_counter(): void {
global $DB, $USER;
// First make sure the state is loaded.
$this->load_locked_state();
// If lockcounter is negative, the field does not exist yet.
if ($this->lockcounter === -1) {
return;
}
$this->lockcounter++;
// Update record in DB.
$DB->set_field('tool_mfa', 'lockcounter', $this->lockcounter, ['userid' => $USER->id, 'factor' => $this->name]);
// Now lock this factor if over the counter.
$lockthreshold = get_config('tool_mfa', 'lockout');
if ($this->lockcounter >= $lockthreshold) {
$this->set_state(\tool_mfa\plugininfo\factor::STATE_LOCKED);
}
}
/**
* Return the number of remaining attempts at this factor.
*
* @return int the number of attempts at this factor remaining.
*/
public function get_remaining_attempts(): int {
$lockthreshold = get_config('tool_mfa', 'lockout');
if ($this->lockcounter === -1) {
// If upgrade.php hasnt been run yet, just return 10.
return $lockthreshold;
} else {
return $lockthreshold - $this->lockcounter;
}
}
/**
* Process a cancel input from a user.
*
* @return void
*/
public function process_cancel_action(): void {
$this->set_state(\tool_mfa\plugininfo\factor::STATE_NEUTRAL);
}
/**
* Hook point for global auth form action hooks.
*
* @param \MoodleQuickForm $mform Form to inject global elements into.
* @return void
*/
public function global_definition(\MoodleQuickForm $mform): void {
return;
}
/**
* Hook point for global auth form action hooks.
*
* @param \MoodleQuickForm $mform Form to inject global elements into.
* @return void
*/
public function global_definition_after_data(\MoodleQuickForm $mform): void {
return;
}
/**
* Hook point for global auth form action hooks.
*
* @param array $data Data from the form.
* @param array $files Files form the form.
* @return array of errors from validation.
*/
public function global_validation(array $data, array $files): array {
return [];
}
/**
* Hook point for global auth form action hooks.
*
* @param object $data Data from the form.
* @return void
*/
public function global_submit(object $data): void {
return;
}
/**
* Get the icon associated with this factor.
*
* @return string the icon name.
*/
public function get_icon(): string {
return $this->icon;
}
/**
* 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 {
return get_string('logindesc', 'factor_'.$this->name);
}
}
@@ -0,0 +1,56 @@
<?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 tool_mfa\local\form;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . "/formslib.php");
/**
* Factor action confirmation form.
*
* @package tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor_action_confirmation_form extends \moodleform {
/**
* Form definition.
*/
public function definition(): void {
$mform = $this->_form;
$factor = $this->_customdata['factor'];
$devicename = $this->_customdata['devicename'];
$factorid = $this->_customdata['factorid'];
$action = $this->_customdata['action'];
$mform->addElement('html', get_string('confirmation' . $action, 'tool_mfa', $devicename));
$mform->setType('factorid', PARAM_INT);
$mform->addElement('hidden', 'factorid', $factorid);
$mform->setType('factor', PARAM_TEXT);
$mform->addElement('hidden', 'factor', $factor);
$mform->setType('action', PARAM_TEXT);
$mform->addElement('hidden', 'action', $action);
$mform->addElement('hidden', 'sesskey', sesskey());
}
}
@@ -0,0 +1,90 @@
<?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 tool_mfa\local\form;
use tool_mfa\plugininfo\factor;
/**
* MFA login form
*
* @package tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class global_form_manager {
/** @var array factors to call hooks upon. */
private $activefactors;
/**
* Create an instance of this class.
*/
public function __construct() {
$this->activefactors = factor::get_active_user_factor_types();
}
/**
* Hook point for global auth form action hooks.
*
* @param \MoodleQuickForm $mform Form to inject global elements into.
* @return void
*/
public function definition(\MoodleQuickForm &$mform): void {
foreach ($this->activefactors as $factor) {
$factor->global_definition($mform);
}
}
/**
* Hook point for global auth form action hooks.
*
* @param \MoodleQuickForm $mform Form to inject global elements into.
* @return void
*/
public function definition_after_data(\MoodleQuickForm &$mform): void {
foreach ($this->activefactors as $factor) {
$factor->global_definition_after_data($mform);
}
}
/**
* Hook point for global auth form action hooks.
*
* @param array $data Data from the form.
* @param array $files Files form the form.
* @return array of errors from validation.
*/
public function validation(array $data, array $files): array {
$errors = [];
foreach ($this->activefactors as $factor) {
$errors = array_merge($errors, $factor->global_validation($data, $files));
}
return $errors;
}
/**
* Hook point for global auth form submission hooks.
*
* @param \stdClass $data Data from the form.
* @return void
*/
public function submit(\stdClass $data): void {
foreach ($this->activefactors as $factor) {
$factor->global_submit($data);
}
}
}
@@ -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 tool_mfa\local\form;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . "/formslib.php");
/**
* MFA login form
*
* @package tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class login_form extends \moodleform {
/** @var \tool_mfa\local\form\global_form_manager */
public $globalmanager;
/**
* Create an instance of this class.
*
* @param mixed $action the action attribute for the form. If empty defaults to auto detect the
* current url. If a moodle_url object then outputs params as hidden variables.
* @param mixed $customdata if your form defintion method needs access to data such as $course
* $cm, etc. to construct the form definition then pass it in this array. You can
* use globals for somethings.
* @param string $method if you set this to anything other than 'post' then _GET and _POST will
* be merged and used as incoming data to the form.
* @param string $target target frame for form submission. You will rarely use this. Don't use
* it if you don't need to as the target attribute is deprecated in xhtml strict.
* @param mixed $attributes you can pass a string of html attributes here or an array.
* Special attribute 'data-random-ids' will randomise generated elements ids. This
* is necessary when there are several forms on the same page.
* Special attribute 'data-double-submit-protection' set to 'off' will turn off
* double-submit protection JavaScript - this may be necessary if your form sends
* downloadable files in response to a submit button, and can't call
* \core_form\util::form_download_complete();
* @param bool $editable
* @param array $ajaxformdata Forms submitted via ajax, must pass their data here, instead of relying on _GET and _POST.
*/
public function __construct($action = null, $customdata = null, $method = 'post', $target = '',
$attributes = null, $editable = true, $ajaxformdata = null) {
$this->globalmanager = new \tool_mfa\local\form\global_form_manager();
parent::__construct($action, $customdata, $method, $target, $attributes, $editable, $ajaxformdata);
}
/**
* {@inheritDoc}
* @see moodleform::definition()
*/
public function definition(): void {
$mform = $this->_form;
$factor = $this->_customdata['factor'];
$mform = $factor->login_form_definition($mform);
// Add a hidden field with the factor name so it is always available.
$factorname = $mform->addElement('hidden', 'factor', $factor->name);
$factorname->setType(PARAM_ALPHAEXT);
$this->globalmanager->definition($mform);
}
/**
* Invokes factor login_form_definition_after_data() method after form data has been set.
*
* @return void
*/
public function definition_after_data(): void {
$mform = $this->_form;
$factor = $this->_customdata['factor'];
$factor->login_form_definition_after_data($mform);
$this->globalmanager->definition_after_data($mform);
$buttonarray = [];
$buttonarray[] = &$mform->createElement('submit', 'submitbutton', get_string('loginsubmit', 'factor_' . $factor->name));
$mform->addGroup($buttonarray, 'buttonar', '', [' '], false);
$mform->closeHeaderBefore('buttonar');
}
/**
* Validates the login form with given factor validation method.
*
* @param array $data
* @param array $files
* @return array
*/
public function validation($data, $files) {
$errors = parent::validation($data, $files);
$factor = $this->_customdata['factor'];
$errors += $factor->login_form_validation($data);
$errors += $this->globalmanager->validation($data, $files);
// Execute sleep time bruteforce mitigation.
\tool_mfa\manager::sleep_timer();
return $errors;
}
/**
* Returns error corresponding to validated element.
*
* @param string $elementname Name of form element to check.
* @return string|null Error message corresponding to the validated element.
*/
public function get_element_error(string $elementname): ?string {
return $this->_form->getElementError($elementname);
}
/**
* Set an error message for a form element.
*
* @param string $elementname Name of form element to set error for.
* @param string $error Error message, if empty then removes the current error message.
* @return void
*/
public function set_element_error(string $elementname, string $error): void {
$this->_form->setElementError($elementname, $error);
}
/**
* Freeze a form element.
*
* @param string $elementname Name of form element to freeze.
* @return void
*/
public function freeze(string $elementname): void {
$this->_form->freeze($elementname);
}
/**
* Returns true if the form element exists.
*
* @param string $elementname Name of form element to check.
* @return bool
*/
public function element_exists(string $elementname): bool {
return $this->_form->elementExists($elementname);
}
}
@@ -0,0 +1,98 @@
<?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 tool_mfa\local\form;
defined('MOODLE_INTERNAL') || die();
require_once("$CFG->libdir/formslib.php");
/**
* Form to reset gracemode timer for users.
*
* @package 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 reset_factor extends \moodleform {
/**
* Form definition.
*/
public function definition(): void {
$mform = $this->_form;
$factors = $this->_customdata['factors'];
$bulkaction = $this->_customdata['bulk'];
$mform->addElement('hidden', 'bulkaction', $bulkaction);
$mform->setType('bulkaction', PARAM_BOOL);
$mform->addElement('hidden', 'returnurl');
$mform->setType('returnurl', PARAM_LOCALURL);
$factors = array_map(function ($element) {
return $element->get_display_name();
}, $factors);
// Add an 'all' action.
$factors['all'] = get_string('all');
$mform->addElement('select', 'factor', get_string('selectfactor', 'tool_mfa'), $factors);
if (!$bulkaction) {
$mform->addElement('text', 'resetfactor', get_string('resetuser', 'tool_mfa'),
['placeholder' => get_string('resetfactorplaceholder', 'tool_mfa')]);
$mform->setType('resetfactor', PARAM_TEXT);
$mform->addRule('resetfactor', get_string('userempty', 'tool_mfa'), 'required');
}
$this->add_action_buttons(true, get_string('resetconfirm', 'tool_mfa'));
}
/**
* Form validation.
*
* Server side rules do not work for uploaded files, implement serverside rules here if needed.
*
* @param array $data array of ("fieldname"=>value) of submitted data
* @param array $files array of uploaded files "element_name"=>tmp_file_path
* @return array of "element_name"=>"error_description" if there are errors,
* or an empty array if everything is OK (true allowed for backwards compatibility too).
*/
public function validation($data, $files) {
global $DB;
$errors = parent::validation($data, $files);
if (!$data['bulkaction']) {
$userinfo = $data['resetfactor'];
// Try input as username first, then email.
$user = $DB->get_record('user', ['username' => $userinfo]);
if (empty($user)) {
// If not found, try username.
$user = $DB->get_record('user', ['email' => $userinfo]);
}
if (empty($user)) {
$errors['resetfactor'] = get_string('usernotfound', 'tool_mfa');
} else {
// Add custom field to store user.
$this->_form->addElement('hidden', 'user', $user);
$this->_form->setType('user', PARAM_RAW);
}
}
return $errors;
}
}
@@ -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/>.
namespace tool_mfa\local\form;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . "/formslib.php");
/**
* Setup factor form
*
* @package tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class setup_factor_form extends \moodleform {
/**
* {@inheritDoc}
* @see moodleform::definition()
*/
public function definition(): void {
$mform = $this->_form;
// Indicate a factor id that will be replaced with this setup.
$replaceid = $this->_customdata['replaceid'] ?? null;
if (!empty($replaceid)) {
$mform->addelement('hidden', 'replaceid', $replaceid);
$mform->setType('replaceid', PARAM_INT);
}
$factorname = $this->_customdata['factorname'];
$factor = \tool_mfa\plugininfo\factor::get_factor($factorname);
$mform = $factor->setup_factor_form_definition($mform);
$this->xss_whitelist_static_form_elements($mform);
}
/**
* Validates setup_factor form with given factor validation method.
*
* @param array $data
* @param array $files
* @return array
*/
public function validation($data, $files) {
$errors = parent::validation($data, $files);
$factorname = $this->_customdata['factorname'];
$factor = \tool_mfa\plugininfo\factor::get_factor($factorname);
$errors += $factor->setup_factor_form_validation($data);
return $errors;
}
/**
* Invokes factor setup_factor_form_definition_after_data() method after form data has been set.
*/
public function definition_after_data(): void {
$mform = $this->_form;
$factorname = $this->_customdata['factorname'];
$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(true, $factor->setup_factor_form_submit_button_string());
}
/**
* Form elements clean up
*
* @param \HTML_QuickForm $mform
* @return void
*/
private function xss_whitelist_static_form_elements($mform): void {
if (!method_exists('MoodleQuickForm_static', 'set_allow_xss')) {
return;
}
$elements = $mform->_elements;
foreach ($elements as $element) {
if (is_a($element, 'MoodleQuickForm_static')) {
$element->set_allow_xss(true);
}
}
}
}
@@ -0,0 +1,124 @@
<?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 tool_mfa\local\form;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/form/text.php');
/**
* MFA Verification code element.
*
* @package 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 verification_field extends \MoodleQuickForm_text {
/** @var bool */
private $appendjs;
/**
* Verification field is a text entry box that features some useful extras.
*
* Contains JS to autosubmit the auth page when code is entered, as well as additional styling.
*
* @param array $attributes
* @param boolean $auth is this constructed in auth.php loginform_* definitions. Set to false to prevent autosubmission of form.
* @param string|null $elementlabel Provide a different element label.
*/
public function __construct($attributes = null, $auth = true, string $elementlabel = null) {
global $PAGE;
// Force attributes.
if (empty($attributes)) {
$attributes = [];
}
$attributes['autocomplete'] = 'one-time-code';
$attributes['inputmode'] = 'numeric';
$attributes['pattern'] = '[0-9]*';
// Overwrite default classes if set.
$attributes['class'] = isset($attributes['class']) ? $attributes['class'] : 'tool-mfa-verification-code font-weight-bold';
$attributes['maxlength'] = 6;
// If we aren't on the auth page, this might be part of a larger form such as for setup.
// We shouldn't autofocus here, as it probably isn't the only element, or main target.
if ($auth) {
$attributes['autofocus'] = 'autofocus';
}
// If we are on the auth page, load JS for element.
$this->appendjs = false;
if ($auth) {
$PAGE->requires->js_call_amd('tool_mfa/autosubmit_verification_code', 'init', []);
}
// Force element name to match JS.
$elementname = 'verificationcode';
// Overwrite default element label if set.
$elementlabel = !empty($elementlabel) ? $elementlabel : get_string('entercode', 'tool_mfa');
return parent::__construct($elementname, $elementlabel, $attributes);
}
/**
* Returns HTML for this form element.
*
* phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
*
* @return string
*/
public function toHtml(): string {
// Empty the value after all attributes decided.
$this->_attributes['value'] = '';
$result = parent::toHtml();
$submitjs = "<script>
document.querySelector('#id_verificationcode').addEventListener('keyup', function() {
if (this.value.length == 6) {
// Submits the closes form (parent).
this.closest('form').submit();
}
});
</script>";
if ($this->appendjs) {
$result .= $submitjs;
}
return $result;
}
/**
* Setup and return the script for autosubmission while inside the secure layout.
*
* @return string the JS to inline attach to the rendered object.
*/
public function secure_js(): string {
// Empty the value after all attributes decided.
$this->_attributes['value'] = '';
return "<script>
document.querySelector('#id_verificationcode').addEventListener('keyup', function() {
if (this.value.length == 6) {
// Submits the closes form (parent).
this.closest('form').submit();
}
});
</script>";
}
}
@@ -0,0 +1,41 @@
<?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 tool_mfa\local\hooks;
/**
* Extend user bulk actions menu
*
* @package tool_mfa
* @copyright 2024 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class extend_bulk_user_actions {
/**
* Add action to reset MFA factors
*
* @param \core_user\hook\extend_bulk_user_actions $hook
*/
public static function callback(\core_user\hook\extend_bulk_user_actions $hook): void {
if (has_capability('moodle/site:config', \context_system::instance())) {
$hook->add_action('tool_mfa_reset_factors', new \action_link(
new \moodle_url('/admin/tool/mfa/reset_factor.php'),
get_string('resetfactor', 'tool_mfa')
));
}
}
}
@@ -0,0 +1,244 @@
<?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 tool_mfa\local;
/**
* MFA secret management class.
*
* @package 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 secret_manager {
/** @var string */
const REVOKED = 'revoked';
/** @var string */
const VALID = 'valid';
/** @var string */
const NONVALID = 'nonvalid';
/** @var string */
private $factor;
/** @var string|false */
private $sessionid;
/**
* Initialises a secret manager instance
*
* @param string $factor
*/
public function __construct(string $factor) {
$this->factor = $factor;
$this->sessionid = session_id();
}
/**
* This function creates or takes a secret, and stores it in the database or session.
*
* @param int $expires the length of time the secret is valid. e.g. 1 min = 60
* @param bool $session whether this secret should be linked to the session.
* @param string $secret an optional provided secret
* @return string the secret code, or 0 if no new code created.
*/
public function create_secret(int $expires, bool $session, string $secret = null): string {
// Check if there already an active secret, unless we are forcibly given a code.
if ($this->has_active_secret($session) && empty($secret)) {
return '';
}
// Setup a secret if not provided.
if (empty($secret)) {
$secret = random_int(100000, 999999);
}
// Now pass the code where it needs to go.
if ($session) {
$this->add_secret_to_db($secret, $expires, $this->sessionid);
} else {
$this->add_secret_to_db($secret, $expires);
}
return $secret;
}
/**
* Inserts the provided secret into the database with a given expiry duration.
*
* @param string $secret the secret to store
* @param int $expires expiry duration in seconds
* @param string $sessionid an optional sessionID to tie this record to
* @return void
*/
private function add_secret_to_db(string $secret, int $expires, string $sessionid = null): void {
global $DB, $USER;
$expirytime = time() + $expires;
$data = [
'userid' => $USER->id,
'factor' => $this->factor,
'secret' => $secret,
'timecreated' => time(),
'expiry' => $expirytime,
'revoked' => 0,
];
if (!empty($sessionid)) {
$data['sessionid'] = $sessionid;
}
$DB->insert_record('tool_mfa_secrets', $data);
}
/**
* Validates whether the provided secret is currently valid.
*
* @param string $secret the secret to check
* @param bool $keep should the secret be kept for reuse until expiry?
* @return string a secret manager state constant
*/
public function validate_secret(string $secret, bool $keep = false): string {
global $DB, $USER;
$status = $this->check_secret_against_db($secret, $this->sessionid);
if ($status !== self::NONVALID) {
if ($status === self::VALID && !$keep) {
// Cleanup DB $record.
$DB->delete_records('tool_mfa_secrets', ['userid' => $USER->id, 'factor' => $this->factor]);
}
return $status;
}
// This is always nonvalid.
return $status;
}
/**
* Checks if a given secret is valid from the Database.
*
* @param string $secret the secret to check.
* @param string $sessionid the session id to check for.
* @return string a secret manager state constant.
*/
private function check_secret_against_db(string $secret, string $sessionid): string {
global $DB, $USER;
$sql = "SELECT *
FROM {tool_mfa_secrets}
WHERE secret = :secret
AND expiry > :now
AND userid = :userid
AND factor = :factor";
$params = [
'secret' => $secret,
'now' => time(),
'userid' => $USER->id,
'factor' => $this->factor,
];
$record = $DB->get_record_sql($sql, $params);
if (!empty($record)) {
// If revoked it should always be revoked status.
if ($record->revoked) {
return self::REVOKED;
}
// Check if this is valid in only one session.
if (!empty($record->sessionid)) {
if ($record->sessionid === $sessionid) {
return self::VALID;
}
return self::NONVALID;
}
return self::VALID;
}
return self::NONVALID;
}
/**
* Revokes the provided secret code for the user.
*
* @param string $secret the secret to revoke.
* @param int $userid the userid to revoke the secret for.
* @return void
*/
public function revoke_secret(string $secret, $userid = null): void {
global $DB, $USER;
$userid = $userid ?? $USER->id;
// We do not need to worry about session vs global here.
// A factor should only ever use one.
// We know this secret is valid, so we don't need to check expiry.
$DB->set_field('tool_mfa_secrets', 'revoked', 1, ['userid' => $userid, 'factor' => $this->factor, 'secret' => $secret]);
}
/**
* Checks whether this factor currently has an active secret, and should not add another.
*
* @param bool $checksession should we only check if a current session secret is active?
* @return bool
*/
private function has_active_secret(bool $checksession = false): bool {
global $DB, $USER;
$sql = "SELECT *
FROM {tool_mfa_secrets}
WHERE expiry > :now
AND userid = :userid
AND factor = :factor
AND revoked = 0";
$params = [
'now' => time(),
'userid' => $USER->id,
'factor' => $this->factor,
];
if ($checksession) {
$sql .= ' AND sessionid = :sessionid';
$params['sessionid'] = $this->sessionid;
}
if ($DB->record_exists_sql($sql, $params)) {
return true;
}
return false;
}
/**
* Deletes any user secrets hanging around in the database.
*
* @param int $userid the userid to cleanup temp secrets for.
* @return void
*/
public function cleanup_temp_secrets($userid = null): void {
global $DB, $USER;
// Session records are autocleaned up.
// Only DB cleanup required.
$userid = $userid ?? $USER->id;
$sql = 'DELETE FROM {tool_mfa_secrets}
WHERE userid = :userid
AND factor = :factor';
$DB->execute($sql, ['userid' => $userid, 'factor' => $this->factor]);
}
}
+877
View File
@@ -0,0 +1,877 @@
<?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 tool_mfa;
use dml_exception;
use tool_mfa\plugininfo\factor;
/**
* MFA management class.
*
* @package 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 manager {
/** @var int */
const REDIRECT = 1;
/** @var int */
const NO_REDIRECT = 0;
/** @var int */
const REDIRECT_EXCEPTION = -1;
/** @var int */
const REDIR_LOOP_THRESHOLD = 5;
/**
* Displays a debug table with current factor information.
*
* @return void
*/
public static function display_debug_notification(): void {
global $OUTPUT, $PAGE;
if (!get_config('tool_mfa', 'debugmode')) {
return;
}
$html = $OUTPUT->heading(get_string('debugmode:heading', 'tool_mfa'), 3);
$table = new \html_table();
$table->head = [
get_string('weight', 'tool_mfa'),
get_string('factor', 'tool_mfa'),
get_string('setup', 'tool_mfa'),
get_string('achievedweight', 'tool_mfa'),
get_string('status'),
];
$table->attributes['class'] = 'admintable generaltable table table-bordered';
$table->colclasses = [
'text-right',
'',
'',
'text-right',
'text-center',
];
$factors = factor::get_enabled_factors();
$userfactors = factor::get_active_user_factor_types();
$runningtotal = 0;
$weighttoggle = false;
foreach ($factors as $factor) {
$namespace = 'factor_'.$factor->name;
$name = get_string('pluginname', $namespace);
// If factor is unknown, pending from here.
if ($factor->get_state() == factor::STATE_UNKNOWN) {
$weighttoggle = true;
}
// Stop adding weight if 100 achieved.
if (!$weighttoggle) {
$achieved = $factor->get_state() == factor::STATE_PASS ? $factor->get_weight() : 0;
$achieved = '+'.$achieved;
$runningtotal += $achieved;
} else {
$achieved = '';
}
// Setup.
if ($factor->has_setup()) {
$found = false;
foreach ($userfactors as $userfactor) {
if ($userfactor->name == $factor->name) {
$found = true;
}
}
$setup = $found ? get_string('yes') : get_string('no');
} else {
$setup = get_string('na', 'tool_mfa');
}
// Status.
$OUTPUT = $PAGE->get_renderer('tool_mfa');
// If toggle has been flipped, fall to default pending badge.
if ($weighttoggle) {
$state = $OUTPUT->get_state_badge('');
} else {
$state = $OUTPUT->get_state_badge($factor->get_state());
}
$table->data[] = [
$factor->get_weight(),
$name,
$setup,
$achieved,
$state,
];
// If we just hit 100, flip toggle.
if ($runningtotal >= 100) {
$weighttoggle = true;
}
}
$finalstate = self::get_status();
$table->data[] = [
'',
'',
'<b>' . get_string('overall', 'tool_mfa') . '</b>',
self::get_cumulative_weight(),
$OUTPUT->get_state_badge($finalstate),
];
$html .= \html_writer::table($table);
echo $html;
}
/**
* Returns the total weight from all factors currently enabled for user.
*
* @return int
*/
public static function get_total_weight(): int {
$totalweight = 0;
$factors = factor::get_active_user_factor_types();
foreach ($factors as $factor) {
if ($factor->get_state() == factor::STATE_PASS) {
$totalweight += $factor->get_weight();
}
}
return $totalweight;
}
/**
* Checks that provided factorid exists and belongs to current user.
*
* @param int $factorid
* @param object $user
* @return bool
* @throws \dml_exception
*/
public static function is_factorid_valid(int $factorid, object $user): bool {
global $DB;
return $DB->record_exists('tool_mfa', ['userid' => $user->id, 'id' => $factorid]);
}
/**
* Function to display to the user that they cannot login, then log them out.
*
* @return void
*/
public static function cannot_login(): void {
global $ME, $PAGE, $SESSION, $USER;
// Determine page URL without triggering warnings from $PAGE.
if (!preg_match("~(\/admin\/tool\/mfa\/auth.php)~", $ME)) {
// If URL isn't set, we need to redir to auth.php.
// This ensures URL and required info is correctly set.
// Then we arrive back here.
redirect(new \moodle_url('/admin/tool/mfa/auth.php'));
}
$renderer = $PAGE->get_renderer('tool_mfa');
echo $renderer->header();
if (get_config('tool_mfa', 'debugmode')) {
self::display_debug_notification();
}
echo $renderer->not_enough_factors();
echo $renderer->footer();
// Emit an event for failure, then logout.
$event = \tool_mfa\event\user_failed_mfa::user_failed_mfa_event($USER);
$event->trigger();
// We should set the redir flag, as this page is generated through auth.php.
$SESSION->tool_mfa_has_been_redirected = true;
die;
}
/**
* Logout user.
*
* @return void
*/
public static function mfa_logout(): void {
$authsequence = get_enabled_auth_plugins();
foreach ($authsequence as $authname) {
$authplugin = get_auth_plugin($authname);
$authplugin->logoutpage_hook();
}
require_logout();
}
/**
* Function to get the overall status of a user's authentication.
*
* @return string a STATE variable from plugininfo
*/
public static function get_status(): string {
global $SESSION;
// Check for any instant fail states.
$factors = factor::get_active_user_factor_types();
foreach ($factors as $factor) {
$factor->load_locked_state();
if ($factor->get_state() == factor::STATE_FAIL) {
return factor::STATE_FAIL;
}
}
$passcondition = ((isset($SESSION->tool_mfa_authenticated) && $SESSION->tool_mfa_authenticated) ||
self::passed_enough_factors());
// Check next factor for instant fail (fallback).
if (factor::get_next_user_login_factor()->get_state() == factor::STATE_FAIL) {
// We need to handle a special case here, where someone reached the fallback,
// If they were able to modify their state on the error page, such as passing iprange,
// We must return pass.
if ($passcondition) {
return factor::STATE_PASS;
}
return factor::STATE_FAIL;
}
// Now check for general passing state. If found, ensure that session var is set.
if ($passcondition) {
return factor::STATE_PASS;
}
// Else return neutral state.
return factor::STATE_NEUTRAL;
}
/**
* Function to check the overall status of a users authentication,
* and perform any required actions.
*
* @param bool $shouldreload whether the function should reload (used for auth.php).
* @return void
*/
public static function resolve_mfa_status(bool $shouldreload = false): void {
global $SESSION;
$state = self::get_status();
if ($state == factor::STATE_PASS) {
self::set_pass_state();
// Check if user even had to reach auth page.
if (isset($SESSION->tool_mfa_has_been_redirected)) {
if (empty($SESSION->wantsurl)) {
$wantsurl = '/';
} else {
$wantsurl = $SESSION->wantsurl;
}
unset($SESSION->wantsurl);
redirect(new \moodle_url($wantsurl));
} else {
// Don't touch anything, let user be on their way.
return;
}
} else if ($state == factor::STATE_FAIL) {
self::cannot_login();
} else if ($shouldreload) {
// Set a session variable to track whether user is where they want to be.
$SESSION->tool_mfa_has_been_redirected = true;
$authurl = new \moodle_url('/admin/tool/mfa/auth.php');
redirect($authurl);
}
}
/**
* Checks whether user has passed enough factors to be allowed in.
*
* @return bool true if user has passed enough factors.
*/
public static function passed_enough_factors(): bool {
// Check for any instant fail states.
$factors = factor::get_active_user_factor_types();
foreach ($factors as $factor) {
if ($factor->get_state() == factor::STATE_FAIL) {
self::mfa_logout();
}
}
$totalweight = self::get_cumulative_weight();
if ($totalweight >= 100) {
return true;
}
return false;
}
/**
* Sets the session variable for pass_state, if not already set.
*
* @return void
*/
public static function set_pass_state(): void {
global $DB, $SESSION, $USER;
if (!isset($SESSION->tool_mfa_authenticated)) {
$SESSION->tool_mfa_authenticated = true;
$event = \tool_mfa\event\user_passed_mfa::user_passed_mfa_event($USER);
$event->trigger();
// Allow plugins to callback as soon possible after user has passed MFA.
$hook = new \tool_mfa\hook\after_user_passed_mfa();
\core\di::get(\core\hook\manager::class)->dispatch($hook);
// Add/update record in DB for users last mfa auth.
self::update_pass_time();
// Unset session vars during mfa auth.
unset($SESSION->mfa_redir_referer);
unset($SESSION->mfa_redir_count);
// Unset user preferences during mfa auth.
unset_user_preference('mfa_sleep_duration', $USER);
try {
// Clear locked user factors, they may now reauth with anything.
@$DB->set_field('tool_mfa', 'lockcounter', 0, ['userid' => $USER->id]);
// phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
} catch (\Exception $e) {
// This occurs when upgrade.php hasn't been run. Nothing to do here.
}
// Fire post pass state factor actions.
$factors = factor::get_active_user_factor_types();
foreach ($factors as $factor) {
$factor->post_pass_state();
// Also set the states for this session to neutral if they were locked.
if ($factor->get_state() == factor::STATE_LOCKED) {
$factor->set_state(factor::STATE_NEUTRAL);
}
}
// Output notifications if any factors were reset for this user.
$enabledfactors = factor::get_enabled_factors();
foreach ($enabledfactors as $factor) {
$pref = 'tool_mfa_reset_' . $factor->name;
$factorpref = get_user_preferences($pref, false);
if ($factorpref) {
$url = new \moodle_url('/admin/tool/mfa/user_preferences.php');
$link = \html_writer::link($url, get_string('preferenceslink', 'tool_mfa'));
$data = ['factor' => $factor->get_display_name(), 'url' => $link];
\core\notification::warning(get_string('factorreset', 'tool_mfa', $data));
unset_user_preference($pref);
}
}
// Also check for a global reset.
// TODO: Delete this in a few months, the reset all preference is no longer set.
$allfactor = get_user_preferences('tool_mfa_reset_all', false);
if ($allfactor) {
$url = new \moodle_url('/admin/tool/mfa/user_preferences.php');
$link = \html_writer::link($url, get_string('preferenceslink', 'tool_mfa'));
\core\notification::warning(get_string('factorresetall', 'tool_mfa', $link));
unset_user_preference('tool_mfa_reset_all');
}
}
}
/**
* Inserts or updates user's last MFA pass time in DB.
* This should only be called from set_pass_state.
*
* @return void
*/
private static function update_pass_time(): void {
global $DB, $USER;
$exists = $DB->record_exists('tool_mfa_auth', ['userid' => $USER->id]);
if ($exists) {
$DB->set_field('tool_mfa_auth', 'lastverified', time(), ['userid' => $USER->id]);
} else {
$DB->insert_record('tool_mfa_auth', ['userid' => $USER->id, 'lastverified' => time()]);
}
}
/**
* Checks whether the user should be redirected from the provided url.
*
* @param string|\moodle_url $url
* @param bool|null $preventredirect
* @return int
*/
public static function should_require_mfa(string|\moodle_url $url, bool|null $preventredirect): int {
global $CFG, $USER, $SESSION;
// If no cookies then no session so cannot do MFA.
// Unit testing based on defines is not viable.
if (NO_MOODLE_COOKIES && !PHPUNIT_TEST) {
return self::NO_REDIRECT;
}
// Remove all params before comparison.
$url->remove_all_params();
// Checks for upgrades pending.
if (is_siteadmin()) {
// We should only allow an upgrade from the frontend to complete.
// After that is completed, only the settings shouldn't redirect.
// Everything else should be safe to enforce MFA.
if (moodle_needs_upgrading()) {
return self::NO_REDIRECT;
}
// An upgrade isn't complete if there are settings that must be saved.
$upgradesettings = new \moodle_url('/admin/upgradesettings.php');
if ($url->compare($upgradesettings, URL_MATCH_BASE)) {
return self::NO_REDIRECT;
}
}
// Dont redirect logo images from pluginfile.php (for example: logo in header).
$logourl = new \moodle_url('/pluginfile.php/1/core_admin/logocompact/');
if ($url->compare($logourl)) {
return self::NO_REDIRECT;
}
// Admin not setup.
if (!empty($CFG->adminsetuppending)) {
return self::NO_REDIRECT;
}
// Initial installation.
// We get this for free from get_plugins_with_function.
// Upgrade check.
// We get this for free from get_plugins_with_function.
// Honor prevent_redirect.
if ($preventredirect) {
return self::NO_REDIRECT;
}
// User not properly setup.
if (user_not_fully_set_up($USER)) {
return self::NO_REDIRECT;
}
// Enrolment.
$enrol = new \moodle_url('/enrol/index.php');
if ($enrol->compare($url, URL_MATCH_BASE)) {
return self::NO_REDIRECT;
}
// Guest access.
if (isguestuser()) {
return self::NO_REDIRECT;
}
// Forced password changes.
if (get_user_preferences('auth_forcepasswordchange')) {
return self::NO_REDIRECT;
}
// Login as.
if (\core\session\manager::is_loggedinas()) {
return self::NO_REDIRECT;
}
// Site policy.
if (isset($USER->policyagreed) && !$USER->policyagreed) {
$manager = new \core_privacy\local\sitepolicy\manager();
$policyurl = $manager->get_redirect_url(false);
if (!empty($policyurl) && $url->compare($policyurl, URL_MATCH_BASE)) {
return self::NO_REDIRECT;
}
}
// WS/AJAX check.
if (WS_SERVER || AJAX_SCRIPT) {
if (isset($SESSION->mfa_pending) && !empty($SESSION->mfa_pending)) {
// Allow AJAX and WS, but never from auth.php.
return self::NO_REDIRECT;
}
return self::REDIRECT_EXCEPTION;
}
// Check factor defined safe urls.
$factorurls = self::get_no_redirect_urls();
foreach ($factorurls as $factorurl) {
if ($factorurl->compare($url)) {
return self::NO_REDIRECT;
}
}
// Circular checks.
$authurl = new \moodle_url('/admin/tool/mfa/auth.php');
$authlocal = $authurl->out_as_local_url();
if (isset($SESSION->mfa_redir_referer)
&& $SESSION->mfa_redir_referer != $authlocal) {
if ($SESSION->mfa_redir_referer == get_local_referer(true)) {
// Possible redirect loop.
if (!isset($SESSION->mfa_redir_count)) {
$SESSION->mfa_redir_count = 1;
} else {
$SESSION->mfa_redir_count++;
}
if ($SESSION->mfa_redir_count > self::REDIR_LOOP_THRESHOLD) {
return self::REDIRECT_EXCEPTION;
}
} else {
// If not a match, reset counter.
$SESSION->mfa_redir_count = 0;
}
}
// Set referer after checks.
$SESSION->mfa_redir_referer = get_local_referer(true);
// Don't redirect if already on auth.php.
if ($url->compare($authurl, URL_MATCH_BASE)) {
return self::NO_REDIRECT;
}
return self::REDIRECT;
}
/**
* Clears the redirect counter for infinite redirect loops. Called from auth.php when a valid load is resolved.
*
* @return void
*/
public static function clear_redirect_counter(): void {
global $SESSION;
unset($SESSION->mfa_redir_referer);
unset($SESSION->mfa_redir_count);
}
/**
* Gets all defined factor urls that should not redirect.
*
* @return array
*/
public static function get_no_redirect_urls(): array {
$factors = factor::get_factors();
$urls = [
new \moodle_url('/login/logout.php'),
new \moodle_url('/admin/tool/mfa/guide.php'),
];
foreach ($factors as $factor) {
$urls = array_merge($urls, $factor->get_no_redirect_urls());
}
// Allow forced redirection exclusions.
if ($exclusions = get_config('tool_mfa', 'redir_exclusions')) {
foreach (explode("\n", $exclusions) as $exclusion) {
$urls[] = new \moodle_url($exclusion);
}
}
return $urls;
}
/**
* Sleeps for an increasing period of time.
*
* @return void
*/
public static function sleep_timer(): void {
global $USER;
$duration = get_user_preferences('mfa_sleep_duration', null, $USER);
if (!empty($duration)) {
// Double current time.
$duration *= 2;
$duration = min(2, $duration);
} else {
// No duration set.
$duration = 0.05;
}
set_user_preference('mfa_sleep_duration', $duration, $USER);
sleep((int)$duration);
}
/**
* If MFA Plugin is ready check tool_mfa_authenticated USER property and
* start MFA authentication if it's not set or false.
*
* @param mixed $courseorid
* @param mixed $autologinguest
* @param mixed $cm
* @param mixed $setwantsurltome
* @param mixed $preventredirect
* @return void
*/
public static function require_auth($courseorid = null, $autologinguest = null, $cm = null,
$setwantsurltome = null, $preventredirect = null): void {
global $PAGE, $SESSION, $FULLME;
// Guest user should never interact with MFA,
// And $SESSION->tool_mfa_authenticated should never be set in a guest session.
if (isguestuser()) {
return;
}
if (!self::is_ready()) {
// Set session var so if MFA becomes ready, you dont get locked from session.
$SESSION->tool_mfa_authenticated = true;
return;
}
if (empty($SESSION->tool_mfa_authenticated) || !$SESSION->tool_mfa_authenticated) {
if ($PAGE->has_set_url()) {
$cleanurl = $PAGE->url;
} else {
// Use $FULLME instead.
$cleanurl = new \moodle_url($FULLME);
}
$authurl = new \moodle_url('/admin/tool/mfa/auth.php');
$redir = self::should_require_mfa($cleanurl, $preventredirect);
if ($redir == self::NO_REDIRECT && !$cleanurl->compare($authurl, URL_MATCH_BASE)) {
// A non-MFA page that should take precedence.
// This check is for any pages, such as site policy, that must occur before MFA.
// This check allows AJAX and WS requests to fire on these pages without throwing an exception.
$SESSION->mfa_pending = true;
}
if ($redir == self::REDIRECT) {
if (empty($SESSION->wantsurl)) {
!empty($setwantsurltome)
? $SESSION->wantsurl = qualified_me()
: $SESSION->wantsurl = new \moodle_url('/');
$SESSION->tool_mfa_setwantsurl = true;
}
// Remove pending status.
// We must now auth with MFA, now that pending statuses are resolved.
unset($SESSION->mfa_pending);
// Call resolve_status to instantly pass if no redirect is required.
self::resolve_mfa_status(true);
} else if ($redir == self::REDIRECT_EXCEPTION) {
if (!empty($SESSION->mfa_redir_referer)) {
throw new \moodle_exception('redirecterrordetected', 'tool_mfa',
$SESSION->mfa_redir_referer, $SESSION->mfa_redir_referer);
} else {
throw new \moodle_exception('redirecterrordetected', 'error');
}
}
}
}
/**
* Sets config variable for given factor.
*
* @param array $data
* @param string $factor
*
* @return bool true or exception
* @throws dml_exception
*/
public static function set_factor_config(array $data, string $factor): bool|dml_exception {
$factorconf = get_config($factor);
foreach ($data as $key => $newvalue) {
if (empty($factorconf->$key)) {
add_to_config_log($key, null, $newvalue, $factor);
set_config($key, $newvalue, $factor);
} else if ($factorconf->$key != $newvalue) {
add_to_config_log($key, $factorconf->$key, $newvalue, $factor);
set_config($key, $newvalue, $factor);
}
}
return true;
}
/**
* Checks if MFA Plugin is enabled and has enabled factor.
* If plugin is disabled or there is no enabled factors,
* it means there is nothing to do from user side.
* Thus, login flow shouldn't be extended with MFA.
*
* @return bool
* @throws \dml_exception
*/
public static function is_ready(): bool {
global $CFG, $USER;
if (!empty($CFG->upgraderunning)) {
return false;
}
$pluginenabled = get_config('tool_mfa', 'enabled');
if (empty($pluginenabled)) {
return false;
}
// Check if user can interact with MFA.
$usercontext = \context_user::instance($USER->id);
if (!has_capability('tool/mfa:mfaaccess', $usercontext)) {
return false;
}
$enabledfactors = factor::get_enabled_factors();
if (count($enabledfactors) == 0) {
return false;
}
return true;
}
/**
* Performs factor actions for given factor.
* Change factor order and enable/disable.
*
* @param string $factorname
* @param string $action
*
* @return void
* @throws dml_exception
*/
public static function do_factor_action(string $factorname, string $action): void {
$order = explode(',', get_config('tool_mfa', 'factor_order'));
$key = array_search($factorname, $order);
switch ($action) {
case 'up':
if ($key >= 1) {
$fsave = $order[$key];
$order[$key] = $order[$key - 1];
$order[$key - 1] = $fsave;
}
break;
case 'down':
if ($key < (count($order) - 1)) {
$fsave = $order[$key];
$order[$key] = $order[$key + 1];
$order[$key + 1] = $fsave;
}
break;
case 'enable':
if (!$key) {
$order[] = $factorname;
}
break;
case 'disable':
if ($key) {
unset($order[$key]);
}
break;
default:
break;
}
self::set_factor_config(['factor_order' => implode(',', $order)], 'tool_mfa');
}
/**
* Checks if a factor that can make a user pass can be setup.
* It checks if a user will always pass regardless,
* then checks if there are factors that can be setup to let a user pass.
*
* @return bool
*/
public static function possible_factor_setup(): bool {
global $USER;
// Get all active factors.
$factors = factor::get_enabled_factors();
// Check if there are enough factors that a user can ONLY pass, if so, don't display the menu.
$weight = 0;
foreach ($factors as $factor) {
$states = $factor->possible_states($USER);
if (count($states) == 1 && reset($states) == factor::STATE_PASS) {
$weight += $factor->get_weight();
if ($weight >= 100) {
return false;
}
}
}
// Now if there is a factor that can be setup, that may return a pass state for the user, display menu.
foreach ($factors as $factor) {
if ($factor->has_setup()) {
if (in_array(factor::STATE_PASS, $factor->possible_states($USER))) {
return true;
}
}
}
return false;
}
/**
* Gets current user weight, up until first unknown factor.
*
* @return int $totalweight Total weight of all factors.
*/
public static function get_cumulative_weight(): int {
$factors = factor::get_active_user_factor_types();
// Factor order is important here, so sort the factors by state.
$sortedfactors = factor::sort_factors_by_state($factors, factor::STATE_PASS);
$totalweight = 0;
foreach ($sortedfactors as $factor) {
if ($factor->get_state() == factor::STATE_PASS) {
$totalweight += $factor->get_weight();
// If over 100, break. Don't care about >100.
if ($totalweight >= 100) {
break;
}
} else if ($factor->get_state() == factor::STATE_UNKNOWN) {
break;
}
}
return $totalweight;
}
/**
* Checks whether the factor was actually used in the login process.
*
* @param string $factorname the name of the factor.
* @return bool true if factor is pending.
*/
public static function check_factor_pending(string $factorname): bool {
$factors = factor::get_active_user_factor_types();
// Setup vars.
$pending = [];
$totalweight = 0;
$weighttoggle = false;
foreach ($factors as $factor) {
// If toggle is reached, put in pending and continue.
if ($weighttoggle) {
$pending[] = $factor->name;
continue;
}
if ($factor->get_state() == factor::STATE_PASS) {
$totalweight += $factor->get_weight();
if ($totalweight >= 100) {
$weighttoggle = true;
}
}
}
// Check whether factor falls into pending category.
return in_array($factorname, $pending);
}
}
+732
View File
@@ -0,0 +1,732 @@
<?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 tool_mfa\output;
use core\context\system;
use tool_mfa\local\factor\object_factor;
use tool_mfa\local\form\login_form;
use \html_writer;
use tool_mfa\plugininfo\factor;
/**
* MFA renderer.
*
* @package tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class renderer extends \plugin_renderer_base {
/**
* Returns the state of the factor as a badge.
*
* @param string $state
* @return string
*/
public function get_state_badge(string $state): string {
switch ($state) {
case factor::STATE_PASS:
return html_writer::tag('span', get_string('state:pass', 'tool_mfa'), ['class' => 'badge bg-success text-white']);
case factor::STATE_FAIL:
return html_writer::tag('span', get_string('state:fail', 'tool_mfa'), ['class' => 'badge bg-danger text-white']);
case factor::STATE_NEUTRAL:
return html_writer::tag('span', get_string('state:neutral', 'tool_mfa'),
['class' => 'badge bg-warning text-dark']);
case factor::STATE_UNKNOWN:
return html_writer::tag('span', get_string('state:unknown', 'tool_mfa'),
['class' => 'badge bg-secondary text-dark']);
case factor::STATE_LOCKED:
return html_writer::tag('span', get_string('state:locked', 'tool_mfa'), ['class' => 'badge bg-danger text-white']);
default:
return html_writer::tag('span', get_string('pending', 'tool_mfa'), ['class' => 'badge bg-secondary text-dark']);
}
}
/**
* Returns a list of factors which a user can add.
*
* @return string
*/
public function available_factors(): string {
global $USER;
$factors = factor::get_enabled_factors();
$data = [];
foreach ($factors as $factor) {
// Allow all factors with setup and button.
// Make an exception for email factor as this is currently set up by admins only and required on this list.
if ((!$factor->has_setup() || !$factor->show_setup_buttons()) && !$factor instanceof \factor_email\factor) {
continue;
}
$userfactors = $factor->get_active_user_factors($USER);
$active = !empty($userfactors) ?? false;
$button = null;
$icon = $factor->get_icon();
$params = [
'action' => 'setup',
'factor' => $factor->name,
];
if (!$active) {
// Not active yet and requires set up.
$info = $factor->get_info();
if ($factor->show_setup_buttons()) {
$params['action'] = 'setup';
$button = new \single_button(
url: new \moodle_url('action.php', $params),
label: $factor->get_setup_string(),
method: 'post',
type: \single_button::BUTTON_PRIMARY,
attributes: [
'aria-label' => get_string('setupfactor', 'factor_' . $factor->name),
],
);
$button = $button->export_for_template($this->output);
}
} else {
// Active and can be managed.
$factorid = reset($userfactors)->id;
$info = $factor->get_manage_info($factorid);
if ($factor->show_setup_buttons()) {
$params['action'] = 'manage';
$button = new \single_button(
url: new \moodle_url('action.php', $params),
label: $factor->get_manage_string(),
method: 'post',
type: \single_button::BUTTON_PRIMARY,
attributes: [
'aria-label' => get_string('managefactor', 'factor_' . $factor->name),
],
);
$button = $button->export_for_template($this->output);
}
}
// Prepare data for template.
$data['factors'][] = [
'active' => $active,
'label' => $factor->get_display_name(),
'name' => $factor->name,
'info' => $info,
'icon' => $icon,
'button' => $button,
];
}
return $this->render_from_template('tool_mfa/mfa_selector', $data);
}
/**
* Returns the html section for factor setup
*
* @param object $factor object of the factor class
* @return string
* @deprecated since Moodle 4.4
* @todo Final deprecation in Moodle 4.8 MDL-80995
*/
public function setup_factor(object $factor): string {
debugging('The method setup_factor() has been deprecated. The HTML derived from this method is no longer needed.
Similar HTML is now achieved as part of available_factors().', DEBUG_DEVELOPER);
$html = '';
$html .= html_writer::start_tag('div', ['class' => 'card']);
$html .= html_writer::tag('h4', $factor->get_display_name(), ['class' => 'card-header']);
$html .= html_writer::start_tag('div', ['class' => 'card-body']);
$html .= $factor->get_info();
$setupparams = ['action' => 'setup', 'factor' => $factor->name, 'sesskey' => sesskey()];
$setupurl = new \moodle_url('action.php', $setupparams);
$html .= $this->output->single_button($setupurl, $factor->get_setup_string());
$html .= html_writer::end_tag('div');
$html .= html_writer::end_tag('div');
$html .= '<br>';
return $html;
}
/**
* Show a table displaying a users active factors.
*
* @param string|null $filterfactor The factor name to filter on.
* @return string $html
* @throws \coding_exception
*/
public function active_factors(string $filterfactor = null): string {
global $USER, $CFG;
require_once($CFG->dirroot . '/iplookup/lib.php');
$html = '';
$headers = get_strings([
'devicename',
'added',
'lastused',
'replace',
'remove',
], 'tool_mfa');
$table = new \html_table();
$table->id = 'active_factors';
$table->attributes['class'] = 'generaltable table table-bordered';
$table->head = [
$headers->devicename,
$headers->added,
$headers->lastused,
$headers->replace,
$headers->remove,
];
$table->colclasses = [
'text-left',
'text-left',
'text-left',
'text-center',
'text-center',
];
$table->data = [];
$factors = factor::get_enabled_factors();
$hasmorethanone = factor::user_has_more_than_one_active_factors();
foreach ($factors as $factor) {
// Filter results to match the specified factor.
if (!empty($filterfactor) && $factor->name !== $filterfactor) {
continue;
}
$userfactors = $factor->get_active_user_factors($USER);
if (!$factor->has_setup()) {
continue;
}
foreach ($userfactors as $userfactor) {
// Revoke option.
if ($factor->has_revoke() && $hasmorethanone) {
$content = $headers->remove;
$attributes = [
'data-action' => 'revoke',
'data-factor' => $factor->name,
'data-factorid' => $userfactor->id,
'data-factorname' => $factor->get_display_name(),
'data-devicename' => $userfactor->label,
'aria-label' => get_string('revokefactor', 'tool_mfa'),
'class' => 'btn btn-primary mfa-action-button',
];
$revokebutton = \html_writer::tag('button', $content, $attributes);
} else {
$revokebutton = get_string('statusna');
}
// Replace option.
if ($factor->has_replace()) {
$content = $headers->replace;
$attributes = [
'data-action' => 'replace',
'data-factor' => $factor->name,
'data-factorid' => $userfactor->id,
'data-factorname' => $factor->get_display_name(),
'data-devicename' => $userfactor->label,
'aria-label' => get_string('replacefactor', 'tool_mfa'),
'class' => 'btn btn-primary mfa-action-button',
];
$replacebutton = \html_writer::tag('button', $content, $attributes);
} else {
$replacebutton = get_string('statusna');
}
$timecreated = $userfactor->timecreated == '-' ? '-'
: userdate($userfactor->timecreated, get_string('strftimedatetime'));
$lastverified = $userfactor->lastverified;
if ($lastverified == 0) {
$lastverified = '-';
} else if ($lastverified != '-') {
$lastverified = userdate($userfactor->lastverified, get_string('strftimedatetime'));
$lastverified .= '<br>';
$lastverified .= get_string('ago', 'core_message', format_time(time() - $userfactor->lastverified));
}
$row = new \html_table_row([
$userfactor->label,
$timecreated,
$lastverified,
$replacebutton,
$revokebutton,
]);
$table->data[] = $row;
}
}
// If table has no data, don't output.
if (count($table->data) == 0) {
return '';
}
$html .= \html_writer::table($table);
$html .= '<br>';
return $html;
}
/**
* Generates notification text for display when user cannot login.
*
* @return string $notification
*/
public function not_enough_factors(): string {
global $CFG, $SITE;
$notification = \html_writer::tag('h4', get_string('error:notenoughfactors', 'tool_mfa'));
$notification .= \html_writer::tag('p', get_string('error:reauth', 'tool_mfa'));
// Support link.
$supportemail = $CFG->supportemail;
if (!empty($supportemail)) {
$subject = get_string('email:subject', 'tool_mfa',
format_string($SITE->fullname, true, ['context' => system::instance()]));
$maillink = \html_writer::link("mailto:$supportemail?Subject=$subject", $supportemail);
$notification .= get_string('error:support', 'tool_mfa');
$notification .= \html_writer::tag('p', $maillink);
}
// Support page link.
$supportpage = $CFG->supportpage;
if (!empty($supportpage)) {
$linktext = \html_writer::link($supportpage, $supportpage);
$notification .= $linktext;
}
$return = $this->output->notification($notification, 'notifyerror', false);
// Logout button.
$url = new \moodle_url('/admin/tool/mfa/auth.php', ['logout' => 1]);
$btn = new \single_button($url, get_string('logout'), 'post', \single_button::BUTTON_PRIMARY);
$return .= $this->render($btn);
$return .= $this->get_support_link();
return $return;
}
/**
* Displays a table of all factors in use currently.
*
* @param int $lookback the period to view.
* @return string the HTML for the table
*/
public function factors_in_use_table(int $lookback): string {
global $DB;
$factors = factor::get_factors();
// Setup 2 arrays, one with internal names, one pretty.
$columns = [''];
$displaynames = $columns;
$colclasses = ['center', 'center', 'center', 'center', 'center'];
// Force the first 4 columns to custom data.
$displaynames[] = get_string('totalusers', 'tool_mfa');
$displaynames[] = get_string('usersauthedinperiod', 'tool_mfa');
$displaynames[] = get_string('nonauthusers', 'tool_mfa');
$displaynames[] = get_string('nologinusers', 'tool_mfa');
foreach ($factors as $factor) {
$columns[] = $factor->name;
$displaynames[] = get_string('pluginname', 'factor_'.$factor->name);
$colclasses[] = 'right';
}
// Add total column to the end.
$displaynames[] = get_string('total');
$colclasses[] = 'center';
$table = new \html_table();
$table->head = $displaynames;
$table->align = $colclasses;
$table->attributes['class'] = 'generaltable table table-bordered w-auto';
$table->attributes['style'] = 'width: auto; min-width: 50%; margin-bottom: 0;';
// Manually handle Total users and MFA users.
$alluserssql = "SELECT auth,
COUNT(id)
FROM {user}
WHERE deleted = 0
AND suspended = 0
GROUP BY auth";
$allusersinfo = $DB->get_records_sql($alluserssql, []);
$noncompletesql = "SELECT u.auth, COUNT(u.id)
FROM {user} u
LEFT JOIN {tool_mfa_auth} mfaa ON u.id = mfaa.userid
WHERE u.lastlogin >= ?
AND (mfaa.lastverified < ?
OR mfaa.lastverified IS NULL)
GROUP BY u.auth";
$noncompleteinfo = $DB->get_records_sql($noncompletesql, [$lookback, $lookback]);
$nologinsql = "SELECT auth, COUNT(id)
FROM {user}
WHERE deleted = 0
AND suspended = 0
AND lastlogin < ?
GROUP BY auth";
$nologininfo = $DB->get_records_sql($nologinsql, [$lookback]);
$mfauserssql = "SELECT auth,
COUNT(DISTINCT tm.userid)
FROM {tool_mfa} tm
JOIN {user} u ON u.id = tm.userid
WHERE tm.lastverified >= ?
AND u.deleted = 0
AND u.suspended = 0
GROUP BY u.auth";
$mfausersinfo = $DB->get_records_sql($mfauserssql, [$lookback]);
$factorsusedsql = "SELECT CONCAT(u.auth, '_', tm.factor) as id,
COUNT(*)
FROM {tool_mfa} tm
JOIN {user} u ON u.id = tm.userid
WHERE tm.lastverified >= ?
AND u.deleted = 0
AND u.suspended = 0
AND (tm.revoked = 0 OR (tm.revoked = 1 AND tm.timemodified > ?))
GROUP BY CONCAT(u.auth, '_', tm.factor)";
$factorsusedinfo = $DB->get_records_sql($factorsusedsql, [$lookback, $lookback]);
// Auth rows.
$authtypes = get_enabled_auth_plugins(true);
foreach ($authtypes as $authtype) {
$row = [];
$row[] = \html_writer::tag('b', $authtype);
// Setup the overall totals columns.
$row[] = $allusersinfo[$authtype]->count ?? '-';
$row[] = $mfausersinfo[$authtype]->count ?? '-';
$row[] = $noncompleteinfo[$authtype]->count ?? '-';
$row[] = $nologininfo[$authtype]->count ?? '-';
// Create a running counter for the total.
$authtotal = 0;
// Now for each factor add the count from the factor query, and increment the running total.
foreach ($columns as $column) {
if (!empty($column)) {
// Get the information from the data key.
$key = $authtype . '_' . $column;
$count = $factorsusedinfo[$key]->count ?? 0;
$authtotal += $count;
$row[] = $count ? format_float($count, 0) : '-';
}
}
// Append the total of all factors to final column.
$row[] = $authtotal ? format_float($authtotal, 0) : '-';
$table->data[] = $row;
}
// Total row.
$totals = [0 => html_writer::tag('b', get_string('total'))];
for ($colcounter = 1; $colcounter < count($row); $colcounter++) {
$column = array_column($table->data, $colcounter);
// Transform string to int forcibly, remove -.
$column = array_map(function ($element) {
return $element === '-' ? 0 : (int) $element;
}, $column);
$columnsum = array_sum($column);
$colvalue = $columnsum === 0 ? '-' : $columnsum;
$totals[$colcounter] = $colvalue;
}
$table->data[] = $totals;
// Wrap in a div to cleanly scroll.
return \html_writer::div(\html_writer::table($table), '', ['style' => 'overflow:auto;']);
}
/**
* Displays a table of all factors in use currently.
*
* @return string the HTML for the table
*/
public function factors_locked_table(): string {
global $DB;
$factors = factor::get_factors();
$table = new \html_table();
$table->attributes['class'] = 'generaltable table table-bordered w-auto';
$table->attributes['style'] = 'width: auto; min-width: 50%';
$table->head = [
'factor' => get_string('factor', 'tool_mfa'),
'active' => get_string('active'),
'locked' => get_string('state:locked', 'tool_mfa'),
'actions' => get_string('actions'),
];
$table->align = [
'left',
'left',
'right',
'right',
];
$table->data = [];
$locklevel = (int) get_config('tool_mfa', 'lockout');
foreach ($factors as $factor) {
$sql = "SELECT COUNT(DISTINCT(userid))
FROM {tool_mfa}
WHERE factor = ?
AND lockcounter >= ?
AND revoked = 0";
$lockedusers = $DB->count_records_sql($sql, [$factor->name, $locklevel]);
$enabled = $factor->is_enabled() ? \html_writer::tag('b', get_string('yes')) : get_string('no');
$actions = \html_writer::link( new \moodle_url($this->page->url,
['reset' => $factor->name, 'sesskey' => sesskey()]), get_string('performbulk', 'tool_mfa'));
$lockedusers = \html_writer::link(new \moodle_url($this->page->url, ['view' => $factor->name]), $lockedusers);
$table->data[] = [
$factor->get_display_name(),
$enabled,
$lockedusers,
$actions,
];
}
return \html_writer::table($table);
}
/**
* Displays a table of all users with a locked instance of the given factor.
*
* @param object_factor $factor the factor class
* @return string the HTML for the table
*/
public function factor_locked_users_table(object_factor $factor): string {
global $DB;
$table = new \html_table();
$table->attributes['class'] = 'generaltable table table-bordered w-auto';
$table->attributes['style'] = 'width: auto; min-width: 50%';
$table->head = [
'userid' => get_string('userid', 'grades'),
'fullname' => get_string('fullname'),
'factorip' => get_string('ipatcreation', 'tool_mfa'),
'lastip' => get_string('lastip'),
'modified' => get_string('modified'),
'actions' => get_string('actions'),
];
$table->align = [
'left',
'left',
'left',
'left',
'left',
'right',
];
$table->data = [];
$locklevel = (int) get_config('tool_mfa', 'lockout');
$sql = "SELECT mfa.id as mfaid, u.*, mfa.createdfromip, mfa.timemodified
FROM {tool_mfa} mfa
JOIN {user} u ON mfa.userid = u.id
WHERE factor = ?
AND lockcounter >= ?
AND revoked = 0";
$records = $DB->get_records_sql($sql, [$factor->name, $locklevel]);
foreach ($records as $record) {
// Construct profile link.
$proflink = \html_writer::link(new \moodle_url('/user/profile.php',
['id' => $record->id]), fullname($record));
// IP link.
$creatediplink = \html_writer::link(new \moodle_url('/iplookup/index.php',
['ip' => $record->createdfromip]), $record->createdfromip);
$lastiplink = \html_writer::link(new \moodle_url('/iplookup/index.php',
['ip' => $record->lastip]), $record->lastip);
// Deep link to logs.
$logicon = $this->pix_icon('i/report', get_string('userlogs', 'tool_mfa'));
$actions = \html_writer::link(new \moodle_url('/report/log/index.php', [
'id' => 1, // Site.
'user' => $record->id,
]), $logicon);
$action = new \confirm_action(get_string('resetfactorconfirm', 'tool_mfa', fullname($record)));
$actions .= $this->action_link(
new \moodle_url($this->page->url, ['reset' => $factor->name, 'id' => $record->id, 'sesskey' => sesskey()]),
$this->pix_icon('t/delete', get_string('resetconfirm', 'tool_mfa')),
$action
);
$table->data[] = [
$record->id,
$proflink,
$creatediplink,
$lastiplink,
userdate($record->timemodified, get_string('strftimedatetime', 'langconfig')),
$actions,
];
}
return \html_writer::table($table);
}
/**
* Returns a rendered support link.
* If the MFA guidance page is enabled, this is returned.
* Otherwise, the site support link is returned.
* If neither support link is configured, an empty string is returned.
*
* @return string
*/
public function get_support_link(): string {
// Try the guidance page link first.
if (get_config('tool_mfa', 'guidance')) {
return $this->render_from_template('tool_mfa/guide_link', []);
} else {
return $this->output->supportemail([], true);
}
}
/**
* Renders an mform element from a template
*
* In certain situations, includes a script element which adds autosubmission behaviour.
*
* @param mixed $element element
* @param bool $required if input is required field
* @param bool $advanced if input is an advanced field
* @param string|null $error error message to display
* @param bool $ingroup True if this element is rendered as part of a group
* @return mixed string|bool
*/
public function mform_element(mixed $element, bool $required,
bool $advanced, string|null $error, bool $ingroup): string|bool {
$script = null;
if ($element instanceof \tool_mfa\local\form\verification_field) {
if ($this->page->pagelayout === 'secure') {
$script = $element->secure_js();
}
}
$result = parent::mform_element($element, $required, $advanced, $error, $ingroup);
if (!empty($script) && $result !== false) {
$result .= $script;
}
return $result;
}
/**
* Renders the verification form.
*
* @param object_factor $factor The factor to render the form for.
* @param login_form $form The login form object.
* @return string
* @throws \coding_exception
* @throws \dml_exception
* @throws \moodle_exception
*/
public function verification_form(object_factor $factor, login_form $form): string {
$allloginfactors = factor::get_all_user_login_factors();
$additionalfactors = [];
$disabledfactors = [];
$displaycount = 0;
$disablefactor = false;
foreach ($allloginfactors as $loginfactor) {
if ($loginfactor->name != $factor->name) {
$additionalfactor = [
'name' => $loginfactor->name,
'icon' => $loginfactor->get_icon(),
'loginoption' => get_string('loginoption', 'factor_' . $loginfactor->name),
];
// We mark the factor as disabled if it is locked.
// We store the disabled factors in a separate array so that they can be displayed at the bottom of the template.
if ($loginfactor->get_state() == factor::STATE_LOCKED) {
$additionalfactor['loginoption'] = get_string('locked', 'tool_mfa', $additionalfactor['loginoption']);
$additionalfactor['disable'] = true;
$disabledfactors[] = $additionalfactor;
} else {
$additionalfactors[] = $additionalfactor;
}
$displaycount++;
}
}
// We merge the additional factors placing the disabled ones last.
$alladitionalfactors = array_merge($additionalfactors, $disabledfactors);
$hasadditionalfactors = $displaycount > 0;
$authurl = new \moodle_url('/admin/tool/mfa/auth.php');
// Set the form to better display vertically.
$form->set_display_vertical();
// Check if we need to display a remaining attempts message.
$remattempts = $factor->get_remaining_attempts();
$verificationerror = $form->get_element_error('verificationcode');
if ($remattempts < get_config('tool_mfa', 'lockout') && !empty($verificationerror)) {
// Update the validation error for the code form field to include the remaining attempts.
$remattemptsstr = get_string('lockoutnotification', 'tool_mfa', $factor->get_remaining_attempts());
$updatederror = $verificationerror . '&nbsp;' . $remattemptsstr;
$form->set_element_error('verificationcode', $updatederror);
}
// If all attempts for this factor have been used, disable the form.
// This forces the user to choose another factor or cancel their login.
if ($remattempts <= 0) {
$disablefactor = true;
$form->freeze('verificationcode');
// Handle the trust factor if present.
if ($form->element_exists('factor_token_trust')) {
$form->freeze('factor_token_trust');
}
}
$context = [
'logintitle' => get_string('logintitle', 'factor_'.$factor->name),
'logindesc' => $factor->get_login_desc(),
'factoricon' => $factor->get_icon(),
'form' => $form->render(),
'hasadditionalfactors' => $hasadditionalfactors,
'additionalfactors' => $alladitionalfactors,
'authurl' => $authurl->out(),
'sesskey' => sesskey(),
'supportlink' => $this->get_support_link(),
'disablefactor' => $disablefactor
];
return $this->render_from_template('tool_mfa/verification_form', $context);
}
}
@@ -0,0 +1,386 @@
<?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 tool_mfa\plugininfo;
use moodle_url;
use stdClass;
/**
* Subplugin info class.
*
* @package tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor extends \core\plugininfo\base {
/** @var string */
const STATE_UNKNOWN = 'unknown';
/** @var string */
const STATE_PASS = 'pass';
/** @var string */
const STATE_FAIL = 'fail';
/** @var string */
const STATE_NEUTRAL = 'neutral';
/** @var string Locked state is identical to neutral, but can't be overridden */
const STATE_LOCKED = 'locked';
/**
* Finds all MFA factors.
*
* @return array of factor objects.
*/
public static function get_factors(): array {
$return = [];
$factors = \core_plugin_manager::instance()->get_plugins_of_type('factor');
foreach ($factors as $factor) {
$classname = '\\factor_'.$factor->name.'\\factor';
if (class_exists($classname)) {
$return[] = new $classname($factor->name);
}
}
return self::sort_factors_by_order($return);
}
/**
* Sorts factors by configured order.
*
* @param array $unsorted of factor objects
* @return array of factor objects
* @throws \dml_exception
*/
public static function sort_factors_by_order(array $unsorted): array {
$sorted = [];
$orderarray = explode(',', get_config('tool_mfa', 'factor_order'));
foreach ($orderarray as $order => $factorname) {
foreach ($unsorted as $key => $factor) {
if ($factor->name == $factorname) {
$sorted[] = $factor;
unset($unsorted[$key]);
}
}
}
$sorted = array_merge($sorted, $unsorted);
return $sorted;
}
/**
* Finds factor by its name.
*
* @param string $name
*
* @return mixed factor object or false if factor not found.
*/
public static function get_factor(string $name): object|bool {
$factors = \core_plugin_manager::instance()->get_plugins_of_type('factor');
foreach ($factors as $factor) {
if ($name == $factor->name) {
$classname = '\\factor_'.$factor->name.'\\factor';
if (class_exists($classname)) {
return new $classname($factor->name);
}
}
}
return false;
}
/**
* Finds all enabled factors.
*
* @return array of factor objects
*/
public static function get_enabled_factors(): array {
$return = [];
$factors = self::get_factors();
foreach ($factors as $factor) {
if ($factor->is_enabled()) {
$return[] = $factor;
}
}
return $return;
}
/**
* Finds active factors for a user.
* If user is not specified, current user is used.
*
* @param mixed $user user object or null.
* @return array of factor objects.
*/
public static function get_active_user_factor_types(mixed $user = null): array {
global $USER;
if (is_null($user)) {
$user = $USER;
}
$return = [];
$factors = self::get_enabled_factors();
foreach ($factors as $factor) {
$userfactors = $factor->get_active_user_factors($user);
if (count($userfactors) > 0) {
$return[] = $factor;
}
}
return $return;
}
/**
* Returns next factor to authenticate user.
* Only returns factors that require user input.
*
* @return mixed factor object the next factor to be authenticated or false.
*/
public static function get_next_user_login_factor(): mixed {
$factors = self::get_active_user_factor_types();
foreach ($factors as $factor) {
if (!$factor->has_input()) {
continue;
}
if ($factor->get_state() == self::STATE_UNKNOWN) {
return $factor;
}
}
return new \tool_mfa\local\factor\fallback();
}
/**
* Returns all factors that require user input.
*
* @return array of factor objects.
*/
public static function get_all_user_login_factors(): array {
$factors = self::get_active_user_factor_types();
$loginfactors = [];
foreach ($factors as $factor) {
if ($factor->has_input()) {
$loginfactors[] = $factor;
}
}
return $loginfactors;
}
/**
* Returns the list of available actions with factor.
*
* @return array
*/
public static function get_factor_actions(): array {
$actions = [];
$actions[] = 'setup';
$actions[] = 'revoke';
$actions[] = 'enable';
$actions[] = 'revoke';
$actions[] = 'disable';
$actions[] = 'up';
$actions[] = 'down';
$actions[] = 'manage';
$actions[] = 'replace';
return $actions;
}
/**
* Returns the information about plugin availability
*
* True means that the plugin is enabled. False means that the plugin is
* disabled. Null means that the information is not available, or the
* plugin does not support configurable availability or the availability
* can not be changed.
*
* @return null|bool
*/
public function is_enabled(): null|bool {
if (!$this->rootdir) {
// Plugin missing.
return false;
}
$factor = $this->get_factor($this->name);
if ($factor) {
return $factor->is_enabled();
}
return false;
}
/**
* Returns section name for settings.
*
* @return string
*/
public function get_settings_section_name(): string {
return $this->type . '_' . $this->name;
}
/**
* Loads factor settings to the settings tree
*
* This function usually includes settings.php file in plugins folder.
* Alternatively it can create a link to some settings page (instance of admin_externalpage)
*
* @param \part_of_admin_tree $adminroot
* @param string $parentnodename
* @param bool $hassiteconfig whether the current user has moodle/site:config capability
*/
public function load_settings(\part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig): void {
if (!$this->is_installed_and_upgraded()) {
return;
}
if (!$hassiteconfig || !file_exists($this->full_path('settings.php'))) {
return;
}
$section = $this->get_settings_section_name();
$settings = new \admin_settingpage($section, $this->displayname, 'moodle/site:config', $this->is_enabled() === false);
if ($adminroot->fulltree) {
include($this->full_path('settings.php'));
}
$adminroot->add($parentnodename, $settings);
}
/**
* Checks that given factor exists.
*
* @param string $factorname
*
* @return bool
*/
public static function factor_exists(string $factorname): bool {
$factor = self::get_factor($factorname);
return !$factor ? false : true;
}
/**
* Returns instance of any factor from the factorid.
*
* @param int $factorid
*
* @return stdClass|null Factor instance or nothing if not found.
*/
public static function get_instance_from_id(int $factorid): stdClass|null {
global $DB;
return $DB->get_record('tool_mfa', ['id' => $factorid]);
}
/**
* Return URL used for management of plugins of this type.
*
* @return moodle_url
*/
public static function get_manage_url(): moodle_url {
return new moodle_url('/admin/settings.php', [
'section' => 'managemfa',
]);
}
/**
* These subplugins can be uninstalled.
*
* @return bool
*/
public function is_uninstall_allowed(): bool {
return $this->name !== 'nosetup';
}
/**
* Pre-uninstall hook.
*
* This is intended for disabling of plugin, some DB table purging, etc.
*
* NOTE: to be called from uninstall_plugin() only.
* @private
*/
public function uninstall_cleanup() {
global $DB, $CFG;
$DB->delete_records('tool_mfa', ['factor' => $this->name]);
$DB->delete_records('tool_mfa_secrets', ['factor' => $this->name]);
$order = explode(',', get_config('tool_mfa', 'factor_order'));
if (in_array($this->name, $order)) {
$order = array_diff($order, [$this->name]);
\tool_mfa\manager::set_factor_config(['factor_order' => implode(',', $order)], 'tool_mfa');
}
parent::uninstall_cleanup();
}
/**
* Sorts factors by state.
*
* @param array $factors The factors to sort.
* @param string $state The state to sort by.
* @return array $factors The sorted factors.
*/
public static function sort_factors_by_state(array $factors, string $state): array {
usort($factors, function ($a, $b) use ($state) {
$statea = $a->get_state();
$stateb = $b->get_state();
if ($statea === $state && $stateb !== $state) {
return -1; // A comes before B.
}
if ($stateb === $state && $statea !== $state) {
return 1; // B comes before A.
}
return 0; // They are the same, keep current order.
});
return $factors;
}
/**
* Check if the current user has more than one active factor.
*
* @return bool Returns true if there are more than one.
*/
public static function user_has_more_than_one_active_factors(): bool {
$factors = self::get_active_user_factor_types();
$count = count(array_filter($factors, function($factor) {
// Include only user factors that can be set.
return $factor->has_input();
}));
return $count > 1;
}
}
+217
View File
@@ -0,0 +1,217 @@
<?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/>.
/**
* Privacy provider.
*
* @package tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_mfa\privacy;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\approved_userlist;
use core_privacy\local\request\writer;
use core_privacy\local\request\userlist;
/**
* Privacy provider
*
* @package tool_mfa
*/
class provider implements
\core_privacy\local\metadata\provider,
\core_privacy\local\request\data_provider {
/**
* Returns metadata about this plugin's privacy policy.
*
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection): collection {
$collection->add_database_table(
'tool_mfa',
[
'id' => 'privacy:metadata:tool_mfa:id',
'userid' => 'privacy:metadata:tool_mfa:userid',
'factor' => 'privacy:metadata:tool_mfa:factor',
'secret' => 'privacy:metadata:tool_mfa:secret',
'label' => 'privacy:metadata:tool_mfa:label',
'timecreated' => 'privacy:metadata:tool_mfa:timecreated',
'createdfromip' => 'privacy:metadata:tool_mfa:createdfromip',
'timemodified' => 'privacy:metadata:tool_mfa:timemodified',
'lastverified' => 'privacy:metadata:tool_mfa:lastverified',
],
'privacy:metadata:tool_mfa'
);
$collection->add_database_table(
'tool_mfa_secrets',
[
'userid' => 'privacy:metadata:tool_mfa_secrets:userid',
'factor' => 'privacy:metadata:tool_mfa_secrets:factor',
'secret' => 'privacy:metadata:tool_mfa_secrets:secret',
'sessionid' => 'privacy:metadata:tool_mfa_secrets:sessionid',
],
'privacy:metadata:tool_mfa_secrets'
);
$collection->add_database_table(
'tool_mfa_auth',
[
'userid' => 'privacy:metadata:tool_mfa_auth:userid',
'lastverified' => 'privacy:metadata:tool_mfa_auth:lastverified',
],
'privacy:metadata:tool_mfa_auth'
);
return $collection;
}
/**
* Get the list of contexts that contain user information for the given user.
*
* @param int $userid the userid to search.
* @return contextlist the contexts in which data is contained.
*/
public static function get_contexts_for_userid(int $userid): contextlist {
$contextlist = new \core_privacy\local\request\contextlist();
$contextlist->add_user_context($userid);
$contextlist->add_system_context();
return $contextlist;
}
/**
* Gets the list of users who have data with a context. Secrets context is a subset of this table.
*
* @param userlist $userlist the userlist containing users who have data in this context.
* @return void
*/
public static function get_users_in_context(userlist $userlist): void {
$context = $userlist->get_context();
// If current context is system, all users are contained within, get all users.
if ($context->contextlevel == CONTEXT_SYSTEM) {
$sql = "
SELECT *
FROM {tool_mfa}";
$userlist->add_from_sql('userid', $sql, []);
}
}
/**
* Exports all data stored in provided contexts for user. Secrets should not be exported as they are transient.
*
* @param approved_contextlist $contextlist the list of contexts to export for.
* @return void
*/
public static function export_user_data(approved_contextlist $contextlist): void {
global $DB;
$userid = $contextlist->get_user()->id;
foreach ($contextlist as $context) {
// If not in system context, exit loop.
if ($context->contextlevel == CONTEXT_SYSTEM) {
$parentclass = [];
// Get records for user ID.
$rows = $DB->get_records('tool_mfa', ['userid' => $userid]);
if (count($rows) > 0) {
$i = 0;
foreach ($rows as $row) {
$parentclass[$i]['userid'] = $row->userid;
$timecreated = \core_privacy\local\request\transform::datetime($row->timecreated);
$parentclass[$i]['factor'] = $row->factor;
$parentclass[$i]['timecreated'] = $timecreated;
$parentclass[$i]['createdfromip'] = $row->createdfromip;
$timemodified = \core_privacy\local\request\transform::datetime($row->timemodified);
$parentclass[$i]['timemodified'] = $timemodified;
$lastverified = \core_privacy\local\request\transform::datetime($row->lastverified);
$parentclass[$i]['lastverified'] = $lastverified;
$parentclass[$i]['revoked'] = $row->revoked;
$i++;
}
}
// Also get lastverified auth time for user, and add.
$lastverifiedauth = $DB->get_field('tool_mfa_auth', 'lastverified', ['userid' => $userid]);
if (!empty($lastverifiedauth)) {
$lastverifiedauth = \core_privacy\local\request\transform::datetime($lastverifiedauth);
$parentclass['lastverifiedauth'] = $lastverifiedauth;
}
writer::with_context($context)->export_data(
[get_string('privacy:metadata:tool_mfa', 'tool_mfa')],
(object) $parentclass);
}
}
}
/**
* Deletes data for all users in context.
*
* @param context $context The context to delete for.
* @return void
*/
public static function delete_data_for_all_users_in_context(\context $context): void {
global $DB;
// All data contained in system context.
if ($context->contextlevel == CONTEXT_SYSTEM) {
$DB->delete_records('tool_mfa', []);
$DB->delete_records('tool_mfa_secrets', []);
$DB->delete_records('tool_mfa_auth', []);
}
}
/**
* Deletes all data in all provided contexts for user.
*
* @param approved_contextlist $contextlist the list of contexts to delete for.
* @return void
*/
public static function delete_data_for_user(approved_contextlist $contextlist): void {
global $DB;
$userid = $contextlist->get_user()->id;
foreach ($contextlist as $context) {
// If not in system context, skip context.
if ($context->contextlevel == CONTEXT_SYSTEM) {
$DB->delete_records('tool_mfa', ['userid' => $userid]);
$DB->delete_records('tool_mfa_secrets', ['userid' => $userid]);
$DB->delete_records('tool_mfa_auth', ['userid' => $userid]);
}
}
}
/**
* Given a userlist, deletes all data in all provided contexts for the users
*
* @param approved_userlist $userlist the list of users to delete data for
* @return void
*/
public static function delete_data_for_users(approved_userlist $userlist): void {
$users = $userlist->get_users();
foreach ($users as $user) {
// Create contextlist.
$contextlist = new approved_contextlist($user, 'tool_mfa', [CONTEXT_SYSTEM]);
// Call delete data.
self::delete_data_for_user($contextlist);
}
}
}
+33
View File
@@ -0,0 +1,33 @@
<?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/>.
/**
* Definition of MFA sub-plugins (factors).
*
* @package 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;
$capabilities = [
'tool/mfa:mfaaccess' => [
'captype' => 'write',
'contextlevel' => CONTEXT_USER,
'archetypes' => ['user' => CAP_ALLOW],
],
];
+33
View File
@@ -0,0 +1,33 @@
<?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/>.
/**
* Hook callbacks for Multi-factor authentication
*
* @package tool_mfa
* @copyright 2024 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$callbacks = [
[
'hook' => core_user\hook\extend_bulk_user_actions::class,
'callback' => 'tool_mfa\local\hooks\extend_bulk_user_actions::callback',
'priority' => 0,
],
];
+62
View File
@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="admin/tool/mfa/db" VERSION="20210219" COMMENT="XMLDB file for Moodle admin/tool/mfa"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
>
<TABLES>
<TABLE NAME="tool_mfa" COMMENT="Table to store factor configurations for users">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="User ID"/>
<FIELD NAME="factor" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false" COMMENT="Factor type"/>
<FIELD NAME="secret" TYPE="char" LENGTH="1333" NOTNULL="false" SEQUENCE="false" COMMENT="Any secret data for factor"/>
<FIELD NAME="label" TYPE="char" LENGTH="1333" NOTNULL="false" SEQUENCE="false" COMMENT="label for factor instance, eg device or email."/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="15" NOTNULL="false" SEQUENCE="false" COMMENT="Time the factor instance was setup"/>
<FIELD NAME="createdfromip" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false" COMMENT="IP that the factor was setup from"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="15" NOTNULL="false" SEQUENCE="false" COMMENT="Time factor was last modified."/>
<FIELD NAME="lastverified" TYPE="int" LENGTH="15" NOTNULL="false" SEQUENCE="false" COMMENT="Time user was last verified with this factor."/>
<FIELD NAME="revoked" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="lockcounter" TYPE="int" LENGTH="5" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Counter of failed attempts"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
</KEYS>
<INDEXES>
<INDEX NAME="userid" UNIQUE="false" FIELDS="userid"/>
<INDEX NAME="factor" UNIQUE="false" FIELDS="factor"/>
<INDEX NAME="lockcounter" UNIQUE="false" FIELDS="userid, factor, lockcounter"/>
</INDEXES>
</TABLE>
<TABLE NAME="tool_mfa_secrets" COMMENT="Table to store factor secrets">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="factor" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="secret" TYPE="char" LENGTH="1333" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="15" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="expiry" TYPE="int" LENGTH="15" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="revoked" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="sessionid" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
</KEYS>
<INDEXES>
<INDEX NAME="factor" UNIQUE="false" FIELDS="factor"/>
<INDEX NAME="expiry" UNIQUE="false" FIELDS="expiry"/>
</INDEXES>
</TABLE>
<TABLE NAME="tool_mfa_auth" COMMENT="Stores the last time a successful MFA auth was registered for a userid">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="User id"/>
<FIELD NAME="lastverified" TYPE="int" LENGTH="15" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Timestamp of last MFA verification."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id" COMMENT="Link to user table"/>
</KEYS>
</TABLE>
</TABLES>
</XMLDB>
+30
View File
@@ -0,0 +1,30 @@
<?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/>.
/**
* This file contains mappings for classes that have been renamed.
*
* @package tool_mfa
* @copyright 2024 David Woloszyn <david.woloszyn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$renamedclasses = [
// Since Moodle 4.4.
'tool_mfa\\local\\form\\revoke_factor_form' => 'tool_mfa\\local\\form\\factor_action_confirmation_form',
];
+5
View File
@@ -0,0 +1,5 @@
{
"plugintypes": {
"factor": "admin\/tool\/mfa\/factor"
}
}
+28
View File
@@ -0,0 +1,28 @@
<?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/>.
/**
* Definition of MFA sub-plugins (factors).
*
* @package tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$subplugins = (array) json_decode(file_get_contents($CFG->dirroot."/admin/tool/mfa/db/subplugins.json"))->plugintypes;
@@ -0,0 +1,93 @@
<?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_admin;
use stdClass;
use tool_mfa\local\factor\object_factor_base;
/**
* Admin factor class.
*
* @package factor_admin
* @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 {
/**
* Admin Factor implementation.
* Factor is a singleton, can only be one instance.
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_all_user_factors(stdClass $user): array {
global $DB;
$records = $DB->get_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]);
if (!empty($records)) {
return $records;
}
// Null records returned, build new record.
$record = [
'userid' => $user->id,
'factor' => $this->name,
'timecreated' => time(),
'createdfromip' => $user->lastip,
'timemodified' => time(),
'revoked' => 0,
];
$record['id'] = $DB->insert_record('tool_mfa', $record, true);
return [(object) $record];
}
/**
* Admin Factor implementation.
* Factor does not have input.
*
* {@inheritDoc}
*/
public function has_input(): bool {
return false;
}
/**
* Admin Factor implementation.
* State check is performed here, as there is no form to do it in.
*
* {@inheritDoc}
*/
public function get_state(): string {
if (is_siteadmin()) {
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
}
return \tool_mfa\plugininfo\factor::STATE_PASS;
}
/**
* Admin Factor implementation.
* The state can never be set. Always return true.
*
* @param string $state the state constant to set
* @return bool
*/
public function set_state($state): bool {
return true;
}
}
@@ -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_admin\privacy;
use core_privacy\local\metadata\null_provider;
/**
* Privacy provider.
*
* @package factor_admin
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @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';
}
}
@@ -0,0 +1,30 @@
<?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_admin
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['info'] = 'This factor allows for NOT being an administrator to count as a factor. Its intended use is to ensure administators require tighter security, so regular users get the weight for free, while admins must use other factors.';
$string['pluginname'] = 'Non-administrator';
$string['privacy:metadata'] = 'The Non-administrator factor plugin does not store any personal data.';
$string['settings:weight_help'] = 'Weight is given to regular users for this factor, so admins must have more factors than a regular user to pass.';
$string['summarycondition'] = 'is not an admin';
+38
View File
@@ -0,0 +1,38 @@
<?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 factor Settings.
*
* @package factor_admin
* @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();
$enabled = new admin_setting_configcheckbox('factor_admin/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('admin', get_config('factor_admin', 'enabled') ? 'enable' : 'disable');
});
$settings->add($enabled);
$settings->add(new admin_setting_configtext('factor_admin/weight',
new lang_string('settings:weight', 'tool_mfa'),
new lang_string('settings:weight_help', 'factor_admin'), 100, PARAM_INT));
+32
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_admin
* @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 = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'factor_admin'; // Full name of the plugin (used for diagnostics).
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,115 @@
<?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_auth;
use stdClass;
use tool_mfa\local\factor\object_factor_base;
/**
* Auth factor class.
*
* @package factor_auth
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor extends object_factor_base {
/**
* Auth Factor implementation.
* Factor is a singleton, can only be one instance.
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_all_user_factors(stdClass $user): array {
global $DB;
$records = $DB->get_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]);
if (!empty($records)) {
return $records;
}
// Null records returned, build new record.
$record = [
'userid' => $user->id,
'factor' => $this->name,
'timecreated' => time(),
'createdfromip' => $user->lastip,
'timemodified' => time(),
'revoked' => 0,
];
$record['id'] = $DB->insert_record('tool_mfa', $record, true);
return [(object) $record];
}
/**
* Auth Factor implementation.
* Factor does not have input.
*
* {@inheritDoc}
*/
public function has_input(): bool {
return false;
}
/**
* Auth Factor implementation.
* State check is performed here, as there is no form to do it in.
*
* {@inheritDoc}
*/
public function get_state(): string {
global $USER;
$safetypes = get_config('factor_auth', 'goodauth');
if (strlen($safetypes) != 0) {
$safetypes = explode(',', $safetypes);
// Check all safetypes against user auth.
if (in_array($USER->auth, $safetypes, true)) {
return \tool_mfa\plugininfo\factor::STATE_PASS;
}
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
} else {
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
}
}
/**
* Auth Factor implementation.
* The state can never be set. Always return true.
*
* @param string $state the state constant to set
* @return bool
*/
public function set_state(string $state): bool {
return true;
}
/**
* Auth factor implementation.
* Return list of auth types that are safe.
*
* {@inheritDoc}
*/
public function get_summary_condition(): string {
$safetypes = get_config('factor_auth', 'goodauth');
return get_string('summarycondition', 'factor_'.$this->name, $safetypes);
}
}
@@ -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_auth\privacy;
use core_privacy\local\metadata\null_provider;
/**
* Privacy provider.
*
* @package factor_auth
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @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';
}
}
+55
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/>.
/**
* factor_auth upgrade library.
*
* @package factor_auth
* @copyright 2021 Peter Burnett <peterburnett@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Factor auth upgrade helper function
*
* @param int $oldversion
*/
function xmldb_factor_auth_upgrade($oldversion) {
if ($oldversion < 2021020500) {
$authtypes = get_enabled_auth_plugins(true);
// Upgrade goodauth config from number to name.
$goodauth = explode(',', get_config('factor_auth', 'goodauth'));
$newauths = [];
foreach ($goodauth as $auth) {
// Check if index exists before access. If not, ignore, settings were out of sync.
if (array_key_exists($auth, $authtypes)) {
$newauths[] = $authtypes[$auth];
}
}
set_config('goodauth', implode(',', $newauths), 'factor_auth');
// MFA savepoint reached.
upgrade_plugin_savepoint(true, 2021020500, 'factor', 'auth');
}
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.4.0 release upgrade line.
// Put any upgrade step following this.
return true;
}
@@ -0,0 +1,31 @@
<?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_auth
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['info'] = 'Check the type of authentication used to log in as an MFA factor.';
$string['pluginname'] = 'Authentication type';
$string['privacy:metadata'] = 'The Authentication type factor plugin does not store any personal data.';
$string['settings:goodauth'] = 'Factor authentication types';
$string['settings:goodauth_help'] = 'Select all authentication types to use as a factor for MFA. Any types not selected will not be treated as a FAIL in MFA.';
$string['summarycondition'] = 'has an authentication type of {$a}';
+49
View File
@@ -0,0 +1,49 @@
<?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_auth
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$enabled = new admin_setting_configcheckbox('factor_auth/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('auth', get_config('factor_auth', 'enabled') ? 'enable' : 'disable');
});
$settings->add($enabled);
$settings->add(new admin_setting_configtext('factor_auth/weight',
new lang_string('settings:weight', 'tool_mfa'),
new lang_string('settings:weight_help', 'tool_mfa'), 100, PARAM_INT));
$authtypes = get_enabled_auth_plugins(true);
$authselect = [];
foreach ($authtypes as $type) {
$auth = get_auth_plugin($type);
$authselect[$type] = $auth->get_title();
}
$settings->add(new admin_setting_configmulticheckbox('factor_auth/goodauth',
get_string('settings:goodauth', 'factor_auth'),
get_string('settings:goodauth_help', 'factor_auth'), [], $authselect));
+32
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_auth
* @subpackage tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'factor_auth'; // Full name of the plugin (used for diagnostics).
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,112 @@
<?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_capability;
use stdClass;
use tool_mfa\local\factor\object_factor_base;
/**
* User capability factor class.
*
* @package factor_capability
* @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 {
/**
* User capability implementation.
* This factor is a singleton, return single instance.
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_all_user_factors(stdClass $user): array {
global $DB;
$records = $DB->get_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]);
if (!empty($records)) {
return $records;
}
// Null records returned, build new record.
$record = [
'userid' => $user->id,
'factor' => $this->name,
'timecreated' => time(),
'createdfromip' => $user->lastip,
'timemodified' => time(),
'revoked' => 0,
];
$record['id'] = $DB->insert_record('tool_mfa', $record, true);
return [(object) $record];
}
/**
* User capability implementation.
* Factor has no input
*
* {@inheritDoc}
*/
public function has_input(): bool {
return false;
}
/**
* User capability implementation.
* Checks whether user has the negative capability.
*
* {@inheritDoc}
*/
public function get_state(): string {
global $USER;
$adminpass = (bool) get_config('factor_capability', 'adminpasses');
// Do anything check is controlled from factor config.
if (!has_capability('factor/capability:cannotpassfactor', \context_system::instance(), $USER, $adminpass)) {
return \tool_mfa\plugininfo\factor::STATE_PASS;
} else {
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
}
}
/**
* User Capability implementation.
* Cannot set state, return true.
*
* @param string $state the state constant to set
* @return bool
*/
public function set_state(string $state): bool {
return true;
}
/**
* User capability implementation.
* Possible states are either neutral or pass.
*
* @param stdClass $user
* @return array
*/
public function possible_states(stdClass $user): array {
return [
\tool_mfa\plugininfo\factor::STATE_PASS,
\tool_mfa\plugininfo\factor::STATE_NEUTRAL,
];
}
}
@@ -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_capability\privacy;
use core_privacy\local\metadata\null_provider;
/**
* Privacy provider.
*
* @package factor_capability
* @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';
}
}
@@ -0,0 +1,33 @@
<?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/>.
/**
* User capability factor access declaration.
*
* @package factor_capability
* @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();
$capabilities = [
'factor/capability:cannotpassfactor' => [
'captype' => 'read',
'contextlevel' => CONTEXT_SYSTEM,
],
];
@@ -0,0 +1,33 @@
<?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_capability
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['capability:cannotpassfactor'] = 'STOPS a role from passing the MFA user capability factor.';
$string['pluginname'] = 'User capability';
$string['privacy:metadata'] = 'The User capability factor plugin does not store any personal data.';
$string['settings:adminpasses'] = 'Site admins can pass this factor';
$string['settings:adminpasses_help'] = 'By default admins pass all capability checks, including this one which uses \'factor/capability:cannotpassfactor\', which means they will fail this factor.
If checked then all site admins will pass this factor if they do not have this capability from another role.
If unchecked site admins will fail this factor.';
$string['summarycondition'] = 'does NOT have the factor/capability:cannotpassfactor capability in any role including site administrator.';
@@ -0,0 +1,43 @@
<?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_capability
* @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();
$enabled = new admin_setting_configcheckbox('factor_capability/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('capability', get_config('factor_capability', 'enabled') ? 'enable' : 'disable');
});
$settings->add($enabled);
$settings->add(new admin_setting_configtext('factor_capability/weight',
new lang_string('settings:weight', 'tool_mfa'),
new lang_string('settings:weight_help', 'tool_mfa'), 100, PARAM_INT));
// Admin passes bool logic is inverted due to negative capability check.
$settings->add(new admin_setting_configcheckbox('factor_capability/adminpasses',
new lang_string('settings:adminpasses', 'factor_capability'),
new lang_string('settings:adminpasses_help', 'factor_capability'), 1, 0, 1));
@@ -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_capability
* @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 = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'factor_capability'; // Full name of the plugin (used for diagnostics).
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,153 @@
<?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_cohort;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../../../../../../cohort/lib.php');
use stdClass;
use tool_mfa\local\factor\object_factor_base;
/**
* cohort factor class.
*
* @package factor_cohort
* @author Chris Pratt <tonyyeb@gmail.com>
* @copyright Chris Pratt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor extends object_factor_base {
/**
* cohort implementation.
* This factor is a singleton, return single instance.
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_all_user_factors(stdClass $user): array {
global $DB;
$records = $DB->get_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]);
if (!empty($records)) {
return $records;
}
// Null records returned, build new record.
$record = [
'userid' => $user->id,
'factor' => $this->name,
'timecreated' => time(),
'createdfromip' => $user->lastip,
'timemodified' => time(),
'revoked' => 0,
];
$record['id'] = $DB->insert_record('tool_mfa', $record, true);
return [(object) $record];
}
/**
* cohort implementation.
* Factor has no input
*
* {@inheritDoc}
*/
public function has_input(): bool {
return false;
}
/**
* cohort implementation.
* Checks whether the user has selected cohorts in any context.
*
* {@inheritDoc}
*/
public function get_state(): string {
global $USER;
$cohortstring = get_config('factor_cohort', 'cohorts');
// Nothing selected, everyone passes.
if (empty($cohortstring)) {
return \tool_mfa\plugininfo\factor::STATE_PASS;
}
$selected = explode(',', $cohortstring);
foreach ($selected as $id) {
if (cohort_is_member($id, $USER->id)) {
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
}
}
// If we got here, no cohorts matched, allow access.
return \tool_mfa\plugininfo\factor::STATE_PASS;
}
/**
* cohort implementation.
* Cannot set state, return true.
*
* @param string $state the state constant to set
* @return bool
*/
public function set_state(string $state): bool {
return true;
}
/**
* cohort implementation.
* User can not influence. Result is whatever current state is.
*
* @param stdClass $user
*/
public function possible_states(stdClass $user): array {
return [$this->get_state()];
}
/**
* cohort implementation
* Formats the cohort list nicely.
*
* {@inheritDoc}
*/
public function get_summary_condition(): string {
$selectedcohorts = get_config('factor_cohort', 'cohorts');
if (empty($selectedcohorts)) {
return get_string('summarycondition', 'factor_cohort', get_string('none'));
}
$selectedcohorts = $this->get_cohorts(explode(',', $selectedcohorts));
if (empty($selectedcohorts)) {
return get_string('summarycondition', 'factor_cohort', get_string('none'));
}
return get_string('summarycondition', 'factor_cohort', implode(', ', $selectedcohorts));
}
/**
* Get cohorts information by given ids.
*
* @param array $selectedcohorts List of cohort ids.
* @return array
*/
public function get_cohorts(array $selectedcohorts): array {
global $DB;
[$insql, $inparams] = $DB->get_in_or_equal($selectedcohorts);
$sql = "SELECT id, name FROM {cohort} WHERE id $insql";
$cohorts = $DB->get_records_sql_menu($sql, $inparams);
return $cohorts;
}
}
@@ -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_cohort\privacy;
use core_privacy\local\metadata\null_provider;
/**
* Privacy provider.
*
* @package factor_cohort
* @author Chris Pratt <tonyyeb@gmail.com>
* @copyright Chris Pratt
* @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';
}
}
@@ -0,0 +1,30 @@
<?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_cohort
* @author Chris Pratt <tonyyeb@gmail.com>
* @copyright Chris Pratt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['pluginname'] = 'Cohort';
$string['privacy:metadata'] = 'The Cohort factor plugin does not store any personal data.';
$string['settings:cohort'] = 'Non-passing cohorts';
$string['settings:cohort_help'] = 'Select the cohorts that will not pass this factor. This allows you to force these cohorts to use other factors to authenticate.';
$string['summarycondition'] = 'does NOT have any of the following cohorts assigned in any context: {$a}';
+52
View File
@@ -0,0 +1,52 @@
<?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_cohort
* @author Chris Pratt <tonyyeb@gmail.com>
* @copyright Chris Pratt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../../../../../cohort/lib.php');
$enabled = new admin_setting_configcheckbox('factor_cohort/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('cohort', get_config('factor_cohort', 'enabled') ? 'enable' : 'disable');
});
$settings->add($enabled);
$settings->add(new admin_setting_configtext('factor_cohort/weight',
new lang_string('settings:weight', 'tool_mfa'),
new lang_string('settings:weight_help', 'tool_mfa'), 100, PARAM_INT));
$cohorts = cohort_get_all_cohorts();
$choices = [];
foreach ($cohorts['cohorts'] as $cohort) {
$choices[$cohort->id] = $cohort->name;
}
if (!empty($choices)) {
$settings->add(new admin_setting_configmultiselect('factor_cohort/cohorts',
new lang_string('settings:cohort', 'factor_cohort'),
new lang_string('settings:cohort_help', 'factor_cohort'), [], $choices));
}
@@ -0,0 +1,56 @@
<?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_cohort;
/**
* Tests for cohort factor.
*
* @covers \factor_cohort\factor
* @package factor_cohort
* @copyright 2023 Stevani Andolo <stevani@hotmail.com.au>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor_test extends \advanced_testcase {
/**
* Tests getting the summary condition
*
* @covers ::get_summary_condition
* @covers ::get_cohorts
*/
public function test_get_summary_condition(): void {
$this->resetAfterTest();
set_config('enabled', 1, 'factor_cohort');
$cohortfactor = \tool_mfa\plugininfo\factor::get_factor('cohort');
$cohort = $this->getDataGenerator()->create_cohort();
$userassignover = $this->getDataGenerator()->create_user();
cohort_add_member($cohort->id, $userassignover->id);
// Add the created cohortid into factor_cohort plugin.
set_config('cohorts', $cohort->id, 'factor_cohort');
$selectedcohorts = get_config('factor_cohort', 'cohorts');
$selectedcohorts = $cohortfactor->get_cohorts(explode(',', $selectedcohorts));
$this->assertArrayHasKey($cohort->id, $selectedcohorts);
$this->assertStringContainsString(
implode(', ', $selectedcohorts),
$cohortfactor->get_summary_condition()
);
}
}
+32
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_cohort
* @subpackage tool_mfa
* @author Chris Pratt <tonyyeb@gmail.com>
* @copyright Chris Pratt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'factor_cohort'; // Full name of the plugin (used for diagnostics).
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,93 @@
<?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_email\event;
use stdClass;
/**
* Event for when a user receives an unauthorised email from MFA.
*
* @property-read array $other {
* Extra information about event.
* }
*
* @package factor_email
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class unauth_email extends \core\event\base {
/**
* Create instance of event.
*
* @param stdClass $user the User object of the User who passed all MFA factor checks.
* @param string $ip the ip address the unauthorised email came from.
* @param string $useragent the browser fingerpring the unauthorised email came from.
*
* @return \core\event\base the user_passed_mfa event
*
* @throws \coding_exception
*/
public static function unauth_email_event(stdClass $user, string $ip, string $useragent): \core\event\base {
$data = [
'relateduserid' => null,
'context' => \context_user::instance($user->id),
'other' => [
'userid' => $user->id,
'ip' => $ip,
'useragent' => $useragent,
],
];
return self::create($data);
}
/**
* Init method.
*
* @return void
*/
protected function init(): void {
$this->data['crud'] = 'r';
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description(): string {
$data = new stdClass();
$data->userid = $this->other['userid'];
$data->ip = $this->other['ip'];
$data->useragent = $this->other['useragent'];
return get_string('unauthloginattempt', 'factor_email', $data);
}
/**
* Return localised event name.
*
* @return string
* @throws \coding_exception
*/
public static function get_name(): string {
return get_string('event:unauthemail', 'factor_email');
}
}
@@ -0,0 +1,338 @@
<?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_email;
use stdClass;
use tool_mfa\local\factor\object_factor_base;
/**
* Email factor class.
*
* @package factor_email
* @subpackage tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @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-envelope';
/**
* E-Mail Factor implementation.
*
* @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;
}
/**
* E-Mail Factor implementation.
*
* @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_email_code();
return $mform;
}
/**
* Sends and e-mail to user with given verification code.
*
* @param int $instanceid
* @return void
*/
public static function email_verification_code(int $instanceid): void {
global $PAGE, $USER;
$noreplyuser = \core_user::get_noreply_user();
$subject = get_string('email:subject', 'factor_email');
$renderer = $PAGE->get_renderer('factor_email');
$body = $renderer->generate_email($instanceid);
email_to_user($USER, $noreplyuser, $subject, $body, $body);
}
/**
* E-Mail Factor implementation.
*
* @param array $data
* @return array
*/
public function login_form_validation(array $data): array {
global $USER;
$return = [];
if (!$this->check_verification_code($data['verificationcode'])) {
$return['verificationcode'] = get_string('error:wrongverification', 'factor_email');
}
return $return;
}
/**
* E-Mail Factor implementation.
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_all_user_factors(stdClass $user): array {
global $DB;
$records = $DB->get_records('tool_mfa', [
'userid' => $user->id,
'factor' => $this->name,
'label' => $user->email,
]);
if (!empty($records)) {
return $records;
}
// Null records returned, build new record.
$record = [
'userid' => $user->id,
'factor' => $this->name,
'label' => $user->email,
'createdfromip' => $user->lastip,
'timecreated' => time(),
'revoked' => 0,
];
$record['id'] = $DB->insert_record('tool_mfa', $record, true);
return [(object) $record];
}
/**
* E-Mail Factor implementation.
*
* {@inheritDoc}
*/
public function has_input(): bool {
if (self::is_ready()) {
return true;
}
return false;
}
/**
* E-Mail Factor implementation.
*
* {@inheritDoc}
*/
public function get_state(): string {
if (!self::is_ready()) {
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
}
return parent::get_state();
}
/**
* Checks whether user email is correctly configured.
*
* @return bool
*/
private static function is_ready(): bool {
global $DB, $USER;
if (empty($USER->email)) {
return false;
}
if (!validate_email($USER->email)) {
return false;
}
if (over_bounce_threshold($USER)) {
return false;
}
// If this factor is revoked, set to not ready.
if ($DB->record_exists('tool_mfa', ['userid' => $USER->id, 'factor' => 'email', 'revoked' => 1])) {
return false;
}
return true;
}
/**
* Generates and emails the code for login to the user, stores codes in DB.
*
* @return void
*/
private function generate_and_email_code(): void {
global $DB, $USER;
// Get instance that isnt parent email type (label check).
// This check must exclude the main singleton record, with the label as the email.
// It must only grab the record with the user agent as the label.
$sql = 'SELECT *
FROM {tool_mfa}
WHERE userid = ?
AND factor = ?
AND NOT label = ?';
$record = $DB->get_record_sql($sql, [$USER->id, 'email', $USER->email]);
$duration = get_config('factor_email', 'duration');
$newcode = random_int(100000, 999999);
if (empty($record)) {
// No code active, generate new code.
$instanceid = $DB->insert_record('tool_mfa', [
'userid' => $USER->id,
'factor' => 'email',
'secret' => $newcode,
'label' => $_SERVER['HTTP_USER_AGENT'],
'timecreated' => time(),
'createdfromip' => $USER->lastip,
'timemodified' => time(),
'lastverified' => time(),
'revoked' => 0,
], true);
$this->email_verification_code($instanceid);
} else if ($record->timecreated + $duration < time()) {
// Old code found. Keep id, update fields.
$DB->update_record('tool_mfa', [
'id' => $record->id,
'secret' => $newcode,
'label' => $_SERVER['HTTP_USER_AGENT'],
'timecreated' => time(),
'createdfromip' => $USER->lastip,
'timemodified' => time(),
'lastverified' => time(),
'revoked' => 0,
]);
$instanceid = $record->id;
$this->email_verification_code($instanceid);
}
}
/**
* Verifies entered code against stored DB record.
*
* @param string $enteredcode
* @return bool
*/
private function check_verification_code(string $enteredcode): bool {
global $DB, $USER;
$duration = get_config('factor_email', 'duration');
// Get instance that isnt parent email type (label check).
// This check must exclude the main singleton record, with the label as the email.
// It must only grab the record with the user agent as the label.
$sql = 'SELECT *
FROM {tool_mfa}
WHERE userid = ?
AND factor = ?
AND NOT label = ?';
$record = $DB->get_record_sql($sql, [$USER->id, 'email', $USER->email]);
if ($enteredcode == $record->secret) {
if ($record->timecreated + $duration > time()) {
return true;
}
}
return false;
}
/**
* Cleans up email records once MFA passed.
*
* {@inheritDoc}
*/
public function post_pass_state(): void {
global $DB, $USER;
// Delete all email records except base record.
$selectsql = 'userid = ?
AND factor = ?
AND NOT label = ?';
$DB->delete_records_select('tool_mfa', $selectsql, [$USER->id, 'email', $USER->email]);
// Update factor timeverified.
parent::post_pass_state();
}
/**
* Email factor implementation.
* Email page must be safe to authorise session from link.
*
* {@inheritDoc}
*/
public function get_no_redirect_urls(): array {
$email = new \moodle_url('/admin/tool/mfa/factor/email/email.php');
return [$email];
}
/**
* Email factor implementation.
*
* @param stdClass $user
*/
public function possible_states(stdClass $user): array {
// Email can return all states.
return [
\tool_mfa\plugininfo\factor::STATE_FAIL,
\tool_mfa\plugininfo\factor::STATE_PASS,
\tool_mfa\plugininfo\factor::STATE_NEUTRAL,
\tool_mfa\plugininfo\factor::STATE_UNKNOWN,
];
}
/**
* Obscure an email address by replacing all but the first and last character of the local part with a dot.
* So the users full email isn't displayed during login.
*
* @param string $email The email address to obfuscate.
* @return string
* @throws \coding_exception
*/
protected function obfuscate_email(string $email): string {
// Split the email address at the '@' symbol.
$parts = explode('@', $email);
if (count($parts) != 2) {
throw new \coding_exception('Invalid email format');
}
$local = $parts[0];
$domain = $parts[1];
// Obfuscate all but the first and last character of the local part.
$length = strlen($local);
$middledot = "\u{00B7}";
if ($length > 2) {
$local = $local[0] . str_repeat($middledot, $length - 2) . $local[$length - 1];
}
// Put the email address back together and return it.
return $local . '@' . $domain;
}
/**
* 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 {
global $USER;
$email = $this->obfuscate_email($USER->email);
return get_string('logindesc', 'factor_' . $this->name, $email);
}
}
@@ -0,0 +1,56 @@
<?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_email\form;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . "/formslib.php");
/**
* Revoke email form.
*
* @package factor_email
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class email extends \moodleform {
/**
* Form definition.
*/
public function definition(): void {
$mform = $this->_form;
$mform->addElement('html', get_string('email:accident', 'factor_email'));
$this->add_action_buttons(true, get_string('continue'));
}
/**
* Form validation.
*
* Server side rules do not work for uploaded files, implement serverside rules here if needed.
*
* @param array $data array of ("fieldname"=>value) of submitted data
* @param array $files array of uploaded files "element_name"=>tmp_file_path
* @return array of "element_name"=>"error_description" if there are errors,
* or an empty array if everything is OK (true allowed for backwards compatibility too).
*/
public function validation($data, $files): array {
$errors = parent::validation($data, $files);
return $errors;
}
}
@@ -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/>.
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/iplookup/lib.php');
/**
* Email renderer.
*
* @package factor_email
* @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_email_renderer extends plugin_renderer_base {
/**
* Generates an email
*
* @param int $instanceid
* @return string|boolean
*/
public function generate_email(int $instanceid): string|bool {
global $DB, $USER, $CFG;;
$instance = $DB->get_record('tool_mfa', ['id' => $instanceid]);
$site = get_site();
$validity = get_config('factor_email', 'duration');
$authurl = new \moodle_url('/admin/tool/mfa/factor/email/email.php',
['instance' => $instance->id, 'pass' => 1, 'secret' => $instance->secret]);
$authurlstring = \html_writer::link($authurl, get_string('email:link', 'factor_email'));
$blockurl = new \moodle_url('/admin/tool/mfa/factor/email/email.php', ['instance' => $instanceid]);
$blockurlstring = \html_writer::link($blockurl, get_string('email:stoploginlink', 'factor_email'));
$geoinfo = iplookup_find_location($instance->createdfromip);
$templateinfo = [
'logo' => $this->get_compact_logo_url(100, 100),
'name' => $USER->firstname,
'sitename' => $site->fullname,
'siteurl' => $CFG->wwwroot,
'code' => $instance->secret,
'validity' => format_time($validity),
'authlink' => get_string('email:loginlink', 'factor_email', $authurlstring),
'revokelink' => get_string('email:revokelink', 'factor_email', $blockurlstring),
'ip' => $instance->createdfromip,
'geocity' => $geoinfo['city'],
'geocountry' => $geoinfo['country'],
'ua' => $instance->label,
];
return $this->render_from_template('factor_email/email', $templateinfo);
}
}
@@ -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_email\privacy;
use core_privacy\local\metadata\null_provider;
/**
* Privacy provider.
*
* @package factor_email
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @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';
}
}
+106
View File
@@ -0,0 +1,106 @@
<?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/>.
/**
* Page to revoke and disable an email code.
*
* @package factor_email
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// Ignore coding standards for login check, this page does not require login.
// phpcs:disable moodle.Files.RequireLogin.Missing
require_once(__DIR__ . '/../../../../../config.php');
$instanceid = required_param('instance', PARAM_INT);
$pass = optional_param('pass', '0', PARAM_INT);
$secret = optional_param('secret', 0, PARAM_INT);
$context = context_system::instance();
$PAGE->set_context($context);
$url = new moodle_url('/admin/tool/mfa/factor/email/email.php',
['instance' => $instanceid, 'pass' => $pass, 'secret' => $secret]);
$PAGE->set_url($url);
$PAGE->set_pagelayout('secure');
$PAGE->set_title(get_string('unauthemail', 'factor_email'));
$PAGE->set_cacheable(false);
$instance = $DB->get_record('tool_mfa', ['id' => $instanceid]);
$factor = \tool_mfa\plugininfo\factor::get_factor('email');
// If pass is set, require login to force $SESSION and user, and pass for that session.
if (!empty($instance) && $pass != 0 && $secret != 0) {
require_login();
if ($factor->get_state() === \tool_mfa\plugininfo\factor::STATE_LOCKED) {
// Redirect through to auth, this will bounce them to the next factor.
redirect(new moodle_url('/admin/tool/mfa/auth.php'));
}
// Check the code with the same measures on the page entry.
if ($instance->secret != $secret) {
\tool_mfa\manager::sleep_timer();
$factor->increment_lock_counter();
throw new moodle_exception('error:parameters', 'factor_email');
}
$factor = \tool_mfa\plugininfo\factor::get_factor('email');
$factor->set_state(\tool_mfa\plugininfo\factor::STATE_PASS);
// If wantsurl is already set in session, go to it.
if (!empty($SESSION->wantsurl)) {
redirect($SESSION->wantsurl);
} else {
redirect(new moodle_url('/'));
}
}
$form = new \factor_email\form\email($url);
if ($form->is_cancelled()) {
redirect(new moodle_url('/'));
} else if ($fromform = $form->get_data()) {
if (empty($instance)) {
$message = get_string('error:badcode', 'factor_email');
} else {
$user = $DB->get_record('user', ['id' => $instance->userid]);
// Stop attacker from using email factor at all, by revoking all email until admin fixes.
$DB->set_field('tool_mfa', 'revoked', 1, ['userid' => $user->id, 'factor' => 'email']);
// Remotely logout all sessions for user.
$manager = \core\session\manager::kill_user_sessions($instance->userid);
// Log event.
$ip = $instance->createdfromip;
$useragent = $instance->label;
$event = \factor_email\event\unauth_email::unauth_email_event($user, $ip, $useragent);
$event->trigger();
// Suspend user account.
if (get_config('factor_email', 'suspend')) {
$DB->set_field('user', 'suspended', 1, ['id' => $user->id]);
}
$message = get_string('email:revokesuccess', 'factor_email', fullname($user));
}
}
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('unauthemail', 'factor_email'));
if (!empty($message)) {
echo $message;
} else {
$form->display();
}
echo $OUTPUT->footer();
@@ -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/>.
/**
* Language strings.
*
* @package factor_email
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['email:accident'] = 'If you didn\'t request the email, click continue to invalidate the login attempt. If you clicked the link by accident, click cancel, and no action will be taken.';
$string['email:browseragent'] = 'The browser details for this request are: \'{$a}\'';
$string['email:geoinfo'] = 'This request appears to have originated from approximately:';
$string['email:greeting'] = 'Hello {$a} &#128075;';
$string['email:ipinfo'] = 'Login request details:';
$string['email:link'] = 'verification link';
$string['email:loginlink'] = 'Or, if you\'re on the same device, use this {$a}.';
$string['email:message'] = 'Here\'s your verification code for {$a->sitename} ({$a->siteurl}).';
$string['email:originatingip'] = 'This login request was made from \'{$a}\'';
$string['email:revokelink'] = 'If this wasn\'t you, you can {$a}.';
$string['email:revokesuccess'] = 'This code has been successfully revoked. All sessions for {$a} have been ended.
Email will not be usable as a factor until account security has been verified.';
$string['email:subject'] = 'Here\'s your verification code';
$string['email:stoploginlink'] = 'stop this login attempt';
$string['email:uadescription'] = 'Browser identity for this request:';
$string['email:validity'] = 'The code can only be used once and is valid for {$a}.';
$string['error:badcode'] = 'Code was not found. This may be an old link, a new code may have been emailed, or the login attempt with this code was successful.';
$string['error:parameters'] = 'Incorrect page parameters.';
$string['error:wrongverification'] = 'Wrong code. Try again.';
$string['event:unauthemail'] = 'Unauthorised email received';
$string['info'] = 'You are using email {$a} to authenticate. This has been set up by your site administrator.';
$string['logindesc'] = 'We\'ve just sent a 6-digit code to your email: {$a}';
$string['loginoption'] = 'Have a code emailed to you';
$string['loginskip'] = "I didn't receive a code";
$string['loginsubmit'] = 'Continue';
$string['logintitle'] = "Verify it's you by email";
$string['managefactor'] = 'Manage email';
$string['manageinfo'] = '\'{$a}\' is being used to authenticate. This has been set up by your administrator.';
$string['pluginname'] = 'Email';
$string['privacy:metadata'] = 'The Email factor plugin does not store any personal data';
$string['settings:duration'] = 'Validity duration';
$string['settings:duration_help'] = 'The period of time that the code is valid.';
$string['settings:suspend'] = 'Suspend unauthorised accounts';
$string['settings:suspend_help'] = 'Check this to suspend user accounts if an unauthorised email verification is received.';
$string['setupfactor'] = 'Set up email';
$string['summarycondition'] = 'has valid email setup';
$string['unauthloginattempt'] = 'The user with ID {$a->userid} made an unauthorised login attempt using email verification from
IP {$a->ip} with browser agent {$a->useragent}.';
$string['unauthemail'] = 'Unauthorised email';
$string['verificationcode'] = 'Enter verification code for confirmation';
$string['verificationcode_help'] = 'A verification code has been sent to your email.';
+46
View File
@@ -0,0 +1,46 @@
<?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_email
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$enabled = new admin_setting_configcheckbox('factor_email/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('email', get_config('factor_email', 'enabled') ? 'enable' : 'disable');
});
$settings->add($enabled);
$settings->add(new admin_setting_configtext('factor_email/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_email/duration',
get_string('settings:duration', 'factor_email'),
get_string('settings:duration_help', 'factor_email'), 30 * MINSECS, MINSECS));
$settings->add(new admin_setting_configcheckbox('factor_email/suspend',
get_string('settings:suspend', 'factor_email'),
get_string('settings:suspend_help', 'factor_email'), 0));
@@ -0,0 +1,65 @@
{{!
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_email/email
Template for email body sent to users.
Example context (json):
{
"message": "Your verification code is 123456.",
"ip": "The IP was 127.0.0.1",
"geoinfo": "Request originated from Brisbane, Australia",
"ua": "The user agent is Firefox 70",
"linkstring": "If this wasn't you, click here"
}
}}
<div style="font-family: Arial, sans-serif; font-size: 18px">
{{#logo}}
<table style="width: 600px; margin-bottom: 10px;">
<tr>
<td></td>
<td style="text-align: right;">
<img src="{{{logo}}}" alt="{{sitename}}" />
</td>
</tr>
</table>
{{/logo}}
<p>{{#str}} email:greeting, factor_email, {{name}}{{/str}}</p>
<p>
{{#str}} email:message, factor_email, {"sitename":{{#quote}}{{sitename}}{{/quote}}, "siteurl":{{#quote}}{{siteurl}}{{/quote}} }{{/str}}
</p>
<h2 style="letter-spacing: 5px;">{{code}}</h2>
<p>
{{#str}} email:validity, factor_email, {{validity}}{{/str}}
<br/>
{{{authlink}}}
</p>
<br/>
<p>{{{revokelink}}}</p>
<br/>
<div style="font-family: Arial, sans-serif; font-size: 12px">
<p><strong>{{#str}} email:ipinfo, factor_email {{/str}}</strong></p>
<p>{{#str}} email:originatingip, factor_email, {{ip}} {{/str}}</p>
{{#geocountry}}
<p> {{#str}} email:geoinfo, factor_email {{/str}}{{#geocity}}{{geocity}},{{/geocity}} {{geocountry}}</p>
{{/geocountry}}
<p><strong>{{#str}} email:uadescription, factor_email {{/str}}</strong></p>
<p>{{ua}}</p>
<p>{{{linkstring}}}</p>
</div>
</div>
@@ -0,0 +1,84 @@
<?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_email;
/**
* Tests for email factor.
*
* @covers \factor_email\factor
* @package factor_email
* @copyright 2023 Stevani Andolo <stevani@hotmail.com.au>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor_test extends \advanced_testcase {
/**
* Tests checking verification code
*
* @covers ::check_verification_code
* @covers ::post_pass_state
*/
public function test_check_verification_code(): void {
global $DB, $USER;
$this->resetAfterTest(true);
$emailfactorclass = new \factor_email\factor('email');
$rc = new \ReflectionClass($emailfactorclass::class);
$rcm = $rc->getMethod('check_verification_code');
// Assigned email to be used in getting the email factor.
$USER->email = 'user@mail.com';
set_config('enabled', 1, 'factor_email');
// Testing with current timecreated.
$newcode = random_int(100000, 999999);
$instanceid = $DB->insert_record('tool_mfa', [
'userid' => $USER->id,
'factor' => 'email',
'secret' => $newcode,
'label' => 'unittest',
'timecreated' => time(),
'timemodified' => time(),
'lastverified' => time(),
'revoked' => 0,
]);
$data = $DB->get_record('tool_mfa', ['id' => $instanceid]);
$this->assertTrue($rcm->invoke($emailfactorclass, $data->secret));
// Update the data to test with really old timecreated.
$DB->update_record('tool_mfa', [
'id' => $instanceid,
'timecreated' => time() - 1689657581,
'timemodified' => time() - 1689657581,
'lastverified' => time() - 1689657581,
'revoked' => 0,
]);
$data = $DB->get_record('tool_mfa', ['id' => $instanceid]);
$this->assertFalse($rcm->invoke($emailfactorclass, $data->secret));
// Cleans up email records once MFA passed.
$rcm = $rc->getMethod('post_pass_state');
$rcm->invoke($emailfactorclass);
// Check if the email records have been deleted.
$data = $DB->count_records('tool_mfa', ['factor' => 'email']);
$this->assertEquals(0, $data);
}
}
+32
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_email
* @subpackage tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'factor_email'; // Full name of the plugin (used for diagnostics).
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,307 @@
<?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_grace;
use stdClass;
use tool_mfa\local\factor\object_factor_base;
/**
* Grace period factor class.
*
* @package factor_grace
* @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 {
/**
* Grace Factor implementation.
* This factor is a singleton, return single instance.
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_all_user_factors(stdClass $user): array {
global $DB;
$records = $DB->get_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]);
if (!empty($records)) {
return $records;
}
// Null records returned, build new record.
$record = [
'userid' => $user->id,
'factor' => $this->name,
'createdfromip' => $user->lastip,
'timecreated' => time(),
'revoked' => 0,
];
$record['id'] = $DB->insert_record('tool_mfa', $record, true);
return [(object) $record];
}
/**
* Grace Factor implementation.
* Singleton instance, no additional filtering needed.
*
* @param stdClass $user object to check against.
* @return array the array of active factors.
*/
public function get_active_user_factors(stdClass $user): array {
return $this->get_all_user_factors($user);
}
/**
* Grace Factor implementation.
* Factor has no input.
*
* {@inheritDoc}
*/
public function has_input(): bool {
return false;
}
/**
* Grace Factor implementation.
* Checks the user login time against their first login after MFA activation.
*
* @param bool $redirectable should this state call be allowed to redirect the user?
* @return string state constant
*/
public function get_state($redirectable = true): string {
global $FULLME, $SESSION, $USER;
$records = ($this->get_all_user_factors($USER));
$record = reset($records);
// First check if user has any other input or setup factors active.
$factors = $this->get_affecting_factors();
$total = 0;
foreach ($factors as $factor) {
$total += $factor->get_weight();
// If we have hit 100 total, then we know it is possible to auth with the current setup.
// Gracemode should no longer give points.
if ($total >= 100) {
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
}
}
$starttime = $record->timecreated;
// If no start time is recorded, status is unknown.
if (empty($starttime)) {
return \tool_mfa\plugininfo\factor::STATE_UNKNOWN;
} else {
$duration = get_config('factor_grace', 'graceperiod');
if (!empty($duration)) {
if (time() > $starttime + $duration) {
// If gracemode would have given points, but now doesnt,
// Jump out of the loop and force a factor setup.
// We will return once there is a setup, or the user tries to leave.
if (get_config('factor_grace', 'forcesetup') && $redirectable) {
if (empty($SESSION->mfa_gracemode_recursive)) {
// Set a gracemode lock so any further recursive gets fall past any recursive calls.
$SESSION->mfa_gracemode_recursive = true;
$factorurls = \tool_mfa\manager::get_no_redirect_urls();
$cleanurl = new \moodle_url($FULLME);
foreach ($factorurls as $factorurl) {
if ($factorurl->compare($cleanurl)) {
$redirectable = false;
}
}
// We should never redirect if we have already passed.
if ($redirectable && \tool_mfa\manager::get_cumulative_weight() >= 100) {
$redirectable = false;
}
unset($SESSION->mfa_gracemode_recursive);
if ($redirectable) {
redirect(new \moodle_url('/admin/tool/mfa/user_preferences.php'),
get_string('redirectsetup', 'factor_grace'));
}
}
}
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
} else {
return \tool_mfa\plugininfo\factor::STATE_PASS;
}
} else {
return \tool_mfa\plugininfo\factor::STATE_UNKNOWN;
}
}
}
/**
* Grace Factor implementation.
* State cannot be set. Return true.
*
* @param string $state the state constant to set
* @return bool
*/
public function set_state(string $state): bool {
return true;
}
/**
* Grace Factor implementation.
* Add a notification on the next page.
*
* {@inheritDoc}
*/
public function post_pass_state(): void {
global $USER;
parent::post_pass_state();
// Ensure grace factor passed before displaying notification.
if ($this->get_state() == \tool_mfa\plugininfo\factor::STATE_PASS
&& !\tool_mfa\manager::check_factor_pending($this->name)) {
$url = new \moodle_url('/admin/tool/mfa/user_preferences.php');
$link = \html_writer::link($url, get_string('preferences', 'factor_grace'));
$records = ($this->get_all_user_factors($USER));
$record = reset($records);
$starttime = $record->timecreated;
$timeremaining = ($starttime + get_config('factor_grace', 'graceperiod')) - time();
$time = format_time($timeremaining);
$data = ['url' => $link, 'time' => $time];
$customwarning = get_config('factor_grace', 'customwarning');
if (!empty($customwarning)) {
// Clean text, then swap placeholders for time and the setup link.
$message = preg_replace("/{timeremaining}/", $time, $customwarning);
$message = preg_replace("/{setuplink}/", $url, $message);
$message = clean_text($message, FORMAT_MOODLE);
} else {
$message = get_string('setupfactors', 'factor_grace', $data);
}
\core\notification::error($message);
}
}
/**
* Grace Factor implementation.
* Gracemode should not be a valid combination with another factor.
*
* @param array $combination array of factors that make up the combination
* @return bool
*/
public function check_combination(array $combination): bool {
// If this combination has more than 1 factor that has setup or input, not valid.
foreach ($combination as $factor) {
if ($factor->has_setup() || $factor->has_input()) {
return false;
}
}
return true;
}
/**
* Grace Factor implementation.
* Gracemode can change outcome just by waiting, or based on other factors.
*
* @param stdClass $user
* @return array
*/
public function possible_states(stdClass $user): array {
return [
\tool_mfa\plugininfo\factor::STATE_PASS,
\tool_mfa\plugininfo\factor::STATE_NEUTRAL,
];
}
/**
* Grace factor implementation.
*
* If grace period should redirect at end, make this a no-redirect url.
*
* @return array
*/
public function get_no_redirect_urls(): array {
$redirect = get_config('factor_grace', 'forcesetup');
// First check if user has any other input or setup factors active.
$factors = $this->get_affecting_factors();
$total = 0;
foreach ($factors as $factor) {
$total += $factor->get_weight();
// If we have hit 100 total, then we know it is possible to auth with the current setup.
// The setup URL should no longer be a no-redirect URL. User MUST use existing auth.
if ($total >= 100) {
return [];
}
}
if ($redirect && $this->get_state(false) === \tool_mfa\plugininfo\factor::STATE_NEUTRAL) {
// If the config is enabled, the user should be able to access + setup a factor using these pages.
return [
new \moodle_url('/admin/tool/mfa/user_preferences.php'),
new \moodle_url('/admin/tool/mfa/action.php'),
];
} else {
return [];
}
}
/**
* Returns a list of factor objects that can affect gracemode giving points.
*
* Only factors that a user can setup or manually use can affect whether gracemode gives points.
* The intest is to provide a grace period for users to go in, setup factors, phone numbers, etc.,
* so that they are able to authenticate correctly once the grace period ends.
*
* @return array
*/
public function get_all_affecting_factors(): array {
// Check if user has any other input or setup factors active.
$factors = \tool_mfa\plugininfo\factor::get_factors();
$factors = array_filter($factors, function ($el) {
return $el->has_input() || $el->has_setup();
});
return $factors;
}
/**
* Get the factor list that is currently affecting gracemode. Active and not ignored.
*
* @return array
*/
public function get_affecting_factors(): array {
// We need to filter all active user factors against the affecting factors and ignorelist.
// Map active to names for filtering.
$active = \tool_mfa\plugininfo\factor::get_active_user_factor_types();
$active = array_map(function ($el) {
return $el->name;
}, $active);
$factors = $this->get_all_affecting_factors();
$ignorelist = get_config('factor_grace', 'ignorelist');
$ignorelist = !empty($ignorelist) ? explode(',', $ignorelist) : [];
$factors = array_filter($factors, function ($el) use ($ignorelist, $active) {
return !in_array($el->name, $ignorelist) && in_array($el->name, $active);
});
return $factors;
}
}
@@ -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_grace\privacy;
use core_privacy\local\metadata\null_provider;
/**
* Privacy provider.
*
* @package factor_grace
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @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';
}
}
@@ -0,0 +1,78 @@
<?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/>.
/**
* Scheduled task to revoke expired factors
*
* @package factor_grace
* @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_grace\task;
/**
* Scheduled task to revoke expired gracemode factors
*/
class revoke_expired_factors extends \core\task\scheduled_task {
/**
* Return the task's name as shown in admin screens.
*
* @return string
*/
public function get_name(): string {
return get_string('revokeexpiredfactors', 'factor_grace');
}
/**
* Execute the task.
*
* @return void
*/
public function execute(): void {
mtrace('Starting to revoke expired Grace factors');
$this->revoke_factors();
}
/**
* Revokes all grace factors that have a valid timecreated and are outside the duration.
*
* @return void
*/
private function revoke_factors(): void {
global $DB;
// If config is not set, pull out.
$duration = get_config('factor_grace', 'graceperiod');
if (!$duration) {
mtrace('Gracemode duration is not set. Exiting...');
return;
}
$revoketime = time() - $duration;
// Single query implementation.
$sql = "UPDATE {tool_mfa}
SET revoked = 1,
timemodified = :timemodified
WHERE timecreated < :revoketime
AND factor = :factor";
$DB->execute($sql, ['timemodified' => time(), 'revoketime' => $revoketime, 'factor' => 'grace']);
mtrace('Finished revoking expired Grace factors');
}
}
+38
View File
@@ -0,0 +1,38 @@
<?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/>.
/**
* Task scheduler
*
* @package factor_grace
* @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();
$tasks = [
[
'classname' => 'factor_grace\task\revoke_expired_factors',
'blocking' => 0,
'minute' => 'R',
'hour' => '0',
'day' => '*',
'month' => '*',
'dayofweek' => '*',
],
];
@@ -0,0 +1,41 @@
<?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_grace
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['info'] = 'Allows login without other factor for a specified period of time.';
$string['pluginname'] = 'Grace period';
$string['preferences'] = 'User preferences';
$string['privacy:metadata'] = 'The Grace period factor plugin does not store any personal data';
$string['redirectsetup'] = 'You must complete setup for Multi-factor authentication before you can proceed.';
$string['revokeexpiredfactors'] = 'Revoke expired grace period factors';
$string['settings:customwarning'] = 'Warning banner content';
$string['settings:customwarning_help'] = 'Add content here to replace the grace warning notification with custom HTML contents. Adding {timeremaining} in text will replace it with the current grace duration for the user, and {setuplink} will replace with the URL of the setup page for the user.';
$string['settings:forcesetup'] = 'Force factor setup';
$string['settings:forcesetup_help'] = 'Forces a user to the preferences page to set up multi-factor authentication when the grace period expires. If unchecked, users will be unable to authenticate when the grace period expires.';
$string['settings:graceperiod'] = 'Grace period';
$string['settings:graceperiod_help'] = 'Period of time when users can access the site without configured and enabled factors.';
$string['settings:ignorelist'] = 'Ignored factors';
$string['settings:ignorelist_help'] = 'Grace period will not give points if there are other factors that users can use to authenticate with multi-factor authentication. Any factors here will not be counted by Grace period when deciding whether to give points. This can allow Grace period to allow authentication if another factor like email, has configuration or system issues.';
$string['setupfactors'] = 'You are currently in the grace period, and may not have enough factors set up to log in once the grace period expires. Go to {$a->url} to check your authentication status and set up more authentication factors. Your grace period expires in {$a->time}.';
$string['summarycondition'] = 'is within grace period';
+60
View File
@@ -0,0 +1,60 @@
<?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_grace
* @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();
$enabled = new admin_setting_configcheckbox('factor_grace/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('grace', get_config('factor_grace', 'enabled') ? 'enable' : 'disable');
});
$settings->add($enabled);
$settings->add(new admin_setting_configtext('factor_grace/weight',
new lang_string('settings:weight', 'tool_mfa'),
new lang_string('settings:weight_help', 'tool_mfa'), 100, PARAM_INT));
$settings->add(new admin_setting_configcheckbox('factor_grace/forcesetup',
new lang_string('settings:forcesetup', 'factor_grace'),
new lang_string('settings:forcesetup_help', 'factor_grace'), 0));
$settings->add(new admin_setting_configduration('factor_grace/graceperiod',
new lang_string('settings:graceperiod', 'factor_grace'),
new lang_string('settings:graceperiod_help', 'factor_grace'), '604800'));
$gracefactor = \tool_mfa\plugininfo\factor::get_factor('grace');
$factors = $gracefactor->get_all_affecting_factors();
$gracefactors = [];
foreach ($factors as $factor) {
$gracefactors[$factor->name] = $factor->get_display_name();
}
$settings->add(new admin_setting_configmultiselect('factor_grace/ignorelist',
new lang_string('settings:ignorelist', 'factor_grace'),
new lang_string('settings:ignorelist_help', 'factor_grace'), [], $gracefactors));
$settings->add(new admin_setting_confightmleditor('factor_grace/customwarning',
new lang_string('settings:customwarning', 'factor_grace'),
new lang_string('settings:customwarning_help', 'factor_grace'), '', PARAM_RAW));
@@ -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/>.
namespace factor_grace;
/**
* Tests for grace factor.
*
* @package factor_grace
* @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_test extends \advanced_testcase {
/**
* Test affecting factors
*
* @covers ::get_affecting_factors
* @return void
*/
public function test_affecting_factors(): void {
$this->resetAfterTest(true);
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$grace = \tool_mfa\plugininfo\factor::get_factor('grace');
$affecting = $grace->get_affecting_factors();
$this->assertEquals(0, count($affecting));
set_config('enabled', 1, 'factor_totp');
$totpfactor = \tool_mfa\plugininfo\factor::get_factor('totp');
$totpdata = [
'secret' => 'fakekey',
'devicename' => 'fakedevice',
];
$totpfactor->setup_user_factor((object) $totpdata);
// Confirm that MFA is the only affecting factor.
$affecting = $grace->get_affecting_factors();
$this->assertEquals(1, count($affecting));
$totp = reset($affecting);
$this->assertTrue($totp instanceof \factor_totp\factor);
// Now put it in the ignorelist.
set_config('ignorelist', 'totp', 'factor_grace');
// Confirm that MFA is the only affecting factor.
$affecting = $grace->get_affecting_factors();
$this->assertEquals(0, count($affecting));
}
}
+32
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_grace
* @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 = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'factor_grace'; // Full name of the plugin (used for diagnostics).
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,114 @@
<?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_iprange;
use stdClass;
use tool_mfa\local\factor\object_factor_base;
/**
* IP Range factor class.
*
* @package factor_iprange
* @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 {
/**
* IP Range Factor implementation.
* This factor is a singleton, return single instance.
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_all_user_factors(stdClass $user): array {
global $DB;
$records = $DB->get_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]);
if (!empty($records)) {
return $records;
}
// Null records returned, build new record.
$record = [
'userid' => $user->id,
'factor' => $this->name,
'timecreated' => time(),
'createdfromip' => $user->lastip,
'timemodified' => time(),
'revoked' => 0,
];
$record['id'] = $DB->insert_record('tool_mfa', $record, true);
return [(object) $record];
}
/**
* IP Range Factor implementation.
* Factor has no input
*
* {@inheritDoc}
*/
public function has_input(): bool {
return false;
}
/**
* IP Range Factor implementation.
* Checks a users current IP against allowed and disallowed ranges.
*
* {@inheritDoc}
*/
public function get_state(): string {
$safeips = get_config('factor_iprange', 'safeips');
// TODO: Check for failures here.
if (!empty($safeips)) {
if (remoteip_in_list($safeips)) {
return \tool_mfa\plugininfo\factor::STATE_PASS;
}
}
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
}
/**
* IP Range Factor implementation.
* Cannot set state, return true.
*
* @param string $state the state constant to set
* @return bool
*/
public function set_state(string $state): bool {
return true;
}
/**
* IP Range Factor implementation.
* User can influence state prior to login.
* Possible states are either neutral or pass.
*
* @param stdClass $user
*/
public function possible_states(stdClass $user): array {
return [
\tool_mfa\plugininfo\factor::STATE_PASS,
\tool_mfa\plugininfo\factor::STATE_NEUTRAL,
];
}
}
@@ -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_iprange\privacy;
use core_privacy\local\metadata\null_provider;
/**
* Privacy provider.
*
* @package factor_iprange
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @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';
}
}
@@ -0,0 +1,33 @@
<?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_iprange
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['allowedipsempty'] = 'Nobody will currently pass this factor! You can add your own IP address (<i>{$a->ip}</i>)';
$string['allowedipshasmyip'] = 'Your IP (<i>{$a->ip}</i>) is in the list and you will pass this factor.';
$string['allowedipshasntmyip'] = 'Your IP (<i>{$a->ip}</i>) is not in the list and you will not pass this factor.';
$string['pluginname'] = 'IP range';
$string['privacy:metadata'] = 'The IP range factor plugin does not store any personal data.';
$string['settings:safeips'] = 'Safe IP ranges';
$string['settings:safeips_help'] = 'Enter a list of IP addresses or subnets to be counted as a pass in factor. If empty nobody will pass this factor. {$a->info} {$a->syntax}';
$string['summarycondition'] = 'is on a secured network';
@@ -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/>.
/**
* Settings
*
* @package factor_iprange
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $OUTPUT;
$enabled = new admin_setting_configcheckbox('factor_iprange/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('iprange', get_config('factor_iprange', 'enabled') ? 'enable' : 'disable');
});
$settings->add($enabled);
$settings->add(new admin_setting_configtext('factor_iprange/weight',
new lang_string('settings:weight', 'tool_mfa'),
new lang_string('settings:weight_help', 'tool_mfa'), 100, PARAM_INT));
// Current IP validation against list for description.
$allowedips = get_config('factor_iprange', 'safeips');
if (trim($allowedips) == '') {
$message = 'allowedipsempty';
$type = 'notifyerror';
} else if (remoteip_in_list($allowedips)) {
$message = 'allowedipshasmyip';
$type = 'notifysuccess';
} else {
$message = 'allowedipshasntmyip';
$type = 'notifyerror';
};
$info = $OUTPUT->notification(get_string($message, 'factor_iprange', ['ip' => getremoteaddr()]), $type);
$settings->add(new admin_setting_configiplist('factor_iprange/safeips',
new lang_string('settings:safeips', 'factor_iprange'),
new lang_string('settings:safeips_help', 'factor_iprange',
['info' => $info, 'syntax' => get_string('ipblockersyntax', 'admin')]), '', PARAM_TEXT));
+31
View File
@@ -0,0 +1,31 @@
<?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_iprange
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'factor_iprange'; // Full name of the plugin (used for diagnostics).
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,128 @@
<?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_nosetup;
use stdClass;
use tool_mfa\local\factor\object_factor_base;
/**
* No setup factor class.
*
* @package factor_nosetup
* @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 {
/**
* No Setup Factor implementation.
* Factor is a singleton, can only be one instance.
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_all_user_factors(stdClass $user): array {
global $DB;
$records = $DB->get_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]);
if (!empty($records)) {
return $records;
}
// Null records returned, build new record.
$record = [
'userid' => $user->id,
'factor' => $this->name,
'timecreated' => time(),
'createdfromip' => $user->lastip,
'timemodified' => time(),
'revoked' => 0,
];
$record['id'] = $DB->insert_record('tool_mfa', $record, true);
return [(object) $record];
}
/**
* No Setup Factor implementation.
* Factor does not have input.
*
* {@inheritDoc}
*/
public function has_input(): bool {
return false;
}
/**
* No Setup Factor implementation.
* State check is performed here, as there is no form to do it in.
*
* {@inheritDoc}
*/
public function get_state(): string {
// Check if user has any other input or setup factors active.
$factors = \tool_mfa\plugininfo\factor::get_active_user_factor_types();
foreach ($factors as $factor) {
if ($factor->has_input() || $factor->has_setup()) {
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
}
}
return \tool_mfa\plugininfo\factor::STATE_PASS;
}
/**
* No setup implementation.
* Copy of get_state, but can take other user..
*
* @param stdClass $user
* @return void
*/
public function possible_states(stdClass $user): array {
// We return Neutral here because to support optional rollouts
// it needs to report neutral or the menu to setup will not display.
return [\tool_mfa\plugininfo\factor::STATE_NEUTRAL];
}
/**
* No Setup Factor implementation.
* The state can never be set. Always return true.
*
* @param string $state the state constant to set
* @return bool
*/
public function set_state(string $state): bool {
return true;
}
/**
* No Setup Factor implementation.
* nosetup should not be a valid combination with another factor.
*
* @param array $combination array of factors that make up the combination
* @return bool
*/
public function check_combination(array $combination): bool {
// If this combination has more than 1 factor that has setup or input, not valid.
foreach ($combination as $factor) {
if ($factor->has_setup() || $factor->has_input()) {
return false;
}
}
return true;
}
}
@@ -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_nosetup\privacy;
use core_privacy\local\metadata\null_provider;
/**
* Privacy provider.
*
* @package factor_nosetup
* @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';
}
}
@@ -0,0 +1,86 @@
<?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/>.
/**
* Scheduled task to revoke unusable factors that will never pass.
*
* @package factor_nosetup
* @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_nosetup\task;
/**
* Scheduled task to add log events into DB table.
*/
class delete_unusable_factors extends \core\task\scheduled_task {
/**
* Return the task's name as shown in admin screens.
*
* @return string
*/
public function get_name(): string {
return get_string('deleteunusablefactors', 'factor_nosetup');
}
/**
* Execute the task.
*
* @return void
*/
public function execute(): void {
mtrace('Starting to revoke unusable Nosetup factors');
$this->revoke_factors();
}
/**
* Revokes all nosetup factors that will now always fail.
*
* @return void
*/
private function revoke_factors(): void {
global $DB;
$factorobject = \tool_mfa\plugininfo\factor::get_factor('nosetup');
// We need to get all nosetup factors, and check that for ones that no longer have a pass state.
$allfactorssql = "SELECT DISTINCT tm.userid
FROM {tool_mfa} tm
JOIN {user} u ON u.id = tm.userid
WHERE tm.factor = :factor
AND u.suspended = 0
AND u.deleted = 0
AND (
SELECT COUNT(id) as count
FROM {tool_mfa}
WHERE userid = tm.userid
AND factor <> :notfactor
) > 0";
$useridrecordset = $DB->get_recordset_sql($allfactorssql, ['factor' => 'nosetup', 'notfactor' => 'nosetup']);
foreach ($useridrecordset as $userid) {
// If pass state is no longer possible, add delete user factor.
$user = \core_user::get_user($userid->userid);
if (!in_array(\tool_mfa\plugininfo\factor::STATE_PASS, $factorobject->possible_states($user))) {
$factorobject->delete_factor_for_user($user);
}
}
$useridrecordset->close();
mtrace('Finished revoking unusable Nosetup factors');
}
}
@@ -0,0 +1,38 @@
<?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/>.
/**
* Task scheduler
*
* @package factor_nosetup
* @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();
$tasks = [
[
'classname' => 'factor_nosetup\task\delete_unusable_factors',
'blocking' => 0,
'minute' => 'R',
'hour' => '0',
'day' => '*',
'month' => '*',
'dayofweek' => '*',
],
];
@@ -0,0 +1,30 @@
<?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_nosetup
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['deleteunusablefactors'] = 'Delete unusable Nosetup factors';
$string['info'] = 'This factor passes if the user has no other factors set up.';
$string['pluginname'] = 'No other factors';
$string['privacy:metadata'] = 'The No other factors plugin does not store any personal data';
$string['summarycondition'] = 'has no other factors set up';
@@ -0,0 +1,38 @@
<?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_nosetup
* @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();
$enabled = new admin_setting_configcheckbox('factor_nosetup/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('nosetup', get_config('factor_nosetup', 'enabled') ? 'enable' : 'disable');
});
$settings->add($enabled);
$settings->add(new admin_setting_configtext('factor_nosetup/weight',
new lang_string('settings:weight', 'tool_mfa'),
new lang_string('settings:weight_help', 'tool_mfa'), 100, PARAM_INT));
+32
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_nosetup
* @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 = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'factor_nosetup'; // Full name of the plugin (used for diagnostics).
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,182 @@
<?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_role;
use stdClass;
use tool_mfa\local\factor\object_factor_base;
/**
* Role factor class.
*
* @package factor_role
* @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 {
/**
* Role implementation.
* This factor is a singleton, return single instance.
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_all_user_factors(stdClass $user): array {
global $DB;
$records = $DB->get_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]);
if (!empty($records)) {
return $records;
}
// Null records returned, build new record.
$record = [
'userid' => $user->id,
'factor' => $this->name,
'timecreated' => time(),
'createdfromip' => $user->lastip,
'timemodified' => time(),
'revoked' => 0,
];
$record['id'] = $DB->insert_record('tool_mfa', $record, true);
return [(object) $record];
}
/**
* Role implementation.
* Factor has no input
*
* {@inheritDoc}
*/
public function has_input(): bool {
return false;
}
/**
* Role implementation.
* Checks whether the user has selected roles in any context.
*
* {@inheritDoc}
*/
public function get_state(): string {
global $USER;
$rolestring = get_config('factor_role', 'roles');
// Nothing selected, everyone passes.
if (empty($rolestring)) {
return \tool_mfa\plugininfo\factor::STATE_PASS;
}
$selected = explode(',', $rolestring);
$syscon = \context_system::instance();
$specials = get_user_roles_with_special($syscon, $USER->id);
// Transform the special roles to the matching format.
$specials = array_map(function ($el) {
return $el->roleid;
}, $specials);
foreach ($selected as $id) {
if ($id === 'admin') {
if (is_siteadmin()) {
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
}
} else {
if (user_has_role_assignment($USER->id, $id)) {
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
}
// Some system default roles do not have an explicit binding. eg Authenticated user.
if (in_array((int) $id, $specials)) {
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
}
}
}
// If we got here, no roles matched, allow access.
return \tool_mfa\plugininfo\factor::STATE_PASS;
}
/**
* Role implementation.
* Cannot set state, return true.
*
* @param string $state the state constant to set
* @return bool
*/
public function set_state(string $state): bool {
return true;
}
/**
* Role implementation.
* User can not influence. Result is whatever current state is.
*
* @param stdClass $user
* @return array
*/
public function possible_states(stdClass $user): array {
return [$this->get_state()];
}
/**
* Role implementation
* Formats the role list nicely.
*
* {@inheritDoc}
*/
public function get_summary_condition(): string {
$selectedroles = get_config('factor_role', 'roles');
if (empty($selectedroles)) {
return get_string('summarycondition', 'factor_role', get_string('none'));
}
$selectedroles = $this->get_roles(explode(',', $selectedroles));
if (empty($selectedroles)) {
return get_string('summarycondition', 'factor_role', get_string('none'));
}
return get_string('summarycondition', 'factor_role', implode(', ', $selectedroles));
}
/**
* Get roles information by given ids.
*
* @param array $selectedroles List of role ids.
* @return array
*/
public function get_roles(array $selectedroles): array {
global $DB;
$roles = [];
// Checks for admin role and gets its role name.
if (in_array('admin', $selectedroles)) {
$roles[] = get_string('administrator');
}
$integerroles = array_map('intval', $selectedroles);
// Gets role name for all non admin roles.
if (!empty($integerroles)) {
[$insql, $inparams] = $DB->get_in_or_equal($integerroles);
$otherroles = $DB->get_records_select('role', 'id ' . $insql, $inparams);
$otherrolenames = role_fix_names($otherroles, null, ROLENAME_ALIAS, true);
$roles = array_merge($roles, $otherrolenames);
}
return $roles;
}
}
@@ -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_role\privacy;
use core_privacy\local\metadata\null_provider;
/**
* Privacy provider.
*
* @package factor_role
* @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';
}
}
@@ -0,0 +1,30 @@
<?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_role
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['pluginname'] = 'Role';
$string['privacy:metadata'] = 'The Role factor plugin does not store any personal data.';
$string['settings:roles'] = 'Non-passing roles';
$string['settings:roles_help'] = 'Select the roles that will not pass this factor. This allows you to force these roles to use other factors to authenticate.';
$string['summarycondition'] = 'does NOT have any of the following roles assigned in any context: {$a}';
+48
View File
@@ -0,0 +1,48 @@
<?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_role
* @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();
$enabled = new admin_setting_configcheckbox('factor_role/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('role', get_config('factor_role', 'enabled') ? 'enable' : 'disable');
});
$settings->add($enabled);
$settings->add(new admin_setting_configtext('factor_role/weight',
new lang_string('settings:weight', 'tool_mfa'),
new lang_string('settings:weight_help', 'tool_mfa'), 100, PARAM_INT));
$choices = ['admin' => get_string('administrator')];
$roles = get_all_roles();
foreach ($roles as $role) {
$choices[$role->id] = role_get_name($role);
}
$settings->add(new admin_setting_configmultiselect('factor_role/roles',
new lang_string('settings:roles', 'factor_role'),
new lang_string('settings:roles_help', 'factor_role'), ['admin'], $choices));
@@ -0,0 +1,126 @@
<?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_role;
/**
* Tests for role factor.
*
* @covers \factor_role\factor
* @package factor_role
* @copyright 2023 Stevani Andolo <stevani@hotmail.com.au>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor_test extends \advanced_testcase {
/**
* Tests getting the summary condition
*
* @covers ::get_summary_condition
* @covers ::get_roles
*/
public function test_get_summary_condition(): void {
global $DB;
$this->resetAfterTest();
$managerrole = $DB->get_record('role', ['shortname' => 'manager']);
$teacherrole = $DB->get_record('role', ['shortname' => 'teacher']);
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
$adminrolename = get_string('administrator');
$managerrolename = role_get_name($managerrole);
$teacherrolename = role_get_name($teacherrole);
$studentrolename = role_get_name($studentrole);
set_config('enabled', 1, 'factor_role');
$rolefactor = \tool_mfa\plugininfo\factor::get_factor('role');
// Admin is disabled by default in this factor.
$selectedroles = get_config('factor_role', 'roles');
$selectedroles = $rolefactor->get_roles(explode(',', $selectedroles));
$this->assertContains($adminrolename, $selectedroles);
$this->assertNotContains($managerrolename, $selectedroles);
$this->assertNotContains($teacherrolename, $selectedroles);
$this->assertNotContains($studentrolename, $selectedroles);
$this->assertStringContainsString(
implode(', ', $selectedroles),
$rolefactor->get_summary_condition()
);
// Disabled role factor for managers.
set_config('roles', $managerrole->id, 'factor_role');
$selectedroles = get_config('factor_role', 'roles');
$selectedroles = $rolefactor->get_roles(explode(',', $selectedroles));
$this->assertNotContains($adminrolename, $selectedroles);
$this->assertContains($managerrolename, $selectedroles);
$this->assertNotContains($teacherrolename, $selectedroles);
$this->assertNotContains($studentrolename, $selectedroles);
$this->assertStringContainsString(
implode(', ', $selectedroles),
$rolefactor->get_summary_condition()
);
// Disabled role factor for teachers.
set_config('roles', $teacherrole->id, 'factor_role');
$selectedroles = get_config('factor_role', 'roles');
$selectedroles = $rolefactor->get_roles(explode(',', $selectedroles));
$this->assertNotContains($adminrolename, $selectedroles);
$this->assertNotContains($managerrolename, $selectedroles);
$this->assertContains($teacherrolename, $selectedroles);
$this->assertNotContains($studentrolename, $selectedroles);
$this->assertStringContainsString(
implode(', ', $selectedroles),
$rolefactor->get_summary_condition()
);
// Disabled role factor for students.
set_config('roles', $studentrole->id, 'factor_role');
$selectedroles = get_config('factor_role', 'roles');
$selectedroles = $rolefactor->get_roles(explode(',', $selectedroles));
$this->assertNotContains($adminrolename, $selectedroles);
$this->assertNotContains($managerrolename, $selectedroles);
$this->assertNotContains($teacherrolename, $selectedroles);
$this->assertContains($studentrolename, $selectedroles);
$this->assertStringContainsString(
implode(', ', $selectedroles),
$rolefactor->get_summary_condition()
);
// Disabled role factor for admins, managers, teachers and students.
set_config('roles', "admin,$managerrole->id,$teacherrole->id,$studentrole->id", 'factor_role');
$selectedroles = get_config('factor_role', 'roles');
$selectedroles = $rolefactor->get_roles(explode(',', $selectedroles));
$this->assertContains($adminrolename, $selectedroles);
$this->assertContains($managerrolename, $selectedroles);
$this->assertContains($teacherrolename, $selectedroles);
$this->assertContains($studentrolename, $selectedroles);
$this->assertStringContainsString(
implode(', ', $selectedroles),
$rolefactor->get_summary_condition()
);
// Enable all roles.
unset_config('roles', 'factor_role');
$this->assertEquals(
get_string('summarycondition', 'factor_role', get_string('none')),
$rolefactor->get_summary_condition()
);
}
}
+32
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_role
* @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 = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'factor_role'; // Full name of the plugin (used for diagnostics).
$plugin->maturity = MATURITY_STABLE;
@@ -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');
}
}
@@ -0,0 +1,460 @@
<?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');
}
/**
* Gets the string for manage button on preferences page.
*
* @return string
*/
public function get_manage_string(): string {
return get_string('managefactorbutton', '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 {
return true;
}
/**
* 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);
}
}
}
@@ -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;
}
}
@@ -0,0 +1,153 @@
<?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 core\aws\admin_settings_aws_region;
use core\aws\aws_helper;
use factor_sms\event\sms_sent;
/**
* 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 {
$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;
}
}

Some files were not shown because too many files have changed in this diff Show More