forked from DSpace/DSpace
-
Notifications
You must be signed in to change notification settings - Fork 73
Expand file tree
/
Copy pathAccountServiceImpl.java
More file actions
600 lines (524 loc) · 23.1 KB
/
AccountServiceImpl.java
File metadata and controls
600 lines (524 loc) · 23.1 KB
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
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.eperson;
import java.io.IOException;
import java.sql.SQLException;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Stream;
import jakarta.mail.MessagingException;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.authenticate.service.AuthenticationService;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.content.service.MetadataValueService;
import org.dspace.core.Context;
import org.dspace.core.Email;
import org.dspace.core.I18nUtil;
import org.dspace.core.Utils;
import org.dspace.eperson.dto.RegistrationDataPatch;
import org.dspace.eperson.service.AccountService;
import org.dspace.eperson.service.EPersonService;
import org.dspace.eperson.service.GroupService;
import org.dspace.eperson.service.RegistrationDataService;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.log.LogMessage;
/**
* Methods for handling registration by email and forgotten passwords. When
* someone registers as a user, or forgets their password, the
* sendRegistrationInfo or sendForgotPasswordInfo methods can be used to send an
* email to the user. The email contains a special token, a long string which is
* randomly generated and thus hard to guess. When the user presents the token
* back to the system, the AccountManager can use the token to determine the
* identity of the eperson.
*
* *NEW* now ignores expiration dates so that tokens never expire
*
* @author Peter Breton
* @version $Revision$
*/
public class AccountServiceImpl implements AccountService {
/**
* log4j log
*/
private static final Logger log = LogManager.getLogger(AccountServiceImpl.class);
private static final Map<String, BiConsumer<RegistrationData, EPerson>> allowedMergeArguments =
Map.of(
"email",
(RegistrationData registrationData, EPerson eperson) -> eperson.setEmail(registrationData.getEmail())
);
@Autowired(required = true)
protected EPersonService ePersonService;
@Autowired(required = true)
protected RegistrationDataService registrationDataService;
@Autowired
private ConfigurationService configurationService;
@Autowired
private GroupService groupService;
@Autowired
private AuthenticationService authenticationService;
@Autowired
private MetadataValueService metadataValueService;
protected AccountServiceImpl() {
}
/**
* Email registration info to the given email address.
* Potential error conditions:
* <ul>
* <li>Cannot create registration data in database (throws SQLException).</li>
* <li>Error sending email (throws MessagingException).</li>
* <li>Error reading email template (throws IOException).</li>
* <li>Authorization error (throws AuthorizeException).</li>
* </ul>
*
* @param context DSpace context
* @param email Email address to send the registration email to
* @throws java.sql.SQLException passed through.
* @throws java.io.IOException passed through.
* @throws jakarta.mail.MessagingException passed through.
* @throws org.dspace.authorize.AuthorizeException passed through.
*/
@Override
public void sendRegistrationInfo(Context context, String email, List<UUID> groups)
throws SQLException, IOException, MessagingException,
AuthorizeException {
if (!configurationService.getBooleanProperty("user.registration", true)) {
throw new IllegalStateException("The user.registration parameter was set to false");
}
if (!authenticationService.canSelfRegister(context, null, email)) {
throw new IllegalStateException("self registration is not allowed with this email address");
}
sendInfo(context, email, groups, RegistrationTypeEnum.REGISTER, true);
}
/**
* Email forgot password info to the given email address.
* Potential error conditions:
* <ul>
* <li>No EPerson with that email (returns null).</li>
* <li>Cannot create registration data in database (throws SQLException).</li>
* <li>Error sending email (throws MessagingException).</li>
* <li>Error reading email template (throws IOException).</li>
* <li>Authorization error (throws AuthorizeException).</li>
* </ul>
*
* @param context DSpace context
* @param email Email address to send the forgot-password email to
* @throws java.sql.SQLException passed through.
* @throws java.io.IOException passed through.
* @throws jakarta.mail.MessagingException passed through.
* @throws org.dspace.authorize.AuthorizeException passed through.
*/
@Override
public void sendForgotPasswordInfo(Context context, String email, List<UUID> groups)
throws SQLException, IOException, MessagingException, AuthorizeException {
sendInfo(context, email, groups, RegistrationTypeEnum.FORGOT, true);
}
/**
* Checks if exists an account related to the token provided
*
* @param context DSpace context
* @param token Account token
* @return true if exists, false otherwise
* @throws SQLException
* @throws AuthorizeException
*/
@Override
public boolean existsAccountFor(Context context, String token) throws SQLException, AuthorizeException {
return getEPerson(context, token) != null;
}
@Override
public boolean existsAccountWithEmail(Context context, String email) throws SQLException {
return ePersonService.findByEmail(context, email) != null;
}
/**
* <p>
* Return the EPerson corresponding to token, where token was emailed to the
* person by either the sendRegistrationInfo or sendForgotPasswordInfo
* methods.
* </p>
*
* <p>
* If the token is not found return null.
* </p>
*
* @param context DSpace context
* @param token Account token
* @return The EPerson corresponding to token, or null.
* @throws SQLException If the token or eperson cannot be retrieved from the
* database.
* @throws AuthorizeException passed through.
*/
@Override
public EPerson getEPerson(Context context, String token)
throws SQLException, AuthorizeException {
String email = getEmail(context, token);
if (email == null) {
return null;
}
return ePersonService.findByEmail(context, email);
}
/**
* Return the e-mail address referred to by a token, or null if email
* address can't be found ignores expiration of token
*
* @param context DSpace context
* @param token Account token
* @return The email address corresponding to token, or null.
* @throws java.sql.SQLException passed through.
*/
@Override
public String getEmail(Context context, String token)
throws SQLException {
RegistrationData registrationData = registrationDataService.findByToken(context, token);
if (registrationData == null) {
return null;
}
/*
* ignore the expiration date on tokens Date expires =
* rd.getDateColumn("expires"); if (expires != null) { if ((new
* java.util.Date()).after(expires)) return null; }
*/
return registrationData.getEmail();
}
/**
* Delete token.
*
* @param context DSpace context
* @param token The token to delete
* @throws SQLException If a database error occurs
*/
@Override
public void deleteToken(Context context, String token)
throws SQLException {
registrationDataService.deleteByToken(context, token);
}
public EPerson mergeRegistration(Context context, UUID personId, String token, List<String> overrides)
throws AuthorizeException, SQLException {
RegistrationData registrationData = getRegistrationData(context, token);
EPerson eperson = null;
if (personId != null) {
eperson = ePersonService.findByIdOrLegacyId(context, personId.toString());
}
if (!canCreateUserBy(context, registrationData.getRegistrationType())) {
throw new AuthorizeException("Token type invalid for the current user.");
}
if (hasLoggedEPerson(context) && !isSameContextEPerson(context, eperson)) {
throw new AuthorizeException("Only the user with id: " + personId + " can make this action.");
}
context.turnOffAuthorisationSystem();
eperson = Optional.ofNullable(eperson).orElseGet(() -> createEPerson(context, registrationData));
updateValuesFromRegistration(context, eperson, registrationData, overrides);
addEPersonToGroups(context, eperson, registrationData.getGroups());
deleteToken(context, token);
ePersonService.update(context, eperson);
context.commit();
context.restoreAuthSystemState();
return eperson;
}
private EPerson createEPerson(Context context, RegistrationData registrationData) {
EPerson eperson;
try {
eperson = ePersonService.create(context);
eperson.setNetid(registrationData.getNetId());
eperson.setEmail(registrationData.getEmail());
RegistrationDataMetadata firstName =
registrationDataService.getMetadataByMetadataString(
registrationData,
"eperson.firstname"
);
if (firstName != null) {
eperson.setFirstName(context, firstName.getValue());
}
RegistrationDataMetadata lastName =
registrationDataService.getMetadataByMetadataString(
registrationData,
"eperson.lastname"
);
if (lastName != null) {
eperson.setLastName(context, lastName.getValue());
}
eperson.setCanLogIn(true);
eperson.setSelfRegistered(true);
} catch (SQLException | AuthorizeException e) {
throw new RuntimeException(
"Cannote create the eperson linked to the token: " + registrationData.getToken(),
e
);
}
return eperson;
}
private boolean hasLoggedEPerson(Context context) {
return context.getCurrentUser() != null;
}
private boolean isSameContextEPerson(Context context, EPerson eperson) {
return eperson.equals(context.getCurrentUser());
}
@Override
public RegistrationData renewRegistrationForEmail(
Context context, RegistrationDataPatch registrationDataPatch
) throws AuthorizeException {
try {
RegistrationData newRegistration = registrationDataService.clone(context, registrationDataPatch);
registrationDataService.delete(context, registrationDataPatch.getOldRegistration());
fillAndSendEmail(context, newRegistration);
return newRegistration;
} catch (SQLException | MessagingException | IOException e) {
log.error(e);
throw new RuntimeException(e);
}
}
private boolean isEmailConfirmed(RegistrationData oldRegistration, String email) {
return email.equals(oldRegistration.getEmail());
}
@Override
public boolean isTokenValidForCreation(RegistrationData registrationData) {
return (
isExternalRegistrationToken(registrationData.getRegistrationType()) ||
isValidationToken(registrationData.getRegistrationType())
) &&
StringUtils.isNotBlank(registrationData.getNetId());
}
private boolean canCreateUserBy(Context context, RegistrationTypeEnum registrationTypeEnum) {
return isValidationToken(registrationTypeEnum) ||
canCreateUserFromExternalRegistrationToken(context, registrationTypeEnum);
}
private static boolean canCreateUserFromExternalRegistrationToken(
Context context, RegistrationTypeEnum registrationTypeEnum
) {
return context.getCurrentUser() != null && isExternalRegistrationToken(registrationTypeEnum);
}
private static boolean isExternalRegistrationToken(RegistrationTypeEnum registrationTypeEnum) {
return RegistrationTypeEnum.ORCID.equals(registrationTypeEnum);
}
private static boolean isValidationToken(RegistrationTypeEnum registrationTypeEnum) {
return RegistrationTypeEnum.VALIDATION_ORCID.equals(registrationTypeEnum);
}
protected void updateValuesFromRegistration(
Context context, EPerson eperson, RegistrationData registrationData, List<String> overrides
) {
Stream.concat(
getMergeActions(registrationData, overrides),
getUpdateActions(context, eperson, registrationData)
).forEach(c -> c.accept(eperson));
}
private Stream<Consumer<EPerson>> getMergeActions(RegistrationData registrationData, List<String> overrides) {
if (overrides == null || overrides.isEmpty()) {
return Stream.empty();
}
return overrides.stream().map(f -> mergeField(f, registrationData));
}
protected Stream<Consumer<EPerson>> getUpdateActions(
Context context, EPerson eperson, RegistrationData registrationData
) {
Stream.Builder<Consumer<EPerson>> actions = Stream.builder();
if (eperson.getNetid() == null) {
actions.add(p -> p.setNetid(registrationData.getNetId()));
}
if (eperson.getEmail() == null) {
actions.add(p -> p.setEmail(registrationData.getEmail()));
}
for (RegistrationDataMetadata metadatum : registrationData.getMetadata()) {
Optional<List<MetadataValue>> epersonMetadata =
Optional.ofNullable(
ePersonService.getMetadataByMetadataString(
eperson, metadatum.getMetadataField().toString('.')
)
).filter(l -> !l.isEmpty());
if (epersonMetadata.isEmpty()) {
actions.add(p -> addMetadataValue(context, metadatum, p));
}
}
return actions.build();
}
private List<MetadataValue> addMetadataValue(Context context, RegistrationDataMetadata metadatum, EPerson p) {
try {
return ePersonService.addMetadata(
context, p, metadatum.getMetadataField(), Item.ANY, List.of(metadatum.getValue())
);
} catch (SQLException e) {
throw new RuntimeException(
"Could not add metadata" + metadatum.getMetadataField() + " to eperson with uuid: " + p.getID(), e);
}
}
protected Consumer<EPerson> mergeField(String field, RegistrationData registrationData) {
return person ->
allowedMergeArguments.getOrDefault(
field,
mergeRegistrationMetadata(field)
).accept(registrationData, person);
}
protected BiConsumer<RegistrationData, EPerson> mergeRegistrationMetadata(String field) {
return (registrationData, person) -> {
RegistrationDataMetadata registrationMetadata = getMetadataOrThrow(registrationData, field);
MetadataValue metadata = getMetadataOrThrow(person, field);
metadata.setValue(registrationMetadata.getValue());
ePersonService.setMetadataModified(person);
};
}
private RegistrationDataMetadata getMetadataOrThrow(RegistrationData registrationData, String field) {
return registrationDataService.getMetadataByMetadataString(registrationData, field);
}
private MetadataValue getMetadataOrThrow(EPerson eperson, String field) {
return ePersonService.getMetadataByMetadataString(eperson, field).stream().findFirst()
.orElseThrow(
() -> new IllegalArgumentException(
"Could not find the metadata field: " + field + " for eperson: " + eperson.getID())
);
}
protected void addEPersonToGroups(Context context, EPerson eperson, List<Group> groups) {
if (CollectionUtils.isEmpty(groups)) {
return;
}
for (Group group : groups) {
groupService.addMember(context, group, eperson);
}
}
private RegistrationData getRegistrationData(Context context, String token)
throws SQLException, AuthorizeException {
return Optional.ofNullable(registrationDataService.findByToken(context, token))
.filter(rd ->
isValid(rd) ||
!isValidationToken(rd.getRegistrationType())
)
.orElseThrow(
() -> new AuthorizeException(
"The registration token: " + token + " is not valid!"
)
);
}
private boolean isValid(RegistrationData rd) {
return registrationDataService.isValid(rd);
}
/**
* THIS IS AN INTERNAL METHOD. THE SEND PARAMETER ALLOWS IT TO BE USED FOR
* TESTING PURPOSES.
*
* Send an info to the EPerson with the given email address. If isRegister
* is TRUE, this is registration email; otherwise, it is forgot-password
* email. If send is TRUE, the email is sent; otherwise it is skipped.
*
* Potential error conditions:
*
* @param context DSpace context
* @param email Email address to send the forgot-password email to
* @param type Type of registration {@link RegistrationTypeEnum}
* @param send If true, send email; otherwise do not send any email
* @return null if no EPerson with that email found
* @throws SQLException Cannot create registration data in database
* @throws MessagingException Error sending email
* @throws IOException Error reading email template
* @throws AuthorizeException Authorization error
*/
protected RegistrationData sendInfo(
Context context, String email, List<UUID> groups, RegistrationTypeEnum type, boolean send
) throws SQLException, IOException, MessagingException, AuthorizeException {
// See if a registration token already exists for this user
RegistrationData rd = registrationDataService.findBy(context, email, type);
boolean isRegister = RegistrationTypeEnum.REGISTER.equals(type);
// If it already exists, just re-issue it
if (rd == null) {
rd = registrationDataService.create(context);
rd.setRegistrationType(type);
rd.setToken(Utils.generateHexKey());
// don't set expiration date any more
// rd.setColumn("expires", getDefaultExpirationDate());
rd.setEmail(email);
registrationDataService.update(context, rd);
// This is a potential problem -- if we create the callback
// and then crash, registration will get SNAFU-ed.
// So FIRST leave some breadcrumbs
if (log.isDebugEnabled()) {
log.debug("Created callback "
+ rd.getID()
+ " with token " + rd.getToken()
+ " with email \"" + email + "\"");
}
}
if (CollectionUtils.isNotEmpty(groups)) {
for (UUID groupUuid : groups) {
Group group = groupService.find(context, groupUuid);
if (Objects.nonNull(group)) {
rd.addGroup(group);
}
}
}
if (send) {
fillAndSendEmail(context, email, isRegister, rd);
}
return rd;
}
/**
* Send a DSpace message to the given email address.
*
* If isRegister is <code>true</code>, this is registration email;
* otherwise, it is a forgot-password email.
*
* @param context The relevant DSpace Context.
* @param email The email address to mail to
* @param isRegister If true, this is registration email; otherwise it is
* forgot-password email.
* @param rd The RDBMS row representing the registration data.
* @throws MessagingException If an error occurs while sending email
* @throws IOException A general class of exceptions produced by failed or interrupted I/O operations.
* @throws SQLException An exception that provides information on a database access error or other errors.
*/
protected void fillAndSendEmail(Context context, String email, boolean isRegister, RegistrationData rd)
throws MessagingException, IOException, SQLException {
String base = configurationService.getProperty("dspace.ui.url");
// Note change from "key=" to "token="
String specialLink = getSpecialLink(
base, rd, isRegister ? "register" : ((rd.getGroups().size() == 0) ? "forgot" : "invitation")
);
Locale locale = context.getCurrentLocale();
String emailFilename = I18nUtil.getEmailFilename(locale, isRegister ? "register" : "change_password");
fillAndSendEmail(email, emailFilename, specialLink);
// Breadcrumbs
if (log.isInfoEnabled()) {
log.info("Sent " + (isRegister ? "registration" : "account")
+ " information to " + email);
}
}
private static String getSpecialLink(String base, RegistrationData rd, String subPath) {
return new StringBuffer(base)
.append(base.endsWith("/") ? "" : "/")
.append(subPath)
.append("/")
.append(rd.getToken())
.toString();
}
protected void fillAndSendEmail(
Context context, RegistrationData rd
) throws MessagingException, IOException {
String base = configurationService.getProperty("dspace.ui.url");
// Note change from "key=" to "token="
String specialLink = getSpecialLink(base, rd, rd.getRegistrationType().getLink());
String emailFilename = I18nUtil.getEmailFilename(
context.getCurrentLocale(), rd.getRegistrationType().toString().toLowerCase()
);
fillAndSendEmail(rd.getEmail(), emailFilename, specialLink);
log.info(LogMessage.of(() -> "Sent " + rd.getRegistrationType().getLink() + " link to " + rd.getEmail()));
}
protected void fillAndSendEmail(String email, String emailFilename, String specialLink)
throws IOException, MessagingException {
Email bean = Email.getEmail(emailFilename);
bean.addRecipient(email);
bean.addArgument(specialLink);
bean.send();
}
}