Merge branch 'DSpace:main' into w2p-78245_Bugfix-to-UsageReportRestPermissionEvaluatorPlugin

This commit is contained in:
benbosman
2021-05-28 12:20:23 +02:00
92 changed files with 3587 additions and 489 deletions

View File

@@ -34,7 +34,7 @@ Past releases are all available via GitHub at https://github.com/DSpace/DSpace/r
Documentation for each release may be viewed online or downloaded via our [Documentation Wiki](https://wiki.lyrasis.org/display/DSDOC/). Documentation for each release may be viewed online or downloaded via our [Documentation Wiki](https://wiki.lyrasis.org/display/DSDOC/).
The latest DSpace Installation instructions are available at: The latest DSpace Installation instructions are available at:
https://wiki.lyrasis.org/display/DSDOC6x/Installing+DSpace https://wiki.lyrasis.org/display/DSDOC7x/Installing+DSpace
Please be aware that, as a Java web application, DSpace requires a database (PostgreSQL or Oracle) Please be aware that, as a Java web application, DSpace requires a database (PostgreSQL or Oracle)
and a servlet container (usually Tomcat) in order to function. and a servlet container (usually Tomcat) in order to function.

View File

@@ -1,6 +1,11 @@
version: '3.7' version: '3.7'
networks: networks:
dspacenet: dspacenet:
ipam:
config:
# Define a custom subnet for our DSpace network, so that we can easily trust requests from host to container.
# If you customize this value, be sure to customize the 'proxies.trusted.ipranges' in your local.cfg.
- subnet: 172.23.0.0/16
services: services:
# DSpace (backend) webapp container # DSpace (backend) webapp container
dspace: dspace:

View File

@@ -12,7 +12,7 @@
<parent> <parent>
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<artifactId>dspace-parent</artifactId> <artifactId>dspace-parent</artifactId>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
<relativePath>..</relativePath> <relativePath>..</relativePath>
</parent> </parent>
@@ -856,6 +856,10 @@
<groupId>org.javassist</groupId> <groupId>org.javassist</groupId>
<artifactId>javassist</artifactId> <artifactId>javassist</artifactId>
</exclusion> </exclusion>
<exclusion>
<groupId>io.swagger</groupId>
<artifactId>swagger-jersey-jaxrs</artifactId>
</exclusion>
</exclusions> </exclusions>
</dependency> </dependency>

View File

@@ -8,9 +8,12 @@
package org.dspace.app.bulkedit; package org.dspace.app.bulkedit;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.UUID;
import org.apache.commons.cli.ParseException; import org.apache.commons.cli.ParseException;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.dspace.app.util.factory.UtilServiceFactory;
import org.dspace.app.util.service.DSpaceObjectUtils;
import org.dspace.content.DSpaceObject; import org.dspace.content.DSpaceObject;
import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.MetadataDSpaceCsvExportService; import org.dspace.content.service.MetadataDSpaceCsvExportService;
@@ -30,7 +33,7 @@ public class MetadataExport extends DSpaceRunnable<MetadataExportScriptConfigura
private boolean help = false; private boolean help = false;
private String filename = null; private String filename = null;
private String handle = null; private String identifier = null;
private boolean exportAllMetadata = false; private boolean exportAllMetadata = false;
private boolean exportAllItems = false; private boolean exportAllItems = false;
@@ -41,6 +44,8 @@ public class MetadataExport extends DSpaceRunnable<MetadataExportScriptConfigura
private EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService(); private EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService();
private DSpaceObjectUtils dSpaceObjectUtils = UtilServiceFactory.getInstance().getDSpaceObjectUtils();
@Override @Override
public void internalRun() throws Exception { public void internalRun() throws Exception {
@@ -57,7 +62,7 @@ public class MetadataExport extends DSpaceRunnable<MetadataExportScriptConfigura
handler.handleException(e); handler.handleException(e);
} }
DSpaceCSV dSpaceCSV = metadataDSpaceCsvExportService DSpaceCSV dSpaceCSV = metadataDSpaceCsvExportService
.handleExport(context, exportAllItems, exportAllMetadata, handle, .handleExport(context, exportAllItems, exportAllMetadata, identifier,
handler); handler);
handler.writeFilestream(context, filename, dSpaceCSV.getInputStream(), EXPORT_CSV); handler.writeFilestream(context, filename, dSpaceCSV.getInputStream(), EXPORT_CSV);
context.restoreAuthSystemState(); context.restoreAuthSystemState();
@@ -66,7 +71,7 @@ public class MetadataExport extends DSpaceRunnable<MetadataExportScriptConfigura
protected void logHelpInfo() { protected void logHelpInfo() {
handler.logInfo("\nfull export: metadata-export"); handler.logInfo("\nfull export: metadata-export");
handler.logInfo("partial export: metadata-export -i handle"); handler.logInfo("partial export: metadata-export -i handle/UUID");
} }
@Override @Override
@@ -86,7 +91,7 @@ public class MetadataExport extends DSpaceRunnable<MetadataExportScriptConfigura
if (!commandLine.hasOption('i')) { if (!commandLine.hasOption('i')) {
exportAllItems = true; exportAllItems = true;
} }
handle = commandLine.getOptionValue('i'); identifier = commandLine.getOptionValue('i');
filename = getFileNameForExportFile(); filename = getFileNameForExportFile();
exportAllMetadata = commandLine.hasOption('a'); exportAllMetadata = commandLine.hasOption('a');
@@ -97,17 +102,20 @@ public class MetadataExport extends DSpaceRunnable<MetadataExportScriptConfigura
Context context = new Context(); Context context = new Context();
try { try {
DSpaceObject dso = null; DSpaceObject dso = null;
if (StringUtils.isNotBlank(handle)) { if (StringUtils.isNotBlank(identifier)) {
dso = HandleServiceFactory.getInstance().getHandleService().resolveToObject(context, handle); dso = HandleServiceFactory.getInstance().getHandleService().resolveToObject(context, identifier);
if (dso == null) {
dso = dSpaceObjectUtils.findDSpaceObject(context, UUID.fromString(identifier));
}
} else { } else {
dso = ContentServiceFactory.getInstance().getSiteService().findSite(context); dso = ContentServiceFactory.getInstance().getSiteService().findSite(context);
} }
if (dso == null) { if (dso == null) {
throw new ParseException("A handle got given that wasn't able to be parsed to a DSpaceObject"); throw new ParseException("An identifier was given that wasn't able to be parsed to a DSpaceObject");
} }
return dso.getID().toString() + ".csv"; return dso.getID().toString() + ".csv";
} catch (SQLException e) { } catch (SQLException e) {
handler.handleException("Something went wrong trying to retrieve DSO for handle: " + handle, e); handler.handleException("Something went wrong trying to retrieve DSO for identifier: " + identifier, e);
} }
return null; return null;
} }

View File

@@ -152,8 +152,7 @@ public class ItemImportServiceImpl implements ItemImportService, InitializingBea
@Autowired(required = true) @Autowired(required = true)
protected ConfigurationService configurationService; protected ConfigurationService configurationService;
protected final String tempWorkDir protected String tempWorkDir;
= configurationService.getProperty("org.dspace.app.batchitemimport.work.dir");
protected boolean isTest = false; protected boolean isTest = false;
protected boolean isResume = false; protected boolean isResume = false;
@@ -163,6 +162,7 @@ public class ItemImportServiceImpl implements ItemImportService, InitializingBea
@Override @Override
public void afterPropertiesSet() throws Exception { public void afterPropertiesSet() throws Exception {
tempWorkDir = configurationService.getProperty("org.dspace.app.batchitemimport.work.dir");
//Ensure tempWorkDir exists //Ensure tempWorkDir exists
File tempWorkDirFile = new File(tempWorkDir); File tempWorkDirFile = new File(tempWorkDir);
if (!tempWorkDirFile.exists()) { if (!tempWorkDirFile.exists()) {

View File

@@ -10,12 +10,16 @@ package org.dspace.app.launcher;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.TreeMap; import java.util.TreeMap;
import org.apache.commons.cli.ParseException; import org.apache.commons.cli.ParseException;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.dspace.core.Context;
import org.dspace.scripts.DSpaceRunnable; import org.dspace.scripts.DSpaceRunnable;
import org.dspace.scripts.configuration.ScriptConfiguration; import org.dspace.scripts.configuration.ScriptConfiguration;
import org.dspace.scripts.factory.ScriptServiceFactory; import org.dspace.scripts.factory.ScriptServiceFactory;
@@ -318,11 +322,53 @@ public class ScriptLauncher {
} }
/** /**
* Display the commands that the current launcher config file knows about * Display the commands that are defined in launcher.xml and/or the script service.
*
* @param commandConfigs configs as Document * @param commandConfigs configs as Document
*/ */
private static void display(Document commandConfigs) { private static void display(Document commandConfigs) {
// usage
System.out.println("Usage: dspace [command-name] {parameters}");
// commands from launcher.xml
Collection<Element> launcherCommands = getLauncherCommands(commandConfigs);
if (launcherCommands.size() > 0) {
System.out.println("\nCommands from launcher.xml");
for (Element command : launcherCommands) {
displayCommand(
command.getChild("name").getValue(),
command.getChild("description").getValue()
);
}
}
// commands from script service
Collection<ScriptConfiguration> serviceCommands = getServiceCommands();
if (serviceCommands.size() > 0) {
System.out.println("\nCommands from script service");
for (ScriptConfiguration command : serviceCommands) {
displayCommand(
command.getName(),
command.getDescription()
);
}
}
}
/**
* Display a single command using a fixed format. Used by {@link #display}.
* @param name the name that can be used to invoke the command
* @param description the description of the command
*/
private static void displayCommand(String name, String description) {
System.out.format(" - %s: %s\n", name, description);
}
/**
* Get a sorted collection of the commands that are specified in launcher.xml. Used by {@link #display}.
* @param commandConfigs the contexts of launcher.xml
* @return sorted collection of commands
*/
private static Collection<Element> getLauncherCommands(Document commandConfigs) {
// List all command elements // List all command elements
List<Element> commands = commandConfigs.getRootElement().getChildren("command"); List<Element> commands = commandConfigs.getRootElement().getChildren("command");
@@ -334,11 +380,32 @@ public class ScriptLauncher {
sortedCommands.put(command.getChild("name").getValue(), command); sortedCommands.put(command.getChild("name").getValue(), command);
} }
// Display the sorted list return sortedCommands.values();
System.out.println("Usage: dspace [command-name] {parameters}");
for (Element command : sortedCommands.values()) {
System.out.println(" - " + command.getChild("name").getValue() +
": " + command.getChild("description").getValue());
}
} }
/**
* Get a sorted collection of the commands that are defined as beans. Used by {@link #display}.
* @return sorted collection of commands
*/
private static Collection<ScriptConfiguration> getServiceCommands() {
ScriptService scriptService = ScriptServiceFactory.getInstance().getScriptService();
Context throwAwayContext = new Context();
throwAwayContext.turnOffAuthorisationSystem();
List<ScriptConfiguration> scriptConfigurations = scriptService.getScriptConfigurations(throwAwayContext);
throwAwayContext.restoreAuthSystemState();
try {
throwAwayContext.complete();
} catch (SQLException exception) {
exception.printStackTrace();
throwAwayContext.abort();
}
scriptConfigurations.sort(Comparator.comparing(ScriptConfiguration::getName));
return scriptConfigurations;
}
} }

View File

@@ -0,0 +1,47 @@
/**
* 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.app.util;
import java.sql.SQLException;
import java.util.UUID;
import org.dspace.app.util.service.DSpaceObjectUtils;
import org.dspace.content.DSpaceObject;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.DSpaceObjectService;
import org.dspace.core.Context;
import org.springframework.beans.factory.annotation.Autowired;
public class DSpaceObjectUtilsImpl implements DSpaceObjectUtils {
@Autowired
private ContentServiceFactory contentServiceFactory;
/**
* Retrieve a DSpaceObject from its uuid. As this method need to iterate over all the different services that
* support concrete class of DSpaceObject it has poor performance. Please consider the use of the direct service
* (ItemService, CommunityService, etc.) if you know in advance the type of DSpaceObject that you are looking for
*
* @param context
* DSpace context
* @param uuid
* the uuid to lookup
* @return the DSpaceObject if any with the supplied uuid
* @throws SQLException
*/
public DSpaceObject findDSpaceObject(Context context, UUID uuid) throws SQLException {
for (DSpaceObjectService<? extends DSpaceObject> dSpaceObjectService :
contentServiceFactory.getDSpaceObjectServices()) {
DSpaceObject dso = dSpaceObjectService.find(context, uuid);
if (dso != null) {
return dso;
}
}
return null;
}
}

View File

