Test Failed
Push — master ( 5d347f...d6f93a )
by Stefan
07:03
created

IdP::maxProfileStatus()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 7
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
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
    /**
222
     * This function sets the timestamp of last modification of the child profiles to the current timestamp.
223
     * 
224
     * This is needed for installer caching: all installers which are on disk 
225
     * must be re-created if an attribute changes. This timestamp here
226
     * is used to determine if the installer on disk is still new enough.
227
     * 
228
     * @return void
229
     */
230
    public function updateFreshness() {
231
        // freshness is always defined for *Profiles*
232
        // IdP needs to update timestamp of all its profiles if an IdP-wide attribute changed
233
        $this->databaseHandle->exec("UPDATE profile SET last_change = CURRENT_TIMESTAMP WHERE inst_id = '$this->identifier'");
234
    }
235
236
    /**
237
     * Adds a new profile to this IdP.
238
     * 
239
     * Only creates the DB entry for the Profile. If you want to add attributes later, see Profile::addAttribute().
240
     *
241
     * @param string $type exactly "RADIUS" or "SILVERBULLET", all other values throw an Exception
242
     * @return AbstractProfile|NULL new Profile object if successful, or NULL if an error occured
243
     */
244
    public function newProfile(string $type) {
245
        $this->databaseHandle->exec("INSERT INTO profile (inst_id) VALUES($this->identifier)");
246
        $identifier = $this->databaseHandle->lastID();
247
        if ($identifier > 0) {
248
            switch ($type) {
249
                case AbstractProfile::PROFILETYPE_RADIUS:
250
                    return new ProfileRADIUS($identifier, $this);
251
                case AbstractProfile::PROFILETYPE_SILVERBULLET:
252
                    $theProfile = new ProfileSilverbullet($identifier, $this);
253
                    $theProfile->addSupportedEapMethod(new \core\common\EAP(\core\common\EAP::EAPTYPE_SILVERBULLET), 1);
254
                    $theProfile->setRealm($this->identifier . "-" . $theProfile->identifier . "." . strtolower($this->federation) . strtolower(CONFIG_CONFASSISTANT['SILVERBULLET']['realm_suffix']));
255
                    return $theProfile;
256
                default:
257
                    throw new Exception("This type of profile is unknown and can not be added.");
258
            }
259
        }
260
        return NULL;
261
    }
262
263
    /**
264
     * Adds a new hotspot deployment to this IdP.
265
     * 
266
     * Only creates the DB entry for the deployment. If you want to add attributes later, see Profile::addAttribute().
267
     *
268
     * @param string $type exactly "RADIUS-SP" or "MANAGED-SP", all other values throw an Exception
269
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
270
    public function newDeployment(string $type) {
271
            switch ($type) {
272
                case AbstractDeployment::DEPLOYMENTTYPE_CLASSIC:
273
                    // classic deployment exist in the eduroam DB. We don't do anything here.
274
                    throw new Exception("This type of deployment is handled externally and requesting it here makes no sense.");
275
                case AbstractDeployment::DEPLOYMENTTYPE_MANAGED:
276
                    $this->databaseHandle->exec("INSERT INTO deployment (inst_id) VALUES($this->identifier)");
277
                    $identifier = $this->databaseHandle->lastID();
278
                    return new DeploymentManaged($this, $identifier);
279
                default:
280
                    throw new Exception("This type of deployment is unknown and can not be added.");
281
            }
282
        return NULL;
0 ignored issues
show
Unused Code introduced by
return NULL is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
283
    }
284
285
    /**
286
     * deletes the IdP and all its profiles
287
     * 
288
     * @return void
289
     */
