Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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\local\factor;
18
 
19
use stdClass;
20
 
21
/**
22
 * MFA factor abstract class.
23
 *
24
 * @package     tool_mfa
25
 * @author      Mikhail Golenkov <golenkovm@gmail.com>
26
 * @copyright   Catalyst IT
27
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28
 */
29
abstract class object_factor_base implements object_factor {
30
 
31
    /** @var string Factor name */
32
    public $name;
33
 
34
    /** @var int Lock counter */
35
    private $lockcounter;
36
 
37
    /**
38
     * Secret manager
39
     *
40
     * @var \tool_mfa\local\secret_manager
41
     */
42
    protected $secretmanager;
43
 
44
    /** @var string Factor icon */
45
    protected $icon = 'fa-lock';
46
 
47
    /**
48
     * Class constructor
49
     *
50
     * @param string $name factor name
51
     */
52
    public function __construct($name) {
53
        global $DB, $USER;
54
        $this->name = $name;
55
 
56
        // Setup secret manager.
57
        $this->secretmanager = new \tool_mfa\local\secret_manager($this->name);
58
    }
59
 
60
    /**
61
     * This loads the locked state from the DB
62
     * Base class implementation.
63
     *
64
     * @return void
65
     */
66
    public function load_locked_state(): void {
67
        global $DB, $USER;
68
 
69
        // Check if lockcounter column exists (incase upgrade hasnt run yet).
70
        // Only 'input factors' are lockable.
71
        if ($this->is_enabled() && $this->is_lockable()) {
72
            try {
73
                // Setup the lock counter.
74
                $sql = "SELECT MAX(lockcounter) FROM {tool_mfa} WHERE userid = ? AND factor = ? AND revoked = ?";
75
                @$this->lockcounter = $DB->get_field_sql($sql, [$USER->id, $this->name, 0]);
76
 
77
                if (empty($this->lockcounter)) {
78
                    $this->lockcounter = 0;
79
                }
80
 
81
                // Now lock this factor if over the counter.
82
                $lockthreshold = get_config('tool_mfa', 'lockout');
83
                if ($this->lockcounter >= $lockthreshold) {
84
                    $this->set_state(\tool_mfa\plugininfo\factor::STATE_LOCKED);
85
                }
86
            } catch (\dml_exception $e) {
87
                // Set counter to -1.
88
                $this->lockcounter = -1;
89
            }
90
        }
91
    }
92
 
93
    /**
94
     * Returns true if factor is enabled, otherwise false.
95
     *
96
     * Base class implementation.
97
     *
98
     * @return bool
99
     * @throws \dml_exception
100
     */
101
    public function is_enabled(): bool {
102
        $status = get_config('factor_'.$this->name, 'enabled');
103
        if ($status == 1) {
104
            return true;
105
        }
106
        return false;
107
    }
108
 
109
    /**
110
     * Returns configured factor weight.
111
     *
112
     * Base class implementation.
113
     *
114
     * @return int
115
     * @throws \dml_exception
116
     */
117
    public function get_weight(): int {
118
        $weight = get_config('factor_'.$this->name, 'weight');
119
        if ($weight) {
120
            return (int) $weight;
121
        }
122
        return 0;
123
    }
124
 
125
    /**
126
     * Returns factor name from language string.
127
     *
128
     * Base class implementation.
129
     *
130
     * @return string
131
     * @throws \coding_exception
132
     */
133
    public function get_display_name(): string {
134
        return get_string('pluginname', 'factor_'.$this->name);
135
    }
136
 
137
    /**
1441 ariadna 138
     * Returns factor short description from language string.
139
     *
140
     * Base class implementation.
141
     *
142
     * @return string
143
     * @throws \coding_exception
144
     */
145
    public function get_short_description(): string {
146
        return get_string('settings:shortdescription', 'factor_'.$this->name);
147
    }
148
 
149
    /**
1 efrain 150
     * Returns factor help from language string.
151
     *
152
     * Base class implementation.
153
     *
154
     * @return string
155
     * @throws \coding_exception
156
     */
157
    public function get_info(): string {
158
        return get_string('info', 'factor_'.$this->name);
159
    }
160
 
161
    /**
162
     * Returns factor help from language string when there is factor management available.
163
     *
164
     * Base class implementation.
165
     *
166
     * @param int $factorid The factor we want manage info for.
167
     * @return string
168
     * @throws \coding_exception
169
     */
170
    public function get_manage_info(int $factorid): string {
171
        return get_string('manageinfo', 'factor_'.$this->name, $this->get_label($factorid));
172
    }
173
 
174
    /**
175
     * Defines setup_factor form definition page for particular factor.
176
     *
177
     * Dummy implementation. Should be overridden in child class.
178
     *
179
     * @param \MoodleQuickForm $mform
180
     * @return \MoodleQuickForm $mform
181
     */
182
    public function setup_factor_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm {
183
        return $mform;
184
    }
185
 
186
    /**
187
     * Defines setup_factor form definition page after form data has been set.
188
     *
189
     * Dummy implementation. Should be overridden in child class.
190
     *
191
     * @param \MoodleQuickForm $mform
192
     * @return \MoodleQuickForm $mform
193
     */
194
    public function setup_factor_form_definition_after_data(\MoodleQuickForm $mform): \MoodleQuickForm {
195
        return $mform;
196
    }
197
 
198
    /**
199
     * Implements setup_factor form validation for particular factor.
200
     * Returns an array of errors, where array key = field id and array value = error text.
201
     *
202
     * Dummy implementation. Should be overridden in child class.
203
     *
204
     * @param array $data
205
     * @return array
206
     */
207
    public function setup_factor_form_validation(array $data): array {
208
        return [];
209
    }
210
 
211
    /**
212
     * Setups in given factor when the form is cancelled
213
     *
214
     * Dummy implementation. Should be overridden in child class.
215
     *
216
     * @param int $factorid
217
     * @return void
218
     */
219
    public function setup_factor_form_is_cancelled(int $factorid): void {
220
    }
221
 
222
    /**
223
     * Setup submit button string in given factor
224
     *
225
     * Dummy implementation. Should be overridden in child class.
226
     *
227
     * @return string|null
228
     */
229
    public function setup_factor_form_submit_button_string(): ?string {
230
        return null;
231
    }
232
 
233
    /**
234
     * Setups given factor and adds it to user's active factors list.
235
     * Returns true if factor has been successfully added, otherwise false.
236
     *
237
     * Dummy implementation. Should be overridden in child class.
238
     *
239
     * @param stdClass $data
240
     * @return stdClass|null the record if created, or null.
241
     */
242
    public function setup_user_factor(stdClass $data): stdClass|null {
243
        return null;
244
    }
245
 
246
    /**
247
     * Replaces a given factor and adds it to user's active factors list.
248
     * Returns the new factor if it has been successfully replaced.
249
     *
250
     * Dummy implementation. Should be overridden in child class.
251
     *
252
     * @param stdClass $data The new factor data.
253
     * @param int $id The id of the factor to replace.
254
     * @return stdClass|null the record if created, or null.
255
     */
256
    public function replace_user_factor(stdClass $data, int $id): stdClass|null {
257
        return null;
258
    }
259
 
260
    /**
261
     * Returns an array of all user factors of given type (both active and revoked).
262
     *
263
     * Dummy implementation. Should be overridden in child class.
264
     *
265
     * @param stdClass $user the user to check against.
266
     * @return array
267
     */
268
    public function get_all_user_factors(stdClass $user): array {
269
        return [];
270
    }
271
 
272
    /**
273
     * Returns an array of active user factor records.
274
     * Filters get_all_user_factors() output.
275
     *
276
     * @param stdClass $user object to check against.
277
     * @return array
278
     */
279
    public function get_active_user_factors(stdClass $user): array {
280
        $return = [];
281
        $factors = $this->get_all_user_factors($user);
282
        foreach ($factors as $factor) {
283
            if ($factor->revoked == 0) {
284
                $return[] = $factor;
285
            }
286
        }
287
        return $return;
288
    }
289
 
290
    /**
291
     * Defines login form definition page for particular factor.
292
     *
293
     * Dummy implementation. Should be overridden in child class.
294
     *
295
     * @param \MoodleQuickForm $mform
296
     * @return \MoodleQuickForm $mform
297
     */
298
    public function login_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm {
299
        return $mform;
300
    }
301
 
302
    /**
303
     * Defines login form definition page after form data has been set.
304
     *
305
     * Dummy implementation. Should be overridden in child class.
306
     *
307
     * @param \MoodleQuickForm $mform
308
     * @return \MoodleQuickForm $mform
309
     */
310
    public function login_form_definition_after_data(\MoodleQuickForm $mform): \MoodleQuickForm {
311
        return $mform;
312
    }
313
 
314
    /**
315
     * Implements login form validation for particular factor.
316
     * Returns an array of errors, where array key = field id and array value = error text.
317
     *
318
     * Dummy implementation. Should be overridden in child class.
319
     *
320
     * @param array $data
321
     * @return array
322
     */
323
    public function login_form_validation(array $data): array {
324
        return [];
325
    }
326
 
327
    /**
328
     * Returns true if factor class has factor records that might be revoked.
329
     * It means that user can revoke factor record from their profile.
330
     *
331
     * Override in child class if necessary.
332
     *
333
     * @return bool
334
     */
335
    public function has_revoke(): bool {
336
        return false;
337
    }
338
 
339
    /**
340
     * Marks factor record as revoked.
341
     * If factorid is not provided, revoke all instances of factor.
342
     *
343
     * @param int|null $factorid
344
     * @return bool
345
     * @throws \coding_exception
346
     * @throws \dml_exception
347
     */
348
    public function revoke_user_factor(?int $factorid = null): bool {
349
        global $DB, $USER;
350
 
351
        if (!empty($factorid)) {
352
            // If we have an explicit factor id, this means we need to be careful about the user.
353
            $params = ['id' => $factorid];
354
            $existing = $DB->get_record('tool_mfa', $params);
355
            if (empty($existing)) {
356
                return false;
357
            }
358
            $matchinguser = $existing->userid == $USER->id;
359
            if (!is_siteadmin() && !$matchinguser) {
360
                // We aren't admin, and this isn't our factor.
361
                return false;
362
            }
363
        } else {
364
            $params = ['userid' => $USER->id, 'factor' => $this->name];
365
        }
366
        $DB->set_field('tool_mfa', 'revoked', 1, $params);
367
 
368
        $event = \tool_mfa\event\user_revoked_factor::user_revoked_factor_event($USER, $this->get_display_name());
369
        $event->trigger();
370
 
371
        return true;
372
    }
373
 
374
 
375
    /**
376
     * Returns true if factor class has factor records that can be replaced.
377
     *
378
     * Override in child class if necessary.
379
     *
380
     * @return bool
381
     */
382
    public function has_replace(): bool {
383
        return false;
384
    }
385
 
386
    /**
387
     * When validation code is correct - update lastverified field for given factor.
388
     * If factor id is not provided, update all factor entries for user.
389
     *
390
     * @param int|null $factorid
391
     * @return bool|\dml_exception
392
     * @throws \dml_exception
393
     */
394
    public function update_lastverified(?int $factorid = null): bool|\dml_exception {
395
        global $DB, $USER;
396
        if (!empty($factorid)) {
397
            $params = ['id' => $factorid];
398
        } else {
399
            $params = ['factor' => $this->name, 'userid' => $USER->id];
400
        }
401
        return $DB->set_field('tool_mfa', 'lastverified', time(), $params);
402
    }
403
 
404
    /**
405
     * Gets lastverified timestamp.
406
     *
407
     * @param int $factorid
408
     * @return int|bool the lastverified timestamp, or false if not found.
409
     */
410
    public function get_lastverified(int $factorid): int|bool {
411
        global $DB;
412
 
413
        $record = $DB->get_record('tool_mfa', ['id' => $factorid]);
414
        return $record->lastverified;
415
    }
416
 
417
    /**
418
     * Returns true if factor needs to be setup by user and has setup_form.
419
     * Override in child class if necessary.
420
     *
421
     * @return bool
422
     */
423
    public function has_setup(): bool {
424
        return false;
425
    }
426
 
427
    /**
428
     * If has_setup returns true, decides if the setup buttons should be shown on the preferences page.
429
     *
430
     * @return bool
431
     */
432
    public function show_setup_buttons(): bool {
433
        return $this->has_setup();
434
    }
435
 
436
    /**
1441 ariadna 437
     * Returns true if a button should be shown to add factors of the same kind on the preferences page.
438
     * For example, give user's the ability to set up multiple security keys.
439
     *
440
     * Override in child class if necessary.
441
     *
442
     * @return bool
443
     */
444
    public function show_additional_setup_button(): bool {
445
        return false;
446
    }
447
 
448
    /**
1 efrain 449
     * Returns true if a factor requires input from the user to verify.
450
     *
451
     * Override in child class if necessary
452
     *
453
     * @return bool
454
     */
455
    public function has_input(): bool {
456
        return true;
457
    }
458
 
459
    /**
460
     * Returns true if a factor is able to be locked if it fails.
461
     *
462
     * Generally only input factors are lockable.
463
     * Override in child class if necessary
464
     *
465
     * @return bool
466
     */
467
    public function is_lockable(): bool {
468
        return $this->has_input();
469
    }
470
 
471
    /**
472
     * Returns the state of the factor from session information.
473
     *
474
     * Implementation for factors that require input.
475
     * Should be overridden in child classes with no input.
476
     *
477
     * @return mixed
478
     */
479
    public function get_state(): string {
480
        global $SESSION;
481
 
482
        $property = 'factor_'.$this->name;
483
 
484
        if (property_exists($SESSION, $property)) {
485
            return $SESSION->$property;
486
        } else {
487
            return \tool_mfa\plugininfo\factor::STATE_UNKNOWN;
488
        }
489
    }
490
 
491
    /**
492
     * Sets the state of the factor into the session.
493
     *
494
     * Implementation for factors that require input.
495
     * Should be overridden in child classes with no input.
496
     *
497
     * @param string $state the state constant to set.
498
     * @return bool
499
     */
500
    public function set_state(string $state): bool {
501
        global $SESSION;
502
 
503
        // Do not allow overwriting fail states.
504
        if ($this->get_state() == \tool_mfa\plugininfo\factor::STATE_FAIL) {
505
            return false;
506
        }
507
 
508
        $property = 'factor_'.$this->name;
509
        $SESSION->$property = $state;
510
        return true;
511
    }
512
 
513
    /**
514
     * Creates an event when user successfully setup a factor
515
     *
516
     * @param object $user
517
     * @return void
518
     */
519
    public function create_event_after_factor_setup(object $user): void {
520
        $event = \tool_mfa\event\user_setup_factor::user_setup_factor_event($user, $this->get_display_name());
521
        $event->trigger();
522
    }
523
 
524
    /**
525
     * Function for factor actions in the pass state.
526
     * Override in child class if necessary.
527
     *
528
     * @return void
529
     */
530
    public function post_pass_state(): void {
531
        // Update lastverified for factor.
532
        if ($this->get_state() == \tool_mfa\plugininfo\factor::STATE_PASS) {
533
            $this->update_lastverified();
534
        }
535
 
536
        // Now clean temp secrets for factor.
537
        $this->secretmanager->cleanup_temp_secrets();
538
    }
539
 
540
    /**
541
     * Function to retrieve the label for a factorid.
542
     *
543
     * @param int $factorid
544
     * @return string|\dml_exception
545
     */
546
    public function get_label(int $factorid): string|\dml_exception {
547
        global $DB;
548
        return $DB->get_field('tool_mfa', 'label', ['id' => $factorid]);
549
    }
550
 
551
    /**
552
     * Function to get urls that should not be redirected from.
553
     *
554
     * @return array
555
     */
556
    public function get_no_redirect_urls(): array {
557
        return [];
558
    }
559
 
560
    /**
561
     * Function to get possible states for a user from factor.
562
     * Implementation where state is based on deterministic user data.
563
     * This should be overridden in factors where state is non-deterministic.
564
     * E.g. IP changes based on whether a user is using a VPN.
565
     *
566
     * @param stdClass $user
567
     * @return array
568
     */
569
    public function possible_states(stdClass $user): array {
570
        return [$this->get_state()];
571
    }
572
 
573
    /**
574
     * Returns condition for passing factor.
575
     * Implementation for basic conditions.
576
     * Override for complex conditions such as auth type.
577
     *
578
     * @return string
579
     */
580
    public function get_summary_condition(): string {
581
        return get_string('summarycondition', 'factor_'.$this->name);
582
    }
583
 
584
    /**
585
     * Checks whether the factor combination is valid based on factor behaviour.
586
     * E.g. a combination with nosetup and another factor is not valid,
587
     * as you cannot pass nosetup with another factor.
588
     *
589
     * @param array $combination array of factors that make up the combination
590
     * @return bool
591
     */
592
    public function check_combination(array $combination): bool {
593
        return true;
594
    }
595
 
596
    /**
597
     * Gets the string for setup button on preferences page.
598
     *
599
     * @return string
600
     */
601
    public function get_setup_string(): string {
602
        return get_string('setupfactor', 'tool_mfa');
603
    }
604
 
605
    /**
1441 ariadna 606
     * Gets the string for additional setup button on preferences page.
607
     * If the user has the ability to set up multiple security keys.
608
     *
609
     * Override in child class if necessary.
610
     *
611
     * @return string
612
     */
613
    public function get_additional_setup_string(): string {
614
        return get_string('setupfactorbuttonadditional', 'tool_mfa');
615
    }
616
 
617
    /**
1 efrain 618
     * Gets the string for manage button on preferences page.
619
     *
620
     * @return string
621
     */
622
    public function get_manage_string(): string {
623
        return get_string('managefactor', 'tool_mfa');
624
    }
625
 
626
    /**
627
     * Deletes all instances of factor for a user.
628
     *
629
     * @param stdClass $user the user to delete for.
630
     * @return void
631
     */
632
    public function delete_factor_for_user(stdClass $user): void {
633
        global $DB, $USER;
634
        $DB->delete_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]);
