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