Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]
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 if (!empty($data->id)) { 332 foreach ($data as $property => $value) { 333 $issuer->set($property, $value); 334 } 335 } 336 337 // Will throw exceptions on validation failures. 338 if ($create) { 339 $issuer->create(); 340 341 // Perform service discovery. 342 $classname = self::get_service_classname($issuer->get('servicetype')); 343 $classname::discover_endpoints($issuer); 344 self::guess_image($issuer); 345 } else { 346 $issuer->update(); 347 } 348 349 return $issuer; 350 } 351 352 /** 353 * Get the service classname for an issuer. 354 * 355 * @param string $type The OAuth issuer type (google, facebook...). 356 * 357 * @return string The classname for this issuer or "Custom" service class if the class for the defined type doesn't exist 358 * or null type is defined. 359 */ 360 protected static function get_service_classname(?string $type): string { 361 // Default custom service class. 362 $classname = 'core\\oauth2\\service\\custom'; 363 364 if (!empty($type)) { 365 $typeclassname = 'core\\oauth2\\service\\' . $type; 366 if (class_exists($typeclassname)) { 367 $classname = $typeclassname; 368 } 369 } 370 371 return $classname; 372 } 373 374 /** 375 * Take the data from the mform and update the endpoint. 376 * 377 * @param stdClass $data 378 * @return \core\oauth2\endpoint 379 */ 380 public static function update_endpoint($data) { 381 require_capability('moodle/site:config', context_system::instance()); 382 $endpoint = new endpoint(0, $data); 383 384 // Will throw exceptions on validation failures. 385 $endpoint->update(); 386 387 return $endpoint; 388 } 389 390 /** 391 * Take the data from the mform and create the endpoint. 392 * 393 * @param stdClass $data 394 * @return \core\oauth2\endpoint 395 */ 396 public static function create_endpoint($data) { 397 require_capability('moodle/site:config', context_system::instance()); 398 $endpoint = new endpoint(0, $data); 399 400 // Will throw exceptions on validation failures. 401 $endpoint->create(); 402 return $endpoint; 403 } 404 405 /** 406 * Take the data from the mform and update the user field mapping. 407 * 408 * @param stdClass $data 409 * @return \core\oauth2\user_field_mapping 410 */ 411 public static function update_user_field_mapping($data) { 412 require_capability('moodle/site:config', context_system::instance()); 413 $userfieldmapping = new user_field_mapping(0, $data); 414 415 // Will throw exceptions on validation failures. 416 $userfieldmapping->update(); 417 418 return $userfieldmapping; 419 } 420 421 /** 422 * Take the data from the mform and create the user field mapping. 423 * 424 * @param stdClass $data 425 * @return \core\oauth2\user_field_mapping 426 */ 427 public static function create_user_field_mapping($data) { 428 require_capability('moodle/site:config', context_system::instance()); 429 $userfieldmapping = new user_field_mapping(0, $data); 430 431 // Will throw exceptions on validation failures. 432 $userfieldmapping->create(); 433 return $userfieldmapping; 434 } 435 436 /** 437 * Reorder this identity issuer. 438 * 439 * Requires moodle/site:config capability at the system context. 440 * 441 * @param int $id The id of the identity issuer to move. 442 * @return boolean 443 */ 444 public static function move_up_issuer($id) { 445 require_capability('moodle/site:config', context_system::instance()); 446 $current = new issuer($id); 447 448 $sortorder = $current->get('sortorder'); 449 if ($sortorder == 0) { 450 return false; 451 } 452 453 $sortorder = $sortorder - 1; 454 $current->set('sortorder', $sortorder); 455 456 $filters = array('sortorder' => $sortorder); 457 $children = issuer::get_records($filters, 'id'); 458 foreach ($children as $needtoswap) { 459 $needtoswap->set('sortorder', $sortorder + 1); 460 $needtoswap->update(); 461 } 462 463 // OK - all set. 464 $result = $current->update(); 465 466 return $result; 467 } 468 469 /** 470 * Reorder this identity issuer. 471 * 472 * Requires moodle/site:config capability at the system context. 473 * 474 * @param int $id The id of the identity issuer to move. 475 * @return boolean 476 */ 477 public static function move_down_issuer($id) { 478 require_capability('moodle/site:config', context_system::instance()); 479 $current = new issuer($id); 480 481 $max = issuer::count_records(); 482 if ($max > 0) { 483 $max--; 484 } 485 486 $sortorder = $current->get('sortorder'); 487 if ($sortorder >= $max) { 488 return false; 489 } 490 $sortorder = $sortorder + 1; 491 $current->set('sortorder', $sortorder); 492 493 $filters = array('sortorder' => $sortorder); 494 $children = issuer::get_records($filters); 495 foreach ($children as $needtoswap) { 496 $needtoswap->set('sortorder', $sortorder - 1); 497 $needtoswap->update(); 498 } 499 500 // OK - all set. 501 $result = $current->update(); 502 503 return $result; 504 } 505 506 /** 507 * Disable an identity issuer. 508 * 509 * Requires moodle/site:config capability at the system context. 510 * 511 * @param int $id The id of the identity issuer to disable. 512 * @return boolean 513 */ 514 public static function disable_issuer($id) { 515 require_capability('moodle/site:config', context_system::instance()); 516 $issuer = new issuer($id); 517 518 $issuer->set('enabled', 0); 519 return $issuer->update(); 520 } 521 522 523 /** 524 * Enable an identity issuer. 525 * 526 * Requires moodle/site:config capability at the system context. 527 * 528 * @param int $id The id of the identity issuer to enable. 529 * @return boolean 530 */ 531 public static function enable_issuer($id) { 532 require_capability('moodle/site:config', context_system::instance()); 533 $issuer = new issuer($id); 534 535 $issuer->set('enabled', 1); 536 return $issuer->update(); 537 } 538 539 /** 540 * Delete an identity issuer. 541 * 542 * Requires moodle/site:config capability at the system context. 543 * 544 * @param int $id The id of the identity issuer to delete. 545 * @return boolean 546 */ 547 public static function delete_issuer($id) { 548 require_capability('moodle/site:config', context_system::instance()); 549 $issuer = new issuer($id); 550 551 $systemaccount = self::get_system_account($issuer); 552 if ($systemaccount) { 553 $systemaccount->delete(); 554 } 555 $endpoints = self::get_endpoints($issuer); 556 if ($endpoints) { 557 foreach ($endpoints as $endpoint) { 558 $endpoint->delete(); 559 } 560 } 561 562 // Will throw exceptions on validation failures. 563 return $issuer->delete(); 564 } 565 566 /** 567 * Delete an endpoint. 568 * 569 * Requires moodle/site:config capability at the system context. 570 * 571 * @param int $id The id of the endpoint to delete. 572 * @return boolean 573 */ 574 public static function delete_endpoint($id) { 575 require_capability('moodle/site:config', context_system::instance()); 576 $endpoint = new endpoint($id); 577 578 // Will throw exceptions on validation failures. 579 return $endpoint->delete(); 580 } 581 582 /** 583 * Delete a user_field_mapping. 584 * 585 * Requires moodle/site:config capability at the system context. 586 * 587 * @param int $id The id of the user_field_mapping to delete. 588 * @return boolean 589 */ 590 public static function delete_user_field_mapping($id) { 591 require_capability('moodle/site:config', context_system::instance()); 592 $userfieldmapping = new user_field_mapping($id); 593 594 // Will throw exceptions on validation failures. 595 return $userfieldmapping->delete(); 596 } 597 598 /** 599 * Perform the OAuth dance and get a refresh token. 600 * 601 * Requires moodle/site:config capability at the system context. 602 * 603 * @param \core\oauth2\issuer $issuer 604 * @param moodle_url $returnurl The url to the current page (we will be redirected back here after authentication). 605 * @return boolean 606 */ 607 public static function connect_system_account($issuer, $returnurl) { 608 require_capability('moodle/site:config', context_system::instance()); 609 610 // We need to authenticate with an oauth 2 client AS a system user and get a refresh token for offline access. 611 $scopes = self::get_system_scopes_for_issuer($issuer); 612 613 // Allow callbacks to inject non-standard scopes to the auth request. 614 $class = self::get_client_classname($issuer->get('servicetype')); 615 $client = new $class($issuer, $returnurl, $scopes, true); 616 617 if (!optional_param('response', false, PARAM_BOOL)) { 618 $client->log_out(); 619 } 620 621 if (optional_param('error', '', PARAM_RAW)) { 622 return false; 623 } 624 625 if (!$client->is_logged_in()) { 626 redirect($client->get_login_url()); 627 } 628 629 $refreshtoken = $client->get_refresh_token(); 630 if (!$refreshtoken) { 631 return false; 632 } 633 634 $systemaccount = self::get_system_account($issuer); 635 if ($systemaccount) { 636 $systemaccount->delete(); 637 } 638 639 $userinfo = $client->get_userinfo(); 640 641 $record = new stdClass(); 642 $record->issuerid = $issuer->get('id'); 643 $record->refreshtoken = $refreshtoken; 644 $record->grantedscopes = $scopes; 645 $record->email = isset($userinfo['email']) ? $userinfo['email'] : ''; 646 $record->username = $userinfo['username']; 647 648 $systemaccount = new system_account(0, $record); 649 650 $systemaccount->create(); 651 652 $client->log_out(); 653 return true; 654 } 655 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body