mirror of
https://github.com/DSpace/DSpace.git
synced 2025-10-15 14:03:17 +00:00
Request-a-copy: Access expiry sent as delta or date, stored as date
This commit is contained in:
@@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,7 +10,9 @@ package org.dspace.app.requestitem;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.text.DateFormat;
|
import java.time.Instant;
|
||||||
|
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;
|
||||||
@@ -175,6 +177,12 @@ 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;
|
Email email;
|
||||||
// If this item has a secure access token, send the template with that link instead of attaching files
|
// 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) {
|
if (ri.isAccept_request() && ri.getAccess_token() != null) {
|
||||||
@@ -201,11 +209,10 @@ public class RequestItemEmailNotifier {
|
|||||||
// {6} secure access link
|
// {6} secure access link
|
||||||
email.addArgument(configurationService.getProperty("dspace.ui.url")
|
email.addArgument(configurationService.getProperty("dspace.ui.url")
|
||||||
+ "/items/" + ri.getItem().getID()
|
+ "/items/" + ri.getItem().getID()
|
||||||
+ "/access-by-token?accessToken=" + ri.getAccess_token());
|
+ "?accessToken=" + ri.getAccess_token());
|
||||||
// {7} access end date
|
// {7} access end date, but only add formatted date string if it is set and not "forever"
|
||||||
if (ri.getAccess_period() > 0) {
|
if (ri.getAccess_expiry() != null && !ri.getAccess_expiry().equals(Instant.MAX)) {
|
||||||
DateFormat dateFormat = DateFormat.getDateInstance();
|
email.addArgument(dateTimeFormatter.format(ri.getAccess_expiry()));
|
||||||
email.addArgument(dateFormat.format(ri.getAccessEndDate()));
|
|
||||||
} else {
|
} else {
|
||||||
email.addArgument(null);
|
email.addArgument(null);
|
||||||
}
|
}
|
||||||
|
@@ -11,9 +11,16 @@ import java.net.MalformedURLException;
|
|||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
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.Date;
|
||||||
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.http.client.utils.URIBuilder;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
@@ -32,8 +39,9 @@ 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.eperson.EPerson;
|
|
||||||
import org.dspace.services.ConfigurationService;
|
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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,6 +69,11 @@ public class RequestItemServiceImpl implements RequestItemService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
protected ConfigurationService configurationService;
|
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;
|
private static final int DEFAULT_MINIMUM_FILE_SIZE = 20;
|
||||||
|
|
||||||
protected RequestItemServiceImpl() {
|
protected RequestItemServiceImpl() {
|
||||||
@@ -145,8 +158,8 @@ public class RequestItemServiceImpl implements RequestItemService {
|
|||||||
// Save the request item
|
// Save the request item
|
||||||
requestItemDAO.save(context, requestItem);
|
requestItemDAO.save(context, requestItem);
|
||||||
|
|
||||||
log.debug("Created RequestItem with ID {}, approval token {}, access token {}, access period {}",
|
log.debug("Created RequestItem with ID {}, approval token {}, access token {}, access expiry {}",
|
||||||
requestItem::getID, requestItem::getToken, requestItem::getAccess_token, requestItem::getAccess_period);
|
requestItem::getID, requestItem::getToken, requestItem::getAccess_token, requestItem::getAccess_expiry);
|
||||||
|
|
||||||
// Return the approver token
|
// Return the approver token
|
||||||
return requestItem.getToken();
|
return requestItem.getToken();
|
||||||
@@ -225,6 +238,35 @@ public class RequestItemServiceImpl implements RequestItemService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
* Taking into account 'accepted' flag, bitstream id or allfiles flag, decision date and access period,
|
||||||
* either return cleanly or throw an AuthorizeException
|
* either return cleanly or throw an AuthorizeException
|
||||||
@@ -247,9 +289,8 @@ public class RequestItemServiceImpl implements RequestItemService {
|
|||||||
&& (requestItem.getAccess_token() != null && requestItem.getAccess_token().equals(accessToken))
|
&& (requestItem.getAccess_token() != null && requestItem.getAccess_token().equals(accessToken))
|
||||||
// 3. Request is 'allfiles' or for this bitstream ID
|
// 3. Request is 'allfiles' or for this bitstream ID
|
||||||
&& (requestItem.isAllfiles() || bitstream.equals(requestItem.getBitstream()))
|
&& (requestItem.isAllfiles() || bitstream.equals(requestItem.getBitstream()))
|
||||||
// 4. access period is 0 (forever), or the elapsed seconds since decision date is less than the
|
// 4. access expiry timestamp is null (forever), or is *after* the current time
|
||||||
// access period granted
|
&& (requestItem.getAccess_expiry() == null || requestItem.getAccess_expiry().isAfter(Instant.now()))
|
||||||
&& requestItem.accessPeriodCurrent()
|
|
||||||
) {
|
) {
|
||||||
log.info("Authorizing access to bitstream {} by access token", bitstream.getID());
|
log.info("Authorizing access to bitstream {} by access token", bitstream.getID());
|
||||||
return;
|
return;
|
||||||
@@ -306,8 +347,7 @@ public class RequestItemServiceImpl implements RequestItemService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sanitize a RequestItem depending on the current session user. If the current user is not
|
* Sanitize a RequestItem. The following values in the referenced RequestItem
|
||||||
* the approver, an administrator or other privileged group, the following values in the return object
|
|
||||||
* are nullified:
|
* are nullified:
|
||||||
* - approver token (aka token)
|
* - approver token (aka token)
|
||||||
* - requester name
|
* - requester name
|
||||||
@@ -325,28 +365,39 @@ public class RequestItemServiceImpl implements RequestItemService {
|
|||||||
log.error("Null request item passed for sanitization, skipping");
|
log.error("Null request item passed for sanitization, skipping");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (null != context) {
|
|
||||||
// Get current user, if any
|
|
||||||
EPerson currentUser = context.getCurrentUser();
|
|
||||||
// Get item
|
|
||||||
Item item = requestItem.getItem();
|
|
||||||
if (null != currentUser) {
|
|
||||||
try {
|
|
||||||
if (currentUser == requestItem.getItem().getSubmitter()
|
|
||||||
&& authorizeService.isAdmin(context, requestItem.getItem())) {
|
|
||||||
// Return original object, this person technically had full access to the request item data via
|
|
||||||
// the original approval link
|
|
||||||
log.debug("User is authorized to receive all request item data: {}", currentUser.getEmail());
|
|
||||||
}
|
|
||||||
} catch (SQLException e) {
|
|
||||||
log.error("Could not determine isAdmin for item {}: {}",item.getID(), e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// By default, sanitize (strips requester name, email, message, and the approver token)
|
// Sanitized referenced data (strips requester name, email, message, and the approver token)
|
||||||
// This is the case if we have a non-admin, non-submitter or a null user/session
|
|
||||||
requestItem.sanitizePersonalData();
|
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 Instant.MAX;
|
||||||
|
}
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,6 +10,7 @@ package org.dspace.app.requestitem.service;
|
|||||||
import java.net.MalformedURLException;
|
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.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -112,6 +113,10 @@ public interface RequestItemService {
|
|||||||
public boolean isRestricted(Context context, DSpaceObject o)
|
public boolean isRestricted(Context context, DSpaceObject o)
|
||||||
throws SQLException;
|
throws SQLException;
|
||||||
|
|
||||||
|
void setAccessExpiry(RequestItem requestItem, Instant accessExpiry);
|
||||||
|
|
||||||
|
void setAccessExpiry(RequestItem requestItem, String delta);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Taking into account 'accepted' flag, bitstream id or allfiles flag, decision date and access period,
|
* Taking into account 'accepted' flag, bitstream id or allfiles flag, decision date and access period,
|
||||||
* either return cleanly or throw an AuthorizeException
|
* either return cleanly or throw an AuthorizeException
|
||||||
|
@@ -8,6 +8,6 @@
|
|||||||
|
|
||||||
-- Add new access_token column to hold a secure access token for the requestor to use for weblink-based access
|
-- 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);
|
ALTER TABLE requestitem ADD COLUMN IF NOT EXISTS access_token VARCHAR(48);
|
||||||
-- Add new access_period column to hold a time delta in seconds (from decision_date timestamp) to calculate validity
|
-- Add new access_expiry DATESTAMP column to hold the expiry date of the access token
|
||||||
-- and expiry, with NULL interpreted as 'forever' (if accept_request is true). int4 allows for 68 year period max.
|
-- (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_period INT4;
|
ALTER TABLE requestitem ADD COLUMN IF NOT EXISTS access_expiry TIMESTAMP;
|
@@ -21,6 +21,13 @@ import java.net.MalformedURLException;
|
|||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.sql.Date;
|
import java.sql.Date;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
|
||||||
import org.dspace.AbstractUnitTest;
|
import org.dspace.AbstractUnitTest;
|
||||||
@@ -44,6 +51,7 @@ import org.dspace.handle.factory.HandleServiceFactory;
|
|||||||
import org.dspace.handle.service.HandleService;
|
import org.dspace.handle.service.HandleService;
|
||||||
import org.dspace.services.ConfigurationService;
|
import org.dspace.services.ConfigurationService;
|
||||||
import org.dspace.services.factory.DSpaceServicesFactory;
|
import org.dspace.services.factory.DSpaceServicesFactory;
|
||||||
|
import org.dspace.util.MultiFormatDateParser;
|
||||||
import org.junit.AfterClass;
|
import org.junit.AfterClass;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
@@ -162,12 +170,13 @@ public class RequestItemTest extends AbstractUnitTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAuthorizeWithValidPeriod() throws Exception {
|
public void testAuthorizeWithValidPeriod() throws Exception {
|
||||||
|
Instant decisionDate = getYesterdayAsInstant();
|
||||||
RequestItem request = RequestItemBuilder
|
RequestItem request = RequestItemBuilder
|
||||||
.createRequestItem(context, item, bitstream)
|
.createRequestItem(context, item, bitstream)
|
||||||
.withAcceptRequest(true)
|
.withAcceptRequest(true)
|
||||||
.withAccessToken("test-token")
|
.withAccessToken("test-token")
|
||||||
.withDecisionDate(new Date(System.currentTimeMillis() + 86400000)) // Yesterday
|
.withDecisionDate(decisionDate) // Yesterday
|
||||||
.withAccessPeriod(10 * 86400) // 10 day period
|
.withAccessExpiry(getExpiryAsInstant("+10DAYS", decisionDate)) // 10 day period
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// The access token should be valid so we expect no exceptions
|
// The access token should be valid so we expect no exceptions
|
||||||
@@ -181,12 +190,13 @@ public class RequestItemTest extends AbstractUnitTest {
|
|||||||
|
|
||||||
@Test(expected = AuthorizeException.class)
|
@Test(expected = AuthorizeException.class)
|
||||||
public void testAuthorizeWithExpiredPeriod() throws Exception {
|
public void testAuthorizeWithExpiredPeriod() throws Exception {
|
||||||
|
Instant decisionDate = getYesterdayAsInstant();
|
||||||
RequestItem request = RequestItemBuilder
|
RequestItem request = RequestItemBuilder
|
||||||
.createRequestItem(context, item, bitstream)
|
.createRequestItem(context, item, bitstream)
|
||||||
.withAcceptRequest(true)
|
.withAcceptRequest(true)
|
||||||
.withAccessToken("test-token")
|
.withAccessToken("test-token")
|
||||||
.withDecisionDate(new Date(System.currentTimeMillis() - 86400000)) // Yesterday
|
.withDecisionDate(decisionDate) // Yesterday
|
||||||
.withAccessPeriod(86400) // 1 day period
|
.withAccessExpiry(getExpiryAsInstant("+1DAY", decisionDate)) // 1 day period
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// The access token should not be valid so we expect to catch an AuthorizeException
|
// The access token should not be valid so we expect to catch an AuthorizeException
|
||||||
@@ -207,12 +217,13 @@ public class RequestItemTest extends AbstractUnitTest {
|
|||||||
|
|
||||||
@Test(expected = AuthorizeException.class)
|
@Test(expected = AuthorizeException.class)
|
||||||
public void testAuthorizeWithMismatchedToken() throws Exception {
|
public void testAuthorizeWithMismatchedToken() throws Exception {
|
||||||
|
Instant decisionDate = getYesterdayAsInstant();
|
||||||
RequestItem request = RequestItemBuilder
|
RequestItem request = RequestItemBuilder
|
||||||
.createRequestItem(context, item, bitstream)
|
.createRequestItem(context, item, bitstream)
|
||||||
.withAcceptRequest(true)
|
.withAcceptRequest(true)
|
||||||
.withAccessToken("test-token")
|
.withAccessToken("test-token")
|
||||||
.withDecisionDate(new Date(System.currentTimeMillis() - 86400000)) // Yesterday
|
.withDecisionDate(decisionDate) // Yesterday
|
||||||
.withAccessPeriod(0) // forever
|
.withAccessExpiry(getExpiryAsInstant("FOREVER", decisionDate)) // forever
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// The access token should NOT valid so we expect to catch an AuthorizeException
|
// The access token should NOT valid so we expect to catch an AuthorizeException
|
||||||
@@ -262,29 +273,32 @@ public class RequestItemTest extends AbstractUnitTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGrantRequestWithAccessPeriod() throws Exception {
|
public void testGrantRequestWithAccessPeriod() throws Exception {
|
||||||
|
Instant decisionDate = Instant.now();
|
||||||
|
Instant expectedExpiryDate = decisionDate.plus(7, ChronoUnit.DAYS);
|
||||||
RequestItem request = RequestItemBuilder
|
RequestItem request = RequestItemBuilder
|
||||||
.createRequestItem(context, item, bitstream)
|
.createRequestItem(context, item, bitstream)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
request.setAccept_request(true);
|
request.setAccept_request(true);
|
||||||
request.setDecision_date(new Date(System.currentTimeMillis()));
|
request.setDecision_date(decisionDate);
|
||||||
request.setAccess_period(7); // 7 day access
|
request.setAccess_expiry(getExpiryAsInstant("+7DAYS", decisionDate)); // 7 day access
|
||||||
requestItemService.update(context, request);
|
requestItemService.update(context, request);
|
||||||
|
|
||||||
RequestItem found = requestItemService.findByToken(context, request.getToken());
|
RequestItem found = requestItemService.findByToken(context, request.getToken());
|
||||||
assertTrue(found.isAccept_request());
|
assertTrue(found.isAccept_request());
|
||||||
assertNotNull(found.getDecision_date());
|
assertEquals(decisionDate, found.getDecision_date());
|
||||||
assertEquals(7, found.getAccess_period());
|
assertEquals(expectedExpiryDate, found.getAccess_expiry());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testDenyRequest() throws Exception {
|
public void testDenyRequest() throws Exception {
|
||||||
|
Instant decisionDate = Instant.now();
|
||||||
RequestItem request = RequestItemBuilder
|
RequestItem request = RequestItemBuilder
|
||||||
.createRequestItem(context, item, bitstream)
|
.createRequestItem(context, item, bitstream)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
request.setAccept_request(false);
|
request.setAccept_request(false);
|
||||||
request.setDecision_date(new Date(System.currentTimeMillis()));
|
request.setDecision_date(decisionDate);
|
||||||
requestItemService.update(context, request);
|
requestItemService.update(context, request);
|
||||||
|
|
||||||
RequestItem found = requestItemService.findByToken(context, request.getToken());
|
RequestItem found = requestItemService.findByToken(context, request.getToken());
|
||||||
@@ -314,19 +328,22 @@ public class RequestItemTest extends AbstractUnitTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testModifyGrantedRequest() throws Exception {
|
public void testModifyGrantedRequest() throws Exception {
|
||||||
|
Instant decisionDate = Instant.now();
|
||||||
|
Instant expectedExpiryDate = decisionDate.plus(10, ChronoUnit.DAYS);
|
||||||
RequestItem request = RequestItemBuilder
|
RequestItem request = RequestItemBuilder
|
||||||
.createRequestItem(context, item, bitstream)
|
.createRequestItem(context, item, bitstream)
|
||||||
.withAcceptRequest(true)
|
.withAcceptRequest(true)
|
||||||
.withDecisionDate(new Date(System.currentTimeMillis()))
|
.withDecisionDate(decisionDate)
|
||||||
.withAccessPeriod(30)
|
.withAccessExpiry(getExpiryAsInstant("+1DAY", decisionDate))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
request.setAccess_period(60);
|
// Manually set new expiry date
|
||||||
|
request.setAccess_expiry(getExpiryAsInstant("+10DAYS", decisionDate));
|
||||||
request.setAllfiles(true);
|
request.setAllfiles(true);
|
||||||
requestItemService.update(context, request);
|
requestItemService.update(context, request);
|
||||||
|
|
||||||
RequestItem found = requestItemService.findByToken(context, request.getToken());
|
RequestItem found = requestItemService.findByToken(context, request.getToken());
|
||||||
assertEquals(60, found.getAccess_period());
|
assertEquals(expectedExpiryDate, found.getAccess_expiry());
|
||||||
assertTrue(found.isAccept_request());
|
assertTrue(found.isAccept_request());
|
||||||
assertTrue(found.isAllfiles());
|
assertTrue(found.isAllfiles());
|
||||||
}
|
}
|
||||||
@@ -359,6 +376,17 @@ public class RequestItemTest extends AbstractUnitTest {
|
|||||||
configurationService.reloadConfig();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -40,7 +40,7 @@ public class RequestItemBuilder
|
|||||||
private Instant decisionDate;
|
private Instant decisionDate;
|
||||||
private boolean accepted;
|
private boolean accepted;
|
||||||
private String accessToken = null;
|
private String accessToken = null;
|
||||||
private int accessPeriod;
|
private Instant accessExpiry = null;
|
||||||
private boolean allFiles;
|
private boolean allFiles;
|
||||||
|
|
||||||
protected RequestItemBuilder(Context context) {
|
protected RequestItemBuilder(Context context) {
|
||||||
@@ -95,8 +95,8 @@ public class RequestItemBuilder
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RequestItemBuilder withAccessPeriod(int accessPeriod) {
|
public RequestItemBuilder withAccessExpiry(Instant accessExpiry) {
|
||||||
this.accessPeriod = accessPeriod;
|
this.accessExpiry = accessExpiry;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ public class RequestItemBuilder
|
|||||||
if (accessToken != null) {
|
if (accessToken != null) {
|
||||||
requestItem.setAccess_token(accessToken);
|
requestItem.setAccess_token(accessToken);
|
||||||
}
|
}
|
||||||
requestItem.setAccess_period(accessPeriod);
|
requestItem.setAccess_expiry(accessExpiry);
|
||||||
requestItem.setAllfiles(allFiles);
|
requestItem.setAllfiles(allFiles);
|
||||||
|
|
||||||
requestItemService.update(context, requestItem);
|
requestItemService.update(context, requestItem);
|
||||||
|
@@ -46,7 +46,7 @@ public class RequestItemConverter
|
|||||||
requestItemRest.setRequestDate(requestItem.getRequest_date());
|
requestItemRest.setRequestDate(requestItem.getRequest_date());
|
||||||
requestItemRest.setToken(requestItem.getToken());
|
requestItemRest.setToken(requestItem.getToken());
|
||||||
requestItemRest.setAccessToken(requestItem.getAccess_token());
|
requestItemRest.setAccessToken(requestItem.getAccess_token());
|
||||||
requestItemRest.setAccessPeriod(requestItem.getAccess_period());
|
requestItemRest.setAccessExpiry(requestItem.getAccess_expiry());
|
||||||
return requestItemRest;
|
return requestItemRest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -56,9 +56,7 @@ public class RequestItemRest extends BaseObjectRest<Integer> {
|
|||||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||||
protected String accessToken;
|
protected String accessToken;
|
||||||
|
|
||||||
protected int accessPeriod;
|
protected Instant accessExpiry;
|
||||||
|
|
||||||
protected String decisionNote;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the bitstream requested.
|
* @return the bitstream requested.
|
||||||
@@ -229,17 +227,17 @@ public class RequestItemRest extends BaseObjectRest<Integer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return access period (in seconds) which indicates how long this access is valid from the decision date
|
* @return the date the access token expires.
|
||||||
*/
|
*/
|
||||||
public int getAccessPeriod() {
|
public Instant getAccessExpiry() {
|
||||||
return accessPeriod;
|
return this.accessExpiry;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param accessPeriod access period, in seconds, indicating how long this access is valid from the decision date
|
* @param accessExpiry the date the access token expires.
|
||||||
*/
|
*/
|
||||||
public void setAccessPeriod(int accessPeriod) {
|
public void setAccessExpiry(Instant accessExpiry) {
|
||||||
this.accessPeriod = accessPeriod;
|
this.accessExpiry = accessExpiry;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@@ -14,14 +14,19 @@ import java.io.IOException;
|
|||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.text.ParseException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.TimeZone;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import net.bytebuddy.asm.Advice;
|
||||||
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.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
@@ -49,6 +54,10 @@ import org.dspace.eperson.InvalidReCaptchaException;
|
|||||||
import org.dspace.eperson.factory.CaptchaServiceFactory;
|
import org.dspace.eperson.factory.CaptchaServiceFactory;
|
||||||
import org.dspace.eperson.service.CaptchaService;
|
import org.dspace.eperson.service.CaptchaService;
|
||||||
import org.dspace.services.ConfigurationService;
|
import org.dspace.services.ConfigurationService;
|
||||||
|
import org.dspace.util.DateMathParser;
|
||||||
|
import static org.dspace.util.MultiFormatDateParser.parse;
|
||||||
|
|
||||||
|
import org.dspace.util.MultiFormatDateParser;
|
||||||
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;
|
||||||
@@ -291,15 +300,13 @@ public class RequestItemRepository
|
|||||||
// Set the decision date (now)`
|
// Set the decision date (now)`
|
||||||
ri.setDecision_date(Instant.now());
|
ri.setDecision_date(Instant.now());
|
||||||
|
|
||||||
// If the (optional) access period was included, extract it here and set accordingly
|
// If the (optional) access expiry period was included, extract it here and set accordingly
|
||||||
JsonNode accessPeriodNode = requestBody.findValue("accessPeriod");
|
// We expect it to be sent either as a timestamp or as a delta math like +7DAYS
|
||||||
int accessPeriod = 0;
|
JsonNode accessPeriod = requestBody.findValue("accessPeriod");
|
||||||
if (accessPeriodNode != null && !accessPeriodNode.isNull()) {
|
if (accessPeriod != null && !accessPeriod.isNull()) {
|
||||||
accessPeriod = accessPeriodNode.asInt(0);
|
// 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
|
||||||
// If a valid access period was set, update the request, otherwise we will leave it as null
|
requestItemService.setAccessExpiry(ri, accessPeriod.asText());
|
||||||
if (accessPeriod > 0) {
|
|
||||||
ri.setAccess_period(accessPeriod);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonNode responseSubjectNode = requestBody.findValue("subject");
|
JsonNode responseSubjectNode = requestBody.findValue("subject");
|
||||||
@@ -307,7 +314,6 @@ public class RequestItemRepository
|
|||||||
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
|
||||||
@@ -356,8 +362,10 @@ public class RequestItemRepository
|
|||||||
throw new ResourceNotFoundException("No such request item for accessToken=" + accessToken);
|
throw new ResourceNotFoundException("No such request item for accessToken=" + accessToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send 403 FORBIDDEN if request access has not been granted or access period is in the past
|
// Send 403 FORBIDDEN if request access has not been granted or access period is null or in the past
|
||||||
if (!requestItem.isAccept_request() || !requestItem.accessPeriodCurrent()) {
|
if (!requestItem.isAccept_request() ||
|
||||||
|
requestItem.getAccess_expiry() == null ||
|
||||||
|
requestItem.getAccess_expiry().isBefore(Instant.now())) {
|
||||||
throw new AccessDeniedException("Access has not been granted for this item request");
|
throw new AccessDeniedException("Access has not been granted for this item request");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -34,6 +34,7 @@ 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;
|
||||||
|
@@ -7,37 +7,49 @@
|
|||||||
# logged - Login is mandatory to request an item
|
# logged - Login is mandatory to request an item
|
||||||
# empty/commented out - request-copy not allowed
|
# empty/commented out - request-copy not allowed
|
||||||
#request.item.type = all
|
#request.item.type = all
|
||||||
|
|
||||||
# Should all Request Copy emails go to the helpdesk instead of the item submitter?
|
# Should all Request Copy emails go to the helpdesk instead of the item submitter?
|
||||||
#request.item.helpdesk.override = false
|
#request.item.helpdesk.override = false
|
||||||
|
|
||||||
# Should a rejection of a copy request send an email back to the requester?
|
# 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.
|
# Defaults to "true", which means a rejection email is sent back.
|
||||||
# Setting it to "false" results in a silent rejection.
|
# Setting it to "false" results in a silent rejection.
|
||||||
#request.item.reject.email = true
|
#request.item.reject.email = true
|
||||||
# Bundles to use when granting access and inspecting file size, if "all files" is indicated
|
|
||||||
# Default: ORIGINAL
|
## Bundles to use when granting access and inspecting file size, if "all files" is indicated
|
||||||
#request.item.grant.bundles = ORIGINAL
|
## Default: ORIGINAL
|
||||||
# By default a granted request will send the file as an attachment.
|
request.item.grant.bundles = ORIGINAL
|
||||||
|
|
||||||
|
## By default a granted request will send the file as an attachment.
|
||||||
# If a secure web link to the bitstream(s) should be sent in some cases, enable the following property
|
# If a secure web link to the bitstream(s) should be sent in some cases, enable the following property
|
||||||
# Default: false
|
# Default: false
|
||||||
#request.item.grant.link = true
|
#request.item.grant.link = true
|
||||||
|
|
||||||
# If request.item.grant.link is enabled, you can specify a minimum file size (in megabytes) to use as
|
# If request.item.grant.link is enabled, you can specify a minimum file size (in megabytes) to use as
|
||||||
# a threshold, or else the default attachment functionality will be used instead.
|
# a threshold, or else the default attachment functionality will be used instead.
|
||||||
# If 'all files' was indicated in the request, this threshold will be activated if any of the files
|
# If 'all files' was indicated in the request, this threshold will be activated if any of the files
|
||||||
# meet the configured size minimum.
|
# meet the configured size minimum.
|
||||||
# To send links instead of attachments for all files, set this property to 0.
|
# To send links instead of attachments for all files, set this property to 0.
|
||||||
# Default: 20
|
# Default: 20
|
||||||
#request.item.grant.link.filesize = 0
|
request.item.grant.link.filesize = 0
|
||||||
|
|
||||||
# Valid access periods, in seconds, to allow the approver to select when using links.
|
# Valid access periods, in seconds, to allow the approver to select when using links.
|
||||||
# These are presented to users with friendly labels via i18n keys in the angular frontend.
|
# These are presented to users with friendly labels via i18n keys in the angular frontend.
|
||||||
# A 0 (or null) access period will be interpreted as 'forever'.
|
# These should be in a format like +<n><unit>, where <n> is the number of units and <unit> is one of
|
||||||
|
# "SECONDS", "MINUTES", "HOURS", "DAYS", "WEEKS", "MONTHS", "YEARS". (or singular if n=1)
|
||||||
|
# The default is FOREVER, which will set a very distant expiry date for permanent access.
|
||||||
# The first access period in the list will be the default option in the grant form dropdown.
|
# The first access period in the list will be the default option in the grant form dropdown.
|
||||||
# 1 day = 86400
|
#
|
||||||
# 7 days = 604800
|
request.item.grant.link.period = FOREVER
|
||||||
#request.item.grant.link.period = 0
|
request.item.grant.link.period = +1DAY
|
||||||
#request.item.grant.link.period = 120
|
request.item.grant.link.period = +1WEEK
|
||||||
#request.item.grant.link.period = 240
|
request.item.grant.link.period = +1MONTH
|
||||||
#request.item.grant.link.period = 86400
|
request.item.grant.link.period = +3MONTHS
|
||||||
#request.item.grant.link.period = 604800
|
|
||||||
|
# Date format to use for the access expiry date in the grant email
|
||||||
|
request.item.grant.link.dateformat = dd/MM/yyyy HH:mm:ss
|
||||||
|
#request.item.grant.link.dateformat = MM/dd/yy
|
||||||
|
|
||||||
# Require a captcha for item request creation
|
# Require a captcha for item request creation
|
||||||
# Default: false
|
# Default: false
|
||||||
#request.item.create.captcha=true
|
#request.item.create.captcha=true
|
||||||
|
Reference in New Issue
Block a user