UserManagement::newIdPFromExternal()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 11
c 0
b 0
f 0
dl 0
loc 16
rs 9.9
cc 3
nc 4
nop 5
1
<?php
2
3
/*
4
 * *****************************************************************************
5
 * Contributions to this work were made on behalf of the GÉANT project, a 
6
 * project that has received funding from the European Union’s Framework 
7
 * Programme 7 under Grant Agreements No. 238875 (GN3) and No. 605243 (GN3plus),
8
 * Horizon 2020 research and innovation programme under Grant Agreements No. 
9
 * 691567 (GN4-1) and No. 731122 (GN4-2).
10
 * On behalf of the aforementioned projects, GEANT Association is the sole owner
11
 * of the copyright in all material which was developed by a member of the GÉANT
12
 * project. GÉANT Vereniging (Association) is registered with the Chamber of 
13
 * Commerce in Amsterdam with registration number 40535155 and operates in the 
14
 * UK as a branch of GÉANT Vereniging.
15
 * 
16
 * Registered office: Hoekenrode 3, 1102BR Amsterdam, The Netherlands. 
17
 * UK branch address: City House, 126-130 Hills Road, Cambridge CB2 1PQ, UK
18
 *
19
 * License: see the web/copyright.inc.php file in the file structure or
20
 *          <base_url>/copyright.php after deploying the software
21
 */
22
23
/**
24
 * This file contains the UserManagement class.
25
 *
26
 * @author Stefan Winter <[email protected]>
27
 * @author Tomasz Wolniewicz <[email protected]>
28
 * 
29
 * @license see LICENSE file in root directory
30
 * 
31
 * @package Developer
32
 */
33
/**
34
 * necessary includes
35
 */
36
37
namespace core;
38
39
use \Exception;
0 ignored issues
show
Bug introduced by
The type \Exception was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
40
41
/**
42
 * This class manages user privileges and bindings to institutions
43
 *
44
 * @author Stefan Winter <[email protected]>
45
 * @author Tomasz Wolniewicz <[email protected]>
46
 * 
47
 * @package Developer
48
 */
