Test Failed
Push — master ( d6f93a...0071b0 )
by Stefan
06:52
created

IdP::destroy()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 34
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 16
dl 0
loc 34
rs 9.7333
c 0
b 0
f 0
cc 4
nc 6
nop 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
55
    /**
56
     *
57
     * @var int synchronisation state with external database, if any
58
     */
59
    private $externalDbSyncstate;
60
61
    /**
62
     * The shortname of this IdP's federation
63
     * @var string 
64
     */
65
    public $federation;
66
67
    /**
68
     * Constructs an IdP object based on its details in the database.
69
     * Cannot be used to define a new IdP in the database! This happens via Federation::newIdP()
70
     *
71
     * @param int $instId the database row identifier
72
     */
73
    public function __construct(int $instId) {
74
        $this->databaseType = "INST";
75
        parent::__construct(); // now databaseHandle and logging is available
76
        $this->entityOptionTable = "institution_option";
77
        $this->entityIdColumn = "institution_id";
78
79
        $this->identifier = $instId;
80
81
        $idp = $this->databaseHandle->exec("SELECT inst_id, country,external_db_syncstate FROM institution WHERE inst_id = $this->identifier");
82
        // SELECT -> returns resource, not boolean
83
        if (!$instQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $idp)) {
84
            throw new Exception("IdP $this->identifier not found in database!");
85
        }
86
87
        $this->federation = $instQuery->country;
88
        $this->externalDbSyncstate = $instQuery->external_db_syncstate;
89
90
        // fetch attributes from DB; populates $this->attributes array
91
        $this->attributes = $this->retrieveOptionsFromDatabase("SELECT DISTINCT option_name, option_lang, option_value, row 
92
                                            FROM $this->entityOptionTable
93
                                            WHERE $this->entityIdColumn = ?  
94
                                            ORDER BY option_name", "IdP");
95
96
        $this->attributes[] = ["name" => "internal:country",
97
            "lang" => NULL,
98
            "value" => $this->federation,
99
            "level" => "IdP",
100
            "row" => 0,
101
            "flag" => NULL];
102
103
        $this->name = $this->languageInstance->getLocalisedValue($this->getAttributes('general:instname'));
104
        $this->loggerInstance->debug(3, "--- END Constructing new IdP object ... ---\n");
105
    }
106
107
    /**
108
     * This function retrieves all registered profiles for this IdP from the database
109
     *
110
     * @param bool $activeOnly if and set to non-zero will cause listing of only those institutions which have some valid profiles defined.
111
     * @return array list of Profiles of this IdP
112
     */
113
    public function listProfiles(bool $activeOnly = FALSE) {
114
        $query = "SELECT profile_id FROM profile WHERE inst_id = $this->identifier" . ($activeOnly ? " AND showtime = 1" : "");
115
        $allProfiles = $this->databaseHandle->exec($query);
116
        $returnarray = [];
117
        // SELECT -> resource, not boolean
118
        while ($profileQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $allProfiles)) {
119
            $oneProfile = ProfileFactory::instantiate($profileQuery->profile_id, $this);
120
            $oneProfile->institution = $this->identifier;
121
            $returnarray[] = $oneProfile;
122
        }
123
124
        $this->loggerInstance->debug(4, "listProfiles: " . print_r($returnarray, true));
125
        return $returnarray;
126
    }
127
    
128
    /**
129
     * This function retrieves all SP deployments for this organisation from the database
130
     *
131
     * @param bool $activeOnly if and set to non-zero will cause listing of only those institutions which have some valid profiles defined.
132
     * @return array list of Profiles of this IdP
133
     */
134
    public function listDeployments(bool $activeOnly = FALSE) {
135
        $query = "SELECT deployment_id FROM deployment WHERE inst_id = $this->identifier" . ($activeOnly ? " AND status = " . AbstractDeployment::ACTIVE : "");
136
        $allDeployments = $this->databaseHandle->exec($query);
137
        $returnarray = [];
138
        // SELECT -> resource, not boolean
139
        while ($deploymentQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $allDeployments)) {
140
            $returnarray[] = new DeploymentManaged($this,$deploymentQuery->deployment_id);
141
        }
142
143
        $this->loggerInstance->debug(4, "listDeployments: " . print_r($returnarray, true));
144
        return $returnarray;
145
    }
146
147
    const PROFILES_INCOMPLETE = 0;
148
    const PROFILES_CONFIGURED = 1;
149
    const PROFILES_SHOWTIME = 2;
150
151
    /**
152
     * looks through all the profiles of the inst and determines the highest prod-ready level among the profiles
153
     * @return int highest level of completeness of all the profiles of the inst
154
     */
