Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

   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 quizaccess_seb;
  18  
  19  defined('MOODLE_INTERNAL') || die();
  20  
  21  require_once (__DIR__ . '/test_helper_trait.php');
  22  
  23  /**
  24   * PHPUnit tests for the access manager.
  25   *
  26   * @package   quizaccess_seb
  27   * @author    Andrew Madden <andrewmadden@catalyst-au.net>
  28   * @copyright 2020 Catalyst IT
  29   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  30   */
  31  class access_manager_test extends \advanced_testcase {
  32      use \quizaccess_seb_test_helper_trait;
  33  
  34      /**
  35       * Called before every test.
  36       */
  37      public function setUp(): void {
  38          parent::setUp();
  39  
  40          $this->resetAfterTest();
  41          $this->setAdminUser();
  42          $this->course = $this->getDataGenerator()->create_course();
  43      }
  44  
  45      /**
  46       * Test access_manager private property quizsettings is null.
  47       */
  48      public function test_access_manager_quizsettings_null() {
  49          $this->quiz = $this->create_test_quiz($this->course);
  50  
  51          $accessmanager = $this->get_access_manager();
  52  
  53          $this->assertFalse($accessmanager->seb_required());
  54  
  55          $reflection = new \ReflectionClass('\quizaccess_seb\access_manager');
  56          $property = $reflection->getProperty('quizsettings');
  57          $property->setAccessible(true);
  58  
  59          $this->assertFalse($property->getValue($accessmanager));
  60      }
  61  
  62      /**
  63       * Test that SEB is not required.
  64       */
  65      public function test_seb_required_false() {
  66          $this->quiz = $this->create_test_quiz($this->course);
  67  
  68          $accessmanager = $this->get_access_manager();
  69          $this->assertFalse($accessmanager->seb_required());
  70      }
  71  
  72      /**
  73       * Test that SEB is required.
  74       */
  75      public function test_seb_required_true() {
  76          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
  77  
  78          $accessmanager = $this->get_access_manager();
  79          $this->assertTrue($accessmanager->seb_required());
  80      }
  81  
  82      /**
  83       * Test that user has capability to bypass SEB check.
  84       */
  85      public function test_user_can_bypass_seb_check() {
  86          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
  87  
  88          $user = $this->getDataGenerator()->create_user();
  89          $this->setUser($user);
  90  
  91          // Set the bypass SEB check capability to $USER.
  92          $this->assign_user_capability('quizaccess/seb:bypassseb', \context_module::instance($this->quiz->cmid)->id);
  93  
  94          $accessmanager = $this->get_access_manager();
  95          $this->assertTrue($accessmanager->can_bypass_seb());
  96      }
  97  
  98      /**
  99       * Test user does not have capability to bypass SEB check.
 100       */
 101      public function test_user_cannot_bypass_seb_check() {
 102          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 103  
 104          $user = $this->getDataGenerator()->create_user();
 105          $this->setUser($user);
 106  
 107          $accessmanager = $this->get_access_manager();
 108          $this->assertFalse($accessmanager->can_bypass_seb());
 109      }
 110  
 111      /**
 112       * Test we can detect SEB usage.
 113       */
 114      public function test_is_using_seb() {
 115          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 116  
 117          $accessmanager = $this->get_access_manager();
 118  
 119          $this->assertFalse($accessmanager->is_using_seb());
 120  
 121          $_SERVER['HTTP_USER_AGENT'] = 'Test';
 122          $this->assertFalse($accessmanager->is_using_seb());
 123  
 124          $_SERVER['HTTP_USER_AGENT'] = 'SEB';
 125          $this->assertTrue($accessmanager->is_using_seb());
 126      }
 127  
 128      /**
 129       * Test that the quiz Config Key matches the incoming request header.
 130       */
 131      public function test_access_keys_validate_with_config_key() {
 132          global $FULLME;
 133          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 134  
 135          $accessmanager = $this->get_access_manager();
 136  
 137          $configkey = quiz_settings::get_record(['quizid' => $this->quiz->id])->get_config_key();
 138  
 139          // Set up dummy request.
 140          $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4';
 141          $expectedhash = hash('sha256', $FULLME . $configkey);
 142          $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = $expectedhash;
 143  
 144          $this->assertTrue($accessmanager->validate_browser_exam_keys());
 145          $this->assertTrue($accessmanager->validate_config_key());
 146      }
 147  
 148      /**
 149       * Test that the quiz Config Key does not match the incoming request header.
 150       */
 151      public function test_access_keys_fail_to_validate_with_config_key() {
 152          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 153          $accessmanager = $this->get_access_manager();
 154  
 155          $this->assertFalse($accessmanager->validate_config_key());
 156          $this->assertTrue($accessmanager->validate_browser_exam_keys());
 157      }
 158  
 159      /**
 160       * Test that config key is not checked when using client configuration with SEB.
 161       */
 162      public function test_config_key_not_checked_if_client_requirement_is_selected() {
 163          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
 164          $accessmanager = $this->get_access_manager();
 165          $this->assertTrue($accessmanager->validate_config_key());
 166          $this->assertTrue($accessmanager->validate_browser_exam_keys());
 167      }
 168  
 169      /**
 170       * Test that if there are no browser exam keys for quiz, check is skipped.
 171       */
 172      public function test_no_browser_exam_keys_cause_check_to_be_skipped() {
 173          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
 174  
 175          $settings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
 176          $settings->set('allowedbrowserexamkeys', '');
 177          $settings->save();
 178          $accessmanager = $this->get_access_manager();
 179          $this->assertTrue($accessmanager->validate_config_key());
 180          $this->assertTrue($accessmanager->validate_browser_exam_keys());
 181      }
 182  
 183      /**
 184       * Test that access fails if there is no hash in header.
 185       */
 186      public function test_access_keys_fail_if_browser_exam_key_header_does_not_exist() {
 187          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
 188  
 189          $settings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
 190          $settings->set('allowedbrowserexamkeys', hash('sha256', 'one') . "\n" . hash('sha256', 'two'));
 191          $settings->save();
 192          $accessmanager = $this->get_access_manager();
 193          $this->assertTrue($accessmanager->validate_config_key());
 194          $this->assertFalse($accessmanager->validate_browser_exam_keys());
 195      }
 196  
 197      /**
 198       * Test that access fails if browser exam key doesn't match hash in header.
 199       */
 200      public function test_access_keys_fail_if_browser_exam_key_header_does_not_match_provided_hash() {
 201          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
 202  
 203          $settings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
 204          $settings->set('allowedbrowserexamkeys', hash('sha256', 'one') . "\n" . hash('sha256', 'two'));
 205          $settings->save();
 206          $accessmanager = $this->get_access_manager();
 207          $_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = hash('sha256', 'notwhatyouwereexpectinghuh');
 208          $this->assertTrue($accessmanager->validate_config_key());
 209          $this->assertFalse($accessmanager->validate_browser_exam_keys());
 210      }
 211  
 212      /**
 213       * Test that browser exam key matches hash in header.
 214       */
 215      public function test_browser_exam_keys_match_header_hash() {
 216          global $FULLME;
 217  
 218          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
 219          $settings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
 220          $browserexamkey = hash('sha256', 'browserexamkey');
 221          $settings->set('allowedbrowserexamkeys', $browserexamkey); // Add a hashed BEK.
 222          $settings->save();
 223          $accessmanager = $this->get_access_manager();
 224  
 225          // Set up dummy request.
 226          $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4';
 227          $expectedhash = hash('sha256', $FULLME . $browserexamkey);
 228          $_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = $expectedhash;
 229          $this->assertTrue($accessmanager->validate_config_key());
 230          $this->assertTrue($accessmanager->validate_browser_exam_keys());
 231      }
 232  
 233      /**
 234       * Test can get received config key.
 235       */
 236      public function test_get_received_config_key() {
 237          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
 238          $accessmanager = $this->get_access_manager();
 239  
 240          $this->assertNull($accessmanager->get_received_config_key());
 241  
 242          $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = 'Test key';
 243          $this->assertEquals('Test key', $accessmanager->get_received_config_key());
 244      }
 245  
 246      /**
 247       * Test can get received browser key.
 248       */
 249      public function get_received_browser_exam_key() {
 250          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
 251          $accessmanager = $this->get_access_manager();
 252  
 253          $this->assertNull($accessmanager->get_received_browser_exam_key());
 254  
 255          $_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = 'Test browser key';
 256          $this->assertEquals('Test browser key', $accessmanager->get_received_browser_exam_key());
 257      }
 258  
 259      /**
 260       * Test can correctly get type of SEB usage for the quiz.
 261       */
 262      public function test_get_seb_use_type() {
 263          // No SEB.
 264          $this->quiz = $this->create_test_quiz($this->course);
 265          $accessmanager = $this->get_access_manager();
 266          $this->assertEquals(settings_provider::USE_SEB_NO, $accessmanager->get_seb_use_type());
 267  
 268          // Manually.
 269          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 270          $accessmanager = $this->get_access_manager();
 271          $this->assertEquals(settings_provider::USE_SEB_CONFIG_MANUALLY, $accessmanager->get_seb_use_type());
 272  
 273          // Use template.
 274          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 275          $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
 276          $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE);
 277          $quizsettings->set('templateid', $this->create_template()->get('id'));
 278          $quizsettings->save();
 279          $accessmanager = $this->get_access_manager();
 280          $this->assertEquals(settings_provider::USE_SEB_TEMPLATE, $accessmanager->get_seb_use_type());
 281  
 282          // Use uploaded config.
 283          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 284          $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
 285          $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); // Doesn't check basic header.
 286          $xml = file_get_contents(__DIR__ . '/fixtures/unencrypted.seb');
 287          $this->create_module_test_file($xml, $this->quiz->cmid);
 288          $quizsettings->save();
 289          $accessmanager = $this->get_access_manager();
 290          $this->assertEquals(settings_provider::USE_SEB_UPLOAD_CONFIG, $accessmanager->get_seb_use_type());
 291  
 292          // Use client config.
 293          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
 294          $accessmanager = $this->get_access_manager();
 295          $this->assertEquals(settings_provider::USE_SEB_CLIENT_CONFIG, $accessmanager->get_seb_use_type());
 296      }
 297  
 298      /**
 299       * Data provider for self::test_should_validate_basic_header.
 300       *
 301       * @return array
 302       */
 303      public function should_validate_basic_header_data_provider() {
 304          return [
 305              [settings_provider::USE_SEB_NO, false],
 306              [settings_provider::USE_SEB_CONFIG_MANUALLY, false],
 307              [settings_provider::USE_SEB_TEMPLATE, false],
 308              [settings_provider::USE_SEB_UPLOAD_CONFIG, false],
 309              [settings_provider::USE_SEB_CLIENT_CONFIG, true],
 310          ];
 311      }
 312  
 313      /**
 314       * Test we know when we should validate basic header.
 315       *
 316       * @param int $type Type of SEB usage.
 317       * @param bool $expected Expected result.
 318       *
 319       * @dataProvider should_validate_basic_header_data_provider
 320       */
 321      public function test_should_validate_basic_header($type, $expected) {
 322          $accessmanager = $this->getMockBuilder(access_manager::class)
 323              ->disableOriginalConstructor()
 324              ->onlyMethods(['get_seb_use_type'])
 325              ->getMock();
 326          $accessmanager->method('get_seb_use_type')->willReturn($type);
 327  
 328          $this->assertEquals($expected, $accessmanager->should_validate_basic_header());
 329  
 330      }
 331  
 332      /**
 333       * Data provider for self::test_should_validate_config_key.
 334       *
 335       * @return array
 336       */
 337      public function should_validate_config_key_data_provider() {
 338          return [
 339              [settings_provider::USE_SEB_NO, false],
 340              [settings_provider::USE_SEB_CONFIG_MANUALLY, true],
 341              [settings_provider::USE_SEB_TEMPLATE, true],
 342              [settings_provider::USE_SEB_UPLOAD_CONFIG, true],
 343              [settings_provider::USE_SEB_CLIENT_CONFIG, false],
 344          ];
 345      }
 346  
 347      /**
 348       * Test we know when we should validate config key.
 349       *
 350       * @param int $type Type of SEB usage.
 351       * @param bool $expected Expected result.
 352       *
 353       * @dataProvider should_validate_config_key_data_provider
 354       */
 355      public function test_should_validate_config_key($type, $expected) {
 356          $accessmanager = $this->getMockBuilder(access_manager::class)
 357              ->disableOriginalConstructor()
 358              ->onlyMethods(['get_seb_use_type'])
 359              ->getMock();
 360          $accessmanager->method('get_seb_use_type')->willReturn($type);
 361  
 362          $this->assertEquals($expected, $accessmanager->should_validate_config_key());
 363      }
 364  
 365      /**
 366       * Data provider for self::test_should_validate_browser_exam_key.
 367       *
 368       * @return array
 369       */
 370      public function should_validate_browser_exam_key_data_provider() {
 371          return [
 372              [settings_provider::USE_SEB_NO, false],
 373              [settings_provider::USE_SEB_CONFIG_MANUALLY, false],
 374              [settings_provider::USE_SEB_TEMPLATE, false],
 375              [settings_provider::USE_SEB_UPLOAD_CONFIG, true],
 376              [settings_provider::USE_SEB_CLIENT_CONFIG, true],
 377          ];
 378      }
 379  
 380      /**
 381       * Test we know when we should browser exam key.
 382       *
 383       * @param int $type Type of SEB usage.
 384       * @param bool $expected Expected result.
 385       *
 386       * @dataProvider should_validate_browser_exam_key_data_provider
 387       */
 388      public function test_should_validate_browser_exam_key($type, $expected) {
 389          $accessmanager = $this->getMockBuilder(access_manager::class)
 390              ->disableOriginalConstructor()
 391              ->onlyMethods(['get_seb_use_type'])
 392              ->getMock();
 393          $accessmanager->method('get_seb_use_type')->willReturn($type);
 394  
 395          $this->assertEquals($expected, $accessmanager->should_validate_browser_exam_key());
 396      }
 397  
 398      /**
 399       * Test that access manager uses cached Config Key.
 400       */
 401      public function test_access_manager_uses_cached_config_key() {
 402          global $FULLME;
 403          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 404  
 405          $accessmanager = $this->get_access_manager();
 406  
 407          $configkey = $accessmanager->get_valid_config_key();
 408  
 409          // Set up dummy request.
 410          $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4';
 411          $expectedhash = hash('sha256', $FULLME . $configkey);
 412          $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = $expectedhash;
 413  
 414          $this->assertTrue($accessmanager->validate_config_key());
 415  
 416          // Change settings (but don't save) and check that still can validate config key.
 417          $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
 418          $quizsettings->set('showsebtaskbar', 0);
 419          $this->assertNotEquals($quizsettings->get_config_key(), $configkey);
 420          $this->assertTrue($accessmanager->validate_config_key());
 421  
 422          // Now save settings which should purge caches but access manager still has config key.
 423          $quizsettings->save();
 424          $this->assertNotEquals($quizsettings->get_config_key(), $configkey);
 425          $this->assertTrue($accessmanager->validate_config_key());
 426  
 427          // Initialise a new access manager. Now validation should fail.
 428          $accessmanager = $this->get_access_manager();
 429          $this->assertFalse($accessmanager->validate_config_key());
 430      }
 431  
 432      /**
 433       * Check that valid SEB config key is null if quiz doesn't have SEB settings.
 434       */
 435      public function test_valid_config_key_is_null_if_no_settings() {
 436          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_NO);
 437          $accessmanager = $this->get_access_manager();
 438  
 439          $this->assertEmpty(quiz_settings::get_record(['quizid' => $this->quiz->id]));
 440          $this->assertNull($accessmanager->get_valid_config_key());
 441  
 442      }
 443  
 444  }