Passed
Push — master ( f19d09...17bffe )
by Stefan
04:30
created

IdP::isPrimaryOwner()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 5
nc 3
nop 1
dl 0
loc 8
rs 9.2
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * ******************************************************************************
5
 * Copyright 2011-2017 DANTE Ltd. and GÉANT on behalf of the GN3, GN3+, GN4-1 
6
 * and GN4-2 consortia
7
 *
8
 * License: see the web/copyright.php file in the file structure
9
 * ******************************************************************************
10
 */
11
12
/**
13
 * This file contains Federation, IdP and Profile classes.
14
 * These should be split into separate files later.
15
 *
16
 * @package Developer
17
 */
18
/**
19
 * 
20
 */
21
22
namespace core;
23
24
use \Exception;
25
26
/**
27
 * This class represents an Identity Provider (IdP).
28
 * IdPs have properties of their own, and may have one or more Profiles. The
29
 * profiles can override the institution-wide properties.
30
 *
31
 * @author Stefan Winter <[email protected]>
32
 * @author Tomasz Wolniewicz <[email protected]>
33
 *
34
 * @license see LICENSE file in root directory
35
 *
36
 * @package Developer
37
 */
38
class IdP extends EntityWithDBProperties {
39
40
    const EXTERNAL_DB_SYNCSTATE_NOT_SYNCED = 0;
41
    const EXTERNAL_DB_SYNCSTATE_SYNCED = 1;
42
    const EXTERNAL_DB_SYNCSTATE_NOTSUBJECTTOSYNCING = 2;
43
44
    /**
45
     *
46
     * @var int synchronisation state with external database, if any
47
     */
48
    private $externalDbSyncstate;
49
50
    /**
51
     * The shortname of this IdP's federation
52
     * @var string 
53
     */
54
    public $federation;
55
56
    /**
57
     * Constructs an IdP object based on its details in the database.
58
     * Cannot be used to define a new IdP in the database! This happens via Federation::newIdP()
59
     *
60
     * @param int $instId the database row identifier
61
     */
62
    public function __construct(int $instId) {
63
        $this->databaseType = "INST";
64
        parent::__construct(); // now databaseHandle and logging is available
65
        $this->entityOptionTable = "institution_option";
66
        $this->entityIdColumn = "institution_id";
67
        if (!is_numeric($instId)) {
68
            throw new Exception("An " . CONFIG_CONFASSISTANT['CONSORTIUM']['nomenclature_inst'] . " is identified by an integer index!");
69
        }
70
        $this->identifier = (int) $instId;
71
72
        $idp = $this->databaseHandle->exec("SELECT inst_id, country,external_db_syncstate FROM institution WHERE inst_id = $this->identifier");
73
        // SELECT -> returns resource, not boolean
74
        if (!$instQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $idp)) {
75
            throw new Exception("IdP $this->identifier not found in database!");
76
        }
77
78
        $this->federation = $instQuery->country;
79
        $this->externalDbSyncstate = $instQuery->external_db_syncstate;
80
81
        // fetch attributes from DB; populates $this->attributes array
82
        $this->attributes = $this->retrieveOptionsFromDatabase("SELECT DISTINCT option_name, option_lang, option_value, row 
83
                                            FROM $this->entityOptionTable
84
                                            WHERE $this->entityIdColumn = ?  
85
                                            ORDER BY option_name", "IdP", "i", $this->identifier);
86
87
        $this->attributes[] = ["name" => "internal:country",
88
            "lang" => NULL,
89
            "value" => $this->federation,
90
            "level" => "IdP",
91
            "row" => 0,
92
            "flag" => NULL];
93
94
        $this->name = $this->languageInstance->getLocalisedValue($this->getAttributes('general:instname'));
95
        $this->loggerInstance->debug(3, "--- END Constructing new IdP object ... ---\n");
96
    }
97
98
    /**
99
     * This function retrieves all registered profiles for this IdP from the database
100
     *
101
     * @param bool $activeOnly if and set to non-zero will cause listing of only those institutions which have some valid profiles defined.
102
     * @return array list of Profiles of this IdP
103
     */
104
    public function listProfiles(bool $activeOnly = FALSE) {
105
        $query = "SELECT profile_id FROM profile WHERE inst_id = $this->identifier" . ($activeOnly ? " AND showtime = 1" : "");
106
        $allProfiles = $this->databaseHandle->exec($query);
107
        $returnarray = [];
108
        // SELECT -> resource, not boolean
109
        while ($profileQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $allProfiles)) {
110
            $oneProfile = ProfileFactory::instantiate($profileQuery->profile_id, $this);
111
            $oneProfile->institution = $this->identifier;
112
            $returnarray[] = $oneProfile;
113
        }
114
115
        $this->loggerInstance->debug(2, "listProfiles: " . print_r($returnarray, true));
116
        return $returnarray;
117
    }
