Passed
Push — master ( a6c5e6...8eb207 )
by Stefan
07:14
created

IdP::listOwners()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 0
dl 0
loc 8
rs 10
c 0
b 0
f 0
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 Federation, IdP and Profile classes.
25
 * These should be split into separate files later.
26
 *
27
 * @package Developer
28
 */
29
/**
30
 * 
31
 */
32
33
namespace core;
34
35
use \Exception;
36
37
/**
38
 * This class represents an Identity Provider (IdP).
39
 * IdPs have properties of their own, and may have one or more Profiles. The
40
 * profiles can override the institution-wide properties.
41
 *
42
 * @author Stefan Winter <[email protected]>
43
 * @author Tomasz Wolniewicz <[email protected]>
44
 *
45
 * @license see LICENSE file in root directory
46
 *
47
 * @package Developer
48
 */
49
class IdP extends EntityWithDBProperties {
50
51
    const EXTERNAL_DB_SYNCSTATE_NOT_SYNCED = 0;
52
    const EXTERNAL_DB_SYNCSTATE_SYNCED = 1;
53
    const EXTERNAL_DB_SYNCSTATE_NOTSUBJECTTOSYNCING = 2;
54
    const TYPE_IDP = 'IdP';
55
    const TYPE_SP = 'SP';
56
    const TYPE_IDPSP = 'IdPSP';
57
58
    /**
59
     *
60
     * @var integer synchronisation state with external database, if any
61
     */
62
    private $externalDbSyncstate;
63
64
    /**
65
     * The shortname of this IdP's federation
66
     * @var string 
67
     */
68
    public $federation;
69
70
    /**
71
     * The type of participant in DB enum notation
72
     * @var string
73
     */
74
    public $type;
75
76
    /**
77
     * Constructs an IdP object based on its details in the database.
78
     * Cannot be used to define a new IdP in the database! This happens via Federation::newIdP()
79
     *
80
     * @param int $instId the database row identifier
81
     * @throws Exception
82
     */
83
    public function __construct(int $instId) {
84
        $this->databaseType = "INST";
85
        parent::__construct(); // now databaseHandle and logging is available
86
        $this->entityOptionTable = "institution_option";
87
        $this->entityIdColumn = "institution_id";
88
89
        $this->identifier = $instId;
90
91
        $idp = $this->databaseHandle->exec("SELECT inst_id, country,external_db_syncstate FROM institution WHERE inst_id = $this->identifier");
92
        // SELECT -> returns resource, not boolean
93
        if (!$instQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $idp)) {
94
            throw new Exception("IdP $this->identifier not found in database!");
95
        }
96
97
        $this->federation = $instQuery->country;
98
        $this->externalDbSyncstate = $instQuery->external_db_syncstate;
99
100
        // fetch attributes from DB; populates $this->attributes array
101
        $this->attributes = $this->retrieveOptionsFromDatabase("SELECT DISTINCT option_name, option_lang, option_value, row 
102
                                            FROM $this->entityOptionTable
103
                                            WHERE $this->entityIdColumn = ?  
104
                                            ORDER BY option_name", "IdP");
105
106
        $this->attributes[] = ["name" => "internal:country",
107
            "lang" => NULL,
108
            "value" => $this->federation,
109
            "level" => "IdP",
110
            "row" => 0,
111
            "flag" => NULL];
112
113
        $this->name = $this->languageInstance->getLocalisedValue($this->getAttributes('general:instname'));
114
        $eligibility = $this->eligibility();
115
        if (in_array(IdP::ELIGIBILITY_IDP, $eligibility) && in_array(IdP::ELIGIBILITY_SP, $eligibility)) {
116
            $this->type = IdP::TYPE_IDPSP;
117
        } elseif (in_array(IdP::ELIGIBILITY_IDP, $eligibility)) {
118
            $this->type = IdP::TYPE_IDP;
119
        } else {
120
            $this->type = IdP::TYPE_SP;
121
        }
122
        $this->loggerInstance->debug(3, "--- END Constructing new IdP object ... ---\n");
123
    }
124
125
    /**
126
     * This function retrieves all registered profiles for this IdP from the database
127
     *
128
     * @param bool $activeOnly if and set to non-zero will cause listing of only those institutions which have some valid profiles defined.
129
     * @return \core\AbstractProfile[] list of Profiles of this IdP
130
     */
131
    public function listProfiles(bool $activeOnly = FALSE) {
132
        $query = "SELECT profile_id FROM profile WHERE inst_id = $this->identifier" . ($activeOnly ? " AND showtime = 1" : "");
133
        $allProfiles = $this->databaseHandle->exec($query);
134
        $returnarray = [];
135
        // SELECT -> resource, not boolean
136
        while ($profileQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $allProfiles)) {
137
            $oneProfile = ProfileFactory::instantiate($profileQuery->profile_id, $this);
138
            $oneProfile->institution = $this->identifier;
139
            $returnarray[] = $oneProfile;
140
        }
141
142
        $this->loggerInstance->debug(4, "listProfiles: " . print_r($returnarray, true));
143
        return $returnarray;
144
    }
