Differences Between: [Versions 310 and 400] [Versions 311 and 400] [Versions 39 and 400] [Versions 400 and 402] [Versions 400 and 403]
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 * Class for loading/storing oauth2 endpoints from the DB. 19 * 20 * @package core 21 * @copyright 2017 Damyon Wiese 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 namespace core\oauth2; 25 26 defined('MOODLE_INTERNAL') || die(); 27 28 require_once($CFG->libdir . '/filelib.php'); 29 30 use stdClass; 31 use moodle_url; 32 use context_system; 33 use moodle_exception; 34 35 /** 36 * Static list of api methods for system oauth2 configuration. 37 * 38 * @copyright 2017 Damyon Wiese 39 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 40 */ 41 class api { 42 43 /** 44 * Initializes a record for one of the standard issuers to be displayed in the settings. 45 * The issuer is not yet created in the database. 46 * @param string $type One of google, facebook, microsoft, nextcloud, imsobv2p1 47 * @return \core\oauth2\issuer 48 */ 49 public static function init_standard_issuer($type) { 50 require_capability('moodle/site:config', context_system::instance()); 51 52 $classname = self::get_service_classname($type); 53 if (class_exists($classname)) { 54 return $classname::init(); 55 } 56 throw new moodle_exception('OAuth 2 service type not recognised: ' . $type); 57 } 58 59 /** 60 * Create endpoints for standard issuers, based on the issuer created from submitted data. 61 * @param string $type One of google, facebook, microsoft, nextcloud, imsobv2p1 62 * @param issuer $issuer issuer the endpoints should be created for. 63 * @return \core\oauth2\issuer 64 */ 65 public static function create_endpoints_for_standard_issuer($type, $issuer) { 66 require_capability('moodle/site:config', context_system::instance()); 67 68 $classname = self::get_service_classname($type); 69 if (class_exists($classname)) { 70 $classname::create_endpoints($issuer); 71 return $issuer; 72 } 73 throw new moodle_exception('OAuth 2 service type not recognised: ' . $type); 74 } 75 76 /** 77 * Create one of the standard issuers. 78 * 79 * @param string $type One of google, facebook, microsoft, nextcloud or imsobv2p1 80 * @param string|false $baseurl Baseurl (only required for nextcloud and imsobv2p1) 81 * @return \core\oauth2\issuer 82 */ 83 public static function create_standard_issuer($type, $baseurl = false) { 84 require_capability('moodle/site:config', context_system::instance()); 85 86 switch ($type) { 87 case 'imsobv2p1': 88 if (!$baseurl) { 89 throw new moodle_exception('IMS OBv2.1 service type requires the baseurl parameter.'); 90 } 91 case 'nextcloud': 92 if (!$baseurl) { 93 throw new moodle_exception('Nextcloud service type requires the baseurl parameter.'); 94 } 95 case 'google': 96 case 'facebook': 97 case 'microsoft': 98 $classname = self::get_service_classname($type); 99 $issuer = $classname::init(); 100 if ($baseurl) { 101 $issuer->set('baseurl', $baseurl); 102 } 103 $issuer->create(); 104 return self::create_endpoints_for_standard_issuer($type, $issuer); 105 } 106 107 throw new moodle_exception('OAuth 2 service type not recognised: ' . $type); 108 } 109 110 111 /** 112 * List all the issuers, ordered by the sortorder field 113 * 114 * @param bool $includeloginonly also include issuers that are configured to be shown only on login page, 115 * By default false, in this case the method returns all issuers that can be used in services 116 * @return \core\oauth2\issuer[] 117 */ 118 public static function get_all_issuers(bool $includeloginonly = false) { 119 if ($includeloginonly) { 120 return issuer::get_records([], 'sortorder'); 121 } else { 122 return array_values(issuer::get_records_select('showonloginpage<>?', [issuer::LOGINONLY], 'sortorder')); 123 } 124 } 125 126 /** 127 * Get a single issuer by id. 128 * 129 * @param int $id 130 * @return \core\oauth2\issuer 131 */ 132 public static function get_issuer($id) { 133 return new issuer($id); 134 } 135 136 /** 137 * Get a single endpoint by id. 138 * 139 * @param int $id 140 * @return \core\oauth2\endpoint 141 */ 142 public static function get_endpoint($id) { 143 return new endpoint($id); 144 } 145 146 /** 147 * Get a single user field mapping by id. 148 * 149 * @param int $id 150 * @return \core\oauth2\user_field_mapping 151 */ 152 public static function get_user_field_mapping($id) { 153 return new user_field_mapping($id); 154 } 155 156 /** 157 * Get the system account for an installed OAuth service. 158 * Never ever ever expose this to a webservice because it contains the refresh token which grants API access. 159 * 160 * @param \core\oauth2\issuer $issuer 161 * @return system_account|false 162 */ 163 public static function get_system_account(issuer $issuer) { 164 return system_account::get_record(['issuerid' => $issuer->get('id')]); 165 } 166 167 /** 168 * Get the full list of system scopes required by an oauth issuer. 169 * This includes the list required for login as well as any scopes injected by the oauth2_system_scopes callback in plugins. 170 * 171 * @param \core\oauth2\issuer $issuer 172 * @return string 173 */ 174 public static function get_system_scopes_for_issuer($issuer) { 175 $scopes = $issuer->get('loginscopesoffline'); 176 177 $pluginsfunction = get_plugins_with_function('oauth2_system_scopes', 'lib.php'); 178 foreach ($pluginsfunction as $plugintype => $plugins) { 179 foreach ($plugins as $pluginfunction) { 180 // Get additional scopes from the plugin. 181 $pluginscopes = $pluginfunction($issuer); 182 if (empty($pluginscopes)) { 183 continue; 184 } 185 186 // Merge the additional scopes with the existing ones. 187 $additionalscopes = explode(' ', $pluginscopes); 188 189 foreach ($additionalscopes as $scope) { 190 if (!empty($scope)) { 191 if (strpos(' ' . $scopes . ' ', ' ' . $scope . ' ') === false) { 192 $scopes .= ' ' . $scope; 193 } 194 } 195 } 196 } 197 } 198 199 return $scopes; 200 } 201 202 /** 203 * Get an authenticated oauth2 client using the system account. 204 * This call uses the refresh token to get an access token. 205 * 206 * @param \core\oauth2\issuer $issuer 207 * @return \core\oauth2\client|false An authenticated client (or false if the token could not be upgraded) 208 * @throws moodle_exception Request for token upgrade failed for technical reasons 209 */ 210 public static function get_system_oauth_client(issuer $issuer) { 211 $systemaccount = self::get_system_account($issuer); 212 if (empty($systemaccount)) { 213 return false; 214 } 215 // Get all the scopes! 216 $scopes = self::get_system_scopes_for_issuer($issuer); 217 $class = self::get_client_classname($issuer->get('servicetype')); 218 $client = new $class($issuer, null, $scopes, true); 219 220 if (!$client->is_logged_in()) { 221 if (!$client->upgrade_refresh_token($systemaccount)) { 222 return false; 223 } 224 } 225 return $client; 226 } 227 228 /** 229 * Get an authenticated oauth2 client using the current user account. 230 * This call does the redirect dance back to the current page after authentication. 231 * 232 * @param \core\oauth2\issuer $issuer The desired OAuth issuer 233 * @param moodle_url $currenturl The url to the current page. 234 * @param string $additionalscopes The additional scopes required for authorization. 235 * @param bool $autorefresh Should the client support the use of refresh tokens to persist access across sessions. 236 * @return \core\oauth2\client 237 */ 238 public static function get_user_oauth_client(issuer $issuer, moodle_url $currenturl, $additionalscopes = '', 239 $autorefresh = false) { 240 $class = self::get_client_classname($issuer->get('servicetype')); 241 $client = new $class($issuer, $currenturl, $additionalscopes, false, $autorefresh); 242 243 return $client; 244 } 245 246 /** 247 * Get the client classname for an issuer. 248 * 249 * @param string $type The OAuth issuer type (google, facebook...). 250 * @return string The classname for the custom client or core client class if the class for the defined type 251 * doesn't exist or null type is defined. 252 */ 253 protected static function get_client_classname(?string $type): string { 254 // Default core client class. 255 $classname = 'core\\oauth2\\client'; 256 257 if (!empty($type)) { 258 $typeclassname = 'core\\oauth2\\client\\' . $type; 259 if (class_exists($typeclassname)) { 260 $classname = $typeclassname; 261 } 262 } 263 264 return $classname; 265 } 266 267 /** 268 * Get the list of defined endpoints for this OAuth issuer 269 * 270 * @param \core\oauth2\issuer $issuer The desired OAuth issuer 271 * @return \core\oauth2\endpoint[] 272 */ 273 public static function get_endpoints(issuer $issuer) { 274 return endpoint::get_records(['issuerid' => $issuer->get('id')]); 275 } 276 277 /** 278 * Get the list of defined mapping from OAuth user fields to moodle user fields. 279 * 280 * @param \core\oauth2\issuer $issuer The desired OAuth issuer 281 * @return \core\oauth2\user_field_mapping[] 282 */ 283 public static function get_user_field_mappings(issuer $issuer) { 284 return user_field_mapping::get_records(['issuerid' => $issuer->get('id')]); 285 } 286 287 /** 288 * Guess an image from the discovery URL. 289 * 290 * @param \core\oauth2\issuer $issuer The desired OAuth issuer 291 */ 292 protected static function guess_image($issuer) { 293 if (empty($issuer->get('image')) && !empty($issuer->get('baseurl'))) { 294 $baseurl = parse_url($issuer->get('baseurl')); 295 $imageurl = $baseurl['scheme'] . '://' . $baseurl['host'] . '/favicon.ico'; 296 $issuer->set('image', $imageurl); 297 $issuer->update(); 298 } 299 } 300 301 /** 302 * Take the data from the mform and update the issuer. 303 * 304 * @param stdClass $data 305 * @return \core\oauth2\issuer 306 */ 307 public static function update_issuer($data) { 308 return self::create_or_update_issuer($data, false); 309 } 310 311 /** 312 * Take the data from the mform and create the issuer. 313 * 314 * @param stdClass $data 315 * @return \core\oauth2\issuer 316 */ 317 public static function create_issuer($data) { 318 return self::create_or_update_issuer($data, true); 319 } 320 321 /** 322 * Take the data from the mform and create or update the issuer. 323 * 324 * @param stdClass $data Form data for them issuer to be created/updated. 325 * @param bool $create If true, the issuer will be created; otherwise, it will be updated. 326 * @return issuer The created/updated issuer. 327 */ 328 protected static function create_or_update_issuer($data, bool $create): issuer { 329 require_capability('moodle/site:config', context_system::instance()); 330 $issuer = new issuer($data->id ?? 0, $data); 331 332 // Will throw exceptions on validation failures. 333 if ($create) { 334 $issuer->create(); 335 336 // Perform service discovery. 337 $classname = self::get_service_classname($issuer->get('servicetype')); 338 $classname::discover_endpoints($issuer); 339 self::guess_image($issuer); 340 } else { 341 $issuer->update(); 342 } 343 344 return $issuer; 345 } 346 347 /** 348 * Get the service classname for an issuer. 349 * 350 * @param string $type The OAuth issuer type (google, facebook...). 351 * 352 * @return string The classname for this issuer or "Custom" service class if the class for the defined type doesn't exist 353 * or null type is defined. 354 */ 355 protected static function get_service_classname(?string $type): string { 356 // Default custom service class. 357 $classname = 'core\\oauth2\\service\\custom'; 358 359 if (!empty($type)) { 360 $typeclassname = 'core\\oauth2\\service\\' . $type; 361 if (class_exists($typeclassname)) { 362 $classname = $typeclassname; 363 } 364 } 365 366 return $classname; 367 } 368 369 /** 370 * Take the data from the mform and update the endpoint. 371 * 372 * @param stdClass $data 373 * @return \core\oauth2\endpoint 374 */ 375 public static function update_endpoint($data) { 376 require_capability('moodle/site:config', context_system::instance()); 377 $endpoint = new endpoint(0, $data); 378 379 // Will throw exceptions on validation failures. 380 $endpoint->update(); 381 382 return $endpoint; 383 } 384 385 /** 386 * Take the data from the mform and create the endpoint. 387 * 388 * @param stdClass $data 389 * @return \core\oauth2\endpoint 390 */ 391 public static function create_endpoint($data) { 392 require_capability('moodle/site:config', context_system::instance()); 393 $endpoint = new endpoint(0, $data); 394 395 // Will throw exceptions on validation failures. 396 $endpoint->create(); 397 return $endpoint; 398 } 399 400 /** 401 * Take the data from the mform and update the user field mapping. 402 * 403 * @param stdClass $data 404 * @return \core\oauth2\user_field_mapping 405 */ 406 public static function update_user_field_mapping($data) { 407 require_capability('moodle/site:config', context_system::instance()); 408 $userfieldmapping = new user_field_mapping(0, $data); 409 410 // Will throw exceptions on validation failures. 411 $userfieldmapping->update(); 412 413 return $userfieldmapping; 414 } 415 416 /** 417 * Take the data from the mform and create the user field mapping. 418 * 419 * @param stdClass $data 420 * @return \core\oauth2\user_field_mapping 421 */ 422 public static function create_user_field_mapping($data) { 423 require_capability('moodle/site:config', context_system::instance()); 424 $userfieldmapping = new user_field_mapping(0, $data); 425 426 // Will throw exceptions on validation failures. 427 $userfieldmapping->create(); 428 return $userfieldmapping; 429 } 430 431 /** 432 * Reorder this identity issuer. 433 * 434 * Requires moodle/site:config capability at the system context. 435 * 436 * @param int $id The id of the identity issuer to move. 437 * @return boolean 438 */ 439 public static function move_up_issuer($id) { 440 require_capability('moodle/site:config', context_system::instance()); 441 $current = new issuer($id); 442 443 $sortorder = $current->get('sortorder'); 444 if ($sortorder == 0) { 445 return false; 446 } 447 448 $sortorder = $sortorder - 1; 449 $current->set('sortorder', $sortorder); 450 451 $filters = array('sortorder' => $sortorder); 452 $children = issuer::get_records($filters, 'id'); 453 foreach ($children as $needtoswap) { 454 $needtoswap->set('sortorder', $sortorder + 1); 455 $needtoswap->update(); 456 } 457 458 // OK - all set. 459 $result = $current->update(); 460 461 return $result; 462 } 463 464 /** 465 * Reorder this identity issuer. 466 * 467 * Requires moodle/site:config capability at the system context. 468 * 469 * @param int $id The id of the identity issuer to move. 470 * @return boolean 471 */ 472 public static function move_down_issuer($id) { 473 require_capability('moodle/site:config', context_system::instance()); 474 $current = new issuer($id); 475 476 $max = issuer::count_records(); 477 if ($max > 0) { 478 $max--; 479 } 480 481 $sortorder = $current->get('sortorder'); 482 if ($sortorder >= $max) { 483 return false; 484 } 485 $sortorder = $sortorder + 1; 486 $current->set('sortorder', $sortorder); 487 488 $filters = array('sortorder' => $sortorder); 489 $children = issuer::get_records($filters); 490 foreach ($children as $needtoswap) { 491 $needtoswap->set('sortorder', $sortorder - 1); 492 $needtoswap->update(); 493 } 494 495 // OK - all set. 496 $result = $current->update(); 497 498 return $result; 499 } 500 501 /** 502 * Disable an identity issuer. 503 * 504 * Requires moodle/site:config capability at the system context. 505 * 506 * @param int $id The id of the identity issuer to disable. 507 * @return boolean 508 */ 509 public static function disable_issuer($id) { 510 require_capability('moodle/site:config', context_system::instance()); 511 $issuer = new issuer($id); 512 513 $issuer->set('enabled', 0); 514 return $issuer->update(); 515 } 516 517 518 /** 519 * Enable an identity issuer. 520 * 521 * Requires moodle/site:config capability at the system context. 522 * 523 * @param int $id The id of the identity issuer to enable. 524 * @return boolean 525 */ 526 public static function enable_issuer($id) { 527 require_capability('moodle/site:config', context_system::instance()); 528 $issuer = new issuer($id); 529 530 $issuer->set('enabled', 1); 531 return $issuer->update(); 532 } 533 534 /** 535 * Delete an identity issuer. 536 * 537 * Requires moodle/site:config capability at the system context. 538 * 539 * @param int $id The id of the identity issuer to delete. 540 * @return boolean 541 */ 542 public static function delete_issuer($id) { 543 require_capability('moodle/site:config', context_system::instance()); 544 $issuer = new issuer($id); 545 546 $systemaccount = self::get_system_account($issuer); 547 if ($systemaccount) { 548 $systemaccount->delete(); 549 } 550 $endpoints = self::get_endpoints($issuer); 551 if ($endpoints) { 552 foreach ($endpoints as $endpoint) { 553 $endpoint->delete(); 554 } 555 } 556 557 // Will throw exceptions on validation failures. 558 return $issuer->delete(); 559 } 560 561 /** 562 * Delete an endpoint. 563 * 564 * Requires moodle/site:config capability at the system context. 565 * 566 * @param int $id The id of the endpoint to delete. 567 * @return boolean 568 */ 569 public static function delete_endpoint($id) { 570 require_capability('moodle/site:config', context_system::instance()); 571 $endpoint = new endpoint($id); 572 573 // Will throw exceptions on validation failures. 574 return $endpoint->delete(); 575 } 576 577 /** 578 * Delete a user_field_mapping. 579 * 580 * Requires moodle/site:config capability at the system context. 581 * 582 * @param int $id The id of the user_field_mapping to delete. 583 * @return boolean 584 */ 585 public static function delete_user_field_mapping($id) { 586 require_capability('moodle/site:config', context_system::instance()); 587 $userfieldmapping = new user_field_mapping($id); 588 589 // Will throw exceptions on validation failures. 590 return $userfieldmapping->delete(); 591 } 592 593 /** 594 * Perform the OAuth dance and get a refresh token. 595 * 596 * Requires moodle/site:config capability at the system context. 597 * 598 * @param \core\oauth2\issuer $issuer 599 * @param moodle_url $returnurl The url to the current page (we will be redirected back here after authentication). 600 * @return boolean 601 */ 602 public static function connect_system_account($issuer, $returnurl) { 603 require_capability('moodle/site:config', context_system::instance()); 604 605 // We need to authenticate with an oauth 2 client AS a system user and get a refresh token for offline access. 606 $scopes = self::get_system_scopes_for_issuer($issuer); 607 608 // Allow callbacks to inject non-standard scopes to the auth request. 609 $class = self::get_client_classname($issuer->get('servicetype')); 610 $client = new $class($issuer, $returnurl, $scopes, true); 611 612 if (!optional_param('response', false, PARAM_BOOL)) { 613 $client->log_out(); 614 } 615 616 if (optional_param('error', '', PARAM_RAW)) { 617 return false; 618 } 619 620 if (!$client->is_logged_in()) { 621 redirect($client->get_login_url()); 622 } 623 624 $refreshtoken = $client->get_refresh_token(); 625 if (!$refreshtoken) { 626 return false; 627 } 628 629 $systemaccount = self::get_system_account($issuer); 630 if ($systemaccount) { 631 $systemaccount->delete(); 632 } 633 634 $userinfo = $client->get_userinfo(); 635 636 $record = new stdClass(); 637 $record->issuerid = $issuer->get('id'); 638 $record->refreshtoken = $refreshtoken; 639 $record->grantedscopes = $scopes; 640 $record->email = isset($userinfo['email']) ? $userinfo['email'] : ''; 641 $record->username = $userinfo['username']; 642 643 $systemaccount = new system_account(0, $record); 644 645 $systemaccount->create(); 646 647 $client->log_out(); 648 return true; 649 } 650 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body