290
    public function destroy() {
291
        common\Entity::intoThePotatoes();
292
        /* delete all profiles */
293
        foreach ($this->listProfiles() as $profile) {
294
            $profile->destroy();
295
        }
296
        /* double-check that all profiles are gone */
297
        $profiles = $this->listProfiles();
298
299
        if (count($profiles) > 0) {
300
            throw new Exception("This IdP shouldn't have any profiles any more!");
301
        }
302
303
        $this->databaseHandle->exec("DELETE FROM ownership WHERE institution_id = $this->identifier");
304
        $this->databaseHandle->exec("DELETE FROM institution_option WHERE institution_id = $this->identifier");
305
        $this->databaseHandle->exec("DELETE FROM institution WHERE inst_id = $this->identifier");
306
307
        // notify federation admins
308
309
        $fed = new Federation($this->federation);
310
        foreach ($fed->listFederationAdmins() as $id) {
311
            $user = new User($id);
312
            $message = sprintf(_("Hi,
313
314
the %s %s in your %s federation %s has been deleted from %s.
315
316
We thought you might want to know.
317
318
Best regards,
319
320
%s"), common\Entity::$nomenclature_inst, $this->name, CONFIG_CONFASSISTANT['CONSORTIUM']['display_name'], strtoupper($fed->name), CONFIG['APPEARANCE']['productname'], CONFIG['APPEARANCE']['productname_long']);
321
            $user->sendMailToUser(sprintf(_("%s in your federation was deleted"), common\Entity::$nomenclature_inst), $message);
322
        }
323
        common\Entity::outOfThePotatoes();
324
    }
325
326
    /**
327
     * Performs a lookup in an external database to determine matching entities to this IdP. 
328
     * 
329
     * The business logic of this function is roaming consortium specific; if no match algorithm is known for the consortium, FALSE is returned.
330
     * 
331
     * @return mixed list of entities in external database that correspond to this IdP or FALSE if no consortium-specific matching function is defined
332
     */
333
    public function getExternalDBSyncCandidates() {
334
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
335
            $list = [];
336
            $usedarray = [];
337
            // extract all institutions from the country
338
            $externalHandle = DBConnection::handle("EXTERNAL");
339
            $lowerFed = strtolower($this->federation);
340
            $candidateList = $externalHandle->exec("SELECT id_institution AS id, name AS collapsed_name FROM view_active_idp_institution WHERE country = ?", "s", $lowerFed);
341
            $syncstate = self::EXTERNAL_DB_SYNCSTATE_SYNCED;
342
            $alreadyUsed = $this->databaseHandle->exec("SELECT DISTINCT external_db_id FROM institution WHERE external_db_id IS NOT NULL AND external_db_syncstate = ?", "i", $syncstate);
343
            // SELECT -> resource, not boolean
344
            while ($alreadyUsedQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $alreadyUsed)) {
345
                $usedarray[] = $alreadyUsedQuery->external_db_id;
346
            }
347
348
            // and split them into ID, LANG, NAME pairs (operating on a resource, not boolean)
349
            while ($candidateListQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $candidateList)) {
350
                if (in_array($candidateListQuery->id, $usedarray)) {
351
                    continue;
352
                }
353
                $names = explode('#', $candidateListQuery->collapsed_name);
354
                foreach ($names as $name) {
355
                    $perlang = explode(': ', $name, 2);
356
                    $list[] = ["ID" => $candidateListQuery->id, "lang" => $perlang[0], "name" => $perlang[1]];
357
                }
358
            }
359
            // now see if any of the languages in CAT match any of those in the external DB
360
            $mynames = $this->getAttributes("general:instname");
361
            $matchingCandidates = [];
362
            foreach ($mynames as $onename) {
363
                foreach ($list as $listentry) {
364
                    if (($onename['lang'] == $listentry['lang'] || $onename['lang'] == "C") && $onename['value'] == $listentry['name'] && array_search($listentry['ID'], $matchingCandidates) === FALSE) {
365
                        $matchingCandidates[] = $listentry['ID'];
366
                    }
367
                }
368
            }
369
            return $matchingCandidates;
370
        }
371
        return FALSE;
372
    }
373
374
    /**
375
     * returns the state of sync with the external DB.
376
     * 
377
     * @return int
378
     */
379
    public function getExternalDBSyncState() {
380
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
381
            return $this->externalDbSyncstate;
382
        }
383
        return self::EXTERNAL_DB_SYNCSTATE_NOTSUBJECTTOSYNCING;
384
    }
385
386
    /**
387
     * Retrieves the external DB identifier of this institution. Returns FALSE if no ID is known.
388
     * 
389
     * @return string|boolean the external identifier; or FALSE if no external ID is known
390
     */
391
    public function getExternalDBId() {
392
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
393
            $idQuery = $this->databaseHandle->exec("SELECT external_db_id FROM institution WHERE inst_id = $this->identifier AND external_db_syncstate = " . self::EXTERNAL_DB_SYNCSTATE_SYNCED);
394
            // SELECT -> it's a resource, not a boolean
395
            if (mysqli_num_rows(/** @scrutinizer ignore-type */ $idQuery) == 0) {
396
                return FALSE;
397
            }
398
            $externalIdQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $idQuery);
399
            return $externalIdQuery->external_db_id;
400
        }
401
        return FALSE;
402
    }
403
404
    /**
405
     * Associates the external DB id with a CAT id
406
     * 
407
     * @param string $identifier the external DB id, which can be alpha-numeric
408
     * @return void
409
     */
410
    public function setExternalDBId(string $identifier) {
411
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
412
            $syncState = self::EXTERNAL_DB_SYNCSTATE_SYNCED;
413
            $alreadyUsed = $this->databaseHandle->exec("SELECT DISTINCT external_db_id FROM institution WHERE external_db_id = ? AND external_db_syncstate = ?", "si", $identifier, $syncState);
414
            // SELECT -> resource, not boolean
415
            if (mysqli_num_rows(/** @scrutinizer ignore-type */ $alreadyUsed) == 0) {
416
                $this->databaseHandle->exec("UPDATE institution SET external_db_id = ?, external_db_syncstate = ? WHERE inst_id = ?", "sii", $identifier, $syncState, $this->identifier);
417
            }
418
        }
419
    }
420
421
    /**
422
     * removes the link between a CAT institution and the external DB
423
     * 
424
     * @return void
425
     */
426
    public function removeExternalDBId() {
427
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
428
            if ($this->getExternalDBId() !== FALSE) {
429
                $syncState = self::EXTERNAL_DB_SYNCSTATE_NOT_SYNCED;
430
                $this->databaseHandle->exec("UPDATE institution SET external_db_id = NULL, external_db_syncstate = ? WHERE inst_id = ?", "ii", $syncState, $this->identifier);
431
            }
432
        }
433
    }
434
435
}
436