diff --git a/README.md b/README.md index b8fee04d3d..6d640b475f 100644 --- a/README.md +++ b/README.md @@ -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/). 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) and a servlet container (usually Tomcat) in order to function. diff --git a/docker-compose.yml b/docker-compose.yml index db87402413..5d80d95090 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,11 @@ version: '3.7' networks: 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: # DSpace (backend) webapp container dspace: diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 7a5c9031a2..9dc1b54ed5 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -12,7 +12,7 @@ org.dspace dspace-parent - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT .. @@ -856,6 +856,10 @@ org.javassist javassist + + io.swagger + swagger-jersey-jaxrs + diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExport.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExport.java index 3332440f06..739e7a648e 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExport.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExport.java @@ -8,9 +8,12 @@ package org.dspace.app.bulkedit; import java.sql.SQLException; +import java.util.UUID; import org.apache.commons.cli.ParseException; 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.factory.ContentServiceFactory; import org.dspace.content.service.MetadataDSpaceCsvExportService; @@ -30,7 +33,7 @@ public class MetadataExport extends DSpaceRunnable 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 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 getLauncherCommands(Document commandConfigs) { // List all command elements List commands = commandConfigs.getRootElement().getChildren("command"); @@ -334,11 +380,32 @@ public class ScriptLauncher { sortedCommands.put(command.getChild("name").getValue(), command); } - // Display the sorted list - System.out.println("Usage: dspace [command-name] {parameters}"); - for (Element command : sortedCommands.values()) { - System.out.println(" - " + command.getChild("name").getValue() + - ": " + command.getChild("description").getValue()); - } + return sortedCommands.values(); } + + /** + * Get a sorted collection of the commands that are defined as beans. Used by {@link #display}. + * @return sorted collection of commands + */ + private static Collection getServiceCommands() { + ScriptService scriptService = ScriptServiceFactory.getInstance().getScriptService(); + + Context throwAwayContext = new Context(); + + throwAwayContext.turnOffAuthorisationSystem(); + List scriptConfigurations = scriptService.getScriptConfigurations(throwAwayContext); + throwAwayContext.restoreAuthSystemState(); + + try { + throwAwayContext.complete(); + } catch (SQLException exception) { + exception.printStackTrace(); + throwAwayContext.abort(); + } + + scriptConfigurations.sort(Comparator.comparing(ScriptConfiguration::getName)); + + return scriptConfigurations; + } + } diff --git a/dspace-api/src/main/java/org/dspace/app/util/DSpaceObjectUtilsImpl.java b/dspace-api/src/main/java/org/dspace/app/util/DSpaceObjectUtilsImpl.java new file mode 100644 index 0000000000..e3f2b0ea5f --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/util/DSpaceObjectUtilsImpl.java @@ -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 dSpaceObjectService : + contentServiceFactory.getDSpaceObjectServices()) { + DSpaceObject dso = dSpaceObjectService.find(context, uuid); + if (dso != null) { + return dso; + } + } + return null; + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/util/factory/UtilServiceFactory.java b/dspace-api/src/main/java/org/dspace/app/util/factory/UtilServiceFactory.java index 3b2dcaf646..02dcaac376 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/factory/UtilServiceFactory.java +++ b/dspace-api/src/main/java/org/dspace/app/util/factory/UtilServiceFactory.java @@ -7,6 +7,7 @@ */ 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.OpenSearchService; import org.dspace.app.util.service.WebAppService; @@ -25,6 +26,8 @@ public abstract class UtilServiceFactory { public abstract MetadataExposureService getMetadataExposureService(); + public abstract DSpaceObjectUtils getDSpaceObjectUtils(); + public static UtilServiceFactory getInstance() { return DSpaceServicesFactory.getInstance().getServiceManager() .getServiceByName("appUtilServiceFactory", UtilServiceFactory.class); diff --git a/dspace-api/src/main/java/org/dspace/app/util/factory/UtilServiceFactoryImpl.java b/dspace-api/src/main/java/org/dspace/app/util/factory/UtilServiceFactoryImpl.java index f301926a67..0f9a5164a4 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/factory/UtilServiceFactoryImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/util/factory/UtilServiceFactoryImpl.java @@ -7,6 +7,7 @@ */ 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.OpenSearchService; import org.dspace.app.util.service.WebAppService; @@ -26,6 +27,8 @@ public class UtilServiceFactoryImpl extends UtilServiceFactory { private OpenSearchService openSearchService; @Autowired(required = true) private WebAppService webAppService; + @Autowired(required = true) + private DSpaceObjectUtils dSpaceObjectUtils; @Override public WebAppService getWebAppService() { @@ -41,4 +44,9 @@ public class UtilServiceFactoryImpl extends UtilServiceFactory { public MetadataExposureService getMetadataExposureService() { return metadataExposureService; } + + @Override + public DSpaceObjectUtils getDSpaceObjectUtils() { + return dSpaceObjectUtils; + } } diff --git a/dspace-api/src/main/java/org/dspace/app/util/service/DSpaceObjectUtils.java b/dspace-api/src/main/java/org/dspace/app/util/service/DSpaceObjectUtils.java new file mode 100644 index 0000000000..e6a97004ef --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/util/service/DSpaceObjectUtils.java @@ -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; +} diff --git a/dspace-api/src/main/java/org/dspace/authenticate/ShibAuthentication.java b/dspace-api/src/main/java/org/dspace/authenticate/ShibAuthentication.java index fbe65c1c2a..53502a22ce 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/ShibAuthentication.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/ShibAuthentication.java @@ -16,6 +16,7 @@ import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; 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 * redirect to obtain credentials (either password prompt or e.g. HTTPS port * for client cert.); null means no redirect. - * + *

+ * 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] + *