@@ -7,6 +7,7 @@
*/ */
package org.dspace.app.util.factory; package org.dspace.app.util.factory;
import org.dspace.app.util.service.DSpaceObjectUtils;
import org.dspace.app.util.service.MetadataExposureService; import org.dspace.app.util.service.MetadataExposureService;
import org.dspace.app.util.service.OpenSearchService; import org.dspace.app.util.service.OpenSearchService;
import org.dspace.app.util.service.WebAppService; import org.dspace.app.util.service.WebAppService;
@@ -25,6 +26,8 @@ public abstract class UtilServiceFactory {
public abstract MetadataExposureService getMetadataExposureService(); public abstract MetadataExposureService getMetadataExposureService();
public abstract DSpaceObjectUtils getDSpaceObjectUtils();
public static UtilServiceFactory getInstance() { public static UtilServiceFactory getInstance() {
return DSpaceServicesFactory.getInstance().getServiceManager() return DSpaceServicesFactory.getInstance().getServiceManager()
.getServiceByName("appUtilServiceFactory", UtilServiceFactory.class); .getServiceByName("appUtilServiceFactory", UtilServiceFactory.class);

View File

@@ -7,6 +7,7 @@
*/ */
package org.dspace.app.util.factory; package org.dspace.app.util.factory;
import org.dspace.app.util.service.DSpaceObjectUtils;
import org.dspace.app.util.service.MetadataExposureService; import org.dspace.app.util.service.MetadataExposureService;
import org.dspace.app.util.service.OpenSearchService; import org.dspace.app.util.service.OpenSearchService;
import org.dspace.app.util.service.WebAppService; import org.dspace.app.util.service.WebAppService;
@@ -26,6 +27,8 @@ public class UtilServiceFactoryImpl extends UtilServiceFactory {
private OpenSearchService openSearchService; private OpenSearchService openSearchService;
@Autowired(required = true) @Autowired(required = true)
private WebAppService webAppService; private WebAppService webAppService;
@Autowired(required = true)
private DSpaceObjectUtils dSpaceObjectUtils;
@Override @Override
public WebAppService getWebAppService() { public WebAppService getWebAppService() {
@@ -41,4 +44,9 @@ public class UtilServiceFactoryImpl extends UtilServiceFactory {
public MetadataExposureService getMetadataExposureService() { public MetadataExposureService getMetadataExposureService() {
return metadataExposureService; return metadataExposureService;
} }
@Override
public DSpaceObjectUtils getDSpaceObjectUtils() {
return dSpaceObjectUtils;
}
} }

View File

@@ -0,0 +1,33 @@
/**
* 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.app.util.service;
import java.sql.SQLException;
import java.util.UUID;
import org.dspace.content.DSpaceObject;
import org.dspace.core.Context;
/**
* Utility class providing methods to deal with generic DSpace Object of unknown type
*/
public interface DSpaceObjectUtils {
/**
* Retrieve a DSpaceObject from its uuid. As this method need to iterate over all the different services that
* support concrete class of DSpaceObject it has poor performance. Please consider the use of the direct service
* (ItemService, CommunityService, etc.) if you know in advance the type of DSpaceObject that you are looking for
*
* @param context
* DSpace context
* @param uuid
* the uuid to lookup
* @return the DSpaceObject if any with the supplied uuid
* @throws SQLException
*/
public DSpaceObject findDSpaceObject(Context context, UUID uuid) throws SQLException;
}

View File

@@ -16,6 +16,7 @@ import java.util.Collections;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -480,7 +481,14 @@ public class ShibAuthentication implements AuthenticationMethod {
* Get login page to which to redirect. Returns URL (as string) to which to * Get login page to which to redirect. Returns URL (as string) to which to
* redirect to obtain credentials (either password prompt or e.g. HTTPS port * redirect to obtain credentials (either password prompt or e.g. HTTPS port
* for client cert.); null means no redirect. * for client cert.); null means no redirect.
* * <P>
* For Shibboleth, this URL looks like (note 'target' param is URL encoded, but shown as unencoded in this example)
* [shibURL]?target=[dspace.server.url]/api/authn/shibboleth?redirectUrl=[dspace.ui.url]
* <P>
* This URL is used by the client to redirect directly to Shibboleth for authentication. The "target" param
* is then the location (in REST API) where Shibboleth redirects back to. The "redirectUrl" is the path/URL in the
* client (e.g. Angular UI) which the REST API redirects the user to (after capturing/storing any auth info from
* Shibboleth).
* @param context DSpace context, will be modified (ePerson set) upon success. * @param context DSpace context, will be modified (ePerson set) upon success.
* @param request The HTTP request that started this operation, or null if not * @param request The HTTP request that started this operation, or null if not
* applicable. * applicable.
@@ -507,8 +515,8 @@ public class ShibAuthentication implements AuthenticationMethod {
} }
// Determine the server return URL, where shib will send the user after authenticating. // Determine the server return URL, where shib will send the user after authenticating.
// We need it to go back to DSpace's shibboleth-login url so we will extract the user's information // We need it to go back to DSpace's ShibbolethRestController so we will extract the user's information,
// and locally authenticate them. // locally authenticate them & then redirect back to the UI.
String returnURL = configurationService.getProperty("dspace.server.url") + "/api/authn/shibboleth" String returnURL = configurationService.getProperty("dspace.server.url") + "/api/authn/shibboleth"
+ ((redirectUrl != null) ? "?redirectUrl=" + redirectUrl : ""); + ((redirectUrl != null) ? "?redirectUrl=" + redirectUrl : "");
@@ -533,6 +541,25 @@ public class ShibAuthentication implements AuthenticationMethod {
return "shibboleth"; return "shibboleth";
} }
/**
* Check if Shibboleth plugin is enabled
* @return true if enabled, false otherwise
*/
public static boolean isEnabled() {
final String shibPluginName = new ShibAuthentication().getName();
boolean shibEnabled = false;
// Loop through all enabled authentication plugins to see if Shibboleth is one of them.
Iterator<AuthenticationMethod> authenticationMethodIterator =
AuthenticateServiceFactory.getInstance().getAuthenticationService().authenticationMethodIterator();
while (authenticationMethodIterator.hasNext()) {
if (shibPluginName.equals(authenticationMethodIterator.next().getName())) {
shibEnabled = true;
break;
}
}
return shibEnabled;
}
/** /**
* Identify an existing EPerson based upon the shibboleth attributes provided on * Identify an existing EPerson based upon the shibboleth attributes provided on
* the request object. There are three cases where this can occurr, each as * the request object. There are three cases where this can occurr, each as

View File

@@ -17,8 +17,10 @@ import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.StringTokenizer; import java.util.StringTokenizer;
import java.util.function.Supplier;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.NotImplementedException;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -238,6 +240,21 @@ public abstract class DSpaceObjectServiceImpl<T extends DSpaceObject> implements
public List<MetadataValue> addMetadata(Context context, T dso, MetadataField metadataField, String lang, public List<MetadataValue> addMetadata(Context context, T dso, MetadataField metadataField, String lang,
List<String> values, List<String> authorities, List<Integer> confidences) List<String> values, List<String> authorities, List<Integer> confidences)
throws SQLException { throws SQLException {
//Set place to list length of all metadatavalues for the given schema.element.qualifier combination.
// Subtract one to adhere to the 0 as first element rule
final Supplier<Integer> placeSupplier = () ->
this.getMetadata(dso, metadataField.getMetadataSchema().getName(), metadataField.getElement(),
metadataField.getQualifier(), Item.ANY).size() - 1;
return addMetadata(context, dso, metadataField, lang, values, authorities, confidences, placeSupplier);
}
public List<MetadataValue> addMetadata(Context context, T dso, MetadataField metadataField, String lang,
List<String> values, List<String> authorities, List<Integer> confidences, Supplier<Integer> placeSupplier)
throws SQLException {
boolean authorityControlled = metadataAuthorityService.isAuthorityControlled(metadataField); boolean authorityControlled = metadataAuthorityService.isAuthorityControlled(metadataField);
boolean authorityRequired = metadataAuthorityService.isAuthorityRequired(metadataField); boolean authorityRequired = metadataAuthorityService.isAuthorityRequired(metadataField);
List<MetadataValue> newMetadata = new ArrayList<>(values.size()); List<MetadataValue> newMetadata = new ArrayList<>(values.size());
@@ -252,11 +269,8 @@ public abstract class DSpaceObjectServiceImpl<T extends DSpaceObject> implements
} }
MetadataValue metadataValue = metadataValueService.create(context, dso, metadataField); MetadataValue metadataValue = metadataValueService.create(context, dso, metadataField);
newMetadata.add(metadataValue); newMetadata.add(metadataValue);
//Set place to list length of all metadatavalues for the given schema.element.qualifier combination.
// Subtract one to adhere to the 0 as first element rule metadataValue.setPlace(placeSupplier.get());
metadataValue.setPlace(
this.getMetadata(dso, metadataField.getMetadataSchema().getName(), metadataField.getElement(),
metadataField.getQualifier(), Item.ANY).size() - 1);
metadataValue.setLanguage(lang == null ? null : lang.trim()); metadataValue.setLanguage(lang == null ? null : lang.trim());
@@ -359,7 +373,7 @@ public abstract class DSpaceObjectServiceImpl<T extends DSpaceObject> implements
public MetadataValue addMetadata(Context context, T dso, String schema, String element, String qualifier, public MetadataValue addMetadata(Context context, T dso, String schema, String element, String qualifier,
String lang, String value, String authority, int confidence) throws SQLException { String lang, String value, String authority, int confidence) throws SQLException {
return addMetadata(context, dso, schema, element, qualifier, lang, Arrays.asList(value), return addMetadata(context, dso, schema, element, qualifier, lang, Arrays.asList(value),
Arrays.asList(authority), Arrays.asList(confidence)).get(0); Arrays.asList(authority), Arrays.asList(confidence)).stream().findFirst().orElse(null);
} }
@Override @Override
@@ -805,4 +819,12 @@ public abstract class DSpaceObjectServiceImpl<T extends DSpaceObject> implements
dso.setMetadataModified(); dso.setMetadataModified();
} }
@Override
public MetadataValue addMetadata(Context context, T dso, String schema, String element, String qualifier,
String lang, String value, String authority, int confidence, int place) throws SQLException {
throw new NotImplementedException();
}
} }

View File

@@ -11,12 +11,14 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@@ -1405,5 +1407,25 @@ prevent the generation of resource policy entry values with null dspace_object a
return listToReturn; return listToReturn;
} }
@Override
public MetadataValue addMetadata(Context context, Item dso, String schema, String element, String qualifier,
String lang, String value, String authority, int confidence, int place) throws SQLException {
// We will not verify that they are valid entries in the registry
// until update() is called.
MetadataField metadataField = metadataFieldService.findByElement(context, schema, element, qualifier);
if (metadataField == null) {
throw new SQLException(
"bad_dublin_core schema=" + schema + "." + element + "." + qualifier + ". Metadata field does not " +
"exist!");
}
final Supplier<Integer> placeSupplier = () -> place;
return addMetadata(context, dso, metadataField, lang, Arrays.asList(value),
Arrays.asList(authority), Arrays.asList(confidence), placeSupplier)
.stream().findFirst().orElse(null);
}
} }

View File

@@ -12,9 +12,11 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.UUID;
import com.google.common.collect.Iterators; import com.google.common.collect.Iterators;
import org.dspace.app.bulkedit.DSpaceCSV; import org.dspace.app.bulkedit.DSpaceCSV;
import org.dspace.app.util.service.DSpaceObjectUtils;
import org.dspace.content.service.ItemService; import org.dspace.content.service.ItemService;
import org.dspace.content.service.MetadataDSpaceCsvExportService; import org.dspace.content.service.MetadataDSpaceCsvExportService;
import org.dspace.core.Constants; import org.dspace.core.Constants;
@@ -31,8 +33,11 @@ public class MetadataDSpaceCsvExportServiceImpl implements MetadataDSpaceCsvExpo
@Autowired @Autowired
private ItemService itemService; private ItemService itemService;
@Autowired
private DSpaceObjectUtils dSpaceObjectUtils;
@Override @Override
public DSpaceCSV handleExport(Context context, boolean exportAllItems, boolean exportAllMetadata, String handle, public DSpaceCSV handleExport(Context context, boolean exportAllItems, boolean exportAllMetadata, String identifier,
DSpaceRunnableHandler handler) throws Exception { DSpaceRunnableHandler handler) throws Exception {
Iterator<Item> toExport = null; Iterator<Item> toExport = null;
@@ -40,26 +45,32 @@ public class MetadataDSpaceCsvExportServiceImpl implements MetadataDSpaceCsvExpo
handler.logInfo("Exporting whole repository WARNING: May take some time!"); handler.logInfo("Exporting whole repository WARNING: May take some time!");
toExport = itemService.findAll(context); toExport = itemService.findAll(context);
} else { } else {
DSpaceObject dso = HandleServiceFactory.getInstance().getHandleService().resolveToObject(context, handle); DSpaceObject dso = HandleServiceFactory.getInstance().getHandleService()
.resolveToObject(context, identifier);
if (dso == null) {
dso = dSpaceObjectUtils.findDSpaceObject(context, UUID.fromString(identifier));
}
if (dso == null) { if (dso == null) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Item '" + handle + "' does not resolve to an item in your repository!"); "DSO '" + identifier + "' does not resolve to a DSpace Object in your repository!");
} }
if (dso.getType() == Constants.ITEM) { if (dso.getType() == Constants.ITEM) {
handler.logInfo("Exporting item '" + dso.getName() + "' (" + handle + ")"); handler.logInfo("Exporting item '" + dso.getName() + "' (" + identifier + ")");
List<Item> item = new ArrayList<>(); List<Item> item = new ArrayList<>();
item.add((Item) dso); item.add((Item) dso);
toExport = item.iterator(); toExport = item.iterator();
} else if (dso.getType() == Constants.COLLECTION) { } else if (dso.getType() == Constants.COLLECTION) {
handler.logInfo("Exporting collection '" + dso.getName() + "' (" + handle + ")"); handler.logInfo("Exporting collection '" + dso.getName() + "' (" + identifier + ")");
Collection collection = (Collection) dso; Collection collection = (Collection) dso;
toExport = itemService.findByCollection(context, collection); toExport = itemService.findByCollection(context, collection);
} else if (dso.getType() == Constants.COMMUNITY) { } else if (dso.getType() == Constants.COMMUNITY) {
handler.logInfo("Exporting community '" + dso.getName() + "' (" + handle + ")"); handler.logInfo("Exporting community '" + dso.getName() + "' (" + identifier + ")");
toExport = buildFromCommunity(context, (Community) dso); toExport = buildFromCommunity(context, (Community) dso);
} else { } else {
throw new IllegalArgumentException("Error identifying '" + handle + "'"); throw new IllegalArgumentException(
String.format("DSO with id '%s' (type: %s) can't be exported. Supported types: %s", identifier,
Constants.typeText[dso.getType()], "Item | Collection | Community"));
} }
} }

View File

@@ -367,6 +367,30 @@ public interface DSpaceObjectService<T extends DSpaceObject> {
public MetadataValue addMetadata(Context context, T dso, String schema, String element, String qualifier, public MetadataValue addMetadata(Context context, T dso, String schema, String element, String qualifier,
String lang, String value) throws SQLException; String lang, String value) throws SQLException;
/**
* Add a single metadata value at the given place position.
*
* @param context DSpace context
* @param dso DSpaceObject
* @param schema the schema for the metadata field. <em>Must</em> match
* the <code>name</code> of an existing metadata schema.
* @param element the metadata element name
* @param qualifier the metadata qualifier, or <code>null</code> for
* unqualified
* @param lang the ISO639 language code, optionally followed by an underscore
* and the ISO3166 country code. <code>null</code> means the
* value has no language (for example, a date).
* @param value the value to add.
* @param authority the external authority key for this value (or null)
* @param confidence the authority confidence (default 0)
* @param place the metadata position
* @return the MetadataValue added ot the object
* @throws SQLException if database error
*/
public MetadataValue addMetadata(Context context, T dso, String schema, String element, String qualifier,
String lang, String value, String authority, int confidence, int place) throws SQLException;
/** /**
* Add a single metadata field. This is appended to existing * Add a single metadata field. This is appended to existing
* values. Use <code>clearMetadata</code> to remove values. * values. Use <code>clearMetadata</code> to remove values.

View File

@@ -741,5 +741,4 @@ public interface ItemService
public List<MetadataValue> getMetadata(Item item, String schema, String element, String qualifier, public List<MetadataValue> getMetadata(Item item, String schema, String element, String qualifier,
String lang, boolean enableVirtualMetadata); String lang, boolean enableVirtualMetadata);
} }

View File

@@ -28,12 +28,13 @@ public interface MetadataDSpaceCsvExportService {
* @param context The relevant DSpace context * @param context The relevant DSpace context
* @param exportAllItems A boolean indicating whether or not the entire repository should be exported * @param exportAllItems A boolean indicating whether or not the entire repository should be exported
* @param exportAllMetadata Defines if all metadata should be exported or only the allowed ones * @param exportAllMetadata Defines if all metadata should be exported or only the allowed ones
* @param handle The handle for the DSpaceObject to be exported, can be a Community, Collection or Item * @param identifier The handle or UUID for the DSpaceObject to be exported, can be a Community,
* Collection or Item
* @return A DSpaceCSV object containing the exported information * @return A DSpaceCSV object containing the exported information
* @throws Exception If something goes wrong * @throws Exception If something goes wrong
*/ */
public DSpaceCSV handleExport(Context context, boolean exportAllItems, boolean exportAllMetadata, public DSpaceCSV handleExport(Context context, boolean exportAllItems, boolean exportAllMetadata,
String handle, DSpaceRunnableHandler dSpaceRunnableHandler) throws Exception; String identifier, DSpaceRunnableHandler dSpaceRunnableHandler) throws Exception;
/** /**
* This method will export all the Items in the given toExport iterator to a DSpaceCSV * This method will export all the Items in the given toExport iterator to a DSpaceCSV

View File

@@ -12,6 +12,7 @@ import java.util.List;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.discovery.configuration.DiscoveryConfiguration;
import org.dspace.discovery.configuration.DiscoveryMoreLikeThisConfiguration; import org.dspace.discovery.configuration.DiscoveryMoreLikeThisConfiguration;
import org.dspace.discovery.configuration.DiscoverySearchFilterFacet; import org.dspace.discovery.configuration.DiscoverySearchFilterFacet;
@@ -62,11 +63,14 @@ public interface SearchService {
* @param field the field of the filter query * @param field the field of the filter query
* @param operator equals/notequals/notcontains/authority/notauthority * @param operator equals/notequals/notcontains/authority/notauthority
* @param value the filter query value * @param value the filter query value
* @param config (nullable) the discovery configuration (if not null, field's corresponding facet.type checked to
* be standard so suffix is not added for equals operator)
* @return a filter query * @return a filter query
* @throws SQLException if database error * @throws SQLException if database error
* An exception that provides information on a database access error or other errors. * An exception that provides information on a database access error or other errors.
*/ */
DiscoverFilterQuery toFilterQuery(Context context, String field, String operator, String value) throws SQLException; DiscoverFilterQuery toFilterQuery(Context context, String field, String operator, String value,
DiscoveryConfiguration config) throws SQLException;
List<Item> getRelatedItems(Context context, Item item, List<Item> getRelatedItems(Context context, Item item,
DiscoveryMoreLikeThisConfiguration moreLikeThisConfiguration); DiscoveryMoreLikeThisConfiguration moreLikeThisConfiguration);

View File

@@ -60,6 +60,7 @@ import org.dspace.core.Context;
import org.dspace.core.Email; import org.dspace.core.Email;
import org.dspace.core.I18nUtil; import org.dspace.core.I18nUtil;
import org.dspace.core.LogManager; import org.dspace.core.LogManager;
import org.dspace.discovery.configuration.DiscoveryConfiguration;
import org.dspace.discovery.configuration.DiscoveryConfigurationParameters; import org.dspace.discovery.configuration.DiscoveryConfigurationParameters;
import org.dspace.discovery.configuration.DiscoveryMoreLikeThisConfiguration; import org.dspace.discovery.configuration.DiscoveryMoreLikeThisConfiguration;
import org.dspace.discovery.configuration.DiscoverySearchFilterFacet; import org.dspace.discovery.configuration.DiscoverySearchFilterFacet;
@@ -1069,9 +1070,9 @@ public class SolrServiceImpl implements SearchService, IndexingService {
return new ArrayList<>(0); return new ArrayList<>(0);
} }
} }
@Override @Override
public DiscoverFilterQuery toFilterQuery(Context context, String field, String operator, String value) public DiscoverFilterQuery toFilterQuery(Context context, String field, String operator, String value,
DiscoveryConfiguration config)
throws SQLException { throws SQLException {
DiscoverFilterQuery result = new DiscoverFilterQuery(); DiscoverFilterQuery result = new DiscoverFilterQuery();
@@ -1081,7 +1082,14 @@ public class SolrServiceImpl implements SearchService, IndexingService {
if (operator.endsWith("equals")) { if (operator.endsWith("equals")) {
filterQuery.append("_keyword"); final boolean isStandardField
= Optional.ofNullable(config)
.flatMap(c -> Optional.ofNullable(c.getSidebarFacet(field)))
.map(facet -> facet.getType().equals(DiscoveryConfigurationParameters.TYPE_STANDARD))
.orElse(false);
if (!isStandardField) {
filterQuery.append("_keyword");
}
} else if (operator.endsWith("authority")) { } else if (operator.endsWith("authority")) {
filterQuery.append("_authority"); filterQuery.append("_authority");
} }

View File

@@ -20,26 +20,8 @@ public class DiscoverySortConfiguration {
public static final String SCORE = "score"; public static final String SCORE = "score";
/** Attributes used for sorting of results **/
public enum SORT_ORDER {
desc,
asc
}
private DiscoverySortFieldConfiguration defaultSort = null;
private List<DiscoverySortFieldConfiguration> sortFields = new ArrayList<DiscoverySortFieldConfiguration>(); private List<DiscoverySortFieldConfiguration> sortFields = new ArrayList<DiscoverySortFieldConfiguration>();
private SORT_ORDER defaultSortOrder = SORT_ORDER.desc;
public DiscoverySortFieldConfiguration getDefaultSort() {
return defaultSort;
}
public void setDefaultSort(DiscoverySortFieldConfiguration defaultSort) {
this.defaultSort = defaultSort;
}
public List<DiscoverySortFieldConfiguration> getSortFields() { public List<DiscoverySortFieldConfiguration> getSortFields() {
return sortFields; return sortFields;
} }
@@ -48,14 +30,6 @@ public class DiscoverySortConfiguration {
this.sortFields = sortFields; this.sortFields = sortFields;
} }
public SORT_ORDER getDefaultSortOrder() {
return defaultSortOrder;
}
public void setDefaultSortOrder(SORT_ORDER defaultSortOrder) {
this.defaultSortOrder = defaultSortOrder;
}
public DiscoverySortFieldConfiguration getSortFieldConfiguration(String sortField) { public DiscoverySortFieldConfiguration getSortFieldConfiguration(String sortField) {
if (StringUtils.isBlank(sortField)) { if (StringUtils.isBlank(sortField)) {
return null; return null;
@@ -67,10 +41,6 @@ public class DiscoverySortConfiguration {
return configuration; return configuration;
} }
if (defaultSort != null && StringUtils.equals(defaultSort.getMetadataField(), sortField)) {
return defaultSort;
}
for (DiscoverySortFieldConfiguration sortFieldConfiguration : CollectionUtils.emptyIfNull(sortFields)) { for (DiscoverySortFieldConfiguration sortFieldConfiguration : CollectionUtils.emptyIfNull(sortFields)) {
if (StringUtils.equals(sortFieldConfiguration.getMetadataField(), sortField)) { if (StringUtils.equals(sortFieldConfiguration.getMetadataField(), sortField)) {
return sortFieldConfiguration; return sortFieldConfiguration;

View File

@@ -18,11 +18,18 @@ public class DiscoverySortFieldConfiguration {
private String metadataField; private String metadataField;
private String type = DiscoveryConfigurationParameters.TYPE_TEXT; private String type = DiscoveryConfigurationParameters.TYPE_TEXT;
/** Attributes used for sorting of results **/
public enum SORT_ORDER {
desc,
asc
}
private SORT_ORDER defaultSortOrder;
public String getMetadataField() { public String getMetadataField() {
return metadataField; return metadataField;
} }
@Autowired(required = true)
public void setMetadataField(String metadataField) { public void setMetadataField(String metadataField) {
this.metadataField = metadataField; this.metadataField = metadataField;
} }
@@ -35,6 +42,15 @@ public class DiscoverySortFieldConfiguration {
this.type = type; this.type = type;
} }
public SORT_ORDER getDefaultSortOrder() {
return defaultSortOrder;
}
@Autowired(required = true)
public void setDefaultSortOrder(SORT_ORDER defaultSortOrder) {
this.defaultSortOrder = defaultSortOrder;
}
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (obj != null && obj instanceof DiscoverySortFieldConfiguration) { if (obj != null && obj instanceof DiscoverySortFieldConfiguration) {

View File

@@ -47,8 +47,6 @@ BEGIN
updateseq('fileextension_seq', 'fileextension', 'file_extension_id'); updateseq('fileextension_seq', 'fileextension', 'file_extension_id');
updateseq('resourcepolicy_seq', 'resourcepolicy', 'policy_id'); updateseq('resourcepolicy_seq', 'resourcepolicy', 'policy_id');
updateseq('workspaceitem_seq', 'workspaceitem', 'workspace_item_id'); updateseq('workspaceitem_seq', 'workspaceitem', 'workspace_item_id');
updateseq('workflowitem_seq', 'workflowitem', 'workflow_id');
updateseq('tasklistitem_seq', 'tasklistitem', 'tasklist_id');
updateseq('registrationdata_seq', 'registrationdata', updateseq('registrationdata_seq', 'registrationdata',
'registrationdata_id'); 'registrationdata_id');
updateseq('subscription_seq', 'subscription', 'subscription_id'); updateseq('subscription_seq', 'subscription', 'subscription_id');

View File

@@ -24,8 +24,6 @@ SELECT setval('bitstreamformatregistry_seq', max(bitstream_format_id)) FROM bits
SELECT setval('fileextension_seq', max(file_extension_id)) FROM fileextension; SELECT setval('fileextension_seq', max(file_extension_id)) FROM fileextension;
SELECT setval('resourcepolicy_seq', max(policy_id)) FROM resourcepolicy; SELECT setval('resourcepolicy_seq', max(policy_id)) FROM resourcepolicy;
SELECT setval('workspaceitem_seq', max(workspace_item_id)) FROM workspaceitem; SELECT setval('workspaceitem_seq', max(workspace_item_id)) FROM workspaceitem;
SELECT setval('workflowitem_seq', max(workflow_id)) FROM workflowitem;
SELECT setval('tasklistitem_seq', max(tasklist_id)) FROM tasklistitem;
SELECT setval('registrationdata_seq', max(registrationdata_id)) FROM registrationdata; SELECT setval('registrationdata_seq', max(registrationdata_id)) FROM registrationdata;
SELECT setval('subscription_seq', max(subscription_id)) FROM subscription; SELECT setval('subscription_seq', max(subscription_id)) FROM subscription;
SELECT setval('metadatafieldregistry_seq', max(metadata_field_id)) FROM metadatafieldregistry; SELECT setval('metadatafieldregistry_seq', max(metadata_field_id)) FROM metadatafieldregistry;

View File

@@ -46,9 +46,11 @@
<required></required> <required></required>
</field> </field>
</row> </row>
</form> </form>
<form name="traditionalpageone"> <form name="traditionalpageone">
<!-- NOTE: this Author <row> is customized from the default submission-forms.xml in order to make it
easier to test Entity functionality -->
<row> <row>
<relation-field> <relation-field>
<relationship-type>isAuthorOfPublication</relationship-type> <relationship-type>isAuthorOfPublication</relationship-type>
@@ -182,7 +184,7 @@ it, please enter the types and the actual numbers or codes.</hint>
</field> </field>
</row> </row>
</form> </form>
<form name="traditionalpagetwo"> <form name="traditionalpagetwo">
<row> <row>
<field> <field>
@@ -310,7 +312,7 @@ it, please enter the types and the actual numbers or codes.</hint>
</field> </field>
</row> </row>
</form> </form>
<form name="sampleauthority"> <form name="sampleauthority">
<row> <row>
<field> <field>
@@ -347,7 +349,7 @@ it, please enter the types and the actual numbers or codes.</hint>
<required></required> <required></required>
</field> </field>
</row> </row>
</form> </form>
</form-definitions> </form-definitions>

View File

@@ -12,9 +12,11 @@ import static junit.framework.TestCase.assertTrue;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.UUID;
import org.apache.commons.cli.ParseException; import org.apache.commons.cli.ParseException;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.dspace.AbstractIntegrationTestWithDatabase; import org.dspace.AbstractIntegrationTestWithDatabase;
import org.dspace.app.launcher.ScriptLauncher; import org.dspace.app.launcher.ScriptLauncher;
import org.dspace.app.scripts.handler.impl.TestDSpaceRunnableHandler; import org.dspace.app.scripts.handler.impl.TestDSpaceRunnableHandler;
@@ -24,6 +26,7 @@ import org.dspace.builder.ItemBuilder;
import org.dspace.content.Collection; import org.dspace.content.Collection;
import org.dspace.content.Community; import org.dspace.content.Community;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.core.Constants;
import org.dspace.scripts.DSpaceRunnable; import org.dspace.scripts.DSpaceRunnable;
import org.dspace.scripts.configuration.ScriptConfiguration; import org.dspace.scripts.configuration.ScriptConfiguration;
import org.dspace.scripts.factory.ScriptServiceFactory; import org.dspace.scripts.factory.ScriptServiceFactory;
@@ -100,4 +103,148 @@ public class MetadataExportIT
script.run(); script.run();
} }
} }
@Test
public void metadataExportToCsvTestUUID() throws Exception {
context.turnOffAuthorisationSystem();
Community community = CommunityBuilder.createCommunity(context)
.build();
Collection collection = CollectionBuilder.createCollection(context, community)
.build();
Item item = ItemBuilder.createItem(context, collection)
.withAuthor("Donald, Smith")
.build();
context.restoreAuthSystemState();
String fileLocation = configurationService.getProperty("dspace.dir")
+ testProps.get("test.exportcsv").toString();
String[] args = new String[] {"metadata-export",
"-i", String.valueOf(item.getID()),
"-f", fileLocation};
TestDSpaceRunnableHandler testDSpaceRunnableHandler
= new TestDSpaceRunnableHandler();
ScriptLauncher.handleScript(args, ScriptLauncher.getConfig(kernelImpl),
testDSpaceRunnableHandler, kernelImpl);
File file = new File(fileLocation);
String fileContent = IOUtils.toString(new FileInputStream(file), StandardCharsets.UTF_8);
assertTrue(fileContent.contains("Donald, Smith"));
assertTrue(fileContent.contains(String.valueOf(item.getID())));
}
@Test
public void metadataExportToCsvTestUUIDParent() throws Exception {
context.turnOffAuthorisationSystem();
Community community = CommunityBuilder.createCommunity(context)
.build();
Collection collection = CollectionBuilder.createCollection(context, community)
.build();
Item item = ItemBuilder.createItem(context, collection)
.withAuthor("Donald, Smith")
.build();
context.restoreAuthSystemState();
String fileLocation = configurationService.getProperty("dspace.dir")
+ testProps.get("test.exportcsv").toString();
String[] args = new String[] {"metadata-export",
"-i", String.valueOf(collection.getID()),
"-f", fileLocation};
TestDSpaceRunnableHandler testDSpaceRunnableHandler
= new TestDSpaceRunnableHandler();
ScriptLauncher.handleScript(args, ScriptLauncher.getConfig(kernelImpl),
testDSpaceRunnableHandler, kernelImpl);
File file = new File(fileLocation);
String fileContent = IOUtils.toString(new FileInputStream(file), StandardCharsets.UTF_8);
assertTrue(fileContent.contains("Donald, Smith"));
assertTrue(fileContent.contains(String.valueOf(item.getID())));
}
@Test
public void metadataExportToCsvTestUUIDGrandParent() throws Exception {
context.turnOffAuthorisationSystem();
Community community = CommunityBuilder.createCommunity(context)
.build();
Collection collection = CollectionBuilder.createCollection(context, community)
.build();
Item item = ItemBuilder.createItem(context, collection)
.withAuthor("Donald, Smith")
.build();
context.restoreAuthSystemState();
String fileLocation = configurationService.getProperty("dspace.dir")
+ testProps.get("test.exportcsv").toString();
String[] args = new String[] {"metadata-export",
"-i", String.valueOf(community.getID()),
"-f", fileLocation};
TestDSpaceRunnableHandler testDSpaceRunnableHandler
= new TestDSpaceRunnableHandler();
ScriptLauncher.handleScript(args, ScriptLauncher.getConfig(kernelImpl),
testDSpaceRunnableHandler, kernelImpl);
File file = new File(fileLocation);
String fileContent = IOUtils.toString(new FileInputStream(file), StandardCharsets.UTF_8);
assertTrue(fileContent.contains("Donald, Smith"));
assertTrue(fileContent.contains(String.valueOf(item.getID())));
}
@Test
public void metadataExportToCsvTest_NonValidIdentifier() throws Exception {
String fileLocation = configurationService.getProperty("dspace.dir")
+ testProps.get("test.exportcsv").toString();
String nonValidUUID = String.valueOf(UUID.randomUUID());
String[] args = new String[] {"metadata-export", "-i", nonValidUUID, "-f", fileLocation};
TestDSpaceRunnableHandler testDSpaceRunnableHandler
= new TestDSpaceRunnableHandler();
ScriptService scriptService = ScriptServiceFactory.getInstance().getScriptService();
ScriptConfiguration scriptConfiguration = scriptService.getScriptConfiguration(args[0]);
DSpaceRunnable script = null;
if (scriptConfiguration != null) {
script = scriptService.createDSpaceRunnableForScriptConfiguration(scriptConfiguration);
}
if (script != null) {
script.initialize(args, testDSpaceRunnableHandler, null);
script.run();
}
Exception exceptionDuringTestRun = testDSpaceRunnableHandler.getException();
assertTrue("Random UUID caused IllegalArgumentException",
exceptionDuringTestRun instanceof IllegalArgumentException);
assertTrue("IllegalArgumentException contains mention of the non-valid UUID",
StringUtils.contains(exceptionDuringTestRun.getMessage(), nonValidUUID));
}
@Test
public void metadataExportToCsvTest_NonValidDSOType() throws Exception {
String fileLocation = configurationService.getProperty("dspace.dir")
+ testProps.get("test.exportcsv").toString();
String uuidNonValidDSOType = String.valueOf(eperson.getID());
String[] args = new String[] {"metadata-export", "-i", uuidNonValidDSOType, "-f", fileLocation};
TestDSpaceRunnableHandler testDSpaceRunnableHandler
= new TestDSpaceRunnableHandler();
ScriptService scriptService = ScriptServiceFactory.getInstance().getScriptService();
ScriptConfiguration scriptConfiguration = scriptService.getScriptConfiguration(args[0]);
DSpaceRunnable script = null;
if (scriptConfiguration != null) {
script = scriptService.createDSpaceRunnableForScriptConfiguration(scriptConfiguration);
}
if (script != null) {
script.initialize(args, testDSpaceRunnableHandler, null);
script.run();
}
Exception exceptionDuringTestRun = testDSpaceRunnableHandler.getException();
assertTrue("UUID of non-supported dsoType IllegalArgumentException",
exceptionDuringTestRun instanceof IllegalArgumentException);
assertTrue("IllegalArgumentException contains mention of the UUID of non-supported dsoType",
StringUtils.contains(exceptionDuringTestRun.getMessage(), uuidNonValidDSOType));
assertTrue("IllegalArgumentException contains mention of the non-supported dsoType",
StringUtils.contains(exceptionDuringTestRun.getMessage(), Constants.typeText[eperson.getType()]));
}
} }

View File

@@ -8,7 +8,7 @@
<parent> <parent>
<artifactId>dspace-parent</artifactId> <artifactId>dspace-parent</artifactId>
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
<relativePath>..</relativePath> <relativePath>..</relativePath>
</parent> </parent>

View File

@@ -9,7 +9,7 @@
<parent> <parent>
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<artifactId>dspace-parent</artifactId> <artifactId>dspace-parent</artifactId>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
<relativePath>..</relativePath> <relativePath>..</relativePath>
</parent> </parent>

View File

@@ -3,7 +3,7 @@
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<artifactId>dspace-rest</artifactId> <artifactId>dspace-rest</artifactId>
<packaging>war</packaging> <packaging>war</packaging>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
<name>DSpace (Deprecated) REST Webapp</name> <name>DSpace (Deprecated) REST Webapp</name>
<description>DSpace RESTful Web Services API. NOTE: this REST API is DEPRECATED. <description>DSpace RESTful Web Services API. NOTE: this REST API is DEPRECATED.
Please consider using the REST API in the dspace-server-webapp instead!</description> Please consider using the REST API in the dspace-server-webapp instead!</description>
@@ -12,7 +12,7 @@
<parent> <parent>
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<artifactId>dspace-parent</artifactId> <artifactId>dspace-parent</artifactId>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
<relativePath>..</relativePath> <relativePath>..</relativePath>
</parent> </parent>

View File

@@ -15,7 +15,7 @@
<parent> <parent>
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<artifactId>dspace-parent</artifactId> <artifactId>dspace-parent</artifactId>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
<relativePath>..</relativePath> <relativePath>..</relativePath>
</parent> </parent>

View File

@@ -14,6 +14,7 @@ import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.dspace.app.rest.model.AuthnRest; import org.dspace.app.rest.model.AuthnRest;
import org.dspace.authenticate.ShibAuthentication;
import org.dspace.core.Utils; import org.dspace.core.Utils;
import org.dspace.services.ConfigurationService; import org.dspace.services.ConfigurationService;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -27,10 +28,27 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
/** /**
* Rest controller that handles redirect after shibboleth authentication succeded * Rest controller that handles redirect *after* shibboleth authentication succeeded.
* <P>
* Shibboleth authentication does NOT occur in this Controller, but occurs before this class is called.
* The general Shibboleth login process is as follows:
* 1. When Shibboleth plugin is enabled, client/UI receives Shibboleth's absolute URL in WWW-Authenticate header.
* See {@link org.dspace.authenticate.ShibAuthentication} loginPageURL() method.
* 2. Client sends the user to that URL when they select Shibboleth authentication.
* 3. User logs in using Shibboleth
* 4. If successful, they are redirected by Shibboleth to this Controller (the path of this controller is passed
* to Shibboleth as a URL param in step 1)
* 5. NOTE: Prior to hitting this Controller, {@link org.dspace.app.rest.security.ShibbolethAuthenticationFilter}
* briefly intercepts the request in order to check for a valid Shibboleth login (see
* ShibAuthentication.authenticate()) and store that user info in a JWT.
* 6. This Controller then gets the request & looks for a "redirectUrl" param (also a part of the original URL from
* step 1), and redirects the user to that location (after verifying it's a trusted URL). Usually this is a
* redirect back to the Client/UI page where the User started.
* *
* @author Andrea Bollini (andrea dot bollini at 4science dot it) * @author Andrea Bollini (andrea dot bollini at 4science dot it)
* @author Giuseppe Digilio (giuseppe dot digilio at 4science dot it) * @author Giuseppe Digilio (giuseppe dot digilio at 4science dot it)
* @see ShibAuthentication
* @see org.dspace.app.rest.security.ShibbolethAuthenticationFilter
*/ */
@RequestMapping(value = "/api/" + AuthnRest.CATEGORY + "/shibboleth") @RequestMapping(value = "/api/" + AuthnRest.CATEGORY + "/shibboleth")
@RestController @RestController
@@ -56,7 +74,11 @@ public class ShibbolethRestController implements InitializingBean {
@RequestMapping(method = RequestMethod.GET) @RequestMapping(method = RequestMethod.GET)
public void shibboleth(HttpServletResponse response, public void shibboleth(HttpServletResponse response,
@RequestParam(name = "redirectUrl", required = false) String redirectUrl) throws IOException { @RequestParam(name = "redirectUrl", required = false) String redirectUrl) throws IOException {
if (redirectUrl == null) { // NOTE: By the time we get here, we already know that Shibboleth is enabled & authentication succeeded,
// as both of those are verified by ShibbolethAuthenticationFilter which runs before this controller
// If redirectUrl unspecified, default to the configured UI
if (StringUtils.isEmpty(redirectUrl)) {
redirectUrl = configurationService.getProperty("dspace.ui.url"); redirectUrl = configurationService.getProperty("dspace.ui.url");
} }

View File

@@ -0,0 +1,74 @@
/**
* 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.app.rest.authorization.impl;
import java.sql.SQLException;
import java.util.Objects;
import java.util.UUID;
import org.dspace.app.rest.authorization.AuthorizationFeature;
import org.dspace.app.rest.authorization.AuthorizationFeatureDocumentation;
import org.dspace.app.rest.model.BaseObjectRest;
import org.dspace.app.rest.model.ItemRest;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.content.Item;
import org.dspace.content.service.ItemService;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* The create version feature. It can be used to verify if the user can create the version of an Item.
*
* @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.it)
*/
@Component
@AuthorizationFeatureDocumentation(name = CanCreateVersionFeature.NAME,
description = "It can be used to verify if the user can create a new version of an Item")
public class CanCreateVersionFeature implements AuthorizationFeature {
public static final String NAME = "canCreateVersion";
@Autowired
private ConfigurationService configurationService;
@Autowired
private AuthorizeService authorizeService;
@Autowired
private ItemService itemService;
@Override
public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException {
if (object instanceof ItemRest) {
EPerson currentUser = context.getCurrentUser();
if (Objects.isNull(currentUser)) {
return false;
}
if (authorizeService.isAdmin(context)) {
return true;
}
if (configurationService.getBooleanProperty("versioning.submitterCanCreateNewVersion")) {
Item item = itemService.find(context, UUID.fromString(((ItemRest) object).getUuid()));
EPerson submitter = item.getSubmitter();
return Objects.nonNull(submitter) && currentUser.getID().equals(submitter.getID());
}
}
return false;
}
@Override
public String[] getSupportedTypes() {
return new String[]{
ItemRest.CATEGORY + "." + ItemRest.NAME
};
}
}

View File

@@ -0,0 +1,62 @@
/**
* 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.app.rest.authorization.impl;
import java.sql.SQLException;
import org.dspace.app.rest.authorization.AuthorizationFeature;
import org.dspace.app.rest.authorization.AuthorizationFeatureDocumentation;
import org.dspace.app.rest.model.BaseObjectRest;
import org.dspace.app.rest.model.ItemRest;
import org.dspace.app.rest.utils.Utils;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.content.DSpaceObject;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* The manageBitstreamBundles feature. It can be used to verify
* if the user can manage (ADD | REMOVE) the bundles of bitstreams of an Item.
*
* @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.it)
*/
@Component
@AuthorizationFeatureDocumentation(name = CanManageBitstreamBundlesFeature.NAME,
description = "It can be used to verify if the user can manage (ADD | REMOVE) the bundles of bitstreams of an Item")
public class CanManageBitstreamBundlesFeature implements AuthorizationFeature {
public static final String NAME = "canManageBitstreamBundles";
@Autowired
private AuthorizeService authorizeService;
@Autowired
private Utils utils;
@Override
public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException {
if (object instanceof ItemRest) {
DSpaceObject dSpaceObject = (DSpaceObject) utils.getDSpaceAPIObjectFromRest(context, object);
boolean hasRemovePermission = authorizeService.authorizeActionBoolean(context, context.getCurrentUser(),
dSpaceObject, Constants.REMOVE, true);
boolean hasAddPermission = authorizeService.authorizeActionBoolean(context, context.getCurrentUser(),
dSpaceObject, Constants.ADD, true);
return (hasRemovePermission && hasAddPermission);
}
return false;
}
@Override
public String[] getSupportedTypes() {
return new String[]{
ItemRest.CATEGORY + "." + ItemRest.NAME
};
}
}

View File

@@ -8,16 +8,25 @@
package org.dspace.app.rest.authorization.impl; package org.dspace.app.rest.authorization.impl;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.commons.collections.CollectionUtils;
import org.dspace.app.rest.authorization.AuthorizationFeature; import org.dspace.app.rest.authorization.AuthorizationFeature;
import org.dspace.app.rest.authorization.AuthorizationFeatureDocumentation; import org.dspace.app.rest.authorization.AuthorizationFeatureDocumentation;
import org.dspace.app.rest.model.BaseObjectRest; import org.dspace.app.rest.model.BaseObjectRest;
import org.dspace.app.rest.model.CollectionRest; import org.dspace.app.rest.model.CollectionRest;
import org.dspace.app.rest.model.ItemRest;
import org.dspace.app.rest.utils.Utils; import org.dspace.app.rest.utils.Utils;
import org.dspace.authorize.service.AuthorizeService; import org.dspace.authorize.service.AuthorizeService;
import org.dspace.content.Collection; import org.dspace.content.Collection;
import org.dspace.content.Item;
import org.dspace.content.service.CollectionService;
import org.dspace.content.service.ItemService;
import org.dspace.core.Constants; import org.dspace.core.Constants;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.discovery.SearchServiceException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -27,11 +36,11 @@ import org.springframework.stereotype.Component;
* Authorization is granted if the current user has ADD and WRITE permissions on the given Collection. * Authorization is granted if the current user has ADD and WRITE permissions on the given Collection.
*/ */
@Component @Component
@AuthorizationFeatureDocumentation(name = ManageMappedItemsFeature.NAME, @AuthorizationFeatureDocumentation(name = CanManageMappingsFeature.NAME,
description = "It can be used to verify if mapped items can be listed, searched, added and removed") description = "It can be used to verify if mapped items can be listed, searched, added and removed")
public class ManageMappedItemsFeature implements AuthorizationFeature { public class CanManageMappingsFeature implements AuthorizationFeature {
public final static String NAME = "canManageMappedItems"; public final static String NAME = "canManageMappings";
@Autowired @Autowired
private AuthorizeService authorizeService; private AuthorizeService authorizeService;
@@ -39,6 +48,12 @@ public class ManageMappedItemsFeature implements AuthorizationFeature {
@Autowired @Autowired
private Utils utils; private Utils utils;
@Autowired
private ItemService itemService;
@Autowired
private CollectionService collectionService;
@Override @Override
public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException { public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException {
if (object instanceof CollectionRest) { if (object instanceof CollectionRest) {
@@ -49,13 +64,26 @@ public class ManageMappedItemsFeature implements AuthorizationFeature {
return true; return true;
} }
} }
if (object instanceof ItemRest) {
Item item = itemService.find(context, UUID.fromString(((ItemRest) object).getUuid()));
try {
List<Collection> collections = collectionService.findCollectionsWithSubmit("", context, null, 0, 2)
.stream()
.filter(c -> !c.getID().equals(item.getOwningCollection().getID()))
.collect(Collectors.toList());
return CollectionUtils.isNotEmpty(collections);
} catch (SearchServiceException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
return false; return false;
} }
@Override @Override
public String[] getSupportedTypes() { public String[] getSupportedTypes() {
return new String[]{ return new String[]{
CollectionRest.CATEGORY + "." + CollectionRest.NAME CollectionRest.CATEGORY + "." + CollectionRest.NAME,
ItemRest.CATEGORY + "." + ItemRest.NAME
}; };
} }
} }

View File

@@ -0,0 +1,58 @@
/**
* 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.app.rest.authorization.impl;
import java.sql.SQLException;
import org.dspace.app.rest.authorization.AuthorizationFeature;
import org.dspace.app.rest.authorization.AuthorizationFeatureDocumentation;
import org.dspace.app.rest.model.BaseObjectRest;
import org.dspace.app.rest.model.ItemRest;
import org.dspace.app.rest.utils.Utils;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.content.DSpaceObject;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* The CanManageRelationshipsFeature feature. It can be used to verify
* if the user has WRITE permission on the Item.
*
* @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.it)
*/
@Component
@AuthorizationFeatureDocumentation(name = CanManageRelationshipsFeature.NAME,
description = "It can be used to verify if the user has permissions to manage relationships of the Item")
public class CanManageRelationshipsFeature implements AuthorizationFeature {
public static final String NAME = "canManageRelationships";
@Autowired
private AuthorizeService authorizeService;
@Autowired
private Utils utils;
@Override
public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException {
if (object instanceof ItemRest) {
DSpaceObject dSpaceObject = (DSpaceObject) utils.getDSpaceAPIObjectFromRest(context, object);
return authorizeService.authorizeActionBoolean(context, context.getCurrentUser(),
dSpaceObject, Constants.WRITE, true);
}
return false;
}
@Override
public String[] getSupportedTypes() {
return new String[]{
ItemRest.CATEGORY + "." + ItemRest.NAME
};
}
}

View File

@@ -0,0 +1,75 @@
/**
* 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.app.rest.authorization.impl;
import java.sql.SQLException;
import java.util.Objects;
import java.util.UUID;
import org.dspace.app.rest.authorization.AuthorizationFeature;
import org.dspace.app.rest.authorization.AuthorizationFeatureDocumentation;
import org.dspace.app.rest.model.BaseObjectRest;
import org.dspace.app.rest.model.ItemRest;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.content.Item;
import org.dspace.content.service.ItemService;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* The manage versions feature. It can be used to verify
* if the user can create/delete or update the version of an Item.
*
* @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.it)
*/
@Component
@AuthorizationFeatureDocumentation(name = CanManageVersionsFeature.NAME,
description = "It can be used to verify if the user can create/delete or update the version of an Item")
public class CanManageVersionsFeature implements AuthorizationFeature {
public static final String NAME = "canManageVersions";
@Autowired
private ConfigurationService configurationService;
@Autowired
private AuthorizeService authorizeService;
@Autowired
private ItemService itemService;
@Override
public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException {
if (object instanceof ItemRest) {
EPerson currentUser = context.getCurrentUser();
if (Objects.isNull(currentUser)) {
return false;
}
if (authorizeService.isAdmin(context)) {
return true;
}
if (configurationService.getBooleanProperty("versioning.submitterCanCreateNewVersion")) {
Item item = itemService.find(context, UUID.fromString(((ItemRest) object).getUuid()));
EPerson submitter = item.getSubmitter();
return Objects.nonNull(submitter) && currentUser.getID().equals(submitter.getID());
}
}
return false;
}
@Override
public String[] getSupportedTypes() {
return new String[]{
ItemRest.CATEGORY + "." + ItemRest.NAME
};
}
}

View File

@@ -27,11 +27,11 @@ import org.springframework.stereotype.Component;
* current user is the object's admin. Otherwise, authorization is granted if the current user can view the object. * current user is the object's admin. Otherwise, authorization is granted if the current user can view the object.
*/ */
@Component @Component
@AuthorizationFeatureDocumentation(name = ViewVersionsFeature.NAME, @AuthorizationFeatureDocumentation(name = CanSeeVersionsFeature.NAME,
description = "It can be used to verify if the user can view the versions of an Item") description = "It can be used to verify if the user can view the versions of an Item")
public class ViewVersionsFeature implements AuthorizationFeature { public class CanSeeVersionsFeature implements AuthorizationFeature {
public final static String NAME = "canViewVersions"; public final static String NAME = "canSeeVersions";
@Autowired @Autowired
private ConfigurationService configurationService; private ConfigurationService configurationService;

View File

@@ -11,6 +11,7 @@ import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.dspace.app.rest.model.SearchConfigurationRest; import org.dspace.app.rest.model.SearchConfigurationRest;
import org.dspace.app.rest.projection.Projection; import org.dspace.app.rest.projection.Projection;
import org.dspace.discovery.configuration.DiscoveryConfiguration; import org.dspace.discovery.configuration.DiscoveryConfiguration;
@@ -36,7 +37,6 @@ public class DiscoverConfigurationConverter
addSearchFilters(searchConfigurationRest, addSearchFilters(searchConfigurationRest,
configuration.getSearchFilters(), configuration.getSidebarFacets()); configuration.getSearchFilters(), configuration.getSidebarFacets());
addSortOptions(searchConfigurationRest, configuration.getSearchSortConfiguration()); addSortOptions(searchConfigurationRest, configuration.getSearchSortConfiguration());
setDefaultSortOption(configuration, searchConfigurationRest);
} }
return searchConfigurationRest; return searchConfigurationRest;
} }
@@ -46,23 +46,6 @@ public class DiscoverConfigurationConverter
return DiscoveryConfiguration.class; return DiscoveryConfiguration.class;
} }
private void setDefaultSortOption(DiscoveryConfiguration configuration,
SearchConfigurationRest searchConfigurationRest) {
String defaultSort = configuration.getSearchSortConfiguration().SCORE;
if (configuration.getSearchSortConfiguration() != null) {
DiscoverySortFieldConfiguration discoverySortFieldConfiguration = configuration.getSearchSortConfiguration()
.getSortFieldConfiguration(
defaultSort);
if (discoverySortFieldConfiguration != null) {
SearchConfigurationRest.SortOption sortOption = new SearchConfigurationRest.SortOption();
sortOption.setName(discoverySortFieldConfiguration.getMetadataField());
sortOption.setActualName(discoverySortFieldConfiguration.getType());
searchConfigurationRest.addSortOption(sortOption);
}
}
}
public void addSearchFilters(SearchConfigurationRest searchConfigurationRest, public void addSearchFilters(SearchConfigurationRest searchConfigurationRest,
List<DiscoverySearchFilter> searchFilterList, List<DiscoverySearchFilter> searchFilterList,
List<DiscoverySearchFilterFacet> facetList) { List<DiscoverySearchFilterFacet> facetList) {
@@ -88,8 +71,13 @@ public class DiscoverConfigurationConverter
for (DiscoverySortFieldConfiguration discoverySearchSortConfiguration : CollectionUtils for (DiscoverySortFieldConfiguration discoverySearchSortConfiguration : CollectionUtils
.emptyIfNull(searchSortConfiguration.getSortFields())) { .emptyIfNull(searchSortConfiguration.getSortFields())) {
SearchConfigurationRest.SortOption sortOption = new SearchConfigurationRest.SortOption(); SearchConfigurationRest.SortOption sortOption = new SearchConfigurationRest.SortOption();
sortOption.setName(discoverySearchSortConfiguration.getMetadataField()); if (StringUtils.isBlank(discoverySearchSortConfiguration.getMetadataField())) {
sortOption.setName(DiscoverySortConfiguration.SCORE);
} else {
sortOption.setName(discoverySearchSortConfiguration.getMetadataField());
}
sortOption.setActualName(discoverySearchSortConfiguration.getType()); sortOption.setActualName(discoverySearchSortConfiguration.getType());
sortOption.setSortOrder(discoverySearchSortConfiguration.getDefaultSortOrder().name());
searchConfigurationRest.addSortOption(sortOption); searchConfigurationRest.addSortOption(sortOption);
} }
} }

View File

@@ -11,18 +11,17 @@ import static org.springframework.web.servlet.DispatcherServlet.EXCEPTION_ATTRIB
import java.io.IOException; import java.io.IOException;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.dspace.app.rest.security.RestAuthenticationService;
import org.dspace.app.rest.utils.ContextUtil; import org.dspace.app.rest.utils.ContextUtil;
import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.AuthorizeException;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.springframework.beans.TypeMismatchException; import org.springframework.beans.TypeMismatchException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.repository.support.QueryMethodParameterConversionException; import org.springframework.data.repository.support.QueryMethodParameterConversionException;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@@ -59,13 +58,11 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH
*/ */
private static final Set<Integer> LOG_AS_ERROR = Set.of(422); private static final Set<Integer> LOG_AS_ERROR = Set.of(422);
@Autowired
private RestAuthenticationService restAuthenticationService;
@ExceptionHandler({AuthorizeException.class, RESTAuthorizationException.class, AccessDeniedException.class}) @ExceptionHandler({AuthorizeException.class, RESTAuthorizationException.class, AccessDeniedException.class})
protected void handleAuthorizeException(HttpServletRequest request, HttpServletResponse response, Exception ex) protected void handleAuthorizeException(HttpServletRequest request, HttpServletResponse response, Exception ex)
throws IOException { throws IOException {
if (restAuthenticationService.hasAuthenticationData(request)) { Context context = ContextUtil.obtainContext(request);
if (Objects.nonNull(context.getCurrentUser())) {
sendErrorResponse(request, response, ex, "Access is denied", HttpServletResponse.SC_FORBIDDEN); sendErrorResponse(request, response, ex, "Access is denied", HttpServletResponse.SC_FORBIDDEN);
} else { } else {
sendErrorResponse(request, response, ex, "Authentication is required", HttpServletResponse.SC_UNAUTHORIZED); sendErrorResponse(request, response, ex, "Authentication is required", HttpServletResponse.SC_UNAUTHORIZED);
@@ -119,6 +116,14 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH
HttpStatus.UNPROCESSABLE_ENTITY.value()); HttpStatus.UNPROCESSABLE_ENTITY.value());
} }
@ExceptionHandler( {InvalidSearchRequestException.class})
protected void handleInvalidSearchRequestException(HttpServletRequest request, HttpServletResponse response,
Exception ex) throws IOException {
sendErrorResponse(request, response, null,
"Invalid search request",
HttpStatus.UNPROCESSABLE_ENTITY.value());
}
/** /**
* Add user-friendly error messages to the response body for selected errors. * Add user-friendly error messages to the response body for selected errors.
* Since the error messages will be exposed to the API user, the exception classes are expected to implement * Since the error messages will be exposed to the API user, the exception classes are expected to implement

View File

@@ -0,0 +1,28 @@
/**
* 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.app.rest.exception;
import org.dspace.app.rest.utils.DiscoverQueryBuilder;
/**
* This exception is thrown when the given search configuration
* passed to {@link DiscoverQueryBuilder} is invalid
*
* @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.it)
*/
public class InvalidSearchRequestException extends RuntimeException {
public InvalidSearchRequestException(String message, Throwable cause) {
super(message, cause);
}
public InvalidSearchRequestException(String message) {
super(message);
}
}

View File

@@ -247,6 +247,7 @@ public class SearchConfigurationRest extends BaseObjectRest<String> {
@JsonIgnore @JsonIgnore
private String actualName; private String actualName;
private String name; private String name;
private String sortOrder;
public void setActualName(String name) { public void setActualName(String name) {
this.actualName = name; this.actualName = name;
@@ -264,6 +265,14 @@ public class SearchConfigurationRest extends BaseObjectRest<String> {
return name; return name;
} }
public String getSortOrder() {
return sortOrder;
}
public void setSortOrder(String sortOrder) {
this.sortOrder = sortOrder;
}
@Override @Override
public boolean equals(Object object) { public boolean equals(Object object) {
return (object instanceof SearchConfigurationRest.SortOption && return (object instanceof SearchConfigurationRest.SortOption &&

View File

@@ -12,6 +12,7 @@ import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import org.dspace.app.rest.utils.URLUtils; import org.dspace.app.rest.utils.URLUtils;
import org.dspace.app.rest.utils.Utils;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
@@ -102,6 +103,8 @@ public class EmbeddedPageHeader {
if (page != null) { if (page != null) {
// replace existing page & size params (if exist), otherwise append them // replace existing page & size params (if exist), otherwise append them
uriComp = uriComp.replaceQueryParam("page", page); uriComp = uriComp.replaceQueryParam("page", page);
}
if (size != Utils.DEFAULT_PAGE_SIZE) {
uriComp = uriComp.replaceQueryParam("size", size); uriComp = uriComp.replaceQueryParam("size", size);
} }
return new Href(uriComp.build().toUriString()); return new Href(uriComp.build().toUriString());

View File

@@ -190,22 +190,22 @@ public class MetadataFieldRestRepository extends DSpaceRestRepository<MetadataFi
"forming schema.element.qualifier metadata field name"); "forming schema.element.qualifier metadata field name");
} }
filterQueries.add(searchService.toFilterQuery(context, MetadataFieldIndexFactoryImpl.FIELD_NAME_VARIATIONS, filterQueries.add(searchService.toFilterQuery(context, MetadataFieldIndexFactoryImpl.FIELD_NAME_VARIATIONS,
OPERATOR_EQUALS, query).getFilterQuery() + "*"); OPERATOR_EQUALS, query, null).getFilterQuery() + "*");
} }
if (StringUtils.isNotBlank(schemaName)) { if (StringUtils.isNotBlank(schemaName)) {
filterQueries.add( filterQueries.add(
searchService.toFilterQuery(context, MetadataFieldIndexFactoryImpl.SCHEMA_FIELD_NAME, OPERATOR_EQUALS, searchService.toFilterQuery(context, MetadataFieldIndexFactoryImpl.SCHEMA_FIELD_NAME, OPERATOR_EQUALS,
schemaName).getFilterQuery()); schemaName, null).getFilterQuery());
} }
if (StringUtils.isNotBlank(elementName)) { if (StringUtils.isNotBlank(elementName)) {
filterQueries.add( filterQueries.add(
searchService.toFilterQuery(context, MetadataFieldIndexFactoryImpl.ELEMENT_FIELD_NAME, OPERATOR_EQUALS, searchService.toFilterQuery(context, MetadataFieldIndexFactoryImpl.ELEMENT_FIELD_NAME, OPERATOR_EQUALS,
elementName).getFilterQuery()); elementName, null).getFilterQuery());
} }
if (StringUtils.isNotBlank(qualifierName)) { if (StringUtils.isNotBlank(qualifierName)) {
filterQueries.add(searchService filterQueries.add(searchService
.toFilterQuery(context, MetadataFieldIndexFactoryImpl.QUALIFIER_FIELD_NAME, OPERATOR_EQUALS, .toFilterQuery(context, MetadataFieldIndexFactoryImpl.QUALIFIER_FIELD_NAME, OPERATOR_EQUALS,
qualifierName).getFilterQuery()); qualifierName, null).getFilterQuery());
} }
DiscoverQuery discoverQuery = new DiscoverQuery(); DiscoverQuery discoverQuery = new DiscoverQuery();

View File

@@ -53,10 +53,11 @@ public interface RestAuthenticationService {
* Checks the current request for a valid authentication token. If found, extracts that token and obtains the * Checks the current request for a valid authentication token. If found, extracts that token and obtains the
* currently logged in EPerson. * currently logged in EPerson.
* @param request current request * @param request current request
* @param request current response
* @param context current DSpace Context * @param context current DSpace Context
* @return EPerson of the logged in user (if auth token found), or null if no auth token is found * @return EPerson of the logged in user (if auth token found), or null if no auth token is found
*/ */
EPerson getAuthenticatedEPerson(HttpServletRequest request, Context context); EPerson getAuthenticatedEPerson(HttpServletRequest request, HttpServletResponse response, Context context);
/** /**
* Checks the current request for a valid authentication token. If found, returns true. If not found, returns false * Checks the current request for a valid authentication token. If found, returns true. If not found, returns false
@@ -70,8 +71,7 @@ public interface RestAuthenticationService {
* existing authentication data/token is destroyed/invalidated and cannot be reused in later requests. * existing authentication data/token is destroyed/invalidated and cannot be reused in later requests.
* <P> * <P>
* In other words, this method invalidates the authentication data created by addAuthenticationDataForUser(). * In other words, this method invalidates the authentication data created by addAuthenticationDataForUser().
* This also should include clearing any Cookie created by that method, usually by calling the separate *
* invalidateAuthenticationCookie() method in this same class.
* @param request current request * @param request current request
* @param response current response * @param response current response
* @param context current DSpace Context. * @param context current DSpace Context.
@@ -102,8 +102,9 @@ public interface RestAuthenticationService {
* addAuthenticationDataForUser()). It's useful for those services to immediately *remove/discard* the Cookie after * addAuthenticationDataForUser()). It's useful for those services to immediately *remove/discard* the Cookie after
* it has been used. This ensures the auth Cookie is temporary in nature, and is destroyed as soon as it is no * it has been used. This ensures the auth Cookie is temporary in nature, and is destroyed as soon as it is no
* longer needed. * longer needed.
* @param request current request
* @param res current response (where Cookie should be destroyed) * @param res current response (where Cookie should be destroyed)
*/ */
void invalidateAuthenticationCookie(HttpServletResponse res); void invalidateAuthenticationCookie(HttpServletRequest request, HttpServletResponse res);
} }

View File

@@ -14,14 +14,21 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.dspace.authenticate.ShibAuthentication;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderNotFoundException;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
/** /**
* This class will filter shibboleth requests to try and authenticate them * This class will filter Shibboleth requests to see if the user has been authenticated via Shibboleth.
* <P>
* This filter runs before the ShibbolethRestController, in order to verify Shibboleth authentication succeeded,
* and create the authentication token (JWT).
* *
* @author Giuseppe Digilio (giuseppe dot digilio at 4science dot it) * @author Giuseppe Digilio (giuseppe dot digilio at 4science dot it)
* @see org.dspace.app.rest.ShibbolethRestController
* @see org.dspace.authenticate.ShibAuthentication
*/ */
public class ShibbolethAuthenticationFilter extends StatelessLoginFilter { public class ShibbolethAuthenticationFilter extends StatelessLoginFilter {
@@ -33,7 +40,16 @@ public class ShibbolethAuthenticationFilter extends StatelessLoginFilter {
@Override @Override
public Authentication attemptAuthentication(HttpServletRequest req, public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) throws AuthenticationException { HttpServletResponse res) throws AuthenticationException {
// First, if Shibboleth is not enabled, throw an immediate ProviderNotFoundException
// This tells Spring Security that authentication failed
if (!ShibAuthentication.isEnabled()) {
throw new ProviderNotFoundException("Shibboleth is disabled.");
}
// In the case of Shibboleth, this method does NOT actually authenticate us. The authentication
// has already happened in Shibboleth & we are just intercepting the return request in order to check
// for a valid Shibboleth login (using ShibAuthentication.authenticate()) & save current user to Context
// See org.dspace.app.rest.ShibbolethRestController JavaDocs for an outline of the entire Shib login process.
return authenticationManager.authenticate( return authenticationManager.authenticate(
new DSpaceAuthentication(null, null, new ArrayList<>()) new DSpaceAuthentication(null, null, new ArrayList<>())
); );
@@ -44,8 +60,14 @@ public class ShibbolethAuthenticationFilter extends StatelessLoginFilter {
HttpServletResponse res, HttpServletResponse res,
FilterChain chain, FilterChain chain,
Authentication auth) throws IOException, ServletException { Authentication auth) throws IOException, ServletException {
// Once we've gotten here, we know we have a successful login (i.e. attemptAuthentication() succeeded)
DSpaceAuthentication dSpaceAuthentication = (DSpaceAuthentication) auth; DSpaceAuthentication dSpaceAuthentication = (DSpaceAuthentication) auth;
// OVERRIDE DEFAULT behavior of StatelessLoginFilter to return a temporary authentication cookie containing
// the Auth Token (JWT). This Cookie is required because ShibbolethRestController *redirects* the user
// back to the client/UI after a successful Shibboleth login. Headers cannot be sent via a redirect, so a Cookie
// must be sent to provide the auth token to the client. On the next request from the client, the cookie is
// read and destroyed & the Auth token is only used in the Header from that point forward.
restAuthenticationService.addAuthenticationDataForUser(req, res, dSpaceAuthentication, true); restAuthenticationService.addAuthenticationDataForUser(req, res, dSpaceAuthentication, true);
chain.doFilter(req, res); chain.doFilter(req, res);
} }

View File

@@ -39,7 +39,8 @@ import org.springframework.security.web.authentication.www.BasicAuthenticationFi
/** /**
* Custom Spring authentication filter for Stateless authentication, intercepts requests to check for valid * Custom Spring authentication filter for Stateless authentication, intercepts requests to check for valid
* authentication * authentication. This runs before *every* request in the DSpace backend to see if any authentication data
* is passed in that request. If so, it authenticates the EPerson in the current Context.
* *
* @author Frederic Van Reet (frederic dot vanreet at atmire dot com) * @author Frederic Van Reet (frederic dot vanreet at atmire dot com)
* @author Tom Desair (tom dot desair at atmire dot com) * @author Tom Desair (tom dot desair at atmire dot com)
@@ -94,9 +95,9 @@ public class StatelessAuthenticationFilter extends BasicAuthenticationFilter {
log.error("Access is denied (status:{})", HttpServletResponse.SC_FORBIDDEN, e); log.error("Access is denied (status:{})", HttpServletResponse.SC_FORBIDDEN, e);
return; return;
} }
// If we have a valid Authentication, save it to Spring Security
if (authentication != null) { if (authentication != null) {
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
restAuthenticationService.invalidateAuthenticationCookie(res);
} }
chain.doFilter(req, res); chain.doFilter(req, res);
} }
@@ -123,7 +124,7 @@ public class StatelessAuthenticationFilter extends BasicAuthenticationFilter {
Context context = ContextUtil.obtainContext(request); Context context = ContextUtil.obtainContext(request);
EPerson eperson = restAuthenticationService.getAuthenticatedEPerson(request, context); EPerson eperson = restAuthenticationService.getAuthenticatedEPerson(request, res, context);
if (eperson != null) { if (eperson != null) {
//Pass the eperson ID to the request service //Pass the eperson ID to the request service
requestService.setCurrentUserId(eperson.getID()); requestService.setCurrentUserId(eperson.getID());

View File

@@ -23,7 +23,10 @@ import org.springframework.security.web.authentication.AbstractAuthenticationPro
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
/** /**
* This class will filter login requests to try and authenticate them * This class will filter /api/authn/login requests to try and authenticate them. Keep in mind, this filter runs *after*
* StatelessAuthenticationFilter (which looks for authentication data in the request itself). So, in some scenarios
* (e.g. after a Shibboleth login) the StatelessAuthenticationFilter does the actual authentication, and this Filter
* just ensures the auth token (JWT) is sent back in an Authorization header.
* *
* @author Frederic Van Reet (frederic dot vanreet at atmire dot com) * @author Frederic Van Reet (frederic dot vanreet at atmire dot com)
* @author Tom Desair (tom dot desair at atmire dot com) * @author Tom Desair (tom dot desair at atmire dot com)
@@ -46,6 +49,19 @@ public class StatelessLoginFilter extends AbstractAuthenticationProcessingFilter
this.restAuthenticationService = restAuthenticationService; this.restAuthenticationService = restAuthenticationService;
} }
/**
* Attempt to authenticate the user by using Spring Security's AuthenticationManager.
* The AuthenticationManager will delegate this task to one or more AuthenticationProvider classes.
* <P>
* For DSpace, our custom AuthenticationProvider is {@link EPersonRestAuthenticationProvider}, so that
* is the authenticate() method which is called below.
*
* @param req current request
* @param res current response
* @return a valid Spring Security Authentication object if authentication succeeds
* @throws AuthenticationException if authentication fails
* @see EPersonRestAuthenticationProvider
*/
@Override @Override
public Authentication attemptAuthentication(HttpServletRequest req, public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) throws AuthenticationException { HttpServletResponse res) throws AuthenticationException {
@@ -53,12 +69,28 @@ public class StatelessLoginFilter extends AbstractAuthenticationProcessingFilter
String user = req.getParameter("user"); String user = req.getParameter("user");
String password = req.getParameter("password"); String password = req.getParameter("password");
// Attempt to authenticate by passing user & password (if provided) to AuthenticationProvider class(es)
return authenticationManager.authenticate( return authenticationManager.authenticate(
new DSpaceAuthentication(user, password, new ArrayList<>()) new DSpaceAuthentication(user, password, new ArrayList<>())
); );
} }
/**
* If the above attemptAuthentication() call was successful (no authentication error was thrown),
* then this method will take the returned {@link DSpaceAuthentication} class (which includes all
* the data from the authenticated user) and add the authentication data to the response.
* <P>
* For DSpace, this is calling our {@link org.dspace.app.rest.security.jwt.JWTTokenRestAuthenticationServiceImpl}
* in order to create a JWT based on the authentication data & send that JWT back in the response.
*
* @param req current request
* @param res response
* @param chain FilterChain
* @param auth Authentication object containing info about user who had a successful authentication
* @throws IOException
* @throws ServletException
* @see org.dspace.app.rest.security.jwt.JWTTokenRestAuthenticationServiceImpl
*/
@Override @Override
protected void successfulAuthentication(HttpServletRequest req, protected void successfulAuthentication(HttpServletRequest req,
HttpServletResponse res, HttpServletResponse res,
@@ -69,6 +101,16 @@ public class StatelessLoginFilter extends AbstractAuthenticationProcessingFilter
restAuthenticationService.addAuthenticationDataForUser(req, res, dSpaceAuthentication, false); restAuthenticationService.addAuthenticationDataForUser(req, res, dSpaceAuthentication, false);
} }
/**
* If the above attemptAuthentication() call was unsuccessful, then ensure that the response is a 401 Unauthorized
* AND it includes a WWW-Authentication header. We use this header in DSpace to return all the enabled
* authentication options available to the UI (along with the path to the login URL for each option)
* @param request current request
* @param response current response
* @param failed exception that was thrown by attemptAuthentication()
* @throws IOException
* @throws ServletException
*/
@Override @Override
protected void unsuccessfulAuthentication(HttpServletRequest request, protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed) HttpServletResponse response, AuthenticationException failed)

View File

@@ -89,10 +89,7 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication
context.commit(); context.commit();
// Add newly generated auth token to the response // Add newly generated auth token to the response
addTokenToResponse(response, token, addCookie); addTokenToResponse(request, response, token, addCookie);
// Reset our CSRF token, generating a new one
resetCSRFToken(request, response);
} catch (JOSEException e) { } catch (JOSEException e) {
log.error("JOSE Exception", e); log.error("JOSE Exception", e);
@@ -125,9 +122,9 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication
} }
@Override @Override
public EPerson getAuthenticatedEPerson(HttpServletRequest request, Context context) { public EPerson getAuthenticatedEPerson(HttpServletRequest request, HttpServletResponse response, Context context) {
try { try {
String token = getLoginToken(request); String token = getLoginToken(request, response);
EPerson ePerson = null; EPerson ePerson = null;
if (token == null) { if (token == null) {
token = getShortLivedToken(request); token = getShortLivedToken(request);
@@ -156,22 +153,29 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication
@Override @Override
public void invalidateAuthenticationData(HttpServletRequest request, HttpServletResponse response, public void invalidateAuthenticationData(HttpServletRequest request, HttpServletResponse response,
Context context) throws Exception { Context context) throws Exception {
String token = getLoginToken(request); String token = getLoginToken(request, response);
invalidateAuthenticationCookie(response);
loginJWTTokenHandler.invalidateToken(token, request, context); loginJWTTokenHandler.invalidateToken(token, request, context);
// Reset our CSRF token, generating a new one // Reset our CSRF token, generating a new one
resetCSRFToken(request, response); resetCSRFToken(request, response);
} }
/**
* Invalidate our temporary authentication cookie by overwriting it in the response.
* @param request
* @param response
*/
@Override @Override
public void invalidateAuthenticationCookie(HttpServletResponse response) { public void invalidateAuthenticationCookie(HttpServletRequest request, HttpServletResponse response) {
// Re-send the same cookie (as addTokenToResponse()) with no value and a Max-Age of 0 seconds // Re-send the same cookie (as addTokenToResponse()) with no value and a Max-Age of 0 seconds
ResponseCookie cookie = ResponseCookie.from(AUTHORIZATION_COOKIE, "") ResponseCookie cookie = ResponseCookie.from(AUTHORIZATION_COOKIE, "")
.maxAge(0).httpOnly(true).secure(true).sameSite("None").build(); .maxAge(0).httpOnly(true).secure(true).sameSite("None").build();
// Write the cookie to the Set-Cookie header in order to send it // Write the cookie to the Set-Cookie header in order to send it
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
// Reset our CSRF token, generating a new one
resetCSRFToken(request, response);
} }
@Override @Override
@@ -179,6 +183,18 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication
return authenticationService; return authenticationService;
} }
/**
* Return a comma-separated list of all currently enabled authentication options (based on DSpace configuration).
* This list is sent to the client in the WWW-Authenticate header in order to inform it of all the enabled
* authentication plugins *and* (optionally) to provide it with the "location" of the login page, if
* the authentication plugin requires an external login page (e.g. Shibboleth).
* <P>
* Example output looks like:
* shibboleth realm="DSpace REST API" location=[shibboleth-url], password realm="DSpace REST API"
* @param request The current client request
* @param response The response being build for the client
* @return comma separated list of authentication options
*/
@Override @Override
public String getWwwAuthenticateHeaderValue(final HttpServletRequest request, final HttpServletResponse response) { public String getWwwAuthenticateHeaderValue(final HttpServletRequest request, final HttpServletResponse response) {
Iterator<AuthenticationMethod> authenticationMethodIterator Iterator<AuthenticationMethod> authenticationMethodIterator
@@ -195,10 +211,12 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication
wwwAuthenticate.append(authenticationMethod.getName()).append(" realm=\"DSpace REST API\""); wwwAuthenticate.append(authenticationMethod.getName()).append(" realm=\"DSpace REST API\"");
// If authentication method requires a custom login page, add that as the "location". The client is
// expected to read this "location" and send users to that URL when this authentication option is selected
// We cannot reply with a 303 code because many browsers handle 3xx response codes transparently. This
// means that the JavaScript client code is not aware of the 303 status and fails to react accordingly.
String loginPageURL = authenticationMethod.loginPageURL(context, request, response); String loginPageURL = authenticationMethod.loginPageURL(context, request, response);
if (org.apache.commons.lang3.StringUtils.isNotBlank(loginPageURL)) { if (org.apache.commons.lang3.StringUtils.isNotBlank(loginPageURL)) {
// We cannot reply with a 303 code because may browsers handle 3xx response codes transparently. This
// means that the JavaScript client code is not aware of the 303 status and fails to react accordingly.
wwwAuthenticate.append(", location=\"").append(loginPageURL).append("\""); wwwAuthenticate.append(", location=\"").append(loginPageURL).append("\"");
} }
} }
@@ -207,32 +225,57 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication
} }
/** /**
* Adds the Authentication token (JWT) to the response either in a header (default) or in a cookie. * Adds the Authentication token (JWT) to the response either in a header (default) or in a temporary cookie.
* <P> * <P>
* If 'addCookie' is true, then the JWT is also added to a response cookie. This is primarily for support of auth * If 'addCookie' is true, then the JWT is also added to a response cookie. This is primarily for support of auth
* plugins which _require_ cookie-based auth (e.g. Shibboleth). Note that this cookie can be used cross-site * plugins which _require_ cookie-based auth (e.g. Shibboleth). Note that this cookie can be used cross-site
* (i.e. SameSite=None), but cannot be used by Javascript (HttpOnly) including the Angular UI. It also will only be * (i.e. SameSite=None), but cannot be used by Javascript (HttpOnly), including the Angular UI. It also will only be
* sent via HTTPS (Secure). * sent via HTTPS (Secure).
* <P> * <P>
* If 'addCookie' is false, then the JWT is only added in the Authorization header. This is recommended behavior * If 'addCookie' is false, then the JWT is only added in the Authorization header & the auth cookie (if it exists)
* as it is the most secure. For the UI (or any JS clients) the JWT must be sent in the Authorization header. * is removed. This ensures we are primarily using the Authorization header & remove the temporary auth cookie as
* soon as it is no longer needed.
* <P>
* Because this method is called for login actions, it usually resets the CSRF token, *except* when the auth cookie
* is being created. This is because we will reset the CSRF token once the auth cookie is used & invalidated.
* @param request current request
* @param response current response * @param response current response
* @param token the authentication token * @param token the authentication token
* @param addCookie whether to send token in a cookie (true) or header (false) * @param addCookie whether to send token in a cookie & header (true) or header only (false)
*/ */
private void addTokenToResponse(final HttpServletResponse response, final String token, final Boolean addCookie) { private void addTokenToResponse(final HttpServletRequest request, final HttpServletResponse response,
// we need authentication cookies because Shibboleth can't use the authentication headers due to the redirects final String token, final Boolean addCookie) {
// If addCookie=true, create a temporary authentication cookie. This is primarily used for the initial
// Shibboleth response (which requires a number of redirects), as headers cannot be sent via a redirect. As soon
// as the UI (or Hal Browser) obtains the Shibboleth login data, it makes a call to /login (addCookie=false)
// which destroys this temporary auth cookie. So, the auth cookie only exists a few seconds.
if (addCookie) { if (addCookie) {
ResponseCookie cookie = ResponseCookie.from(AUTHORIZATION_COOKIE, token) ResponseCookie cookie = ResponseCookie.from(AUTHORIZATION_COOKIE, token)
.httpOnly(true).secure(true).sameSite("None").build(); .httpOnly(true).secure(true).sameSite("None").build();
// Write the cookie to the Set-Cookie header in order to send it // Write the cookie to the Set-Cookie header in order to send it
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
// NOTE: Because the auth cookie is meant to be temporary, we do NOT reset our CSRF token when creating it.
// Instead, we'll reset the CSRF token when the auth cookie is *destroyed* during call to /login.
} else if (hasAuthorizationCookie(request)) {
// Since an auth cookie exists & is no longer needed (addCookie=false), remove/invalidate the auth cookie.
// This also resets the CSRF token, as auth cookie is destroyed when /login is called.
invalidateAuthenticationCookie(request, response);
} else {
// If we are just adding a new token to header, then reset the CSRF token.
// This forces the token to change when login process doesn't rely on auth cookie.
resetCSRFToken(request, response);
} }
response.setHeader(AUTHORIZATION_HEADER, String.format("%s %s", AUTHORIZATION_TYPE, token)); response.setHeader(AUTHORIZATION_HEADER, String.format("%s %s", AUTHORIZATION_TYPE, token));
} }
private String getLoginToken(HttpServletRequest request) { /**
* Get the Login token (JWT) in the current request. First we check the Authorization header.
* If not found there, we check for a temporary authentication cookie and use that.
* @param request current request
* @return authentication token (if found), or null
*/
private String getLoginToken(HttpServletRequest request, HttpServletResponse response) {
String tokenValue = null; String tokenValue = null;
String authHeader = request.getHeader(AUTHORIZATION_HEADER); String authHeader = request.getHeader(AUTHORIZATION_HEADER);
String authCookie = getAuthorizationCookie(request); String authCookie = getAuthorizationCookie(request);
@@ -254,6 +297,11 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication
return tokenValue; return tokenValue;
} }
/**
* Get the value of the (temporary) authorization cookie, if exists.
* @param request current request
* @return string cookie value
*/
private String getAuthorizationCookie(HttpServletRequest request) { private String getAuthorizationCookie(HttpServletRequest request) {
String authCookie = ""; String authCookie = "";
Cookie[] cookies = request.getCookies(); Cookie[] cookies = request.getCookies();
@@ -261,12 +309,26 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication
for (Cookie cookie : cookies) { for (Cookie cookie : cookies) {
if (cookie.getName().equals(AUTHORIZATION_COOKIE) && StringUtils.isNotEmpty(cookie.getValue())) { if (cookie.getName().equals(AUTHORIZATION_COOKIE) && StringUtils.isNotEmpty(cookie.getValue())) {
authCookie = cookie.getValue(); authCookie = cookie.getValue();
break;
} }
} }
} }
return authCookie; return authCookie;
} }
/**
* Check if the (temporary) authorization cookie exists and is not empty.
* @param request current request
* @return true if cookie is found in request. false otherwise.
*/
private boolean hasAuthorizationCookie(HttpServletRequest request) {
if (StringUtils.isNotEmpty(getAuthorizationCookie(request))) {
return true;
} else {
return false;
}
}
/** /**
* Force reset the CSRF Token, causing a new one to be generated. * Force reset the CSRF Token, causing a new one to be generated.
* This method is used internally during login/logout to ensure a new CSRF token is generated anytime authentication * This method is used internally during login/logout to ensure a new CSRF token is generated anytime authentication

View File

@@ -8,16 +8,28 @@
package org.dspace.app.rest.submit.factory.impl; package org.dspace.app.rest.submit.factory.impl;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
import org.dspace.app.rest.exception.UnprocessableEntityException;
import org.dspace.app.rest.model.MetadataValueRest; import org.dspace.app.rest.model.MetadataValueRest;
import org.dspace.app.rest.model.patch.LateObjectEvaluator; import org.dspace.app.rest.model.patch.LateObjectEvaluator;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.InProgressSubmission; import org.dspace.content.InProgressSubmission;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.content.MetadataValue; import org.dspace.content.MetadataValue;
import org.dspace.content.Relationship;
import org.dspace.content.RelationshipMetadataValue;
import org.dspace.content.service.ItemService; import org.dspace.content.service.ItemService;
import org.dspace.content.service.RelationshipService;
import org.dspace.core.Constants;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.core.Utils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@@ -66,9 +78,18 @@ import org.springframework.util.Assert;
*/ */
public class ItemMetadataValueAddPatchOperation extends MetadataValueAddPatchOperation<Item> { public class ItemMetadataValueAddPatchOperation extends MetadataValueAddPatchOperation<Item> {
/**
* log4j category
*/
private static final Logger log =
org.apache.logging.log4j.LogManager.getLogger(ItemMetadataValueAddPatchOperation.class);
@Autowired @Autowired
ItemService itemService; ItemService itemService;
@Autowired
RelationshipService relationshipService;
@Override @Override
void add(Context context, HttpServletRequest currentRequest, InProgressSubmission source, String path, Object value) void add(Context context, HttpServletRequest currentRequest, InProgressSubmission source, String path, Object value)
throws SQLException { throws SQLException {
@@ -109,6 +130,102 @@ public class ItemMetadataValueAddPatchOperation extends MetadataValueAddPatchOpe
} }
protected void replaceValue(Context context, Item source, String target, List<MetadataValueRest> list)
throws SQLException {
String[] metadata = Utils.tokenize(target);
// fetch pre-existent metadata
List<MetadataValue> preExistentMetadata =
getDSpaceObjectService().getMetadata(source, metadata[0], metadata[1], metadata[2], Item.ANY);
// fetch pre-existent relationships
Map<Integer, Relationship> preExistentRelationships = preExistentRelationships(context, preExistentMetadata);
// clear all plain metadata
getDSpaceObjectService().clearMetadata(context, source, metadata[0], metadata[1], metadata[2], Item.ANY);
// remove all deleted relationships
for (Relationship rel : preExistentRelationships.values()) {
try {
Optional<MetadataValueRest> stillPresent = list.stream()
.filter(ll -> ll.getAuthority() != null && rel.getID().equals(getRelId(ll.getAuthority())))
.findAny();
if (stillPresent.isEmpty()) {
relationshipService.delete(context, rel);
}
} catch (AuthorizeException e) {
e.printStackTrace();
throw new RuntimeException("Authorize Exception during relationship deletion.");
}
}
// create plain metadata / move relationships in the list order
// if a virtual value is present in the list, it must be present in preExistentRelationships too.
// (with this operator virtual value can only be moved or deleted).
int idx = 0;
for (MetadataValueRest ll : list) {
if (StringUtils.startsWith(ll.getAuthority(), Constants.VIRTUAL_AUTHORITY_PREFIX)) {
Optional<MetadataValue> preExistentMv = preExistentMetadata.stream().filter(mvr ->
StringUtils.equals(ll.getAuthority(), mvr.getAuthority())).findFirst();
if (!preExistentMv.isPresent()) {
throw new UnprocessableEntityException(
"Relationship with authority=" + ll.getAuthority() + " not found");
}
final RelationshipMetadataValue rmv = (RelationshipMetadataValue) preExistentMv.get();
final Relationship rel = preExistentRelationships.get(rmv.getRelationshipId());
this.updateRelationshipPlace(context, source, idx, rel);
} else {
getDSpaceObjectService()
.addMetadata(context, source, metadata[0], metadata[1], metadata[2],
ll.getLanguage(), ll.getValue(), ll.getAuthority(), ll.getConfidence(), idx);
}
idx++;
}
}
/**
* Retrieve Relationship Objects from a List of MetadataValue.
*/
private Map<Integer, Relationship> preExistentRelationships(Context context,
List<MetadataValue> preExistentMetadata) throws SQLException {
Map<Integer, Relationship> relationshipsMap = new HashMap<Integer, Relationship>();
for (MetadataValue ll : preExistentMetadata) {
if (ll instanceof RelationshipMetadataValue) {
Relationship relationship = relationshipService
.find(context, ((RelationshipMetadataValue) ll).getRelationshipId());
if (relationship != null) {
relationshipsMap.put(relationship.getID(), relationship);
}
}
}
return relationshipsMap;
}
private Integer getRelId(String authority) {
final int relId = Integer.parseInt(authority.split(Constants.VIRTUAL_AUTHORITY_PREFIX)[1]);
return relId;
}
private void updateRelationshipPlace(Context context, Item dso, int place, Relationship rs) {
try {
if (rs.getLeftItem() == dso) {
rs.setLeftPlace(place);
} else {
rs.setRightPlace(place);
}
relationshipService.update(context, rs);
} catch (Exception e) {
//should not occur, otherwise metadata can't be updated either
log.error("An error occurred while moving " + rs.getID() + " for item " + dso.getID(), e);
}
}
@Override @Override
protected ItemService getDSpaceObjectService() { protected ItemService getDSpaceObjectService() {
return itemService; return itemService;

View File

@@ -14,12 +14,14 @@ import static org.apache.commons.collections4.CollectionUtils.isNotEmpty;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.dspace.app.rest.converter.query.SearchQueryConverter; import org.dspace.app.rest.converter.query.SearchQueryConverter;
import org.dspace.app.rest.exception.DSpaceBadRequestException; import org.dspace.app.rest.exception.DSpaceBadRequestException;
import org.dspace.app.rest.exception.InvalidSearchRequestException;
import org.dspace.app.rest.parameter.SearchFilter; import org.dspace.app.rest.parameter.SearchFilter;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.core.LogManager; import org.dspace.core.LogManager;
@@ -292,6 +294,11 @@ public class DiscoverQueryBuilder implements InitializingBean {
} }
} }
if (StringUtils.isNotBlank(sortBy) && !isConfigured(sortBy, searchSortConfiguration)) {
throw new InvalidSearchRequestException(
"The field: " + sortBy + "is not configured for the configuration!");
}
//Load defaults if we did not receive values //Load defaults if we did not receive values
if (sortBy == null) { if (sortBy == null) {
sortBy = getDefaultSortField(searchSortConfiguration); sortBy = getDefaultSortField(searchSortConfiguration);
@@ -321,10 +328,14 @@ public class DiscoverQueryBuilder implements InitializingBean {
} }
} }
private boolean isConfigured(String sortBy, DiscoverySortConfiguration searchSortConfiguration) {
return Objects.nonNull(searchSortConfiguration.getSortFieldConfiguration(sortBy));
}
private String getDefaultSortDirection(DiscoverySortConfiguration searchSortConfiguration, String sortOrder) { private String getDefaultSortDirection(DiscoverySortConfiguration searchSortConfiguration, String sortOrder) {
if (searchSortConfiguration != null) { if (Objects.nonNull(searchSortConfiguration.getSortFields()) &&
sortOrder = searchSortConfiguration.getDefaultSortOrder() !searchSortConfiguration.getSortFields().isEmpty()) {
.toString(); sortOrder = searchSortConfiguration.getSortFields().get(0).getDefaultSortOrder().name();
} }
return sortOrder; return sortOrder;
} }
@@ -332,8 +343,12 @@ public class DiscoverQueryBuilder implements InitializingBean {
private String getDefaultSortField(DiscoverySortConfiguration searchSortConfiguration) { private String getDefaultSortField(DiscoverySortConfiguration searchSortConfiguration) {
String sortBy;// Attempt to find the default one, if none found we use SCORE String sortBy;// Attempt to find the default one, if none found we use SCORE
sortBy = "score"; sortBy = "score";
if (searchSortConfiguration != null && searchSortConfiguration.getDefaultSort() != null) { if (Objects.nonNull(searchSortConfiguration.getSortFields()) &&
DiscoverySortFieldConfiguration defaultSort = searchSortConfiguration.getDefaultSort(); !searchSortConfiguration.getSortFields().isEmpty()) {
DiscoverySortFieldConfiguration defaultSort = searchSortConfiguration.getSortFields().get(0);
if (StringUtils.isBlank(defaultSort.getMetadataField())) {
return sortBy;
}
sortBy = defaultSort.getMetadataField(); sortBy = defaultSort.getMetadataField();
} }
return sortBy; return sortBy;
@@ -378,7 +393,8 @@ public class DiscoverQueryBuilder implements InitializingBean {
DiscoverFilterQuery filterQuery = searchService.toFilterQuery(context, DiscoverFilterQuery filterQuery = searchService.toFilterQuery(context,
filter.getIndexFieldName(), filter.getIndexFieldName(),
searchFilter.getOperator(), searchFilter.getOperator(),
searchFilter.getValue()); searchFilter.getValue(),
discoveryConfiguration);
if (filterQuery != null) { if (filterQuery != null) {
filterQueries.add(filterQuery.getFilterQuery()); filterQueries.add(filterQuery.getFilterQuery());

View File

@@ -108,7 +108,7 @@ public class Utils {
/** /**
* The default page size, if unspecified in the request. * The default page size, if unspecified in the request.
*/ */
private static final int DEFAULT_PAGE_SIZE = 20; public static final int DEFAULT_PAGE_SIZE = 20;
/** /**
* The maximum number of embed levels to allow. * The maximum number of embed levels to allow.

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. # However, you may wish to set this to "always" in your 'local.cfg' for development or debugging purposes.
server.error.include-stacktrace = never 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 # Spring Boot Autoconfigure
# #
@@ -107,6 +119,9 @@ spring.main.allow-bean-definition-overriding = true
# Log4J configuration # Log4J configuration
logging.config = ${dspace.dir}/config/log4j2.xml logging.config = ${dspace.dir}/config/log4j2.xml
##################################
# Spring MVC file upload settings
#
# Maximum size of a single uploaded file (default = 1MB) # Maximum size of a single uploaded file (default = 1MB)
spring.servlet.multipart.max-file-size = 512MB spring.servlet.multipart.max-file-size = 512MB

View File

@@ -98,39 +98,62 @@
"onclick" : function() { toastr.remove(); } "onclick" : function() { toastr.remove(); }
} }
// When the login page loads, we do *two* AJAX requests.
// (1) Call GET /api/authn/status. This call has two purposes. First, it checks to see if you are logged in,
// (if not, WWW-Authenticate will return login options). Second, it retrieves the CSRF token, if a
// new one has been assigned (as a valid CSRF token is required for the POST call).
// (2) If that /api/authn/status call finds authentication data, call POST /api/authn/login.
// This scenario occurs when you login via an external authentication system (e.g. Shibboleth)...
// in which case the main role of /api/authn/login is to simply ensure the "Authorization" header
// is sent back to the client (based on your authentication data).
$.ajax({ $.ajax({
url : window.location.href.replace("login.html", "") + 'api/authn/login', url : window.location.href.replace("login.html", "") + 'api/authn/status',
type : 'POST', type : 'GET',
beforeSend: function (xhr, settings) { success : function(result, status, xhr) {
// If CSRF token found in cookie, send it back as X-XSRF-Token header // Check for an update to the CSRF Token & save to a MyHalBrowserCsrfToken cookie (if found)
var csrfToken = getCSRFToken(); checkForUpdatedCSRFTokenInResponse(xhr);
if (csrfToken != null) {
xhr.setRequestHeader('X-XSRF-Token', csrfToken);
}
},
success : successHandler,
error : function(xhr, textStatus, errorThrown) {
// Check for an update to the CSRF Token & save to a MyHalBrowserCsrfToken cookie (if found)
checkForUpdatedCSRFTokenInResponse(xhr);
// If 401 Unauthorized, check WWW-Authenticate for authentication options // Check for WWW-Authenticate header. If found, this means we are not yet authenticated, and
if (xhr.status === 401) { // therefore we need to display available authentication options.
var authenticate = xhr.getResponseHeader("WWW-Authenticate"); var authenticate = xhr.getResponseHeader("WWW-Authenticate");
var element = $('div.other-login-methods'); if (authenticate !== null) {
if(authenticate !== null) { var element = $('div.other-login-methods');
var realms = authenticate.match(/(\w+ (\w+=((".*?")|[^,]*)(, )?)*)/g); var realms = authenticate.match(/(\w+ (\w+=((".*?")|[^,]*)(, )?)*)/g);
if (realms.length == 1){ if (realms.length == 1){
var loc = /location="([^,]*)"/.exec(authenticate); var loc = /location="([^,]*)"/.exec(authenticate);
if (loc !== null && loc.length === 2) { if (loc !== null && loc.length === 2) {
document.location = loc[1]; document.location = loc[1];
} }
} else if (realms.length > 1){ } else if (realms.length > 1){
for (var i = 0; i < realms.length; i++){ for (var i = 0; i < realms.length; i++){
addLocationButton(realms[i], element); addLocationButton(realms[i], element);
} }
} }
} else {
// If Authentication data was found, do a POST /api/authn/login to ensure that data's JWT
// is sent back in the "Authorization" header. This simply completes an external authentication
// process (e.g. Shibboleth)
$.ajax({
url : window.location.href.replace("login.html", "") + 'api/authn/login',
type : 'POST',
beforeSend: function (xhr, settings) {
// If CSRF token found in cookie, send it back as X-XSRF-Token header
var csrfToken = getCSRFToken();
if (csrfToken != null) {
xhr.setRequestHeader('X-XSRF-Token', csrfToken);
}
},
success : successHandler,
error : function(xhr, textStatus, errorThrown) {
// Check for an update to the CSRF Token & save to a MyHalBrowserCsrfToken cookie (if found)
checkForUpdatedCSRFTokenInResponse(xhr);
toastr.error('Failed to logged in. Please check for errors in Javascript console.', 'Login Failed');
} }
} });
}
},
error : function(xhr, textStatus, errorThrown) {
toastr.error('Failed to connect with backend. Please check for errors in Javascript console.', 'Could Not Load');
} }
}); });
@@ -172,6 +195,8 @@
} }
} }
// When the Username/Password Login form is submitted, POST that data directly to /api/authn/login.
// This logs the user in and ensures the "Authorization" header is set with the JWT.
$("#login-form").submit(function(event) { $("#login-form").submit(function(event) {
event.preventDefault(); event.preventDefault();
$.ajax({ $.ajax({
@@ -191,7 +216,9 @@
} }
}, },
success : successHandler, success : successHandler,
error : function() { error : function(xhr) {
// Check for an update to the CSRF Token & save to a MyHalBrowserCsrfToken cookie (if found)
checkForUpdatedCSRFTokenInResponse(xhr);
toastr.error('The credentials you entered are invalid. Please try again.', 'Login Failed'); toastr.error('The credentials you entered are invalid. Please try again.', 'Login Failed');
} }
}); });

View File

@@ -17,6 +17,7 @@ import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -24,6 +25,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.io.InputStream; import java.io.InputStream;
@@ -164,35 +166,111 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
@Test @Test
public void testStatusShibAuthenticatedWithCookie() throws Exception { public void testStatusShibAuthenticatedWithCookie() throws Exception {
//Enable Shibboleth login //Enable Shibboleth login only
configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", SHIB_ONLY); configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", SHIB_ONLY);
//Simulate that a shibboleth authentication has happened String uiURL = configurationService.getProperty("dspace.ui.url");
String token = getClient().perform(post("/api/authn/login")
// In order to fully simulate a Shibboleth authentication, we'll call
// /api/authn/shibboleth?redirectUrl=[UI-URL] , with valid Shibboleth request attributes.
// In this situation, we are mocking how Shibboleth works from our UI (see also ShibbolethRestController):
// (1) The UI sends the user to Shibboleth to login
// (2) After a successful login, Shibboleth redirects user to /api/authn/shibboleth?redirectUrl=[url]
// (3) That triggers generation of the auth token (JWT), and redirects the user to 'redirectUrl', sending along
// a temporary cookie containing the auth token.
// In below call, we're sending a GET request (as that's what a redirect is), with a Referer of a "fake"
// Shibboleth server to simulate this request coming back from Shibboleth (after a successful login).
// We are then verifying the user will be redirected to the 'redirectUrl' with a single-use auth cookie
// (NOTE: Additional tests of this /api/authn/shibboleth endpoint can be found in ShibbolethRestControllerIT)
Cookie authCookie = getClient().perform(get("/api/authn/shibboleth")
.header("Referer", "https://myshib.example.com")
.param("redirectUrl", uiURL)
.requestAttr("SHIB-MAIL", eperson.getEmail()) .requestAttr("SHIB-MAIL", eperson.getEmail())
.requestAttr("SHIB-SCOPED-AFFILIATION", "faculty;staff")) .requestAttr("SHIB-SCOPED-AFFILIATION", "faculty;staff"))
.andExpect(status().isOk()) .andExpect(status().is3xxRedirection())
.andReturn().getResponse().getHeader(AUTHORIZATION_HEADER).replace("Bearer ", ""); .andExpect(redirectedUrl(uiURL))
// Verify that the CSRF token has NOT been changed. Creating the auth cookie should NOT change our CSRF
// token. The CSRF token should only change when we call /login with the cookie (see later in this test)
.andExpect(cookie().doesNotExist("DSPACE-XSRF-COOKIE"))
.andExpect(header().doesNotExist("DSPACE-XSRF-TOKEN"))
.andExpect(cookie().exists(AUTHORIZATION_COOKIE))
.andReturn().getResponse().getCookie(AUTHORIZATION_COOKIE);
Cookie[] cookies = new Cookie[1]; // Verify the temporary cookie now exists & obtain its token for use below
cookies[0] = new Cookie(AUTHORIZATION_COOKIE, token); assertNotNull(authCookie);
String token = authCookie.getValue();
//Check if we are authenticated with a status request with authorization cookie // This step is _not required_ to successfully authenticate, but it mocks the behavior of our UI & HAL Browser.
getClient().perform(get("/api/authn/status") // We'll send a "/status" request to the REST API with our auth cookie. This should return that we have a
.secure(true) // *valid* authentication (as auth cookie is valid), however the cookie will remain. To complete the login
.cookie(cookies)) // process we MUST call the "/login" endpoint (see the next step in this test).
// (NOTE that this call has an "Origin" matching the UI, to better mock that the request came from there &
// to verify the temporary auth cookie is valid for the UI's origin.)
getClient().perform(get("/api/authn/status").header("Origin", uiURL)
.secure(true)
.cookie(authCookie))
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.type", is("status")))
// Verify that the CSRF token has NOT been changed... status checks won't change the token
// (only login/logout will)
.andExpect(cookie().doesNotExist("DSPACE-XSRF-COOKIE"))
.andExpect(header().doesNotExist("DSPACE-XSRF-TOKEN"));
// To complete the authentication process, we pass our auth cookie to the "/login" endpoint.
// This is where the temporary cookie will be read, verified & destroyed. After this point, the UI will
// only use the 'Authorization' header for all future requests.
// (NOTE that this call has an "Origin" matching the UI, to better mock that the request came from there &
// to verify the temporary auth cookie is valid for the UI's origin.)
getClient().perform(post("/api/authn/login").header("Origin", uiURL)
.secure(true)
.cookie(authCookie))
.andExpect(status().isOk())
// Verify the Auth cookie has been destroyed
.andExpect(cookie().value(AUTHORIZATION_COOKIE, ""))
// Verify token is now sent back in the Authorization header as the Bearer token
.andExpect(header().string(AUTHORIZATION_HEADER, "Bearer " + token))
// Verify that the CSRF token has been changed
// (as both cookie and header should be sent back)
.andExpect(cookie().exists("DSPACE-XSRF-COOKIE"))
.andExpect(header().exists("DSPACE-XSRF-TOKEN"));
// Now that the auth cookie is cleared, all future requests (from UI)
// should be made via the Authorization header. So, this tests the token is still valid if sent via header.
getClient(token).perform(get("/api/authn/status").header("Origin", uiURL))
.andExpect(status().isOk()) .andExpect(status().isOk())
//We expect the content type to be "application/hal+json;charset=UTF-8"
.andExpect(content().contentType(contentType)) .andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.okay", is(true))) .andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(true))) .andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.type", is("status"))); .andExpect(jsonPath("$.type", is("status")));
//Logout //Logout, invalidating the token
getClient(token).perform(post("/api/authn/logout")) getClient(token).perform(post("/api/authn/logout").header("Origin", uiURL))
.andExpect(status().isNoContent()); .andExpect(status().isNoContent());
} }
@Test
public void testShibbolethEndpointCannotBeUsedWithShibDisabled() throws Exception {
// Enable only password login
configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", PASS_ONLY);
String uiURL = configurationService.getProperty("dspace.ui.url");
// Verify /api/authn/shibboleth endpoint does not work
// NOTE: this is the same call as in testStatusShibAuthenticatedWithCookie())
getClient().perform(get("/api/authn/shibboleth")
.header("Referer", "https://myshib.example.com")
.param("redirectUrl", uiURL)
.requestAttr("SHIB-MAIL", eperson.getEmail())
.requestAttr("SHIB-SCOPED-AFFILIATION", "faculty;staff"))
.andExpect(status().isUnauthorized());
}
// NOTE: This test is similar to testStatusShibAuthenticatedWithCookie(), but proves the same process works
// for Password Authentication in theory (NOTE: at this time, there's no way to create an auth cookie via the
// Password Authentication process).
@Test @Test
public void testStatusPasswordAuthenticatedWithCookie() throws Exception { public void testStatusPasswordAuthenticatedWithCookie() throws Exception {
// Login via password to retrieve a valid token // Login via password to retrieve a valid token
@@ -201,21 +279,49 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
// Remove "Bearer " from that token, so that we are left with the token itself // Remove "Bearer " from that token, so that we are left with the token itself
token = token.replace("Bearer ", ""); token = token.replace("Bearer ", "");
// Save token to an Authorization cookie // Fake the creation of an auth cookie, just for testing. (Currently, it's not possible to create an auth cookie
Cookie[] cookies = new Cookie[1]; // via Password auth, but this test proves it would work if enabled)
cookies[0] = new Cookie(AUTHORIZATION_COOKIE, token); Cookie authCookie = new Cookie(AUTHORIZATION_COOKIE, token);
//Check if we are authenticated with a status request using authorization cookie // Now, similar to how both the UI & Hal Browser authentication works, send a "/status" request to the REST API
getClient().perform(get("/api/authn/status") // with our auth cookie. This should return that we *have a valid* authentication (in the auth cookie).
.secure(true) // However, this is just a validation check, so this auth cookie will remain. To complete the login process
.cookie(cookies)) // we'll need to call the "/login" endpoint (see the next step in this test).
getClient().perform(get("/api/authn/status").secure(true).cookie(authCookie))
.andExpect(status().isOk()) .andExpect(status().isOk())
//We expect the content type to be "application/hal+json"
.andExpect(content().contentType(contentType)) .andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.okay", is(true))) .andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(true))) .andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.type", is("status"))); .andExpect(jsonPath("$.type", is("status")))
//Logout // Verify that the CSRF token has NOT been changed... status checks won't change the token
// (only login/logout will)
.andExpect(cookie().doesNotExist("DSPACE-XSRF-COOKIE"))
.andExpect(header().doesNotExist("DSPACE-XSRF-TOKEN"));
// To complete the authentication process, we pass our auth cookie to the "/login" endpoint.
// This is where the temporary cookie will be read, verified & destroyed. After this point, the UI will
// only use the Authorization header for all future requests.
getClient().perform(post("/api/authn/login").secure(true).cookie(authCookie))
.andExpect(status().isOk())
// Verify the Auth cookie has been destroyed
.andExpect(cookie().value(AUTHORIZATION_COOKIE, ""))
// Verify token is now sent back in the Authorization header
.andExpect(header().string(AUTHORIZATION_HEADER, "Bearer " + token))
// Verify that the CSRF token has been changed
// (as both cookie and header should be sent back)
.andExpect(cookie().exists("DSPACE-XSRF-COOKIE"))
.andExpect(header().exists("DSPACE-XSRF-TOKEN"));
// Now that the auth cookie is cleared, all future requests (from UI)
// should be made via the Authorization header. So, this tests the token is still valid if sent via header.
getClient(token).perform(get("/api/authn/status"))
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.type", is("status")));
// Logout, invalidating the token
getClient(token).perform(post("/api/authn/logout")) getClient(token).perform(post("/api/authn/logout"))
.andExpect(status().isNoContent()); .andExpect(status().isNoContent());
} }
@@ -1087,7 +1193,7 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
String loginToken = getAuthToken(eperson.getEmail(), password); String loginToken = getAuthToken(eperson.getEmail(), password);
getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() getClient().perform(get("/api/core/bitstreams/" + bitstream.getID()
+ "/content?authentication-token=" + loginToken)) + "/content?authentication-token=" + loginToken))
.andExpect(status().isForbidden()); .andExpect(status().isUnauthorized());
} }
@Test @Test
@@ -1098,7 +1204,7 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
Thread.sleep(1); Thread.sleep(1);
getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() getClient().perform(get("/api/core/bitstreams/" + bitstream.getID()
+ "/content?authentication-token=" + shortLivedToken)) + "/content?authentication-token=" + shortLivedToken))
.andExpect(status().isForbidden()); .andExpect(status().isUnauthorized());
} }
@Test @Test

View File

@@ -2478,4 +2478,118 @@ public class CommunityRestRepositoryIT extends AbstractControllerIntegrationTest
getClient().perform(get("/api/core/communities/search/findAdminAuthorized")) getClient().perform(get("/api/core/communities/search/findAdminAuthorized"))
.andExpect(status().isUnauthorized()); .andExpect(status().isUnauthorized());
} }
@Test
public void findAllSearchTopEmbeddedPaginationTest() throws Exception {
context.turnOffAuthorisationSystem();
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent Community")
.withLogo("ThisIsSomeDummyText")
.build();
Collection col = CollectionBuilder.createCollection(context, parentCommunity)
.withName("Collection 1").build();
Collection col2 = CollectionBuilder.createCollection(context, parentCommunity)
.withName("Collection 2").build();
CommunityBuilder.createCommunity(context)
.withName("Parent Community 2")
.withLogo("SomeTest").build();
Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity)
.withName("Sub Community").build();
Community child2 = CommunityBuilder.createSubCommunity(context, parentCommunity)
.withName("Sub Community 2").build();
context.restoreAuthSystemState();
getClient().perform(get("/api/core/communities/search/top")
.param("size", "1")
.param("embed", "subcommunities")
.param("embed", "collections")
.param("embed.size", "subcommunities=1")
.param("embed.size", "collections=1"))
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$._embedded.communities", Matchers.contains(
CommunityMatcher.matchCommunity(parentCommunity))))
// Verify subcommunities
.andExpect(jsonPath("$._embedded.communities[0]._embedded.subcommunities._embedded.subcommunities",
Matchers.contains(CommunityMatcher.matchCommunity(child1))))
.andExpect(jsonPath("$._embedded.communities[0]._embedded.subcommunities._links.self.href",
Matchers.containsString("/api/core/communities/" + parentCommunity.getID()
+ "/subcommunities?size=1")))
.andExpect(jsonPath("$._embedded.communities[0]._embedded.subcommunities._links.next.href",
Matchers.containsString("/api/core/communities/" + parentCommunity.getID()
+ "/subcommunities?page=1&size=1")))
.andExpect(jsonPath("$._embedded.communities[0]._embedded.subcommunities._links.last.href",
Matchers.containsString("/api/core/communities/" + parentCommunity.getID()
+ "/subcommunities?page=1&size=1")))
// Verify collections
.andExpect(jsonPath("$._embedded.communities[0]._embedded.collections._embedded.collections",
Matchers.contains(CollectionMatcher.matchCollection(col))))
.andExpect(jsonPath("$._embedded.communities[0]._embedded.collections._links.self.href",
Matchers.containsString("/api/core/communities/" + parentCommunity.getID()
+ "/collections?size=1")))
.andExpect(jsonPath("$._embedded.communities[0]._embedded.collections._links.next.href",
Matchers.containsString("/api/core/communities/" + parentCommunity.getID()
+ "/collections?page=1&size=1")))
.andExpect(jsonPath("$._embedded.communities[0]._embedded.collections._links.last.href",
Matchers.containsString("/api/core/communities/" + parentCommunity.getID()
+ "/collections?page=1&size=1")))
.andExpect(jsonPath("$._links.self.href",
Matchers.containsString("/api/core/communities/search/top?size=1")))
.andExpect(jsonPath("$._links.first.href",
Matchers.containsString("/api/core/communities/search/top?page=0&size=1")))
.andExpect(jsonPath("$._links.next.href",
Matchers.containsString("/api/core/communities/search/top?page=1&size=1")))
.andExpect(jsonPath("$._links.last.href",
Matchers.containsString("/api/core/communities/search/top?page=1&size=1")))
.andExpect(jsonPath("$.page.size", is(1)))
.andExpect(jsonPath("$.page.totalPages", is(2)))
.andExpect(jsonPath("$.page.totalElements", is(2)));
getClient().perform(get("/api/core/communities/search/top")
.param("size", "1")
.param("embed", "subcommunities")
.param("embed", "collections")
.param("embed.size", "subcommunities=2")
.param("embed.size", "collections=2"))
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$._embedded.communities", Matchers.contains(
CommunityMatcher.matchCommunity(parentCommunity))))
// Verify subcommunities
.andExpect(jsonPath("$._embedded.communities[0]._embedded.subcommunities._embedded.subcommunities",
Matchers.containsInAnyOrder(CommunityMatcher.matchCommunity(child1),
CommunityMatcher.matchCommunity(child2)
)))
.andExpect(jsonPath("$._embedded.communities[0]._embedded.subcommunities._links.self.href",
Matchers.containsString("/api/core/communities/" + parentCommunity.getID()
+ "/subcommunities?size=2")))
// Verify collections
.andExpect(jsonPath("$._embedded.communities[0]._embedded.collections._embedded.collections",
Matchers.containsInAnyOrder(CollectionMatcher.matchCollection(col),
CollectionMatcher.matchCollection(col2)
)))
.andExpect(jsonPath("$._embedded.communities[0]._embedded.collections._links.self.href",
Matchers.containsString("/api/core/communities/" + parentCommunity.getID()
+ "/collections?size=2")))
.andExpect(jsonPath("$._links.self.href",
Matchers.containsString("/api/core/communities/search/top?size=1")))
.andExpect(jsonPath("$._links.first.href",
Matchers.containsString("/api/core/communities/search/top?page=0&size=1")))
.andExpect(jsonPath("$._links.next.href",
Matchers.containsString("/api/core/communities/search/top?page=1&size=1")))
.andExpect(jsonPath("$._links.last.href",
Matchers.containsString("/api/core/communities/search/top?page=1&size=1")))
.andExpect(jsonPath("$.page.size", is(1)))
.andExpect(jsonPath("$.page.totalPages", is(2)))
.andExpect(jsonPath("$.page.totalElements", is(2)));
}
} }

View File

@@ -55,6 +55,7 @@ import org.dspace.content.Item;
import org.dspace.content.WorkspaceItem; import org.dspace.content.WorkspaceItem;
import org.dspace.content.authority.Choices; import org.dspace.content.authority.Choices;
import org.dspace.content.authority.service.MetadataAuthorityService; import org.dspace.content.authority.service.MetadataAuthorityService;
import org.dspace.discovery.configuration.DiscoverySortFieldConfiguration;
import org.dspace.eperson.EPerson; import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group; import org.dspace.eperson.Group;
import org.dspace.services.ConfigurationService; import org.dspace.services.ConfigurationService;
@@ -988,14 +989,74 @@ public class DiscoveryRestControllerIT extends AbstractControllerIntegrationTest
SearchFilterMatcher.isJournalOfPublicationRelation() SearchFilterMatcher.isJournalOfPublicationRelation()
))) )))
//These sortOptions need to be present as it's the default in the configuration //These sortOptions need to be present as it's the default in the configuration
.andExpect(jsonPath("$.sortOptions", containsInAnyOrder( .andExpect(jsonPath("$.sortOptions", contains(
SortOptionMatcher.titleSortOption(), SortOptionMatcher.sortOptionMatcher(
SortOptionMatcher.dateIssuedSortOption(), "score", DiscoverySortFieldConfiguration.SORT_ORDER.desc.name()),
SortOptionMatcher.dateAccessionedSortOption(), SortOptionMatcher.sortOptionMatcher(
SortOptionMatcher.scoreSortOption() "dc.title", DiscoverySortFieldConfiguration.SORT_ORDER.asc.name()),
SortOptionMatcher.sortOptionMatcher(
"dc.date.issued", DiscoverySortFieldConfiguration.SORT_ORDER.desc.name()),
SortOptionMatcher.sortOptionMatcher(
"dc.date.accessioned", DiscoverySortFieldConfiguration.SORT_ORDER.desc.name())
))); )));
} }
@Test
public void checkSortOrderInPersonOrOrgunitConfigurationTest() throws Exception {
getClient().perform(get("/api/discover/search")
.param("configuration", "personOrOrgunit"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.type", is("discover")))
.andExpect(jsonPath("$._links.objects.href", containsString("api/discover/search/objects")))
.andExpect(jsonPath("$._links.self.href", containsString("api/discover/search")))
.andExpect(jsonPath("$.sortOptions", contains(
SortOptionMatcher.sortOptionMatcher("dspace.entity.type",
DiscoverySortFieldConfiguration.SORT_ORDER.desc.name()),
SortOptionMatcher.sortOptionMatcher("organization.legalName",
DiscoverySortFieldConfiguration.SORT_ORDER.asc.name()),
SortOptionMatcher.sortOptionMatcher("organisation.address.addressCountry",
DiscoverySortFieldConfiguration.SORT_ORDER.asc.name()),
SortOptionMatcher.sortOptionMatcher("organisation.address.addressLocality",
DiscoverySortFieldConfiguration.SORT_ORDER.asc.name()),
SortOptionMatcher.sortOptionMatcher("organisation.foundingDate",
DiscoverySortFieldConfiguration.SORT_ORDER.desc.name()),
SortOptionMatcher.sortOptionMatcher("dc.date.accessioned",
DiscoverySortFieldConfiguration.SORT_ORDER.desc.name()),
SortOptionMatcher.sortOptionMatcher("person.familyName",
DiscoverySortFieldConfiguration.SORT_ORDER.asc.name()),
SortOptionMatcher.sortOptionMatcher("person.givenName",
DiscoverySortFieldConfiguration.SORT_ORDER.asc.name()),
SortOptionMatcher.sortOptionMatcher("person.birthDate",
DiscoverySortFieldConfiguration.SORT_ORDER.desc.name())
)));
}
@Test
public void discoverSearchByFieldNotConfiguredTest() throws Exception {
context.turnOffAuthorisationSystem();
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent Community")
.build();
Collection col1 = CollectionBuilder.createCollection(context, parentCommunity)
.withName("Collection 1")
.build();
ItemBuilder.createItem(context, col1)
.withTitle("Test")
.withIssueDate("2010-10-17")
.withAuthor("Testing, Works")
.withSubject("ExtraEntry").build();
context.restoreAuthSystemState();
getClient().perform(get("/api/discover/search/objects")
.param("sort", "dc.date.accessioned, ASC")
.param("configuration", "workspace"))
.andExpect(status().isUnprocessableEntity());
}
@Test @Test
public void discoverSearchObjectsTest() throws Exception { public void discoverSearchObjectsTest() throws Exception {
@@ -5177,6 +5238,137 @@ public class DiscoveryRestControllerIT extends AbstractControllerIntegrationTest
.andExpect(jsonPath("$._links.self.href", containsString("/api/discover/search/objects"))); .andExpect(jsonPath("$._links.self.href", containsString("/api/discover/search/objects")));
} }
@Test
public void discoverSearchObjectsTestForAdministrativeViewWithFiltersEquals() throws Exception {
context.turnOffAuthorisationSystem();
parentCommunity = CommunityBuilder
.createCommunity(context)
.withName("Parent Community")
.build();
Community child1 = CommunityBuilder
.createSubCommunity(context, parentCommunity)
.withName("Sub Community")
.build();
Collection col1 = CollectionBuilder
.createCollection(context, child1)
.withName("Collection 1")
.build();
Collection col2 = CollectionBuilder
.createCollection(context, child1)
.withName("Collection 2")
.build();
ItemBuilder.createItem(context, col1)
.withTitle("Public Test Item")
.withIssueDate("2010-10-17")
.withAuthor("Smith, Donald")
.withSubject("ExtraEntry")
.build();
ItemBuilder.createItem(context, col2)
.withTitle("Withdrawn Test Item")
.withIssueDate("1990-02-13")
.withAuthor("Smith, Maria")
.withAuthor("Doe, Jane")
.withSubject("ExtraEntry")
.withdrawn()
.build();
ItemBuilder.createItem(context, col2)
.withTitle("Private Test Item")
.withIssueDate("2010-02-13")
.withAuthor("Smith, Maria")
.withAuthor("Doe, Jane")
.withSubject("AnotherTest")
.withSubject("ExtraEntry")
.makeUnDiscoverable()
.build();
context.restoreAuthSystemState();
String adminToken = getAuthToken(admin.getEmail(), password);
getClient(adminToken)
.perform(get("/api/discover/search/objects")
.param("configuration", "administrativeView")
.param("query", "Test")
.param("f.withdrawn", "true,equals")
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.type", is("discover")))
.andExpect(jsonPath("$._embedded.searchResult.page", is(
PageMatcher.pageEntryWithTotalPagesAndElements(0, 20, 1, 1)
)))
.andExpect(jsonPath("$._embedded.searchResult._embedded.objects",
Matchers.contains(
SearchResultMatcher.matchOnItemName("item", "items", "Withdrawn Test Item")
)
))
.andExpect(jsonPath("$._links.self.href", containsString("/api/discover/search/objects")));
getClient(adminToken)
.perform(get("/api/discover/search/objects")
.param("configuration", "administrativeView")
.param("query", "Test")
.param("f.withdrawn", "false,equals")
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.type", is("discover")))
.andExpect(jsonPath("$._embedded.searchResult.page", is(
PageMatcher.pageEntryWithTotalPagesAndElements(0, 20, 1, 2)
)))
.andExpect(jsonPath("$._embedded.searchResult._embedded.objects",
Matchers.containsInAnyOrder(
SearchResultMatcher.matchOnItemName("item", "items", "Public Test Item"),
SearchResultMatcher.matchOnItemName("item", "items", "Private Test Item")
)
))
.andExpect(jsonPath("$._links.self.href", containsString("/api/discover/search/objects")));
getClient(adminToken)
.perform(get("/api/discover/search/objects")
.param("configuration", "administrativeView")
.param("query", "Test")
.param("f.discoverable", "true,equals")
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.type", is("discover")))
.andExpect(jsonPath("$._embedded.searchResult.page", is(
PageMatcher.pageEntryWithTotalPagesAndElements(0, 20, 1, 2)
)))
.andExpect(jsonPath("$._embedded.searchResult._embedded.objects",
Matchers.containsInAnyOrder(
SearchResultMatcher.matchOnItemName("item", "items", "Public Test Item"),
SearchResultMatcher.matchOnItemName("item", "items", "Withdrawn Test Item")
)
))
.andExpect(jsonPath("$._links.self.href", containsString("/api/discover/search/objects")));
getClient(adminToken)
.perform(get("/api/discover/search/objects")
.param("configuration", "administrativeView")
.param("query", "Test")
.param("f.discoverable", "false,equals")
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.type", is("discover")))
.andExpect(jsonPath("$._embedded.searchResult.page", is(
PageMatcher.pageEntryWithTotalPagesAndElements(0, 20, 1, 1)
)))
.andExpect(jsonPath("$._embedded.searchResult._embedded.objects",
Matchers.contains(
SearchResultMatcher.matchOnItemName("item", "items", "Private Test Item")
)
))
.andExpect(jsonPath("$._links.self.href", containsString("/api/discover/search/objects")));
}
@Test @Test
public void discoverSearchPoolTaskObjectsTest() throws Exception { public void discoverSearchPoolTaskObjectsTest() throws Exception {
context.turnOffAuthorisationSystem(); context.turnOffAuthorisationSystem();

View File

@@ -25,6 +25,7 @@ import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.IntStream;
import org.dspace.app.rest.matcher.MetadataMatcher; import org.dspace.app.rest.matcher.MetadataMatcher;
import org.dspace.app.rest.model.MetadataValueRest; import org.dspace.app.rest.model.MetadataValueRest;
@@ -49,6 +50,7 @@ import org.dspace.content.service.EntityTypeService;
import org.dspace.content.service.ItemService; import org.dspace.content.service.ItemService;
import org.dspace.content.service.RelationshipTypeService; import org.dspace.content.service.RelationshipTypeService;
import org.dspace.content.service.WorkspaceItemService; import org.dspace.content.service.WorkspaceItemService;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
@@ -81,6 +83,8 @@ public class PatchMetadataIT extends AbstractEntityIntegrationTest {
private List<String> authorsOriginalOrder; private List<String> authorsOriginalOrder;
private List<MetadataValue> authorsMetadataOriginalOrder;
private AtomicReference<Integer> idRef1; private AtomicReference<Integer> idRef1;
private AtomicReference<Integer> idRef2; private AtomicReference<Integer> idRef2;
@@ -202,19 +206,19 @@ public class PatchMetadataIT extends AbstractEntityIntegrationTest {
.andDo(result -> idRef2.set(read(result.getResponse().getContentAsString(), "$.id"))); .andDo(result -> idRef2.set(read(result.getResponse().getContentAsString(), "$.id")));
publication = workspaceItemService.find(context, publicationItem.getID()); publication = workspaceItemService.find(context, publicationItem.getID());
List<MetadataValue> publicationAuthorList = authorsMetadataOriginalOrder =
itemService.getMetadata(publication.getItem(), "dc", "contributor", "author", Item.ANY); itemService.getMetadata(publication.getItem(), "dc", "contributor", "author", Item.ANY);
assertEquals(publicationAuthorList.size(), 5); assertEquals(authorsMetadataOriginalOrder.size(), 5);
assertThat(publicationAuthorList.get(0).getValue(), equalTo(authorsOriginalOrder.get(0))); assertThat(authorsMetadataOriginalOrder.get(0).getValue(), equalTo(authorsOriginalOrder.get(0)));
assertThat(publicationAuthorList.get(0).getAuthority(), not(startsWith("virtual::"))); assertThat(authorsMetadataOriginalOrder.get(0).getAuthority(), not(startsWith("virtual::")));
assertThat(publicationAuthorList.get(1).getValue(), equalTo(authorsOriginalOrder.get(1))); assertThat(authorsMetadataOriginalOrder.get(1).getValue(), equalTo(authorsOriginalOrder.get(1)));
assertThat(publicationAuthorList.get(1).getAuthority(), startsWith("virtual::")); assertThat(authorsMetadataOriginalOrder.get(1).getAuthority(), startsWith("virtual::"));
assertThat(publicationAuthorList.get(2).getValue(), equalTo(authorsOriginalOrder.get(2))); assertThat(authorsMetadataOriginalOrder.get(2).getValue(), equalTo(authorsOriginalOrder.get(2)));
assertThat(publicationAuthorList.get(2).getAuthority(), not(startsWith("virtual::"))); assertThat(authorsMetadataOriginalOrder.get(2).getAuthority(), not(startsWith("virtual::")));
assertThat(publicationAuthorList.get(3).getValue(), equalTo(authorsOriginalOrder.get(3))); assertThat(authorsMetadataOriginalOrder.get(3).getValue(), equalTo(authorsOriginalOrder.get(3)));
assertThat(publicationAuthorList.get(3).getAuthority(), not(startsWith("virtual::"))); assertThat(authorsMetadataOriginalOrder.get(3).getAuthority(), not(startsWith("virtual::")));
assertThat(publicationAuthorList.get(4).getValue(), equalTo(authorsOriginalOrder.get(4))); assertThat(authorsMetadataOriginalOrder.get(4).getValue(), equalTo(authorsOriginalOrder.get(4)));
assertThat(publicationAuthorList.get(4).getAuthority(), startsWith("virtual::")); assertThat(authorsMetadataOriginalOrder.get(4).getAuthority(), startsWith("virtual::"));
} }
/** /**
@@ -1162,6 +1166,59 @@ public class PatchMetadataIT extends AbstractEntityIntegrationTest {
}
/**
* This test will overwrite all authors (dc.contributor.author) of a workspace publication's "traditionalpageone"
* section using a PATCH add with the entire array values.
* It makes sure that virtual values are correctly reordered or deleted.
*/
@Test
public void patchAddAllAuthorsOnTraditionalPageTest() throws Exception {
// "Whyte, William"
// "Dahlen, Sarah" (virtual)
// "Peterson, Karrie"
// "Perotti, Enrico"
// "Linton, Oliver" (virtual)
initPersonPublicationWorkspace();
List<MetadataValue> expectedValues = new ArrayList<MetadataValue>();
expectedValues.add(this.authorsMetadataOriginalOrder.get(2)); // "Peterson, Karrie"
expectedValues.add(this.authorsMetadataOriginalOrder.get(4)); // "Linton, Oliver" (virtual)
expectedValues.add(this.authorsMetadataOriginalOrder.get(0)); // "Whyte, William"
patchAddEntireArray(expectedValues);
}
/**
* This test will overwrite all authors (dc.contributor.author) of a workspace publication's "traditionalpageone"
* section using a PATCH add with an array composed by only a not existent virtual metadata.
*/
@Test
public void patchAddAllAuthorsOnTraditionalPageNotExistentRelationTest() throws Exception {
initPersonPublicationWorkspace();
List<Operation> ops = new ArrayList<Operation>();
List<MetadataValueRest> value = new ArrayList<MetadataValueRest>();
MetadataValueRest mrv = new MetadataValueRest();
value.add(mrv);
mrv.setValue("Dumbar, John");
mrv.setAuthority("virtual::" + Integer.MAX_VALUE);
AddOperation add = new AddOperation("/sections/traditionalpageone/dc.contributor.author", value);
ops.add(add);
String patchBody = getPatchContent(ops);
String token = getAuthToken(admin.getEmail(), password);
getClient(token).perform(patch("/api/submission/workspaceitems/" + publicationItem.getID())
.content(patchBody)
.contentType(javax.ws.rs.core.MediaType.APPLICATION_JSON_PATCH_JSON))
.andExpect(status().isUnprocessableEntity());
} }
/** /**
@@ -1305,6 +1362,50 @@ public class PatchMetadataIT extends AbstractEntityIntegrationTest {
))); )));
} }
/**
* This method set the entire authors list (dc.contributor.author) within a workspace
* publication's "traditionalpageone" section
* @param metadataValues The metadata list of all the metadata values
*/
private void patchAddEntireArray(List<MetadataValue> metadataValues) throws Exception {
List<Operation> ops = new ArrayList<Operation>();
List<MetadataValueRest> value = new ArrayList<MetadataValueRest>();
// generates the MetadataValueRest list
metadataValues.stream().forEach(mv -> {
MetadataValueRest mrv = new MetadataValueRest();
value.add(mrv);
mrv.setValue(mv.getValue());
if (mv.getAuthority() != null && mv.getAuthority().startsWith("virtual::")) {
mrv.setAuthority(mv.getAuthority());
mrv.setConfidence(mv.getConfidence());
}
});
AddOperation add = new AddOperation("/sections/traditionalpageone/dc.contributor.author", value);
ops.add(add);
String patchBody = getPatchContent(ops);
String token = getAuthToken(admin.getEmail(), password);
getClient(token).perform(patch("/api/submission/workspaceitems/" + publicationItem.getID())
.content(patchBody)
.contentType(javax.ws.rs.core.MediaType.APPLICATION_JSON_PATCH_JSON))
.andExpect(status().isOk());
final String authorField = "dc.contributor.author";
final List<Matcher<? super Object>> matchers = new ArrayList<>();
IntStream.range(0, metadataValues.size()).forEach((i) -> {
matchers.add(Matchers.is(MetadataMatcher.matchMetadata(authorField, metadataValues.get(i).getValue(), i)));
});
getClient(token).perform(get("/api/submission/workspaceitems/" + publicationItem.getID()))
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.sections.traditionalpageone", Matchers.allOf(matchers)));
}
/** /**
* Create a move operation on a workspace item's "traditionalpageone" section for * Create a move operation on a workspace item's "traditionalpageone" section for
* metadata field "dc.contributor.author". * metadata field "dc.contributor.author".

View File

@@ -27,58 +27,96 @@ public class ShibbolethRestControllerIT extends AbstractControllerIntegrationTes
@Autowired @Autowired
ConfigurationService configurationService; ConfigurationService configurationService;
public static final String[] PASS_ONLY = {"org.dspace.authenticate.PasswordAuthentication"};
public static final String[] SHIB_ONLY = {"org.dspace.authenticate.ShibAuthentication"};
@Before @Before
public void setup() throws Exception { public void setup() throws Exception {
super.setUp(); super.setUp();
// Add a second trusted host for some tests
configurationService.setProperty("rest.cors.allowed-origins", configurationService.setProperty("rest.cors.allowed-origins",
"${dspace.ui.url}, http://anotherdspacehost:4000"); "${dspace.ui.url}, http://anotherdspacehost:4000");
// Enable Shibboleth login for all tests
configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", SHIB_ONLY);
} }
@Test @Test
public void testRedirectToDefaultDspaceUrl() throws Exception { public void testRedirectToDefaultDspaceUrl() throws Exception {
String token = getAuthToken(eperson.getEmail(), password); // NOTE: The initial call to /shibboleth comes *from* an external Shibboleth site. So, it is always
// unauthenticated, but it must include some expected SHIB attributes.
getClient(token).perform(get("/api/authn/shibboleth")) // SHIB-MAIL attribute is the default email header sent from Shibboleth after a successful login.
// In this test we are simply mocking that behavior by setting it to an existing EPerson.
getClient().perform(get("/api/authn/shibboleth").requestAttr("SHIB-MAIL", eperson.getEmail()))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost:4000")); .andExpect(redirectedUrl("http://localhost:4000"));
} }
@Test @Test
public void testRedirectToGivenTrustedUrl() throws Exception { public void testRedirectToGivenTrustedUrl() throws Exception {
getClient().perform(get("/api/authn/shibboleth")
String token = getAuthToken(eperson.getEmail(), password); .param("redirectUrl", "http://localhost:8080/server/api/authn/status")
.requestAttr("SHIB-MAIL", eperson.getEmail()))
getClient(token).perform(get("/api/authn/shibboleth")
.param("redirectUrl", "http://localhost:8080/server/api/authn/status"))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost:8080/server/api/authn/status")); .andExpect(redirectedUrl("http://localhost:8080/server/api/authn/status"));
} }
@Test
public void testNoRedirectIfShibbolethDisabled() throws Exception {
// Enable Password authentication ONLY
configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", PASS_ONLY);
// Test redirecting to a trusted URL (same as previous test).
// This time we should be unauthorized as Shibboleth is disabled.
getClient().perform(get("/api/authn/shibboleth")
.param("redirectUrl", "http://localhost:8080/server/api/authn/status")
.requestAttr("SHIB-MAIL", eperson.getEmail()))
.andExpect(status().isUnauthorized());
}
@Test @Test
public void testRedirectToAnotherGivenTrustedUrl() throws Exception { public void testRedirectToAnotherGivenTrustedUrl() throws Exception {
String token = getAuthToken(eperson.getEmail(), password); String token = getAuthToken(eperson.getEmail(), password);
getClient(token).perform(get("/api/authn/shibboleth") getClient().perform(get("/api/authn/shibboleth")
.param("redirectUrl", "http://anotherdspacehost:4000/home")) .param("redirectUrl", "http://anotherdspacehost:4000/home")
.requestAttr("SHIB-MAIL", eperson.getEmail()))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://anotherdspacehost:4000/home")); .andExpect(redirectedUrl("http://anotherdspacehost:4000/home"));
} }
@Test @Test
public void testRedirectToGivenUntrustedUrl() throws Exception { public void testRedirectToGivenUntrustedUrl() throws Exception {
String token = getAuthToken(eperson.getEmail(), password); // Now attempt to redirect to a URL that is NOT trusted (i.e. not in 'rest.cors.allowed-origins').
// Now attempt to redirect to a URL that is NOT trusted (i.e. not the Server or UI).
// Should result in a 400 error. // Should result in a 400 error.
getClient(token).perform(get("/api/authn/shibboleth") getClient().perform(get("/api/authn/shibboleth")
.param("redirectUrl", "http://dspace.org")) .param("redirectUrl", "http://dspace.org")
.andExpect(status().isBadRequest()); .requestAttr("SHIB-MAIL", eperson.getEmail()))
.andExpect(status().isBadRequest());
} }
@Test @Test
public void testRedirectRequiresAuth() throws Exception { public void testNoRedirectIfInvalidShibAttributes() throws Exception {
// In this request, we use a SHIB-MAIL attribute which does NOT match an EPerson.
getClient().perform(get("/api/authn/shibboleth")
.requestAttr("SHIB-MAIL", "not-an-eperson@example.com"))
.andExpect(status().isUnauthorized());
}
@Test
public void testRedirectRequiresShibAttributes() throws Exception {
// Verify this endpoint doesn't work if no SHIB-* attributes are set
getClient().perform(get("/api/authn/shibboleth")) getClient().perform(get("/api/authn/shibboleth"))
.andExpect(status().isUnauthorized()); .andExpect(status().isUnauthorized());
}
@Test
public void testRedirectRequiresShibAttributes2() throws Exception {
String token = getAuthToken(eperson.getEmail(), password);
// Verify this endpoint also doesn't work using a regular auth token (again if SHIB-* attributes missing)
getClient(token).perform(get("/api/authn/shibboleth"))
.andExpect(status().isUnauthorized());
} }
} }

View File

@@ -179,7 +179,7 @@ public class StatisticsRestRepositoryIT extends AbstractControllerIntegrationTes
getClient("unvalidToken").perform( getClient("unvalidToken").perform(
get("/api/statistics/usagereports/" + itemNotVisitedWithBitstreams.getID() + "_" + TOTAL_VISITS_REPORT_ID)) get("/api/statistics/usagereports/" + itemNotVisitedWithBitstreams.getID() + "_" + TOTAL_VISITS_REPORT_ID))
// ** THEN ** // ** THEN **
.andExpect(status().isForbidden()); .andExpect(status().isUnauthorized());
} }
@Test @Test
@@ -829,7 +829,7 @@ public class StatisticsRestRepositoryIT extends AbstractControllerIntegrationTes
.perform(get("/api/statistics/usagereports/search/object?uri=http://localhost:8080/server/api/core" + .perform(get("/api/statistics/usagereports/search/object?uri=http://localhost:8080/server/api/core" +
"/items/" + itemNotVisitedWithBitstreams.getID())) "/items/" + itemNotVisitedWithBitstreams.getID()))
// ** THEN ** // ** THEN **
.andExpect(status().isForbidden()); .andExpect(status().isUnauthorized());
} }
@Test @Test

View File

@@ -49,8 +49,8 @@ public class WorkflowActionRestRepositoryIT extends AbstractControllerIntegratio
String token = "nonValidToken"; String token = "nonValidToken";
//When we call this facets endpoint //When we call this facets endpoint
getClient(token).perform(get(WORKFLOW_ACTIONS_ENDPOINT)) getClient(token).perform(get(WORKFLOW_ACTIONS_ENDPOINT))
//We expect a 403 Forbidden status //We expect a 401 Unauthorized status
.andExpect(status().isForbidden()); .andExpect(status().isUnauthorized());
} }
@Test @Test
@@ -112,8 +112,8 @@ public class WorkflowActionRestRepositoryIT extends AbstractControllerIntegratio
WorkflowActionConfig existentWorkflow = xmlWorkflowFactory.getActionByName(nameActionWithOptions); WorkflowActionConfig existentWorkflow = xmlWorkflowFactory.getActionByName(nameActionWithOptions);
//When we call this facets endpoint //When we call this facets endpoint
getClient(token).perform(get(WORKFLOW_ACTIONS_ENDPOINT + "/" + nameActionWithOptions)) getClient(token).perform(get(WORKFLOW_ACTIONS_ENDPOINT + "/" + nameActionWithOptions))
//We expect a 403 Forbidden status //We expect a 401 Unauthorized status
.andExpect(status().isForbidden()); .andExpect(status().isUnauthorized());
} }
@Test @Test

View File

@@ -121,8 +121,8 @@ public class WorkflowDefinitionRestRepositoryIT extends AbstractControllerIntegr
String token = "NonValidToken"; String token = "NonValidToken";
//When we call this facets endpoint //When we call this facets endpoint
getClient(token).perform(get(WORKFLOW_DEFINITIONS_ENDPOINT)) getClient(token).perform(get(WORKFLOW_DEFINITIONS_ENDPOINT))
//We expect a 403 Forbidden status //We expect a 401 Unauthorized status
.andExpect(status().isForbidden()); .andExpect(status().isUnauthorized());
} }
@Test @Test
@@ -192,8 +192,8 @@ public class WorkflowDefinitionRestRepositoryIT extends AbstractControllerIntegr
String workflowName = defaultWorkflow.getID(); String workflowName = defaultWorkflow.getID();
//When we call this facets endpoint //When we call this facets endpoint
getClient(token).perform(get(WORKFLOW_DEFINITIONS_ENDPOINT + "/" + workflowName)) getClient(token).perform(get(WORKFLOW_DEFINITIONS_ENDPOINT + "/" + workflowName))
//We expect a 403 Forbidden status //We expect a 401 Unauthorized status
.andExpect(status().isForbidden()); .andExpect(status().isUnauthorized());
} }
@Test @Test
@@ -402,8 +402,8 @@ public class WorkflowDefinitionRestRepositoryIT extends AbstractControllerIntegr
//When we call this facets endpoint //When we call this facets endpoint
getClient(token).perform(get(WORKFLOW_DEFINITIONS_ENDPOINT + "/" + defaultWorkflow.getID() getClient(token).perform(get(WORKFLOW_DEFINITIONS_ENDPOINT + "/" + defaultWorkflow.getID()
+ "/collections")) + "/collections"))
//We expect a 403 Forbidden status //We expect a 401 Unauthorized status
.andExpect(status().isForbidden()); .andExpect(status().isUnauthorized());
} }
@Test @Test
@@ -441,8 +441,8 @@ public class WorkflowDefinitionRestRepositoryIT extends AbstractControllerIntegr
//When we call this facets endpoint //When we call this facets endpoint
getClient(token).perform(get(WORKFLOW_DEFINITIONS_ENDPOINT + "/" + defaultWorkflow.getID() getClient(token).perform(get(WORKFLOW_DEFINITIONS_ENDPOINT + "/" + defaultWorkflow.getID()
+ "/steps")) + "/steps"))
//We expect a 403 Forbidden status //We expect a 401 Unauthorized status
.andExpect(status().isForbidden()); .andExpect(status().isUnauthorized());
} }
@Test @Test

View File

@@ -47,8 +47,8 @@ public class WorkflowStepRestRepositoryIT extends AbstractControllerIntegrationT
String token = "NonValidToken"; String token = "NonValidToken";
//When we call this facets endpoint //When we call this facets endpoint
getClient(token).perform(get(WORKFLOW_ACTIONS_ENDPOINT)) getClient(token).perform(get(WORKFLOW_ACTIONS_ENDPOINT))
//We expect a 403 Forbidden status //We expect a 401 Unauthorized status
.andExpect(status().isForbidden()); .andExpect(status().isUnauthorized());
} }
@Test @Test

View File

@@ -0,0 +1,219 @@
/**
* 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.app.rest.authorization;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.dspace.app.rest.authorization.impl.CanCreateVersionFeature;
import org.dspace.app.rest.converter.ItemConverter;
import org.dspace.app.rest.matcher.AuthorizationMatcher;
import org.dspace.app.rest.model.ItemRest;
import org.dspace.app.rest.projection.DefaultProjection;
import org.dspace.app.rest.projection.Projection;
import org.dspace.app.rest.test.AbstractControllerIntegrationTest;
import org.dspace.app.rest.utils.Utils;
import org.dspace.builder.CollectionBuilder;
import org.dspace.builder.CommunityBuilder;
import org.dspace.builder.EPersonBuilder;
import org.dspace.builder.ItemBuilder;
import org.dspace.content.Collection;
import org.dspace.content.Community;
import org.dspace.content.Item;
import org.dspace.eperson.EPerson;
import org.dspace.services.ConfigurationService;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Test for the canCreateVersion authorization feature.
*
* @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.it)
*/
public class CanCreateVersionFeatureIT extends AbstractControllerIntegrationTest {
@Autowired
private Utils utils;
@Autowired
private ItemConverter itemConverter;
@Autowired
private ConfigurationService configurationService;
@Autowired
private AuthorizationFeatureService authorizationFeatureService;
private Item itemA;
private Item itemB;
private EPerson user;
private ItemRest itemARest;
private Community communityA;
private Collection collectionA;
private AuthorizationFeature canCreateVersionFeature;
final String feature = "canCreateVersion";
@Override
@Before
public void setUp() throws Exception {
super.setUp();
context.turnOffAuthorisationSystem();
canCreateVersionFeature = authorizationFeatureService.find(CanCreateVersionFeature.NAME);
user = EPersonBuilder.createEPerson(context)
.withEmail("userEmail@test.com")
.withPassword(password).build();
communityA = CommunityBuilder.createCommunity(context)
.withName("communityA").build();
collectionA = CollectionBuilder.createCollection(context, communityA)
.withName("collectionA").build();
itemA = ItemBuilder.createItem(context, collectionA)
.withTitle("Item A").build();
itemB = ItemBuilder.createItem(context, collectionA)
.withTitle("Item B").build();
context.restoreAuthSystemState();
itemARest = itemConverter.convert(itemA, Projection.DEFAULT);
}
@Test
public void anonymousHasNotAccessTest() throws Exception {
getClient().perform(get("/api/authz/authorizations/search/object")
.param("embed", "feature")
.param("feature", feature)
.param("uri", utils.linkToSingleResource(itemARest, "self").getHref()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.page.totalElements", is(0)))
.andExpect(jsonPath("$._embedded").doesNotExist());
}
@Test
public void epersonHasNotAccessTest() throws Exception {
String epersonToken = getAuthToken(eperson.getEmail(), password);
getClient(epersonToken).perform(get("/api/authz/authorizations/search/object")
.param("embed", "feature")
.param("feature", feature)
.param("uri", utils.linkToSingleResource(itemARest, "self").getHref()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.page.totalElements", is(0)))
.andExpect(jsonPath("$._embedded").doesNotExist());
}
@Test
public void adminItemSuccessTest() throws Exception {
String adminToken = getAuthToken(admin.getEmail(), password);
getClient(adminToken).perform(get("/api/authz/authorizations/search/object")
.param("embed", "feature")
.param("feature", feature)
.param("uri", utils.linkToSingleResource(itemARest, "self").getHref()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.page.totalElements", greaterThan(0)))
.andExpect(jsonPath("$._embedded").exists());
}
@Test
public void submitterItemSuccessTest() throws Exception {
context.turnOffAuthorisationSystem();
configurationService.setProperty("versioning.submitterCanCreateNewVersion", true);
itemA.setSubmitter(user);
context.restoreAuthSystemState();
String userToken = getAuthToken(user.getEmail(), password);
getClient(userToken).perform(get("/api/authz/authorizations/search/object")
.param("embed", "feature")
.param("feature", feature)
.param("uri", utils.linkToSingleResource(itemARest, "self").getHref()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.page.totalElements", greaterThan(0)))
.andExpect(jsonPath("$._embedded").exists());
}
@Test
public void submitterItemWithPropertySubmitterCanCreateNewVersionIsFalseTest() throws Exception {
context.turnOffAuthorisationSystem();
configurationService.setProperty("versioning.submitterCanCreateNewVersion", false);
itemA.setSubmitter(user);
context.restoreAuthSystemState();
String userToken = getAuthToken(user.getEmail(), password);
getClient(userToken).perform(get("/api/authz/authorizations/search/object")
.param("embed", "feature")
.param("feature", feature)
.param("uri", utils.linkToSingleResource(itemARest, "self").getHref()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.page.totalElements", is(0)))
.andExpect(jsonPath("$._embedded").doesNotExist());
}
@Test
@SuppressWarnings("unchecked")
public void checkCanCreateVersionsFeatureTest() throws Exception {
context.turnOffAuthorisationSystem();
configurationService.setProperty("versioning.submitterCanCreateNewVersion", true);
itemA.setSubmitter(user);
itemB.setSubmitter(admin);
context.restoreAuthSystemState();
ItemRest itemRestA = itemConverter.convert(itemA, DefaultProjection.DEFAULT);
ItemRest itemRestB = itemConverter.convert(itemB, DefaultProjection.DEFAULT);
String tokenEPerson = getAuthToken(eperson.getEmail(), password);
String tokenAdmin = getAuthToken(admin.getEmail(), password);
String tokenUser = getAuthToken(user.getEmail(), password);
// define authorizations that we know must exists
Authorization admin2ItemA = new Authorization(admin, canCreateVersionFeature, itemRestA);
Authorization admin2ItemB = new Authorization(admin, canCreateVersionFeature, itemRestB);
Authorization user2ItemA = new Authorization(user, canCreateVersionFeature, itemRestA);
// define authorization that we know not exists
Authorization eperson2ItemA = new Authorization(eperson, canCreateVersionFeature, itemRestA);
Authorization eperson2ItemB = new Authorization(eperson, canCreateVersionFeature, itemRestB);
Authorization user2ItemB = new Authorization(user, canCreateVersionFeature, itemRestB);
getClient(tokenAdmin).perform(get("/api/authz/authorizations/" + admin2ItemA.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(admin2ItemA))));
getClient(tokenAdmin).perform(get("/api/authz/authorizations/" + admin2ItemB.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(admin2ItemB))));
getClient(tokenUser).perform(get("/api/authz/authorizations/" + user2ItemA.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(user2ItemA))));
getClient(tokenEPerson).perform(get("/api/authz/authorizations/" + eperson2ItemA.getID()))
.andExpect(status().isNotFound());
getClient(tokenEPerson).perform(get("/api/authz/authorizations/" + eperson2ItemB.getID()))
.andExpect(status().isNotFound());
getClient(tokenUser).perform(get("/api/authz/authorizations/" + user2ItemB.getID()))
.andExpect(status().isNotFound());
}
}

View File

@@ -0,0 +1,547 @@
/**
* 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.app.rest.authorization;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.dspace.app.rest.authorization.impl.CanManageBitstreamBundlesFeature;
import org.dspace.app.rest.converter.ItemConverter;
import org.dspace.app.rest.matcher.AuthorizationMatcher;
import org.dspace.app.rest.model.ItemRest;
import org.dspace.app.rest.projection.DefaultProjection;
import org.dspace.app.rest.test.AbstractControllerIntegrationTest;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.builder.CollectionBuilder;
import org.dspace.builder.CommunityBuilder;
import org.dspace.builder.EPersonBuilder;
import org.dspace.builder.ItemBuilder;
import org.dspace.builder.ResourcePolicyBuilder;
import org.dspace.content.Collection;
import org.dspace.content.Community;
import org.dspace.content.Item;
import org.dspace.core.Constants;
import org.dspace.eperson.EPerson;
import org.dspace.services.ConfigurationService;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Test for the canManageBitstreamBundles authorization feature.
*
* @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.it)
*/
public class CanManageBitstreamBundlesFeatureIT extends AbstractControllerIntegrationTest {
@Autowired
private ItemConverter itemConverter;
@Autowired
private AuthorizeService authorizeService;
@Autowired
private ConfigurationService configurationService;
@Autowired
private AuthorizationFeatureService authorizationFeatureService;
private Item itemA;
private Item itemB;
private EPerson userA;
private EPerson userB;
private EPerson userColAadmin;
private EPerson userColBadmin;
private EPerson userComAdmin;
private Community communityA;
private Collection collectionA;
private Collection collectionB;
private AuthorizationFeature canManageBitstreamBundlesFeature;
final String feature = "canManageBitstreamBundles";
@Before
@Override
public void setUp() throws Exception {
super.setUp();
context.turnOffAuthorisationSystem();
canManageBitstreamBundlesFeature = authorizationFeatureService.find(CanManageBitstreamBundlesFeature.NAME);
userA = EPersonBuilder.createEPerson(context)
.withEmail("userEmail@test.com")
.withPassword(password).build();
userB = EPersonBuilder.createEPerson(context)
.withEmail("userB.email@test.com")
.withPassword(password).build();
userColAadmin = EPersonBuilder.createEPerson(context)
.withEmail("userColAadmin@test.com")
.withPassword(password).build();
userColBadmin = EPersonBuilder.createEPerson(context)
.withEmail("userColBadmin@test.com")
.withPassword(password).build();
userComAdmin = EPersonBuilder.createEPerson(context)
.withEmail("userComAdmin@test.com")
.withPassword(password).build();
communityA = CommunityBuilder.createCommunity(context)
.withName("communityA")
.withAdminGroup(userComAdmin).build();
collectionA = CollectionBuilder.createCollection(context, communityA)
.withName("Collection A")
.withAdminGroup(userColAadmin).build();
collectionB = CollectionBuilder.createCollection(context, communityA)
.withName("Collection B")
.withAdminGroup(userColBadmin).build();
itemA = ItemBuilder.createItem(context, collectionA)
.withTitle("Item A").build();
itemB = ItemBuilder.createItem(context, collectionB)
.withTitle("Item B").build();
context.restoreAuthSystemState();
}
@Test
@SuppressWarnings("unchecked")
public void checkCanCreateVersionsFeatureTest() throws Exception {
context.turnOffAuthorisationSystem();
//permissions for userA
authorizeService.addPolicy(context, itemA, Constants.ADD, userA);
authorizeService.addPolicy(context, itemA, Constants.REMOVE, userA);
// permissions for userB
authorizeService.addPolicy(context, itemA, Constants.REMOVE, userB);
authorizeService.addPolicy(context, itemB, Constants.REMOVE, userB);
authorizeService.addPolicy(context, itemB, Constants.ADD, userB);
context.restoreAuthSystemState();
ItemRest itemRestA = itemConverter.convert(itemA, DefaultProjection.DEFAULT);
ItemRest itemRestB = itemConverter.convert(itemB, DefaultProjection.DEFAULT);
String tokenEPerson = getAuthToken(eperson.getEmail(), password);
String tokenAdmin = getAuthToken(admin.getEmail(), password);
String tokenAUser = getAuthToken(userA.getEmail(), password);
String tokenBUser = getAuthToken(userB.getEmail(), password);
String tokenComAdmin = getAuthToken(userComAdmin.getEmail(), password);
String tokenColAadmin = getAuthToken(userColAadmin.getEmail(), password);
String tokenColBadmin = getAuthToken(userColBadmin.getEmail(), password);
// define authorizations that we know must exists
Authorization admin2ItemA = new Authorization(admin, canManageBitstreamBundlesFeature, itemRestA);
Authorization admin2ItemB = new Authorization(admin, canManageBitstreamBundlesFeature, itemRestB);
Authorization userA2ItemA = new Authorization(userA, canManageBitstreamBundlesFeature, itemRestA);
Authorization userB2ItemB = new Authorization(userB, canManageBitstreamBundlesFeature, itemRestB);
Authorization comAdmin2ItemB = new Authorization(userComAdmin, canManageBitstreamBundlesFeature, itemRestB);
Authorization comAdmin2ItemA = new Authorization(userComAdmin, canManageBitstreamBundlesFeature, itemRestA);
Authorization colAadmin2ItemA = new Authorization(userColAadmin, canManageBitstreamBundlesFeature, itemRestA);
// define authorization that we know not exists
Authorization userB2ItemA = new Authorization(userB, canManageBitstreamBundlesFeature, itemRestA);
Authorization userA2ItemB = new Authorization(userA, canManageBitstreamBundlesFeature, itemRestB);
Authorization eperson2ItemA = new Authorization(eperson, canManageBitstreamBundlesFeature, itemRestA);
Authorization eperson2ItemB = new Authorization(eperson, canManageBitstreamBundlesFeature, itemRestB);
Authorization anonymous2ItemA = new Authorization(null, canManageBitstreamBundlesFeature, itemRestA);
Authorization anonymous2ItemB = new Authorization(null, canManageBitstreamBundlesFeature, itemRestB);
Authorization colAadmin2ItemB = new Authorization(userColAadmin, canManageBitstreamBundlesFeature, itemRestB);
Authorization colBadmin2ItemA = new Authorization(userColBadmin, canManageBitstreamBundlesFeature, itemRestA);
getClient(tokenAdmin).perform(get("/api/authz/authorizations/" + admin2ItemA.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(admin2ItemA))));
getClient(tokenAdmin).perform(get("/api/authz/authorizations/" + admin2ItemB.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(admin2ItemB))));
getClient(tokenAUser).perform(get("/api/authz/authorizations/" + userA2ItemA.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(userA2ItemA))));
getClient(tokenBUser).perform(get("/api/authz/authorizations/" + userB2ItemB.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(userB2ItemB))));
getClient(tokenComAdmin).perform(get("/api/authz/authorizations/" + comAdmin2ItemB.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(comAdmin2ItemB))));
getClient(tokenComAdmin).perform(get("/api/authz/authorizations/" + comAdmin2ItemA.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(comAdmin2ItemA))));
getClient(tokenColAadmin).perform(get("/api/authz/authorizations/" + colAadmin2ItemA.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(colAadmin2ItemA))));
getClient(tokenColAadmin).perform(get("/api/authz/authorizations/" + colAadmin2ItemB.getID()))
.andExpect(status().isNotFound());
getClient(tokenColBadmin).perform(get("/api/authz/authorizations/" + colBadmin2ItemA.getID()))
.andExpect(status().isNotFound());
getClient(tokenBUser).perform(get("/api/authz/authorizations/" + userB2ItemA.getID()))
.andExpect(status().isNotFound());
getClient(tokenAUser).perform(get("/api/authz/authorizations/" + userA2ItemB.getID()))
.andExpect(status().isNotFound());
getClient(tokenEPerson).perform(get("/api/authz/authorizations/" + eperson2ItemA.getID()))
.andExpect(status().isNotFound());
getClient(tokenEPerson).perform(get("/api/authz/authorizations/" + eperson2ItemB.getID()))
.andExpect(status().isNotFound());
getClient().perform(get("/api/authz/authorizations/" + anonymous2ItemA.getID()))
.andExpect(status().isNotFound());
getClient().perform(get("/api/authz/authorizations/" + anonymous2ItemB.getID()))
.andExpect(status().isNotFound());
}
@Test
@SuppressWarnings("unchecked")
public void itemAdminSetPropertyCreateBitstreamToFalseTest() throws Exception {
context.turnOffAuthorisationSystem();
ResourcePolicyBuilder.createResourcePolicy(context)
.withAction(Constants.ADMIN)
.withUser(userA)
.withDspaceObject(itemA).build();
configurationService.setProperty("core.authorization.item-admin.create-bitstream", false);
context.restoreAuthSystemState();
ItemRest itemRestA = itemConverter.convert(itemA, DefaultProjection.DEFAULT);
String tokenAdmin = getAuthToken(admin.getEmail(), password);
String tokenAUser = getAuthToken(userA.getEmail(), password);
String tokenComAdmin = getAuthToken(userComAdmin.getEmail(), password);
String tokenColAadmin = getAuthToken(userColAadmin.getEmail(), password);
// define authorizations that we know must exists
Authorization admin2ItemA = new Authorization(admin, canManageBitstreamBundlesFeature, itemRestA);
Authorization comAdmin2ItemA = new Authorization(userComAdmin, canManageBitstreamBundlesFeature, itemRestA);
Authorization colAadmin2ItemA = new Authorization(userColAadmin, canManageBitstreamBundlesFeature, itemRestA);
// define authorization that we know not exists
Authorization userA2ItemA = new Authorization(userA, canManageBitstreamBundlesFeature, itemRestA);
getClient(tokenAdmin).perform(get("/api/authz/authorizations/" + admin2ItemA.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(admin2ItemA))));
getClient(tokenComAdmin).perform(get("/api/authz/authorizations/" + comAdmin2ItemA.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(comAdmin2ItemA))));
getClient(tokenColAadmin).perform(get("/api/authz/authorizations/" + colAadmin2ItemA.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(colAadmin2ItemA))));
getClient(tokenAUser).perform(get("/api/authz/authorizations/" + userA2ItemA.getID()))
.andExpect(status().isNotFound());
}
@Test
@SuppressWarnings("unchecked")
public void itemAdminSetPropertyDeleteBitstreamToFalseTest() throws Exception {
context.turnOffAuthorisationSystem();
ResourcePolicyBuilder.createResourcePolicy(context)
.withAction(Constants.ADMIN)
.withUser(userA)
.withDspaceObject(itemA).build();
configurationService.setProperty("core.authorization.item-admin.delete-bitstream", false);
context.restoreAuthSystemState();
ItemRest itemRestA = itemConverter.convert(itemA, DefaultProjection.DEFAULT);
String tokenAdmin = getAuthToken(admin.getEmail(), password);
String tokenAUser = getAuthToken(userA.getEmail(), password);
String tokenComAdmin = getAuthToken(userComAdmin.getEmail(), password);
String tokenColAadmin = getAuthToken(userColAadmin.getEmail(), password);
// define authorizations that we know must exists
Authorization admin2ItemA = new Authorization(admin, canManageBitstreamBundlesFeature, itemRestA);
Authorization comAdmin2ItemA = new Authorization(userComAdmin, canManageBitstreamBundlesFeature, itemRestA);
Authorization colAadmin2ItemA = new Authorization(userColAadmin, canManageBitstreamBundlesFeature, itemRestA);
// define authorization that we know not exists
Authorization userA2ItemA = new Authorization(userA, canManageBitstreamBundlesFeature, itemRestA);
getClient(tokenAdmin).perform(get("/api/authz/authorizations/" + admin2ItemA.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(admin2ItemA))));
getClient(tokenComAdmin).perform(get("/api/authz/authorizations/" + comAdmin2ItemA.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(comAdmin2ItemA))));
getClient(tokenColAadmin).perform(get("/api/authz/authorizations/" + colAadmin2ItemA.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(colAadmin2ItemA))));
getClient(tokenAUser).perform(get("/api/authz/authorizations/" + userA2ItemA.getID()))
.andExpect(status().isNotFound());
}
@Test
@SuppressWarnings("unchecked")
public void itemAdminSetPropertyCollectionAdminCreateBitstreamToFalseTest() throws Exception {
context.turnOffAuthorisationSystem();
ResourcePolicyBuilder.createResourcePolicy(context)
.withAction(Constants.ADMIN)
.withUser(userA)
.withDspaceObject(itemA).build();
configurationService.setProperty("core.authorization.collection-admin.item.create-bitstream", false);
configurationService.setProperty("core.authorization.item-admin.delete-bitstream", false);
configurationService.setProperty("core.authorization.item-admin.create-bitstream", false);
context.restoreAuthSystemState();
ItemRest itemRestA = itemConverter.convert(itemA, DefaultProjection.DEFAULT);
ItemRest itemRestB = itemConverter.convert(itemB, DefaultProjection.DEFAULT);
String tokenAdmin = getAuthToken(admin.getEmail(), password);
String tokenAUser = getAuthToken(userA.getEmail(), password);
String tokenComAdmin = getAuthToken(userComAdmin.getEmail(), password);
String tokenColAadmin = getAuthToken(userColAadmin.getEmail(), password);
String tokenColBadmin = getAuthToken(userColBadmin.getEmail(), password);
// define authorizations that we know must exists
Authorization admin2ItemA = new Authorization(admin, canManageBitstreamBundlesFeature, itemRestA);
Authorization admin2ItemB = new Authorization(admin, canManageBitstreamBundlesFeature, itemRestB);
Authorization comAdmin2ItemA = new Authorization(userComAdmin, canManageBitstreamBundlesFeature, itemRestA);
Authorization comAdmin2ItemB = new Authorization(userComAdmin, canManageBitstreamBundlesFeature, itemRestB);
// define authorization that we know not exists
Authorization colAadmin2ItemA = new Authorization(userColAadmin, canManageBitstreamBundlesFeature, itemRestA);
Authorization colBadmin2ItemB = new Authorization(userColBadmin, canManageBitstreamBundlesFeature, itemRestA);
Authorization userA2ItemA = new Authorization(userA, canManageBitstreamBundlesFeature, itemRestA);
getClient(tokenAdmin).perform(get("/api/authz/authorizations/" + admin2ItemA.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(admin2ItemA))));
getClient(tokenAdmin).perform(get("/api/authz/authorizations/" + admin2ItemB.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(admin2ItemB))));
getClient(tokenComAdmin).perform(get("/api/authz/authorizations/" + comAdmin2ItemA.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(comAdmin2ItemA))));
getClient(tokenComAdmin).perform(get("/api/authz/authorizations/" + comAdmin2ItemB.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(comAdmin2ItemB))));
getClient(tokenColAadmin).perform(get("/api/authz/authorizations/" + colAadmin2ItemA.getID()))
.andExpect(status().isNotFound());
getClient(tokenColBadmin).perform(get("/api/authz/authorizations/" + colBadmin2ItemB.getID()))
.andExpect(status().isNotFound());
getClient(tokenAUser).perform(get("/api/authz/authorizations/" + userA2ItemA.getID()))
.andExpect(status().isNotFound());
}
@Test
@SuppressWarnings("unchecked")
public void itemAdminSetPropertyCollectionAdminDeleteBitstreamToFalseTest() throws Exception {
context.turnOffAuthorisationSystem();
ResourcePolicyBuilder.createResourcePolicy(context)
.withAction(Constants.ADMIN)
.withUser(userA)
.withDspaceObject(itemA).build();
configurationService.setProperty("core.authorization.collection-admin.item.delete-bitstream", false);
configurationService.setProperty("core.authorization.item-admin.delete-bitstream", false);
configurationService.setProperty("core.authorization.item-admin.create-bitstream", false);
context.restoreAuthSystemState();
ItemRest itemRestA = itemConverter.convert(itemA, DefaultProjection.DEFAULT);
ItemRest itemRestB = itemConverter.convert(itemB, DefaultProjection.DEFAULT);
String tokenAdmin = getAuthToken(admin.getEmail(), password);
String tokenAUser = getAuthToken(userA.getEmail(), password);
String tokenComAdmin = getAuthToken(userComAdmin.getEmail(), password);
String tokenColAadmin = getAuthToken(userColAadmin.getEmail(), password);
String tokenColBadmin = getAuthToken(userColBadmin.getEmail(), password);
// define authorizations that we know must exists
Authorization admin2ItemA = new Authorization(admin, canManageBitstreamBundlesFeature, itemRestA);
Authorization admin2ItemB = new Authorization(admin, canManageBitstreamBundlesFeature, itemRestB);
Authorization comAdmin2ItemA = new Authorization(userComAdmin, canManageBitstreamBundlesFeature, itemRestA);
Authorization comAdmin2ItemB = new Authorization(userComAdmin, canManageBitstreamBundlesFeature, itemRestB);
// define authorization that we know not exists
Authorization colAadmin2ItemA = new Authorization(userColAadmin, canManageBitstreamBundlesFeature, itemRestA);
Authorization colBadmin2ItemB = new Authorization(userColBadmin, canManageBitstreamBundlesFeature, itemRestA);
Authorization userA2ItemA = new Authorization(userA, canManageBitstreamBundlesFeature, itemRestA);
getClient(tokenAdmin).perform(get("/api/authz/authorizations/" + admin2ItemA.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(admin2ItemA))));
getClient(tokenAdmin).perform(get("/api/authz/authorizations/" + admin2ItemB.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(admin2ItemB))));
getClient(tokenComAdmin).perform(get("/api/authz/authorizations/" + comAdmin2ItemA.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(comAdmin2ItemA))));
getClient(tokenComAdmin).perform(get("/api/authz/authorizations/" + comAdmin2ItemB.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(comAdmin2ItemB))));
getClient(tokenColAadmin).perform(get("/api/authz/authorizations/" + colAadmin2ItemA.getID()))
.andExpect(status().isNotFound());
getClient(tokenColBadmin).perform(get("/api/authz/authorizations/" + colBadmin2ItemB.getID()))
.andExpect(status().isNotFound());
getClient(tokenAUser).perform(get("/api/authz/authorizations/" + userA2ItemA.getID()))
.andExpect(status().isNotFound());
}
@Test
@SuppressWarnings("unchecked")
public void itemAdminSetPropertyCommunityAdminCreateBitstreamToFalseTest() throws Exception {
context.turnOffAuthorisationSystem();
ResourcePolicyBuilder.createResourcePolicy(context)
.withAction(Constants.ADMIN)
.withUser(userA)
.withDspaceObject(itemA).build();
configurationService.setProperty("core.authorization.community-admin.item.create-bitstream", false);
configurationService.setProperty("core.authorization.collection-admin.item.create-bitstream", false);
configurationService.setProperty("core.authorization.collection-admin.item.delete-bitstream", false);
configurationService.setProperty("core.authorization.item-admin.delete-bitstream", false);
configurationService.setProperty("core.authorization.item-admin.create-bitstream", false);
context.restoreAuthSystemState();
ItemRest itemRestA = itemConverter.convert(itemA, DefaultProjection.DEFAULT);
ItemRest itemRestB = itemConverter.convert(itemB, DefaultProjection.DEFAULT);
String tokenAdmin = getAuthToken(admin.getEmail(), password);
String tokenAUser = getAuthToken(userA.getEmail(), password);
String tokenComAdmin = getAuthToken(userComAdmin.getEmail(), password);
String tokenColAadmin = getAuthToken(userColAadmin.getEmail(), password);
String tokenColBadmin = getAuthToken(userColBadmin.getEmail(), password);
// define authorizations that we know must exists
Authorization admin2ItemA = new Authorization(admin, canManageBitstreamBundlesFeature, itemRestA);
Authorization admin2ItemB = new Authorization(admin, canManageBitstreamBundlesFeature, itemRestB);
// define authorization that we know not exists
Authorization comAdmin2ItemA = new Authorization(userComAdmin, canManageBitstreamBundlesFeature, itemRestA);
Authorization comAdmin2ItemB = new Authorization(userComAdmin, canManageBitstreamBundlesFeature, itemRestB);
Authorization colAadmin2ItemA = new Authorization(userColAadmin, canManageBitstreamBundlesFeature, itemRestA);
Authorization colBadmin2ItemB = new Authorization(userColBadmin, canManageBitstreamBundlesFeature, itemRestA);
Authorization userA2ItemA = new Authorization(userA, canManageBitstreamBundlesFeature, itemRestA);
getClient(tokenAdmin).perform(get("/api/authz/authorizations/" + admin2ItemA.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(admin2ItemA))));
getClient(tokenAdmin).perform(get("/api/authz/authorizations/" + admin2ItemB.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(admin2ItemB))));
getClient(tokenComAdmin).perform(get("/api/authz/authorizations/" + comAdmin2ItemA.getID()))
.andExpect(status().isNotFound());
getClient(tokenComAdmin).perform(get("/api/authz/authorizations/" + comAdmin2ItemB.getID()))
.andExpect(status().isNotFound());
getClient(tokenColAadmin).perform(get("/api/authz/authorizations/" + colAadmin2ItemA.getID()))
.andExpect(status().isNotFound());
getClient(tokenColBadmin).perform(get("/api/authz/authorizations/" + colBadmin2ItemB.getID()))
.andExpect(status().isNotFound());
getClient(tokenAUser).perform(get("/api/authz/authorizations/" + userA2ItemA.getID()))
.andExpect(status().isNotFound());
}
@Test
@SuppressWarnings("unchecked")
public void itemAdminSetPropertyCommunityAdminDeleteBitstreamToFalseTest() throws Exception {
context.turnOffAuthorisationSystem();
ResourcePolicyBuilder.createResourcePolicy(context)
.withAction(Constants.ADMIN)
.withUser(userA)
.withDspaceObject(itemA).build();
configurationService.setProperty("core.authorization.community-admin.item.delete-bitstream", false);
configurationService.setProperty("core.authorization.collection-admin.item.create-bitstream", false);
configurationService.setProperty("core.authorization.collection-admin.item.delete-bitstream", false);
configurationService.setProperty("core.authorization.item-admin.delete-bitstream", false);
configurationService.setProperty("core.authorization.item-admin.create-bitstream", false);
context.restoreAuthSystemState();
ItemRest itemRestA = itemConverter.convert(itemA, DefaultProjection.DEFAULT);
ItemRest itemRestB = itemConverter.convert(itemB, DefaultProjection.DEFAULT);
String tokenAdmin = getAuthToken(admin.getEmail(), password);
String tokenAUser = getAuthToken(userA.getEmail(), password);
String tokenComAdmin = getAuthToken(userComAdmin.getEmail(), password);
String tokenColAadmin = getAuthToken(userColAadmin.getEmail(), password);
String tokenColBadmin = getAuthToken(userColBadmin.getEmail(), password);
// define authorizations that we know must exists
Authorization admin2ItemA = new Authorization(admin, canManageBitstreamBundlesFeature, itemRestA);
Authorization admin2ItemB = new Authorization(admin, canManageBitstreamBundlesFeature, itemRestB);
// define authorization that we know not exists
Authorization comAdmin2ItemA = new Authorization(userComAdmin, canManageBitstreamBundlesFeature, itemRestA);
Authorization comAdmin2ItemB = new Authorization(userComAdmin, canManageBitstreamBundlesFeature, itemRestB);
Authorization colAadmin2ItemA = new Authorization(userColAadmin, canManageBitstreamBundlesFeature, itemRestA);
Authorization colBadmin2ItemB = new Authorization(userColBadmin, canManageBitstreamBundlesFeature, itemRestA);
Authorization userA2ItemA = new Authorization(userA, canManageBitstreamBundlesFeature, itemRestA);
getClient(tokenAdmin).perform(get("/api/authz/authorizations/" + admin2ItemA.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(admin2ItemA))));
getClient(tokenAdmin).perform(get("/api/authz/authorizations/" + admin2ItemB.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(admin2ItemB))));
getClient(tokenComAdmin).perform(get("/api/authz/authorizations/" + comAdmin2ItemA.getID()))
.andExpect(status().isNotFound());
getClient(tokenComAdmin).perform(get("/api/authz/authorizations/" + comAdmin2ItemB.getID()))
.andExpect(status().isNotFound());
getClient(tokenColAadmin).perform(get("/api/authz/authorizations/" + colAadmin2ItemA.getID()))
.andExpect(status().isNotFound());
getClient(tokenColBadmin).perform(get("/api/authz/authorizations/" + colBadmin2ItemB.getID()))
.andExpect(status().isNotFound());
getClient(tokenAUser).perform(get("/api/authz/authorizations/" + userA2ItemA.getID()))
.andExpect(status().isNotFound());
}
}

