mirror of
https://github.com/DSpace/DSpace.git
synced 2025-10-07 01:54:22 +00:00
Request-a-copy improvement: Core model, DAO, service changes
This commit is contained in:
@@ -7,25 +7,33 @@
|
||||
*/
|
||||
package org.dspace.app.requestitem;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.sql.SQLException;
|
||||
import java.time.Instant;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.http.client.utils.URIBuilder;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.dspace.app.requestitem.dao.RequestItemDAO;
|
||||
import org.dspace.app.requestitem.service.RequestItemService;
|
||||
import org.dspace.authorize.AuthorizeException;
|
||||
import org.dspace.authorize.ResourcePolicy;
|
||||
import org.dspace.authorize.service.AuthorizeService;
|
||||
import org.dspace.authorize.service.ResourcePolicyService;
|
||||
import org.dspace.content.Bitstream;
|
||||
import org.dspace.content.Bundle;
|
||||
import org.dspace.content.DSpaceObject;
|
||||
import org.dspace.content.Item;
|
||||
import org.dspace.core.Constants;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.core.LogHelper;
|
||||
import org.dspace.core.Utils;
|
||||
import org.dspace.eperson.EPerson;
|
||||
import org.dspace.services.ConfigurationService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
/**
|
||||
@@ -35,6 +43,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
* This class should never be accessed directly.
|
||||
*
|
||||
* @author kevinvandevelde at atmire.com
|
||||
* @author Kim Shepherd
|
||||
*/
|
||||
public class RequestItemServiceImpl implements RequestItemService {
|
||||
|
||||
@@ -49,16 +58,38 @@ public class RequestItemServiceImpl implements RequestItemService {
|
||||
@Autowired(required = true)
|
||||
protected ResourcePolicyService resourcePolicyService;
|
||||
|
||||
@Autowired
|
||||
protected ConfigurationService configurationService;
|
||||
|
||||
private static final int DEFAULT_MINIMUM_FILE_SIZE = 20;
|
||||
|
||||
protected RequestItemServiceImpl() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new request-a-copy item request.
|
||||
*
|
||||
* @param context The relevant DSpace Context.
|
||||
* @param bitstream The requested bitstream
|
||||
* @param item The requested item
|
||||
* @param allFiles true indicates that all bitstreams of this item are requested
|
||||
* @param reqEmail email
|
||||
* Requester email
|
||||
* @param reqName Requester name
|
||||
* @param reqMessage Request message text
|
||||
* @return token to be used to approver for grant/deny
|
||||
* @throws SQLException
|
||||
*/
|
||||
@Override
|
||||
public String createRequest(Context context, Bitstream bitstream, Item item,
|
||||
boolean allFiles, String reqEmail, String reqName, String reqMessage)
|
||||
throws SQLException {
|
||||
|
||||
// Create an empty request item
|
||||
RequestItem requestItem = requestItemDAO.create(context, new RequestItem());
|
||||
|
||||
// Set values of the request item based on supplied parameters
|
||||
requestItem.setToken(Utils.generateHexKey());
|
||||
requestItem.setBitstream(bitstream);
|
||||
requestItem.setItem(item);
|
||||
@@ -68,10 +99,56 @@ public class RequestItemServiceImpl implements RequestItemService {
|
||||
requestItem.setReqMessage(reqMessage);
|
||||
requestItem.setRequest_date(Instant.now());
|
||||
|
||||
// If the 'link' feature is enabled and the filesize threshold is met, pre-generate access token now
|
||||
// so it can be previewed by approver and so Angular and REST services can use the existence of this token
|
||||
// as an indication of which delivery method to use.
|
||||
// Access period will be created upon actual approval.
|
||||
if (configurationService.getBooleanProperty("request.item.grant.link", false)) {
|
||||
// The 'send link' feature is enabled, is the file(s) requested over the size threshold (megabytes as int)?
|
||||
// Default is 20MB minimum. For inspection purposes we convert to bytes.
|
||||
long minimumSize = configurationService.getLongProperty(
|
||||
"request.item.grant.link.filesize", DEFAULT_MINIMUM_FILE_SIZE) * 1024 * 1024;
|
||||
// If we have a single bitstream, we will initialise the "minimum threshold reached" correctly
|
||||
boolean minimumSizeThresholdReached = (null != bitstream && bitstream.getSizeBytes() >= minimumSize);
|
||||
// If all files (and presumably no min reached since bitstream should be null), we look for ANY >= min size
|
||||
if (!minimumSizeThresholdReached && allFiles) {
|
||||
// Iterate bitstream and inspect file sizes. At each loop iteration we will break out if the min
|
||||
// was already reached.
|
||||
String[] bundleNames = configurationService.getArrayProperty("request.item.grant.link.bundles",
|
||||
new String[]{"ORIGINAL"});
|
||||
for (String bundleName : bundleNames) {
|
||||
if (!minimumSizeThresholdReached) {
|
||||
for (Bundle bundle : item.getBundles(bundleName)) {
|
||||
if (null != bundle && !minimumSizeThresholdReached) {
|
||||
for (Bitstream bitstreamToCheck : bundle.getBitstreams()) {
|
||||
if (bitstreamToCheck.getSizeBytes() >= minimumSize) {
|
||||
minimumSizeThresholdReached = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now, only generate and set an access token if the minimum file size threshold was reached.
|
||||
// Otherwise, an email attachment will still be used.
|
||||
// From now on, the existence of an access token in the RequestItem indicates that a web link should be
|
||||
// sent instead of attaching file(s) as an attachment.
|
||||
if (minimumSizeThresholdReached) {
|
||||
requestItem.setAccess_token(Utils.generateHexKey());
|
||||
}
|
||||
}
|
||||
|
||||
// Save the request item
|
||||
requestItemDAO.save(context, requestItem);
|
||||
|
||||
log.debug("Created RequestItem with ID {} and token {}",
|
||||
requestItem::getID, requestItem::getToken);
|
||||
log.debug("Created RequestItem with ID {}, approval token {}, access token {}, access period {}",
|
||||
requestItem::getID, requestItem::getToken, requestItem::getAccess_token, requestItem::getAccess_period);
|
||||
|
||||
// Return the approver token
|
||||
return requestItem.getToken();
|
||||
}
|
||||
|
||||
@@ -128,4 +205,148 @@ public class RequestItemServiceImpl implements RequestItemService {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a request item by its access token. This is the token that a requester would use
|
||||
* to authenticate themselves as a granted requester.
|
||||
* It is up to the RequestItemRepository to check validity of the item, access granted, data sanitization, etc.
|
||||
*
|
||||
* @param context current DSpace session.
|
||||
* @param accessToken the token identifying the request to be temporarily accessed
|
||||
* @return request item data
|
||||
*/
|
||||
@Override
|
||||
public RequestItem findByAccessToken(Context context, String accessToken) {
|
||||
try {
|
||||
return requestItemDAO.findByAccessToken(context, accessToken);
|
||||
} catch (SQLException e) {
|
||||
log.error(e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 period is 0 (forever), or the elapsed seconds since decision date is less than the
|
||||
// access period granted
|
||||
&& requestItem.accessPeriodCurrent()
|
||||
) {
|
||||
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 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
|
||||
*/
|
||||
@Override
|
||||
public void sanitizeRequestItem(Context context, RequestItem requestItem) {
|
||||
if (null == requestItem) {
|
||||
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
|
||||
requestItem.sanitizePersonalData();
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -26,7 +26,7 @@ import org.dspace.core.GenericDAO;
|
||||
*/
|
||||
public interface RequestItemDAO extends GenericDAO<RequestItem> {
|
||||
/**
|
||||
* Fetch a request named by its unique token (passed in emails).
|
||||
* Fetch a request named by its unique approval token (passed in emails).
|
||||
*
|
||||
* @param context the current DSpace context.
|
||||
* @param token uniquely identifies the request.
|
||||
@@ -35,5 +35,18 @@ public interface RequestItemDAO extends GenericDAO<RequestItem> {
|
||||
*/
|
||||
public RequestItem findByToken(Context context, String token) throws SQLException;
|
||||
|
||||
/**
|
||||
* Fetch a request named by its unique access token (passed in emails).
|
||||
* Note this is the token used by the requester to access an approved resource, not the token
|
||||
* used by the item submitter or helpdesk to grant the access.
|
||||
*
|
||||
* @param context the current DSpace context.
|
||||
* @param accessToken uniquely identifies the request
|
||||
* @return the found request or {@code null}
|
||||
* @throws SQLException passed through.
|
||||
*/
|
||||
public RequestItem findByAccessToken(Context context, String accessToken) throws SQLException;
|
||||
|
||||
public Iterator<RequestItem> findByItem(Context context, Item item) throws SQLException;
|
||||
|
||||
}
|
||||
|
@@ -42,6 +42,17 @@ public class RequestItemDAOImpl extends AbstractHibernateDAO<RequestItem> implem
|
||||
criteriaQuery.where(criteriaBuilder.equal(requestItemRoot.get(RequestItem_.token), token));
|
||||
return uniqueResult(context, criteriaQuery, false, RequestItem.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestItem findByAccessToken(Context context, String accessToken) throws SQLException {
|
||||
CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
|
||||
CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, RequestItem.class);
|
||||
Root<RequestItem> requestItemRoot = criteriaQuery.from(RequestItem.class);
|
||||
criteriaQuery.select(requestItemRoot);
|
||||
criteriaQuery.where(criteriaBuilder.equal(requestItemRoot.get(RequestItem_.access_token), accessToken));
|
||||
return uniqueResult(context, criteriaQuery, true, RequestItem.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<RequestItem> findByItem(Context context, Item item) throws SQLException {
|
||||
CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
|
||||
|
@@ -7,11 +7,14 @@
|
||||
*/
|
||||
package org.dspace.app.requestitem.service;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import org.dspace.app.requestitem.RequestItem;
|
||||
import org.dspace.authorize.AuthorizeException;
|
||||
import org.dspace.content.Bitstream;
|
||||
import org.dspace.content.DSpaceObject;
|
||||
import org.dspace.content.Item;
|
||||
@@ -23,6 +26,7 @@ import org.dspace.core.Context;
|
||||
* for the RequestItem object and is autowired by Spring.
|
||||
*
|
||||
* @author kevinvandevelde at atmire.com
|
||||
* @author Kim Shepherd
|
||||
*/
|
||||
public interface RequestItemService {
|
||||
|
||||
@@ -49,20 +53,28 @@ public interface RequestItemService {
|
||||
*
|
||||
* @param context current DSpace session.
|
||||
* @return all item requests.
|
||||
* @throws java.sql.SQLException passed through.
|
||||
* @throws SQLException passed through.
|
||||
*/
|
||||
public List<RequestItem> findAll(Context context)
|
||||
throws SQLException;
|
||||
|
||||
/**
|
||||
* Retrieve a request by its token.
|
||||
* Retrieve a request by its approver token.
|
||||
*
|
||||
* @param context current DSpace session.
|
||||
* @param token the token identifying the request.
|
||||
* @param token the token identifying the request to be approved.
|
||||
* @return the matching request, or null if not found.
|
||||
*/
|
||||
public RequestItem findByToken(Context context, String token);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public RequestItem findByAccessToken(Context context, String token);
|
||||
/**
|
||||
* Retrieve a request based on the item.
|
||||
* @param context current DSpace session.
|
||||
@@ -72,7 +84,10 @@ public interface RequestItemService {
|
||||
public Iterator<RequestItem> findByItem(Context context, Item item) throws SQLException;
|
||||
|
||||
/**
|
||||
* Save updates to the record. Only accept_request, and decision_date are set-able.
|
||||
* Save updates to the record. Only accept_request, decision_date, access_period are settable.
|
||||
*
|
||||
* Note: the "is settable" rules mentioned here are enforced in RequestItemRest with annotations meaning that
|
||||
* these JSON properties are considered READ-ONLY by the core DSpaceRestRepository methods
|
||||
*
|
||||
* @param context The relevant DSpace Context.
|
||||
* @param requestItem requested item
|
||||
@@ -96,4 +111,58 @@ public interface RequestItemService {
|
||||
*/
|
||||
public boolean isRestricted(Context context, DSpaceObject o)
|
||||
throws SQLException;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public 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
|
||||
*/
|
||||
public 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);
|
||||
}
|
||||
|
Reference in New Issue
Block a user