635
 
636
        // Emit event for deletion.
637
        $event = \tool_mfa\event\user_deleted_factor::user_deleted_factor_event($user, $USER, $this->name);
638
        $event->trigger();
639
    }
640
 
641
    /**
642
     * Increments the lock counter for a factor.
643
     *
644
     * @return void
645
     */
646
    public function increment_lock_counter(): void {
647
        global $DB, $USER;
648
 
649
        // First make sure the state is loaded.
650
        $this->load_locked_state();
651
 
652
        // If lockcounter is negative, the field does not exist yet.
653
        if ($this->lockcounter === -1) {
654
            return;
655
        }
656
 
657
        $this->lockcounter++;
658
        // Update record in DB.
659
        $DB->set_field('tool_mfa', 'lockcounter', $this->lockcounter, ['userid' => $USER->id, 'factor' => $this->name]);
660
 
661
        // Now lock this factor if over the counter.
662
        $lockthreshold = get_config('tool_mfa', 'lockout');
663
        if ($this->lockcounter >= $lockthreshold) {
664
            $this->set_state(\tool_mfa\plugininfo\factor::STATE_LOCKED);
665
        }
666
    }
667
 
668
    /**
669
     * Return the number of remaining attempts at this factor.
670
     *
671
     * @return int the number of attempts at this factor remaining.
672
     */
