Merge remote-tracking branch 'origin/main' into task/main/CST-18963

# Conflicts:
#	dspace/config/modules/rest.cfg
This commit is contained in:
Vincenzo Mecca
2025-03-28 16:24:20 +01:00
107 changed files with 7360 additions and 684 deletions

View File

@@ -10,6 +10,8 @@ package org.dspace.access.status;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.LocalDate; import java.time.LocalDate;
import org.dspace.content.AccessStatus;
import org.dspace.content.Bitstream;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.core.Context; import org.dspace.core.Context;
@@ -21,22 +23,37 @@ public interface AccessStatusHelper {
* Calculate the access status for the item. * Calculate the access status for the item.
* *
* @param context the DSpace context * @param context the DSpace context
* @param item the item * @param item the item
* @param threshold the embargo threshold date * @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. * @throws SQLException An exception that provides information on a database access error or other errors.
*/ */
public String getAccessStatusFromItem(Context context, Item item, LocalDate threshold) public AccessStatus getAccessStatusFromItem(Context context,
throws SQLException; 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 context the DSpace context
* @param item the item to check for embargo information * @param item the item to check for embargo information
* @param threshold the embargo threshold date * @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. * @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;
} }

View File

@@ -11,7 +11,12 @@ import java.sql.SQLException;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.ZoneId; 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.access.status.service.AccessStatusService;
import org.dspace.content.AccessStatus;
import org.dspace.content.Bitstream;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.core.service.PluginService; import org.dspace.core.service.PluginService;
@@ -22,11 +27,16 @@ import org.springframework.beans.factory.annotation.Autowired;
* Implementation for the access status calculation service. * Implementation for the access status calculation service.
*/ */
public class AccessStatusServiceImpl implements AccessStatusService { public class AccessStatusServiceImpl implements AccessStatusService {
private static final Logger log = LogManager.getLogger(AccessStatusServiceImpl.class);
// Plugin implementation, set from the DSpace configuration by init(). // Plugin implementation, set from the DSpace configuration by init().
protected AccessStatusHelper helper = null; protected AccessStatusHelper helper = null;
protected LocalDate forever_date = null; protected LocalDate forever_date = null;
protected String itemCalculationType = null;
protected String bitstreamCalculationType = null;
@Autowired(required = true) @Autowired(required = true)
protected ConfigurationService configurationService; protected ConfigurationService configurationService;
@@ -59,16 +69,35 @@ public class AccessStatusServiceImpl implements AccessStatusService {
.atStartOfDay() .atStartOfDay()
.atZone(ZoneId.systemDefault()) .atZone(ZoneId.systemDefault())
.toLocalDate(); .toLocalDate();
itemCalculationType = getAccessStatusCalculationType("access.status.for-user.item");
bitstreamCalculationType = getAccessStatusCalculationType("access.status.for-user.bitstream");
} }
} }
@Override @Override
public String getAccessStatus(Context context, Item item) throws SQLException { public AccessStatus getAccessStatus(Context context, Item item) throws SQLException {
return helper.getAccessStatusFromItem(context, item, forever_date); return helper.getAccessStatusFromItem(context, item, forever_date, itemCalculationType);
} }
@Override @Override
public String getEmbargoFromItem(Context context, Item item) throws SQLException { public AccessStatus getAnonymousAccessStatus(Context context, Item item) throws SQLException {
return helper.getEmbargoFromItem(context, item, forever_date); 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;
} }
} }

View File