155
    public function maxProfileStatus() {
156
        $allProfiles = $this->databaseHandle->exec("SELECT sufficient_config + showtime AS maxlevel FROM profile WHERE inst_id = $this->identifier ORDER BY maxlevel DESC LIMIT 1");
157
        // SELECT yields a resource, not a boolean
158
        while ($res = mysqli_fetch_object(/** @scrutinizer ignore-type */ $allProfiles)) {
159
            return $res->maxlevel;
160
        }
161
        return self::PROFILES_INCOMPLETE;
162
    }
163
164
    /** This function retrieves an array of authorised users which can
165
     * manipulate this institution.
166
     * 
167
     * @return array owners of the institution; numbered array with members ID, MAIL and LEVEL
168
     */
169
    public function listOwners() {
170
        $returnarray = [];
171
        $admins = $this->databaseHandle->exec("SELECT user_id, orig_mail, blesslevel FROM ownership WHERE institution_id = $this->identifier ORDER BY user_id");
172
        // SELECT -> resource, not boolean
173
        while ($ownerQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $admins)) {
174
            $returnarray[] = ['ID' => $ownerQuery->user_id, 'MAIL' => $ownerQuery->orig_mail, 'LEVEL' => $ownerQuery->blesslevel];
175
        }
176
        return $returnarray;
177
    }
178
179
    /**
180
     * Primary owners are allowed to invite other (secondary) admins to the institution
181
     * 
182
     * @param string $user ID of a logged-in user
183
     * @return boolean TRUE if this user is an admin with FED-level blessing
184
     */
185
    public function isPrimaryOwner($user) {
186
        foreach ($this->listOwners() as $oneOwner) {
187
            if ($oneOwner['ID'] == $user && $oneOwner['LEVEL'] == "FED") {
188
                return TRUE;
189
            }
190
        }
191
        return FALSE;
192
    }
193
194
    /**
195
     * This function gets the profile count for a given IdP.
196
     * 
197
     * The count could be retreived from the listProfiles method
198
     * but this is less expensive.
199
     *
200
     * @return int profile count
201
     */
