878 lines
29 KiB
PHP
878 lines
29 KiB
PHP
<?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);
|
|
}
|
|
}
|