145
146
    /**
147
     * This function retrieves all SP deployments for this organisation from the database
148
     *
149
     * @param bool $activeOnly if and set to non-zero will cause listing of only those institutions which have some valid profiles defined.
150
     * @return \core\AbstractDeployment[] list of deployments of this IdP
151
     */
152
    public function listDeployments(bool $activeOnly = FALSE) {
153
        $query = "SELECT deployment_id FROM deployment WHERE inst_id = $this->identifier" . ($activeOnly ? " AND status = " . AbstractDeployment::ACTIVE : "");
154
        $allDeployments = $this->databaseHandle->exec($query);
155
        $returnarray = [];
156
        // SELECT -> resource, not boolean
157
        while ($deploymentQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $allDeployments)) {
158
            $returnarray[] = new DeploymentManaged($this, $deploymentQuery->deployment_id);
159
        }
160
161
        $this->loggerInstance->debug(4, "listDeployments: " . print_r($returnarray, true));
162
        return $returnarray;
163
    }
164
165
    const PROFILES_INCOMPLETE = 0;
166
    const PROFILES_CONFIGURED = 1;
167
    const PROFILES_SHOWTIME = 2;
168
169
    /**
170
     * looks through all the profiles of the inst and determines the highest prod-ready level among the profiles
171
     * @return int highest level of completeness of all the profiles of the inst
172
     */
173
    public function maxProfileStatus() {
174
        $allProfiles = $this->databaseHandle->exec("SELECT sufficient_config + showtime AS maxlevel FROM profile WHERE inst_id = $this->identifier ORDER BY maxlevel DESC LIMIT 1");
175
        // SELECT yields a resource, not a boolean
176
        while ($res = mysqli_fetch_object(/** @scrutinizer ignore-type */ $allProfiles)) {
177
            return $res->maxlevel;
178
        }
179
        return self::PROFILES_INCOMPLETE;
180
    }
181
182
    /** This function retrieves an array of authorised users which can
183
     * manipulate this institution.
184
     * 
185
     * @return array owners of the institution; numbered array with members ID, MAIL and LEVEL
186
     */
187
    public function listOwners() {
188
        $returnarray = [];
189
        $admins = $this->databaseHandle->exec("SELECT user_id, orig_mail, blesslevel FROM ownership WHERE institution_id = $this->identifier ORDER BY user_id");
190
        // SELECT -> resource, not boolean
191
        while ($ownerQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $admins)) {
192
            $returnarray[] = ['ID' => $ownerQuery->user_id, 'MAIL' => $ownerQuery->orig_mail, 'LEVEL' => $ownerQuery->blesslevel];
193
        }
194
        return $returnarray;
195
    }
196
197
    /**
198
     * Primary owners are allowed to invite other (secondary) admins to the institution
199
     * 
200
     * @param string $user ID of a logged-in user
201
     * @return boolean TRUE if this user is an admin with FED-level blessing
202
     */
203
    public function isPrimaryOwner($user) {
204
        foreach ($this->listOwners() as $oneOwner) {
205
            if ($oneOwner['ID'] == $user && $oneOwner['LEVEL'] == "FED") {
206
                return TRUE;
207
            }
208
        }
209
        return FALSE;
210
    }
211
212
    /**
213
     * This function gets the profile count for a given IdP.
214
     * 
215
     * The count could be retreived from the listProfiles method
216
     * but this is less expensive.
217
     *
218
     * @return int profile count
219
     */