View File

@@ -17,17 +17,24 @@ import java.io.InputStream;
import org.apache.commons.codec.CharEncoding; import org.apache.commons.codec.CharEncoding;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.dspace.app.rest.authorization.impl.CanManageMappingsFeature;
import org.dspace.app.rest.converter.BitstreamConverter; import org.dspace.app.rest.converter.BitstreamConverter;
import org.dspace.app.rest.converter.CollectionConverter; import org.dspace.app.rest.converter.CollectionConverter;
import org.dspace.app.rest.converter.ItemConverter;
import org.dspace.app.rest.matcher.AuthorizationMatcher;
import org.dspace.app.rest.model.BitstreamRest; import org.dspace.app.rest.model.BitstreamRest;
import org.dspace.app.rest.model.CollectionRest; import org.dspace.app.rest.model.CollectionRest;
import org.dspace.app.rest.model.ItemRest;
import org.dspace.app.rest.projection.DefaultProjection;
import org.dspace.app.rest.projection.Projection; import org.dspace.app.rest.projection.Projection;
import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.app.rest.test.AbstractControllerIntegrationTest;
import org.dspace.app.rest.utils.Utils; import org.dspace.app.rest.utils.Utils;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.builder.BitstreamBuilder; import org.dspace.builder.BitstreamBuilder;
import org.dspace.builder.BundleBuilder; import org.dspace.builder.BundleBuilder;
import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CollectionBuilder;
import org.dspace.builder.CommunityBuilder; import org.dspace.builder.CommunityBuilder;
import org.dspace.builder.EPersonBuilder;
import org.dspace.builder.ItemBuilder; import org.dspace.builder.ItemBuilder;
import org.dspace.builder.ResourcePolicyBuilder; import org.dspace.builder.ResourcePolicyBuilder;
import org.dspace.content.Bitstream; import org.dspace.content.Bitstream;
@@ -36,14 +43,16 @@ import org.dspace.content.Collection;
import org.dspace.content.Community; import org.dspace.content.Community;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.core.Constants; import org.dspace.core.Constants;
import org.dspace.eperson.EPerson;
import org.hamcrest.Matchers;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
/** /**
* Test for the canManageMappedItems authorization feature * Test for the canManageMappings authorization feature.
*/ */
public class ManageMappedItemsFeatureIT extends AbstractControllerIntegrationTest { public class CanManageMappingsFeatureIT extends AbstractControllerIntegrationTest {
@Autowired @Autowired
private Utils utils; private Utils utils;
@@ -54,15 +63,27 @@ public class ManageMappedItemsFeatureIT extends AbstractControllerIntegrationTes
@Autowired @Autowired
private BitstreamConverter bitstreamConverter; private BitstreamConverter bitstreamConverter;
@Autowired
private ItemConverter itemConverter;
@Autowired
private AuthorizeService authorizeService;
@Autowired
private AuthorizationFeatureService authorizationFeatureService;
private EPerson userA;
private Community communityA; private Community communityA;
private Collection collectionA; private Collection collectionA;
private Collection collectionB;
private CollectionRest collectionARest; private CollectionRest collectionARest;
private Item itemA; private Item itemA;
private Bitstream bitstreamA; private Bitstream bitstreamA;
private BitstreamRest bitstreamARest; private BitstreamRest bitstreamARest;
private Bundle bundleA; private Bundle bundleA;
private AuthorizationFeature canManageMappingsFeature;
final String feature = "canManageMappedItems"; final String feature = "canManageMappings";
@Override @Override
@Before @Before
@@ -70,12 +91,19 @@ public class ManageMappedItemsFeatureIT extends AbstractControllerIntegrationTes
super.setUp(); super.setUp();
context.turnOffAuthorisationSystem(); context.turnOffAuthorisationSystem();
userA = EPersonBuilder.createEPerson(context)
.withEmail("userEmail@test.com")
.withPassword(password).build();
communityA = CommunityBuilder.createCommunity(context) communityA = CommunityBuilder.createCommunity(context)
.withName("communityA") .withName("communityA")
.build(); .build();
collectionA = CollectionBuilder.createCollection(context, communityA) collectionA = CollectionBuilder.createCollection(context, communityA)
.withName("collectionA") .withName("collectionA")
.build(); .build();
collectionB = CollectionBuilder.createCollection(context, communityA)
.withName("collectionB")
.build();
itemA = ItemBuilder.createItem(context, collectionA) itemA = ItemBuilder.createItem(context, collectionA)
.withTitle("itemA") .withTitle("itemA")
.build(); .build();
@@ -88,7 +116,7 @@ public class ManageMappedItemsFeatureIT extends AbstractControllerIntegrationTes
.withName("bistreamA") .withName("bistreamA")
.build(); .build();
} }
canManageMappingsFeature = authorizationFeatureService.find(CanManageMappingsFeature.NAME);
context.restoreAuthSystemState(); context.restoreAuthSystemState();
collectionARest = collectionConverter.convert(collectionA, Projection.DEFAULT); collectionARest = collectionConverter.convert(collectionA, Projection.DEFAULT);
@@ -188,4 +216,65 @@ public class ManageMappedItemsFeatureIT extends AbstractControllerIntegrationTes
.andExpect(jsonPath("$.page.totalElements", is(0))) .andExpect(jsonPath("$.page.totalElements", is(0)))
.andExpect(jsonPath("$._embedded").doesNotExist()); .andExpect(jsonPath("$._embedded").doesNotExist());
} }
@Test
@SuppressWarnings("unchecked")
public void canManageMappingsWithUserThatCanManageTwoCollectionsTest() throws Exception {
context.turnOffAuthorisationSystem();
authorizeService.addPolicy(context, collectionA, Constants.ADD, userA);
authorizeService.addPolicy(context, collectionB, Constants.ADD, userA);
context.restoreAuthSystemState();
ItemRest itemRestA = itemConverter.convert(itemA, DefaultProjection.DEFAULT);
String tokenAdmin = getAuthToken(admin.getEmail(), password);
String tokenAUser = getAuthToken(userA.getEmail(), password);
String tokenEPerson = getAuthToken(eperson.getEmail(), password);
// define authorizations that we know must exists
Authorization admin2ItemA = new Authorization(admin, canManageMappingsFeature, itemRestA);
Authorization userA2ItemA = new Authorization(userA, canManageMappingsFeature, itemRestA);
// define authorization that we know not exists
Authorization eperson2ItemA = new Authorization(eperson, canManageMappingsFeature, itemRestA);
Authorization anonymous2ItemA = new Authorization(null, canManageMappingsFeature, itemRestA);
getClient(tokenAdmin).perform(get("/api/authz/authorizations/" + admin2ItemA.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(admin2ItemA))));
getClient(tokenAUser).perform(get("/api/authz/authorizations/" + userA2ItemA.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(userA2ItemA))));
getClient(tokenEPerson).perform(get("/api/authz/authorizations/" + eperson2ItemA.getID()))
.andExpect(status().isNotFound());
getClient().perform(get("/api/authz/authorizations/" + anonymous2ItemA.getID()))
.andExpect(status().isNotFound());
}
@Test
@SuppressWarnings("unchecked")
public void canManageMappingsOnlyAdminHasAccessTest() throws Exception {
ItemRest itemRestA = itemConverter.convert(itemA, DefaultProjection.DEFAULT);
String tokenAdmin = getAuthToken(admin.getEmail(), password);
String tokenAUser = getAuthToken(userA.getEmail(), password);
// define authorizations that we know must exists
Authorization admin2ItemA = new Authorization(admin, canManageMappingsFeature, itemRestA);
// define authorization that we know not exists
Authorization userA2ItemA = new Authorization(userA, canManageMappingsFeature, itemRestA);
getClient(tokenAdmin).perform(get("/api/authz/authorizations/" + admin2ItemA.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(admin2ItemA))));
getClient(tokenAUser).perform(get("/api/authz/authorizations/" + userA2ItemA.getID()))
.andExpect(status().isNotFound());
}
} }

