mirror of
https://github.com/DSpace/DSpace.git
synced 2025-10-17 23:13:10 +00:00
Merge remote-tracking branch 'origin/main' into task/main/CST-18963
# Conflicts: # dspace/config/modules/rest.cfg
This commit is contained in:
@@ -10,6 +10,8 @@ package org.dspace.access.status;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDate;
|
||||
|
||||
import org.dspace.content.AccessStatus;
|
||||
import org.dspace.content.Bitstream;
|
||||
import org.dspace.content.Item;
|
||||
import org.dspace.core.Context;
|
||||
|
||||
@@ -21,22 +23,37 @@ public interface AccessStatusHelper {
|
||||
* Calculate the access status for the item.
|
||||
*
|
||||
* @param context the DSpace context
|
||||
* @param item the item
|
||||
* @param item the item
|
||||
* @param threshold the embargo threshold date
|
||||
* @return an access status value
|
||||
* @param type the type of calculation
|
||||
* @return the access status
|
||||
* @throws SQLException An exception that provides information on a database access error or other errors.
|
||||
*/
|
||||
public String getAccessStatusFromItem(Context context, Item item, LocalDate threshold)
|
||||
throws SQLException;
|
||||
public AccessStatus getAccessStatusFromItem(Context context,
|
||||
Item item, LocalDate threshold, String type) throws SQLException;
|
||||
|
||||
/**
|
||||
* Retrieve embargo information for the item
|
||||
* Calculate the anonymous access status for the item.
|
||||
*
|
||||
* @param context the DSpace context
|
||||
* @param item the item to check for embargo information
|
||||
* @param threshold the embargo threshold date
|
||||
* @return an embargo date
|
||||
* @return the access status
|
||||
* @throws SQLException An exception that provides information on a database access error or other errors.
|
||||
*/
|
||||
public String getEmbargoFromItem(Context context, Item item, LocalDate threshold) throws SQLException;
|
||||
public AccessStatus getAnonymousAccessStatusFromItem(Context context,
|
||||
Item item, LocalDate threshold) throws SQLException;
|
||||
|
||||
/**
|
||||
* Calculate the access status for the bitstream.
|
||||
*
|
||||
* @param context the DSpace context
|
||||
* @param bitstream the bitstream
|
||||
* @param threshold the embargo threshold date
|
||||
* @param type the type of calculation
|
||||
* @return the access status
|
||||
* @throws SQLException An exception that provides information on a database access error or other errors.
|
||||
*/
|
||||
public AccessStatus getAccessStatusFromBitstream(Context context,
|
||||
Bitstream bitstream, LocalDate threshold, String type) throws SQLException;
|
||||
}
|
||||
|
@@ -11,7 +11,12 @@ import java.sql.SQLException;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.dspace.access.status.service.AccessStatusService;
|
||||
import org.dspace.content.AccessStatus;
|
||||
import org.dspace.content.Bitstream;
|
||||
import org.dspace.content.Item;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.core.service.PluginService;
|
||||
@@ -22,11 +27,16 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
* Implementation for the access status calculation service.
|
||||
*/
|
||||
public class AccessStatusServiceImpl implements AccessStatusService {
|
||||
private static final Logger log = LogManager.getLogger(AccessStatusServiceImpl.class);
|
||||
|
||||
// Plugin implementation, set from the DSpace configuration by init().
|
||||
protected AccessStatusHelper helper = null;
|
||||
|
||||
protected LocalDate forever_date = null;
|
||||
|
||||
protected String itemCalculationType = null;
|
||||
protected String bitstreamCalculationType = null;
|
||||
|
||||
@Autowired(required = true)
|
||||
protected ConfigurationService configurationService;
|
||||
|
||||
@@ -59,16 +69,35 @@ public class AccessStatusServiceImpl implements AccessStatusService {
|
||||
.atStartOfDay()
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalDate();
|
||||
|
||||
itemCalculationType = getAccessStatusCalculationType("access.status.for-user.item");
|
||||
bitstreamCalculationType = getAccessStatusCalculationType("access.status.for-user.bitstream");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccessStatus(Context context, Item item) throws SQLException {
|
||||
return helper.getAccessStatusFromItem(context, item, forever_date);
|
||||
public AccessStatus getAccessStatus(Context context, Item item) throws SQLException {
|
||||
return helper.getAccessStatusFromItem(context, item, forever_date, itemCalculationType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEmbargoFromItem(Context context, Item item) throws SQLException {
|
||||
return helper.getEmbargoFromItem(context, item, forever_date);
|
||||
public AccessStatus getAnonymousAccessStatus(Context context, Item item) throws SQLException {
|
||||
return helper.getAnonymousAccessStatusFromItem(context, item, forever_date);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccessStatus getAccessStatus(Context context, Bitstream bitstream) throws SQLException {
|
||||
return helper.getAccessStatusFromBitstream(context, bitstream, forever_date, bitstreamCalculationType);
|
||||
}
|
||||
|
||||
private String getAccessStatusCalculationType(String key) {
|
||||
String value = configurationService.getProperty(key, DefaultAccessStatusHelper.STATUS_FOR_ANONYMOUS);
|
||||
if (!StringUtils.equalsIgnoreCase(value, DefaultAccessStatusHelper.STATUS_FOR_ANONYMOUS) &&
|
||||
!StringUtils.equalsIgnoreCase(value, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER)) {
|
||||
log.warn("The configuration parameter \"" + key
|
||||
+ "\" contains an invalid value. Valid values include: 'anonymous' and 'current'.");
|
||||
value = DefaultAccessStatusHelper.STATUS_FOR_ANONYMOUS;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
@@ -9,14 +9,17 @@ package org.dspace.access.status;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.dspace.authorize.ResourcePolicy;
|
||||
import org.dspace.authorize.factory.AuthorizeServiceFactory;
|
||||
import org.dspace.authorize.service.AuthorizeService;
|
||||
import org.dspace.authorize.service.ResourcePolicyService;
|
||||
import org.dspace.content.AccessStatus;
|
||||
import org.dspace.content.Bitstream;
|
||||
import org.dspace.content.Bundle;
|
||||
import org.dspace.content.DSpaceObject;
|
||||
@@ -25,21 +28,23 @@ import org.dspace.content.factory.ContentServiceFactory;
|
||||
import org.dspace.content.service.ItemService;
|
||||
import org.dspace.core.Constants;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.eperson.EPerson;
|
||||
import org.dspace.eperson.Group;
|
||||
import org.dspace.eperson.factory.EPersonServiceFactory;
|
||||
import org.dspace.eperson.service.GroupService;
|
||||
|
||||
/**
|
||||
* Default plugin implementation of the access status helper.
|
||||
* The getAccessStatusFromItem method provides a simple logic to
|
||||
* calculate the access status of an item based on the policies of
|
||||
* the primary or the first bitstream in the original bundle.
|
||||
* Users can override this method for enhanced functionality.
|
||||
*
|
||||
* The getEmbargoInformationFromItem method provides a simple logic to
|
||||
* * retrieve embargo information of bitstreams from an item based on the policies of
|
||||
* * the primary or the first bitstream in the original bundle.
|
||||
* * Users can override this method for enhanced functionality.
|
||||
*
|
||||
* The methods provides a simple logic to calculate the access status
|
||||
* of an item based on the policies of the primary or the first bitstream
|
||||
* in the original bundle. Users can override those methods for
|
||||
* enhanced functionality.
|
||||
*/
|
||||
public class DefaultAccessStatusHelper implements AccessStatusHelper {
|
||||
public static final String STATUS_FOR_CURRENT_USER = "current";
|
||||
public static final String STATUS_FOR_ANONYMOUS = "anonymous";
|
||||
|
||||
public static final String EMBARGO = "embargo";
|
||||
public static final String METADATA_ONLY = "metadata.only";
|
||||
public static final String OPEN_ACCESS = "open.access";
|
||||
@@ -52,13 +57,15 @@ public class DefaultAccessStatusHelper implements AccessStatusHelper {
|
||||
AuthorizeServiceFactory.getInstance().getResourcePolicyService();
|
||||
protected AuthorizeService authorizeService =
|
||||
AuthorizeServiceFactory.getInstance().getAuthorizeService();
|
||||
protected GroupService groupService =
|
||||
EPersonServiceFactory.getInstance().getGroupService();
|
||||
|
||||
public DefaultAccessStatusHelper() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Look at the item's policies to determine an access status value.
|
||||
* Look at the item's primary or first bitstream policies to determine an access status value.
|
||||
* It is also considering a date threshold for embargoes and restrictions.
|
||||
*
|
||||
* If the item is null, simply returns the "unknown" value.
|
||||
@@ -66,14 +73,70 @@ public class DefaultAccessStatusHelper implements AccessStatusHelper {
|
||||
* @param context the DSpace context
|
||||
* @param item the item to check for embargoes
|
||||
* @param threshold the embargo threshold date
|
||||
* @return an access status value
|
||||
* @param type the type of calculation
|
||||
* @return the access status
|
||||
*/
|
||||
@Override
|
||||
public String getAccessStatusFromItem(Context context, Item item, LocalDate threshold)
|
||||
public AccessStatus getAccessStatusFromItem(Context context, Item item, LocalDate threshold, String type)
|
||||
throws SQLException {
|
||||
if (item == null) {
|
||||
return UNKNOWN;
|
||||
return new AccessStatus(UNKNOWN, null);
|
||||
}
|
||||
Bitstream bitstream = getPrimaryOrFirstBitstreamInOriginalBundle(item);
|
||||
if (bitstream == null) {
|
||||
return new AccessStatus(METADATA_ONLY, null);
|
||||
}
|
||||
return getAccessStatusFromBitstream(context, bitstream, threshold, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look at the bitstream policies to determine an access status value.
|
||||
* It is also considering a date threshold for embargoes and restrictions.
|
||||
*
|
||||
* If the bitstream is null, simply returns the "unknown" value.
|
||||
*
|
||||
* @param context the DSpace context
|
||||
* @param bitstream the bitstream to check for embargoes
|
||||
* @param threshold the embargo threshold date
|
||||
* @param type the type of calculation
|
||||
* @return the access status
|
||||
*/
|
||||
@Override
|
||||
public AccessStatus getAccessStatusFromBitstream(Context context,
|
||||
Bitstream bitstream, LocalDate threshold, String type) throws SQLException {
|
||||
if (bitstream == null) {
|
||||
return new AccessStatus(UNKNOWN, null);
|
||||
}
|
||||
List<ResourcePolicy> policies = getReadPolicies(context, bitstream, type);
|
||||
LocalDate availabilityDate = findAvailabilityDate(policies, threshold);
|
||||
// Get the access status based on the availability date
|
||||
String accessStatus = getAccessStatusFromAvailabilityDate(availabilityDate, threshold);
|
||||
return new AccessStatus(accessStatus, availabilityDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look at the anonymous policies of the primary (or first)
|
||||
* bitstream of the item to retrieve its embargo.
|
||||
*
|
||||
* @param context the DSpace context
|
||||
* @param item the item
|
||||
* @param threshold the embargo threshold date
|
||||
* @return the access status
|
||||
*/
|
||||
@Override
|
||||
public AccessStatus getAnonymousAccessStatusFromItem(Context context, Item item, LocalDate threshold)
|
||||
throws SQLException {
|
||||
return getAccessStatusFromItem(context, item, threshold, STATUS_FOR_ANONYMOUS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look in the item's original bundle. First, try to get the primary bitstream.
|
||||
* If the bitstream is null, simply returns the first one.
|
||||
*
|
||||
* @param item the DSpace item
|
||||
* @return the bitstream
|
||||
*/
|
||||
private Bitstream getPrimaryOrFirstBitstreamInOriginalBundle(Item item) {
|
||||
// Consider only the original bundles.
|
||||
List<Bundle> bundles = item.getBundles(Constants.DEFAULT_BUNDLE_NAME);
|
||||
// Check for primary bitstreams first.
|
||||
@@ -91,157 +154,159 @@ public class DefaultAccessStatusHelper implements AccessStatusHelper {
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
return calculateAccessStatusForDso(context, bitstream, threshold);
|
||||
return bitstream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look at the DSpace object's policies to determine an access status value.
|
||||
* Retrieves the anonymous read policies for a DSpace object
|
||||
*
|
||||
* @param context the DSpace context
|
||||
* @param dso the DSpace object
|
||||
* @return a list of policies
|
||||
*/
|
||||
private List<ResourcePolicy> getAnonymousReadPolicies(Context context, DSpaceObject dso)
|
||||
throws SQLException {
|
||||
// Only consider read policies. Use the find without a group
|
||||
// as it's not returning all expected values
|
||||
List<ResourcePolicy> readPolicies = resourcePolicyService.find(context, dso, Constants.READ);
|
||||
// Filter the policies with the anonymous group
|
||||
List<ResourcePolicy> filteredPolicies = readPolicies.stream()
|
||||
.filter(p -> StringUtils.equals(p.getGroup().getName(), Group.ANONYMOUS))
|
||||
.collect(Collectors.toList());
|
||||
return filteredPolicies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current user read policies for a DSpace object
|
||||
*
|
||||
* @param context the DSpace context
|
||||
* @param dso the DSpace object
|
||||
* @return a list of policies
|
||||
*/
|
||||
private List<ResourcePolicy> getCurrentUserReadPolicies(Context context, DSpaceObject dso)
|
||||
throws SQLException {
|
||||
// First, look if the current user can read the object
|
||||
boolean canRead = authorizeService.authorizeActionBoolean(context, dso, Constants.READ);
|
||||
// If it's true, it can't be an embargo or a restriction, shortcircuit the process
|
||||
// and return a null value (indicating an open access)
|
||||
if (canRead) {
|
||||
return null;
|
||||
}
|
||||
// Only consider read policies
|
||||
List<ResourcePolicy> policies = resourcePolicyService.find(context, dso, Constants.READ);
|
||||
// Only calculate the embargo date for the current user
|
||||
EPerson currentUser = context.getCurrentUser();
|
||||
List<ResourcePolicy> readPolicies = new ArrayList<ResourcePolicy>();
|
||||
for (ResourcePolicy policy : policies) {
|
||||
EPerson eperson = policy.getEPerson();
|
||||
if (eperson != null && currentUser != null && eperson.getID() == currentUser.getID()) {
|
||||
readPolicies.add(policy);
|
||||
continue;
|
||||
}
|
||||
Group group = policy.getGroup();
|
||||
if (group != null && groupService.isMember(context, currentUser, group)) {
|
||||
readPolicies.add(policy);
|
||||
}
|
||||
}
|
||||
return readPolicies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the read policies for a DSpace object based on the type
|
||||
*
|
||||
* If the type is current, consider the current logged in user
|
||||
* If the type is anonymous, only consider the anonymous group
|
||||
*
|
||||
* @param context the DSpace context
|
||||
* @param dso the DSpace object
|
||||
* @param type the type of calculation
|
||||
* @return a list of policies
|
||||
*/
|
||||
private List<ResourcePolicy> getReadPolicies(Context context, DSpaceObject dso, String type)
|
||||
throws SQLException {
|
||||
if (StringUtils.equalsIgnoreCase(type, STATUS_FOR_CURRENT_USER)) {
|
||||
return getCurrentUserReadPolicies(context, dso);
|
||||
} else {
|
||||
// Only calculate the status for the anonymous group read policies
|
||||
return getAnonymousReadPolicies(context, dso);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Look at the read policies to retrieve the access status availability date.
|
||||
*
|
||||
* @param readPolicies the read policies
|
||||
* @param threshold the embargo threshold date
|
||||
* @return an availability date
|
||||
*/
|
||||
private LocalDate findAvailabilityDate(List<ResourcePolicy> readPolicies, LocalDate threshold) {
|
||||
// If the list is null, the object is readable
|
||||
if (readPolicies == null) {
|
||||
return null;
|
||||
}
|
||||
// If there's no policies, return the threshold date (restriction)
|
||||
if (readPolicies.size() == 0) {
|
||||
return threshold;
|
||||
}
|
||||
LocalDate availabilityDate = null;
|
||||
LocalDate currentDate = LocalDate.now();
|
||||
boolean takeMostRecentDate = true;
|
||||
// Looks at all read policies
|
||||
for (ResourcePolicy policy : readPolicies) {
|
||||
boolean isValid = resourcePolicyService.isDateValid(policy);
|
||||
// If any policy is valid, the object is accessible
|
||||
if (isValid) {
|
||||
return null;
|
||||
}
|
||||
// There may be an active embargo
|
||||
LocalDate startDate = policy.getStartDate();
|
||||
// Ignore policy with no start date or which is expired
|
||||
if (startDate == null || startDate.isBefore(currentDate)) {
|
||||
continue;
|
||||
}
|
||||
// Policy with a start date over the threshold (restriction)
|
||||
// overrides the embargos
|
||||
if (!startDate.isBefore(threshold)) {
|
||||
takeMostRecentDate = false;
|
||||
}
|
||||
// Take the most recent embargo date if there is no restriction, otherwise
|
||||
// take the highest date (account for rare cases where more than one resource
|
||||
// policy exists)
|
||||
if (availabilityDate == null) {
|
||||
availabilityDate = startDate;
|
||||
} else if (takeMostRecentDate) {
|
||||
availabilityDate = startDate.isBefore(availabilityDate) ? startDate : availabilityDate;
|
||||
} else {
|
||||
availabilityDate = startDate.isAfter(availabilityDate) ? startDate : availabilityDate;
|
||||
}
|
||||
}
|
||||
return availabilityDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look at the DSpace object availability date to determine an access status value.
|
||||
*
|
||||
* If the object is null, returns the "metadata.only" value.
|
||||
* If any policy attached to the object is valid for the anonymous group,
|
||||
* returns the "open.access" value.
|
||||
* Otherwise, if the policy start date is before the embargo threshold date,
|
||||
* returns the "embargo" value.
|
||||
* Every other cases return the "restricted" value.
|
||||
* If there's no availability date, returns the "open.access" value.
|
||||
* If the availability date is after or equal to the embargo
|
||||
* threshold date, returns the "restricted" value.
|
||||
* Every other cases return the "embargo" value.
|
||||
*
|
||||
* @param context the DSpace context
|
||||
* @param dso the DSpace object
|
||||
* @param threshold the embargo threshold date
|
||||
* @param availabilityDate the DSpace object availability date
|
||||
* @param threshold the embargo threshold date
|
||||
* @return an access status value
|
||||
*/
|
||||
private String calculateAccessStatusForDso(Context context, DSpaceObject dso, LocalDate threshold)
|
||||
throws SQLException {
|
||||
if (dso == null) {
|
||||
return METADATA_ONLY;
|
||||
}
|
||||
// Only consider read policies.
|
||||
List<ResourcePolicy> policies = authorizeService
|
||||
.getPoliciesActionFilter(context, dso, Constants.READ);
|
||||
int openAccessCount = 0;
|
||||
int embargoCount = 0;
|
||||
int restrictedCount = 0;
|
||||
int unknownCount = 0;
|
||||
// Looks at all read policies.
|
||||
for (ResourcePolicy policy : policies) {
|
||||
boolean isValid = resourcePolicyService.isDateValid(policy);
|
||||
Group group = policy.getGroup();
|
||||
// The group must not be null here. However,
|
||||
// if it is, consider this as an unexpected case.
|
||||
if (group == null) {
|
||||
unknownCount++;
|
||||
} else if (StringUtils.equals(group.getName(), Group.ANONYMOUS)) {
|
||||
// Only calculate the status for the anonymous group.
|
||||
if (isValid) {
|
||||
// If the policy is valid, the anonymous group have access
|
||||
// to the bitstream.
|
||||
openAccessCount++;
|
||||
} else {
|
||||
LocalDate startDate = policy.getStartDate();
|
||||
if (startDate != null && !startDate.isBefore(threshold)) {
|
||||
// If the policy start date have a value and if this value
|
||||
// is equal or superior to the configured forever date, the
|
||||
// access status is also restricted.
|
||||
restrictedCount++;
|
||||
} else {
|
||||
// If the current date is not between the policy start date
|
||||
// and end date, the access status is embargo.
|
||||
embargoCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (openAccessCount > 0) {
|
||||
private String getAccessStatusFromAvailabilityDate(LocalDate availabilityDate, LocalDate threshold) {
|
||||
// If there is no availability date, it's an open access.
|
||||
if (availabilityDate == null) {
|
||||
return OPEN_ACCESS;
|
||||
}
|
||||
if (embargoCount > 0 && restrictedCount == 0) {
|
||||
return EMBARGO;
|
||||
// If the policy start date have a value and if this value
|
||||
// is equal or superior to the configured forever date, the
|
||||
// access status is also restricted.
|
||||
if (!availabilityDate.isBefore(threshold)) {
|
||||
return RESTRICTED;
|
||||
}
|
||||
if (unknownCount > 0) {
|
||||
return UNKNOWN;
|
||||
}
|
||||
return RESTRICTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look at the policies of the primary (or first) bitstream of the item to retrieve its embargo.
|
||||
*
|
||||
* If the item is null, simply returns an empty map with no embargo information.
|
||||
*
|
||||
* @param context the DSpace context
|
||||
* @param item the item to embargo
|
||||
* @return an access status value
|
||||
*/
|
||||
@Override
|
||||
public String getEmbargoFromItem(Context context, Item item, LocalDate threshold)
|
||||
throws SQLException {
|
||||
LocalDate embargoDate;
|
||||
|
||||
// If Item status is not "embargo" then return a null embargo date.
|
||||
String accessStatus = getAccessStatusFromItem(context, item, threshold);
|
||||
|
||||
if (item == null || !accessStatus.equals(EMBARGO)) {
|
||||
return null;
|
||||
}
|
||||
// Consider only the original bundles.
|
||||
List<Bundle> bundles = item.getBundles(Constants.DEFAULT_BUNDLE_NAME);
|
||||
// Check for primary bitstreams first.
|
||||
Bitstream bitstream = bundles.stream()
|
||||
.map(bundle -> bundle.getPrimaryBitstream())
|
||||
.filter(Objects::nonNull)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (bitstream == null) {
|
||||
// If there is no primary bitstream,
|
||||
// take the first bitstream in the bundles.
|
||||
bitstream = bundles.stream()
|
||||
.map(bundle -> bundle.getBitstreams())
|
||||
.flatMap(List::stream)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
if (bitstream == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
embargoDate = this.retrieveShortestEmbargo(context, bitstream);
|
||||
|
||||
return embargoDate != null ? embargoDate.toString() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private LocalDate retrieveShortestEmbargo(Context context, Bitstream bitstream) throws SQLException {
|
||||
LocalDate embargoDate = null;
|
||||
// Only consider read policies.
|
||||
List<ResourcePolicy> policies = authorizeService
|
||||
.getPoliciesActionFilter(context, bitstream, Constants.READ);
|
||||
|
||||
// Looks at all read policies.
|
||||
for (ResourcePolicy policy : policies) {
|
||||
boolean isValid = resourcePolicyService.isDateValid(policy);
|
||||
Group group = policy.getGroup();
|
||||
|
||||
if (group != null && StringUtils.equals(group.getName(), Group.ANONYMOUS)) {
|
||||
// Only calculate the status for the anonymous group.
|
||||
if (!isValid) {
|
||||
// If the policy is not valid there is an active embargo
|
||||
LocalDate startDate = policy.getStartDate();
|
||||
|
||||
if (startDate != null && !startDate.isBefore(LocalDate.now())) {
|
||||
// There is an active embargo: aim to take the shortest embargo (account for rare cases where
|
||||
// more than one resource policy exists)
|
||||
if (embargoDate == null) {
|
||||
embargoDate = startDate;
|
||||
} else {
|
||||
embargoDate = startDate.isBefore(embargoDate) ? startDate : embargoDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return embargoDate;
|
||||
return EMBARGO;
|
||||
}
|
||||
}
|
||||
|
@@ -9,6 +9,8 @@ package org.dspace.access.status.service;
|
||||
|
||||
import java.sql.SQLException;
|
||||
|
||||
import org.dspace.content.AccessStatus;
|
||||
import org.dspace.content.Bitstream;
|
||||
import org.dspace.content.Item;
|
||||
import org.dspace.core.Context;
|
||||
|
||||
@@ -39,19 +41,29 @@ public interface AccessStatusService {
|
||||
* Calculate the access status for an Item while considering the forever embargo date threshold.
|
||||
*
|
||||
* @param context the DSpace context
|
||||
* @param item the item
|
||||
* @return an access status value
|
||||
* @param item the item
|
||||
* @return the access status
|
||||
* @throws SQLException An exception that provides information on a database access error or other errors.
|
||||
*/
|
||||
public String getAccessStatus(Context context, Item item) throws SQLException;
|
||||
public AccessStatus getAccessStatus(Context context, Item item) throws SQLException;
|
||||
|
||||
/**
|
||||
* Retrieve embargo information for the item
|
||||
* Calculate the anonymous access status for an Item while considering the forever embargo date threshold.
|
||||
*
|
||||
* @param context the DSpace context
|
||||
* @param item the item to check for embargo information
|
||||
* @return an embargo date
|
||||
* @return the access status
|
||||
* @throws SQLException An exception that provides information on a database access error or other errors.
|
||||
*/
|
||||
public String getEmbargoFromItem(Context context, Item item) throws SQLException;
|
||||
public AccessStatus getAnonymousAccessStatus(Context context, Item item) throws SQLException;
|
||||
|
||||
/**
|
||||
* Calculate the access status for a bitstream while considering the forever embargo date threshold.
|
||||
*
|
||||
* @param context the DSpace context
|
||||
* @param bitstream the bitstream
|
||||
* @return the access status
|
||||
* @throws SQLException An exception that provides information on a database access error or other errors.
|
||||
*/
|
||||
public AccessStatus getAccessStatus(Context context, Bitstream bitstream) throws SQLException;
|
||||
}
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
||||
|
@@ -72,6 +72,12 @@ public class RequestItem implements ReloadableEntity<Integer> {
|
||||
@Column(name = "accept_request")
|
||||
private boolean accept_request;
|
||||
|
||||
@Column(name = "access_token", unique = true, length = 48)
|
||||
private String access_token = null;
|
||||
|
||||
@Column(name = "access_expiry")
|
||||
private Instant access_expiry = null;
|
||||
|
||||
/**
|
||||
* Protected constructor, create object using:
|
||||
* {@link org.dspace.app.requestitem.service.RequestItemService#createRequest(
|
||||
@@ -85,7 +91,7 @@ public class RequestItem implements ReloadableEntity<Integer> {
|
||||
return requestitem_id;
|
||||
}
|
||||
|
||||
void setAllfiles(boolean allfiles) {
|
||||
public void setAllfiles(boolean allfiles) {
|
||||
this.allfiles = allfiles;
|
||||
}
|
||||
|
||||
@@ -134,7 +140,8 @@ public class RequestItem implements ReloadableEntity<Integer> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a unique request identifier which can be emailed.
|
||||
* @return a unique request identifier which can be emailed to the *approver* of the request.
|
||||
* This is not the same as the access token, which is used by the requester to access the item after approval.
|
||||
*/
|
||||
public String getToken() {
|
||||
return token;
|
||||
@@ -187,4 +194,38 @@ public class RequestItem implements ReloadableEntity<Integer> {
|
||||
void setRequest_date(Instant request_date) {
|
||||
this.request_date = request_date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A unique token to be used by the requester when granted access to the resource, which
|
||||
* can be emailed upon approval
|
||||
*/
|
||||
public String getAccess_token() {
|
||||
return access_token;
|
||||
}
|
||||
|
||||
public void setAccess_token(String access_token) {
|
||||
this.access_token = access_token;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The date and time when the access token expires.
|
||||
*/
|
||||
public Instant getAccess_expiry() {
|
||||
return access_expiry;
|
||||
}
|
||||
public void setAccess_expiry(Instant access_expiry) {
|
||||
this.access_expiry = access_expiry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize personal information and the approval token, to be used when returning a RequestItem
|
||||
* to Angular, especially for users clicking on the secure link
|
||||
*/
|
||||
public void sanitizePersonalData() {
|
||||
setReqEmail("sanitized");
|
||||
setReqName("sanitized");
|
||||
setReqMessage("sanitized");
|
||||
// Even though [approval] token is not a name, it can be used to access the original object
|
||||
setToken("sanitized");
|
||||
}
|
||||
}
|
||||
|
@@ -10,6 +10,8 @@ package org.dspace.app.requestitem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.annotation.ManagedBean;
|
||||
@@ -28,6 +30,7 @@ import org.dspace.core.Context;
|
||||
import org.dspace.core.Email;
|
||||
import org.dspace.core.I18nUtil;
|
||||
import org.dspace.core.LogHelper;
|
||||
import org.dspace.core.Utils;
|
||||
import org.dspace.eperson.EPerson;
|
||||
import org.dspace.handle.service.HandleService;
|
||||
import org.dspace.services.ConfigurationService;
|
||||
@@ -174,9 +177,23 @@ public class RequestItemEmailNotifier {
|
||||
grantorAddress = grantor.getEmail();
|
||||
}
|
||||
|
||||
// Set date format for access expiry date
|
||||
String accessExpiryFormat = configurationService.getProperty("request.item.grant.link.dateformat",
|
||||
"yyyy-MM-dd");
|
||||
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(accessExpiryFormat)
|
||||
.withZone(ZoneId.of("UTC"));
|
||||
|
||||
Email email;
|
||||
// If this item has a secure access token, send the template with that link instead of attaching files
|
||||
if (ri.isAccept_request() && ri.getAccess_token() != null) {
|
||||
email = Email.getEmail(I18nUtil.getEmailFilename(context.getCurrentLocale(),
|
||||
"request_item.granted_token"));
|
||||
} else {
|
||||
email = Email.getEmail(I18nUtil.getEmailFilename(context.getCurrentLocale(),
|
||||
ri.isAccept_request() ? "request_item.granted" : "request_item.rejected"));
|
||||
}
|
||||
|
||||
// Build an email back to the requester.
|
||||
Email email = Email.getEmail(I18nUtil.getEmailFilename(context.getCurrentLocale(),
|
||||
ri.isAccept_request() ? "request_item.granted" : "request_item.rejected"));
|
||||
email.addArgument(ri.getReqName()); // {0} requestor's name
|
||||
email.addArgument(handleService.getCanonicalForm(ri.getItem().getHandle())); // {1} URL of the requested Item
|
||||
email.addArgument(ri.getItem().getName()); // {2} title of the requested Item
|
||||
@@ -188,34 +205,47 @@ public class RequestItemEmailNotifier {
|
||||
// Attach bitstreams.
|
||||
try {
|
||||
if (ri.isAccept_request()) {
|
||||
if (ri.isAllfiles()) {
|
||||
Item item = ri.getItem();
|
||||
List<Bundle> bundles = item.getBundles("ORIGINAL");
|
||||
for (Bundle bundle : bundles) {
|
||||
List<Bitstream> bitstreams = bundle.getBitstreams();
|
||||
for (Bitstream bitstream : bitstreams) {
|
||||
if (!bitstream.getFormat(context).isInternal() &&
|
||||
requestItemService.isRestricted(context,
|
||||
bitstream)) {
|
||||
// #8636 Anyone receiving the email can respond to the
|
||||
// request without authenticating into DSpace
|
||||
context.turnOffAuthorisationSystem();
|
||||
email.addAttachment(
|
||||
bitstreamService.retrieve(context, bitstream),
|
||||
bitstream.getName(),
|
||||
bitstream.getFormat(context).getMIMEType());
|
||||
context.restoreAuthSystemState();
|
||||
}
|
||||
}
|
||||
if (ri.getAccess_token() != null) {
|
||||
// {6} secure access link
|
||||
email.addArgument(configurationService.getProperty("dspace.ui.url")
|
||||
+ "/items/" + ri.getItem().getID()
|
||||
+ "?accessToken=" + ri.getAccess_token());
|
||||
// {7} access end date, but only add formatted date string if it is set and not "forever"
|
||||
if (ri.getAccess_expiry() != null && !ri.getAccess_expiry().equals(Utils.getMaxTimestamp())) {
|
||||
email.addArgument(dateTimeFormatter.format(ri.getAccess_expiry()));
|
||||
} else {
|
||||
email.addArgument(null);
|
||||
}
|
||||
} else {
|
||||
Bitstream bitstream = ri.getBitstream();
|
||||
// #8636 Anyone receiving the email can respond to the request without authenticating into DSpace
|
||||
context.turnOffAuthorisationSystem();
|
||||
email.addAttachment(bitstreamService.retrieve(context, bitstream),
|
||||
bitstream.getName(),
|
||||
bitstream.getFormat(context).getMIMEType());
|
||||
context.restoreAuthSystemState();
|
||||
if (ri.isAllfiles()) {
|
||||
Item item = ri.getItem();
|
||||
List<Bundle> bundles = item.getBundles("ORIGINAL");
|
||||
for (Bundle bundle : bundles) {
|
||||
List<Bitstream> bitstreams = bundle.getBitstreams();
|
||||
for (Bitstream bitstream : bitstreams) {
|
||||
if (!bitstream.getFormat(context).isInternal() &&
|
||||
requestItemService.isRestricted(context,
|
||||
bitstream)) {
|
||||
// #8636 Anyone receiving the email can respond to the
|
||||
// request without authenticating into DSpace
|
||||
context.turnOffAuthorisationSystem();
|
||||
email.addAttachment(
|
||||
bitstreamService.retrieve(context, bitstream),
|
||||
bitstream.getName(),
|
||||
bitstream.getFormat(context).getMIMEType());
|
||||
context.restoreAuthSystemState();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Bitstream bitstream = ri.getBitstream();
|
||||
//#8636 Anyone receiving the email can respond to the request without authenticating into DSpace
|
||||
context.turnOffAuthorisationSystem();
|
||||
email.addAttachment(bitstreamService.retrieve(context, bitstream),
|
||||
bitstream.getName(),
|
||||
bitstream.getFormat(context).getMIMEType());
|
||||
context.restoreAuthSystemState();
|
||||
}
|
||||
}
|
||||
email.send();
|
||||
} else {
|
||||
|
@@ -7,25 +7,40 @@
|
||||
*/
|
||||
package org.dspace.app.requestitem;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.sql.SQLException;
|
||||
import java.text.ParseException;
|
||||
import java.time.DateTimeException;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import org.apache.http.client.utils.URIBuilder;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.dspace.app.requestitem.dao.RequestItemDAO;
|
||||
import org.dspace.app.requestitem.service.RequestItemService;
|
||||
import org.dspace.authorize.AuthorizeException;
|
||||
import org.dspace.authorize.ResourcePolicy;
|
||||
import org.dspace.authorize.service.AuthorizeService;
|
||||
import org.dspace.authorize.service.ResourcePolicyService;
|
||||
import org.dspace.content.Bitstream;
|
||||
import org.dspace.content.Bundle;
|
||||
import org.dspace.content.DSpaceObject;
|
||||
import org.dspace.content.Item;
|
||||
import org.dspace.core.Constants;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.core.LogHelper;
|
||||
import org.dspace.core.Utils;
|
||||
import org.dspace.services.ConfigurationService;
|
||||
import org.dspace.util.DateMathParser;
|
||||
import org.dspace.util.MultiFormatDateParser;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
/**
|
||||
@@ -35,6 +50,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
* This class should never be accessed directly.
|
||||
*
|
||||
* @author kevinvandevelde at atmire.com
|
||||
* @author Kim Shepherd
|
||||
*/
|
||||
public class RequestItemServiceImpl implements RequestItemService {
|
||||
|
||||
@@ -49,16 +65,43 @@ public class RequestItemServiceImpl implements RequestItemService {
|
||||
@Autowired(required = true)
|
||||
protected ResourcePolicyService resourcePolicyService;
|
||||
|
||||
@Autowired
|
||||
protected ConfigurationService configurationService;
|
||||
|
||||
/**
|
||||
* Always set UTC for dateMathParser for consistent database date handling
|
||||
*/
|
||||
static DateMathParser dateMathParser = new DateMathParser(TimeZone.getTimeZone("UTC"));
|
||||
|
||||
private static final int DEFAULT_MINIMUM_FILE_SIZE = 20;
|
||||
|
||||
protected RequestItemServiceImpl() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new request-a-copy item request.
|
||||
*
|
||||
* @param context The relevant DSpace Context.
|
||||
* @param bitstream The requested bitstream
|
||||
* @param item The requested item
|
||||
* @param allFiles true indicates that all bitstreams of this item are requested
|
||||
* @param reqEmail email
|
||||
* Requester email
|
||||
* @param reqName Requester name
|
||||
* @param reqMessage Request message text
|
||||
* @return token to be used to approver for grant/deny
|
||||
* @throws SQLException
|
||||
*/
|
||||
@Override
|
||||
public String createRequest(Context context, Bitstream bitstream, Item item,
|
||||
boolean allFiles, String reqEmail, String reqName, String reqMessage)
|
||||
throws SQLException {
|
||||
|
||||
// Create an empty request item
|
||||
RequestItem requestItem = requestItemDAO.create(context, new RequestItem());
|
||||
|
||||
// Set values of the request item based on supplied parameters
|
||||
requestItem.setToken(Utils.generateHexKey());
|
||||
requestItem.setBitstream(bitstream);
|
||||
requestItem.setItem(item);
|
||||
@@ -68,10 +111,56 @@ public class RequestItemServiceImpl implements RequestItemService {
|
||||
requestItem.setReqMessage(reqMessage);
|
||||
requestItem.setRequest_date(Instant.now());
|
||||
|
||||
// If the 'link' feature is enabled and the filesize threshold is met, pre-generate access token now
|
||||
// so it can be previewed by approver and so Angular and REST services can use the existence of this token
|
||||
// as an indication of which delivery method to use.
|
||||
// Access period will be created upon actual approval.
|
||||
if (configurationService.getBooleanProperty("request.item.grant.link", false)) {
|
||||
// The 'send link' feature is enabled, is the file(s) requested over the size threshold (megabytes as int)?
|
||||
// Default is 20MB minimum. For inspection purposes we convert to bytes.
|
||||
long minimumSize = configurationService.getLongProperty(
|
||||
"request.item.grant.link.filesize", DEFAULT_MINIMUM_FILE_SIZE) * 1024 * 1024;
|
||||
// If we have a single bitstream, we will initialise the "minimum threshold reached" correctly
|
||||
boolean minimumSizeThresholdReached = (null != bitstream && bitstream.getSizeBytes() >= minimumSize);
|
||||
// If all files (and presumably no min reached since bitstream should be null), we look for ANY >= min size
|
||||
if (!minimumSizeThresholdReached && allFiles) {
|
||||
// Iterate bitstream and inspect file sizes. At each loop iteration we will break out if the min
|
||||
// was already reached.
|
||||
String[] bundleNames = configurationService.getArrayProperty("request.item.grant.link.bundles",
|
||||
new String[]{"ORIGINAL"});
|
||||
for (String bundleName : bundleNames) {
|
||||
if (!minimumSizeThresholdReached) {
|
||||
for (Bundle bundle : item.getBundles(bundleName)) {
|
||||
if (null != bundle && !minimumSizeThresholdReached) {
|
||||
for (Bitstream bitstreamToCheck : bundle.getBitstreams()) {
|
||||
if (bitstreamToCheck.getSizeBytes() >= minimumSize) {
|
||||
minimumSizeThresholdReached = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now, only generate and set an access token if the minimum file size threshold was reached.
|
||||
// Otherwise, an email attachment will still be used.
|
||||
// From now on, the existence of an access token in the RequestItem indicates that a web link should be
|
||||
// sent instead of attaching file(s) as an attachment.
|
||||
if (minimumSizeThresholdReached) {
|
||||
requestItem.setAccess_token(Utils.generateHexKey());
|
||||
}
|
||||
}
|
||||
|
||||
// Save the request item
|
||||
requestItemDAO.save(context, requestItem);
|
||||
|
||||
log.debug("Created RequestItem with ID {} and token {}",
|
||||
requestItem::getID, requestItem::getToken);
|
||||
log.debug("Created RequestItem with ID {}, approval token {}, access token {}, access expiry {}",
|
||||
requestItem::getID, requestItem::getToken, requestItem::getAccess_token, requestItem::getAccess_expiry);
|
||||
|
||||
// Return the approver token
|
||||
return requestItem.getToken();
|
||||
}
|
||||
|
||||
@@ -128,4 +217,186 @@ public class RequestItemServiceImpl implements RequestItemService {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a request item by its access token. This is the token that a requester would use
|
||||
* to authenticate themselves as a granted requester.
|
||||
* It is up to the RequestItemRepository to check validity of the item, access granted, data sanitization, etc.
|
||||
*
|
||||
* @param context current DSpace session.
|
||||
* @param accessToken the token identifying the request to be temporarily accessed
|
||||
* @return request item data
|
||||
*/
|
||||
@Override
|
||||
public RequestItem findByAccessToken(Context context, String accessToken) {
|
||||
try {
|
||||
return requestItemDAO.findByAccessToken(context, accessToken);
|
||||
} catch (SQLException e) {
|
||||
log.error(e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the access expiry date for the request item.
|
||||
* @param requestItem the request item to update
|
||||
* @param accessExpiry the expiry date to set
|
||||
*/
|
||||
@Override
|
||||
public void setAccessExpiry(RequestItem requestItem, Instant accessExpiry) {
|
||||
requestItem.setAccess_expiry(accessExpiry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a string either as a formatted date, or in the "math" format expected by
|
||||
* the DateMathParser, e.g. +7DAYS or +10MONTHS, and set the access expiry date accordingly.
|
||||
* There are no special checks here to check that the date is in the future, or after the
|
||||
* 'decision date', as there may be legitimate reasons to set past dates.
|
||||
* If past dates are not allowed by some interface, then the caller should check this.
|
||||
*
|
||||
* @param requestItem the request item to update
|
||||
* @param dateOrDelta the delta as a string in format expected by the DateMathParser
|
||||
*/
|
||||
@Override
|
||||
public void setAccessExpiry(RequestItem requestItem, String dateOrDelta) {
|
||||
try {
|
||||
setAccessExpiry(requestItem, parseDateOrDelta(dateOrDelta, requestItem.getDecision_date()));
|
||||
} catch (ParseException e) {
|
||||
log.error("Error parsing access expiry or duration: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Taking into account 'accepted' flag, bitstream id or allfiles flag, decision date and access period,
|
||||
* either return cleanly or throw an AuthorizeException
|
||||
*
|
||||
* @param context the DSpace context
|
||||
* @param requestItem the request item containing request and approval data
|
||||
* @param bitstream the bitstream to which access is requested
|
||||
* @param accessToken the access token supplied by the user (e.g. to REST controller)
|
||||
* @throws AuthorizeException
|
||||
*/
|
||||
@Override
|
||||
public void authorizeAccessByAccessToken(Context context, RequestItem requestItem, Bitstream bitstream,
|
||||
String accessToken) throws AuthorizeException {
|
||||
if (requestItem == null || bitstream == null || context == null || accessToken == null) {
|
||||
throw new AuthorizeException("Null resources provided, not authorized");
|
||||
}
|
||||
// 1. Request is accepted
|
||||
if (requestItem.isAccept_request()
|
||||
// 2. Request access token is not null and matches supplied string
|
||||
&& (requestItem.getAccess_token() != null && requestItem.getAccess_token().equals(accessToken))
|
||||
// 3. Request is 'allfiles' or for this bitstream ID
|
||||
&& (requestItem.isAllfiles() || bitstream.equals(requestItem.getBitstream()))
|
||||
// 4. access expiry timestamp is null (forever), or is *after* the current time
|
||||
&& (requestItem.getAccess_expiry() == null || requestItem.getAccess_expiry().isAfter(Instant.now()))
|
||||
) {
|
||||
log.info("Authorizing access to bitstream {} by access token", bitstream.getID());
|
||||
return;
|
||||
}
|
||||
// Default, throw authorize exception
|
||||
throw new AuthorizeException("Unauthorized access to bitstream by access token for bitstream ID "
|
||||
+ bitstream.getID());
|
||||
}
|
||||
|
||||
/**
|
||||
* Taking into account 'accepted' flag, bitstream id or allfiles flag, decision date and access period,
|
||||
* either return cleanly or throw an AuthorizeException
|
||||
*
|
||||
* @param context the DSpace context
|
||||
* @param bitstream the bitstream to which access is requested
|
||||
* @param accessToken the access token supplied by the user (e.g. to REST controller)
|
||||
* @throws AuthorizeException
|
||||
*/
|
||||
@Override
|
||||
public void authorizeAccessByAccessToken(Context context, Bitstream bitstream, String accessToken)
|
||||
throws AuthorizeException {
|
||||
if (bitstream == null || context == null || accessToken == null) {
|
||||
throw new AuthorizeException("Null resources provided, not authorized");
|
||||
}
|
||||
// get request item from access token
|
||||
RequestItem requestItem = findByAccessToken(context, accessToken);
|
||||
if (requestItem == null) {
|
||||
throw new AuthorizeException("Null item request provided, not authorized");
|
||||
}
|
||||
// Continue with authorization check
|
||||
authorizeAccessByAccessToken(context, requestItem, bitstream, accessToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a link back to DSpace, to act on a request.
|
||||
*
|
||||
* @param token identifies the request.
|
||||
* @return URL to the item request API, with the token as request parameter
|
||||
* "token".
|
||||
* @throws URISyntaxException passed through.
|
||||
* @throws MalformedURLException passed through.
|
||||
*/
|
||||
@Override
|
||||
public String getLinkTokenEmail(String token)
|
||||
throws URISyntaxException, MalformedURLException {
|
||||
final String base = configurationService.getProperty("dspace.ui.url");
|
||||
URIBuilder uriBuilder = new URIBuilder(base);
|
||||
String currentPath = uriBuilder.getPath();
|
||||
String newPath = (currentPath == null || currentPath.isEmpty() || currentPath.equals("/"))
|
||||
? "/request-a-copy/" + token
|
||||
: currentPath + "/request-a-copy/" + token;
|
||||
URI uri = uriBuilder.setPath(newPath).build();
|
||||
return uri.toURL().toExternalForm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a RequestItem. The following values in the referenced RequestItem
|
||||
* are nullified:
|
||||
* - approver token (aka token)
|
||||
* - requester name
|
||||
* - requester email
|
||||
* - requester message
|
||||
*
|
||||
* These properties contain personal information, or can be used to access personal information
|
||||
* and are not needed except for sending the original request and grant/deny emails
|
||||
*
|
||||
* @param requestItem
|
||||
*/
|
||||
@Override
|
||||
public void sanitizeRequestItem(Context context, RequestItem requestItem) {
|
||||
if (null == requestItem) {
|
||||
log.error("Null request item passed for sanitization, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
// Sanitized referenced data (strips requester name, email, message, and the approver token)
|
||||
requestItem.sanitizePersonalData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a date or delta string into an Instant. Kept here as a static method for use in unit tests
|
||||
* and other areas that might not have access to the full spring service
|
||||
*
|
||||
* @param dateOrDelta
|
||||
* @param decisionDate
|
||||
* @return parsed date as instant
|
||||
* @throws ParseException
|
||||
*/
|
||||
public static Instant parseDateOrDelta(String dateOrDelta, Instant decisionDate)
|
||||
throws ParseException, DateTimeException {
|
||||
// First, if dateOrDelta is a null string or "FOREVER", we will set the expiry
|
||||
// date to a very distant date in the future.
|
||||
if (dateOrDelta == null || dateOrDelta.equals("FOREVER")) {
|
||||
return Utils.getMaxTimestamp();
|
||||
}
|
||||
// Next, try parsing as a straight date using the multiple format parser
|
||||
ZonedDateTime parsedExpiryDate = MultiFormatDateParser.parse(dateOrDelta);
|
||||
|
||||
if (parsedExpiryDate == null) {
|
||||
// That did not work, so try parsing as a delta
|
||||
// Set the 'now' date to the decision date of the request item
|
||||
dateMathParser.setNow(LocalDateTime.ofInstant(decisionDate, ZoneOffset.UTC));
|
||||
// Parse the delta (e.g. +7DAYS) and set the new access expiry date
|
||||
return dateMathParser.parseMath(dateOrDelta).toInstant(ZoneOffset.UTC);
|
||||
} else {
|
||||
// The expiry date was a valid formatted date string, so set the access expiry date
|
||||
return parsedExpiryDate.toInstant();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -26,7 +26,7 @@ import org.dspace.core.GenericDAO;
|
||||
*/
|
||||
public interface RequestItemDAO extends GenericDAO<RequestItem> {
|
||||
/**
|
||||
* Fetch a request named by its unique token (passed in emails).
|
||||
* Fetch a request named by its unique approval token (passed in emails).
|
||||
*
|
||||
* @param context the current DSpace context.
|
||||
* @param token uniquely identifies the request.
|
||||
@@ -35,5 +35,18 @@ public interface RequestItemDAO extends GenericDAO<RequestItem> {
|
||||
*/
|
||||
public RequestItem findByToken(Context context, String token) throws SQLException;
|
||||
|
||||
/**
|
||||
* Fetch a request named by its unique access token (passed in emails).
|
||||
* Note this is the token used by the requester to access an approved resource, not the token
|
||||
* used by the item submitter or helpdesk to grant the access.
|
||||
*
|
||||
* @param context the current DSpace context.
|
||||
* @param accessToken uniquely identifies the request
|
||||
* @return the found request or {@code null}
|
||||
* @throws SQLException passed through.
|
||||
*/
|
||||
public RequestItem findByAccessToken(Context context, String accessToken) throws SQLException;
|
||||
|
||||
public Iterator<RequestItem> findByItem(Context context, Item item) throws SQLException;
|
||||
|
||||
}
|
||||
|
@@ -42,6 +42,17 @@ public class RequestItemDAOImpl extends AbstractHibernateDAO<RequestItem> implem
|
||||
criteriaQuery.where(criteriaBuilder.equal(requestItemRoot.get(RequestItem_.token), token));
|
||||
return uniqueResult(context, criteriaQuery, false, RequestItem.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestItem findByAccessToken(Context context, String accessToken) throws SQLException {
|
||||
CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
|
||||
CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, RequestItem.class);
|
||||
Root<RequestItem> requestItemRoot = criteriaQuery.from(RequestItem.class);
|
||||
criteriaQuery.select(requestItemRoot);
|
||||
criteriaQuery.where(criteriaBuilder.equal(requestItemRoot.get(RequestItem_.access_token), accessToken));
|
||||
return uniqueResult(context, criteriaQuery, true, RequestItem.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<RequestItem> findByItem(Context context, Item item) throws SQLException {
|
||||
CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
|
||||
|
@@ -8,13 +8,19 @@
|
||||
|
||||
/**
|
||||
* Feature for conveying a request that materials forbidden to the requester
|
||||
* by resource policy be made available by other means. The request will be
|
||||
* e-mailed to a responsible party for consideration and action. Find details
|
||||
* in the user documentation under the rubric "Request a Copy".
|
||||
* by resource policy be made available by other means.
|
||||
*
|
||||
* There are several methods of making the resource(s) available to the requester:
|
||||
* 1. The request will be e-mailed to a responsible party for consideration and action.
|
||||
* Find details in the user documentation under the rubric "Request a Copy".
|
||||
*
|
||||
* <p>Mailing is handled by {@link RequestItemEmailNotifier}. Responsible
|
||||
* parties are represented by {@link RequestItemAuthor}
|
||||
*
|
||||
* 2. A unique 48-char token will be generated and included in a special weblink emailed to the requester.
|
||||
* This link will provide access to the requester as though they had READ policy access while the access period
|
||||
* has not expired, or forever if the access period is null.
|
||||
*
|
||||
* <p>This package includes several "strategy" classes which discover
|
||||
* responsible parties in various ways. See
|
||||
* {@link RequestItemSubmitterStrategy} and the classes which extend it, and
|
||||
|
@@ -7,11 +7,15 @@
|
||||
*/
|
||||
package org.dspace.app.requestitem.service;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.sql.SQLException;
|
||||
import java.time.Instant;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import org.dspace.app.requestitem.RequestItem;
|
||||
import org.dspace.authorize.AuthorizeException;
|
||||
import org.dspace.content.Bitstream;
|
||||
import org.dspace.content.DSpaceObject;
|
||||
import org.dspace.content.Item;
|
||||
@@ -23,6 +27,7 @@ import org.dspace.core.Context;
|
||||
* for the RequestItem object and is autowired by Spring.
|
||||
*
|
||||
* @author kevinvandevelde at atmire.com
|
||||
* @author Kim Shepherd
|
||||
*/
|
||||
public interface RequestItemService {
|
||||
|
||||
@@ -40,7 +45,7 @@ public interface RequestItemService {
|
||||
* @return the token of the request item
|
||||
* @throws SQLException if database error
|
||||
*/
|
||||
public String createRequest(Context context, Bitstream bitstream, Item item,
|
||||
String createRequest(Context context, Bitstream bitstream, Item item,
|
||||
boolean allFiles, String reqEmail, String reqName, String reqMessage)
|
||||
throws SQLException;
|
||||
|
||||
@@ -49,35 +54,46 @@ public interface RequestItemService {
|
||||
*
|
||||
* @param context current DSpace session.
|
||||
* @return all item requests.
|
||||
* @throws java.sql.SQLException passed through.
|
||||
* @throws SQLException passed through.
|
||||
*/
|
||||
public List<RequestItem> findAll(Context context)
|
||||
List<RequestItem> findAll(Context context)
|
||||
throws SQLException;
|
||||
|
||||
/**
|
||||
* Retrieve a request by its token.
|
||||
* Retrieve a request by its approver token.
|
||||
*
|
||||
* @param context current DSpace session.
|
||||
* @param token the token identifying the request.
|
||||
* @param token the token identifying the request to be approved.
|
||||
* @return the matching request, or null if not found.
|
||||
*/
|
||||
public RequestItem findByToken(Context context, String token);
|
||||
RequestItem findByToken(Context context, String token);
|
||||
|
||||
/**
|
||||
* Retrieve a request by its access token, for use by the requester
|
||||
*
|
||||
* @param context current DSpace session.
|
||||
* @param token the token identifying the request to be temporarily accessed
|
||||
* @return the matching request, or null if not found.
|
||||
*/
|
||||
RequestItem findByAccessToken(Context context, String token);
|
||||
/**
|
||||
* Retrieve a request based on the item.
|
||||
* @param context current DSpace session.
|
||||
* @param item the item to find requests for.
|
||||
* @return the matching requests, or null if not found.
|
||||
*/
|
||||
public Iterator<RequestItem> findByItem(Context context, Item item) throws SQLException;
|
||||
Iterator<RequestItem> findByItem(Context context, Item item) throws SQLException;
|
||||
|
||||
/**
|
||||
* Save updates to the record. Only accept_request, and decision_date are set-able.
|
||||
* Save updates to the record. Only accept_request, decision_date, access_period are settable.
|
||||
*
|
||||
* Note: the "is settable" rules mentioned here are enforced in RequestItemRest with annotations meaning that
|
||||
* these JSON properties are considered READ-ONLY by the core DSpaceRestRepository methods
|
||||
*
|
||||
* @param context The relevant DSpace Context.
|
||||
* @param requestItem requested item
|
||||
*/
|
||||
public void update(Context context, RequestItem requestItem);
|
||||
void update(Context context, RequestItem requestItem);
|
||||
|
||||
/**
|
||||
* Remove the record from the database.
|
||||
@@ -85,7 +101,7 @@ public interface RequestItemService {
|
||||
* @param context current DSpace context.
|
||||
* @param request record to be removed.
|
||||
*/
|
||||
public void delete(Context context, RequestItem request);
|
||||
void delete(Context context, RequestItem request);
|
||||
|
||||
/**
|
||||
* Is there at least one valid READ resource policy for this object?
|
||||
@@ -94,6 +110,77 @@ public interface RequestItemService {
|
||||
* @return true if a READ policy applies.
|
||||
* @throws SQLException passed through.
|
||||
*/
|
||||
public boolean isRestricted(Context context, DSpaceObject o)
|
||||
boolean isRestricted(Context context, DSpaceObject o)
|
||||
throws SQLException;
|
||||
|
||||
/**
|
||||
* Set the access expiry timestamp for a request item. After this date, the
|
||||
* bitstream(s) will no longer be available for download even with a token.
|
||||
* @param requestItem the request item
|
||||
* @param accessExpiry the expiry timestamp
|
||||
*/
|
||||
void setAccessExpiry(RequestItem requestItem, Instant accessExpiry);
|
||||
|
||||
/**
|
||||
* Set the access expiry timestamp for a request item by delta string.
|
||||
* After this date, the bitstream(s) will no longer be available for download
|
||||
* even with a token.
|
||||
* @param requestItem the request item
|
||||
* @param delta the delta to calculate the expiry timestamp, from the decision date
|
||||
*/
|
||||
void setAccessExpiry(RequestItem requestItem, String delta);
|
||||
|
||||
/**
|
||||
* Taking into account 'accepted' flag, bitstream id or allfiles flag, decision date and access period,
|
||||
* either return cleanly or throw an AuthorizeException
|
||||
*
|
||||
* @param context the DSpace context
|
||||
* @param requestItem the request item containing request and approval data
|
||||
* @param bitstream the bitstream to which access is requested
|
||||
* @param accessToken the access token supplied by the user (e.g. to REST controller)
|
||||
* @throws AuthorizeException
|
||||
*/
|
||||
void authorizeAccessByAccessToken(Context context, RequestItem requestItem, Bitstream bitstream,
|
||||
String accessToken)
|
||||
throws AuthorizeException;
|
||||
|
||||
/**
|
||||
* Taking into account 'accepted' flag, bitstream id or allfiles flag, decision date and access period,
|
||||
* either return cleanly or throw an AuthorizeException
|
||||
*
|
||||
* @param context the DSpace context
|
||||
* @param bitstream the bitstream to which access is requested
|
||||
* @param accessToken the access token supplied by the user (e.g. to REST controller)
|
||||
* @throws AuthorizeException
|
||||
*/
|
||||
void authorizeAccessByAccessToken(Context context, Bitstream bitstream, String accessToken)
|
||||
throws AuthorizeException;
|
||||
|
||||
/**
|
||||
* Generate a link back to DSpace, to act on a request.
|
||||
*
|
||||
* @param token identifies the request.
|
||||
* @return URL to the item request API, with the token as request parameter
|
||||
* "token".
|
||||
* @throws URISyntaxException passed through.
|
||||
* @throws MalformedURLException passed through.
|
||||
*/
|
||||
String getLinkTokenEmail(String token)
|
||||
throws URISyntaxException, MalformedURLException;
|
||||
|
||||
/**
|
||||
* Sanitize a RequestItem depending on the current session user. If the current user is not
|
||||
* the approver, an administrator or other privileged group, the following values in the return object
|
||||
* are nullified:
|
||||
* - approver token (aka token)
|
||||
* - requester name
|
||||
* - requester email
|
||||
* - requester message
|
||||
*
|
||||
* These properties contain personal information, or can be used to access personal information
|
||||
* and are not needed except for sending the original request and grant/deny emails
|
||||
*
|
||||
* @param requestItem
|
||||
*/
|
||||
void sanitizeRequestItem(Context context, RequestItem requestItem);
|
||||
}
|
||||
|
@@ -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() {
|
||||
|
@@ -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.content;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* Utility class for access status
|
||||
*/
|
||||
public class AccessStatus {
|
||||
/**
|
||||
* the status value
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* the availability date if required
|
||||
*/
|
||||
private LocalDate availabilityDate;
|
||||
|
||||
/**
|
||||
* Construct a new access status
|
||||
*
|
||||
* @param status the status value
|
||||
* @param availabilityDate the availability date
|
||||
*/
|
||||
public AccessStatus(String status, LocalDate availabilityDate) {
|
||||
this.status = status;
|
||||
this.availabilityDate = availabilityDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Returns the status value.
|
||||
*/
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param status The status value.
|
||||
*/
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Returns the availability date.
|
||||
*/
|
||||
public LocalDate getAvailabilityDate() {
|
||||
return availabilityDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param availabilityDate The availability date.
|
||||
*/
|
||||
public void setAvailabilityDate(LocalDate availabilityDate) {
|
||||
this.availabilityDate = availabilityDate;
|
||||
}
|
||||
}
|
@@ -24,6 +24,8 @@ import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.ParseException;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
@@ -478,4 +480,24 @@ public final class Utils {
|
||||
ConfigurationService config = DSpaceServicesFactory.getInstance().getConfigurationService();
|
||||
return StringSubstitutor.replace(string, config.getProperties());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum timestamp that can be stored in a PostgreSQL database with hibernate,
|
||||
* for our "distant future" access expiry date.
|
||||
* @return the maximum timestamp that can be stored with Postgres + Hibernate
|
||||
*/
|
||||
public static Instant getMaxTimestamp() {
|
||||
return LocalDateTime.of(294276, 12, 31, 23, 59, 59)
|
||||
.toInstant(ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the minimum timestamp that can be stored in a PostgreSQL database, for date validation or any other
|
||||
* purpose to ensure we don't try to store a date before the epoch.
|
||||
* @return the minimum timestamp that can be stored with Postgres + Hibernate
|
||||
*/
|
||||
public static Instant getMinTimestamp() {
|
||||
return LocalDateTime.of(-4713, 11, 12, 0, 0, 0)
|
||||
.toInstant(ZoneOffset.UTC);
|
||||
}
|
||||
}
|
||||
|
@@ -13,6 +13,7 @@ import org.apache.solr.common.SolrInputDocument;
|
||||
import org.dspace.access.status.DefaultAccessStatusHelper;
|
||||
import org.dspace.access.status.factory.AccessStatusServiceFactory;
|
||||
import org.dspace.access.status.service.AccessStatusService;
|
||||
import org.dspace.content.AccessStatus;
|
||||
import org.dspace.content.Item;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.discovery.indexobject.IndexableItem;
|
||||
@@ -61,6 +62,7 @@ public class SolrServiceIndexAccessStatusPlugin implements SolrServiceIndexPlugi
|
||||
UNKNOWN = "unknown"
|
||||
*/
|
||||
private String retrieveItemAccessStatus(Context context, Item item) throws SQLException {
|
||||
return accessStatusService.getAccessStatus(context, item);
|
||||
AccessStatus accessStatus = accessStatusService.getAccessStatus(context, item);
|
||||
return accessStatus.getStatus();
|
||||
}
|
||||
}
|
||||
|
@@ -25,6 +25,7 @@ import org.dspace.app.ldn.service.LDNMessageService;
|
||||
import org.dspace.content.Item;
|
||||
import org.dspace.content.service.ItemService;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.util.SolrUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
/**
|
||||
@@ -146,7 +147,7 @@ public class LDNMessageEntityIndexFactoryImpl extends IndexFactoryImpl<Indexable
|
||||
ZoneOffset.UTC));
|
||||
addFacetIndex(doc, "queue_last_start_time", value, value);
|
||||
doc.addField("queue_last_start_time", value);
|
||||
doc.addField("queue_last_start_time_dt", queueLastStartTime);
|
||||
doc.addField("queue_last_start_time_dt", SolrUtils.getDateFormatter().format(queueLastStartTime));
|
||||
doc.addField("queue_last_start_time_min", value);
|
||||
doc.addField("queue_last_start_time_min_sort", value);
|
||||
doc.addField("queue_last_start_time_max", value);
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 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.nio.charset.StandardCharsets;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.commons.codec.digest.HmacUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.dspace.eperson.service.CaptchaService;
|
||||
import org.dspace.services.ConfigurationService;
|
||||
import org.json.JSONObject;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
/**
|
||||
* Basic services implementation for a Proof of Work Captcha like Altcha.
|
||||
* Unlike Google ReCaptcha, there is no remote API or service to rely on, we simply
|
||||
* compare the client-side crypto puzzle challenge to our own work and pass or fail the validation that way
|
||||
* See https://altcha.org/docs/server-integration for implementation pseudocode.
|
||||
*
|
||||
* @see AltchaCaptchaController for REST impl
|
||||
*
|
||||
* @author Kim Shepherd
|
||||
*/
|
||||
public class AltchaCaptchaServiceImpl implements CaptchaService {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(AltchaCaptchaServiceImpl.class);
|
||||
|
||||
@Autowired
|
||||
private ConfigurationService configurationService;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a response string and validate as per interface
|
||||
*
|
||||
* @param captchaPayloadHeader reCaptcha token to be validated
|
||||
* @param action action of reCaptcha
|
||||
* @throws InvalidReCaptchaException
|
||||
*/
|
||||
@Override
|
||||
public void processResponse(String captchaPayloadHeader, String action) throws InvalidReCaptchaException {
|
||||
if (!validateAltchaCaptcha(captchaPayloadHeader)) {
|
||||
throw new InvalidReCaptchaException("ALTCHA captcha validation failed");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate captcha payload by reproducing the work
|
||||
* @param captchaPayloadHeader header conforming to altcha specs
|
||||
* See: https://altcha.org/docs/server-integration
|
||||
* @return
|
||||
*/
|
||||
private boolean validateAltchaCaptcha(String captchaPayloadHeader) throws InvalidReCaptchaException {
|
||||
// Decode base64 string for parsing to json
|
||||
String captchaPayloadJson =
|
||||
new String(Base64.decodeBase64(captchaPayloadHeader.getBytes(StandardCharsets.UTF_8)));
|
||||
// Parse as JSON
|
||||
JSONObject captchaPayload = new JSONObject(captchaPayloadJson);
|
||||
// Extract data and validate work
|
||||
try {
|
||||
// Make sure the required fields are present
|
||||
if (captchaPayload.has("challenge") && captchaPayload.has("salt")
|
||||
&& captchaPayload.has("number") && captchaPayload.has("signature")
|
||||
&& captchaPayload.has("algorithm")) {
|
||||
String algorithm = captchaPayload.getString("algorithm");
|
||||
if (!"SHA-256".equals(algorithm)) {
|
||||
throw new InvalidReCaptchaException("ALTCHA algorithm must be SHA-256, check config and payload");
|
||||
}
|
||||
// Get fields for code readability and debugging
|
||||
String challenge = captchaPayload.getString("challenge");
|
||||
String salt = captchaPayload.getString("salt");
|
||||
String number = String.valueOf(captchaPayload.getNumber("number"));
|
||||
String signature = captchaPayload.getString("signature");
|
||||
// Calculate hash
|
||||
String hash = CaptchaService.calculateHash(salt + number, captchaPayload.getString("algorithm"));
|
||||
// Get hmacKey
|
||||
String hmacKey = configurationService.getProperty("altcha.hmac.key");
|
||||
if (hmacKey == null) {
|
||||
log.error("hmac key not found, see: altcha.hmac.key in altcha.cfg");
|
||||
throw new InvalidReCaptchaException("hmac key not found");
|
||||
}
|
||||
// HMAC signature, using configured HMAC key and the generated challenge string
|
||||
String hmac = new HmacUtils("HmacSHA256", hmacKey).hmacHex(challenge);
|
||||
if (org.apache.commons.lang3.StringUtils.isBlank(hmac)) {
|
||||
log.error("Error generating HMAC signature");
|
||||
// Default, return no content
|
||||
throw new InvalidReCaptchaException("error generating hmac signature");
|
||||
}
|
||||
// Compare received and expected values
|
||||
boolean challengeVerified = challenge.equals(hash);
|
||||
boolean signatureVerified = signature.equals(hmac);
|
||||
return challengeVerified && signatureVerified;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// If *any* error is enountered, throw InvalidReCaptchaException
|
||||
throw new InvalidReCaptchaException("Failed to validate ALTCHA captcha: " + e.getMessage());
|
||||
}
|
||||
|
||||
// By default, fail the validation
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
@@ -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> {
|
||||
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -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 {
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 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.factory;
|
||||
|
||||
import org.dspace.eperson.service.CaptchaService;
|
||||
import org.dspace.services.factory.DSpaceServicesFactory;
|
||||
|
||||
public abstract class CaptchaServiceFactory {
|
||||
|
||||
public abstract CaptchaService getCaptchaService();
|
||||
|
||||
public static CaptchaServiceFactory getInstance() {
|
||||
return DSpaceServicesFactory.getInstance().getServiceManager()
|
||||
.getServiceByName("captchaServiceFactory", CaptchaServiceFactory.class);
|
||||
}
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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.factory;
|
||||
|
||||
import org.dspace.eperson.service.CaptchaService;
|
||||
import org.dspace.services.ConfigurationService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
|
||||
public class CaptchaServiceFactoryImpl extends CaptchaServiceFactory {
|
||||
|
||||
@Autowired
|
||||
@Qualifier("googleCaptchaService")
|
||||
private CaptchaService googleCaptchaService;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("altchaCaptchaService")
|
||||
private CaptchaService altchaCaptchaService;
|
||||
|
||||
@Autowired
|
||||
private ConfigurationService configurationService;
|
||||
|
||||
@Override
|
||||
public CaptchaService getCaptchaService() {
|
||||
String provider = configurationService.getProperty("captcha.provider");
|
||||
|
||||
if ("altcha".equalsIgnoreCase(provider)) {
|
||||
return altchaCaptchaService;
|
||||
}
|
||||
|
||||
return googleCaptchaService; // default to Google ReCaptcha
|
||||
}
|
||||
}
|
@@ -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();
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -7,6 +7,9 @@
|
||||
*/
|
||||
package org.dspace.eperson.service;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import org.dspace.eperson.InvalidReCaptchaException;
|
||||
|
||||
/**
|
||||
@@ -27,4 +30,30 @@ public interface CaptchaService {
|
||||
*/
|
||||
public void processResponse(String response, String action) throws InvalidReCaptchaException;
|
||||
|
||||
/**
|
||||
* Encode bytes to hex string
|
||||
* @param bytes bytes to encode
|
||||
* @return hex string
|
||||
*/
|
||||
public static String bytesToHex(byte[] bytes) {
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
stringBuilder.append(String.format("%02x", b));
|
||||
}
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate a hex string from a digest, given an input string
|
||||
* @param input input string
|
||||
* @param algorithm algorithm key, eg. SHA-256
|
||||
* @return
|
||||
* @throws NoSuchAlgorithmException
|
||||
*/
|
||||
public static String calculateHash(String input, String algorithm) throws NoSuchAlgorithmException {
|
||||
MessageDigest sha256 = MessageDigest.getInstance(algorithm);
|
||||
byte[] hashBytes = sha256.digest(input.getBytes());
|
||||
return bytesToHex(hashBytes);
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -0,0 +1,13 @@
|
||||
--
|
||||
-- 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/
|
||||
--
|
||||
|
||||
-- Add new access_token column to hold a secure access token for the requestor to use for weblink-based access
|
||||
ALTER TABLE requestitem ADD COLUMN IF NOT EXISTS access_token VARCHAR(48);
|
||||
-- Add new access_expiry DATESTAMP column to hold the expiry date of the access token
|
||||
-- (note this is separate from the existing 'expires' column which was intended as the expiry date of the request itself)
|
||||
ALTER TABLE requestitem ADD COLUMN IF NOT EXISTS access_expiry TIMESTAMP;
|
@@ -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;
|
@@ -0,0 +1,13 @@
|
||||
--
|
||||
-- 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/
|
||||
--
|
||||
|
||||
-- Add new access_token column to hold a secure access token for the requestor to use for weblink-based access
|
||||
ALTER TABLE requestitem ADD COLUMN IF NOT EXISTS access_token VARCHAR(48);
|
||||
-- Add new access_expiry DATESTAMP column to hold the expiry date of the access token
|
||||
-- (note this is separate from the existing 'expires' column which was intended as the expiry date of the request itself)
|
||||
ALTER TABLE requestitem ADD COLUMN IF NOT EXISTS access_expiry TIMESTAMP;
|
@@ -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;
|
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"
|
||||
default-lazy-init="true">
|
||||
<!-- Use CaptchaServiceImpl for Google ReCaptcha -->
|
||||
<bean class="org.dspace.eperson.CaptchaServiceImpl" id="googleCaptchaService"/>
|
||||
<!-- Use AltchaCaptchaServiceImpl for ALTCHA captcha -->
|
||||
<bean class="org.dspace.eperson.AltchaCaptchaServiceImpl" id="altchaCaptchaService"/>
|
||||
</beans>
|
@@ -8,24 +8,35 @@
|
||||
package org.dspace.access.status;
|
||||
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDate;
|
||||
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.dspace.AbstractUnitTest;
|
||||
import org.dspace.access.status.factory.AccessStatusServiceFactory;
|
||||
import org.dspace.access.status.service.AccessStatusService;
|
||||
import org.dspace.authorize.AuthorizeException;
|
||||
import org.dspace.content.AccessStatus;
|
||||
import org.dspace.content.Bitstream;
|
||||
import org.dspace.content.Bundle;
|
||||
import org.dspace.content.Collection;
|
||||
import org.dspace.content.Community;
|
||||
import org.dspace.content.Item;
|
||||
import org.dspace.content.factory.ContentServiceFactory;
|
||||
import org.dspace.content.service.BitstreamService;
|
||||
import org.dspace.content.service.BundleService;
|
||||
import org.dspace.content.service.CollectionService;
|
||||
import org.dspace.content.service.CommunityService;
|
||||
import org.dspace.content.service.InstallItemService;
|
||||
import org.dspace.content.service.ItemService;
|
||||
import org.dspace.content.service.WorkspaceItemService;
|
||||
import org.dspace.core.Constants;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -40,6 +51,8 @@ public class AccessStatusServiceTest extends AbstractUnitTest {
|
||||
private Collection collection;
|
||||
private Community owningCommunity;
|
||||
private Item item;
|
||||
private Bundle bundle;
|
||||
private Bitstream bitstream;
|
||||
|
||||
protected CommunityService communityService =
|
||||
ContentServiceFactory.getInstance().getCommunityService();
|
||||
@@ -47,6 +60,10 @@ public class AccessStatusServiceTest extends AbstractUnitTest {
|
||||
ContentServiceFactory.getInstance().getCollectionService();
|
||||
protected ItemService itemService =
|
||||
ContentServiceFactory.getInstance().getItemService();
|
||||
protected BundleService bundleService =
|
||||
ContentServiceFactory.getInstance().getBundleService();
|
||||
protected BitstreamService bitstreamService =
|
||||
ContentServiceFactory.getInstance().getBitstreamService();
|
||||
protected WorkspaceItemService workspaceItemService =
|
||||
ContentServiceFactory.getInstance().getWorkspaceItemService();
|
||||
protected InstallItemService installItemService =
|
||||
@@ -71,6 +88,10 @@ public class AccessStatusServiceTest extends AbstractUnitTest {
|
||||
collection = collectionService.create(context, owningCommunity);
|
||||
item = installItemService.installItem(context,
|
||||
workspaceItemService.create(context, collection, true));
|
||||
bundle = bundleService.create(context, item, Constants.CONTENT_BUNDLE_NAME);
|
||||
bitstream = bitstreamService.create(context, bundle,
|
||||
new ByteArrayInputStream("1".getBytes(StandardCharsets.UTF_8)));
|
||||
bitstream.setName(context, "primary");
|
||||
context.restoreAuthSystemState();
|
||||
} catch (AuthorizeException ex) {
|
||||
log.error("Authorization Error in init", ex);
|
||||
@@ -78,6 +99,9 @@ public class AccessStatusServiceTest extends AbstractUnitTest {
|
||||
} catch (SQLException ex) {
|
||||
log.error("SQL Error in init", ex);
|
||||
fail("SQL Error in init: " + ex.getMessage());
|
||||
} catch (IOException ex) {
|
||||
log.error("IO Error in init", ex);
|
||||
fail("IO Error in init: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +116,16 @@ public class AccessStatusServiceTest extends AbstractUnitTest {
|
||||
@Override
|
||||
public void destroy() {
|
||||
context.turnOffAuthorisationSystem();
|
||||
try {
|
||||
bitstreamService.delete(context, bitstream);
|
||||
} catch (Exception e) {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
bundleService.delete(context, bundle);
|
||||
} catch (Exception e) {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
itemService.delete(context, item);
|
||||
} catch (Exception e) {
|
||||
@@ -108,6 +142,8 @@ public class AccessStatusServiceTest extends AbstractUnitTest {
|
||||
// ignore
|
||||
}
|
||||
context.restoreAuthSystemState();
|
||||
bitstream = null;
|
||||
bundle = null;
|
||||
item = null;
|
||||
collection = null;
|
||||
owningCommunity = null;
|
||||
@@ -119,8 +155,29 @@ public class AccessStatusServiceTest extends AbstractUnitTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAccessStatus() throws Exception {
|
||||
String status = accessStatusService.getAccessStatus(context, item);
|
||||
assertNotEquals("testGetAccessStatus 0", status, DefaultAccessStatusHelper.UNKNOWN);
|
||||
public void testGetAccessStatusItem() throws Exception {
|
||||
AccessStatus accessStatus = accessStatusService.getAccessStatus(context, item);
|
||||
String status = accessStatus.getStatus();
|
||||
LocalDate availabilityDate = accessStatus.getAvailabilityDate();
|
||||
assertNotEquals("testGetAccessStatusItem 0", status, DefaultAccessStatusHelper.UNKNOWN);
|
||||
assertNull("testGetAccessStatusItem 1", availabilityDate);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAnonymousAccessStatusItem() throws Exception {
|
||||
AccessStatus accessStatus = accessStatusService.getAnonymousAccessStatus(context, item);
|
||||
String status = accessStatus.getStatus();
|
||||
LocalDate availabilityDate = accessStatus.getAvailabilityDate();
|
||||
assertNotEquals("testGetAnonymousAccessStatusItem 0", status, DefaultAccessStatusHelper.UNKNOWN);
|
||||
assertNull("testGetAnonymousAccessStatusItem 1", availabilityDate);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAccessStatusBitstream() throws Exception {
|
||||
AccessStatus accessStatus = accessStatusService.getAccessStatus(context, bitstream);
|
||||
String status = accessStatus.getStatus();
|
||||
LocalDate availabilityDate = accessStatus.getAvailabilityDate();
|
||||
assertNotEquals("testGetAccessStatusBitstream 0", status, DefaultAccessStatusHelper.UNKNOWN);
|
||||
assertNull("testGetAccessStatusBitstream 1", availabilityDate);
|
||||
}
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ package org.dspace.access.status;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
@@ -25,6 +26,7 @@ import org.dspace.authorize.AuthorizeException;
|
||||
import org.dspace.authorize.ResourcePolicy;
|
||||
import org.dspace.authorize.factory.AuthorizeServiceFactory;
|
||||
import org.dspace.authorize.service.ResourcePolicyService;
|
||||
import org.dspace.content.AccessStatus;
|
||||
import org.dspace.content.Bitstream;
|
||||
import org.dspace.content.Bundle;
|
||||
import org.dspace.content.Collection;
|
||||
@@ -39,8 +41,10 @@ import org.dspace.content.service.InstallItemService;
|
||||
import org.dspace.content.service.ItemService;
|
||||
import org.dspace.content.service.WorkspaceItemService;
|
||||
import org.dspace.core.Constants;
|
||||
import org.dspace.eperson.EPerson;
|
||||
import org.dspace.eperson.Group;
|
||||
import org.dspace.eperson.factory.EPersonServiceFactory;
|
||||
import org.dspace.eperson.service.EPersonService;
|
||||
import org.dspace.eperson.service.GroupService;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
@@ -83,6 +87,8 @@ public class DefaultAccessStatusHelperTest extends AbstractUnitTest {
|
||||
AuthorizeServiceFactory.getInstance().getResourcePolicyService();
|
||||
protected GroupService groupService =
|
||||
EPersonServiceFactory.getInstance().getGroupService();
|
||||
protected EPersonService ePersonService =
|
||||
EPersonServiceFactory.getInstance().getEPersonService();
|
||||
|
||||
/**
|
||||
* This method will be run before every test as per @Before. It will
|
||||
@@ -203,8 +209,13 @@ public class DefaultAccessStatusHelperTest extends AbstractUnitTest {
|
||||
*/
|
||||
@Test
|
||||
public void testWithNullItem() throws Exception {
|
||||
String status = helper.getAccessStatusFromItem(context, null, threshold);
|
||||
// getAccessStatusFromItem
|
||||
AccessStatus accessStatus = helper.getAccessStatusFromItem(context,
|
||||
null, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
|
||||
String status = accessStatus.getStatus();
|
||||
assertThat("testWithNullItem 0", status, equalTo(DefaultAccessStatusHelper.UNKNOWN));
|
||||
LocalDate availabilityDate = accessStatus.getAvailabilityDate();
|
||||
assertNull("testWithNullItem 1", availabilityDate);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -213,8 +224,13 @@ public class DefaultAccessStatusHelperTest extends AbstractUnitTest {
|
||||
*/
|
||||
@Test
|
||||
public void testWithoutBundle() throws Exception {
|
||||
String status = helper.getAccessStatusFromItem(context, itemWithoutBundle, threshold);
|
||||
// getAccessStatusFromItem
|
||||
AccessStatus accessStatus = helper.getAccessStatusFromItem(context,
|
||||
itemWithoutBundle, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
|
||||
String status = accessStatus.getStatus();
|
||||
assertThat("testWithoutBundle 0", status, equalTo(DefaultAccessStatusHelper.METADATA_ONLY));
|
||||
LocalDate availabilityDate = accessStatus.getAvailabilityDate();
|
||||
assertNull("testWithoutBundle 1", availabilityDate);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -226,8 +242,20 @@ public class DefaultAccessStatusHelperTest extends AbstractUnitTest {
|
||||
context.turnOffAuthorisationSystem();
|
||||
bundleService.create(context, itemWithoutBitstream, Constants.CONTENT_BUNDLE_NAME);
|
||||
context.restoreAuthSystemState();
|
||||
String status = helper.getAccessStatusFromItem(context, itemWithoutBitstream, threshold);
|
||||
// getAccessStatusFromItem
|
||||
AccessStatus accessStatus = helper.getAccessStatusFromItem(context,
|
||||
itemWithoutBitstream, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
|
||||
String status = accessStatus.getStatus();
|
||||
assertThat("testWithoutBitstream 0", status, equalTo(DefaultAccessStatusHelper.METADATA_ONLY));
|
||||
LocalDate availabilityDate = accessStatus.getAvailabilityDate();
|
||||
assertNull("testWithoutBitstream 1", availabilityDate);
|
||||
// getAccessStatusFromBitstream
|
||||
AccessStatus accessStatusBitstream = helper.getAccessStatusFromBitstream(context,
|
||||
null, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
|
||||
String bitstreamStatus = accessStatusBitstream.getStatus();
|
||||
assertThat("testWithoutBitstream 3", bitstreamStatus, equalTo(DefaultAccessStatusHelper.UNKNOWN));
|
||||
LocalDate bitstreamAvailabilityDate = accessStatusBitstream.getAvailabilityDate();
|
||||
assertNull("testWithoutBitstream 4", bitstreamAvailabilityDate);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -243,8 +271,20 @@ public class DefaultAccessStatusHelperTest extends AbstractUnitTest {
|
||||
bitstream.setName(context, "primary");
|
||||
bundle.setPrimaryBitstreamID(bitstream);
|
||||
context.restoreAuthSystemState();
|
||||
String status = helper.getAccessStatusFromItem(context, itemWithBitstream, threshold);
|
||||
// getAccessStatusFromItem
|
||||
AccessStatus accessStatus = helper.getAccessStatusFromItem(context,
|
||||
itemWithBitstream, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
|
||||
String status = accessStatus.getStatus();
|
||||
assertThat("testWithBitstream 0", status, equalTo(DefaultAccessStatusHelper.OPEN_ACCESS));
|
||||
LocalDate availabilityDate = accessStatus.getAvailabilityDate();
|
||||
assertNull("testWithBitstream 1", availabilityDate);
|
||||
// getAccessStatusFromBitstream
|
||||
AccessStatus accessStatusBitstream = helper.getAccessStatusFromBitstream(context,
|
||||
bitstream, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
|
||||
String bitstreamStatus = accessStatusBitstream.getStatus();
|
||||
assertThat("testWithBitstream 3", bitstreamStatus, equalTo(DefaultAccessStatusHelper.OPEN_ACCESS));
|
||||
LocalDate bitstreamAvailabilityDate = accessStatusBitstream.getAvailabilityDate();
|
||||
assertNull("testWithBitstream 4", bitstreamAvailabilityDate);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -264,15 +304,26 @@ public class DefaultAccessStatusHelperTest extends AbstractUnitTest {
|
||||
ResourcePolicy policy = resourcePolicyService.create(context, null, group);
|
||||
policy.setRpName("Embargo");
|
||||
policy.setAction(Constants.READ);
|
||||
policy.setStartDate(LocalDate.of(9999, 12, 31));
|
||||
LocalDate startDate = LocalDate.of(9999, 12, 31);
|
||||
policy.setStartDate(startDate);
|
||||
policies.add(policy);
|
||||
authorizeService.removeAllPolicies(context, bitstream);
|
||||
authorizeService.addPolicies(context, policies, bitstream);
|
||||
context.restoreAuthSystemState();
|
||||
String status = helper.getAccessStatusFromItem(context, itemWithEmbargo, threshold);
|
||||
// getAccessStatusFromItem
|
||||
AccessStatus accessStatus = helper.getAccessStatusFromItem(context,
|
||||
itemWithEmbargo, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
|
||||
String status = accessStatus.getStatus();
|
||||
assertThat("testWithEmbargo 0", status, equalTo(DefaultAccessStatusHelper.EMBARGO));
|
||||
String embargoDate = helper.getEmbargoFromItem(context, itemWithEmbargo, threshold);
|
||||
assertThat("testWithEmbargo 1", embargoDate, equalTo(policy.getStartDate().toString()));
|
||||
LocalDate availabilityDate = accessStatus.getAvailabilityDate();
|
||||
assertThat("testWithEmbargo 1", availabilityDate, equalTo(startDate));
|
||||
// getAccessStatusFromBitstream
|
||||
AccessStatus accessStatusBitstream = helper.getAccessStatusFromBitstream(context,
|
||||
bitstream, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
|
||||
String bitstreamStatus = accessStatusBitstream.getStatus();
|
||||
assertThat("testWithEmbargo 3", bitstreamStatus, equalTo(DefaultAccessStatusHelper.EMBARGO));
|
||||
LocalDate bitstreamAvailabilityDate = accessStatusBitstream.getAvailabilityDate();
|
||||
assertThat("testWithEmbargo 4", bitstreamAvailabilityDate, equalTo(startDate));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -292,13 +343,26 @@ public class DefaultAccessStatusHelperTest extends AbstractUnitTest {
|
||||
ResourcePolicy policy = resourcePolicyService.create(context, null, group);
|
||||
policy.setRpName("Restriction");
|
||||
policy.setAction(Constants.READ);
|
||||
policy.setStartDate(LocalDate.of(10000, 1, 1));
|
||||
LocalDate startDate = LocalDate.of(10000, 1, 1);
|
||||
policy.setStartDate(startDate);
|
||||
policies.add(policy);
|
||||
authorizeService.removeAllPolicies(context, bitstream);
|
||||
authorizeService.addPolicies(context, policies, bitstream);
|
||||
context.restoreAuthSystemState();
|
||||
String status = helper.getAccessStatusFromItem(context, itemWithDateRestriction, threshold);
|
||||
// getAccessStatusFromItem
|
||||
AccessStatus accessStatus = helper.getAccessStatusFromItem(context,
|
||||
itemWithDateRestriction, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
|
||||
String status = accessStatus.getStatus();
|
||||
assertThat("testWithDateRestriction 0", status, equalTo(DefaultAccessStatusHelper.RESTRICTED));
|
||||
LocalDate availabilityDate = accessStatus.getAvailabilityDate();
|
||||
assertThat("testWithDateRestriction 1", availabilityDate, equalTo(startDate));
|
||||
// getAccessStatusFromBitstream
|
||||
AccessStatus accessStatusBitstream = helper.getAccessStatusFromBitstream(context,
|
||||
bitstream, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
|
||||
String bitstreamStatus = accessStatusBitstream.getStatus();
|
||||
assertThat("testWithDateRestriction 3", bitstreamStatus, equalTo(DefaultAccessStatusHelper.RESTRICTED));
|
||||
LocalDate bistreamAvailabilityDate = accessStatusBitstream.getAvailabilityDate();
|
||||
assertThat("testWithDateRestriction 4", bistreamAvailabilityDate, equalTo(startDate));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -322,8 +386,20 @@ public class DefaultAccessStatusHelperTest extends AbstractUnitTest {
|
||||
authorizeService.removeAllPolicies(context, bitstream);
|
||||
authorizeService.addPolicies(context, policies, bitstream);
|
||||
context.restoreAuthSystemState();
|
||||
String status = helper.getAccessStatusFromItem(context, itemWithGroupRestriction, threshold);
|
||||
// getAccessStatusFromItem
|
||||
AccessStatus accessStatus = helper.getAccessStatusFromItem(context,
|
||||
itemWithGroupRestriction, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
|
||||
String status = accessStatus.getStatus();
|
||||
assertThat("testWithGroupRestriction 0", status, equalTo(DefaultAccessStatusHelper.RESTRICTED));
|
||||
LocalDate availabilityDate = accessStatus.getAvailabilityDate();
|
||||
assertThat("testWithGroupRestriction 1", availabilityDate, equalTo(threshold));
|
||||
// getAccessStatusFromBitstream
|
||||
AccessStatus accessStatusBitstream = helper.getAccessStatusFromBitstream(context,
|
||||
bitstream, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
|
||||
String bitstreamStatus = accessStatusBitstream.getStatus();
|
||||
assertThat("testWithGroupRestriction 3", bitstreamStatus, equalTo(DefaultAccessStatusHelper.RESTRICTED));
|
||||
LocalDate bitstreamAvailabilityDate = accessStatusBitstream.getAvailabilityDate();
|
||||
assertThat("testWithGroupRestriction 4", bitstreamAvailabilityDate, equalTo(threshold));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -340,8 +416,20 @@ public class DefaultAccessStatusHelperTest extends AbstractUnitTest {
|
||||
bundle.setPrimaryBitstreamID(bitstream);
|
||||
authorizeService.removeAllPolicies(context, bitstream);
|
||||
context.restoreAuthSystemState();
|
||||
String status = helper.getAccessStatusFromItem(context, itemWithoutPolicy, threshold);
|
||||
// getAccessStatusFromItem
|
||||
AccessStatus accessStatus = helper.getAccessStatusFromItem(context,
|
||||
itemWithoutPolicy, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
|
||||
String status = accessStatus.getStatus();
|
||||
assertThat("testWithoutPolicy 0", status, equalTo(DefaultAccessStatusHelper.RESTRICTED));
|
||||
LocalDate availabilityDate = accessStatus.getAvailabilityDate();
|
||||
assertThat("testWithoutPolicy 1", availabilityDate, equalTo(threshold));
|
||||
// getAccessStatusFromBitstream
|
||||
AccessStatus accessStatusBitstream = helper.getAccessStatusFromBitstream(context,
|
||||
bitstream, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
|
||||
String bitstreamStatus = accessStatusBitstream.getStatus();
|
||||
assertThat("testWithoutPolicy 3", bitstreamStatus, equalTo(DefaultAccessStatusHelper.RESTRICTED));
|
||||
LocalDate bitstreamAvailabilityDate = accessStatusBitstream.getAvailabilityDate();
|
||||
assertThat("testWithoutPolicy 4", bitstreamAvailabilityDate, equalTo(threshold));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -356,8 +444,20 @@ public class DefaultAccessStatusHelperTest extends AbstractUnitTest {
|
||||
new ByteArrayInputStream("1".getBytes(StandardCharsets.UTF_8)));
|
||||
bitstream.setName(context, "first");
|
||||
context.restoreAuthSystemState();
|
||||
String status = helper.getAccessStatusFromItem(context, itemWithoutPrimaryBitstream, threshold);
|
||||
// getAccessStatusFromItem
|
||||
AccessStatus accessStatus = helper.getAccessStatusFromItem(context,
|
||||
itemWithoutPrimaryBitstream, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
|
||||
String status = accessStatus.getStatus();
|
||||
assertThat("testWithoutPrimaryBitstream 0", status, equalTo(DefaultAccessStatusHelper.OPEN_ACCESS));
|
||||
LocalDate availabilityDate = accessStatus.getAvailabilityDate();
|
||||
assertNull("testWithoutPrimaryBitstream 1", availabilityDate);
|
||||
// getAccessStatusFromBitstream
|
||||
AccessStatus accessStatusBitstream = helper.getAccessStatusFromBitstream(context,
|
||||
bitstream, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
|
||||
String bitstreamStatus = accessStatusBitstream.getStatus();
|
||||
assertThat("testWithoutPrimaryBitstream 3", bitstreamStatus, equalTo(DefaultAccessStatusHelper.OPEN_ACCESS));
|
||||
LocalDate bitstreamAvailabilityDate = accessStatusBitstream.getAvailabilityDate();
|
||||
assertNull("testWithoutPrimaryBitstream 4", bitstreamAvailabilityDate);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -370,7 +470,7 @@ public class DefaultAccessStatusHelperTest extends AbstractUnitTest {
|
||||
context.turnOffAuthorisationSystem();
|
||||
Bundle bundle = bundleService.create(context, itemWithPrimaryAndMultipleBitstreams,
|
||||
Constants.CONTENT_BUNDLE_NAME);
|
||||
bitstreamService.create(context, bundle,
|
||||
Bitstream otherBitstream = bitstreamService.create(context, bundle,
|
||||
new ByteArrayInputStream("1".getBytes(StandardCharsets.UTF_8)));
|
||||
Bitstream primaryBitstream = bitstreamService.create(context, bundle,
|
||||
new ByteArrayInputStream("1".getBytes(StandardCharsets.UTF_8)));
|
||||
@@ -380,15 +480,35 @@ public class DefaultAccessStatusHelperTest extends AbstractUnitTest {
|
||||
ResourcePolicy policy = resourcePolicyService.create(context, null, group);
|
||||
policy.setRpName("Embargo");
|
||||
policy.setAction(Constants.READ);
|
||||
policy.setStartDate(LocalDate.of(9999, 12, 31));
|
||||
LocalDate startDate = LocalDate.of(9999, 12, 31);
|
||||
policy.setStartDate(startDate);
|
||||
policies.add(policy);
|
||||
authorizeService.removeAllPolicies(context, primaryBitstream);
|
||||
authorizeService.addPolicies(context, policies, primaryBitstream);
|
||||
context.restoreAuthSystemState();
|
||||
String status = helper.getAccessStatusFromItem(context, itemWithPrimaryAndMultipleBitstreams, threshold);
|
||||
// getAccessStatusFromItem
|
||||
AccessStatus accessStatus = helper.getAccessStatusFromItem(context,
|
||||
itemWithPrimaryAndMultipleBitstreams, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
|
||||
String status = accessStatus.getStatus();
|
||||
assertThat("testWithPrimaryAndMultipleBitstreams 0", status, equalTo(DefaultAccessStatusHelper.EMBARGO));
|
||||
String embargoDate = helper.getEmbargoFromItem(context, itemWithPrimaryAndMultipleBitstreams, threshold);
|
||||
assertThat("testWithPrimaryAndMultipleBitstreams 1", embargoDate, equalTo(policy.getStartDate().toString()));
|
||||
LocalDate availabilityDate = accessStatus.getAvailabilityDate();
|
||||
assertThat("testWithPrimaryAndMultipleBitstreams 1", availabilityDate, equalTo(startDate));
|
||||
// getAccessStatusFromBitstream -> primary
|
||||
AccessStatus accessStatusPrimaryBitstream = helper.getAccessStatusFromBitstream(context,
|
||||
primaryBitstream, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
|
||||
String primaryBitstreamStatus = accessStatusPrimaryBitstream.getStatus();
|
||||
assertThat("testWithPrimaryAndMultipleBitstreams 3", primaryBitstreamStatus,
|
||||
equalTo(DefaultAccessStatusHelper.EMBARGO));
|
||||
LocalDate primaryAvailabilityDate = accessStatusPrimaryBitstream.getAvailabilityDate();
|
||||
assertThat("testWithPrimaryAndMultipleBitstreams 4", primaryAvailabilityDate, equalTo(startDate));
|
||||
// getAccessStatusFromBitstream -> other
|
||||
AccessStatus accessStatusOtherBitstream = helper.getAccessStatusFromBitstream(context,
|
||||
otherBitstream, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
|
||||
String otherBitstreamStatus = accessStatusOtherBitstream.getStatus();
|
||||
assertThat("testWithPrimaryAndMultipleBitstreams 5", otherBitstreamStatus,
|
||||
equalTo(DefaultAccessStatusHelper.OPEN_ACCESS));
|
||||
LocalDate otherAvailabilityDate = accessStatusOtherBitstream.getAvailabilityDate();
|
||||
assertNull("testWithPrimaryAndMultipleBitstreams 6", otherAvailabilityDate);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -401,7 +521,7 @@ public class DefaultAccessStatusHelperTest extends AbstractUnitTest {
|
||||
context.turnOffAuthorisationSystem();
|
||||
Bundle bundle = bundleService.create(context, itemWithoutPrimaryAndMultipleBitstreams,
|
||||
Constants.CONTENT_BUNDLE_NAME);
|
||||
bitstreamService.create(context, bundle,
|
||||
Bitstream firstBitstream = bitstreamService.create(context, bundle,
|
||||
new ByteArrayInputStream("1".getBytes(StandardCharsets.UTF_8)));
|
||||
Bitstream anotherBitstream = bitstreamService.create(context, bundle,
|
||||
new ByteArrayInputStream("1".getBytes(StandardCharsets.UTF_8)));
|
||||
@@ -410,14 +530,167 @@ public class DefaultAccessStatusHelperTest extends AbstractUnitTest {
|
||||
ResourcePolicy policy = resourcePolicyService.create(context, null, group);
|
||||
policy.setRpName("Embargo");
|
||||
policy.setAction(Constants.READ);
|
||||
policy.setStartDate(LocalDate.of(9999, 12, 31));
|
||||
LocalDate startDate = LocalDate.of(9999, 12, 31);
|
||||
policy.setStartDate(startDate);
|
||||
policies.add(policy);
|
||||
authorizeService.removeAllPolicies(context, anotherBitstream);
|
||||
authorizeService.addPolicies(context, policies, anotherBitstream);
|
||||
context.restoreAuthSystemState();
|
||||
String status = helper.getAccessStatusFromItem(context, itemWithoutPrimaryAndMultipleBitstreams, threshold);
|
||||
assertThat("testWithNoPrimaryAndMultipleBitstreams 0", status, equalTo(DefaultAccessStatusHelper.OPEN_ACCESS));
|
||||
String embargoDate = helper.getEmbargoFromItem(context, itemWithEmbargo, threshold);
|
||||
assertThat("testWithNoPrimaryAndMultipleBitstreams 1", embargoDate, equalTo(null));
|
||||
// getAccessStatusFromItem
|
||||
AccessStatus accessStatus = helper.getAccessStatusFromItem(context,
|
||||
itemWithoutPrimaryAndMultipleBitstreams, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
|
||||
String status = accessStatus.getStatus();
|
||||
assertThat("testWithNoPrimaryAndMultipleBitstreams 0", status,
|
||||
equalTo(DefaultAccessStatusHelper.OPEN_ACCESS));
|
||||
LocalDate availabilityDate = accessStatus.getAvailabilityDate();
|
||||
assertNull("testWithNoPrimaryAndMultipleBitstreams 1", availabilityDate);
|
||||
// getAccessStatusFromBitstream -> first
|
||||
AccessStatus accessStatusFirstBitstream = helper.getAccessStatusFromBitstream(context,
|
||||
firstBitstream, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
|
||||
String firstBitstreamStatus = accessStatusFirstBitstream.getStatus();
|
||||
assertThat("testWithNoPrimaryAndMultipleBitstreams 3", firstBitstreamStatus,
|
||||
equalTo(DefaultAccessStatusHelper.OPEN_ACCESS));
|
||||
LocalDate firstAvailabilityDate = accessStatusFirstBitstream.getAvailabilityDate();
|
||||
assertNull("testWithNoPrimaryAndMultipleBitstreams 4", firstAvailabilityDate);
|
||||
// getAccessStatusFromBitstream -> other
|
||||
AccessStatus accessStatusOtherBitstream = helper.getAccessStatusFromBitstream(context,
|
||||
anotherBitstream, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
|
||||
String otherBitstreamStatus = accessStatusOtherBitstream.getStatus();
|
||||
assertThat("testWithNoPrimaryAndMultipleBitstreams 5", otherBitstreamStatus,
|
||||
equalTo(DefaultAccessStatusHelper.EMBARGO));
|
||||
LocalDate otherAvailabilityDate = accessStatusOtherBitstream.getAvailabilityDate();
|
||||
assertThat("testWithNoPrimaryAndMultipleBitstreams 6", otherAvailabilityDate, equalTo(startDate));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for an item with an embargo for both configurations (current, anonymous) and as a guest
|
||||
* @throws java.lang.Exception passed through.
|
||||
*/
|
||||
@Test
|
||||
public void testWithEmbargoForCurrentOrAnonymousAsGuest() throws Exception {
|
||||
context.turnOffAuthorisationSystem();
|
||||
Bundle bundle = bundleService.create(context, itemWithEmbargo, Constants.CONTENT_BUNDLE_NAME);
|
||||
Bitstream bitstream = bitstreamService.create(context, bundle,
|
||||
new ByteArrayInputStream("1".getBytes(StandardCharsets.UTF_8)));
|
||||
bitstream.setName(context, "primary");
|
||||
bundle.setPrimaryBitstreamID(bitstream);
|
||||
List<ResourcePolicy> policies = new ArrayList<>();
|
||||
Group group = groupService.findByName(context, Group.ANONYMOUS);
|
||||
ResourcePolicy policy = resourcePolicyService.create(context, null, group);
|
||||
policy.setRpName("Embargo");
|
||||
policy.setAction(Constants.READ);
|
||||
LocalDate startDate = LocalDate.of(9999, 12, 31);
|
||||
policy.setStartDate(startDate);
|
||||
policies.add(policy);
|
||||
EPerson admin = ePersonService.create(context);
|
||||
Group adminGroup = groupService.findByName(context, Group.ADMIN);
|
||||
ResourcePolicy adminPolicy = resourcePolicyService.create(context, admin, adminGroup);
|
||||
adminPolicy.setRpName("Open Access For Admin");
|
||||
adminPolicy.setAction(Constants.READ);
|
||||
policies.add(adminPolicy);
|
||||
authorizeService.removeAllPolicies(context, bitstream);
|
||||
authorizeService.addPolicies(context, policies, bitstream);
|
||||
context.restoreAuthSystemState();
|
||||
// Configuration: current
|
||||
// getAccessStatusFromItem
|
||||
AccessStatus accessStatus = helper.getAccessStatusFromItem(context,
|
||||
itemWithEmbargo, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
|
||||
String status = accessStatus.getStatus();
|
||||
assertThat("testWithEmbargoForCurrentOrAnonymousAsGuest 1", status,
|
||||
equalTo(DefaultAccessStatusHelper.EMBARGO));
|
||||
LocalDate availabilityDate = accessStatus.getAvailabilityDate();
|
||||
assertThat("testWithEmbargoForCurrentOrAnonymousAsGuest 2", availabilityDate, equalTo(startDate));
|
||||
// getAccessStatusFromBitstream
|
||||
AccessStatus accessStatusBitstream = helper.getAccessStatusFromBitstream(context,
|
||||
bitstream, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
|
||||
String bitstreamStatus = accessStatusBitstream.getStatus();
|
||||
assertThat("testWithEmbargoForCurrentOrAnonymousAsGuest 3", bitstreamStatus,
|
||||
equalTo(DefaultAccessStatusHelper.EMBARGO));
|
||||
LocalDate bistreamAvailabilityDate = accessStatusBitstream.getAvailabilityDate();
|
||||
assertThat("testWithEmbargoForCurrentOrAnonymousAsGuest 4", bistreamAvailabilityDate, equalTo(startDate));
|
||||
// Configuration: anonymous
|
||||
// getAccessStatusFromItem
|
||||
accessStatus = helper.getAccessStatusFromItem(context,
|
||||
itemWithEmbargo, threshold, DefaultAccessStatusHelper.STATUS_FOR_ANONYMOUS);
|
||||
status = accessStatus.getStatus();
|
||||
assertThat("testWithEmbargoForCurrentOrAnonymousAsGuest 5", status,
|
||||
equalTo(DefaultAccessStatusHelper.EMBARGO));
|
||||
availabilityDate = accessStatus.getAvailabilityDate();
|
||||
assertThat("testWithEmbargoForCurrentOrAnonymousAsGuest 6", availabilityDate, equalTo(startDate));
|
||||
// getAccessStatusFromBitstream
|
||||
accessStatusBitstream = helper.getAccessStatusFromBitstream(context,
|
||||
bitstream, threshold, DefaultAccessStatusHelper.STATUS_FOR_ANONYMOUS);
|
||||
bitstreamStatus = accessStatusBitstream.getStatus();
|
||||
assertThat("testWithEmbargoForCurrentOrAnonymousAsGuest 7", bitstreamStatus,
|
||||
equalTo(DefaultAccessStatusHelper.EMBARGO));
|
||||
bistreamAvailabilityDate = accessStatusBitstream.getAvailabilityDate();
|
||||
assertThat("testWithEmbargoForCurrentOrAnonymousAsGuest 8", bistreamAvailabilityDate, equalTo(startDate));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for an item with an embargo for both configurations (current, anonymous) and as an admin
|
||||
* @throws java.lang.Exception passed through.
|
||||
*/
|
||||
@Test
|
||||
public void testWithEmbargoForCurrentOrAnonymousAsAdmin() throws Exception {
|
||||
context.turnOffAuthorisationSystem();
|
||||
Bundle bundle = bundleService.create(context, itemWithEmbargo, Constants.CONTENT_BUNDLE_NAME);
|
||||
Bitstream bitstream = bitstreamService.create(context, bundle,
|
||||
new ByteArrayInputStream("1".getBytes(StandardCharsets.UTF_8)));
|
||||
bitstream.setName(context, "primary");
|
||||
bundle.setPrimaryBitstreamID(bitstream);
|
||||
List<ResourcePolicy> policies = new ArrayList<>();
|
||||
Group group = groupService.findByName(context, Group.ANONYMOUS);
|
||||
ResourcePolicy policy = resourcePolicyService.create(context, null, group);
|
||||
policy.setRpName("Embargo");
|
||||
policy.setAction(Constants.READ);
|
||||
LocalDate startDate = LocalDate.of(9999, 12, 31);
|
||||
policy.setStartDate(startDate);
|
||||
policies.add(policy);
|
||||
EPerson admin = ePersonService.create(context);
|
||||
Group adminGroup = groupService.findByName(context, Group.ADMIN);
|
||||
ResourcePolicy adminPolicy = resourcePolicyService.create(context, admin, adminGroup);
|
||||
adminPolicy.setRpName("Open Access For Admin");
|
||||
adminPolicy.setAction(Constants.READ);
|
||||
policies.add(adminPolicy);
|
||||
authorizeService.removeAllPolicies(context, bitstream);
|
||||
authorizeService.addPolicies(context, policies, bitstream);
|
||||
context.restoreAuthSystemState();
|
||||
EPerson currentUser = context.getCurrentUser();
|
||||
context.setCurrentUser(admin);
|
||||
// Configuration: current
|
||||
// getAccessStatusFromItem
|
||||
AccessStatus accessStatus = helper.getAccessStatusFromItem(context,
|
||||
itemWithEmbargo, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
|
||||
String status = accessStatus.getStatus();
|
||||
assertThat("testWithEmbargoForCurrentOrAnonymousAsAdmin 1", status,
|
||||
equalTo(DefaultAccessStatusHelper.OPEN_ACCESS));
|
||||
LocalDate availabilityDate = accessStatus.getAvailabilityDate();
|
||||
assertNull("testWithEmbargoForCurrentOrAnonymousAsAdmin 2", availabilityDate);
|
||||
// getAccessStatusFromBitstream
|
||||
AccessStatus accessStatusBitstream = helper.getAccessStatusFromBitstream(context,
|
||||
bitstream, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
|
||||
String bitstreamStatus = accessStatusBitstream.getStatus();
|
||||
assertThat("testWithEmbargoForCurrentOrAnonymousAsAdmin 3", bitstreamStatus,
|
||||
equalTo(DefaultAccessStatusHelper.OPEN_ACCESS));
|
||||
LocalDate bitstreamAvailabilityDate = accessStatusBitstream.getAvailabilityDate();
|
||||
assertNull("testWithEmbargoForCurrentOrAnonymousAsAdmin 4", bitstreamAvailabilityDate);
|
||||
// Configuration: anonymous
|
||||
accessStatus = helper.getAccessStatusFromItem(context,
|
||||
itemWithEmbargo, threshold, DefaultAccessStatusHelper.STATUS_FOR_ANONYMOUS);
|
||||
status = accessStatus.getStatus();
|
||||
assertThat("testWithEmbargoForCurrentOrAnonymousAsAdmin 5", status,
|
||||
equalTo(DefaultAccessStatusHelper.EMBARGO));
|
||||
availabilityDate = accessStatus.getAvailabilityDate();
|
||||
assertThat("testWithEmbargoForCurrentOrAnonymousAsAdmin 6", availabilityDate, equalTo(startDate));
|
||||
// getAccessStatusFromBitstream
|
||||
accessStatusBitstream = helper.getAccessStatusFromBitstream(context,
|
||||
bitstream, threshold, DefaultAccessStatusHelper.STATUS_FOR_ANONYMOUS);
|
||||
bitstreamStatus = accessStatusBitstream.getStatus();
|
||||
assertThat("testWithEmbargoForCurrentOrAnonymousAsAdmin 7", bitstreamStatus,
|
||||
equalTo(DefaultAccessStatusHelper.EMBARGO));
|
||||
bitstreamAvailabilityDate = accessStatusBitstream.getAvailabilityDate();
|
||||
assertThat("testWithEmbargoForCurrentOrAnonymousAsAdmin 8", bitstreamAvailabilityDate, equalTo(startDate));
|
||||
context.setCurrentUser(currentUser);
|
||||
}
|
||||
}
|
||||
|
@@ -10,8 +10,12 @@ package org.dspace.app.requestitem;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
import jakarta.mail.Address;
|
||||
import jakarta.mail.Message;
|
||||
import jakarta.mail.Provider;
|
||||
@@ -21,9 +25,12 @@ import org.dspace.AbstractUnitTest;
|
||||
import org.dspace.app.requestitem.factory.RequestItemServiceFactory;
|
||||
import org.dspace.app.requestitem.service.RequestItemService;
|
||||
import org.dspace.builder.AbstractBuilder;
|
||||
import org.dspace.builder.BitstreamBuilder;
|
||||
import org.dspace.builder.CollectionBuilder;
|
||||
import org.dspace.builder.CommunityBuilder;
|
||||
import org.dspace.builder.ItemBuilder;
|
||||
import org.dspace.builder.RequestItemBuilder;
|
||||
import org.dspace.content.Bitstream;
|
||||
import org.dspace.content.Collection;
|
||||
import org.dspace.content.Community;
|
||||
import org.dspace.content.Item;
|
||||
@@ -38,6 +45,7 @@ import org.junit.BeforeClass;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
|
||||
/**
|
||||
* Tests for {@link RequestItemEmailNotifier}.
|
||||
*
|
||||
@@ -59,6 +67,7 @@ public class RequestItemEmailNotifierTest
|
||||
private static BitstreamService bitstreamService;
|
||||
private static HandleService handleService;
|
||||
private static RequestItemService requestItemService;
|
||||
private static RequestItemEmailNotifier requestItemEmailNotifier;
|
||||
|
||||
public RequestItemEmailNotifierTest() {
|
||||
super();
|
||||
@@ -76,6 +85,18 @@ public class RequestItemEmailNotifierTest
|
||||
= HandleServiceFactory.getInstance().getHandleService();
|
||||
requestItemService
|
||||
= RequestItemServiceFactory.getInstance().getRequestItemService();
|
||||
|
||||
// Instantiate and initialize the unit, using the "help desk" strategy.
|
||||
requestItemEmailNotifier
|
||||
= new RequestItemEmailNotifier(
|
||||
DSpaceServicesFactory.getInstance()
|
||||
.getServiceManager()
|
||||
.getServiceByName(RequestItemHelpdeskStrategy.class.getName(),
|
||||
RequestItemAuthorExtractor.class));
|
||||
requestItemEmailNotifier.bitstreamService = bitstreamService;
|
||||
requestItemEmailNotifier.configurationService = configurationService;
|
||||
requestItemEmailNotifier.handleService = handleService;
|
||||
requestItemEmailNotifier.requestItemService = requestItemService;
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
@@ -87,7 +108,7 @@ public class RequestItemEmailNotifierTest
|
||||
|
||||
/**
|
||||
* Test of sendRequest method, of class RequestItemEmailNotifier.
|
||||
* @throws java.lang.Exception passed through.
|
||||
* @throws Exception passed through.
|
||||
*/
|
||||
@Ignore
|
||||
@Test
|
||||
@@ -96,7 +117,7 @@ public class RequestItemEmailNotifierTest
|
||||
|
||||
/**
|
||||
* Test of sendResponse method, of class RequestItemEmailNotifier.
|
||||
* @throws java.lang.Exception passed through.
|
||||
* @throws Exception passed through.
|
||||
*/
|
||||
@Test
|
||||
public void testSendResponse() throws Exception {
|
||||
@@ -137,18 +158,6 @@ public class RequestItemEmailNotifierTest
|
||||
// Ensure that mail is "sent".
|
||||
configurationService.setProperty("mail.server.disabled", "false");
|
||||
|
||||
// Instantiate and initialize the unit, using the "help desk" strategy.
|
||||
RequestItemEmailNotifier requestItemEmailNotifier
|
||||
= new RequestItemEmailNotifier(
|
||||
DSpaceServicesFactory.getInstance()
|
||||
.getServiceManager()
|
||||
.getServiceByName(RequestItemHelpdeskStrategy.class.getName(),
|
||||
RequestItemAuthorExtractor.class));
|
||||
requestItemEmailNotifier.bitstreamService = bitstreamService;
|
||||
requestItemEmailNotifier.configurationService = configurationService;
|
||||
requestItemEmailNotifier.handleService = handleService;
|
||||
requestItemEmailNotifier.requestItemService = requestItemService;
|
||||
|
||||
// Test the unit. Template supplies the Subject: value
|
||||
requestItemEmailNotifier.sendResponse(context, ri, null, TEST_MESSAGE);
|
||||
|
||||
@@ -180,7 +189,7 @@ public class RequestItemEmailNotifierTest
|
||||
|
||||
/**
|
||||
* Test of sendResponse method -- rejection case.
|
||||
* @throws java.lang.Exception passed through.
|
||||
* @throws Exception passed through.
|
||||
*/
|
||||
@Test
|
||||
public void testSendRejection()
|
||||
@@ -222,18 +231,6 @@ public class RequestItemEmailNotifierTest
|
||||
// Ensure that mail is "sent".
|
||||
configurationService.setProperty("mail.server.disabled", "false");
|
||||
|
||||
// Instantiate and initialize the unit, using the "help desk" strategy.
|
||||
RequestItemEmailNotifier requestItemEmailNotifier
|
||||
= new RequestItemEmailNotifier(
|
||||
DSpaceServicesFactory.getInstance()
|
||||
.getServiceManager()
|
||||
.getServiceByName(RequestItemHelpdeskStrategy.class.getName(),
|
||||
RequestItemAuthorExtractor.class));
|
||||
requestItemEmailNotifier.bitstreamService = bitstreamService;
|
||||
requestItemEmailNotifier.configurationService = configurationService;
|
||||
requestItemEmailNotifier.handleService = handleService;
|
||||
requestItemEmailNotifier.requestItemService = requestItemService;
|
||||
|
||||
// Test the unit. Template supplies the Subject: value
|
||||
requestItemEmailNotifier.sendResponse(context, ri, null, TEST_MESSAGE);
|
||||
|
||||
@@ -267,9 +264,54 @@ public class RequestItemEmailNotifierTest
|
||||
(String)content, containsString("denied"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmailGenerationWithLargeFileLink() throws Exception {
|
||||
// Create some content to send.
|
||||
context.turnOffAuthorisationSystem();
|
||||
Community com = CommunityBuilder.createCommunity(context)
|
||||
.withName("Top Community")
|
||||
.build();
|
||||
Collection col = CollectionBuilder.createCollection(context, com)
|
||||
.build();
|
||||
Item item = ItemBuilder.createItem(context, col)
|
||||
.withTitle("Test Item")
|
||||
.build();
|
||||
// Create a large bitstream so that the 20MB threshold is reached for large file link generation.
|
||||
byte[] bytes = new byte[21 * 1024 * 1024];
|
||||
InputStream is = new ByteArrayInputStream(bytes);
|
||||
Bitstream largeBitstream = BitstreamBuilder
|
||||
.createBitstream(context, item, is)
|
||||
.withName("large.pdf")
|
||||
.build();
|
||||
context.restoreAuthSystemState();
|
||||
// Create a request to which we can respond.
|
||||
RequestItem request = RequestItemBuilder
|
||||
.createRequestItem(context, item, largeBitstream)
|
||||
.withAcceptRequest(true)
|
||||
.build();
|
||||
|
||||
// Install a fake transport for RFC2822 email addresses.
|
||||
Session session = DSpaceServicesFactory.getInstance().getEmailService().getSession();
|
||||
Provider transportProvider = new Provider(Provider.Type.TRANSPORT,
|
||||
DUMMY_PROTO, JavaMailTestTransport.class.getCanonicalName(),
|
||||
"DSpace", "1.0");
|
||||
session.addProvider(transportProvider);
|
||||
session.setProvider(transportProvider);
|
||||
session.setProtocolForAddress("rfc822", DUMMY_PROTO);
|
||||
String responseLink = request.getAccess_token();
|
||||
requestItemEmailNotifier.sendResponse(context, request, "Subject", "Message");
|
||||
|
||||
// Check that the email contains the access link and no attachment.
|
||||
Message myMessage = JavaMailTestTransport.getMessage();
|
||||
String content = (String)myMessage.getContent();
|
||||
assertThat("Should contain access link", content, containsString(responseLink));
|
||||
assertThat("Should not contain attachment marker", content, not(containsString("Attachment")));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test of requestOpenAccess method, of class RequestItemEmailNotifier.
|
||||
* @throws java.lang.Exception passed through.
|
||||
* @throws Exception passed through.
|
||||
*/
|
||||
@Ignore
|
||||
@Test
|
||||
|
@@ -0,0 +1,387 @@
|
||||
/**
|
||||
* 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.requestitem;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.sql.SQLException;
|
||||
import java.text.ParseException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Iterator;
|
||||
|
||||
import org.dspace.AbstractUnitTest;
|
||||
import org.dspace.app.requestitem.factory.RequestItemServiceFactory;
|
||||
import org.dspace.app.requestitem.service.RequestItemService;
|
||||
import org.dspace.authorize.AuthorizeException;
|
||||
import org.dspace.builder.AbstractBuilder;
|
||||
import org.dspace.builder.BitstreamBuilder;
|
||||
import org.dspace.builder.CollectionBuilder;
|
||||
import org.dspace.builder.CommunityBuilder;
|
||||
import org.dspace.builder.ItemBuilder;
|
||||
import org.dspace.builder.RequestItemBuilder;
|
||||
import org.dspace.content.Bitstream;
|
||||
import org.dspace.content.Collection;
|
||||
import org.dspace.content.Community;
|
||||
import org.dspace.content.Item;
|
||||
import org.dspace.content.factory.ContentServiceFactory;
|
||||
import org.dspace.content.service.BitstreamService;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.handle.factory.HandleServiceFactory;
|
||||
import org.dspace.handle.service.HandleService;
|
||||
import org.dspace.services.ConfigurationService;
|
||||
import org.dspace.services.factory.DSpaceServicesFactory;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Unit testing for RequestItem and RequestItemService ("Request-a-copy" feature)
|
||||
*
|
||||
* @author Kim Shepherd
|
||||
*/
|
||||
public class RequestItemTest extends AbstractUnitTest {
|
||||
|
||||
private static RequestItemService requestItemService;
|
||||
private static ConfigurationService configurationService;
|
||||
private static HandleService handleService;
|
||||
private static BitstreamService bitstreamService;
|
||||
|
||||
private Community parentCommunity;
|
||||
private Collection collection;
|
||||
private Item item;
|
||||
private Bitstream bitstream;
|
||||
private Context context;
|
||||
|
||||
@BeforeClass
|
||||
public static void setUpClass()
|
||||
throws SQLException {
|
||||
AbstractBuilder.init(); // AbstractUnitTest doesn't do this for us.
|
||||
Context ctx = new Context();
|
||||
ctx.turnOffAuthorisationSystem();
|
||||
ctx.restoreAuthSystemState();
|
||||
ctx.complete();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void tearDownClass() throws Exception {
|
||||
// AbstractUnitTest doesn't do this for us.
|
||||
AbstractBuilder.cleanupObjects();
|
||||
AbstractBuilder.destroy();
|
||||
}
|
||||
|
||||
// @Override
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
//super.setUp();
|
||||
configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
|
||||
handleService
|
||||
= HandleServiceFactory.getInstance().getHandleService();
|
||||
requestItemService = RequestItemServiceFactory.getInstance().getRequestItemService();
|
||||
bitstreamService = ContentServiceFactory.getInstance().getBitstreamService();
|
||||
configurationService.setProperty("mail.server.disabled", "false");
|
||||
|
||||
try {
|
||||
context = new Context();
|
||||
context.turnOffAuthorisationSystem();
|
||||
context.setCurrentUser(eperson);
|
||||
// Create test resources
|
||||
parentCommunity = CommunityBuilder
|
||||
.createCommunity(context)
|
||||
.withName("Community")
|
||||
.build();
|
||||
collection = CollectionBuilder
|
||||
.createCollection(context, parentCommunity)
|
||||
.withName("Collection")
|
||||
.withAdminGroup(eperson)
|
||||
.build();
|
||||
item = ItemBuilder
|
||||
.createItem(context, collection)
|
||||
.withTitle("Item")
|
||||
.build();
|
||||
InputStream is = new ByteArrayInputStream(new byte[0]);
|
||||
bitstream = BitstreamBuilder
|
||||
.createBitstream(context, item, is)
|
||||
.withName("Bitstream")
|
||||
.build();
|
||||
} catch (SQLException | AuthorizeException | IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAccessTokenGenerationWithLargeFile() throws Exception {
|
||||
// Create large bitstream over threshold
|
||||
byte[] bytes = new byte[21 * 1024 * 1024]; // 21MB
|
||||
InputStream is = new ByteArrayInputStream(bytes);
|
||||
Bitstream largeBitstream = BitstreamBuilder
|
||||
.createBitstream(context, item, is)
|
||||
.withName("LargeBitstream")
|
||||
.build();
|
||||
|
||||
RequestItem request = RequestItemBuilder
|
||||
.createRequestItem(context, item, largeBitstream)
|
||||
.build();
|
||||
|
||||
// Since we are over the threshold, the token should not be null
|
||||
assertNotNull("Request token should not be null", request.getAccess_token());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAccessTokenGenerationWithSmallFile() throws Exception {
|
||||
// Create small file under the default threshold of 20MB
|
||||
byte[] bytes = new byte[1 * 1024 * 1024]; // 1MB
|
||||
InputStream is = new ByteArrayInputStream(bytes);
|
||||
Bitstream smallBitstream = BitstreamBuilder
|
||||
.createBitstream(context, item, is)
|
||||
.withName("SmallBitstream")
|
||||
.build();
|
||||
|
||||
RequestItem request = RequestItemBuilder
|
||||
.createRequestItem(context, item, smallBitstream)
|
||||
.build();
|
||||
|
||||
// Since we are under the threshold, the token should be null and
|
||||
// the item will be sent via email attachment
|
||||
assertNull("Request token should be null", request.getAccess_token());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthorizeWithValidPeriod() throws Exception {
|
||||
Instant decisionDate = getYesterdayAsInstant();
|
||||
RequestItem request = RequestItemBuilder
|
||||
.createRequestItem(context, item, bitstream)
|
||||
.withAcceptRequest(true)
|
||||
.withAccessToken("test-token")
|
||||
.withDecisionDate(decisionDate) // Yesterday
|
||||
.withAccessExpiry(getExpiryAsInstant("+10DAYS", decisionDate)) // 10 day period
|
||||
.build();
|
||||
|
||||
// The access token should be valid so we expect no exceptions
|
||||
try {
|
||||
requestItemService.authorizeAccessByAccessToken(context, request, bitstream, request.getAccess_token());
|
||||
assertNotNull(request.getAccess_token());
|
||||
} catch (AuthorizeException e) {
|
||||
fail("AuthorizeException should not be thrown for a valid expiry period");
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = AuthorizeException.class)
|
||||
public void testAuthorizeWithExpiredPeriod() throws Exception {
|
||||
Instant decisionDate = getYesterdayAsInstant();
|
||||
RequestItem request = RequestItemBuilder
|
||||
.createRequestItem(context, item, bitstream)
|
||||
.withAcceptRequest(true)
|
||||
.withAccessToken("test-token")
|
||||
.withDecisionDate(decisionDate) // Yesterday
|
||||
.withAccessExpiry(getExpiryAsInstant("+1DAY", decisionDate)) // 1 day period
|
||||
.build();
|
||||
|
||||
// The access token should not be valid so we expect to catch an AuthorizeException
|
||||
// when we call RequestItemService.authorizeByAccessToken
|
||||
requestItemService.authorizeAccessByAccessToken(context, request, bitstream, request.getAccess_token());
|
||||
}
|
||||
|
||||
|
||||
@Test(expected = AuthorizeException.class)
|
||||
public void testAuthorizeWithNullToken() throws Exception {
|
||||
requestItemService.authorizeAccessByAccessToken(context, bitstream, null);
|
||||
}
|
||||
|
||||
@Test(expected = AuthorizeException.class)
|
||||
public void testAuthorizeWithInvalidToken() throws Exception {
|
||||
requestItemService.authorizeAccessByAccessToken(context, bitstream, "invalid-token-123");
|
||||
}
|
||||
|
||||
@Test(expected = AuthorizeException.class)
|
||||
public void testAuthorizeWithMismatchedToken() throws Exception {
|
||||
Instant decisionDate = getYesterdayAsInstant();
|
||||
RequestItem request = RequestItemBuilder
|
||||
.createRequestItem(context, item, bitstream)
|
||||
.withAcceptRequest(true)
|
||||
.withAccessToken("test-token")
|
||||
.withDecisionDate(decisionDate) // Yesterday
|
||||
.withAccessExpiry(getExpiryAsInstant("FOREVER", decisionDate)) // forever
|
||||
.build();
|
||||
|
||||
// The access token should NOT valid so we expect to catch an AuthorizeException
|
||||
// when we call RequestItemService.authorizeByAccessToken
|
||||
requestItemService.authorizeAccessByAccessToken(context, request, bitstream, "invalid-token-123");
|
||||
}
|
||||
@Test(expected = AuthorizeException.class)
|
||||
public void testAuthorizeWithMismatchedBitstream() throws Exception {
|
||||
// Create request for one bitstream
|
||||
RequestItem request = RequestItemBuilder
|
||||
.createRequestItem(context, item, bitstream)
|
||||
.withAcceptRequest(true)
|
||||
.withAccessToken("test-token")
|
||||
.build();
|
||||
|
||||
// Create different bitstream
|
||||
InputStream is = new ByteArrayInputStream(new byte[0]);
|
||||
Bitstream otherBitstream = BitstreamBuilder
|
||||
.createBitstream(context, item, is)
|
||||
.withName("OtherBitstream")
|
||||
.build();
|
||||
|
||||
// Try to authorize access to different bitstream
|
||||
requestItemService.authorizeAccessByAccessToken(context, request, otherBitstream, request.getAccess_token());
|
||||
}
|
||||
|
||||
@Test(expected = AuthorizeException.class)
|
||||
public void testAuthorizeWithAllFilesDisabled() throws Exception {
|
||||
// Create request for specific bitstream
|
||||
RequestItem request = RequestItemBuilder
|
||||
.createRequestItem(context, item, bitstream)
|
||||
.withAcceptRequest(true)
|
||||
.withAccessToken("test-token")
|
||||
.withAllFiles(false)
|
||||
.build();
|
||||
|
||||
// Create different bitstream
|
||||
InputStream is = new ByteArrayInputStream(new byte[0]);
|
||||
Bitstream otherBitstream = BitstreamBuilder
|
||||
.createBitstream(context, item, is)
|
||||
.withName("OtherBitstream")
|
||||
.build();
|
||||
|
||||
// Try to access different bitstream when allfiles=false
|
||||
requestItemService.authorizeAccessByAccessToken(context, request, otherBitstream, request.getAccess_token());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGrantRequestWithAccessPeriod() throws Exception {
|
||||
Instant decisionDate = Instant.now();
|
||||
Instant expectedExpiryDate = decisionDate.plus(7, ChronoUnit.DAYS);
|
||||
RequestItem request = RequestItemBuilder
|
||||
.createRequestItem(context, item, bitstream)
|
||||
.build();
|
||||
|
||||
request.setAccept_request(true);
|
||||
request.setDecision_date(decisionDate);
|
||||
request.setAccess_expiry(getExpiryAsInstant("+7DAYS", decisionDate)); // 7 day access
|
||||
requestItemService.update(context, request);
|
||||
|
||||
RequestItem found = requestItemService.findByToken(context, request.getToken());
|
||||
assertTrue(found.isAccept_request());
|
||||
assertEquals(decisionDate, found.getDecision_date());
|
||||
assertEquals(expectedExpiryDate, found.getAccess_expiry());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDenyRequest() throws Exception {
|
||||
Instant decisionDate = Instant.now();
|
||||
RequestItem request = RequestItemBuilder
|
||||
.createRequestItem(context, item, bitstream)
|
||||
.build();
|
||||
|
||||
request.setAccept_request(false);
|
||||
request.setDecision_date(decisionDate);
|
||||
requestItemService.update(context, request);
|
||||
|
||||
RequestItem found = requestItemService.findByToken(context, request.getToken());
|
||||
assertFalse(found.isAccept_request());
|
||||
assertNotNull(found.getDecision_date());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindRequestsByItem() throws Exception {
|
||||
// Create multiple requests
|
||||
RequestItem request1 = RequestItemBuilder
|
||||
.createRequestItem(context, item, bitstream)
|
||||
.build();
|
||||
|
||||
RequestItem request2 = RequestItemBuilder
|
||||
.createRequestItem(context, item, bitstream)
|
||||
.build();
|
||||
|
||||
Iterator<RequestItem> requests = requestItemService.findByItem(context, item);
|
||||
int count = 0;
|
||||
while (requests.hasNext()) {
|
||||
count++;
|
||||
requests.next();
|
||||
}
|
||||
assertEquals(2, count);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testModifyGrantedRequest() throws Exception {
|
||||
Instant decisionDate = Instant.now();
|
||||
Instant expectedExpiryDate = decisionDate.plus(10, ChronoUnit.DAYS);
|
||||
RequestItem request = RequestItemBuilder
|
||||
.createRequestItem(context, item, bitstream)
|
||||
.withAcceptRequest(true)
|
||||
.withDecisionDate(decisionDate)
|
||||
.withAccessExpiry(getExpiryAsInstant("+1DAY", decisionDate))
|
||||
.build();
|
||||
|
||||
// Manually set new expiry date
|
||||
request.setAccess_expiry(getExpiryAsInstant("+10DAYS", decisionDate));
|
||||
request.setAllfiles(true);
|
||||
requestItemService.update(context, request);
|
||||
|
||||
RequestItem found = requestItemService.findByToken(context, request.getToken());
|
||||
assertEquals(expectedExpiryDate, found.getAccess_expiry());
|
||||
assertTrue(found.isAccept_request());
|
||||
assertTrue(found.isAllfiles());
|
||||
}
|
||||
/**
|
||||
* Test that generated links include the correct base URL, where the UI URL has a subpath like /subdir
|
||||
*/
|
||||
@Test
|
||||
public void testGetLinkTokenEmailWithSubPath() throws MalformedURLException, URISyntaxException {
|
||||
String currentDspaceUrl = configurationService.getProperty("dspace.ui.url");
|
||||
String newDspaceUrl = currentDspaceUrl + "/subdir";
|
||||
// Add a /subdir to the url for this test
|
||||
configurationService.setProperty("dspace.ui.url", newDspaceUrl);
|
||||
String expectedUrl = newDspaceUrl + "/request-a-copy/token";
|
||||
String generatedLink = requestItemService.getLinkTokenEmail("token");
|
||||
// The URLs should match
|
||||
assertEquals(expectedUrl, generatedLink);
|
||||
configurationService.reloadConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that generated links include the correct base URL, with NO subpath elements
|
||||
*/
|
||||
@Test
|
||||
public void testGetLinkTokenEmailWithoutSubPath() throws MalformedURLException, URISyntaxException {
|
||||
String currentDspaceUrl = configurationService.getProperty("dspace.ui.url");
|
||||
String expectedUrl = currentDspaceUrl + "/request-a-copy/token";
|
||||
String generatedLink = requestItemService.getLinkTokenEmail("token");
|
||||
// The URLs should match
|
||||
assertEquals(expectedUrl, generatedLink);
|
||||
configurationService.reloadConfig();
|
||||
}
|
||||
|
||||
private Instant getYesterdayAsInstant() {
|
||||
return Instant.now().minus(Duration.ofDays(1));
|
||||
}
|
||||
|
||||
private Instant getExpiryAsInstant(String dateOrDelta, Instant decision) {
|
||||
try {
|
||||
return RequestItemServiceImpl.parseDateOrDelta(dateOrDelta, decision);
|
||||
} catch (ParseException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -39,6 +39,9 @@ public class RequestItemBuilder
|
||||
private Bitstream bitstream;
|
||||
private Instant decisionDate;
|
||||
private boolean accepted;
|
||||
private String accessToken = null;
|
||||
private Instant accessExpiry = null;
|
||||
private boolean allFiles;
|
||||
|
||||
protected RequestItemBuilder(Context context) {
|
||||
super(context);
|
||||
@@ -87,13 +90,29 @@ public class RequestItemBuilder
|
||||
return this;
|
||||
}
|
||||
|
||||
public RequestItemBuilder withAccessToken(String accessToken) {
|
||||
this.accessToken = accessToken;
|
||||
return this;
|
||||
}
|
||||
|
||||
public RequestItemBuilder withAccessExpiry(Instant accessExpiry) {
|
||||
this.accessExpiry = accessExpiry;
|
||||
return this;
|
||||
}
|
||||
|
||||
public RequestItemBuilder withAllFiles(boolean allFiles) {
|
||||
this.allFiles = allFiles;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestItem build() {
|
||||
LOG.atDebug()
|
||||
.withLocation()
|
||||
.log("Building request with item ID {} and bitstream ID {}",
|
||||
.log("Building request with item ID {} and bitstream ID {} and allfiles {}",
|
||||
() -> item.getID().toString(),
|
||||
() -> bitstream.getID().toString());
|
||||
() -> (bitstream == null ? "" : bitstream.getID().toString()),
|
||||
() -> Boolean.toString(allFiles));
|
||||
|
||||
String token;
|
||||
try {
|
||||
@@ -106,6 +125,11 @@ public class RequestItemBuilder
|
||||
requestItem = requestItemService.findByToken(context, token);
|
||||
requestItem.setAccept_request(accepted);
|
||||
requestItem.setDecision_date(decisionDate);
|
||||
if (accessToken != null) {
|
||||
requestItem.setAccess_token(accessToken);
|
||||
}
|
||||
requestItem.setAccess_expiry(accessExpiry);
|
||||
requestItem.setAllfiles(allFiles);
|
||||
|
||||
requestItemService.update(context, requestItem);
|
||||
|
||||
|
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 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.junit.Assert.fail;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import org.dspace.AbstractUnitTest;
|
||||
import org.dspace.eperson.factory.CaptchaServiceFactory;
|
||||
import org.dspace.eperson.service.CaptchaService;
|
||||
import org.dspace.services.ConfigurationService;
|
||||
import org.dspace.services.factory.DSpaceServicesFactory;
|
||||
import org.json.JSONObject;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Basic tests to verity the Altcha captcha service can verify payloads from the client
|
||||
*
|
||||
* @author Kim Shepherd
|
||||
*/
|
||||
public class AltchaCaptchaServiceTest extends AbstractUnitTest {
|
||||
|
||||
CaptchaService captchaService;
|
||||
ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
configurationService.setProperty("captcha.provider", "google");
|
||||
}
|
||||
@Before
|
||||
public void setUp() {
|
||||
configurationService.setProperty("captcha.provider", "altcha");
|
||||
configurationService.setProperty("altcha.hmac.key", "onetwothreesecret");
|
||||
captchaService = CaptchaServiceFactory.getInstance().getCaptchaService();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidJSONCaptchaPayloadValidation() {
|
||||
// Create raw JSON first, using previous known-good payload with our test hmac secret of "onetwothreesecret"
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("algorithm", "SHA-256");
|
||||
json.put("salt", "dcf5eba26e");
|
||||
json.put("number", 4791);
|
||||
json.put("challenge", "0d8dd34089fdd610bd9a8857ea1fa4a5f9fe4b53f5df0c4e1eff6dc987c4d2bf");
|
||||
json.put("signature", "dfe4ec56f3d61e3a021b1c3b3ea4c7d6aea9812ab719ffe130fd386ce0b4158c");
|
||||
// Base64 encode it
|
||||
String payload = Base64.getEncoder().encodeToString(json.toString().getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// Now validate
|
||||
captchaService.processResponse(payload, "validate");
|
||||
}
|
||||
|
||||
@Test(expected = InvalidReCaptchaException.class)
|
||||
public void testInvalidCaptchaPayloadValidation() {
|
||||
// Create raw JSON first
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("algorithm", "SHA-256");
|
||||
json.put("challenge", "abcdefg");
|
||||
json.put("salt", "salt123");
|
||||
json.put("number", 1);
|
||||
json.put("signature", "123123123123");
|
||||
String payload = Base64.getEncoder().encodeToString(json.toString().getBytes(StandardCharsets.UTF_8));
|
||||
// Ask the captcha service to validate the payload
|
||||
captchaService.processResponse(payload, "validate");
|
||||
// If we got here, something is off - an exception should have been thrown
|
||||
fail("Invalid captcha payload should have failed with IllegalReCaptchaException");
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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
|
||||
|
@@ -8,13 +8,16 @@
|
||||
package org.dspace.xoai.app.plugins;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
import com.lyncode.xoai.dataprovider.xml.xoai.Element;
|
||||
import com.lyncode.xoai.dataprovider.xml.xoai.Metadata;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.dspace.access.status.DefaultAccessStatusHelper;
|
||||
import org.dspace.access.status.factory.AccessStatusServiceFactory;
|
||||
import org.dspace.access.status.service.AccessStatusService;
|
||||
import org.dspace.content.AccessStatus;
|
||||
import org.dspace.content.Item;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.xoai.app.XOAIExtensionItemCompilePlugin;
|
||||
@@ -51,10 +54,13 @@ public class AccessStatusElementItemCompilePlugin implements XOAIExtensionItemCo
|
||||
AccessStatusService accessStatusService = AccessStatusServiceFactory.getInstance().getAccessStatusService();
|
||||
|
||||
try {
|
||||
String accessStatusType;
|
||||
accessStatusType = accessStatusService.getAccessStatus(context, item);
|
||||
|
||||
String embargoFromItem = accessStatusService.getEmbargoFromItem(context, item);
|
||||
AccessStatus accessStatusResult = accessStatusService.getAnonymousAccessStatus(context, item);
|
||||
String accessStatusType = accessStatusResult.getStatus();
|
||||
LocalDate availabilityDate = accessStatusResult.getAvailabilityDate();
|
||||
String embargoFromItem = null;
|
||||
if (accessStatusType == DefaultAccessStatusHelper.EMBARGO && availabilityDate != null) {
|
||||
embargoFromItem = availabilityDate.toString();
|
||||
}
|
||||
|
||||
Element accessStatus = ItemUtils.create("access-status");
|
||||
accessStatus.getField().add(ItemUtils.createValue("value", accessStatusType));
|
||||
|
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* 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.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.random.RandomGenerator;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.commons.codec.digest.HmacUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.tomcat.util.http.FastHttpDateFormat;
|
||||
import org.dspace.services.ConfigurationService;
|
||||
import org.json.JSONObject;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.rest.webmvc.ControllerUtils;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* ALTCHA (Proof of Work, cookie-less) controller to handle challenge requests.
|
||||
* A salt, challenge hash will be sent and the client will have to calculate the number and send it back
|
||||
* to implementing controllers (e.g. request-a-copy) for validation.
|
||||
* The proof-of-work makes spam uneconomic, without requiring annoying puzzle tests or 3rd party services.
|
||||
* @see org.dspace.app.rest.repository.RequestItemRepository
|
||||
* @see <a href="https://altcha.org/docs/server-integration">Altcha docs></a>
|
||||
*
|
||||
* @author Kim Shepherd
|
||||
*/
|
||||
@RequestMapping(value = "/api/captcha")
|
||||
@RestController
|
||||
public class AltchaCaptchaRestController implements InitializingBean {
|
||||
@Autowired
|
||||
ConfigurationService configurationService;
|
||||
@Autowired
|
||||
DiscoverableEndpointsService discoverableEndpointsService;
|
||||
|
||||
// Logger
|
||||
private static Logger log = LogManager.getLogger();
|
||||
|
||||
// Default response expiry
|
||||
private static final long DEFAULT_EXPIRE_TIME = 60L * 60L * 1000L;
|
||||
|
||||
/**
|
||||
* Calculate a challenge for ALTCHA captcha
|
||||
*
|
||||
* See https://altcha.org/docs/server-integration for implementation details and examples
|
||||
* @param request HTTP request
|
||||
* @param response HTTP response
|
||||
* @return response entity with JSON challenge for client to begin proof-of-work
|
||||
*/
|
||||
@GetMapping("/challenge")
|
||||
@PreAuthorize("permitAll()")
|
||||
public ResponseEntity getAltchaChallenge(HttpServletRequest request, HttpServletResponse response) {
|
||||
// Check if captcha provider is set to altcha
|
||||
if (!configurationService.getProperty("captcha.provider", "google").equals("altcha")) {
|
||||
log.error("altcha is not enabled");
|
||||
return ControllerUtils.toEmptyResponse(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
// Set algorithm and hmac key
|
||||
// Algorithm
|
||||
String algorithm = configurationService.getProperty("altcha.algorithm", "SHA-256");
|
||||
String hmacKey = configurationService.getProperty("altcha.hmac.key");
|
||||
if (hmacKey == null) {
|
||||
log.error("hmac key not found, see: altcha.hmac.key in altcha.cfg");
|
||||
return ControllerUtils.toEmptyResponse(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Instantiate random generator
|
||||
RandomGenerator generator = RandomGenerator.getDefault();
|
||||
|
||||
// Generate a random salt
|
||||
String randomSalt = bytesToHex(generateSalt());
|
||||
|
||||
// Generate a random integer and write as string, 0 - 100000
|
||||
// This number is kept fairly low to keep proof of work at a decent trade-off for the client
|
||||
// We want to dissuade spammers, while keeping form functionality smooth for the user
|
||||
int randomNumber = generator.nextInt(100000);
|
||||
String randomNumberString = String.valueOf(randomNumber);
|
||||
|
||||
// Generate the challenge as a hex string sha256 hash of concatenated salt and random string
|
||||
try {
|
||||
// Challenge = sha256 of salt + secret
|
||||
String challenge = calculateHash(randomSalt + randomNumberString, algorithm);
|
||||
if (StringUtils.isBlank(challenge)) {
|
||||
log.error("Error generating altcha challenge");
|
||||
// Default, return no content
|
||||
return ControllerUtils.toEmptyResponse(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
// HMAC signature, using configured HMAC key and the generated challenge string
|
||||
String hmac = new HmacUtils("HmacSHA256", hmacKey).hmacHex(challenge);
|
||||
if (StringUtils.isBlank(hmac)) {
|
||||
log.error("Error generating HMAC signature");
|
||||
// Default, return no content
|
||||
return ControllerUtils.toEmptyResponse(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
// Set response body and headers
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put("algorithm", algorithm);
|
||||
jsonObject.put("challenge", challenge);
|
||||
jsonObject.put("salt", randomSalt);
|
||||
jsonObject.put("signature", hmac);
|
||||
String body = jsonObject.toString();
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
httpHeaders.put("Cache-Control", Collections.singletonList("private,no-cache"));
|
||||
httpHeaders.put("Expires", Collections.singletonList(FastHttpDateFormat.formatDate(
|
||||
System.currentTimeMillis() + DEFAULT_EXPIRE_TIME)));
|
||||
httpHeaders.put("Content-Type", Collections.singletonList("application/json"));
|
||||
httpHeaders.put("Content-Length", Collections.singletonList(
|
||||
String.valueOf(body.getBytes(StandardCharsets.UTF_8).length)));
|
||||
return ResponseEntity.ok().headers(httpHeaders).body(body);
|
||||
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
log.error("No such algorithm: {}, {}", algorithm, e.getMessage());
|
||||
return ControllerUtils.toEmptyResponse(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a salt like 0c9c5ef19f
|
||||
* Kept fairly simple as all we want is basic proof-of-work from the client
|
||||
* @return salt string
|
||||
*/
|
||||
private static byte[] generateSalt() {
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] salt = new byte[5];
|
||||
random.nextBytes(salt);
|
||||
return salt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode bytes to hex string
|
||||
* @param bytes bytes to encode
|
||||
* @return hex string
|
||||
*/
|
||||
public static String bytesToHex(byte[] bytes) {
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
stringBuilder.append(String.format("%02x", b));
|
||||
}
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate a hex string from a digest, given an input string
|
||||
* @param input input string
|
||||
* @param algorithm algorithm key, eg. SHA-256
|
||||
* @return
|
||||
* @throws NoSuchAlgorithmException
|
||||
*/
|
||||
public static String calculateHash(String input, String algorithm) throws NoSuchAlgorithmException {
|
||||
MessageDigest sha256 = MessageDigest.getInstance(algorithm);
|
||||
byte[] hashBytes = sha256.digest(input.getBytes());
|
||||
return bytesToHex(hashBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
discoverableEndpointsService
|
||||
.register(this, Arrays
|
||||
.asList(Link.of("/api/captcha", "captcha")));
|
||||
}
|
||||
}
|
@@ -23,6 +23,8 @@ import org.apache.catalina.connector.ClientAbortException;
|
||||
import org.apache.commons.collections4.ListUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.dspace.app.requestitem.RequestItem;
|
||||
import org.dspace.app.requestitem.service.RequestItemService;
|
||||
import org.dspace.app.rest.converter.ConverterService;
|
||||
import org.dspace.app.rest.exception.DSpaceBadRequestException;
|
||||
import org.dspace.app.rest.model.BitstreamRest;
|
||||
@@ -93,33 +95,71 @@ public class BitstreamRestController {
|
||||
@Autowired
|
||||
private ConfigurationService configurationService;
|
||||
|
||||
@Autowired
|
||||
private RequestItemService requestItemService;
|
||||
|
||||
@Autowired
|
||||
ConverterService converter;
|
||||
|
||||
@Autowired
|
||||
Utils utils;
|
||||
|
||||
@PreAuthorize("hasPermission(#uuid, 'BITSTREAM', 'READ')")
|
||||
/**
|
||||
* Retrieve bitstream. An access token (created by request a copy for some files, if enabled) can optionally
|
||||
* be used for authorization instead of current user/group
|
||||
*
|
||||
* @param uuid bitstream ID
|
||||
* @param accessToken request-a-copy access token (optional)
|
||||
* @param response HTTP response
|
||||
* @param request HTTP request
|
||||
* @return response entity with bitstream content
|
||||
* @throws IOException
|
||||
* @throws SQLException
|
||||
* @throws AuthorizeException
|
||||
*/
|
||||
@PreAuthorize("#accessToken != null|| hasPermission(#uuid, 'BITSTREAM', 'READ')")
|
||||
@RequestMapping( method = {RequestMethod.GET, RequestMethod.HEAD}, value = "content")
|
||||
public ResponseEntity retrieve(@PathVariable UUID uuid, HttpServletResponse response,
|
||||
HttpServletRequest request) throws IOException, SQLException, AuthorizeException {
|
||||
|
||||
public ResponseEntity retrieve(@PathVariable UUID uuid,
|
||||
@Parameter(value = "accessToken", required = false) String accessToken,
|
||||
HttpServletResponse response,
|
||||
HttpServletRequest request) throws IOException, SQLException, AuthorizeException {
|
||||
|
||||
// Obtain context
|
||||
Context context = ContextUtil.obtainContext(request);
|
||||
|
||||
// Find bitstream
|
||||
Bitstream bit = bitstreamService.find(context, uuid);
|
||||
EPerson currentUser = context.getCurrentUser();
|
||||
|
||||
if (bit == null) {
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
return null;
|
||||
}
|
||||
// Get EPerson
|
||||
EPerson currentUser = context.getCurrentUser();
|
||||
|
||||
// Get bitstream metadata
|
||||
Long lastModified = bitstreamService.getLastModified(bit);
|
||||
BitstreamFormat format = bit.getFormat(context);
|
||||
String mimetype = format.getMIMEType();
|
||||
String name = getBitstreamName(bit, format);
|
||||
|
||||
// If an access token is found, immediately authenticate it if request a copy is enabled
|
||||
// Though, if we do further "has to be loggd in requester" checks we'll have to check here anyway
|
||||
// Even if eperson is not null and has access, we will treat this token as the primary means of
|
||||
// authorizing bitstream download access
|
||||
boolean authorizedByAccessToken = false;
|
||||
// There may be a way of checking enabled in preauth
|
||||
if (StringUtils.isNotBlank(accessToken) && requestACopyEnabled()) {
|
||||
RequestItem requestItem = requestItemService.findByAccessToken(context, accessToken);
|
||||
// Try authorize by token. An AuthorizeException will be thrown if the token is invalid, expired,
|
||||
// for the wrong bitstream, or does not match (see RequestItemService)
|
||||
requestItemService.authorizeAccessByAccessToken(context, requestItem, bit, accessToken);
|
||||
authorizedByAccessToken = true;
|
||||
log.debug("Authorize access by token={} bitstream={}", accessToken, bit.getID());
|
||||
}
|
||||
// If an authorization error was encountered it will be rethrown by this method even if the eperson
|
||||
// could technically READ the bitstream normally. This is for consistency and clarify of usage - if we
|
||||
// want a fallback we will need to reauthenticate as otherwise any eperson could have supplied a non-blank
|
||||
// access token here
|
||||
|
||||
if (StringUtils.isBlank(request.getHeader("Range"))) {
|
||||
//We only log a download request when serving a request without Range header. This is because
|
||||
//a browser always sends a regular request first to check for Range support.
|
||||
@@ -131,42 +171,59 @@ public class BitstreamRestController {
|
||||
bit));
|
||||
}
|
||||
|
||||
// Begin actual bitstream delivery
|
||||
try {
|
||||
// Check if a citation coverpage is valid for this download
|
||||
long filesize = bit.getSizeBytes();
|
||||
Boolean citationEnabledForBitstream = citationDocumentService.isCitationEnabledForBitstream(bit, context);
|
||||
|
||||
var bitstreamResource =
|
||||
new org.dspace.app.rest.utils.BitstreamResource(name, uuid,
|
||||
currentUser != null ? currentUser.getID() : null,
|
||||
context.getSpecialGroupUuids(), citationEnabledForBitstream);
|
||||
|
||||
|
||||
// Generate a special bitstream resource stream depending on whether we are accessing by token
|
||||
// or eperson / group access
|
||||
org.dspace.app.rest.utils.BitstreamResource bitstreamResource;
|
||||
if (authorizedByAccessToken) {
|
||||
// Get input stream using temporary privileged context
|
||||
bitstreamResource = new org.dspace.app.rest.utils.BitstreamResourceAccessByToken(name, uuid,
|
||||
currentUser != null ? currentUser.getID() : null,
|
||||
context.getSpecialGroupUuids(), citationEnabledForBitstream, accessToken);
|
||||
} else {
|
||||
// Get input stream using default user/group authorization
|
||||
bitstreamResource =
|
||||
new org.dspace.app.rest.utils.BitstreamResource(name, uuid,
|
||||
currentUser != null ? currentUser.getID() : null,
|
||||
context.getSpecialGroupUuids(), citationEnabledForBitstream);
|
||||
}
|
||||
|
||||
// We have all the data we need, close the connection to the database so that it doesn't stay open during
|
||||
// download/streaming
|
||||
context.complete();
|
||||
|
||||
// Set http headers
|
||||
HttpHeadersInitializer httpHeadersInitializer = new HttpHeadersInitializer()
|
||||
.withBufferSize(BUFFER_SIZE)
|
||||
.withFileName(name)
|
||||
.withChecksum(bitstreamResource.getChecksum())
|
||||
.withLength(bitstreamResource.contentLength())
|
||||
.withMimetype(mimetype)
|
||||
.with(request)
|
||||
.with(response);
|
||||
.withBufferSize(BUFFER_SIZE)
|
||||
.withFileName(name)
|
||||
.withChecksum(bitstreamResource.getChecksum())
|
||||
.withLength(bitstreamResource.contentLength())
|
||||
.withMimetype(mimetype)
|
||||
.with(request)
|
||||
.with(response);
|
||||
|
||||
// Set last modified in headers
|
||||
if (lastModified != null) {
|
||||
httpHeadersInitializer.withLastModified(lastModified);
|
||||
}
|
||||
|
||||
//Determine if we need to send the file as a download or if the browser can open it inline
|
||||
//The file will be downloaded if its size is larger than the configured threshold,
|
||||
//or if its mimetype/extension appears in the "webui.content_disposition_format" config
|
||||
// Determine if we need to send the file as a download or if the browser can open it inline
|
||||
// The file will be downloaded if its size is larger than the configured threshold,
|
||||
// or if its mimetype/extension appears in the "webui.content_disposition_format" config
|
||||
long dispositionThreshold = configurationService.getLongProperty("webui.content_disposition_threshold");
|
||||
if ((dispositionThreshold >= 0 && filesize > dispositionThreshold)
|
||||
|| checkFormatForContentDisposition(format)) {
|
||||
httpHeadersInitializer.withDisposition(HttpHeadersInitializer.CONTENT_DISPOSITION_ATTACHMENT);
|
||||
}
|
||||
|
||||
//We have all the data we need, close the connection to the database so that it doesn't stay open during
|
||||
//download/streaming
|
||||
context.complete();
|
||||
|
||||
//Send the data
|
||||
// Send the data
|
||||
if (httpHeadersInitializer.isValid()) {
|
||||
HttpHeaders httpHeaders = httpHeadersInitializer.initialiseHeaders();
|
||||
|
||||
@@ -187,6 +244,12 @@ public class BitstreamRestController {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name for attachment disposition headers
|
||||
* @param bit bitstream
|
||||
* @param format bitstream format
|
||||
* @return name
|
||||
*/
|
||||
private String getBitstreamName(Bitstream bit, BitstreamFormat format) {
|
||||
String name = bit.getName();
|
||||
if (name == null) {
|
||||
@@ -199,6 +262,11 @@ public class BitstreamRestController {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for a success or other non-error response message
|
||||
* @param response HTTP resposnse
|
||||
* @return success or redirection code indication as boolean
|
||||
*/
|
||||
private boolean isNotAnErrorResponse(HttpServletResponse response) {
|
||||
Response.Status.Family responseCode = Response.Status.Family.familyOf(response.getStatus());
|
||||
return responseCode.equals(Response.Status.Family.SUCCESSFUL)
|
||||
@@ -250,6 +318,18 @@ public class BitstreamRestController {
|
||||
return download;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick check to see if request a copy is enabled. If not, for safety, we'll deny any downoads
|
||||
* @return true or false
|
||||
*/
|
||||
private boolean requestACopyEnabled() {
|
||||
// If the feature is not enabled, throw exception
|
||||
if (configurationService.getProperty("request.item.type") != null) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will update the bitstream format of the bitstream that corresponds to the provided bitstream uuid.
|
||||
*
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -183,7 +183,7 @@ public class WebApplication {
|
||||
// Allow list of request preflight headers allowed to be sent to us from the client
|
||||
.allowedHeaders("Accept", "Authorization", "Content-Type", "Origin", "X-On-Behalf-Of",
|
||||
"X-Requested-With", "X-XSRF-TOKEN", "X-CORRELATION-ID", "X-REFERRER",
|
||||
"x-recaptcha-token")
|
||||
"x-captcha-payload")
|
||||
// Allow list of response headers allowed to be sent by us (the server) to the client
|
||||
.exposedHeaders("Authorization", "DSPACE-XSRF-TOKEN", "Location", "WWW-Authenticate");
|
||||
}
|
||||
@@ -195,7 +195,7 @@ public class WebApplication {
|
||||
// Allow list of request preflight headers allowed to be sent to us from the client
|
||||
.allowedHeaders("Accept", "Authorization", "Content-Type", "Origin", "X-On-Behalf-Of",
|
||||
"X-Requested-With", "X-XSRF-TOKEN", "X-CORRELATION-ID", "X-REFERRER",
|
||||
"x-recaptcha-token")
|
||||
"x-captcha-payload")
|
||||
// Allow list of response headers allowed to be sent by us (the server) to the client
|
||||
.exposedHeaders("Authorization", "DSPACE-XSRF-TOKEN", "Location", "WWW-Authenticate");
|
||||
}
|
||||
@@ -207,7 +207,7 @@ public class WebApplication {
|
||||
// Allow list of request preflight headers allowed to be sent to us from the client
|
||||
.allowedHeaders("Accept", "Authorization", "Content-Type", "Origin", "X-On-Behalf-Of",
|
||||
"X-Requested-With", "X-XSRF-TOKEN", "X-CORRELATION-ID", "X-REFERRER",
|
||||
"x-recaptcha-token", "access-control-allow-headers")
|
||||
"x-captcha-payload", "access-control-allow-headers")
|
||||
// Allow list of response headers allowed to be sent by us (the server) to the client
|
||||
.exposedHeaders("Authorization", "DSPACE-XSRF-TOKEN", "Location", "WWW-Authenticate");
|
||||
}
|
||||
|
@@ -62,6 +62,14 @@ public class RequestCopyFeature implements AuthorizationFeature {
|
||||
@Autowired
|
||||
private ConfigurationService configurationService;
|
||||
|
||||
/**
|
||||
* Check if the user is authorized to request a copy of a bitstream belonging to a given item
|
||||
* @param context
|
||||
* the DSpace Context
|
||||
* @param object the item for which we are requesting a copy
|
||||
* @return
|
||||
* @throws SQLException
|
||||
*/
|
||||
@Override
|
||||
public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException {
|
||||
String requestType = configurationService.getProperty("request.item.type");
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -8,6 +8,8 @@
|
||||
|
||||
package org.dspace.app.rest.converter;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import jakarta.inject.Named;
|
||||
import org.dspace.app.requestitem.RequestItem;
|
||||
import org.dspace.app.rest.model.RequestItemRest;
|
||||
@@ -15,8 +17,8 @@ import org.dspace.app.rest.projection.Projection;
|
||||
import org.dspace.content.Bitstream;
|
||||
|
||||
/**
|
||||
* Convert between {@link org.dspace.app.requestitem.RequestItem} and
|
||||
* {@link org.dspace.app.rest.model.RequestItemRest}.
|
||||
* Convert between {@link RequestItem} and
|
||||
* {@link RequestItemRest}.
|
||||
*
|
||||
* @author Mark H. Wood <mwood@iupui.edu>
|
||||
*/
|
||||
@@ -45,6 +47,14 @@ public class RequestItemConverter
|
||||
requestItemRest.setRequestName(requestItem.getReqName());
|
||||
requestItemRest.setRequestDate(requestItem.getRequest_date());
|
||||
requestItemRest.setToken(requestItem.getToken());
|
||||
requestItemRest.setAccessToken(requestItem.getAccess_token());
|
||||
requestItemRest.setAccessExpiry(requestItem.getAccess_expiry());
|
||||
if ( requestItem.getAccess_expiry() == null ||
|
||||
requestItem.getAccess_expiry().isBefore(Instant.now())) {
|
||||
requestItemRest.setAccessExpired(true);
|
||||
} else {
|
||||
requestItemRest.setAccessExpired(false);
|
||||
}
|
||||
return requestItemRest;
|
||||
}
|
||||
|
||||
|
@@ -18,6 +18,7 @@ public class AccessStatusRest implements RestModel {
|
||||
public static final String PLURAL_NAME = NAME;
|
||||
|
||||
String status;
|
||||
String embargoDate;
|
||||
|
||||
@Override
|
||||
@JsonProperty(access = Access.READ_ONLY)
|
||||
@@ -35,10 +36,12 @@ public class AccessStatusRest implements RestModel {
|
||||
|
||||
public AccessStatusRest() {
|
||||
setStatus(null);
|
||||
setEmbargoDate(null);
|
||||
}
|
||||
|
||||
public AccessStatusRest(String status) {
|
||||
setStatus(status);
|
||||
setEmbargoDate(null);
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
@@ -48,4 +51,12 @@ public class AccessStatusRest implements RestModel {
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getEmbargoDate() {
|
||||
return embargoDate;
|
||||
}
|
||||
|
||||
public void setEmbargoDate(String embargoDate) {
|
||||
this.embargoDate = embargoDate;
|
||||
}
|
||||
}
|
||||
|
@@ -17,6 +17,7 @@ import com.fasterxml.jackson.annotation.JsonProperty.Access;
|
||||
*/
|
||||
@LinksRest(links = {
|
||||
@LinkRest(name = BitstreamRest.BUNDLE, method = "getBundle"),
|
||||
@LinkRest(name = BitstreamRest.ACCESS_STATUS, method = "getAccessStatus"),
|
||||
@LinkRest(name = BitstreamRest.FORMAT, method = "getFormat"),
|
||||
@LinkRest(name = BitstreamRest.THUMBNAIL, method = "getThumbnail")
|
||||
})
|
||||
@@ -26,6 +27,7 @@ public class BitstreamRest extends DSpaceObjectRest {
|
||||
public static final String CATEGORY = RestAddressableModel.CORE;
|
||||
|
||||
public static final String BUNDLE = "bundle";
|
||||
public static final String ACCESS_STATUS = "accessStatus";
|
||||
public static final String FORMAT = "format";
|
||||
public static final String THUMBNAIL = "thumbnail";
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
@@ -53,6 +53,14 @@ public class RequestItemRest extends BaseObjectRest<Integer> {
|
||||
|
||||
protected boolean allfiles;
|
||||
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
protected String accessToken;
|
||||
|
||||
protected Instant accessExpiry;
|
||||
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
protected boolean accessExpired;
|
||||
|
||||
/**
|
||||
* @return the bitstream requested.
|
||||
*/
|
||||
@@ -207,6 +215,41 @@ public class RequestItemRest extends BaseObjectRest<Integer> {
|
||||
this.allfiles = allfiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the unique access token to be used by the requester. This is separate to the approval token ('token')
|
||||
*/
|
||||
public String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param accessToken the access token to be used by the requester (not settable through JSON update)
|
||||
*/
|
||||
public void setAccessToken(String accessToken) {
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the date the access token expires.
|
||||
*/
|
||||
public Instant getAccessExpiry() {
|
||||
return this.accessExpiry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param accessExpiry the date the access token expires.
|
||||
*/
|
||||
public void setAccessExpiry(Instant accessExpiry) {
|
||||
this.accessExpiry = accessExpiry;
|
||||
}
|
||||
|
||||
public boolean isAccessExpired() {
|
||||
return accessExpired;
|
||||
}
|
||||
|
||||
public void setAccessExpired(boolean accessExpired) {
|
||||
this.accessExpired = accessExpired;
|
||||
}
|
||||
/*
|
||||
* Common REST object methods.
|
||||
*/
|
||||
|
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.dspace.access.status.DefaultAccessStatusHelper;
|
||||
import org.dspace.access.status.service.AccessStatusService;
|
||||
import org.dspace.app.rest.model.AccessStatusRest;
|
||||
import org.dspace.app.rest.model.BitstreamRest;
|
||||
import org.dspace.app.rest.projection.Projection;
|
||||
import org.dspace.content.AccessStatus;
|
||||
import org.dspace.content.Bitstream;
|
||||
import org.dspace.content.service.BitstreamService;
|
||||
import org.dspace.core.Context;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Link repository for calculating the access status of a Bitstream,
|
||||
* including the embargo date
|
||||
*/
|
||||
@Component(BitstreamRest.CATEGORY + "." + BitstreamRest.PLURAL_NAME + "." + BitstreamRest.ACCESS_STATUS)
|
||||
public class BitstreamAccessStatusLinkRepository extends AbstractDSpaceRestRepository
|
||||
implements LinkRestRepository {
|
||||
|
||||
@Autowired
|
||||
BitstreamService bitstreamService;
|
||||
|
||||
@Autowired
|
||||
AccessStatusService accessStatusService;
|
||||
|
||||
@PreAuthorize("hasPermission(#bitstreamId, 'BITSTREAM', 'METADATA_READ')")
|
||||
public AccessStatusRest getAccessStatus(@Nullable HttpServletRequest request,
|
||||
UUID bitstreamId,
|
||||
@Nullable Pageable optionalPageable,
|
||||
Projection projection) {
|
||||
try {
|
||||
Context context = obtainContext();
|
||||
Bitstream bitstream = bitstreamService.find(context, bitstreamId);
|
||||
if (bitstream == null) {
|
||||
throw new ResourceNotFoundException("No such bitstream: " + bitstreamId);
|
||||
}
|
||||
AccessStatusRest accessStatusRest = new AccessStatusRest();
|
||||
AccessStatus accessStatus = accessStatusService.getAccessStatus(context, bitstream);
|
||||
String status = accessStatus.getStatus();
|
||||
if (status == DefaultAccessStatusHelper.EMBARGO) {
|
||||
LocalDate availabilityDate = accessStatus.getAvailabilityDate();
|
||||
String embargoDate = availabilityDate.toString();
|
||||
accessStatusRest.setEmbargoDate(embargoDate);
|
||||
}
|
||||
accessStatusRest.setStatus(status);
|
||||
return accessStatusRest;
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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(
|
||||
|
@@ -9,14 +9,17 @@
|
||||
package org.dspace.app.rest.repository;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.dspace.access.status.DefaultAccessStatusHelper;
|
||||
import org.dspace.access.status.service.AccessStatusService;
|
||||
import org.dspace.app.rest.model.AccessStatusRest;
|
||||
import org.dspace.app.rest.model.ItemRest;
|
||||
import org.dspace.app.rest.projection.Projection;
|
||||
import org.dspace.content.AccessStatus;
|
||||
import org.dspace.content.Item;
|
||||
import org.dspace.content.service.ItemService;
|
||||
import org.dspace.core.Context;
|
||||
@@ -51,8 +54,14 @@ public class ItemAccessStatusLinkRepository extends AbstractDSpaceRestRepository
|
||||
throw new ResourceNotFoundException("No such item: " + itemId);
|
||||
}
|
||||
AccessStatusRest accessStatusRest = new AccessStatusRest();
|
||||
String accessStatus = accessStatusService.getAccessStatus(context, item);
|
||||
accessStatusRest.setStatus(accessStatus);
|
||||
AccessStatus accessStatus = accessStatusService.getAccessStatus(context, item);
|
||||
String status = accessStatus.getStatus();
|
||||
if (status == DefaultAccessStatusHelper.EMBARGO) {
|
||||
LocalDate availabilityDate = accessStatus.getAvailabilityDate();
|
||||
String embargoDate = availabilityDate.toString();
|
||||
accessStatusRest.setEmbargoDate(embargoDate);
|
||||
}
|
||||
accessStatusRest.setStatus(status);
|
||||
return accessStatusRest;
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
|
@@ -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,8 @@ 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.factory.CaptchaServiceFactory;
|
||||
import org.dspace.eperson.service.AccountService;
|
||||
import org.dspace.eperson.service.CaptchaService;
|
||||
import org.dspace.eperson.service.EPersonService;
|
||||
@@ -54,9 +61,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;
|
||||
@@ -70,8 +78,7 @@ public class RegistrationRestRepository extends DSpaceRestRepository<Registratio
|
||||
@Autowired
|
||||
private RequestService requestService;
|
||||
|
||||
@Autowired
|
||||
private CaptchaService captchaService;
|
||||
private CaptchaService captchaService = CaptchaServiceFactory.getInstance().getCaptchaService();
|
||||
|
||||
@Autowired
|
||||
private ConfigurationService configurationService;
|
||||
@@ -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;
|
||||
|
||||
@@ -103,7 +116,7 @@ public class RegistrationRestRepository extends DSpaceRestRepository<Registratio
|
||||
throw new IllegalArgumentException(String.format("Needs query param '%s' with value %s or %s indicating " +
|
||||
"what kind of registration request it is", TYPE_QUERY_PARAM, TYPE_FORGOT, TYPE_REGISTER));
|
||||
}
|
||||
String captchaToken = request.getHeader("X-Recaptcha-Token");
|
||||
String captchaToken = request.getHeader("x-captcha-payload");
|
||||
boolean verificationEnabled = configurationService.getBooleanProperty("registration.verification.enabled");
|
||||
|
||||
if (verificationEnabled && !accountType.equalsIgnoreCase(TYPE_FORGOT)) {
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -15,8 +15,6 @@ import java.net.MalformedURLException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.sql.SQLException;
|
||||
import java.time.Instant;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
@@ -24,12 +22,13 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.validator.routines.EmailValidator;
|
||||
import org.apache.http.client.utils.URIBuilder;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.dspace.app.requestitem.RequestItem;
|
||||
import org.dspace.app.requestitem.RequestItemEmailNotifier;
|
||||
import org.dspace.app.requestitem.service.RequestItemService;
|
||||
import org.dspace.app.rest.Parameter;
|
||||
import org.dspace.app.rest.SearchRestMethod;
|
||||
import org.dspace.app.rest.converter.RequestItemConverter;
|
||||
import org.dspace.app.rest.exception.IncompleteItemRequestException;
|
||||
import org.dspace.app.rest.exception.RepositoryMethodNotImplementedException;
|
||||
@@ -37,23 +36,31 @@ import org.dspace.app.rest.exception.UnprocessableEntityException;
|
||||
import org.dspace.app.rest.model.RequestItemRest;
|
||||
import org.dspace.app.rest.projection.Projection;
|
||||
import org.dspace.authorize.AuthorizeException;
|
||||
import org.dspace.authorize.service.AuthorizeService;
|
||||
import org.dspace.content.Bitstream;
|
||||
import org.dspace.content.Item;
|
||||
import org.dspace.content.service.BitstreamService;
|
||||
import org.dspace.content.service.ItemService;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.eperson.EPerson;
|
||||
import org.dspace.eperson.InvalidReCaptchaException;
|
||||
import org.dspace.eperson.factory.CaptchaServiceFactory;
|
||||
import org.dspace.eperson.service.CaptchaService;
|
||||
import org.dspace.services.ConfigurationService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.util.HtmlUtils;
|
||||
/**
|
||||
* Component to expose item requests.
|
||||
* Component to expose item requests and handle operations like create (request), put (grant/deny), and
|
||||
* email sending. Support for requested item access by a secure token / link is supported as well as the legacy
|
||||
* "attach files to email" method. See dspace.cfg for configuration.
|
||||
*
|
||||
* @author Mark H. Wood <mwood@iupui.edu>
|
||||
* @author Kim Shepherd
|
||||
*/
|
||||
@Component(RequestItemRest.CATEGORY + '.' + RequestItemRest.PLURAL_NAME)
|
||||
public class RequestItemRepository
|
||||
@@ -77,6 +84,12 @@ public class RequestItemRepository
|
||||
|
||||
@Autowired(required = true)
|
||||
protected RequestItemEmailNotifier requestItemEmailNotifier;
|
||||
@Autowired
|
||||
protected AuthorizeService authorizeService;
|
||||
|
||||
private CaptchaService captchaService = CaptchaServiceFactory.getInstance().getCaptchaService();
|
||||
|
||||
private static final Logger log = LogManager.getLogger();
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper mapper;
|
||||
@@ -109,6 +122,24 @@ public class RequestItemRepository
|
||||
HttpServletRequest req = getRequestService()
|
||||
.getCurrentRequest()
|
||||
.getHttpServletRequest();
|
||||
|
||||
// If captcha is configured for this action, perform validation
|
||||
if (configurationService.getBooleanProperty("request.item.create.captcha", false)) {
|
||||
// Get captcha payload header, if any
|
||||
String captchaPayloadHeader = req.getHeader("x-captcha-payload");
|
||||
if (StringUtils.isBlank(captchaPayloadHeader)) {
|
||||
throw new AuthorizeException("Valid captcha payload is required");
|
||||
}
|
||||
// Validate and verify captcha payload token or proof of work
|
||||
// Rethrow exception as authZ exception if validation fails
|
||||
try {
|
||||
captchaService.processResponse(captchaPayloadHeader, "request_item");
|
||||
} catch (InvalidReCaptchaException e) {
|
||||
throw new AuthorizeException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
RequestItemRest rir;
|
||||
try {
|
||||
rir = mapper.readValue(req.getInputStream(), RequestItemRest.class);
|
||||
@@ -195,7 +226,7 @@ public class RequestItemRepository
|
||||
// Create a link back to DSpace for the approver's response.
|
||||
String responseLink;
|
||||
try {
|
||||
responseLink = getLinkTokenEmail(ri.getToken());
|
||||
responseLink = requestItemService.getLinkTokenEmail(ri.getToken());
|
||||
} catch (URISyntaxException | MalformedURLException e) {
|
||||
LOG.warn("Impossible URL error while composing email: {}",
|
||||
e::getMessage);
|
||||
@@ -229,15 +260,17 @@ public class RequestItemRepository
|
||||
throw new UnprocessableEntityException("Item request not found");
|
||||
}
|
||||
|
||||
// Do not permit updates after a decision has been given.
|
||||
Instant decisionDate = ri.getDecision_date();
|
||||
if (null != decisionDate) {
|
||||
throw new UnprocessableEntityException("Request was "
|
||||
+ (ri.isAccept_request() ? "granted" : "denied")
|
||||
+ " on " + decisionDate + " and may not be updated.");
|
||||
// Previously there was a check here to prevent updates after *any* decision was given.
|
||||
// This is now updated to allow specific updates to *granted* requests, so that it is possible
|
||||
// to revoke access tokens or alter access period
|
||||
// Throw error only if decision date was set but was denied
|
||||
if (null != ri.getDecision_date() && !ri.isAccept_request()) {
|
||||
throw new UnprocessableEntityException("Item request was already denied, no further updates are possible");
|
||||
}
|
||||
|
||||
// Make the changes
|
||||
|
||||
// Extract and set the 'accept' indicator
|
||||
JsonNode acceptRequestNode = requestBody.findValue("acceptRequest");
|
||||
if (null == acceptRequestNode) {
|
||||
throw new UnprocessableEntityException("acceptRequest is required");
|
||||
@@ -245,18 +278,30 @@ public class RequestItemRepository
|
||||
ri.setAccept_request(acceptRequestNode.asBoolean());
|
||||
}
|
||||
|
||||
// Extract and set the response message to include in the email
|
||||
JsonNode responseMessageNode = requestBody.findValue("responseMessage");
|
||||
String message = null;
|
||||
if (responseMessageNode != null && !responseMessageNode.isNull()) {
|
||||
message = responseMessageNode.asText();
|
||||
}
|
||||
|
||||
// Set the decision date (now)`
|
||||
ri.setDecision_date(Instant.now());
|
||||
|
||||
// If the (optional) access expiry period was included, extract it here and set accordingly
|
||||
// We expect it to be sent either as a timestamp or as a delta math like +7DAYS
|
||||
JsonNode accessPeriod = requestBody.findValue("accessPeriod");
|
||||
if (accessPeriod != null && !accessPeriod.isNull()) {
|
||||
// The request item service is responsible for parsing and setting the expiry date based
|
||||
// on a delta like "+7DAYS" or special string like "FOREVER", or a formatted date
|
||||
requestItemService.setAccessExpiry(ri, accessPeriod.asText());
|
||||
}
|
||||
|
||||
JsonNode responseSubjectNode = requestBody.findValue("subject");
|
||||
String subject = null;
|
||||
if (responseSubjectNode != null && !responseSubjectNode.isNull()) {
|
||||
subject = responseSubjectNode.asText();
|
||||
}
|
||||
ri.setDecision_date(Instant.now());
|
||||
requestItemService.update(context, ri);
|
||||
|
||||
// Send the response email
|
||||
@@ -282,33 +327,39 @@ public class RequestItemRepository
|
||||
return rir;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param accessToken
|
||||
* @return
|
||||
*/
|
||||
@PreAuthorize("permitAll()")
|
||||
@SearchRestMethod(name = "byAccessToken")
|
||||
public RequestItemRest findByAccessToken(@Parameter(value = "accessToken", required = true) String accessToken) {
|
||||
|
||||
// Send 404 NOT FOUND if access token is null
|
||||
if (StringUtils.isBlank(accessToken)) {
|
||||
throw new ResourceNotFoundException("No such accessToken=" + accessToken);
|
||||
}
|
||||
|
||||
// Get the current context and request item
|
||||
Context context = obtainContext();
|
||||
RequestItem requestItem = requestItemService.findByAccessToken(context, accessToken);
|
||||
|
||||
// Previously, a 404 was thrown if the request item was not found, and a 401 or 403 was thrown depending
|
||||
// on authorization and validity checks. These checks are still strictly enforced in the BitstreamContoller
|
||||
// and BitstreamResourceAccessByToken classes for actual downloads, but here we continue to pass a 200 OK
|
||||
// response so that we can display more meaningful alerts to users in the item page rather than serve hard
|
||||
// redirects or lose information like expiry dates and access status
|
||||
|
||||
// Sanitize the request item (stripping personal data) for privacy
|
||||
requestItemService.sanitizeRequestItem(context, requestItem);
|
||||
// Convert and return the final request item
|
||||
return requestItemConverter.convert(requestItem, utils.obtainProjection());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<RequestItemRest> getDomainClass() {
|
||||
return RequestItemRest.class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a link back to DSpace, to act on a request.
|
||||
*
|
||||
* @param token identifies the request.
|
||||
* @return URL to the item request API, with /request-a-copy/{token} as the last URL segments
|
||||
* @throws URISyntaxException passed through.
|
||||
* @throws MalformedURLException passed through.
|
||||
*/
|
||||
public String getLinkTokenEmail(String token)
|
||||
throws URISyntaxException, MalformedURLException {
|
||||
final String base = configurationService.getProperty("dspace.ui.url");
|
||||
|
||||
// Construct the link, making sure to support sub-paths
|
||||
URIBuilder uriBuilder = new URIBuilder(base);
|
||||
List<String> segments = new LinkedList<>();
|
||||
if (StringUtils.isNotBlank(uriBuilder.getPath())) {
|
||||
segments.add(StringUtils.strip(uriBuilder.getPath(), "/"));
|
||||
}
|
||||
segments.add("request-a-copy");
|
||||
segments.add(token);
|
||||
|
||||
// Build and return the URL from segments (or throw exception)
|
||||
return uriBuilder.setPathSegments(segments).build().toURL().toExternalForm();
|
||||
}
|
||||
}
|
||||
|
@@ -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));
|
||||
}
|
||||
}
|
||||
|
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -38,21 +38,21 @@ import org.springframework.util.DigestUtils;
|
||||
*/
|
||||
public class BitstreamResource extends AbstractResource {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(BitstreamResource.class);
|
||||
static final Logger LOG = LogManager.getLogger(BitstreamResource.class);
|
||||
|
||||
private final String name;
|
||||
private final UUID uuid;
|
||||
private final UUID currentUserUUID;
|
||||
private final boolean shouldGenerateCoverPage;
|
||||
private final Set<UUID> currentSpecialGroups;
|
||||
protected final String name;
|
||||
protected final UUID uuid;
|
||||
protected final UUID currentUserUUID;
|
||||
protected final boolean shouldGenerateCoverPage;
|
||||
protected final Set<UUID> currentSpecialGroups;
|
||||
|
||||
private final BitstreamService bitstreamService = ContentServiceFactory.getInstance().getBitstreamService();
|
||||
private final EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService();
|
||||
private final CitationDocumentService citationDocumentService =
|
||||
protected final BitstreamService bitstreamService = ContentServiceFactory.getInstance().getBitstreamService();
|
||||
protected final EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService();
|
||||
protected final CitationDocumentService citationDocumentService =
|
||||
new DSpace().getServiceManager()
|
||||
.getServicesByType(CitationDocumentService.class).get(0);
|
||||
|
||||
private BitstreamDocument document;
|
||||
protected BitstreamDocument document;
|
||||
|
||||
public BitstreamResource(String name, UUID uuid, UUID currentUserUUID, Set<UUID> currentSpecialGroups,
|
||||
boolean shouldGenerateCoverPage) {
|
||||
@@ -71,7 +71,7 @@ public class BitstreamResource extends AbstractResource {
|
||||
* @param bitstream the pdf for which we want to generate a coverpage
|
||||
* @return a byte array containing the cover page
|
||||
*/
|
||||
private byte[] getCoverpageByteArray(Context context, Bitstream bitstream)
|
||||
byte[] getCoverpageByteArray(Context context, Bitstream bitstream)
|
||||
throws IOException, SQLException, AuthorizeException {
|
||||
try {
|
||||
var citedDocument = citationDocumentService.makeCitedDocument(context, bitstream);
|
||||
@@ -101,7 +101,7 @@ public class BitstreamResource extends AbstractResource {
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() {
|
||||
public long contentLength() throws IOException {
|
||||
fetchDocument();
|
||||
|
||||
return document.length();
|
||||
@@ -113,7 +113,7 @@ public class BitstreamResource extends AbstractResource {
|
||||
return document.etag();
|
||||
}
|
||||
|
||||
private void fetchDocument() {
|
||||
void fetchDocument() {
|
||||
if (document != null) {
|
||||
return;
|
||||
}
|
||||
@@ -138,7 +138,7 @@ public class BitstreamResource extends AbstractResource {
|
||||
LOG.debug("fetched document {} {}", shouldGenerateCoverPage, document);
|
||||
}
|
||||
|
||||
private String etag(Bitstream bitstream) {
|
||||
String etag(Bitstream bitstream) {
|
||||
|
||||
/* Ideally we would calculate the md5 checksum based on the document with coverpage.
|
||||
However it looks like the coverpage generation is not stable (e.g. if invoked twice it will return
|
||||
@@ -157,7 +157,7 @@ public class BitstreamResource extends AbstractResource {
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private Context initializeContext() throws SQLException {
|
||||
Context initializeContext() throws SQLException {
|
||||
Context context = new Context();
|
||||
EPerson currentUser = ePersonService.find(context, currentUserUUID);
|
||||
context.setCurrentUser(currentUser);
|
||||
@@ -165,5 +165,5 @@ public class BitstreamResource extends AbstractResource {
|
||||
return context;
|
||||
}
|
||||
|
||||
private record BitstreamDocument(String etag, long length, InputStream inputStream) {}
|
||||
record BitstreamDocument(String etag, long length, InputStream inputStream) {}
|
||||
}
|
||||
|
@@ -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.app.rest.utils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.dspace.app.requestitem.factory.RequestItemServiceFactory;
|
||||
import org.dspace.app.requestitem.service.RequestItemService;
|
||||
import org.dspace.authorize.AuthorizeException;
|
||||
import org.dspace.content.Bitstream;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.services.ConfigurationService;
|
||||
import org.dspace.services.factory.DSpaceServicesFactory;
|
||||
import org.springframework.core.io.AbstractResource;
|
||||
|
||||
/**
|
||||
* This class acts as a {@link AbstractResource} used by Spring's framework to send the data in a proper and
|
||||
* streamlined way inside the {@link org.springframework.http.ResponseEntity} body.
|
||||
* This class' attributes are being used by Spring's framework in the overridden methods so that the proper
|
||||
* attributes are given and used in the response.
|
||||
*
|
||||
* Unlike the BitstreamResource, this resource expects a valid and authorised access token to use when
|
||||
* retrieving actual bitstream data. It will either set a special group in the temp context for READ of the
|
||||
* bitstream, or turn off authorisation for the lifetime of the temp (autocloseable in try-with-resources) context
|
||||
*
|
||||
* @author Kim Shepherd
|
||||
*/
|
||||
public class BitstreamResourceAccessByToken extends BitstreamResource {
|
||||
|
||||
private String accessToken;
|
||||
|
||||
private RequestItemService requestItemService = RequestItemServiceFactory.getInstance().getRequestItemService();
|
||||
|
||||
private ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
|
||||
|
||||
public BitstreamResourceAccessByToken(String name, UUID uuid, UUID currentUserUUID, Set<UUID> currentSpecialGroups,
|
||||
boolean shouldGenerateCoverPage, String accessToken) {
|
||||
super(name, uuid, currentUserUUID, currentSpecialGroups, shouldGenerateCoverPage);
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bitstream content using the special temporary context if the request-a-copy access request
|
||||
* is properly authenticated and authorised. After this method is called, the bitstream content is
|
||||
* updated so the correct input stream can be returned
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
@Override
|
||||
public void fetchDocument() {
|
||||
// If the feature is not enabled, throw exception
|
||||
if (configurationService.getProperty("request.item.type") == null) {
|
||||
throw new RuntimeException("Request a copy is not enabled, download via access token will not be allowed");
|
||||
}
|
||||
|
||||
// If document is already set for this BitstreamResource, return
|
||||
if (document != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (Context fileRetrievalContext = initializeContext()) {
|
||||
// Set special privileges for context for this access
|
||||
// Note - this is a try-with-resources statement. The context has a very limited lifetime.
|
||||
// However, be very careful using context in this block! It should ONLY perform authorization
|
||||
// of the access token and retrieval of the bitstream content.
|
||||
fileRetrievalContext.turnOffAuthorisationSystem();
|
||||
// Get bitstream from uuid
|
||||
Bitstream bitstream = bitstreamService.find(fileRetrievalContext, uuid);
|
||||
|
||||
try {
|
||||
// Explicitly authenticate the access request acceptance for the bitstream
|
||||
// even if we have already done it in the REST controller and throw Authorize exception if not valid
|
||||
requestItemService.authorizeAccessByAccessToken(fileRetrievalContext, bitstream, accessToken);
|
||||
|
||||
} catch (AuthorizeException e) {
|
||||
throw new AuthorizeException("Authorization to bitstream " + uuid + " by access token FAILED");
|
||||
}
|
||||
if (shouldGenerateCoverPage) {
|
||||
var coverPage = getCoverpageByteArray(fileRetrievalContext, bitstream);
|
||||
|
||||
this.document = new BitstreamDocument(etag(bitstream),
|
||||
coverPage.length,
|
||||
new ByteArrayInputStream(coverPage));
|
||||
} else {
|
||||
this.document = new BitstreamDocument(bitstream.getChecksum(),
|
||||
bitstream.getSizeBytes(),
|
||||
bitstreamService.retrieve(fileRetrievalContext, bitstream));
|
||||
}
|
||||
} catch (SQLException | AuthorizeException | IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
LOG.debug("fetched document {} {}", shouldGenerateCoverPage, document);
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* 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.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||
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.test.AbstractControllerIntegrationTest;
|
||||
import org.dspace.services.ConfigurationService;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
/**
|
||||
* Test basic challenge method in AltchaCaptchaRestController
|
||||
* Actual payload validation tests are done in AltchaCaptchaServiceTest
|
||||
*
|
||||
* @author Kim Shepherd
|
||||
*/
|
||||
public class AltchaCaptchaRestControllerIT extends AbstractControllerIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
ConfigurationService configurationService;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
configurationService.setProperty("captcha.provider", "altcha");
|
||||
configurationService.setProperty("altcha.hmac.key", "onetwothreesecret");
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
configurationService.setProperty("captcha.provider", "google");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAltchaChallengeAuthenticated() throws Exception {
|
||||
String authToken = getAuthToken(eperson.getEmail(), password);
|
||||
|
||||
getClient(authToken).perform(get("/api/captcha/challenge"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string("Content-Type", "application/json;charset=UTF-8"))
|
||||
.andExpect(header().exists("Cache-Control"))
|
||||
.andExpect(header().exists("Expires"))
|
||||
.andExpect(jsonPath("$.algorithm").value("SHA-256"))
|
||||
.andExpect(jsonPath("$.challenge").isString())
|
||||
.andExpect(jsonPath("$.salt").isString())
|
||||
.andExpect(jsonPath("$.signature").isString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAltchaChallengeUnauthenticated() throws Exception {
|
||||
getClient().perform(get("/api/captcha/challenge"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string("Content-Type", "application/json;charset=UTF-8"))
|
||||
.andExpect(jsonPath("$.algorithm").value("SHA-256"))
|
||||
.andExpect(jsonPath("$.challenge").isString())
|
||||
.andExpect(jsonPath("$.salt").isString())
|
||||
.andExpect(jsonPath("$.signature").isString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAltchaChallengeWithMissingHmacKey() throws Exception {
|
||||
// Temporarily clear the HMAC key config
|
||||
configurationService.setProperty("altcha.hmac.key", null);
|
||||
|
||||
getClient().perform(get("/api/captcha/challenge"))
|
||||
.andExpect(status().isBadRequest());
|
||||
|
||||
// Reset the config
|
||||
configurationService.setProperty("altcha.hmac.key", "onetwothreesecret");
|
||||
}
|
||||
|
||||
}
|
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
package org.dspace.app.rest;
|
||||
|
||||
import static com.jayway.jsonpath.JsonPath.read;
|
||||
import static jakarta.mail.internet.MimeUtility.encodeText;
|
||||
import static java.util.UUID.randomUUID;
|
||||
import static org.apache.commons.codec.CharEncoding.UTF_8;
|
||||
@@ -52,8 +53,11 @@ import java.io.StringWriter;
|
||||
import java.io.Writer;
|
||||
import java.nio.file.Files;
|
||||
import java.time.Period;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.CharEncoding;
|
||||
@@ -61,6 +65,9 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.text.PDFTextStripper;
|
||||
import org.apache.solr.client.solrj.SolrServerException;
|
||||
import org.dspace.app.requestitem.RequestItem;
|
||||
import org.dspace.app.requestitem.service.RequestItemService;
|
||||
import org.dspace.app.rest.model.RequestItemRest;
|
||||
import org.dspace.app.rest.test.AbstractControllerIntegrationTest;
|
||||
import org.dspace.authorize.service.AuthorizeService;
|
||||
import org.dspace.authorize.service.ResourcePolicyService;
|
||||
@@ -70,6 +77,7 @@ import org.dspace.builder.CommunityBuilder;
|
||||
import org.dspace.builder.EPersonBuilder;
|
||||
import org.dspace.builder.GroupBuilder;
|
||||
import org.dspace.builder.ItemBuilder;
|
||||
import org.dspace.builder.RequestItemBuilder;
|
||||
import org.dspace.content.Bitstream;
|
||||
import org.dspace.content.BitstreamFormat;
|
||||
import org.dspace.content.Collection;
|
||||
@@ -127,6 +135,16 @@ public class BitstreamRestControllerIT extends AbstractControllerIntegrationTest
|
||||
@Autowired
|
||||
private CollectionService collectionService;
|
||||
|
||||
@Autowired
|
||||
private RequestItemService requestItemService;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper mapper;
|
||||
|
||||
public static final String requestItemUrl = REST_SERVER_URL
|
||||
+ RequestItemRest.CATEGORY + '/'
|
||||
+ RequestItemRest.PLURAL_NAME;
|
||||
|
||||
private Bitstream bitstream;
|
||||
private BitstreamFormat supportedFormat;
|
||||
private BitstreamFormat knownFormat;
|
||||
@@ -1500,4 +1518,103 @@ public class BitstreamRestControllerIT extends AbstractControllerIntegrationTest
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void restrictedBitstreamWithAccessTokenTest() throws Exception {
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
EPerson eperson2 = EPersonBuilder.createEPerson(context)
|
||||
.withEmail("eperson2@mail.com")
|
||||
.withPassword("qwerty02")
|
||||
.build();
|
||||
|
||||
parentCommunity = CommunityBuilder.createCommunity(context)
|
||||
.withName("Parent Community")
|
||||
.build();
|
||||
|
||||
Collection col1 = CollectionBuilder.createCollection(context, parentCommunity)
|
||||
.withName("Collection 1")
|
||||
.build();
|
||||
|
||||
Group restrictedGroup = GroupBuilder.createGroup(context)
|
||||
.withName("Restricted Group")
|
||||
.addMember(eperson)
|
||||
.build();
|
||||
|
||||
Item item;
|
||||
|
||||
// Create large bitstream over threshold
|
||||
byte[] bytes = new byte[21 * 1024 * 1024]; // 21MB
|
||||
try (InputStream is = new ByteArrayInputStream(bytes)) {
|
||||
|
||||
item = ItemBuilder.createItem(context, col1)
|
||||
.withTitle("item 1")
|
||||
.withIssueDate("2013-01-17")
|
||||
.withAuthor("Doe, John")
|
||||
.build();
|
||||
bitstream = BitstreamBuilder
|
||||
.createBitstream(context, item, is)
|
||||
.withName("Test Embargoed Bitstream")
|
||||
.withDescription("This bitstream is embargoed")
|
||||
.withMimeType("text/plain")
|
||||
.withReaderGroup(restrictedGroup)
|
||||
.build();
|
||||
}
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
// Create an item request to approve.
|
||||
RequestItem itemRequest = RequestItemBuilder
|
||||
.createRequestItem(context, item, bitstream)
|
||||
.build();
|
||||
|
||||
// Create the HTTP request body.
|
||||
Map<String, String> parameters = Map.of(
|
||||
"acceptRequest", "true",
|
||||
"subject", "subject",
|
||||
"responseMessage", "Request accepted",
|
||||
"accessPeriod", "+1DAY",
|
||||
"suggestOpenAccess", "true");
|
||||
String content = mapper
|
||||
.writer()
|
||||
.writeValueAsString(parameters);
|
||||
|
||||
// Send the request to approve the request.
|
||||
String authToken = getAuthToken(eperson.getEmail(), password);
|
||||
AtomicReference<String> requestTokenRef = new AtomicReference<>();
|
||||
getClient(authToken).perform(put(requestItemUrl + '/' + itemRequest.getToken())
|
||||
.contentType(contentType)
|
||||
.content(content))
|
||||
.andExpect(status().isOk()
|
||||
)
|
||||
.andDo((var result) -> requestTokenRef.set(
|
||||
read(result.getResponse().getContentAsString(), "token")));
|
||||
RequestItem foundRequest
|
||||
= requestItemService.findByToken(context, requestTokenRef.get());
|
||||
|
||||
// download the bitstream - 4 scenarios
|
||||
|
||||
// Someone in the restricted group is allowed access to the item without any access token
|
||||
getClient(authToken).perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content"))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
checkNumberOfStatsRecords(bitstream, 1);
|
||||
|
||||
String tokenEPerson2 = getAuthToken(eperson2.getEmail(), "qwerty02");
|
||||
getClient(tokenEPerson2).perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content"))
|
||||
.andExpect(status().isForbidden());
|
||||
|
||||
// Anonymous users CANNOT access/download Bitstreams that are restricted, without the access token
|
||||
getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
|
||||
// Anonymous users CANNOT access/download Bitstreams that are restricted, with an invalid access token
|
||||
getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content")
|
||||
.param("accessToken", "invalid_token")).andExpect(status().isUnauthorized());
|
||||
|
||||
// Anonymous users CAN access/download Bitstreams that are restricted, with a valid access token
|
||||
getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content")
|
||||
.param("accessToken", foundRequest.getAccess_token())).andExpect(status().isOk());
|
||||
|
||||
// Cleanup created request
|
||||
RequestItemBuilder.deleteRequestItem(foundRequest.getToken());
|
||||
}
|
||||
}
|
||||
|
@@ -16,6 +16,8 @@ import static org.dspace.core.Constants.WRITE;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
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.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
@@ -2933,6 +2935,82 @@ public class BitstreamRestRepositoryIT extends AbstractControllerIntegrationTest
|
||||
.andExpect(status().isNoContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findAccessStatusForBitstreamBadRequestTest() throws Exception {
|
||||
getClient().perform(get("/api/core/bitstreams/{uuid}/accessStatus", "1"))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findAccessStatusForItemNotFoundTest() throws Exception {
|
||||
UUID fakeUUID = UUID.randomUUID();
|
||||
getClient().perform(get("/api/core/items/{uuid}/accessStatus", fakeUUID))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findAccessStatusForBitstreamTest() throws Exception {
|
||||
context.turnOffAuthorisationSystem();
|
||||
parentCommunity = CommunityBuilder.createCommunity(context)
|
||||
.withName("Parent Community")
|
||||
.build();
|
||||
Collection col1 = CollectionBuilder.createCollection(context, parentCommunity)
|
||||
.withName("Collection 1")
|
||||
.build();
|
||||
Item publicItem1 = ItemBuilder.createItem(context, col1)
|
||||
.withTitle("Test item 1")
|
||||
.build();
|
||||
String bitstreamContent = "ThisIsSomeDummyText";
|
||||
Bitstream bitstream = null;
|
||||
try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
|
||||
bitstream = BitstreamBuilder.createBitstream(context, publicItem1, is)
|
||||
.withName("Bitstream")
|
||||
.withDescription("Description")
|
||||
.withMimeType("text/plain")
|
||||
.build();
|
||||
}
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
// Bitstream access status should still be accessible by anonymous request
|
||||
getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/accessStatus"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$", HalMatcher.matchNoEmbeds()))
|
||||
.andExpect(jsonPath("$.status", notNullValue()))
|
||||
.andExpect(jsonPath("$.embargoDate", nullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findAccessStatusWithEmbargoDateForBitstreamTest() throws Exception {
|
||||
context.turnOffAuthorisationSystem();
|
||||
parentCommunity = CommunityBuilder.createCommunity(context)
|
||||
.withName("Parent Community")
|
||||
.build();
|
||||
Collection col1 = CollectionBuilder.createCollection(context, parentCommunity)
|
||||
.withName("Collection 1")
|
||||
.build();
|
||||
Item publicItem1 = ItemBuilder.createItem(context, col1)
|
||||
.withTitle("Test item 1")
|
||||
.build();
|
||||
String bitstreamContent = "ThisIsSomeDummyText";
|
||||
Bitstream bitstream = null;
|
||||
try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
|
||||
bitstream = BitstreamBuilder.createBitstream(context, publicItem1, is)
|
||||
.withName("Bitstream")
|
||||
.withDescription("Description")
|
||||
.withMimeType("text/plain")
|
||||
.withEmbargoPeriod(Period.ofMonths(6))
|
||||
.build();
|
||||
}
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
// Bitstream access status should still be accessible by anonymous request
|
||||
getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/accessStatus"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$", HalMatcher.matchNoEmbeds()))
|
||||
.andExpect(jsonPath("$.status", notNullValue()))
|
||||
.andExpect(jsonPath("$.embargoDate", notNullValue()));
|
||||
}
|
||||
|
||||
public boolean bitstreamExists(String token, Bitstream ...bitstreams) throws Exception {
|
||||
for (Bitstream bitstream : bitstreams) {
|
||||
if (getClient(token).perform(get("/api/core/bitstreams/" + bitstream.getID()))
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -4688,7 +4688,36 @@ public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest {
|
||||
context.restoreAuthSystemState();
|
||||
getClient().perform(get("/api/core/items/{uuid}/accessStatus", item.getID()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.status", notNullValue()));
|
||||
.andExpect(jsonPath("$.status", notNullValue()))
|
||||
.andExpect(jsonPath("$.embargoDate", nullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findAccessStatusWithEmbargoDateForItemTest() throws Exception {
|
||||
context.turnOffAuthorisationSystem();
|
||||
parentCommunity = CommunityBuilder.createCommunity(context)
|
||||
.withName("Parent Community")
|
||||
.build();
|
||||
Collection owningCollection = CollectionBuilder.createCollection(context, parentCommunity)
|
||||
.withName("Owning Collection")
|
||||
.build();
|
||||
Item item = ItemBuilder.createItem(context, owningCollection)
|
||||
.withTitle("Test item")
|
||||
.build();
|
||||
Bundle originalBundle = BundleBuilder.createBundle(context, item)
|
||||
.withName(Constants.DEFAULT_BUNDLE_NAME)
|
||||
.build();
|
||||
InputStream is = IOUtils.toInputStream("dummy", "utf-8");
|
||||
Bitstream bitstream = BitstreamBuilder.createBitstream(context, originalBundle, is)
|
||||
.withName("test.pdf")
|
||||
.withMimeType("application/pdf")
|
||||
.withEmbargoPeriod(Period.ofMonths(6))
|
||||
.build();
|
||||
context.restoreAuthSystemState();
|
||||
getClient().perform(get("/api/core/items/{uuid}/accessStatus", item.getID()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.status", notNullValue()))
|
||||
.andExpect(jsonPath("$.embargoDate", notNullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@@ -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);
|
||||
|
@@ -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();
|
||||
@@ -319,7 +366,7 @@ public class RegistrationRestRepositoryIT extends AbstractControllerIntegrationT
|
||||
RegistrationRest registrationRest = new RegistrationRest();
|
||||
registrationRest.setEmail(eperson.getEmail());
|
||||
|
||||
// when reCAPTCHA enabled and request doesn't contain "X-Recaptcha-Token” header
|
||||
// when reCAPTCHA enabled and request doesn't contain "x-captcha-payload” header
|
||||
getClient().perform(post("/api/eperson/registrations")
|
||||
.param(TYPE_QUERY_PARAM, TYPE_REGISTER)
|
||||
.content(mapper.writeValueAsBytes(registrationRest))
|
||||
@@ -340,10 +387,10 @@ public class RegistrationRestRepositoryIT extends AbstractControllerIntegrationT
|
||||
registrationRest.setEmail(eperson.getEmail());
|
||||
|
||||
String captchaToken = "invalid-captcha-Token";
|
||||
// when reCAPTCHA enabled and request contains Invalid "X-Recaptcha-Token” header
|
||||
// when reCAPTCHA enabled and request contains Invalid "x-captcha-payload” header
|
||||
getClient().perform(post("/api/eperson/registrations")
|
||||
.param(TYPE_QUERY_PARAM, TYPE_REGISTER)
|
||||
.header("X-Recaptcha-Token", captchaToken)
|
||||
.header("x-captcha-payload", captchaToken)
|
||||
.content(mapper.writeValueAsBytes(registrationRest))
|
||||
.contentType(contentType))
|
||||
.andExpect(status().isForbidden());
|
||||
@@ -376,17 +423,17 @@ public class RegistrationRestRepositoryIT extends AbstractControllerIntegrationT
|
||||
RegistrationRest registrationRest = new RegistrationRest();
|
||||
registrationRest.setEmail(eperson.getEmail());
|
||||
try {
|
||||
// will throw InvalidReCaptchaException because 'X-Recaptcha-Token' not equal captchaToken
|
||||
// will throw InvalidReCaptchaException because 'x-captcha-payload' not equal captchaToken
|
||||
getClient().perform(post("/api/eperson/registrations")
|
||||
.param(TYPE_QUERY_PARAM, TYPE_REGISTER)
|
||||
.header("X-Recaptcha-Token", captchaToken1)
|
||||
.header("x-captcha-payload", captchaToken1)
|
||||
.content(mapper.writeValueAsBytes(registrationRest))
|
||||
.contentType(contentType))
|
||||
.andExpect(status().isForbidden());
|
||||
|
||||
getClient().perform(post("/api/eperson/registrations")
|
||||
.param(TYPE_QUERY_PARAM, TYPE_REGISTER)
|
||||
.header("X-Recaptcha-Token", captchaToken)
|
||||
.header("x-captcha-payload", captchaToken)
|
||||
.content(mapper.writeValueAsBytes(registrationRest))
|
||||
.contentType(contentType))
|
||||
.andExpect(status().isCreated());
|
||||
@@ -399,7 +446,7 @@ public class RegistrationRestRepositoryIT extends AbstractControllerIntegrationT
|
||||
registrationRest.setEmail(newEmail);
|
||||
getClient().perform(post("/api/eperson/registrations")
|
||||
.param(TYPE_QUERY_PARAM, TYPE_REGISTER)
|
||||
.header("X-Recaptcha-Token", captchaToken)
|
||||
.header("x-captcha-payload", captchaToken)
|
||||
.content(mapper.writeValueAsBytes(registrationRest))
|
||||
.contentType(contentType))
|
||||
.andExpect(status().isCreated());
|
||||
@@ -415,7 +462,7 @@ public class RegistrationRestRepositoryIT extends AbstractControllerIntegrationT
|
||||
registrationRest.setEmail(newEmail);
|
||||
getClient().perform(post("/api/eperson/registrations")
|
||||
.param(TYPE_QUERY_PARAM, TYPE_REGISTER)
|
||||
.header("X-Recaptcha-Token", captchaToken)
|
||||
.header("x-captcha-payload", captchaToken)
|
||||
.content(mapper.writeValueAsBytes(registrationRest))
|
||||
.contentType(contentType))
|
||||
.andExpect(status().is(HttpServletResponse.SC_UNAUTHORIZED));
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -27,13 +27,12 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.sql.SQLException;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
@@ -61,6 +60,7 @@ import org.dspace.content.Item;
|
||||
import org.dspace.services.ConfigurationService;
|
||||
import org.exparity.hamcrest.date.LocalDateTimeMatchers;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -102,6 +102,13 @@ public class RequestItemRepositoryIT
|
||||
|
||||
private Bitstream bitstream;
|
||||
|
||||
private Map<String, Object> altchaPayload;
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
configurationService.setProperty("captcha.provider", "google");
|
||||
}
|
||||
|
||||
@Before
|
||||
public void init()
|
||||
throws SQLException, AuthorizeException, IOException {
|
||||
@@ -130,6 +137,18 @@ public class RequestItemRepositoryIT
|
||||
.withName("Bitstream")
|
||||
.build();
|
||||
|
||||
altchaPayload = new HashMap<>();
|
||||
altchaPayload.put("challenge", "a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3");
|
||||
altchaPayload.put("salt", "salt123");
|
||||
altchaPayload.put("number", 1);
|
||||
altchaPayload.put("signature", "f5cd3ed4161f5f3c914c5778e716d6b446fa277086bbb8fd3e2b0c4b89f18833");
|
||||
altchaPayload.put("algorithm", "SHA-256");
|
||||
|
||||
// Set up altcha configuration
|
||||
configurationService.setProperty("captcha.provider", "altcha");
|
||||
configurationService.setProperty("altcha.algorithm", "SHA-256");
|
||||
configurationService.setProperty("altcha.hmac.key", "onetwothreesecret");
|
||||
|
||||
context.restoreAuthSystemState();
|
||||
}
|
||||
|
||||
@@ -263,8 +282,6 @@ public class RequestItemRepositoryIT
|
||||
@Test
|
||||
public void testCreateAndReturnNotAuthenticated()
|
||||
throws SQLException, AuthorizeException, IOException, Exception {
|
||||
System.out.println("createAndReturn (not authenticated)");
|
||||
|
||||
// Fake up a request in REST form.
|
||||
RequestItemRest rir = new RequestItemRest();
|
||||
rir.setAllfiles(false);
|
||||
@@ -273,10 +290,16 @@ public class RequestItemRepositoryIT
|
||||
rir.setRequestEmail(RequestItemBuilder.REQ_EMAIL);
|
||||
rir.setRequestMessage(RequestItemBuilder.REQ_MESSAGE);
|
||||
rir.setRequestName(RequestItemBuilder.REQ_NAME);
|
||||
String base64Payload =
|
||||
"eyJjaGFsbGVuZ2UiOiJhNjY1YTQ1OTIwNDIyZjlkNDE3ZTQ4NjdlZmRjNGZiOGEwNGExZ" +
|
||||
"jNmZmYxZmEwN2U5OThlODZmN2Y3YTI3YWUzIiwic2FsdCI6InNhbHQxMjMiLCJudW1iZX" +
|
||||
"IiOjEsInNpZ25hdHVyZSI6ImY1Y2QzZWQ0MTYxZjVmM2M5MTRjNTc3OGU3MTZkNmI0NDZ" +
|
||||
"mYTI3NzA4NmJiYjhmZDNlMmIwYzRiODlmMTg4MzMiLCJhbGdvcml0aG0iOiJTSEEtMjU2In0=";
|
||||
|
||||
// Create it and see if it was created correctly.
|
||||
try {
|
||||
getClient().perform(post(URI_ROOT)
|
||||
.header("x-captcha-payload", base64Payload)
|
||||
.content(mapper.writeValueAsBytes(rir))
|
||||
.contentType(contentType))
|
||||
.andExpect(status().isCreated())
|
||||
@@ -314,8 +337,6 @@ public class RequestItemRepositoryIT
|
||||
@Test
|
||||
public void testCreateAndReturnBadRequest()
|
||||
throws SQLException, AuthorizeException, IOException, Exception {
|
||||
System.out.println("createAndReturn (bad requests)");
|
||||
|
||||
// Fake up a request in REST form.
|
||||
RequestItemRest rir = new RequestItemRest();
|
||||
rir.setBitstreamId(bitstream.getID().toString());
|
||||
@@ -391,7 +412,6 @@ public class RequestItemRepositoryIT
|
||||
@Test
|
||||
public void testCreateWithInvalidCSRF()
|
||||
throws Exception {
|
||||
|
||||
// Login via password to retrieve a valid token
|
||||
String token = getAuthToken(eperson.getEmail(), password);
|
||||
|
||||
@@ -494,6 +514,7 @@ public class RequestItemRepositoryIT
|
||||
Map<String, String> parameters = Map.of(
|
||||
"acceptRequest", "true",
|
||||
"subject", "subject",
|
||||
"accessPeriod", "+1DAY",
|
||||
"responseMessage", "Request accepted",
|
||||
"suggestOpenAccess", "true");
|
||||
String content = mapper
|
||||
@@ -574,7 +595,6 @@ public class RequestItemRepositoryIT
|
||||
@Test
|
||||
public void testPutCompletedRequest()
|
||||
throws Exception {
|
||||
|
||||
// Create an item request that is already denied.
|
||||
RequestItem itemRequest = RequestItemBuilder
|
||||
.createRequestItem(context, item, bitstream)
|
||||
@@ -606,38 +626,5 @@ public class RequestItemRepositoryIT
|
||||
assertEquals("Wrong domain class", RequestItemRest.class, instanceClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that generated links include the correct base URL, where the UI URL has a subpath like /subdir
|
||||
*/
|
||||
@Test
|
||||
public void testGetLinkTokenEmailWithSubPath() throws MalformedURLException, URISyntaxException {
|
||||
RequestItemRepository instance = applicationContext.getBean(
|
||||
RequestItemRest.CATEGORY + '.' + RequestItemRest.PLURAL_NAME,
|
||||
RequestItemRepository.class);
|
||||
String currentDspaceUrl = configurationService.getProperty("dspace.ui.url");
|
||||
String newDspaceUrl = currentDspaceUrl + "/subdir";
|
||||
// Add a /subdir to the url for this test
|
||||
configurationService.setProperty("dspace.ui.url", newDspaceUrl);
|
||||
String expectedUrl = newDspaceUrl + "/request-a-copy/token";
|
||||
String generatedLink = instance.getLinkTokenEmail("token");
|
||||
// The URLs should match
|
||||
assertEquals(expectedUrl, generatedLink);
|
||||
configurationService.reloadConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that generated links include the correct base URL, with NO subpath elements
|
||||
*/
|
||||
@Test
|
||||
public void testGetLinkTokenEmailWithoutSubPath() throws MalformedURLException, URISyntaxException {
|
||||
RequestItemRepository instance = applicationContext.getBean(
|
||||
RequestItemRest.CATEGORY + '.' + RequestItemRest.PLURAL_NAME,
|
||||
RequestItemRepository.class);
|
||||
String currentDspaceUrl = configurationService.getProperty("dspace.ui.url");
|
||||
String expectedUrl = currentDspaceUrl + "/request-a-copy/token";
|
||||
String generatedLink = instance.getLinkTokenEmail("token");
|
||||
// The URLs should match
|
||||
assertEquals(expectedUrl, generatedLink);
|
||||
configurationService.reloadConfig();
|
||||
}
|
||||
}
|
||||
|
@@ -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()));
|
||||
|
@@ -47,6 +47,8 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
|
||||
|
||||
public class RequestCopyFeatureIT extends AbstractControllerIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
|
@@ -102,7 +102,8 @@ public class BitstreamMatcher {
|
||||
return matchEmbeds(
|
||||
"bundle",
|
||||
"format",
|
||||
"thumbnail"
|
||||
"thumbnail",
|
||||
"accessStatus"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -115,7 +116,8 @@ public class BitstreamMatcher {
|
||||
"content",
|
||||
"format",
|
||||
"self",
|
||||
"thumbnail"
|
||||
"thumbnail",
|
||||
"accessStatus"
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -5,7 +5,6 @@
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
package org.dspace.app.rest.matcher;
|
||||
|
||||
import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath;
|
||||
|
@@ -15,7 +15,7 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Test the AccessStatusRestTest class
|
||||
* Test the AccessStatusRest class
|
||||
*/
|
||||
public class AccessStatusRestTest {
|
||||
|
||||
@@ -36,4 +36,15 @@ public class AccessStatusRestTest {
|
||||
accessStatusRest.setStatus(DefaultAccessStatusHelper.UNKNOWN);
|
||||
assertNotNull(accessStatusRest.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmbargoDateIsNullBeforeEmbargoDateSet() throws Exception {
|
||||
assertNull(accessStatusRest.getEmbargoDate());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmbargoDateIsNotNullAfterEmbargoDateSet() throws Exception {
|
||||
accessStatusRest.setEmbargoDate("2050-01-01");
|
||||
assertNotNull(accessStatusRest.getEmbargoDate());
|
||||
}
|
||||
}
|
||||
|
@@ -878,6 +878,19 @@ access.status.embargo.forever.year = 10000
|
||||
access.status.embargo.forever.month = 1
|
||||
access.status.embargo.forever.day = 1
|
||||
|
||||
# How to determine the access status:
|
||||
# anonymous - Only consider the anonymous group
|
||||
# current - Consider the current user
|
||||
#
|
||||
# For example, if an object is embargoed, "anonymous" will ensure the embargo status is displayed
|
||||
# for everyone (regardless of permissions). But, "current" will only display the embargo status
|
||||
# if the current user does not have read permissions on the object.
|
||||
#
|
||||
# This configuration doesn't impact the calculation for OAI-PMH. The XOAI plugin will always
|
||||
# calculate the status with the "anonymous" type.
|
||||
access.status.for-user.item = anonymous
|
||||
access.status.for-user.bitstream = current
|
||||
|
||||
# implementation of access status helper plugin - replace with local implementation if applicable
|
||||
# This default access status helper provides an item status based on the policies of the primary
|
||||
# bitstream (or first bitstream in the original bundles if no primary file is specified).
|
||||
@@ -1549,23 +1562,6 @@ log.report.dir = ${dspace.dir}/log
|
||||
# google-analytics.bundles = none
|
||||
google-analytics.bundles = ORIGINAL
|
||||
|
||||
####################################################################
|
||||
#---------------------------------------------------------------#
|
||||
#----------------REQUEST ITEM CONFIGURATION---------------------#
|
||||
#---------------------------------------------------------------#
|
||||
|
||||
# Configuration of request-item. Possible values:
|
||||
# all - Anonymous users can request an item
|
||||
# logged - Login is mandatory to request an item
|
||||
# empty/commented out - request-copy not allowed
|
||||
request.item.type = all
|
||||
# Should all Request Copy emails go to the helpdesk instead of the item submitter?
|
||||
request.item.helpdesk.override = false
|
||||
# Should a rejection of a copy request send an email back to the requester?
|
||||
# Defaults to "true", which means a rejection email is sent back.
|
||||
# Setting it to "false" results in a silent rejection.
|
||||
request.item.reject.email = true
|
||||
|
||||
#------------------------------------------------------------------#
|
||||
#------------------SUBMISSION CONFIGURATION------------------------#
|
||||
#------------------------------------------------------------------#
|
||||
@@ -1595,12 +1591,15 @@ solr-database-resync.cron = 0 15 2 * * ?
|
||||
# process-cleaner.days = 14
|
||||
|
||||
#---------------------------------------------------------------#
|
||||
#----------------GOOGLE CAPTCHA CONFIGURATION-------------------#
|
||||
#--------------------CAPTCHA CONFIGURATION----------------------#
|
||||
#---------------------------------------------------------------#
|
||||
# Enable CAPTCHA verification on ePerson registration
|
||||
|
||||
# (see modules/requestitem.cfg to enable CAPTCHA verification for request-a-copy)
|
||||
registration.verification.enabled = false
|
||||
|
||||
# Captcha provider to use, either google (default) or altcha (see modules/altcha.cfg)
|
||||
# captcha.provider = google
|
||||
|
||||
# version we want to use, possible values (v2 or v3)
|
||||
#google.recaptcha.version =
|
||||
|
||||
@@ -1624,6 +1623,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--------------------------#
|
||||
#------------------------------------------------------------------#
|
||||
@@ -1647,8 +1683,8 @@ module_dir = modules
|
||||
# However, please note that "include" statements in local.cfg will be loaded
|
||||
# PRIOR to those below (and therefore may override configs in these default
|
||||
# module configuration files).
|
||||
|
||||
include = ${module_dir}/actuator.cfg
|
||||
include = ${module_dir}/altcha.cfg
|
||||
include = ${module_dir}/altmetrics.cfg
|
||||
include = ${module_dir}/assetstore.cfg
|
||||
include = ${module_dir}/authentication.cfg
|
||||
@@ -1676,6 +1712,7 @@ include = ${module_dir}/openaire-client.cfg
|
||||
include = ${module_dir}/orcid.cfg
|
||||
include = ${module_dir}/qaevents.cfg
|
||||
include = ${module_dir}/rdf.cfg
|
||||
include = ${module_dir}/requestitem.cfg
|
||||
include = ${module_dir}/rest.cfg
|
||||
include = ${module_dir}/iiif.cfg
|
||||
include = ${module_dir}/saml-relying-party.cfg
|
||||
|
22
dspace/config/emails/orcid
Normal file
22
dspace/config/emails/orcid
Normal 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
|
39
dspace/config/emails/request_item.granted_token
Normal file
39
dspace/config/emails/request_item.granted_token
Normal file
@@ -0,0 +1,39 @@
|
||||
## Sent to the person requesting a copy of a restricted document when the
|
||||
## request is granted.
|
||||
##
|
||||
## Parameters:
|
||||
## {0} name of the requestor
|
||||
## {1} Handle URL of the requested Item
|
||||
## {2} title of the requested Item
|
||||
## {3} name of the grantor
|
||||
## {4} email address of the grantor (unused)
|
||||
## {5} custom message sent by the grantor.
|
||||
## {6} URL of the requested Item including the secure access token
|
||||
## {7} expiration date of the granted access (timestamp, formatted in requestitem.cfg)
|
||||
|
||||
#set($subject = 'Request for Copy of Restricted Document is Granted')
|
||||
Dear ${params[0]}:
|
||||
|
||||
Your request for a copy of the file(s) from the below document has been approved. The following link will grant access to the files you requested.
|
||||
|
||||
Secure link: ${params[6]}
|
||||
|
||||
${params[2]}
|
||||
${params[1]}
|
||||
|
||||
#if( $params[7] )
|
||||
|
||||
|
||||
Your access will last until ${params[7]}.
|
||||
#end
|
||||
#if( $params[5] )
|
||||
|
||||
|
||||
|
||||
An additional message from the approver follows:
|
||||
|
||||
${params[5]}
|
||||
#end
|
||||
|
||||
Best regards,
|
||||
The ${config.get('dspace.name')} Team
|
22
dspace/config/emails/validation_orcid
Normal file
22
dspace/config/emails/validation_orcid
Normal 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
|
@@ -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"/>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user