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
26use quizaccess_seb\access_manager;
27use quizaccess_seb\quiz_settings;
28use quizaccess_seb\settings_provider;
29
30defined('MOODLE_INTERNAL') || die();
31
32require_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 */
40class 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(): void {
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            ->onlyMethods(['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            ->onlyMethods(['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            ->onlyMethods(['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}
454