202
    public function profileCount() {
203
        $result = $this->databaseHandle->exec("SELECT profile_id FROM profile 
204
             WHERE inst_id = $this->identifier");
205
        // SELECT -> resource, not boolean
206
        return(mysqli_num_rows(/** @scrutinizer ignore-type */ $result));
207
    }
208
209
    /**
210
     * This function gets the deployment count for a given IdP.
211
     *
212
     * @return int deployment count
213
     */
214
    public function deploymentCount() {
215
        $result = $this->databaseHandle->exec("SELECT deployment_id FROM deployment
216
             WHERE inst_id = $this->identifier");
217
        // SELECT -> resource, not boolean
218
        return(mysqli_num_rows(/** @scrutinizer ignore-type */ $result));
219
    }
220
221
    const ELIGIBILITY_IDP = "IdP";
222
    const ELIGIBILITY_SP = "SP";
223
    /**
224
     * checks whether the participant is an IdP, an SP, or both.
225
     * 
226
     * @return array list of eligibilities
227
     */
228
    public function eligility() {
229
        $eligibilites = $this->databaseHandle->exec("SELECT type FROM institution WHERE institution_id = $this->identifier");
230
        while ($iterator = mysqli_fetch_object($eligibilites)) {
0 ignored issues
show
Bug introduced by
It seems like $eligibilites can also be of type true; however, parameter $result of mysqli_fetch_object() does only seem to accept mysqli_result, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

230
        while ($iterator = mysqli_fetch_object(/** @scrutinizer ignore-type */ $eligibilites)) {
Loading history...
231
            switch ($iterator->type) {
232
                case "IdP":
233
                    return [ IdP::ELIGIBILITY_IDP ];
234
                case "SP":
235
                    return [ IdP::ELIGIBILITY_SP ];
236
                default:
237
                    return [ IdP::ELIGIBILITY_IDP, IdP::ELIGIBILITY_SP ];
238
            }
239
        }
240
    }
241
    
242
    /**
243
     * This function sets the timestamp of last modification of the child profiles to the current timestamp.
244
     * 
245
     * This is needed for installer caching: all installers which are on disk 
246
     * must be re-created if an attribute changes. This timestamp here
247
     * is used to determine if the installer on disk is still new enough.
248
     * 
249
     * @return void
250
     */
251
    public function updateFreshness() {
252
        // freshness is always defined for *Profiles*
253
        // IdP needs to update timestamp of all its profiles if an IdP-wide attribute changed
254
        $this->databaseHandle->exec("UPDATE profile SET last_change = CURRENT_TIMESTAMP WHERE inst_id = '$this->identifier'");
255
    }
256
257
    /**
258
     * Adds a new profile to this IdP.
259
     * 
260
     * Only creates the DB entry for the Profile. If you want to add attributes later, see Profile::addAttribute().
261
     *
262
     * @param string $type exactly "RADIUS" or "SILVERBULLET", all other values throw an Exception
263
     * @return AbstractProfile|NULL new Profile object if successful, or NULL if an error occured
264
     */
265
    public function newProfile(string $type) {
266
        $this->databaseHandle->exec("INSERT INTO profile (inst_id) VALUES($this->identifier)");
267
        $identifier = $this->databaseHandle->lastID();
268
        if ($identifier > 0) {
269
            switch ($type) {
270
                case AbstractProfile::PROFILETYPE_RADIUS:
271
                    return new ProfileRADIUS($identifier, $this);
272
                case AbstractProfile::PROFILETYPE_SILVERBULLET:
273
                    $theProfile = new ProfileSilverbullet($identifier, $this);
274
                    $theProfile->addSupportedEapMethod(new \core\common\EAP(\core\common\EAP::EAPTYPE_SILVERBULLET), 1);
275
                    $theProfile->setRealm($this->identifier . "-" . $theProfile->identifier . "." . strtolower($this->federation) . strtolower(CONFIG_CONFASSISTANT['SILVERBULLET']['realm_suffix']));
276
                    return $theProfile;
277
                default:
278
                    throw new Exception("This type of profile is unknown and can not be added.");
279
            }
280
        }
281
        return NULL;
282
    }
283
284
    /**
285
     * Adds a new hotspot deployment to this IdP.
286
     * 
287
     * Only creates the DB entry for the deployment. If you want to add attributes later, see Profile::addAttribute().
288
     *
289
     * @param string $type exactly "RADIUS-SP" or "MANAGED-SP", all other values throw an Exception
290
     * @return DeploymentManaged the newly created deployment
291
     */
292
    public function newDeployment(string $type) {
293
            switch ($type) {
294
                case AbstractDeployment::DEPLOYMENTTYPE_CLASSIC:
295
                    // classic deployment exist in the eduroam DB. We don't do anything here.
296
                    throw new Exception("This type of deployment is handled externally and requesting it here makes no sense.");
297
                case AbstractDeployment::DEPLOYMENTTYPE_MANAGED:
298
                    $this->databaseHandle->exec("INSERT INTO deployment (inst_id) VALUES($this->identifier)");
299
                    $identifier = $this->databaseHandle->lastID();
300
                    return new DeploymentManaged($this, $identifier);
301
                default:
302
                    throw new Exception("This type of deployment is unknown and can not be added.");
303
            }
304
    }
305
306
    /**
307
     * deletes the IdP and all its profiles
308
     * 
309
     * @return void
310
     */
311
    public function destroy() {
312
        common\Entity::intoThePotatoes();
313
        /* delete all profiles */
314
        foreach ($this->listProfiles() as $profile) {
315
            $profile->destroy();
316
        }
317
        /* double-check that all profiles are gone */
318
        $profiles = $this->listProfiles();
319
320
        if (count($profiles) > 0) {
321
            throw new Exception("This IdP shouldn't have any profiles any more!");
322
        }
323
324
        $this->databaseHandle->exec("DELETE FROM ownership WHERE institution_id = $this->identifier");
325
        $this->databaseHandle->exec("DELETE FROM institution_option WHERE institution_id = $this->identifier");
326
        $this->databaseHandle->exec("DELETE FROM institution WHERE inst_id = $this->identifier");
327
328
        // notify federation admins
329
330
        $fed = new Federation($this->federation);
331
        foreach ($fed->listFederationAdmins() as $id) {
332
            $user = new User($id);
333
            $message = sprintf(_("Hi,
334
335
the %s %s in your %s federation %s has been deleted from %s.
336
337
We thought you might want to know.
338
339
Best regards,
340
341
%s"), common\Entity::$nomenclature_inst, $this->name, CONFIG_CONFASSISTANT['CONSORTIUM']['display_name'], strtoupper($fed->name), CONFIG['APPEARANCE']['productname'], CONFIG['APPEARANCE']['productname_long']);
342
            $user->sendMailToUser(sprintf(_("%s in your federation was deleted"), common\Entity::$nomenclature_inst), $message);
343
        }
344
        common\Entity::outOfThePotatoes();
345
    }
346
347
    /**
348
     * Performs a lookup in an external database to determine matching entities to this IdP. 
349
     * 
350
     * The business logic of this function is roaming consortium specific; if no match algorithm is known for the consortium, FALSE is returned.
351
     * 
352
     * @return mixed list of entities in external database that correspond to this IdP or FALSE if no consortium-specific matching function is defined
353
     */
354
    public function getExternalDBSyncCandidates() {
355
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
356
            $list = [];
357
            $usedarray = [];
358
            // extract all institutions from the country
359
            $externalHandle = DBConnection::handle("EXTERNAL");
360
            $lowerFed = strtolower($this->federation);
361
            $candidateList = $externalHandle->exec("SELECT id_institution AS id, name AS collapsed_name FROM view_active_idp_institution WHERE country = ?", "s", $lowerFed);
362
            $syncstate = self::EXTERNAL_DB_SYNCSTATE_SYNCED;
363
            $alreadyUsed = $this->databaseHandle->exec("SELECT DISTINCT external_db_id FROM institution WHERE external_db_id IS NOT NULL AND external_db_syncstate = ?", "i", $syncstate);
364
            // SELECT -> resource, not boolean
365
            while ($alreadyUsedQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $alreadyUsed)) {
366
                $usedarray[] = $alreadyUsedQuery->external_db_id;
367
            }
368
369
            // and split them into ID, LANG, NAME pairs (operating on a resource, not boolean)
370
            while ($candidateListQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $candidateList)) {
371
                if (in_array($candidateListQuery->id, $usedarray)) {
372
                    continue;
373
                }
374
                $names = explode('#', $candidateListQuery->collapsed_name);
375
                foreach ($names as $name) {
376
                    $perlang = explode(': ', $name, 2);
377
                    $list[] = ["ID" => $candidateListQuery->id, "lang" => $perlang[0], "name" => $perlang[1]];
378
                }
379
            }
380
            // now see if any of the languages in CAT match any of those in the external DB
381
            $mynames = $this->getAttributes("general:instname");
382
            $matchingCandidates = [];
383
            foreach ($mynames as $onename) {
384
                foreach ($list as $listentry) {
385
                    if (($onename['lang'] == $listentry['lang'] || $onename['lang'] == "C") && $onename['value'] == $listentry['name'] && array_search($listentry['ID'], $matchingCandidates) === FALSE) {
386
                        $matchingCandidates[] = $listentry['ID'];
387
                    }
388
                }
389
            }
390
            return $matchingCandidates;
391
        }
392
        return FALSE;
393
    }
394
395
    /**
396
     * returns the state of sync with the external DB.
397
     * 
398
     * @return int
399
     */
400
    public function getExternalDBSyncState() {
401
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
402
            return $this->externalDbSyncstate;
403
        }
404
        return self::EXTERNAL_DB_SYNCSTATE_NOTSUBJECTTOSYNCING;
405
    }
406
407
    /**
408
     * Retrieves the external DB identifier of this institution. Returns FALSE if no ID is known.
409
     * 
410
     * @return string|boolean the external identifier; or FALSE if no external ID is known
411
     */
412
    public function getExternalDBId() {
413
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
414
            $idQuery = $this->databaseHandle->exec("SELECT external_db_id FROM institution WHERE inst_id = $this->identifier AND external_db_syncstate = " . self::EXTERNAL_DB_SYNCSTATE_SYNCED);
415
            // SELECT -> it's a resource, not a boolean
416
            if (mysqli_num_rows(/** @scrutinizer ignore-type */ $idQuery) == 0) {
417
                return FALSE;
418
            }
419
            $externalIdQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $idQuery);
420
            return $externalIdQuery->external_db_id;
421
        }
