Merge pull request #9849 from 4Science/task/main/CST-15074

ORCID Login flow for private emails
This commit is contained in:
Tim Donohue
2025-03-26 14:53:54 -05:00
committed by GitHub
50 changed files with 4129 additions and 240 deletions

View File

@@ -19,6 +19,7 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -457,7 +458,7 @@ public class DSpaceCSV implements Serializable {
List<Collection> collections = i.getCollections();
for (Collection c : collections) {
// Only add if it is not the owning collection
if (!c.getHandle().equals(owningCollectionHandle)) {
if (!Objects.equals(c.getHandle(), owningCollectionHandle)) {
line.add("collection", c.getHandle());
}
}

View File

@@ -29,7 +29,10 @@ import org.dspace.authorize.AuthorizeException;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
import org.dspace.eperson.RegistrationData;
import org.dspace.eperson.RegistrationTypeEnum;
import org.dspace.eperson.service.EPersonService;
import org.dspace.eperson.service.RegistrationDataService;
import org.dspace.orcid.OrcidToken;
import org.dspace.orcid.client.OrcidClient;
import org.dspace.orcid.client.OrcidConfiguration;
@@ -47,11 +50,15 @@ import org.springframework.beans.factory.annotation.Autowired;
* ORCID authentication for DSpace.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class OrcidAuthenticationBean implements AuthenticationMethod {
public static final String ORCID_DEFAULT_FIRSTNAME = "Unnamed";
public static final String ORCID_DEFAULT_LASTNAME = ORCID_DEFAULT_FIRSTNAME;
public static final String ORCID_AUTH_ATTRIBUTE = "orcid-authentication";
public static final String ORCID_REGISTRATION_TOKEN = "orcid-registration-token";
public static final String ORCID_DEFAULT_REGISTRATION_URL = "/external-login/{0}";
private final static Logger LOGGER = LogManager.getLogger();
@@ -78,6 +85,9 @@ public class OrcidAuthenticationBean implements AuthenticationMethod {
@Autowired
private OrcidTokenService orcidTokenService;
@Autowired
private RegistrationDataService registrationDataService;
@Override
public int authenticate(Context context, String username, String password, String realm, HttpServletRequest request)
throws SQLException {
@@ -183,7 +193,7 @@ public class OrcidAuthenticationBean implements AuthenticationMethod {
return ePerson.canLogIn() ? logInEPerson(context, token, ePerson) : BAD_ARGS;
}
return canSelfRegister() ? registerNewEPerson(context, person, token) : NO_SUCH_USER;
return canSelfRegister() ? createRegistrationData(context, request, person, token) : NO_SUCH_USER;
}
@@ -211,48 +221,59 @@ public class OrcidAuthenticationBean implements AuthenticationMethod {
}
}
private int registerNewEPerson(Context context, Person person, OrcidTokenResponseDTO token) throws SQLException {
private int createRegistrationData(
Context context, HttpServletRequest request, Person person, OrcidTokenResponseDTO token
) throws SQLException {
try {
context.turnOffAuthorisationSystem();
String email = getEmail(person)
.orElseThrow(() -> new IllegalStateException("The email is configured private on orcid"));
RegistrationData registrationData =
this.registrationDataService.create(context, token.getOrcid(), RegistrationTypeEnum.ORCID);
String orcid = token.getOrcid();
registrationData.setEmail(getEmail(person).orElse(null));
setOrcidMetadataOnRegistration(context, registrationData, person, token);
EPerson eperson = ePersonService.create(context);
registrationDataService.update(context, registrationData);
eperson.setNetid(orcid);
eperson.setEmail(email);
Optional<String> firstName = getFirstName(person);
if (firstName.isPresent()) {
eperson.setFirstName(context, firstName.get());
}
Optional<String> lastName = getLastName(person);
if (lastName.isPresent()) {
eperson.setLastName(context, lastName.get());
}
eperson.setCanLogIn(true);
eperson.setSelfRegistered(true);
setOrcidMetadataOnEPerson(context, eperson, token);
ePersonService.update(context, eperson);
context.setCurrentUser(eperson);
request.setAttribute(ORCID_REGISTRATION_TOKEN, registrationData.getToken());
context.commit();
context.dispatchEvents();
return SUCCESS;
} catch (Exception ex) {
LOGGER.error("An error occurs registering a new EPerson from ORCID", ex);
context.rollback();
return NO_SUCH_USER;
} finally {
context.restoreAuthSystemState();
return NO_SUCH_USER;
}
}
private void setOrcidMetadataOnRegistration(
Context context, RegistrationData registration, Person person, OrcidTokenResponseDTO token
) throws SQLException, AuthorizeException {
String orcid = token.getOrcid();
setRegistrationMetadata(context, registration, "eperson.firstname", getFirstName(person));
setRegistrationMetadata(context, registration, "eperson.lastname", getLastName(person));
registrationDataService.setRegistrationMetadataValue(context, registration, "eperson", "orcid", null, orcid);
for (String scope : token.getScopeAsArray()) {
registrationDataService.addMetadata(context, registration, "eperson", "orcid", "scope", scope);
}
}
private void setRegistrationMetadata(
Context context, RegistrationData registration, String metadataString, String value) {
String[] split = metadataString.split("\\.");
String qualifier = split.length > 2 ? split[2] : null;
try {
registrationDataService.setRegistrationMetadataValue(
context, registration, split[0], split[1], qualifier, value
);
} catch (SQLException | AuthorizeException ex) {
LOGGER.error("An error occurs setting metadata", ex);
throw new RuntimeException(ex);
}
}
@@ -296,16 +317,20 @@ public class OrcidAuthenticationBean implements AuthenticationMethod {
return Optional.ofNullable(emails.get(0).getEmail());
}
private Optional<String> getFirstName(Person person) {
private String getFirstName(Person person) {
return Optional.ofNullable(person.getName())
.map(name -> name.getGivenNames())
.map(givenNames -> givenNames.getContent());
.map(name -> name.getGivenNames())
.map(givenNames -> givenNames.getContent())
.filter(StringUtils::isNotBlank)
.orElse(ORCID_DEFAULT_FIRSTNAME);
}
private Optional<String> getLastName(Person person) {
private String getLastName(Person person) {
return Optional.ofNullable(person.getName())
.map(name -> name.getFamilyName())
.map(givenNames -> givenNames.getContent());
.map(name -> name.getFamilyName())
.map(givenNames -> givenNames.getContent())
.filter(StringUtils::isNotBlank)
.orElse(ORCID_DEFAULT_LASTNAME);
}
private boolean canSelfRegister() {

View File

@@ -9,22 +9,36 @@ 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.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.lang.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
@@ -45,16 +59,30 @@ 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() {
}
@@ -86,7 +114,7 @@ public class AccountServiceImpl implements AccountService {
if (!authenticationService.canSelfRegister(context, null, email)) {
throw new IllegalStateException("self registration is not allowed with this email address");
}
sendInfo(context, email, true, true);
sendInfo(context, email, RegistrationTypeEnum.REGISTER, true);
}
/**
@@ -110,9 +138,27 @@ public class AccountServiceImpl implements AccountService {
*/
@Override
public void sendForgotPasswordInfo(Context context, String email)
throws SQLException, IOException, MessagingException,
AuthorizeException {
sendInfo(context, email, false, true);
throws SQLException, IOException, MessagingException, AuthorizeException {
sendInfo(context, email, 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;
}
/**
@@ -179,6 +225,271 @@ public class AccountServiceImpl implements AccountService {
registrationDataService.deleteByToken(context, token);
}
@Override
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);
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 context.getCurrentUser().equals(eperson);
}
@Override
public RegistrationData renewRegistrationForEmail(
Context context, RegistrationDataPatch registrationDataPatch
) throws AuthorizeException {
try {
RegistrationData newRegistration = registrationDataService.clone(context, registrationDataPatch);
registrationDataService.delete(context, registrationDataPatch.getOldRegistration());
sendRegistationLinkByEmail(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);
}
/**
* Updates Eperson using the provided {@link RegistrationData}.<br/>
* Tries to replace {@code metadata} already set inside the {@link EPerson} with the ones
* listed inside the {@code overrides} field by taking the value from the {@link RegistrationData}. <br/>
* Updates the empty values inside the {@link EPerson} by taking them directly from the {@link RegistrationData},
* according to the method {@link AccountServiceImpl#getUpdateActions(Context, EPerson, RegistrationData)}
*
* @param context The DSpace context
* @param eperson The EPerson that should be updated
* @param registrationData The RegistrationData related to that EPerson
* @param overrides List of metadata that will be overwritten inside the EPerson
*/
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));
}
/**
* This methods tries to fullfill missing values inside the {@link EPerson} by taking them directly from the
* {@link RegistrationData}. <br/>
* Returns a {@link Stream} of consumers that will be evaluated on an {@link EPerson}, this stream contains
* the following actions:
* <ul>
* <li>Copies {@code netId} and {@code email} to the {@link EPerson} <br/></li>
* <li>Copies any {@link RegistrationData#metadata} inside {@link EPerson#metadata} if isn't already set.</li>
* </ul>
*
* @param context DSpace context
* @param eperson EPerson that will be evaluated
* @param registrationData RegistrationData used as a base to copy value from.
* @return a stream of consumers to be evaluated on EPerson.
*/
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);
}
}
/**
* This method returns a Consumer that will override a given {@link MetadataValue} of the {@link EPerson} by taking
* that directly from the {@link RegistrationData}.
*
* @param field The metadatafield
* @param registrationData The RegistrationData where the metadata wil be taken
* @return a Consumer of the person that will replace that field
*/
protected Consumer<EPerson> mergeField(String field, RegistrationData registrationData) {
return person ->
allowedMergeArguments.getOrDefault(
field,
mergeRegistrationMetadata(field)
).accept(registrationData, person);
}
/**
* This method returns a {@link BiConsumer} that can be evaluated on any {@link RegistrationData} and
* {@link EPerson} in order to replace the value of the metadata {@code field} placed on the {@link EPerson}
* by taking the value directly from the {@link RegistrationData}.
*
* @param field The metadata that will be overwritten inside the {@link EPerson}
* @return a BiConsumer
*/
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())
);
}
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.
@@ -191,8 +502,7 @@ public class AccountServiceImpl implements AccountService {
*
* @param context DSpace context
* @param email Email address to send the forgot-password email to
* @param isRegister If true, this is for registration; otherwise, it is
* for forgot-password
* @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
@@ -200,16 +510,17 @@ public class AccountServiceImpl implements AccountService {
* @throws IOException Error reading email template
* @throws AuthorizeException Authorization error
*/
protected RegistrationData sendInfo(Context context, String email,
boolean isRegister, boolean send) throws SQLException, IOException,
MessagingException, AuthorizeException {
protected RegistrationData sendInfo(
Context context, String email, RegistrationTypeEnum type, boolean send
) throws SQLException, IOException, MessagingException, AuthorizeException {
// See if a registration token already exists for this user
RegistrationData rd = registrationDataService.findByEmail(context, email);
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
@@ -229,7 +540,7 @@ public class AccountServiceImpl implements AccountService {
}
if (send) {
sendEmail(context, email, isRegister, rd);
fillAndSendEmail(context, email, isRegister, rd);
}
return rd;
@@ -250,7 +561,7 @@ public class AccountServiceImpl implements AccountService {
* @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 sendEmail(Context context, String email, boolean isRegister, RegistrationData rd)
protected void fillAndSendEmail(Context context, String email, boolean isRegister, RegistrationData rd)
throws MessagingException, IOException, SQLException {
String base = configurationService.getProperty("dspace.ui.url");
@@ -261,11 +572,9 @@ public class AccountServiceImpl implements AccountService {
.append(rd.getToken())
.toString();
Locale locale = context.getCurrentLocale();
Email bean = Email.getEmail(I18nUtil.getEmailFilename(locale, isRegister ? "register"
: "change_password"));
bean.addRecipient(email);
bean.addArgument(specialLink);
bean.send();
String emailFilename = I18nUtil.getEmailFilename(locale, isRegister ? "register" : "change_password");
fillAndSendEmail(email, emailFilename, specialLink);
// Breadcrumbs
if (log.isInfoEnabled()) {
@@ -273,4 +582,64 @@ public class AccountServiceImpl implements AccountService {
+ " information to " + email);
}
}
/**
* This method returns a link that will point to the Angular UI that will be used by the user to complete the
* registration process.
*
* @param base is the UI url of DSpace
* @param rd is the RegistrationData related to the user
* @param subPath is the specific page that will be loaded on the FE
* @return String that represents that link
*/
private static String getSpecialLink(String base, RegistrationData rd, String subPath) {
return new StringBuffer(base)
.append(base.endsWith("/") ? "" : "/")
.append(subPath)
.append("/")
.append(rd.getToken())
.toString();
}
/**
* Fills out a given email template obtained starting from the {@link RegistrationTypeEnum}.
*
* @param context The DSpace Context
* @param rd The RegistrationData that will be used as a registration.
* @throws MessagingException
* @throws IOException
*/
protected void sendRegistationLinkByEmail(
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()));
}
/**
* This method fills out the given email with all the fields and sends out the email.
*
* @param email - The recipient
* @param emailFilename The name of the email
* @param specialLink - The link that will be set inside the email
* @throws IOException
* @throws MessagingException
*/
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();
}
}

View File

@@ -8,16 +8,24 @@
package org.dspace.eperson;
import java.time.Instant;
import java.util.SortedSet;
import java.util.TreeSet;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;
import org.dspace.core.Context;
import org.dspace.core.ReloadableEntity;
import org.hibernate.annotations.SortNatural;
/**
* Database entity representation of the registrationdata table
@@ -34,21 +42,65 @@ public class RegistrationData implements ReloadableEntity<Integer> {
@SequenceGenerator(name = "registrationdata_seq", sequenceName = "registrationdata_seq", allocationSize = 1)
private Integer id;
@Column(name = "email", unique = true, length = 64)
/**
* Contains the email used to register the user.
*/
@Column(name = "email", length = 64)
private String email;
/**
* Contains the unique id generated fot the user.
*/
@Column(name = "token", length = 48)
private String token;
/**
* Expiration date of this registration data.
*/
@Column(name = "expires")
private Instant expires;
/**
* Metadata linked to this registration data
*/
@SortNatural
@OneToMany(
fetch = FetchType.LAZY,
mappedBy = "registrationData",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private SortedSet<RegistrationDataMetadata> metadata = new TreeSet<>();
/**
* External service used to register the user.
* Allowed values are inside {@link RegistrationTypeEnum}
*/
@Column(name = "registration_type")
@Enumerated(EnumType.STRING)
private RegistrationTypeEnum registrationType;
/**
* Contains the external id provided by the external service
* accordingly to the registration type.
*/
@Column(name = "net_id", length = 64)
private final String netId;
/**
* Protected constructor, create object using:
* {@link org.dspace.eperson.service.RegistrationDataService#create(Context)}
*/
protected RegistrationData() {
this(null);
}
/**
* Protected constructor, create object using:
* {@link org.dspace.eperson.service.RegistrationDataService#create(Context, String)}
*/
protected RegistrationData(String netId) {
this.netId = netId;
}
public Integer getID() {
@@ -59,7 +111,7 @@ public class RegistrationData implements ReloadableEntity<Integer> {
return email;
}
void setEmail(String email) {
public void setEmail(String email) {
this.email = email;
}
@@ -78,4 +130,24 @@ public class RegistrationData implements ReloadableEntity<Integer> {
void setExpires(Instant expires) {
this.expires = expires;
}
public RegistrationTypeEnum getRegistrationType() {
return registrationType;
}
public void setRegistrationType(RegistrationTypeEnum registrationType) {
this.registrationType = registrationType;
}
public SortedSet<RegistrationDataMetadata> getMetadata() {
return metadata;
}
public void setMetadata(SortedSet<RegistrationDataMetadata> metadata) {
this.metadata = metadata;
}
public String getNetId() {
return netId;
}
}

View File

@@ -0,0 +1,97 @@
/**
* 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.text.MessageFormat;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
/**
* Singleton that encapsulates the configuration of each different token {@link RegistrationTypeEnum} duration. <br/>
* Contains also utility methods to compute the expiration date of the registered token.
*
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
**/
public class RegistrationDataExpirationConfiguration {
private static final String EXPIRATION_PROP = "eperson.registration-data.token.{0}.expiration";
private static final String DURATION_FORMAT = "PT{0}";
public static final RegistrationDataExpirationConfiguration INSTANCE =
new RegistrationDataExpirationConfiguration();
public static RegistrationDataExpirationConfiguration getInstance() {
return INSTANCE;
}
private final Map<RegistrationTypeEnum, Duration> expirationMap;
private RegistrationDataExpirationConfiguration() {
this.expirationMap =
Stream.of(RegistrationTypeEnum.values())
.map(type -> Optional.ofNullable(getDurationOf(type))
.map(duration -> Map.entry(type, duration))
.orElse(null)
)
.filter(Objects::nonNull)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
private Duration getDurationOf(RegistrationTypeEnum type) {
String format = MessageFormat.format(EXPIRATION_PROP, type.toString().toLowerCase());
ConfigurationService config = DSpaceServicesFactory.getInstance().getConfigurationService();
String typeValue = config.getProperty(format);
if (StringUtils.isBlank(typeValue)) {
return null;
}
return Duration.parse(MessageFormat.format(DURATION_FORMAT, typeValue));
}
/**
* Retrieves the {@link Duration} configuration of a given {@link RegistrationTypeEnum}.
*
* @param type is the type of the given registration token
* @return the {@link Duration} of that specific token.
*/
public Duration getExpiration(RegistrationTypeEnum type) {
return expirationMap.get(type);
}
/**
* Retrieves the expiration date of the given {@link RegistrationTypeEnum}.
*
* @param type is the RegistrationTypeEnum of the token
* @return a Date that represents the expiration date.
*/
public Instant computeExpirationDate(RegistrationTypeEnum type) {
if (type == null) {
return null;
}
Duration duration = this.expirationMap.get(type);
if (duration == null) {
return null;
}
return Instant.now().plus(duration);
}
}

View File

@@ -0,0 +1,105 @@
/**
* 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 jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;
import org.dspace.content.MetadataField;
import org.dspace.core.ReloadableEntity;
import org.hibernate.Length;
/**
* Metadata related to a registration data {@link RegistrationData}
*
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
**/
@Entity
@Table(name = "registrationdata_metadata")
public class RegistrationDataMetadata implements ReloadableEntity<Integer>, Comparable<RegistrationDataMetadata> {
@Id
@Column(name = "registrationdata_metadata_id")
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "registrationdata_metadatavalue_seq")
@SequenceGenerator(
name = "registrationdata_metadatavalue_seq",
sequenceName = "registrationdata_metadatavalue_seq",
allocationSize = 1
)
private final Integer id;
/**
* {@link RegistrationData} linked to this metadata value
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "registrationdata_id")
private RegistrationData registrationData = null;
/**
* The linked {@link MetadataField} instance
*/
@ManyToOne
@JoinColumn(name = "metadata_field_id")
private MetadataField metadataField = null;
/**
* Value represented by this {@link RegistrationDataMetadata} instance
* related to the metadataField {@link MetadataField}
*/
@Column(name = "text_value", length = Length.LONG32)
private String value = null;
/**
* Protected constructor
*/
protected RegistrationDataMetadata() {
id = 0;
}
@Override
public Integer getID() {
return id;
}
public MetadataField getMetadataField() {
return metadataField;
}
void setMetadataField(MetadataField metadataField) {
this.metadataField = metadataField;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public int compareTo(RegistrationDataMetadata o) {
return Integer.compare(this.id, o.id);
}
void setRegistrationData(RegistrationData registrationData) {
this.registrationData = registrationData;
}
public RegistrationData getRegistrationData() {
return registrationData;
}
}

View File

@@ -0,0 +1,90 @@
/**
* 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.sql.SQLException;
import java.util.List;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.MetadataField;
import org.dspace.content.service.MetadataFieldService;
import org.dspace.core.Context;
import org.dspace.eperson.dao.RegistrationDataMetadataDAO;
import org.dspace.eperson.service.RegistrationDataMetadataService;
import org.springframework.beans.factory.annotation.Autowired;
/**
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
**/
public class RegistrationDataMetadataServiceImpl implements RegistrationDataMetadataService {
@Autowired
private RegistrationDataMetadataDAO registrationDataMetadataDAO;
@Autowired
private MetadataFieldService metadataFieldService;
@Override
public RegistrationDataMetadata create(Context context, RegistrationData registrationData, String schema,
String element, String qualifier, String value) throws SQLException {
return create(
context, registrationData,
metadataFieldService.findByElement(context, schema, element, qualifier),
value
);
}
@Override
public RegistrationDataMetadata create(Context context, RegistrationData registrationData,
MetadataField metadataField) throws SQLException {
RegistrationDataMetadata metadata = new RegistrationDataMetadata();
metadata.setRegistrationData(registrationData);
metadata.setMetadataField(metadataField);
return registrationDataMetadataDAO.create(context, metadata);
}
@Override
public RegistrationDataMetadata create(
Context context, RegistrationData registrationData, MetadataField metadataField, String value
) throws SQLException {
RegistrationDataMetadata metadata = new RegistrationDataMetadata();
metadata.setRegistrationData(registrationData);
metadata.setMetadataField(metadataField);
metadata.setValue(value);
return registrationDataMetadataDAO.create(context, metadata);
}
@Override
public RegistrationDataMetadata create(Context context) throws SQLException, AuthorizeException {
return registrationDataMetadataDAO.create(context, new RegistrationDataMetadata());
}
@Override
public RegistrationDataMetadata find(Context context, int id) throws SQLException {
return registrationDataMetadataDAO.findByID(context, RegistrationDataMetadata.class, id);
}
@Override
public void update(Context context, RegistrationDataMetadata registrationDataMetadata)
throws SQLException, AuthorizeException {
registrationDataMetadataDAO.save(context, registrationDataMetadata);
}
@Override
public void update(Context context, List<RegistrationDataMetadata> t) throws SQLException, AuthorizeException {
for (RegistrationDataMetadata registrationDataMetadata : t) {
update(context, registrationDataMetadata);
}
}
@Override
public void delete(Context context, RegistrationDataMetadata registrationDataMetadata)
throws SQLException, AuthorizeException {
registrationDataMetadataDAO.delete(context, registrationDataMetadata);
}
}

View File

@@ -8,13 +8,26 @@
package org.dspace.eperson;
import java.sql.SQLException;
import java.time.Instant;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.MetadataField;
import org.dspace.content.MetadataValue;
import org.dspace.content.service.MetadataFieldService;
import org.dspace.core.Context;
import org.dspace.core.Utils;
import org.dspace.eperson.dao.RegistrationDataDAO;
import org.dspace.eperson.dto.RegistrationDataChanges;
import org.dspace.eperson.dto.RegistrationDataPatch;
import org.dspace.eperson.service.RegistrationDataMetadataService;
import org.dspace.eperson.service.RegistrationDataService;
import org.springframework.beans.factory.annotation.Autowired;
@@ -26,19 +39,67 @@ import org.springframework.beans.factory.annotation.Autowired;
* @author kevinvandevelde at atmire.com
*/
public class RegistrationDataServiceImpl implements RegistrationDataService {
@Autowired(required = true)
@Autowired()
protected RegistrationDataDAO registrationDataDAO;
@Autowired()
protected RegistrationDataMetadataService registrationDataMetadataService;
@Autowired()
protected MetadataFieldService metadataFieldService;
protected RegistrationDataExpirationConfiguration expirationConfiguration =
RegistrationDataExpirationConfiguration.getInstance();
protected RegistrationDataServiceImpl() {
}
@Override
public RegistrationData create(Context context) throws SQLException, AuthorizeException {
return registrationDataDAO.create(context, new RegistrationData());
return create(context, null, null);
}
@Override
public RegistrationData create(Context context, String netId) throws SQLException, AuthorizeException {
return this.create(context, netId, null);
}
@Override
public RegistrationData create(Context context, String netId, RegistrationTypeEnum type)
throws SQLException, AuthorizeException {
return registrationDataDAO.create(context, newInstance(netId, type, null));
}
private RegistrationData newInstance(String netId, RegistrationTypeEnum type, String email) {
RegistrationData rd = new RegistrationData(netId);
rd.setToken(Utils.generateHexKey());
rd.setRegistrationType(type);
rd.setExpires(expirationConfiguration.computeExpirationDate(type));
rd.setEmail(email);
return rd;
}
@Override
public RegistrationData clone(
Context context, RegistrationDataPatch registrationDataPatch
) throws SQLException, AuthorizeException {
RegistrationData old = registrationDataPatch.getOldRegistration();
RegistrationDataChanges changes = registrationDataPatch.getChanges();
RegistrationData rd = newInstance(old.getNetId(), changes.getRegistrationType(), changes.getEmail());
for (RegistrationDataMetadata metadata : old.getMetadata()) {
addMetadata(context, rd, metadata.getMetadataField(), metadata.getValue());
}
return registrationDataDAO.create(context, rd);
}
private boolean isEmailConfirmed(RegistrationData old, String newEmail) {
return newEmail.equals(old.getEmail());
}
@Override
public RegistrationData findByToken(Context context, String token) throws SQLException {
return registrationDataDAO.findByToken(context, token);
@@ -49,12 +110,124 @@ public class RegistrationDataServiceImpl implements RegistrationDataService {
return registrationDataDAO.findByEmail(context, email);
}
@Override
public RegistrationData findBy(Context context, String email, RegistrationTypeEnum type) throws SQLException {
return registrationDataDAO.findBy(context, email, type);
}
@Override
public void deleteByToken(Context context, String token) throws SQLException {
registrationDataDAO.deleteByToken(context, token);
}
@Override
public Stream<Map.Entry<RegistrationDataMetadata, Optional<MetadataValue>>> groupEpersonMetadataByRegistrationData(
EPerson ePerson, RegistrationData registrationData
)
throws SQLException {
Map<MetadataField, List<MetadataValue>> epersonMeta =
ePerson.getMetadata()
.stream()
.collect(
Collectors.groupingBy(
MetadataValue::getMetadataField
)
);
return registrationData.getMetadata()
.stream()
.map(meta ->
Map.entry(
meta,
Optional.ofNullable(epersonMeta.get(meta.getMetadataField()))
.filter(list -> list.size() == 1)
.map(values -> values.get(0))
)
);
}
@Override
public void setRegistrationMetadataValue(
Context context, RegistrationData registration, String schema, String element, String qualifier, String value
) throws SQLException, AuthorizeException {
List<RegistrationDataMetadata> metadata =
registration.getMetadata()
.stream()
.filter(m -> areEquals(m, schema, element, qualifier))
.collect(Collectors.toList());
if (metadata.size() > 1) {
throw new IllegalStateException("Find more than one registration metadata to update!");
}
RegistrationDataMetadata registrationDataMetadata;
if (metadata.isEmpty()) {
registrationDataMetadata =
createMetadata(context, registration, schema, element, qualifier, value);
} else {
registrationDataMetadata = metadata.get(0);
registrationDataMetadata.setValue(value);
}
registrationDataMetadataService.update(context, registrationDataMetadata);
}
@Override
public void addMetadata(
Context context, RegistrationData registration, MetadataField mf, String value
) throws SQLException, AuthorizeException {
registration.getMetadata().add(
registrationDataMetadataService.create(context, registration, mf, value)
);
this.update(context, registration);
}
@Override
public void addMetadata(
Context context, RegistrationData registration, String schema, String element, String qualifier, String value
) throws SQLException, AuthorizeException {
MetadataField mf = metadataFieldService.findByElement(context, schema, element, qualifier);
registration.getMetadata().add(
registrationDataMetadataService.create(context, registration, mf, value)
);
this.update(context, registration);
}
@Override
public RegistrationDataMetadata getMetadataByMetadataString(RegistrationData registrationData, String field) {
return registrationData.getMetadata().stream()
.filter(m -> field.equals(m.getMetadataField().toString('.')))
.findFirst().orElse(null);
}
private boolean areEquals(RegistrationDataMetadata m, String schema, String element, String qualifier) {
return m.getMetadataField().getMetadataSchema().getName().equals(schema)
&& m.getMetadataField().getElement().equals(element)
&& StringUtils.equals(m.getMetadataField().getQualifier(), qualifier);
}
private RegistrationDataMetadata createMetadata(
Context context, RegistrationData registration,
String schema, String element, String qualifier,
String value
) {
try {
return registrationDataMetadataService.create(
context, registration, schema, element, qualifier, value
);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private RegistrationDataMetadata createMetadata(Context context, RegistrationData registration, MetadataField mf) {
try {
return registrationDataMetadataService.create(context, registration, mf);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public RegistrationData find(Context context, int id) throws SQLException {
return registrationDataDAO.findByID(context, RegistrationData.class, id);
@@ -75,8 +248,25 @@ public class RegistrationDataServiceImpl implements RegistrationDataService {
}
}
@Override
public void markAsExpired(Context context, RegistrationData registrationData) throws SQLException {
registrationData.setExpires(Instant.now());
registrationDataDAO.save(context, registrationData);
}
@Override
public void delete(Context context, RegistrationData registrationData) throws SQLException, AuthorizeException {
registrationDataDAO.delete(context, registrationData);
}
@Override
public void deleteExpiredRegistrations(Context context) throws SQLException {
registrationDataDAO.deleteExpiredBy(context, Instant.now());
}
@Override
public boolean isValid(RegistrationData rd) {
return rd.getExpires() == null || rd.getExpires().isAfter(Instant.now());
}
}

View File

@@ -0,0 +1,33 @@
/**
* 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;
/**
* External provider allowed to register e-persons stored with {@link RegistrationData}
*
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
**/
public enum RegistrationTypeEnum {
ORCID("external-login"),
VALIDATION_ORCID("review-account"),
FORGOT("forgot"),
REGISTER("register"),
INVITATION("invitation"),
CHANGE_PASSWORD("change-password");
private final String link;
RegistrationTypeEnum(String link) {
this.link = link;
}
public String getLink() {
return link;
}
}

View File

@@ -8,10 +8,12 @@
package org.dspace.eperson.dao;
import java.sql.SQLException;
import java.time.Instant;
import org.dspace.core.Context;
import org.dspace.core.GenericDAO;
import org.dspace.eperson.RegistrationData;
import org.dspace.eperson.RegistrationTypeEnum;
/**
* Database Access Object interface class for the RegistrationData object.
@@ -23,9 +25,52 @@ import org.dspace.eperson.RegistrationData;
*/
public interface RegistrationDataDAO extends GenericDAO<RegistrationData> {
/**
* Finds {@link RegistrationData} by email.
*
* @param context Context for the current request
* @param email The email
* @return
* @throws SQLException
*/
public RegistrationData findByEmail(Context context, String email) throws SQLException;
/**
* Finds {@link RegistrationData} by email and type.
*
* @param context Context for the current request
* @param email The email
* @param type The type of the {@link RegistrationData}
* @return
* @throws SQLException
*/
public RegistrationData findBy(Context context, String email, RegistrationTypeEnum type) throws SQLException;
/**
* Finds {@link RegistrationData} by token.
*
* @param context the context
* @param token The token related to the {@link RegistrationData}.
* @return
* @throws SQLException
*/
public RegistrationData findByToken(Context context, String token) throws SQLException;
/**
* Deletes {@link RegistrationData} by token.
*
* @param context Context for the current request
* @param token The token to delete registrations for
* @throws SQLException
*/
public void deleteByToken(Context context, String token) throws SQLException;
/**
* Deletes expired {@link RegistrationData}.
*
* @param context Context for the current request
* @param instant The date to delete expired registrations for
* @throws SQLException
*/
void deleteExpiredBy(Context context, Instant instant) throws SQLException;
}

View File

@@ -0,0 +1,22 @@
/**
* 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.dao;
import org.dspace.core.GenericDAO;
import org.dspace.eperson.RegistrationDataMetadata;
/**
* Database Access Object interface class for the {@link org.dspace.eperson.RegistrationDataMetadata} object.
* The implementation of this class is responsible for all database calls for the RegistrationData object and is
* autowired by spring
*
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
**/
public interface RegistrationDataMetadataDAO extends GenericDAO<RegistrationDataMetadata> {
}

View File

@@ -8,15 +8,18 @@
package org.dspace.eperson.dao.impl;
import java.sql.SQLException;
import java.time.Instant;
import jakarta.persistence.Query;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaDelete;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Root;
import org.dspace.core.AbstractHibernateDAO;
import org.dspace.core.Context;
import org.dspace.eperson.RegistrationData;
import org.dspace.eperson.RegistrationData_;
import org.dspace.eperson.RegistrationTypeEnum;
import org.dspace.eperson.dao.RegistrationDataDAO;
/**
@@ -42,6 +45,21 @@ public class RegistrationDataDAOImpl extends AbstractHibernateDAO<RegistrationDa
return uniqueResult(context, criteriaQuery, false, RegistrationData.class);
}
@Override
public RegistrationData findBy(Context context, String email, RegistrationTypeEnum type) throws SQLException {
CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, RegistrationData.class);
Root<RegistrationData> registrationDataRoot = criteriaQuery.from(RegistrationData.class);
criteriaQuery.select(registrationDataRoot);
criteriaQuery.where(
criteriaBuilder.and(
criteriaBuilder.equal(registrationDataRoot.get(RegistrationData_.email), email),
criteriaBuilder.equal(registrationDataRoot.get(RegistrationData_.registrationType), type)
)
);
return uniqueResult(context, criteriaQuery, false, RegistrationData.class);
}
@Override
public RegistrationData findByToken(Context context, String token) throws SQLException {
CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
@@ -59,4 +77,15 @@ public class RegistrationDataDAOImpl extends AbstractHibernateDAO<RegistrationDa
query.setParameter("token", token);
query.executeUpdate();
}
@Override
public void deleteExpiredBy(Context context, Instant instant) throws SQLException {
CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
CriteriaDelete<RegistrationData> deleteQuery = criteriaBuilder.createCriteriaDelete(RegistrationData.class);
Root<RegistrationData> deleteRoot = deleteQuery.from(RegistrationData.class);
deleteQuery.where(
criteriaBuilder.lessThanOrEqualTo(deleteRoot.get(RegistrationData_.expires), instant)
);
getHibernateSession(context).createQuery(deleteQuery).executeUpdate();
}
}

View File

@@ -0,0 +1,19 @@
/**
* 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.dao.impl;
import org.dspace.core.AbstractHibernateDAO;
import org.dspace.eperson.RegistrationDataMetadata;
import org.dspace.eperson.dao.RegistrationDataMetadataDAO;
/**
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
**/
public class RegistrationDataMetadataDAOImpl extends AbstractHibernateDAO<RegistrationDataMetadata>
implements RegistrationDataMetadataDAO {
}

View File

@@ -0,0 +1,64 @@
/**
* 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.dto;
import org.dspace.eperson.RegistrationTypeEnum;
/**
* Class that embeds a change done for the {@link org.dspace.eperson.RegistrationData}
*
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
**/
public class RegistrationDataChanges {
@SuppressWarnings("checkstyle:LineLength")
private static final String EMAIL_PATTERN = "^[a-zA-Z0-9.!#$%&'*+\\\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$";
private final String email;
private final RegistrationTypeEnum registrationType;
public RegistrationDataChanges(String email, RegistrationTypeEnum type) {
if (email == null || email.trim().isBlank()) {
throw new IllegalArgumentException("Cannot update with an empty email address");
}
if (type == null) {
throw new IllegalArgumentException("Cannot update with a null registration type");
}
this.email = email;
if (!isValidEmail()) {
throw new IllegalArgumentException("Invalid email address provided!");
}
this.registrationType = type;
}
/**
* Checks if the email is valid using the EMAIL_PATTERN.
* @return true if valid, false otherwise
*/
public boolean isValidEmail() {
return email.matches(EMAIL_PATTERN);
}
/**
* Returns the email of change.
*
* @return the email of the change
*/
public String getEmail() {
return email;
}
/**
* Returns the {@link RegistrationTypeEnum} of the registration.
*
* @return the type of the change
*/
public RegistrationTypeEnum getRegistrationType() {
return registrationType;
}
}

View File

@@ -0,0 +1,44 @@
/**
* 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.dto;
import org.dspace.eperson.RegistrationData;
/**
* This POJO encapsulates the details of the PATCH request that updates the {@link RegistrationData}.
*
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
**/
public class RegistrationDataPatch {
private final RegistrationData oldRegistration;
private final RegistrationDataChanges changes;
public RegistrationDataPatch(RegistrationData oldRegistration, RegistrationDataChanges changes) {
this.oldRegistration = oldRegistration;
this.changes = changes;
}
/**
* Returns the value of the previous registration
*
* @return RegistrationData
*/
public RegistrationData getOldRegistration() {
return oldRegistration;
}
/**
* Returns the changes related to the registration
*
* @return RegistrationDataChanges
*/
public RegistrationDataChanges getChanges() {
return changes;
}
}

View File

@@ -10,6 +10,7 @@ package org.dspace.eperson.factory;
import org.dspace.eperson.service.AccountService;
import org.dspace.eperson.service.EPersonService;
import org.dspace.eperson.service.GroupService;
import org.dspace.eperson.service.RegistrationDataMetadataService;
import org.dspace.eperson.service.RegistrationDataService;
import org.dspace.eperson.service.SubscribeService;
import org.dspace.services.factory.DSpaceServicesFactory;
@@ -28,6 +29,8 @@ public abstract class EPersonServiceFactory {
public abstract RegistrationDataService getRegistrationDataService();
public abstract RegistrationDataMetadataService getRegistrationDAtaDataMetadataService();
public abstract AccountService getAccountService();
public abstract SubscribeService getSubscribeService();

View File

@@ -10,6 +10,7 @@ package org.dspace.eperson.factory;
import org.dspace.eperson.service.AccountService;
import org.dspace.eperson.service.EPersonService;
import org.dspace.eperson.service.GroupService;
import org.dspace.eperson.service.RegistrationDataMetadataService;
import org.dspace.eperson.service.RegistrationDataService;
import org.dspace.eperson.service.SubscribeService;
import org.springframework.beans.factory.annotation.Autowired;
@@ -29,6 +30,8 @@ public class EPersonServiceFactoryImpl extends EPersonServiceFactory {
@Autowired(required = true)
private RegistrationDataService registrationDataService;
@Autowired(required = true)
private RegistrationDataMetadataService registrationDataMetadataService;
@Autowired(required = true)
private AccountService accountService;
@Autowired(required = true)
private SubscribeService subscribeService;
@@ -58,4 +61,8 @@ public class EPersonServiceFactoryImpl extends EPersonServiceFactory {
return subscribeService;
}
@Override
public RegistrationDataMetadataService getRegistrationDAtaDataMetadataService() {
return registrationDataMetadataService;
}
}

View File

@@ -9,11 +9,15 @@ package org.dspace.eperson.service;
import java.io.IOException;
import java.sql.SQLException;
import java.util.List;
import java.util.UUID;
import jakarta.mail.MessagingException;
import org.dspace.authorize.AuthorizeException;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.RegistrationData;
import org.dspace.eperson.dto.RegistrationDataPatch;
/**
* Methods for handling registration by email and forgotten passwords. When
@@ -30,20 +34,79 @@ import org.dspace.eperson.EPerson;
* @version $Revision$
*/
public interface AccountService {
public void sendRegistrationInfo(Context context, String email)
throws SQLException, IOException, MessagingException, AuthorizeException;
public void sendForgotPasswordInfo(Context context, String email)
throws SQLException, IOException, MessagingException, AuthorizeException;
/**
* 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
*/
boolean existsAccountFor(Context context, String token)
throws SQLException, AuthorizeException;
/**
* Checks if exists an account related to the email provided
*
* @param context DSpace context
* @param email String email to search for
* @return true if exists, false otherwise
* @throws SQLException
*/
boolean existsAccountWithEmail(Context context, String email)
throws SQLException;
public EPerson getEPerson(Context context, String token)
throws SQLException, AuthorizeException;
public String getEmail(Context context, String token) throws SQLException;
public String getEmail(Context context, String token)
throws SQLException;
public void deleteToken(Context context, String token) throws SQLException;
public void deleteToken(Context context, String token)
throws SQLException;
/**
* Merge registration data with an existing EPerson or create a new one.
*
* @param context DSpace context
* @param userId The ID of the EPerson to merge with or create
* @param token The token to use for registration data
* @param overrides List of fields to override in the EPerson
* @return The merged or created EPerson
* @throws AuthorizeException If the user is not authorized to perform the action
* @throws SQLException If a database error occurs
*/
EPerson mergeRegistration(
Context context,
UUID userId,
String token,
List<String> overrides
) throws AuthorizeException, SQLException;
/**
* This method creates a fresh new {@link RegistrationData} based on the {@link RegistrationDataPatch} requested
* by a given user.
*
* @param context - The DSapce Context
* @param registrationDataPatch - Details of the patch request coming from the Controller
* @return a newly created {@link RegistrationData}
* @throws AuthorizeException
*/
RegistrationData renewRegistrationForEmail(
Context context,
RegistrationDataPatch registrationDataPatch
) throws AuthorizeException;
/**
* Checks if the {@link RegistrationData#token} is valid.
*
* @param registrationData that will be checked
* @return true if valid, false otherwise
*/
boolean isTokenValidForCreation(RegistrationData registrationData);
}

View File

@@ -0,0 +1,66 @@
/**
* 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.service;
import java.sql.SQLException;
import org.dspace.content.MetadataField;
import org.dspace.core.Context;
import org.dspace.eperson.RegistrationData;
import org.dspace.eperson.RegistrationDataMetadata;
import org.dspace.service.DSpaceCRUDService;
/**
* This class contains business-logic to handle {@link RegistrationDataMetadata}.
*
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
**/
public interface RegistrationDataMetadataService extends DSpaceCRUDService<RegistrationDataMetadata> {
/**
* Creates a new {@link RegistrationDataMetadata} that will be stored starting from the parameters of the method.
*
* @param context - the DSpace Context
* @param registrationData - the Registration data that will contain the metadata
* @param schema - the schema of the metadata field
* @param element - the element of the metadata field
* @param qualifier - the qualifier of the metadata field
* @param value - the value of that metadata
* @return the newly created RegistrationDataMetadata
* @throws SQLException
*/
RegistrationDataMetadata create(Context context, RegistrationData registrationData, String schema,
String element, String qualifier, String value) throws SQLException;
/**
* Creates a new {@link RegistrationDataMetadata}
*
* @param context - the DSpace Context
* @param registrationData - the RegistrationData that will contain that metadata
* @param metadataField - the metadataField
* @return the newly created RegistrationDataMetadata
* @throws SQLException
*/
RegistrationDataMetadata create(
Context context, RegistrationData registrationData, MetadataField metadataField
) throws SQLException;
/**
* Creates a new {@link RegistrationDataMetadata}
*
* @param context - the DSpace Context
* @param registrationData - the RegistrationData that will contain that metadata
* @param metadataField - the metadataField that will be stored
* @param value - the value that will be placed inside the RegistrationDataMetadata
* @return the newly created {@link RegistrationDataMetadata}
* @throws SQLException
*/
RegistrationDataMetadata create(
Context context, RegistrationData registrationData, MetadataField metadataField, String value
) throws SQLException;
}

View File

@@ -8,13 +8,23 @@
package org.dspace.eperson.service;
import java.sql.SQLException;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.MetadataField;
import org.dspace.content.MetadataValue;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.RegistrationData;
import org.dspace.eperson.RegistrationDataMetadata;
import org.dspace.eperson.RegistrationTypeEnum;
import org.dspace.eperson.dto.RegistrationDataPatch;
import org.dspace.service.DSpaceCRUDService;
/**
* Service interface class for the RegistrationData object.
* Service interface class for the {@link RegistrationData} object.
* The implementation of this class is responsible for all business logic calls for the RegistrationData object and
* is autowired by spring
*
@@ -22,10 +32,45 @@ import org.dspace.service.DSpaceCRUDService;
*/
public interface RegistrationDataService extends DSpaceCRUDService<RegistrationData> {
RegistrationData create(Context context) throws SQLException, AuthorizeException;
RegistrationData create(Context context, String netId) throws SQLException, AuthorizeException;
RegistrationData create(Context context, String netId, RegistrationTypeEnum type)
throws SQLException, AuthorizeException;
RegistrationData clone(
Context context, RegistrationDataPatch registrationDataPatch
) throws SQLException, AuthorizeException;
public RegistrationData findByToken(Context context, String token) throws SQLException;
public RegistrationData findByEmail(Context context, String email) throws SQLException;
RegistrationData findBy(Context context, String email, RegistrationTypeEnum type) throws SQLException;
public void deleteByToken(Context context, String token) throws SQLException;
Stream<Map.Entry<RegistrationDataMetadata, Optional<MetadataValue>>> groupEpersonMetadataByRegistrationData(
EPerson ePerson, RegistrationData registrationData
) throws SQLException;
void setRegistrationMetadataValue(
Context context, RegistrationData registration, String schema, String element, String qualifier, String value
) throws SQLException, AuthorizeException;
void addMetadata(
Context context, RegistrationData registration, String schema, String element, String qualifier, String value
) throws SQLException, AuthorizeException;
RegistrationDataMetadata getMetadataByMetadataString(RegistrationData registrationData, String field);
void addMetadata(Context context, RegistrationData rd, MetadataField metadataField, String value)
throws SQLException, AuthorizeException;
void markAsExpired(Context context, RegistrationData registrationData) throws SQLException, AuthorizeException;
void deleteExpiredRegistrations(Context context) throws SQLException;
boolean isValid(RegistrationData rd);
}

View File

@@ -36,10 +36,12 @@ import org.dspace.discovery.SearchServiceException;
import org.dspace.discovery.indexobject.IndexableItem;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.service.EPersonService;
import org.dspace.orcid.OrcidQueue;
import org.dspace.orcid.OrcidToken;
import org.dspace.orcid.client.OrcidClient;
import org.dspace.orcid.model.OrcidEntityType;
import org.dspace.orcid.model.OrcidTokenResponseDTO;
import org.dspace.orcid.service.OrcidQueueService;
import org.dspace.orcid.service.OrcidSynchronizationService;
import org.dspace.orcid.service.OrcidTokenService;
import org.dspace.profile.OrcidEntitySyncPreference;
@@ -61,9 +63,13 @@ import org.springframework.beans.factory.annotation.Autowired;
public class OrcidSynchronizationServiceImpl implements OrcidSynchronizationService {
private static final Logger log = LoggerFactory.getLogger(OrcidSynchronizationServiceImpl.class);
@Autowired
private ItemService itemService;
@Autowired
private OrcidQueueService orcidQueueService;
@Autowired
private ConfigurationService configurationService;
@@ -120,7 +126,6 @@ public class OrcidSynchronizationServiceImpl implements OrcidSynchronizationServ
@Override
public void unlinkProfile(Context context, Item profile) throws SQLException {
clearOrcidProfileMetadata(context, profile);
clearSynchronizationSettings(context, profile);
@@ -129,6 +134,11 @@ public class OrcidSynchronizationServiceImpl implements OrcidSynchronizationServ
updateItem(context, profile);
List<OrcidQueue> queueRecords = orcidQueueService.findByProfileItemId(context, profile.getID());
for (OrcidQueue queueRecord : queueRecords) {
orcidQueueService.delete(context, queueRecord);
}
}
private void clearOrcidToken(Context context, Item profile) {

View File

@@ -0,0 +1,46 @@
--
-- 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/
--
-----------------------------------------------------------------------------------
-- ALTER table registrationdata
-----------------------------------------------------------------------------------
EXECUTE IMMEDIATE 'ALTER TABLE registrationdata DROP CONSTRAINT ' ||
QUOTE_IDENT((SELECT CONSTRAINT_NAME
FROM information_schema.key_column_usage
WHERE TABLE_SCHEMA = 'PUBLIC' AND TABLE_NAME = 'REGISTRATIONDATA' AND COLUMN_NAME = 'EMAIL'));
ALTER TABLE registrationdata
ADD COLUMN registration_type VARCHAR2(255);
ALTER TABLE registrationdata
ADD COLUMN net_id VARCHAR2(64);
CREATE SEQUENCE IF NOT EXISTS registrationdata_metadatavalue_seq START WITH 1 INCREMENT BY 1;
-----------------------------------------------------------------------------------
-- Creates table registrationdata_metadata
-----------------------------------------------------------------------------------
CREATE TABLE registrationdata_metadata (
registrationdata_metadata_id INTEGER NOT NULL,
registrationdata_id INTEGER,
metadata_field_id INTEGER,
text_value CLOB,
CONSTRAINT pk_registrationdata_metadata PRIMARY KEY (registrationdata_metadata_id)
);
ALTER TABLE registrationdata_metadata
ADD CONSTRAINT FK_REGISTRATIONDATA_METADATA_ON_METADATA_FIELD
FOREIGN KEY (metadata_field_id)
REFERENCES metadatafieldregistry (metadata_field_id) ON DELETE CASCADE;
ALTER TABLE registrationdata_metadata
ADD CONSTRAINT FK_REGISTRATIONDATA_METADATA_ON_REGISTRATIONDATA
FOREIGN KEY (registrationdata_id)
REFERENCES registrationdata (registrationdata_id) ON DELETE CASCADE;

View File

@@ -0,0 +1,52 @@
--
-- 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/
--
-----------------------------------------------------------------------------------
-- ALTER table registrationdata
-----------------------------------------------------------------------------------
DO $$
BEGIN
EXECUTE 'ALTER TABLE registrationdata DROP CONSTRAINT IF EXISTS ' ||
QUOTE_IDENT((
SELECT CONSTRAINT_NAME
FROM information_schema.key_column_usage
WHERE TABLE_NAME = 'registrationdata' AND COLUMN_NAME = 'email'
));
end
$$;
ALTER TABLE registrationdata
ADD COLUMN registration_type VARCHAR(255);
ALTER TABLE registrationdata
ADD COLUMN net_id VARCHAR(64);
CREATE SEQUENCE IF NOT EXISTS registrationdata_metadatavalue_seq START WITH 1 INCREMENT BY 1;
-----------------------------------------------------------------------------------
-- Creates table registrationdata_metadata
-----------------------------------------------------------------------------------
CREATE TABLE registrationdata_metadata (
registrationdata_metadata_id INTEGER NOT NULL,
registrationdata_id INTEGER,
metadata_field_id INTEGER,
text_value TEXT,
CONSTRAINT pk_registrationdata_metadata PRIMARY KEY (registrationdata_metadata_id)
);
ALTER TABLE registrationdata_metadata
ADD CONSTRAINT FK_REGISTRATIONDATA_METADATA_ON_METADATA_FIELD
FOREIGN KEY (metadata_field_id)
REFERENCES metadatafieldregistry (metadata_field_id) ON DELETE CASCADE;
ALTER TABLE registrationdata_metadata
ADD CONSTRAINT FK_REGISTRATIONDATA_METADATA_ON_REGISTRATIONDATA
FOREIGN KEY (registrationdata_id)
REFERENCES registrationdata (registrationdata_id) ON DELETE CASCADE;

View File

@@ -0,0 +1,263 @@
/**
* 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 static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertThrows;
import java.io.IOException;
import java.sql.SQLException;
import java.util.List;
import org.dspace.AbstractIntegrationTestWithDatabase;
import org.dspace.authorize.AuthorizeException;
import org.dspace.builder.EPersonBuilder;
import org.dspace.builder.MetadataFieldBuilder;
import org.dspace.content.Item;
import org.dspace.content.MetadataField;
import org.dspace.eperson.factory.EPersonServiceFactory;
import org.dspace.eperson.service.AccountService;
import org.dspace.eperson.service.EPersonService;
import org.dspace.eperson.service.RegistrationDataService;
import org.junit.Before;
import org.junit.Test;
public class AccountServiceImplIT extends AbstractIntegrationTestWithDatabase {
public static final String ORCID_NETID = "vins01";
public static final String ORCID_EMAIL = "vins-01@fake.mail";
public static final String CUSTOM_METADATA_VALUE = "vins01-customID";
AccountService accountService =
EPersonServiceFactory.getInstance().getAccountService();
EPersonService ePersonService =
EPersonServiceFactory.getInstance().getEPersonService();
RegistrationDataService registrationDataService =
EPersonServiceFactory.getInstance().getRegistrationDataService();
;
EPerson tokenPerson;
RegistrationData orcidToken;
MetadataField metadataField;
@Before
public void setUp() throws Exception {
super.setUp();
context.turnOffAuthorisationSystem();
tokenPerson =
EPersonBuilder.createEPerson(context)
.withNameInMetadata("Vincenzo", "Mecca")
.withEmail(null)
.withNetId(null)
.withCanLogin(true)
.build();
metadataField =
MetadataFieldBuilder.createMetadataField(context, "identifier", "custom", null)
.build();
orcidToken =
registrationDataService.create(context, ORCID_NETID, RegistrationTypeEnum.ORCID);
orcidToken.setEmail(ORCID_EMAIL);
registrationDataService.addMetadata(context, orcidToken, metadataField, CUSTOM_METADATA_VALUE);
registrationDataService.update(context, orcidToken);
context.restoreAuthSystemState();
}
@Test
public void testMergedORCIDRegistration() throws SQLException, AuthorizeException {
// set current logged-in eperson
context.setCurrentUser(tokenPerson);
// try to update account details with the ORCID token
EPerson updatedEperson =
accountService.mergeRegistration(
context, tokenPerson.getID(), orcidToken.getToken(),
List.of()
);
// updates value with the one inside the ORCID token
assertThat(updatedEperson, notNullValue());
assertThat(updatedEperson.getEmail(), is(ORCID_EMAIL));
assertThat(updatedEperson.getNetid(), is(ORCID_NETID));
String customMetadataFound =
ePersonService.getMetadataFirstValue(
updatedEperson, metadataField.getMetadataSchema().getName(), metadataField.getElement(),
metadataField.getQualifier(), Item.ANY
);
// updates the metadata with the one set in the ORCID token
assertThat(customMetadataFound, is(CUSTOM_METADATA_VALUE));
// deletes the token
assertThat(registrationDataService.findByToken(context, orcidToken.getToken()), nullValue());
}
@Test
public void testMergedORCIDRegistrationWithOverwrittenMetadata() throws SQLException, AuthorizeException {
// set current logged-in eperson
context.setCurrentUser(tokenPerson);
registrationDataService.addMetadata(
context, orcidToken, "eperson", "firstname", null, "Vins"
);
registrationDataService.addMetadata(
context, orcidToken, "eperson", "lastname", null, "4Science"
);
registrationDataService.update(context, orcidToken);
// try to update account details with the ORCID token
EPerson updatedEperson =
accountService.mergeRegistration(context, tokenPerson.getID(), orcidToken.getToken(),
List.of("eperson.firstname", "eperson.lastname"));
// updates value with the one inside the ORCID token
assertThat(updatedEperson, notNullValue());
assertThat(updatedEperson.getEmail(), is(ORCID_EMAIL));
assertThat(updatedEperson.getNetid(), is(ORCID_NETID));
// overwrites values with the one from the token
assertThat(updatedEperson.getFirstName(), is("Vins"));
assertThat(updatedEperson.getLastName(), is("4Science"));
String customMetadataFound =
ePersonService.getMetadataFirstValue(
updatedEperson, metadataField.getMetadataSchema().getName(), metadataField.getElement(),
metadataField.getQualifier(), Item.ANY
);
// updates the metadata with the one set in the ORCID token
assertThat(customMetadataFound, is(CUSTOM_METADATA_VALUE));
// deletes the token
assertThat(registrationDataService.findByToken(context, orcidToken.getToken()), nullValue());
}
@Test
public void testCannotMergedORCIDRegistrationWithDifferentLoggedEperson() {
// set current logged-in admin
context.setCurrentUser(admin);
// try to update eperson details with the ORCID token while logged in as admin
assertThrows(
AuthorizeException.class,
() -> accountService.mergeRegistration(context, tokenPerson.getID(), orcidToken.getToken(), List.of())
);
}
@Test
public void testCreateUserWithRegistration() throws SQLException, AuthorizeException, IOException {
// set current logged-in eperson
context.setCurrentUser(null);
context.turnOffAuthorisationSystem();
// create an orcid validation token
RegistrationData orcidRegistration =
registrationDataService.create(context, ORCID_NETID, RegistrationTypeEnum.VALIDATION_ORCID);
registrationDataService.addMetadata(
context, orcidRegistration, "eperson", "firstname", null, "Vincenzo"
);
registrationDataService.addMetadata(
context, orcidRegistration, "eperson", "lastname", null, "Mecca"
);
orcidRegistration.setEmail(ORCID_EMAIL);
registrationDataService.update(context, orcidRegistration);
context.commit();
context.restoreAuthSystemState();
EPerson createdEPerson = null;
try {
// try to create a new account during orcid registration
createdEPerson =
accountService.mergeRegistration(context, null, orcidRegistration.getToken(), List.of());
// updates value with the one inside the validation token
assertThat(createdEPerson, notNullValue());
assertThat(createdEPerson.getFirstName(), is("Vincenzo"));
assertThat(createdEPerson.getLastName(), is("Mecca"));
assertThat(createdEPerson.getEmail(), is(ORCID_EMAIL));
assertThat(createdEPerson.getNetid(), is(ORCID_NETID));
// deletes the token
assertThat(registrationDataService.findByToken(context, orcidRegistration.getToken()), nullValue());
} finally {
context.turnOffAuthorisationSystem();
ePersonService.delete(context, context.reloadEntity(createdEPerson));
RegistrationData found = context.reloadEntity(orcidRegistration);
if (found != null) {
registrationDataService.delete(context, found);
}
context.restoreAuthSystemState();
}
}
@Test
public void testInvalidMergeWithoutValidToken() throws SQLException, AuthorizeException {
// create a register token
RegistrationData anyToken =
registrationDataService.create(context, ORCID_NETID, RegistrationTypeEnum.REGISTER);
try {
assertThrows(
AuthorizeException.class,
() -> accountService.mergeRegistration(context, null, anyToken.getToken(), List.of())
);
// sets as forgot token
anyToken.setRegistrationType(RegistrationTypeEnum.FORGOT);
registrationDataService.update(context, anyToken);
assertThrows(
AuthorizeException.class,
() -> accountService.mergeRegistration(context, null, anyToken.getToken(), List.of())
);
// sets as change_password token
anyToken.setRegistrationType(RegistrationTypeEnum.CHANGE_PASSWORD);
registrationDataService.update(context, anyToken);
assertThrows(
AuthorizeException.class,
() -> accountService.mergeRegistration(context, null, anyToken.getToken(), List.of())
);
// sets as invitation token
anyToken.setRegistrationType(RegistrationTypeEnum.INVITATION);
registrationDataService.update(context, anyToken);
assertThrows(
AuthorizeException.class,
() -> accountService.mergeRegistration(context, null, anyToken.getToken(), List.of())
);
} finally {
registrationDataService.delete(context, context.reloadEntity(anyToken));
}
}
}

View File

@@ -0,0 +1,167 @@
/**
* 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 static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import org.dspace.AbstractIntegrationTestWithDatabase;
import org.dspace.builder.MetadataFieldBuilder;
import org.dspace.content.MetadataField;
import org.dspace.eperson.factory.EPersonServiceFactory;
import org.dspace.eperson.service.RegistrationDataMetadataService;
import org.dspace.eperson.service.RegistrationDataService;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
/**
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
*/
public class RegistrationDataMetadataServiceImplIT extends AbstractIntegrationTestWithDatabase {
RegistrationDataMetadataService registrationDataMetadataService =
EPersonServiceFactory.getInstance().getRegistrationDAtaDataMetadataService();
RegistrationDataService registrationDataService =
EPersonServiceFactory.getInstance().getRegistrationDataService();
MetadataField metadataField;
RegistrationData registrationData;
RegistrationDataMetadata metadata;
@Before
@Override
public void setUp() throws Exception {
super.setUp();
context.turnOffAuthorisationSystem();
this.registrationData =
this.registrationDataService.create(context);
this.metadataField =
MetadataFieldBuilder.createMetadataField(context, "dc", "identifier", "custom")
.build();
context.restoreAuthSystemState();
}
@After
@Override
public void destroy() throws Exception {
this.registrationDataService.delete(context, registrationData);
super.destroy();
}
@Test
public void testEmptyMetadataCreation() throws Exception {
try {
metadata = registrationDataMetadataService.create(context, registrationData, metadataField);
assertThat(metadata, notNullValue());
assertThat(metadata.getValue(), nullValue());
assertThat(metadata.getRegistrationData().getID(), is(registrationData.getID()));
assertThat(metadata.getMetadataField(), is(metadataField));
} finally {
registrationDataMetadataService.delete(context, metadata);
}
}
@Test
public void testValidMetadataCreation() throws Exception {
try {
metadata =
registrationDataMetadataService.create(context, registrationData, metadataField, "my-identifier");
assertThat(metadata, notNullValue());
assertThat(metadata.getValue(), is("my-identifier"));
assertThat(metadata.getRegistrationData().getID(), is(registrationData.getID()));
assertThat(metadata.getMetadataField(), is(metadataField));
} finally {
registrationDataMetadataService.delete(context, metadata);
}
}
@Test
public void testExistingMetadataFieldMetadataCreation() throws Exception {
try {
metadata =
registrationDataMetadataService.create(
context, registrationData, "dc", "identifier", "other", "my-identifier"
);
assertThat(metadata, notNullValue());
assertThat(metadata.getValue(), is("my-identifier"));
assertThat(metadata.getRegistrationData().getID(), is(registrationData.getID()));
} finally {
registrationDataMetadataService.delete(context, metadata);
}
}
@Test
public void testFindMetadata() throws Exception {
try {
metadata = registrationDataMetadataService.create(context, registrationData, metadataField);
RegistrationDataMetadata found =
registrationDataMetadataService.find(context, metadata.getID());
assertThat(found.getID(), is(metadata.getID()));
} finally {
registrationDataMetadataService.delete(context, metadata);
}
}
@Test
public void testUpdateMetadata() throws Exception {
try {
metadata = registrationDataMetadataService.create(context, registrationData, metadataField);
metadata.setValue("custom-value");
registrationDataMetadataService.update(context, metadata);
RegistrationDataMetadata found =
registrationDataMetadataService.find(context, metadata.getID());
assertThat(found.getID(), is(metadata.getID()));
assertThat(found.getValue(), is("custom-value"));
} finally {
registrationDataMetadataService.delete(context, metadata);
}
}
@Test
public void testDeleteMetadata() throws Exception {
try {
metadata = registrationDataMetadataService.create(context, registrationData, metadataField);
RegistrationDataMetadata found =
registrationDataMetadataService.find(context, metadata.getID());
assertThat(found, notNullValue());
registrationDataMetadataService.delete(context, metadata);
found = registrationDataMetadataService.find(context, metadata.getID());
assertThat(found, nullValue());
} finally {
registrationDataMetadataService.delete(context, metadata);
}
}
}

View File

@@ -36,21 +36,28 @@ public class VersionedHandleIdentifierProviderIT extends AbstractIdentifierProvi
public void setUp() throws Exception {
super.setUp();
context.turnOffAuthorisationSystem();
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent Community")
.build();
collection = CollectionBuilder.createCollection(context, parentCommunity)
.withName("Collection")
.build();
context.restoreAuthSystemState();
}
private void createVersions() throws SQLException, AuthorizeException {
context.turnOffAuthorisationSystem();
itemV1 = ItemBuilder.createItem(context, collection)
.withTitle("First version")
.build();
firstHandle = itemV1.getHandle();
itemV2 = VersionBuilder.createVersion(context, itemV1, "Second version").build().getItem();
itemV3 = VersionBuilder.createVersion(context, itemV1, "Third version").build().getItem();
context.restoreAuthSystemState();
}
@Test

View File

@@ -0,0 +1,89 @@
/**
* 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.app.rest;
import java.util.List;
import java.util.UUID;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.constraints.NotNull;
import org.dspace.app.rest.converter.ConverterService;
import org.dspace.app.rest.model.EPersonRest;
import org.dspace.app.rest.model.hateoas.EPersonResource;
import org.dspace.app.rest.repository.EPersonRestRepository;
import org.dspace.app.rest.utils.ContextUtil;
import org.dspace.core.Context;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.rest.webmvc.ControllerUtils;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* This controller is responsible to handle {@link org.dspace.eperson.RegistrationData}
* of a given {@link org.dspace.eperson.EPerson}
*
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
**/
@RestController
@RequestMapping("/api/" + EPersonRest.CATEGORY + "/" + EPersonRest.PLURAL_NAME)
public class EPersonRegistrationRestController {
@Autowired
private EPersonRestRepository ePersonRestRepository;
@Autowired
private ConverterService converter;
/**
* This method will merge the data coming from a {@link org.dspace.eperson.RegistrationData} into the current
* logged-in user.
* <br/>
* The request must have an empty body, and a token parameter should be provided:
* <pre>
* <code>
* curl -X POST http://${dspace.url}/api/eperson/epersons/${id-eperson}?token=${token}&override=${metadata-fields}
* -H "Content-Type: application/json"
* -H "Authorization: Bearer ${bearer-token}"
* </code>
* </pre>
* @param request httpServletRequest incoming
* @param uuid uuid of the eperson
* @param token registration token
* @param override fields to override inside from the registration data to the eperson
* @return
* @throws Exception
*/
@RequestMapping(method = RequestMethod.POST, value = "/{uuid}")
public ResponseEntity<RepresentationModel<?>> post(
HttpServletRequest request,
@PathVariable String uuid,
@RequestParam @NotNull String token,
@RequestParam(required = false) List<String> override
) throws Exception {
Context context = ContextUtil.obtainContext(request);
try {
context.turnOffAuthorisationSystem();
EPersonRest epersonRest =
ePersonRestRepository.mergeFromRegistrationData(context, UUID.fromString(uuid), token, override);
EPersonResource resource = converter.toResource(epersonRest);
return ControllerUtils.toResponseEntity(HttpStatus.CREATED, new HttpHeaders(), resource);
} catch (Exception e) {
throw e;
} finally {
context.restoreAuthSystemState();
}
}
}

View File

@@ -35,7 +35,7 @@ import org.springframework.stereotype.Component;
* Converter to translate between lists of domain {@link MetadataValue}s and {@link MetadataRest} representations.
*/
@Component
public class MetadataConverter implements DSpaceConverter<MetadataValueList, MetadataRest> {
public class MetadataConverter implements DSpaceConverter<MetadataValueList, MetadataRest<MetadataValueRest>> {
@Autowired
private ContentServiceFactory contentServiceFactory;
@@ -46,7 +46,7 @@ public class MetadataConverter implements DSpaceConverter<MetadataValueList, Met
private ConverterService converter;
@Override
public MetadataRest convert(MetadataValueList metadataValues,
public MetadataRest<MetadataValueRest> convert(MetadataValueList metadataValues,
Projection projection) {
// Convert each value to a DTO while retaining place order in a map of key -> SortedSet
Map<String, SortedSet<MetadataValueRest>> mapOfSortedSets = new HashMap<>();
@@ -60,7 +60,7 @@ public class MetadataConverter implements DSpaceConverter<MetadataValueList, Met
set.add(converter.toRest(metadataValue, projection));
}
MetadataRest metadataRest = new MetadataRest();
MetadataRest<MetadataValueRest> metadataRest = new MetadataRest<>();
// Populate MetadataRest's map of key -> List while respecting SortedSet's order
Map<String, List<MetadataValueRest>> mapOfLists = metadataRest.getMap();
@@ -80,14 +80,14 @@ public class MetadataConverter implements DSpaceConverter<MetadataValueList, Met
* Sets a DSpace object's domain metadata values from a rest representation.
* Any existing metadata value is deleted or overwritten.
*
* @param context the context to use.
* @param dso the DSpace object.
* @param context the context to use.
* @param dso the DSpace object.
* @param metadataRest the rest representation of the new metadata.
* @throws SQLException if a database error occurs.
* @throws SQLException if a database error occurs.
* @throws AuthorizeException if an authorization error occurs.
*/
public <T extends DSpaceObject> void setMetadata(Context context, T dso, MetadataRest metadataRest)
throws SQLException, AuthorizeException {
throws SQLException, AuthorizeException {
DSpaceObjectService<T> dsoService = contentServiceFactory.getDSpaceObjectService(dso);
dsoService.clearMetadata(context, dso, Item.ANY, Item.ANY, Item.ANY, Item.ANY);
persistMetadataRest(context, dso, metadataRest, dsoService);
@@ -97,14 +97,14 @@ public class MetadataConverter implements DSpaceConverter<MetadataValueList, Met
* Add to a DSpace object's domain metadata values from a rest representation.
* Any existing metadata value is preserved.
*
* @param context the context to use.
* @param dso the DSpace object.
* @param context the context to use.
* @param dso the DSpace object.
* @param metadataRest the rest representation of the new metadata.
* @throws SQLException if a database error occurs.
* @throws SQLException if a database error occurs.
* @throws AuthorizeException if an authorization error occurs.
*/
public <T extends DSpaceObject> void addMetadata(Context context, T dso, MetadataRest metadataRest)
throws SQLException, AuthorizeException {
throws SQLException, AuthorizeException {
DSpaceObjectService<T> dsoService = contentServiceFactory.getDSpaceObjectService(dso);
persistMetadataRest(context, dso, metadataRest, dsoService);
}
@@ -113,33 +113,34 @@ public class MetadataConverter implements DSpaceConverter<MetadataValueList, Met
* Merge into a DSpace object's domain metadata values from a rest representation.
* Any existing metadata value is preserved or overwritten with the new ones
*
* @param context the context to use.
* @param dso the DSpace object.
* @param context the context to use.
* @param dso the DSpace object.
* @param metadataRest the rest representation of the new metadata.
* @throws SQLException if a database error occurs.
* @throws SQLException if a database error occurs.
* @throws AuthorizeException if an authorization error occurs.
*/
public <T extends DSpaceObject> void mergeMetadata(Context context, T dso, MetadataRest metadataRest)
throws SQLException, AuthorizeException {
public <T extends DSpaceObject> void mergeMetadata(
Context context, T dso, MetadataRest<MetadataValueRest> metadataRest
) throws SQLException, AuthorizeException {
DSpaceObjectService<T> dsoService = contentServiceFactory.getDSpaceObjectService(dso);
for (Map.Entry<String, List<MetadataValueRest>> entry: metadataRest.getMap().entrySet()) {
for (Map.Entry<String, List<MetadataValueRest>> entry : metadataRest.getMap().entrySet()) {
List<MetadataValue> metadataByMetadataString = dsoService.getMetadataByMetadataString(dso, entry.getKey());
dsoService.removeMetadataValues(context, dso, metadataByMetadataString);
}
persistMetadataRest(context, dso, metadataRest, dsoService);
}
private <T extends DSpaceObject> void persistMetadataRest(Context context, T dso, MetadataRest metadataRest,
DSpaceObjectService<T> dsoService)
throws SQLException, AuthorizeException {
for (Map.Entry<String, List<MetadataValueRest>> entry: metadataRest.getMap().entrySet()) {
private <T extends DSpaceObject> void persistMetadataRest(
Context context, T dso, MetadataRest<MetadataValueRest> metadataRest, DSpaceObjectService<T> dsoService
) throws SQLException, AuthorizeException {
for (Map.Entry<String, List<MetadataValueRest>> entry : metadataRest.getMap().entrySet()) {
String[] seq = entry.getKey().split("\\.");
String schema = seq[0];
String element = seq[1];
String qualifier = seq.length == 3 ? seq[2] : null;
for (MetadataValueRest mvr: entry.getValue()) {
for (MetadataValueRest mvr : entry.getValue()) {
dsoService.addMetadata(context, dso, schema, element, qualifier, mvr.getLanguage(),
mvr.getValue(), mvr.getAuthority(), mvr.getConfidence());
mvr.getValue(), mvr.getAuthority(), mvr.getConfidence());
}
}
dsoService.update(context, dso);

View File

@@ -0,0 +1,129 @@
/**
* 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.app.rest.converter;
import java.sql.SQLException;
import java.util.Optional;
import jakarta.servlet.http.HttpServletRequest;
import org.dspace.app.rest.model.MetadataRest;
import org.dspace.app.rest.model.RegistrationMetadataRest;
import org.dspace.app.rest.model.RegistrationRest;
import org.dspace.app.rest.projection.Projection;
import org.dspace.app.rest.utils.ContextUtil;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.MetadataValue;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.RegistrationData;
import org.dspace.eperson.RegistrationTypeEnum;
import org.dspace.eperson.factory.EPersonServiceFactory;
import org.dspace.eperson.service.AccountService;
import org.dspace.eperson.service.RegistrationDataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Converts a given {@link RegistrationRest} DTO into a {@link RegistrationData} entity.
*
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
**/
@Component
public class RegistrationDataConverter implements DSpaceConverter<RegistrationData, RegistrationRest> {
@Autowired
private HttpServletRequest request;
@Autowired
private RegistrationDataService registrationDataService;
@Override
public RegistrationRest convert(RegistrationData registrationData, Projection projection) {
if (registrationData == null) {
return null;
}
Context context = ContextUtil.obtainContext(request);
AccountService accountService = EPersonServiceFactory.getInstance().getAccountService();
RegistrationRest registrationRest = new RegistrationRest();
registrationRest.setId(registrationData.getID());
registrationRest.setEmail(registrationData.getEmail());
registrationRest.setNetId(registrationData.getNetId());
registrationRest.setRegistrationType(
Optional.ofNullable(registrationData.getRegistrationType())
.map(RegistrationTypeEnum::toString)
.orElse(null)
);
EPerson ePerson = null;
try {
ePerson = accountService.getEPerson(context, registrationData.getToken());
} catch (SQLException | AuthorizeException e) {
throw new RuntimeException(e);
}
if (ePerson != null) {
registrationRest.setUser(ePerson.getID());
try {
MetadataRest<RegistrationMetadataRest> metadataRest = getMetadataRest(ePerson, registrationData);
if (registrationData.getEmail() != null) {
metadataRest.put(
"email",
new RegistrationMetadataRest(registrationData.getEmail(), ePerson.getEmail())
);
}
registrationRest.setRegistrationMetadata(metadataRest);
} catch (SQLException e) {
throw new RuntimeException(e);
}
} else {
registrationRest.setRegistrationMetadata(getMetadataRest(registrationData));
}
return registrationRest;
}
private MetadataRest<RegistrationMetadataRest> getMetadataRest(EPerson ePerson, RegistrationData registrationData)
throws SQLException {
return registrationDataService.groupEpersonMetadataByRegistrationData(ePerson, registrationData)
.reduce(
new MetadataRest<>(),
(map, entry) -> map.put(
entry.getKey().getMetadataField().toString('.'),
new RegistrationMetadataRest(
entry.getKey().getValue(),
entry.getValue().map(MetadataValue::getValue).orElse(null)
)
),
(m1, m2) -> {
m1.getMap().putAll(m2.getMap());
return m1;
}
);
}
private MetadataRest<RegistrationMetadataRest> getMetadataRest(RegistrationData registrationData) {
MetadataRest<RegistrationMetadataRest> metadataRest = new MetadataRest<>();
registrationData.getMetadata().forEach(
(m) -> metadataRest.put(
m.getMetadataField().toString('.'),
new RegistrationMetadataRest(m.getValue())
)
);
return metadataRest;
}
@Override
public Class<RegistrationData> getModelClass() {
return RegistrationData.class;
}
}

View File

@@ -20,7 +20,7 @@ public abstract class DSpaceObjectRest extends BaseObjectRest<String> {
private String name;
private String handle;
MetadataRest metadata = new MetadataRest();
MetadataRest<MetadataValueRest> metadata = new MetadataRest<>();
@Override
public String getId() {
@@ -56,11 +56,11 @@ public abstract class DSpaceObjectRest extends BaseObjectRest<String> {
*
* @return the metadata.
*/
public MetadataRest getMetadata() {
public MetadataRest<MetadataValueRest> getMetadata() {
return metadata;
}
public void setMetadata(MetadataRest metadata) {
public void setMetadata(MetadataRest<MetadataValueRest> metadata) {
this.metadata = metadata;
}

View File

@@ -19,10 +19,10 @@ import org.apache.commons.lang3.builder.HashCodeBuilder;
/**
* Rest representation of a map of metadata keys to ordered lists of values.
*/
public class MetadataRest {
public class MetadataRest<T extends MetadataValueRest> {
@JsonAnySetter
private SortedMap<String, List<MetadataValueRest>> map = new TreeMap();
private SortedMap<String, List<T>> map = new TreeMap();
/**
* Gets the map.
@@ -30,7 +30,7 @@ public class MetadataRest {
* @return the map of keys to ordered values.
*/
@JsonAnyGetter
public SortedMap<String, List<MetadataValueRest>> getMap() {
public SortedMap<String, List<T>> getMap() {
return map;
}
@@ -44,16 +44,16 @@ public class MetadataRest {
* they are passed to this method.
* @return this instance, to support chaining calls for easy initialization.
*/
public MetadataRest put(String key, MetadataValueRest... values) {
public MetadataRest put(String key, T... values) {
// determine highest explicitly ordered value
int highest = -1;
for (MetadataValueRest value : values) {
for (T value : values) {
if (value.getPlace() > highest) {
highest = value.getPlace();
}
}
// add any non-explicitly ordered values after highest
for (MetadataValueRest value : values) {
for (T value : values) {
if (value.getPlace() < 0) {
highest++;
value.setPlace(highest);

View File

@@ -0,0 +1,40 @@
/**
* 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.app.rest.model;
import com.fasterxml.jackson.annotation.JsonInclude;
/**
* This POJO represents a {@link MetadataValueRest} that will be placed inside a given
* {@link org.dspace.eperson.RegistrationData} that is coming directly from the REST controller.
*
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
**/
public class RegistrationMetadataRest extends MetadataValueRest {
@JsonInclude(JsonInclude.Include.NON_NULL)
private String overrides;
public RegistrationMetadataRest(String value, String overrides) {
super();
this.value = value;
this.overrides = overrides;
}
public RegistrationMetadataRest(String value) {
this(value, null);
}
public String getOverrides() {
return overrides;
}
public void setOverrides(String overrides) {
this.overrides = overrides;
}
}

View File

@@ -9,6 +9,7 @@ package org.dspace.app.rest.model;
import java.util.UUID;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.dspace.app.rest.RestResourceController;
@@ -24,11 +25,25 @@ public class RegistrationRest extends RestAddressableModel {
public static final String PLURAL_NAME = "registrations";
public static final String CATEGORY = EPERSON;
private Integer id;
private String email;
private UUID user;
private String registrationType;
private String netId;
@JsonInclude(JsonInclude.Include.NON_NULL)
private MetadataRest<RegistrationMetadataRest> registrationMetadata;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
/**
* Generic getter for the email
*
* @return the email value of this RegisterRest
*/
public String getEmail() {
@@ -37,7 +52,8 @@ public class RegistrationRest extends RestAddressableModel {
/**
* Generic setter for the email
* @param email The email to be set on this RegisterRest
*
* @param email The email to be set on this RegisterRest
*/
public void setEmail(String email) {
this.email = email;
@@ -45,6 +61,7 @@ public class RegistrationRest extends RestAddressableModel {
/**
* Generic getter for the user
*
* @return the user value of this RegisterRest
*/
public UUID getUser() {
@@ -53,12 +70,38 @@ public class RegistrationRest extends RestAddressableModel {
/**
* Generic setter for the user
* @param user The user to be set on this RegisterRest
*
* @param user The user to be set on this RegisterRest
*/
public void setUser(UUID user) {
this.user = user;
}
public String getRegistrationType() {
return registrationType;
}
public void setRegistrationType(String registrationType) {
this.registrationType = registrationType;
}
public String getNetId() {
return netId;
}
public void setNetId(String netId) {
this.netId = netId;
}
public MetadataRest<RegistrationMetadataRest> getRegistrationMetadata() {
return registrationMetadata;
}
public void setRegistrationMetadata(
MetadataRest<RegistrationMetadataRest> registrationMetadata) {
this.registrationMetadata = registrationMetadata;
}
@Override
public String getCategory() {
return CATEGORY;

View File

@@ -19,6 +19,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.app.rest.DiscoverableEndpointsService;
import org.dspace.app.rest.EPersonRegistrationRestController;
import org.dspace.app.rest.Parameter;
import org.dspace.app.rest.SearchRestMethod;
import org.dspace.app.rest.exception.DSpaceBadRequestException;
@@ -190,7 +191,7 @@ public class EPersonRestRepository extends DSpaceObjectRestRepository<EPerson, E
throw new DSpaceBadRequestException("The self registered property cannot be set to false using this method"
+ " with a token");
}
checkRequiredProperties(epersonRest);
checkRequiredProperties(registrationData, epersonRest);
// We'll turn off authorisation system because this call isn't admin based as it's token based
context.turnOffAuthorisationSystem();
EPerson ePerson = createEPersonFromRestObject(context, epersonRest);
@@ -203,8 +204,8 @@ public class EPersonRestRepository extends DSpaceObjectRestRepository<EPerson, E
return converter.toRest(ePerson, utils.obtainProjection());
}
private void checkRequiredProperties(EPersonRest epersonRest) {
MetadataRest metadataRest = epersonRest.getMetadata();
private void checkRequiredProperties(RegistrationData registration, EPersonRest epersonRest) {
MetadataRest<MetadataValueRest> metadataRest = epersonRest.getMetadata();
if (metadataRest != null) {
List<MetadataValueRest> epersonFirstName = metadataRest.getMap().get("eperson.firstname");
List<MetadataValueRest> epersonLastName = metadataRest.getMap().get("eperson.lastname");
@@ -213,10 +214,25 @@ public class EPersonRestRepository extends DSpaceObjectRestRepository<EPerson, E
throw new EPersonNameNotProvidedException();
}
}
String password = epersonRest.getPassword();
if (StringUtils.isBlank(password)) {
throw new DSpaceBadRequestException("A password is required");
String netId = epersonRest.getNetid();
if (StringUtils.isBlank(password) && StringUtils.isBlank(netId)) {
throw new DSpaceBadRequestException(
"You must provide a password or register using an external account"
);
}
if (StringUtils.isBlank(password) && !canRegisterExternalAccount(registration, epersonRest)) {
throw new DSpaceBadRequestException(
"Cannot register external account with netId: " + netId
);
}
}
private boolean canRegisterExternalAccount(RegistrationData registration, EPersonRest epersonRest) {
return accountService.isTokenValidForCreation(registration) &&
StringUtils.equals(registration.getNetId(), epersonRest.getNetid());
}
@Override
@@ -369,6 +385,40 @@ public class EPersonRestRepository extends DSpaceObjectRestRepository<EPerson, E
return EPersonRest.class;
}
/**
* This method tries to merge the details coming from the {@link EPersonRegistrationRestController} of a given
* {@code uuid} eperson. <br/>
*
* @param context - The Dspace Context
* @param uuid - The uuid of the eperson
* @param token - A valid registration token
* @param override - An optional list of metadata fields that will be overwritten
* @return a EPersonRest entity updated with the registration data.
* @throws AuthorizeException
*/
public EPersonRest mergeFromRegistrationData(
Context context, UUID uuid, String token, List<String> override
) throws AuthorizeException {
try {
if (uuid == null) {
throw new DSpaceBadRequestException("The uuid of the person cannot be null");
}
if (token == null) {
throw new DSpaceBadRequestException("You must provide a token for the eperson");
}
return converter.toRest(
accountService.mergeRegistration(context, uuid, token, override),
utils.obtainProjection()
);
} catch (SQLException e) {
log.error(e);
throw new RuntimeException(e);
}
}
@Override
public void afterPropertiesSet() throws Exception {
discoverableEndpointsService.register(this, Arrays.asList(

View File

@@ -16,6 +16,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.mail.MessagingException;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.ws.rs.BadRequestException;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -25,6 +26,10 @@ import org.dspace.app.rest.exception.DSpaceBadRequestException;
import org.dspace.app.rest.exception.RepositoryMethodNotImplementedException;
import org.dspace.app.rest.exception.UnprocessableEntityException;
import org.dspace.app.rest.model.RegistrationRest;
import org.dspace.app.rest.model.patch.Patch;
import org.dspace.app.rest.repository.patch.ResourcePatch;
import org.dspace.app.rest.repository.patch.operation.RegistrationEmailPatchOperation;
import org.dspace.app.rest.utils.Utils;
import org.dspace.app.util.AuthorizeUtil;
import org.dspace.authenticate.service.AuthenticationService;
import org.dspace.authorize.AuthorizeException;
@@ -32,6 +37,7 @@ import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.InvalidReCaptchaException;
import org.dspace.eperson.RegistrationData;
import org.dspace.eperson.RegistrationTypeEnum;
import org.dspace.eperson.service.AccountService;
import org.dspace.eperson.service.CaptchaService;
import org.dspace.eperson.service.EPersonService;
@@ -54,9 +60,10 @@ public class RegistrationRestRepository extends DSpaceRestRepository<Registratio
private static Logger log = LogManager.getLogger(RegistrationRestRepository.class);
public static final String TOKEN_QUERY_PARAM = "token";
public static final String TYPE_QUERY_PARAM = "accountRequestType";
public static final String TYPE_REGISTER = "register";
public static final String TYPE_FORGOT = "forgot";
public static final String TYPE_REGISTER = RegistrationTypeEnum.REGISTER.toString().toLowerCase();
public static final String TYPE_FORGOT = RegistrationTypeEnum.FORGOT.toString().toLowerCase();
@Autowired
private EPersonService ePersonService;
@@ -79,6 +86,12 @@ public class RegistrationRestRepository extends DSpaceRestRepository<Registratio
@Autowired
private RegistrationDataService registrationDataService;
@Autowired
private Utils utils;
@Autowired
private ResourcePatch<RegistrationData> resourcePatch;
@Autowired
private ObjectMapper mapper;
@@ -144,46 +157,42 @@ public class RegistrationRestRepository extends DSpaceRestRepository<Registratio
+ registrationRest.getEmail(), e);
}
} else if (accountType.equalsIgnoreCase(TYPE_REGISTER)) {
if (eperson == null) {
try {
String email = registrationRest.getEmail();
if (!AuthorizeUtil.authorizeNewAccountRegistration(context, request)) {
throw new AccessDeniedException(
"Registration is disabled, you are not authorized to create a new Authorization");
}
if (!authenticationService.canSelfRegister(context, request, email)) {
throw new UnprocessableEntityException(
String.format("Registration is not allowed with email address" +
try {
String email = registrationRest.getEmail();
if (!AuthorizeUtil.authorizeNewAccountRegistration(context, request)) {
throw new AccessDeniedException(
"Registration is disabled, you are not authorized to create a new Authorization");
}
if (!authenticationService.canSelfRegister(context, request, registrationRest.getEmail())) {
throw new UnprocessableEntityException(
String.format("Registration is not allowed with email address" +
" %s", email));
}
accountService.sendRegistrationInfo(context, email);
} catch (SQLException | IOException | MessagingException | AuthorizeException e) {
log.error("Something went wrong with sending registration info email: "
+ registrationRest.getEmail(), e);
}
} else {
// if an eperson with this email already exists then send "forgot password" email instead
try {
accountService.sendForgotPasswordInfo(context, registrationRest.getEmail());
} catch (SQLException | IOException | MessagingException | AuthorizeException e) {
log.error("Something went wrong with sending forgot password info email: "
accountService.sendRegistrationInfo(context, registrationRest.getEmail());
} catch (SQLException | IOException | MessagingException | AuthorizeException e) {
log.error("Something went wrong with sending registration info email: "
+ registrationRest.getEmail(), e);
}
} else {
// if an eperson with this email already exists then send "forgot password" email instead
try {
accountService.sendForgotPasswordInfo(context, registrationRest.getEmail());
} catch (SQLException | IOException | MessagingException | AuthorizeException e) {
log.error("Something went wrong with sending forgot password info email: "
+ registrationRest.getEmail(), e);
}
}
}
return null;
}
@Override
public Class<RegistrationRest> getDomainClass() {
return RegistrationRest.class;
}
/**
* This method will find the RegistrationRest object that is associated with the token given
*
* @param token The token to be found and for which a RegistrationRest object will be found
* @return A RegistrationRest object for the given token
* @throws SQLException If something goes wrong
* @return A RegistrationRest object for the given token
* @throws SQLException If something goes wrong
* @throws AuthorizeException If something goes wrong
*/
@SearchRestMethod(name = "findByToken")
@@ -194,17 +203,62 @@ public class RegistrationRestRepository extends DSpaceRestRepository<Registratio
if (registrationData == null) {
throw new ResourceNotFoundException("The token: " + token + " couldn't be found");
}
RegistrationRest registrationRest = new RegistrationRest();
registrationRest.setEmail(registrationData.getEmail());
EPerson ePerson = accountService.getEPerson(context, token);
if (ePerson != null) {
registrationRest.setUser(ePerson.getID());
return converter.toRest(registrationData, utils.obtainProjection());
}
private void validateToken(Context context, String token) {
try {
RegistrationData registrationData =
registrationDataService.findByToken(context, token);
if (registrationData == null || !registrationDataService.isValid(registrationData)) {
throw new AccessDeniedException("The token is invalid");
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
return registrationRest;
}
/**
* This method can be used to update a {@link RegistrationData} with a given {@code id} that has a valid
* {@code token} with the actions described in the {@link Patch} object.
* This method is used to patch the email value, and will generate a completely new {@code token} that will be
* sent with an email {@link RegistrationEmailPatchOperation}.
*
*/
@Override
public RegistrationRest patch(
HttpServletRequest request, String apiCategory, String model, Integer id, Patch patch
) throws UnprocessableEntityException, DSpaceBadRequestException {
if (id == null || id <= 0) {
throw new BadRequestException("The id of the registration cannot be null or negative");
}
if (patch == null || patch.getOperations() == null || patch.getOperations().isEmpty()) {
throw new BadRequestException("Patch request is incomplete: cannot find operations");
}
String token = request.getParameter("token");
if (token == null || token.trim().isBlank()) {
throw new AccessDeniedException("The token is required");
}
Context context = obtainContext();
validateToken(context, token);
try {
resourcePatch.patch(context, registrationDataService.find(context, id), patch.getOperations());
context.commit();
} catch (SQLException e) {
throw new RuntimeException(e.getMessage(), e);
}
return null;
}
public void setCaptchaService(CaptchaService captchaService) {
this.captchaService = captchaService;
}
@Override
public Class<RegistrationRest> getDomainClass() {
return RegistrationRest.class;
}
}

View File

@@ -0,0 +1,166 @@
/**
* 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.app.rest.repository.patch.operation;
import java.sql.SQLException;
import java.text.MessageFormat;
import java.util.Optional;
import com.fasterxml.jackson.databind.JsonNode;
import org.dspace.app.rest.exception.DSpaceBadRequestException;
import org.dspace.app.rest.exception.UnprocessableEntityException;
import org.dspace.app.rest.model.patch.JsonValueEvaluator;
import org.dspace.app.rest.model.patch.Operation;
import org.dspace.authorize.AuthorizeException;
import org.dspace.core.Context;
import org.dspace.eperson.RegistrationData;
import org.dspace.eperson.RegistrationTypeEnum;
import org.dspace.eperson.dto.RegistrationDataChanges;
import org.dspace.eperson.dto.RegistrationDataPatch;
import org.dspace.eperson.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Implementation for RegistrationData email patches.
*
* Example: <code>
* curl -X PATCH http://${dspace.server.url}/api/eperson/registration/<:registration-id>?token=<:token> -H "
* Content-Type: application/json" -d '[{ "op": "replace", "path": "/email", "value": "new@email"]'
* </code>
*
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
**/
@Component
public class RegistrationEmailPatchOperation<R extends RegistrationData> extends PatchOperation<R> {
/**
* Path in json body of patch that uses this operation
*/
private static final String OPERATION_PATH_EMAIL = "/email";
@Autowired
private AccountService accountService;
@Override
public R perform(Context context, R object, Operation operation) {
checkOperationValue(operation.getValue());
RegistrationDataPatch registrationDataPatch;
try {
String email = getTextValue(operation);
registrationDataPatch =
new RegistrationDataPatch(
object,
new RegistrationDataChanges(
email,
registrationTypeFor(context, object, email)
)
);
} catch (IllegalArgumentException e) {
throw new UnprocessableEntityException(
"Cannot perform the patch operation",
e
);
} catch (SQLException e) {
throw new RuntimeException(e);
}
if (!supports(object, operation)) {
throw new UnprocessableEntityException(
MessageFormat.format(
"RegistrationEmailReplaceOperation does not support {0} operation",
operation.getOp()
)
);
}
if (!isOperationAllowed(operation, object)) {
throw new UnprocessableEntityException(
MessageFormat.format(
"Attempting to perform {0} operation over {1} value (e-mail).",
operation.getOp(),
object.getEmail() == null ? "null" : "not null"
)
);
}
try {
return (R) accountService.renewRegistrationForEmail(context, registrationDataPatch);
} catch (AuthorizeException e) {
throw new DSpaceBadRequestException(
MessageFormat.format(
"Cannot perform {0} operation over {1} value (e-mail).",
operation.getOp(),
object.getEmail() == null ? "null" : "not null"
),
e
);
}
}
private static String getTextValue(Operation operation) {
Object value = operation.getValue();
if (value instanceof String) {
return ((String) value);
}
if (value instanceof JsonValueEvaluator) {
return Optional.of((JsonValueEvaluator) value)
.map(JsonValueEvaluator::getValueNode)
.filter(nodes -> !nodes.isEmpty())
.map(nodes -> nodes.get(0))
.map(JsonNode::asText)
.orElseThrow(() -> new DSpaceBadRequestException("No value provided for operation"));
}
throw new DSpaceBadRequestException("Invalid patch value for operation!");
}
private RegistrationTypeEnum registrationTypeFor(
Context context, R object, String email
)
throws SQLException {
if (accountService.existsAccountWithEmail(context, email)) {
return RegistrationTypeEnum.VALIDATION_ORCID;
}
return object.getRegistrationType();
}
/**
* Checks whether the email of RegistrationData has an existing value to replace or adds a new value.
*
* @param operation operation to check
* @param registrationData Object on which patch is being done
*/
private boolean isOperationAllowed(Operation operation, RegistrationData registrationData) {
return isReplaceOperationAllowed(operation, registrationData) ||
isAddOperationAllowed(operation, registrationData);
}
private boolean isAddOperationAllowed(Operation operation, RegistrationData registrationData) {
return operation.getOp().trim().equalsIgnoreCase(OPERATION_ADD) && registrationData.getEmail() == null;
}
private static boolean isReplaceOperationAllowed(Operation operation, RegistrationData registrationData) {
return operation.getOp().trim().equalsIgnoreCase(OPERATION_REPLACE) && registrationData.getEmail() != null;
}
@Override
public boolean supports(Object objectToMatch, Operation operation) {
return (objectToMatch instanceof RegistrationData &&
(
operation.getOp().trim().equalsIgnoreCase(OPERATION_REPLACE) ||
operation.getOp().trim().equalsIgnoreCase(OPERATION_ADD)
) &&
operation.getPath().trim().equalsIgnoreCase(OPERATION_PATH_EMAIL));
}
}

View File

@@ -7,7 +7,12 @@
*/
package org.dspace.app.rest.security;
import static org.dspace.authenticate.OrcidAuthenticationBean.ORCID_AUTH_ATTRIBUTE;
import static org.dspace.authenticate.OrcidAuthenticationBean.ORCID_DEFAULT_REGISTRATION_URL;
import static org.dspace.authenticate.OrcidAuthenticationBean.ORCID_REGISTRATION_TOKEN;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import jakarta.servlet.FilterChain;
@@ -45,7 +50,8 @@ public class OrcidLoginFilter extends StatelessLoginFilter {
private ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
private OrcidAuthenticationBean orcidAuthentication = new DSpace().getServiceManager()
.getServiceByName("orcidAuthentication", OrcidAuthenticationBean.class);
.getServiceByName("orcidAuthentication",
OrcidAuthenticationBean.class);
public OrcidLoginFilter(String url, String httpMethod, AuthenticationManager authenticationManager,
RestAuthenticationService restAuthenticationService) {
@@ -66,13 +72,13 @@ public class OrcidLoginFilter extends StatelessLoginFilter {
@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
Authentication auth) throws IOException, ServletException {
Authentication auth) throws IOException, ServletException {
DSpaceAuthentication dSpaceAuthentication = (DSpaceAuthentication) auth;
log.debug("Orcid authentication successful for EPerson {}. Sending back temporary auth cookie",
dSpaceAuthentication.getName());
dSpaceAuthentication.getName());
restAuthenticationService.addAuthenticationDataForUser(req, res, dSpaceAuthentication, true);
@@ -81,26 +87,41 @@ public class OrcidLoginFilter extends StatelessLoginFilter {
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
AuthenticationException failed) throws IOException, ServletException {
Context context = ContextUtil.obtainContext(request);
if (orcidAuthentication.isUsed(context, request)) {
String baseRediredirectUrl = configurationService.getProperty("dspace.ui.url");
String redirectUrl = baseRediredirectUrl + "/error?status=401&code=orcid.generic-error";
response.sendRedirect(redirectUrl); // lgtm [java/unvalidated-url-redirection]
} else {
if (!orcidAuthentication.isUsed(context, request)) {
super.unsuccessfulAuthentication(request, response, failed);
return;
}
String baseRediredirectUrl = configurationService.getProperty("dspace.ui.url");
String redirectUrl = baseRediredirectUrl + "/error?status=401&code=orcid.generic-error";
Object registrationToken = request.getAttribute(ORCID_REGISTRATION_TOKEN);
if (registrationToken != null) {
final String orcidRegistrationDataUrl =
configurationService.getProperty("orcid.registration-data.url", ORCID_DEFAULT_REGISTRATION_URL);
redirectUrl = baseRediredirectUrl + MessageFormat.format(orcidRegistrationDataUrl, registrationToken);
if (log.isDebugEnabled()) {
log.debug(
"Orcid authentication failed for user with ORCID {}.",
request.getAttribute(ORCID_AUTH_ATTRIBUTE)
);
log.debug("Redirecting to {} for registration completion.", redirectUrl);
}
}
response.sendRedirect(redirectUrl); // lgtm [java/unvalidated-url-redirection]
}
/**
* After successful login, redirect to the DSpace URL specified by this Orcid
* request (in the "redirectUrl" request parameter). If that 'redirectUrl' is
* not valid or trusted for this DSpace site, then return a 400 error.
* @param request
* @param response
*
* @param request
* @param response
* @throws IOException
*/
private void redirectAfterSuccess(HttpServletRequest request, HttpServletResponse response) throws IOException {
@@ -128,9 +149,9 @@ public class OrcidLoginFilter extends StatelessLoginFilter {
response.sendRedirect(redirectUrl);
} else {
log.error("Invalid Orcid redirectURL=" + redirectUrl +
". URL doesn't match hostname of server or UI!");
". URL doesn't match hostname of server or UI!");
response.sendError(HttpServletResponse.SC_BAD_REQUEST,
"Invalid redirectURL! Must match server or ui hostname.");
"Invalid redirectURL! Must match server or ui hostname.");
}
}

View File

@@ -32,10 +32,13 @@ import org.springframework.context.annotation.Configuration;
"org.dspace.app.rest.converter",
"org.dspace.app.rest.repository",
"org.dspace.app.rest.utils",
"org.dspace.app.rest.link",
"org.dspace.app.rest.converter.factory",
"org.dspace.app.configuration",
"org.dspace.iiif",
"org.dspace.app.iiif",
"org.dspace.app.ldn"
"org.dspace.app.ldn",
"org.dspace.app.scheduler"
})
public class ApplicationConfig {
// Allowed CORS origins ("Access-Control-Allow-Origin" header)

View File

@@ -0,0 +1,60 @@
/**
* 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.app.scheduler.eperson;
import java.sql.SQLException;
import org.dspace.core.Context;
import org.dspace.eperson.RegistrationData;
import org.dspace.eperson.service.RegistrationDataService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
/**
* Contains all the schedulable task related to {@link RegistrationData} entities.
* Can be enabled via the configuration property {@code eperson.registration-data.scheduler.enabled}
*
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
**/
@Service
@ConditionalOnProperty(prefix = "eperson.registration-data.scheduler", name = "enabled", havingValue = "true")
public class RegistrationDataScheduler {
private static final Logger log = LoggerFactory.getLogger(RegistrationDataScheduler.class);
@Autowired
private RegistrationDataService registrationDataService;
/**
* Deletes expired {@link RegistrationData}.
* This task is scheduled to be run by the cron expression defined in the configuration file.
*
*/
@Scheduled(cron = "${eperson.registration-data.scheduler.expired-registration-data.cron:-}")
protected void deleteExpiredRegistrationData() throws SQLException {
Context context = new Context();
context.turnOffAuthorisationSystem();
try {
registrationDataService.deleteExpiredRegistrations(context);
context.restoreAuthSystemState();
context.complete();
} catch (Exception e) {
context.abort();
log.error("Failed to delete expired registrations", e);
throw e;
}
}
}

View File

@@ -0,0 +1,404 @@
/**
* 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.app.rest;
import static org.hamcrest.Matchers.equalTo;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.dspace.app.rest.matcher.MetadataMatcher;
import org.dspace.app.rest.test.AbstractControllerIntegrationTest;
import org.dspace.builder.EPersonBuilder;
import org.dspace.content.MetadataField;
import org.dspace.content.service.MetadataFieldService;
import org.dspace.core.Email;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.RegistrationData;
import org.dspace.eperson.RegistrationTypeEnum;
import org.dspace.eperson.dto.RegistrationDataChanges;
import org.dspace.eperson.dto.RegistrationDataPatch;
import org.dspace.eperson.service.AccountService;
import org.dspace.eperson.service.RegistrationDataService;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
/**
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
**/
public class EPersonRegistrationRestControllerIT extends AbstractControllerIntegrationTest {
private static MockedStatic<Email> emailMockedStatic;
@Autowired
private AccountService accountService;
@Autowired
private RegistrationDataService registrationDataService;
@Autowired
private MetadataFieldService metadataFieldService;
private RegistrationData orcidRegistration;
private MetadataField orcidMf;
private MetadataField firstNameMf;
private MetadataField lastNameMf;
private EPerson customEPerson;
private String customPassword;
@BeforeClass
public static void init() throws Exception {
emailMockedStatic = Mockito.mockStatic(Email.class);
}
@AfterClass
public static void tearDownClass() throws Exception {
emailMockedStatic.close();
}
@Before
public void setUp() throws Exception {
super.setUp();
context.turnOffAuthorisationSystem();
orcidRegistration =
registrationDataService.create(context, "0000-0000-0000-0000", RegistrationTypeEnum.ORCID);
orcidMf =
metadataFieldService.findByElement(context, "eperson", "orcid", null);
firstNameMf =
metadataFieldService.findByElement(context, "eperson", "firstname", null);
lastNameMf =
metadataFieldService.findByElement(context, "eperson", "lastname", null);
registrationDataService.addMetadata(
context, orcidRegistration, orcidMf, "0000-0000-0000-0000"
);
registrationDataService.addMetadata(
context, orcidRegistration, firstNameMf, "Vincenzo"
);
registrationDataService.addMetadata(
context, orcidRegistration, lastNameMf, "Mecca"
);
registrationDataService.update(context, orcidRegistration);
customPassword = "vins-01";
customEPerson =
EPersonBuilder.createEPerson(context)
.withEmail("vins-01@fake.mail")
.withNameInMetadata("Vins", "4Science")
.withPassword(customPassword)
.withCanLogin(true)
.build();
context.restoreAuthSystemState();
}
@After
public void destroy() throws Exception {
RegistrationData found = context.reloadEntity(orcidRegistration);
if (found != null) {
this.registrationDataService.delete(context, found);
}
super.destroy();
}
@Test
public void givenOrcidToken_whenPostForMerge_thenUnauthorized() throws Exception {
getClient().perform(
post("/api/eperson/epersons/" + customEPerson.getID())
.param("token", orcidRegistration.getToken())
.param("override", "eperson.firtname,eperson.lastname,eperson.orcid")
).andExpect(status().isUnauthorized());
}
@Test
public void givenExpiredToken_whenPostForMerge_thenUnauthorized() throws Exception {
context.turnOffAuthorisationSystem();
registrationDataService.markAsExpired(context, orcidRegistration);
context.restoreAuthSystemState();
getClient().perform(
post("/api/eperson/epersons/" + customEPerson.getID())
.param("token", orcidRegistration.getToken())
.param("override", "eperson.firtname,eperson.lastname,eperson.orcid")
).andExpect(status().isUnauthorized());
}
@Test
public void givenExpiredToken_whenPostAuthForMerge_thenForbidden() throws Exception {
context.turnOffAuthorisationSystem();
registrationDataService.markAsExpired(context, orcidRegistration);
context.restoreAuthSystemState();
String tokenAdmin = getAuthToken(admin.getEmail(), password);
getClient(tokenAdmin).perform(
post("/api/eperson/epersons/" + customEPerson.getID())
.param("token", orcidRegistration.getToken())
.param("override", "eperson.firtname,eperson.lastname,eperson.orcid")
).andExpect(status().isForbidden());
}
@Test
public void givenValidationRegistration_whenPostAuthDiffersFromIdPathParam_thenForbidden() throws Exception {
context.turnOffAuthorisationSystem();
RegistrationData validationRegistration =
registrationDataService.create(context, "0000-0000-0000-0000", RegistrationTypeEnum.VALIDATION_ORCID);
context.restoreAuthSystemState();
try {
String tokenAdmin = getAuthToken(admin.getEmail(), password);
getClient(tokenAdmin).perform(
post("/api/eperson/epersons/" + customEPerson.getID())
.param("token", validationRegistration.getToken())
).andExpect(status().isForbidden());
} finally {
RegistrationData found = context.reloadEntity(validationRegistration);
if (found != null) {
this.registrationDataService.delete(context, found);
}
}
}
@Test
public void givenValidationRegistration_whenPostWithoutOverride_thenCreated() throws Exception {
Email spy = Mockito.spy(Email.class);
doNothing().when(spy).send();
emailMockedStatic.when(() -> Email.getEmail(any())).thenReturn(spy);
context.turnOffAuthorisationSystem();
RegistrationDataChanges changes =
new RegistrationDataChanges("vins-01@fake.mail", RegistrationTypeEnum.VALIDATION_ORCID);
RegistrationData validationRegistration =
this.accountService.renewRegistrationForEmail(
context, new RegistrationDataPatch(orcidRegistration, changes)
);
context.restoreAuthSystemState();
try {
String customToken = getAuthToken(customEPerson.getEmail(), customPassword);
getClient(customToken).perform(
post("/api/eperson/epersons/" + customEPerson.getID())
.param("token", validationRegistration.getToken())
).andExpect(status().isCreated());
} finally {
RegistrationData found = context.reloadEntity(validationRegistration);
if (found != null) {
this.registrationDataService.delete(context, found);
}
}
}
@Test
public void givenValidationRegistration_whenPostWithOverride_thenCreated() throws Exception {
Email spy = Mockito.spy(Email.class);
doNothing().when(spy).send();
emailMockedStatic.when(() -> Email.getEmail(any())).thenReturn(spy);
context.turnOffAuthorisationSystem();
RegistrationDataChanges changes =
new RegistrationDataChanges("vins-01@fake.mail", RegistrationTypeEnum.VALIDATION_ORCID);
RegistrationData validationRegistration =
this.accountService.renewRegistrationForEmail(
context, new RegistrationDataPatch(orcidRegistration, changes)
);
context.restoreAuthSystemState();
try {
String customToken = getAuthToken(customEPerson.getEmail(), customPassword);
getClient(customToken).perform(
post("/api/eperson/epersons/" + customEPerson.getID())
.param("token", validationRegistration.getToken())
.param("override", "eperson.firstname,eperson.lastname")
).andExpect(status().isCreated());
} finally {
RegistrationData found = context.reloadEntity(validationRegistration);
if (found != null) {
this.registrationDataService.delete(context, found);
}
}
}
@Test
public void givenValidationRegistration_whenPostWithoutOverride_thenOnlyNewMetadataAdded() throws Exception {
Email spy = Mockito.spy(Email.class);
doNothing().when(spy).send();
emailMockedStatic.when(() -> Email.getEmail(any())).thenReturn(spy);
context.turnOffAuthorisationSystem();
RegistrationDataChanges changes =
new RegistrationDataChanges("vins-01@fake.mail", RegistrationTypeEnum.VALIDATION_ORCID);
RegistrationData validationRegistration =
this.accountService.renewRegistrationForEmail(
context, new RegistrationDataPatch(orcidRegistration, changes)
);
context.restoreAuthSystemState();
try {
String customToken = getAuthToken(customEPerson.getEmail(), customPassword);
getClient(customToken).perform(
post("/api/eperson/epersons/" + customEPerson.getID())
.param("token", validationRegistration.getToken())
).andExpect(status().isCreated())
.andExpect(
jsonPath("$.netid", equalTo("0000-0000-0000-0000"))
)
.andExpect(
jsonPath("$.metadata",
Matchers.allOf(
MetadataMatcher.matchMetadata("eperson.firstname", "Vins"),
MetadataMatcher.matchMetadata("eperson.lastname", "4Science"),
MetadataMatcher.matchMetadata("eperson.orcid", "0000-0000-0000-0000")
)
)
);
} finally {
RegistrationData found = context.reloadEntity(validationRegistration);
if (found != null) {
this.registrationDataService.delete(context, found);
}
}
}
@Test
public void givenValidationRegistration_whenPostWithOverride_thenMetadataReplaced() throws Exception {
Email spy = Mockito.spy(Email.class);
doNothing().when(spy).send();
emailMockedStatic.when(() -> Email.getEmail(any())).thenReturn(spy);
context.turnOffAuthorisationSystem();
RegistrationDataChanges changes =
new RegistrationDataChanges("vins-01@fake.mail", RegistrationTypeEnum.VALIDATION_ORCID);
RegistrationData validationRegistration =
this.accountService.renewRegistrationForEmail(
context, new RegistrationDataPatch(orcidRegistration, changes)
);
context.restoreAuthSystemState();
try {
String customToken = getAuthToken(customEPerson.getEmail(), customPassword);
getClient(customToken).perform(
post("/api/eperson/epersons/" + customEPerson.getID())
.param("token", validationRegistration.getToken())
.param("override", "eperson.firstname,eperson.lastname")
)
.andExpect(status().isCreated())
.andExpect(
jsonPath("$.netid", equalTo("0000-0000-0000-0000"))
)
.andExpect(
jsonPath("$.metadata",
Matchers.allOf(
MetadataMatcher.matchMetadata("eperson.firstname", "Vincenzo"),
MetadataMatcher.matchMetadata("eperson.lastname", "Mecca"),
MetadataMatcher.matchMetadata("eperson.orcid", "0000-0000-0000-0000")
)
)
);
} finally {
RegistrationData found = context.reloadEntity(validationRegistration);
if (found != null) {
this.registrationDataService.delete(context, found);
}
}
}
@Test
public void givenValidationRegistration_whenPostWithOverrideAndMetadataNotFound_thenBadRequest() throws Exception {
Email spy = Mockito.spy(Email.class);
doNothing().when(spy).send();
emailMockedStatic.when(() -> Email.getEmail(any())).thenReturn(spy);
context.turnOffAuthorisationSystem();
RegistrationDataChanges changes =
new RegistrationDataChanges("vins-01@fake.mail", RegistrationTypeEnum.VALIDATION_ORCID);
RegistrationData validationRegistration =
this.accountService.renewRegistrationForEmail(
context, new RegistrationDataPatch(orcidRegistration, changes)
);
try {
context.restoreAuthSystemState();
String customToken = getAuthToken(customEPerson.getEmail(), customPassword);
getClient(customToken).perform(
post("/api/eperson/epersons/" + customEPerson.getID())
.param("token", validationRegistration.getToken())
.param("override", "eperson.phone")
).andExpect(status().isBadRequest());
context.turnOffAuthorisationSystem();
MetadataField phoneMf =
metadataFieldService.findByElement(context, "eperson", "phone", null);
registrationDataService.addMetadata(
context, validationRegistration, phoneMf, "1234567890"
);
context.restoreAuthSystemState();
getClient(customToken).perform(
post("/api/eperson/epersons/" + customEPerson.getID())
.param("token", validationRegistration.getToken())
.param("override", "eperson.phone")
).andExpect(status().isBadRequest());
} finally {
RegistrationData found = context.reloadEntity(validationRegistration);
if (found != null) {
this.registrationDataService.delete(context, found);
}
}
}
}

View File

@@ -36,6 +36,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.sql.SQLException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
@@ -65,6 +66,7 @@ import org.dspace.app.rest.model.patch.Operation;
import org.dspace.app.rest.model.patch.ReplaceOperation;
import org.dspace.app.rest.test.AbstractControllerIntegrationTest;
import org.dspace.app.rest.test.MetadataPatchSuite;
import org.dspace.authorize.AuthorizeException;
import org.dspace.builder.CollectionBuilder;
import org.dspace.builder.CommunityBuilder;
import org.dspace.builder.EPersonBuilder;
@@ -72,10 +74,14 @@ import org.dspace.builder.GroupBuilder;
import org.dspace.builder.WorkflowItemBuilder;
import org.dspace.content.Collection;
import org.dspace.content.Community;
import org.dspace.content.MetadataField;
import org.dspace.content.service.MetadataFieldService;
import org.dspace.core.I18nUtil;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
import org.dspace.eperson.PasswordHash;
import org.dspace.eperson.RegistrationData;
import org.dspace.eperson.RegistrationTypeEnum;
import org.dspace.eperson.service.AccountService;
import org.dspace.eperson.service.EPersonService;
import org.dspace.eperson.service.GroupService;
@@ -102,6 +108,9 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest {
@Autowired
private ConfigurationService configurationService;
@Autowired
private MetadataFieldService metadataFieldService;
@Autowired
private ObjectMapper mapper;
@@ -3176,6 +3185,138 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest {
}
}
@Test
public void postEpersonFromOrcidRegistrationToken() throws Exception {
context.turnOffAuthorisationSystem();
String registrationEmail = "vins-01@fake.mail";
RegistrationData orcidRegistration =
createRegistrationData(RegistrationTypeEnum.ORCID, registrationEmail);
context.restoreAuthSystemState();
ObjectMapper mapper = new ObjectMapper();
EPersonRest ePersonRest = new EPersonRest();
MetadataRest metadataRest = new MetadataRest();
ePersonRest.setEmail(registrationEmail);
ePersonRest.setCanLogIn(true);
ePersonRest.setNetid(orcidRegistration.getNetId());
MetadataValueRest surname = new MetadataValueRest();
surname.setValue("Doe");
metadataRest.put("eperson.lastname", surname);
MetadataValueRest firstname = new MetadataValueRest();
firstname.setValue("John");
metadataRest.put("eperson.firstname", firstname);
ePersonRest.setMetadata(metadataRest);
AtomicReference<UUID> idRef = new AtomicReference<UUID>();
try {
getClient().perform(post("/api/eperson/epersons")
.param("token", orcidRegistration.getToken())
.content(mapper.writeValueAsBytes(ePersonRest))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isCreated())
.andDo(result -> idRef
.set(UUID.fromString(read(result.getResponse().getContentAsString(), "$.id"))));
} finally {
EPersonBuilder.deleteEPerson(idRef.get());
}
}
@Test
public void postEPersonFromOrcidValidationRegistrationToken() throws Exception {
context.turnOffAuthorisationSystem();
String registrationEmail = "vins-01@fake.mail";
RegistrationData orcidRegistration =
createRegistrationData(RegistrationTypeEnum.VALIDATION_ORCID, registrationEmail);
context.restoreAuthSystemState();
ObjectMapper mapper = new ObjectMapper();
EPersonRest ePersonRest = createEPersonRest(registrationEmail, orcidRegistration.getNetId());
AtomicReference<UUID> idRef = new AtomicReference<>();
try {
getClient().perform(post("/api/eperson/epersons")
.param("token", orcidRegistration.getToken())
.content(mapper.writeValueAsBytes(ePersonRest))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isCreated())
.andExpect(jsonPath("$", Matchers.allOf(
hasJsonPath("$.uuid", not(empty())),
// is it what you expect? EPerson.getName() returns the email...
//hasJsonPath("$.name", is("Doe John")),
hasJsonPath("$.email", is(registrationEmail)),
hasJsonPath("$.type", is("eperson")),
hasJsonPath("$.netid", is("0000-0000-0000-0000")),
hasJsonPath("$._links.self.href", not(empty())),
hasJsonPath("$.metadata", Matchers.allOf(
matchMetadata("eperson.firstname", "Vincenzo"),
matchMetadata("eperson.lastname", "Mecca"),
matchMetadata("eperson.orcid", "0000-0000-0000-0000")
)))))
.andDo(result -> idRef
.set(UUID.fromString(read(result.getResponse().getContentAsString(), "$.id"))));
} finally {
EPersonBuilder.deleteEPerson(idRef.get());
}
}
@Test
public void postEpersonNetIdWithoutPasswordNotExternalRegistrationToken() throws Exception {
ObjectMapper mapper = new ObjectMapper();
String newRegisterEmail = "new-register@fake-email.com";
RegistrationRest registrationRest = new RegistrationRest();
registrationRest.setEmail(newRegisterEmail);
registrationRest.setNetId("0000-0000-0000-0000");
getClient().perform(post("/api/eperson/registrations")
.param(TYPE_QUERY_PARAM, TYPE_REGISTER)
.contentType(MediaType.APPLICATION_JSON)
.content(mapper.writeValueAsBytes(registrationRest)))
.andExpect(status().isCreated());
RegistrationData byEmail = registrationDataService.findByEmail(context, newRegisterEmail);
String newRegisterToken = byEmail.getToken();
EPersonRest ePersonRest = new EPersonRest();
MetadataRest metadataRest = new MetadataRest();
ePersonRest.setEmail(newRegisterEmail);
ePersonRest.setCanLogIn(true);
ePersonRest.setNetid("0000-0000-0000-0000");
MetadataValueRest surname = new MetadataValueRest();
surname.setValue("Doe");
metadataRest.put("eperson.lastname", surname);
MetadataValueRest firstname = new MetadataValueRest();
firstname.setValue("John");
metadataRest.put("eperson.firstname", firstname);
ePersonRest.setMetadata(metadataRest);
String token = getAuthToken(admin.getEmail(), password);
try {
getClient().perform(post("/api/eperson/epersons")
.param("token", newRegisterToken)
.content(mapper.writeValueAsBytes(ePersonRest))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest());
} finally {
context.turnOffAuthorisationSystem();
registrationDataService.delete(context, byEmail);
context.restoreAuthSystemState();
}
}
@Test
public void findByMetadataByCommAdminAndByColAdminTest() throws Exception {
context.turnOffAuthorisationSystem();
@@ -3732,4 +3873,51 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest {
}
private static EPersonRest createEPersonRest(String registrationEmail, String netId) {
EPersonRest ePersonRest = new EPersonRest();
MetadataRest metadataRest = new MetadataRest();
ePersonRest.setEmail(registrationEmail);
ePersonRest.setCanLogIn(true);
ePersonRest.setNetid(netId);
MetadataValueRest surname = new MetadataValueRest();
surname.setValue("Mecca");
metadataRest.put("eperson.lastname", surname);
MetadataValueRest firstname = new MetadataValueRest();
firstname.setValue("Vincenzo");
metadataRest.put("eperson.firstname", firstname);
MetadataValueRest orcid = new MetadataValueRest();
orcid.setValue("0000-0000-0000-0000");
metadataRest.put("eperson.orcid", orcid);
ePersonRest.setMetadata(metadataRest);
return ePersonRest;
}
private RegistrationData createRegistrationData(RegistrationTypeEnum validationOrcid, String registrationEmail)
throws SQLException, AuthorizeException {
RegistrationData orcidRegistration =
registrationDataService.create(context, "0000-0000-0000-0000", validationOrcid);
orcidRegistration.setEmail(registrationEmail);
MetadataField orcidMf =
metadataFieldService.findByElement(context, "eperson", "orcid", null);
MetadataField firstNameMf =
metadataFieldService.findByElement(context, "eperson", "firstname", null);
MetadataField lastNameMf =
metadataFieldService.findByElement(context, "eperson", "lastname", null);
registrationDataService.addMetadata(
context, orcidRegistration, orcidMf, "0000-0000-0000-0000"
);
registrationDataService.addMetadata(
context, orcidRegistration, firstNameMf, "Vincenzo"
);
registrationDataService.addMetadata(
context, orcidRegistration, lastNameMf, "Mecca"
);
registrationDataService.update(context, orcidRegistration);
return orcidRegistration;
}
}

View File

@@ -10,9 +10,11 @@ package org.dspace.app.rest;
import static java.util.Arrays.asList;
import static org.dspace.app.matcher.MetadataValueMatcher.with;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.emptyString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -21,6 +23,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
@@ -29,11 +32,14 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import java.sql.SQLException;
import java.text.ParseException;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.jayway.jsonpath.JsonPath;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jwt.SignedJWT;
import jakarta.servlet.http.Cookie;
import org.dspace.app.rest.matcher.MetadataMatcher;
import org.dspace.app.rest.model.AuthnRest;
import org.dspace.app.rest.security.OrcidLoginFilter;
import org.dspace.app.rest.security.jwt.EPersonClaimProvider;
@@ -46,14 +52,16 @@ import org.dspace.content.Community;
import org.dspace.content.Item;
import org.dspace.content.service.ItemService;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.RegistrationTypeEnum;
import org.dspace.eperson.service.EPersonService;
import org.dspace.eperson.service.RegistrationDataService;
import org.dspace.orcid.OrcidToken;
import org.dspace.orcid.client.OrcidClient;
import org.dspace.orcid.exception.OrcidClientException;
import org.dspace.orcid.model.OrcidTokenResponseDTO;
import org.dspace.orcid.service.OrcidTokenService;
import org.dspace.services.ConfigurationService;
import org.dspace.util.UUIDUtils;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -104,6 +112,9 @@ public class OrcidLoginFilterIT extends AbstractControllerIntegrationTest {
@Autowired
private OrcidTokenService orcidTokenService;
@Autowired
private RegistrationDataService registrationDataService;
@Before
public void setup() {
originalOrcidClient = orcidAuthentication.getOrcidClient();
@@ -137,45 +148,76 @@ public class OrcidLoginFilterIT extends AbstractControllerIntegrationTest {
@Test
public void testEPersonCreationViaOrcidLogin() throws Exception {
when(orcidClientMock.getAccessToken(CODE)).thenReturn(buildOrcidTokenResponse(ORCID, ACCESS_TOKEN));
when(orcidClientMock.getPerson(ACCESS_TOKEN, ORCID)).thenReturn(buildPerson("Test", "User", "test@email.it"));
String defaultProp = configurationService.getProperty("orcid.registration-data.url");
configurationService.setProperty("orcid.registration-data.url", "/test-redirect?random-token={0}");
try {
when(orcidClientMock.getAccessToken(CODE)).thenReturn(buildOrcidTokenResponse(ORCID, ACCESS_TOKEN));
when(orcidClientMock.getPerson(ACCESS_TOKEN, ORCID)).thenReturn(
buildPerson("Test", "User", "test@email.it"));
MvcResult mvcResult = getClient().perform(get("/api/" + AuthnRest.CATEGORY + "/orcid")
.param("code", CODE))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl(configurationService.getProperty("dspace.ui.url")))
.andExpect(cookie().exists("Authorization-cookie"))
.andReturn();
MvcResult mvcResult =
getClient().perform(get("/api/" + AuthnRest.CATEGORY + "/orcid").param("code", CODE))
.andExpect(status().is3xxRedirection())
.andReturn();
verify(orcidClientMock).getAccessToken(CODE);
verify(orcidClientMock).getPerson(ACCESS_TOKEN, ORCID);
verifyNoMoreInteractions(orcidClientMock);
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
assertThat(redirectedUrl, not(emptyString()));
String ePersonId = getEPersonIdFromAuthorizationCookie(mvcResult);
verify(orcidClientMock).getAccessToken(CODE);
verify(orcidClientMock).getPerson(ACCESS_TOKEN, ORCID);
verifyNoMoreInteractions(orcidClientMock);
createdEperson = ePersonService.find(context, UUIDUtils.fromString(ePersonId));
assertThat(createdEperson, notNullValue());
assertThat(createdEperson.getEmail(), equalTo("test@email.it"));
assertThat(createdEperson.getFullName(), equalTo("Test User"));
assertThat(createdEperson.getNetid(), equalTo(ORCID));
assertThat(createdEperson.canLogIn(), equalTo(true));
assertThat(createdEperson.getMetadata(), hasItem(with("eperson.orcid", ORCID)));
assertThat(createdEperson.getMetadata(), hasItem(with("eperson.orcid.scope", ORCID_SCOPES[0], 0)));
assertThat(createdEperson.getMetadata(), hasItem(with("eperson.orcid.scope", ORCID_SCOPES[1], 1)));
final Pattern pattern = Pattern.compile("test-redirect\\?random-token=([a-zA-Z0-9]+)");
final Matcher matcher = pattern.matcher(redirectedUrl);
matcher.find();
assertThat(getOrcidAccessToken(createdEperson), is(ACCESS_TOKEN));
assertThat(matcher.groupCount(), is(1));
assertThat(matcher.group(1), not(emptyString()));
String rdToken = matcher.group(1);
getClient().perform(get("/api/eperson/registrations/search/findByToken")
.param("token", rdToken))
.andExpect(status().is2xxSuccessful())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.netId", equalTo(ORCID)))
.andExpect(jsonPath("$.registrationType", equalTo(RegistrationTypeEnum.ORCID.toString())))
.andExpect(jsonPath("$.email", equalTo("test@email.it")))
.andExpect(
jsonPath("$.registrationMetadata",
Matchers.allOf(
MetadataMatcher.matchMetadata("eperson.orcid", ORCID),
MetadataMatcher.matchMetadata("eperson.firstname", "Test"),
MetadataMatcher.matchMetadata("eperson.lastname", "User")
)
)
);
} finally {
configurationService.setProperty("orcid.registration-data.url", defaultProp);
}
}
@Test
public void testEPersonCreationViaOrcidLoginWithoutEmail() throws Exception {
public void testRedirectiViaOrcidLoginWithoutEmail() throws Exception {
when(orcidClientMock.getAccessToken(CODE)).thenReturn(buildOrcidTokenResponse(ORCID, ACCESS_TOKEN));
when(orcidClientMock.getPerson(ACCESS_TOKEN, ORCID)).thenReturn(buildPerson("Test", "User"));
getClient().perform(get("/api/" + AuthnRest.CATEGORY + "/orcid")
.param("code", CODE))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost:4000/error?status=401&code=orcid.generic-error"));
MvcResult orcidLogin =
getClient().perform(get("/api/" + AuthnRest.CATEGORY + "/orcid").param("code", CODE))
.andExpect(status().is3xxRedirection())
.andReturn();
String redirectedUrl = orcidLogin.getResponse().getRedirectedUrl();
assertThat(redirectedUrl, notNullValue());
final Pattern pattern = Pattern.compile("external-login/([a-zA-Z0-9]+)");
final Matcher matcher = pattern.matcher(redirectedUrl);
matcher.find();
assertThat(matcher.groupCount(), is(1));
assertThat(matcher.group(1), not(emptyString()));
verify(orcidClientMock).getAccessToken(CODE);
verify(orcidClientMock).getPerson(ACCESS_TOKEN, ORCID);

View File

@@ -7,21 +7,32 @@
*/
package org.dspace.app.rest;
import static org.dspace.app.rest.repository.RegistrationRestRepository.TOKEN_QUERY_PARAM;
import static org.dspace.app.rest.repository.RegistrationRestRepository.TYPE_FORGOT;
import static org.dspace.app.rest.repository.RegistrationRestRepository.TYPE_QUERY_PARAM;
import static org.dspace.app.rest.repository.RegistrationRestRepository.TYPE_REGISTER;
import static org.hamcrest.Matchers.emptyOrNullString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.List;
@@ -30,17 +41,30 @@ import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.dspace.app.rest.matcher.RegistrationMatcher;
import org.dspace.app.rest.model.RegistrationRest;
import org.dspace.app.rest.model.patch.AddOperation;
import org.dspace.app.rest.model.patch.ReplaceOperation;
import org.dspace.app.rest.repository.RegistrationRestRepository;
import org.dspace.app.rest.test.AbstractControllerIntegrationTest;
import org.dspace.authorize.AuthorizeException;
import org.dspace.builder.EPersonBuilder;
import org.dspace.core.Email;
import org.dspace.eperson.CaptchaServiceImpl;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.InvalidReCaptchaException;
import org.dspace.eperson.RegistrationData;
import org.dspace.eperson.RegistrationTypeEnum;
import org.dspace.eperson.dao.RegistrationDataDAO;
import org.dspace.eperson.service.CaptchaService;
import org.dspace.eperson.service.RegistrationDataService;
import org.dspace.services.ConfigurationService;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.ArgumentMatchers;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
public class RegistrationRestRepositoryIT extends AbstractControllerIntegrationTest {
@@ -50,12 +74,35 @@ public class RegistrationRestRepositoryIT extends AbstractControllerIntegrationT
@Autowired
private RegistrationDataDAO registrationDataDAO;
@Autowired
private RegistrationDataService registrationDataService;
@Autowired
private ConfigurationService configurationService;
@Autowired
private RegistrationRestRepository registrationRestRepository;
@Autowired
private ObjectMapper mapper;
private static MockedStatic<Email> emailMockedStatic;
@After
public void tearDown() throws Exception {
Iterator<RegistrationData> iterator = registrationDataDAO.findAll(context, RegistrationData.class).iterator();
while (iterator.hasNext()) {
RegistrationData registrationData = iterator.next();
registrationDataDAO.delete(context, registrationData);
}
}
@BeforeClass
public static void init() throws Exception {
emailMockedStatic = Mockito.mockStatic(Email.class);
}
@AfterClass
public static void tearDownClass() throws Exception {
emailMockedStatic.close();
}
@Test
public void findByTokenTestExistingUserTest() throws Exception {
String email = eperson.getEmail();
@@ -462,4 +509,507 @@ public class RegistrationRestRepositoryIT extends AbstractControllerIntegrationT
.andExpect(status().isBadRequest());
}
@Test
public void givenRegistrationData_whenPatchInvalidValue_thenUnprocessableEntityResponse()
throws Exception {
ObjectMapper mapper = new ObjectMapper();
RegistrationRest registrationRest = new RegistrationRest();
registrationRest.setEmail(eperson.getEmail());
registrationRest.setUser(eperson.getID());
Email spy = Mockito.spy(Email.class);
doNothing().when(spy).send();
emailMockedStatic.when(() -> Email.getEmail(any())).thenReturn(spy);
// given RegistrationData with email
getClient().perform(post("/api/eperson/registrations")
.param(TYPE_QUERY_PARAM, TYPE_REGISTER)
.content(mapper.writeValueAsBytes(registrationRest))
.contentType(contentType))
.andExpect(status().isCreated());
RegistrationData registrationData =
registrationDataService.findByEmail(context, registrationRest.getEmail());
assertThat(registrationData, notNullValue());
assertThat(registrationData.getToken(), not(emptyOrNullString()));
String token = registrationData.getToken();
String newMail = null;
String patchContent = getPatchContent(
List.of(new ReplaceOperation("/email", newMail))
);
// when patch for replace email
getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID())
.param(TOKEN_QUERY_PARAM, token)
.content(patchContent)
.contentType(contentType))
// then succesful response returned
.andExpect(status().isBadRequest());
newMail = "test@email.com";
patchContent = getPatchContent(
List.of(new AddOperation("/email", newMail))
);
getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID())
.param(TOKEN_QUERY_PARAM, token)
.content(patchContent)
.contentType(contentType))
// then succesful response returned
.andExpect(status().isUnprocessableEntity());
newMail = "invalidemail!!!!";
patchContent = getPatchContent(
List.of(new ReplaceOperation("/email", newMail))
);
getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID())
.param(TOKEN_QUERY_PARAM, token)
.content(patchContent)
.contentType(contentType))
// then succesful response returned
.andExpect(status().isUnprocessableEntity());
}
@Test
public void givenRegistrationData_whenPatchWithInvalidToken_thenUnprocessableEntityResponse()
throws Exception {
ObjectMapper mapper = new ObjectMapper();
RegistrationRest registrationRest = new RegistrationRest();
registrationRest.setEmail(eperson.getEmail());
registrationRest.setUser(eperson.getID());
Email spy = Mockito.spy(Email.class);
doNothing().when(spy).send();
emailMockedStatic.when(() -> Email.getEmail(any())).thenReturn(spy);
// given RegistrationData with email
getClient().perform(post("/api/eperson/registrations")
.param(TYPE_QUERY_PARAM, TYPE_REGISTER)
.content(mapper.writeValueAsBytes(registrationRest))
.contentType(contentType))
.andExpect(status().isCreated());
RegistrationData registrationData =
registrationDataService.findByEmail(context, registrationRest.getEmail());
assertThat(registrationData, notNullValue());
assertThat(registrationData.getToken(), not(emptyOrNullString()));
String token = null;
String newMail = "validemail@email.com";
String patchContent = getPatchContent(
List.of(new ReplaceOperation("/email", newMail))
);
// when patch for replace email
getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID())
.param(TOKEN_QUERY_PARAM, token)
.content(patchContent)
.contentType(contentType))
// then succesful response returned
.andExpect(status().isUnauthorized());
token = "notexistingtoken";
// when patch for replace email
getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID())
.param(TOKEN_QUERY_PARAM, token)
.content(patchContent)
.contentType(contentType))
// then succesful response returned
.andExpect(status().isUnauthorized());
context.turnOffAuthorisationSystem();
registrationData = context.reloadEntity(registrationData);
registrationDataService.markAsExpired(context, registrationData);
context.commit();
context.restoreAuthSystemState();
registrationData = context.reloadEntity(registrationData);
assertThat(registrationData.getExpires(), notNullValue());
token = registrationData.getToken();
newMail = "validemail@email.com";
patchContent = getPatchContent(
List.of(new ReplaceOperation("/email", newMail))
);
// when patch for replace email
getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID())
.param(TOKEN_QUERY_PARAM, token)
.content(patchContent)
.contentType(contentType))
// then succesful response returned
.andExpect(status().isUnauthorized());
}
@Test
public void givenRegistrationDataWithEmail_whenPatchForReplaceEmail_thenSuccessfullResponse()
throws Exception {
ObjectMapper mapper = new ObjectMapper();
RegistrationRest registrationRest = new RegistrationRest();
registrationRest.setEmail(eperson.getEmail());
registrationRest.setUser(eperson.getID());
// given RegistrationData with email
getClient().perform(post("/api/eperson/registrations")
.param(TYPE_QUERY_PARAM, TYPE_REGISTER)
.content(mapper.writeValueAsBytes(registrationRest))
.contentType(contentType))
.andExpect(status().isCreated());
RegistrationData registrationData =
registrationDataService.findByEmail(context, registrationRest.getEmail());
assertThat(registrationData, notNullValue());
assertThat(registrationData.getToken(), not(emptyOrNullString()));
String token = registrationData.getToken();
String newMail = "vins-01@fake.mail";
String patchContent = getPatchContent(
List.of(new ReplaceOperation("/email", newMail))
);
// when patch for replace email
getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID())
.param(TOKEN_QUERY_PARAM, token)
.content(patchContent)
.contentType(contentType))
// then succesful response returned
.andExpect(status().is2xxSuccessful());
}
@Test
public void givenRegistrationDataWithoutEmail_whenPatchForAddEmail_thenSuccessfullResponse()
throws Exception {
RegistrationData registrationData =
createNewRegistrationData("0000-1111-2222-3333", RegistrationTypeEnum.ORCID);
assertThat(registrationData, notNullValue());
assertThat(registrationData.getToken(), not(emptyOrNullString()));
String token = registrationData.getToken();
String newMail = "vins-01@fake.mail";
String patchContent = getPatchContent(
List.of(new AddOperation("/email", newMail))
);
// when patch for replace email
getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID())
.param(TOKEN_QUERY_PARAM, token)
.content(patchContent)
.contentType(contentType))
// then succesful response returned
.andExpect(status().is2xxSuccessful());
}
@Test
public void givenRegistrationDataWithEmail_whenPatchForReplaceEmail_thenNewRegistrationDataCreated()
throws Exception {
ObjectMapper mapper = new ObjectMapper();
RegistrationRest registrationRest = new RegistrationRest();
registrationRest.setEmail(eperson.getEmail());
registrationRest.setUser(eperson.getID());
// given RegistrationData with email
getClient().perform(post("/api/eperson/registrations")
.param(TYPE_QUERY_PARAM, TYPE_REGISTER)
.content(mapper.writeValueAsBytes(registrationRest))
.contentType(contentType))
.andExpect(status().isCreated());
RegistrationData registrationData =
registrationDataService.findByEmail(context, registrationRest.getEmail());
assertThat(registrationData, notNullValue());
assertThat(registrationData.getToken(), not(emptyOrNullString()));
String token = registrationData.getToken();
String newMail = "vins-01@fake.mail";
String patchContent = getPatchContent(
List.of(new ReplaceOperation("/email", newMail))
);
// when patch for replace email
getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID())
.param(TOKEN_QUERY_PARAM, token)
.content(patchContent)
.contentType(contentType))
.andExpect(status().is2xxSuccessful());
// then email updated with new registration
RegistrationData newRegistration = registrationDataService.findByEmail(context, newMail);
assertThat(newRegistration, notNullValue());
assertThat(newRegistration.getToken(), not(emptyOrNullString()));
assertThat(newRegistration.getEmail(), equalTo(newMail));
assertThat(newRegistration.getEmail(), not(equalTo(registrationData.getEmail())));
assertThat(newRegistration.getToken(), not(equalTo(registrationData.getToken())));
registrationData = context.reloadEntity(registrationData);
assertThat(registrationData, nullValue());
}
@Test
public void givenRegistrationDataWithoutEmail_whenPatchForReplaceEmail_thenNewRegistrationDataCreated()
throws Exception {
RegistrationData registrationData =
createNewRegistrationData("0000-1111-2222-3333", RegistrationTypeEnum.ORCID);
assertThat(registrationData.getToken(), not(emptyOrNullString()));
String token = registrationData.getToken();
String newMail = "vins-01@fake.mail";
String patchContent = getPatchContent(
List.of(new AddOperation("/email", newMail))
);
// when patch for replace email
getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID())
.param(TOKEN_QUERY_PARAM, token)
.content(patchContent)
.contentType(contentType))
.andExpect(status().is2xxSuccessful());
// then email updated with new registration
RegistrationData newRegistration = registrationDataService.findByEmail(context, newMail);
assertThat(newRegistration, notNullValue());
assertThat(newRegistration.getToken(), not(emptyOrNullString()));
assertThat(newRegistration.getEmail(), equalTo(newMail));
assertThat(newRegistration.getEmail(), not(equalTo(registrationData.getEmail())));
assertThat(newRegistration.getToken(), not(equalTo(registrationData.getToken())));
registrationData = context.reloadEntity(registrationData);
assertThat(registrationData, nullValue());
}
@Test
public void givenRegistrationDataWithoutEmail_whenPatchForAddEmail_thenExternalLoginSent() throws Exception {
RegistrationData registrationData =
createNewRegistrationData("0000-1111-2222-3333", RegistrationTypeEnum.ORCID);
assertThat(registrationData, notNullValue());
assertThat(registrationData.getToken(), not(emptyOrNullString()));
String token = registrationData.getToken();
String newMail = "vins-01@fake.mail";
String patchContent = getPatchContent(
List.of(new AddOperation("/email", newMail))
);
Email spy = Mockito.spy(Email.class);
doNothing().when(spy).send();
emailMockedStatic.when(() -> Email.getEmail(any())).thenReturn(spy);
// when patch for replace email
getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID())
.param(TOKEN_QUERY_PARAM, token)
.content(patchContent)
.contentType(contentType))
.andExpect(status().is2xxSuccessful());
// then verification email sent
verify(spy, times(1)).addRecipient(newMail);
verify(spy).addArgument(
ArgumentMatchers.contains(
RegistrationTypeEnum.ORCID.getLink()
)
);
verify(spy, times(1)).send();
}
@Test
public void givenRegistrationDataWithEmail_whenPatchForNewEmail_thenExternalLoginSent() throws Exception {
RegistrationData registrationData =
createNewRegistrationData("0000-1111-2222-3333", RegistrationTypeEnum.ORCID);
String token = registrationData.getToken();
String newMail = "vincenzo.mecca@orcid.com";
String patchContent = getPatchContent(
List.of(new AddOperation("/email", newMail))
);
Email spy = Mockito.spy(Email.class);
doNothing().when(spy).send();
emailMockedStatic.when(() -> Email.getEmail(any())).thenReturn(spy);
// when patch for replace email
getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID())
.param(TOKEN_QUERY_PARAM, token)
.content(patchContent)
.contentType(contentType))
.andExpect(status().is2xxSuccessful());
verify(spy, times(1)).addRecipient(newMail);
verify(spy).addArgument(
ArgumentMatchers.contains(
registrationData.getRegistrationType().getLink()
)
);
verify(spy, times(1)).send();
registrationData = registrationDataService.findByEmail(context, newMail);
assertThat(registrationData, notNullValue());
assertThat(registrationData.getToken(), not(emptyOrNullString()));
token = registrationData.getToken();
newMail = "vins-01@fake.mail";
patchContent = getPatchContent(
List.of(new ReplaceOperation("/email", newMail))
);
spy = Mockito.spy(Email.class);
doNothing().when(spy).send();
emailMockedStatic.when(() -> Email.getEmail(any())).thenReturn(spy);
// when patch for replace email
getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID())
.param(TOKEN_QUERY_PARAM, token)
.content(patchContent)
.contentType(contentType))
.andExpect(status().is2xxSuccessful());
// then verification email sent
verify(spy, times(1)).addRecipient(newMail);
verify(spy).addArgument(
ArgumentMatchers.contains(
registrationData.getRegistrationType().getLink()
)
);
verify(spy, times(1)).send();
}
@Test
public void givenRegistrationDataWithEmail_whenPatchForExistingEPersonEmail_thenReviewAccountLinkSent()
throws Exception {
ObjectMapper mapper = new ObjectMapper();
RegistrationRest registrationRest = new RegistrationRest();
registrationRest.setEmail(eperson.getEmail());
registrationRest.setNetId("0000-0000-0000-0000");
// given RegistrationData with email
getClient().perform(post("/api/eperson/registrations")
.param(TYPE_QUERY_PARAM, TYPE_REGISTER)
.content(mapper.writeValueAsBytes(registrationRest))
.contentType(contentType))
.andExpect(status().isCreated());
RegistrationData registrationData =
registrationDataService.findByEmail(context, registrationRest.getEmail());
assertThat(registrationData, notNullValue());
assertThat(registrationData.getToken(), not(emptyOrNullString()));
context.turnOffAuthorisationSystem();
final EPerson vins =
EPersonBuilder.createEPerson(context)
.withEmail("vins-01@fake.mail")
.withNameInMetadata("Vincenzo", "Mecca")
.withOrcid("0101-0101-0101-0101")
.build();
context.restoreAuthSystemState();
String token = registrationData.getToken();
String vinsEmail = vins.getEmail();
String patchContent = getPatchContent(
List.of(new ReplaceOperation("/email", vins.getEmail()))
);
Email spy = Mockito.spy(Email.class);
doNothing().when(spy).send();
emailMockedStatic.when(() -> Email.getEmail(any())).thenReturn(spy);
// when patch for replace email
getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID())
.param(TOKEN_QUERY_PARAM, token)
.content(patchContent)
.contentType(contentType))
.andExpect(status().is2xxSuccessful());
// then verification email sent
verify(spy, times(1)).addRecipient(vinsEmail);
verify(spy).addArgument(
ArgumentMatchers.contains(
RegistrationTypeEnum.VALIDATION_ORCID.getLink()
)
);
verify(spy, times(1)).send();
}
@Test
public void givenRegistrationDataWithoutEmail_whenPatchForExistingAccount_thenReviewAccountSent() throws Exception {
RegistrationData registrationData =
createNewRegistrationData("0000-1111-2222-3333", RegistrationTypeEnum.ORCID);
assertThat(registrationData, notNullValue());
assertThat(registrationData.getToken(), not(emptyOrNullString()));
context.turnOffAuthorisationSystem();
final EPerson vins =
EPersonBuilder.createEPerson(context)
.withEmail("vins-01@fake.mail")
.withNameInMetadata("Vincenzo", "Mecca")
.withOrcid("0101-0101-0101-0101")
.build();
context.commit();
context.restoreAuthSystemState();
String token = registrationData.getToken();
String vinsEmail = vins.getEmail();
String patchContent = getPatchContent(
List.of(new AddOperation("/email", vins.getEmail()))
);
Email spy = Mockito.spy(Email.class);
doNothing().when(spy).send();
emailMockedStatic.when(() -> Email.getEmail(any())).thenReturn(spy);
// when patch for replace email
getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID())
.param(TOKEN_QUERY_PARAM, token)
.content(patchContent)
.contentType(contentType))
.andExpect(status().is2xxSuccessful());
// then verification email sent
verify(spy, times(1)).addRecipient(vinsEmail);
verify(spy).addArgument(
ArgumentMatchers.contains(
RegistrationTypeEnum.VALIDATION_ORCID.getLink()
)
);
verify(spy, times(1)).send();
}
private RegistrationData createNewRegistrationData(
String netId, RegistrationTypeEnum type
) throws SQLException, AuthorizeException {
context.turnOffAuthorisationSystem();
RegistrationData registrationData =
registrationDataService.create(context, netId, type);
context.commit();
context.restoreAuthSystemState();
return registrationData;
}
}

View File

@@ -1747,14 +1747,15 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra
context.turnOffAuthorisationSystem();
EPerson ePerson = EPersonBuilder.createEPerson(context)
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withOrcidScope("/read")
.withOrcidScope("/write")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.build();
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withNetId("0000-1111-2222-3333")
.withOrcidScope("/read")
.withOrcidScope("/write")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.build();
OrcidTokenBuilder.create(context, ePerson, "3de2e370-8aa9-4bbe-8d7e-f5b1577bdad4").build();
@@ -1774,7 +1775,9 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra
.andExpect(status().isForbidden());
profile = context.reloadEntity(profile);
ePerson = context.reloadEntity(ePerson);
assertThat(ePerson.getNetid(), notNullValue());
assertThat(getMetadataValues(profile, "person.identifier.orcid"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.scope"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.authenticated"), not(empty()));
@@ -1789,14 +1792,15 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra
context.turnOffAuthorisationSystem();
EPerson ePerson = EPersonBuilder.createEPerson(context)
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withOrcidScope("/read")
.withOrcidScope("/write")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.build();
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withNetId("0000-1111-2222-3333")
.withOrcidScope("/read")
.withOrcidScope("/write")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.build();
OrcidTokenBuilder.create(context, ePerson, "3de2e370-8aa9-4bbe-8d7e-f5b1577bdad4").build();
@@ -1816,7 +1820,9 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra
.andExpect(status().isForbidden());
profile = context.reloadEntity(profile);
ePerson = context.reloadEntity(ePerson);
assertThat(ePerson.getNetid(), notNullValue());
assertThat(getMetadataValues(profile, "person.identifier.orcid"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.scope"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.authenticated"), not(empty()));
@@ -1831,14 +1837,15 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra
context.turnOffAuthorisationSystem();
EPerson ePerson = EPersonBuilder.createEPerson(context)
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withOrcidScope("/read")
.withOrcidScope("/write")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.build();
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withNetId("0000-1111-2222-3333")
.withOrcidScope("/read")
.withOrcidScope("/write")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.build();
OrcidTokenBuilder.create(context, ePerson, "3de2e370-8aa9-4bbe-8d7e-f5b1577bdad4").build();
@@ -1865,7 +1872,9 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra
.andExpect(status().isForbidden());
profile = context.reloadEntity(profile);
ePerson = context.reloadEntity(ePerson);
assertThat(ePerson.getNetid(), notNullValue());
assertThat(getMetadataValues(profile, "person.identifier.orcid"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.scope"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.authenticated"), not(empty()));
@@ -1968,7 +1977,9 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra
verifyNoMoreInteractions(orcidClientMock);
profile = context.reloadEntity(profile);
eperson = context.reloadEntity(eperson);
assertThat(eperson.getNetid(), nullValue());
assertThat(getMetadataValues(profile, "person.identifier.orcid"), empty());
assertThat(getMetadataValues(profile, "dspace.orcid.scope"), empty());
assertThat(getMetadataValues(profile, "dspace.orcid.authenticated"), empty());
@@ -2058,7 +2069,9 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra
.andExpect(status().isForbidden());
profile = context.reloadEntity(profile);
eperson = context.reloadEntity(eperson);
assertThat(eperson.getNetid(), nullValue());
assertThat(getMetadataValues(profile, "person.identifier.orcid"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.scope"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.authenticated"), not(empty()));
@@ -2073,14 +2086,15 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra
context.turnOffAuthorisationSystem();
EPerson ePerson = EPersonBuilder.createEPerson(context)
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withOrcidScope("/read")
.withOrcidScope("/write")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.build();
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withNetId("0000-1111-2222-3333")
.withOrcidScope("/read")
.withOrcidScope("/write")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.build();
OrcidTokenBuilder.create(context, ePerson, "3de2e370-8aa9-4bbe-8d7e-f5b1577bdad4").build();
@@ -2100,7 +2114,9 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra
.andExpect(status().isForbidden());
profile = context.reloadEntity(profile);
ePerson = context.reloadEntity(ePerson);
assertThat(ePerson.getNetid(), notNullValue());
assertThat(getMetadataValues(profile, "person.identifier.orcid"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.scope"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.authenticated"), not(empty()));
@@ -2115,14 +2131,15 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra
context.turnOffAuthorisationSystem();
EPerson ePerson = EPersonBuilder.createEPerson(context)
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withOrcidScope("/read")
.withOrcidScope("/write")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.build();
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withNetId("0000-1111-2222-3333")
.withOrcidScope("/read")
.withOrcidScope("/write")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.build();
OrcidTokenBuilder.create(context, ePerson, "3de2e370-8aa9-4bbe-8d7e-f5b1577bdad4").build();
@@ -2142,7 +2159,9 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra
.andExpect(status().isForbidden());
profile = context.reloadEntity(profile);
ePerson = context.reloadEntity(ePerson);
assertThat(ePerson.getNetid(), notNullValue());
assertThat(getMetadataValues(profile, "person.identifier.orcid"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.scope"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.authenticated"), not(empty()));
@@ -2194,7 +2213,9 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra
verifyNoMoreInteractions(orcidClientMock);
profile = context.reloadEntity(profile);
eperson = context.reloadEntity(eperson);
assertThat(eperson.getNetid(), nullValue());
assertThat(getMetadataValues(profile, "person.identifier.orcid"), empty());
assertThat(getMetadataValues(profile, "dspace.orcid.scope"), empty());
assertThat(getMetadataValues(profile, "dspace.orcid.authenticated"), empty());
@@ -2209,14 +2230,15 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra
context.turnOffAuthorisationSystem();
EPerson ePerson = EPersonBuilder.createEPerson(context)
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withOrcidScope("/read")
.withOrcidScope("/write")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.build();
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withNetId("0000-1111-2222-3333")
.withOrcidScope("/read")
.withOrcidScope("/write")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.build();
OrcidTokenBuilder.create(context, ePerson, "3de2e370-8aa9-4bbe-8d7e-f5b1577bdad4").build();
@@ -2236,7 +2258,9 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra
.andExpect(status().isForbidden());
profile = context.reloadEntity(profile);
ePerson = context.reloadEntity(ePerson);
assertThat(ePerson.getNetid(), notNullValue());
assertThat(getMetadataValues(profile, "person.identifier.orcid"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.scope"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.authenticated"), not(empty()));
@@ -2287,7 +2311,9 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra
verifyNoMoreInteractions(orcidClientMock);
profile = context.reloadEntity(profile);
eperson = context.reloadEntity(eperson);
assertThat(eperson.getNetid(), nullValue());
assertThat(getMetadataValues(profile, "person.identifier.orcid"), empty());
assertThat(getMetadataValues(profile, "dspace.orcid.scope"), empty());
assertThat(getMetadataValues(profile, "dspace.orcid.authenticated"), empty());
@@ -2340,7 +2366,9 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra
verifyNoMoreInteractions(orcidClientMock);
profile = context.reloadEntity(profile);
eperson = context.reloadEntity(eperson);
assertThat(eperson.getNetid(), nullValue());
assertThat(getMetadataValues(profile, "person.identifier.orcid"), empty());
assertThat(getMetadataValues(profile, "dspace.orcid.scope"), empty());
assertThat(getMetadataValues(profile, "dspace.orcid.authenticated"), empty());
@@ -2355,14 +2383,15 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra
context.turnOffAuthorisationSystem();
EPerson ePerson = EPersonBuilder.createEPerson(context)
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withOrcidScope("/read")
.withOrcidScope("/write")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.build();
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withNetId("0000-1111-2222-3333")
.withOrcidScope("/read")
.withOrcidScope("/write")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.build();
OrcidTokenBuilder.create(context, ePerson, "3de2e370-8aa9-4bbe-8d7e-f5b1577bdad4").build();
@@ -2382,7 +2411,9 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra
.andExpect(status().isForbidden());
profile = context.reloadEntity(profile);
ePerson = context.reloadEntity(ePerson);
assertThat(ePerson.getNetid(), notNullValue());
assertThat(getMetadataValues(profile, "person.identifier.orcid"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.scope"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.authenticated"), not(empty()));

View File

@@ -1637,6 +1637,43 @@ google.recaptcha.site-verify = https://www.google.com/recaptcha/api/siteverify
# checkbox - The "I'm not a robot" Checkbox requires the user to click a checkbox indicating the user is not a robot.
#google.recaptcha.mode =
#------------------------------------------------------------------#
#---------------REGISTRATION DATA CONFIGURATION--------------------#
#------------------------------------------------------------------#
# Configuration for the duration of the token depending on the type
# the format used should be compatible with the standard DURATION format (ISO-8601),
# but without the prefix `PT`:
#
# - PT1H -> 1H // hours
# - PT1M -> 1M // minutes
# - PT1S -> 1S // seconds
#
# reference: https://www.digi.com/resources/documentation/digidocs/90001488-13/reference/r_iso_8601_duration_format.htm
#
# Sets the token expiration to complete the login with orcid to be 1H
eperson.registration-data.token.orcid.expiration = 1H
# Sets the token expiration for the email validation sent with orcid login to be 1H
eperson.registration-data.token.validation_orcid.expiration = 1H
# Sets the token expiration for the forgot token type to be 24H
eperson.registration-data.token.forgot.expiration = 24H
# Sets the token expiration for the register token type to be 24H
eperson.registration-data.token.register.expiration = 24H
# Sets the token expiration for the invitation token type to be 24H
eperson.registration-data.token.invitation.expiration = 24H
# Sets the token expiration for the change_password token type to be 1H
eperson.registration-data.token.change_password.expiration = 1H
# Configuration that enables the schedulable tasks related to the registration, as of now the class schedules a cleanup
# of the registationdata table. This action will remove all the expired token from that table.
# Just take a look to org.dspace.app.scheduler.eperson.RegistrationDataScheduler for a deeper understanding.
# The property `enabled` should be setted to true to enable it.
eperson.registration-data.scheduler.enabled = true
# Configuration for the task that deletes expired registrations.
# Its value should be compatible with the cron format.
# By default it's scheduled to be run every 15 minutes.
eperson.registration-data.scheduler.expired-registration-data.cron = 0 0/15 * * * ?
#------------------------------------------------------------------#
#-------------------MODULE CONFIGURATIONS--------------------------#
#------------------------------------------------------------------#

View File

@@ -0,0 +1,22 @@
## E-mail sent to DSpace users when they try to register with an ORCID account
##
## Parameters: {0} is expanded to a special registration URL
##
## See org.dspace.core.Email for information on the format of this file.
##
#set($subject = "${config.get('dspace.name')} Account Registration")
#set($phone = ${config.get('mail.message.helpdesk.telephone')})
To complete registration for a DSpace account, please click the link
below:
${params[0]}
If you need assistance with your account, please email
${config.get("mail.helpdesk")}
#if( $phone )
or call us at ${phone}.
#end
The ${config.get("dspace.name")} Team

View File

@@ -0,0 +1,22 @@
## E-mail sent to DSpace users when they confirm the orcid email address for the account
##
## Parameters: {0} is expanded to a special registration URL
##
## See org.dspace.core.Email for information on the format of this file.
##
#set($subject = "${config.get('dspace.name')} Account Registration")
#set($phone = ${config.get('mail.message.helpdesk.telephone')})
To confirm your email and create the needed account, please click the link
below:
${params[0]}
If you need assistance with your account, please email
${config.get("mail.helpdesk")}
#if( $phone )
or call us at ${phone}.
#end
The ${config.get("dspace.name")} Team

View File

@@ -73,6 +73,7 @@
<mapping class="org.dspace.eperson.Group"/>
<mapping class="org.dspace.eperson.Group2GroupCache"/>
<mapping class="org.dspace.eperson.RegistrationData"/>
<mapping class="org.dspace.eperson.RegistrationDataMetadata"/>
<mapping class="org.dspace.eperson.Subscription"/>
<mapping class="org.dspace.eperson.SubscriptionParameter"/>
<mapping class="org.dspace.handle.Handle"/>

View File

@@ -40,6 +40,7 @@
<bean class="org.dspace.eperson.dao.impl.Group2GroupCacheDAOImpl"/>
<bean class="org.dspace.eperson.dao.impl.GroupDAOImpl"/>
<bean class="org.dspace.eperson.dao.impl.RegistrationDataDAOImpl"/>
<bean class="org.dspace.eperson.dao.impl.RegistrationDataMetadataDAOImpl"/>
<bean class="org.dspace.eperson.dao.impl.SubscriptionDAOImpl"/>
<bean class="org.dspace.eperson.dao.impl.SubscriptionParameterDAOImpl"/>

View File

@@ -107,6 +107,7 @@
<bean class="org.dspace.eperson.EPersonServiceImpl"/>
<bean class="org.dspace.eperson.GroupServiceImpl"/>
<bean class="org.dspace.eperson.RegistrationDataServiceImpl"/>
<bean class="org.dspace.eperson.RegistrationDataMetadataServiceImpl"/>
<bean class="org.dspace.eperson.SubscribeServiceImpl"/>
<bean class="org.dspace.eperson.CaptchaServiceImpl"/>
<bean class="org.dspace.event.EventServiceImpl"/>