View File

@@ -0,0 +1,213 @@
/**
* 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.app.rest.authorization;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.dspace.app.rest.authorization.impl.CanManageRelationshipsFeature;
import org.dspace.app.rest.converter.ItemConverter;
import org.dspace.app.rest.matcher.AuthorizationMatcher;
import org.dspace.app.rest.model.ItemRest;
import org.dspace.app.rest.projection.DefaultProjection;
import org.dspace.app.rest.test.AbstractControllerIntegrationTest;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.builder.CollectionBuilder;
import org.dspace.builder.CommunityBuilder;
import org.dspace.builder.EPersonBuilder;
import org.dspace.builder.ItemBuilder;
import org.dspace.content.Collection;
import org.dspace.content.Community;
import org.dspace.content.Item;
import org.dspace.core.Constants;
import org.dspace.eperson.EPerson;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Test for the canManageRelationships authorization feature.
*
* @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.it)
*/
public class CanManageRelationshipsFeatureIT extends AbstractControllerIntegrationTest {
@Autowired
private ItemConverter itemConverter;
@Autowired
private AuthorizeService authorizeService;
@Autowired
private AuthorizationFeatureService authorizationFeatureService;
private Item itemA;
private Item itemB;
private EPerson userA;
private EPerson userB;
private EPerson userColAadmin;
private EPerson userColBadmin;
private EPerson userComAdmin;
private Community communityA;
private Collection collectionA;
private Collection collectionB;
private AuthorizationFeature canManageRelationshipsFeature;
final String feature = "canManageRelationships";
@Before
@Override
public void setUp() throws Exception {
super.setUp();
context.turnOffAuthorisationSystem();
canManageRelationshipsFeature = authorizationFeatureService.find(CanManageRelationshipsFeature.NAME);
userA = EPersonBuilder.createEPerson(context)
.withEmail("userEmail@test.com")
.withPassword(password).build();
userB = EPersonBuilder.createEPerson(context)
.withEmail("userB.email@test.com")
.withPassword(password).build();
userColAadmin = EPersonBuilder.createEPerson(context)
.withEmail("userColAadmin@test.com")
.withPassword(password).build();
userColBadmin = EPersonBuilder.createEPerson(context)
.withEmail("userColBadmin@test.com")
.withPassword(password).build();
userComAdmin = EPersonBuilder.createEPerson(context)
.withEmail("userComAdmin@test.com")
.withPassword(password).build();
communityA = CommunityBuilder.createCommunity(context)
.withName("communityA")
.withAdminGroup(userComAdmin).build();
collectionA = CollectionBuilder.createCollection(context, communityA)
.withName("Collection A")
.withAdminGroup(userColAadmin).build();
collectionB = CollectionBuilder.createCollection(context, communityA)
.withName("Collection B")
.withAdminGroup(userColBadmin).build();
itemA = ItemBuilder.createItem(context, collectionA)
.withTitle("Item A").build();
itemB = ItemBuilder.createItem(context, collectionB)
.withTitle("Item B").build();
context.restoreAuthSystemState();
}
@Test
@SuppressWarnings("unchecked")
public void canManageRelationshipsFeatureTest() throws Exception {
context.turnOffAuthorisationSystem();
// permissions for userA
authorizeService.addPolicy(context, itemA, Constants.WRITE, userA);
// permissions for userB
authorizeService.addPolicy(context, itemB, Constants.WRITE, userB);
context.restoreAuthSystemState();
ItemRest itemRestA = itemConverter.convert(itemA, DefaultProjection.DEFAULT);
ItemRest itemRestB = itemConverter.convert(itemB, DefaultProjection.DEFAULT);
String tokenAdmin = getAuthToken(admin.getEmail(), password);
String tokenComAdmin = getAuthToken(userComAdmin.getEmail(), password);
String tokenColAadmin = getAuthToken(userColAadmin.getEmail(), password);
String tokenColBadmin = getAuthToken(userColBadmin.getEmail(), password);
String tokenAUser = getAuthToken(userA.getEmail(), password);
String tokenBUser = getAuthToken(userB.getEmail(), password);
String tokenEPerson = getAuthToken(eperson.getEmail(), password);
// define authorizations that we know must exists
Authorization admin2ItemA = new Authorization(admin, canManageRelationshipsFeature, itemRestA);
Authorization admin2ItemB = new Authorization(admin, canManageRelationshipsFeature, itemRestB);
Authorization comAdmin2ItemA = new Authorization(userComAdmin, canManageRelationshipsFeature, itemRestA);
Authorization comAdmin2ItemB = new Authorization(userComAdmin, canManageRelationshipsFeature, itemRestB);
Authorization colAadmin2ItemA = new Authorization(userColAadmin, canManageRelationshipsFeature, itemRestA);
Authorization colBadmin2ItemB = new Authorization(userColBadmin, canManageRelationshipsFeature, itemRestB);
Authorization userA2ItemA = new Authorization(userA, canManageRelationshipsFeature, itemRestA);
Authorization userB2ItemB = new Authorization(userB, canManageRelationshipsFeature, itemRestB);
// define authorization that we know not exists
Authorization userB2ItemA = new Authorization(userB, canManageRelationshipsFeature, itemRestA);
Authorization userA2ItemB = new Authorization(userA, canManageRelationshipsFeature, itemRestB);
Authorization eperson2ItemA = new Authorization(eperson, canManageRelationshipsFeature, itemRestA);
Authorization eperson2ItemB = new Authorization(eperson, canManageRelationshipsFeature, itemRestB);
Authorization anonymous2ItemA = new Authorization(null, canManageRelationshipsFeature, itemRestA);
Authorization anonymous2ItemB = new Authorization(null, canManageRelationshipsFeature, itemRestB);
Authorization colAadmin2ItemB = new Authorization(userColAadmin, canManageRelationshipsFeature, itemRestB);
Authorization colBadmin2ItemA = new Authorization(userColBadmin, canManageRelationshipsFeature, itemRestA);
getClient(tokenAdmin).perform(get("/api/authz/authorizations/" + admin2ItemA.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(admin2ItemA))));
getClient(tokenAdmin).perform(get("/api/authz/authorizations/" + admin2ItemB.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(admin2ItemB))));
getClient(tokenComAdmin).perform(get("/api/authz/authorizations/" + comAdmin2ItemA.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(comAdmin2ItemA))));
getClient(tokenComAdmin).perform(get("/api/authz/authorizations/" + comAdmin2ItemB.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(comAdmin2ItemB))));
getClient(tokenColAadmin).perform(get("/api/authz/authorizations/" + colAadmin2ItemA.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(colAadmin2ItemA))));
getClient(tokenColBadmin).perform(get("/api/authz/authorizations/" + colBadmin2ItemB.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(colBadmin2ItemB))));
getClient(tokenAUser).perform(get("/api/authz/authorizations/" + userA2ItemA.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(userA2ItemA))));
getClient(tokenBUser).perform(get("/api/authz/authorizations/" + userB2ItemB.getID()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(userB2ItemB))));
getClient(tokenColAadmin).perform(get("/api/authz/authorizations/" + colAadmin2ItemB.getID()))
.andExpect(status().isNotFound());
getClient(tokenColBadmin).perform(get("/api/authz/authorizations/" + colBadmin2ItemA.getID()))
.andExpect(status().isNotFound());
getClient(tokenBUser).perform(get("/api/authz/authorizations/" + userB2ItemA.getID()))
.andExpect(status().isNotFound());
getClient(tokenAUser).perform(get("/api/authz/authorizations/" + userA2ItemB.getID()))
.andExpect(status().isNotFound());
getClient(tokenEPerson).perform(get("/api/authz/authorizations/" + eperson2ItemA.getID()))
.andExpect(status().isNotFound());
getClient(tokenEPerson).perform(get("/api/authz/authorizations/" + eperson2ItemB.getID()))
.andExpect(status().isNotFound());
getClient().perform(get("/api/authz/authorizations/" + anonymous2ItemA.getID()))
.andExpect(status().isNotFound());
getClient().perform(get("/api/authz/authorizations/" + anonymous2ItemB.getID()))
.andExpect(status().isNotFound());
}
}