673
    public function get_remaining_attempts(): int {
674
        $lockthreshold = get_config('tool_mfa', 'lockout');
675
        if ($this->lockcounter === -1) {
676
            // If upgrade.php hasnt been run yet, just return 10.
677
            return $lockthreshold;
678
        } else {
679
            return $lockthreshold - $this->lockcounter;
680
        }
681
    }
682
 
683
    /**
684
     * Process a cancel input from a user.
685
     *
686
     * @return void
687
     */
688
    public function process_cancel_action(): void {
689
        $this->set_state(\tool_mfa\plugininfo\factor::STATE_NEUTRAL);
690
    }
691
 
692
    /**
693
     * Hook point for global auth form action hooks.
694
     *
695
     * @param \MoodleQuickForm $mform Form to inject global elements into.
696
     * @return void
697
     */
698
    public function global_definition(\MoodleQuickForm $mform): void {
699
        return;
700
    }
701
 
702
    /**
703
     * Hook point for global auth form action hooks.
704
     *
705
     * @param \MoodleQuickForm $mform Form to inject global elements into.
706
     * @return void
707
     */
708
    public function global_definition_after_data(\MoodleQuickForm $mform): void {
709
        return;
710
    }
711
 
712
    /**
713
     * Hook point for global auth form action hooks.
714
     *
715
     * @param array $data Data from the form.
716
     * @param array $files Files form the form.
717
     * @return array of errors from validation.
718
     */
719
    public function global_validation(array $data, array $files): array {
720
        return [];
721
    }
722
 
723
    /**
724
     * Hook point for global auth form action hooks.
725
     *
726
     * @param object $data Data from the form.
727
     * @return void
728
     */
729
    public function global_submit(object $data): void {
730
        return;
731
    }
732
 
733
    /**
734
     * Get the icon associated with this factor.
735
     *
736
     * @return string the icon name.
737
     */
738
    public function get_icon(): string {
739
        return $this->icon;
740
    }
741
 
742
    /**
743
     * Get the login description associated with this factor.
744
     * Override for factors that have a user input.
745
     *
746
     * @return string The login option.
747
     */
748
    public function get_login_desc(): string {
749
        return get_string('logindesc', 'factor_'.$this->name);
750
    }
751
}