Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

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