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

AzureActiveDirectory::install()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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