01148a0816
While this change is not 100% required now, it's good habit and we are checking for it since Moodle 4.4. All the changes in this commit have been applied automatically using the moodle.PHPUnit.TestReturnType sniff and are, exclusively adding the ": void" return types when missing.
452 lines
19 KiB
PHP
452 lines
19 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 tool_mfa\tool_mfa_trait;
|
|
|
|
defined('MOODLE_INTERNAL') || die();
|
|
require_once(__DIR__ . '/tool_mfa_trait.php');
|
|
|
|
/**
|
|
* Tests for MFA manager 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_test extends \advanced_testcase {
|
|
|
|
use tool_mfa_trait;
|
|
|
|
/**
|
|
* Tests getting the factor total weight
|
|
*
|
|
* @covers ::get_total_weight
|
|
* @covers ::setup_user_factor
|
|
*/
|
|
public function test_get_total_weight(): void {
|
|
$this->resetAfterTest(true);
|
|
|
|
// Create and login a user.
|
|
$user = $this->getDataGenerator()->create_user();
|
|
$this->setUser($user);
|
|
|
|
// First get weight with no active factors.
|
|
$this->assertEquals(0, \tool_mfa\manager::get_total_weight());
|
|
|
|
// Now setup a couple of input based factors.
|
|
$this->set_factor_state('totp', 1, 100);
|
|
|
|
$this->set_factor_state('email', 1, 100);
|
|
|
|
// Check weight is still 0 with no passes.
|
|
$this->assertEquals(0, \tool_mfa\manager::get_total_weight());
|
|
|
|
// Manually pass 1 .
|
|
$factor = \tool_mfa\plugininfo\factor::get_factor('totp');
|
|
$totpdata = [
|
|
'secret' => 'fakekey',
|
|
'devicename' => 'fakedevice',
|
|
];
|
|
$this->assertNotEmpty($factor->setup_user_factor((object) $totpdata));
|
|
$factor->set_state(\tool_mfa\plugininfo\factor::STATE_PASS);
|
|
$this->assertEquals(100, \tool_mfa\manager::get_total_weight());
|
|
|
|
// Now both.
|
|
$factor2 = \tool_mfa\plugininfo\factor::get_factor('email');
|
|
$factor2->set_state(\tool_mfa\plugininfo\factor::STATE_PASS);
|
|
$this->assertEquals(200, \tool_mfa\manager::get_total_weight());
|
|
|
|
// Now setup a no input factor, and check that weight is automatically added without input.
|
|
$this->set_factor_state('auth', 1, 100);
|
|
set_config('goodauth', 'manual', 'factor_auth');
|
|
|
|
$this->assertEquals(300, \tool_mfa\manager::get_total_weight());
|
|
}
|
|
|
|
/**
|
|
* Tests getting the factor status
|
|
*
|
|
* @covers ::get_status
|
|
*/
|
|
public function test_get_status(): void {
|
|
$this->resetAfterTest(true);
|
|
|
|
// Create and login a user.
|
|
$user = $this->getDataGenerator()->create_user();
|
|
$this->setUser($user);
|
|
|
|
// Check for fail status with no factors.
|
|
$this->assertEquals(\tool_mfa\plugininfo\factor::STATE_FAIL, \tool_mfa\manager::get_status());
|
|
|
|
// Now add a no input factor.
|
|
$this->set_factor_state('auth', 1, 100);
|
|
set_config('goodauth', 'manual', 'factor_auth');
|
|
|
|
// Check state is now passing.
|
|
$this->assertEquals(\tool_mfa\plugininfo\factor::STATE_PASS, \tool_mfa\manager::get_status());
|
|
|
|
// Now add a failure state factor, and ensure that fail takes precedent.
|
|
$this->set_factor_state('email', 1, 100);
|
|
$factoremail = \tool_mfa\plugininfo\factor::get_factor('email');
|
|
$factoremail->set_state(\tool_mfa\plugininfo\factor::STATE_FAIL);
|
|
|
|
$this->assertEquals(\tool_mfa\plugininfo\factor::STATE_FAIL, \tool_mfa\manager::get_status());
|
|
|
|
// Remove no input factor, and remove fail state by logging in/out. Simulates no data entered yet.
|
|
$this->setUser(null);
|
|
$this->setUser($user);
|
|
$this->set_factor_state('auth', 0, 100);
|
|
$factoremail->set_state(\tool_mfa\plugininfo\factor::STATE_UNKNOWN);
|
|
|
|
$this->assertEquals(\tool_mfa\plugininfo\factor::STATE_NEUTRAL, \tool_mfa\manager::get_status());
|
|
}
|
|
|
|
/**
|
|
* Tests checking if passed enough factors
|
|
*
|
|
* @covers ::passed_enough_factors
|
|
*/
|
|
public function test_passed_enough_factors(): void {
|
|
$this->resetAfterTest(true);
|
|
|
|
// Create and login a user.
|
|
$user = $this->getDataGenerator()->create_user();
|
|
$this->setUser($user);
|
|
|
|
// Check when no factors are setup.
|
|
$this->assertEquals(false, \tool_mfa\manager::passed_enough_factors());
|
|
|
|
// Setup a no input factor.
|
|
$this->set_factor_state('auth', 1, 100);
|
|
set_config('goodauth', 'manual', 'factor_auth');
|
|
|
|
// Check that is enough to pass.
|
|
$this->assertEquals(true, \tool_mfa\manager::passed_enough_factors());
|
|
|
|
// Lower the weight of the factor.
|
|
$this->set_factor_state('auth', 1, 75);
|
|
$this->assertEquals(false, \tool_mfa\manager::passed_enough_factors());
|
|
|
|
// Add another factor to get enough weight to pass, but dont set pass state yet.
|
|
$this->set_factor_state('email', 1, 100);
|
|
$factoremail = \tool_mfa\plugininfo\factor::get_factor('email');
|
|
$this->assertEquals(false, \tool_mfa\manager::passed_enough_factors());
|
|
|
|
// Now pass the factor and check weight.
|
|
$factoremail->set_state(\tool_mfa\plugininfo\factor::STATE_PASS);
|
|
$this->assertEquals(true, \tool_mfa\manager::passed_enough_factors());
|
|
}
|
|
|
|
/**
|
|
* The data provider for whether urls should be redirected or not
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function should_redirect_urls_provider() {
|
|
$badurl1 = new \moodle_url('/');
|
|
$badparam1 = $badurl1->out();
|
|
$badurl2 = new \moodle_url('admin/tool/mfa/auth.php');
|
|
$badparam2 = $badurl2->out();
|
|
return [
|
|
['/', 'http://test.server', true],
|
|
['/admin/tool/mfa/action.php', 'http://test.server', true],
|
|
['/admin/tool/mfa/factor/totp/settings.php', 'http://test.server', true],
|
|
['/', 'http://test.server', true, ['url' => $badparam1]],
|
|
['/', 'http://test.server', true, ['url' => $badparam2]],
|
|
['/admin/tool/mfa/auth.php', 'http://test.server', false],
|
|
['/admin/tool/mfa/auth.php', 'http://test.server/parent/directory', false],
|
|
['/admin/tool/mfa/action.php', 'http://test.server/parent/directory', true],
|
|
['/', 'http://test.server/parent/directory', true, ['url' => $badparam1]],
|
|
['/', 'http://test.server/parent/directory', true, ['url' => $badparam2]],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Tests whether it should require mfa
|
|
*
|
|
* @covers ::should_require_mfa
|
|
* @param string $urlstring
|
|
* @param string $webroot
|
|
* @param bool $status
|
|
* @param array|null $params
|
|
* @dataProvider should_redirect_urls_provider
|
|
*/
|
|
public function test_should_require_mfa_urls($urlstring, $webroot, $status, $params = null): void {
|
|
$this->resetAfterTest(true);
|
|
global $CFG;
|
|
$user = $this->getDataGenerator()->create_user();
|
|
$this->setUser($user);
|
|
$CFG->wwwroot = $webroot;
|
|
$url = new \moodle_url($urlstring, $params);
|
|
$this->assertEquals($status, \tool_mfa\manager::should_require_mfa($url, false));
|
|
}
|
|
|
|
/**
|
|
* Tests whether it should require the mfa checks
|
|
*
|
|
* @covers ::should_require_mfa
|
|
*/
|
|
public function test_should_require_mfa_checks(): void {
|
|
// Setup test and user.
|
|
global $CFG;
|
|
$this->resetAfterTest(true);
|
|
$user = $this->getDataGenerator()->create_user();
|
|
|
|
$badurl = new \moodle_url('/');
|
|
|
|
// Upgrade checks.
|
|
$this->setAdminUser();
|
|
// Mark the site as upgraded so it will not fail when running the unittest as a whole.
|
|
$CFG->allversionshash = \core_component::get_all_versions_hash();
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
|
|
$oldhash = $CFG->allversionshash;
|
|
$CFG->allversionshash = 'abc';
|
|
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
|
|
$CFG->allversionshash = $oldhash;
|
|
$upgradesettings = new \moodle_url('/admin/upgradesettings.php');
|
|
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($upgradesettings, false));
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
|
|
|
|
// Admin not setup.
|
|
$this->setUser($user);
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
|
|
$CFG->adminsetuppending = 1;
|
|
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
|
|
$CFG->adminsetuppending = 0;
|
|
|
|
// Check prevent_redirect.
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
|
|
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, true));
|
|
|
|
// User not setup properly.
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
|
|
$notsetup = clone($user);
|
|
unset($notsetup->firstname);
|
|
$this->setUser($notsetup);
|
|
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
|
|
$this->setUser($user);
|
|
|
|
// Enrolment.
|
|
$enrolurl = new \moodle_url('/enrol/index.php');
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
|
|
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($enrolurl, false));
|
|
|
|
// Guest User.
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
|
|
$this->setGuestUser();
|
|
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
|
|
$this->setUser($user);
|
|
|
|
// Forced password changes.
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
|
|
set_user_preference('auth_forcepasswordchange', true);
|
|
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
|
|
set_user_preference('auth_forcepasswordchange', false);
|
|
|
|
// Login as check.
|
|
$user2 = $this->getDataGenerator()->create_user();
|
|
$syscontext = \context_system::instance();
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
|
|
$this->setAdminUser();
|
|
\core\session\manager::loginas($user2->id, $syscontext, false);
|
|
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
|
|
$this->setUser($user);
|
|
}
|
|
|
|
/**
|
|
* Tests should require the mfa redirection loop
|
|
*
|
|
* @covers ::should_require_mfa
|
|
*/
|
|
public function test_should_require_mfa_redirection_loop(): void {
|
|
// Setup test and user.
|
|
global $CFG, $SESSION;
|
|
$CFG->wwwroot = 'http://phpunit.test';
|
|
$this->resetAfterTest(true);
|
|
$user = $this->getDataGenerator()->create_user();
|
|
$this->setUser($user);
|
|
|
|
// Set first referer url.
|
|
$_SERVER['HTTP_REFERER'] = 'http://phpunit.test';
|
|
$url = new \moodle_url('/');
|
|
|
|
// Test you get three redirs then exception.
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
|
|
// Set count to threshold.
|
|
$SESSION->mfa_redir_count = 5;
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT_EXCEPTION, \tool_mfa\manager::should_require_mfa($url, false));
|
|
// Reset session vars.
|
|
unset($SESSION->mfa_redir_referer);
|
|
unset($SESSION->mfa_redir_count);
|
|
|
|
// Test 4 different redir urls.
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
|
|
$_SERVER['HTTP_REFERER'] = 'http://phpunit.test/2';
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
|
|
$_SERVER['HTTP_REFERER'] = 'http://phpunit3.test/3';
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
|
|
$_SERVER['HTTP_REFERER'] = 'http://phpunit4.test/4';
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
|
|
// Reset session vars.
|
|
unset($SESSION->mfa_redir_referer);
|
|
unset($SESSION->mfa_redir_count);
|
|
|
|
// Test 6 then jump to new referer (5 + 1 to set the first time).
|
|
$_SERVER['HTTP_REFERER'] = 'http://phpunit.test';
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
|
|
|
|
$_SERVER['HTTP_REFERER'] = 'http://phpunit.test/2';
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
|
|
// Now test that going back to original URL doesnt cause exception.
|
|
$_SERVER['HTTP_REFERER'] = 'http://phpunit.test';
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
|
|
}
|
|
|
|
/**
|
|
* Tests checking for possible setup factor
|
|
*
|
|
* @covers ::possible_factor_setup
|
|
* @covers ::setup_user_factor
|
|
*/
|
|
public function test_possible_factor_setup(): void {
|
|
// Setup test and user.
|
|
$this->resetAfterTest(true);
|
|
$user = $this->getDataGenerator()->create_user();
|
|
$this->setUser($user);
|
|
|
|
// Test for totp is able to be setup.
|
|
set_config('enabled', 1, 'factor_totp');
|
|
$this->assertTrue(\tool_mfa\manager::possible_factor_setup());
|
|
set_config('enabled', 0, 'factor_totp');
|
|
|
|
// Test TOTP is already setup and can be managed.
|
|
$totp = \tool_mfa\plugininfo\factor::get_factor('totp');
|
|
set_config('enabled', 1, 'factor_totp');
|
|
$totpdata = [
|
|
'secret' => 'fakekey',
|
|
'devicename' => 'fakedevice',
|
|
];
|
|
$this->assertNotEmpty($totp->setup_user_factor((object) $totpdata));
|
|
$this->assertTrue(\tool_mfa\manager::possible_factor_setup());
|
|
set_config('enabled', 0, 'factor_totp');
|
|
|
|
// Test no factors can be setup.
|
|
set_config('enabled', 1, 'factor_email');
|
|
set_config('enabled', 1, 'factor_admin');
|
|
$this->assertFalse(\tool_mfa\manager::possible_factor_setup());
|
|
set_config('enabled', 0, 'factor_email');
|
|
set_config('enabled', 0, 'factor_admin');
|
|
}
|
|
|
|
/**
|
|
* Tests checking if a factor is ready
|
|
*
|
|
* @covers ::is_ready
|
|
*/
|
|
public function test_is_ready(): void {
|
|
// Setup test and user.
|
|
global $CFG;
|
|
$this->resetAfterTest(true);
|
|
$user = $this->getDataGenerator()->create_user();
|
|
$this->setUser($user);
|
|
set_config('enabled', 1, 'factor_nosetup');
|
|
set_config('enabled', 1, 'tool_mfa');
|
|
|
|
// Capability Check.
|
|
$this->assertTrue(\tool_mfa\manager::is_ready());
|
|
// Swap to role without capability.
|
|
$this->setGuestUser();
|
|
$this->assertFalse(\tool_mfa\manager::is_ready());
|
|
$this->setUser($user);
|
|
|
|
// Enabled check.
|
|
$this->assertTrue(\tool_mfa\manager::is_ready());
|
|
set_config('enabled', 0, 'tool_mfa');
|
|
$this->assertFalse(\tool_mfa\manager::is_ready());
|
|
set_config('enabled', 1, 'tool_mfa');
|
|
|
|
// Upgrade check.
|
|
$this->assertTrue(\tool_mfa\manager::is_ready());
|
|
$CFG->upgraderunning = true;
|
|
$this->assertFalse(\tool_mfa\manager::is_ready());
|
|
unset($CFG->upgraderunning);
|
|
|
|
// No factors check.
|
|
$this->assertTrue(\tool_mfa\manager::is_ready());
|
|
set_config('enabled', 0, 'factor_nosetup');
|
|
$this->assertFalse(\tool_mfa\manager::is_ready());
|
|
set_config('enabled', 1, 'factor_nosetup');
|
|
}
|
|
|
|
/**
|
|
* Tests core hooks
|
|
*
|
|
* @covers ::mfa_config_hook_test
|
|
* @covers ::mfa_login_hook_test
|
|
*/
|
|
public function test_core_hooks(): void {
|
|
// Setup test and user.
|
|
global $CFG, $SESSION;
|
|
$this->resetAfterTest(true);
|
|
$user = $this->getDataGenerator()->create_user();
|
|
$this->setUser($user);
|
|
|
|
// Require login to fire hooks. Config we get for free.
|
|
require_login();
|
|
|
|
$this->assertTrue($CFG->mfa_config_hook_test);
|
|
$this->assertTrue($SESSION->mfa_login_hook_test);
|
|
}
|
|
|
|
/**
|
|
* Tests circular redirect auth
|
|
*
|
|
* @covers ::should_require_mfa
|
|
*/
|
|
public function test_circular_redirect_auth(): void {
|
|
// Setup test and user.
|
|
$this->resetAfterTest(true);
|
|
$user = $this->getDataGenerator()->create_user();
|
|
$this->setUser($user);
|
|
|
|
// Spoof the referrer for the redirect check.
|
|
$_SERVER['HTTP_REFERER'] = '/admin/tool/mfa/auth.php';
|
|
$baseurl = new \moodle_url('/my/naughty/page.php');
|
|
|
|
// After a single check, we should redirect.
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT,
|
|
\tool_mfa\manager::should_require_mfa($baseurl, false));
|
|
|
|
// Now hammer it up to the threshold to emulate a repeated force browse from auth.php.
|
|
for ($i = 0; $i < \tool_mfa\manager::REDIR_LOOP_THRESHOLD; $i++) {
|
|
\tool_mfa\manager::should_require_mfa($baseurl, false);
|
|
}
|
|
|
|
// Now finally confirm that a 6th access attempt (after loop safety trigger) still redirects.
|
|
$this->assertEquals(\tool_mfa\manager::REDIRECT,
|
|
\tool_mfa\manager::should_require_mfa($baseurl, false));
|
|
}
|
|
}
|