Proyectos de Subversion Moodle

Rev

| 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
    /**
138
     * Returns factor help from language string.
139
     *
140
     * Base class implementation.
141
     *
142
     * @return string
143
     * @throws \coding_exception
144
     */
145
    public function get_info(): string {
146
        return get_string('info', 'factor_'.$this->name);
147
    }
148
 
149
    /**
150
     * Returns factor help from language string when there is factor management available.
151
     *
152
     * Base class implementation.
153
     *
154
     * @param int $factorid The factor we want manage info for.
155
     * @return string
156
     * @throws \coding_exception
157
     */
158
    public function get_manage_info(int $factorid): string {
159
        return get_string('manageinfo', 'factor_'.$this->name, $this->get_label($factorid));
160
    }
161
 
162
    /**
163
     * Defines setup_factor form definition page for particular factor.
164
     *
165
     * Dummy implementation. Should be overridden in child class.
166
     *
167
     * @param \MoodleQuickForm $mform
168
     * @return \MoodleQuickForm $mform
169
     */
170
    public function setup_factor_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm {
171
        return $mform;
172
    }
173
 
174
    /**
175
     * Defines setup_factor form definition page after form data has been set.
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_after_data(\MoodleQuickForm $mform): \MoodleQuickForm {
183
        return $mform;
184
    }
185
 
186
    /**
187
     * Implements setup_factor form validation for particular factor.
188
     * Returns an array of errors, where array key = field id and array value = error text.
189
     *
190
     * Dummy implementation. Should be overridden in child class.
191
     *
192
     * @param array $data
193
     * @return array
194
     */
195
    public function setup_factor_form_validation(array $data): array {
196
        return [];
197
    }
198
 
199
    /**
200
     * Setups in given factor when the form is cancelled
201
     *
202
     * Dummy implementation. Should be overridden in child class.
203
     *
204
     * @param int $factorid
205
     * @return void
206
     */
207
    public function setup_factor_form_is_cancelled(int $factorid): void {
208
    }
209
 
210
    /**
211
     * Setup submit button string in given factor
212
     *
213
     * Dummy implementation. Should be overridden in child class.
214
     *
215
     * @return string|null
216
     */
217
    public function setup_factor_form_submit_button_string(): ?string {
218
        return null;
219
    }
220
 
221
    /**
222
     * Setups given factor and adds it to user's active factors list.
223
     * Returns true if factor has been successfully added, otherwise false.
224
     *
225
     * Dummy implementation. Should be overridden in child class.
226
     *
227
     * @param stdClass $data
228
     * @return stdClass|null the record if created, or null.
229
     */
230
    public function setup_user_factor(stdClass $data): stdClass|null {
231
        return null;
232
    }
233
 
234
    /**
235
     * Replaces a given factor and adds it to user's active factors list.
236
     * Returns the new factor if it has been successfully replaced.
237
     *
238
     * Dummy implementation. Should be overridden in child class.
239
     *
240
     * @param stdClass $data The new factor data.
241
     * @param int $id The id of the factor to replace.
242
     * @return stdClass|null the record if created, or null.
243
     */
244
    public function replace_user_factor(stdClass $data, int $id): stdClass|null {
245
        return null;
246
    }
247
 
248
    /**
249
     * Returns an array of all user factors of given type (both active and revoked).
250
     *
251
     * Dummy implementation. Should be overridden in child class.
252
     *
253
     * @param stdClass $user the user to check against.
254
     * @return array
255
     */
256
    public function get_all_user_factors(stdClass $user): array {
257
        return [];
258
    }
259
 
260
    /**
261
     * Returns an array of active user factor records.
262
     * Filters get_all_user_factors() output.
263
     *
264
     * @param stdClass $user object to check against.
265
     * @return array
266
     */
267
    public function get_active_user_factors(stdClass $user): array {
268
        $return = [];
269
        $factors = $this->get_all_user_factors($user);
270
        foreach ($factors as $factor) {
271
            if ($factor->revoked == 0) {
272
                $return[] = $factor;
273
            }
274
        }
275
        return $return;
276
    }
277
 
278
    /**
279
     * Defines login form definition page for particular factor.
280
     *
281
     * Dummy implementation. Should be overridden in child class.
282
     *
283
     * @param \MoodleQuickForm $mform
284
     * @return \MoodleQuickForm $mform
285
     */
