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