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 the UserManagement class. |
25
|
|
|
* |
26
|
|
|
* @author Stefan Winter <[email protected]> |
27
|
|
|
* @author Tomasz Wolniewicz <[email protected]> |
28
|
|
|
* |
29
|
|
|
* @license see LICENSE file in root directory |
30
|
|
|
* |
31
|
|
|
* @package Developer |
32
|
|
|
*/ |
33
|
|
|
/** |
34
|
|
|
* necessary includes |
35
|
|
|
*/ |
36
|
|
|
|
37
|
|
|
namespace core; |
38
|
|
|
|
39
|
|
|
use \Exception; |
|
|
|
|
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* This class manages user privileges and bindings to institutions |
43
|
|
|
* |
44
|
|
|
* @author Stefan Winter <[email protected]> |
45
|
|
|
* @author Tomasz Wolniewicz <[email protected]> |
46
|
|
|
* |
47
|
|
|
* @package Developer |
48
|
|
|
*/ |
49
|
|
|
class UserManagement extends \core\common\Entity |
50
|
|
|
{ |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* our handle to the INST database |
54
|
|
|
* |
55
|
|
|
* @var DBConnection |
56
|
|
|
*/ |
57
|
|
|
private $databaseHandle; |
58
|
|
|
public $currentInstitutions; |
59
|
|
|
public $newUser = false; |
60
|
|
|
public $hasPotenialNewInst = false; |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* Class constructor. Nothing special to be done when constructing. |
64
|
|
|
* |
65
|
|
|
* @throws Exception |
66
|
|
|
*/ |
67
|
|
|
public function __construct() |
68
|
|
|
{ |
69
|
|
|
parent::__construct(); |
70
|
|
|
$handle = DBConnection::handle(self::$databaseType); |
71
|
|
|
if ($handle instanceof DBConnection) { |
72
|
|
|
$this->databaseHandle = $handle; |
73
|
|
|
} else { |
74
|
|
|
throw new Exception("This database type is never an array!"); |
75
|
|
|
} |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* database which this class queries by default |
80
|
|
|
* |
81
|
|
|
* @var string |
82
|
|
|
*/ |
83
|
|
|
private static $databaseType = "INST"; |
84
|
|
|
|
85
|
|
|
const TOKENSTATUS_OK_NEW = 1; |
86
|
|
|
const TOKENSTATUS_OK_EXISTING = 2; |
87
|
|
|
const TOKENSTATUS_FAIL_ALREADYCONSUMED = -1; |
88
|
|
|
const TOKENSTATUS_FAIL_EXPIRED = -2; |
89
|
|
|
const TOKENSTATUS_FAIL_NONEXISTING = -3; |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* Checks if a given invitation token exists and is valid in the invitations database |
93
|
|
|
* returns a string with the following values: |
94
|
|
|
* |
95
|
|
|
* OK-NEW valid token exists, and is not attached to an existing institution. When consuming the token, a new inst will be created |
96
|
|
|
* OK-EXISTING valid token exists, and is attached to an existing institution. When consuming the token, user will be added as an admin |
97
|
|
|
* FAIL-NONEXISTINGTOKEN this token does not exist at all in the database |
98
|
|
|
* FAIL-ALREADYCONSUMED the token exists, but has been used before |
99
|
|
|
* FAIL-EXPIRED the token exists, but has expired |
100
|
|
|
* |
101
|
|
|
* @param string $token the invitation token |
102
|
|
|
* @return int |
103
|
|
|
*/ |
104
|
|
|
public function checkTokenValidity($token) |
105
|
|
|
{ |
106
|
|
|
$check = $this->databaseHandle->exec("SELECT invite_token, cat_institution_id |
107
|
|
|
FROM invitations |
108
|
|
|
WHERE invite_token = ? AND invite_created >= TIMESTAMPADD(DAY, -1, NOW()) AND used = 0", "s", $token); |
109
|
|
|
// SELECT -> resource, not boolean |
110
|
|
|
if ($tokenCheck = mysqli_fetch_object(/** @scrutinizer ignore-type */ $check)) { |
111
|
|
|
if ($tokenCheck->cat_institution_id === NULL) { |
112
|
|
|
return self::TOKENSTATUS_OK_NEW; |
113
|
|
|
} |
114
|
|
|
return self::TOKENSTATUS_OK_EXISTING; |
115
|
|
|
} |
116
|
|
|
// if we haven't returned from the function yet, it is an invalid token... |
117
|
|
|
// be a little verbose what's wrong with it |
118
|
|
|
$checkReason = $this->databaseHandle->exec("SELECT invite_token, used FROM invitations WHERE invite_token = ?", "s", $token); |
119
|
|
|
// SELECT -> resource, not boolean |
120
|
|
|
if ($invalidTokenCheck = mysqli_fetch_object(/** @scrutinizer ignore-type */ $checkReason)) { |
121
|
|
|
if ($invalidTokenCheck->used == 1) { |
122
|
|
|
return self::TOKENSTATUS_FAIL_ALREADYCONSUMED; |
123
|
|
|
} |
124
|
|
|
return self::TOKENSTATUS_FAIL_EXPIRED; |
125
|
|
|
} |
126
|
|
|
return self::TOKENSTATUS_FAIL_NONEXISTING; |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
/** |
130
|
|
|
* This function creates a new IdP in the database based on a valid invitation token - or adds a new administrator |
131
|
|
|
* to an existing one. The institution is created for the logged-in user (second argument) who presents the token (first |
132
|
|
|
* argument). The tokens are created via createToken(). |
133
|
|
|
* |
134
|
|
|
* @param string $token The invitation token (must exist in the database and be valid). |
135
|
|
|
* @param string $owner Persistent User ID who becomes the administrator of the institution |
136
|
|
|
* @return IdP |
137
|
|
|
*/ |
138
|
|
|
public function createIdPFromToken(string $token, string $owner) |
139
|
|
|
{ |
140
|
|
|
new CAT(); // be sure that Entity's static members are initialised |
141
|
|
|
common\Entity::intoThePotatoes(); |
142
|
|
|
// the token either has cat_institution_id set -> new admin for existing inst |
143
|
|
|
// or contains a number of parameters from external DB -> set up new inst |
144
|
|
|
$instinfo = $this->databaseHandle->exec("SELECT cat_institution_id, country, name, invite_issuer_level, invite_dest_mail, external_db_uniquehandle, invite_fortype |
145
|
|
|
FROM invitations |
146
|
|
|
WHERE invite_token = ? AND invite_created >= TIMESTAMPADD(DAY, -1, NOW()) AND used = 0", "s", $token); |
147
|
|
|
// SELECT -> resource, no boolean |
148
|
|
|
if ($invitationDetails = mysqli_fetch_assoc(/** @scrutinizer ignore-type */ $instinfo)) { |
149
|
|
|
if ($invitationDetails['cat_institution_id'] !== NULL) { // add new admin to existing IdP |
150
|
|
|
// we can't rely on a unique key on this table (user IDs |
151
|
|
|
// possibly too long), so run a query to find there's an |
152
|
|
|
// tuple already; and act accordingly |
153
|
|
|
$catId = $invitationDetails['cat_institution_id']; |
154
|
|
|
$level = $invitationDetails['invite_issuer_level']; |
155
|
|
|
$destMail = $invitationDetails['invite_dest_mail']; |
156
|
|
|
$existing = $this->databaseHandle->exec("SELECT user_id FROM ownership WHERE user_id = ? AND institution_id = ?", "si", $owner, $catId); |
157
|
|
|
// SELECT -> resource, not boolean |
158
|
|
|
if (mysqli_num_rows(/** @scrutinizer ignore-type */ $existing) > 0) { |
159
|
|
|
$this->databaseHandle->exec("UPDATE ownership SET blesslevel = ?, orig_mail = ? WHERE user_id = ? AND institution_id = ?", "sssi", $level, $destMail, $owner, $catId); |
160
|
|
|
} else { |
161
|
|
|
$this->databaseHandle->exec("INSERT INTO ownership (user_id, institution_id, blesslevel, orig_mail) VALUES(?, ?, ?, ?)", "siss", $owner, $catId, $level, $destMail); |
162
|
|
|
} |
163
|
|
|
common\Logging::writeAudit_s((string) $owner, "OWN", "IdP " . $invitationDetails['cat_institution_id'] . " - added user as owner"); |
164
|
|
|
common\Entity::outOfThePotatoes(); |
165
|
|
|
return new IdP($invitationDetails['cat_institution_id']); |
166
|
|
|
} |
167
|
|
|
// create new IdP |
168
|
|
|
$fed = new Federation($invitationDetails['country']); |
169
|
|
|
// find the best name for the entity: C if specified, otherwise English, otherwise whatever |
170
|
|
|
if ($invitationDetails['external_db_uniquehandle'] != NULL) { |
171
|
|
|
$idp = $this->newIdPFromExternal($invitationDetails['external_db_uniquehandle'], $fed, $invitationDetails, $owner); |
172
|
|
|
} else { |
173
|
|
|
$bestnameguess = $invitationDetails['name']; |
174
|
|
|
$idp = new IdP($fed->newIdP('TOKEN', $invitationDetails['invite_fortype'], $owner, $invitationDetails['invite_issuer_level'], $invitationDetails['invite_dest_mail'], $bestnameguess)); |
175
|
|
|
$idp->addAttribute("general:instname", 'C', $bestnameguess); |
176
|
|
|
} |
177
|
|
|
common\Logging::writeAudit_s($owner, "NEW", "IdP " . $idp->identifier . " - created from invitation"); |
178
|
|
|
|
179
|
|
|
// in case we have more admins in the queue which were invited to |
180
|
|
|
// administer the same inst but haven't redeemed their invitations |
181
|
|
|
// yet, then we will have to rewrite the invitations to point to the |
182
|
|
|
// newly created actual IdP rather than the placeholder entry in the |
183
|
|
|
// invitations table |
184
|
|
|
// which other pending invites do we have? |
185
|
|
|
|
186
|
|
|
$otherPending = $this->databaseHandle->exec("SELECT id |
187
|
|
|
FROM invitations |
188
|
|
|
WHERE invite_created >= TIMESTAMPADD(DAY, -1, NOW()) AND used = 0 AND name = ? AND country = ? AND ( cat_institution_id IS NULL OR external_db_uniquehandle IS NULL ) ", "ss", $invitationDetails['name'], $invitationDetails['country']); |
189
|
|
|
// SELECT -> resource, no boolean |
190
|
|
|
while ($pendingDetail = mysqli_fetch_object(/** @scrutinizer ignore-type */ $otherPending)) { |
191
|
|
|
$this->databaseHandle->exec("UPDATE invitations SET cat_institution_id = " . $idp->identifier . " WHERE id = " . $pendingDetail->id); |
192
|
|
|
} |
193
|
|
|
common\Entity::outOfThePotatoes(); |
194
|
|
|
return $idp; |
195
|
|
|
} |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
/** |
199
|
|
|
* create new institution based on the edxternalDB data |
200
|
|
|
* @param string $extId - the eduroam database identifier |
201
|
|
|
* @param object $fed - the CAT federation object where the institution should be created |
202
|
|
|
* @param string $owner |
203
|
|
|
* @return type |
|
|
|
|
204
|
|
|
*/ |
205
|
|
|
public function createIdPFromExternal($extId, $fed, $owner) |
206
|
|
|
{ |
207
|
|
|
$cat = new CAT(); |
208
|
|
|
$ROid = strtoupper($fed->tld).'01'; |
209
|
|
|
$externalinfo = $cat->getExternalDBEntityDetails($extId, $ROid); |
210
|
|
|
$invitationDetails = [ |
211
|
|
|
'invite_fortype' => $externalinfo['type'], |
212
|
|
|
'invite_issuer_level' => "FED", |
213
|
|
|
'invite_dest_mail' => $_SESSION['auth_email'], |
214
|
|
|
]; |
215
|
|
|
$idp = $this->newIdPFromExternal($extId, $fed, $invitationDetails, $owner, $externalinfo); |
216
|
|
|
common\Logging::writeAudit_s($owner, "NEW", "IdP " . $idp->identifier . " - created from auto-registration of $extId"); |
217
|
|
|
return $idp; |
|
|
|
|
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/* |
221
|
|
|
* This is the common part of the code for createIdPFromToken and createIdPFromExternal |
222
|
|
|
*/ |
223
|
|
|
private function newIdPFromExternal($extId, $fed, $invitationDetails, $owner, $externalinfo = []) |
224
|
|
|
{ |
225
|
|
|
// see if we had a C language, and if not, pick a good candidate |
226
|
|
|
if ($externalinfo == []) { |
227
|
|
|
$cat = new CAT(); |
228
|
|
|
$ROid = strtoupper($fed->tld).'01'; |
229
|
|
|
$externalinfo = $cat->getExternalDBEntityDetails($extId, $ROid); |
230
|
|
|
} |
231
|
|
|
$bestnameguess = $externalinfo['names']['C'] ?? $externalinfo['names']['en'] ?? reset($externalinfo['names']); |
232
|
|
|
$idp = new IdP($fed->newIdP('SELF', $invitationDetails['invite_fortype'], $owner, $invitationDetails['invite_issuer_level'], $invitationDetails['invite_dest_mail'], $bestnameguess)); |
233
|
|
|
foreach ($externalinfo['names'] as $instlang => $instname) { |
234
|
|
|
$idp->addAttribute("general:instname", $instlang, $instname); |
235
|
|
|
} |
236
|
|
|
$idp->setExternalDBId($extId, strtolower($fed->tld)); |
237
|
|
|
$idp->addAttribute("general:instname", 'C', $bestnameguess); |
238
|
|
|
return $idp; |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
/** |
242
|
|
|
* Adds a new administrator to an existing IdP |
243
|
|
|
* @param IdP $idp institution to which the admin is to be added. |
244
|
|
|
* @param string $user persistent user ID that is to be added as an admin. |
245
|
|
|
* @return boolean This function always returns TRUE. |
246
|
|
|
*/ |
247
|
|
|
public function addAdminToIdp($idp, $user) |
248
|
|
|
{ |
249
|
|
|
$existing = $this->databaseHandle->exec("SELECT user_id FROM ownership WHERE user_id = ? AND institution_id = ?", "si", $user, $idp->identifier); |
250
|
|
|
// SELECT -> resource, not boolean |
251
|
|
|
if (mysqli_num_rows(/** @scrutinizer ignore-type */ $existing) == 0) { |
252
|
|
|
$this->databaseHandle->exec("INSERT INTO ownership (institution_id,user_id,blesslevel,orig_mail) VALUES(?, ?, 'FED', 'SELF-APPOINTED')", "is", $idp->identifier, $user); |
253
|
|
|
} |
254
|
|
|
return TRUE; |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
/** |
258
|
|
|
* Deletes an administrator from the IdP. If the IdP and user combination doesn't match, nothing happens. |
259
|
|
|
* @param IdP $idp institution from which the admin is to be deleted. |
260
|
|
|
* @param string $user persistent user ID that is to be deleted as an admin. |
261
|
|
|
* @return boolean This function always returns TRUE. |
262
|
|
|
*/ |
263
|
|
|
public function removeAdminFromIdP($idp, $user) |
264
|
|
|
{ |
265
|
|
|
$this->databaseHandle->exec("DELETE from ownership WHERE institution_id = $idp->identifier AND user_id = ?", "s", $user); |
266
|
|
|
return TRUE; |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
/** |
270
|
|
|
* Invalidates a token so that it can't be used any more. Tokens automatically expire after 24h, but can be invalidated |
271
|
|
|
* earlier, e.g. after having been used to create an institution. If the token doesn't exist in the DB or is already invalidated, |
272
|
|
|
* nothing happens. |
273
|
|
|
* |
274
|
|
|
* @param string $token the token to invalidate |
275
|
|
|
* @return boolean This function always returns TRUE. |
276
|
|
|
*/ |
277
|
|
|
public function invalidateToken($token) |
278
|
|
|
{ |
279
|
|
|
$this->databaseHandle->exec("UPDATE invitations SET used = 1 WHERE invite_token = ?", "s", $token); |
280
|
|
|
return TRUE; |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
/** |
284
|
|
|
* Creates a new invitation token. The token's main purpose is to be sent out by mail. The function either can generate a token for a new |
285
|
|
|
* administrator of an existing institution, or for a new institution. In the latter case, the institution only actually gets |
286
|
|
|
* created in the DB if the token is actually consumed via createIdPFromToken(). |
287
|
|
|
* |
288
|
|
|
* @param boolean $isByFedadmin is the invitation token created for a federation admin (TRUE) or from an existing inst admin (FALSE) |
289
|
|
|
* @param array $for identifiers (typically email addresses) for which the invitation is created |
290
|
|
|
* @param mixed $instIdentifier either an instance of the IdP class (for existing institutions to invite new admins) or a string (new institution - this is the inst name then) |
291
|
|
|
* @param string $externalId if the IdP to be created is related to an external DB entity, this parameter contains that ID |
292
|
|
|
* @param string $country if the institution is new (i.e. $inst is a string) this parameter needs to specify the federation of the new inst |
293
|
|
|
* @param string $partType the type of participant |
294
|
|
|
* @return mixed The function returns either the token (as string) or FALSE if something went wrong |
295
|
|
|
* @throws Exception |
296
|
|
|
*/ |
297
|
|
|
public function createTokens($isByFedadmin, $for, $instIdentifier, $externalId = 0, $country = 0, $partType = 0) |
298
|
|
|
{ |
299
|
|
|
$level = ($isByFedadmin ? "FED" : "INST"); |
300
|
|
|
$tokenList = []; |
301
|
|
|
foreach ($for as $oneDest) { |
302
|
|
|
$token = bin2hex(random_bytes(40)); |
303
|
|
|
if ($instIdentifier instanceof IdP) { |
304
|
|
|
$this->databaseHandle->exec("INSERT INTO invitations (invite_fortype, invite_issuer_level, invite_dest_mail, invite_token,cat_institution_id) VALUES(?, ?, ?, ?, ?)", "ssssi", $instIdentifier->type, $level, $oneDest, $token, $instIdentifier->identifier); |
305
|
|
|
$tokenList[$token] = $oneDest; |
306
|
|
|
} else if (func_num_args() == 5) { |
307
|
|
|
$ROid = strtoupper($country).'01'; |
308
|
|
|
$cat = new CAT(); |
309
|
|
|
$extinfo = $cat->getExternalDBEntityDetails($externalId, $ROid); |
310
|
|
|
$extCountry = $extinfo['country']; |
311
|
|
|
$extType = $extinfo['type']; |
312
|
|
|
if(\config\Master::FUNCTIONALITY_FLAGS['SINGLE_SERVICE'] === 'MSP') { |
313
|
|
|
$extType = \core\IdP::TYPE_SP; |
314
|
|
|
} |
315
|
|
|
$this->databaseHandle->exec("INSERT INTO invitations (invite_fortype, invite_issuer_level, invite_dest_mail, invite_token,name,country, external_db_uniquehandle) VALUES(?, ?, ?, ?, ?, ?, ?)", "sssssss", $extType, $level, $oneDest, $token, $instIdentifier, $extCountry, $externalId); |
316
|
|
|
$tokenList[$token] = $oneDest; |
317
|
|
|
} else if (func_num_args() == 6) { // string name, and country set - whole new IdP |
318
|
|
|
$this->databaseHandle->exec("INSERT INTO invitations (invite_fortype, invite_issuer_level, invite_dest_mail, invite_token,name,country) VALUES(?, ?, ?, ?, ?, ?)", "ssssss", $partType, $level, $oneDest, $token, $instIdentifier, $country); |
319
|
|
|
$tokenList[$token] = $oneDest; |
320
|
|
|
} else { |
321
|
|
|
throw new Exception("The invitation is somehow ... wrong."); |
322
|
|
|
} |
323
|
|
|
} |
324
|
|
|
if (count($for) != count($tokenList)) { |
325
|
|
|
throw new Exception("Creation of a new token failed!"); |
326
|
|
|
} |
327
|
|
|
return $tokenList; |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
/** |
331
|
|
|
* Retrieves all pending invitations for an institution or for a federation. |
332
|
|
|
* |
333
|
|
|
* @param int $idpIdentifier the identifier of the institution. If not set, returns invitations for not-yet-created insts |
334
|
|
|
* @return array if idp_identifier is set: an array of strings (mail addresses); otherwise an array of tuples (country;name;mail) |
335
|
|
|
*/ |
336
|
|
|
public function listPendingInvitations($idpIdentifier = 0) |
337
|
|
|
{ |
338
|
|
|
$retval = []; |
339
|
|
|
$invitations = $this->databaseHandle->exec("SELECT cat_institution_id, country, name, invite_issuer_level, invite_dest_mail, invite_token , TIMESTAMPADD(DAY, 1, invite_created) as expiry |
340
|
|
|
FROM invitations |
341
|
|
|
WHERE cat_institution_id " . ( $idpIdentifier != 0 ? "= $idpIdentifier" : "IS NULL") . " AND invite_created >= TIMESTAMPADD(DAY, -1, NOW()) AND used = 0"); |
342
|
|
|
// SELECT -> resource, not boolean |
343
|
|
|
common\Logging::debug_s(4, "Retrieving pending invitations for " . ($idpIdentifier != 0 ? "IdP $idpIdentifier" : "IdPs awaiting initial creation" ) . ".\n"); |
344
|
|
|
while ($invitationQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $invitations)) { |
345
|
|
|
$retval[] = ["country" => $invitationQuery->country, "name" => $invitationQuery->name, "mail" => $invitationQuery->invite_dest_mail, "token" => $invitationQuery->invite_token, "expiry" => $invitationQuery->expiry]; |
346
|
|
|
} |
347
|
|
|
return $retval; |
348
|
|
|
} |
349
|
|
|
|
350
|
|
|
/** Retrieves all invitations which have expired in the last hour. |
351
|
|
|
* |
352
|
|
|
* @return array of expired invitations |
353
|
|
|
*/ |
354
|
|
|
public function listRecentlyExpiredInvitations() |
355
|
|
|
{ |
356
|
|
|
$retval = []; |
357
|
|
|
$invitations = $this->databaseHandle->exec("SELECT cat_institution_id, country, name, invite_issuer_level, invite_dest_mail, invite_token |
358
|
|
|
FROM invitations |
359
|
|
|
WHERE invite_created >= TIMESTAMPADD(HOUR, -25, NOW()) AND invite_created < TIMESTAMPADD(HOUR, -24, NOW()) AND used = 0"); |
360
|
|
|
// SELECT -> resource, not boolean |
361
|
|
|
while ($expInvitationQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $invitations)) { |
362
|
|
|
common\Logging::debug_s(4, "Retrieving recently expired invitations (expired in last hour)\n"); |
363
|
|
|
if ($expInvitationQuery->cat_institution_id == NULL) { |
364
|
|
|
$retval[] = ["country" => $expInvitationQuery->country, "level" => $expInvitationQuery->invite_issuer_level, "name" => $expInvitationQuery->name, "mail" => $expInvitationQuery->invite_dest_mail]; |
365
|
|
|
} else { |
366
|
|
|
$retval[] = ["country" => $expInvitationQuery->country, "level" => $expInvitationQuery->invite_issuer_level, "name" => "Existing IdP", "mail" => $expInvitationQuery->invite_dest_mail]; |
367
|
|
|
} |
368
|
|
|
} |
369
|
|
|
return $retval; |
370
|
|
|
} |
371
|
|
|
|
372
|
|
|
/** |
373
|
|
|
* For a given persistent user identifier, returns an array of institution identifiers (not the actual objects!) for which this |
374
|
|
|
* user is the/a administrator and also do comparisons to the eduroam DB results. |
375
|
|
|
* If the federation autoregister-synced flag is set if it turns out that the eduroam DB |
376
|
|
|
* lists the email of the current logged-in admin as an admin of an existing CAT institution |
377
|
|
|
* and this institution is synced to the matching external institutuin then this admin |
378
|
|
|
* will be automatically added tho the institution and the 'existing' part of $this->currentInstitutions |
379
|
|
|
* will be updated. This identifier will also be listed to $this->currentInstitutions['resynced'] |
380
|
|
|
* |
381
|
|
|
* If the federation autoregister-new-inst flag is set and there are exeternal institututions which could be |
382
|
|
|
* candidated for creating them in CAT - add the identifiers of these institutuins to this->currentInstitutions[new'] |
383
|
|
|
* |
384
|
|
|
* @param boolean $applyAutoSync controls if automatic additions if the user to the admins should be performed |
385
|
|
|
* @return array array of institution IDs |
386
|
|
|
*/ |
387
|
|
|
public function listInstitutionsByAdmin($applyAutoSync = false) |
388
|
|
|
{ |
389
|
|
|
$edugain = $_SESSION['eduGAIN']; |
390
|
|
|
// get the list of local identifers of institutions managed by this user |
391
|
|
|
// it will be returned as $this->currentInstitutions |
392
|
|
|
$this->getCurrentInstitutionsByAdmin(); |
393
|
|
|
if (count($this->currentInstitutions) == 0) { |
394
|
|
|
$this->newUser = true; |
395
|
|
|
} |
396
|
|
|
// check if selfservice_registration is set to eduGAIN - if not then return with the durrent list |
397
|
|
|
if (\config\ConfAssistant::CONSORTIUM['selfservice_registration'] !== 'eduGAIN') { |
398
|
|
|
common\Logging::debug_s(4, "selfservice_registration not set to eduGAIN\n"); |
399
|
|
|
return $this->currentInstitutions; |
400
|
|
|
} |
401
|
|
|
// now add additional institutions based on the external DB |
402
|
|
|
// proceed only if user has been authenticated fron an eduGAIN IdP |
403
|
|
|
if ($edugain == false) { |
404
|
|
|
return $this->currentInstitutions; |
405
|
|
|
} |
406
|
|
|
$email = $_SESSION['auth_email']; |
407
|
|
|
$externalDB = CAT::determineExternalConnection(); |
408
|
|
|
// get the list of identifiers in the external DB with this user listed as the admin and linked to CAT institutions |
409
|
|
|
$extInstList = $externalDB->listExternalEntitiesByUserEmail($email); |
410
|
|
|
$extInstListTmp = $extInstList; |
411
|
|
|
// we begin by removing entites in $extInstList which are already managed by this user and synced - |
412
|
|
|
// these require no further checking |
413
|
|
|
foreach ($extInstListTmp as $country => $extInstCountryList) { |
414
|
|
|
$len = count($extInstCountryList); |
415
|
|
|
for($i = 0; $i < $len; ++$i) { |
416
|
|
|
$extInst = $extInstCountryList[$i]; |
417
|
|
|
if ($extInst['inst_id'] != NULL && in_array($extInst['inst_id'], $this->currentInstitutions['existing'])) { |
418
|
|
|
unset($extInstList[$country][$i]); |
419
|
|
|
} |
420
|
|
|
} |
421
|
|
|
if (count($extInstList[$country]) == 0) { |
422
|
|
|
unset($extInstList[$country]); |
423
|
|
|
} |
424
|
|
|
} |
425
|
|
|
// now verify the $extInstList separately for each federation making sure |
426
|
|
|
// that the federation allows autoregistration |
427
|
|
|
foreach ($extInstList as $country => $extInstCountryList) { |
428
|
|
|
$fed = new Federation($country); |
429
|
|
|
$this->doExternalDBAutoregister($extInstCountryList, $fed, $applyAutoSync); |
430
|
|
|
$this->doExternalDBAutoregisterNew($extInstCountryList, $fed); |
431
|
|
|
} |
432
|
|
|
// now we run tests for adding admins based on pairwise-id and entitlement |
433
|
|
|
$entitledCountries = $this->getUserEntitledFed(); |
434
|
|
|
$entitlementInst = $this->listCatInstitutionsByPairwiseId(); |
435
|
|
|
common\Logging::debug_s(4, $entitlementInst, "entitlementInst\n", "\n"); |
436
|
|
|
foreach ($entitlementInst as $country => $instList) { |
437
|
|
|
if (!in_array($country, $entitledCountries)) { |
438
|
|
|
continue; |
439
|
|
|
} |
440
|
|
|
$fed = new Federation($country); |
441
|
|
|
$this->doEntitlementAutoregister($instList, $fed); |
442
|
|
|
} |
443
|
|
|
$_SESSION['entitledIdPs'] = array_column($this->currentInstitutions['entitlement'], 0); |
444
|
|
|
common\Logging::debug_s(4, $this->currentInstitutions, "currentInstitutions\n", "\n"); |
445
|
|
|
return $this->currentInstitutions; |
446
|
|
|
} |
447
|
|
|
|
448
|
|
|
/** |
449
|
|
|
* Handle auto-registration of admin for CAT institutions which are synced to |
450
|
|
|
* eduroam DB institutions which have the current admin listed |
451
|
|
|
* The method verifies that the federation allows auto-registration |
452
|
|
|
* |
453
|
|
|
* @param array $extInstCountryList - list of eduroam DB candidate institutions |
454
|
|
|
* @param object $fed - the Federation object |
455
|
|
|
* @param boolean $applyAutoSync - do we write updated to the databaes? |
456
|
|
|
*/ |
457
|
|
|
private function doExternalDBAutoregister($extInstCountryList, $fed, $applyAutoSync = false) { |
458
|
|
|
$userId = $_SESSION['user']; |
459
|
|
|
$email = $_SESSION['auth_email']; |
460
|
|
|
$autoSyncedFlag = $fed->getAttributes('fed:autoregister-synced'); |
461
|
|
|
if ($autoSyncedFlag == []) { |
462
|
|
|
return; |
463
|
|
|
} |
464
|
|
|
foreach ($extInstCountryList as $extInst) { |
465
|
|
|
common\Logging::debug_s(4, "Testing ".$extInst['external_db_id']."\n"); |
466
|
|
|
if ($extInst['inst_id'] == null) { |
467
|
|
|
// this institution is not synced, skip |
468
|
|
|
continue; |
469
|
|
|
} |
470
|
|
|
// is institution synced, if so we add this admin if the federation allows |
471
|
|
|
common\Logging::debug_s(4, "It is synced\n"); |
472
|
|
|
$this->currentInstitutions['resynced'][] = $extInst['inst_id']; |
473
|
|
|
if ($applyAutoSync) { |
474
|
|
|
common\Logging::debug_s(4, "Adding admin to ".$extInst['inst_id']."\n"); |
475
|
|
|
$this->currentInstitutions['existing'][] = $extInst['inst_id']; |
476
|
|
|
$query = "INSERT INTO ownership (user_id, institution_id, blesslevel, orig_mail) VALUES (?, ?, 'FED', ?)"; |
477
|
|
|
$this->databaseHandle->exec($query, 'sis', $userId, $extInst['inst_id'], $email); |
478
|
|
|
} |
479
|
|
|
} |
480
|
|
|
} |
481
|
|
|
|
482
|
|
|
/** |
483
|
|
|
* Handle auto-creation of new CAT institutions which match |
484
|
|
|
* eduroam DB institutions which have the current admin listed |
485
|
|
|
* The method verifies that the federation allows auto-cereation |
486
|
|
|
* |
487
|
|
|
* @param array $extInstCountryList - list of eduroam DB candidate institutions |
488
|
|
|
* @param object $fed - the Federation object |
489
|
|
|
*/ |
490
|
|
|
private function doExternalDBAutoregisterNew($extInstCountryList, $fed) { |
491
|
|
|
$newInstFlag = $fed->getAttributes('fed:autoregister-new-inst'); |
492
|
|
|
if ($newInstFlag == []) { |
493
|
|
|
return; |
494
|
|
|
} |
495
|
|
|
foreach ($extInstCountryList as $extInst) { |
496
|
|
|
common\Logging::debug_s(4, "Testing ".$extInst['external_db_id']." for potential new inst\n"); |
497
|
|
|
if ($extInst['inst_id'] != null) { // there alreay exeists a CAT institution synced to this one |
498
|
|
|
continue; |
499
|
|
|
} |
500
|
|
|
$country = $fed->tld; |
501
|
|
|
// now run checks against creating dupplicates in CAT DB |
502
|
|
|
$disectedNames = ExternalEduroamDBData::dissectCollapsedInstitutionNames($extInst['name']); |
503
|
|
|
$names = $disectedNames['joint']; |
504
|
|
|
$realms = ExternalEduroamDBData::dissectCollapsedInstitutionRealms($extInst['realm']); |
505
|
|
|
$foundMatch = $this->checkForSimilarInstitutions($names, $realms); |
506
|
|
|
common\Logging::debug_s(4, $foundMatch, "checkForSimilarInstitutions returned: ","\n"); |
507
|
|
|
if ($foundMatch == 0) { |
508
|
|
|
$this->currentInstitutions['new'][] = [$extInst['external_db_id'], $disectedNames['perlang'], $country]; |
509
|
|
|
} |
510
|
|
|
} |
511
|
|
|
} |
512
|
|
|
|
513
|
|
|
/** |
514
|
|
|
* Handle auto-registration of admin for CAT institutions |
515
|
|
|
* based on data from eduGAIN login |
516
|
|
|
* The method verifies that the federation allows this type of auto-registration |
517
|
|
|
* |
518
|
|
|
* @param boolean $applyAutoSync - do we write updated to the databaes? |
519
|
|
|
* @param object $fed - the Federation object |
520
|
|
|
*/ |
521
|
|
|
private function doEntitlementAutoregister($instList, $fed) { |
522
|
|
|
$useEntitlementFlag = $fed->getAttributes('fed:autoregister-entitlement'); |
523
|
|
|
if ($useEntitlementFlag == []) { |
524
|
|
|
return; |
525
|
|
|
} |
526
|
|
|
$country = $fed->tld; |
527
|
|
|
foreach ($instList as $instId) { |
528
|
|
|
if (!in_array($instId, $this->currentInstitutions['existing'])) { |
529
|
|
|
$this->currentInstitutions['entitlement'][] = [$instId, $country]; |
530
|
|
|
} |
531
|
|
|
} |
532
|
|
|
|
533
|
|
|
} |
534
|
|
|
|
535
|
|
|
/** |
536
|
|
|
* Generate a list of externalDB institutions for which the admin is entitled |
537
|
|
|
* based on the eduPersonEntitlement setting and scope from pairwise-id |
538
|
|
|
* The |
539
|
|
|
* @return array indexed by countries |
540
|
|
|
*/ |
541
|
|
|
private function listCatInstitutionsByPairwiseId() { |
542
|
|
|
if (!isset(\config\ConfAssistant::CONSORTIUM['entitlement'])) { |
543
|
|
|
return []; |
544
|
|
|
} |
545
|
|
|
// first check if pairwise-id is set |
546
|
|
|
$userId = $_SESSION['user']; |
547
|
|
|
if (substr($userId, 0, 12) !== 'pairwise-id:') { |
548
|
|
|
return []; |
549
|
|
|
} |
550
|
|
|
|
551
|
|
|
// next check if entitlement is setand matches our expectations |
552
|
|
|
if (!isset($_SESSION['entitlement'])) { |
553
|
|
|
return []; |
554
|
|
|
} |
555
|
|
|
|
556
|
|
|
// get realm from pariwise-id |
557
|
|
|
if (preg_match('/^pairwise-id:[^@]+@([^!]+)!/', $userId, $matches) == 0) { |
558
|
|
|
return []; |
559
|
|
|
} |
560
|
|
|
|
561
|
|
|
$userRealm = $matches[1]; |
562
|
|
|
common\Logging::debug_s(4, $userRealm, "userRealm:", "\n"); |
563
|
|
|
// list CAT inst_id and country from the profile |
564
|
|
|
$query = "SELECT DISTINCT profile.inst_id,country FROM profile JOIN institution on profile.inst_id = institution.inst_id WHERE realm LIKE '%@$userRealm' OR realm LIKE '%@%.$userRealm'"; |
565
|
|
|
$institutions = $this->databaseHandle->exec($query); |
566
|
|
|
$catInstList = $institutions->fetch_all(); |
567
|
|
|
$returnarray = []; |
568
|
|
|
foreach ($catInstList as $inst) { |
569
|
|
|
$country = $inst[1]; |
570
|
|
|
if (!isset($returnarray[$country])) { |
571
|
|
|
$returnarray[$country] = []; |
572
|
|
|
} |
573
|
|
|
$returnarray[$country][] = $inst[0]; |
574
|
|
|
} |
575
|
|
|
return $returnarray; |
576
|
|
|
} |
577
|
|
|
|
578
|
|
|
/** |
579
|
|
|
* Get the list of eduroam countries that the user could potentially auto-register |
580
|
|
|
* based on eduPersonEntitlement. The list of countries is based on matching between |
581
|
|
|
* the eduGAIN federation where the eduGAIN IdP is registered and the counters that this federation serves |
582
|
|
|
* Before passing on the list it is checked if particular countries allow entitlement-based |
583
|
|
|
* autoregistration |
584
|
|
|
* |
585
|
|
|
* @return array indexed by countries |
586
|
|
|
*/ |
587
|
|
|
private function getUserEntitledFed() { |
588
|
|
|
if (!isset($_SESSION['entitlement'])) { |
589
|
|
|
return []; |
590
|
|
|
} |
591
|
|
|
$entitledCountries = []; |
592
|
|
|
$countries = $this->databaseHandle->exec("SELECT country FROM edugain WHERE reg_auth = ?", 's', $_SESSION['eduGAIN']); |
593
|
|
|
$countryList = $countries->fetch_all(); |
594
|
|
|
$countriesTmp = []; |
|
|
|
|
595
|
|
|
foreach ($countryList as $country) { |
596
|
|
|
$requiredEntitlement = NULL; |
597
|
|
|
$countryCode = $country[0]; |
598
|
|
|
$fed = new Federation($countryCode); |
599
|
|
|
$autoreg = $fed->getAttributes('fed:autoregister-entitlement'); |
600
|
|
|
$entitlementVal = $fed->getAttributes('fed:entitlement-attr'); |
601
|
|
|
if (isset($autoreg[0]['value']) && $autoreg[0]['value'] == 'on') { |
602
|
|
|
$requiredEntitlement = isset($entitlementVal[0]['value']) ? $entitlementVal[0]['value'] : \config\ConfAssistant::CONSORTIUM['entitlement']; |
603
|
|
|
} |
604
|
|
|
common\Logging::debug_s(4, $requiredEntitlement, "$countryCode requiredEntitlement\n", "\n"); |
605
|
|
|
if (in_array($requiredEntitlement, $_SESSION['entitlement'])) { |
606
|
|
|
$entitledCountries[] = $countryCode; |
607
|
|
|
} |
608
|
|
|
} |
609
|
|
|
common\Logging::debug_s(4, $entitledCountries, "entitledCountries:\n", "\n"); |
610
|
|
|
return $entitledCountries; |
611
|
|
|
} |
612
|
|
|
|
613
|
|
|
/** |
614
|
|
|
* Tests if the institution with these identifier does not yet exist in CAT. |
615
|
|
|
* This is done by testing the admins "new" institutions, this way we also make sure |
616
|
|
|
* that this admin is actually also allowed to create the new one |
617
|
|
|
* |
618
|
|
|
* @return int 1 or 0. 1 means we are free to create the inst. |
619
|
|
|
*/ |
620
|
|
|
|
621
|
|
|
public function checkForCatMatch($extId, $ROid) { |
622
|
|
|
$this->listInstitutionsByAdmin(); |
623
|
|
|
foreach ($this->currentInstitutions['new'] as $newInst) { |
624
|
|
|
if ($extId == $newInst[0] && $ROid == strtoupper($newInst[2]).'01') { |
625
|
|
|
return 0; |
626
|
|
|
} |
627
|
|
|
} |
628
|
|
|
return 1; |
629
|
|
|
} |
630
|
|
|
|
631
|
|
|
/** |
632
|
|
|
* get the list of current institutions of the given admin |
633
|
|
|
* |
634
|
|
|
* This method does not rerurn anything but sets $this->currentInstitutions |
635
|
|
|
* it only fillsh the 'existing' block, leaving the other two for other methods |
636
|
|
|
* to deal with |
637
|
|
|
*/ |
638
|
|
|
private function getCurrentInstitutionsByAdmin() { |
639
|
|
|
$returnarray = [ |
640
|
|
|
'existing' => [], |
641
|
|
|
'resynced' => [], |
642
|
|
|
'new' => [], |
643
|
|
|
'entitlement' => [] |
644
|
|
|
]; |
645
|
|
|
$userId = $_SESSION['user']; |
646
|
|
|
// get the list of local identifers of institutions managed by this user |
647
|
|
|
$institutions = $this->databaseHandle->exec("SELECT ownership.institution_id as inst_id FROM ownership WHERE user_id = ? ORDER BY institution_id", "s", $userId); |
648
|
|
|
// SELECT -> resource, not boolean |
649
|
|
|
$catInstList = $institutions->fetch_all(); |
650
|
|
|
foreach ($catInstList as $inst) { |
651
|
|
|
$returnarray['existing'][] = $inst[0]; |
652
|
|
|
} |
653
|
|
|
$this->currentInstitutions = $returnarray; |
654
|
|
|
} |
655
|
|
|
|
656
|
|
|
/** |
657
|
|
|
* given arrays of realms and names check if there already are institutions in CAT that |
658
|
|
|
* could be a match - this is for ellimination and against creating duplicates |
659
|
|
|
* still this is not perfect, no realms given and institutions with a slightly different |
660
|
|
|
* name will return no-match and thus open possibility for dupplicates |
661
|
|
|
* |
662
|
|
|
* @param array $namesToTest |
663
|
|
|
* @param array $realmsToTest |
664
|
|
|
* @return int - 1 - a match was found, 0 - no match found |
665
|
|
|
*/ |
666
|
|
|
private function checkForSimilarInstitutions($namesToTest, $realmsToTest) { |
667
|
|
|
//generate a list of all existing realms |
668
|
|
|
$realmsList = []; |
669
|
|
|
$query = 'SELECT DISTINCT realm FROM profile'; |
670
|
|
|
$realmsResult = $this->databaseHandle->exec($query); |
671
|
|
|
while ($anonId = $realmsResult->fetch_row()) { |
672
|
|
|
$realmsList[] = mb_strtolower(preg_replace('/^.*@/', '', $anonId[0]), 'UTF-8'); |
673
|
|
|
} |
674
|
|
|
// now test realms |
675
|
|
|
$results = array_intersect($realmsToTest, $realmsList); |
676
|
|
|
if (count($results) !== 0) { |
677
|
|
|
return 1; |
678
|
|
|
} |
679
|
|
|
|
680
|
|
|
// generate a list of all institution names |
681
|
|
|
$query = "SELECT DISTINCT CONVERT(option_value USING utf8mb4) FROM institution_option WHERE option_name='general:instname'"; |
682
|
|
|
$namesResult = $this->databaseHandle->exec($query); |
683
|
|
|
$namesList = []; |
684
|
|
|
while ($name = $namesResult->fetch_row()) { |
685
|
|
|
$namesList[] = mb_strtolower($name[0], 'UTF-8'); |
686
|
|
|
} |
687
|
|
|
|
688
|
|
|
// now test names |
689
|
|
|
$results = array_intersect($namesToTest, $namesList); |
690
|
|
|
if (count($results) !== 0) { |
691
|
|
|
return 1; |
692
|
|
|
} |
693
|
|
|
return 0; |
694
|
|
|
} |
695
|
|
|
} |
The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g.
excluded_paths: ["lib/*"]
, you can move it to the dependency path list as follows:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths