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 factor_grace; 18 19 use stdClass; 20 use tool_mfa\local\factor\object_factor_base; 21 22 /** 23 * Grace period factor class. 24 * 25 * @package factor_grace 26 * @author Peter Burnett <peterburnett@catalyst-au.net> 27 * @copyright Catalyst IT 28 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 29 */ 30 class factor extends object_factor_base { 31 32 /** 33 * Grace Factor implementation. 34 * This factor is a singleton, return single instance. 35 * 36 * @param stdClass $user the user to check against. 37 * @return array 38 */ 39 public function get_all_user_factors(stdClass $user): array { 40 global $DB; 41 42 $records = $DB->get_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]); 43 44 if (!empty($records)) { 45 return $records; 46 } 47 48 // Null records returned, build new record. 49 $record = [ 50 'userid' => $user->id, 51 'factor' => $this->name, 52 'createdfromip' => $user->lastip, 53 'timecreated' => time(), 54 'revoked' => 0, 55 ]; 56 $record['id'] = $DB->insert_record('tool_mfa', $record, true); 57 return [(object) $record]; 58 } 59 60 /** 61 * Grace Factor implementation. 62 * Singleton instance, no additional filtering needed. 63 * 64 * @param stdClass $user object to check against. 65 * @return array the array of active factors. 66 */ 67 public function get_active_user_factors(stdClass $user): array { 68 return $this->get_all_user_factors($user); 69 } 70 71 /** 72 * Grace Factor implementation. 73 * Factor has no input. 74 * 75 * {@inheritDoc} 76 */ 77 public function has_input(): bool { 78 return false; 79 } 80 81 /** 82 * Grace Factor implementation. 83 * Checks the user login time against their first login after MFA activation. 84 * 85 * @param bool $redirectable should this state call be allowed to redirect the user? 86 * @return string state constant 87 */ 88 public function get_state($redirectable = true): string { 89 global $FULLME, $SESSION, $USER; 90 $records = ($this->get_all_user_factors($USER)); 91 $record = reset($records); 92 93 // First check if user has any other input or setup factors active. 94 $factors = $this->get_affecting_factors(); 95 $total = 0; 96 foreach ($factors as $factor) { 97 $total += $factor->get_weight(); 98 // If we have hit 100 total, then we know it is possible to auth with the current setup. 99 // Gracemode should no longer give points. 100 if ($total >= 100) { 101 return \tool_mfa\plugininfo\factor::STATE_NEUTRAL; 102 } 103 } 104 105 $starttime = $record->timecreated; 106 // If no start time is recorded, status is unknown. 107 if (empty($starttime)) { 108 return \tool_mfa\plugininfo\factor::STATE_UNKNOWN; 109 } else { 110 $duration = get_config('factor_grace', 'graceperiod'); 111 112 if (!empty($duration)) { 113 if (time() > $starttime + $duration) { 114 // If gracemode would have given points, but now doesnt, 115 // Jump out of the loop and force a factor setup. 116 // We will return once there is a setup, or the user tries to leave. 117 if (get_config('factor_grace', 'forcesetup') && $redirectable) { 118 if (empty($SESSION->mfa_gracemode_recursive)) { 119 // Set a gracemode lock so any further recursive gets fall past any recursive calls. 120 $SESSION->mfa_gracemode_recursive = true; 121 122 $factorurls = \tool_mfa\manager::get_no_redirect_urls(); 123 $cleanurl = new \moodle_url($FULLME); 124 125 foreach ($factorurls as $factorurl) { 126 if ($factorurl->compare($cleanurl)) { 127 $redirectable = false; 128 } 129 } 130 131 // We should never redirect if we have already passed. 132 if ($redirectable && \tool_mfa\manager::get_cumulative_weight() >= 100) { 133 $redirectable = false; 134 } 135 136 unset($SESSION->mfa_gracemode_recursive); 137 138 if ($redirectable) { 139 redirect(new \moodle_url('/admin/tool/mfa/user_preferences.php'), 140 get_string('redirectsetup', 'factor_grace')); 141 } 142 } 143 } 144 return \tool_mfa\plugininfo\factor::STATE_NEUTRAL; 145 } else { 146 return \tool_mfa\plugininfo\factor::STATE_PASS; 147 } 148 } else { 149 return \tool_mfa\plugininfo\factor::STATE_UNKNOWN; 150 } 151 } 152 } 153 154 /** 155 * Grace Factor implementation. 156 * State cannot be set. Return true. 157 * 158 * @param string $state the state constant to set 159 * @return bool 160 */ 161 public function set_state(string $state): bool { 162 return true; 163 } 164 165 /** 166 * Grace Factor implementation. 167 * Add a notification on the next page. 168 * 169 * {@inheritDoc} 170 */ 171 public function post_pass_state(): void { 172 global $USER; 173 parent::post_pass_state(); 174 175 // Ensure grace factor passed before displaying notification. 176 if ($this->get_state() == \tool_mfa\plugininfo\factor::STATE_PASS 177 && !\tool_mfa\manager::check_factor_pending($this->name)) { 178 $url = new \moodle_url('/admin/tool/mfa/user_preferences.php'); 179 $link = \html_writer::link($url, get_string('preferences', 'factor_grace')); 180 181 $records = ($this->get_all_user_factors($USER)); 182 $record = reset($records); 183 $starttime = $record->timecreated; 184 $timeremaining = ($starttime + get_config('factor_grace', 'graceperiod')) - time(); 185 $time = format_time($timeremaining); 186 187 $data = ['url' => $link, 'time' => $time]; 188 189 $customwarning = get_config('factor_grace', 'customwarning'); 190 if (!empty($customwarning)) { 191 // Clean text, then swap placeholders for time and the setup link. 192 $message = preg_replace("/{timeremaining}/", $time, $customwarning); 193 $message = preg_replace("/{setuplink}/", $url, $message); 194 $message = clean_text($message, FORMAT_MOODLE); 195 } else { 196 $message = get_string('setupfactors', 'factor_grace', $data); 197 } 198 199 \core\notification::error($message); 200 } 201 } 202 203 /** 204 * Grace Factor implementation. 205 * Gracemode should not be a valid combination with another factor. 206 * 207 * @param array $combination array of factors that make up the combination 208 * @return bool 209 */ 210 public function check_combination(array $combination): bool { 211 // If this combination has more than 1 factor that has setup or input, not valid. 212 foreach ($combination as $factor) { 213 if ($factor->has_setup() || $factor->has_input()) { 214 return false; 215 } 216 } 217 return true; 218 } 219 220 /** 221 * Grace Factor implementation. 222 * Gracemode can change outcome just by waiting, or based on other factors. 223 * 224 * @param stdClass $user 225 * @return array 226 */ 227 public function possible_states(stdClass $user): array { 228 return [ 229 \tool_mfa\plugininfo\factor::STATE_PASS, 230 \tool_mfa\plugininfo\factor::STATE_NEUTRAL, 231 ]; 232 } 233 234 /** 235 * Grace factor implementation. 236 * 237 * If grace period should redirect at end, make this a no-redirect url. 238 * 239 * @return array 240 */ 241 public function get_no_redirect_urls(): array { 242 $redirect = get_config('factor_grace', 'forcesetup'); 243 244 // First check if user has any other input or setup factors active. 245 $factors = $this->get_affecting_factors(); 246 $total = 0; 247 foreach ($factors as $factor) { 248 $total += $factor->get_weight(); 249 // If we have hit 100 total, then we know it is possible to auth with the current setup. 250 // The setup URL should no longer be a no-redirect URL. User MUST use existing auth. 251 if ($total >= 100) { 252 return []; 253 } 254 } 255 256 if ($redirect && $this->get_state(false) === \tool_mfa\plugininfo\factor::STATE_NEUTRAL) { 257 // If the config is enabled, the user should be able to access + setup a factor using these pages. 258 return [ 259 new \moodle_url('/admin/tool/mfa/user_preferences.php'), 260 new \moodle_url('/admin/tool/mfa/action.php'), 261 ]; 262 } else { 263 return []; 264 } 265 } 266 267 /** 268 * Returns a list of factor objects that can affect gracemode giving points. 269 * 270 * Only factors that a user can setup or manually use can affect whether gracemode gives points. 271 * The intest is to provide a grace period for users to go in, setup factors, phone numbers, etc., 272 * so that they are able to authenticate correctly once the grace period ends. 273 * 274 * @return array 275 */ 276 public function get_all_affecting_factors(): array { 277 // Check if user has any other input or setup factors active. 278 $factors = \tool_mfa\plugininfo\factor::get_factors(); 279 $factors = array_filter($factors, function ($el) { 280 return $el->has_input() || $el->has_setup(); 281 }); 282 return $factors; 283 } 284 285 /** 286 * Get the factor list that is currently affecting gracemode. Active and not ignored. 287 * 288 * @return array 289 */ 290 public function get_affecting_factors(): array { 291 // We need to filter all active user factors against the affecting factors and ignorelist. 292 // Map active to names for filtering. 293 $active = \tool_mfa\plugininfo\factor::get_active_user_factor_types(); 294 $active = array_map(function ($el) { 295 return $el->name; 296 }, $active); 297 $factors = $this->get_all_affecting_factors(); 298 299 $ignorelist = get_config('factor_grace', 'ignorelist'); 300 $ignorelist = !empty($ignorelist) ? explode(',', $ignorelist) : []; 301 302 $factors = array_filter($factors, function ($el) use ($ignorelist, $active) { 303 return !in_array($el->name, $ignorelist) && in_array($el->name, $active); 304 }); 305 return $factors; 306 } 307 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body