Request-a-copy: Access expiry sent as delta or date, stored as date

This commit is contained in:
Kim Shepherd
2025-03-18 14:30:41 +01:00
parent 16bda9e043
commit 8135ba25a5
12 changed files with 244 additions and 93 deletions

View File

@@ -72,6 +72,12 @@ public class RequestItem implements ReloadableEntity<Integer> {
@Column(name = "accept_request")
private boolean accept_request;
@Column(name = "access_token", unique = true, length = 48)
private String access_token = null;
@Column(name = "access_expiry")
private Instant access_expiry = null;
/**
* Protected constructor, create object using:
* {@link org.dspace.app.requestitem.service.RequestItemService#createRequest(
@@ -85,7 +91,7 @@ public class RequestItem implements ReloadableEntity<Integer> {
return requestitem_id;
}
void setAllfiles(boolean allfiles) {
public void setAllfiles(boolean allfiles) {
this.allfiles = allfiles;
}
@@ -134,7 +140,8 @@ public class RequestItem implements ReloadableEntity<Integer> {
}
/**
* @return a unique request identifier which can be emailed.
* @return a unique request identifier which can be emailed to the *approver* of the request.
* This is not the same as the access token, which is used by the requester to access the item after approval.
*/
public String getToken() {
return token;
@@ -187,4 +194,38 @@ public class RequestItem implements ReloadableEntity<Integer> {
void setRequest_date(Instant request_date) {
this.request_date = request_date;
}
/**
* @return A unique token to be used by the requester when granted access to the resource, which
* can be emailed upon approval
*/
public String getAccess_token() {
return access_token;
}
public void setAccess_token(String access_token) {
this.access_token = access_token;
}
/**
* @return The date and time when the access token expires.
*/
public Instant getAccess_expiry() {
return access_expiry;
}
public void setAccess_expiry(Instant access_expiry) {
this.access_expiry = access_expiry;
}
/**
* Sanitize personal information and the approval token, to be used when returning a RequestItem
* to Angular, especially for users clicking on the secure link
*/
public void sanitizePersonalData() {
setReqEmail("sanitized");
setReqName("sanitized");
setReqMessage("sanitized");
// Even though [approval] token is not a name, it can be used to access the original object
setToken("sanitized");
}
}

View File

@@ -10,7 +10,9 @@ package org.dspace.app.requestitem;
import java.io.IOException;
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 jakarta.annotation.ManagedBean;
@@ -175,6 +177,12 @@ public class RequestItemEmailNotifier {
grantorAddress = grantor.getEmail();
}
// Set date format for access expiry date
String accessExpiryFormat = configurationService.getProperty("request.item.grant.link.dateformat",
"yyyy-MM-dd");
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(accessExpiryFormat)
.withZone(ZoneId.of("UTC"));
Email email;
// If this item has a secure access token, send the template with that link instead of attaching files
if (ri.isAccept_request() && ri.getAccess_token() != null) {
@@ -201,11 +209,10 @@ public class RequestItemEmailNotifier {
// {6} secure access link
email.addArgument(configurationService.getProperty("dspace.ui.url")
+ "/items/" + ri.getItem().getID()
+ "/access-by-token?accessToken=" + ri.getAccess_token());
// {7} access end date
if (ri.getAccess_period() > 0) {
DateFormat dateFormat = DateFormat.getDateInstance();
email.addArgument(dateFormat.format(ri.getAccessEndDate()));
+ "?accessToken=" + ri.getAccess_token());
// {7} access end date, but only add formatted date string if it is set and not "forever"
if (ri.getAccess_expiry() != null && !ri.getAccess_expiry().equals(Instant.MAX)) {
email.addArgument(dateTimeFormatter.format(ri.getAccess_expiry()));
} else {
email.addArgument(null);
}

View File

@@ -11,9 +11,16 @@ import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.sql.SQLException;
import java.text.ParseException;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.TimeZone;
import org.apache.http.client.utils.URIBuilder;
import org.apache.logging.log4j.LogManager;
@@ -32,8 +39,9 @@ import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.core.LogHelper;
import org.dspace.core.Utils;
import org.dspace.eperson.EPerson;
import org.dspace.services.ConfigurationService;
import org.dspace.util.DateMathParser;
import org.dspace.util.MultiFormatDateParser;
import org.springframework.beans.factory.annotation.Autowired;
/**
@@ -61,6 +69,11 @@ public class RequestItemServiceImpl implements RequestItemService {
@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() {
@@ -145,8 +158,8 @@ public class RequestItemServiceImpl implements RequestItemService {
// Save the request item
requestItemDAO.save(context, requestItem);
log.debug("Created RequestItem with ID {}, approval token {}, access token {}, access period {}",
requestItem::getID, requestItem::getToken, requestItem::getAccess_token, requestItem::getAccess_period);
log.debug("Created RequestItem with ID {}, approval token {}, access token {}, access expiry {}",
requestItem::getID, requestItem::getToken, requestItem::getAccess_token, requestItem::getAccess_expiry);
// Return the approver token
return requestItem.getToken();
@@ -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,
* 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))
// 3. Request is 'allfiles' or for this bitstream ID
&& (requestItem.isAllfiles() || bitstream.equals(requestItem.getBitstream()))
// 4. access period is 0 (forever), or the elapsed seconds since decision date is less than the
// access period granted
&& requestItem.accessPeriodCurrent()
// 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;
@@ -306,8 +347,7 @@ public class RequestItemServiceImpl implements RequestItemService {
}
/**
* 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
* Sanitize a RequestItem. The following values in the referenced RequestItem
* are nullified:
* - approver token (aka token)
* - requester name
@@ -325,28 +365,39 @@ public class RequestItemServiceImpl implements RequestItemService {
log.error("Null request item passed for sanitization, skipping");
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)
// This is the case if we have a non-admin, non-submitter or a null user/session
// 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 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();
}
}
}

View File

@@ -10,6 +10,7 @@ package org.dspace.app.requestitem.service;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.sql.SQLException;
import java.time.Instant;
import java.util.Iterator;
import java.util.List;
@@ -112,6 +113,10 @@ public interface RequestItemService {
public boolean isRestricted(Context context, DSpaceObject o)
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,
* either return cleanly or throw an AuthorizeException

View File

@@ -8,6 +8,6 @@
-- 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_period column to hold a time delta in seconds (from decision_date timestamp) to calculate validity
-- and expiry, with NULL interpreted as 'forever' (if accept_request is true). int4 allows for 68 year period max.
ALTER TABLE requestitem ADD COLUMN IF NOT EXISTS access_period INT4;
-- 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

@@ -21,6 +21,13 @@ import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.sql.Date;
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 org.dspace.AbstractUnitTest;
@@ -44,6 +51,7 @@ 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.dspace.util.MultiFormatDateParser;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
@@ -162,12 +170,13 @@ public class RequestItemTest extends AbstractUnitTest {
@Test
public void testAuthorizeWithValidPeriod() throws Exception {
Instant decisionDate = getYesterdayAsInstant();
RequestItem request = RequestItemBuilder
.createRequestItem(context, item, bitstream)
.withAcceptRequest(true)
.withAccessToken("test-token")
.withDecisionDate(new Date(System.currentTimeMillis() + 86400000)) // Yesterday
.withAccessPeriod(10 * 86400) // 10 day period
.withDecisionDate(decisionDate) // Yesterday
.withAccessExpiry(getExpiryAsInstant("+10DAYS", decisionDate)) // 10 day period
.build();
// The access token should be valid so we expect no exceptions
@@ -181,12 +190,13 @@ public class RequestItemTest extends AbstractUnitTest {
@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(new Date(System.currentTimeMillis() - 86400000)) // Yesterday
.withAccessPeriod(86400) // 1 day period
.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
@@ -207,12 +217,13 @@ public class RequestItemTest extends AbstractUnitTest {
@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(new Date(System.currentTimeMillis() - 86400000)) // Yesterday
.withAccessPeriod(0) // forever
.withDecisionDate(decisionDate) // Yesterday
.withAccessExpiry(getExpiryAsInstant("FOREVER", decisionDate)) // forever
.build();
// The access token should NOT valid so we expect to catch an AuthorizeException
@@ -262,29 +273,32 @@ public class RequestItemTest extends AbstractUnitTest {
@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(new Date(System.currentTimeMillis()));
request.setAccess_period(7); // 7 day access
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());
assertNotNull(found.getDecision_date());
assertEquals(7, found.getAccess_period());
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(new Date(System.currentTimeMillis()));
request.setDecision_date(decisionDate);
requestItemService.update(context, request);
RequestItem found = requestItemService.findByToken(context, request.getToken());
@@ -314,19 +328,22 @@ public class RequestItemTest extends AbstractUnitTest {
@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(new Date(System.currentTimeMillis()))
.withAccessPeriod(30)
.withDecisionDate(decisionDate)
.withAccessExpiry(getExpiryAsInstant("+1DAY", decisionDate))
.build();
request.setAccess_period(60);
// 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(60, found.getAccess_period());
assertEquals(expectedExpiryDate, found.getAccess_expiry());
assertTrue(found.isAccept_request());
assertTrue(found.isAllfiles());
}
@@ -359,6 +376,17 @@ public class RequestItemTest extends AbstractUnitTest {
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

@@ -40,7 +40,7 @@ public class RequestItemBuilder
private Instant decisionDate;
private boolean accepted;
private String accessToken = null;
private int accessPeriod;
private Instant accessExpiry = null;
private boolean allFiles;
protected RequestItemBuilder(Context context) {
@@ -95,8 +95,8 @@ public class RequestItemBuilder
return this;
}
public RequestItemBuilder withAccessPeriod(int accessPeriod) {
this.accessPeriod = accessPeriod;
public RequestItemBuilder withAccessExpiry(Instant accessExpiry) {
this.accessExpiry = accessExpiry;
return this;
}
@@ -128,7 +128,7 @@ public class RequestItemBuilder
if (accessToken != null) {
requestItem.setAccess_token(accessToken);
}
requestItem.setAccess_period(accessPeriod);
requestItem.setAccess_expiry(accessExpiry);
requestItem.setAllfiles(allFiles);
requestItemService.update(context, requestItem);

View File

@@ -46,7 +46,7 @@ public class RequestItemConverter
requestItemRest.setRequestDate(requestItem.getRequest_date());
requestItemRest.setToken(requestItem.getToken());
requestItemRest.setAccessToken(requestItem.getAccess_token());
requestItemRest.setAccessPeriod(requestItem.getAccess_period());
requestItemRest.setAccessExpiry(requestItem.getAccess_expiry());
return requestItemRest;
}

View File

@@ -56,9 +56,7 @@ public class RequestItemRest extends BaseObjectRest<Integer> {
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
protected String accessToken;
protected int accessPeriod;
protected String decisionNote;
protected Instant accessExpiry;
/**
* @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() {
return accessPeriod;
public Instant getAccessExpiry() {
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) {
this.accessPeriod = accessPeriod;
public void setAccessExpiry(Instant accessExpiry) {
this.accessExpiry = accessExpiry;
}
/*

View File

@@ -14,14 +14,19 @@ import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.sql.SQLException;
import java.text.ParseException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.LinkedList;
import java.util.List;
import java.util.TimeZone;
import java.util.UUID;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import net.bytebuddy.asm.Advice;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.validator.routines.EmailValidator;
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.service.CaptchaService;
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.data.domain.Page;
import org.springframework.data.domain.Pageable;
@@ -291,15 +300,13 @@ public class RequestItemRepository
// Set the decision date (now)`
ri.setDecision_date(Instant.now());
// If the (optional) access period was included, extract it here and set accordingly
JsonNode accessPeriodNode = requestBody.findValue("accessPeriod");
int accessPeriod = 0;
if (accessPeriodNode != null && !accessPeriodNode.isNull()) {
accessPeriod = accessPeriodNode.asInt(0);
}
// If a valid access period was set, update the request, otherwise we will leave it as null
if (accessPeriod > 0) {
ri.setAccess_period(accessPeriod);
// 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");
@@ -307,7 +314,6 @@ public class RequestItemRepository
if (responseSubjectNode != null && !responseSubjectNode.isNull()) {
subject = responseSubjectNode.asText();
}
ri.setDecision_date(Instant.now());
requestItemService.update(context, ri);
// Send the response email
@@ -356,8 +362,10 @@ public class RequestItemRepository
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
if (!requestItem.isAccept_request() || !requestItem.accessPeriodCurrent()) {
// Send 403 FORBIDDEN if request access has not been granted or access period is null or in the past
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");
}

View File

@@ -34,6 +34,7 @@ import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;

View File

@@ -7,37 +7,49 @@
# 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
# Bundles to use when granting access and inspecting file size, if "all files" is indicated
# Default: ORIGINAL
#request.item.grant.bundles = ORIGINAL
# By default a granted request will send the file as an attachment.
## Bundles to use when granting access and inspecting file size, if "all files" is indicated
## Default: ORIGINAL
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
# Default: false
#request.item.grant.link = true
# 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.
# If 'all files' was indicated in the request, this threshold will be activated if any of the files
# meet the configured size minimum.
# To send links instead of attachments for all files, set this property to 0.
# 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.
# 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.
# 1 day = 86400
# 7 days = 604800
#request.item.grant.link.period = 0
#request.item.grant.link.period = 120
#request.item.grant.link.period = 240
#request.item.grant.link.period = 86400
#request.item.grant.link.period = 604800
#
request.item.grant.link.period = FOREVER
request.item.grant.link.period = +1DAY
request.item.grant.link.period = +1WEEK
request.item.grant.link.period = +1MONTH
request.item.grant.link.period = +3MONTHS
# 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
# Default: false
#request.item.create.captcha=true