286
    public function login_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm {
287
        return $mform;
288
    }
289
 
290
    /**
291
     * Defines login form definition page after form data has been set.
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_after_data(\MoodleQuickForm $mform): \MoodleQuickForm {
299
        return $mform;
300
    }
301
 
302
    /**
303
     * Implements login form validation for particular factor.
304
     * Returns an array of errors, where array key = field id and array value = error text.
305
     *
306
     * Dummy implementation. Should be overridden in child class.
307
     *
308
     * @param array $data
309
     * @return array
310
     */
311
    public function login_form_validation(array $data): array {
312
        return [];
313
    }
314
 
315
    /**
316
     * Returns true if factor class has factor records that might be revoked.
317
     * It means that user can revoke factor record from their profile.
318
     *
319
     * Override in child class if necessary.
320
     *
321
     * @return bool
322
     */
323
    public function has_revoke(): bool {
324
        return false;
325
    }
326
 
327
    /**
328
     * Marks factor record as revoked.
329
     * If factorid is not provided, revoke all instances of factor.
330
     *
331
     * @param int|null $factorid
332
     * @return bool
333
     * @throws \coding_exception
334
     * @throws \dml_exception
335
     */
336
    public function revoke_user_factor(?int $factorid = null): bool {
337
        global $DB, $USER;
338
 
339
        if (!empty($factorid)) {
340
            // If we have an explicit factor id, this means we need to be careful about the user.
341
            $params = ['id' => $factorid];
342
            $existing = $DB->get_record('tool_mfa', $params);
343
            if (empty($existing)) {
344
                return false;
345
            }
346
            $matchinguser = $existing->userid == $USER->id;
347
            if (!is_siteadmin() && !$matchinguser) {
348
                // We aren't admin, and this isn't our factor.
349
                return false;
350
            }
351
        } else {
352
            $params = ['userid' => $USER->id, 'factor' => $this->name];
353
        }
354
        $DB->set_field('tool_mfa', 'revoked', 1, $params);
355
 
356
        $event = \tool_mfa\event\user_revoked_factor::user_revoked_factor_event($USER, $this->get_display_name());
357
        $event->trigger();
358
 
359
        return true;
360
    }
361
 
362
 
363
    /**
364
     * Returns true if factor class has factor records that can be replaced.
365
     *
366
     * Override in child class if necessary.
367
     *
368
     * @return bool
369
     */
370
    public function has_replace(): bool {
371
        return false;
372
    }
373
 
374
    /**
375
     * When validation code is correct - update lastverified field for given factor.
376
     * If factor id is not provided, update all factor entries for user.
377
     *
378
     * @param int|null $factorid
379
     * @return bool|\dml_exception
380
     * @throws \dml_exception
381
     */
382
    public function update_lastverified(?int $factorid = null): bool|\dml_exception {
383
        global $DB, $USER;
384
        if (!empty($factorid)) {
385
            $params = ['id' => $factorid];
386
        } else {
387
            $params = ['factor' => $this->name, 'userid' => $USER->id];
388
        }
389
        return $DB->set_field('tool_mfa', 'lastverified', time(), $params);
390
    }
391
 
392
    /**
393
     * Gets lastverified timestamp.
394
     *
395
     * @param int $factorid
396
     * @return int|bool the lastverified timestamp, or false if not found.
397
     */
398
    public function get_lastverified(int $factorid): int|bool {
399
        global $DB;
400
 
401
        $record = $DB->get_record('tool_mfa', ['id' => $factorid]);
402
        return $record->lastverified;
403
    }
404
 
405
    /**
406
     * Returns true if factor needs to be setup by user and has setup_form.
407
     * Override in child class if necessary.
408
     *
409
     * @return bool
410
     */
411
    public function has_setup(): bool {
412
        return false;
413
    }
414
 
415
    /**
416
     * If has_setup returns true, decides if the setup buttons should be shown on the preferences page.
417
     *
418
     * @return bool
419
     */
420
    public function show_setup_buttons(): bool {
421
        return $this->has_setup();
422
    }
423
 
424
    /**
425
     * Returns true if a factor requires input from the user to verify.
426
     *
427
     * Override in child class if necessary
428
     *
429
     * @return bool
430
     */
