Merge branch 'main' into CST-4122-ExposewWithdrawnItemsToAnonymousUsersFilteringOutTheMetadata

This commit is contained in:
Mykhaylo
2021-06-17 14:52:21 +02:00
28 changed files with 661 additions and 87 deletions

15
SECURITY.md Normal file
View File

@@ -0,0 +1,15 @@
# Security Policy
## Supported Versions
For information regarding which versions of DSpace are currently under support, please see our DSpace Software Support Policy:
https://wiki.lyrasis.org/display/DSPACE/DSpace+Software+Support+Policy
## Reporting a Vulnerability
If you believe you have found a security vulnerability in a supported version of DSpace, we encourage you to let us know right away.
We will investigate all legitimate reports and do our best to quickly fix the problem. Please see our DSpace Software Support Policy
for information on privately reporting vulnerabilities:
https://wiki.lyrasis.org/display/DSPACE/DSpace+Software+Support+Policy

View File

@@ -501,10 +501,10 @@ public class BundleServiceImpl extends DSpaceObjectServiceImpl<Bundle> implement
// Remove bitstreams
List<Bitstream> bitstreams = bundle.getBitstreams();
bundle.clearBitstreams();
for (Bitstream bitstream : bitstreams) {
removeBitstream(context, bundle, bitstream);
}
bundle.clearBitstreams();
List<Item> items = new LinkedList<>(bundle.getItems());
bundle.getItems().clear();

View File

@@ -0,0 +1,61 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.xoai.app;
import java.util.List;
import com.lyncode.xoai.dataprovider.xml.xoai.Element;
import com.lyncode.xoai.dataprovider.xml.xoai.Metadata;
import org.apache.commons.lang.StringUtils;
import org.dspace.content.Item;
import org.dspace.core.Context;
import org.dspace.license.factory.LicenseServiceFactory;
import org.dspace.license.service.CreativeCommonsService;
import org.dspace.xoai.util.ItemUtils;
/**
* XOAIExtensionItemCompilePlugin aims to add structured information about the
* creative commons license applied to the item (if any).
* The xoai document will be enriched with a structure like that
* <code>
* <element name="other">
* <element name="cc">
* <field name="uri"></field>
* <field name="name"></field>
* </element>
* </element>
* </code>
*
*/
public class CCElementItemCompilePlugin implements XOAIExtensionItemCompilePlugin {
@Override
public Metadata additionalMetadata(Context context, Metadata metadata, Item item) {
CreativeCommonsService creativeCommonsService = LicenseServiceFactory.getInstance().getCreativeCommonsService();
String licenseURI = creativeCommonsService.getLicenseURI(item);
String licenseName = creativeCommonsService.getLicenseName(item);
// licence uri is mandatory, name is optional
if (StringUtils.isNotBlank(licenseURI)) {
Element ccLicense = ItemUtils.create("cc");
ccLicense.getField().add(ItemUtils.createValue("uri", licenseURI));
if (StringUtils.isNotBlank(licenseName)) {
ccLicense.getField().add(ItemUtils.createValue("name", licenseName));
}
Element other;
List<Element> elements = metadata.getElement();
if (ItemUtils.getElement(elements, "others") != null) {
other = ItemUtils.getElement(elements, "others");
} else {
other = ItemUtils.create("others");
}
other.getElement().add(ccLicense);
}
return metadata;
}
}

View File

@@ -0,0 +1,42 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.xoai.app;
import java.io.File;
import java.net.MalformedURLException;
import org.dspace.kernel.config.SpringLoader;
import org.dspace.services.ConfigurationService;
/**
* Spring Loader to load specific bean implementation only for OAI Spring
* context
*
*/
public class OAISpringLoader implements SpringLoader {
@Override
public String[] getResourcePaths(ConfigurationService configurationService) {
StringBuffer filePath = new StringBuffer();
filePath.append(configurationService.getProperty("dspace.dir"));
filePath.append(File.separator);
filePath.append("config");
filePath.append(File.separator);
filePath.append("spring");
filePath.append(File.separator);
filePath.append("oai");
filePath.append(File.separator);
try {
return new String[] { new File(filePath.toString()).toURI().toURL().toString() + XML_SUFFIX };
} catch (MalformedURLException e) {
return new String[0];
}
}
}

View File