@@ -9,14 +9,17 @@ package org.dspace.access.status;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.ResourcePolicy;
import org.dspace.authorize.factory.AuthorizeServiceFactory; import org.dspace.authorize.factory.AuthorizeServiceFactory;
import org.dspace.authorize.service.AuthorizeService; import org.dspace.authorize.service.AuthorizeService;
import org.dspace.authorize.service.ResourcePolicyService; import org.dspace.authorize.service.ResourcePolicyService;
import org.dspace.content.AccessStatus;
import org.dspace.content.Bitstream; import org.dspace.content.Bitstream;
import org.dspace.content.Bundle; import org.dspace.content.Bundle;
import org.dspace.content.DSpaceObject; import org.dspace.content.DSpaceObject;
@@ -25,21 +28,23 @@ import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.ItemService; import org.dspace.content.service.ItemService;
import org.dspace.core.Constants; import org.dspace.core.Constants;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group; 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. * 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 methods provides a simple logic to calculate the access status
* the primary or the first bitstream in the original bundle. * of an item based on the policies of the primary or the first bitstream
* Users can override this method for enhanced functionality. * in the original bundle. Users can override those methods 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.
*/ */
public class DefaultAccessStatusHelper implements AccessStatusHelper { 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 EMBARGO = "embargo";
public static final String METADATA_ONLY = "metadata.only"; public static final String METADATA_ONLY = "metadata.only";
public static final String OPEN_ACCESS = "open.access"; public static final String OPEN_ACCESS = "open.access";
@@ -52,13 +57,15 @@ public class DefaultAccessStatusHelper implements AccessStatusHelper {
AuthorizeServiceFactory.getInstance().getResourcePolicyService(); AuthorizeServiceFactory.getInstance().getResourcePolicyService();
protected AuthorizeService authorizeService = protected AuthorizeService authorizeService =
AuthorizeServiceFactory.getInstance().getAuthorizeService(); AuthorizeServiceFactory.getInstance().getAuthorizeService();
protected GroupService groupService =
EPersonServiceFactory.getInstance().getGroupService();
public DefaultAccessStatusHelper() { public DefaultAccessStatusHelper() {
super(); 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. * It is also considering a date threshold for embargoes and restrictions.
* *
* If the item is null, simply returns the "unknown" value. * 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 context the DSpace context
* @param item the item to check for embargoes * @param item the item to check for embargoes
* @param threshold the embargo threshold date * @param threshold the embargo threshold date
* @return an access status value * @param type the type of calculation
* @return the access status
*/ */
@Override @Override
public String getAccessStatusFromItem(Context context, Item item, LocalDate threshold) public AccessStatus getAccessStatusFromItem(Context context, Item item, LocalDate threshold, String type)
throws SQLException { throws SQLException {
if (item == null) { 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. // Consider only the original bundles.
List<Bundle> bundles = item.getBundles(Constants.DEFAULT_BUNDLE_NAME); List<Bundle> bundles = item.getBundles(Constants.DEFAULT_BUNDLE_NAME);
// Check for primary bitstreams first. // Check for primary bitstreams first.
@@ -91,157 +154,159 @@ public class DefaultAccessStatusHelper implements AccessStatusHelper {
.findFirst() .findFirst()
.orElse(null); .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 the object is null, returns the "metadata.only" value.
* If any policy attached to the object is valid for the anonymous group, * If there's no availability date, returns the "open.access" value.
* returns the "open.access" value. * If the availability date is after or equal to the embargo
* Otherwise, if the policy start date is before the embargo threshold date, * threshold date, returns the "restricted" value.
* returns the "embargo" value. * Every other cases return the "embargo" value.
* Every other cases return the "restricted" value.
* *
* @param context the DSpace context * @param availabilityDate the DSpace object availability date
* @param dso the DSpace object * @param threshold the embargo threshold date
* @param threshold the embargo threshold date
* @return an access status value * @return an access status value
*/ */
private String calculateAccessStatusForDso(Context context, DSpaceObject dso, LocalDate threshold) private String getAccessStatusFromAvailabilityDate(LocalDate availabilityDate, LocalDate threshold) {
throws SQLException { // If there is no availability date, it's an open access.
if (dso == null) { if (availabilityDate == 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) {
return OPEN_ACCESS; return OPEN_ACCESS;
} }
if (embargoCount > 0 && restrictedCount == 0) { // If the policy start date have a value and if this value
return EMBARGO; // 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 EMBARGO;
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;
} }
} }

View File

@@ -9,6 +9,8 @@ package org.dspace.access.status.service;
import java.sql.SQLException; import java.sql.SQLException;
import org.dspace.content.AccessStatus;
import org.dspace.content.Bitstream;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.core.Context; 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. * Calculate the access status for an Item while considering the forever embargo date threshold.
* *
* @param context the DSpace context * @param context the DSpace context
* @param item the item * @param item the item
* @return an access status value * @return the access status
* @throws SQLException An exception that provides information on a database access error or other errors. * @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 context the DSpace context
* @param item the item to check for embargo information * @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. * @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;
} }

View File

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

View File

@@ -72,6 +72,12 @@ public class RequestItem implements ReloadableEntity<Integer> {
@Column(name = "accept_request") @Column(name = "accept_request")
private boolean 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: * Protected constructor, create object using:
* {@link org.dspace.app.requestitem.service.RequestItemService#createRequest( * {@link org.dspace.app.requestitem.service.RequestItemService#createRequest(
@@ -85,7 +91,7 @@ public class RequestItem implements ReloadableEntity<Integer> {
return requestitem_id; return requestitem_id;
} }
void setAllfiles(boolean allfiles) { public void setAllfiles(boolean allfiles) {
this.allfiles = 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() { public String getToken() {
return token; return token;
@@ -187,4 +194,38 @@ public class RequestItem implements ReloadableEntity<Integer> {
void setRequest_date(Instant request_date) { void setRequest_date(Instant request_date) {
this.request_date = 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");
}
} }

View File

@@ -10,6 +10,8 @@ package org.dspace.app.requestitem;
import java.io.IOException; import java.io.IOException;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List; import java.util.List;
import jakarta.annotation.ManagedBean; import jakarta.annotation.ManagedBean;
@@ -28,6 +30,7 @@ import org.dspace.core.Context;
import org.dspace.core.Email; import org.dspace.core.Email;
import org.dspace.core.I18nUtil; import org.dspace.core.I18nUtil;
import org.dspace.core.LogHelper; import org.dspace.core.LogHelper;
import org.dspace.core.Utils;
import org.dspace.eperson.EPerson; import org.dspace.eperson.EPerson;
import org.dspace.handle.service.HandleService; import org.dspace.handle.service.HandleService;
import org.dspace.services.ConfigurationService; import org.dspace.services.ConfigurationService;
@@ -174,9 +177,23 @@ public class RequestItemEmailNotifier {
grantorAddress = grantor.getEmail(); 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. // 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(ri.getReqName()); // {0} requestor's name
email.addArgument(handleService.getCanonicalForm(ri.getItem().getHandle())); // {1} URL of the requested Item email.addArgument(handleService.getCanonicalForm(ri.getItem().getHandle())); // {1} URL of the requested Item
email.addArgument(ri.getItem().getName()); // {2} title of the requested Item email.addArgument(ri.getItem().getName()); // {2} title of the requested Item
@@ -188,34 +205,47 @@ public class RequestItemEmailNotifier {
// Attach bitstreams. // Attach bitstreams.
try { try {
if (ri.isAccept_request()) { if (ri.isAccept_request()) {
if (ri.isAllfiles()) { if (ri.getAccess_token() != null) {
Item item = ri.getItem(); // {6} secure access link
List<Bundle> bundles = item.getBundles("ORIGINAL"); email.addArgument(configurationService.getProperty("dspace.ui.url")
for (Bundle bundle : bundles) { + "/items/" + ri.getItem().getID()
List<Bitstream> bitstreams = bundle.getBitstreams(); + "?accessToken=" + ri.getAccess_token());
for (Bitstream bitstream : bitstreams) { // {7} access end date, but only add formatted date string if it is set and not "forever"
if (!bitstream.getFormat(context).isInternal() && if (ri.getAccess_expiry() != null && !ri.getAccess_expiry().equals(Utils.getMaxTimestamp())) {
requestItemService.isRestricted(context, email.addArgument(dateTimeFormatter.format(ri.getAccess_expiry()));
bitstream)) { } else {
// #8636 Anyone receiving the email can respond to the email.addArgument(null);
// request without authenticating into DSpace
context.turnOffAuthorisationSystem();
email.addAttachment(
bitstreamService.retrieve(context, bitstream),
bitstream.getName(),
bitstream.getFormat(context).getMIMEType());
context.restoreAuthSystemState();
}
}
} }
} else { } else {
Bitstream bitstream = ri.getBitstream(); if (ri.isAllfiles()) {
// #8636 Anyone receiving the email can respond to the request without authenticating into DSpace Item item = ri.getItem();
context.turnOffAuthorisationSystem(); List<Bundle> bundles = item.getBundles("ORIGINAL");
email.addAttachment(bitstreamService.retrieve(context, bitstream), for (Bundle bundle : bundles) {
bitstream.getName(), List<Bitstream> bitstreams = bundle.getBitstreams();
bitstream.getFormat(context).getMIMEType()); for (Bitstream bitstream : bitstreams) {
context.restoreAuthSystemState(); 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(); email.send();
} else { } else {

View File

@@ -7,25 +7,40 @@
*/ */
package org.dspace.app.requestitem; package org.dspace.app.requestitem;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.sql.SQLException; import java.sql.SQLException;
import java.text.ParseException;
import java.time.DateTimeException;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; 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.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.dspace.app.requestitem.dao.RequestItemDAO; import org.dspace.app.requestitem.dao.RequestItemDAO;
import org.dspace.app.requestitem.service.RequestItemService; import org.dspace.app.requestitem.service.RequestItemService;
import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.ResourcePolicy;
import org.dspace.authorize.service.AuthorizeService; import org.dspace.authorize.service.AuthorizeService;
import org.dspace.authorize.service.ResourcePolicyService; import org.dspace.authorize.service.ResourcePolicyService;
import org.dspace.content.Bitstream; import org.dspace.content.Bitstream;
import org.dspace.content.Bundle;
import org.dspace.content.DSpaceObject; import org.dspace.content.DSpaceObject;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.core.Constants; import org.dspace.core.Constants;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.core.LogHelper; import org.dspace.core.LogHelper;
import org.dspace.core.Utils; 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; 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. * This class should never be accessed directly.
* *
* @author kevinvandevelde at atmire.com * @author kevinvandevelde at atmire.com
* @author Kim Shepherd
*/ */
public class RequestItemServiceImpl implements RequestItemService { public class RequestItemServiceImpl implements RequestItemService {
@@ -49,16 +65,43 @@ public class RequestItemServiceImpl implements RequestItemService {
@Autowired(required = true) @Autowired(required = true)
protected ResourcePolicyService resourcePolicyService; 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() { 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 @Override
public String createRequest(Context context, Bitstream bitstream, Item item, public String createRequest(Context context, Bitstream bitstream, Item item,
boolean allFiles, String reqEmail, String reqName, String reqMessage) boolean allFiles, String reqEmail, String reqName, String reqMessage)
throws SQLException { throws SQLException {
// Create an empty request item
RequestItem requestItem = requestItemDAO.create(context, new RequestItem()); RequestItem requestItem = requestItemDAO.create(context, new RequestItem());
// Set values of the request item based on supplied parameters
requestItem.setToken(Utils.generateHexKey()); requestItem.setToken(Utils.generateHexKey());
requestItem.setBitstream(bitstream); requestItem.setBitstream(bitstream);
requestItem.setItem(item); requestItem.setItem(item);
@@ -68,10 +111,56 @@ public class RequestItemServiceImpl implements RequestItemService {
requestItem.setReqMessage(reqMessage); requestItem.setReqMessage(reqMessage);
requestItem.setRequest_date(Instant.now()); 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); requestItemDAO.save(context, requestItem);
log.debug("Created RequestItem with ID {} and token {}", log.debug("Created RequestItem with ID {}, approval token {}, access token {}, access expiry {}",
requestItem::getID, requestItem::getToken); requestItem::getID, requestItem::getToken, requestItem::getAccess_token, requestItem::getAccess_expiry);
// Return the approver token
return requestItem.getToken(); return requestItem.getToken();
} }
@@ -128,4 +217,186 @@ public class RequestItemServiceImpl implements RequestItemService {
} }
return true; 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();
}
}
} }

View File

@@ -26,7 +26,7 @@ import org.dspace.core.GenericDAO;
*/ */
public interface RequestItemDAO extends GenericDAO<RequestItem> { 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 context the current DSpace context.
* @param token uniquely identifies the request. * @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; 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; public Iterator<RequestItem> findByItem(Context context, Item item) throws SQLException;
} }

View File

@@ -42,6 +42,17 @@ public class RequestItemDAOImpl extends AbstractHibernateDAO<RequestItem> implem
criteriaQuery.where(criteriaBuilder.equal(requestItemRoot.get(RequestItem_.token), token)); criteriaQuery.where(criteriaBuilder.equal(requestItemRoot.get(RequestItem_.token), token));
return uniqueResult(context, criteriaQuery, false, RequestItem.class); 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 @Override
public Iterator<RequestItem> findByItem(Context context, Item item) throws SQLException { public Iterator<RequestItem> findByItem(Context context, Item item) throws SQLException {
CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);

View File

@@ -8,13 +8,19 @@
/** /**
* Feature for conveying a request that materials forbidden to the requester * Feature for conveying a request that materials forbidden to the requester
* by resource policy be made available by other means. The request will be * by resource policy be made available by other means.
* e-mailed to a responsible party for consideration and action. Find details *
* in the user documentation under the rubric "Request a Copy". * 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 * <p>Mailing is handled by {@link RequestItemEmailNotifier}. Responsible
* parties are represented by {@link RequestItemAuthor} * 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 * <p>This package includes several "strategy" classes which discover
* responsible parties in various ways. See * responsible parties in various ways. See
* {@link RequestItemSubmitterStrategy} and the classes which extend it, and * {@link RequestItemSubmitterStrategy} and the classes which extend it, and

View File

@@ -7,11 +7,15 @@
*/ */
package org.dspace.app.requestitem.service; package org.dspace.app.requestitem.service;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.Instant;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import org.dspace.app.requestitem.RequestItem; import org.dspace.app.requestitem.RequestItem;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Bitstream; import org.dspace.content.Bitstream;
import org.dspace.content.DSpaceObject; import org.dspace.content.DSpaceObject;
import org.dspace.content.Item; import org.dspace.content.Item;
@@ -23,6 +27,7 @@ import org.dspace.core.Context;
* for the RequestItem object and is autowired by Spring. * for the RequestItem object and is autowired by Spring.
* *
* @author kevinvandevelde at atmire.com * @author kevinvandevelde at atmire.com
* @author Kim Shepherd
*/ */
public interface RequestItemService { public interface RequestItemService {
@@ -40,7 +45,7 @@ public interface RequestItemService {
* @return the token of the request item * @return the token of the request item
* @throws SQLException if database error * @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) boolean allFiles, String reqEmail, String reqName, String reqMessage)
throws SQLException; throws SQLException;
@@ -49,35 +54,46 @@ public interface RequestItemService {
* *
* @param context current DSpace session. * @param context current DSpace session.
* @return all item requests. * @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; throws SQLException;
/** /**
* Retrieve a request by its token. * Retrieve a request by its approver token.
* *
* @param context current DSpace session. * @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. * @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. * Retrieve a request based on the item.
* @param context current DSpace session. * @param context current DSpace session.
* @param item the item to find requests for. * @param item the item to find requests for.
* @return the matching requests, or null if not found. * @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 context The relevant DSpace Context.
* @param requestItem requested item * @param requestItem requested item
*/ */
public void update(Context context, RequestItem requestItem); void update(Context context, RequestItem requestItem);
/** /**
* Remove the record from the database. * Remove the record from the database.
@@ -85,7 +101,7 @@ public interface RequestItemService {
* @param context current DSpace context. * @param context current DSpace context.
* @param request record to be removed. * @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? * 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. * @return true if a READ policy applies.
* @throws SQLException passed through. * @throws SQLException passed through.
*/ */
public boolean isRestricted(Context context, DSpaceObject o) boolean isRestricted(Context context, DSpaceObject o)
throws SQLException; 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);
} }

View File

@@ -29,7 +29,10 @@ import org.dspace.authorize.AuthorizeException;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.eperson.EPerson; import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group; 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.EPersonService;
import org.dspace.eperson.service.RegistrationDataService;
import org.dspace.orcid.OrcidToken; import org.dspace.orcid.OrcidToken;
import org.dspace.orcid.client.OrcidClient; import org.dspace.orcid.client.OrcidClient;
import org.dspace.orcid.client.OrcidConfiguration; import org.dspace.orcid.client.OrcidConfiguration;
@@ -47,11 +50,15 @@ import org.springframework.beans.factory.annotation.Autowired;
* ORCID authentication for DSpace. * ORCID authentication for DSpace.
* *
* @author Luca Giamminonni (luca.giamminonni at 4science.it) * @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/ */
public class OrcidAuthenticationBean implements AuthenticationMethod { 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_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(); private final static Logger LOGGER = LogManager.getLogger();
@@ -78,6 +85,9 @@ public class OrcidAuthenticationBean implements AuthenticationMethod {
@Autowired @Autowired
private OrcidTokenService orcidTokenService; private OrcidTokenService orcidTokenService;
@Autowired
private RegistrationDataService registrationDataService;
@Override @Override
public int authenticate(Context context, String username, String password, String realm, HttpServletRequest request) public int authenticate(Context context, String username, String password, String realm, HttpServletRequest request)
throws SQLException { throws SQLException {
@@ -183,7 +193,7 @@ public class OrcidAuthenticationBean implements AuthenticationMethod {
return ePerson.canLogIn() ? logInEPerson(context, token, ePerson) : BAD_ARGS; 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 { try {
context.turnOffAuthorisationSystem(); context.turnOffAuthorisationSystem();
String email = getEmail(person) RegistrationData registrationData =
.orElseThrow(() -> new IllegalStateException("The email is configured private on orcid")); 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); request.setAttribute(ORCID_REGISTRATION_TOKEN, registrationData.getToken());
context.commit();
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);
context.dispatchEvents(); context.dispatchEvents();
return SUCCESS;
} catch (Exception ex) { } catch (Exception ex) {
LOGGER.error("An error occurs registering a new EPerson from ORCID", ex); LOGGER.error("An error occurs registering a new EPerson from ORCID", ex);
context.rollback(); context.rollback();
return NO_SUCH_USER;
} finally { } finally {
context.restoreAuthSystemState(); 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()); return Optional.ofNullable(emails.get(0).getEmail());
} }
private Optional<String> getFirstName(Person person) { private String getFirstName(Person person) {
return Optional.ofNullable(person.getName()) return Optional.ofNullable(person.getName())
.map(name -> name.getGivenNames()) .map(name -> name.getGivenNames())
.map(givenNames -> givenNames.getContent()); .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()) return Optional.ofNullable(person.getName())
.map(name -> name.getFamilyName()) .map(name -> name.getFamilyName())
.map(givenNames -> givenNames.getContent()); .map(givenNames -> givenNames.getContent())
.filter(StringUtils::isNotBlank)
.orElse(ORCID_DEFAULT_LASTNAME);
} }
private boolean canSelfRegister() { private boolean canSelfRegister() {

View File

@@ -0,0 +1,64 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.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;
}
}

View File

@@ -24,6 +24,8 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.text.ParseException; import java.text.ParseException;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException; import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalAccessor;
@@ -478,4 +480,24 @@ public final class Utils {
ConfigurationService config = DSpaceServicesFactory.getInstance().getConfigurationService(); ConfigurationService config = DSpaceServicesFactory.getInstance().getConfigurationService();
return StringSubstitutor.replace(string, config.getProperties()); 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);
}
} }

View File

@@ -13,6 +13,7 @@ import org.apache.solr.common.SolrInputDocument;
import org.dspace.access.status.DefaultAccessStatusHelper; import org.dspace.access.status.DefaultAccessStatusHelper;
import org.dspace.access.status.factory.AccessStatusServiceFactory; import org.dspace.access.status.factory.AccessStatusServiceFactory;
import org.dspace.access.status.service.AccessStatusService; import org.dspace.access.status.service.AccessStatusService;
import org.dspace.content.AccessStatus;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.discovery.indexobject.IndexableItem; import org.dspace.discovery.indexobject.IndexableItem;
@@ -61,6 +62,7 @@ public class SolrServiceIndexAccessStatusPlugin implements SolrServiceIndexPlugi
UNKNOWN = "unknown" UNKNOWN = "unknown"
*/ */
private String retrieveItemAccessStatus(Context context, Item item) throws SQLException { private String retrieveItemAccessStatus(Context context, Item item) throws SQLException {
return accessStatusService.getAccessStatus(context, item); AccessStatus accessStatus = accessStatusService.getAccessStatus(context, item);
return accessStatus.getStatus();
} }
} }

View File

@@ -25,6 +25,7 @@ import org.dspace.app.ldn.service.LDNMessageService;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.content.service.ItemService; import org.dspace.content.service.ItemService;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.util.SolrUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
/** /**
@@ -146,7 +147,7 @@ public class LDNMessageEntityIndexFactoryImpl extends IndexFactoryImpl<Indexable
ZoneOffset.UTC)); ZoneOffset.UTC));
addFacetIndex(doc, "queue_last_start_time", value, value); addFacetIndex(doc, "queue_last_start_time", value, value);
doc.addField("queue_last_start_time", 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", value);
doc.addField("queue_last_start_time_min_sort", value); doc.addField("queue_last_start_time_min_sort", value);
doc.addField("queue_last_start_time_max", value); doc.addField("queue_last_start_time_max", value);

View File

@@ -9,22 +9,36 @@ package org.dspace.eperson;
import java.io.IOException; import java.io.IOException;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.List;
import java.util.Locale; 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 jakarta.mail.MessagingException;
import org.apache.commons.lang.StringUtils;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.dspace.authenticate.service.AuthenticationService; import org.dspace.authenticate.service.AuthenticationService;
import org.dspace.authorize.AuthorizeException; 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.Context;
import org.dspace.core.Email; import org.dspace.core.Email;
import org.dspace.core.I18nUtil; import org.dspace.core.I18nUtil;
import org.dspace.core.Utils; import org.dspace.core.Utils;
import org.dspace.eperson.dto.RegistrationDataPatch;
import org.dspace.eperson.service.AccountService; import org.dspace.eperson.service.AccountService;
import org.dspace.eperson.service.EPersonService; import org.dspace.eperson.service.EPersonService;
import org.dspace.eperson.service.GroupService;
import org.dspace.eperson.service.RegistrationDataService; import org.dspace.eperson.service.RegistrationDataService;
import org.dspace.services.ConfigurationService; import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.log.LogMessage;
/** /**
* Methods for handling registration by email and forgotten passwords. When * Methods for handling registration by email and forgotten passwords. When
@@ -45,16 +59,30 @@ public class AccountServiceImpl implements AccountService {
* log4j log * log4j log
*/ */
private static final Logger log = LogManager.getLogger(AccountServiceImpl.class); 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) @Autowired(required = true)
protected EPersonService ePersonService; protected EPersonService ePersonService;
@Autowired(required = true) @Autowired(required = true)
protected RegistrationDataService registrationDataService; protected RegistrationDataService registrationDataService;
@Autowired @Autowired
private ConfigurationService configurationService; private ConfigurationService configurationService;
@Autowired
private GroupService groupService;
@Autowired @Autowired
private AuthenticationService authenticationService; private AuthenticationService authenticationService;
@Autowired
private MetadataValueService metadataValueService;
protected AccountServiceImpl() { protected AccountServiceImpl() {
} }
@@ -86,7 +114,7 @@ public class AccountServiceImpl implements AccountService {
if (!authenticationService.canSelfRegister(context, null, email)) { if (!authenticationService.canSelfRegister(context, null, email)) {
throw new IllegalStateException("self registration is not allowed with this email address"); 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 @Override
public void sendForgotPasswordInfo(Context context, String email) public void sendForgotPasswordInfo(Context context, String email)
throws SQLException, IOException, MessagingException, throws SQLException, IOException, MessagingException, AuthorizeException {
AuthorizeException { sendInfo(context, email, RegistrationTypeEnum.FORGOT, true);
sendInfo(context, email, false, 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); 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 * THIS IS AN INTERNAL METHOD. THE SEND PARAMETER ALLOWS IT TO BE USED FOR
* TESTING PURPOSES. * TESTING PURPOSES.
@@ -191,8 +502,7 @@ public class AccountServiceImpl implements AccountService {
* *
* @param context DSpace context * @param context DSpace context
* @param email Email address to send the forgot-password email to * @param email Email address to send the forgot-password email to
* @param isRegister If true, this is for registration; otherwise, it is * @param type Type of registration {@link RegistrationTypeEnum}
* for forgot-password
* @param send If true, send email; otherwise do not send any email * @param send If true, send email; otherwise do not send any email
* @return null if no EPerson with that email found * @return null if no EPerson with that email found
* @throws SQLException Cannot create registration data in database * @throws SQLException Cannot create registration data in database
@@ -200,16 +510,17 @@ public class AccountServiceImpl implements AccountService {
* @throws IOException Error reading email template * @throws IOException Error reading email template
* @throws AuthorizeException Authorization error * @throws AuthorizeException Authorization error
*/ */
protected RegistrationData sendInfo(Context context, String email, protected RegistrationData sendInfo(
boolean isRegister, boolean send) throws SQLException, IOException, Context context, String email, RegistrationTypeEnum type, boolean send
MessagingException, AuthorizeException { ) throws SQLException, IOException, MessagingException, AuthorizeException {
// See if a registration token already exists for this user // 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 it already exists, just re-issue it
if (rd == null) { if (rd == null) {
rd = registrationDataService.create(context); rd = registrationDataService.create(context);
rd.setRegistrationType(type);
rd.setToken(Utils.generateHexKey()); rd.setToken(Utils.generateHexKey());
// don't set expiration date any more // don't set expiration date any more
@@ -229,7 +540,7 @@ public class AccountServiceImpl implements AccountService {
} }
if (send) { if (send) {
sendEmail(context, email, isRegister, rd); fillAndSendEmail(context, email, isRegister, rd);
} }
return 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 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. * @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 { throws MessagingException, IOException, SQLException {
String base = configurationService.getProperty("dspace.ui.url"); String base = configurationService.getProperty("dspace.ui.url");
@@ -261,11 +572,9 @@ public class AccountServiceImpl implements AccountService {
.append(rd.getToken()) .append(rd.getToken())
.toString(); .toString();
Locale locale = context.getCurrentLocale(); Locale locale = context.getCurrentLocale();
Email bean = Email.getEmail(I18nUtil.getEmailFilename(locale, isRegister ? "register" String emailFilename = I18nUtil.getEmailFilename(locale, isRegister ? "register" : "change_password");
: "change_password"));
bean.addRecipient(email); fillAndSendEmail(email, emailFilename, specialLink);
bean.addArgument(specialLink);
bean.send();
// Breadcrumbs // Breadcrumbs
if (log.isInfoEnabled()) { if (log.isInfoEnabled()) {
@@ -273,4 +582,64 @@ public class AccountServiceImpl implements AccountService {
+ " information to " + email); + " information to " + email);
} }
} }
/**
* This method returns a link that will point to the Angular UI that will be used by the user to complete the
* registration process.
*
* @param base is the UI url of DSpace
* @param rd is the RegistrationData related to the user
* @param subPath is the specific page that will be loaded on the FE
* @return String that represents that link
*/
private static String getSpecialLink(String base, RegistrationData rd, String subPath) {
return new StringBuffer(base)
.append(base.endsWith("/") ? "" : "/")
.append(subPath)
.append("/")
.append(rd.getToken())
.toString();
}
/**
* Fills out a given email template obtained starting from the {@link RegistrationTypeEnum}.
*
* @param context The DSpace Context
* @param rd The RegistrationData that will be used as a registration.
* @throws MessagingException
* @throws IOException
*/
protected void sendRegistationLinkByEmail(
Context context, RegistrationData rd
) throws MessagingException, IOException {
String base = configurationService.getProperty("dspace.ui.url");
// Note change from "key=" to "token="
String specialLink = getSpecialLink(base, rd, rd.getRegistrationType().getLink());
String emailFilename = I18nUtil.getEmailFilename(
context.getCurrentLocale(), rd.getRegistrationType().toString().toLowerCase()
);
fillAndSendEmail(rd.getEmail(), emailFilename, specialLink);
log.info(LogMessage.of(() -> "Sent " + rd.getRegistrationType().getLink() + " link to " + rd.getEmail()));
}
/**
* This method fills out the given email with all the fields and sends out the email.
*
* @param email - The recipient
* @param emailFilename The name of the email
* @param specialLink - The link that will be set inside the email
* @throws IOException
* @throws MessagingException
*/
protected void fillAndSendEmail(String email, String emailFilename, String specialLink)
throws IOException, MessagingException {
Email bean = Email.getEmail(emailFilename);
bean.addRecipient(email);
bean.addArgument(specialLink);
bean.send();
}
} }

View File

@@ -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;
}
}

View File

@@ -8,16 +8,24 @@
package org.dspace.eperson; package org.dspace.eperson;
import java.time.Instant; import java.time.Instant;
import java.util.SortedSet;
import java.util.TreeSet;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType; import jakarta.persistence.GenerationType;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.SequenceGenerator; import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.core.ReloadableEntity; import org.dspace.core.ReloadableEntity;
import org.hibernate.annotations.SortNatural;
/** /**
* Database entity representation of the registrationdata table * 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) @SequenceGenerator(name = "registrationdata_seq", sequenceName = "registrationdata_seq", allocationSize = 1)
private Integer id; 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; private String email;
/**
* Contains the unique id generated fot the user.
*/
@Column(name = "token", length = 48) @Column(name = "token", length = 48)
private String token; private String token;
/**
* Expiration date of this registration data.
*/
@Column(name = "expires") @Column(name = "expires")
private Instant 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: * Protected constructor, create object using:
* {@link org.dspace.eperson.service.RegistrationDataService#create(Context)} * {@link org.dspace.eperson.service.RegistrationDataService#create(Context)}
*/ */
protected RegistrationData() { 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() { public Integer getID() {
@@ -59,7 +111,7 @@ public class RegistrationData implements ReloadableEntity<Integer> {
return email; return email;
} }
void setEmail(String email) { public void setEmail(String email) {
this.email = email; this.email = email;
} }
@@ -78,4 +130,24 @@ public class RegistrationData implements ReloadableEntity<Integer> {
void setExpires(Instant expires) { void setExpires(Instant expires) {
this.expires = expires; this.expires = expires;
} }
public RegistrationTypeEnum getRegistrationType() {
return registrationType;
}
public void setRegistrationType(RegistrationTypeEnum registrationType) {
this.registrationType = registrationType;
}
public SortedSet<RegistrationDataMetadata> getMetadata() {
return metadata;
}
public void setMetadata(SortedSet<RegistrationDataMetadata> metadata) {
this.metadata = metadata;
}
public String getNetId() {
return netId;
}
} }

View File

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

View File

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

View File

@@ -0,0 +1,90 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.eperson;
import java.sql.SQLException;
import java.util.List;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.MetadataField;
import org.dspace.content.service.MetadataFieldService;
import org.dspace.core.Context;
import org.dspace.eperson.dao.RegistrationDataMetadataDAO;
import org.dspace.eperson.service.RegistrationDataMetadataService;
import org.springframework.beans.factory.annotation.Autowired;
/**
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
**/
public class RegistrationDataMetadataServiceImpl implements RegistrationDataMetadataService {
@Autowired
private RegistrationDataMetadataDAO registrationDataMetadataDAO;
@Autowired
private MetadataFieldService metadataFieldService;
@Override
public RegistrationDataMetadata create(Context context, RegistrationData registrationData, String schema,
String element, String qualifier, String value) throws SQLException {
return create(
context, registrationData,
metadataFieldService.findByElement(context, schema, element, qualifier),
value
);
}
@Override
public RegistrationDataMetadata create(Context context, RegistrationData registrationData,
MetadataField metadataField) throws SQLException {
RegistrationDataMetadata metadata = new RegistrationDataMetadata();
metadata.setRegistrationData(registrationData);
metadata.setMetadataField(metadataField);
return registrationDataMetadataDAO.create(context, metadata);
}
@Override
public RegistrationDataMetadata create(
Context context, RegistrationData registrationData, MetadataField metadataField, String value
) throws SQLException {
RegistrationDataMetadata metadata = new RegistrationDataMetadata();
metadata.setRegistrationData(registrationData);
metadata.setMetadataField(metadataField);
metadata.setValue(value);
return registrationDataMetadataDAO.create(context, metadata);
}
@Override
public RegistrationDataMetadata create(Context context) throws SQLException, AuthorizeException {
return registrationDataMetadataDAO.create(context, new RegistrationDataMetadata());
}
@Override
public RegistrationDataMetadata find(Context context, int id) throws SQLException {
return registrationDataMetadataDAO.findByID(context, RegistrationDataMetadata.class, id);
}
@Override
public void update(Context context, RegistrationDataMetadata registrationDataMetadata)
throws SQLException, AuthorizeException {
registrationDataMetadataDAO.save(context, registrationDataMetadata);
}
@Override
public void update(Context context, List<RegistrationDataMetadata> t) throws SQLException, AuthorizeException {
for (RegistrationDataMetadata registrationDataMetadata : t) {
update(context, registrationDataMetadata);
}
}
@Override
public void delete(Context context, RegistrationDataMetadata registrationDataMetadata)
throws SQLException, AuthorizeException {
registrationDataMetadataDAO.delete(context, registrationDataMetadata);
}
}

View File

@@ -8,13 +8,26 @@
package org.dspace.eperson; package org.dspace.eperson;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.Instant;
import java.util.Collections; import java.util.Collections;
import java.util.List; 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.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.dspace.authorize.AuthorizeException; 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.Context;
import org.dspace.core.Utils;
import org.dspace.eperson.dao.RegistrationDataDAO; 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.dspace.eperson.service.RegistrationDataService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -26,19 +39,67 @@ import org.springframework.beans.factory.annotation.Autowired;
* @author kevinvandevelde at atmire.com * @author kevinvandevelde at atmire.com
*/ */
public class RegistrationDataServiceImpl implements RegistrationDataService { public class RegistrationDataServiceImpl implements RegistrationDataService {
@Autowired(required = true) @Autowired()
protected RegistrationDataDAO registrationDataDAO; protected RegistrationDataDAO registrationDataDAO;
@Autowired()
protected RegistrationDataMetadataService registrationDataMetadataService;
@Autowired()
protected MetadataFieldService metadataFieldService;
protected RegistrationDataExpirationConfiguration expirationConfiguration =
RegistrationDataExpirationConfiguration.getInstance();
protected RegistrationDataServiceImpl() { protected RegistrationDataServiceImpl() {
} }
@Override @Override
public RegistrationData create(Context context) throws SQLException, AuthorizeException { 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 @Override
public RegistrationData findByToken(Context context, String token) throws SQLException { public RegistrationData findByToken(Context context, String token) throws SQLException {
return registrationDataDAO.findByToken(context, token); return registrationDataDAO.findByToken(context, token);
@@ -49,12 +110,124 @@ public class RegistrationDataServiceImpl implements RegistrationDataService {
return registrationDataDAO.findByEmail(context, email); return registrationDataDAO.findByEmail(context, email);
} }
@Override
public RegistrationData findBy(Context context, String email, RegistrationTypeEnum type) throws SQLException {
return registrationDataDAO.findBy(context, email, type);
}
@Override @Override
public void deleteByToken(Context context, String token) throws SQLException { public void deleteByToken(Context context, String token) throws SQLException {
registrationDataDAO.deleteByToken(context, token); 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 @Override
public RegistrationData find(Context context, int id) throws SQLException { public RegistrationData find(Context context, int id) throws SQLException {
return registrationDataDAO.findByID(context, RegistrationData.class, id); 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 @Override
public void delete(Context context, RegistrationData registrationData) throws SQLException, AuthorizeException { public void delete(Context context, RegistrationData registrationData) throws SQLException, AuthorizeException {
registrationDataDAO.delete(context, registrationData); registrationDataDAO.delete(context, registrationData);
} }
@Override
public void deleteExpiredRegistrations(Context context) throws SQLException {
registrationDataDAO.deleteExpiredBy(context, Instant.now());
}
@Override
public boolean isValid(RegistrationData rd) {
return rd.getExpires() == null || rd.getExpires().isAfter(Instant.now());
}
} }

View File

@@ -0,0 +1,33 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.eperson;
/**
* External provider allowed to register e-persons stored with {@link RegistrationData}
*
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
**/
public enum RegistrationTypeEnum {
ORCID("external-login"),
VALIDATION_ORCID("review-account"),
FORGOT("forgot"),
REGISTER("register"),
INVITATION("invitation"),
CHANGE_PASSWORD("change-password");
private final String link;
RegistrationTypeEnum(String link) {
this.link = link;
}
public String getLink() {
return link;
}
}

View File

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

View File

@@ -0,0 +1,22 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.eperson.dao;
import org.dspace.core.GenericDAO;
import org.dspace.eperson.RegistrationDataMetadata;
/**
* Database Access Object interface class for the {@link org.dspace.eperson.RegistrationDataMetadata} object.
* The implementation of this class is responsible for all database calls for the RegistrationData object and is
* autowired by spring
*
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
**/
public interface RegistrationDataMetadataDAO extends GenericDAO<RegistrationDataMetadata> {
}

View File

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

View File

@@ -0,0 +1,19 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.eperson.dao.impl;
import org.dspace.core.AbstractHibernateDAO;
import org.dspace.eperson.RegistrationDataMetadata;
import org.dspace.eperson.dao.RegistrationDataMetadataDAO;
/**
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
**/
public class RegistrationDataMetadataDAOImpl extends AbstractHibernateDAO<RegistrationDataMetadata>
implements RegistrationDataMetadataDAO {
}

View File

@@ -0,0 +1,64 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.eperson.dto;
import org.dspace.eperson.RegistrationTypeEnum;
/**
* Class that embeds a change done for the {@link org.dspace.eperson.RegistrationData}
*
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
**/
public class RegistrationDataChanges {
@SuppressWarnings("checkstyle:LineLength")
private static final String EMAIL_PATTERN = "^[a-zA-Z0-9.!#$%&'*+\\\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$";
private final String email;
private final RegistrationTypeEnum registrationType;
public RegistrationDataChanges(String email, RegistrationTypeEnum type) {
if (email == null || email.trim().isBlank()) {
throw new IllegalArgumentException("Cannot update with an empty email address");
}
if (type == null) {
throw new IllegalArgumentException("Cannot update with a null registration type");
}
this.email = email;
if (!isValidEmail()) {
throw new IllegalArgumentException("Invalid email address provided!");
}
this.registrationType = type;
}
/**
* Checks if the email is valid using the EMAIL_PATTERN.
* @return true if valid, false otherwise
*/
public boolean isValidEmail() {
return email.matches(EMAIL_PATTERN);
}
/**
* Returns the email of change.
*
* @return the email of the change
*/
public String getEmail() {
return email;
}
/**
* Returns the {@link RegistrationTypeEnum} of the registration.
*
* @return the type of the change
*/
public RegistrationTypeEnum getRegistrationType() {
return registrationType;
}
}

View File

@@ -0,0 +1,44 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.eperson.dto;
import org.dspace.eperson.RegistrationData;
/**
* This POJO encapsulates the details of the PATCH request that updates the {@link RegistrationData}.
*
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
**/
public class RegistrationDataPatch {
private final RegistrationData oldRegistration;
private final RegistrationDataChanges changes;
public RegistrationDataPatch(RegistrationData oldRegistration, RegistrationDataChanges changes) {
this.oldRegistration = oldRegistration;
this.changes = changes;
}
/**
* Returns the value of the previous registration
*
* @return RegistrationData
*/
public RegistrationData getOldRegistration() {
return oldRegistration;
}
/**
* Returns the changes related to the registration
*
* @return RegistrationDataChanges
*/
public RegistrationDataChanges getChanges() {
return changes;
}
}

View File

@@ -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);
}
}

View File

@@ -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
}
}

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,9 @@
*/ */
package org.dspace.eperson.service; package org.dspace.eperson.service;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.dspace.eperson.InvalidReCaptchaException; import org.dspace.eperson.InvalidReCaptchaException;
/** /**
@@ -27,4 +30,30 @@ public interface CaptchaService {
*/ */
public void processResponse(String response, String action) throws InvalidReCaptchaException; 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);
}
} }

View File

@@ -0,0 +1,66 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.eperson.service;
import java.sql.SQLException;
import org.dspace.content.MetadataField;
import org.dspace.core.Context;
import org.dspace.eperson.RegistrationData;
import org.dspace.eperson.RegistrationDataMetadata;
import org.dspace.service.DSpaceCRUDService;
/**
* This class contains business-logic to handle {@link RegistrationDataMetadata}.
*
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
**/
public interface RegistrationDataMetadataService extends DSpaceCRUDService<RegistrationDataMetadata> {
/**
* Creates a new {@link RegistrationDataMetadata} that will be stored starting from the parameters of the method.
*
* @param context - the DSpace Context
* @param registrationData - the Registration data that will contain the metadata
* @param schema - the schema of the metadata field
* @param element - the element of the metadata field
* @param qualifier - the qualifier of the metadata field
* @param value - the value of that metadata
* @return the newly created RegistrationDataMetadata
* @throws SQLException
*/
RegistrationDataMetadata create(Context context, RegistrationData registrationData, String schema,
String element, String qualifier, String value) throws SQLException;
/**
* Creates a new {@link RegistrationDataMetadata}
*
* @param context - the DSpace Context
* @param registrationData - the RegistrationData that will contain that metadata
* @param metadataField - the metadataField
* @return the newly created RegistrationDataMetadata
* @throws SQLException
*/
RegistrationDataMetadata create(
Context context, RegistrationData registrationData, MetadataField metadataField
) throws SQLException;
/**
* Creates a new {@link RegistrationDataMetadata}
*
* @param context - the DSpace Context
* @param registrationData - the RegistrationData that will contain that metadata
* @param metadataField - the metadataField that will be stored
* @param value - the value that will be placed inside the RegistrationDataMetadata
* @return the newly created {@link RegistrationDataMetadata}
* @throws SQLException
*/
RegistrationDataMetadata create(
Context context, RegistrationData registrationData, MetadataField metadataField, String value
) throws SQLException;
}

View File

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

View File

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

View File

@@ -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;

View File

@@ -0,0 +1,46 @@
--
-- The contents of this file are subject to the license and copyright
-- detailed in the LICENSE and NOTICE files at the root of the source
-- tree and available online at
--
-- http://www.dspace.org/license/
--
-----------------------------------------------------------------------------------
-- ALTER table registrationdata
-----------------------------------------------------------------------------------
EXECUTE IMMEDIATE 'ALTER TABLE registrationdata DROP CONSTRAINT ' ||
QUOTE_IDENT((SELECT CONSTRAINT_NAME
FROM information_schema.key_column_usage
WHERE TABLE_SCHEMA = 'PUBLIC' AND TABLE_NAME = 'REGISTRATIONDATA' AND COLUMN_NAME = 'EMAIL'));
ALTER TABLE registrationdata
ADD COLUMN registration_type VARCHAR2(255);
ALTER TABLE registrationdata
ADD COLUMN net_id VARCHAR2(64);
CREATE SEQUENCE IF NOT EXISTS registrationdata_metadatavalue_seq START WITH 1 INCREMENT BY 1;
-----------------------------------------------------------------------------------
-- Creates table registrationdata_metadata
-----------------------------------------------------------------------------------
CREATE TABLE registrationdata_metadata (
registrationdata_metadata_id INTEGER NOT NULL,
registrationdata_id INTEGER,
metadata_field_id INTEGER,
text_value CLOB,
CONSTRAINT pk_registrationdata_metadata PRIMARY KEY (registrationdata_metadata_id)
);
ALTER TABLE registrationdata_metadata
ADD CONSTRAINT FK_REGISTRATIONDATA_METADATA_ON_METADATA_FIELD
FOREIGN KEY (metadata_field_id)
REFERENCES metadatafieldregistry (metadata_field_id) ON DELETE CASCADE;
ALTER TABLE registrationdata_metadata
ADD CONSTRAINT FK_REGISTRATIONDATA_METADATA_ON_REGISTRATIONDATA
FOREIGN KEY (registrationdata_id)
REFERENCES registrationdata (registrationdata_id) ON DELETE CASCADE;

View File

@@ -0,0 +1,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;

View File

@@ -0,0 +1,52 @@
--
-- The contents of this file are subject to the license and copyright
-- detailed in the LICENSE and NOTICE files at the root of the source
-- tree and available online at
--
-- http://www.dspace.org/license/
--
-----------------------------------------------------------------------------------
-- ALTER table registrationdata
-----------------------------------------------------------------------------------
DO $$
BEGIN
EXECUTE 'ALTER TABLE registrationdata DROP CONSTRAINT IF EXISTS ' ||
QUOTE_IDENT((
SELECT CONSTRAINT_NAME
FROM information_schema.key_column_usage
WHERE TABLE_NAME = 'registrationdata' AND COLUMN_NAME = 'email'
));
end
$$;
ALTER TABLE registrationdata
ADD COLUMN registration_type VARCHAR(255);
ALTER TABLE registrationdata
ADD COLUMN net_id VARCHAR(64);
CREATE SEQUENCE IF NOT EXISTS registrationdata_metadatavalue_seq START WITH 1 INCREMENT BY 1;
-----------------------------------------------------------------------------------
-- Creates table registrationdata_metadata
-----------------------------------------------------------------------------------
CREATE TABLE registrationdata_metadata (
registrationdata_metadata_id INTEGER NOT NULL,
registrationdata_id INTEGER,
metadata_field_id INTEGER,
text_value TEXT,
CONSTRAINT pk_registrationdata_metadata PRIMARY KEY (registrationdata_metadata_id)
);
ALTER TABLE registrationdata_metadata
ADD CONSTRAINT FK_REGISTRATIONDATA_METADATA_ON_METADATA_FIELD
FOREIGN KEY (metadata_field_id)
REFERENCES metadatafieldregistry (metadata_field_id) ON DELETE CASCADE;
ALTER TABLE registrationdata_metadata
ADD CONSTRAINT FK_REGISTRATIONDATA_METADATA_ON_REGISTRATIONDATA
FOREIGN KEY (registrationdata_id)
REFERENCES registrationdata (registrationdata_id) ON DELETE CASCADE;

View File

@@ -0,0 +1,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>

View File

@@ -8,24 +8,35 @@
package org.dspace.access.status; package org.dspace.access.status;
import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail; 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.sql.SQLException;
import java.time.LocalDate;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.dspace.AbstractUnitTest; import org.dspace.AbstractUnitTest;
import org.dspace.access.status.factory.AccessStatusServiceFactory; import org.dspace.access.status.factory.AccessStatusServiceFactory;
import org.dspace.access.status.service.AccessStatusService; import org.dspace.access.status.service.AccessStatusService;
import org.dspace.authorize.AuthorizeException; 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.Collection;
import org.dspace.content.Community; import org.dspace.content.Community;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.content.factory.ContentServiceFactory; 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.CollectionService;
import org.dspace.content.service.CommunityService; import org.dspace.content.service.CommunityService;
import org.dspace.content.service.InstallItemService; import org.dspace.content.service.InstallItemService;
import org.dspace.content.service.ItemService; import org.dspace.content.service.ItemService;
import org.dspace.content.service.WorkspaceItemService; import org.dspace.content.service.WorkspaceItemService;
import org.dspace.core.Constants;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@@ -40,6 +51,8 @@ public class AccessStatusServiceTest extends AbstractUnitTest {
private Collection collection; private Collection collection;
private Community owningCommunity; private Community owningCommunity;
private Item item; private Item item;
private Bundle bundle;
private Bitstream bitstream;
protected CommunityService communityService = protected CommunityService communityService =
ContentServiceFactory.getInstance().getCommunityService(); ContentServiceFactory.getInstance().getCommunityService();
@@ -47,6 +60,10 @@ public class AccessStatusServiceTest extends AbstractUnitTest {
ContentServiceFactory.getInstance().getCollectionService(); ContentServiceFactory.getInstance().getCollectionService();
protected ItemService itemService = protected ItemService itemService =
ContentServiceFactory.getInstance().getItemService(); ContentServiceFactory.getInstance().getItemService();
protected BundleService bundleService =
ContentServiceFactory.getInstance().getBundleService();
protected BitstreamService bitstreamService =
ContentServiceFactory.getInstance().getBitstreamService();
protected WorkspaceItemService workspaceItemService = protected WorkspaceItemService workspaceItemService =
ContentServiceFactory.getInstance().getWorkspaceItemService(); ContentServiceFactory.getInstance().getWorkspaceItemService();
protected InstallItemService installItemService = protected InstallItemService installItemService =
@@ -71,6 +88,10 @@ public class AccessStatusServiceTest extends AbstractUnitTest {
collection = collectionService.create(context, owningCommunity); collection = collectionService.create(context, owningCommunity);
item = installItemService.installItem(context, item = installItemService.installItem(context,
workspaceItemService.create(context, collection, true)); 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(); context.restoreAuthSystemState();
} catch (AuthorizeException ex) { } catch (AuthorizeException ex) {
log.error("Authorization Error in init", ex); log.error("Authorization Error in init", ex);
@@ -78,6 +99,9 @@ public class AccessStatusServiceTest extends AbstractUnitTest {
} catch (SQLException ex) { } catch (SQLException ex) {
log.error("SQL Error in init", ex); log.error("SQL Error in init", ex);
fail("SQL Error in init: " + ex.getMessage()); 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 @Override
public void destroy() { public void destroy() {
context.turnOffAuthorisationSystem(); context.turnOffAuthorisationSystem();
try {
bitstreamService.delete(context, bitstream);
} catch (Exception e) {
// ignore
}
try {
bundleService.delete(context, bundle);
} catch (Exception e) {
// ignore
}
try { try {
itemService.delete(context, item); itemService.delete(context, item);
} catch (Exception e) { } catch (Exception e) {
@@ -108,6 +142,8 @@ public class AccessStatusServiceTest extends AbstractUnitTest {
// ignore // ignore
} }
context.restoreAuthSystemState(); context.restoreAuthSystemState();
bitstream = null;
bundle = null;
item = null; item = null;
collection = null; collection = null;
owningCommunity = null; owningCommunity = null;
@@ -119,8 +155,29 @@ public class AccessStatusServiceTest extends AbstractUnitTest {
} }
@Test @Test
public void testGetAccessStatus() throws Exception { public void testGetAccessStatusItem() throws Exception {
String status = accessStatusService.getAccessStatus(context, item); AccessStatus accessStatus = accessStatusService.getAccessStatus(context, item);
assertNotEquals("testGetAccessStatus 0", status, DefaultAccessStatusHelper.UNKNOWN); 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);
} }
} }

View File

@@ -9,6 +9,7 @@ package org.dspace.access.status;
import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
@@ -25,6 +26,7 @@ import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.ResourcePolicy;
import org.dspace.authorize.factory.AuthorizeServiceFactory; import org.dspace.authorize.factory.AuthorizeServiceFactory;
import org.dspace.authorize.service.ResourcePolicyService; import org.dspace.authorize.service.ResourcePolicyService;
import org.dspace.content.AccessStatus;
import org.dspace.content.Bitstream; import org.dspace.content.Bitstream;
import org.dspace.content.Bundle; import org.dspace.content.Bundle;
import org.dspace.content.Collection; 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.ItemService;
import org.dspace.content.service.WorkspaceItemService; import org.dspace.content.service.WorkspaceItemService;
import org.dspace.core.Constants; import org.dspace.core.Constants;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group; import org.dspace.eperson.Group;
import org.dspace.eperson.factory.EPersonServiceFactory; import org.dspace.eperson.factory.EPersonServiceFactory;
import org.dspace.eperson.service.EPersonService;
import org.dspace.eperson.service.GroupService; import org.dspace.eperson.service.GroupService;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
@@ -83,6 +87,8 @@ public class DefaultAccessStatusHelperTest extends AbstractUnitTest {
AuthorizeServiceFactory.getInstance().getResourcePolicyService(); AuthorizeServiceFactory.getInstance().getResourcePolicyService();
protected GroupService groupService = protected GroupService groupService =
EPersonServiceFactory.getInstance().getGroupService(); EPersonServiceFactory.getInstance().getGroupService();
protected EPersonService ePersonService =
EPersonServiceFactory.getInstance().getEPersonService();
/** /**
* This method will be run before every test as per @Before. It will * This method will be run before every test as per @Before. It will
@@ -203,8 +209,13 @@ public class DefaultAccessStatusHelperTest extends AbstractUnitTest {
*/ */
@Test @Test
public void testWithNullItem() throws Exception { 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)); 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 @Test
public void testWithoutBundle() throws Exception { 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)); 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(); context.turnOffAuthorisationSystem();
bundleService.create(context, itemWithoutBitstream, Constants.CONTENT_BUNDLE_NAME); bundleService.create(context, itemWithoutBitstream, Constants.CONTENT_BUNDLE_NAME);
context.restoreAuthSystemState(); 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)); 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"); bitstream.setName(context, "primary");
bundle.setPrimaryBitstreamID(bitstream); bundle.setPrimaryBitstreamID(bitstream);
context.restoreAuthSystemState(); 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)); 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); ResourcePolicy policy = resourcePolicyService.create(context, null, group);
policy.setRpName("Embargo"); policy.setRpName("Embargo");
policy.setAction(Constants.READ); policy.setAction(Constants.READ);
policy.setStartDate(LocalDate.of(9999, 12, 31)); LocalDate startDate = LocalDate.of(9999, 12, 31);
policy.setStartDate(startDate);
policies.add(policy); policies.add(policy);
authorizeService.removeAllPolicies(context, bitstream); authorizeService.removeAllPolicies(context, bitstream);
authorizeService.addPolicies(context, policies, bitstream); authorizeService.addPolicies(context, policies, bitstream);
context.restoreAuthSystemState(); 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)); assertThat("testWithEmbargo 0", status, equalTo(DefaultAccessStatusHelper.EMBARGO));
String embargoDate = helper.getEmbargoFromItem(context, itemWithEmbargo, threshold); LocalDate availabilityDate = accessStatus.getAvailabilityDate();
assertThat("testWithEmbargo 1", embargoDate, equalTo(policy.getStartDate().toString())); 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); ResourcePolicy policy = resourcePolicyService.create(context, null, group);
policy.setRpName("Restriction"); policy.setRpName("Restriction");
policy.setAction(Constants.READ); policy.setAction(Constants.READ);
policy.setStartDate(LocalDate.of(10000, 1, 1)); LocalDate startDate = LocalDate.of(10000, 1, 1);
policy.setStartDate(startDate);
policies.add(policy); policies.add(policy);
authorizeService.removeAllPolicies(context, bitstream); authorizeService.removeAllPolicies(context, bitstream);
authorizeService.addPolicies(context, policies, bitstream); authorizeService.addPolicies(context, policies, bitstream);
context.restoreAuthSystemState(); 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)); 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.removeAllPolicies(context, bitstream);
authorizeService.addPolicies(context, policies, bitstream); authorizeService.addPolicies(context, policies, bitstream);
context.restoreAuthSystemState(); 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)); 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); bundle.setPrimaryBitstreamID(bitstream);
authorizeService.removeAllPolicies(context, bitstream); authorizeService.removeAllPolicies(context, bitstream);
context.restoreAuthSystemState(); 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)); 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))); new ByteArrayInputStream("1".getBytes(StandardCharsets.UTF_8)));
bitstream.setName(context, "first"); bitstream.setName(context, "first");
context.restoreAuthSystemState(); 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)); 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(); context.turnOffAuthorisationSystem();
Bundle bundle = bundleService.create(context, itemWithPrimaryAndMultipleBitstreams, Bundle bundle = bundleService.create(context, itemWithPrimaryAndMultipleBitstreams,
Constants.CONTENT_BUNDLE_NAME); Constants.CONTENT_BUNDLE_NAME);
bitstreamService.create(context, bundle, Bitstream otherBitstream = bitstreamService.create(context, bundle,
new ByteArrayInputStream("1".getBytes(StandardCharsets.UTF_8))); new ByteArrayInputStream("1".getBytes(StandardCharsets.UTF_8)));
Bitstream primaryBitstream = bitstreamService.create(context, bundle, Bitstream primaryBitstream = bitstreamService.create(context, bundle,
new ByteArrayInputStream("1".getBytes(StandardCharsets.UTF_8))); new ByteArrayInputStream("1".getBytes(StandardCharsets.UTF_8)));
@@ -380,15 +480,35 @@ public class DefaultAccessStatusHelperTest extends AbstractUnitTest {
ResourcePolicy policy = resourcePolicyService.create(context, null, group); ResourcePolicy policy = resourcePolicyService.create(context, null, group);
policy.setRpName("Embargo"); policy.setRpName("Embargo");
policy.setAction(Constants.READ); policy.setAction(Constants.READ);
policy.setStartDate(LocalDate.of(9999, 12, 31)); LocalDate startDate = LocalDate.of(9999, 12, 31);
policy.setStartDate(startDate);
policies.add(policy); policies.add(policy);
authorizeService.removeAllPolicies(context, primaryBitstream); authorizeService.removeAllPolicies(context, primaryBitstream);
authorizeService.addPolicies(context, policies, primaryBitstream); authorizeService.addPolicies(context, policies, primaryBitstream);
context.restoreAuthSystemState(); 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)); assertThat("testWithPrimaryAndMultipleBitstreams 0", status, equalTo(DefaultAccessStatusHelper.EMBARGO));
String embargoDate = helper.getEmbargoFromItem(context, itemWithPrimaryAndMultipleBitstreams, threshold); LocalDate availabilityDate = accessStatus.getAvailabilityDate();
assertThat("testWithPrimaryAndMultipleBitstreams 1", embargoDate, equalTo(policy.getStartDate().toString())); 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(); context.turnOffAuthorisationSystem();
Bundle bundle = bundleService.create(context, itemWithoutPrimaryAndMultipleBitstreams, Bundle bundle = bundleService.create(context, itemWithoutPrimaryAndMultipleBitstreams,
Constants.CONTENT_BUNDLE_NAME); Constants.CONTENT_BUNDLE_NAME);
bitstreamService.create(context, bundle, Bitstream firstBitstream = bitstreamService.create(context, bundle,
new ByteArrayInputStream("1".getBytes(StandardCharsets.UTF_8))); new ByteArrayInputStream("1".getBytes(StandardCharsets.UTF_8)));
Bitstream anotherBitstream = bitstreamService.create(context, bundle, Bitstream anotherBitstream = bitstreamService.create(context, bundle,
new ByteArrayInputStream("1".getBytes(StandardCharsets.UTF_8))); new ByteArrayInputStream("1".getBytes(StandardCharsets.UTF_8)));
@@ -410,14 +530,167 @@ public class DefaultAccessStatusHelperTest extends AbstractUnitTest {
ResourcePolicy policy = resourcePolicyService.create(context, null, group); ResourcePolicy policy = resourcePolicyService.create(context, null, group);
policy.setRpName("Embargo"); policy.setRpName("Embargo");
policy.setAction(Constants.READ); policy.setAction(Constants.READ);
policy.setStartDate(LocalDate.of(9999, 12, 31)); LocalDate startDate = LocalDate.of(9999, 12, 31);
policy.setStartDate(startDate);
policies.add(policy); policies.add(policy);
authorizeService.removeAllPolicies(context, anotherBitstream); authorizeService.removeAllPolicies(context, anotherBitstream);
authorizeService.addPolicies(context, policies, anotherBitstream); authorizeService.addPolicies(context, policies, anotherBitstream);
context.restoreAuthSystemState(); context.restoreAuthSystemState();
String status = helper.getAccessStatusFromItem(context, itemWithoutPrimaryAndMultipleBitstreams, threshold); // getAccessStatusFromItem
assertThat("testWithNoPrimaryAndMultipleBitstreams 0", status, equalTo(DefaultAccessStatusHelper.OPEN_ACCESS)); AccessStatus accessStatus = helper.getAccessStatusFromItem(context,
String embargoDate = helper.getEmbargoFromItem(context, itemWithEmbargo, threshold); itemWithoutPrimaryAndMultipleBitstreams, threshold, DefaultAccessStatusHelper.STATUS_FOR_CURRENT_USER);
assertThat("testWithNoPrimaryAndMultipleBitstreams 1", embargoDate, equalTo(null)); 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);
} }
} }