220
    public function profileCount() {
221
        $result = $this->databaseHandle->exec("SELECT profile_id FROM profile 
222
             WHERE inst_id = $this->identifier");
223
        // SELECT -> resource, not boolean
224
        return(mysqli_num_rows(/** @scrutinizer ignore-type */ $result));
225
    }
226
227
    /**
228
     * This function gets the deployment count for a given IdP.
229
     *
230
     * @return int deployment count
231
     */
232
    public function deploymentCount() {
233
        $result = $this->databaseHandle->exec("SELECT deployment_id FROM deployment
234
             WHERE inst_id = $this->identifier");
235
        // SELECT -> resource, not boolean
236
        return(mysqli_num_rows(/** @scrutinizer ignore-type */ $result));
237
    }
238
239
    const ELIGIBILITY_IDP = "IdP";
240
    const ELIGIBILITY_SP = "SP";
241
242
    /**
243
     * checks whether the participant is an IdP, an SP, or both.
244
     * 
245
     * @return array list of eligibilities
246
     */
247
    public function eligibility() {
248
        $eligibilites = $this->databaseHandle->exec("SELECT type FROM institution WHERE inst_id = $this->identifier");
249
        while ($iterator = mysqli_fetch_object(/** @scrutinizer ignore-type */ $eligibilites)) {
250
            switch ($iterator->type) {
251
                case "IdP":
252
                    return [IdP::ELIGIBILITY_IDP];
253
                case "SP":
254
                    return [IdP::ELIGIBILITY_SP];
255
                default:
256
                    return [IdP::ELIGIBILITY_IDP, IdP::ELIGIBILITY_SP];
257
            }
258
        }
259
    }
260
261
    /**
262
     * This function sets the timestamp of last modification of the child profiles to the current timestamp.
263
     * 
264
     * This is needed for installer caching: all installers which are on disk 
265
     * must be re-created if an attribute changes. This timestamp here
266
     * is used to determine if the installer on disk is still new enough.
267
     * 
268
     * @return void
269
     */
270
    public function updateFreshness() {
271
        // freshness is always defined for *Profiles*
272
        // IdP needs to update timestamp of all its profiles if an IdP-wide attribute changed
273
        $this->databaseHandle->exec("UPDATE profile SET last_change = CURRENT_TIMESTAMP WHERE inst_id = '$this->identifier'");
274
    }
275
276
    /**
277
     * Adds a new profile to this IdP.
278
     * 
279
     * Only creates the DB entry for the Profile. If you want to add attributes later, see Profile::addAttribute().
280
     *
281
     * @param string $type exactly "RADIUS" or "SILVERBULLET", all other values throw an Exception
282
     * @return AbstractProfile|NULL new Profile object if successful, or NULL if an error occured
283
     */
0 ignored issues
show
Coding Style Documentation introduced by
Missing @throws tag in function comment
Loading history...
284
    public function newProfile(string $type) {
285
        $this->databaseHandle->exec("INSERT INTO profile (inst_id) VALUES($this->identifier)");
286
        $identifier = $this->databaseHandle->lastID();
287
        if ($identifier > 0) {
288
            switch ($type) {
289
                case AbstractProfile::PROFILETYPE_RADIUS:
290
                    return new ProfileRADIUS($identifier, $this);
291
                case AbstractProfile::PROFILETYPE_SILVERBULLET:
292
                    $theProfile = new ProfileSilverbullet($identifier, $this);
293
                    $theProfile->addSupportedEapMethod(new \core\common\EAP(\core\common\EAP::EAPTYPE_SILVERBULLET), 1);
294
                    $theProfile->setRealm($this->identifier . "-" . $theProfile->identifier . "." . strtolower($this->federation) . strtolower(CONFIG_CONFASSISTANT['SILVERBULLET']['realm_suffix']));
295
                    return $theProfile;
296
                default:
297
                    throw new Exception("This type of profile is unknown and can not be added.");
298
            }
299
        }
300
        return NULL;
301
    }
302
303
    /**
304
     * Adds a new hotspot deployment to this IdP.
305
     * 
306
     * Only creates the DB entry for the deployment. If you want to add attributes later, see Profile::addAttribute().
307
     *
308
     * @param string $type exactly "RADIUS-SP" or "MANAGED-SP", all other values throw an Exception
309
     * @return DeploymentManaged the newly created deployment
310
     */
0 ignored issues
show
Coding Style Documentation introduced by
Missing @throws tag in function comment
Loading history...
311
    public function newDeployment(string $type) {
312
        switch ($type) {
313
            case AbstractDeployment::DEPLOYMENTTYPE_CLASSIC:
314
                // classic deployment exist in the eduroam DB. We don't do anything here.
315
                throw new Exception("This type of deployment is handled externally and requesting it here makes no sense.");
316
            case AbstractDeployment::DEPLOYMENTTYPE_MANAGED:
317
                $this->databaseHandle->exec("INSERT INTO deployment (inst_id) VALUES($this->identifier)");
318
                $identifier = $this->databaseHandle->lastID();
319
                return new DeploymentManaged($this, $identifier);
320
            default:
321
                throw new Exception("This type of deployment is unknown and can not be added.");
322
        }
323
    }
324
325
    /**
326
     * deletes the IdP and all its profiles
327
     * 
328
     * @return void
329
     */
0 ignored issues
show
Coding Style Documentation introduced by
Missing @throws tag in function comment
Loading history...
330
    public function destroy() {
331
        common\Entity::intoThePotatoes();
332
        /* delete all profiles */
333
        foreach ($this->listProfiles() as $profile) {
334
            $profile->destroy();
335
        }
336
        /* double-check that all profiles are gone */
337
        $profiles = $this->listProfiles();
338
339
        if (count($profiles) > 0) {
340
            throw new Exception("This IdP shouldn't have any profiles any more!");
341
        }
342
343
        $this->databaseHandle->exec("DELETE FROM ownership WHERE institution_id = $this->identifier");
344
        $this->databaseHandle->exec("DELETE FROM institution_option WHERE institution_id = $this->identifier");
345
        $this->databaseHandle->exec("DELETE FROM institution WHERE inst_id = $this->identifier");
346
347
        // notify federation admins
348
349
        $fed = new Federation($this->federation);
350
        foreach ($fed->listFederationAdmins() as $id) {
351
            $user = new User($id);
352
            $message = sprintf(_("Hi,
353
354
the %s %s in your %s federation %s has been deleted from %s.
355
356
We thought you might want to know.
357
358
Best regards,
359
360
%s"), common\Entity::$nomenclature_inst, $this->name, CONFIG_CONFASSISTANT['CONSORTIUM']['display_name'], strtoupper($fed->name), CONFIG['APPEARANCE']['productname'], CONFIG['APPEARANCE']['productname_long']);
361
            $user->sendMailToUser(sprintf(_("%s in your federation was deleted"), common\Entity::$nomenclature_inst), $message);
362
        }
363
        common\Entity::outOfThePotatoes();
364
    }
365
366
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $type should have a doc-comment as per coding-style.
Loading history...
367
     * Performs a lookup in an external database to determine matching entities to this IdP. 
368
     * 
369
     * The business logic of this function is roaming consortium specific; if no match algorithm is known for the consortium, FALSE is returned.
370
     * 
371
     * @return mixed list of entities in external database that correspond to this IdP or FALSE if no consortium-specific matching function is defined
372
     */
373
    public function getExternalDBSyncCandidates($type) {
374
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
375
            $list = [];
376
            $usedarray = [];
377
            // extract all institutions from the country
378
379
380
            $syncstate = self::EXTERNAL_DB_SYNCSTATE_SYNCED;
381
            $alreadyUsed = $this->databaseHandle->exec("SELECT DISTINCT external_db_id FROM institution WHERE external_db_id IS NOT NULL AND external_db_syncstate = ?", "i", $syncstate);
382
            // SELECT -> resource, not boolean
383
            while ($alreadyUsedQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $alreadyUsed)) {
384
                $usedarray[] = $alreadyUsedQuery->external_db_id;
385
            }
386
            $lowerFed = strtolower($this->federation);
387
            $returnarray[] = ["ID" => $externalQuery->id, "name" => $thelanguage, "contactlist" => $mailnames, "country" => $externalQuery->country, "realmlist" => $externalQuery->realmlist, "type" => $externalQuery->type];
0 ignored issues
show
Comprehensibility Best Practice introduced by
$returnarray was never initialized. Although not strictly required by PHP, it is generally a good practice to add $returnarray = array(); before regardless.
Loading history...
Comprehensibility Best Practice introduced by
The variable $mailnames seems to be never defined.
Loading history...
Comprehensibility Best Practice introduced by
The variable $thelanguage seems to be never defined.
Loading history...
Comprehensibility Best Practice introduced by
The variable $externalQuery seems to be never defined.
Loading history...
388
            $eduroamDb = new ExternalEduroamDBData();
389
            $candidateList = $eduroamDb->listExternalEntities($lowerFed, $type);
390
            // and split them into ID, LANG, NAME pairs (operating on a resource, not boolean)
391
            foreach ($candidateList as $oneCandidate) {
392
                if (in_array($oneCandidate['ID'], $usedarray)) {
393
                    continue;
394
                }
395
                $list[] = $oneCandidate;
396
            }
397
            // now see if any of the languages in CAT match the best one we have gotten from DB
398
            $mynames = $this->getAttributes("general:instname");
399
            $matchingCandidates = [];
400
            foreach ($mynames as $onename) {
401
                foreach ($list as $listentry) {
402
                    if ($onename['value'] == $listentry['name'] && array_search($listentry['ID'], $matchingCandidates) === FALSE) {
403
                        $matchingCandidates[] = $listentry['ID'];
404
                    }
405
                }
406
            }
407
            return $matchingCandidates;
408
        }
409
        return FALSE;
410
    }
411
412
    /**
413
     * returns the state of sync with the external DB.
414
     * 
415
     * @return int
416
     */
417
    public function getExternalDBSyncState() {
418
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
419
            return $this->externalDbSyncstate;
420
        }
421
        return self::EXTERNAL_DB_SYNCSTATE_NOTSUBJECTTOSYNCING;
422
    }
423
424
    /**
425
     * Retrieves the external DB identifier of this institution. Returns FALSE if no ID is known.
426
     * 
427
     * @return string|boolean the external identifier; or FALSE if no external ID is known
428
     */
429
    public function getExternalDBId() {
430
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
431
            $idQuery = $this->databaseHandle->exec("SELECT external_db_id FROM institution WHERE inst_id = $this->identifier AND external_db_syncstate = " . self::EXTERNAL_DB_SYNCSTATE_SYNCED);
432
            // SELECT -> it's a resource, not a boolean
433
            if (mysqli_num_rows(/** @scrutinizer ignore-type */ $idQuery) == 0) {
434
                return FALSE;
435
            }
436
            $externalIdQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $idQuery);
437
            return $externalIdQuery->external_db_id;
438
        }
