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 extends DSpaceObject> 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 @@
-
-
+
+
-
+
-
+
+
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 super Object> 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 @@
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+