1
|
|
|
<?php |
2
|
|
|
/* For license terms, see /license.txt */ |
3
|
|
|
|
4
|
|
|
use Chamilo\PluginBundle\Entity\AzureActiveDirectory\AzureSyncState; |
5
|
|
|
use Chamilo\UserBundle\Entity\User; |
6
|
|
|
use Doctrine\ORM\Tools\SchemaTool; |
7
|
|
|
use Doctrine\ORM\Tools\ToolsException; |
8
|
|
|
use TheNetworg\OAuth2\Client\Provider\Azure; |
9
|
|
|
|
10
|
|
|
/** |
11
|
|
|
* AzureActiveDirectory plugin class. |
12
|
|
|
* |
13
|
|
|
* @author Angel Fernando Quiroz Campos <[email protected]> |
14
|
|
|
* |
15
|
|
|
* @package chamilo.plugin.azure_active_directory |
16
|
|
|
*/ |
17
|
|
|
class AzureActiveDirectory extends Plugin |
18
|
|
|
{ |
19
|
|
|
public const SETTING_ENABLE = 'enable'; |
20
|
|
|
public const SETTING_APP_ID = 'app_id'; |
21
|
|
|
public const SETTING_APP_SECRET = 'app_secret'; |
22
|
|
|
public const SETTING_BLOCK_NAME = 'block_name'; |
23
|
|
|
public const SETTING_FORCE_LOGOUT_BUTTON = 'force_logout'; |
24
|
|
|
public const SETTING_MANAGEMENT_LOGIN_ENABLE = 'management_login_enable'; |
25
|
|
|
public const SETTING_MANAGEMENT_LOGIN_NAME = 'management_login_name'; |
26
|
|
|
public const SETTING_PROVISION_USERS = 'provisioning'; |
27
|
|
|
public const SETTING_UPDATE_USERS = 'update_users'; |
28
|
|
|
public const SETTING_GROUP_ID_ADMIN = 'group_id_admin'; |
29
|
|
|
public const SETTING_GROUP_ID_SESSION_ADMIN = 'group_id_session_admin'; |
30
|
|
|
public const SETTING_GROUP_ID_TEACHER = 'group_id_teacher'; |
31
|
|
|
public const SETTING_EXISTING_USER_VERIFICATION_ORDER = 'existing_user_verification_order'; |
32
|
|
|
public const SETTING_TENANT_ID = 'tenant_id'; |
33
|
|
|
public const SETTING_DEACTIVATE_NONEXISTING_USERS = 'deactivate_nonexisting_users'; |
34
|
|
|
public const SETTING_GET_USERS_DELTA = 'script_users_delta'; |
35
|
|
|
public const SETTING_GET_USERGROUPS_DELTA = 'script_usergroups_delta'; |
36
|
|
|
|
37
|
|
|
public const URL_TYPE_AUTHORIZE = 'login'; |
38
|
|
|
public const URL_TYPE_LOGOUT = 'logout'; |
39
|
|
|
|
40
|
|
|
public const EXTRA_FIELD_ORGANISATION_EMAIL = 'organisationemail'; |
41
|
|
|
public const EXTRA_FIELD_AZURE_ID = 'azure_id'; |
42
|
|
|
public const EXTRA_FIELD_AZURE_UID = 'azure_uid'; |
43
|
|
|
|
44
|
|
|
public const API_PAGE_SIZE = 999; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* AzureActiveDirectory constructor. |
48
|
|
|
*/ |
49
|
|
|
protected function __construct() |
50
|
|
|
{ |
51
|
|
|
$settings = [ |
52
|
|
|
self::SETTING_ENABLE => 'boolean', |
53
|
|
|
self::SETTING_APP_ID => 'text', |
54
|
|
|
self::SETTING_APP_SECRET => 'text', |
55
|
|
|
self::SETTING_BLOCK_NAME => 'text', |
56
|
|
|
self::SETTING_FORCE_LOGOUT_BUTTON => 'boolean', |
57
|
|
|
self::SETTING_MANAGEMENT_LOGIN_ENABLE => 'boolean', |
58
|
|
|
self::SETTING_MANAGEMENT_LOGIN_NAME => 'text', |
59
|
|
|
self::SETTING_PROVISION_USERS => 'boolean', |
60
|
|
|
self::SETTING_UPDATE_USERS => 'boolean', |
61
|
|
|
self::SETTING_GROUP_ID_ADMIN => 'text', |
62
|
|
|
self::SETTING_GROUP_ID_SESSION_ADMIN => 'text', |
63
|
|
|
self::SETTING_GROUP_ID_TEACHER => 'text', |
64
|
|
|
self::SETTING_EXISTING_USER_VERIFICATION_ORDER => 'text', |
65
|
|
|
self::SETTING_TENANT_ID => 'text', |
66
|
|
|
self::SETTING_DEACTIVATE_NONEXISTING_USERS => 'boolean', |
67
|
|
|
self::SETTING_GET_USERS_DELTA => 'boolean', |
68
|
|
|
self::SETTING_GET_USERGROUPS_DELTA => 'boolean', |
69
|
|
|
]; |
70
|
|
|
|
71
|
|
|
parent::__construct('2.5', 'Angel Fernando Quiroz Campos, Yannick Warnier', $settings); |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* Instance the plugin. |
76
|
|
|
* |
77
|
|
|
* @staticvar null $result |
78
|
|
|
* |
79
|
|
|
* @return $this |
80
|
|
|
*/ |
81
|
|
|
public static function create() |
82
|
|
|
{ |
83
|
|
|
static $result = null; |
84
|
|
|
|
85
|
|
|
return $result ? $result : $result = new self(); |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
/** |
89
|
|
|
* @return string |
90
|
|
|
*/ |
91
|
|
|
public function get_name() |
92
|
|
|
{ |
93
|
|
|
return 'azure_active_directory'; |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* @return Azure |
98
|
|
|
*/ |
99
|
|
|
public function getProvider() |
100
|
|
|
{ |
101
|
|
|
$provider = new Azure([ |
102
|
|
|
'clientId' => $this->get(self::SETTING_APP_ID), |
103
|
|
|
'clientSecret' => $this->get(self::SETTING_APP_SECRET), |
104
|
|
|
'redirectUri' => api_get_path(WEB_PLUGIN_PATH).'azure_active_directory/src/callback.php', |
105
|
|
|
]); |
106
|
|
|
|
107
|
|
|
return $provider; |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
public function getProviderForApiGraph(): Azure |
111
|
|
|
{ |
112
|
|
|
$provider = $this->getProvider(); |
113
|
|
|
$provider->urlAPI = "https://graph.microsoft.com/v1.0/"; |
114
|
|
|
$provider->resource = "https://graph.microsoft.com/"; |
115
|
|
|
$provider->tenant = $this->get(AzureActiveDirectory::SETTING_TENANT_ID); |
116
|
|
|
$provider->authWithResource = false; |
117
|
|
|
|
118
|
|
|
return $provider; |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* @param string $urlType Type of URL to generate |
123
|
|
|
* |
124
|
|
|
* @return string |
125
|
|
|
*/ |
126
|
|
|
public function getUrl($urlType) |
127
|
|
|
{ |
128
|
|
|
if (self::URL_TYPE_LOGOUT === $urlType) { |
129
|
|
|
$provider = $this->getProvider(); |
130
|
|
|
|
131
|
|
|
return $provider->getLogoutUrl( |
132
|
|
|
api_get_path(WEB_PATH) |
133
|
|
|
); |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
return api_get_path(WEB_PLUGIN_PATH).$this->get_name().'/src/callback.php'; |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* Create extra fields for user when installing. |
141
|
|
|
* |
142
|
|
|
* @throws ToolsException |
143
|
|
|
*/ |
144
|
|
|
public function install() |
145
|
|
|
{ |
146
|
|
|
UserManager::create_extra_field( |
147
|
|
|
self::EXTRA_FIELD_ORGANISATION_EMAIL, |
148
|
|
|
ExtraField::FIELD_TYPE_TEXT, |
149
|
|
|
$this->get_lang('OrganisationEmail'), |
150
|
|
|
'' |
151
|
|
|
); |
152
|
|
|
UserManager::create_extra_field( |
153
|
|
|
self::EXTRA_FIELD_AZURE_ID, |
154
|
|
|
ExtraField::FIELD_TYPE_TEXT, |
155
|
|
|
$this->get_lang('AzureId'), |
156
|
|
|
'' |
157
|
|
|
); |
158
|
|
|
UserManager::create_extra_field( |
159
|
|
|
self::EXTRA_FIELD_AZURE_UID, |
160
|
|
|
ExtraField::FIELD_TYPE_TEXT, |
161
|
|
|
$this->get_lang('AzureUid'), |
162
|
|
|
'' |
163
|
|
|
); |
164
|
|
|
|
165
|
|
|
$em = Database::getManager(); |
166
|
|
|
|
167
|
|
|
if ($em->getConnection()->getSchemaManager()->tablesExist(['course_home_notify_notification'])) { |
168
|
|
|
return; |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
$schemaTool = new SchemaTool($em); |
172
|
|
|
$schemaTool->createSchema( |
173
|
|
|
[ |
174
|
|
|
$em->getClassMetadata(AzureSyncState::class), |
175
|
|
|
] |
176
|
|
|
); |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
public function uninstall() |
180
|
|
|
{ |
181
|
|
|
$em = Database::getManager(); |
182
|
|
|
|
183
|
|
|
if (!$em->getConnection()->getSchemaManager()->tablesExist(['course_home_notify_notification'])) { |
184
|
|
|
return; |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
$schemaTool = new SchemaTool($em); |
188
|
|
|
$schemaTool->dropSchema( |
189
|
|
|
[ |
190
|
|
|
$em->getClassMetadata(AzureSyncState::class), |
191
|
|
|
] |
192
|
|
|
); |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
public function getExistingUserVerificationOrder(): array |
196
|
|
|
{ |
197
|
|
|
$defaultOrder = [1, 2, 3]; |
198
|
|
|
|
199
|
|
|
$settingValue = $this->get(self::SETTING_EXISTING_USER_VERIFICATION_ORDER); |
200
|
|
|
$selectedOrder = array_filter( |
201
|
|
|
array_map( |
202
|
|
|
'trim', |
203
|
|
|
explode(',', $settingValue) |
204
|
|
|
) |
205
|
|
|
); |
206
|
|
|
$selectedOrder = array_map('intval', $selectedOrder); |
207
|
|
|
$selectedOrder = array_filter( |
208
|
|
|
$selectedOrder, |
209
|
|
|
function ($position) use ($defaultOrder): bool { |
210
|
|
|
return in_array($position, $defaultOrder); |
211
|
|
|
} |
212
|
|
|
); |
213
|
|
|
|
214
|
|
|
if ($selectedOrder) { |
|
|
|
|
215
|
|
|
return $selectedOrder; |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
return $defaultOrder; |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
public function getUserIdByVerificationOrder(array $azureUserData, string $azureUidKey = 'objectId'): ?int |
222
|
|
|
{ |
223
|
|
|
$selectedOrder = $this->getExistingUserVerificationOrder(); |
224
|
|
|
|
225
|
|
|
$extraFieldValue = new ExtraFieldValue('user'); |
226
|
|
|
$positionsAndFields = [ |
227
|
|
|
1 => $extraFieldValue->get_item_id_from_field_variable_and_field_value( |
228
|
|
|
AzureActiveDirectory::EXTRA_FIELD_ORGANISATION_EMAIL, |
229
|
|
|
$azureUserData['mail'] |
230
|
|
|
), |
231
|
|
|
2 => $extraFieldValue->get_item_id_from_field_variable_and_field_value( |
232
|
|
|
AzureActiveDirectory::EXTRA_FIELD_AZURE_ID, |
233
|
|
|
$azureUserData['mailNickname'] |
234
|
|
|
), |
235
|
|
|
3 => $extraFieldValue->get_item_id_from_field_variable_and_field_value( |
236
|
|
|
AzureActiveDirectory::EXTRA_FIELD_AZURE_UID, |
237
|
|
|
$azureUserData[$azureUidKey] |
238
|
|
|
), |
239
|
|
|
]; |
240
|
|
|
|
241
|
|
|
foreach ($selectedOrder as $position) { |
242
|
|
|
if (!empty($positionsAndFields[$position]) && isset($positionsAndFields[$position]['item_id'])) { |
243
|
|
|
return (int) $positionsAndFields[$position]['item_id']; |
244
|
|
|
} |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
return null; |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
/** |
251
|
|
|
* @throws Exception |
252
|
|
|
*/ |
253
|
|
|
public function registerUser( |
254
|
|
|
array $azureUserInfo, |
255
|
|
|
string $azureUidKey = 'objectId' |
256
|
|
|
) { |
257
|
|
|
if (empty($azureUserInfo)) { |
258
|
|
|
throw new Exception('Groups info not found.'); |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
$userId = $this->getUserIdByVerificationOrder($azureUserInfo, $azureUidKey); |
262
|
|
|
|
263
|
|
|
if (empty($userId)) { |
264
|
|
|
// If we didn't find the user |
265
|
|
|
if ($this->get(self::SETTING_PROVISION_USERS) !== 'true') { |
266
|
|
|
throw new Exception('User not found when checking the extra fields from '.$azureUserInfo['mail'].' or '.$azureUserInfo['mailNickname'].' or '.$azureUserInfo[$azureUidKey].'.'); |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
[ |
270
|
|
|
$firstNme, |
271
|
|
|
$lastName, |
272
|
|
|
$username, |
273
|
|
|
$email, |
274
|
|
|
$phone, |
275
|
|
|
$authSource, |
276
|
|
|
$active, |
277
|
|
|
$extra, |
278
|
|
|
] = $this->formatUserData($azureUserInfo, $azureUidKey); |
279
|
|
|
|
280
|
|
|
// If the option is set to create users, create it |
281
|
|
|
$userId = UserManager::create_user( |
282
|
|
|
$firstNme, |
283
|
|
|
$lastName, |
284
|
|
|
STUDENT, |
285
|
|
|
$email, |
286
|
|
|
$username, |
287
|
|
|
'', |
288
|
|
|
null, |
289
|
|
|
null, |
290
|
|
|
$phone, |
291
|
|
|
null, |
292
|
|
|
$authSource, |
293
|
|
|
null, |
294
|
|
|
$active, |
295
|
|
|
null, |
296
|
|
|
$extra, |
297
|
|
|
null, |
298
|
|
|
null |
299
|
|
|
); |
300
|
|
|
|
301
|
|
|
if (!$userId) { |
|
|
|
|
302
|
|
|
throw new Exception(get_lang('UserNotAdded').' '.$azureUserInfo['userPrincipalName']); |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
return $userId; |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
if ($this->get(self::SETTING_UPDATE_USERS) === 'true') { |
309
|
|
|
[ |
310
|
|
|
$firstNme, |
311
|
|
|
$lastName, |
312
|
|
|
$username, |
313
|
|
|
$email, |
314
|
|
|
$phone, |
315
|
|
|
$authSource, |
316
|
|
|
$active, |
317
|
|
|
$extra, |
318
|
|
|
] = $this->formatUserData($azureUserInfo, $azureUidKey); |
319
|
|
|
|
320
|
|
|
$userId = UserManager::update_user( |
321
|
|
|
$userId, |
322
|
|
|
$firstNme, |
323
|
|
|
$lastName, |
324
|
|
|
$username, |
325
|
|
|
'', |
326
|
|
|
$authSource, |
327
|
|
|
$email, |
328
|
|
|
STUDENT, |
329
|
|
|
null, |
330
|
|
|
$phone, |
331
|
|
|
null, |
332
|
|
|
null, |
333
|
|
|
$active, |
334
|
|
|
null, |
335
|
|
|
0, |
336
|
|
|
$extra |
337
|
|
|
); |
338
|
|
|
|
339
|
|
|
if (!$userId) { |
|
|
|
|
340
|
|
|
throw new Exception(get_lang('CouldNotUpdateUser').' '.$azureUserInfo['userPrincipalName']); |
341
|
|
|
} |
342
|
|
|
} |
343
|
|
|
|
344
|
|
|
return $userId; |
345
|
|
|
} |
346
|
|
|
|
347
|
|
|
/** |
348
|
|
|
* @return array<string, string|false> |
349
|
|
|
*/ |
350
|
|
|
public function getGroupUidByRole(): array |
351
|
|
|
{ |
352
|
|
|
$groupUidList = [ |
353
|
|
|
'admin' => $this->get(self::SETTING_GROUP_ID_ADMIN), |
354
|
|
|
'sessionAdmin' => $this->get(self::SETTING_GROUP_ID_SESSION_ADMIN), |
355
|
|
|
'teacher' => $this->get(self::SETTING_GROUP_ID_TEACHER), |
356
|
|
|
]; |
357
|
|
|
|
358
|
|
|
return array_filter($groupUidList); |
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
/** |
362
|
|
|
* @return array<string, callable> |
363
|
|
|
*/ |
364
|
|
|
public function getUpdateActionByRole(): array |
365
|
|
|
{ |
366
|
|
|
return [ |
367
|
|
|
'admin' => function (User $user) { |
368
|
|
|
$user->setStatus(COURSEMANAGER); |
369
|
|
|
|
370
|
|
|
UserManager::addUserAsAdmin($user, false); |
371
|
|
|
}, |
372
|
|
|
'sessionAdmin' => function (User $user) { |
373
|
|
|
$user->setStatus(SESSIONADMIN); |
374
|
|
|
|
375
|
|
|
UserManager::removeUserAdmin($user, false); |
376
|
|
|
}, |
377
|
|
|
'teacher' => function (User $user) { |
378
|
|
|
$user->setStatus(COURSEMANAGER); |
379
|
|
|
|
380
|
|
|
UserManager::removeUserAdmin($user, false); |
381
|
|
|
}, |
382
|
|
|
]; |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
/** |
386
|
|
|
* @throws Exception |
387
|
|
|
*/ |
388
|
|
|
private function formatUserData( |
389
|
|
|
array $azureUserInfo, |
390
|
|
|
string $azureUidKey |
391
|
|
|
): array { |
392
|
|
|
$phone = null; |
393
|
|
|
|
394
|
|
|
if (isset($azureUserInfo['telephoneNumber'])) { |
395
|
|
|
$phone = $azureUserInfo['telephoneNumber']; |
396
|
|
|
} elseif (isset($azureUserInfo['businessPhones'][0])) { |
397
|
|
|
$phone = $azureUserInfo['businessPhones'][0]; |
398
|
|
|
} elseif (isset($azureUserInfo['mobilePhone'])) { |
399
|
|
|
$phone = $azureUserInfo['mobilePhone']; |
400
|
|
|
} |
401
|
|
|
|
402
|
|
|
// If the option is set to create users, create it |
403
|
|
|
$firstNme = $azureUserInfo['givenName']; |
404
|
|
|
$lastName = $azureUserInfo['surname']; |
405
|
|
|
$email = $azureUserInfo['mail']; |
406
|
|
|
$username = $azureUserInfo['userPrincipalName']; |
407
|
|
|
$authSource = 'azure'; |
408
|
|
|
$active = ($azureUserInfo['accountEnabled'] ? 1 : 0); |
409
|
|
|
$extra = [ |
410
|
|
|
'extra_'.self::EXTRA_FIELD_ORGANISATION_EMAIL => $azureUserInfo['mail'], |
411
|
|
|
'extra_'.self::EXTRA_FIELD_AZURE_ID => $azureUserInfo['mailNickname'], |
412
|
|
|
'extra_'.self::EXTRA_FIELD_AZURE_UID => $azureUserInfo[$azureUidKey], |
413
|
|
|
]; |
414
|
|
|
|
415
|
|
|
return [ |
416
|
|
|
$firstNme, |
417
|
|
|
$lastName, |
418
|
|
|
$username, |
419
|
|
|
$email, |
420
|
|
|
$phone, |
421
|
|
|
$authSource, |
422
|
|
|
$active, |
423
|
|
|
$extra, |
424
|
|
|
]; |
425
|
|
|
} |
426
|
|
|
|
427
|
|
|
public function getSyncState(string $title): ?AzureSyncState |
428
|
|
|
{ |
429
|
|
|
$stateRepo = Database::getManager()->getRepository(AzureSyncState::class); |
430
|
|
|
|
431
|
|
|
return $stateRepo->findOneBy(['title' => $title]); |
432
|
|
|
} |
433
|
|
|
|
434
|
|
|
public function saveSyncState(string $title, $value) |
435
|
|
|
{ |
436
|
|
|
$state = $this->getSyncState($title); |
437
|
|
|
|
438
|
|
|
if (!$state) { |
439
|
|
|
$state = new AzureSyncState(); |
440
|
|
|
$state->setTitle($title); |
441
|
|
|
|
442
|
|
|
Database::getManager()->persist($state); |
443
|
|
|
} |
444
|
|
|
|
445
|
|
|
$state->setValue($value); |
446
|
|
|
|
447
|
|
|
Database::getManager()->flush(); |
448
|
|
|
} |
449
|
|
|
} |
450
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.