431
    public function has_input(): bool {
432
        return true;
433
    }
434
 
435
    /**
436
     * Returns true if a factor is able to be locked if it fails.
437
     *
438
     * Generally only input factors are lockable.
439
     * Override in child class if necessary
440
     *
441
     * @return bool
442
     */
443
    public function is_lockable(): bool {
444
        return $this->has_input();
445
    }
446
 
447
    /**
448
     * Returns the state of the factor from session information.
449
     *
450
     * Implementation for factors that require input.
451
     * Should be overridden in child classes with no input.
452
     *
453
     * @return mixed
454
     */
455
    public function get_state(): string {
456
        global $SESSION;
457
 
458
        $property = 'factor_'.$this->name;
459
 
460
        if (property_exists($SESSION, $property)) {
461
            return $SESSION->$property;
462
        } else {
463
            return \tool_mfa\plugininfo\factor::STATE_UNKNOWN;
464
        }
465
    }
466
 
467
    /**
468
     * Sets the state of the factor into the session.
469
     *
470
     * Implementation for factors that require input.
471
     * Should be overridden in child classes with no input.
472
     *
473
     * @param string $state the state constant to set.
474
     * @return bool
475
     */
476
    public function set_state(string $state): bool {
477
        global $SESSION;
478
 
479
        // Do not allow overwriting fail states.
480
        if ($this->get_state() == \tool_mfa\plugininfo\factor::STATE_FAIL) {
481
            return false;
482
        }
483
 
484
        $property = 'factor_'.$this->name;
485
        $SESSION->$property = $state;
486
        return true;
487
    }
488
 
489
    /**
490
     * Creates an event when user successfully setup a factor
491
     *
492
     * @param object $user
493
     * @return void
494
     */
495
    public function create_event_after_factor_setup(object $user): void {
496
        $event = \tool_mfa\event\user_setup_factor::user_setup_factor_event($user, $this->get_display_name());
497
        $event->trigger();
498
    }
499
 
500
    /**
501
     * Function for factor actions in the pass state.
502
     * Override in child class if necessary.
503
     *
504
     * @return void
505
     */
506
    public function post_pass_state(): void {
507
        // Update lastverified for factor.
508
        if ($this->get_state() == \tool_mfa\plugininfo\factor::STATE_PASS) {
509
            $this->update_lastverified();
510
        }
511
 
512
        // Now clean temp secrets for factor.
513
        $this->secretmanager->cleanup_temp_secrets();
514
    }
515
 
516
    /**
517
     * Function to retrieve the label for a factorid.
518
     *
519
     * @param int $factorid
520
     * @return string|\dml_exception
521
     */
522
    public function get_label(int $factorid): string|\dml_exception {
523
        global $DB;
524
        return $DB->get_field('tool_mfa', 'label', ['id' => $factorid]);
525
    }
526
 
527
    /**
528
     * Function to get urls that should not be redirected from.
529
     *
530
     * @return array
531
     */
532
    public function get_no_redirect_urls(): array {
533
        return [];
534
    }
535
 
536
    /**
537
     * Function to get possible states for a user from factor.
538
     * Implementation where state is based on deterministic user data.
539
     * This should be overridden in factors where state is non-deterministic.
540
     * E.g. IP changes based on whether a user is using a VPN.
541
     *
542
     * @param stdClass $user
543
     * @return array
544
     */
545
    public function possible_states(stdClass $user): array {
546
        return [$this->get_state()];
547
    }
548
 
549
    /**
550
     * Returns condition for passing factor.
551
     * Implementation for basic conditions.
552
     * Override for complex conditions such as auth type.
553
     *
554
     * @return string
555
     */
556
    public function get_summary_condition(): string {
557
        return get_string('summarycondition', 'factor_'.$this->name);
558
    }
559
 
560
    /**
561
     * Checks whether the factor combination is valid based on factor behaviour.
562
     * E.g. a combination with nosetup and another factor is not valid,
563
     * as you cannot pass nosetup with another factor.
564
     *
565
     * @param array $combination array of factors that make up the combination
566
     * @return bool
567
     */
568
    public function check_combination(array $combination): bool {
569
        return true;
570
    }
571
 
572
    /**
573
     * Gets the string for setup button on preferences page.
574
     *
575
     * @return string
576
     */
