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

getExistingUserVerificationOrder()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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