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