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

AzureActiveDirectory::formatUserData()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 49
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

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