UserManagement::removeAdminFromIdP()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
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 from 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
     * @return array array of institution IDs
385
     */ 
386
    public function listInstitutionsByAdmin()
387
    {
388
        $edugain = $_SESSION['eduGAIN'];
389
        // get the list of local identifers of institutions managed by this user
390
        // it will be returned as $this->currentInstitutions
391
        $this->getCurrentInstitutionsByAdmin();
392
        if (count($this->currentInstitutions) == 0) {
393
            $this->newUser = true;
394
        }
395
        // check if selfservice_registration is set to eduGAIN - if not then return with the durrent list
396
        if (\config\ConfAssistant::CONSORTIUM['selfservice_registration'] !== 'eduGAIN') {
397
            common\Logging::debug_s(4, "selfservice_registration not set to eduGAIN\n");
398
            return $this->currentInstitutions;
399
        }
400
        // now add additional institutions based on the external DB 
401
        // proceed only if user has been authenticated fron an eduGAIN IdP        
402
        if ($edugain == false) {
403
            return $this->currentInstitutions;            
404
        }
405
        $email = $_SESSION['auth_email'];
406
        $externalDB = CAT::determineExternalConnection();
407
        // get the list of identifiers in the external DB with this user listed as the admin and linked to CAT institutions
408
        $extInstList = $externalDB->listExternalEntitiesByUserEmail($email);
409
        $extInstListTmp = $extInstList;
410
        // we begin by removing entites in $extInstList which are already managed by this user and synced -
411
        // these require no further checking
412
        foreach ($extInstListTmp as $country => $extInstCountryList) {
413
            $len = count($extInstCountryList);
414
            for($i = 0; $i < $len; ++$i) {
415
                $extInst = $extInstCountryList[$i];
416
                if ($extInst['inst_id'] != NULL && in_array($extInst['inst_id'], $this->currentInstitutions['existing'])) {
417
                    unset($extInstList[$country][$i]);
418
                }
419
            }
420
            if (count($extInstList[$country]) == 0) {
421
                unset($extInstList[$country]);
422
            }
423
        }
424
        // now verify the $extInstList separately for each federation making sure
425
        // that the federation allows autoregistration
426
        foreach ($extInstList as $country => $extInstCountryList) {
427
            $fed = new Federation($country);
428
            $this->doExternalDBAutoregister($extInstCountryList, $fed);
429
            $this->doExternalDBAutoregisterNew($extInstCountryList, $fed);
430
        }
431
        // now we run tests for adding admins based on pairwise-id and entitlement
432
        $entitledCountries = $this->getUserEntitledFed();
433
        $entitlementInst = $this->listCatInstitutionsByPairwiseId();
434
        common\Logging::debug_s(4, $entitlementInst, "entitlementInst\n", "\n");
435
        foreach ($entitlementInst as $country => $instList) {
436
            if (!in_array($country, $entitledCountries)) {
437
                continue;
438
            }
439
            $fed = new Federation($country);
440
            $this->doEntitlementAutoregister($instList, $fed);
441
        }
442
        $_SESSION['entitledIdPs'] = array_column($this->currentInstitutions['entitlement'], 0);
443
        $_SESSION['resyncedIdPs'] = $this->currentInstitutions['resynced'];
444
        $_SESSION['newIdPs'] = $this->currentInstitutions['new'];
445
        common\Logging::debug_s(4, $this->currentInstitutions, "currentInstitutions\n", "\n");
446
        return $this->currentInstitutions;
447
    }
448
449
    /**
450
     * Handle auto-registration of admin for CAT institutions which are synced to
451
     * eduroam DB institutions which have the current admin listed
452
     * The method verifies that the federation allows auto-registration
453
     * 
454
     * @param array $extInstCountryList - list of eduroam DB candidate institutions
455
     * @param object $fed - the Federation object
456
     */
457
    private function doExternalDBAutoregister($extInstCountryList, $fed) {
458
        $userId = $_SESSION['user'];
0 ignored issues
show
Unused Code introduced by
The assignment to $userId is dead and can be removed.
Loading history...
459
        $email = $_SESSION['auth_email'];
0 ignored issues
show
Unused Code introduced by
The assignment to $email is dead and can be removed.
Loading history...
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
        }
474
    }
475
    
476
    /**
477
     * Handle auto-creation of new CAT institutions which match
478
     * eduroam DB institutions which have the current admin listed
479
     * The method verifies that the federation allows auto-cereation
480
     * 
481
     * @param array $extInstCountryList - list of eduroam DB candidate institutions
482
     * @param object $fed - the Federation object
483
     */