439
        return FALSE;
440
    }
441
442
    /**
443
     * Associates the external DB id with a CAT id
444
     * 
445
     * @param string $identifier the external DB id, which can be alpha-numeric
446
     * @return void
447
     */
448
    public function setExternalDBId(string $identifier) {
449
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
450
            $syncState = self::EXTERNAL_DB_SYNCSTATE_SYNCED;
451
            $alreadyUsed = $this->databaseHandle->exec("SELECT DISTINCT external_db_id FROM institution WHERE external_db_id = ? AND external_db_syncstate = ?", "si", $identifier, $syncState);
452
            // SELECT -> resource, not boolean
453
            if (mysqli_num_rows(/** @scrutinizer ignore-type */ $alreadyUsed) == 0) {
454
                $this->databaseHandle->exec("UPDATE institution SET external_db_id = ?, external_db_syncstate = ? WHERE inst_id = ?", "sii", $identifier, $syncState, $this->identifier);
455
            }
456
        }
457
    }
458
459
    /**
460
     * removes the link between a CAT institution and the external DB
461
     * 
462
     * @return void
463
     */
464
    public function removeExternalDBId() {
465
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
466
            if ($this->getExternalDBId() !== FALSE) {
467
                $syncState = self::EXTERNAL_DB_SYNCSTATE_NOT_SYNCED;
468
                $this->databaseHandle->exec("UPDATE institution SET external_db_id = NULL, external_db_syncstate = ? WHERE inst_id = ?", "ii", $syncState, $this->identifier);
469
            }
470
        }
471
    }
472
473
}
474