/** * Construct a user API client, accounting for unified api presence, and fall back to system api user if desired. * * @param int $muserid The userid to get the outlook token for. If you want to force a system API user client, use an empty * value here and set $systemfallback to true. * @return \local_o365\rest\o365api|bool A constructed user API client (unified or legacy), or false if error. */ public function construct_user_api($muserid = null, $systemfallback = true) { $unifiedconfigured = \local_o365\rest\unified::is_configured(); if ($unifiedconfigured === true) { $resource = \local_o365\rest\unified::get_resource(); } else { $resource = \local_o365\rest\azuread::get_resource(); } $token = null; if (!empty($muserid)) { $token = \local_o365\oauth2\token::instance($muserid, $resource, $this->clientdata, $this->httpclient); } if (empty($token) && $systemfallback === true) { $token = \local_o365\oauth2\systemtoken::instance(null, $resource, $this->clientdata, $this->httpclient); } if (empty($token)) { throw new \Exception('No token available for user #' . $muserid); } if ($unifiedconfigured === true) { $apiclient = new \local_o365\rest\unified($token, $this->httpclient); } else { $apiclient = new \local_o365\rest\azuread($token, $this->httpclient); } return $apiclient; }
/** * Get a unified api token. * * @param bool $system If true, get a system API ser token instead of the user's token. * @param int|null $userid The userid to get a token for. If null, the current user will be used. * @return \local_o365\oauth2\token A unified api token object. */ protected function get_unified_token($system = false, $userid = null) { global $USER; $resource = \local_o365\rest\unified::get_resource(); if ($system === true) { return \local_o365\oauth2\systemtoken::instance(null, $resource, $this->clientdata, $this->httpclient); } else { $userid = !empty($userid) ? $userid : $USER->id; return \local_o365\oauth2\token::instance($userid, $resource, $this->clientdata, $this->httpclient); } }
/** * Get a Unified API instance. * * @param string $caller The calling function, used for logging. * @return \local_o365\rest\unified A Unified API instance. */ public static function get_unified_api($caller = 'get_unified_api') { $clientdata = \local_o365\oauth2\clientdata::instance_from_oidc(); $httpclient = new \local_o365\httpclient(); $resource = \local_o365\rest\unified::get_resource(); $token = \local_o365\oauth2\systemtoken::instance(null, $resource, $clientdata, $httpclient); if (!empty($token)) { return new \local_o365\rest\unified($token, $httpclient); } else { $msg = 'Couldn\'t construct unified api client because we didn\'t have a system API user token.'; $caller = '\\local_o365\\feature\\usergroups\\observers::' . $caller; \local_o365\utils::debug($msg, $caller); return false; } }
/** * Construct an API client. * * @return \local_o365\rest\o365api|bool A constructed user API client (unified or legacy), or false if error. */ public function get_api() { $unifiedconfigured = \local_o365\rest\unified::is_configured(); if ($unifiedconfigured === true) { $resource = \local_o365\rest\unified::get_resource(); } else { $resource = \local_o365\rest\azuread::get_resource(); } $clientdata = \local_o365\oauth2\clientdata::instance_from_oidc(); $httpclient = new \local_o365\httpclient(); $token = \local_o365\oauth2\systemtoken::instance(null, $resource, $clientdata, $httpclient); if (empty($token)) { throw new \Exception('No token available for system user. Please run local_o365 health check.'); } if ($unifiedconfigured === true) { $apiclient = new \local_o365\rest\unified($token, $httpclient); } else { $apiclient = new \local_o365\rest\azuread($token, $httpclient); } return $apiclient; }
/** * Get a unified api token. * * @return \local_o365\oauth2\token A unified api token object. */ protected function get_unified_token() { global $USER; $resource = \local_o365\rest\unified::get_resource(); return \local_o365\oauth2\token::instance($USER->id, $resource, $this->clientdata, $this->httpclient); }
/** * Check setup in Azure. */ public function mode_checksetup() { $data = new \stdClass(); $success = false; $resource = \local_o365\rest\azuread::get_resource(); $clientdata = \local_o365\oauth2\clientdata::instance_from_oidc(); $httpclient = new \local_o365\httpclient(); $token = \local_o365\oauth2\systemtoken::instance(null, $resource, $clientdata, $httpclient); if (empty($token)) { throw new \moodle_exception('errorchecksystemapiuser', 'local_o365'); } // Legacy API. $legacyapi = new \stdClass(); $aadapiclient = new \local_o365\rest\azuread($token, $httpclient); list($missingperms, $haswrite) = $aadapiclient->check_permissions(); $legacyapi->missingperms = $missingperms; $legacyapi->haswrite = $haswrite; // Unified API. $unifiedapi = new \stdClass(); $unifiedapi->active = false; $httpclient = new \local_o365\httpclient(); $unifiedresource = \local_o365\rest\unified::get_resource(); $token = \local_o365\oauth2\systemtoken::instance(null, $unifiedresource, $clientdata, $httpclient); if (empty($token)) { throw new \moodle_exception('errorchecksystemapiuser', 'local_o365'); } $unifiedapiclient = new \local_o365\rest\unified($token, $httpclient); $unifiedpermsresult = $unifiedapiclient->check_permissions(); if ($unifiedpermsresult === null) { $unifiedapi->active = false; } else { $unifiedapi->active = true; $unifiedapi->missingperms = $unifiedpermsresult; } $data->legacyapi = $legacyapi; $data->unifiedapi = $unifiedapi; set_config('azuresetupresult', serialize($data), 'local_o365'); set_config('unifiedapiactive', (int) $unifiedapi->active, 'local_o365'); $success = true; echo $this->ajax_response($data, $success); }
/** * Sync Azure AD Moodle users with the configured Azure AD directory. * * @param array $aadusers Array of Azure AD users from $this->get_users(). * @return bool Success/Failure */ public function sync_users(array $aadusers = array()) { global $DB, $CFG; $aadsync = get_config('local_o365', 'aadsync'); $aadsync = array_flip(explode(',', $aadsync)); $usernames = []; foreach ($aadusers as $i => $user) { $upnlower = \core_text::strtolower($user['userPrincipalName']); $aadusers[$i]['upnlower'] = $upnlower; $usernames[] = $upnlower; $upnsplit = explode('@', $upnlower); if (!empty($upnsplit[0])) { $aadusers[$i]['upnsplit0'] = $upnsplit[0]; $usernames[] = $upnsplit[0]; } } // Retrieve object id for app. if (!PHPUNIT_TEST) { $appinfo = $this->get_application_serviceprincipal_info(); } $objectid = null; if (!empty($appinfo)) { if (\local_o365\rest\unified::is_configured()) { $objectid = $appinfo['value'][0]['id']; } else { $objectid = $appinfo['value'][0]['objectId']; } } list($usernamesql, $usernameparams) = $DB->get_in_or_equal($usernames); $sql = 'SELECT u.username, u.id as muserid, u.auth, tok.id as tokid, conn.id as existingconnectionid, assign.assigned assigned FROM {user} u LEFT JOIN {auth_oidc_token} tok ON tok.username = u.username LEFT JOIN {local_o365_connections} conn ON conn.muserid = u.id LEFT JOIN {local_o365_appassign} assign ON assign.muserid = u.id WHERE u.username ' . $usernamesql . ' AND u.mnethostid = ? AND u.deleted = ? '; $params = array_merge($usernameparams, [$CFG->mnet_localhost_id, '0']); $existingusers = $DB->get_records_sql($sql, $params); foreach ($aadusers as $user) { $this->mtrace(' '); $this->mtrace('Syncing user ' . $user['upnlower']); if (isset($user['aad.isDeleted']) && $user['aad.isDeleted'] == '1') { $this->mtrace('User is deleted. Skipping.'); continue; } if (\local_o365\rest\unified::is_configured()) { $userobjectid = $user['id']; } else { $userobjectid = $user['objectId']; } if (!isset($existingusers[$user['upnlower']]) && !isset($existingusers[$user['upnsplit0']])) { $this->mtrace('User doesn\'t exist in Moodle'); if (!isset($aadsync['create'])) { $this->mtrace('Not creating a Moodle user because that sync option is disabled.'); continue; } try { // Create moodle account, if enabled. $newmuser = $this->create_user_from_aaddata($user); if (!empty($newmuser)) { $this->mtrace('Created user #' . $newmuser->id); } } catch (\Exception $e) { $this->mtrace('Could not create user "' . $user['userPrincipalName'] . '" Reason: ' . $e->getMessage()); } try { if (!PHPUNIT_TEST) { if (!empty($newmuser) && !empty($userobjectid) && !empty($objectid) && isset($aadsync['appassign'])) { $this->assign_user($newmuser->id, $userobjectid, $objectid); } } } catch (\Exception $e) { $this->mtrace('Could not assign user "' . $user['userPrincipalName'] . '" Reason: ' . $e->getMessage()); } } else { $existinguser = null; if (isset($existingusers[$user['upnlower']])) { $existinguser = $existingusers[$user['upnlower']]; } else { if (isset($existingusers[$user['upnsplit0']])) { $existinguser = $existingusers[$user['upnsplit0']]; } } // Assign user to app if not already assigned. if (empty($existinguser->assigned)) { try { if (!PHPUNIT_TEST) { if (!empty($existinguser->muserid) && !empty($userobjectid) && !empty($objectid) && isset($aadsync['appassign'])) { $this->assign_user($existinguser->muserid, $userobjectid, $objectid); } } } catch (\Exception $e) { $this->mtrace('Could not assign user "' . $user['userPrincipalName'] . '" Reason: ' . $e->getMessage()); } } if ($existinguser->auth !== 'oidc' && empty($existinguser->tok)) { $this->mtrace('Found a user in Azure AD that seems to match a user in Moodle'); $this->mtrace(sprintf('moodle username: %s, aad upn: %s', $existinguser->username, $user['upnlower'])); if (!isset($aadsync['match'])) { $this->mtrace('Not matching user because that sync option is disabled.'); continue; } if (!empty($existinguser->existingconnectionid)) { $this->mtrace('User is already matched.'); continue; } // Match to o365 account, if enabled. $matchrec = ['muserid' => $existinguser->muserid, 'aadupn' => $user['upnlower'], 'uselogin' => isset($aadsync['matchswitchauth']) ? 1 : 0]; $DB->insert_record('local_o365_connections', $matchrec); $this->mtrace('Matched user.'); } else { // User already connected. $this->mtrace('User is already synced.'); } } } return true; }
/** * Do the job. */ public function execute() { global $DB; $configsetting = get_config('local_o365', 'creategroups'); if (empty($configsetting)) { mtrace('Groups not enabled, skipping...'); return true; } $now = time(); $httpclient = new \local_o365\httpclient(); $clientdata = \local_o365\oauth2\clientdata::instance_from_oidc(); $unifiedresource = \local_o365\rest\unified::get_resource(); $unifiedtoken = \local_o365\oauth2\systemtoken::instance(null, $unifiedresource, $clientdata, $httpclient); if (empty($unifiedtoken)) { mtrace('Could not get unified API token.'); return true; } $unifiedclient = new \local_o365\rest\unified($unifiedtoken, $httpclient); $aadresource = \local_o365\rest\azuread::get_resource(); $aadtoken = \local_o365\oauth2\systemtoken::instance(null, $aadresource, $clientdata, $httpclient); if (empty($aadtoken)) { mtrace('Could not get Azure AD token.'); return true; } $aadclient = new \local_o365\rest\azuread($aadtoken, $httpclient); $siterec = $DB->get_record('course', ['id' => SITEID]); $siteshortname = strtolower(preg_replace('/[^a-z0-9]+/iu', '', $siterec->shortname)); $sql = 'SELECT crs.* FROM {course} crs LEFT JOIN {local_o365_objects} obj ON obj.type = ? AND obj.subtype = ? AND obj.moodleid = crs.id WHERE obj.id IS NULL AND crs.id != ? LIMIT 0, 5'; $params = ['group', 'course', SITEID]; $courses = $DB->get_recordset_sql($sql, $params); foreach ($courses as $course) { // Create group. $groupname = $siterec->shortname . ': ' . $course->fullname; $groupshortname = $siteshortname . '_' . $course->shortname; $response = $unifiedclient->create_group($groupname, $groupshortname); if (empty($response) || !is_array($response) || empty($response['objectId'])) { mtrace('Could not create group for course #' . $course->id); var_dump($response); continue; } mtrace('Created group ' . $response['objectId'] . ' for course #' . $course->id); $objectrec = ['type' => 'group', 'subtype' => 'course', 'objectid' => $response['objectId'], 'moodleid' => $course->id, 'o365name' => $groupname, 'timecreated' => $now, 'timemodified' => $now]; $objectrec['id'] = $DB->insert_record('local_o365_objects', (object) $objectrec); mtrace('Recorded group object (' . $objectrec['objectid'] . ') into object table with record id ' . $objectrec['id']); // It takes a little while for the group object to register. mtrace('Waiting 10 seconds for group to register...'); sleep(10); // Add enrolled users to group. mtrace('Adding users to group (' . $objectrec['objectid'] . ')'); $coursecontext = \context_course::instance($course->id); list($esql, $params) = get_enrolled_sql($coursecontext); $sql = "SELECT u.*,\n tok.oidcuniqid as userobjectid\n FROM {user} u\n JOIN ({$esql}) je ON je.id = u.id\n JOIN {auth_oidc_token} tok ON tok.username = u.username AND tok.resource = :tokresource\n WHERE u.deleted = 0"; $params['tokresource'] = 'https://graph.windows.net'; $enrolled = $DB->get_recordset_sql($sql, $params); foreach ($enrolled as $user) { $response = $aadclient->add_member_to_group($objectrec['objectid'], $user->userobjectid); if ($response === true) { mtrace('Added user #' . $user->id . ' (' . $user->userobjectid . ')'); } else { mtrace('Could not add user #' . $user->id . ' (' . $user->userobjectid . ')'); mtrace('Received: ' . $response); } } $enrolled->close(); } $courses->close(); }
/** * Return an XHTML string for the setting * @return string Returns an XHTML string */ public function output_html($data, $query = '') { global $OUTPUT; $button = \html_writer::tag('button', get_string('settings_detectperms_update', 'local_o365'), ['class' => 'refreshperms']); $results = \html_writer::tag('div', '', ['class' => 'results']); $settinghtml = $button . $results; if (\local_o365\adminsetting\detectoidc::setup_step_complete() === true) { $existingsetting = $this->config_read($this->name); if (!empty($existingsetting)) { $messageattrs = ['class' => 'permmessage']; $message = \html_writer::tag('span', get_string('settings_detectperms_valid', 'local_o365'), $messageattrs); } else { $messageattrs = ['class' => 'permmessage']; $message = \html_writer::tag('span', get_string('settings_detectperms_invalid', 'local_o365'), $messageattrs); } } else { $icon = $OUTPUT->pix_icon('i/warning', 'prerequisite not complete', 'moodle'); $message = \html_writer::tag('span', get_string('settings_detectperms_nocreds', 'local_o365')); $settinghtml .= \html_writer::tag('div', $icon . $message, ['class' => 'alert-info local_o365_statusmessage']); } // Using a <script> tag here instead of $PAGE->requires->js() because using $PAGE object loads file too late. $scripturl = new \moodle_url('/local/o365/classes/adminsetting/azuresetup.js'); $settinghtml .= '<script src="' . $scripturl->out() . '"></script>'; $lastresults = get_config('local_o365', 'azuresetupresult'); if (!empty($lastresults)) { $lastresults = @unserialize($lastresults); $lastresults = !empty($lastresults) && is_object($lastresults) ? $lastresults : false; $lastresults = json_encode(['success' => true, 'data' => $lastresults]); } else { $lastresults = json_encode(false); } $unifiedenabled = \local_o365\rest\unified::is_enabled() === true ? 'true' : 'false'; $ajaxurl = new \moodle_url('/local/o365/ajax.php'); $settinghtml .= '<script> $(function() { var opts = { url: "' . $ajaxurl->out() . '", lastresults: ' . $lastresults . ', iconsuccess: "' . addslashes($OUTPUT->pix_icon('t/check', 'success', 'moodle')) . '", iconinfo: "' . addslashes($OUTPUT->pix_icon('i/warning', 'information', 'moodle')) . '", iconerror: "' . addslashes($OUTPUT->pix_icon('t/delete', 'error', 'moodle')) . '", strupdate: "' . addslashes(get_string('settings_azuresetup_update', 'local_o365')) . '", strchecking: "' . addslashes(get_string('settings_azuresetup_checking', 'local_o365')) . '", strmissingperms: "' . addslashes(get_string('settings_azuresetup_missingperms', 'local_o365')) . '", strpermscorrect: "' . addslashes(get_string('settings_azuresetup_permscorrect', 'local_o365')) . '", strfixperms: "' . addslashes(get_string('settings_detectperms_fixperms', 'local_o365')) . '", strfixprereq: "' . addslashes(get_string('settings_detectperms_fixprereq', 'local_o365')) . '", strerrorfix: "' . addslashes(get_string('settings_detectperms_errorfix', 'local_o365')) . '", strerrorcheck: "' . addslashes(get_string('settings_azuresetup_errorcheck', 'local_o365')) . '", strnoinfo: "' . addslashes(get_string('settings_azuresetup_noinfo', 'local_o365')) . '", showunified: ' . $unifiedenabled . ', strunifiedheader: "' . addslashes(get_string('settings_azuresetup_unifiedheader', 'local_o365')) . '", strunifieddesc: "' . addslashes(get_string('settings_azuresetup_unifieddesc', 'local_o365')) . '", strunifiederror: "' . addslashes(get_string('settings_azuresetup_unifiederror', 'local_o365')) . '", strunifiedpermerror: "' . addslashes(get_string('settings_azuresetup_strunifiedpermerror', 'local_o365')) . '", strunifiedmissing: "' . addslashes(get_string('settings_azuresetup_unifiedmissing', 'local_o365')) . '", strunifiedactive: "' . addslashes(get_string('settings_azuresetup_unifiedactive', 'local_o365')) . '", strlegacyheader: "' . addslashes(get_string('settings_azuresetup_legacyheader', 'local_o365')) . '", strlegacydesc: "' . addslashes(get_string('settings_azuresetup_legacydesc', 'local_o365')) . '", strlegacyerror: "' . addslashes(get_string('settings_azuresetup_legacyerror', 'local_o365')) . '", strtenanterror: "' . addslashes(get_string('settings_azuresetup_strtenanterror', 'local_o365')) . '" }; $("#admin-' . $this->name . '").azuresetup(opts); }); </script>'; return format_admin_setting($this, $this->visiblename, $settinghtml, $this->description, true, '', null, $query); }
/** * Check setup in Azure. */ public function mode_checksetup() { $data = new \stdClass(); $success = false; $enableunifiedapi = optional_param('enableunifiedapi', 0, PARAM_INT); set_config('enableunifiedapi', $enableunifiedapi, 'local_o365'); $chineseapi = optional_param('chineseapi', 0, PARAM_INT); set_config('chineseapi', $chineseapi, 'local_o365'); $aadtenant = required_param('aadtenant', PARAM_TEXT); set_config('aadtenant', $aadtenant, 'local_o365'); $odburl = required_param('odburl', PARAM_TEXT); set_config('odburl', $odburl, 'local_o365'); $resource = \local_o365\rest\azuread::get_resource(); $clientdata = \local_o365\oauth2\clientdata::instance_from_oidc(); $httpclient = new \local_o365\httpclient(); $token = \local_o365\oauth2\systemtoken::instance(null, $resource, $clientdata, $httpclient); if (empty($token)) { throw new \moodle_exception('errorchecksystemapiuser', 'local_o365'); } // Legacy API. $legacyapi = new \stdClass(); try { $aadapiclient = new \local_o365\rest\azuread($token, $httpclient); list($missingperms, $haswrite) = $aadapiclient->check_permissions(); $legacyapi->missingperms = $missingperms; $legacyapi->haswrite = $haswrite; } catch (\Exception $e) { \local_o365\utils::debug($e->getMessage(), 'mode_checksetup:legacy'); $legacyapi->error = $e->getMessage(); } $data->legacyapi = $legacyapi; // Unified API. $unifiedapi = new \stdClass(); $unifiedapi->active = false; if (\local_o365\rest\unified::is_enabled() === true) { try { $httpclient = new \local_o365\httpclient(); $unifiedresource = \local_o365\rest\unified::get_resource(); $token = \local_o365\oauth2\systemtoken::instance(null, $unifiedresource, $clientdata, $httpclient); if (empty($token)) { throw new \moodle_exception('errorchecksystemapiuser', 'local_o365'); } $unifiedapiclient = new \local_o365\rest\unified($token, $httpclient); $unifiedpermsresult = $unifiedapiclient->check_permissions(); if ($unifiedpermsresult === null) { $unifiedapi->active = false; } else { $unifiedapi->active = true; $unifiedapi->missingperms = $unifiedpermsresult; } } catch (\Exception $e) { $unifiedapi->active = false; \local_o365\utils::debug($e->getMessage(), 'mode_checksetup:unified'); $unifiedapi->error = $e->getMessage(); } } $data->unifiedapi = $unifiedapi; set_config('unifiedapiactive', (int) $unifiedapi->active, 'local_o365'); set_config('azuresetupresult', serialize($data), 'local_o365'); $success = true; echo $this->ajax_response($data, $success); }