@@ -27,6 +27,7 @@ import javax.xml.stream.XMLStreamException;
import com.lyncode.xoai.dataprovider.exceptions.ConfigurationException;
import com.lyncode.xoai.dataprovider.exceptions.WritingXmlException;
import com.lyncode.xoai.dataprovider.xml.XmlOutputContext;
import com.lyncode.xoai.dataprovider.xml.xoai.Metadata;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
@@ -56,6 +57,7 @@ import org.dspace.core.Context;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.dspace.util.SolrUtils;
import org.dspace.utils.DSpace;
import org.dspace.xoai.exceptions.CompilingException;
import org.dspace.xoai.services.api.CollectionsService;
import org.dspace.xoai.services.api.cache.XOAICacheService;
@@ -95,6 +97,8 @@ public class XOAI {
private final static ConfigurationService configurationService = DSpaceServicesFactory
.getInstance().getConfigurationService();
private List<XOAIExtensionItemCompilePlugin> extensionPlugins;
private List<String> getFileFormats(Item item) {
List<String> formats = new ArrayList<>();
try {
@@ -120,6 +124,8 @@ public class XOAI {
// Load necessary DSpace services
this.authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService();
this.itemService = ContentServiceFactory.getInstance().getItemService();
this.extensionPlugins = new DSpace().getServiceManager()
.getServicesByType(XOAIExtensionItemCompilePlugin.class);
}
public XOAI(Context ctx, boolean hasOption) {
@@ -129,6 +135,8 @@ public class XOAI {
// Load necessary DSpace services
this.authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService();
this.itemService = ContentServiceFactory.getInstance().getItemService();
this.extensionPlugins = new DSpace().getServiceManager()
.getServicesByType(XOAIExtensionItemCompilePlugin.class);
}
private void println(String line) {
@@ -455,7 +463,14 @@ public class XOAI {
ByteArrayOutputStream out = new ByteArrayOutputStream();
XmlOutputContext xmlContext = XmlOutputContext.emptyContext(out, Second);
retrieveMetadata(context, item).write(xmlContext);
Metadata metadata = retrieveMetadata(context, item);
// Do any additional metadata element, depends on the plugins
for (XOAIExtensionItemCompilePlugin plugin : extensionPlugins) {
metadata = plugin.additionalMetadata(context, metadata, item);
}
metadata.write(xmlContext);
xmlContext.getWriter().flush();
xmlContext.getWriter().close();
doc.addField("item.compile", out.toString());
@@ -698,4 +713,5 @@ public class XOAI {
System.out.println(" -h Shows this text");
}
}
}

View File

@@ -0,0 +1,35 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.xoai.app;
import com.lyncode.xoai.dataprovider.xml.xoai.Metadata;
import org.dspace.content.Item;
import org.dspace.core.Context;
/**
* This interface can be implemented by plugins that aims to contribute to the
* generation of the xoai document stored in the item.compile solr OAI core
* field
*
*/
public interface XOAIExtensionItemCompilePlugin {
/**
* This method allows plugins to add content to the xoai document generated for
* the item.
*
* @param context the DSpace Context
* @param metadata the basic xoai representation of the item that can be
* manipulated by the plugin
* @param item the dspace item to index
* @return the altered xoai metadata
*/
public Metadata additionalMetadata(Context context, Metadata metadata, Item item);
}

View File

@@ -65,7 +65,7 @@ public class ItemUtils {
private ItemUtils() {
}
private static Element getElement(List<Element> list, String name) {
public static Element getElement(List<Element> list, String name) {
for (Element e : list) {
if (name.equals(e.getName())) {
return e;
@@ -75,13 +75,13 @@ public class ItemUtils {
return null;
}
private static Element create(String name) {
public static Element create(String name) {
Element e = new Element();
e.setName(name);
return e;
}
private static Element.Field createValue(String name, String value) {
public static Element.Field createValue(String name, String value) {
Element.Field e = new Element.Field();
e.setValue(value);
e.setName(name);
@@ -108,7 +108,7 @@ public class ItemUtils {
String url = "";
String bsName = bit.getName();
String sid = String.valueOf(bit.getSequenceID());
String baseUrl = configurationService.getProperty("oai", "bitstream.baseUrl");
String baseUrl = configurationService.getProperty("oai.bitstream.baseUrl");
String handle = null;
// get handle of parent Item of this bitstream, if there
// is one:
@@ -119,15 +119,7 @@ public class ItemUtils {
handle = bi.get(0).getHandle();
}
}
if (bsName == null) {
List<String> ext = bit.getFormat(context).getExtensions();
bsName = "bitstream_" + sid + (ext.isEmpty() ? "" : ext.get(0));
}
if (handle != null && baseUrl != null) {
url = baseUrl + "/bitstream/" + handle + "/" + sid + "/" + URLUtils.encode(bsName);
} else {
url = URLUtils.encode(bsName);
}
url = baseUrl + "/bitstreams/" + bit.getID().toString() + "/download";
String cks = bit.getChecksum();
String cka = bit.getChecksumAlgorithm();

View File

@@ -19,7 +19,7 @@
<properties>
<!-- This is the path to the root [dspace-src] directory. -->
<root.basedir>${basedir}/..</root.basedir>
<spring-security.version>5.3.3.RELEASE</spring-security.version>
<spring-security.version>5.3.9.RELEASE</spring-security.version>
</properties>
<build>
<plugins>

View File

@@ -42,7 +42,7 @@ public class BrowseEntryHalLinkFactory extends HalLinkFactory<BrowseEntryResourc
addFilterParams(baseLink, data);
list.add(buildLink(BrowseIndexRest.ITEMS,
baseLink.build().toUriString()));
baseLink.build().encode().toUriString()));
}
}

View File

@@ -36,7 +36,7 @@ public class SearchFacetValueHalLinkFactory extends DiscoveryRestHalLinkFactory<
addFilterForFacetValue(builder, halResource.getFacetData(), halResource.getValueData());
list.add(buildLink("search", builder.build().toUriString()));
list.add(buildLink("search", builder.build().encode().toUriString()));
}
}

View File

@@ -147,7 +147,7 @@ public class ItemRestRepository extends DSpaceObjectRestRepository<Item, ItemRes
}
@Override
@PreAuthorize("hasAuthority('ADMIN')")
@PreAuthorize("hasPermission(#id, 'ITEM', 'DELETE')")
protected void delete(Context context, UUID id) throws AuthorizeException {
String[] copyVirtual =
requestService.getCurrentRequest().getServletRequest()

View File

@@ -13,6 +13,7 @@ import java.util.Iterator;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.collections.CollectionUtils;
import org.dspace.app.rest.model.UploadBitstreamAccessConditionDTO;
import org.dspace.app.rest.model.patch.LateObjectEvaluator;
import org.dspace.authorize.service.AuthorizeService;
@@ -57,23 +58,23 @@ public class BitstreamResourcePolicyAddPatchOperation extends AddPatchOperation<
Iterator<UploadConfiguration> uploadConfigs = uploadConfigsCollection.iterator();
for (Bundle bb : bundle) {
int idx = 0;
for (Bitstream b : bb.getBitstreams()) {
for (Bitstream bitstream : bb.getBitstreams()) {
if (idx == Integer.parseInt(split[1])) {
List<UploadBitstreamAccessConditionDTO> newAccessConditions =
new ArrayList<UploadBitstreamAccessConditionDTO>();
if (split.length == 3) {
authorizeService.removePoliciesActionFilter(context, b, Constants.READ);
authorizeService.removePoliciesActionFilter(context, bitstream, Constants.READ);
newAccessConditions = evaluateArrayObject((LateObjectEvaluator) value);
} else if (split.length == 4) {
// contains "-", call index-based accessConditions it make not sense
newAccessConditions.add(evaluateSingleObject((LateObjectEvaluator) value));
}
for (UploadBitstreamAccessConditionDTO newAccessCondition : newAccessConditions) {
// TODO manage duplicate policy
BitstreamResourcePolicyUtils.findApplyResourcePolicy(context, uploadConfigs,
b, newAccessCondition);
// TODO manage duplicate policy
if (CollectionUtils.isNotEmpty(newAccessConditions)) {
BitstreamResourcePolicyUtils.findApplyResourcePolicy(context, uploadConfigs, bitstream,
newAccessConditions);
}
}
idx++;

View File

@@ -79,15 +79,17 @@ public class BitstreamResourcePolicyReplacePatchOperation extends ReplacePatchOp
}
if (split.length == 4) {
ResourcePolicyRest newAccessCondition = evaluateSingleObject((LateObjectEvaluator) value);
while (uploadConfigs.hasNext()) {
ResourcePolicyRest newAccessCondition = evaluateSingleObject((LateObjectEvaluator) value);
String name = newAccessCondition.getName();
String description = newAccessCondition.getDescription();
Date startDate = newAccessCondition.getStartDate();
Date endDate = newAccessCondition.getEndDate();
// TODO manage duplicate policy
BitstreamResourcePolicyUtils.findApplyResourcePolicy(context, uploadConfigs,
b, name, description, startDate, endDate);
String name = newAccessCondition.getName();
String description = newAccessCondition.getDescription();
Date startDate = newAccessCondition.getStartDate();
Date endDate = newAccessCondition.getEndDate();
// TODO manage duplicate policy
BitstreamResourcePolicyUtils.findApplyResourcePolicy(context, uploadConfigs.next(), b, name,
description, startDate, endDate);
}
} else {
// "path":
// "/sections/upload/files/0/accessConditions/0/startDate"

View File

@@ -11,6 +11,7 @@ import java.sql.SQLException;
import java.text.ParseException;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import org.dspace.app.rest.model.UploadBitstreamAccessConditionDTO;
import org.dspace.authorize.AuthorizeException;
@@ -47,15 +48,20 @@ public class BitstreamResourcePolicyUtils {
* @throws AuthorizeException If the user is not authorized
*/
public static void findApplyResourcePolicy(Context context, Iterator<UploadConfiguration> uploadConfigs,
Bitstream b, UploadBitstreamAccessConditionDTO newAccessCondition)
Bitstream b, List<UploadBitstreamAccessConditionDTO> newAccessConditions)
throws SQLException, AuthorizeException, ParseException {
String name = newAccessCondition.getName();
String description = newAccessCondition.getDescription();
while (uploadConfigs.hasNext()) {
UploadConfiguration uploadConfiguration = uploadConfigs.next();
for (UploadBitstreamAccessConditionDTO newAccessCondition : newAccessConditions) {
String name = newAccessCondition.getName();
String description = newAccessCondition.getDescription();
Date startDate = newAccessCondition.getStartDate();
Date endDate = newAccessCondition.getEndDate();
Date startDate = newAccessCondition.getStartDate();
Date endDate = newAccessCondition.getEndDate();
findApplyResourcePolicy(context, uploadConfigs, b, name, description, startDate, endDate);
findApplyResourcePolicy(context, uploadConfiguration, b, name, description, startDate, endDate);
}
}
}
/**
@@ -73,21 +79,15 @@ public class BitstreamResourcePolicyUtils {
* @throws SQLException If a database error occurs
* @throws AuthorizeException If the user is not authorized
*/
public static void findApplyResourcePolicy(Context context, Iterator<UploadConfiguration> uploadConfigs,
public static void findApplyResourcePolicy(Context context, UploadConfiguration uploadConfiguration,
Bitstream b, String name, String description,
Date startDate, Date endDate)
throws SQLException, AuthorizeException, ParseException {
while (uploadConfigs
.hasNext()) {
UploadConfiguration uploadConfiguration = uploadConfigs.next();
for (AccessConditionOption aco : uploadConfiguration.getOptions()) {
if (aco.getName().equalsIgnoreCase(name)) {
aco.createResourcePolicy(context, b, name, description, startDate, endDate);
return;
}
for (AccessConditionOption aco : uploadConfiguration.getOptions()) {
if (aco.getName().equalsIgnoreCase(name)) {
aco.createResourcePolicy(context, b, name, description, startDate, endDate);
return;
}
}
log.warn("no AccessCondition found or applied for bitstream " + b.getID() +
" with AccessCondition " + name);
}
}

View File

@@ -77,6 +77,18 @@ spring.http.encoding.force=true
# However, you may wish to set this to "always" in your 'local.cfg' for development or debugging purposes.
server.error.include-stacktrace = never
# Spring Boot proxy configuration (can be overridden in local.cfg).
# By default, Spring Boot does not automatically use X-Forwarded-* Headers when generating links (and similar) in the
# DSpace REST API. Three options are currently supported by Spring Boot:
# * NATIVE = allows your web server to natively support standard Forwarded headers
# * FRAMEWORK = (DSpace default) enables Spring Framework's built in filter to manage these headers in Spring Boot.
# This setting is used by default to support all X-Forwarded-* headers, as the DSpace backend is often
# installed behind Apache HTTPD or Nginx proxy (both of which pass those headers to Tomcat).
# * NONE = (Spring default) Forwarded headers are ignored
# For more information see
# https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto-use-behind-a-proxy-server
server.forward-headers-strategy=FRAMEWORK
######################
# Spring Boot Autoconfigure
#
@@ -107,6 +119,9 @@ spring.main.allow-bean-definition-overriding = true
# Log4J configuration
logging.config = ${dspace.dir}/config/log4j2.xml
##################################
# Spring MVC file upload settings
#
# Maximum size of a single uploaded file (default = 1MB)
spring.servlet.multipart.max-file-size = 512MB

View File

@@ -537,11 +537,7 @@ public class BundleRestRepositoryIT extends AbstractControllerIntegrationTest {
.build();
}
bundle1 = BundleBuilder.createBundle(context, item)
.withName("testname")
.withBitstream(bitstream1)
.withBitstream(bitstream2)
.build();
bundle1 = item.getBundles("ORIGINAL").get(0);
context.restoreAuthSystemState();

View File

@@ -7,12 +7,14 @@
*/
package org.dspace.app.rest;
import static com.google.common.net.UrlEscapers.urlPathSegmentEscaper;
import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.emptyOrNullString;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
@@ -1140,6 +1142,58 @@ public class DiscoveryRestControllerIT extends AbstractControllerIntegrationTest
;
}
@Test
public void discoverSearchObjectsWithSpecialCharacterTest() throws Exception {
context.turnOffAuthorisationSystem();
parentCommunity = CommunityBuilder.createCommunity(context).build();
Collection collection = CollectionBuilder.createCollection(context, parentCommunity).build();
ItemBuilder.createItem(context, collection)
.withAuthor("DSpace & friends")
.build();
context.restoreAuthSystemState();
getClient().perform(
get("/api/discover/search/objects")
.param("sort", "score,DESC")
.param("page", "0")
.param("size", "10"))
.andExpect(status().isOk())
.andExpect(jsonPath("$._embedded.facets", hasItem(allOf(
hasJsonPath("$.name", is("author")),
hasJsonPath("$._embedded.values", hasItem(
hasJsonPath("$._links.search.href", containsString("DSpace%20%26%20friends"))
))
))));
}
@Test
public void discoverSearchBrowsesWithSpecialCharacterTest() throws Exception {
context.turnOffAuthorisationSystem();
parentCommunity = CommunityBuilder.createCommunity(context).build();
Collection collection = CollectionBuilder.createCollection(context, parentCommunity).build();
ItemBuilder.createItem(context, collection)
.withAuthor("DSpace & friends")
.build();
context.restoreAuthSystemState();
getClient().perform(
get("/api/discover/browses/author/entries")
.param("sort", "default,ASC")
.param("page", "0")
.param("size", "20"))
.andExpect(status().isOk())
.andExpect(jsonPath("$._embedded.entries", hasItem(allOf(
hasJsonPath("$.value", is("DSpace & friends")),
hasJsonPath("$._links.items.href", containsString("DSpace%20%26%20friends"))
))));
}
@Test
public void discoverSearchObjectsTestHasMoreAuthorFacet() throws Exception {
//We turn off the authorization system in order to create the structure defined below
@@ -5711,7 +5765,9 @@ public class DiscoveryRestControllerIT extends AbstractControllerIntegrationTest
.andExpect(jsonPath("$._embedded.values[0].label", is("Smith, Donald")))
.andExpect(jsonPath("$._embedded.values[0].count", is(1)))
.andExpect(jsonPath("$._embedded.values[0]._links.search.href",
containsString("api/discover/search/objects?query=Donald&f.author=Smith, Donald,equals")))
containsString("api/discover/search/objects?query=Donald&f.author=" +
urlPathSegmentEscaper().escape("Smith, Donald,equals")
)))
.andExpect(jsonPath("$._embedded.values").value(Matchers.hasSize(1)));
}
@@ -5764,7 +5820,9 @@ public class DiscoveryRestControllerIT extends AbstractControllerIntegrationTest
.andExpect(jsonPath("$._embedded.values[0].label", is("2017 - 2020")))
.andExpect(jsonPath("$._embedded.values[0].count", is(3)))
.andExpect(jsonPath("$._embedded.values[0]._links.search.href",
containsString("api/discover/search/objects?dsoType=Item&f.dateIssued=[2017 TO 2020],equals")))
containsString("api/discover/search/objects?dsoType=Item&f.dateIssued=" +
urlPathSegmentEscaper().escape("[2017 TO 2020],equals")
)))
.andExpect(jsonPath("$._embedded.values").value(Matchers.hasSize(1)));
}

View File

@@ -1260,7 +1260,7 @@ public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest {
}
@Test
public void deleteOneArchivedTest() throws Exception {
public void deleteOneArchivedTestAsSystemAdmin() throws Exception {
context.turnOffAuthorisationSystem();
//** GIVEN **
@@ -1313,10 +1313,166 @@ public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest {
.andExpect(status().is(404));
//Trying to get deleted item bitstream should fail with 404
getClient().perform(get("/api/core/biststreams/" + bitstream.getID()))
// NOTE: it currently does not work without an admin token
String adminToken = getAuthToken(admin.getEmail(), password);
getClient(adminToken).perform(get("/api/core/bitstreams/" + bitstream.getID()))
.andExpect(status().is(404));
}
@Test
public void deleteOneArchivedTestAsCollectionAdmin() throws Exception {
context.turnOffAuthorisationSystem();
//** GIVEN **
// A collection administrator
EPerson col1Admin = EPersonBuilder.createEPerson(context)
.withCanLogin(true)
.withEmail("col1admin@email.com")
.withPassword(password)
.withNameInMetadata("Col1", "Admin")
.build();
// A community with one collection.
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent Community")
.build();
Collection col1 = CollectionBuilder
.createCollection(context, parentCommunity)
.withName("Collection 1")
.withAdminGroup(col1Admin)
.build();
// One public item, one workspace item and one template item.
Item publicItem = ItemBuilder.createItem(context, col1)
.withTitle("Public item 1")
.withIssueDate("2017-10-17")
.withAuthor("Smith, Donald").withAuthor("Doe, John")
.withSubject("ExtraEntry")
.build();
//Add a bitstream to an item
String bitstreamContent = "ThisIsSomeDummyText";
Bitstream bitstream = null;
try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
bitstream = BitstreamBuilder.
createBitstream(context, publicItem, is)
.withName("Bitstream1")
.withMimeType("text/plain")
.build();
}
context.restoreAuthSystemState();
// Check publicItem creation
getClient().perform(get("/api/core/items/" + publicItem.getID()))
.andExpect(status().isOk());
// Check publicItem bitstream creation (shuold be stored in bundle)
getClient().perform(get("/api/core/items/" + publicItem.getID() + "/bundles"))
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$._links.self.href", Matchers
.containsString("/api/core/items/" + publicItem.getID() + "/bundles")));
String token = getAuthToken(col1Admin.getEmail(), password);
//Delete public item
getClient(token).perform(delete("/api/core/items/" + publicItem.getID()))
.andExpect(status().is(204));
//Trying to get deleted item should fail with 404
getClient().perform(get("/api/core/items/" + publicItem.getID()))
.andExpect(status().is(404));
//Trying to get deleted item bitstream should fail with 404
// NOTE: it currently does not work without an admin token
String adminToken = getAuthToken(admin.getEmail(), password);
getClient(adminToken).perform(get("/api/core/bitstreams/" + bitstream.getID()))
.andExpect(status().is(404));
}
@Test
public void deleteOneArchivedTestAsOtherCollectionAdmin() throws Exception {
context.turnOffAuthorisationSystem();
//** GIVEN **
// two collection administrators
EPerson col1Admin = EPersonBuilder.createEPerson(context)
.withCanLogin(true)
.withEmail("col1admin@email.com")
.withPassword(password)
.withNameInMetadata("Col1", "Admin")
.build();
EPerson col2Admin = EPersonBuilder.createEPerson(context)
.withCanLogin(true)
.withEmail("col2admin@email.com")
.withPassword(password)
.withNameInMetadata("Col2", "Admin")
.build();
// A community with two collections.
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent Community")
.build();
Collection col1 = CollectionBuilder
.createCollection(context, parentCommunity)
.withName("Collection 1")
.withAdminGroup(col1Admin)
.build();
CollectionBuilder
.createCollection(context, parentCommunity)
.withName("Collection 2")
.withAdminGroup(col2Admin)
.build();
// One public item, one workspace item and one template item in the first collection.
Item publicItem = ItemBuilder.createItem(context, col1)
.withTitle("Public item 1")
.withIssueDate("2017-10-17")
.withAuthor("Smith, Donald").withAuthor("Doe, John")
.withSubject("ExtraEntry")
.build();
//Add a bitstream to an item in the first collection
String bitstreamContent = "ThisIsSomeDummyText";
Bitstream bitstream = null;
try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
bitstream = BitstreamBuilder.
createBitstream(context, publicItem, is)
.withName("Bitstream1")
.withMimeType("text/plain")
.build();
}
context.restoreAuthSystemState();
// Check publicItem creation
getClient().perform(get("/api/core/items/" + publicItem.getID()))
.andExpect(status().isOk());
// Check publicItem bitstream creation (should be stored in bundle)
getClient().perform(get("/api/core/items/" + publicItem.getID() + "/bundles"))
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$._links.self.href", Matchers
.containsString("/api/core/items/" + publicItem.getID() + "/bundles")));
// the admin of collection 2 will try to delete an item of collection 1
String token = getAuthToken(col2Admin.getEmail(), password);
// trying to delete the public item should fail
getClient(token).perform(delete("/api/core/items/" + publicItem.getID()))
.andExpect(status().isForbidden());
// the item should still exist
getClient().perform(get("/api/core/items/" + publicItem.getID()))
.andExpect(status().isOk());
// the bitstream should still exist
getClient().perform(get("/api/core/bitstreams/" + bitstream.getID()))
.andExpect(status().isOk());
}
@Test
public void deleteOneTemplateTest() throws Exception {
context.turnOffAuthorisationSystem();

View File

@@ -23,6 +23,7 @@ import java.util.List;
import org.dspace.app.rest.test.AbstractEntityIntegrationTest;
import org.dspace.builder.CollectionBuilder;
import org.dspace.builder.CommunityBuilder;
import org.dspace.builder.EPersonBuilder;
import org.dspace.builder.ItemBuilder;
import org.dspace.builder.RelationshipBuilder;
import org.dspace.content.Collection;
@@ -868,10 +869,94 @@ public class RelationshipDeleteRestRepositoryIT extends AbstractEntityIntegratio
}
@Test
public void deleteItemCopyVirtualMetadataAllInsufficientPermissions() throws Exception {
public void deleteItemCopyVirtualMetadataAllAsCollectionAdmin() throws Exception {
initPersonProjectPublication();
getClient(getAuthToken(collectionAdmin.getEmail(), password)).perform(
delete("/api/core/items/" + personItem.getID()))
.andExpect(status().isNoContent());
publicationItem = itemService.find(context, publicationItem.getID());
List<MetadataValue> publicationAuthorList =
itemService.getMetadata(publicationItem, "dc", "contributor", "author", Item.ANY);
assertThat(publicationAuthorList.size(), equalTo(0));
List<MetadataValue> publicationRelationships = itemService.getMetadata(publicationItem,
"relation", "isAuthorOfPublication", Item.ANY, Item.ANY);
assertThat(publicationRelationships.size(), equalTo(0));
projectItem = itemService.find(context, projectItem.getID());
List<MetadataValue> projectAuthorList =
itemService.getMetadata(projectItem, "dc", "contributor", "author", Item.ANY);
assertThat(projectAuthorList.size(), equalTo(0));
List<MetadataValue> projectRelationships = itemService.getMetadata(projectItem,
"relation", "isPersonOfProject", Item.ANY, Item.ANY);
assertThat(projectRelationships.size(), equalTo(0));
}
@Test
public void deleteItemCopyVirtualMetadataTypeAsCollectionAdmin() throws Exception {
initPersonProjectPublication();
getClient(getAuthToken(collectionAdmin.getEmail(), password)).perform(
delete("/api/core/items/" + personItem.getID()
+ "?copyVirtualMetadata=" + publicationPersonRelationshipType.getID()))
.andExpect(status().isNoContent());
publicationItem = itemService.find(context, publicationItem.getID());
List<MetadataValue> publicationAuthorList =
itemService.getMetadata(publicationItem, "dc", "contributor", "author", Item.ANY);
assertThat(publicationAuthorList.size(), equalTo(1));
assertThat(publicationAuthorList.get(0).getValue(), equalTo("Smith, Donald"));
assertThat(publicationAuthorList.get(0).getAuthority(), equalTo(null));
List<MetadataValue> publicationRelationships = itemService.getMetadata(publicationItem,
"relation", "isAuthorOfPublication", Item.ANY, Item.ANY);
assertThat(publicationRelationships.size(), equalTo(1));
projectItem = itemService.find(context, projectItem.getID());
List<MetadataValue> projectAuthorList =
itemService.getMetadata(projectItem, "dc", "contributor", "author", Item.ANY);
assertThat(projectAuthorList.size(), equalTo(0));
List<MetadataValue> projectRelationships = itemService.getMetadata(projectItem,
"relation", "isPersonOfProject", Item.ANY, Item.ANY);
assertThat(projectRelationships.size(), equalTo(0));
}
/**
* Create a collection admin that is unrelated to {@link #collection}.
* @return an EPerson
*/
protected EPerson createOtherCollectionAdmin() throws Exception {
context.turnOffAuthorisationSystem();
EPerson otherCollectionAdmin = EPersonBuilder.createEPerson(context)
.withNameInMetadata("other", "col admin")
.withCanLogin(true)
.withEmail("otherCollectionAdmin@email.com")
.withPassword(password)
.withLanguage(I18nUtil.getDefaultLocale().getLanguage())
.build();
Community otherCommunity = CommunityBuilder.createCommunity(context)
.withName("Other Community")
.build();
CollectionBuilder.createCollection(context, otherCommunity)
.withName("Other Collection")
.withAdminGroup(otherCollectionAdmin)
.build();
context.restoreAuthSystemState();
return otherCollectionAdmin;
}
@Test
public void deleteItemCopyVirtualMetadataAllInsufficientPermissions() throws Exception {
initPersonProjectPublication();
EPerson otherCollectionAdmin = createOtherCollectionAdmin();
getClient(getAuthToken(otherCollectionAdmin.getEmail(), password)).perform(
delete("/api/core/items/" + personItem.getID()))
.andExpect(status().isForbidden());
@@ -900,7 +985,9 @@ public class RelationshipDeleteRestRepositoryIT extends AbstractEntityIntegratio
public void deleteItemCopyVirtualMetadataTypeInsufficientPermissions() throws Exception {
initPersonProjectPublication();
getClient(getAuthToken(collectionAdmin.getEmail(), password)).perform(
EPerson otherCollectionAdmin = createOtherCollectionAdmin();
getClient(getAuthToken(otherCollectionAdmin.getEmail(), password)).perform(
delete("/api/core/items/" + personItem.getID()
+ "?copyVirtualMetadata=" + publicationPersonRelationshipType.getID()))
.andExpect(status().isForbidden());

View File

@@ -5247,4 +5247,56 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration
}
@Test
public void patchUploadAddMultiAccessConditionTest() throws Exception {
context.turnOffAuthorisationSystem();
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent Community")
.build();
Collection col1 = CollectionBuilder.createCollection(context, parentCommunity)
.withName("Collection 1")
.build();
InputStream pdf = getClass().getResourceAsStream("simple-article.pdf");
WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, col1)
.withTitle("Test WorkspaceItem")
.withIssueDate("2019-10-01")
.withFulltext("simple-article.pdf", "/local/path/simple-article.pdf", pdf)
.build();
context.restoreAuthSystemState();
// create a list of values to use in add operation
List<Operation> addAccessCondition = new ArrayList<>();
List<Map<String, String>> values = new ArrayList<Map<String,String>>();
Map<String, String> value = new HashMap<>();
value.put("name", "openaccess");
Map<String, String> value2 = new HashMap<>();
value2.put("name", "administrator");
values.add(value);
values.add(value2);
addAccessCondition.add(new AddOperation("/sections/upload/files/0/accessConditions", values));
String authToken = getAuthToken(eperson.getEmail(), password);
getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID())
.content(getPatchContent(addAccessCondition))
.contentType(MediaType.APPLICATION_JSON_PATCH_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.sections.upload.files[0].accessConditions[0].name", is("openaccess")))
.andExpect(jsonPath("$.sections.upload.files[0].accessConditions[1].name", is("administrator")));
// verify that the patch changes have been persisted
getClient(authToken).perform(get("/api/submission/workspaceitems/" + witem.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.sections.upload.files[0].accessConditions[0].name", is("openaccess")))
.andExpect(jsonPath("$.sections.upload.files[0].accessConditions[1].name", is("administrator")));
}
}

View File

@@ -7,6 +7,7 @@
*/
package org.dspace.app.rest.matcher;
import static com.google.common.net.UrlEscapers.urlPathSegmentEscaper;
import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
@@ -24,7 +25,9 @@ public class FacetValueMatcher {
hasJsonPath("$.label", is(label)),
hasJsonPath("$.type", is("discover")),
hasJsonPath("$._links.search.href", containsString("api/discover/search/objects")),
hasJsonPath("$._links.search.href", containsString("f.author=" + label + ",equals"))
hasJsonPath("$._links.search.href", containsString(
"f.author=" + urlPathSegmentEscaper().escape(label) + ",equals"
))
);
}

View File

@@ -90,7 +90,9 @@
<!-- oaire:citation* -->
<xsl:apply-templates
select="doc:metadata/doc:element[@name='oaire']/doc:element[@name='citation']" mode="oaire"/>
<!-- CREATIVE COMMON LICENSE -->
<xsl:apply-templates
select="doc:metadata/doc:element[@name='others']/doc:element[@name='cc']" mode="oaire" />
</oaire:resource>
</xsl:template>
@@ -641,9 +643,9 @@
<xsl:with-param name="value" select="$rightsValue"/>
</xsl:call-template>
</xsl:variable>
<!-- We are checking to ensure that only values ending in "access" can be used as datacite:rights.
This is a valid solution as we pre-normalize dc.rights values in openaire4.xsl to end in the term
"access" according to COAR Controlled Vocabulary -->
<!-- We are checking to ensure that only values ending in "access" can be used as datacite:rights.
This is a valid solution as we pre-normalize dc.rights values in openaire4.xsl to end in the term
"access" according to COAR Controlled Vocabulary -->
<xsl:if test="ends-with($lc_rightsValue,'access')">
<datacite:rights>
<xsl:if test="$rightsURI">
@@ -1642,7 +1644,25 @@
</xsl:if>
</xsl:template>
<!-- Prepare data for CC License -->
<xsl:variable name="ccstart">
<xsl:value-of select="doc:metadata/doc:element[@name='dc']/doc:element[@name='date']/doc:element[@name='issued']/doc:element/doc:field[@name='value']/text()"/>
</xsl:variable>
<xsl:template
match="doc:element[@name='others']/doc:element[@name='cc']"
mode="oaire">
<oaire:licenseCondition>
<xsl:attribute name="startDate">
<xsl:value-of
select="$ccstart"/>
</xsl:attribute>
<xsl:attribute name="uri">
<xsl:value-of select="./doc:field[@name='uri']/text()" />
</xsl:attribute>
<xsl:value-of select="./doc:field[@name='name']/text()" />
</oaire:licenseCondition>
</xsl:template>
<!-- ignore all non specified text values or attributes -->
<xsl:template match="text()|@*"/>

View File

@@ -20,11 +20,15 @@
dspace.dir = /dspace
# URL of DSpace backend ('server' webapp). Include port number etc.
# This is where REST API and all enabled server modules (OAI-PMH, SWORD, SWORDv2, RDF, etc) will respond
# DO NOT end it with '/'.
# This is where REST API and all enabled server modules (OAI-PMH, SWORD,
# SWORDv2, RDF, etc) will respond.
dspace.server.url = http://localhost:8080/server
# URL of DSpace frontend (Angular UI). Include port number etc
# This is used by the backend to provide links in emails, RSS feeds, Sitemaps, etc.
# URL of DSpace frontend (Angular UI). Include port number etc.
# DO NOT end it with '/'.
# This is used by the backend to provide links in emails, RSS feeds, Sitemaps,
# etc.
dspace.ui.url = http://localhost:4000
# Name of the site
@@ -371,16 +375,6 @@ useProxies = true
# (Requires reboot of servlet container, e.g. Tomcat, to reload)
#proxies.trusted.include_ui_ip = true
# Spring Boot proxy configuration (can be set in local.cfg or in application.properties).
# By default, Spring Boot does not automatically use X-Forwarded-* Headers when generating links (and similar) in the
# REST API. When using a proxy in front of the REST API, you may need to modify this setting:
# * NATIVE = allows your web server to natively support standard Forwarded headers
# * FRAMEWORK = enables Spring Framework's built in filter to manage these headers in Spring Boot
# * NONE = default value. Forwarded headers are ignored
# For more information see
# https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto-use-behind-a-proxy-server
#server.forward-headers-strategy=FRAMEWORK
#### Media Filter / Format Filter plugins (through PluginService) ####
# Media/Format Filters help to full-text index content or
# perform automated format conversions

View File

@@ -33,11 +33,15 @@
dspace.dir=/dspace
# URL of DSpace backend ('server' webapp). Include port number etc.
# This is where REST API and all enabled server modules (OAI-PMH, SWORD, SWORDv2, RDF, etc) will respond
# DO NOT end it with '/'.
# This is where REST API and all enabled server modules (OAI-PMH, SWORD,
# SWORDv2, RDF, etc) will respond.
dspace.server.url = http://localhost:8080/server
# URL of DSpace frontend (Angular UI). Include port number etc
# This is used by the backend to provide links in emails, RSS feeds, Sitemaps, etc.
# URL of DSpace frontend (Angular UI). Include port number etc.
# DO NOT end it with '/'.
# This is used by the backend to provide links in emails, RSS feeds, Sitemaps,
# etc.
dspace.ui.url = http://localhost:4000
# Name of the site

View File

@@ -7,4 +7,5 @@
# The class names of the modules which the dspace servicemanager will attempt to retrieve.
# These classes contain the paths to where to spring files are loaded
spring.springloader.modules=org.dspace.app.configuration.APISpringLoader,\
org.dspace.app.rest.configuration.RESTSpringLoader
org.dspace.app.rest.configuration.RESTSpringLoader,\
org.dspace.xoai.app.OAISpringLoader

View File

@@ -45,7 +45,7 @@
<bitstream-type>
<mimetype>text/plain; charset=utf-8</mimetype>
<short_description>License</short_description>
<description>Item-specific license agreed upon to submission</description>
<description>Item-specific license agreed to upon submission</description>
<support_level>1</support_level>
<internal>true</internal>
</bitstream-type>
@@ -54,7 +54,7 @@
<bitstream-type>
<mimetype>text/html; charset=utf-8</mimetype>
<short_description>CC License</short_description>
<description>Item-specific Creative Commons license agreed upon to submission</description>
<description>Item-specific Creative Commons license agreed to upon submission</description>
<support_level>1</support_level>
<internal>true</internal>
</bitstream-type>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
The contents of this file are subject to the license and copyright
detailed in the LICENSE and NOTICE files at the root of the source
tree and available online at
http://www.dspace.org/license/
-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd">
<context:annotation-config /> <!-- allows us to use spring annotations in beans -->
<!-- Additional item.compile plugin to enrich field with information about
Creative Commons License metadata -->
<bean class="org.dspace.xoai.app.CCElementItemCompilePlugin"/>
</beans>

View File

@@ -37,7 +37,7 @@
<!-- NOTE: Jetty needed for Solr, Handle Server & tests -->
<jetty.version>9.4.38.v20210224</jetty.version>
<log4j.version>2.13.3</log4j.version>
<pdfbox-version>2.0.15</pdfbox-version>
<pdfbox-version>2.0.24</pdfbox-version>
<poi-version>3.17</poi-version>
<slf4j.version>1.7.25</slf4j.version>