118
119
    const PROFILES_INCOMPLETE = 0;
120
    const PROFILES_CONFIGURED = 1;
121
    const PROFILES_SHOWTIME = 2;
122
123
    /**
124
     * looks through all the profiles of the inst and determines the highest prod-ready level among the profiles
125
     * @return int highest level of completeness of all the profiles of the inst
126
     */
127
    public function maxProfileStatus() {
128
        $allProfiles = $this->databaseHandle->exec("SELECT sufficient_config + showtime AS maxlevel FROM profile WHERE inst_id = $this->identifier ORDER BY maxlevel DESC LIMIT 1");
129
        // SELECT yields a resource, not a boolean
130
        while ($res = mysqli_fetch_object(/** @scrutinizer ignore-type */ $allProfiles)) {
131
            return $res->maxlevel;
132
        }
133
        return self::PROFILES_INCOMPLETE;
134
    }
135
136
    /** This function retrieves an array of authorised users which can
137
     * manipulate this institution.
138
     * 
139
     * @return array owners of the institution; numbered array with members ID, MAIL and LEVEL
140
     */
141
    public function listOwners() {
142
        $returnarray = [];
143
        $admins = $this->databaseHandle->exec("SELECT user_id, orig_mail, blesslevel FROM ownership WHERE institution_id = $this->identifier ORDER BY user_id");
144
        // SELECT -> resource, not boolean
145
        while ($ownerQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $admins)) {
146
            $returnarray[] = ['ID' => $ownerQuery->user_id, 'MAIL' => $ownerQuery->orig_mail, 'LEVEL' => $ownerQuery->blesslevel];
147
        }
148
        return $returnarray;
149
    }
150
151
    /**
152
     * Primary owners are allowed to invite other (secondary) admins to the institution
153
     * 
154
     * @param string $user ID of a logged-in user
155
     * @return bool TRUE if this user is an admin with FED-level blessing
156
     */
157
    public function isPrimaryOwner($user) {
158
       foreach ($this->listOwners() as $oneOwner) {
159
           if ($oneOwner['ID'] == $user && $oneOwner['LEVEL'] == "FED") {
160
               return TRUE;
161
           }
162
       }
163
       return FALSE;
164
    }
165
    
166
    /**
167
     * This function gets the profile count for a given IdP.
168
     * 
169
     * The count could be retreived from the listProfiles method
170
     * but this is less expensive.
171
     *
172
     * @return int profile count
173
     */
