forked from GOCDB/gocdb
/
LinkIdentity.php
417 lines (343 loc) · 17.5 KB
/
LinkIdentity.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
<?php
namespace org\gocdb\services;
require_once __DIR__ . '/AbstractEntityService.php';
class LinkIdentity extends AbstractEntityService {
/**
* Processes an identity link request
* @param string $primaryIdString ID string of primary user
* @param string $currentIdString ID string of current user
* @param string $primaryAuthType auth type of primary ID string
* @param string $currentAuthType auth type of current ID string
* @param string $givenEmail email of primary user
*/
public function newLinkIdentityRequest($primaryIdString, $currentIdString, $primaryAuthType, $currentAuthType, $givenEmail) {
$serv = \Factory::getUserService();
// $primaryUser is user who will have ID string updated/added
// Ideally, ID string and auth type match a user identifier
$primaryUser = $serv->getUserByPrincipleAndType($primaryIdString, $primaryAuthType);
if ($primaryUser === null) {
// If no valid user identifiers, check certificateDNs
$primaryUser = $serv->getUserByCertificateDn($primaryIdString);
}
// $currentUser is user making request
// May not be registered so can be null
$currentUser = $serv->getUserByPrinciple($currentIdString);
// Recovery or identity linking
if ($primaryAuthType === $currentAuthType) {
$isLinking = false;
} else {
$isLinking = true;
}
// Validate details. For most errors, return without throwing an error to avoid sharing info
if ($this->validate($primaryUser, $currentUser, $currentAuthType, $isLinking, $givenEmail) === 1) {
return;
}
// Remove any existing requests involving either user
$this->removeRelatedRequests($primaryUser, $currentUser, $primaryIdString, $currentIdString);
// Generate confirmation code
$code = $this->generateConfirmationCode($primaryIdString);
// Create link identity request
$linkIdentityReq = new \LinkIdentityRequest($primaryUser, $currentUser, $code, $primaryIdString, $currentIdString, $primaryAuthType, $currentAuthType);
// Recovery or identity linking
if ($currentUser === null) {
$isRegistered = false;
} else {
$isRegistered = true;
}
// Apply change
try {
$this->em->getConnection()->beginTransaction();
$this->em->persist($linkIdentityReq);
$this->em->flush();
// Send confirmation email(s) to primary user, and current user if registered with a different email
// (before commit - if it fails we'll need a rollback)
if (\Factory::getConfigService()->getSendEmails()) {
$this->sendConfirmationEmails($primaryUser, $currentUser, $code, $primaryIdString, $currentIdString, $primaryAuthType, $currentAuthType, $isLinking, $isRegistered);
}
$this->em->getConnection()->commit();
} catch(\Exception $e) {
$this->em->getConnection()->rollback();
$this->em->close();
throw $e;
}
}
/**
* Performs validation on request
* @param \User $primaryUser user who will have identifier added/updated
* @param \User $currentUser user creating the request
* @param string $currentAuthType auth type of current ID string
* @param bool $isLinking true if linking, false if recovering
* @param string $givenEmail email of primary user
*/
private function validate($primaryUser, $currentUser, $currentAuthType, $isLinking, $givenEmail) {
if ($primaryUser === null) {
// Don't throw exception to limit info shared
return 1;
}
if ($primaryUser === $currentUser) {
// Can throw exception as it's their own ID string
throw new \Exception("The details entered are already associated with this account");
}
// Check the portal is not in read only mode, throws exception if it is
// If portal is read only, but the current user is an admin, we will still be able to proceed
$this->checkPortalIsNotReadOnlyOrUserIsAdmin($currentUser);
// Check the given email address matches the one given
if (strcasecmp($primaryUser->getEmail(), $givenEmail)) {
// Don't throw exception to limit info shared
return 1;
}
// Prevent attempt to add duplicate auth type when linking
if ($isLinking) {
foreach ($primaryUser->getUserIdentifiers() as $identifier) {
if ($identifier->getKeyName() === $currentAuthType) {
return 1;
}
}
}
return 0;
}
/**
* Removes any existing requests which involve either user
* @param \User $primaryUser user who will have identifier added/updated
* @param \User $currentUser user creating the request
* @param string $primaryIdString ID string of primary user
* @param string $currentIdString ID string of current user
*/
private function removeRelatedRequests($primaryUser, $currentUser, $primaryIdString, $currentIdString) {
// Set up list for previous requests matching various criteria
$previousRequests = [];
// Matching the primary user
$previousRequests[] = $this->getRequestByUserId($primaryUser->getId());
// Matching the primary user's ID string - unlikely to exist but not impossible
$previousRequests[] = $this->getRequestByIdString($primaryIdString);
// Matching the current user, if registered
if ($currentUser !== null) {
$previousRequests[] = $this->getRequestByUserId($currentUser->getId());
}
// Matching the current ID string
$previousRequests[] = $this->getRequestByIdString($currentIdString);
// Remove any requests found
foreach ($previousRequests as $previousRequest) {
if (!is_null($previousRequest)) {
try{
$this->em->getConnection()->beginTransaction();
$this->em->remove($previousRequest);
$this->em->flush();
$this->em->getConnection()->commit();
} catch(\Exception $e) {
$this->em->getConnection()->rollback();
$this->em->close();
throw $e;
}
}
}
}
/**
* Generates a confirmation code
* @param string $idString ID string used to generated code
*/
private function generateConfirmationCode($idString) {
$confirmCode = rand(1, 10000000);
$confirmHash = sha1($idString.$confirmCode);
return $confirmHash;
}
/**
* Gets a link identity request from the database based on user ID
* @param integer $userId userid of the request to be linked
* @return arraycollection
*/
private function getRequestByUserId($userId) {
$dql = "SELECT l
FROM LinkIdentityRequest l
JOIN l.primaryUser pu
JOIN l.currentUser cu
WHERE pu.id = :id OR cu.id = :id";
$request = $this->em
->createQuery($dql)
->setParameter('id', $userId)
->getOneOrNullResult();
return $request;
}
/**
* Gets a link identity request from the database based on current ID string
* ID string may be present as primary or current user
* @param string $idString ID string of user to be linked in primary account
* @return arraycollection
*/
private function getRequestByIdString($idString) {
$dql = "SELECT l
FROM LinkIdentityRequest l
WHERE l.primaryIdString = :idString
OR l.currentIdString = :idString";
$request = $this->em
->createQuery($dql)
->setParameter('idString', $idString)
->getOneOrNullResult();
return $request;
}
/**
* Gets an identity link request from the database based on the confirmation code
* @param string $code confirmation code of the request being retrieved
* @return arraycollection
*/
public function getRequestByConfirmationCode($code) {
$dql = "SELECT l
FROM LinkIdentityRequest l
WHERE l.confirmCode = :code";
$request = $this->em
->createQuery($dql)
->setParameter('code', $code)
->getOneOrNullResult();
return $request;
}
/**
* Composes confimation email to be sent to the user
* @param string $primaryIdString ID string of primary user
* @param string $currentIdString ID string of current user
* @param string $primaryAuthType auth type of primary ID string
* @param string $currentAuthType auth type of current ID string
* @param bool $isLinking true if linking, false if recovering
* @param bool $isRegistered true if current user is registered
* @param bool $isPrimary true if composing for primary user
* @param string $link to be clicked (only sent to primary user email)
* @return arraycollection
*/
private function composeEmail($primaryIdString, $currentIdString, $primaryAuthType, $currentAuthType, $isLinking, $isRegistered, $isPrimary, $link=null) {
$subject = "Validation of " . ($isLinking ? "linking" : "recovering") . " your GOCDB account";
$body = "Dear GOCDB User,"
. "\n\nA request to " . ($isLinking ? "associate a new identifier with" : "update an identifier associated with")
. ($isRegistered ? " one of your accounts" : " your account") . " has just been made on GOCDB."
. " The details of this request are:"
. "\n\n" . ($isLinking ? "Identifier" : "Current identifier") . " of GOCDB account being " . ($isLinking ? "linked" : "recovered") . ":"
. "\n\n - Authentication type: $primaryAuthType"
. "\n - ID string: $primaryIdString"
. "\n\nRequested new identifier " . ($isLinking ? "to be added to your account" : "with updated ID string") . ":"
. "\n\n - Authentication type: $currentAuthType"
. "\n - ID string: $currentIdString";
if ($isRegistered) {
$body .= "\n\nThis new identifier is currently associated with a second registered account."
. " If " . ($isLinking ? "identity linking" : "account recovery") . " is successful, any roles currently associated with this second account ($currentIdString)"
. " will be requested for your primary GOCDB account ($primaryIdString)."
. " These roles will be approved automatically if either account has permission to do so."
. "\n\nYour second account will then be deleted.";
}
if (!$isLinking) {
$body .= "\n\nPlease note that you will no longer be able to access your account using your old ID string ($primaryIdString).";
}
if ($isPrimary) {
$body .= "\n\nIf you wish to associate your GOCDB account with this new identifier, please validate your request by clicking on the link below"
. " while authenticated with the new identifier:"
. "\n\n$link";
}
$body .= "\n\nIf you did not create this request, please immediately contact gocdb-admins@mailman.egi.eu";
return array('subject'=>$subject, 'body'=>$body);
}
/**
* Send confirmation email(s) to primary user, and current user if registered with a different email
* @param \User $primaryUser user who will have identifier added/updated
* @param \User $currentUser user creating the request
* @param string $code confirmation code of the request being retrieved
* @param string $primaryIdString ID string of primary user
* @param string $currentIdString ID string of current user
* @param string $primaryAuthType auth type of primary ID string
* @param string $currentAuthType auth type of current ID string
* @param bool $isLinking true if linking, false if recovering
* @param bool $isRegistered true if current user is registered
*/
private function sendConfirmationEmails($primaryUser, $currentUser, $code, $primaryIdString, $currentIdString, $primaryAuthType, $currentAuthType, $isLinking, $isRegistered) {
// Create link to be clicked in email
$portalUrl = \Factory::getConfigService()->GetPortalURL();
$link = $portalUrl."/index.php?Page_Type=User_Validate_Identity_Link&c=" . $code;
// Compose email to send to primary user
$isPrimary = true;
$composedPrimaryEmail = $this->composeEmail($primaryIdString, $currentIdString, $primaryAuthType, $currentAuthType, $isLinking, $isRegistered, $isPrimary, $link);
$primarySubject = $composedPrimaryEmail['subject'];
$primaryBody = $composedPrimaryEmail['body'];
// If "sendmail_from" is set in php.ini, use second line ($headers = '';):
$headers = "From: GOCDB <gocdb-admins@mailman.egi.eu>";
// Mail command returns boolean. False if message not accepted for delivery.
if (!mail($primaryUser->getEmail(), $primarySubject, $primaryBody, $headers)) {
throw new \Exception("Unable to send email message");
}
// Send confirmation email to current user, if registered with different email to primary user
if ($isRegistered) {
if ($currentUser->getEmail() !== $primaryUser->getEmail()) {
// Compose email to send to current user
$isPrimary = false;
$composedCurrentEmail = $this->composeEmail($primaryIdString, $currentIdString, $primaryAuthType, $currentAuthType, $isLinking, $isRegistered, $isPrimary);
$currentSubject = $composedCurrentEmail['subject'];
$currentBody = $composedCurrentEmail['body'];
// Mail command returns boolean. False if message not accepted for delivery.
if (!mail($currentUser->getEmail(), $currentSubject, $currentBody, $headers)) {
throw new \Exception("Unable to send email message");
}
}
}
}
/**
* Confirm and execute linking or recovery request
* @param string $code confirmation code of the request being retrieved
* @param string $currentIdString ID string of current user
*/
public function confirmIdentityLinking($code, $currentIdString) {
$serv = \Factory::getUserService();
// Get the request
$request = $this->getRequestByConfirmationCode($code);
$invalidURL = "Confirmation URL invalid."
. " If you have submitted multiple requests for the same account, please ensure you have used the link in the most recent email."
. " Please also ensure you are authenticated in the same way as when you made the request.";
// Check there is a result
if (is_null($request)) {
throw new \Exception($invalidURL);
}
$primaryUser = $request->getPrimaryUser();
$currentUser = $request->getCurrentUser();
// Check the portal is not in read only mode, throws exception if it is.
// If portal is read only, but the primary user is an admin, we will still be able to proceed.
$this->checkPortalIsNotReadOnlyOrUserIsAdmin($primaryUser);
// Check the ID string currently being used by the user is same as in the request
if ($currentIdString !== $request->getCurrentIdString()) {
throw new \Exception($invalidURL);
}
// Create identifier array from the current user's credentials
$identifierArr = array($request->getCurrentAuthType(), $request->getCurrentIdString());
// Are we recovering or linking an identity? True if linking
$isLinking = ($request->getPrimaryAuthType() !== $request->getCurrentAuthType());
// If primary user does not have user identifiers, add using the request info
$isOldUser = ($primaryUser->getCertificateDn() === $request->getPrimaryIdString());
if ($isOldUser) {
$identifierArrOld = array($request->getPrimaryAuthType(), $request->getPrimaryIdString());
}
// Update primary user, remove request (and current user)
try {
$this->em->getConnection()->beginTransaction();
// Add certificateDn as identifier if necessary
if ($isOldUser) {
$serv->migrateUserCredentials($primaryUser, $identifierArrOld, $primaryUser);
}
// Delete request before user so references still exist
$this->em->remove($request);
// Merge roles and remove current user so their identifier is free to be added
if ($currentUser !== null) {
\Factory::getRoleService()->mergeRoles($primaryUser, $currentUser);
$serv->deleteUser($currentUser, $currentUser);
}
$this->em->flush();
// Add or update to current identifier
if ($isLinking) {
$serv->addUserIdentifier($primaryUser, $identifierArr, $primaryUser);
} else {
$identifier = $serv->getIdentifierByIdString($request->getPrimaryIdString());
$serv->editUserIdentifier($primaryUser, $identifier, $identifierArr, $primaryUser);
}
$this->em->remove($request);
$this->em->flush();
$this->em->getConnection()->commit();
} catch(\Exception $e) {
$this->em->getConnection()->rollback();
$this->em->close();
throw $e;
}
return $request;
}
}