484
    private function doExternalDBAutoregisterNew($extInstCountryList, $fed) {
485
        $newInstFlag = $fed->getAttributes('fed:autoregister-new-inst');
486
        if ($newInstFlag == []) {
487
            return;
488
        }
489
        foreach ($extInstCountryList as $extInst) {
490
            common\Logging::debug_s(4, "Testing ".$extInst['external_db_id']." for potential new inst\n");
491
            if ($extInst['inst_id'] != null) { // there alreay exeists a CAT institution synced to this one
492
                continue;
493
            }
494
            $country = strtoupper($fed->tld);
495
            // now run checks against creating dupplicates in CAT DB
496
            $disectedNames = ExternalEduroamDBData::dissectCollapsedInstitutionNames($extInst['name']);
497
            $names = $disectedNames['joint'];
498
            $realms = ExternalEduroamDBData::dissectCollapsedInstitutionRealms($extInst['realm']);
499
            $foundMatch = $this->checkForSimilarInstitutions($names, $realms);
500
            common\Logging::debug_s(4, $foundMatch, "checkForSimilarInstitutions returned: ","\n");
501
            if ($foundMatch == 0) {
502
                $this->currentInstitutions['new'][] = [$extInst['external_db_id'], $disectedNames['perlang'], $country];
503
            }
504
        }
505
    }
506
507
    /**
508
     * Handle auto-registration of admin for CAT institutions
509
     * based on data from eduGAIN login
510
     * The method verifies that the federation allows this type of auto-registration
511
     * 
512
     * @param object $fed - the Federation object
513
     */
514
    private function doEntitlementAutoregister($instList, $fed) {
515
        $useEntitlementFlag = $fed->getAttributes('fed:autoregister-entitlement');
516
        if ($useEntitlementFlag == []) {
517
            return;
518
        }
519
        $country = strtoupper($fed->tld);
520
        foreach ($instList as $instId) {
521
            if (!in_array($instId, $this->currentInstitutions['existing'])) {
522
                $this->currentInstitutions['entitlement'][] = [$instId, $country];
523
            }
524
        }
525
      
526
    }
527
528
    /**
529
     * Generate a list of externalDB institutions for which the admin is entitled
530
     * based on the eduPersonEntitlement setting and scope from pairwise-id
531
     * The 
532
     * @return array indexed by countries
533
     */
534
    private function listCatInstitutionsByPairwiseId() {
535
        if (!isset(\config\ConfAssistant::CONSORTIUM['entitlement'])) {
536
            return [];            
537
        }
538
        // first check if pairwise-id is set
539
        $userId = $_SESSION['user'];
540
        if (substr($userId, 0, 12) !== 'pairwise-id:') {
541
            return [];
542
        }
543
544
        // next check if entitlement is setand matches our expectations
545
        if (!isset($_SESSION['entitlement'])) {
546
            return [];
547
        }
548
549
        // get realm from pariwise-id
550
        if (preg_match('/^pairwise-id:[^@]+@([^!]+)!/', $userId, $matches) == 0) {
551
            return [];            
552
        }
553
554
        $userRealm = $matches[1];
555
        common\Logging::debug_s(4, $userRealm, "userRealm:", "\n");
556
        // list CAT inst_id and country from the profile
557
        $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'";
558
        $institutions = $this->databaseHandle->exec($query);
559
        $catInstList = $institutions->fetch_all();
560
        $returnarray = [];
561
        foreach ($catInstList as $inst) {
562
            $country = $inst[1];
563
            if (!isset($returnarray[$country])) {
564
                $returnarray[$country] = [];
565
            }
566
            $returnarray[$country][] = $inst[0];
567
        }
568
        return $returnarray;
569
    }
570
    
571
    /**
572
     * Get the list of eduroam countries that the user could potentially auto-register
573
     * based on eduPersonEntitlement. The list of countries is based on matching between
574
     * the eduGAIN federation where the eduGAIN IdP is registered and the counters that this federation serves
575
     * Before passing on the list it is checked if particular countries allow entitlement-based
576
     * autoregistration
577
     * 
578
     * @return array indexed by countries
579
     */
580
    private function getUserEntitledFed() {
581
        if (!isset($_SESSION['entitlement'])) {
582
            return [];
583
        }
584
        $entitledCountries = [];
585
        $countries = $this->databaseHandle->exec("SELECT country FROM edugain WHERE reg_auth = ?", 's', $_SESSION['eduGAIN']);
586
        $countryList = $countries->fetch_all();
587
        $countriesTmp = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $countriesTmp is dead and can be removed.
Loading history...
588
        foreach ($countryList as $country) {
589
            $requiredEntitlement = NULL;
590
            $countryCode = $country[0];
591
            $fed = new Federation($countryCode);
592
            $autoreg = $fed->getAttributes('fed:autoregister-entitlement');
593
            $entitlementVal = $fed->getAttributes('fed:entitlement-attr');
594
            if (isset($autoreg[0]['value']) && $autoreg[0]['value'] == 'on') {
595
                $requiredEntitlement = isset($entitlementVal[0]['value']) ? $entitlementVal[0]['value'] : \config\ConfAssistant::CONSORTIUM['entitlement'];
596
            }
597
            common\Logging::debug_s(4, $requiredEntitlement, "$countryCode requiredEntitlement\n", "\n");
598
            if (in_array($requiredEntitlement, $_SESSION['entitlement'])) {
599
                $entitledCountries[] = $countryCode;
600
            }
601
        }
602
        common\Logging::debug_s(4, $entitledCountries, "entitledCountries:\n", "\n");
603
        return $entitledCountries;
604
    }
605
    
606
    /**
607
     * Tests if the institution with these identifier does not yet exist in CAT. 
608
     * This is done by testing the admins "new" institutions, this way we also make sure
609
     * that this admin is actually also allowed to create the new one
610
     * 
611
     * @return int 1 or 0. 1 means we are free to create the inst.
612
     */
613
    
614
    public function checkForCatMatch($extId, $ROid) {
615
        $this->listInstitutionsByAdmin();
616
        foreach ($this->currentInstitutions['new'] as $newInst) {
617
            if ($extId == $newInst[0] && $ROid == strtoupper($newInst[2]).'01') {
618
                return 0;
619
            }
620
        }
621
        return 1;
622
    }
623
    
624
    /**
625
     * get the list of current institutions of the given admin
626
     * 
627
     * This method does not rerurn anything but sets $this->currentInstitutions
628
     * it only fillsh the 'existing' block, leaving the other two for other methods
629
     * to deal with
630
     */
631
    private function getCurrentInstitutionsByAdmin() {
632
        $returnarray = [
633
            'existing' => [],
634
            'resynced' => [],
635
            'new' => [],
636
            'entitlement' => []
637
        ];
638
        $userId = $_SESSION['user'];
639
        // get the list of local identifers of institutions managed by this user
640
        $institutions = $this->databaseHandle->exec("SELECT ownership.institution_id as inst_id FROM ownership WHERE user_id = ? ORDER BY institution_id", "s", $userId);
641
        // SELECT -> resource, not boolean
642
        $catInstList = $institutions->fetch_all();
643
        foreach ($catInstList as $inst) {
644
            $returnarray['existing'][] = $inst[0];
645
        }
646
        $this->currentInstitutions = $returnarray;
647
    }
648
649
    /**
650
     * given arrays of realms and names check if there already are institutions in CAT that 
651
     * could be a match - this is for ellimination and against creating duplicates
652
     * still this is not perfect, no realms given and institutions with a slightly different
653
     * name will return no-match and thus open possibility for dupplicates
654
     * 
655
     * @param array $namesToTest
656
     * @param array $realmsToTest
657
     * @return int - 1 - a match was found, 0 - no match found
658
     */
659
    private function checkForSimilarInstitutions($namesToTest, $realmsToTest) {
660
        //generate a list of all existing realms
661
        \core\common\Logging::debug_s(4, $namesToTest, "Testing names:\n", "\n");
662
        \core\common\Logging::debug_s(4, $realmsToTest, "Testing realms:\n", "\n");
663
        $realmsList = [];
664
        $query = 'SELECT DISTINCT realm FROM profile';
665
        $realmsResult = $this->databaseHandle->exec($query);
666
        while ($anonId = $realmsResult->fetch_row()) {
667
            $realmsList[] = mb_strtolower(preg_replace('/^.*@/', '', $anonId[0]), 'UTF-8');
668
        }
669
        // now test realms
670
        $results = array_intersect($realmsToTest, $realmsList);
671
        \core\common\Logging::debug_s(4, $results, "Realms compare result\n", "\n");
672
        if (count($results) !== 0) {
673
            return 1;
674
        }
675
        
676
        // generate a list of all institution names
677
        $query = "SELECT DISTINCT CONVERT(option_value USING utf8mb4) FROM institution_option WHERE option_name='general:instname'";
678
        $namesResult = $this->databaseHandle->exec($query);
679
        $namesList = [];
680
        while ($name = $namesResult->fetch_row()) {
681
            $namesList[] = mb_strtolower($name[0], 'UTF-8');
682
        }
683
684
        // now test names
685
        $results = array_intersect($namesToTest, $namesList);
686
        \core\common\Logging::debug_s(4, $results, "Realms compare result\n", "\n");        
687
        if (count($results) !== 0) {
688
            return 1;
689
        }
690
        return 0;
691
    }
692
    
693
    /**
694
     * read the last login date of given user identifier
695
     * 
696
     * @param string $user
697
     * @return string NULL - the date last seen or NULL if not recorded yet
698
     */
699
    public function getAdminLastAuth($user) {
700
        $truncatedUser = substr($user,0,999);
701
        $result = $this->databaseHandle->exec("SELECT DATE(last_login) FROM admin_logins WHERE user_id='$truncatedUser'");
702
        if ($result->num_rows == 1) {
703
            $date = $result ->fetch_row()[0];
704
            return $date;
705
        }
706
        return NULL;
707
    }
708
}