174
    public function profileCount() {
175
        $result = $this->databaseHandle->exec("SELECT profile_id FROM profile 
176
             WHERE inst_id = $this->identifier");
177
        // SELECT -> resource, not boolean
178
        return(mysqli_num_rows(/** @scrutinizer ignore-type */ $result));
179
    }
180
181
    /**
182
     * This function sets the timestamp of last modification of the child profiles to the current timestamp.
183
     * 
184
     * This is needed for installer caching: all installers which are on disk 
185
     * must be re-created if an attribute changes. This timestamp here
186
     * is used to determine if the installer on disk is still new enough.
187
     */
188
    public function updateFreshness() {
189
        // freshness is always defined for *Profiles*
190
        // IdP needs to update timestamp of all its profiles if an IdP-wide attribute changed
191
        $this->databaseHandle->exec("UPDATE profile SET last_change = CURRENT_TIMESTAMP WHERE inst_id = '$this->identifier'");
192
    }
193
194
    /**
195
     * Adds a new profile to this IdP.
196
     * 
197
     * Only creates the DB entry for the Profile. If you want to add attributes later, see Profile::addAttribute().
198
     *
199
     * @param string $type exactly "RADIUS" or "SILVERBULLET", all other values throw an Exception
200
     * @return object new Profile object if successful, or FALSE if an error occured
201
     */
202
    public function newProfile(string $type) {
203
        $this->databaseHandle->exec("INSERT INTO profile (inst_id) VALUES($this->identifier)");
204
        $identifier = $this->databaseHandle->lastID();
205
206
        if ($identifier > 0) {
207
            switch ($type) {
208
                case "RADIUS":
209
                    return new ProfileRADIUS($identifier, $this);
210
                case "SILVERBULLET":
211
                    $theProfile = new ProfileSilverbullet($identifier, $this);
212
                    $theProfile->addSupportedEapMethod(new \core\common\EAP(\core\common\EAP::EAPTYPE_SILVERBULLET), 1);
213
                    $theProfile->setRealm($this->identifier."-".$theProfile->identifier."." . strtolower($this->federation) . strtolower(CONFIG_CONFASSISTANT['SILVERBULLET']['realm_suffix']));
214
                    return $theProfile;
215
                default:
216
                    throw new Exception("This type of profile is unknown and can not be added.");
217
            }
218
        }
219
        return NULL;
220
    }
221
222
    /**
223
     * deletes the IdP and all its profiles
224
     */
225
    public function destroy() {
226
        /* delete all profiles */
227
        foreach ($this->listProfiles() as $profile) {
228
            $profile->destroy();
229
        }
230
        /* double-check that all profiles are gone */
231
        $profiles = $this->listProfiles();
232
233
        if (count($profiles) > 0) {
234
            throw new Exception("This IdP shouldn't have any profiles any more!");
235
        }
236
237
        $this->databaseHandle->exec("DELETE FROM ownership WHERE institution_id = $this->identifier");
238
        $this->databaseHandle->exec("DELETE FROM institution_option WHERE institution_id = $this->identifier");
239
        $this->databaseHandle->exec("DELETE FROM institution WHERE inst_id = $this->identifier");
240
241
        // notify federation admins
242
243
        $fed = new Federation($this->federation);
244
        foreach ($fed->listFederationAdmins() as $id) {
245
            $user = new User($id);
246
            $message = sprintf(_("Hi,
247
248
the Identity Provider %s in your %s federation %s has been deleted from %s.
249
250
We thought you might want to know.
251
252
Best regards,
253
254
%s"), $this->name, CONFIG_CONFASSISTANT['CONSORTIUM']['display_name'], strtoupper($fed->name), CONFIG['APPEARANCE']['productname'], CONFIG['APPEARANCE']['productname_long']);
255
            $user->sendMailToUser(_("IdP in your federation was deleted"), $message);
256
        }
257
    }
258
259
    /**
260
     * Performs a lookup in an external database to determine matching entities to this IdP. 
261
     * 
262
     * The business logic of this function is roaming consortium specific; if no match algorithm is known for the consortium, FALSE is returned.
263
     * 
264
     * @return mixed list of entities in external database that correspond to this IdP or FALSE if no consortium-specific matching function is defined
265
     */
266
    public function getExternalDBSyncCandidates() {
267
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
268
            $list = [];
269
            $usedarray = [];
270
            // extract all institutions from the country
271
            $externalHandle = DBConnection::handle("EXTERNAL");
272
            $lowerFed = strtolower($this->federation);
273
            $candidateList = $externalHandle->exec("SELECT id_institution AS id, name AS collapsed_name FROM view_active_idp_institution WHERE country = ?", "s", $lowerFed);
274
            $syncstate = self::EXTERNAL_DB_SYNCSTATE_SYNCED;
275
            $alreadyUsed = $this->databaseHandle->exec("SELECT DISTINCT external_db_id FROM institution WHERE external_db_id IS NOT NULL AND external_db_syncstate = ?", "i", $syncstate);
276
            // SELECT -> resource, not boolean
277
            while ($alreadyUsedQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $alreadyUsed)) {
278
                $usedarray[] = $alreadyUsedQuery->external_db_id;
279
            }
280
281
            // and split them into ID, LANG, NAME pairs (operating on a resource, not boolean)
282
            while ($candidateListQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $candidateList)) {
283
                if (in_array($candidateListQuery->id, $usedarray)) {
284
                    continue;
285
                }
286
                $names = explode('#', $candidateListQuery->collapsed_name);
287
                foreach ($names as $name) {
288
                    $perlang = explode(': ', $name, 2);
289
                    $list[] = ["ID" => $candidateListQuery->id, "lang" => $perlang[0], "name" => $perlang[1]];
290
                }
291
            }
292
            // now see if any of the languages in CAT match any of those in the external DB
293
            $mynames = $this->getAttributes("general:instname");
294
            $matchingCandidates = [];
295
            foreach ($mynames as $onename) {
296
                foreach ($list as $listentry) {
297
                    if (($onename['lang'] == $listentry['lang'] || $onename['lang'] == "C") && $onename['value'] == $listentry['name'] && array_search($listentry['ID'], $matchingCandidates) === FALSE) {
298
                        $matchingCandidates[] = $listentry['ID'];
299
                    }
300
                }
301
            }
302
            return $matchingCandidates;
303
        }
304
        return FALSE;
305
    }