View File

@@ -10,8 +10,12 @@ package org.dspace.app.requestitem;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import jakarta.mail.Address; import jakarta.mail.Address;
import jakarta.mail.Message; import jakarta.mail.Message;
import jakarta.mail.Provider; import jakarta.mail.Provider;
@@ -21,9 +25,12 @@ import org.dspace.AbstractUnitTest;
import org.dspace.app.requestitem.factory.RequestItemServiceFactory; import org.dspace.app.requestitem.factory.RequestItemServiceFactory;
import org.dspace.app.requestitem.service.RequestItemService; import org.dspace.app.requestitem.service.RequestItemService;
import org.dspace.builder.AbstractBuilder; import org.dspace.builder.AbstractBuilder;
import org.dspace.builder.BitstreamBuilder;
import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CollectionBuilder;
import org.dspace.builder.CommunityBuilder; import org.dspace.builder.CommunityBuilder;
import org.dspace.builder.ItemBuilder; import org.dspace.builder.ItemBuilder;
import org.dspace.builder.RequestItemBuilder;
import org.dspace.content.Bitstream;
import org.dspace.content.Collection; import org.dspace.content.Collection;
import org.dspace.content.Community; import org.dspace.content.Community;
import org.dspace.content.Item; import org.dspace.content.Item;
@@ -38,6 +45,7 @@ import org.junit.BeforeClass;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
/** /**
* Tests for {@link RequestItemEmailNotifier}. * Tests for {@link RequestItemEmailNotifier}.
* *
@@ -59,6 +67,7 @@ public class RequestItemEmailNotifierTest
private static BitstreamService bitstreamService; private static BitstreamService bitstreamService;
private static HandleService handleService; private static HandleService handleService;
private static RequestItemService requestItemService; private static RequestItemService requestItemService;
private static RequestItemEmailNotifier requestItemEmailNotifier;
public RequestItemEmailNotifierTest() { public RequestItemEmailNotifierTest() {
super(); super();
@@ -76,6 +85,18 @@ public class RequestItemEmailNotifierTest
= HandleServiceFactory.getInstance().getHandleService(); = HandleServiceFactory.getInstance().getHandleService();
requestItemService requestItemService
= RequestItemServiceFactory.getInstance().getRequestItemService(); = 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 @AfterClass
@@ -87,7 +108,7 @@ public class RequestItemEmailNotifierTest
/** /**
* Test of sendRequest method, of class RequestItemEmailNotifier. * Test of sendRequest method, of class RequestItemEmailNotifier.
* @throws java.lang.Exception passed through. * @throws Exception passed through.
*/ */
@Ignore @Ignore
@Test @Test
@@ -96,7 +117,7 @@ public class RequestItemEmailNotifierTest
/** /**
* Test of sendResponse method, of class RequestItemEmailNotifier. * Test of sendResponse method, of class RequestItemEmailNotifier.
* @throws java.lang.Exception passed through. * @throws Exception passed through.
*/ */
@Test @Test
public void testSendResponse() throws Exception { public void testSendResponse() throws Exception {
@@ -137,18 +158,6 @@ public class RequestItemEmailNotifierTest
// Ensure that mail is "sent". // Ensure that mail is "sent".
configurationService.setProperty("mail.server.disabled", "false"); 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 // Test the unit. Template supplies the Subject: value
requestItemEmailNotifier.sendResponse(context, ri, null, TEST_MESSAGE); requestItemEmailNotifier.sendResponse(context, ri, null, TEST_MESSAGE);
@@ -180,7 +189,7 @@ public class RequestItemEmailNotifierTest
/** /**
* Test of sendResponse method -- rejection case. * Test of sendResponse method -- rejection case.
* @throws java.lang.Exception passed through. * @throws Exception passed through.
*/ */
@Test @Test
public void testSendRejection() public void testSendRejection()
@@ -222,18 +231,6 @@ public class RequestItemEmailNotifierTest
// Ensure that mail is "sent". // Ensure that mail is "sent".
configurationService.setProperty("mail.server.disabled", "false"); 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 // Test the unit. Template supplies the Subject: value
requestItemEmailNotifier.sendResponse(context, ri, null, TEST_MESSAGE); requestItemEmailNotifier.sendResponse(context, ri, null, TEST_MESSAGE);
@@ -267,9 +264,54 @@ public class RequestItemEmailNotifierTest
(String)content, containsString("denied")); (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. * Test of requestOpenAccess method, of class RequestItemEmailNotifier.
* @throws java.lang.Exception passed through. * @throws Exception passed through.
*/ */
@Ignore @Ignore
@Test @Test

View File

@@ -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);
}
}
}

