1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 namespace tool_mfa; 18 use tool_mfa\tool_mfa_trait; 19 20 defined('MOODLE_INTERNAL') || die(); 21 require_once (__DIR__ . '/tool_mfa_trait.php'); 22 23 /** 24 * Tests for MFA manager class. 25 * 26 * @package tool_mfa 27 * @author Peter Burnett <peterburnett@catalyst-au.net> 28 * @copyright Catalyst IT 29 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 30 */ 31 class manager_test extends \advanced_testcase { 32 33 use tool_mfa_trait; 34 35 /** 36 * Tests getting the factor total weight 37 * 38 * @covers ::get_total_weight 39 * @covers ::setup_user_factor 40 */ 41 public function test_get_total_weight() { 42 $this->resetAfterTest(true); 43 44 // Create and login a user. 45 $user = $this->getDataGenerator()->create_user(); 46 $this->setUser($user); 47 48 // First get weight with no active factors. 49 $this->assertEquals(0, \tool_mfa\manager::get_total_weight()); 50 51 // Now setup a couple of input based factors. 52 $this->set_factor_state('totp', 1, 100); 53 54 $this->set_factor_state('email', 1, 100); 55 56 // Check weight is still 0 with no passes. 57 $this->assertEquals(0, \tool_mfa\manager::get_total_weight()); 58 59 // Manually pass 1 . 60 $factor = \tool_mfa\plugininfo\factor::get_factor('totp'); 61 $totpdata = [ 62 'secret' => 'fakekey', 63 'devicename' => 'fakedevice', 64 ]; 65 $this->assertNotEmpty($factor->setup_user_factor((object) $totpdata)); 66 $factor->set_state(\tool_mfa\plugininfo\factor::STATE_PASS); 67 $this->assertEquals(100, \tool_mfa\manager::get_total_weight()); 68 69 // Now both. 70 $factor2 = \tool_mfa\plugininfo\factor::get_factor('email'); 71 $factor2->set_state(\tool_mfa\plugininfo\factor::STATE_PASS); 72 $this->assertEquals(200, \tool_mfa\manager::get_total_weight()); 73 74 // Now setup a no input factor, and check that weight is automatically added without input. 75 $this->set_factor_state('auth', 1, 100); 76 set_config('goodauth', 'manual', 'factor_auth'); 77 78 $this->assertEquals(300, \tool_mfa\manager::get_total_weight()); 79 } 80 81 /** 82 * Tests getting the factor status 83 * 84 * @covers ::get_status 85 */ 86 public function test_get_status() { 87 $this->resetAfterTest(true); 88 89 // Create and login a user. 90 $user = $this->getDataGenerator()->create_user(); 91 $this->setUser($user); 92 93 // Check for fail status with no factors. 94 $this->assertEquals(\tool_mfa\plugininfo\factor::STATE_FAIL, \tool_mfa\manager::get_status()); 95 96 // Now add a no input factor. 97 $this->set_factor_state('auth', 1, 100); 98 set_config('goodauth', 'manual', 'factor_auth'); 99 100 // Check state is now passing. 101 $this->assertEquals(\tool_mfa\plugininfo\factor::STATE_PASS, \tool_mfa\manager::get_status()); 102 103 // Now add a failure state factor, and ensure that fail takes precedent. 104 $this->set_factor_state('email', 1, 100); 105 $factoremail = \tool_mfa\plugininfo\factor::get_factor('email'); 106 $factoremail->set_state(\tool_mfa\plugininfo\factor::STATE_FAIL); 107 108 $this->assertEquals(\tool_mfa\plugininfo\factor::STATE_FAIL, \tool_mfa\manager::get_status()); 109 110 // Remove no input factor, and remove fail state by logging in/out. Simulates no data entered yet. 111 $this->setUser(null); 112 $this->setUser($user); 113 $this->set_factor_state('auth', 0, 100); 114 $factoremail->set_state(\tool_mfa\plugininfo\factor::STATE_UNKNOWN); 115 116 $this->assertEquals(\tool_mfa\plugininfo\factor::STATE_NEUTRAL, \tool_mfa\manager::get_status()); 117 } 118 119 /** 120 * Tests checking if passed enough factors 121 * 122 * @covers ::passed_enough_factors 123 */ 124 public function test_passed_enough_factors() { 125 $this->resetAfterTest(true); 126 127 // Create and login a user. 128 $user = $this->getDataGenerator()->create_user(); 129 $this->setUser($user); 130 131 // Check when no factors are setup. 132 $this->assertEquals(false, \tool_mfa\manager::passed_enough_factors()); 133 134 // Setup a no input factor. 135 $this->set_factor_state('auth', 1, 100); 136 set_config('goodauth', 'manual', 'factor_auth'); 137 138 // Check that is enough to pass. 139 $this->assertEquals(true, \tool_mfa\manager::passed_enough_factors()); 140 141 // Lower the weight of the factor. 142 $this->set_factor_state('auth', 1, 75); 143 $this->assertEquals(false, \tool_mfa\manager::passed_enough_factors()); 144 145 // Add another factor to get enough weight to pass, but dont set pass state yet. 146 $this->set_factor_state('email', 1, 100); 147 $factoremail = \tool_mfa\plugininfo\factor::get_factor('email'); 148 $this->assertEquals(false, \tool_mfa\manager::passed_enough_factors()); 149 150 // Now pass the factor and check weight. 151 $factoremail->set_state(\tool_mfa\plugininfo\factor::STATE_PASS); 152 $this->assertEquals(true, \tool_mfa\manager::passed_enough_factors()); 153 } 154 155 /** 156 * The data provider for whether urls should be redirected or not 157 * 158 * @return array 159 */ 160 public static function should_redirect_urls_provider() { 161 $badurl1 = new \moodle_url('/'); 162 $badparam1 = $badurl1->out(); 163 $badurl2 = new \moodle_url('admin/tool/mfa/auth.php'); 164 $badparam2 = $badurl2->out(); 165 return [ 166 ['/', 'http://test.server', true], 167 ['/admin/tool/mfa/action.php', 'http://test.server', true], 168 ['/admin/tool/mfa/factor/totp/settings.php', 'http://test.server', true], 169 ['/', 'http://test.server', true, ['url' => $badparam1]], 170 ['/', 'http://test.server', true, ['url' => $badparam2]], 171 ['/admin/tool/mfa/auth.php', 'http://test.server', false], 172 ['/admin/tool/mfa/auth.php', 'http://test.server/parent/directory', false], 173 ['/admin/tool/mfa/action.php', 'http://test.server/parent/directory', true], 174 ['/', 'http://test.server/parent/directory', true, ['url' => $badparam1]], 175 ['/', 'http://test.server/parent/directory', true, ['url' => $badparam2]], 176 ]; 177 } 178 179 /** 180 * Tests whether it should require mfa 181 * 182 * @covers ::should_require_mfa 183 * @param string $urlstring 184 * @param string $webroot 185 * @param bool $status 186 * @param array|null $params 187 * @dataProvider should_redirect_urls_provider 188 */ 189 public function test_should_require_mfa_urls($urlstring, $webroot, $status, $params = null) { 190 $this->resetAfterTest(true); 191 global $CFG; 192 $user = $this->getDataGenerator()->create_user(); 193 $this->setUser($user); 194 $CFG->wwwroot = $webroot; 195 $url = new \moodle_url($urlstring, $params); 196 $this->assertEquals($status, \tool_mfa\manager::should_require_mfa($url, false)); 197 } 198 199 /** 200 * Tests whether it should require the mfa checks 201 * 202 * @covers ::should_require_mfa 203 */ 204 public function test_should_require_mfa_checks() { 205 // Setup test and user. 206 global $CFG; 207 $this->resetAfterTest(true); 208 $user = $this->getDataGenerator()->create_user(); 209 210 $badurl = new \moodle_url('/'); 211 212 // Upgrade checks. 213 $this->setAdminUser(); 214 // Mark the site as upgraded so it will not fail when running the unittest as a whole. 215 $CFG->allversionshash = \core_component::get_all_versions_hash(); 216 $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false)); 217 $oldhash = $CFG->allversionshash; 218 $CFG->allversionshash = 'abc'; 219 $this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false)); 220 $CFG->allversionshash = $oldhash; 221 $upgradesettings = new \moodle_url('/admin/upgradesettings.php'); 222 $this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($upgradesettings, false)); 223 $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false)); 224 225 // Admin not setup. 226 $this->setUser($user); 227 $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false)); 228 $CFG->adminsetuppending = 1; 229 $this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false)); 230 $CFG->adminsetuppending = 0; 231 232 // Check prevent_redirect. 233 $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false)); 234 $this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, true)); 235 236 // User not setup properly. 237 $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false)); 238 $notsetup = clone($user); 239 unset($notsetup->firstname); 240 $this->setUser($notsetup); 241 $this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false)); 242 $this->setUser($user); 243 244 // Enrolment. 245 $enrolurl = new \moodle_url('/enrol/index.php'); 246 $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false)); 247 $this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($enrolurl, false)); 248 249 // Guest User. 250 $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false)); 251 $this->setGuestUser(); 252 $this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false)); 253 $this->setUser($user); 254 255 // Forced password changes. 256 $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false)); 257 set_user_preference('auth_forcepasswordchange', true); 258 $this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false)); 259 set_user_preference('auth_forcepasswordchange', false); 260 261 // Login as check. 262 $user2 = $this->getDataGenerator()->create_user(); 263 $syscontext = \context_system::instance(); 264 $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false)); 265 $this->setAdminUser(); 266 \core\session\manager::loginas($user2->id, $syscontext, false); 267 $this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false)); 268 $this->setUser($user); 269 } 270 271 /** 272 * Tests should require the mfa redirection loop 273 * 274 * @covers ::should_require_mfa 275 */ 276 public function test_should_require_mfa_redirection_loop() { 277 // Setup test and user. 278 global $CFG, $SESSION; 279 $CFG->wwwroot = 'http://phpunit.test'; 280 $this->resetAfterTest(true); 281 $user = $this->getDataGenerator()->create_user(); 282 $this->setUser($user); 283 284 // Set first referer url. 285 $_SERVER['HTTP_REFERER'] = 'http://phpunit.test'; 286 $url = new \moodle_url('/'); 287 288 // Test you get three redirs then exception. 289 $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false)); 290 $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false)); 291 $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false)); 292 // Set count to threshold. 293 $SESSION->mfa_redir_count = 5; 294 $this->assertEquals(\tool_mfa\manager::REDIRECT_EXCEPTION, \tool_mfa\manager::should_require_mfa($url, false)); 295 // Reset session vars. 296 unset($SESSION->mfa_redir_referer); 297 unset($SESSION->mfa_redir_count); 298 299 // Test 4 different redir urls. 300 $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false)); 301 $_SERVER['HTTP_REFERER'] = 'http://phpunit.test/2'; 302 $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false)); 303 $_SERVER['HTTP_REFERER'] = 'http://phpunit3.test/3'; 304 $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false)); 305 $_SERVER['HTTP_REFERER'] = 'http://phpunit4.test/4'; 306 $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false)); 307 // Reset session vars. 308 unset($SESSION->mfa_redir_referer); 309 unset($SESSION->mfa_redir_count); 310 311 // Test 6 then jump to new referer (5 + 1 to set the first time). 312 $_SERVER['HTTP_REFERER'] = 'http://phpunit.test'; 313 $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false)); 314 $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false)); 315 $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false)); 316 $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false)); 317 $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false)); 318 $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false)); 319 320 $_SERVER['HTTP_REFERER'] = 'http://phpunit.test/2'; 321 $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false)); 322 // Now test that going back to original URL doesnt cause exception. 323 $_SERVER['HTTP_REFERER'] = 'http://phpunit.test'; 324 $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false)); 325 } 326 327 /** 328 * Tests checking for possible setup factor 329 * 330 * @covers ::possible_factor_setup 331 * @covers ::setup_user_factor 332 */ 333 public function test_possible_factor_setup() { 334 // Setup test and user. 335 $this->resetAfterTest(true); 336 $user = $this->getDataGenerator()->create_user(); 337 $this->setUser($user); 338 339 // Test for totp is able to be setup. 340 set_config('enabled', 1, 'factor_totp'); 341 $this->assertTrue(\tool_mfa\manager::possible_factor_setup()); 342 set_config('enabled', 0, 'factor_totp'); 343 344 // Test TOTP is already setup and can be managed. 345 $totp = \tool_mfa\plugininfo\factor::get_factor('totp'); 346 set_config('enabled', 1, 'factor_totp'); 347 $totpdata = [ 348 'secret' => 'fakekey', 349 'devicename' => 'fakedevice', 350 ]; 351 $this->assertNotEmpty($totp->setup_user_factor((object) $totpdata)); 352 $this->assertTrue(\tool_mfa\manager::possible_factor_setup()); 353 set_config('enabled', 0, 'factor_totp'); 354 355 // Test no factors can be setup. 356 set_config('enabled', 1, 'factor_email'); 357 set_config('enabled', 1, 'factor_admin'); 358 $this->assertFalse(\tool_mfa\manager::possible_factor_setup()); 359 set_config('enabled', 0, 'factor_email'); 360 set_config('enabled', 0, 'factor_admin'); 361 } 362 363 /** 364 * Tests checking if a factor is ready 365 * 366 * @covers ::is_ready 367 */ 368 public function test_is_ready() { 369 // Setup test and user. 370 global $CFG; 371 $this->resetAfterTest(true); 372 $user = $this->getDataGenerator()->create_user(); 373 $this->setUser($user); 374 set_config('enabled', 1, 'factor_nosetup'); 375 set_config('enabled', 1, 'tool_mfa'); 376 377 // Capability Check. 378 $this->assertTrue(\tool_mfa\manager::is_ready()); 379 // Swap to role without capability. 380 $this->setGuestUser(); 381 $this->assertFalse(\tool_mfa\manager::is_ready()); 382 $this->setUser($user); 383 384 // Enabled check. 385 $this->assertTrue(\tool_mfa\manager::is_ready()); 386 set_config('enabled', 0, 'tool_mfa'); 387 $this->assertFalse(\tool_mfa\manager::is_ready()); 388 set_config('enabled', 1, 'tool_mfa'); 389 390 // Upgrade check. 391 $this->assertTrue(\tool_mfa\manager::is_ready()); 392 $CFG->upgraderunning = true; 393 $this->assertFalse(\tool_mfa\manager::is_ready()); 394 unset($CFG->upgraderunning); 395 396 // No factors check. 397 $this->assertTrue(\tool_mfa\manager::is_ready()); 398 set_config('enabled', 0, 'factor_nosetup'); 399 $this->assertFalse(\tool_mfa\manager::is_ready()); 400 set_config('enabled', 1, 'factor_nosetup'); 401 } 402 403 /** 404 * Tests core hooks 405 * 406 * @covers ::mfa_config_hook_test 407 * @covers ::mfa_login_hook_test 408 */ 409 public function test_core_hooks() { 410 // Setup test and user. 411 global $CFG, $SESSION; 412 $this->resetAfterTest(true); 413 $user = $this->getDataGenerator()->create_user(); 414 $this->setUser($user); 415 416 // Require login to fire hooks. Config we get for free. 417 require_login(); 418 419 $this->assertTrue($CFG->mfa_config_hook_test); 420 $this->assertTrue($SESSION->mfa_login_hook_test); 421 } 422 423 /** 424 * Tests circular redirect auth 425 * 426 * @covers ::should_require_mfa 427 */ 428 public function test_circular_redirect_auth() { 429 // Setup test and user. 430 $this->resetAfterTest(true); 431 $user = $this->getDataGenerator()->create_user(); 432 $this->setUser($user); 433 434 // Spoof the referrer for the redirect check. 435 $_SERVER['HTTP_REFERER'] = '/admin/tool/mfa/auth.php'; 436 $baseurl = new \moodle_url('/my/naughty/page.php'); 437 438 // After a single check, we should redirect. 439 $this->assertEquals(\tool_mfa\manager::REDIRECT, 440 \tool_mfa\manager::should_require_mfa($baseurl, false)); 441 442 // Now hammer it up to the threshold to emulate a repeated force browse from auth.php. 443 for ($i = 0; $i < \tool_mfa\manager::REDIR_LOOP_THRESHOLD; $i++) { 444 \tool_mfa\manager::should_require_mfa($baseurl, false); 445 } 446 447 // Now finally confirm that a 6th access attempt (after loop safety trigger) still redirects. 448 $this->assertEquals(\tool_mfa\manager::REDIRECT, 449 \tool_mfa\manager::should_require_mfa($baseurl, false)); 450 } 451 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body