306
307
    /**
308
     * returns the state of sync with the external DB.
309
     * 
310
     * @return int
311
     */
312
    public function getExternalDBSyncState() {
313
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
314
            return $this->externalDbSyncstate;
315
        }
316
        return self::EXTERNAL_DB_SYNCSTATE_NOTSUBJECTTOSYNCING;
317
    }
318
319
    /**
320
     * Retrieves the external DB identifier of this institution. Returns FALSE if no ID is known.
321
     * 
322
     * @return string|FALSE the external identifier; or FALSE if no external ID is known
323
     */
324
    public function getExternalDBId() {
325
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
326
            $idQuery = $this->databaseHandle->exec("SELECT external_db_id FROM institution WHERE inst_id = $this->identifier AND external_db_syncstate = " . self::EXTERNAL_DB_SYNCSTATE_SYNCED);
327
            // SELECT -> it's a resource, not a boolean
328
            if (mysqli_num_rows(/** @scrutinizer ignore-type */ $idQuery) == 0) {
329
                return FALSE;
330
            }
331
            $externalIdQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $idQuery);
332
            return $externalIdQuery->external_db_id;
333
        }
334
        return FALSE;
335
    }
336
337
    /**
338
     * Associates the external DB id with a CAT id
339
     * 
340
     * @param string $identifier the external DB id, which can be alpha-numeric
341
     */
342
    public function setExternalDBId(string $identifier) {
343
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
344
            $syncState = self::EXTERNAL_DB_SYNCSTATE_SYNCED;
345
            $alreadyUsed = $this->databaseHandle->exec("SELECT DISTINCT external_db_id FROM institution WHERE external_db_id = ? AND external_db_syncstate = ?", "si", $identifier, $syncState);
346
            // SELECT -> resource, not boolean
347
            if (mysqli_num_rows(/** @scrutinizer ignore-type */ $alreadyUsed) == 0) {
348
                $this->databaseHandle->exec("UPDATE institution SET external_db_id = ?, external_db_syncstate = ? WHERE inst_id = ?", "sii", $identifier, $syncState, $this->identifier);
349
            }
350
        }
351
    }
352
353
    /**
354
     * removes the link between a CAT institution and the external DB
355
     */
356
    public function removeExternalDBId() {
357
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
358
            if ($this->getExternalDBId() !== FALSE) {
359
                $syncState = self::EXTERNAL_DB_SYNCSTATE_NOT_SYNCED;
360
                $this->databaseHandle->exec("UPDATE institution SET external_db_id = NULL, external_db_syncstate = ? WHERE inst_id = ?", "ii", $syncState, $this->identifier);
361
            }
362
        }
363
    }
364
365
}
366