View File

@@ -39,6 +39,9 @@ public class RequestItemBuilder
private Bitstream bitstream; private Bitstream bitstream;
private Instant decisionDate; private Instant decisionDate;
private boolean accepted; private boolean accepted;
private String accessToken = null;
private Instant accessExpiry = null;
private boolean allFiles;
protected RequestItemBuilder(Context context) { protected RequestItemBuilder(Context context) {
super(context); super(context);
@@ -87,13 +90,29 @@ public class RequestItemBuilder
return this; 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 @Override
public RequestItem build() { public RequestItem build() {
LOG.atDebug() LOG.atDebug()
.withLocation() .withLocation()
.log("Building request with item ID {} and bitstream ID {}", .log("Building request with item ID {} and bitstream ID {} and allfiles {}",
() -> item.getID().toString(), () -> item.getID().toString(),
() -> bitstream.getID().toString()); () -> (bitstream == null ? "" : bitstream.getID().toString()),
() -> Boolean.toString(allFiles));
String token; String token;
try { try {
@@ -106,6 +125,11 @@ public class RequestItemBuilder
requestItem = requestItemService.findByToken(context, token); requestItem = requestItemService.findByToken(context, token);
requestItem.setAccept_request(accepted); requestItem.setAccept_request(accepted);
requestItem.setDecision_date(decisionDate); requestItem.setDecision_date(decisionDate);
if (accessToken != null) {
requestItem.setAccess_token(accessToken);
}
requestItem.setAccess_expiry(accessExpiry);
requestItem.setAllfiles(allFiles);
requestItemService.update(context, requestItem); requestItemService.update(context, requestItem);

View File

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

View File

@@ -0,0 +1,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");
}
}

View File

@@ -0,0 +1,167 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.eperson;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import org.dspace.AbstractIntegrationTestWithDatabase;
import org.dspace.builder.MetadataFieldBuilder;
import org.dspace.content.MetadataField;
import org.dspace.eperson.factory.EPersonServiceFactory;
import org.dspace.eperson.service.RegistrationDataMetadataService;
import org.dspace.eperson.service.RegistrationDataService;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
/**
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
*/
public class RegistrationDataMetadataServiceImplIT extends AbstractIntegrationTestWithDatabase {
RegistrationDataMetadataService registrationDataMetadataService =
EPersonServiceFactory.getInstance().getRegistrationDAtaDataMetadataService();
RegistrationDataService registrationDataService =
EPersonServiceFactory.getInstance().getRegistrationDataService();
MetadataField metadataField;
RegistrationData registrationData;
RegistrationDataMetadata metadata;
@Before
@Override
public void setUp() throws Exception {
super.setUp();
context.turnOffAuthorisationSystem();
this.registrationData =
this.registrationDataService.create(context);
this.metadataField =
MetadataFieldBuilder.createMetadataField(context, "dc", "identifier", "custom")
.build();
context.restoreAuthSystemState();
}
@After
@Override
public void destroy() throws Exception {
this.registrationDataService.delete(context, registrationData);
super.destroy();
}
@Test
public void testEmptyMetadataCreation() throws Exception {
try {
metadata = registrationDataMetadataService.create(context, registrationData, metadataField);
assertThat(metadata, notNullValue());
assertThat(metadata.getValue(), nullValue());
assertThat(metadata.getRegistrationData().getID(), is(registrationData.getID()));
assertThat(metadata.getMetadataField(), is(metadataField));
} finally {
registrationDataMetadataService.delete(context, metadata);
}
}
@Test
public void testValidMetadataCreation() throws Exception {
try {
metadata =
registrationDataMetadataService.create(context, registrationData, metadataField, "my-identifier");
assertThat(metadata, notNullValue());
assertThat(metadata.getValue(), is("my-identifier"));
assertThat(metadata.getRegistrationData().getID(), is(registrationData.getID()));
assertThat(metadata.getMetadataField(), is(metadataField));
} finally {
registrationDataMetadataService.delete(context, metadata);
}
}
@Test
public void testExistingMetadataFieldMetadataCreation() throws Exception {
try {
metadata =
registrationDataMetadataService.create(
context, registrationData, "dc", "identifier", "other", "my-identifier"
);
assertThat(metadata, notNullValue());
assertThat(metadata.getValue(), is("my-identifier"));
assertThat(metadata.getRegistrationData().getID(), is(registrationData.getID()));
} finally {
registrationDataMetadataService.delete(context, metadata);
}
}
@Test
public void testFindMetadata() throws Exception {
try {
metadata = registrationDataMetadataService.create(context, registrationData, metadataField);
RegistrationDataMetadata found =
registrationDataMetadataService.find(context, metadata.getID());
assertThat(found.getID(), is(metadata.getID()));
} finally {
registrationDataMetadataService.delete(context, metadata);
}
}
@Test
public void testUpdateMetadata() throws Exception {
try {
metadata = registrationDataMetadataService.create(context, registrationData, metadataField);
metadata.setValue("custom-value");
registrationDataMetadataService.update(context, metadata);
RegistrationDataMetadata found =
registrationDataMetadataService.find(context, metadata.getID());
assertThat(found.getID(), is(metadata.getID()));
assertThat(found.getValue(), is("custom-value"));
} finally {
registrationDataMetadataService.delete(context, metadata);
}
}
@Test
public void testDeleteMetadata() throws Exception {
try {
metadata = registrationDataMetadataService.create(context, registrationData, metadataField);
RegistrationDataMetadata found =
registrationDataMetadataService.find(context, metadata.getID());
assertThat(found, notNullValue());
registrationDataMetadataService.delete(context, metadata);
found = registrationDataMetadataService.find(context, metadata.getID());
assertThat(found, nullValue());
} finally {
registrationDataMetadataService.delete(context, metadata);
}
}
}

View File

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

View File

@@ -8,13 +8,16 @@
package org.dspace.xoai.app.plugins; package org.dspace.xoai.app.plugins;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.LocalDate;
import java.util.List; import java.util.List;
import com.lyncode.xoai.dataprovider.xml.xoai.Element; import com.lyncode.xoai.dataprovider.xml.xoai.Element;
import com.lyncode.xoai.dataprovider.xml.xoai.Metadata; import com.lyncode.xoai.dataprovider.xml.xoai.Metadata;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.dspace.access.status.DefaultAccessStatusHelper;
import org.dspace.access.status.factory.AccessStatusServiceFactory; import org.dspace.access.status.factory.AccessStatusServiceFactory;
import org.dspace.access.status.service.AccessStatusService; import org.dspace.access.status.service.AccessStatusService;
import org.dspace.content.AccessStatus;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.xoai.app.XOAIExtensionItemCompilePlugin; import org.dspace.xoai.app.XOAIExtensionItemCompilePlugin;
@@ -51,10 +54,13 @@ public class AccessStatusElementItemCompilePlugin implements XOAIExtensionItemCo
AccessStatusService accessStatusService = AccessStatusServiceFactory.getInstance().getAccessStatusService(); AccessStatusService accessStatusService = AccessStatusServiceFactory.getInstance().getAccessStatusService();
try { try {
String accessStatusType; AccessStatus accessStatusResult = accessStatusService.getAnonymousAccessStatus(context, item);
accessStatusType = accessStatusService.getAccessStatus(context, item); String accessStatusType = accessStatusResult.getStatus();
LocalDate availabilityDate = accessStatusResult.getAvailabilityDate();
String embargoFromItem = accessStatusService.getEmbargoFromItem(context, item); String embargoFromItem = null;
if (accessStatusType == DefaultAccessStatusHelper.EMBARGO && availabilityDate != null) {
embargoFromItem = availabilityDate.toString();
}
Element accessStatus = ItemUtils.create("access-status"); Element accessStatus = ItemUtils.create("access-status");
accessStatus.getField().add(ItemUtils.createValue("value", accessStatusType)); accessStatus.getField().add(ItemUtils.createValue("value", accessStatusType));

View File

@@ -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")));
}
}

View File

@@ -23,6 +23,8 @@ import org.apache.catalina.connector.ClientAbortException;
import org.apache.commons.collections4.ListUtils; import org.apache.commons.collections4.ListUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger; 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.converter.ConverterService;
import org.dspace.app.rest.exception.DSpaceBadRequestException; import org.dspace.app.rest.exception.DSpaceBadRequestException;
import org.dspace.app.rest.model.BitstreamRest; import org.dspace.app.rest.model.BitstreamRest;
@@ -93,33 +95,71 @@ public class BitstreamRestController {
@Autowired @Autowired
private ConfigurationService configurationService; private ConfigurationService configurationService;
@Autowired
private RequestItemService requestItemService;
@Autowired @Autowired
ConverterService converter; ConverterService converter;
@Autowired @Autowired
Utils utils; 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") @RequestMapping( method = {RequestMethod.GET, RequestMethod.HEAD}, value = "content")
public ResponseEntity retrieve(@PathVariable UUID uuid, HttpServletResponse response, public ResponseEntity retrieve(@PathVariable UUID uuid,
HttpServletRequest request) throws IOException, SQLException, AuthorizeException { @Parameter(value = "accessToken", required = false) String accessToken,
HttpServletResponse response,
HttpServletRequest request) throws IOException, SQLException, AuthorizeException {
// Obtain context
Context context = ContextUtil.obtainContext(request); Context context = ContextUtil.obtainContext(request);
// Find bitstream
Bitstream bit = bitstreamService.find(context, uuid); Bitstream bit = bitstreamService.find(context, uuid);
EPerson currentUser = context.getCurrentUser();
if (bit == null) { if (bit == null) {
response.sendError(HttpServletResponse.SC_NOT_FOUND); response.sendError(HttpServletResponse.SC_NOT_FOUND);
return null; return null;
} }
// Get EPerson
EPerson currentUser = context.getCurrentUser();
// Get bitstream metadata
Long lastModified = bitstreamService.getLastModified(bit); Long lastModified = bitstreamService.getLastModified(bit);
BitstreamFormat format = bit.getFormat(context); BitstreamFormat format = bit.getFormat(context);
String mimetype = format.getMIMEType(); String mimetype = format.getMIMEType();
String name = getBitstreamName(bit, format); 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"))) { if (StringUtils.isBlank(request.getHeader("Range"))) {
//We only log a download request when serving a request without Range header. This is because //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. //a browser always sends a regular request first to check for Range support.
@@ -131,42 +171,59 @@ public class BitstreamRestController {
bit)); bit));
} }
// Begin actual bitstream delivery
try { try {
// Check if a citation coverpage is valid for this download
long filesize = bit.getSizeBytes(); long filesize = bit.getSizeBytes();
Boolean citationEnabledForBitstream = citationDocumentService.isCitationEnabledForBitstream(bit, context); 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() HttpHeadersInitializer httpHeadersInitializer = new HttpHeadersInitializer()
.withBufferSize(BUFFER_SIZE) .withBufferSize(BUFFER_SIZE)
.withFileName(name) .withFileName(name)
.withChecksum(bitstreamResource.getChecksum()) .withChecksum(bitstreamResource.getChecksum())
.withLength(bitstreamResource.contentLength()) .withLength(bitstreamResource.contentLength())
.withMimetype(mimetype) .withMimetype(mimetype)
.with(request) .with(request)
.with(response); .with(response);
// Set last modified in headers
if (lastModified != null) { if (lastModified != null) {
httpHeadersInitializer.withLastModified(lastModified); httpHeadersInitializer.withLastModified(lastModified);
} }
//Determine if we need to send the file as a download or if the browser can open it inline // 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, // 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 // or if its mimetype/extension appears in the "webui.content_disposition_format" config
long dispositionThreshold = configurationService.getLongProperty("webui.content_disposition_threshold"); long dispositionThreshold = configurationService.getLongProperty("webui.content_disposition_threshold");
if ((dispositionThreshold >= 0 && filesize > dispositionThreshold) if ((dispositionThreshold >= 0 && filesize > dispositionThreshold)
|| checkFormatForContentDisposition(format)) { || checkFormatForContentDisposition(format)) {
httpHeadersInitializer.withDisposition(HttpHeadersInitializer.CONTENT_DISPOSITION_ATTACHMENT); 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 // Send the data
//download/streaming
context.complete();
//Send the data
if (httpHeadersInitializer.isValid()) { if (httpHeadersInitializer.isValid()) {
HttpHeaders httpHeaders = httpHeadersInitializer.initialiseHeaders(); HttpHeaders httpHeaders = httpHeadersInitializer.initialiseHeaders();
@@ -187,6 +244,12 @@ public class BitstreamRestController {
return null; 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) { private String getBitstreamName(Bitstream bit, BitstreamFormat format) {
String name = bit.getName(); String name = bit.getName();
if (name == null) { if (name == null) {
@@ -199,6 +262,11 @@ public class BitstreamRestController {
return name; 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) { private boolean isNotAnErrorResponse(HttpServletResponse response) {
Response.Status.Family responseCode = Response.Status.Family.familyOf(response.getStatus()); Response.Status.Family responseCode = Response.Status.Family.familyOf(response.getStatus());
return responseCode.equals(Response.Status.Family.SUCCESSFUL) return responseCode.equals(Response.Status.Family.SUCCESSFUL)
@@ -250,6 +318,18 @@ public class BitstreamRestController {
return download; 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. * This method will update the bitstream format of the bitstream that corresponds to the provided bitstream uuid.
* *

View File

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

View File

@@ -183,7 +183,7 @@ public class WebApplication {
// Allow list of request preflight headers allowed to be sent to us from the client // 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", .allowedHeaders("Accept", "Authorization", "Content-Type", "Origin", "X-On-Behalf-Of",
"X-Requested-With", "X-XSRF-TOKEN", "X-CORRELATION-ID", "X-REFERRER", "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 // Allow list of response headers allowed to be sent by us (the server) to the client
.exposedHeaders("Authorization", "DSPACE-XSRF-TOKEN", "Location", "WWW-Authenticate"); .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 // 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", .allowedHeaders("Accept", "Authorization", "Content-Type", "Origin", "X-On-Behalf-Of",
"X-Requested-With", "X-XSRF-TOKEN", "X-CORRELATION-ID", "X-REFERRER", "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 // Allow list of response headers allowed to be sent by us (the server) to the client
.exposedHeaders("Authorization", "DSPACE-XSRF-TOKEN", "Location", "WWW-Authenticate"); .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 // 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", .allowedHeaders("Accept", "Authorization", "Content-Type", "Origin", "X-On-Behalf-Of",
"X-Requested-With", "X-XSRF-TOKEN", "X-CORRELATION-ID", "X-REFERRER", "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 // Allow list of response headers allowed to be sent by us (the server) to the client
.exposedHeaders("Authorization", "DSPACE-XSRF-TOKEN", "Location", "WWW-Authenticate"); .exposedHeaders("Authorization", "DSPACE-XSRF-TOKEN", "Location", "WWW-Authenticate");
} }

View File

@@ -62,6 +62,14 @@ public class RequestCopyFeature implements AuthorizationFeature {
@Autowired @Autowired
private ConfigurationService configurationService; 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 @Override
public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException { public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException {
String requestType = configurationService.getProperty("request.item.type"); String requestType = configurationService.getProperty("request.item.type");

View File

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

View File

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

View File

@@ -8,6 +8,8 @@
package org.dspace.app.rest.converter; package org.dspace.app.rest.converter;
import java.time.Instant;
import jakarta.inject.Named; import jakarta.inject.Named;
import org.dspace.app.requestitem.RequestItem; import org.dspace.app.requestitem.RequestItem;
import org.dspace.app.rest.model.RequestItemRest; import org.dspace.app.rest.model.RequestItemRest;
@@ -15,8 +17,8 @@ import org.dspace.app.rest.projection.Projection;
import org.dspace.content.Bitstream; import org.dspace.content.Bitstream;
/** /**
* Convert between {@link org.dspace.app.requestitem.RequestItem} and * Convert between {@link RequestItem} and
* {@link org.dspace.app.rest.model.RequestItemRest}. * {@link RequestItemRest}.
* *
* @author Mark H. Wood <mwood@iupui.edu> * @author Mark H. Wood <mwood@iupui.edu>
*/ */
@@ -45,6 +47,14 @@ public class RequestItemConverter
requestItemRest.setRequestName(requestItem.getReqName()); requestItemRest.setRequestName(requestItem.getReqName());
requestItemRest.setRequestDate(requestItem.getRequest_date()); requestItemRest.setRequestDate(requestItem.getRequest_date());
requestItemRest.setToken(requestItem.getToken()); 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; return requestItemRest;
} }

View File

@@ -18,6 +18,7 @@ public class AccessStatusRest implements RestModel {
public static final String PLURAL_NAME = NAME; public static final String PLURAL_NAME = NAME;
String status; String status;
String embargoDate;
@Override @Override
@JsonProperty(access = Access.READ_ONLY) @JsonProperty(access = Access.READ_ONLY)
@@ -35,10 +36,12 @@ public class AccessStatusRest implements RestModel {
public AccessStatusRest() { public AccessStatusRest() {
setStatus(null); setStatus(null);
setEmbargoDate(null);
} }
public AccessStatusRest(String status) { public AccessStatusRest(String status) {
setStatus(status); setStatus(status);
setEmbargoDate(null);
} }
public String getStatus() { public String getStatus() {
@@ -48,4 +51,12 @@ public class AccessStatusRest implements RestModel {
public void setStatus(String status) { public void setStatus(String status) {
this.status = status; this.status = status;
} }
public String getEmbargoDate() {
return embargoDate;
}
public void setEmbargoDate(String embargoDate) {
this.embargoDate = embargoDate;
}
} }

View File

@@ -17,6 +17,7 @@ import com.fasterxml.jackson.annotation.JsonProperty.Access;
*/ */
@LinksRest(links = { @LinksRest(links = {
@LinkRest(name = BitstreamRest.BUNDLE, method = "getBundle"), @LinkRest(name = BitstreamRest.BUNDLE, method = "getBundle"),
@LinkRest(name = BitstreamRest.ACCESS_STATUS, method = "getAccessStatus"),
@LinkRest(name = BitstreamRest.FORMAT, method = "getFormat"), @LinkRest(name = BitstreamRest.FORMAT, method = "getFormat"),
@LinkRest(name = BitstreamRest.THUMBNAIL, method = "getThumbnail") @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 CATEGORY = RestAddressableModel.CORE;
public static final String BUNDLE = "bundle"; public static final String BUNDLE = "bundle";
public static final String ACCESS_STATUS = "accessStatus";
public static final String FORMAT = "format"; public static final String FORMAT = "format";
public static final String THUMBNAIL = "thumbnail"; public static final String THUMBNAIL = "thumbnail";

View File

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

View File

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

View File

@@ -0,0 +1,40 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.model;
import com.fasterxml.jackson.annotation.JsonInclude;
/**
* This POJO represents a {@link MetadataValueRest} that will be placed inside a given
* {@link org.dspace.eperson.RegistrationData} that is coming directly from the REST controller.
*
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
**/
public class RegistrationMetadataRest extends MetadataValueRest {
@JsonInclude(JsonInclude.Include.NON_NULL)
private String overrides;
public RegistrationMetadataRest(String value, String overrides) {
super();
this.value = value;
this.overrides = overrides;
}
public RegistrationMetadataRest(String value) {
this(value, null);
}
public String getOverrides() {
return overrides;
}
public void setOverrides(String overrides) {
this.overrides = overrides;
}
}

View File

@@ -9,6 +9,7 @@ package org.dspace.app.rest.model;
import java.util.UUID; import java.util.UUID;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import org.dspace.app.rest.RestResourceController; 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 PLURAL_NAME = "registrations";
public static final String CATEGORY = EPERSON; public static final String CATEGORY = EPERSON;
private Integer id;
private String email; private String email;
private UUID user; 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 * Generic getter for the email
*
* @return the email value of this RegisterRest * @return the email value of this RegisterRest
*/ */
public String getEmail() { public String getEmail() {
@@ -37,7 +52,8 @@ public class RegistrationRest extends RestAddressableModel {
/** /**
* Generic setter for the email * 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) { public void setEmail(String email) {
this.email = email; this.email = email;
@@ -45,6 +61,7 @@ public class RegistrationRest extends RestAddressableModel {
/** /**
* Generic getter for the user * Generic getter for the user
*
* @return the user value of this RegisterRest * @return the user value of this RegisterRest
*/ */
public UUID getUser() { public UUID getUser() {
@@ -53,12 +70,38 @@ public class RegistrationRest extends RestAddressableModel {
/** /**
* Generic setter for the user * 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) { public void setUser(UUID user) {
this.user = 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 @Override
public String getCategory() { public String getCategory() {
return CATEGORY; return CATEGORY;

View File

@@ -53,6 +53,14 @@ public class RequestItemRest extends BaseObjectRest<Integer> {
protected boolean allfiles; 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. * @return the bitstream requested.
*/ */
@@ -207,6 +215,41 @@ public class RequestItemRest extends BaseObjectRest<Integer> {
this.allfiles = allfiles; 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. * Common REST object methods.
*/ */

View File

@@ -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);
}
}
}

View File

@@ -19,6 +19,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.dspace.app.rest.DiscoverableEndpointsService; import org.dspace.app.rest.DiscoverableEndpointsService;
import org.dspace.app.rest.EPersonRegistrationRestController;
import org.dspace.app.rest.Parameter; import org.dspace.app.rest.Parameter;
import org.dspace.app.rest.SearchRestMethod; import org.dspace.app.rest.SearchRestMethod;
import org.dspace.app.rest.exception.DSpaceBadRequestException; 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" throw new DSpaceBadRequestException("The self registered property cannot be set to false using this method"
+ " with a token"); + " 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 // We'll turn off authorisation system because this call isn't admin based as it's token based
context.turnOffAuthorisationSystem(); context.turnOffAuthorisationSystem();
EPerson ePerson = createEPersonFromRestObject(context, epersonRest); EPerson ePerson = createEPersonFromRestObject(context, epersonRest);
@@ -203,8 +204,8 @@ public class EPersonRestRepository extends DSpaceObjectRestRepository<EPerson, E
return converter.toRest(ePerson, utils.obtainProjection()); return converter.toRest(ePerson, utils.obtainProjection());
} }
private void checkRequiredProperties(EPersonRest epersonRest) { private void checkRequiredProperties(RegistrationData registration, EPersonRest epersonRest) {
MetadataRest metadataRest = epersonRest.getMetadata(); MetadataRest<MetadataValueRest> metadataRest = epersonRest.getMetadata();
if (metadataRest != null) { if (metadataRest != null) {
List<MetadataValueRest> epersonFirstName = metadataRest.getMap().get("eperson.firstname"); List<MetadataValueRest> epersonFirstName = metadataRest.getMap().get("eperson.firstname");
List<MetadataValueRest> epersonLastName = metadataRest.getMap().get("eperson.lastname"); List<MetadataValueRest> epersonLastName = metadataRest.getMap().get("eperson.lastname");
@@ -213,10 +214,25 @@ public class EPersonRestRepository extends DSpaceObjectRestRepository<EPerson, E
throw new EPersonNameNotProvidedException(); throw new EPersonNameNotProvidedException();
} }
} }
String password = epersonRest.getPassword(); String password = epersonRest.getPassword();
if (StringUtils.isBlank(password)) { String netId = epersonRest.getNetid();
throw new DSpaceBadRequestException("A password is required"); 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 @Override
@@ -369,6 +385,40 @@ public class EPersonRestRepository extends DSpaceObjectRestRepository<EPerson, E
return EPersonRest.class; 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 @Override
public void afterPropertiesSet() throws Exception { public void afterPropertiesSet() throws Exception {
discoverableEndpointsService.register(this, Arrays.asList( discoverableEndpointsService.register(this, Arrays.asList(

View File

@@ -9,14 +9,17 @@
package org.dspace.app.rest.repository; package org.dspace.app.rest.repository;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.LocalDate;
import java.util.UUID; import java.util.UUID;
import jakarta.annotation.Nullable; import jakarta.annotation.Nullable;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.dspace.access.status.DefaultAccessStatusHelper;
import org.dspace.access.status.service.AccessStatusService; import org.dspace.access.status.service.AccessStatusService;
import org.dspace.app.rest.model.AccessStatusRest; import org.dspace.app.rest.model.AccessStatusRest;
import org.dspace.app.rest.model.ItemRest; import org.dspace.app.rest.model.ItemRest;
import org.dspace.app.rest.projection.Projection; import org.dspace.app.rest.projection.Projection;
import org.dspace.content.AccessStatus;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.content.service.ItemService; import org.dspace.content.service.ItemService;
import org.dspace.core.Context; import org.dspace.core.Context;
@@ -51,8 +54,14 @@ public class ItemAccessStatusLinkRepository extends AbstractDSpaceRestRepository
throw new ResourceNotFoundException("No such item: " + itemId); throw new ResourceNotFoundException("No such item: " + itemId);
} }
AccessStatusRest accessStatusRest = new AccessStatusRest(); AccessStatusRest accessStatusRest = new AccessStatusRest();
String accessStatus = accessStatusService.getAccessStatus(context, item); AccessStatus accessStatus = accessStatusService.getAccessStatus(context, item);
accessStatusRest.setStatus(accessStatus); String status = accessStatus.getStatus();
if (status == DefaultAccessStatusHelper.EMBARGO) {
LocalDate availabilityDate = accessStatus.getAvailabilityDate();
String embargoDate = availabilityDate.toString();
accessStatusRest.setEmbargoDate(embargoDate);
}
accessStatusRest.setStatus(status);
return accessStatusRest; return accessStatusRest;
} catch (SQLException e) { } catch (SQLException e) {
throw new RuntimeException(e); throw new RuntimeException(e);

View File

@@ -16,6 +16,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.mail.MessagingException; import jakarta.mail.MessagingException;
import jakarta.servlet.ServletInputStream; import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.ws.rs.BadRequestException;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; 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.RepositoryMethodNotImplementedException;
import org.dspace.app.rest.exception.UnprocessableEntityException; import org.dspace.app.rest.exception.UnprocessableEntityException;
import org.dspace.app.rest.model.RegistrationRest; 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.app.util.AuthorizeUtil;
import org.dspace.authenticate.service.AuthenticationService; import org.dspace.authenticate.service.AuthenticationService;
import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.AuthorizeException;
@@ -32,6 +37,8 @@ import org.dspace.core.Context;
import org.dspace.eperson.EPerson; import org.dspace.eperson.EPerson;
import org.dspace.eperson.InvalidReCaptchaException; import org.dspace.eperson.InvalidReCaptchaException;
import org.dspace.eperson.RegistrationData; 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.AccountService;
import org.dspace.eperson.service.CaptchaService; import org.dspace.eperson.service.CaptchaService;
import org.dspace.eperson.service.EPersonService; import org.dspace.eperson.service.EPersonService;
@@ -54,9 +61,10 @@ public class RegistrationRestRepository extends DSpaceRestRepository<Registratio
private static Logger log = LogManager.getLogger(RegistrationRestRepository.class); 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_QUERY_PARAM = "accountRequestType";
public static final String TYPE_REGISTER = "register"; public static final String TYPE_REGISTER = RegistrationTypeEnum.REGISTER.toString().toLowerCase();
public static final String TYPE_FORGOT = "forgot"; public static final String TYPE_FORGOT = RegistrationTypeEnum.FORGOT.toString().toLowerCase();
@Autowired @Autowired
private EPersonService ePersonService; private EPersonService ePersonService;
@@ -70,8 +78,7 @@ public class RegistrationRestRepository extends DSpaceRestRepository<Registratio
@Autowired @Autowired
private RequestService requestService; private RequestService requestService;
@Autowired private CaptchaService captchaService = CaptchaServiceFactory.getInstance().getCaptchaService();
private CaptchaService captchaService;
@Autowired @Autowired
private ConfigurationService configurationService; private ConfigurationService configurationService;
@@ -79,6 +86,12 @@ public class RegistrationRestRepository extends DSpaceRestRepository<Registratio
@Autowired @Autowired
private RegistrationDataService registrationDataService; private RegistrationDataService registrationDataService;
@Autowired
private Utils utils;
@Autowired
private ResourcePatch<RegistrationData> resourcePatch;
@Autowired @Autowired
private ObjectMapper mapper; 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 " + 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)); "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"); boolean verificationEnabled = configurationService.getBooleanProperty("registration.verification.enabled");
if (verificationEnabled && !accountType.equalsIgnoreCase(TYPE_FORGOT)) { if (verificationEnabled && !accountType.equalsIgnoreCase(TYPE_FORGOT)) {
@@ -144,46 +157,42 @@ public class RegistrationRestRepository extends DSpaceRestRepository<Registratio
+ registrationRest.getEmail(), e); + registrationRest.getEmail(), e);
} }
} else if (accountType.equalsIgnoreCase(TYPE_REGISTER)) { } else if (accountType.equalsIgnoreCase(TYPE_REGISTER)) {
if (eperson == null) { try {
try { String email = registrationRest.getEmail();
String email = registrationRest.getEmail(); if (!AuthorizeUtil.authorizeNewAccountRegistration(context, request)) {
if (!AuthorizeUtil.authorizeNewAccountRegistration(context, request)) { throw new AccessDeniedException(
throw new AccessDeniedException( "Registration is disabled, you are not authorized to create a new Authorization");
"Registration is disabled, you are not authorized to create a new Authorization"); }
}
if (!authenticationService.canSelfRegister(context, request, email)) { if (!authenticationService.canSelfRegister(context, request, registrationRest.getEmail())) {
throw new UnprocessableEntityException( throw new UnprocessableEntityException(
String.format("Registration is not allowed with email address" + String.format("Registration is not allowed with email address" +
" %s", email)); " %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 accountService.sendRegistrationInfo(context, registrationRest.getEmail());
try { } catch (SQLException | IOException | MessagingException | AuthorizeException e) {
accountService.sendForgotPasswordInfo(context, registrationRest.getEmail()); log.error("Something went wrong with sending registration info email: "
} catch (SQLException | IOException | MessagingException | AuthorizeException e) { + registrationRest.getEmail(), e);
log.error("Something went wrong with sending forgot password info email: " }
} 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); + registrationRest.getEmail(), e);
}
} }
} }
return null; return null;
} }
@Override
public Class<RegistrationRest> getDomainClass() {
return RegistrationRest.class;
}
/** /**
* This method will find the RegistrationRest object that is associated with the token given * 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 * @param token The token to be found and for which a RegistrationRest object will be found
* @return A RegistrationRest object for the given token * @return A RegistrationRest object for the given token
* @throws SQLException If something goes wrong * @throws SQLException If something goes wrong
* @throws AuthorizeException If something goes wrong * @throws AuthorizeException If something goes wrong
*/ */
@SearchRestMethod(name = "findByToken") @SearchRestMethod(name = "findByToken")
@@ -194,17 +203,62 @@ public class RegistrationRestRepository extends DSpaceRestRepository<Registratio
if (registrationData == null) { if (registrationData == null) {
throw new ResourceNotFoundException("The token: " + token + " couldn't be found"); throw new ResourceNotFoundException("The token: " + token + " couldn't be found");
} }
RegistrationRest registrationRest = new RegistrationRest(); return converter.toRest(registrationData, utils.obtainProjection());
registrationRest.setEmail(registrationData.getEmail()); }
EPerson ePerson = accountService.getEPerson(context, token);
if (ePerson != null) { private void validateToken(Context context, String token) {
registrationRest.setUser(ePerson.getID()); 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) { public void setCaptchaService(CaptchaService captchaService) {
this.captchaService = captchaService; this.captchaService = captchaService;
} }
@Override
public Class<RegistrationRest> getDomainClass() {
return RegistrationRest.class;
}
} }

View File

@@ -15,8 +15,6 @@ import java.net.MalformedURLException;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.Instant; import java.time.Instant;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
@@ -24,12 +22,13 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.validator.routines.EmailValidator; 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.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.dspace.app.requestitem.RequestItem; import org.dspace.app.requestitem.RequestItem;
import org.dspace.app.requestitem.RequestItemEmailNotifier; import org.dspace.app.requestitem.RequestItemEmailNotifier;
import org.dspace.app.requestitem.service.RequestItemService; 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.converter.RequestItemConverter;
import org.dspace.app.rest.exception.IncompleteItemRequestException; import org.dspace.app.rest.exception.IncompleteItemRequestException;
import org.dspace.app.rest.exception.RepositoryMethodNotImplementedException; 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.model.RequestItemRest;
import org.dspace.app.rest.projection.Projection; import org.dspace.app.rest.projection.Projection;
import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.content.Bitstream; import org.dspace.content.Bitstream;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.content.service.BitstreamService; import org.dspace.content.service.BitstreamService;
import org.dspace.content.service.ItemService; import org.dspace.content.service.ItemService;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.eperson.EPerson; 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.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.util.HtmlUtils; 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 Mark H. Wood <mwood@iupui.edu>
* @author Kim Shepherd
*/ */
@Component(RequestItemRest.CATEGORY + '.' + RequestItemRest.PLURAL_NAME) @Component(RequestItemRest.CATEGORY + '.' + RequestItemRest.PLURAL_NAME)
public class RequestItemRepository public class RequestItemRepository
@@ -77,6 +84,12 @@ public class RequestItemRepository
@Autowired(required = true) @Autowired(required = true)
protected RequestItemEmailNotifier requestItemEmailNotifier; protected RequestItemEmailNotifier requestItemEmailNotifier;
@Autowired
protected AuthorizeService authorizeService;
private CaptchaService captchaService = CaptchaServiceFactory.getInstance().getCaptchaService();
private static final Logger log = LogManager.getLogger();
@Autowired @Autowired
private ObjectMapper mapper; private ObjectMapper mapper;
@@ -109,6 +122,24 @@ public class RequestItemRepository
HttpServletRequest req = getRequestService() HttpServletRequest req = getRequestService()
.getCurrentRequest() .getCurrentRequest()
.getHttpServletRequest(); .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; RequestItemRest rir;
try { try {
rir = mapper.readValue(req.getInputStream(), RequestItemRest.class); 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. // Create a link back to DSpace for the approver's response.
String responseLink; String responseLink;
try { try {
responseLink = getLinkTokenEmail(ri.getToken()); responseLink = requestItemService.getLinkTokenEmail(ri.getToken());
} catch (URISyntaxException | MalformedURLException e) { } catch (URISyntaxException | MalformedURLException e) {
LOG.warn("Impossible URL error while composing email: {}", LOG.warn("Impossible URL error while composing email: {}",
e::getMessage); e::getMessage);
@@ -229,15 +260,17 @@ public class RequestItemRepository
throw new UnprocessableEntityException("Item request not found"); throw new UnprocessableEntityException("Item request not found");
} }
// Do not permit updates after a decision has been given. // Previously there was a check here to prevent updates after *any* decision was given.
Instant decisionDate = ri.getDecision_date(); // This is now updated to allow specific updates to *granted* requests, so that it is possible
if (null != decisionDate) { // to revoke access tokens or alter access period
throw new UnprocessableEntityException("Request was " // Throw error only if decision date was set but was denied
+ (ri.isAccept_request() ? "granted" : "denied") if (null != ri.getDecision_date() && !ri.isAccept_request()) {
+ " on " + decisionDate + " and may not be updated."); throw new UnprocessableEntityException("Item request was already denied, no further updates are possible");
} }
// Make the changes // Make the changes
// Extract and set the 'accept' indicator
JsonNode acceptRequestNode = requestBody.findValue("acceptRequest"); JsonNode acceptRequestNode = requestBody.findValue("acceptRequest");
if (null == acceptRequestNode) { if (null == acceptRequestNode) {
throw new UnprocessableEntityException("acceptRequest is required"); throw new UnprocessableEntityException("acceptRequest is required");
@@ -245,18 +278,30 @@ public class RequestItemRepository
ri.setAccept_request(acceptRequestNode.asBoolean()); ri.setAccept_request(acceptRequestNode.asBoolean());
} }
// Extract and set the response message to include in the email
JsonNode responseMessageNode = requestBody.findValue("responseMessage"); JsonNode responseMessageNode = requestBody.findValue("responseMessage");
String message = null; String message = null;
if (responseMessageNode != null && !responseMessageNode.isNull()) { if (responseMessageNode != null && !responseMessageNode.isNull()) {
message = responseMessageNode.asText(); 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"); JsonNode responseSubjectNode = requestBody.findValue("subject");
String subject = null; String subject = null;
if (responseSubjectNode != null && !responseSubjectNode.isNull()) { if (responseSubjectNode != null && !responseSubjectNode.isNull()) {
subject = responseSubjectNode.asText(); subject = responseSubjectNode.asText();
} }
ri.setDecision_date(Instant.now());
requestItemService.update(context, ri); requestItemService.update(context, ri);
// Send the response email // Send the response email
@@ -282,33 +327,39 @@ public class RequestItemRepository
return rir; 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 @Override
public Class<RequestItemRest> getDomainClass() { public Class<RequestItemRest> getDomainClass() {
return RequestItemRest.class; 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();
}
} }

View File

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

View File

@@ -7,7 +7,12 @@
*/ */
package org.dspace.app.rest.security; 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.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList; import java.util.ArrayList;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
@@ -45,7 +50,8 @@ public class OrcidLoginFilter extends StatelessLoginFilter {
private ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); private ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
private OrcidAuthenticationBean orcidAuthentication = new DSpace().getServiceManager() private OrcidAuthenticationBean orcidAuthentication = new DSpace().getServiceManager()
.getServiceByName("orcidAuthentication", OrcidAuthenticationBean.class); .getServiceByName("orcidAuthentication",
OrcidAuthenticationBean.class);
public OrcidLoginFilter(String url, String httpMethod, AuthenticationManager authenticationManager, public OrcidLoginFilter(String url, String httpMethod, AuthenticationManager authenticationManager,
RestAuthenticationService restAuthenticationService) { RestAuthenticationService restAuthenticationService) {
@@ -66,13 +72,13 @@ public class OrcidLoginFilter extends StatelessLoginFilter {
@Override @Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
Authentication auth) throws IOException, ServletException { Authentication auth) throws IOException, ServletException {
DSpaceAuthentication dSpaceAuthentication = (DSpaceAuthentication) auth; DSpaceAuthentication dSpaceAuthentication = (DSpaceAuthentication) auth;
log.debug("Orcid authentication successful for EPerson {}. Sending back temporary auth cookie", log.debug("Orcid authentication successful for EPerson {}. Sending back temporary auth cookie",
dSpaceAuthentication.getName()); dSpaceAuthentication.getName());
restAuthenticationService.addAuthenticationDataForUser(req, res, dSpaceAuthentication, true); restAuthenticationService.addAuthenticationDataForUser(req, res, dSpaceAuthentication, true);
@@ -81,26 +87,41 @@ public class OrcidLoginFilter extends StatelessLoginFilter {
@Override @Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException { AuthenticationException failed) throws IOException, ServletException {
Context context = ContextUtil.obtainContext(request); Context context = ContextUtil.obtainContext(request);
if (orcidAuthentication.isUsed(context, 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 {
super.unsuccessfulAuthentication(request, response, failed); 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 * After successful login, redirect to the DSpace URL specified by this Orcid
* request (in the "redirectUrl" request parameter). If that 'redirectUrl' is * request (in the "redirectUrl" request parameter). If that 'redirectUrl' is
* not valid or trusted for this DSpace site, then return a 400 error. * not valid or trusted for this DSpace site, then return a 400 error.
* @param request *
* @param response * @param request
* @param response
* @throws IOException * @throws IOException
*/ */
private void redirectAfterSuccess(HttpServletRequest request, HttpServletResponse response) throws IOException { private void redirectAfterSuccess(HttpServletRequest request, HttpServletResponse response) throws IOException {
@@ -128,9 +149,9 @@ public class OrcidLoginFilter extends StatelessLoginFilter {
response.sendRedirect(redirectUrl); response.sendRedirect(redirectUrl);
} else { } else {
log.error("Invalid Orcid redirectURL=" + redirectUrl + 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, response.sendError(HttpServletResponse.SC_BAD_REQUEST,
"Invalid redirectURL! Must match server or ui hostname."); "Invalid redirectURL! Must match server or ui hostname.");
} }
} }

View File

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

View File

@@ -38,21 +38,21 @@ import org.springframework.util.DigestUtils;
*/ */
public class BitstreamResource extends AbstractResource { 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; protected final String name;
private final UUID uuid; protected final UUID uuid;
private final UUID currentUserUUID; protected final UUID currentUserUUID;
private final boolean shouldGenerateCoverPage; protected final boolean shouldGenerateCoverPage;
private final Set<UUID> currentSpecialGroups; protected final Set<UUID> currentSpecialGroups;
private final BitstreamService bitstreamService = ContentServiceFactory.getInstance().getBitstreamService(); protected final BitstreamService bitstreamService = ContentServiceFactory.getInstance().getBitstreamService();
private final EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService(); protected final EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService();
private final CitationDocumentService citationDocumentService = protected final CitationDocumentService citationDocumentService =
new DSpace().getServiceManager() new DSpace().getServiceManager()
.getServicesByType(CitationDocumentService.class).get(0); .getServicesByType(CitationDocumentService.class).get(0);
private BitstreamDocument document; protected BitstreamDocument document;
public BitstreamResource(String name, UUID uuid, UUID currentUserUUID, Set<UUID> currentSpecialGroups, public BitstreamResource(String name, UUID uuid, UUID currentUserUUID, Set<UUID> currentSpecialGroups,
boolean shouldGenerateCoverPage) { boolean shouldGenerateCoverPage) {
@@ -71,7 +71,7 @@ public class BitstreamResource extends AbstractResource {
* @param bitstream the pdf for which we want to generate a coverpage * @param bitstream the pdf for which we want to generate a coverpage
* @return a byte array containing the cover page * @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 { throws IOException, SQLException, AuthorizeException {
try { try {
var citedDocument = citationDocumentService.makeCitedDocument(context, bitstream); var citedDocument = citationDocumentService.makeCitedDocument(context, bitstream);
@@ -101,7 +101,7 @@ public class BitstreamResource extends AbstractResource {
} }
@Override @Override
public long contentLength() { public long contentLength() throws IOException {
fetchDocument(); fetchDocument();
return document.length(); return document.length();
@@ -113,7 +113,7 @@ public class BitstreamResource extends AbstractResource {
return document.etag(); return document.etag();
} }
private void fetchDocument() { void fetchDocument() {
if (document != null) { if (document != null) {
return; return;
} }
@@ -138,7 +138,7 @@ public class BitstreamResource extends AbstractResource {
LOG.debug("fetched document {} {}", shouldGenerateCoverPage, document); 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. /* 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 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(); return builder.toString();
} }
private Context initializeContext() throws SQLException { Context initializeContext() throws SQLException {
Context context = new Context(); Context context = new Context();
EPerson currentUser = ePersonService.find(context, currentUserUUID); EPerson currentUser = ePersonService.find(context, currentUserUUID);
context.setCurrentUser(currentUser); context.setCurrentUser(currentUser);
@@ -165,5 +165,5 @@ public class BitstreamResource extends AbstractResource {
return context; return context;
} }
private record BitstreamDocument(String etag, long length, InputStream inputStream) {} record BitstreamDocument(String etag, long length, InputStream inputStream) {}
} }

View File

@@ -0,0 +1,105 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.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);
}
}

View File

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

View File

@@ -0,0 +1,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");
}
}

View File

@@ -7,6 +7,7 @@
*/ */
package org.dspace.app.rest; package org.dspace.app.rest;
import static com.jayway.jsonpath.JsonPath.read;
import static jakarta.mail.internet.MimeUtility.encodeText; import static jakarta.mail.internet.MimeUtility.encodeText;
import static java.util.UUID.randomUUID; import static java.util.UUID.randomUUID;
import static org.apache.commons.codec.CharEncoding.UTF_8; import static org.apache.commons.codec.CharEncoding.UTF_8;
@@ -52,8 +53,11 @@ import java.io.StringWriter;
import java.io.Writer; import java.io.Writer;
import java.nio.file.Files; import java.nio.file.Files;
import java.time.Period; import java.time.Period;
import java.util.Map;
import java.util.UUID; 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.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.CharEncoding; 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.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper; import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.solr.client.solrj.SolrServerException; 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.app.rest.test.AbstractControllerIntegrationTest;
import org.dspace.authorize.service.AuthorizeService; import org.dspace.authorize.service.AuthorizeService;
import org.dspace.authorize.service.ResourcePolicyService; import org.dspace.authorize.service.ResourcePolicyService;
@@ -70,6 +77,7 @@ import org.dspace.builder.CommunityBuilder;
import org.dspace.builder.EPersonBuilder; import org.dspace.builder.EPersonBuilder;
import org.dspace.builder.GroupBuilder; import org.dspace.builder.GroupBuilder;
import org.dspace.builder.ItemBuilder; import org.dspace.builder.ItemBuilder;
import org.dspace.builder.RequestItemBuilder;
import org.dspace.content.Bitstream; import org.dspace.content.Bitstream;
import org.dspace.content.BitstreamFormat; import org.dspace.content.BitstreamFormat;
import org.dspace.content.Collection; import org.dspace.content.Collection;
@@ -127,6 +135,16 @@ public class BitstreamRestControllerIT extends AbstractControllerIntegrationTest
@Autowired @Autowired
private CollectionService collectionService; 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 Bitstream bitstream;
private BitstreamFormat supportedFormat; private BitstreamFormat supportedFormat;
private BitstreamFormat knownFormat; 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());
}
} }

View File

@@ -16,6 +16,8 @@ import static org.dspace.core.Constants.WRITE;
import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not; 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.assertEquals;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@@ -2933,6 +2935,82 @@ public class BitstreamRestRepositoryIT extends AbstractControllerIntegrationTest
.andExpect(status().isNoContent()); .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 { public boolean bitstreamExists(String token, Bitstream ...bitstreams) throws Exception {
for (Bitstream bitstream : bitstreams) { for (Bitstream bitstream : bitstreams) {
if (getClient(token).perform(get("/api/core/bitstreams/" + bitstream.getID())) if (getClient(token).perform(get("/api/core/bitstreams/" + bitstream.getID()))

View File

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

View File

@@ -36,6 +36,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.sql.SQLException;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; 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.model.patch.ReplaceOperation;
import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.app.rest.test.AbstractControllerIntegrationTest;
import org.dspace.app.rest.test.MetadataPatchSuite; import org.dspace.app.rest.test.MetadataPatchSuite;
import org.dspace.authorize.AuthorizeException;
import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CollectionBuilder;
import org.dspace.builder.CommunityBuilder; import org.dspace.builder.CommunityBuilder;
import org.dspace.builder.EPersonBuilder; import org.dspace.builder.EPersonBuilder;
@@ -72,10 +74,14 @@ import org.dspace.builder.GroupBuilder;
import org.dspace.builder.WorkflowItemBuilder; import org.dspace.builder.WorkflowItemBuilder;
import org.dspace.content.Collection; import org.dspace.content.Collection;
import org.dspace.content.Community; import org.dspace.content.Community;
import org.dspace.content.MetadataField;
import org.dspace.content.service.MetadataFieldService;
import org.dspace.core.I18nUtil; import org.dspace.core.I18nUtil;
import org.dspace.eperson.EPerson; import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group; import org.dspace.eperson.Group;
import org.dspace.eperson.PasswordHash; 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.AccountService;
import org.dspace.eperson.service.EPersonService; import org.dspace.eperson.service.EPersonService;
import org.dspace.eperson.service.GroupService; import org.dspace.eperson.service.GroupService;
@@ -102,6 +108,9 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest {
@Autowired @Autowired
private ConfigurationService configurationService; private ConfigurationService configurationService;
@Autowired
private MetadataFieldService metadataFieldService;
@Autowired @Autowired
private ObjectMapper mapper; 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 @Test
public void findByMetadataByCommAdminAndByColAdminTest() throws Exception { public void findByMetadataByCommAdminAndByColAdminTest() throws Exception {
context.turnOffAuthorisationSystem(); context.turnOffAuthorisationSystem();
@@ -3732,4 +3873,51 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest {
} }
private static EPersonRest createEPersonRest(String registrationEmail, String netId) {
EPersonRest ePersonRest = new EPersonRest();
MetadataRest metadataRest = new MetadataRest();
ePersonRest.setEmail(registrationEmail);
ePersonRest.setCanLogIn(true);
ePersonRest.setNetid(netId);
MetadataValueRest surname = new MetadataValueRest();
surname.setValue("Mecca");
metadataRest.put("eperson.lastname", surname);
MetadataValueRest firstname = new MetadataValueRest();
firstname.setValue("Vincenzo");
metadataRest.put("eperson.firstname", firstname);
MetadataValueRest orcid = new MetadataValueRest();
orcid.setValue("0000-0000-0000-0000");
metadataRest.put("eperson.orcid", orcid);
ePersonRest.setMetadata(metadataRest);
return ePersonRest;
}
private RegistrationData createRegistrationData(RegistrationTypeEnum validationOrcid, String registrationEmail)
throws SQLException, AuthorizeException {
RegistrationData orcidRegistration =
registrationDataService.create(context, "0000-0000-0000-0000", validationOrcid);
orcidRegistration.setEmail(registrationEmail);
MetadataField orcidMf =
metadataFieldService.findByElement(context, "eperson", "orcid", null);
MetadataField firstNameMf =
metadataFieldService.findByElement(context, "eperson", "firstname", null);
MetadataField lastNameMf =
metadataFieldService.findByElement(context, "eperson", "lastname", null);
registrationDataService.addMetadata(
context, orcidRegistration, orcidMf, "0000-0000-0000-0000"
);
registrationDataService.addMetadata(
context, orcidRegistration, firstNameMf, "Vincenzo"
);
registrationDataService.addMetadata(
context, orcidRegistration, lastNameMf, "Mecca"
);
registrationDataService.update(context, orcidRegistration);
return orcidRegistration;
}
} }

View File

@@ -4688,7 +4688,36 @@ public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest {
context.restoreAuthSystemState(); context.restoreAuthSystemState();
getClient().perform(get("/api/core/items/{uuid}/accessStatus", item.getID())) getClient().perform(get("/api/core/items/{uuid}/accessStatus", item.getID()))
.andExpect(status().isOk()) .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 @Test

View File

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

View File

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

View File

@@ -27,13 +27,12 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@@ -61,6 +60,7 @@ import org.dspace.content.Item;
import org.dspace.services.ConfigurationService; import org.dspace.services.ConfigurationService;
import org.exparity.hamcrest.date.LocalDateTimeMatchers; import org.exparity.hamcrest.date.LocalDateTimeMatchers;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -102,6 +102,13 @@ public class RequestItemRepositoryIT
private Bitstream bitstream; private Bitstream bitstream;
private Map<String, Object> altchaPayload;
@After
public void tearDown() {
configurationService.setProperty("captcha.provider", "google");
}
@Before @Before
public void init() public void init()
throws SQLException, AuthorizeException, IOException { throws SQLException, AuthorizeException, IOException {
@@ -130,6 +137,18 @@ public class RequestItemRepositoryIT
.withName("Bitstream") .withName("Bitstream")
.build(); .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(); context.restoreAuthSystemState();
} }
@@ -263,8 +282,6 @@ public class RequestItemRepositoryIT
@Test @Test
public void testCreateAndReturnNotAuthenticated() public void testCreateAndReturnNotAuthenticated()
throws SQLException, AuthorizeException, IOException, Exception { throws SQLException, AuthorizeException, IOException, Exception {
System.out.println("createAndReturn (not authenticated)");
// Fake up a request in REST form. // Fake up a request in REST form.
RequestItemRest rir = new RequestItemRest(); RequestItemRest rir = new RequestItemRest();
rir.setAllfiles(false); rir.setAllfiles(false);
@@ -273,10 +290,16 @@ public class RequestItemRepositoryIT
rir.setRequestEmail(RequestItemBuilder.REQ_EMAIL); rir.setRequestEmail(RequestItemBuilder.REQ_EMAIL);
rir.setRequestMessage(RequestItemBuilder.REQ_MESSAGE); rir.setRequestMessage(RequestItemBuilder.REQ_MESSAGE);
rir.setRequestName(RequestItemBuilder.REQ_NAME); rir.setRequestName(RequestItemBuilder.REQ_NAME);
String base64Payload =
"eyJjaGFsbGVuZ2UiOiJhNjY1YTQ1OTIwNDIyZjlkNDE3ZTQ4NjdlZmRjNGZiOGEwNGExZ" +
"jNmZmYxZmEwN2U5OThlODZmN2Y3YTI3YWUzIiwic2FsdCI6InNhbHQxMjMiLCJudW1iZX" +
"IiOjEsInNpZ25hdHVyZSI6ImY1Y2QzZWQ0MTYxZjVmM2M5MTRjNTc3OGU3MTZkNmI0NDZ" +
"mYTI3NzA4NmJiYjhmZDNlMmIwYzRiODlmMTg4MzMiLCJhbGdvcml0aG0iOiJTSEEtMjU2In0=";
// Create it and see if it was created correctly. // Create it and see if it was created correctly.
try { try {
getClient().perform(post(URI_ROOT) getClient().perform(post(URI_ROOT)
.header("x-captcha-payload", base64Payload)
.content(mapper.writeValueAsBytes(rir)) .content(mapper.writeValueAsBytes(rir))
.contentType(contentType)) .contentType(contentType))
.andExpect(status().isCreated()) .andExpect(status().isCreated())
@@ -314,8 +337,6 @@ public class RequestItemRepositoryIT
@Test @Test
public void testCreateAndReturnBadRequest() public void testCreateAndReturnBadRequest()
throws SQLException, AuthorizeException, IOException, Exception { throws SQLException, AuthorizeException, IOException, Exception {
System.out.println("createAndReturn (bad requests)");
// Fake up a request in REST form. // Fake up a request in REST form.
RequestItemRest rir = new RequestItemRest(); RequestItemRest rir = new RequestItemRest();
rir.setBitstreamId(bitstream.getID().toString()); rir.setBitstreamId(bitstream.getID().toString());
@@ -391,7 +412,6 @@ public class RequestItemRepositoryIT
@Test @Test
public void testCreateWithInvalidCSRF() public void testCreateWithInvalidCSRF()
throws Exception { throws Exception {
// Login via password to retrieve a valid token // Login via password to retrieve a valid token
String token = getAuthToken(eperson.getEmail(), password); String token = getAuthToken(eperson.getEmail(), password);
@@ -494,6 +514,7 @@ public class RequestItemRepositoryIT
Map<String, String> parameters = Map.of( Map<String, String> parameters = Map.of(
"acceptRequest", "true", "acceptRequest", "true",
"subject", "subject", "subject", "subject",
"accessPeriod", "+1DAY",
"responseMessage", "Request accepted", "responseMessage", "Request accepted",
"suggestOpenAccess", "true"); "suggestOpenAccess", "true");
String content = mapper String content = mapper
@@ -574,7 +595,6 @@ public class RequestItemRepositoryIT
@Test @Test
public void testPutCompletedRequest() public void testPutCompletedRequest()
throws Exception { throws Exception {
// Create an item request that is already denied. // Create an item request that is already denied.
RequestItem itemRequest = RequestItemBuilder RequestItem itemRequest = RequestItemBuilder
.createRequestItem(context, item, bitstream) .createRequestItem(context, item, bitstream)
@@ -606,38 +626,5 @@ public class RequestItemRepositoryIT
assertEquals("Wrong domain class", RequestItemRest.class, instanceClass); 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();
}
} }

View File

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

View File

@@ -47,6 +47,8 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
public class RequestCopyFeatureIT extends AbstractControllerIntegrationTest { public class RequestCopyFeatureIT extends AbstractControllerIntegrationTest {
@Autowired @Autowired

View File

@@ -102,7 +102,8 @@ public class BitstreamMatcher {
return matchEmbeds( return matchEmbeds(
"bundle", "bundle",
"format", "format",
"thumbnail" "thumbnail",
"accessStatus"
); );
} }
@@ -115,7 +116,8 @@ public class BitstreamMatcher {
"content", "content",
"format", "format",
"self", "self",
"thumbnail" "thumbnail",
"accessStatus"
); );
} }

View File

@@ -5,7 +5,6 @@
* *
* http://www.dspace.org/license/ * http://www.dspace.org/license/
*/ */
package org.dspace.app.rest.matcher; package org.dspace.app.rest.matcher;
import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath;

View File

@@ -15,7 +15,7 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
/** /**
* Test the AccessStatusRestTest class * Test the AccessStatusRest class
*/ */
public class AccessStatusRestTest { public class AccessStatusRestTest {
@@ -36,4 +36,15 @@ public class AccessStatusRestTest {
accessStatusRest.setStatus(DefaultAccessStatusHelper.UNKNOWN); accessStatusRest.setStatus(DefaultAccessStatusHelper.UNKNOWN);
assertNotNull(accessStatusRest.getStatus()); 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());
}
} }

View File

@@ -878,6 +878,19 @@ access.status.embargo.forever.year = 10000
access.status.embargo.forever.month = 1 access.status.embargo.forever.month = 1
access.status.embargo.forever.day = 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 # 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 # 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). # 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 = none
google-analytics.bundles = ORIGINAL 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------------------------# #------------------SUBMISSION CONFIGURATION------------------------#
#------------------------------------------------------------------# #------------------------------------------------------------------#
@@ -1595,12 +1591,15 @@ solr-database-resync.cron = 0 15 2 * * ?
# process-cleaner.days = 14 # process-cleaner.days = 14
#---------------------------------------------------------------# #---------------------------------------------------------------#
#----------------GOOGLE CAPTCHA CONFIGURATION-------------------# #--------------------CAPTCHA CONFIGURATION----------------------#
#---------------------------------------------------------------# #---------------------------------------------------------------#
# Enable CAPTCHA verification on ePerson registration # Enable CAPTCHA verification on ePerson registration
# (see modules/requestitem.cfg to enable CAPTCHA verification for request-a-copy)
registration.verification.enabled = false 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) # version we want to use, possible values (v2 or v3)
#google.recaptcha.version = #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. # 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 = #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--------------------------# #-------------------MODULE CONFIGURATIONS--------------------------#
#------------------------------------------------------------------# #------------------------------------------------------------------#
@@ -1647,8 +1683,8 @@ module_dir = modules
# However, please note that "include" statements in local.cfg will be loaded # However, please note that "include" statements in local.cfg will be loaded
# PRIOR to those below (and therefore may override configs in these default # PRIOR to those below (and therefore may override configs in these default
# module configuration files). # module configuration files).
include = ${module_dir}/actuator.cfg include = ${module_dir}/actuator.cfg
include = ${module_dir}/altcha.cfg
include = ${module_dir}/altmetrics.cfg include = ${module_dir}/altmetrics.cfg
include = ${module_dir}/assetstore.cfg include = ${module_dir}/assetstore.cfg
include = ${module_dir}/authentication.cfg include = ${module_dir}/authentication.cfg
@@ -1676,6 +1712,7 @@ include = ${module_dir}/openaire-client.cfg
include = ${module_dir}/orcid.cfg include = ${module_dir}/orcid.cfg
include = ${module_dir}/qaevents.cfg include = ${module_dir}/qaevents.cfg
include = ${module_dir}/rdf.cfg include = ${module_dir}/rdf.cfg
include = ${module_dir}/requestitem.cfg
include = ${module_dir}/rest.cfg include = ${module_dir}/rest.cfg
include = ${module_dir}/iiif.cfg include = ${module_dir}/iiif.cfg
include = ${module_dir}/saml-relying-party.cfg include = ${module_dir}/saml-relying-party.cfg

View File

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

View File

@@ -0,0 +1,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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More