Skip to content

Commit

Permalink
ufal/downloading-restricted-bitstreams-not-working-properly (#457)
Browse files Browse the repository at this point in the history
* The admin could download restricted bitstreams.

* The user cannot download bitstream if he/she is not signed in.

* Cannot obtain context with user.

* The user is already fetched from the context.

* Added docs.

* Do not use endpoint for downloading the single file because it already exists - remove it.

* Fixed AuthorizationRestControllerIT tests.
  • Loading branch information
milanmajchrak authored Nov 15, 2023
1 parent a3b90fd commit dc628c9
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 189 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import org.dspace.content.Bitstream;
import org.dspace.content.Item;
import org.dspace.content.clarin.ClarinLicense;
import org.dspace.content.clarin.ClarinLicenseLabel;
import org.dspace.content.clarin.ClarinLicenseResourceMapping;
import org.dspace.content.service.BitstreamService;
import org.dspace.content.service.clarin.ClarinLicenseResourceMappingService;
Expand Down Expand Up @@ -93,15 +92,15 @@ public boolean authorizeBitstream(Context context, Bitstream bitstream) throws S
}

/**
* If the bitstream has RES or ACA license and the user is Anonymous do not authorize that user.
* The user will be redirected to the login.
* Do not allow download for anonymous users. Allow it only if the bitstream has Clarin License and the license has
* confirmation = 3 (allow anonymous).
*
* @param context DSpace context object
* @param bitstreamID downloading Bitstream UUID
* @return if the current user is authorized
*/
public boolean authorizeLicenseWithUser(Context context, UUID bitstreamID) throws SQLException {
// If the current user is null that means that the user is not signed in and cannot download the bitstream
// with RES or ACA license
// If the current user is null that means that the user is not signed
if (Objects.nonNull(context.getCurrentUser())) {
// User is signed
return true;
Expand All @@ -118,16 +117,13 @@ public boolean authorizeLicenseWithUser(Context context, UUID bitstreamID) throw

// Bitstream should have only one type of the Clarin license, so we could get first record
ClarinLicense clarinLicense = Objects.requireNonNull(clarinLicenseResourceMappings.get(0)).getLicense();
// Get License Labels from clarin license and check if one of them is ACA or RES
List<ClarinLicenseLabel> clarinLicenseLabels = clarinLicense.getLicenseLabels();
for (ClarinLicenseLabel clarinLicenseLabel : clarinLicenseLabels) {
if (StringUtils.equals(clarinLicenseLabel.getLabel(), "RES") ||
StringUtils.equals(clarinLicenseLabel.getLabel(), "ACA")) {
return false;
}
// 3 - Allow download for anonymous users, but with license confirmation
// 0 - License confirmation is not required
if (Objects.equals(clarinLicense.getConfirmation(), 3) ||
Objects.equals(clarinLicense.getConfirmation(), 0)) {
return true;
}

return true;
return false;
}

private boolean userIsSubmitter(Context context, Bitstream bitstream, EPerson currentUser, UUID userID) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.dspace.content.Bitstream;
import org.dspace.content.service.BitstreamService;
import org.dspace.content.service.clarin.ClarinLicenseResourceMappingService;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -84,28 +85,30 @@ public ResponseEntity authrn(@PathVariable String id, HttpServletResponse respon
return null;
}

// If the bitstream has RES or ACA license and the user is Anonymous return NotAuthorized exception
// Do not allow download for anonymous users. Allow it only if the bitstream has Clarin License and the license
// has confirmation = 3 (allow anonymous).
if (!authorizationBitstreamUtils.authorizeLicenseWithUser(context, bitstream.getID())) {
response.sendError(HttpStatus.UNAUTHORIZED.value(),
"Anonymous user cannot download bitstream with REC or ACA license");
"Anonymous user cannot download this bitstream");
return null;
}

// Wrap exceptions to the AuthrnRest object.
String errorMessage = "User is not authorized to download the bitstream.";
boolean isAuthorized = false;
String errorMessage = "";

try {
isAuthorized = authorizationBitstreamUtils.authorizeBitstream(context, bitstream);
authorizeService.authorizeAction(context, bitstream, Constants.READ);
} catch (AuthorizeException e) {
if (e instanceof MissingLicenseAgreementException) {
errorMessage = MissingLicenseAgreementException.NAME;
} else if (e instanceof DownloadTokenExpiredException) {
errorMessage = DownloadTokenExpiredException.NAME;
} else {
errorMessage = e.getMessage();
}
}

if (!isAuthorized) {
if (StringUtils.isNotBlank(errorMessage)) {
// If the user is not authorized return response with the error message
response.sendError(HttpStatus.UNAUTHORIZED.value(), errorMessage);
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,55 +7,53 @@
*/
package org.dspace.app.rest;

import static org.dspace.app.rest.utils.RegexUtils.REGEX_REQUESTMAPPING_IDENTIFIER_AS_UUID;

import java.io.IOException;
import java.io.InputStream;
import java.sql.SQLException;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.zip.Deflater;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
import org.dspace.app.rest.exception.DSpaceBadRequestException;
import org.dspace.app.rest.exception.UnprocessableEntityException;
import org.dspace.app.rest.model.BitstreamRest;
import org.dspace.app.rest.model.ItemRest;
import org.dspace.app.rest.utils.ContextUtil;
import org.dspace.authorize.AuthorizationBitstreamUtils;
import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.MissingLicenseAgreementException;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.content.Bitstream;
import org.dspace.content.BitstreamFormat;
import org.dspace.content.Bundle;
import org.dspace.content.DSpaceObject;
import org.dspace.content.Item;
import org.dspace.content.service.BitstreamService;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.handle.service.HandleService;
import org.dspace.services.ConfigurationService;
import org.dspace.services.RequestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.security.access.prepost.PreAuthorize;
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 CLARIN Controller download a single file or a ZIP file from the Item's bitstream.
*/
@RestController
@RequestMapping("/api/" + BitstreamRest.CATEGORY + "/" + BitstreamRest.PLURAL_NAME)
@RequestMapping("/api/" + ItemRest.CATEGORY + "/" + ItemRest.PLURAL_NAME + REGEX_REQUESTMAPPING_IDENTIFIER_AS_UUID)
public class MetadataBitstreamController {

private static Logger log = org.apache.logging.log4j.LogManager.getLogger(MetadataBitstreamController.class);
Expand All @@ -69,90 +67,17 @@ public class MetadataBitstreamController {
private AuthorizeService authorizeService;
@Autowired
private ConfigurationService configurationService;


@GetMapping("/handle/{id}/{subId}/{fileName}")
public ResponseEntity<Resource> downloadSingleFile(@PathVariable("id") String id,
@PathVariable("subId") String subId,
@PathVariable("fileName") String fileName,
HttpServletRequest request, HttpServletResponse response)
throws IOException {
String handleID = id + "/" + subId;
if (StringUtils.isBlank(id) || StringUtils.isBlank(subId)) {
log.error("Handle cannot be null! PathVariable `id` or `subId` is null.");
throw new DSpaceBadRequestException("Handle cannot be null!");
}

Context context = ContextUtil.obtainContext(request);
if (Objects.isNull(context)) {
log.error("Cannot obtain the context from the request.");
throw new RuntimeException("Cannot obtain the context from the request.");
}

DSpaceObject dso = null;
try {
dso = handleService.resolveToObject(context, handleID);
} catch (Exception e) {
log.error("Cannot resolve handle: " + handleID);
throw new RuntimeException("Cannot resolve handle: " + handleID);
}


if (Objects.isNull(dso)) {
log.error("DSO is null");
return null;
}

if (!(dso instanceof Item)) {
log.error("DSO is not instance of Item");
return null;
}

Item item = (Item) dso;
List<Bundle> bundles = item.getBundles();
// Find bitstream and start downloading.
for (Bundle bundle: bundles) {
for (Bitstream bitstream: bundle.getBitstreams()) {
// Authorize the action - it will send response redirect if something gets wrong.
authorizeBitstreamAction(context, bitstream, response);

String btName = bitstream.getName();
if (!(btName.equalsIgnoreCase(fileName))) {
continue;
}
try {
BitstreamFormat bitstreamFormat = bitstream.getFormat(context);
// Check if the bitstream has some extensions e.g., `.txt, .jpg,..`
checkBitstreamExtensions(bitstreamFormat);

// Get content of the bitstream
InputStream inputStream = bitstreamService.retrieve(context, bitstream);
InputStreamResource resource = new InputStreamResource(inputStream);
HttpHeaders header = new HttpHeaders();
header.add(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=" + fileName);
header.add("Cache-Control", "no-cache, no-store, must-revalidate");
header.add("Pragma", "no-cache");
header.add("Expires", "0");
return ResponseEntity.ok()
.headers(header)
.contentLength(inputStream.available())
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(resource);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

return null;
}
@Autowired
AuthorizationBitstreamUtils authorizationBitstreamUtils;
@Autowired
private RequestService requestService;

/**
* Download all Item's bitstreams as single ZIP file.
*/
@GetMapping("/allzip")
public void downloadFileZip(@RequestParam("handleId") String handleId,
@PreAuthorize("hasPermission(#uuid, 'ITEM', 'READ')")
@RequestMapping( method = {RequestMethod.GET, RequestMethod.HEAD}, value = "allzip")
public void downloadFileZip(@PathVariable UUID uuid, @RequestParam("handleId") String handleId,
HttpServletResponse response,
HttpServletRequest request) throws IOException, SQLException, AuthorizeException {
if (StringUtils.isBlank(handleId)) {
Expand Down Expand Up @@ -195,11 +120,11 @@ public void downloadFileZip(@RequestParam("handleId") String handleId,
for (Bundle original : bundles) {
List<Bitstream> bss = original.getBitstreams();
for (Bitstream bitstream : bss) {
authorizeBitstreamAction(context, bitstream, response);

String filename = bitstream.getName();
ZipArchiveEntry ze = new ZipArchiveEntry(filename);
zip.putArchiveEntry(ze);
// Get content of the bitstream
// Retrieve method authorize bitstream download action.
InputStream is = bitstreamService.retrieve(context, bitstream);
IOUtils.copy(is, zip);
zip.closeArchiveEntry();
Expand All @@ -209,37 +134,4 @@ public void downloadFileZip(@RequestParam("handleId") String handleId,
zip.close();
response.getOutputStream().flush();
}

/**
* Could the user download that bitstream?
* @param context DSpace context object
* @param bitstream Bitstream to download
* @param response for possibility to redirect
*/
private void authorizeBitstreamAction(Context context, Bitstream bitstream, HttpServletResponse response)
throws IOException {

String uiURL = configurationService.getProperty("dspace.ui.url");
if (StringUtils.isBlank(uiURL)) {
log.error("Configuration property `dspace.ui.url` cannot be empty or null!");
throw new RuntimeException("Configuration property `dspace.ui.url` cannot be empty or null!");
}
try {
authorizeService.authorizeAction(context, bitstream, Constants.READ);
} catch (MissingLicenseAgreementException e) {
response.sendRedirect(uiURL + "/bitstream/" + bitstream.getID() + "/download");
} catch (AuthorizeException | SQLException e) {
response.sendRedirect(uiURL + "/login");
}
}

/**
* Check if the bitstream has file extension.
*/
private void checkBitstreamExtensions(BitstreamFormat bitstreamFormat) {
if ( Objects.isNull(bitstreamFormat) || CollectionUtils.isEmpty(bitstreamFormat.getExtensions())) {
log.error("Bitstream Extensions cannot be empty for downloading/previewing bitstreams.");
throw new RuntimeException("Bitstream Extensions cannot be empty for downloading/previewing bitstreams.");
}
}
}
Loading

0 comments on commit dc628c9

Please sign in to comment.