Passed
Push — release_2_0 ( c99898...ea195d )
by Stefan
08:44
created

IdP::newProfile()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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