+ * 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 request The HTTP request that started this operation, or null if not * applicable. @@ -507,8 +515,8 @@ public class ShibAuthentication implements AuthenticationMethod { } // 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 - // and locally authenticate them. + // We need it to go back to DSpace's ShibbolethRestController so we will extract the user's information, + // locally authenticate them & then redirect back to the UI. String returnURL = configurationService.getProperty("dspace.server.url") + "/api/authn/shibboleth" + ((redirectUrl != null) ? "?redirectUrl=" + redirectUrl : ""); @@ -533,6 +541,25 @@ public class ShibAuthentication implements AuthenticationMethod { 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 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 * the request object. There are three cases where this can occurr, each as diff --git a/dspace-api/src/main/java/org/dspace/content/DSpaceObjectServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/DSpaceObjectServiceImpl.java index 13e4d83fa0..c34291c3dd 100644 --- a/dspace-api/src/main/java/org/dspace/content/DSpaceObjectServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/DSpaceObjectServiceImpl.java @@ -17,8 +17,10 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.StringTokenizer; +import java.util.function.Supplier; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang.NotImplementedException; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; @@ -238,6 +240,21 @@ public abstract class DSpaceObjectServiceImpl implements public List addMetadata(Context context, T dso, MetadataField metadataField, String lang, List values, List authorities, List confidences) 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 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 addMetadata(Context context, T dso, MetadataField metadataField, String lang, + List values, List authorities, List confidences, Supplier placeSupplier) + throws SQLException { + boolean authorityControlled = metadataAuthorityService.isAuthorityControlled(metadataField); boolean authorityRequired = metadataAuthorityService.isAuthorityRequired(metadataField); List newMetadata = new ArrayList<>(values.size()); @@ -252,11 +269,8 @@ public abstract class DSpaceObjectServiceImpl implements } MetadataValue metadataValue = metadataValueService.create(context, dso, metadataField); 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( - this.getMetadata(dso, metadataField.getMetadataSchema().getName(), metadataField.getElement(), - metadataField.getQualifier(), Item.ANY).size() - 1); + + metadataValue.setPlace(placeSupplier.get()); metadataValue.setLanguage(lang == null ? null : lang.trim()); @@ -359,7 +373,7 @@ public abstract class DSpaceObjectServiceImpl implements public MetadataValue addMetadata(Context context, T dso, String schema, String element, String qualifier, String lang, String value, String authority, int confidence) throws SQLException { 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 @@ -805,4 +819,12 @@ public abstract class DSpaceObjectServiceImpl implements 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(); + + } + } diff --git a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java index 39ad2d1d53..59beec72a6 100644 --- a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java @@ -11,12 +11,14 @@ import java.io.IOException; import java.io.InputStream; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Comparator; import java.util.Date; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.UUID; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -1405,5 +1407,25 @@ prevent the generation of resource policy entry values with null dspace_object a 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 placeSupplier = () -> place; + + return addMetadata(context, dso, metadataField, lang, Arrays.asList(value), + Arrays.asList(authority), Arrays.asList(confidence), placeSupplier) + .stream().findFirst().orElse(null); + } + } diff --git a/dspace-api/src/main/java/org/dspace/content/MetadataDSpaceCsvExportServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/MetadataDSpaceCsvExportServiceImpl.java index 1750938937..7dad2117d5 100644 --- a/dspace-api/src/main/java/org/dspace/content/MetadataDSpaceCsvExportServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/MetadataDSpaceCsvExportServiceImpl.java @@ -12,9 +12,11 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.UUID; import com.google.common.collect.Iterators; import org.dspace.app.bulkedit.DSpaceCSV; +import org.dspace.app.util.service.DSpaceObjectUtils; import org.dspace.content.service.ItemService; import org.dspace.content.service.MetadataDSpaceCsvExportService; import org.dspace.core.Constants; @@ -31,8 +33,11 @@ public class MetadataDSpaceCsvExportServiceImpl implements MetadataDSpaceCsvExpo @Autowired private ItemService itemService; + @Autowired + private DSpaceObjectUtils dSpaceObjectUtils; + @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 { Iterator toExport = null; @@ -40,26 +45,32 @@ public class MetadataDSpaceCsvExportServiceImpl implements MetadataDSpaceCsvExpo handler.logInfo("Exporting whole repository WARNING: May take some time!"); toExport = itemService.findAll(context); } 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) { 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) { - handler.logInfo("Exporting item '" + dso.getName() + "' (" + handle + ")"); + handler.logInfo("Exporting item '" + dso.getName() + "' (" + identifier + ")"); List item = new ArrayList<>(); item.add((Item) dso); toExport = item.iterator(); } else if (dso.getType() == Constants.COLLECTION) { - handler.logInfo("Exporting collection '" + dso.getName() + "' (" + handle + ")"); + handler.logInfo("Exporting collection '" + dso.getName() + "' (" + identifier + ")"); Collection collection = (Collection) dso; toExport = itemService.findByCollection(context, collection); } 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); } 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")); } } diff --git a/dspace-api/src/main/java/org/dspace/content/service/DSpaceObjectService.java b/dspace-api/src/main/java/org/dspace/content/service/DSpaceObjectService.java index 39f5cd7d41..606f5bb7c0 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/DSpaceObjectService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/DSpaceObjectService.java @@ -367,6 +367,30 @@ public interface DSpaceObjectService { public MetadataValue addMetadata(Context context, T dso, String schema, String element, String qualifier, 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. Must match + * the name of an existing metadata schema. + * @param element the metadata element name + * @param qualifier the metadata qualifier, or null for + * unqualified + * @param lang the ISO639 language code, optionally followed by an underscore + * and the ISO3166 country code. null 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 * values. Use clearMetadata to remove values. diff --git a/dspace-api/src/main/java/org/dspace/content/service/ItemService.java b/dspace-api/src/main/java/org/dspace/content/service/ItemService.java index ff30ffe0e0..2a38488f7a 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/ItemService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/ItemService.java @@ -741,5 +741,4 @@ public interface ItemService public List getMetadata(Item item, String schema, String element, String qualifier, String lang, boolean enableVirtualMetadata); - } diff --git a/dspace-api/src/main/java/org/dspace/content/service/MetadataDSpaceCsvExportService.java b/dspace-api/src/main/java/org/dspace/content/service/MetadataDSpaceCsvExportService.java index aeb956fc49..d3fc2e8236 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/MetadataDSpaceCsvExportService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/MetadataDSpaceCsvExportService.java @@ -28,12 +28,13 @@ public interface MetadataDSpaceCsvExportService { * @param context The relevant DSpace context * @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 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 * @throws Exception If something goes wrong */ 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 diff --git a/dspace-api/src/main/java/org/dspace/discovery/SearchService.java b/dspace-api/src/main/java/org/dspace/discovery/SearchService.java index c6ad547b69..9b6ac0109d 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SearchService.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SearchService.java @@ -12,6 +12,7 @@ import java.util.List; import org.dspace.content.Item; import org.dspace.core.Context; +import org.dspace.discovery.configuration.DiscoveryConfiguration; import org.dspace.discovery.configuration.DiscoveryMoreLikeThisConfiguration; import org.dspace.discovery.configuration.DiscoverySearchFilterFacet; @@ -62,11 +63,14 @@ public interface SearchService { * @param field the field of the filter query * @param operator equals/notequals/notcontains/authority/notauthority * @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 * @throws SQLException if database error * 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 getRelatedItems(Context context, Item item, DiscoveryMoreLikeThisConfiguration moreLikeThisConfiguration); diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java index fc73009644..dd6dd0d755 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java @@ -60,6 +60,7 @@ import org.dspace.core.Context; import org.dspace.core.Email; import org.dspace.core.I18nUtil; import org.dspace.core.LogManager; +import org.dspace.discovery.configuration.DiscoveryConfiguration; import org.dspace.discovery.configuration.DiscoveryConfigurationParameters; import org.dspace.discovery.configuration.DiscoveryMoreLikeThisConfiguration; import org.dspace.discovery.configuration.DiscoverySearchFilterFacet; @@ -1069,9 +1070,9 @@ public class SolrServiceImpl implements SearchService, IndexingService { return new ArrayList<>(0); } } - @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 { DiscoverFilterQuery result = new DiscoverFilterQuery(); @@ -1081,7 +1082,14 @@ public class SolrServiceImpl implements SearchService, IndexingService { 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")) { filterQuery.append("_authority"); } diff --git a/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoverySortConfiguration.java b/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoverySortConfiguration.java index db6ac80f29..e251d1bc51 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoverySortConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoverySortConfiguration.java @@ -20,26 +20,8 @@ public class DiscoverySortConfiguration { public static final String SCORE = "score"; - /** Attributes used for sorting of results **/ - public enum SORT_ORDER { - desc, - asc - } - - private DiscoverySortFieldConfiguration defaultSort = null; - private List sortFields = new ArrayList(); - private SORT_ORDER defaultSortOrder = SORT_ORDER.desc; - - public DiscoverySortFieldConfiguration getDefaultSort() { - return defaultSort; - } - - public void setDefaultSort(DiscoverySortFieldConfiguration defaultSort) { - this.defaultSort = defaultSort; - } - public List getSortFields() { return sortFields; } @@ -48,14 +30,6 @@ public class DiscoverySortConfiguration { this.sortFields = sortFields; } - public SORT_ORDER getDefaultSortOrder() { - return defaultSortOrder; - } - - public void setDefaultSortOrder(SORT_ORDER defaultSortOrder) { - this.defaultSortOrder = defaultSortOrder; - } - public DiscoverySortFieldConfiguration getSortFieldConfiguration(String sortField) { if (StringUtils.isBlank(sortField)) { return null; @@ -67,10 +41,6 @@ public class DiscoverySortConfiguration { return configuration; } - if (defaultSort != null && StringUtils.equals(defaultSort.getMetadataField(), sortField)) { - return defaultSort; - } - for (DiscoverySortFieldConfiguration sortFieldConfiguration : CollectionUtils.emptyIfNull(sortFields)) { if (StringUtils.equals(sortFieldConfiguration.getMetadataField(), sortField)) { return sortFieldConfiguration; diff --git a/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoverySortFieldConfiguration.java b/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoverySortFieldConfiguration.java index 0dfd1250d3..1232c113b3 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoverySortFieldConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoverySortFieldConfiguration.java @@ -18,11 +18,18 @@ public class DiscoverySortFieldConfiguration { private String metadataField; 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() { return metadataField; } - @Autowired(required = true) public void setMetadataField(String metadataField) { this.metadataField = metadataField; } @@ -35,6 +42,15 @@ public class DiscoverySortFieldConfiguration { this.type = type; } + public SORT_ORDER getDefaultSortOrder() { + return defaultSortOrder; + } + + @Autowired(required = true) + public void setDefaultSortOrder(SORT_ORDER defaultSortOrder) { + this.defaultSortOrder = defaultSortOrder; + } + @Override public boolean equals(Object obj) { if (obj != null && obj instanceof DiscoverySortFieldConfiguration) { diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/oracle/update-sequences.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/oracle/update-sequences.sql index fb76d68762..b4d4d755cb 100644 --- a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/oracle/update-sequences.sql +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/oracle/update-sequences.sql @@ -47,8 +47,6 @@ BEGIN updateseq('fileextension_seq', 'fileextension', 'file_extension_id'); updateseq('resourcepolicy_seq', 'resourcepolicy', 'policy_id'); updateseq('workspaceitem_seq', 'workspaceitem', 'workspace_item_id'); - updateseq('workflowitem_seq', 'workflowitem', 'workflow_id'); - updateseq('tasklistitem_seq', 'tasklistitem', 'tasklist_id'); updateseq('registrationdata_seq', 'registrationdata', 'registrationdata_id'); updateseq('subscription_seq', 'subscription', 'subscription_id'); diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/update-sequences.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/update-sequences.sql index c1aaadce86..749f82382c 100644 --- a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/update-sequences.sql +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/update-sequences.sql @@ -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('resourcepolicy_seq', max(policy_id)) FROM resourcepolicy; 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('subscription_seq', max(subscription_id)) FROM subscription; SELECT setval('metadatafieldregistry_seq', max(metadata_field_id)) FROM metadatafieldregistry; diff --git a/dspace-api/src/test/data/dspaceFolder/config/submission-forms.xml b/dspace-api/src/test/data/dspaceFolder/config/submission-forms.xml index f5d21d835a..f3cc2d20dc 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/submission-forms.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/submission-forms.xml @@ -46,9 +46,11 @@ - - + +

+ isAuthorOfPublication @@ -182,7 +184,7 @@ it, please enter the types and the actual numbers or codes.
- +
@@ -310,7 +312,7 @@ it, please enter the types and the actual numbers or codes.
- +
@@ -347,7 +349,7 @@ it, please enter the types and the actual numbers or codes. -
+ diff --git a/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataExportIT.java b/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataExportIT.java index d7379351e5..f767ba1663 100644 --- a/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataExportIT.java +++ b/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataExportIT.java @@ -12,9 +12,11 @@ import static junit.framework.TestCase.assertTrue; import java.io.File; import java.io.FileInputStream; import java.nio.charset.StandardCharsets; +import java.util.UUID; import org.apache.commons.cli.ParseException; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; import org.dspace.AbstractIntegrationTestWithDatabase; import org.dspace.app.launcher.ScriptLauncher; 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.Community; import org.dspace.content.Item; +import org.dspace.core.Constants; import org.dspace.scripts.DSpaceRunnable; import org.dspace.scripts.configuration.ScriptConfiguration; import org.dspace.scripts.factory.ScriptServiceFactory; @@ -100,4 +103,148 @@ public class MetadataExportIT 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()])); + } } diff --git a/dspace-oai/pom.xml b/dspace-oai/pom.xml index f2560dbf24..0bfc4d53e9 100644 --- a/dspace-oai/pom.xml +++ b/dspace-oai/pom.xml @@ -8,7 +8,7 @@ dspace-parent org.dspace - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT .. diff --git a/dspace-rdf/pom.xml b/dspace-rdf/pom.xml index c483ea5a91..7b4c7dfed3 100644 --- a/dspace-rdf/pom.xml +++ b/dspace-rdf/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT .. diff --git a/dspace-rest/pom.xml b/dspace-rest/pom.xml index c2cdbfe561..a6ad66b4d8 100644 --- a/dspace-rest/pom.xml +++ b/dspace-rest/pom.xml @@ -3,7 +3,7 @@ org.dspace dspace-rest war - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT DSpace (Deprecated) REST Webapp DSpace RESTful Web Services API. NOTE: this REST API is DEPRECATED. Please consider using the REST API in the dspace-server-webapp instead! @@ -12,7 +12,7 @@ org.dspace dspace-parent - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT .. diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 0ec415dfd0..f73d8d2448 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT .. diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ShibbolethRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ShibbolethRestController.java index 159170f8b2..f00967961c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ShibbolethRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ShibbolethRestController.java @@ -14,6 +14,7 @@ import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; import org.dspace.app.rest.model.AuthnRest; +import org.dspace.authenticate.ShibAuthentication; import org.dspace.core.Utils; import org.dspace.services.ConfigurationService; import org.slf4j.Logger; @@ -27,10 +28,27 @@ import org.springframework.web.bind.annotation.RequestParam; 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. + *