422
        return FALSE;
423
    }
424
425
    /**
426
     * Associates the external DB id with a CAT id
427
     * 
428
     * @param string $identifier the external DB id, which can be alpha-numeric
429
     * @return void
430
     */
431
    public function setExternalDBId(string $identifier) {
432
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
433
            $syncState = self::EXTERNAL_DB_SYNCSTATE_SYNCED;
434
            $alreadyUsed = $this->databaseHandle->exec("SELECT DISTINCT external_db_id FROM institution WHERE external_db_id = ? AND external_db_syncstate = ?", "si", $identifier, $syncState);
435
            // SELECT -> resource, not boolean
436
            if (mysqli_num_rows(/** @scrutinizer ignore-type */ $alreadyUsed) == 0) {
437
                $this->databaseHandle->exec("UPDATE institution SET external_db_id = ?, external_db_syncstate = ? WHERE inst_id = ?", "sii", $identifier, $syncState, $this->identifier);
438
            }
439
        }
440
    }
441
442
    /**
443
     * removes the link between a CAT institution and the external DB
444
     * 
445
     * @return void
446
     */
447
    public function removeExternalDBId() {
448
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
449
            if ($this->getExternalDBId() !== FALSE) {
450
                $syncState = self::EXTERNAL_DB_SYNCSTATE_NOT_SYNCED;
451
                $this->databaseHandle->exec("UPDATE institution SET external_db_id = NULL, external_db_syncstate = ? WHERE inst_id = ?", "ii", $syncState, $this->identifier);
452
            }
453
        }
454
    }
455
456
}
457