577
    public function get_setup_string(): string {
578
        return get_string('setupfactor', 'tool_mfa');
579
    }
580
 
581
    /**
582
     * Gets the string for manage button on preferences page.
583
     *
584
     * @return string
585
     */
586
    public function get_manage_string(): string {
587
        return get_string('managefactor', 'tool_mfa');
588
    }
589
 
590
    /**
591
     * Deletes all instances of factor for a user.
592
     *
593
     * @param stdClass $user the user to delete for.
594
     * @return void
595
     */
596
    public function delete_factor_for_user(stdClass $user): void {
597
        global $DB, $USER;
598
        $DB->delete_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]);
599
 
600
        // Emit event for deletion.
601
        $event = \tool_mfa\event\user_deleted_factor::user_deleted_factor_event($user, $USER, $this->name);
602
        $event->trigger();
603
    }
604
 
605
    /**
606
     * Increments the lock counter for a factor.
607
     *
608
     * @return void
609
     */
610
    public function increment_lock_counter(): void {
611
        global $DB, $USER;
612
 
613
        // First make sure the state is loaded.
614
        $this->load_locked_state();
615
 
616
        // If lockcounter is negative, the field does not exist yet.
617
        if ($this->lockcounter === -1) {
618
            return;
619
        }
620
 
621
        $this->lockcounter++;
622
        // Update record in DB.
623
        $DB->set_field('tool_mfa', 'lockcounter', $this->lockcounter, ['userid' => $USER->id, 'factor' => $this->name]);
624
 
625
        // Now lock this factor if over the counter.
626
        $lockthreshold = get_config('tool_mfa', 'lockout');
627
        if ($this->lockcounter >= $lockthreshold) {
628
            $this->set_state(\tool_mfa\plugininfo\factor::STATE_LOCKED);
629
        }
630
    }
631
 
632
    /**
633
     * Return the number of remaining attempts at this factor.
634
     *
635
     * @return int the number of attempts at this factor remaining.
636
     */
637
    public function get_remaining_attempts(): int {
638
        $lockthreshold = get_config('tool_mfa', 'lockout');
639
        if ($this->lockcounter === -1) {
640
            // If upgrade.php hasnt been run yet, just return 10.
641
            return $lockthreshold;
642
        } else {
643
            return $lockthreshold - $this->lockcounter;
644
        }
645
    }
646
 
647
    /**
648
     * Process a cancel input from a user.
649
     *
650
     * @return void
651
     */
652
    public function process_cancel_action(): void {
653
        $this->set_state(\tool_mfa\plugininfo\factor::STATE_NEUTRAL);
654
    }
655
 
656
    /**
657
     * Hook point for global auth form action hooks.
658
     *
659
     * @param \MoodleQuickForm $mform Form to inject global elements into.
660
     * @return void
661
     */
662
    public function global_definition(\MoodleQuickForm $mform): void {
663
        return;
664
    }
665
 
666
    /**
667
     * Hook point for global auth form action hooks.
668
     *
669
     * @param \MoodleQuickForm $mform Form to inject global elements into.
670
     * @return void
671
     */
672
    public function global_definition_after_data(\MoodleQuickForm $mform): void {
673
        return;
674
    }
675
 
676
    /**
677
     * Hook point for global auth form action hooks.
678
     *
679
     * @param array $data Data from the form.
680
     * @param array $files Files form the form.
681
     * @return array of errors from validation.
682
     */
683
    public function global_validation(array $data, array $files): array {
684
        return [];
685
    }
686
 
687
    /**
688
     * Hook point for global auth form action hooks.
689
     *
690
     * @param object $data Data from the form.
691
     * @return void
692
     */
693
    public function global_submit(object $data): void {
694
        return;
695
    }
696
 
697
    /**
698
     * Get the icon associated with this factor.
699
     *
700
     * @return string the icon name.
701
     */
702
    public function get_icon(): string {
703
        return $this->icon;
704
    }
705
 
706
    /**
707
     * Get the login description associated with this factor.
708
     * Override for factors that have a user input.
709
     *
710
     * @return string The login option.
711
     */
712
    public function get_login_desc(): string {
713
        return get_string('logindesc', 'factor_'.$this->name);
714
    }
715
}