Skip to content

Commit 6f9e7ae

Browse files
local_o365 and auth_oidc plugins updated
1 parent 5825a98 commit 6f9e7ae

20 files changed

+301
-99
lines changed

classes/feature/usergroups/coursegroups.php

+60-29
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323

2424
namespace local_o365\feature\usergroups;
2525

26+
use WebDriver\Exception;
27+
2628
define('API_CALL_RETRY_LIMIT', 3);
2729

2830
class coursegroups {
@@ -119,23 +121,23 @@ public function create_groups_for_new_courses() {
119121
foreach ($courses as $course) {
120122
$coursesprocessed++;
121123
$createclassteam = false;
122-
$ownerids = [];
124+
$ownerid = null;
123125

124126
if (\local_o365\feature\usergroups\utils::course_is_group_feature_enabled($course->id, 'team')) {
125127
$teacherids = $this->get_teacher_ids_of_course($course->id);
126128
foreach ($teacherids as $teacherid) {
127129
if ($ownerid = $this->DB->get_field('local_o365_objects', 'objectid',
128130
['type' => 'user', 'moodleid' => $teacherid])) {
129-
$ownerids[] = $ownerid;
130131
$createclassteam = true;
132+
break;
131133
}
132134
}
133135
}
134136

135137
if ($createclassteam) {
136138
// Create class team directly.
137139
try {
138-
$objectrec = $this->create_class_team($course, $ownerids, $groupprefix);
140+
$objectrec = $this->create_class_team($course, $ownerid, $groupprefix);
139141
} catch (\Exception $e) {
140142
$this->mtrace('Could not create class team for course #' . $course->id . '. Reason: ' . $e->getMessage());
141143
continue;
@@ -260,7 +262,7 @@ public function create_group($course, $groupprefix = null) {
260262
if (!empty($course->summary)) {
261263
$description = strip_tags($course->summary);
262264
if (strlen($description) > 1024) {
263-
$description = substr($description, 0, 1020) . ' ...';
265+
$description = shorten_text($description, 1024, true, ' ...');
264266
}
265267
$extra = [
266268
'description' => $description,
@@ -292,11 +294,11 @@ public function create_group($course, $groupprefix = null) {
292294
* Create an Office 365 class team for a Moodle course.
293295
*
294296
* @param stdClass $course
295-
* @param array $ownerids
297+
* @param array $ownerid
296298
* @param string|null $groupprefix
297299
* @return array|bool
298300
*/
299-
public function create_class_team($course, $ownerids, $groupprefix = null) {
301+
public function create_class_team($course, $ownerid, $groupprefix = null) {
300302
$now = time();
301303
$displayname = $course->fullname;
302304
if (!empty($groupprefix)) {
@@ -309,7 +311,7 @@ public function create_class_team($course, $ownerids, $groupprefix = null) {
309311

310312
try {
311313
$teamid = null;
312-
$response = $this->graphclient->create_class_team($displayname, $description, $ownerids, $extra);
314+
$response = $this->graphclient->create_class_team($displayname, $description, $ownerid, $extra);
313315

314316
if (is_array($response) && array_key_exists('Location', $response)) {
315317
$location = $response['Location'];
@@ -483,7 +485,6 @@ public function resync_group_membership($courseid, $groupobjectid = null, $curre
483485
JOIN ($esql) je ON je.id = u.id
484486
JOIN {local_o365_objects} objs ON objs.moodleid = u.id
485487
WHERE u.deleted = 0 AND objs.type = :user";
486-
$params['tokresource'] = 'https://graph.windows.net';
487488
$params['user'] = 'user';
488489
$enrolled = $this->DB->get_recordset_sql($sql, $params);
489490
foreach ($enrolled as $user) {
@@ -516,45 +517,75 @@ public function resync_group_membership($courseid, $groupobjectid = null, $curre
516517

517518
$teacherids = $this->get_teacher_ids_of_course($courseid);
518519

519-
// Add users.
520-
$this->mtrace('Users to add: '.count($toadd));
521-
foreach ($toadd as $userobjectid => $moodleuserid) {
522-
$this->mtrace('... Adding '.$userobjectid.' (muserid: '.$moodleuserid.')...', '');
523-
524-
$retrycounter = 0;
525-
while ($retrycounter <= API_CALL_RETRY_LIMIT) {
526-
$result = $this->graphclient->add_member_to_group($groupobjectid, $userobjectid);
520+
//Check if group object is created
521+
$this->mtrace('... Checking if group is setup ...', '');
522+
$retrycounter = 0;
523+
while ($retrycounter <= API_CALL_RETRY_LIMIT) {
524+
try {
527525
if ($retrycounter) {
528526
$this->mtrace('...... Retry #' . $retrycounter);
529527
sleep(10);
530528
}
531-
if ($result === true) {
529+
$result = $this->graphclient->get_group($groupobjectid);
530+
if (!empty($result['id'])) {
532531
$this->mtrace('Success!');
533532
break;
534533
} else {
535534
$this->mtrace('Error!');
536-
$this->mtrace('...... Received: '.\local_o365\utils::tostring($result));
535+
$this->mtrace('...... Received: ' . \local_o365\utils::tostring($result));
537536
$retrycounter++;
538-
539-
if (strpos($result, 'Request_ResourceNotFound') === false) {
540-
break;
541-
}
542537
}
538+
} catch (\Exception $e) {
539+
$this->mtrace('Error!');
540+
$this->mtrace('...... Received: ' . $e->getMessage());
541+
$retrycounter++;
543542
}
543+
}
544544

545-
// Add teacher as owner of O365 group.
546-
if (in_array($moodleuserid, $teacherids)) {
547-
$this->mtrace('... Adding teacher as owner, Teacher Id: '.$userobjectid.' (muserid: '.$moodleuserid.')...', '');
548-
try {
545+
// Add users.
546+
$this->mtrace('Users to add: '.count($toadd));
547+
foreach ($toadd as $userobjectid => $moodleuserid) {
548+
$this->mtrace('... Adding '.$userobjectid.' (muserid: '.$moodleuserid.')...', '');
549+
$retrycounter = 0;
550+
while ($retrycounter <= API_CALL_RETRY_LIMIT) {
551+
// Add teacher as owner of O365 group.
552+
if (in_array($moodleuserid, $teacherids)) {
553+
$this->mtrace('... Adding teacher as owner, Teacher Id: ' . $userobjectid . ' (muserid: ' . $moodleuserid . ')...', '');
554+
if ($retrycounter) {
555+
$this->mtrace('...... Retry #' . $retrycounter);
556+
sleep(10);
557+
}
549558
$result = $this->graphclient->add_owner_to_group($groupobjectid, $userobjectid);
550559
if ($result === true) {
551560
$this->mtrace('Success!');
561+
break;
562+
} else {
563+
$this->mtrace('Error!');
564+
$this->mtrace('...... Received: ' . \local_o365\utils::tostring($result));
565+
$retrycounter++;
566+
567+
if (strpos($result, 'Request_ResourceNotFound') === false) {
568+
break;
569+
}
570+
}
571+
} else {
572+
if ($retrycounter) {
573+
$this->mtrace('...... Retry #' . $retrycounter);
574+
sleep(10);
575+
}
576+
$result = $this->graphclient->add_member_to_group($groupobjectid, $userobjectid);
577+
if ($result === true) {
578+
$this->mtrace('Success!');
579+
break;
552580
} else {
553581
$this->mtrace('Error!');
554582
$this->mtrace('...... Received: '.\local_o365\utils::tostring($result));
583+
$retrycounter++;
584+
585+
if (strpos($result, 'Request_ResourceNotFound') === false) {
586+
break;
587+
}
555588
}
556-
} catch (\Exception $e) {
557-
$this->mtrace('Error!');
558589
}
559590
}
560591
}
@@ -619,7 +650,7 @@ public function replace_group_notebook_job() {
619650
JOIN ($esql) je ON je.id = u.id
620651
JOIN {auth_oidc_token} tok ON tok.userid = u.id AND tok.resource = :tokresource
621652
WHERE u.deleted = 0";
622-
$params['tokresource'] = 'https://graph.windows.net';
653+
$params['tokresource'] = \local_o365\rest\unified::get_resource();
623654
$enrolled = $this->DB->get_recordset_sql($sql, $params);
624655
foreach ($enrolled as $user) {
625656
if (in_array($user->id, $teacherids)) {

classes/feature/usersync/main.php

+115-37
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,7 @@ public static function get_sync_options() {
635635
*/
636636
public static function sync_option_enabled($option) {
637637
$options = static::get_sync_options();
638-
return (!empty($options[$option])) ? true : false;
638+
return isset($options[$option]);
639639
}
640640

641641
/**
@@ -669,8 +669,17 @@ public function sync_users(array $aadusers = array()) {
669669
}
670670
}
671671

672+
if ($aadsync['emailsync']) {
673+
$select = 'SELECT u.email,
674+
u.username,';
675+
$where = ' WHERE u.email';
676+
} else {
677+
$select = 'SELECT u.username,';
678+
$where = ' WHERE u.username';
679+
}
680+
672681
list($usernamesql, $usernameparams) = $DB->get_in_or_equal($usernames);
673-
$sql = 'SELECT u.username,
682+
$sql = "$select
674683
u.id as muserid,
675684
u.auth,
676685
tok.id as tokid,
@@ -684,8 +693,8 @@ public function sync_users(array $aadusers = array()) {
684693
LEFT JOIN {local_o365_connections} conn ON conn.muserid = u.id
685694
LEFT JOIN {local_o365_appassign} assign ON assign.muserid = u.id
686695
LEFT JOIN {local_o365_objects} obj ON obj.type = ? AND obj.moodleid = u.id
687-
WHERE u.username '.$usernamesql.' AND u.mnethostid = ? AND u.deleted = ?
688-
ORDER BY CONCAT(u.username, \'~\')'; // Sort [email protected] before john.smith.
696+
$where $usernamesql AND u.mnethostid = ? AND u.deleted = ?
697+
ORDER BY CONCAT(u.username, '~')"; // Sort [email protected] before john.smith.
689698
$params = array_merge(['user'], $usernameparams, [$CFG->mnet_localhost_id, '0']);
690699
$existingusers = $DB->get_records_sql($sql, $params);
691700

@@ -715,6 +724,12 @@ public function sync_users(array $aadusers = array()) {
715724

716725
foreach ($aadusers as $user) {
717726
$this->mtrace(' ');
727+
728+
if (empty($user['upnlower'])) {
729+
$this->mtrace('Azure AD user missing UPN (' . $user['objectId'] . '); skipping...');
730+
continue;
731+
}
732+
718733
$this->mtrace('Syncing user '.$user['upnlower']);
719734

720735
if (\local_o365\rest\unified::is_configured()) {
@@ -723,38 +738,6 @@ public function sync_users(array $aadusers = array()) {
723738
$userobjectid = $user['objectId'];
724739
}
725740

726-
if (isset($user['aad.isDeleted']) && $user['aad.isDeleted'] == '1') {
727-
if (isset($aadsync['delete'])) {
728-
// Check for synced user.
729-
$sql = 'SELECT u.*
730-
FROM {user} u
731-
JOIN {local_o365_objects} obj ON obj.type = \'user\' AND obj.moodleid = u.id
732-
JOIN {auth_oidc_token} tok ON tok.userid = u.id
733-
WHERE u.username = ?
734-
AND u.mnethostid = ?
735-
AND u.deleted = ?
736-
AND u.suspended = ?
737-
AND u.auth = ?';
738-
$params = [
739-
trim(\core_text::strtolower($user['userPrincipalName'])),
740-
$CFG->mnet_localhost_id,
741-
'0',
742-
'0',
743-
'oidc',
744-
time()
745-
];
746-
$synceduser = $DB->get_record_sql($sql, $params);
747-
if (!empty($synceduser)) {
748-
$synceduser->suspended = 1;
749-
user_update_user($synceduser, false);
750-
$this->mtrace($synceduser->username.' was marked deleted in Azure.');
751-
}
752-
} else {
753-
$this->mtrace('User is deleted. Skipping.');
754-
}
755-
continue;
756-
}
757-
758741
if (!isset($existingusers[$user['upnlower']]) && !isset($existingusers[$user['upnsplit0']])) {
759742
$newmuser = $this->sync_new_user($aadsync, $user);
760743
} else {
@@ -805,6 +788,7 @@ public function sync_users(array $aadusers = array()) {
805788
}
806789

807790
protected function sync_new_user($syncoptions, $aaduserdata) {
791+
global $DB;
808792
$this->mtrace('User doesn\'t exist in Moodle');
809793

810794
$userobjectid = (\local_o365\rest\unified::is_configured())
@@ -822,7 +806,15 @@ protected function sync_new_user($syncoptions, $aaduserdata) {
822806
$this->mtrace('Created user #'.$newmuser->id);
823807
}
824808
} catch (\Exception $e) {
825-
$this->mtrace('Could not create user "'.$aaduserdata['userPrincipalName'].'" Reason: '.$e->getMessage());
809+
if ($syncoptions['emailsync']) {
810+
if ($DB->record_exists('user', ['username' => $aaduserdata['userPrincipalName']])) {
811+
$this->mtrace('Could not create user "'.$aaduserdata['userPrincipalName'].'" Reason: user with same username, but different email already exists.');
812+
} else {
813+
$this->mtrace('Could not create user with email "'.$aaduserdata['userPrincipalName'].'" Reason: '.$e->getMessage());
814+
}
815+
} else {
816+
$this->mtrace('Could not create user "'.$aaduserdata['userPrincipalName'].'" Reason: '.$e->getMessage());
817+
}
826818
}
827819

828820
// User app assignment.
@@ -953,4 +945,90 @@ protected function sync_users_matchuser($syncoptions, $aaduserdata, $existinguse
953945
return true;
954946
}
955947
}
948+
949+
public function delete_users() {
950+
global $CFG, $DB;
951+
try {
952+
$httpclient = new \local_o365\httpclient();
953+
$clientdata = \local_o365\oauth2\clientdata::instance_from_oidc();
954+
$resource = \local_o365\rest\unified::get_resource();
955+
$token = \local_o365\utils::get_app_or_system_token($resource, $clientdata, $httpclient);
956+
$apiclient = new \local_o365\rest\unified($token, $httpclient);
957+
} catch (\Exception $e) {
958+
\local_o365\utils::debug('Could not construct graph api', 'delete_users', $e);
959+
return false;
960+
}
961+
962+
try {
963+
$deletedusersids = [];
964+
$deletedusers = $apiclient->list_deleted_users();
965+
if (is_array($deletedusers) && !empty($deletedusers['value'])) {
966+
foreach ($deletedusers['value'] as $deleteduser) {
967+
if (!empty($deleteduser) && isset($deleteduser['id'])) {
968+
// Check for synced user.
969+
$sql = 'SELECT u.*
970+
FROM {user} u
971+
JOIN {local_o365_objects} obj ON obj.type = \'user\' AND obj.moodleid = u.id
972+
WHERE u.mnethostid = ?
973+
AND u.deleted = ?
974+
AND u.suspended = ?
975+
AND u.auth = ?
976+
AND obj.objectid = ? ';
977+
$params = [trim(\core_text::strtolower($CFG->mnet_localhost_id)),
978+
'0',
979+
'0',
980+
'oidc',
981+
$deleteduser['id'],
982+
time()
983+
];
984+
$synceduser = $DB->get_record_sql($sql, $params);
985+
if (!empty($synceduser)) {
986+
$synceduser->suspended = 1;
987+
$DB->update_record('user', $synceduser);
988+
$this->mtrace($synceduser->username . ' was deleted in Azure.');
989+
}
990+
$deletedusersids[] = $deleteduser['id'];
991+
}
992+
}
993+
}
994+
// Check if all Moodle users with oidc authentication are still existing users in Azure
995+
list($objectidsql, $objectidparams) = $DB->get_in_or_equal($deletedusersids, SQL_PARAMS_QM,'param', false);
996+
$existingsql = $sql = 'SELECT u.*, obj.objectid
997+
FROM {user} u
998+
JOIN {local_o365_objects} obj ON obj.type = \'user\' AND obj.moodleid = u.id
999+
WHERE u.mnethostid = ?
1000+
AND u.deleted = ?
1001+
AND u.auth = ?
1002+
AND obj.objectid '.$objectidsql;
1003+
$existingsqlparams = array_merge([trim(\core_text::strtolower($CFG->mnet_localhost_id)),
1004+
'0',
1005+
'oidc'
1006+
], $objectidparams);
1007+
$existingusers = $DB->get_records_sql($existingsql, $existingsqlparams);
1008+
foreach ($existingusers as $existinguser) {
1009+
try {
1010+
$user = $apiclient->get_user($existinguser->objectid);
1011+
} catch (\Exception $e) {
1012+
// Do safe delete for missing users - first suspend, on second run delete
1013+
if ($existinguser->suspended) {
1014+
$this->mtrace('Could not find suspended user '.$existinguser->username.' in Azure AD. Deleting user...');
1015+
$userid = $existinguser->id;
1016+
$objectid = $existinguser->objectid;
1017+
if (delete_user($existinguser)) {
1018+
$DB->delete_records('local_o365_objects', ['objectid' => $objectid]);
1019+
$DB->delete_records('auth_oidc_token', ['userid' => $userid]);
1020+
}
1021+
} else if (!$existinguser->suspended) {
1022+
$this->mtrace('Could not find user '.$existinguser->username.' in Azure AD. Suspending user...');
1023+
$existinguser->suspended = 1;
1024+
$DB->update_record('user', $existinguser);
1025+
}
1026+
}
1027+
}
1028+
return true;
1029+
} catch (\Exception $e) {
1030+
\local_o365\utils::debug('Could not delete users', 'delete_users', $e);
1031+
return false;
1032+
}
1033+
}
9561034
}

classes/healthcheck/systemapiuser.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public function run() {
4040
// Check that the system API user has a graph resource.
4141
$tokens = get_config('local_o365', 'systemtokens');
4242
$tokens = unserialize($tokens);
43-
$graphresource = 'https://graph.windows.net';
43+
$graphresource = \local_o365\rest\unified::get_resource();
4444
if (!isset($tokens[$graphresource])) {
4545
return [
4646
'result' => false,

0 commit comments

Comments
 (0)