+ * 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 Giuseppe Digilio (giuseppe dot digilio at 4science dot it) + * @see ShibAuthentication + * @see org.dspace.app.rest.security.ShibbolethAuthenticationFilter */ @RequestMapping(value = "/api/" + AuthnRest.CATEGORY + "/shibboleth") @RestController @@ -56,7 +74,11 @@ public class ShibbolethRestController implements InitializingBean { @RequestMapping(method = RequestMethod.GET) public void shibboleth(HttpServletResponse response, @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"); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanCreateVersionFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanCreateVersionFeature.java new file mode 100644 index 0000000000..7c6763244b --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanCreateVersionFeature.java @@ -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 + }; + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanManageBitstreamBundlesFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanManageBitstreamBundlesFeature.java new file mode 100644 index 0000000000..9a02e9d33e --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanManageBitstreamBundlesFeature.java @@ -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 + }; + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/ManageMappedItemsFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanManageMappingsFeature.java similarity index 56% rename from dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/ManageMappedItemsFeature.java rename to dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanManageMappingsFeature.java index f0c8a13176..fbf4ee6796 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/ManageMappedItemsFeature.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanManageMappingsFeature.java @@ -8,16 +8,25 @@ package org.dspace.app.rest.authorization.impl; 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.AuthorizationFeatureDocumentation; import org.dspace.app.rest.model.BaseObjectRest; import org.dspace.app.rest.model.CollectionRest; +import org.dspace.app.rest.model.ItemRest; import org.dspace.app.rest.utils.Utils; import org.dspace.authorize.service.AuthorizeService; 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.Context; +import org.dspace.discovery.SearchServiceException; import org.springframework.beans.factory.annotation.Autowired; 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. */ @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") -public class ManageMappedItemsFeature implements AuthorizationFeature { +public class CanManageMappingsFeature implements AuthorizationFeature { - public final static String NAME = "canManageMappedItems"; + public final static String NAME = "canManageMappings"; @Autowired private AuthorizeService authorizeService; @@ -39,6 +48,12 @@ public class ManageMappedItemsFeature implements AuthorizationFeature { @Autowired private Utils utils; + @Autowired + private ItemService itemService; + + @Autowired + private CollectionService collectionService; + @Override public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException { if (object instanceof CollectionRest) { @@ -49,13 +64,26 @@ public class ManageMappedItemsFeature implements AuthorizationFeature { return true; } } + if (object instanceof ItemRest) { + Item item = itemService.find(context, UUID.fromString(((ItemRest) object).getUuid())); + try { + List 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; } @Override public String[] getSupportedTypes() { return new String[]{ - CollectionRest.CATEGORY + "." + CollectionRest.NAME + CollectionRest.CATEGORY + "." + CollectionRest.NAME, + ItemRest.CATEGORY + "." + ItemRest.NAME }; } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanManageRelationshipsFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanManageRelationshipsFeature.java new file mode 100644 index 0000000000..891e901ea2 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanManageRelationshipsFeature.java @@ -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 + }; + } +} \ No newline at end of file diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanManageVersionsFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanManageVersionsFeature.java new file mode 100644 index 0000000000..d61832e1e9 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanManageVersionsFeature.java @@ -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 + }; + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/ViewVersionsFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanSeeVersionsFeature.java similarity index 91% rename from dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/ViewVersionsFeature.java rename to dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanSeeVersionsFeature.java index ad4d2a25dd..6eca0b6c18 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/ViewVersionsFeature.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanSeeVersionsFeature.java @@ -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. */ @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") -public class ViewVersionsFeature implements AuthorizationFeature { +public class CanSeeVersionsFeature implements AuthorizationFeature { - public final static String NAME = "canViewVersions"; + public final static String NAME = "canSeeVersions"; @Autowired private ConfigurationService configurationService; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/DiscoverConfigurationConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/DiscoverConfigurationConverter.java index 79435dba83..73851bd945 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/DiscoverConfigurationConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/DiscoverConfigurationConverter.java @@ -11,6 +11,7 @@ import java.util.List; import java.util.stream.Collectors; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang.StringUtils; import org.dspace.app.rest.model.SearchConfigurationRest; import org.dspace.app.rest.projection.Projection; import org.dspace.discovery.configuration.DiscoveryConfiguration; @@ -36,7 +37,6 @@ public class DiscoverConfigurationConverter addSearchFilters(searchConfigurationRest, configuration.getSearchFilters(), configuration.getSidebarFacets()); addSortOptions(searchConfigurationRest, configuration.getSearchSortConfiguration()); - setDefaultSortOption(configuration, searchConfigurationRest); } return searchConfigurationRest; } @@ -46,23 +46,6 @@ public class DiscoverConfigurationConverter 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, List searchFilterList, List facetList) { @@ -88,8 +71,13 @@ public class DiscoverConfigurationConverter for (DiscoverySortFieldConfiguration discoverySearchSortConfiguration : CollectionUtils .emptyIfNull(searchSortConfiguration.getSortFields())) { 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.setSortOrder(discoverySearchSortConfiguration.getDefaultSortOrder().name()); searchConfigurationRest.addSortOption(sortOption); } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java index 6e48ec5478..d57e637047 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java @@ -11,18 +11,17 @@ import static org.springframework.web.servlet.DispatcherServlet.EXCEPTION_ATTRIB import java.io.IOException; import java.sql.SQLException; +import java.util.Objects; import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.dspace.app.rest.security.RestAuthenticationService; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.authorize.AuthorizeException; import org.dspace.core.Context; import org.springframework.beans.TypeMismatchException; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.repository.support.QueryMethodParameterConversionException; import org.springframework.http.HttpHeaders; @@ -59,13 +58,11 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH */ private static final Set LOG_AS_ERROR = Set.of(422); - @Autowired - private RestAuthenticationService restAuthenticationService; - @ExceptionHandler({AuthorizeException.class, RESTAuthorizationException.class, AccessDeniedException.class}) protected void handleAuthorizeException(HttpServletRequest request, HttpServletResponse response, Exception ex) 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); } else { sendErrorResponse(request, response, ex, "Authentication is required", HttpServletResponse.SC_UNAUTHORIZED); @@ -119,6 +116,14 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH 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. * Since the error messages will be exposed to the API user, the exception classes are expected to implement diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/InvalidSearchRequestException.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/InvalidSearchRequestException.java new file mode 100644 index 0000000000..cd9f288453 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/InvalidSearchRequestException.java @@ -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); + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SearchConfigurationRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SearchConfigurationRest.java index 7d8c99584c..7ec1b22500 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SearchConfigurationRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SearchConfigurationRest.java @@ -247,6 +247,7 @@ public class SearchConfigurationRest extends BaseObjectRest { @JsonIgnore private String actualName; private String name; + private String sortOrder; public void setActualName(String name) { this.actualName = name; @@ -264,6 +265,14 @@ public class SearchConfigurationRest extends BaseObjectRest { return name; } + public String getSortOrder() { + return sortOrder; + } + + public void setSortOrder(String sortOrder) { + this.sortOrder = sortOrder; + } + @Override public boolean equals(Object object) { return (object instanceof SearchConfigurationRest.SortOption && diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/EmbeddedPageHeader.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/EmbeddedPageHeader.java index 53158917be..5fe0eabc03 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/EmbeddedPageHeader.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/EmbeddedPageHeader.java @@ -12,6 +12,7 @@ import java.util.Map; import com.fasterxml.jackson.annotation.JsonProperty; 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.Sort; import org.springframework.web.util.UriComponentsBuilder; @@ -102,6 +103,8 @@ public class EmbeddedPageHeader { if (page != null) { // replace existing page & size params (if exist), otherwise append them uriComp = uriComp.replaceQueryParam("page", page); + } + if (size != Utils.DEFAULT_PAGE_SIZE) { uriComp = uriComp.replaceQueryParam("size", size); } return new Href(uriComp.build().toUriString()); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java index 59e547d449..177d149fc7 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java @@ -190,22 +190,22 @@ public class MetadataFieldRestRepository extends DSpaceRestRepository * 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 response current response * @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 * it has been used. This ensures the auth Cookie is temporary in nature, and is destroyed as soon as it is no * longer needed. + * @param request current request * @param res current response (where Cookie should be destroyed) */ - void invalidateAuthenticationCookie(HttpServletResponse res); + void invalidateAuthenticationCookie(HttpServletRequest request, HttpServletResponse res); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ShibbolethAuthenticationFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ShibbolethAuthenticationFilter.java index 736f2f48ab..b6a36d00e9 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ShibbolethAuthenticationFilter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ShibbolethAuthenticationFilter.java @@ -14,14 +14,21 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.dspace.authenticate.ShibAuthentication; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderNotFoundException; import org.springframework.security.core.Authentication; 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. + *

+ * 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) + * @see org.dspace.app.rest.ShibbolethRestController + * @see org.dspace.authenticate.ShibAuthentication */ public class ShibbolethAuthenticationFilter extends StatelessLoginFilter { @@ -33,7 +40,16 @@ public class ShibbolethAuthenticationFilter extends StatelessLoginFilter { @Override public Authentication attemptAuthentication(HttpServletRequest req, 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( new DSpaceAuthentication(null, null, new ArrayList<>()) ); @@ -44,8 +60,14 @@ public class ShibbolethAuthenticationFilter extends StatelessLoginFilter { HttpServletResponse res, FilterChain chain, 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; + // 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); chain.doFilter(req, res); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessAuthenticationFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessAuthenticationFilter.java index ebd0d3cd85..1afedf517c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessAuthenticationFilter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessAuthenticationFilter.java @@ -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 - * 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 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); return; } + // If we have a valid Authentication, save it to Spring Security if (authentication != null) { SecurityContextHolder.getContext().setAuthentication(authentication); - restAuthenticationService.invalidateAuthenticationCookie(res); } chain.doFilter(req, res); } @@ -123,7 +124,7 @@ public class StatelessAuthenticationFilter extends BasicAuthenticationFilter { Context context = ContextUtil.obtainContext(request); - EPerson eperson = restAuthenticationService.getAuthenticatedEPerson(request, context); + EPerson eperson = restAuthenticationService.getAuthenticatedEPerson(request, res, context); if (eperson != null) { //Pass the eperson ID to the request service requestService.setCurrentUserId(eperson.getID()); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessLoginFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessLoginFilter.java index f01728bd57..466687fc68 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessLoginFilter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessLoginFilter.java @@ -23,7 +23,10 @@ import org.springframework.security.web.authentication.AbstractAuthenticationPro 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 Tom Desair (tom dot desair at atmire dot com) @@ -46,6 +49,19 @@ public class StatelessLoginFilter extends AbstractAuthenticationProcessingFilter 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. + *

+ * 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 public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException { @@ -53,12 +69,28 @@ public class StatelessLoginFilter extends AbstractAuthenticationProcessingFilter String user = req.getParameter("user"); String password = req.getParameter("password"); + // Attempt to authenticate by passing user & password (if provided) to AuthenticationProvider class(es) return authenticationManager.authenticate( 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. + *

+ * 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 protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, @@ -69,6 +101,16 @@ public class StatelessLoginFilter extends AbstractAuthenticationProcessingFilter 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 protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java index 863c10b259..c7012c7568 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java @@ -89,10 +89,7 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication context.commit(); // Add newly generated auth token to the response - addTokenToResponse(response, token, addCookie); - - // Reset our CSRF token, generating a new one - resetCSRFToken(request, response); + addTokenToResponse(request, response, token, addCookie); } catch (JOSEException e) { log.error("JOSE Exception", e); @@ -125,9 +122,9 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication } @Override - public EPerson getAuthenticatedEPerson(HttpServletRequest request, Context context) { + public EPerson getAuthenticatedEPerson(HttpServletRequest request, HttpServletResponse response, Context context) { try { - String token = getLoginToken(request); + String token = getLoginToken(request, response); EPerson ePerson = null; if (token == null) { token = getShortLivedToken(request); @@ -156,22 +153,29 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication @Override public void invalidateAuthenticationData(HttpServletRequest request, HttpServletResponse response, Context context) throws Exception { - String token = getLoginToken(request); - invalidateAuthenticationCookie(response); + String token = getLoginToken(request, response); loginJWTTokenHandler.invalidateToken(token, request, context); // Reset our CSRF token, generating a new one resetCSRFToken(request, response); } + /** + * Invalidate our temporary authentication cookie by overwriting it in the response. + * @param request + * @param response + */ @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 ResponseCookie cookie = ResponseCookie.from(AUTHORIZATION_COOKIE, "") .maxAge(0).httpOnly(true).secure(true).sameSite("None").build(); // Write the cookie to the Set-Cookie header in order to send it response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); + + // Reset our CSRF token, generating a new one + resetCSRFToken(request, response); } @Override @@ -179,6 +183,18 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication 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). + *

+ * 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 public String getWwwAuthenticateHeaderValue(final HttpServletRequest request, final HttpServletResponse response) { Iterator authenticationMethodIterator @@ -195,10 +211,12 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication 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); 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("\""); } } @@ -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. *

* 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 - * (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). *

- * If 'addCookie' is false, then the JWT is only added in the Authorization header. This is recommended behavior - * as it is the most secure. For the UI (or any JS clients) the JWT must be sent in the Authorization header. + * If 'addCookie' is false, then the JWT is only added in the Authorization header & the auth cookie (if it exists) + * is removed. This ensures we are primarily using the Authorization header & remove the temporary auth cookie as + * soon as it is no longer needed. + *

+ * 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 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) { - // we need authentication cookies because Shibboleth can't use the authentication headers due to the redirects + private void addTokenToResponse(final HttpServletRequest request, final HttpServletResponse response, + 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) { ResponseCookie cookie = ResponseCookie.from(AUTHORIZATION_COOKIE, token) .httpOnly(true).secure(true).sameSite("None").build(); // Write the cookie to the Set-Cookie header in order to send it 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)); } - 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 authHeader = request.getHeader(AUTHORIZATION_HEADER); String authCookie = getAuthorizationCookie(request); @@ -254,6 +297,11 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication 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) { String authCookie = ""; Cookie[] cookies = request.getCookies(); @@ -261,12 +309,26 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication for (Cookie cookie : cookies) { if (cookie.getName().equals(AUTHORIZATION_COOKIE) && StringUtils.isNotEmpty(cookie.getValue())) { authCookie = cookie.getValue(); + break; } } } 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. * This method is used internally during login/logout to ensure a new CSRF token is generated anytime authentication diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/ItemMetadataValueAddPatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/ItemMetadataValueAddPatchOperation.java index bf146d5aae..e749c4e793 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/ItemMetadataValueAddPatchOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/ItemMetadataValueAddPatchOperation.java @@ -8,16 +8,28 @@ package org.dspace.app.rest.submit.factory.impl; import java.sql.SQLException; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Optional; 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.patch.LateObjectEvaluator; +import org.dspace.authorize.AuthorizeException; import org.dspace.content.InProgressSubmission; import org.dspace.content.Item; 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.RelationshipService; +import org.dspace.core.Constants; import org.dspace.core.Context; +import org.dspace.core.Utils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.Assert; @@ -66,9 +78,18 @@ import org.springframework.util.Assert; */ public class ItemMetadataValueAddPatchOperation extends MetadataValueAddPatchOperation { + /** + * log4j category + */ + private static final Logger log = + org.apache.logging.log4j.LogManager.getLogger(ItemMetadataValueAddPatchOperation.class); + @Autowired ItemService itemService; + @Autowired + RelationshipService relationshipService; + @Override void add(Context context, HttpServletRequest currentRequest, InProgressSubmission source, String path, Object value) throws SQLException { @@ -109,6 +130,102 @@ public class ItemMetadataValueAddPatchOperation extends MetadataValueAddPatchOpe } + protected void replaceValue(Context context, Item source, String target, List list) + throws SQLException { + String[] metadata = Utils.tokenize(target); + + // fetch pre-existent metadata + List preExistentMetadata = + getDSpaceObjectService().getMetadata(source, metadata[0], metadata[1], metadata[2], Item.ANY); + + // fetch pre-existent relationships + Map 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 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 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 preExistentRelationships(Context context, + List preExistentMetadata) throws SQLException { + Map relationshipsMap = new HashMap(); + 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 protected ItemService getDSpaceObjectService() { return itemService; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DiscoverQueryBuilder.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DiscoverQueryBuilder.java index 3afbdfb8a3..c666d9d01d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DiscoverQueryBuilder.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DiscoverQueryBuilder.java @@ -14,12 +14,14 @@ import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.dspace.app.rest.converter.query.SearchQueryConverter; import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.InvalidSearchRequestException; import org.dspace.app.rest.parameter.SearchFilter; import org.dspace.core.Context; 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 if (sortBy == null) { 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) { - if (searchSortConfiguration != null) { - sortOrder = searchSortConfiguration.getDefaultSortOrder() - .toString(); + if (Objects.nonNull(searchSortConfiguration.getSortFields()) && + !searchSortConfiguration.getSortFields().isEmpty()) { + sortOrder = searchSortConfiguration.getSortFields().get(0).getDefaultSortOrder().name(); } return sortOrder; } @@ -332,8 +343,12 @@ public class DiscoverQueryBuilder implements InitializingBean { private String getDefaultSortField(DiscoverySortConfiguration searchSortConfiguration) { String sortBy;// Attempt to find the default one, if none found we use SCORE sortBy = "score"; - if (searchSortConfiguration != null && searchSortConfiguration.getDefaultSort() != null) { - DiscoverySortFieldConfiguration defaultSort = searchSortConfiguration.getDefaultSort(); + if (Objects.nonNull(searchSortConfiguration.getSortFields()) && + !searchSortConfiguration.getSortFields().isEmpty()) { + DiscoverySortFieldConfiguration defaultSort = searchSortConfiguration.getSortFields().get(0); + if (StringUtils.isBlank(defaultSort.getMetadataField())) { + return sortBy; + } sortBy = defaultSort.getMetadataField(); } return sortBy; @@ -378,7 +393,8 @@ public class DiscoverQueryBuilder implements InitializingBean { DiscoverFilterQuery filterQuery = searchService.toFilterQuery(context, filter.getIndexFieldName(), searchFilter.getOperator(), - searchFilter.getValue()); + searchFilter.getValue(), + discoveryConfiguration); if (filterQuery != null) { filterQueries.add(filterQuery.getFilterQuery()); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java index e7c045fde9..1dbb08a3b3 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java @@ -108,7 +108,7 @@ public class Utils { /** * 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. diff --git a/dspace-server-webapp/src/main/resources/application.properties b/dspace-server-webapp/src/main/resources/application.properties index 2b8463e18a..18e1c6dfa3 100644 --- a/dspace-server-webapp/src/main/resources/application.properties +++ b/dspace-server-webapp/src/main/resources/application.properties @@ -77,6 +77,18 @@ spring.http.encoding.force=true # However, you may wish to set this to "always" in your 'local.cfg' for development or debugging purposes. server.error.include-stacktrace = never +# Spring Boot proxy configuration (can be overridden in local.cfg). +# By default, Spring Boot does not automatically use X-Forwarded-* Headers when generating links (and similar) in the +# DSpace REST API. Three options are currently supported by Spring Boot: +# * NATIVE = allows your web server to natively support standard Forwarded headers +# * FRAMEWORK = (DSpace default) enables Spring Framework's built in filter to manage these headers in Spring Boot. +# This setting is used by default to support all X-Forwarded-* headers, as the DSpace backend is often +# installed behind Apache HTTPD or Nginx proxy (both of which pass those headers to Tomcat). +# * NONE = (Spring default) Forwarded headers are ignored +# For more information see +# https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto-use-behind-a-proxy-server +server.forward-headers-strategy=FRAMEWORK + ###################### # Spring Boot Autoconfigure # @@ -107,6 +119,9 @@ spring.main.allow-bean-definition-overriding = true # Log4J configuration logging.config = ${dspace.dir}/config/log4j2.xml +################################## +# Spring MVC file upload settings +# # Maximum size of a single uploaded file (default = 1MB) spring.servlet.multipart.max-file-size = 512MB diff --git a/dspace-server-webapp/src/main/webapp/login.html b/dspace-server-webapp/src/main/webapp/login.html index d66bc843a4..47752ded07 100644 --- a/dspace-server-webapp/src/main/webapp/login.html +++ b/dspace-server-webapp/src/main/webapp/login.html @@ -98,39 +98,62 @@ "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({ - 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); + url : window.location.href.replace("login.html", "") + 'api/authn/status', + type : 'GET', + success : function(result, status, xhr) { + // 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 - if (xhr.status === 401) { - var authenticate = xhr.getResponseHeader("WWW-Authenticate"); - var element = $('div.other-login-methods'); - if(authenticate !== null) { - var realms = authenticate.match(/(\w+ (\w+=((".*?")|[^,]*)(, )?)*)/g); - if (realms.length == 1){ - var loc = /location="([^,]*)"/.exec(authenticate); - if (loc !== null && loc.length === 2) { - document.location = loc[1]; - } - } else if (realms.length > 1){ - for (var i = 0; i < realms.length; i++){ - addLocationButton(realms[i], element); - } - } + // Check for WWW-Authenticate header. If found, this means we are not yet authenticated, and + // therefore we need to display available authentication options. + var authenticate = xhr.getResponseHeader("WWW-Authenticate"); + if (authenticate !== null) { + var element = $('div.other-login-methods'); + var realms = authenticate.match(/(\w+ (\w+=((".*?")|[^,]*)(, )?)*)/g); + if (realms.length == 1){ + var loc = /location="([^,]*)"/.exec(authenticate); + if (loc !== null && loc.length === 2) { + document.location = loc[1]; + } + } else if (realms.length > 1){ + for (var i = 0; i < realms.length; i++){ + 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) { event.preventDefault(); $.ajax({ @@ -191,7 +216,9 @@ } }, 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'); } }); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java index de9fcc2aa9..27efd9ff46 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java @@ -17,6 +17,7 @@ import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.startsWith; import static org.junit.Assert.assertEquals; 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.test.web.servlet.request.MockMvcRequestBuilders.get; 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.header; 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 java.io.InputStream; @@ -164,35 +166,111 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio @Test public void testStatusShibAuthenticatedWithCookie() throws Exception { - //Enable Shibboleth login + //Enable Shibboleth login only configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", SHIB_ONLY); - //Simulate that a shibboleth authentication has happened - String token = getClient().perform(post("/api/authn/login") + String uiURL = configurationService.getProperty("dspace.ui.url"); + + // 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-SCOPED-AFFILIATION", "faculty;staff")) - .andExpect(status().isOk()) - .andReturn().getResponse().getHeader(AUTHORIZATION_HEADER).replace("Bearer ", ""); + .andExpect(status().is3xxRedirection()) + .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]; - cookies[0] = new Cookie(AUTHORIZATION_COOKIE, token); + // Verify the temporary cookie now exists & obtain its token for use below + assertNotNull(authCookie); + String token = authCookie.getValue(); - //Check if we are authenticated with a status request with authorization cookie - getClient().perform(get("/api/authn/status") - .secure(true) - .cookie(cookies)) + // This step is _not required_ to successfully authenticate, but it mocks the behavior of our UI & HAL Browser. + // We'll send a "/status" request to the REST API with our auth cookie. This should return that we have a + // *valid* authentication (as auth cookie is valid), however the cookie will remain. To complete the login + // 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()) - //We expect the content type to be "application/hal+json;charset=UTF-8" .andExpect(content().contentType(contentType)) .andExpect(jsonPath("$.okay", is(true))) .andExpect(jsonPath("$.authenticated", is(true))) .andExpect(jsonPath("$.type", is("status"))); - //Logout - getClient(token).perform(post("/api/authn/logout")) + //Logout, invalidating the token + getClient(token).perform(post("/api/authn/logout").header("Origin", uiURL)) .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 public void testStatusPasswordAuthenticatedWithCookie() throws Exception { // 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 token = token.replace("Bearer ", ""); - // Save token to an Authorization cookie - Cookie[] cookies = new Cookie[1]; - cookies[0] = new Cookie(AUTHORIZATION_COOKIE, token); + // Fake the creation of an auth cookie, just for testing. (Currently, it's not possible to create an auth cookie + // via Password auth, but this test proves it would work if enabled) + Cookie authCookie = new Cookie(AUTHORIZATION_COOKIE, token); - //Check if we are authenticated with a status request using authorization cookie - getClient().perform(get("/api/authn/status") - .secure(true) - .cookie(cookies)) + // Now, similar to how both the UI & Hal Browser authentication works, send a "/status" request to the REST API + // with our auth cookie. This should return that we *have a valid* authentication (in the auth cookie). + // However, this is just a validation check, so this auth cookie will remain. To complete the login process + // 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()) - //We expect the content type to be "application/hal+json" .andExpect(content().contentType(contentType)) .andExpect(jsonPath("$.okay", is(true))) .andExpect(jsonPath("$.authenticated", is(true))) - .andExpect(jsonPath("$.type", is("status"))); - //Logout + .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. + 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")) .andExpect(status().isNoContent()); } @@ -1087,7 +1193,7 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio String loginToken = getAuthToken(eperson.getEmail(), password); getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content?authentication-token=" + loginToken)) - .andExpect(status().isForbidden()); + .andExpect(status().isUnauthorized()); } @Test @@ -1098,7 +1204,7 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio Thread.sleep(1); getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content?authentication-token=" + shortLivedToken)) - .andExpect(status().isForbidden()); + .andExpect(status().isUnauthorized()); } @Test diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CommunityRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CommunityRestRepositoryIT.java index 19f25e5769..7ce1acb85f 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CommunityRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CommunityRestRepositoryIT.java @@ -2478,4 +2478,118 @@ public class CommunityRestRepositoryIT extends AbstractControllerIntegrationTest getClient().perform(get("/api/core/communities/search/findAdminAuthorized")) .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))); + } + } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryRestControllerIT.java index 39cfb495a3..5ef9f91d34 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryRestControllerIT.java @@ -55,6 +55,7 @@ import org.dspace.content.Item; import org.dspace.content.WorkspaceItem; import org.dspace.content.authority.Choices; import org.dspace.content.authority.service.MetadataAuthorityService; +import org.dspace.discovery.configuration.DiscoverySortFieldConfiguration; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.services.ConfigurationService; @@ -988,14 +989,74 @@ public class DiscoveryRestControllerIT extends AbstractControllerIntegrationTest SearchFilterMatcher.isJournalOfPublicationRelation() ))) //These sortOptions need to be present as it's the default in the configuration - .andExpect(jsonPath("$.sortOptions", containsInAnyOrder( - SortOptionMatcher.titleSortOption(), - SortOptionMatcher.dateIssuedSortOption(), - SortOptionMatcher.dateAccessionedSortOption(), - SortOptionMatcher.scoreSortOption() + .andExpect(jsonPath("$.sortOptions", contains( + SortOptionMatcher.sortOptionMatcher( + "score", DiscoverySortFieldConfiguration.SORT_ORDER.desc.name()), + SortOptionMatcher.sortOptionMatcher( + "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 public void discoverSearchObjectsTest() throws Exception { @@ -5177,6 +5238,137 @@ public class DiscoveryRestControllerIT extends AbstractControllerIntegrationTest .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 public void discoverSearchPoolTaskObjectsTest() throws Exception { context.turnOffAuthorisationSystem(); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/PatchMetadataIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/PatchMetadataIT.java index 02920d77bb..7c57e98f1c 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/PatchMetadataIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/PatchMetadataIT.java @@ -25,6 +25,7 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.IntStream; import org.dspace.app.rest.matcher.MetadataMatcher; 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.RelationshipTypeService; import org.dspace.content.service.WorkspaceItemService; +import org.hamcrest.Matcher; import org.hamcrest.Matchers; import org.junit.After; import org.junit.Before; @@ -81,6 +83,8 @@ public class PatchMetadataIT extends AbstractEntityIntegrationTest { private List authorsOriginalOrder; + private List authorsMetadataOriginalOrder; + private AtomicReference idRef1; private AtomicReference idRef2; @@ -202,19 +206,19 @@ public class PatchMetadataIT extends AbstractEntityIntegrationTest { .andDo(result -> idRef2.set(read(result.getResponse().getContentAsString(), "$.id"))); publication = workspaceItemService.find(context, publicationItem.getID()); - List publicationAuthorList = + authorsMetadataOriginalOrder = itemService.getMetadata(publication.getItem(), "dc", "contributor", "author", Item.ANY); - assertEquals(publicationAuthorList.size(), 5); - assertThat(publicationAuthorList.get(0).getValue(), equalTo(authorsOriginalOrder.get(0))); - assertThat(publicationAuthorList.get(0).getAuthority(), not(startsWith("virtual::"))); - assertThat(publicationAuthorList.get(1).getValue(), equalTo(authorsOriginalOrder.get(1))); - assertThat(publicationAuthorList.get(1).getAuthority(), startsWith("virtual::")); - assertThat(publicationAuthorList.get(2).getValue(), equalTo(authorsOriginalOrder.get(2))); - assertThat(publicationAuthorList.get(2).getAuthority(), not(startsWith("virtual::"))); - assertThat(publicationAuthorList.get(3).getValue(), equalTo(authorsOriginalOrder.get(3))); - assertThat(publicationAuthorList.get(3).getAuthority(), not(startsWith("virtual::"))); - assertThat(publicationAuthorList.get(4).getValue(), equalTo(authorsOriginalOrder.get(4))); - assertThat(publicationAuthorList.get(4).getAuthority(), startsWith("virtual::")); + assertEquals(authorsMetadataOriginalOrder.size(), 5); + assertThat(authorsMetadataOriginalOrder.get(0).getValue(), equalTo(authorsOriginalOrder.get(0))); + assertThat(authorsMetadataOriginalOrder.get(0).getAuthority(), not(startsWith("virtual::"))); + assertThat(authorsMetadataOriginalOrder.get(1).getValue(), equalTo(authorsOriginalOrder.get(1))); + assertThat(authorsMetadataOriginalOrder.get(1).getAuthority(), startsWith("virtual::")); + assertThat(authorsMetadataOriginalOrder.get(2).getValue(), equalTo(authorsOriginalOrder.get(2))); + assertThat(authorsMetadataOriginalOrder.get(2).getAuthority(), not(startsWith("virtual::"))); + assertThat(authorsMetadataOriginalOrder.get(3).getValue(), equalTo(authorsOriginalOrder.get(3))); + assertThat(authorsMetadataOriginalOrder.get(3).getAuthority(), not(startsWith("virtual::"))); + assertThat(authorsMetadataOriginalOrder.get(4).getValue(), equalTo(authorsOriginalOrder.get(4))); + 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 expectedValues = new ArrayList(); + 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 ops = new ArrayList(); + List value = new ArrayList(); + + 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 metadataValues) throws Exception { + List ops = new ArrayList(); + List value = new ArrayList(); + + // 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> 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 * metadata field "dc.contributor.author". diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ShibbolethRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ShibbolethRestControllerIT.java index 368714300b..4d3e99c449 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ShibbolethRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ShibbolethRestControllerIT.java @@ -27,58 +27,96 @@ public class ShibbolethRestControllerIT extends AbstractControllerIntegrationTes @Autowired ConfigurationService configurationService; + public static final String[] PASS_ONLY = {"org.dspace.authenticate.PasswordAuthentication"}; + public static final String[] SHIB_ONLY = {"org.dspace.authenticate.ShibAuthentication"}; @Before public void setup() throws Exception { super.setUp(); + // Add a second trusted host for some tests configurationService.setProperty("rest.cors.allowed-origins", "${dspace.ui.url}, http://anotherdspacehost:4000"); + + // Enable Shibboleth login for all tests + configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", SHIB_ONLY); } @Test public void testRedirectToDefaultDspaceUrl() throws Exception { - String token = getAuthToken(eperson.getEmail(), password); - - getClient(token).perform(get("/api/authn/shibboleth")) + // 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. + // 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(redirectedUrl("http://localhost:4000")); } @Test public void testRedirectToGivenTrustedUrl() throws Exception { - - String token = getAuthToken(eperson.getEmail(), password); - - getClient(token).perform(get("/api/authn/shibboleth") - .param("redirectUrl", "http://localhost:8080/server/api/authn/status")) + getClient().perform(get("/api/authn/shibboleth") + .param("redirectUrl", "http://localhost:8080/server/api/authn/status") + .requestAttr("SHIB-MAIL", eperson.getEmail())) .andExpect(status().is3xxRedirection()) .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 public void testRedirectToAnotherGivenTrustedUrl() throws Exception { String token = getAuthToken(eperson.getEmail(), password); - getClient(token).perform(get("/api/authn/shibboleth") - .param("redirectUrl", "http://anotherdspacehost:4000/home")) + getClient().perform(get("/api/authn/shibboleth") + .param("redirectUrl", "http://anotherdspacehost:4000/home") + .requestAttr("SHIB-MAIL", eperson.getEmail())) .andExpect(status().is3xxRedirection()) .andExpect(redirectedUrl("http://anotherdspacehost:4000/home")); } @Test 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. - getClient(token).perform(get("/api/authn/shibboleth") - .param("redirectUrl", "http://dspace.org")) - .andExpect(status().isBadRequest()); + getClient().perform(get("/api/authn/shibboleth") + .param("redirectUrl", "http://dspace.org") + .requestAttr("SHIB-MAIL", eperson.getEmail())) + .andExpect(status().isBadRequest()); } @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")) - .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()); } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/StatisticsRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/StatisticsRestRepositoryIT.java index 08303e57f2..a721a53687 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/StatisticsRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/StatisticsRestRepositoryIT.java @@ -179,7 +179,7 @@ public class StatisticsRestRepositoryIT extends AbstractControllerIntegrationTes getClient("unvalidToken").perform( get("/api/statistics/usagereports/" + itemNotVisitedWithBitstreams.getID() + "_" + TOTAL_VISITS_REPORT_ID)) // ** THEN ** - .andExpect(status().isForbidden()); + .andExpect(status().isUnauthorized()); } @Test @@ -829,7 +829,7 @@ public class StatisticsRestRepositoryIT extends AbstractControllerIntegrationTes .perform(get("/api/statistics/usagereports/search/object?uri=http://localhost:8080/server/api/core" + "/items/" + itemNotVisitedWithBitstreams.getID())) // ** THEN ** - .andExpect(status().isForbidden()); + .andExpect(status().isUnauthorized()); } @Test diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowActionRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowActionRestRepositoryIT.java index 884fc6cfa5..de687ebd9d 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowActionRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowActionRestRepositoryIT.java @@ -49,8 +49,8 @@ public class WorkflowActionRestRepositoryIT extends AbstractControllerIntegratio String token = "nonValidToken"; //When we call this facets endpoint getClient(token).perform(get(WORKFLOW_ACTIONS_ENDPOINT)) - //We expect a 403 Forbidden status - .andExpect(status().isForbidden()); + //We expect a 401 Unauthorized status + .andExpect(status().isUnauthorized()); } @Test @@ -112,8 +112,8 @@ public class WorkflowActionRestRepositoryIT extends AbstractControllerIntegratio WorkflowActionConfig existentWorkflow = xmlWorkflowFactory.getActionByName(nameActionWithOptions); //When we call this facets endpoint getClient(token).perform(get(WORKFLOW_ACTIONS_ENDPOINT + "/" + nameActionWithOptions)) - //We expect a 403 Forbidden status - .andExpect(status().isForbidden()); + //We expect a 401 Unauthorized status + .andExpect(status().isUnauthorized()); } @Test diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowDefinitionRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowDefinitionRestRepositoryIT.java index 3f7ae74000..445ce87abc 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowDefinitionRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowDefinitionRestRepositoryIT.java @@ -121,8 +121,8 @@ public class WorkflowDefinitionRestRepositoryIT extends AbstractControllerIntegr String token = "NonValidToken"; //When we call this facets endpoint getClient(token).perform(get(WORKFLOW_DEFINITIONS_ENDPOINT)) - //We expect a 403 Forbidden status - .andExpect(status().isForbidden()); + //We expect a 401 Unauthorized status + .andExpect(status().isUnauthorized()); } @Test @@ -192,8 +192,8 @@ public class WorkflowDefinitionRestRepositoryIT extends AbstractControllerIntegr String workflowName = defaultWorkflow.getID(); //When we call this facets endpoint getClient(token).perform(get(WORKFLOW_DEFINITIONS_ENDPOINT + "/" + workflowName)) - //We expect a 403 Forbidden status - .andExpect(status().isForbidden()); + //We expect a 401 Unauthorized status + .andExpect(status().isUnauthorized()); } @Test @@ -402,8 +402,8 @@ public class WorkflowDefinitionRestRepositoryIT extends AbstractControllerIntegr //When we call this facets endpoint getClient(token).perform(get(WORKFLOW_DEFINITIONS_ENDPOINT + "/" + defaultWorkflow.getID() + "/collections")) - //We expect a 403 Forbidden status - .andExpect(status().isForbidden()); + //We expect a 401 Unauthorized status + .andExpect(status().isUnauthorized()); } @Test @@ -441,8 +441,8 @@ public class WorkflowDefinitionRestRepositoryIT extends AbstractControllerIntegr //When we call this facets endpoint getClient(token).perform(get(WORKFLOW_DEFINITIONS_ENDPOINT + "/" + defaultWorkflow.getID() + "/steps")) - //We expect a 403 Forbidden status - .andExpect(status().isForbidden()); + //We expect a 401 Unauthorized status + .andExpect(status().isUnauthorized()); } @Test diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowStepRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowStepRestRepositoryIT.java index a9a5b12d94..e06ba08f69 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowStepRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowStepRestRepositoryIT.java @@ -47,8 +47,8 @@ public class WorkflowStepRestRepositoryIT extends AbstractControllerIntegrationT String token = "NonValidToken"; //When we call this facets endpoint getClient(token).perform(get(WORKFLOW_ACTIONS_ENDPOINT)) - //We expect a 403 Forbidden status - .andExpect(status().isForbidden()); + //We expect a 401 Unauthorized status + .andExpect(status().isUnauthorized()); } @Test diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanCreateVersionFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanCreateVersionFeatureIT.java new file mode 100644 index 0000000000..9ead8f8706 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanCreateVersionFeatureIT.java @@ -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()); + + } +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanManageBitstreamBundlesFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanManageBitstreamBundlesFeatureIT.java new file mode 100644 index 0000000000..6e29c5b949 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanManageBitstreamBundlesFeatureIT.java @@ -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()); + + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/ManageMappedItemsFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanManageMappingsFeatureIT.java similarity index 62% rename from dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/ManageMappedItemsFeatureIT.java rename to dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanManageMappingsFeatureIT.java index 566b980a51..1afb62a6ff 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/ManageMappedItemsFeatureIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanManageMappingsFeatureIT.java @@ -17,17 +17,24 @@ import java.io.InputStream; import org.apache.commons.codec.CharEncoding; 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.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.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.test.AbstractControllerIntegrationTest; import org.dspace.app.rest.utils.Utils; +import org.dspace.authorize.service.AuthorizeService; import org.dspace.builder.BitstreamBuilder; import org.dspace.builder.BundleBuilder; 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.Bitstream; @@ -36,14 +43,16 @@ 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 canManageMappedItems authorization feature + * Test for the canManageMappings authorization feature. */ -public class ManageMappedItemsFeatureIT extends AbstractControllerIntegrationTest { +public class CanManageMappingsFeatureIT extends AbstractControllerIntegrationTest { @Autowired private Utils utils; @@ -54,15 +63,27 @@ public class ManageMappedItemsFeatureIT extends AbstractControllerIntegrationTes @Autowired private BitstreamConverter bitstreamConverter; + @Autowired + private ItemConverter itemConverter; + + @Autowired + private AuthorizeService authorizeService; + + @Autowired + private AuthorizationFeatureService authorizationFeatureService; + + private EPerson userA; private Community communityA; private Collection collectionA; + private Collection collectionB; private CollectionRest collectionARest; private Item itemA; private Bitstream bitstreamA; private BitstreamRest bitstreamARest; private Bundle bundleA; + private AuthorizationFeature canManageMappingsFeature; - final String feature = "canManageMappedItems"; + final String feature = "canManageMappings"; @Override @Before @@ -70,12 +91,19 @@ public class ManageMappedItemsFeatureIT extends AbstractControllerIntegrationTes super.setUp(); context.turnOffAuthorisationSystem(); + userA = 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(); + collectionB = CollectionBuilder.createCollection(context, communityA) + .withName("collectionB") + .build(); itemA = ItemBuilder.createItem(context, collectionA) .withTitle("itemA") .build(); @@ -88,7 +116,7 @@ public class ManageMappedItemsFeatureIT extends AbstractControllerIntegrationTes .withName("bistreamA") .build(); } - + canManageMappingsFeature = authorizationFeatureService.find(CanManageMappingsFeature.NAME); context.restoreAuthSystemState(); collectionARest = collectionConverter.convert(collectionA, Projection.DEFAULT); @@ -188,4 +216,65 @@ public class ManageMappedItemsFeatureIT extends AbstractControllerIntegrationTes .andExpect(jsonPath("$.page.totalElements", is(0))) .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()); + + } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanManageRelationshipsFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanManageRelationshipsFeatureIT.java new file mode 100644 index 0000000000..961fae54ba --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanManageRelationshipsFeatureIT.java @@ -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()); + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanManageVersionsFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanManageVersionsFeatureIT.java new file mode 100644 index 0000000000..1fd51a9a75 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanManageVersionsFeatureIT.java @@ -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()); + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/ViewVersionsFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanSeeVersionsFeatureIT.java similarity index 98% rename from dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/ViewVersionsFeatureIT.java rename to dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanSeeVersionsFeatureIT.java index f3758879f7..5fae0b9410 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/ViewVersionsFeatureIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanSeeVersionsFeatureIT.java @@ -41,9 +41,9 @@ import org.junit.Test; 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 private Utils utils; @@ -68,7 +68,7 @@ public class ViewVersionsFeatureIT extends AbstractControllerIntegrationTest { private BitstreamRest bitstreamARest; private Bundle bundleA; - final String feature = "canViewVersions"; + final String feature = "canSeeVersions"; @Override @Before diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/converter/DiscoverConfigurationConverterTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/converter/DiscoverConfigurationConverterTest.java index f69c6b85e9..b4440616d7 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/converter/DiscoverConfigurationConverterTest.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/converter/DiscoverConfigurationConverterTest.java @@ -99,9 +99,11 @@ public class DiscoverConfigurationConverterTest { DiscoverySortFieldConfiguration discoverySortFieldConfiguration = new DiscoverySortFieldConfiguration(); discoverySortFieldConfiguration.setMetadataField("title"); discoverySortFieldConfiguration.setType("text"); + discoverySortFieldConfiguration.setDefaultSortOrder(DiscoverySortFieldConfiguration.SORT_ORDER.asc); DiscoverySortFieldConfiguration discoverySortFieldConfiguration1 = new DiscoverySortFieldConfiguration(); discoverySortFieldConfiguration1.setMetadataField("author"); discoverySortFieldConfiguration1.setType("text"); + discoverySortFieldConfiguration1.setDefaultSortOrder(DiscoverySortFieldConfiguration.SORT_ORDER.asc); LinkedList mockedList = new LinkedList<>(); mockedList.add(discoverySortFieldConfiguration); mockedList.add(discoverySortFieldConfiguration1); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SortOptionMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SortOptionMatcher.java index caef5ecb58..a80f22a076 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SortOptionMatcher.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SortOptionMatcher.java @@ -48,4 +48,10 @@ public class SortOptionMatcher { ); } + public static Matcher sortOptionMatcher(String name, String sortDirection) { + return allOf( + hasJsonPath("$.name", is(name)), + hasJsonPath("$.sortOrder", is(sortDirection)) + ); + } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/model/hateoas/EmbeddedPageHeaderTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/model/hateoas/EmbeddedPageHeaderTest.java index fceabddd07..37a00a5a81 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/model/hateoas/EmbeddedPageHeaderTest.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/model/hateoas/EmbeddedPageHeaderTest.java @@ -86,7 +86,7 @@ public class EmbeddedPageHeaderTest { Map links = embeddedPageHeader.getLinks(); // "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. assertFalse(links.containsKey("first")); // "prev" should not exist, as we are on the first page. diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/utils/DiscoverQueryBuilderTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/utils/DiscoverQueryBuilderTest.java index 5a1e7cd1a9..9a8f07e76a 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/utils/DiscoverQueryBuilderTest.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/utils/DiscoverQueryBuilderTest.java @@ -28,6 +28,7 @@ import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.when; import java.sql.SQLException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedList; @@ -35,6 +36,7 @@ import java.util.List; import java.util.function.Function; import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.InvalidSearchRequestException; import org.dspace.app.rest.parameter.SearchFilter; import org.dspace.core.Context; import org.dspace.discovery.DiscoverFacetField; @@ -111,7 +113,8 @@ public class DiscoverQueryBuilderTest { any(), any(DiscoverQuery.class))) .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], invocation.getArguments()[1] + ":\"" + invocation.getArguments()[3] + "\"", (String) invocation.getArguments()[3])); @@ -139,17 +142,23 @@ public class DiscoverQueryBuilderTest { discoveryConfiguration.setHitHighlightingConfiguration(discoveryHitHighlightingConfiguration); - DiscoverySortConfiguration sortConfiguration = new DiscoverySortConfiguration(); - DiscoverySortFieldConfiguration defaultSort = new DiscoverySortFieldConfiguration(); defaultSort.setMetadataField("dc.date.accessioned"); defaultSort.setType(DiscoveryConfigurationParameters.TYPE_DATE); - sortConfiguration.setDefaultSort(defaultSort); - sortConfiguration.setDefaultSortOrder(DiscoverySortConfiguration.SORT_ORDER.desc); + defaultSort.setDefaultSortOrder(DiscoverySortFieldConfiguration.SORT_ORDER.desc); + + + List listSortField = new ArrayList(); + listSortField.add(defaultSort); + + DiscoverySortConfiguration sortConfiguration = new DiscoverySortConfiguration(); DiscoverySortFieldConfiguration titleSort = new DiscoverySortFieldConfiguration(); titleSort.setMetadataField("dc.title"); - sortConfiguration.setSortFields(Arrays.asList(titleSort)); + titleSort.setDefaultSortOrder(DiscoverySortFieldConfiguration.SORT_ORDER.asc); + listSortField.add(titleSort); + + sortConfiguration.setSortFields(listSortField); discoveryConfiguration.setSearchSortConfiguration(sortConfiguration); @@ -266,7 +275,7 @@ public class DiscoverQueryBuilderTest { .buildQuery(context, scope, discoveryConfiguration, query, Arrays.asList(searchFilter), "TEST", page); } - @Test(expected = DSpaceBadRequestException.class) + @Test(expected = InvalidSearchRequestException.class) public void testInvalidSortField() throws Exception { page = PageRequest.of(2, 10, Sort.Direction.ASC, "test"); queryBuilder @@ -283,7 +292,8 @@ public class DiscoverQueryBuilderTest { @Test(expected = DSpaceBadRequestException.class) 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); queryBuilder diff --git a/dspace-services/pom.xml b/dspace-services/pom.xml index 88a68b6a6e..4cca053d16 100644 --- a/dspace-services/pom.xml +++ b/dspace-services/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT diff --git a/dspace-services/src/main/java/org/dspace/servicemanager/DSpaceServiceManager.java b/dspace-services/src/main/java/org/dspace/servicemanager/DSpaceServiceManager.java index c17b31f68d..afd1627f5e 100644 --- a/dspace-services/src/main/java/org/dspace/servicemanager/DSpaceServiceManager.java +++ b/dspace-services/src/main/java/org/dspace/servicemanager/DSpaceServiceManager.java @@ -426,10 +426,9 @@ public final class DSpaceServiceManager implements ServiceManagerSystem { service = (T) applicationContext.getBean(name, type); } catch (BeansException e) { // no luck, try the fall back option - log.info( + log.warn( "Unable to locate bean by name or id={}." - + " Will try to look up bean by type next." - + " BeansException: {}", name, e.getMessage()); + + " Will try to look up bean by type next.", name, e); service = null; } } else { @@ -438,9 +437,8 @@ public final class DSpaceServiceManager implements ServiceManagerSystem { service = (T) applicationContext.getBean(type.getName(), type); } catch (BeansException e) { // no luck, try the fall back option - log.info("Unable to locate bean by name or id={}." - + " Will try to look up bean by type next." - + " BeansException: {}", type.getName(), e.getMessage()); + log.warn("Unable to locate bean by name or id={}." + + " Will try to look up bean by type next.", type.getName(), e); service = null; } } diff --git a/dspace-sword/pom.xml b/dspace-sword/pom.xml index f98d4a242d..a1d5700e54 100644 --- a/dspace-sword/pom.xml +++ b/dspace-sword/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT .. diff --git a/dspace-swordv2/pom.xml b/dspace-swordv2/pom.xml index bd97b231df..155c8656a1 100644 --- a/dspace-swordv2/pom.xml +++ b/dspace-swordv2/pom.xml @@ -13,7 +13,7 @@ org.dspace dspace-parent - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT .. diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 2c55e6ace4..af00a281f3 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -20,11 +20,15 @@ dspace.dir = /dspace # URL of DSpace backend ('server' webapp). Include port number etc. -# This is where REST API and all enabled server modules (OAI-PMH, SWORD, SWORDv2, RDF, etc) will respond +# DO NOT end it with '/'. +# This is where REST API and all enabled server modules (OAI-PMH, SWORD, +# SWORDv2, RDF, etc) will respond. dspace.server.url = http://localhost:8080/server -# URL of DSpace frontend (Angular UI). Include port number etc -# This is used by the backend to provide links in emails, RSS feeds, Sitemaps, etc. +# URL of DSpace frontend (Angular UI). Include port number etc. +# DO NOT end it with '/'. +# This is used by the backend to provide links in emails, RSS feeds, Sitemaps, +# etc. dspace.ui.url = http://localhost:4000 # Name of the site @@ -120,8 +124,8 @@ db.removeabandonedtimeout = 300 mail.server = smtp.example.com # SMTP mail server authentication username and password (if required) -mail.server.username = -mail.server.password = +#mail.server.username = +#mail.server.password = # SMTP mail server alternate port (defaults to 25) mail.server.port = 25 @@ -371,16 +375,6 @@ useProxies = true # (Requires reboot of servlet container, e.g. Tomcat, to reload) #proxies.trusted.include_ui_ip = true -# Spring Boot proxy configuration (can be set in local.cfg or in application.properties). -# By default, Spring Boot does not automatically use X-Forwarded-* Headers when generating links (and similar) in the -# REST API. When using a proxy in front of the REST API, you may need to modify this setting: -# * NATIVE = allows your web server to natively support standard Forwarded headers -# * FRAMEWORK = enables Spring Framework's built in filter to manage these headers in Spring Boot -# * NONE = default value. Forwarded headers are ignored -# For more information see -# https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto-use-behind-a-proxy-server -#server.forward-headers-strategy=FRAMEWORK - #### Media Filter / Format Filter plugins (through PluginService) #### # Media/Format Filters help to full-text index content or # perform automated format conversions diff --git a/dspace/config/entities/relationship-types.xml b/dspace/config/entities/relationship-types.xml index 4840afdc0e..7ae1ff6116 100644 --- a/dspace/config/entities/relationship-types.xml +++ b/dspace/config/entities/relationship-types.xml @@ -89,7 +89,7 @@ 0 - 1 + 0 @@ -101,8 +101,7 @@ 0 - 1 - 1 + 0 @@ -128,7 +127,6 @@ 0 - 1 true diff --git a/dspace/config/item-submission.xml b/dspace/config/item-submission.xml index 473cd2a303..e6597d68da 100644 --- a/dspace/config/item-submission.xml +++ b/dspace/config/item-submission.xml @@ -17,7 +17,36 @@ + + + + @@ -44,7 +73,7 @@ - + @@ -52,6 +81,9 @@ collection submission + + submit.progressbar.describe.stepone org.dspace.app.rest.submit.step.DescribeStep @@ -62,8 +94,12 @@ org.dspace.app.rest.submit.step.DescribeStep submission-form - - + + submit.progressbar.describe.stepone + org.dspace.app.rest.submit.step.DescribeStep + submission-form + + submit.progressbar.describe.stepone org.dspace.app.rest.submit.step.DescribeStep submission-form @@ -106,38 +142,26 @@ submission - - - - - - + + submit.progressbar.CClicense org.dspace.app.rest.submit.step.CCLicenseStep - cclicense --> + cclicense + - - + - - - - + extract + @@ -166,7 +190,7 @@ submission-form - + Sample org.dspace.submit.step.SampleStep @@ -192,7 +216,6 @@ - @@ -204,26 +227,45 @@ - - - + - + - - - + + - + + + + + + + + + + + + @@ -231,25 +273,44 @@ + - + + + - + + + - + + + diff --git a/dspace/config/local.cfg.EXAMPLE b/dspace/config/local.cfg.EXAMPLE index d5d162851b..f74541bca9 100644 --- a/dspace/config/local.cfg.EXAMPLE +++ b/dspace/config/local.cfg.EXAMPLE @@ -33,11 +33,15 @@ dspace.dir=/dspace # URL of DSpace backend ('server' webapp). Include port number etc. -# This is where REST API and all enabled server modules (OAI-PMH, SWORD, SWORDv2, RDF, etc) will respond +# DO NOT end it with '/'. +# This is where REST API and all enabled server modules (OAI-PMH, SWORD, +# SWORDv2, RDF, etc) will respond. dspace.server.url = http://localhost:8080/server -# URL of DSpace frontend (Angular UI). Include port number etc -# This is used by the backend to provide links in emails, RSS feeds, Sitemaps, etc. +# URL of DSpace frontend (Angular UI). Include port number etc. +# DO NOT end it with '/'. +# This is used by the backend to provide links in emails, RSS feeds, Sitemaps, +# etc. dspace.ui.url = http://localhost:4000 # Name of the site @@ -188,6 +192,7 @@ db.schema = public #plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.LDAPAuthentication # 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 # 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). # Multiple allowed origin URLs may be comma separated. Wildcard value (*) is NOT SUPPORTED. # (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} ################################################# diff --git a/dspace/config/log4j2.xml b/dspace/config/log4j2.xml index 67c2d39e52..1ebdf5ab3e 100644 --- a/dspace/config/log4j2.xml +++ b/dspace/config/log4j2.xml @@ -82,7 +82,7 @@ + level='WARN'/> + diff --git a/dspace/config/spring/api/discovery.xml b/dspace/config/spring/api/discovery.xml index 0c67cac175..1e56a26e77 100644 --- a/dspace/config/spring/api/discovery.xml +++ b/dspace/config/spring/api/discovery.xml @@ -154,11 +154,9 @@ - - - + @@ -295,11 +293,9 @@ - - - + @@ -441,11 +437,9 @@ - - - + @@ -581,11 +575,9 @@ - - - + @@ -687,11 +679,9 @@ - - - + @@ -764,11 +754,9 @@ - - - + @@ -841,11 +829,9 @@ - - - + @@ -927,11 +913,9 @@ - - - + @@ -989,11 +973,9 @@ - - - + @@ -1047,11 +1029,9 @@ - - - + @@ -1107,11 +1087,9 @@ - - - + @@ -1169,11 +1147,9 @@ - - - + @@ -1229,11 +1205,9 @@ - - - + @@ -1290,11 +1264,9 @@ - - - + @@ -1360,9 +1332,6 @@ - - - @@ -1374,7 +1343,6 @@ - @@ -1422,11 +1390,9 @@ - - - + @@ -2180,59 +2146,77 @@ + + + + + + + + + + + + + + + + + + diff --git a/dspace/config/submission-forms.xml b/dspace/config/submission-forms.xml index 277c4ded0f..a948ee36e8 100644 --- a/dspace/config/submission-forms.xml +++ b/dspace/config/submission-forms.xml @@ -21,6 +21,7 @@ +

@@ -47,7 +48,203 @@
+
+ + + dc + contributor + author + true + + onebox + Enter the author's name (Family name, Given names). + + + + + + dc + title + + false + + onebox + Enter the main title of the item. + You must enter a main title for this item. + + + + + dc + title + alternative + true + + onebox + If the item has any alternative titles, please enter them here. + + + + + + dc + date + issued + false + + + date + Please give the date of previous publication or public distribution. + You can leave out the day and/or month if they aren't applicable. + + You must enter at least the year. + + + + dc + publisher + + false + + + onebox + Enter the name of the publisher of the previously issued instance of this item. + + + + + + dc + identifier + citation + false + + onebox + Enter the standard citation for the previously issued instance of this item. + + + + + + dc + relation + ispartofseries + true + + series + Enter the series and number assigned to this item by your community. + + + + + + dc + identifier + + + true + + qualdrop_value + If the item has any identification numbers or codes associated with + it, please enter the types and the actual numbers or codes. + + + + + + + dc + type + + true + + dropdown + 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. + + + + + + + dc + language + iso + false + + dropdown + 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'. + + + + +
+ + +
+ + + dc + subject + + + true + + tag + Enter appropriate subject keywords or phrases. + + srsc + + + + + dc + description + abstract + false + + textarea + Enter the abstract of the item. + + + + + + dc + description + sponsorship + false + + textarea + Enter the names of any sponsors and/or funding codes in the box. + + + + + + dc + description + + false + + textarea + Enter any other description or comments in this box. + + + +
+ + + +
isAuthorOfPublication @@ -190,60 +387,8 @@
-
- - - dc - subject - - - true - - tag - Enter appropriate subject keywords or phrases. - - srsc - - - - - dc - description - abstract - false - - textarea - Enter the abstract of the item. - - - - - - dc - description - sponsorship - false - - textarea - Enter the names of any sponsors and/or funding codes in the box. - - - - - - dc - description - - false - - textarea - Enter any other description or comments in this box. - - - -
- -
+ + isPublicationOfAuthor @@ -320,6 +465,7 @@
+
@@ -390,6 +536,7 @@
+
@@ -449,6 +596,7 @@
+
@@ -498,6 +646,7 @@ +
@@ -548,6 +697,7 @@
+
diff --git a/dspace/modules/additions/pom.xml b/dspace/modules/additions/pom.xml index ecfd213879..6a8cf4c04e 100644 --- a/dspace/modules/additions/pom.xml +++ b/dspace/modules/additions/pom.xml @@ -17,7 +17,7 @@ org.dspace modules - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT .. diff --git a/dspace/modules/pom.xml b/dspace/modules/pom.xml index 4d9e654b7f..12810a9900 100644 --- a/dspace/modules/pom.xml +++ b/dspace/modules/pom.xml @@ -11,7 +11,7 @@ org.dspace dspace-parent - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT ../../pom.xml diff --git a/dspace/modules/rest/pom.xml b/dspace/modules/rest/pom.xml index e801ea7e27..f5b7c939a3 100644 --- a/dspace/modules/rest/pom.xml +++ b/dspace/modules/rest/pom.xml @@ -13,7 +13,7 @@ org.dspace modules - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT .. diff --git a/dspace/modules/server/pom.xml b/dspace/modules/server/pom.xml index bd1820853f..d2db715c62 100644 --- a/dspace/modules/server/pom.xml +++ b/dspace/modules/server/pom.xml @@ -13,7 +13,7 @@ just adding new jar in the classloader modules org.dspace - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT .. diff --git a/dspace/pom.xml b/dspace/pom.xml index dc3d48f089..622fc034f3 100644 --- a/dspace/pom.xml +++ b/dspace/pom.xml @@ -16,7 +16,7 @@ org.dspace dspace-parent - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT ../pom.xml diff --git a/dspace/src/main/docker-compose/cli.assetstore.yml b/dspace/src/main/docker-compose/cli.assetstore.yml index e4dac3a841..7773facae6 100644 --- a/dspace/src/main/docker-compose/cli.assetstore.yml +++ b/dspace/src/main/docker-compose/cli.assetstore.yml @@ -11,7 +11,8 @@ version: "3.7" services: dspace-cli: 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: - /bin/bash - '-c' diff --git a/dspace/src/main/docker-compose/db.entities.yml b/dspace/src/main/docker-compose/db.entities.yml index 0762d900ba..762519f508 100644 --- a/dspace/src/main/docker-compose/db.entities.yml +++ b/dspace/src/main/docker-compose/db.entities.yml @@ -12,5 +12,33 @@ services: dspacedb: image: dspace/dspace-postgres-pgcrypto:loadsql environment: - # Double underbars in env names will be replaced with periods for apache commons - - LOADSQL=https://www.dropbox.com/s/4ap1y6deseoc8ws/dspace7-entities-2019-11-28.sql?dl=1 + # This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data + - 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/null 2>&1; do sleep 1; done; + /dspace/bin/dspace database migrate + sed -i '/name-map collection-handle="default".*/a \\n \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + ' /dspace/config/item-submission.xml + catalina.sh run diff --git a/dspace/src/main/docker-compose/local.cfg b/dspace/src/main/docker-compose/local.cfg index a511c25789..10dc556117 100644 --- a/dspace/src/main/docker-compose/local.cfg +++ b/dspace/src/main/docker-compose/local.cfg @@ -1,6 +1,11 @@ dspace.dir=/dspace -db.url=jdbc:postgresql://dspacedb:5432/dspace dspace.server.url=http://localhost:8080/server dspace.ui.url=http://localhost:4000 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 +# 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 diff --git a/pom.xml b/pom.xml index 99d3dac185..fef4b11eca 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.dspace dspace-parent pom - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT DSpace Parent Project DSpace open source software is a turnkey institutional repository application. @@ -30,12 +30,12 @@ 1.2.22 2.3.4 - 2.10.2 + 2.12.3 1.3.2 2.3.1 2.3.1 - 9.4.35.v20201120 + 9.4.38.v20210224 2.13.3 2.0.15 3.17 @@ -826,14 +826,14 @@ org.dspace dspace-rest - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT jar classes org.dspace dspace-rest - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT war @@ -980,64 +980,64 @@ org.dspace dspace-api - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT org.dspace dspace-api test-jar - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT test org.dspace.modules additions - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT org.dspace dspace-sword - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT org.dspace dspace-swordv2 - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT org.dspace dspace-oai - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT org.dspace dspace-services - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT org.dspace dspace-server-webapp test-jar - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT test org.dspace dspace-rdf - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT org.dspace dspace-server-webapp - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT jar classes org.dspace dspace-server-webapp - 7.0-beta5-SNAPSHOT + 7.0-beta6-SNAPSHOT war @@ -1369,7 +1369,7 @@ commons-io commons-io - 2.6 + 2.7 org.apache.commons