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
@@ -0,0 +1,228 @@
<?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 admin settings
*
* @package tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class admin_setting_managemfa_test extends \advanced_testcase {
use tool_mfa_trait;
/**
* Tests getting the factor combinations
*
* @covers ::get_factor_combinations
*/
public function test_get_factor_combinations_default(): void {
$namagemfa = new \tool_mfa\local\admin_setting_managemfa();
$factors = \tool_mfa\plugininfo\factor::get_enabled_factors();
$combinations = $namagemfa->get_factor_combinations($factors, 0, count($factors) - 1);
$this->assertEquals(0, count($factors));
$this->assertEquals(0, count($combinations));
}
/**
* Data provider for test_get_factor_combinations_with_data_provider().
*
* @return array
*/
public function get_factor_combinations_provider() {
$provider = [];
$factors = [];
$provider[] = [$factors, 0];
$factors = [];
$factors[] = ['name' => 'totp', 'enabled' => 1, 'weight' => 90];
$provider[] = [$factors, 0];
$factors = [];
$factors[] = ['name' => 'email', 'enabled' => 1, 'weight' => 100];
$provider[] = [$factors, 1];
$factors = [];
$factors[] = ['name' => 'iprange', 'enabled' => 1, 'weight' => 150];
$provider[] = [$factors, 1];
$factors = [];
$factors[] = ['name' => 'iprange', 'enabled' => 1, 'weight' => 40];
$factors[] = ['name' => 'email', 'enabled' => 1, 'weight' => 40];
$provider[] = [$factors, 0];
$factors = [];
$factors[] = ['name' => 'email', 'enabled' => 1, 'weight' => 90];
$factors[] = ['name' => 'totp', 'enabled' => 1, 'weight' => 40];
$provider[] = [$factors, 1];
$factors = [];
$factors[] = ['name' => 'totp', 'enabled' => 1, 'weight' => 100];
$factors[] = ['name' => 'email', 'enabled' => 1, 'weight' => 100];
$provider[] = [$factors, 2];
$factors = [];
$factors[] = ['name' => 'totp', 'enabled' => 1, 'weight' => 100];
$factors[] = ['name' => 'email', 'enabled' => 1, 'weight' => 100];
$factors[] = ['name' => 'iprange', 'enabled' => 1, 'weight' => 100];
$provider[] = [$factors, 3];
$factors = [];
$factors[] = ['name' => 'totp', 'enabled' => 1, 'weight' => 90];
$factors[] = ['name' => 'email', 'enabled' => 1, 'weight' => 30];
$factors[] = ['name' => 'iprange', 'enabled' => 1, 'weight' => 40];
$provider[] = [$factors, 2];
$factors = [];
$factors[] = ['name' => 'email', 'enabled' => 1, 'weight' => 30];
$factors[] = ['name' => 'iprange', 'enabled' => 1, 'weight' => 40];
$factors[] = ['name' => 'totp', 'enabled' => 1, 'weight' => 90];
$provider[] = [$factors, 3];
$factors = [];
$factors[] = ['name' => 'email', 'enabled' => 1, 'weight' => 30];
$factors[] = ['name' => 'iprange', 'enabled' => 1, 'weight' => 40];
$factors[] = ['name' => 'totp', 'enabled' => 1, 'weight' => 90];
$factors[] = ['name' => 'auth', 'enabled' => 1, 'weight' => 90];
$provider[] = [$factors, 7];
$factors = [];
$factors[] = ['name' => 'email', 'enabled' => 1, 'weight' => 50];
$factors[] = ['name' => 'iprange', 'enabled' => 1, 'weight' => 50];
$factors[] = ['name' => 'totp', 'enabled' => 1, 'weight' => 50];
$factors[] = ['name' => 'auth', 'enabled' => 1, 'weight' => 50];
$provider[] = [$factors, 6];
$factors = [];
$factors[] = ['name' => 'email', 'enabled' => 0, 'weight' => 50];
$factors[] = ['name' => 'iprange', 'enabled' => 1, 'weight' => 50];
$factors[] = ['name' => 'totp', 'enabled' => 0, 'weight' => 50];
$factors[] = ['name' => 'auth', 'enabled' => 1, 'weight' => 50];
$provider[] = [$factors, 1];
$factors = [];
$factors[] = ['name' => 'email', 'enabled' => 0, 'weight' => 50];
$factors[] = ['name' => 'iprange', 'enabled' => 1, 'weight' => 50];
$factors[] = ['name' => 'totp', 'enabled' => 1, 'weight' => 50];
$factors[] = ['name' => 'auth', 'enabled' => 1, 'weight' => 50];
$provider[] = [$factors, 3];
return $provider;
}
/**
* Tests getting the factor combinations with data provider
*
* @covers ::get_factor_combinations
* @dataProvider get_factor_combinations_provider
* @param array $factorset configured factors
* @param int $combinationscount expected count of available combinations
*/
public function test_get_factor_combinations_with_data_provider($factorset, $combinationscount): void {
$this->resetAfterTest();
$enabledcount = 0;
foreach ($factorset as $factor) {
$this->set_factor_state($factor['name'], $factor['enabled'], $factor['weight']);
if ($factor['enabled'] == 1) {
$enabledcount++;
}
}
$managemfa = new \tool_mfa\local\admin_setting_managemfa();
$factors = \tool_mfa\plugininfo\factor::get_enabled_factors();
$combinations = $managemfa->get_factor_combinations($factors, 0, count($factors) - 1);
foreach ($combinations as $combination) {
$this->assertGreaterThanOrEqual(100, $combination['totalweight']);
$this->assertLessThan(200, $combination['totalweight']);
$this->assertGreaterThanOrEqual(1, count($combination['combination']));
foreach ($combination['combination'] as $combinationfactor) {
$this->assertInstanceOf('\tool_mfa\local\factor\object_factor', $combinationfactor);
}
}
$this->assertEquals($enabledcount, count($factors));
$this->assertEquals($combinationscount, count($combinations));
}
/**
* Tests checking the factor combinations
*
* @covers ::get_factor_combinations
*/
public function test_factor_combination_checker(): void {
$this->resetAfterTest();
$managemfa = new \tool_mfa\local\admin_setting_managemfa();
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
// Test combination with 2 valid compatible factors.
$this->set_factor_state('email', 1, 50);
$this->set_factor_state('totp', 1, 50);
// Check that there is only 1 valid combination.
$factors = \tool_mfa\plugininfo\factor::get_enabled_factors();
$combinations = $managemfa->get_factor_combinations($factors, 0, count($factors) - 1);
$this->assertEquals(1, count($combinations));
// Change weights to 100 for each, and check for 2 valid.
$this->set_factor_state('email', 1, 100);
$this->set_factor_state('totp', 1, 100);
$combinations = $managemfa->get_factor_combinations($factors, 0, count($factors) - 1);
$this->assertEquals(2, count($combinations));
// Add another compatible factors, and check for 3 combinations.
$this->set_factor_state('email', 1, 50);
$this->set_factor_state('totp', 1, 50);
$this->set_factor_state('iprange', 1, 50);
$factors = \tool_mfa\plugininfo\factor::get_enabled_factors();
$combinations = $managemfa->get_factor_combinations($factors, 0, count($factors) - 1);
$this->assertEquals(3, count($combinations));
// Now same tests again, with an invalid combination set.
$this->set_factor_state('email', 1, 100);
$this->set_factor_state('totp', 0, 100);
$this->set_factor_state('iprange', 0, 50);
$this->set_factor_state('nosetup', 1, 100);
$factors = \tool_mfa\plugininfo\factor::get_enabled_factors();
$combinations = $managemfa->get_factor_combinations($factors, 0, count($factors) - 1);
$this->assertEquals(2, count($combinations));
$this->set_factor_state('totp', 1, 50);
$this->set_factor_state('email', 0, 50);
$this->set_factor_state('nosetup', 1, 50);
$combinations = $managemfa->get_factor_combinations($factors, 0, count($factors) - 1);
$this->assertEquals(0, count($combinations));
$this->set_factor_state('email', 1, 50);
$this->set_factor_state('nosetup', 1, 50);
$this->set_factor_state('totp', 1, 50);
$factors = \tool_mfa\plugininfo\factor::get_enabled_factors();
$combinations = $managemfa->get_factor_combinations($factors, 0, count($factors) - 1);
$this->assertEquals(1, count($combinations));
}
}
@@ -0,0 +1,69 @@
@tool @tool_mfa
Feature: Set up and manage user factors
In order to set up or manage my user factor
As a user
I need to configure the user factor settings in my preferences
Background:
Given I log in as "admin"
And the following config values are set as admin:
| enabled | 1 | tool_mfa |
Scenario: I see the correct buttons for factor setup and management displayed
Given the following config values are set as admin:
| enabled | 1 | factor_email |
And the following config values are set as admin:
| enabled | 1 | factor_webauthn |
And the following config values are set as admin:
| enabled | 1 | factor_totp |
And the following "tool_mfa > User factors" exist:
| username | factor | label |
| admin | email | test@test.com |
| admin | webauthn | MacBook |
And I follow "Preferences" in the user menu
When I click on "Multi-factor authentication preferences" "link"
# This is the only factor not yet set up.
Then I should not see "Active" in the "#factor-card-totp" "css_element"
# The following factors are already set up.
And I should see "Active" in the "#factor-card-email" "css_element"
And I should see "Active" in the "#factor-card-webauthn" "css_element"
And I click on "Set up authenticator app" "button"
And I should see "Set up authenticator app"
And I click on "Cancel" "button"
And I click on "Manage security key" "button"
And I should see "Manage security key"
@javascript
Scenario: I can revoke a factor only when there is more than one active factor
Given the following config values are set as admin:
| enabled | 1 | factor_webauthn |
And the following config values are set as admin:
| enabled | 1 | factor_sms |
And the following "tool_mfa > User factors" exist:
| username | factor | label |
| admin | sms | +409111222 |
| admin | webauthn | MacBook |
And I follow "Preferences" in the user menu
And I click on "Multi-factor authentication preferences" "link"
And I click on "Manage SMS" "button"
And I click on "Remove" "button" in the "+409111222" "table_row"
When I click on "Yes, remove" "button" in the "Remove '+409111222' SMS?" "dialogue"
Then I should see "'SMS mobile phone - +409111222' successfully removed"
# Now there is only one active factor left.
And I click on "Manage security key" "button"
And I should see "Replace" in the "MacBook" "table_row"
And I should not see "Remove" in the "MacBook" "table_row"
@javascript
Scenario: I can replace a factor
Given the following config values are set as admin:
| enabled | 1 | factor_webauthn |
And the following "tool_mfa > User factors" exist:
| username | factor | label |
| admin | webauthn | MacBook |
And I follow "Preferences" in the user menu
And I click on "Multi-factor authentication preferences" "link"
And I click on "Manage security key" "button"
And I click on "Replace" "button" in the "MacBook" "table_row"
When I click on "Yes, replace" "button" in the "Replace 'MacBook' security key?" "dialogue"
Then I should see "Replace security key"
@@ -0,0 +1,46 @@
<?php
// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
/**
* Behat data generator for tool_mfa.
*
* @package tool_mfa
* @category test
* @copyright 2024 David Woloszyn <david.woloszyn@moodle.com>
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_tool_mfa_generator extends behat_generator_base {
/**
* Get the list of creatable entities for a tool_mfa.
*
* @return array
*/
protected function get_creatable_entities(): array {
return [
'User factors' => [
'singular' => 'User factor',
'datagenerator' => 'user_factors',
'required' => [
'username',
'factor',
'label',
],
],
];
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/../../lib.php');
/**
* Data generator for tool_mfa plugin.
*
* @package tool_mfa
* @category test
* @copyright 2024 David Woloszyn <david.woloszyn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class tool_mfa_generator extends component_generator_base {
/**
* Create user factors.
*
* @param array $record
* @return stdClass
*/
public function create_user_factors(array $record): \stdClass {
global $DB;
$factorobject = \tool_mfa\plugininfo\factor::get_factor($record['factor']);
if (!$factorobject) {
throw new coding_exception('Unknown factor supplied.');
}
$user = $DB->get_record('user', ['username' => $record['username']]);
if (!$user) {
throw new coding_exception('No user found with that username.');
}
$record = (object) array_merge([
'userid' => $user->id,
'secret' => '555553',
'timecreated' => time() - DAYSECS,
'createdfromip' => '0:0:0:0:0:0:0:1',
'timemodified' => time() - MINSECS,
'lastverified' => time(),
'revoked' => 0,
'lockcounter' => 0,
], $record);
$record->id = $DB->insert_record('tool_mfa', $record);
return $record;
}
}
+451
View File
@@ -0,0 +1,451 @@
<?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));
}
}
@@ -0,0 +1,118 @@
<?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 base factor implementation methods.
*
* @package tool_mfa
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright 2023 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class object_factor_base_test extends \advanced_testcase {
use tool_mfa_trait;
/**
* Test deleting user's configured factors
*
* @covers ::setup_user_factor
* @return void
*/
public function test_revoke_user_factor(): void {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$this->set_factor_state('totp', 1, 100);
$totpfactor = \tool_mfa\plugininfo\factor::get_factor('totp');
$totpdata = [
'secret' => 'fakekey',
'devicename' => 'fakedevice',
];
$factorinstance = $totpfactor->setup_user_factor((object) $totpdata);
$totpdata2 = [
'secret' => 'fakekey2',
'devicename' => 'fakedevice2',
];
$totpfactor->setup_user_factor((object) $totpdata2);
$this->assertFalse((bool) $factorinstance->revoked);
$this->assertEquals(2, count($totpfactor->get_active_user_factors($user)));
// Test that calling the revoke on the generic type revokes all.
$totpfactor->revoke_user_factor();
$this->assertEquals(0, count($totpfactor->get_active_user_factors($user)));
// Add another factor for testing.
$totpdata3 = [
'secret' => 'fakekey3',
'devicename' => 'fakedevice3',
];
$factorinstance2 = $totpfactor->setup_user_factor((object) $totpdata3);
// Now test you can't revoke another users factor.
$user2 = $this->getDataGenerator()->create_user();
$this->setUser($user2);
$this->assertFalse($totpfactor->revoke_user_factor($factorinstance2->id));
$this->assertEquals(1, count($totpfactor->get_active_user_factors($user)));
// Now revoke as ourselves.
$this->setUser($user);
$this->assertTrue($totpfactor->revoke_user_factor($factorinstance2->id));
$this->assertEquals(0, count($totpfactor->get_active_user_factors($user)));
}
/**
* Tests the replacement of a factor.
*
* @covers ::setup_user_factor
* @covers ::replace_user_factor
*/
public function test_replace_user_factor(): void {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$factor = \tool_mfa\plugininfo\factor::get_factor('totp');
// Set up the factor.
$data1 = new \stdClass();
$data1->secret = 'fakesecret1';
$data1->devicename = 'fakedevice1';
$factor1 = $factor->setup_user_factor($data1);
// Prepare some replacement data.
$data2 = new \stdClass();
$data2->secret = 'fakesecret2';
$data2->devicename = 'fakedevice2';
// Replace the active factor with the replacement data.
$factor2 = $factor->replace_user_factor($data2, $factor1->id);
// Check the active factor is the newer one.
$activefactors = $factor->get_active_user_factors($user);
$this->assertEquals(1, count($activefactors));
$this->assertEquals($factor2->id, $activefactors[0]->id);
}
}
@@ -0,0 +1,122 @@
<?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;
/**
* Tests for plugininfo.
*
* @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 plugininfo_factor_test extends \advanced_testcase {
/**
* Tests getting next user factor
*
* @covers ::get_next_user_login_factor
* @covers ::setup_user_factor
* @covers ::get_enabled_factors
* @covers ::is_enabled
* @covers ::has_setup
* @covers ::get_active_user_factor_types
*/
public function test_get_next_user_login_factor(): void {
$this->resetAfterTest(true);
// Create and login a user.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
// Test that with no enabled factors, fallback is returned.
$this->assertEquals('fallback', \tool_mfa\plugininfo\factor::get_next_user_login_factor()->name);
// Setup enabled totp factor for user.
set_config('enabled', 1, 'factor_totp');
$totpfactor = \tool_mfa\plugininfo\factor::get_factor('totp');
$totpdata = [
'secret' => 'fakekey',
'devicename' => 'fakedevice',
];
$this->assertNotEmpty($totpfactor->setup_user_factor((object) $totpdata));
// Test that factor now appears (from STATE_UNKNOWN).
$this->assertEquals('totp', \tool_mfa\plugininfo\factor::get_next_user_login_factor()->name);
// Now pass this factor, check for fallback.
$totpfactor->set_state(\tool_mfa\plugininfo\factor::STATE_PASS);
$this->assertEquals('fallback', \tool_mfa\plugininfo\factor::get_next_user_login_factor()->name);
// Add in a no-input factor.
set_config('enabled', 1, 'factor_auth');
$this->assertEquals(2, count(\tool_mfa\plugininfo\factor::get_enabled_factors()));
$authfactor = \tool_mfa\plugininfo\factor::get_factor('auth');
$this->assertTrue($authfactor->is_enabled());
$this->assertFalse($authfactor->has_setup());
// Check that the next factor is still the fallback factor.
$this->assertEquals(2, count(\tool_mfa\plugininfo\factor::get_active_user_factor_types()));
$this->assertEquals('fallback', \tool_mfa\plugininfo\factor::get_next_user_login_factor()->name);
}
/**
* Tests if a user has more than one active factor.
*
* @covers ::user_has_more_than_one_active_factors
*/
public function test_user_has_more_than_one_active_factors(): void {
global $DB;
$this->resetAfterTest(true);
// Create a user.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
// Create two active user factors.
set_config('enabled', 1, 'factor_totp');
set_config('enabled', 1, 'factor_webauthn');
$data = new \stdClass();
$data->userid = $user->id;
$data->factor = 'totp';
$data->label = 'testtotp';
$data->revoked = 0;
$DB->insert_record('tool_mfa', $data);
$data = new \stdClass();
$data->userid = $user->id;
$data->factor = 'webauthn';
$data->label = 'testwebauthn';
$data->revoked = 0;
$factorid = $DB->insert_record('tool_mfa', $data);
// Test there is more than one active factor.
$hasmorethanonefactor = \tool_mfa\plugininfo\factor::user_has_more_than_one_active_factors();
$this->assertTrue($hasmorethanonefactor);
// Revoke a factor.
$DB->set_field('tool_mfa', 'revoked', 1, ['id' => $factorid]);
// There should no longer be more than one active factor.
$hasmorethanonefactor = \tool_mfa\plugininfo\factor::user_has_more_than_one_active_factors();
$this->assertFalse($hasmorethanonefactor);
}
}
@@ -0,0 +1,285 @@
<?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;
/**
* Tests for MFA secret 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 secret_manager_test extends \advanced_testcase {
/**
* Tests create factor's secret
*
* @covers ::create_secret
*/
public function test_create_secret(): void {
global $DB;
$this->resetAfterTest(true);
$this->setUser($this->getDataGenerator()->create_user());
// Test adding secret to DB.
$secman = new \tool_mfa\local\secret_manager('mock');
// Mutate the sessionid using reflection.
$reflectedsessionid = new \ReflectionProperty($secman, 'sessionid');
$reflectedsessionid->setValue($secman, 'fakesession');
$sec1 = $secman->create_secret(1800, false);
$count1 = $DB->count_records('tool_mfa_secrets', ['factor' => 'mock']);
$record1 = $DB->get_record('tool_mfa_secrets', []);
$this->assertEquals(1, $count1);
$this->assertNotEquals('', $sec1);
$this->assertTrue(empty($record1->sessionid));
$sec2 = $secman->create_secret(1800, false);
$count2 = $DB->count_records('tool_mfa_secrets', ['factor' => 'mock']);
$this->assertEquals(1, $count2);
$this->assertEquals('', $sec2);
$DB->delete_records('tool_mfa_secrets', []);
// Now adding secret to session.
$sec1 = $secman->create_secret(1800, true);
$count1 = $DB->count_records('tool_mfa_secrets', ['factor' => 'mock']);
$record1 = $DB->get_record('tool_mfa_secrets', []);
$this->assertEquals(1, $count1);
$this->assertNotEquals('', $sec1);
$this->assertEquals('fakesession', $record1->sessionid);
$sec2 = $secman->create_secret(1800, true);
$this->assertEquals('', $sec2);
$DB->delete_records('tool_mfa_secrets', []);
// Now test adding a forced code.
$sec1 = $secman->create_secret(1800, false);
$count1 = $DB->count_records('tool_mfa_secrets', ['factor' => 'mock']);
$this->assertEquals(1, $count1);
$this->assertNotEquals('', $sec1);
$sec2 = $secman->create_secret(1800, false, 'code');
$count2 = $DB->count_records('tool_mfa_secrets', ['factor' => 'mock']);
$this->assertEquals(2, $count2);
$this->assertEquals('code', $sec2);
$DB->delete_records('tool_mfa_secrets', []);
}
/**
* Tests add factor's secret to database
*
* @covers ::get_record
* @covers ::delete_records
*/
public function test_add_secret_to_db(): void {
global $DB, $USER;
$this->resetAfterTest(true);
$secman = new \tool_mfa\local\secret_manager('mock');
$this->setUser($this->getDataGenerator()->create_user());
$sid = 'fakeid';
// Let's make stuff public using reflection.
$reflectedscanner = new \ReflectionClass($secman);
$reflectedmethod = $reflectedscanner->getMethod('add_secret_to_db');
// Now add a secret and confirm it creates the correct record.
$reflectedmethod->invoke($secman, 'code', 1800);
$record = $DB->get_record('tool_mfa_secrets', []);
$this->assertEquals('code', $record->secret);
$this->assertEquals($USER->id, $record->userid);
$this->assertEquals('mock', $record->factor);
$this->assertGreaterThanOrEqual(time(), (int) $record->expiry);
$this->assertEquals(0, $record->revoked);
$DB->delete_records('tool_mfa_secrets', []);
// Now add a sessionid and confirm it is added correctly.
$reflectedmethod->invoke($secman, 'code', 1800, $sid);
$record = $DB->get_record('tool_mfa_secrets', []);
$this->assertEquals('code', $record->secret);
$this->assertGreaterThanOrEqual(time(), (int) $record->expiry);
$this->assertEquals(0, $record->revoked);
$this->assertEquals($sid, $record->sessionid);
}
/**
* Tests validating factor's secret
*
* @covers ::validate_secret
* @covers ::create_secret
*/
public function test_validate_secret(): void {
global $DB;
// Test adding a code and getting it returned, then validated.
$this->resetAfterTest(true);
$this->setUser($this->getDataGenerator()->create_user());
$secman = new \tool_mfa\local\secret_manager('mock');
$secret = $secman->create_secret(1800, false);
$this->assertEquals(\tool_mfa\local\secret_manager::VALID, $secman->validate_secret($secret));
$DB->delete_records('tool_mfa_secrets', []);
// Test a manual forced code.
$secret = $secman->create_secret(1800, false, 'code');
$this->assertEquals(\tool_mfa\local\secret_manager::VALID, $secman->validate_secret($secret));
$this->assertEquals('code', $secret);
$DB->delete_records('tool_mfa_secrets', []);
// Test bad codes.
$secret = $secman->create_secret(1800, false);
$this->assertEquals(\tool_mfa\local\secret_manager::NONVALID, $secman->validate_secret('nonvalid'));
$DB->delete_records('tool_mfa_secrets', []);
// Test validate when no secrets present.
$this->assertEquals(\tool_mfa\local\secret_manager::NONVALID, $secman->validate_secret('nonvalid'));
// Test revoked secrets.
$secret = $secman->create_secret(1800, false);
$DB->set_field('tool_mfa_secrets', 'revoked', 1, []);
$this->assertEquals(\tool_mfa\local\secret_manager::REVOKED, $secman->validate_secret($secret));
$DB->delete_records('tool_mfa_secrets', []);
// Test expired secrets.
$secret = $secman->create_secret(-1, false);
$this->assertEquals(\tool_mfa\local\secret_manager::NONVALID, $secman->validate_secret($secret));
$DB->delete_records('tool_mfa_secrets', []);
// Session locked code from the same session id.
// Mutate the sessionid using reflection.
$reflectedsessionid = new \ReflectionProperty($secman, 'sessionid');
$reflectedsessionid->setValue($secman, 'fakesession');
$secret = $secman->create_secret(1800, true);
$this->assertEquals(\tool_mfa\local\secret_manager::VALID, $secman->validate_secret($secret));
$DB->delete_records('tool_mfa_secrets', []);
// Now test a session locked code from a different sessionid.
$secret = $secman->create_secret(1800, true);
$reflectedsessionid->setValue($secman, 'diffsession');
$this->assertEquals(\tool_mfa\local\secret_manager::NONVALID, $secman->validate_secret($secret));
$DB->delete_records('tool_mfa_secrets', []);
}
/**
* Tests revoking factor's secret
*
* @covers ::validate_secret
* @covers ::create_secret
* @covers ::revoke_secret
*/
public function test_revoke_secret(): void {
global $DB, $SESSION;
$this->resetAfterTest(true);
$secman = new \tool_mfa\local\secret_manager('mock');
$this->setUser($this->getDataGenerator()->create_user());
// Session secrets.
$secret = $secman->create_secret(1800, true);
$secman->revoke_secret($secret);
$this->assertEquals(\tool_mfa\local\secret_manager::REVOKED, $secman->validate_secret($secret));
unset($SESSION->tool_mfa_secrets_mock);
// DB secrets.
$secret = $secman->create_secret(1800, false);
$secman->revoke_secret($secret);
$this->assertEquals(\tool_mfa\local\secret_manager::REVOKED, $secman->validate_secret($secret));
$DB->delete_records('tool_mfa_secrets', []);
// Revoke a non-valid secret.
$secret = $secman->create_secret(1800, false);
$secman->revoke_secret('nonvalid');
$this->assertEquals(\tool_mfa\local\secret_manager::NONVALID, $secman->validate_secret('nonvalid'));
}
/**
* Tests checking if factor has an active secret
*
* @covers ::create_secret
* @covers ::revoke_secret
*/
public function test_has_active_secret(): void {
global $DB;
$this->resetAfterTest(true);
$secman = new \tool_mfa\local\secret_manager('mock');
$this->setUser($this->getDataGenerator()->create_user());
// Let's make stuff public using reflection.
$reflectedscanner = new \ReflectionClass($secman);
$reflectedmethod = $reflectedscanner->getMethod('has_active_secret');
// DB secrets.
$this->assertFalse($reflectedmethod->invoke($secman));
$secman->create_secret(1800, false);
$this->assertTrue($reflectedmethod->invoke($secman));
$DB->delete_records('tool_mfa_secrets', []);
$secman->create_secret(-1, false);
$this->assertFalse($reflectedmethod->invoke($secman));
$DB->delete_records('tool_mfa_secrets', []);
$secret = $secman->create_secret(1800, false);
$secman->revoke_secret($secret);
$this->assertFalse($reflectedmethod->invoke($secman));
// Now check a secret with session involvement.
// Mutate the sessionid using reflection.
$reflectedsessionid = new \ReflectionProperty($secman, 'sessionid');
$reflectedsessionid->setValue($secman, 'fakesession');
$this->assertFalse($reflectedmethod->invoke($secman, true));
$secman->create_secret(1800, true);
$this->assertTrue($reflectedmethod->invoke($secman, true));
$DB->delete_records('tool_mfa_secrets', []);
$secman->create_secret(-1, true);
$this->assertFalse($reflectedmethod->invoke($secman, true));
$DB->delete_records('tool_mfa_secrets', []);
$secret = $secman->create_secret(1800, true);
$secman->revoke_secret($secret);
$this->assertFalse($reflectedmethod->invoke($secman, true));
$DB->delete_records('tool_mfa_secrets', []);
$secret = $secman->create_secret(1800, true);
$reflectedsessionid->setValue($secman, 'diffsession');
$this->assertFalse($reflectedmethod->invoke($secman, true));
}
/**
* Tests with cleanup temporal secrets
*
* @covers ::cleanup_temp_secrets
*/
public function test_cleanup_temp_secrets(): void {
global $DB;
$this->resetAfterTest(true);
$secman = new \tool_mfa\local\secret_manager('mock');
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
// Create secrets.
$secman->create_secret(1800, true);
$secman->create_secret(1800, true);
// Cleanup current user secrets.
$secman->cleanup_temp_secrets();
// Check there are no secrets of the current user.
$records = $DB->get_records('tool_mfa_secrets', ['userid' => $user->id]);
$this->assertEmpty($records);
}
}
+67
View File
@@ -0,0 +1,67 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace tool_mfa;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir.'/adminlib.php');
require_once(__DIR__ . '/../lib.php');
/**
* Trait for testing this plugin
*
* @package tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
trait tool_mfa_trait {
/**
* Sets the state of the factor, in particular the weight and whether it is enabled
*
* @param string $factorname
* @param int $enabled
* @param int $weight
*/
public function set_factor_state($factorname, $enabled = 0, $weight = 100) {
$factor = \tool_mfa\plugininfo\factor::get_factor($factorname);
$this->set_factor_config($factor, 'enabled', $enabled);
$this->set_factor_config($factor, 'weight', $weight);
}
/**
* Sets config variable for given factor.
*
* @param object $factor object of the factor class
* @param string $key
* @param mixed $value
*/
public function set_factor_config($factor, $key, $value) {
\tool_mfa\manager::set_factor_config([$key => $value], 'factor_' . $factor->name);
if ($key == 'enabled') {
if ($value == 1) {
\tool_mfa\manager::do_factor_action($factor->name, 'enable');
} else {
\tool_mfa\manager::do_factor_action($factor->name, 'disable');
}
}
}
}