Passed
Pull Request — 1.11.x (#5763)
by Angel Fernando Quiroz
13:34
created

AzureActiveDirectory::get_name()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $selectedOrder of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $userId of type false|integer is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $userId of type false|integer is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
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