View File

@@ -0,0 +1,154 @@
/**
* 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.app.rest.authorization;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.dspace.app.rest.converter.ItemConverter;
import org.dspace.app.rest.model.ItemRest;
import org.dspace.app.rest.projection.Projection;
import org.dspace.app.rest.test.AbstractControllerIntegrationTest;
import org.dspace.app.rest.utils.Utils;
import org.dspace.builder.CollectionBuilder;
import org.dspace.builder.CommunityBuilder;
import org.dspace.builder.EPersonBuilder;
import org.dspace.builder.ItemBuilder;
import org.dspace.content.Collection;
import org.dspace.content.Community;
import org.dspace.content.Item;
import org.dspace.eperson.EPerson;
import org.dspace.services.ConfigurationService;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Test for the canManageVersions authorization feature.
*
* @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.it)
*/
public class CanManageVersionsFeatureIT extends AbstractControllerIntegrationTest {
@Autowired
private Utils utils;
@Autowired
private ItemConverter itemConverter;
@Autowired
private ConfigurationService configurationService;
private Item itemA;
private EPerson user;
private ItemRest itemARest;
private Community communityA;
private Collection collectionA;
final String feature = "canManageVersions";
@Override
@Before
public void setUp() throws Exception {
super.setUp();
context.turnOffAuthorisationSystem();
user = EPersonBuilder.createEPerson(context)
.withEmail("userEmail@test.com")
.withPassword(password).build();
communityA = CommunityBuilder.createCommunity(context)
.withName("communityA").build();
collectionA = CollectionBuilder.createCollection(context, communityA)
.withName("collectionA").build();
itemA = ItemBuilder.createItem(context, collectionA)
.withTitle("itemA").build();
context.restoreAuthSystemState();
itemARest = itemConverter.convert(itemA, Projection.DEFAULT);
}
@Test
public void anonymousHasNotAccessTest() throws Exception {
getClient().perform(get("/api/authz/authorizations/search/object")
.param("embed", "feature")
.param("feature", feature)
.param("uri", utils.linkToSingleResource(itemARest, "self").getHref()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.page.totalElements", is(0)))
.andExpect(jsonPath("$._embedded").doesNotExist());
}
@Test
public void epersonHasNotAccessTest() throws Exception {
String epersonToken = getAuthToken(eperson.getEmail(), password);
getClient(epersonToken).perform(get("/api/authz/authorizations/search/object")
.param("embed", "feature")
.param("feature", feature)
.param("uri", utils.linkToSingleResource(itemARest, "self").getHref()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.page.totalElements", is(0)))
.andExpect(jsonPath("$._embedded").doesNotExist());
}
@Test
public void adminItemSuccessTest() throws Exception {
String adminToken = getAuthToken(admin.getEmail(), password);
getClient(adminToken).perform(get("/api/authz/authorizations/search/object")
.param("embed", "feature")
.param("feature", feature)
.param("uri", utils.linkToSingleResource(itemARest, "self").getHref()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.page.totalElements", greaterThan(0)))
.andExpect(jsonPath("$._embedded").exists());
}
@Test
public void submitterItemSuccessTest() throws Exception {
context.turnOffAuthorisationSystem();
configurationService.setProperty("versioning.submitterCanCreateNewVersion", true);
itemA.setSubmitter(user);
context.restoreAuthSystemState();
String userToken = getAuthToken(user.getEmail(), password);
getClient(userToken).perform(get("/api/authz/authorizations/search/object")
.param("embed", "feature")
.param("feature", feature)
.param("uri", utils.linkToSingleResource(itemARest, "self").getHref()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.page.totalElements", greaterThan(0)))
.andExpect(jsonPath("$._embedded").exists());
}
@Test
public void submitterItemWithPropertySubmitterCanCreateNewVersionIsFalseTest() throws Exception {
context.turnOffAuthorisationSystem();
configurationService.setProperty("versioning.submitterCanCreateNewVersion", false);
itemA.setSubmitter(user);
context.restoreAuthSystemState();
String userToken = getAuthToken(user.getEmail(), password);
getClient(userToken).perform(get("/api/authz/authorizations/search/object")
.param("embed", "feature")
.param("feature", feature)
.param("uri", utils.linkToSingleResource(itemARest, "self").getHref()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.page.totalElements", is(0)))
.andExpect(jsonPath("$._embedded").doesNotExist());
}
}

View File

@@ -41,9 +41,9 @@ import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
/** /**
* Test for the canViewVersions authorization feature * Test for the canSeeVersions authorization feature
*/ */
public class ViewVersionsFeatureIT extends AbstractControllerIntegrationTest { public class CanSeeVersionsFeatureIT extends AbstractControllerIntegrationTest {
@Autowired @Autowired
private Utils utils; private Utils utils;
@@ -68,7 +68,7 @@ public class ViewVersionsFeatureIT extends AbstractControllerIntegrationTest {
private BitstreamRest bitstreamARest; private BitstreamRest bitstreamARest;
private Bundle bundleA; private Bundle bundleA;
final String feature = "canViewVersions"; final String feature = "canSeeVersions";
@Override @Override
@Before @Before

View File

@@ -99,9 +99,11 @@ public class DiscoverConfigurationConverterTest {
DiscoverySortFieldConfiguration discoverySortFieldConfiguration = new DiscoverySortFieldConfiguration(); DiscoverySortFieldConfiguration discoverySortFieldConfiguration = new DiscoverySortFieldConfiguration();
discoverySortFieldConfiguration.setMetadataField("title"); discoverySortFieldConfiguration.setMetadataField("title");
discoverySortFieldConfiguration.setType("text"); discoverySortFieldConfiguration.setType("text");
discoverySortFieldConfiguration.setDefaultSortOrder(DiscoverySortFieldConfiguration.SORT_ORDER.asc);
DiscoverySortFieldConfiguration discoverySortFieldConfiguration1 = new DiscoverySortFieldConfiguration(); DiscoverySortFieldConfiguration discoverySortFieldConfiguration1 = new DiscoverySortFieldConfiguration();
discoverySortFieldConfiguration1.setMetadataField("author"); discoverySortFieldConfiguration1.setMetadataField("author");
discoverySortFieldConfiguration1.setType("text"); discoverySortFieldConfiguration1.setType("text");
discoverySortFieldConfiguration1.setDefaultSortOrder(DiscoverySortFieldConfiguration.SORT_ORDER.asc);
LinkedList<DiscoverySortFieldConfiguration> mockedList = new LinkedList<>(); LinkedList<DiscoverySortFieldConfiguration> mockedList = new LinkedList<>();
mockedList.add(discoverySortFieldConfiguration); mockedList.add(discoverySortFieldConfiguration);
mockedList.add(discoverySortFieldConfiguration1); mockedList.add(discoverySortFieldConfiguration1);

View File

@@ -48,4 +48,10 @@ public class SortOptionMatcher {
); );
} }
public static Matcher<? super Object> sortOptionMatcher(String name, String sortDirection) {
return allOf(
hasJsonPath("$.name", is(name)),
hasJsonPath("$.sortOrder", is(sortDirection))
);
}
} }

View File

@@ -86,7 +86,7 @@ public class EmbeddedPageHeaderTest {
Map<String,Object> links = embeddedPageHeader.getLinks(); Map<String,Object> links = embeddedPageHeader.getLinks();
// "self" should be same as URL // "self" should be same as URL
assertEquals(dspaceURL, ((EmbeddedPageHeader.Href) links.get("self")).getHref()); assertEquals(dspaceURL + "?size=10", ((EmbeddedPageHeader.Href) links.get("self")).getHref());
// "first" should not exist, as we are on the first page. // "first" should not exist, as we are on the first page.
assertFalse(links.containsKey("first")); assertFalse(links.containsKey("first"));
// "prev" should not exist, as we are on the first page. // "prev" should not exist, as we are on the first page.

View File

@@ -28,6 +28,7 @@ import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
@@ -35,6 +36,7 @@ import java.util.List;
import java.util.function.Function; import java.util.function.Function;
import org.dspace.app.rest.exception.DSpaceBadRequestException; import org.dspace.app.rest.exception.DSpaceBadRequestException;
import org.dspace.app.rest.exception.InvalidSearchRequestException;
import org.dspace.app.rest.parameter.SearchFilter; import org.dspace.app.rest.parameter.SearchFilter;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.discovery.DiscoverFacetField; import org.dspace.discovery.DiscoverFacetField;
@@ -111,7 +113,8 @@ public class DiscoverQueryBuilderTest {
any(), any(DiscoverQuery.class))) any(), any(DiscoverQuery.class)))
.then(invocation -> new FacetYearRange((DiscoverySearchFilterFacet) invocation.getArguments()[2])); .then(invocation -> new FacetYearRange((DiscoverySearchFilterFacet) invocation.getArguments()[2]));
when(searchService.toFilterQuery(any(Context.class), any(String.class), any(String.class), any(String.class))) when(searchService.toFilterQuery(any(Context.class), any(String.class), any(String.class), any(String.class),
any(DiscoveryConfiguration.class)))
.then(invocation -> new DiscoverFilterQuery((String) invocation.getArguments()[1], .then(invocation -> new DiscoverFilterQuery((String) invocation.getArguments()[1],
invocation.getArguments()[1] + ":\"" + invocation.getArguments()[3] + "\"", invocation.getArguments()[1] + ":\"" + invocation.getArguments()[3] + "\"",
(String) invocation.getArguments()[3])); (String) invocation.getArguments()[3]));
@@ -139,17 +142,23 @@ public class DiscoverQueryBuilderTest {
discoveryConfiguration.setHitHighlightingConfiguration(discoveryHitHighlightingConfiguration); discoveryConfiguration.setHitHighlightingConfiguration(discoveryHitHighlightingConfiguration);
DiscoverySortConfiguration sortConfiguration = new DiscoverySortConfiguration();
DiscoverySortFieldConfiguration defaultSort = new DiscoverySortFieldConfiguration(); DiscoverySortFieldConfiguration defaultSort = new DiscoverySortFieldConfiguration();
defaultSort.setMetadataField("dc.date.accessioned"); defaultSort.setMetadataField("dc.date.accessioned");
defaultSort.setType(DiscoveryConfigurationParameters.TYPE_DATE); defaultSort.setType(DiscoveryConfigurationParameters.TYPE_DATE);
sortConfiguration.setDefaultSort(defaultSort); defaultSort.setDefaultSortOrder(DiscoverySortFieldConfiguration.SORT_ORDER.desc);
sortConfiguration.setDefaultSortOrder(DiscoverySortConfiguration.SORT_ORDER.desc);
List<DiscoverySortFieldConfiguration> listSortField = new ArrayList<DiscoverySortFieldConfiguration>();
listSortField.add(defaultSort);
DiscoverySortConfiguration sortConfiguration = new DiscoverySortConfiguration();
DiscoverySortFieldConfiguration titleSort = new DiscoverySortFieldConfiguration(); DiscoverySortFieldConfiguration titleSort = new DiscoverySortFieldConfiguration();
titleSort.setMetadataField("dc.title"); titleSort.setMetadataField("dc.title");
sortConfiguration.setSortFields(Arrays.asList(titleSort)); titleSort.setDefaultSortOrder(DiscoverySortFieldConfiguration.SORT_ORDER.asc);
listSortField.add(titleSort);
sortConfiguration.setSortFields(listSortField);
discoveryConfiguration.setSearchSortConfiguration(sortConfiguration); discoveryConfiguration.setSearchSortConfiguration(sortConfiguration);
@@ -266,7 +275,7 @@ public class DiscoverQueryBuilderTest {
.buildQuery(context, scope, discoveryConfiguration, query, Arrays.asList(searchFilter), "TEST", page); .buildQuery(context, scope, discoveryConfiguration, query, Arrays.asList(searchFilter), "TEST", page);
} }
@Test(expected = DSpaceBadRequestException.class) @Test(expected = InvalidSearchRequestException.class)
public void testInvalidSortField() throws Exception { public void testInvalidSortField() throws Exception {
page = PageRequest.of(2, 10, Sort.Direction.ASC, "test"); page = PageRequest.of(2, 10, Sort.Direction.ASC, "test");
queryBuilder queryBuilder
@@ -283,7 +292,8 @@ public class DiscoverQueryBuilderTest {
@Test(expected = DSpaceBadRequestException.class) @Test(expected = DSpaceBadRequestException.class)
public void testInvalidSearchFilter2() throws Exception { public void testInvalidSearchFilter2() throws Exception {
when(searchService.toFilterQuery(any(Context.class), any(String.class), any(String.class), any(String.class))) when(searchService.toFilterQuery(any(Context.class), any(String.class), any(String.class), any(String.class),
any(DiscoveryConfiguration.class)))
.thenThrow(SQLException.class); .thenThrow(SQLException.class);
queryBuilder queryBuilder

View File

@@ -9,7 +9,7 @@
<parent> <parent>
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<artifactId>dspace-parent</artifactId> <artifactId>dspace-parent</artifactId>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
</parent> </parent>
<properties> <properties>

View File

@@ -426,10 +426,9 @@ public final class DSpaceServiceManager implements ServiceManagerSystem {
service = (T) applicationContext.getBean(name, type); service = (T) applicationContext.getBean(name, type);
} catch (BeansException e) { } catch (BeansException e) {
// no luck, try the fall back option // no luck, try the fall back option
log.info( log.warn(
"Unable to locate bean by name or id={}." "Unable to locate bean by name or id={}."
+ " Will try to look up bean by type next." + " Will try to look up bean by type next.", name, e);
+ " BeansException: {}", name, e.getMessage());
service = null; service = null;
} }
} else { } else {
@@ -438,9 +437,8 @@ public final class DSpaceServiceManager implements ServiceManagerSystem {
service = (T) applicationContext.getBean(type.getName(), type); service = (T) applicationContext.getBean(type.getName(), type);
} catch (BeansException e) { } catch (BeansException e) {
// no luck, try the fall back option // no luck, try the fall back option
log.info("Unable to locate bean by name or id={}." log.warn("Unable to locate bean by name or id={}."
+ " Will try to look up bean by type next." + " Will try to look up bean by type next.", type.getName(), e);
+ " BeansException: {}", type.getName(), e.getMessage());
service = null; service = null;
} }
} }

View File

@@ -15,7 +15,7 @@
<parent> <parent>
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<artifactId>dspace-parent</artifactId> <artifactId>dspace-parent</artifactId>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
<relativePath>..</relativePath> <relativePath>..</relativePath>
</parent> </parent>

View File

@@ -13,7 +13,7 @@
<parent> <parent>
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<artifactId>dspace-parent</artifactId> <artifactId>dspace-parent</artifactId>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
<relativePath>..</relativePath> <relativePath>..</relativePath>
</parent> </parent>

View File

@@ -20,11 +20,15 @@
dspace.dir = /dspace dspace.dir = /dspace
# URL of DSpace backend ('server' webapp). Include port number etc. # 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 dspace.server.url = http://localhost:8080/server
# URL of DSpace frontend (Angular UI). Include port number etc # 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. # 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 dspace.ui.url = http://localhost:4000
# Name of the site # Name of the site
@@ -120,8 +124,8 @@ db.removeabandonedtimeout = 300
mail.server = smtp.example.com mail.server = smtp.example.com
# SMTP mail server authentication username and password (if required) # SMTP mail server authentication username and password (if required)
mail.server.username = #mail.server.username =
mail.server.password = #mail.server.password =
# SMTP mail server alternate port (defaults to 25) # SMTP mail server alternate port (defaults to 25)
mail.server.port = 25 mail.server.port = 25
@@ -371,16 +375,6 @@ useProxies = true
# (Requires reboot of servlet container, e.g. Tomcat, to reload) # (Requires reboot of servlet container, e.g. Tomcat, to reload)
#proxies.trusted.include_ui_ip = true #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 Filter / Format Filter plugins (through PluginService) ####
# Media/Format Filters help to full-text index content or # Media/Format Filters help to full-text index content or
# perform automated format conversions # perform automated format conversions

View File

@@ -89,7 +89,7 @@
<min>0</min> <min>0</min>
</leftCardinality> </leftCardinality>
<rightCardinality> <rightCardinality>
<min>1</min> <min>0</min>
</rightCardinality> </rightCardinality>
</type> </type>
<type> <type>
@@ -101,8 +101,7 @@
<min>0</min> <min>0</min>
</leftCardinality> </leftCardinality>
<rightCardinality> <rightCardinality>
<min>1</min> <min>0</min>
<max>1</max>
</rightCardinality> </rightCardinality>
</type> </type>
<type> <type>
@@ -128,7 +127,6 @@
</leftCardinality> </leftCardinality>
<rightCardinality> <rightCardinality>
<min>0</min> <min>0</min>
<max>1</max>
</rightCardinality> </rightCardinality>
<copyToRight>true</copyToRight> <copyToRight>true</copyToRight>
</type> </type>

View File

@@ -17,7 +17,36 @@
<!-- which does not appear in this map will be associated with the mapping --> <!-- which does not appear in this map will be associated with the mapping -->
<!-- for handle "default". --> <!-- for handle "default". -->
<submission-map> <submission-map>
<!-- Default submission process -->
<name-map collection-handle="default" submission-name="traditional"/> <name-map collection-handle="default" submission-name="traditional"/>
<!-- Sample Entities Collection configuration based on the demo Entities dataset available at:
https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
This sets up the following Entity-based Collections in that dataset:
"Publication" Collections = "Publications" -> "Articles", "Books", "Publications 2", & "Theses"
"OrgUnit" Collection = "Related Objects" -> "OrgUnits"
"Person" Collections = "Related Objects" -> "People" & "People 2"
"Project" Collections = "Related Objects" -> "Projects" & "Projects 2"
"Journal" Collection = "Compound Journals" -> "Journals"
"Journal Volume" Collection = "Compound Journals" -> "Journal Volumes"
"Journal Issue" Collection = "Compound Journals" -> "Journal Issues"
If you are using that demo dataset, you can simply uncomment the below configuration to enable Entity
submission into all of the above Collections.
-->
<!--
<name-map collection-handle="123456789/3" submission-name="Publication"/>
<name-map collection-handle="123456789/4" submission-name="Publication"/>
<name-map collection-handle="123456789/281" submission-name="Publication"/>
<name-map collection-handle="123456789/5" submission-name="Publication"/>
<name-map collection-handle="123456789/8" submission-name="OrgUnit"/>
<name-map collection-handle="123456789/6" submission-name="Person"/>
<name-map collection-handle="123456789/279" submission-name="Person"/>
<name-map collection-handle="123456789/7" submission-name="Project"/>
<name-map collection-handle="123456789/280" submission-name="Project"/>
<name-map collection-handle="123456789/28" submission-name="Journal"/>
<name-map collection-handle="123456789/29" submission-name="JournalVolume"/>
<name-map collection-handle="123456789/30" submission-name="JournalIssue"/>
-->
</submission-map> </submission-map>
@@ -44,7 +73,7 @@
<!-- --> <!-- -->
<step-definitions> <step-definitions>
<!-- The "collection" step is a "special step" which is *REQUIRED* --> <!-- The "collection" step is a "special step" which is *REQUIRED* -->
<!-- In DSpace, all submitted items must be immediately assigned --> <!-- In DSpace, all submitted items must be immediately assigned -->
<!-- to a collection. This step ensures that a collection is always selected. --> <!-- to a collection. This step ensures that a collection is always selected. -->
<step-definition id="collection"> <step-definition id="collection">
<heading></heading> <heading></heading>
@@ -52,6 +81,9 @@
<type>collection</type> <type>collection</type>
<scope visibility="hidden" visibilityOutside="hidden">submission</scope> <scope visibility="hidden" visibilityOutside="hidden">submission</scope>
</step-definition> </step-definition>
<!-- The following set of DescribeStep <step-definition>s all point to forms (of the same name) which are
defined in submission-forms.xml -->
<step-definition id="traditionalpageone" mandatory="true"> <step-definition id="traditionalpageone" mandatory="true">
<heading>submit.progressbar.describe.stepone</heading> <heading>submit.progressbar.describe.stepone</heading>
<processing-class>org.dspace.app.rest.submit.step.DescribeStep</processing-class> <processing-class>org.dspace.app.rest.submit.step.DescribeStep</processing-class>
@@ -62,8 +94,12 @@
<processing-class>org.dspace.app.rest.submit.step.DescribeStep</processing-class> <processing-class>org.dspace.app.rest.submit.step.DescribeStep</processing-class>
<type>submission-form</type> <type>submission-form</type>
</step-definition> </step-definition>
<step-definition id="publicationStep" mandatory="true">
<step-definition id="peopleStep" mandatory="true"> <heading>submit.progressbar.describe.stepone</heading>
<processing-class>org.dspace.app.rest.submit.step.DescribeStep</processing-class>
<type>submission-form</type>
</step-definition>
<step-definition id="personStep" mandatory="true">
<heading>submit.progressbar.describe.stepone</heading> <heading>submit.progressbar.describe.stepone</heading>
<processing-class>org.dspace.app.rest.submit.step.DescribeStep</processing-class> <processing-class>org.dspace.app.rest.submit.step.DescribeStep</processing-class>
<type>submission-form</type> <type>submission-form</type>
@@ -106,38 +142,26 @@
<scope visibilityOutside="read-only">submission</scope> <scope visibilityOutside="read-only">submission</scope>
</step-definition> </step-definition>
<!-- Step Upload Item with Embargo Features to enable this step, please <!-- This optional step may allow the user to select a Creative Commons license -->
make sure to comment-out the previous step "UploadStep" <step-definition id="upload-with-embargo"> <step-definition id="cclicense" mandatory="false">
<heading>submit.progressbar.upload</heading> <processing-class>org.dspace.submit.step.UploadWithEmbargoStep</processing-class> <heading>submit.progressbar.CClicense</heading>
<type>uploadWithEmbargo</type> </step-definition> -->
<!--Step will be to select a Creative Commons License -->
<!-- Uncomment this step to allow the user to select a Creative Commons
license -->
<!-- <step-definition id="cclicense"> <heading>submit.progressbar.CClicense</heading>
<processing-class>org.dspace.app.rest.submit.step.CCLicenseStep</processing-class> <processing-class>org.dspace.app.rest.submit.step.CCLicenseStep</processing-class>
<type>cclicense</type> </step-definition> --> <type>cclicense</type>
</step-definition>
<!--Step will be to enrich the current submission querying external providers or processing the uploaded file --> <!-- This optional step may enrich the current submission using information extracted
<!-- Uncomment this step to enrich the current submission using information extracted from uploaded files or metadata. -->
from uploaded files or metadata. Please note that this step will be triggered only when a request is performed that mean <!-- Please note that this step will be triggered only when a request is performed,
the file to be uploaded or the metadata saved. Angular allows to configure an autosave feature based on a timer or the input e.g. when a file is uploaded or the form is saved. The Angular UI also supports an
of specific metadata such as identifiers used by this step, see the submission.autosave settings in the environment.common.ts autosave feature based on a timer, or the input of specific metadata such as identifiers,
Check also config/spring/api/step-processing-listener.xml for further server side configuration see the 'submission.autosave' settings in the 'environment.common.ts'.
See also 'config/spring/api/step-processing-listener.xml' for further server side configuration
--> -->
<!-- <step-definition id="extractionstep"> <step-definition id="extractionstep">
<heading>submit.progressbar.ExtractMetadataStep</heading> <heading>submit.progressbar.ExtractMetadataStep</heading>
<processing-class>org.dspace.app.rest.submit.step.ExtractMetadataStep</processing-class> <processing-class>org.dspace.app.rest.submit.step.ExtractMetadataStep</processing-class>
<type>extract</type> <type>extract</type>
</step-definition> --> </step-definition>
<!-- Fake Steps to test parsing of all options -->
<!-- <step-definition mandatory="false"> <heading>fake.submission.readonly</heading>
<processing-class>org.dspace.submit.step.SampleStep</processing-class> <type>sample</type>
<scope visibility="read-only">submission</scope> </step-definition> <step-definition mandatory="false">
<heading>fake.workflow.readonly</heading> <processing-class>org.dspace.submit.step.SampleStep</processing-class>
<type>sample</type> <scope visibility="read-only">workflow</scope> </step-definition> -->
<!-- OpenAIRE submission steps/forms --> <!-- OpenAIRE submission steps/forms -->
<step-definition id="openAIREProjectForm" mandatory="true"> <step-definition id="openAIREProjectForm" mandatory="true">
@@ -166,7 +190,7 @@
<type>submission-form</type> <type>submission-form</type>
</step-definition> </step-definition>
<!-- This is the Sample Step which utilizes the JSPSampleStep class --> <!-- This is the Sample/Fake Step which is used for testing only -->
<step-definition id="sample"> <step-definition id="sample">
<heading>Sample</heading> <heading>Sample</heading>
<processing-class>org.dspace.submit.step.SampleStep</processing-class> <processing-class>org.dspace.submit.step.SampleStep</processing-class>
@@ -192,7 +216,6 @@
<!--This "traditional" process defines the DEFAULT item submission process --> <!--This "traditional" process defines the DEFAULT item submission process -->
<submission-process name="traditional"> <submission-process name="traditional">
<!--Uncommment to display the SAMPLE step as your first step --> <!--Uncommment to display the SAMPLE step as your first step -->
<!--<step id="sample"/> --> <!--<step id="sample"/> -->
@@ -204,26 +227,45 @@
<!--Step will be to Upload the item --> <!--Step will be to Upload the item -->
<step id="upload"/> <step id="upload"/>
<!-- <step id="upload-with-embargo"/> -->
<!-- <step id="extractionstep"/> --> <!-- <step id="extractionstep"/> -->
<!--Step will be to select a Creative Commons License --> <!-- Uncomment this step to allow the user to select a Creative Commons License -->
<!-- Uncomment this step to allow the user to select a Creative Commons -->
<!-- <step id="cclicense"/> --> <!-- <step id="cclicense"/> -->
<!--Step will be to Sign off on the License --> <!--Step will be to Sign off on the required DSpace License agreement -->
<step id="license"/> <step id="license"/>
<!-- <step id="creative-commons"/> -->
<!-- <step id="verify"/> -->
</submission-process> </submission-process>
<submission-process name="People"> <!--
Submission Process for 'Publication' Entities.
These are not to be confused with default Items, as Publications may be linked with other Entities
(e.g. Person, JournalIssue, etc).
To enable: create a collection for Publications and add it to the <submission-map> tag linking to this process
-->
<submission-process name="Publication">
<step id="collection"/> <step id="collection"/>
<step id="peopleStep"/> <!-- Publications use a custom first step, but share page 2 with "traditional" Item form -->
<step id="publicationStep"/>
<step id="traditionalpagetwo"/>
<step id="upload"/> <step id="upload"/>
<step id="license"/> <step id="license"/>
</submission-process> </submission-process>
<!--
Submission Process for 'Person' Entities
To enable: create a collection for Persons and add it to the <submission-map> tag linking to this process
-->
<submission-process name="Person">
<step id="collection"/>
<step id="personStep"/>
<step id="upload"/>
<step id="license"/>
</submission-process>
<!--
Submission Process for 'Pproject' Entities
To enable: create a collection for Projects and add it to the <submission-map> tag linking to this process
-->
<submission-process name="Project"> <submission-process name="Project">
<step id="collection"/> <step id="collection"/>
<step id="projectStep"/> <step id="projectStep"/>
@@ -231,25 +273,44 @@
<step id="license"/> <step id="license"/>
</submission-process> </submission-process>
<!--
Submission Process for 'OrgUnit' Entities
To enable: create a collection for OrgUnits and add it to the <submission-map> tag linking to this process
-->
<submission-process name="OrgUnit"> <submission-process name="OrgUnit">
<step id="collection"/> <step id="collection"/>
<step id="orgUnitStep"/> <step id="orgUnitStep"/>
<step id="upload"/> <step id="upload"/>
<step id="license"/> <step id="license"/>
</submission-process> </submission-process>
<submission-process name="Journals">
<!--
Submission Process for 'Journal' Entities
To enable: create a collection for Journals and add it to the <submission-map> tag linking to this process
-->
<submission-process name="Journal">
<step id="collection"/> <step id="collection"/>
<step id="journalStep"/> <step id="journalStep"/>
<step id="upload"/> <step id="upload"/>
<step id="license"/> <step id="license"/>
</submission-process> </submission-process>
<submission-process name="JournalVolumes">
<!--
Submission Process for 'JournalVolume' Entities
To enable: create a collection for JournalVolumes and add it to the <submission-map> tag linking to this process
-->
<submission-process name="JournalVolume">
<step id="collection"/> <step id="collection"/>
<step id="journalVolumeStep"/> <step id="journalVolumeStep"/>
<step id="upload"/> <step id="upload"/>
<step id="license"/> <step id="license"/>
</submission-process> </submission-process>
<submission-process name="JournalIssues">
<!--
Submission Process for 'JournalIssues' Entities
To enable: create a collection for JournalIssues and add it to the <submission-map> tag linking to this process
-->
<submission-process name="JournalIssue">
<step id="collection"/> <step id="collection"/>
<step id="journalIssueStep"/> <step id="journalIssueStep"/>
<step id="upload"/> <step id="upload"/>

View File

@@ -33,11 +33,15 @@
dspace.dir=/dspace dspace.dir=/dspace
# URL of DSpace backend ('server' webapp). Include port number etc. # 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 dspace.server.url = http://localhost:8080/server
# URL of DSpace frontend (Angular UI). Include port number etc # 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. # 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 dspace.ui.url = http://localhost:4000
# Name of the site # Name of the site
@@ -188,6 +192,7 @@ db.schema = public
#plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.LDAPAuthentication #plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.LDAPAuthentication
# Shibboleth authentication/authorization. See authentication-shibboleth.cfg for default configuration. # Shibboleth authentication/authorization. See authentication-shibboleth.cfg for default configuration.
# Check also the cors settings below
#plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.ShibAuthentication #plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.ShibAuthentication
# X.509 certificate authentication. See authentication-x509.cfg for default configuration. # X.509 certificate authentication. See authentication-x509.cfg for default configuration.
@@ -205,6 +210,9 @@ db.schema = public
# Defaults to ${dspace.ui.url} if unspecified (as the UI must have access to the REST API). # Defaults to ${dspace.ui.url} if unspecified (as the UI must have access to the REST API).
# Multiple allowed origin URLs may be comma separated. Wildcard value (*) is NOT SUPPORTED. # Multiple allowed origin URLs may be comma separated. Wildcard value (*) is NOT SUPPORTED.
# (Requires reboot of servlet container, e.g. Tomcat, to reload) # (Requires reboot of servlet container, e.g. Tomcat, to reload)
# When an external authentication system is involved like Shibboleth some browsers (i.e. Safari) include
# in the request the Origin header with the url of the IdP. In such case you need to allow also the IdP to
# avoid trouble for such browsers (i.e. rest.cors.allowed-origins = ${dspace.ui.url}, https://samltest.id )
#rest.cors.allowed-origins = ${dspace.ui.url} #rest.cors.allowed-origins = ${dspace.ui.url}
################################################# #################################################

View File

@@ -82,7 +82,7 @@
<Logger name='org.dspace.services' <Logger name='org.dspace.services'
level='ERROR'/> level='ERROR'/>
<Logger name='org.dspace.servicemanager' <Logger name='org.dspace.servicemanager'
level='ERROR'/> level='WARN'/>
<Logger name='org.dspace.providers' <Logger name='org.dspace.providers'
level='ERROR'/> level='ERROR'/>
<Logger name='org.dspace.utils' <Logger name='org.dspace.utils'

View File

@@ -22,6 +22,7 @@
<bean class="org.dspace.app.util.MetadataExposureServiceImpl"/> <bean class="org.dspace.app.util.MetadataExposureServiceImpl"/>
<bean class="org.dspace.app.util.OpenSearchServiceImpl"/> <bean class="org.dspace.app.util.OpenSearchServiceImpl"/>
<bean class="org.dspace.app.util.WebAppServiceImpl"/> <bean class="org.dspace.app.util.WebAppServiceImpl"/>
<bean class="org.dspace.app.util.DSpaceObjectUtilsImpl"/>
<bean class="org.dspace.authenticate.AuthenticationServiceImpl"/> <bean class="org.dspace.authenticate.AuthenticationServiceImpl"/>

View File

@@ -154,11 +154,9 @@
<!--The sort filters for the discovery search--> <!--The sort filters for the discovery search-->
<property name="searchSortConfiguration"> <property name="searchSortConfiguration">
<bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration"> <bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration">
<!--<property name="defaultSort" ref="sortDateIssued"/>-->
<!--DefaultSortOrder can either be desc or asc (desc is default)-->
<property name="defaultSortOrder" value="desc"/>
<property name="sortFields"> <property name="sortFields">
<list> <list>
<ref bean="sortScore" />
<ref bean="sortTitle" /> <ref bean="sortTitle" />
<ref bean="sortDateIssued" /> <ref bean="sortDateIssued" />
<ref bean="sortDateAccessioned"/> <ref bean="sortDateAccessioned"/>
@@ -295,11 +293,9 @@
<!--The sort filters for the discovery search--> <!--The sort filters for the discovery search-->
<property name="searchSortConfiguration"> <property name="searchSortConfiguration">
<bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration"> <bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration">
<!--<property name="defaultSort" ref="sortDateIssued"/>-->
<!--DefaultSortOrder can either be desc or asc (desc is default)-->
<property name="defaultSortOrder" value="desc"/>
<property name="sortFields"> <property name="sortFields">
<list> <list>
<ref bean="sortScore" />
<ref bean="sortTitle" /> <ref bean="sortTitle" />
<ref bean="sortDateIssued" /> <ref bean="sortDateIssued" />
<ref bean="sortDateAccessioned"/> <ref bean="sortDateAccessioned"/>
@@ -441,11 +437,9 @@
<!--The sort filters for the discovery search--> <!--The sort filters for the discovery search-->
<property name="searchSortConfiguration"> <property name="searchSortConfiguration">
<bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration"> <bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration">
<!--<property name="defaultSort" ref="sortDateIssued"/>-->
<!--DefaultSortOrder can either be desc or asc (desc is default)-->
<property name="defaultSortOrder" value="desc"/>
<property name="sortFields"> <property name="sortFields">
<list> <list>
<ref bean="sortScore" />
<ref bean="sortTitle" /> <ref bean="sortTitle" />
<ref bean="sortDateIssued" /> <ref bean="sortDateIssued" />
<ref bean="sortDateAccessioned"/> <ref bean="sortDateAccessioned"/>
@@ -581,11 +575,9 @@
<!--The sort filters for the discovery search (same as defaultConfiguration above)--> <!--The sort filters for the discovery search (same as defaultConfiguration above)-->
<property name="searchSortConfiguration"> <property name="searchSortConfiguration">
<bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration"> <bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration">
<!--<property name="defaultSort" ref="sortDateIssued"/>-->
<!--DefaultSortOrder can either be desc or asc (desc is default)-->
<property name="defaultSortOrder" value="desc"/>
<property name="sortFields"> <property name="sortFields">
<list> <list>
<ref bean="sortScore" />
<ref bean="sortTitle" /> <ref bean="sortTitle" />
<ref bean="sortDateIssued" /> <ref bean="sortDateIssued" />
<ref bean="sortDateAccessioned" /> <ref bean="sortDateAccessioned" />
@@ -687,11 +679,9 @@
<!--The sort filters for the discovery search--> <!--The sort filters for the discovery search-->
<property name="searchSortConfiguration"> <property name="searchSortConfiguration">
<bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration"> <bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration">
<!--<property name="defaultSort" ref="sortDateIssued"/>-->
<!--DefaultSortOrder can either be desc or asc (desc is default)-->
<property name="defaultSortOrder" value="desc"/>
<property name="sortFields"> <property name="sortFields">
<list> <list>
<ref bean="sortScore" />
<ref bean="sortTitle" /> <ref bean="sortTitle" />
<ref bean="sortDateIssued" /> <ref bean="sortDateIssued" />
</list> </list>
@@ -764,11 +754,9 @@
<!--The sort filters for the discovery search--> <!--The sort filters for the discovery search-->
<property name="searchSortConfiguration"> <property name="searchSortConfiguration">
<bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration"> <bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration">
<!--<property name="defaultSort" ref="sortDateIssued"/>-->
<!--DefaultSortOrder can either be desc or asc (desc is default)-->
<property name="defaultSortOrder" value="desc"/>
<property name="sortFields"> <property name="sortFields">
<list> <list>
<ref bean="sortScore" />
<ref bean="sortTitle" /> <ref bean="sortTitle" />
<ref bean="sortDateIssued" /> <ref bean="sortDateIssued" />
</list> </list>
@@ -841,11 +829,9 @@
<!--The sort filters for the discovery search--> <!--The sort filters for the discovery search-->
<property name="searchSortConfiguration"> <property name="searchSortConfiguration">
<bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration"> <bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration">
<!--<property name="defaultSort" ref="sortDateIssued"/>-->
<!--DefaultSortOrder can either be desc or asc (desc is default)-->
<property name="defaultSortOrder" value="desc"/>
<property name="sortFields"> <property name="sortFields">
<list> <list>
<ref bean="sortScore" />
<ref bean="sortTitle" /> <ref bean="sortTitle" />
<ref bean="sortDateIssued" /> <ref bean="sortDateIssued" />
</list> </list>
@@ -927,11 +913,9 @@
<!--The sort filters for the discovery search--> <!--The sort filters for the discovery search-->
<property name="searchSortConfiguration"> <property name="searchSortConfiguration">
<bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration"> <bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration">
<!--<property name="defaultSort" ref="sortDateIssued"/>-->
<!--DefaultSortOrder can either be desc or asc (desc is default)-->
<property name="defaultSortOrder" value="desc"/>
<property name="sortFields"> <property name="sortFields">
<list> <list>
<ref bean="sortScore" />
<ref bean="sortTitle" /> <ref bean="sortTitle" />
<ref bean="sortDateIssued" /> <ref bean="sortDateIssued" />
<ref bean="sortDateAccessioned"/> <ref bean="sortDateAccessioned"/>
@@ -989,11 +973,9 @@
<!--The sort filters for the discovery search--> <!--The sort filters for the discovery search-->
<property name="searchSortConfiguration"> <property name="searchSortConfiguration">
<bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration"> <bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration">
<!--<property name="defaultSort" ref="sortDateIssued"/>-->
<!--DefaultSortOrder can either be desc or asc (desc is default)-->
<property name="defaultSortOrder" value="desc"/>
<property name="sortFields"> <property name="sortFields">
<list> <list>
<ref bean="sortScore" />
<ref bean="sortFamilyName"/> <ref bean="sortFamilyName"/>
<ref bean="sortGivenName"/> <ref bean="sortGivenName"/>
<ref bean="sortBirthDate"/> <ref bean="sortBirthDate"/>
@@ -1047,11 +1029,9 @@
<!--The sort filters for the discovery search--> <!--The sort filters for the discovery search-->
<property name="searchSortConfiguration"> <property name="searchSortConfiguration">
<bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration"> <bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration">
<!--<property name="defaultSort" ref="sortDateIssued"/>-->
<!--DefaultSortOrder can either be desc or asc (desc is default)-->
<property name="defaultSortOrder" value="desc"/>
<property name="sortFields"> <property name="sortFields">
<list> <list>
<ref bean="sortScore" />
<ref bean="sortTitle"/> <ref bean="sortTitle"/>
</list> </list>
</property> </property>
@@ -1107,11 +1087,9 @@
<!--The sort filters for the discovery search--> <!--The sort filters for the discovery search-->
<property name="searchSortConfiguration"> <property name="searchSortConfiguration">
<bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration"> <bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration">
<!--<property name="defaultSort" ref="sortDateIssued"/>-->
<!--DefaultSortOrder can either be desc or asc (desc is default)-->
<property name="defaultSortOrder" value="desc"/>
<property name="sortFields"> <property name="sortFields">
<list> <list>
<ref bean="sortScore" />
<ref bean="sortOrganizationLegalName"/> <ref bean="sortOrganizationLegalName"/>
<ref bean="sortOrganizationAddressCountry"/> <ref bean="sortOrganizationAddressCountry"/>
<ref bean="sortOrganizationAddressLocality"/> <ref bean="sortOrganizationAddressLocality"/>
@@ -1169,11 +1147,9 @@
<!--The sort filters for the discovery search--> <!--The sort filters for the discovery search-->
<property name="searchSortConfiguration"> <property name="searchSortConfiguration">
<bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration"> <bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration">
<!--<property name="defaultSort" ref="sortDateIssued"/>-->
<!--DefaultSortOrder can either be desc or asc (desc is default)-->
<property name="defaultSortOrder" value="desc"/>
<property name="sortFields"> <property name="sortFields">
<list> <list>
<ref bean="sortScore" />
<ref bean="sortPublicationIssueNumber"/> <ref bean="sortPublicationIssueNumber"/>
<ref bean="sortTitle"/> <ref bean="sortTitle"/>
<ref bean="sortCreativeWorkDatePublished"/> <ref bean="sortCreativeWorkDatePublished"/>
@@ -1229,11 +1205,9 @@
<!--The sort filters for the discovery search--> <!--The sort filters for the discovery search-->
<property name="searchSortConfiguration"> <property name="searchSortConfiguration">
<bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration"> <bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration">
<!--<property name="defaultSort" ref="sortDateIssued"/>-->
<!--DefaultSortOrder can either be desc or asc (desc is default)-->
<property name="defaultSortOrder" value="desc"/>
<property name="sortFields"> <property name="sortFields">
<list> <list>
<ref bean="sortScore" />
<ref bean="sortPublicationVolumeNumber"/> <ref bean="sortPublicationVolumeNumber"/>
<ref bean="sortTitle"/> <ref bean="sortTitle"/>
<ref bean="sortCreativeWorkDatePublished"/> <ref bean="sortCreativeWorkDatePublished"/>
@@ -1290,11 +1264,9 @@
<!--The sort filters for the discovery search--> <!--The sort filters for the discovery search-->
<property name="searchSortConfiguration"> <property name="searchSortConfiguration">
<bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration"> <bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration">
<!--<property name="defaultSort" ref="sortDateIssued"/>-->
<!--DefaultSortOrder can either be desc or asc (desc is default)-->
<property name="defaultSortOrder" value="desc"/>
<property name="sortFields"> <property name="sortFields">
<list> <list>
<ref bean="sortScore" />
<ref bean="sortTitle"/> <ref bean="sortTitle"/>
<ref bean="sortCreativeWorkDatePublished"/> <ref bean="sortCreativeWorkDatePublished"/>
<ref bean="sortDateAccessioned"/> <ref bean="sortDateAccessioned"/>
@@ -1360,9 +1332,6 @@
<!--The sort filters for the discovery search--> <!--The sort filters for the discovery search-->
<property name="searchSortConfiguration"> <property name="searchSortConfiguration">
<bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration"> <bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration">
<property name="defaultSort" ref="sortEntityType"/>
<!--DefaultSortOrder can either be desc or asc (desc is default)-->
<property name="defaultSortOrder" value="desc"/>
<property name="sortFields"> <property name="sortFields">
<list> <list>
<ref bean="sortEntityType"/> <ref bean="sortEntityType"/>
@@ -1374,7 +1343,6 @@
<ref bean="sortFamilyName"/> <ref bean="sortFamilyName"/>
<ref bean="sortGivenName"/> <ref bean="sortGivenName"/>
<ref bean="sortBirthDate"/> <ref bean="sortBirthDate"/>
<ref bean="sortDateAccessioned"/>
</list> </list>
</property> </property>
</bean> </bean>
@@ -1422,11 +1390,9 @@
<!--The sort filters for the discovery search--> <!--The sort filters for the discovery search-->
<property name="searchSortConfiguration"> <property name="searchSortConfiguration">
<bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration"> <bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration">
<!--<property name="defaultSort" ref="sortDateIssued"/>-->
<!--DefaultSortOrder can either be desc or asc (desc is default)-->
<property name="defaultSortOrder" value="desc"/>
<property name="sortFields"> <property name="sortFields">
<list> <list>
<ref bean="sortScore" />
<ref bean="sortOrganizationLegalName"/> <ref bean="sortOrganizationLegalName"/>
<ref bean="sortOrganizationAddressCountry"/> <ref bean="sortOrganizationAddressCountry"/>
<ref bean="sortOrganizationAddressLocality"/> <ref bean="sortOrganizationAddressLocality"/>
@@ -2180,59 +2146,77 @@
</bean> </bean>
<!--Sort properties--> <!--Sort properties-->
<bean id="sortScore" class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration">
<property name="defaultSortOrder" value="desc"/>
</bean>
<bean id="sortTitle" class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration"> <bean id="sortTitle" class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration">
<property name="metadataField" value="dc.title"/> <property name="metadataField" value="dc.title"/>
<property name="defaultSortOrder" value="asc"/>
</bean> </bean>
<bean id="sortDateIssued" class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration"> <bean id="sortDateIssued" class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration">
<property name="metadataField" value="dc.date.issued"/> <property name="metadataField" value="dc.date.issued"/>
<property name="type" value="date"/> <property name="type" value="date"/>
<property name="defaultSortOrder" value="desc"/>
</bean> </bean>
<bean id="sortDateAccessioned" class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration"> <bean id="sortDateAccessioned" class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration">
<property name="metadataField" value="dc.date.accessioned"/> <property name="metadataField" value="dc.date.accessioned"/>
<property name="type" value="date"/> <property name="type" value="date"/>
<property name="defaultSortOrder" value="desc"/>
</bean> </bean>
<bean id="sortFamilyName" class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration"> <bean id="sortFamilyName" class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration">
<property name="metadataField" value="person.familyName"/> <property name="metadataField" value="person.familyName"/>
<property name="defaultSortOrder" value="asc"/>
</bean> </bean>
<bean id="sortGivenName" class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration"> <bean id="sortGivenName" class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration">
<property name="metadataField" value="person.givenName"/> <property name="metadataField" value="person.givenName"/>
<property name="defaultSortOrder" value="asc"/>
</bean> </bean>
<bean id="sortBirthDate" class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration"> <bean id="sortBirthDate" class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration">
<property name="metadataField" value="person.birthDate"/> <property name="metadataField" value="person.birthDate"/>
<property name="type" value="date"/> <property name="type" value="date"/>
<property name="defaultSortOrder" value="desc"/>
</bean> </bean>
<bean id="sortOrganizationLegalName" class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration"> <bean id="sortOrganizationLegalName" class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration">
<property name="metadataField" value="organization.legalName"/> <property name="metadataField" value="organization.legalName"/>
<property name="defaultSortOrder" value="asc"/>
</bean> </bean>
<bean id="sortOrganizationAddressCountry" <bean id="sortOrganizationAddressCountry"
class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration"> class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration">
<property name="metadataField" value="organisation.address.addressCountry"/> <property name="metadataField" value="organisation.address.addressCountry"/>
<property name="defaultSortOrder" value="asc"/>
</bean> </bean>
<bean id="sortOrganizationAddressLocality" <bean id="sortOrganizationAddressLocality"
class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration"> class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration">
<property name="metadataField" value="organisation.address.addressLocality"/> <property name="metadataField" value="organisation.address.addressLocality"/>
<property name="defaultSortOrder" value="asc"/>
</bean> </bean>
<bean id="sortOrganizationFoundingDate" class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration"> <bean id="sortOrganizationFoundingDate" class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration">
<property name="metadataField" value="organisation.foundingDate"/> <property name="metadataField" value="organisation.foundingDate"/>
<property name="type" value="date"/> <property name="type" value="date"/>
<property name="defaultSortOrder" value="desc"/>
</bean> </bean>
<bean id="sortPublicationIssueNumber" class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration"> <bean id="sortPublicationIssueNumber" class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration">
<property name="metadataField" value="publicationissue.issueNumber"/> <property name="metadataField" value="publicationissue.issueNumber"/>
<property name="defaultSortOrder" value="desc"/>
</bean> </bean>
<bean id="sortCreativeWorkDatePublished" class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration"> <bean id="sortCreativeWorkDatePublished" class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration">
<property name="metadataField" value="creativework.datePublished"/> <property name="metadataField" value="creativework.datePublished"/>
<property name="defaultSortOrder" value="desc"/>
</bean> </bean>
<bean id="sortPublicationVolumeNumber" class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration"> <bean id="sortPublicationVolumeNumber" class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration">
<property name="metadataField" value="publicationvolume.volumeNumber"/> <property name="metadataField" value="publicationvolume.volumeNumber"/>
<property name="defaultSortOrder" value="desc"/>
</bean> </bean>
<bean id="sortEntityType" class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration"> <bean id="sortEntityType" class="org.dspace.discovery.configuration.DiscoverySortFieldConfiguration">
<property name="metadataField" value="dspace.entity.type"/> <property name="metadataField" value="dspace.entity.type"/>
<property name="defaultSortOrder" value="desc"/>
</bean> </bean>
</beans> </beans>

View File

@@ -21,6 +21,7 @@
<!-- serves as a 'this field is required' flag. --> <!-- serves as a 'this field is required' flag. -->
<form-definitions> <form-definitions>
<!-- Form used for entering in Bitstream/File metadata after uploading a file -->
<form name="bitstream-metadata"> <form name="bitstream-metadata">
<row> <row>
<field> <field>
@@ -47,7 +48,203 @@
</row> </row>
</form> </form>
<!-- Form which defines the first page/section of the "traditional" DSpace submission process,
often used when depositing normal Items (i.e. Items which are not Entities). -->
<form name="traditionalpageone"> <form name="traditionalpageone">
<row>
<field>
<dc-schema>dc</dc-schema>
<dc-element>contributor</dc-element>
<dc-qualifier>author</dc-qualifier>
<repeatable>true</repeatable>
<label>Author</label>
<input-type>onebox</input-type>
<hint>Enter the author's name (Family name, Given names).</hint>
<required></required>
</field>
</row>
<row>
<field>
<dc-schema>dc</dc-schema>
<dc-element>title</dc-element>
<dc-qualifier></dc-qualifier>
<repeatable>false</repeatable>
<label>Title</label>
<input-type>onebox</input-type>
<hint>Enter the main title of the item.</hint>
<required>You must enter a main title for this item.</required>
</field>
</row>
<row>
<field>
<dc-schema>dc</dc-schema>
<dc-element>title</dc-element>
<dc-qualifier>alternative</dc-qualifier>
<repeatable>true</repeatable>
<label>Other Titles</label>
<input-type>onebox</input-type>
<hint>If the item has any alternative titles, please enter them here.</hint>
<required></required>
</field>
</row>
<row>
<field>
<dc-schema>dc</dc-schema>
<dc-element>date</dc-element>
<dc-qualifier>issued</dc-qualifier>
<repeatable>false</repeatable>
<label>Date of Issue</label>
<style>col-sm-4</style>
<input-type>date</input-type>
<hint>Please give the date of previous publication or public distribution.
You can leave out the day and/or month if they aren't applicable.
</hint>
<required>You must enter at least the year.</required>
</field>
<field>
<dc-schema>dc</dc-schema>
<dc-element>publisher</dc-element>
<dc-qualifier></dc-qualifier>
<repeatable>false</repeatable>
<label>Publisher</label>
<style>col-sm-8</style>
<input-type>onebox</input-type>
<hint>Enter the name of the publisher of the previously issued instance of this item.</hint>
<required></required>
</field>
</row>
<row>
<field>
<dc-schema>dc</dc-schema>
<dc-element>identifier</dc-element>
<dc-qualifier>citation</dc-qualifier>
<repeatable>false</repeatable>
<label>Citation</label>
<input-type>onebox</input-type>
<hint>Enter the standard citation for the previously issued instance of this item.</hint>
<required></required>
</field>
</row>
<row>
<field>
<dc-schema>dc</dc-schema>
<dc-element>relation</dc-element>
<dc-qualifier>ispartofseries</dc-qualifier>
<repeatable>true</repeatable>
<label>Series/Report No.</label>
<input-type>series</input-type>
<hint>Enter the series and number assigned to this item by your community.</hint>
<required></required>
</field>
</row>
<row>
<field>
<dc-schema>dc</dc-schema>
<dc-element>identifier</dc-element>
<dc-qualifier></dc-qualifier>
<!-- An input-type of qualdrop_value MUST be marked as repeatable -->
<repeatable>true</repeatable>
<label>Identifiers</label>
<input-type value-pairs-name="common_identifiers">qualdrop_value</input-type>
<hint>If the item has any identification numbers or codes associated with
it, please enter the types and the actual numbers or codes.
</hint>
<required></required>
</field>
</row>
<row>
<field>
<dc-schema>dc</dc-schema>
<dc-element>type</dc-element>
<dc-qualifier></dc-qualifier>
<repeatable>true</repeatable>
<label>Type</label>
<input-type value-pairs-name="common_types">dropdown</input-type>
<hint>Select the type(s) of content of the item. To select more than one value in the list, you may
have to hold down the "CTRL" or "Shift" key.
</hint>
<required></required>
</field>
</row>
<row>
<field>
<dc-schema>dc</dc-schema>
<dc-element>language</dc-element>
<dc-qualifier>iso</dc-qualifier>
<repeatable>false</repeatable>
<label>Language</label>
<input-type value-pairs-name="common_iso_languages">dropdown</input-type>
<hint>Select the language of the main content of the item. If the language does not appear in the
list, please select 'Other'. If the content does not really have a language (for example, if it
is a dataset or an image) please select 'N/A'.
</hint>
<required></required>
</field>
</row>
</form>
<!-- Form which defines the second page/section of the "traditional" DSpace submission process,
often used when depositing normal Items. -->
<form name="traditionalpagetwo">
<row>
<field>
<dc-schema>dc</dc-schema>
<dc-element>subject</dc-element>
<dc-qualifier></dc-qualifier>
<!-- An input-type of tag MUST be marked as repeatable -->
<repeatable>true</repeatable>
<label>Subject Keywords</label>
<input-type>tag</input-type>
<hint>Enter appropriate subject keywords or phrases.</hint>
<required></required>
<vocabulary>srsc</vocabulary>
</field>
</row>
<row>
<field>
<dc-schema>dc</dc-schema>
<dc-element>description</dc-element>
<dc-qualifier>abstract</dc-qualifier>
<repeatable>false</repeatable>
<label>Abstract</label>
<input-type>textarea</input-type>
<hint>Enter the abstract of the item.</hint>
<required></required>
</field>
</row>
<row>
<field>
<dc-schema>dc</dc-schema>
<dc-element>description</dc-element>
<dc-qualifier>sponsorship</dc-qualifier>
<repeatable>false</repeatable>
<label>Sponsors</label>
<input-type>textarea</input-type>
<hint>Enter the names of any sponsors and/or funding codes in the box.</hint>
<required></required>
</field>
</row>
<row>
<field>
<dc-schema>dc</dc-schema>
<dc-element>description</dc-element>
<dc-qualifier></dc-qualifier>
<repeatable>false</repeatable>
<label>Description</label>
<input-type>textarea</input-type>
<hint>Enter any other description or comments in this box.</hint>
<required></required>
</field>
</row>
</form>
<!-- Form which defines the *first* metadata page/section for depositing Publication Entities.
Since Publication Entities are very similar to normal Items, this is nearly identical to the
'traditionalpageone' form. The only difference is the Author field can be used to link the
Publication Entity to a Person Entity. -->
<form name="publicationStep">
<row> <row>
<relation-field> <relation-field>
<relationship-type>isAuthorOfPublication</relationship-type> <relationship-type>isAuthorOfPublication</relationship-type>
@@ -190,60 +387,8 @@
</row> </row>
</form> </form>
<form name="traditionalpagetwo"> <!-- Form which defines the metadata page/section for depositing Person Entities -->
<row> <form name="personStep">
<field>
<dc-schema>dc</dc-schema>
<dc-element>subject</dc-element>
<dc-qualifier></dc-qualifier>
<!-- An input-type of tag MUST be marked as repeatable -->
<repeatable>true</repeatable>
<label>Subject Keywords</label>
<input-type>tag</input-type>
<hint>Enter appropriate subject keywords or phrases.</hint>
<required></required>
<vocabulary>srsc</vocabulary>
</field>
</row>
<row>
<field>
<dc-schema>dc</dc-schema>
<dc-element>description</dc-element>
<dc-qualifier>abstract</dc-qualifier>
<repeatable>false</repeatable>
<label>Abstract</label>
<input-type>textarea</input-type>
<hint>Enter the abstract of the item.</hint>
<required></required>
</field>
</row>
<row>
<field>
<dc-schema>dc</dc-schema>
<dc-element>description</dc-element>
<dc-qualifier>sponsorship</dc-qualifier>
<repeatable>false</repeatable>
<label>Sponsors</label>
<input-type>textarea</input-type>
<hint>Enter the names of any sponsors and/or funding codes in the box.</hint>
<required></required>
</field>
</row>
<row>
<field>
<dc-schema>dc</dc-schema>
<dc-element>description</dc-element>
<dc-qualifier></dc-qualifier>
<repeatable>false</repeatable>
<label>Description</label>
<input-type>textarea</input-type>
<hint>Enter any other description or comments in this box.</hint>
<required></required>
</field>
</row>
</form>
<form name="peopleStep">
<row> <row>
<relation-field> <relation-field>
<relationship-type>isPublicationOfAuthor</relationship-type> <relationship-type>isPublicationOfAuthor</relationship-type>
@@ -320,6 +465,7 @@
</row> </row>
</form> </form>
<!-- Form which defines the metadata page/section for depositing Project Entities -->
<form name="projectStep"> <form name="projectStep">
<row> <row>
<field> <field>
@@ -390,6 +536,7 @@
</row> </row>
</form> </form>
<!-- Form which defines the metadata page/section for depositing OrgUnit Entities -->
<form name="orgUnitStep"> <form name="orgUnitStep">
<row> <row>
<field> <field>
@@ -449,6 +596,7 @@
</row> </row>
</form> </form>
<!-- Form which defines the metadata page/section for depositing Journal Entities -->
<form name="journalStep"> <form name="journalStep">
<row> <row>
<field> <field>
@@ -498,6 +646,7 @@
</form> </form>
<!-- Form which defines the metadata page/section for depositing JournalVolume Entities -->
<form name="journalVolumeStep"> <form name="journalVolumeStep">
<row> <row>
<relation-field> <relation-field>
@@ -548,6 +697,7 @@
</row> </row>
</form> </form>
<!-- Form which defines the metadata page/section for depositing JournalIssue Entities -->
<form name="journalIssueStep"> <form name="journalIssueStep">
<row> <row>
<relation-field> <relation-field>

View File

@@ -17,7 +17,7 @@
<parent> <parent>
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<artifactId>modules</artifactId> <artifactId>modules</artifactId>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
<relativePath>..</relativePath> <relativePath>..</relativePath>
</parent> </parent>

View File

@@ -11,7 +11,7 @@
<parent> <parent>
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<artifactId>dspace-parent</artifactId> <artifactId>dspace-parent</artifactId>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath> <relativePath>../../pom.xml</relativePath>
</parent> </parent>

View File

@@ -13,7 +13,7 @@
<parent> <parent>
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<artifactId>modules</artifactId> <artifactId>modules</artifactId>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
<relativePath>..</relativePath> <relativePath>..</relativePath>
</parent> </parent>

View File

@@ -13,7 +13,7 @@ just adding new jar in the classloader</description>
<parent> <parent>
<artifactId>modules</artifactId> <artifactId>modules</artifactId>
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
<relativePath>..</relativePath> <relativePath>..</relativePath>
</parent> </parent>

View File

@@ -16,7 +16,7 @@
<parent> <parent>
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<artifactId>dspace-parent</artifactId> <artifactId>dspace-parent</artifactId>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <relativePath>../pom.xml</relativePath>
</parent> </parent>

View File

@@ -11,7 +11,8 @@ version: "3.7"
services: services:
dspace-cli: dspace-cli:
environment: environment:
- LOADASSETS=https://www.dropbox.com/s/v3ahfcuatklbmi0/assetstore-2019-11-28.tar.gz?dl=1 # This assetstore zip is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
- LOADASSETS=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/assetstore.tar.gz
entrypoint: entrypoint:
- /bin/bash - /bin/bash
- '-c' - '-c'

View File

@@ -12,5 +12,33 @@ services:
dspacedb: dspacedb:
image: dspace/dspace-postgres-pgcrypto:loadsql image: dspace/dspace-postgres-pgcrypto:loadsql
environment: environment:
# Double underbars in env names will be replaced with periods for apache commons # This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
- LOADSQL=https://www.dropbox.com/s/4ap1y6deseoc8ws/dspace7-entities-2019-11-28.sql?dl=1 - LOADSQL=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-2021-04-14.sql
dspace:
### OVERRIDE default 'entrypoint' in 'docker-compose.yml ####
# Ensure that the database is ready BEFORE starting tomcat
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
# 2. Then, run database migration to init database tables
# 3. (Custom for Entities) enable Entity-specific collection submission mappings in item-submission.xml
# This 'sed' command inserts the sample configurations specific to the Entities data set, see:
# https://github.com/DSpace/DSpace/blob/main/dspace/config/item-submission.xml#L36-L49
# 4. Finally, start Tomcat
entrypoint:
- /bin/bash
- '-c'
- |
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
/dspace/bin/dspace database migrate
sed -i '/name-map collection-handle="default".*/a \\n <name-map collection-handle="123456789/3" submission-name="Publication"/> \
<name-map collection-handle="123456789/4" submission-name="Publication"/> \
<name-map collection-handle="123456789/281" submission-name="Publication"/> \
<name-map collection-handle="123456789/5" submission-name="Publication"/> \
<name-map collection-handle="123456789/8" submission-name="OrgUnit"/> \
<name-map collection-handle="123456789/6" submission-name="Person"/> \
<name-map collection-handle="123456789/279" submission-name="Person"/> \
<name-map collection-handle="123456789/7" submission-name="Project"/> \
<name-map collection-handle="123456789/280" submission-name="Project"/> \
<name-map collection-handle="123456789/28" submission-name="Journal"/> \
<name-map collection-handle="123456789/29" submission-name="JournalVolume"/> \
<name-map collection-handle="123456789/30" submission-name="JournalIssue"/>' /dspace/config/item-submission.xml
catalina.sh run

View File

@@ -1,6 +1,11 @@
dspace.dir=/dspace dspace.dir=/dspace
db.url=jdbc:postgresql://dspacedb:5432/dspace
dspace.server.url=http://localhost:8080/server dspace.server.url=http://localhost:8080/server
dspace.ui.url=http://localhost:4000 dspace.ui.url=http://localhost:4000
dspace.name=DSpace Started with Docker Compose dspace.name=DSpace Started with Docker Compose
# Ensure we are using the 'dspacedb' image for our database
db.url=jdbc:postgresql://dspacedb:5432/dspace
# Ensure we are using the 'dspacesolr' image for Solr
solr.server=http://dspacesolr:8983/solr solr.server=http://dspacesolr:8983/solr
# NOTE: This setting is required for a REST API running in Docker to trust requests from the host machine.
# This IP range MUST correspond to the 'dspacenet' subnet defined in our 'docker-compose.yml'.
proxies.trusted.ipranges = 172.23.0

34
pom.xml
View File

@@ -4,7 +4,7 @@
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<artifactId>dspace-parent</artifactId> <artifactId>dspace-parent</artifactId>
<packaging>pom</packaging> <packaging>pom</packaging>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
<name>DSpace Parent Project</name> <name>DSpace Parent Project</name>
<description> <description>
DSpace open source software is a turnkey institutional repository application. DSpace open source software is a turnkey institutional repository application.
@@ -30,12 +30,12 @@
<axiom.version>1.2.22</axiom.version> <axiom.version>1.2.22</axiom.version>
<errorprone.version>2.3.4</errorprone.version> <errorprone.version>2.3.4</errorprone.version>
<!-- NOTE: when updating jackson.version, also sync jackson-databind dependency below --> <!-- NOTE: when updating jackson.version, also sync jackson-databind dependency below -->
<jackson.version>2.10.2</jackson.version> <jackson.version>2.12.3</jackson.version>
<javax-annotation.version>1.3.2</javax-annotation.version> <javax-annotation.version>1.3.2</javax-annotation.version>
<jaxb-api.version>2.3.1</jaxb-api.version> <jaxb-api.version>2.3.1</jaxb-api.version>
<jaxb-runtime.version>2.3.1</jaxb-runtime.version> <jaxb-runtime.version>2.3.1</jaxb-runtime.version>
<!-- NOTE: Jetty needed for Solr, Handle Server & tests --> <!-- NOTE: Jetty needed for Solr, Handle Server & tests -->
<jetty.version>9.4.35.v20201120</jetty.version> <jetty.version>9.4.38.v20210224</jetty.version>
<log4j.version>2.13.3</log4j.version> <log4j.version>2.13.3</log4j.version>
<pdfbox-version>2.0.15</pdfbox-version> <pdfbox-version>2.0.15</pdfbox-version>
<poi-version>3.17</poi-version> <poi-version>3.17</poi-version>
@@ -826,14 +826,14 @@
<dependency> <dependency>
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<artifactId>dspace-rest</artifactId> <artifactId>dspace-rest</artifactId>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
<type>jar</type> <type>jar</type>
<classifier>classes</classifier> <classifier>classes</classifier>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<artifactId>dspace-rest</artifactId> <artifactId>dspace-rest</artifactId>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
<type>war</type> <type>war</type>
</dependency> </dependency>
</dependencies> </dependencies>
@@ -980,64 +980,64 @@
<dependency> <dependency>
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<artifactId>dspace-api</artifactId> <artifactId>dspace-api</artifactId>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<artifactId>dspace-api</artifactId> <artifactId>dspace-api</artifactId>
<type>test-jar</type> <type>test-jar</type>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.dspace.modules</groupId> <groupId>org.dspace.modules</groupId>
<artifactId>additions</artifactId> <artifactId>additions</artifactId>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<artifactId>dspace-sword</artifactId> <artifactId>dspace-sword</artifactId>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<artifactId>dspace-swordv2</artifactId> <artifactId>dspace-swordv2</artifactId>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<artifactId>dspace-oai</artifactId> <artifactId>dspace-oai</artifactId>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<artifactId>dspace-services</artifactId> <artifactId>dspace-services</artifactId>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<artifactId>dspace-server-webapp</artifactId> <artifactId>dspace-server-webapp</artifactId>
<type>test-jar</type> <type>test-jar</type>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<artifactId>dspace-rdf</artifactId> <artifactId>dspace-rdf</artifactId>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<artifactId>dspace-server-webapp</artifactId> <artifactId>dspace-server-webapp</artifactId>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
<type>jar</type> <type>jar</type>
<classifier>classes</classifier> <classifier>classes</classifier>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.dspace</groupId> <groupId>org.dspace</groupId>
<artifactId>dspace-server-webapp</artifactId> <artifactId>dspace-server-webapp</artifactId>
<version>7.0-beta5-SNAPSHOT</version> <version>7.0-beta6-SNAPSHOT</version>
<type>war</type> <type>war</type>
</dependency> </dependency>
<!-- DSpace API Localization Packages --> <!-- DSpace API Localization Packages -->
@@ -1369,7 +1369,7 @@
<dependency> <dependency>
<groupId>commons-io</groupId> <groupId>commons-io</groupId>
<artifactId>commons-io</artifactId> <artifactId>commons-io</artifactId>
<version>2.6</version> <version>2.7</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>