49
class UserManagement extends \core\common\Entity
50
{
51
52
    /**
53
     * our handle to the INST database
54
     * 
55
     * @var DBConnection
56
     */
57
    private $databaseHandle;
58
    public $currentInstitutions;
59
    public $newUser = false;
60
    public $hasPotenialNewInst = false;
61
62
    /**
63
     * Class constructor. Nothing special to be done when constructing.
64
     * 
65
     * @throws Exception
66
     */
67
    public function __construct()
68
    {
69
        parent::__construct();
70
        $handle = DBConnection::handle(self::$databaseType);
71
        if ($handle instanceof DBConnection) {
72
            $this->databaseHandle = $handle;
73
        } else {
74
            throw new Exception("This database type is never an array!");
75
        }
76
    }
77
78
    /**
79
     * database which this class queries by default
80
     * 
81
     * @var string
82
     */
83
    private static $databaseType = "INST";
84
85
    const TOKENSTATUS_OK_NEW = 1;
86
    const TOKENSTATUS_OK_EXISTING = 2;
87
    const TOKENSTATUS_FAIL_ALREADYCONSUMED = -1;
88
    const TOKENSTATUS_FAIL_EXPIRED = -2;
89
    const TOKENSTATUS_FAIL_NONEXISTING = -3;
90
91
    /**
92
     * Checks if a given invitation token exists and is valid in the invitations database
93
     * returns a string with the following values:
94
     * 
95
     * OK-NEW valid token exists, and is not attached to an existing institution. When consuming the token, a new inst will be created
96
     * OK-EXISTING valid token exists, and is attached to an existing institution. When consuming the token, user will be added as an admin
97
     * FAIL-NONEXISTINGTOKEN this token does not exist at all in the database
98
     * FAIL-ALREADYCONSUMED the token exists, but has been used before
99
     * FAIL-EXPIRED the token exists, but has expired
100
     * 
101
     * @param string $token the invitation token
102
     * @return int
103
     */
104
    public function checkTokenValidity($token)
105
    {
106
        $check = $this->databaseHandle->exec("SELECT invite_token, cat_institution_id 
107
                           FROM invitations 
108
                           WHERE invite_token = ? AND invite_created >= TIMESTAMPADD(DAY, -1, NOW()) AND used = 0", "s", $token);
109
        // SELECT -> resource, not boolean
110
        if ($tokenCheck = mysqli_fetch_object(/** @scrutinizer ignore-type */ $check)) {
111
            if ($tokenCheck->cat_institution_id === NULL) {
112
                return self::TOKENSTATUS_OK_NEW;
113
            }
114
            return self::TOKENSTATUS_OK_EXISTING;
115
        }
116
        // if we haven't returned from the function yet, it is an invalid token... 
117
        // be a little verbose what's wrong with it
118
        $checkReason = $this->databaseHandle->exec("SELECT invite_token, used FROM invitations WHERE invite_token = ?", "s", $token);
119
        // SELECT -> resource, not boolean
120
        if ($invalidTokenCheck = mysqli_fetch_object(/** @scrutinizer ignore-type */ $checkReason)) {
121
            if ($invalidTokenCheck->used == 1) {
122
                return self::TOKENSTATUS_FAIL_ALREADYCONSUMED;
123
            }
124
            return self::TOKENSTATUS_FAIL_EXPIRED;
125
        }
126
        return self::TOKENSTATUS_FAIL_NONEXISTING;
127
    }
128
129
    /**
130
     * This function creates a new IdP in the database based on a valid invitation token - or adds a new administrator
131
     * to an existing one. The institution is created for the logged-in user (second argument) who presents the token (first 
132
     * argument). The tokens are created via createToken().
133
     * 
134
     * @param string $token The invitation token (must exist in the database and be valid). 
135
     * @param string $owner Persistent User ID who becomes the administrator of the institution
136
     * @return IdP 
137
     */
138
    public function createIdPFromToken(string $token, string $owner)
139
    {
140
        new CAT(); // be sure that Entity's static members are initialised
141
        common\Entity::intoThePotatoes();
142
        // the token either has cat_institution_id set -> new admin for existing inst
143
        // or contains a number of parameters from external DB -> set up new inst
144
        $instinfo = $this->databaseHandle->exec("SELECT cat_institution_id, country, name, invite_issuer_level, invite_dest_mail, external_db_uniquehandle, invite_fortype 
145
                             FROM invitations 
146
                             WHERE invite_token = ? AND invite_created >= TIMESTAMPADD(DAY, -1, NOW()) AND used = 0", "s", $token);
147
        // SELECT -> resource, no boolean
148
        if ($invitationDetails = mysqli_fetch_assoc(/** @scrutinizer ignore-type */ $instinfo)) {
149
            if ($invitationDetails['cat_institution_id'] !== NULL) { // add new admin to existing IdP
150
                // we can't rely on a unique key on this table (user IDs 
151
                // possibly too long), so run a query to find there's an
152
                // tuple already; and act accordingly
153
                $catId = $invitationDetails['cat_institution_id'];
154
                $level = $invitationDetails['invite_issuer_level'];
155
                $destMail = $invitationDetails['invite_dest_mail'];
156
                $existing = $this->databaseHandle->exec("SELECT user_id FROM ownership WHERE user_id = ? AND institution_id = ?", "si", $owner, $catId);
157
                // SELECT -> resource, not boolean
158
                if (mysqli_num_rows(/** @scrutinizer ignore-type */ $existing) > 0) {
159
                    $this->databaseHandle->exec("UPDATE ownership SET blesslevel = ?, orig_mail = ? WHERE user_id = ? AND institution_id = ?", "sssi", $level, $destMail, $owner, $catId);
160
                } else {
161
                    $this->databaseHandle->exec("INSERT INTO ownership (user_id, institution_id, blesslevel, orig_mail) VALUES(?, ?, ?, ?)", "siss", $owner, $catId, $level, $destMail);
162
                }
163
                common\Logging::writeAudit_s((string) $owner, "OWN", "IdP " . $invitationDetails['cat_institution_id'] . " - added user as owner");
164
                common\Entity::outOfThePotatoes();
165
                return new IdP($invitationDetails['cat_institution_id']);
166
            }
167
            // create new IdP
168
            $fed = new Federation($invitationDetails['country']);
169
            // find the best name for the entity: C if specified, otherwise English, otherwise whatever
170
            if ($invitationDetails['external_db_uniquehandle'] != NULL) {
171
                $idp = $this->newIdPFromExternal($invitationDetails['external_db_uniquehandle'], $fed, $invitationDetails, $owner);
172
            } else {
173
                $bestnameguess = $invitationDetails['name'];
174
                $idp = new IdP($fed->newIdP('TOKEN', $invitationDetails['invite_fortype'], $owner, $invitationDetails['invite_issuer_level'], $invitationDetails['invite_dest_mail'], $bestnameguess));
175
                $idp->addAttribute("general:instname", 'C', $bestnameguess);
176
            }
177
            common\Logging::writeAudit_s($owner, "NEW", "IdP " . $idp->identifier . " - created from invitation");
178
179
            // in case we have more admins in the queue which were invited to 
180
            // administer the same inst but haven't redeemed their invitations 
181
            // yet, then we will have to rewrite the invitations to point to the
182
            // newly created actual IdP rather than the placeholder entry in the
183
            // invitations table
184
            // which other pending invites do we have?
185
186
            $otherPending = $this->databaseHandle->exec("SELECT id
187
                             FROM invitations 
188
                             WHERE invite_created >= TIMESTAMPADD(DAY, -1, NOW()) AND used = 0 AND name = ? AND country = ? AND ( cat_institution_id IS NULL OR external_db_uniquehandle IS NULL ) ", "ss", $invitationDetails['name'], $invitationDetails['country']);
189
            // SELECT -> resource, no boolean
190
            while ($pendingDetail = mysqli_fetch_object(/** @scrutinizer ignore-type */ $otherPending)) {
191
                $this->databaseHandle->exec("UPDATE invitations SET cat_institution_id = " . $idp->identifier . " WHERE id = " . $pendingDetail->id);
192
            }
193
            common\Entity::outOfThePotatoes();
194
            return $idp;
195
        }
196
    }
197
198
    /**
199
     * create new institution based on the edxternalDB data 
200
     * @param string $extId - the eduroam database identifier
201
     * @param object $fed - the CAT federation object where the institution should be created
202
     * @param string $owner
203
     * @return type
0 ignored issues
show
Bug introduced by
The type core\type was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
204
     */
205
    public function createIdPFromExternal($extId, $fed, $owner)
206
    {
207
        $cat = new CAT();
208
        $ROid = strtoupper($fed->tld).'01';
209
        $externalinfo = $cat->getExternalDBEntityDetails($extId, $ROid);
210
        $invitationDetails = [
211
            'invite_fortype' => $externalinfo['type'],
212
            'invite_issuer_level' => "FED",
213
            'invite_dest_mail' => $_SESSION['auth_email'],
214
        ];
215
        $idp = $this->newIdPFromExternal($extId, $fed, $invitationDetails, $owner, $externalinfo);
216
        common\Logging::writeAudit_s($owner, "NEW", "IdP " . $idp->identifier . " - created from auto-registration of $extId");
217
        return $idp;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $idp returns the type core\IdP which is incompatible with the documented return type core\type.
Loading history...
218
    }
219
    
220
    /*
221
     * This is the common part of the code for createIdPFromToken and createIdPFromExternal
222
     */
223
    private function newIdPFromExternal($extId, $fed, $invitationDetails, $owner, $externalinfo = [])
224
    {
225
        // see if we had a C language, and if not, pick a good candidate 
226
        if ($externalinfo == []) {
227
            $cat = new CAT();
228
            $ROid = strtoupper($fed->tld).'01';
229
            $externalinfo = $cat->getExternalDBEntityDetails($extId, $ROid);
230
        }
231
        $bestnameguess = $externalinfo['names']['C'] ?? $externalinfo['names']['en'] ?? reset($externalinfo['names']);
232
        $idp = new IdP($fed->newIdP('SELF', $invitationDetails['invite_fortype'], $owner, $invitationDetails['invite_issuer_level'], $invitationDetails['invite_dest_mail'], $bestnameguess));
233
        foreach ($externalinfo['names'] as $instlang => $instname) {
234
            $idp->addAttribute("general:instname", $instlang, $instname);
235
        }
236
        $idp->setExternalDBId($extId, strtolower($fed->tld));
237
        $idp->addAttribute("general:instname", 'C', $bestnameguess);
238
        return $idp;
239
    }
240
241
    /**
242
     * Adds a new administrator to an existing IdP
243
     * @param IdP    $idp  institution to which the admin is to be added.
244
     * @param string $user persistent user ID that is to be added as an admin.
245
     * @return boolean This function always returns TRUE.
246
     */
247
    public function addAdminToIdp($idp, $user)
248
    {
249
        $existing = $this->databaseHandle->exec("SELECT user_id FROM ownership WHERE user_id = ? AND institution_id = ?", "si", $user, $idp->identifier);
250
        // SELECT -> resource, not boolean
251
        if (mysqli_num_rows(/** @scrutinizer ignore-type */ $existing) == 0) {
252
            $this->databaseHandle->exec("INSERT INTO ownership (institution_id,user_id,blesslevel,orig_mail) VALUES(?, ?, 'FED', 'SELF-APPOINTED')", "is", $idp->identifier, $user);
253
        }
254
        return TRUE;
255
    }
256
257
    /**
258
     * Deletes an administrator from the IdP. If the IdP and user combination doesn't match, nothing happens.
259
     * @param IdP    $idp  institution from which the admin is to be deleted.
260
     * @param string $user persistent user ID that is to be deleted as an admin.
261
     * @return boolean This function always returns TRUE.
262
     */
263
    public function removeAdminFromIdP($idp, $user)
264
    {
265
        $this->databaseHandle->exec("DELETE from ownership WHERE institution_id = $idp->identifier AND user_id = ?", "s", $user);
266
        return TRUE;
267
    }
268
269
    /**
270
     * Invalidates a token so that it can't be used any more. Tokens automatically expire after 24h, but can be invalidated
271
     * earlier, e.g. after having been used to create an institution. If the token doesn't exist in the DB or is already invalidated,
272
     * nothing happens.
273
     * 
274
     * @param string $token the token to invalidate
275
     * @return boolean This function always returns TRUE.
276
     */
277
    public function invalidateToken($token)
278
    {
279
        $this->databaseHandle->exec("UPDATE invitations SET used = 1 WHERE invite_token = ?", "s", $token);
280
        return TRUE;
281
    }
282
283
    /**
284
     * Creates a new invitation token. The token's main purpose is to be sent out by mail. The function either can generate a token for a new 
285
     * administrator of an existing institution, or for a new institution. In the latter case, the institution only actually gets 
286
     * created in the DB if the token is actually consumed via createIdPFromToken().
287
     * 
288
     * @param boolean $isByFedadmin   is the invitation token created for a federation admin (TRUE) or from an existing inst admin (FALSE)
289
     * @param array   $for            identifiers (typically email addresses) for which the invitation is created
290
     * @param mixed   $instIdentifier either an instance of the IdP class (for existing institutions to invite new admins) or a string (new institution - this is the inst name then)
291
     * @param string  $externalId     if the IdP to be created is related to an external DB entity, this parameter contains that ID
292
     * @param string  $country        if the institution is new (i.e. $inst is a string) this parameter needs to specify the federation of the new inst
293
     * @param string  $partType       the type of participant
294
     * @return mixed The function returns either the token (as string) or FALSE if something went wrong
295
     * @throws Exception
296
     */
297
    public function createTokens($isByFedadmin, $for, $instIdentifier, $externalId = 0, $country = 0, $partType = 0)
298
    {
299
        $level = ($isByFedadmin ? "FED" : "INST");
300
        $tokenList = [];
301
        foreach ($for as $oneDest) {
302
            $token = bin2hex(random_bytes(40));
303
            if ($instIdentifier instanceof IdP) {
304
                $this->databaseHandle->exec("INSERT INTO invitations (invite_fortype, invite_issuer_level, invite_dest_mail, invite_token,cat_institution_id) VALUES(?, ?, ?, ?, ?)", "ssssi", $instIdentifier->type, $level, $oneDest, $token, $instIdentifier->identifier);
305
                $tokenList[$token] = $oneDest;
306
            } else if (func_num_args() == 5) {
307
                $ROid = strtoupper($country).'01';
308
                $cat = new CAT();
309
                $extinfo = $cat->getExternalDBEntityDetails($externalId, $ROid);
310
                $extCountry = $extinfo['country'];
311
                $extType = $extinfo['type'];
312
                if(\config\Master::FUNCTIONALITY_FLAGS['SINGLE_SERVICE'] === 'MSP') {
313
                    $extType = \core\IdP::TYPE_SP;
314
                }
315
                $this->databaseHandle->exec("INSERT INTO invitations (invite_fortype, invite_issuer_level, invite_dest_mail, invite_token,name,country, external_db_uniquehandle) VALUES(?, ?, ?, ?, ?, ?, ?)", "sssssss", $extType, $level, $oneDest, $token, $instIdentifier, $extCountry, $externalId);
316
                $tokenList[$token] = $oneDest;
317
            } else if (func_num_args() == 6) { // string name, and country set - whole new IdP
318
                $this->databaseHandle->exec("INSERT INTO invitations (invite_fortype, invite_issuer_level, invite_dest_mail, invite_token,name,country) VALUES(?, ?, ?, ?, ?, ?)", "ssssss", $partType, $level, $oneDest, $token, $instIdentifier, $country);
319
                $tokenList[$token] = $oneDest;
320
            } else {
321
                throw new Exception("The invitation is somehow ... wrong.");
322
            }
323
        }
324
        if (count($for) != count($tokenList)) {
325
            throw new Exception("Creation of a new token failed!");
326
        }
327
        return $tokenList;
328
    }
329
330
    /**
331
     * Retrieves all pending invitations for an institution or for a federation.
332
     * 
333
     * @param int $idpIdentifier the identifier of the institution. If not set, returns invitations for not-yet-created insts
334
     * @return array if idp_identifier is set: an array of strings (mail addresses); otherwise an array of tuples (country;name;mail)
335
     */
336
    public function listPendingInvitations($idpIdentifier = 0)
337
    {
338
        $retval = [];
339
        $invitations = $this->databaseHandle->exec("SELECT cat_institution_id, country, name, invite_issuer_level, invite_dest_mail, invite_token , TIMESTAMPADD(DAY, 1, invite_created) as expiry
340
                                        FROM invitations 
341
                                        WHERE cat_institution_id " . ( $idpIdentifier != 0 ? "= $idpIdentifier" : "IS NULL") . " AND invite_created >= TIMESTAMPADD(DAY, -1, NOW()) AND used = 0");
342
        // SELECT -> resource, not boolean
343
        common\Logging::debug_s(4, "Retrieving pending invitations for " . ($idpIdentifier != 0 ? "IdP $idpIdentifier" : "IdPs awaiting initial creation" ) . ".\n");
344
        while ($invitationQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $invitations)) {
345
            $retval[] = ["country" => $invitationQuery->country, "name" => $invitationQuery->name, "mail" => $invitationQuery->invite_dest_mail, "token" => $invitationQuery->invite_token, "expiry" => $invitationQuery->expiry];
346
        }
347
        return $retval;
348
    }
349
350
    /** Retrieves all invitations which have expired in the last hour.
351
     * 
352
     * @return array of expired invitations
353
     */
354
    public function listRecentlyExpiredInvitations()
355
    {
356
        $retval = [];
357
        $invitations = $this->databaseHandle->exec("SELECT cat_institution_id, country, name, invite_issuer_level, invite_dest_mail, invite_token 
358
                                        FROM invitations 
359
                                        WHERE invite_created >= TIMESTAMPADD(HOUR, -25, NOW()) AND invite_created < TIMESTAMPADD(HOUR, -24, NOW()) AND used = 0");
360
        // SELECT -> resource, not boolean
361
        while ($expInvitationQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $invitations)) {
362
            common\Logging::debug_s(4, "Retrieving recently expired invitations (expired in last hour)\n");
363
            if ($expInvitationQuery->cat_institution_id == NULL) {
364
                $retval[] = ["country" => $expInvitationQuery->country, "level" => $expInvitationQuery->invite_issuer_level, "name" => $expInvitationQuery->name, "mail" => $expInvitationQuery->invite_dest_mail];
365
            } else {
366
                $retval[] = ["country" => $expInvitationQuery->country, "level" => $expInvitationQuery->invite_issuer_level, "name" => "Existing IdP", "mail" => $expInvitationQuery->invite_dest_mail];
367
            }
368
        }
369
        return $retval;
370
    }
371
372
    /**
373
     * For a given persistent user identifier, returns an array of institution identifiers (not the actual objects!) for which this
374
     * user is the/a administrator and also do comparisons to the eduroam DB results.
375
     * If the federation autoregister-synced flag is set if it turns out that the eduroam DB
376
     * lists the email of the current logged-in admin as an admin of an existing CAT institution
377
     * and this institution is synced to the matching external institutuin then this admin
378
     * will be automatically added tho the institution and the 'existing' part of $this->currentInstitutions
379
     * will be updated. This identifier will also be listed to $this->currentInstitutions['resynced']
380
     * 
381
     * If the federation autoregister-new-inst flag is set and there are exeternal institututions which could be
382
     * candidated for creating them in CAT - add the identifiers of these institutuins to this->currentInstitutions[new']
383
     * 
384
     * @param boolean $applyAutoSync controls if automatic additions if the user to the admins should be performed
385
     * @return array array of institution IDs
386
     */ 
387
    public function listInstitutionsByAdmin($applyAutoSync = false)
388
    {
389
        $edugain = $_SESSION['eduGAIN'];
390
        // get the list of local identifers of institutions managed by this user
391
        // it will be returned as $this->currentInstitutions
392
        $this->getCurrentInstitutionsByAdmin();
393
        if (count($this->currentInstitutions) == 0) {
394
            $this->newUser = true;
395
        }
396
        // check if selfservice_registration is set to eduGAIN - if not then return with the durrent list
397
        if (\config\ConfAssistant::CONSORTIUM['selfservice_registration'] !== 'eduGAIN') {
398
            common\Logging::debug_s(4, "selfservice_registration not set to eduGAIN\n");
399
            return $this->currentInstitutions;
400
        }
401
        // now add additional institutions based on the external DB 
402
        // proceed only if user has been authenticated fron an eduGAIN IdP        
403
        if ($edugain == false) {
404
            return $this->currentInstitutions;            
405
        }
406
        $email = $_SESSION['auth_email'];
407
        $externalDB = CAT::determineExternalConnection();
408
        // get the list of identifiers in the external DB with this user listed as the admin and linked to CAT institutions
409
        $extInstList = $externalDB->listExternalEntitiesByUserEmail($email);
410
        $extInstListTmp = $extInstList;
411
        // we begin by removing entites in $extInstList which are already managed by this user and synced -
412
        // these require no further checking
413
        foreach ($extInstListTmp as $country => $extInstCountryList) {
414
            $len = count($extInstCountryList);
415
            for($i = 0; $i < $len; ++$i) {
416
                $extInst = $extInstCountryList[$i];
417
                if ($extInst['inst_id'] != NULL && in_array($extInst['inst_id'], $this->currentInstitutions['existing'])) {
418
                    unset($extInstList[$country][$i]);
419
                }
420
            }
421
            if (count($extInstList[$country]) == 0) {
422
                unset($extInstList[$country]);
423
            }
424
        }
425
        // now verify the $extInstList separately for each federation making sure
426
        // that the federation allows autoregistration
427
        foreach ($extInstList as $country => $extInstCountryList) {
428
            $fed = new Federation($country);
429
            $this->doExternalDBAutoregister($extInstCountryList, $fed, $applyAutoSync);
430
            $this->doExternalDBAutoregisterNew($extInstCountryList, $fed);
431
        }
432
        // now we run tests for adding admins based on pairwise-id and entitlement
433
        $entitledCountries = $this->getUserEntitledFed();
434
        $entitlementInst = $this->listCatInstitutionsByPairwiseId();
435
        common\Logging::debug_s(4, $entitlementInst, "entitlementInst\n", "\n");
436
        foreach ($entitlementInst as $country => $instList) {
437
            if (!in_array($country, $entitledCountries)) {
438
                continue;
439
            }
440
            $fed = new Federation($country);
441
            $this->doEntitlementAutoregister($instList, $fed);
442
        }
443
        $_SESSION['entitledIdPs'] = array_column($this->currentInstitutions['entitlement'], 0);
444
        common\Logging::debug_s(4, $this->currentInstitutions, "currentInstitutions\n", "\n");
445
        return $this->currentInstitutions;
446
    }
447
448
    /**
449
     * Handle auto-registration of admin for CAT institutions which are synced to
450
     * eduroam DB institutions which have the current admin listed
451
     * The method verifies that the federation allows auto-registration
452
     * 
453
     * @param array $extInstCountryList - list of eduroam DB candidate institutions
454
     * @param object $fed - the Federation object
455
     * @param boolean $applyAutoSync - do we write updated to the databaes?
456
     */
457
    private function doExternalDBAutoregister($extInstCountryList, $fed, $applyAutoSync = false) {
458
        $userId = $_SESSION['user'];
459
        $email = $_SESSION['auth_email'];
460
        $autoSyncedFlag = $fed->getAttributes('fed:autoregister-synced');
461
        if ($autoSyncedFlag == []) {
462
            return;
463
        }        
464
        foreach ($extInstCountryList as $extInst) {
465
            common\Logging::debug_s(4, "Testing ".$extInst['external_db_id']."\n");
466
            if ($extInst['inst_id'] == null) {
467
                // this institution is not synced, skip
468
                continue;
469
            }
470
            // is institution synced, if so we add this admin if the federation allows
471
            common\Logging::debug_s(4, "It is synced\n");
472
            $this->currentInstitutions['resynced'][] = $extInst['inst_id'];
473
            if ($applyAutoSync) {
474
                common\Logging::debug_s(4, "Adding admin to ".$extInst['inst_id']."\n");
475
                $this->currentInstitutions['existing'][] = $extInst['inst_id'];
476
                $query = "INSERT INTO ownership (user_id, institution_id, blesslevel, orig_mail) VALUES (?, ?, 'FED', ?)";
477
                $this->databaseHandle->exec($query, 'sis', $userId, $extInst['inst_id'], $email);
478
            }
479
        }
480
    }
481
    
482
    /**
483
     * Handle auto-creation of new CAT institutions which match
484
     * eduroam DB institutions which have the current admin listed
485
     * The method verifies that the federation allows auto-cereation
486
     * 
487
     * @param array $extInstCountryList - list of eduroam DB candidate institutions
488
     * @param object $fed - the Federation object
489
     */
490
    private function doExternalDBAutoregisterNew($extInstCountryList, $fed) {
491
        $newInstFlag = $fed->getAttributes('fed:autoregister-new-inst');
492
        if ($newInstFlag == []) {
493
            return;
494
        }
495
        foreach ($extInstCountryList as $extInst) {
496
            common\Logging::debug_s(4, "Testing ".$extInst['external_db_id']." for potential new inst\n");
497
            if ($extInst['inst_id'] != null) { // there alreay exeists a CAT institution synced to this one
498
                continue;
499
            }
500
            $country = $fed->tld;
501
            // now run checks against creating dupplicates in CAT DB
502
            $disectedNames = ExternalEduroamDBData::dissectCollapsedInstitutionNames($extInst['name']);
503
            $names = $disectedNames['joint'];
504
            $realms = ExternalEduroamDBData::dissectCollapsedInstitutionRealms($extInst['realm']);
505
            $foundMatch = $this->checkForSimilarInstitutions($names, $realms);
506
            common\Logging::debug_s(4, $foundMatch, "checkForSimilarInstitutions returned: ","\n");
507
            if ($foundMatch == 0) {
508
                $this->currentInstitutions['new'][] = [$extInst['external_db_id'], $disectedNames['perlang'], $country];
509
            }
510
        }
511
    }
512
513
    /**
514
     * Handle auto-registration of admin for CAT institutions
515
     * based on data from eduGAIN login
516
     * The method verifies that the federation allows this type of auto-registration
517
     * 
518
     * @param boolean $applyAutoSync - do we write updated to the databaes?
519
     * @param object $fed - the Federation object
520
     */
521
    private function doEntitlementAutoregister($instList, $fed) {
522
        $useEntitlementFlag = $fed->getAttributes('fed:autoregister-entitlement');
523
        if ($useEntitlementFlag == []) {
524
            return;
525
        }
526
        $country = $fed->tld;
527
        foreach ($instList as $instId) {
528
            if (!in_array($instId, $this->currentInstitutions['existing'])) {
529
                $this->currentInstitutions['entitlement'][] = [$instId, $country];
530
            }
531
        }
532
      
533
    }
534
535
    /**
536
     * Generate a list of externalDB institutions for which the admin is entitled
537
     * based on the eduPersonEntitlement setting and scope from pairwise-id
538
     * The 
539
     * @return array indexed by countries
540
     */
541
    private function listCatInstitutionsByPairwiseId() {
542
        if (!isset(\config\ConfAssistant::CONSORTIUM['entitlement'])) {
543
            return [];            
544
        }
545
        // first check if pairwise-id is set
546
        $userId = $_SESSION['user'];
547
        if (substr($userId, 0, 12) !== 'pairwise-id:') {
548
            return [];
549
        }
550
551
        // next check if entitlement is setand matches our expectations
552
        if (!isset($_SESSION['entitlement'])) {
553
            return [];
554
        }
555
556
        // get realm from pariwise-id
557
        if (preg_match('/^pairwise-id:[^@]+@([^!]+)!/', $userId, $matches) == 0) {
558
            return [];            
559
        }
560
561
        $userRealm = $matches[1];
562
        common\Logging::debug_s(4, $userRealm, "userRealm:", "\n");
563
        // list CAT inst_id and country from the profile
564
        $query = "SELECT DISTINCT profile.inst_id,country FROM profile JOIN institution on profile.inst_id = institution.inst_id WHERE realm LIKE '%@$userRealm' OR realm LIKE '%@%.$userRealm'";
565
        $institutions = $this->databaseHandle->exec($query);
566
        $catInstList = $institutions->fetch_all();
567
        $returnarray = [];
568
        foreach ($catInstList as $inst) {
569
            $country = $inst[1];
570
            if (!isset($returnarray[$country])) {
571
                $returnarray[$country] = [];
572
            }
573
            $returnarray[$country][] = $inst[0];
574
        }
575
        return $returnarray;
576
    }
577
    
578
    /**
579
     * Get the list of eduroam countries that the user could potentially auto-register
580
     * based on eduPersonEntitlement. The list of countries is based on matching between
581
     * the eduGAIN federation where the eduGAIN IdP is registered and the counters that this federation serves
582
     * Before passing on the list it is checked if particular countries allow entitlement-based
583
     * autoregistration
584
     * 
585
     * @return array indexed by countries
586
     */
587
    private function getUserEntitledFed() {
588
        if (!isset($_SESSION['entitlement'])) {
589
            return [];
590
        }
591
        $entitledCountries = [];
592
        $countries = $this->databaseHandle->exec("SELECT country FROM edugain WHERE reg_auth = ?", 's', $_SESSION['eduGAIN']);
593
        $countryList = $countries->fetch_all();
594
        $countriesTmp = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $countriesTmp is dead and can be removed.
Loading history...
595
        foreach ($countryList as $country) {
596
            $requiredEntitlement = NULL;
597
            $countryCode = $country[0];
598
            $fed = new Federation($countryCode);
599
            $autoreg = $fed->getAttributes('fed:autoregister-entitlement');
600
            $entitlementVal = $fed->getAttributes('fed:entitlement-attr');
601
            if (isset($autoreg[0]['value']) && $autoreg[0]['value'] == 'on') {
602
                $requiredEntitlement = isset($entitlementVal[0]['value']) ? $entitlementVal[0]['value'] : \config\ConfAssistant::CONSORTIUM['entitlement'];
603
            }
604
            common\Logging::debug_s(4, $requiredEntitlement, "$countryCode requiredEntitlement\n", "\n");
605
            if (in_array($requiredEntitlement, $_SESSION['entitlement'])) {
606
                $entitledCountries[] = $countryCode;
607
            }
608
        }
609
        common\Logging::debug_s(4, $entitledCountries, "entitledCountries:\n", "\n");
610
        return $entitledCountries;
611
    }
612
    
613
    /**
614
     * Tests if the institution with these identifier does not yet exist in CAT. 
615
     * This is done by testing the admins "new" institutions, this way we also make sure
616
     * that this admin is actually also allowed to create the new one
617
     * 
618
     * @return int 1 or 0. 1 means we are free to create the inst.
619
     */
620
    
621
    public function checkForCatMatch($extId, $ROid) {
622
        $this->listInstitutionsByAdmin();
623
        foreach ($this->currentInstitutions['new'] as $newInst) {
624
            if ($extId == $newInst[0] && $ROid == strtoupper($newInst[2]).'01') {
625
                return 0;
626
            }
627
        }
628
        return 1;
629
    }
630
    
631
    /**
632
     * get the list of current institutions of the given admin
633
     * 
634
     * This method does not rerurn anything but sets $this->currentInstitutions
635
     * it only fillsh the 'existing' block, leaving the other two for other methods
636
     * to deal with
637
     */
638
    private function getCurrentInstitutionsByAdmin() {
639
        $returnarray = [
640
            'existing' => [],
641
            'resynced' => [],
642
            'new' => [],
643
            'entitlement' => []
644
        ];
645
        $userId = $_SESSION['user'];
646
        // get the list of local identifers of institutions managed by this user
647
        $institutions = $this->databaseHandle->exec("SELECT ownership.institution_id as inst_id FROM ownership WHERE user_id = ? ORDER BY institution_id", "s", $userId);
648
        // SELECT -> resource, not boolean
649
        $catInstList = $institutions->fetch_all();
650
        foreach ($catInstList as $inst) {
651
            $returnarray['existing'][] = $inst[0];
652
        }
653
        $this->currentInstitutions = $returnarray;
654
    }
655
656
    /**
657
     * given arrays of realms and names check if there already are institutions in CAT that 
658
     * could be a match - this is for ellimination and against creating duplicates
659
     * still this is not perfect, no realms given and institutions with a slightly different
660
     * name will return no-match and thus open possibility for dupplicates
661
     * 
662
     * @param array $namesToTest
663
     * @param array $realmsToTest
664
     * @return int - 1 - a match was found, 0 - no match found
665
     */
666
    private function checkForSimilarInstitutions($namesToTest, $realmsToTest) {
667
        //generate a list of all existing realms
668
        $realmsList = [];
669
        $query = 'SELECT DISTINCT realm FROM profile';
670
        $realmsResult = $this->databaseHandle->exec($query);
671
        while ($anonId = $realmsResult->fetch_row()) {
672
            $realmsList[] = mb_strtolower(preg_replace('/^.*@/', '', $anonId[0]), 'UTF-8');
673
        }
674
        // now test realms
675
        $results = array_intersect($realmsToTest, $realmsList);
676
        if (count($results) !== 0) {
677
            return 1;
678
        }
679
        
680
        // generate a list of all institution names
681
        $query = "SELECT DISTINCT CONVERT(option_value USING utf8mb4) FROM institution_option WHERE option_name='general:instname'";
682
        $namesResult = $this->databaseHandle->exec($query);
683
        $namesList = [];
684
        while ($name = $namesResult->fetch_row()) {
685
            $namesList[] = mb_strtolower($name[0], 'UTF-8');
686
        }
687
688
        // now test names
689
        $results = array_intersect($namesToTest, $namesList);
690
        if (count($results) !== 0) {
691
            return 1;
692
        }
693
        return 0;
694
    }
695
}