From 4a7952af155c04a174f3b323f1a23e4dd507b88d Mon Sep 17 00:00:00 2001 From: Koen Pauwels Date: Thu, 1 Dec 2022 08:45:57 +0100 Subject: [PATCH 01/16] 97183 Added SubmitFeature: checks if eperson has ADD rights to any or specific collection --- .../authorization/AuthorizationFeature.java | 5 +- .../authorization/impl/SubmitFeature.java | 58 +++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/SubmitFeature.java diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/AuthorizationFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/AuthorizationFeature.java index f2f1ae31f5..353e2b9957 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/AuthorizationFeature.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/AuthorizationFeature.java @@ -13,6 +13,7 @@ import org.dspace.app.rest.model.BaseObjectRest; import org.dspace.app.rest.model.RestAddressableModel; import org.dspace.app.rest.model.SiteRest; import org.dspace.core.Context; +import org.dspace.discovery.SearchServiceException; import org.springframework.core.annotation.AnnotationUtils; /** @@ -33,7 +34,7 @@ public interface AuthorizationFeature { * wide feature * @return true if the user associated with the context has access to the feature for the specified object */ - boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException; + boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException, SearchServiceException; /** * Return the name of the feature @@ -69,4 +70,4 @@ public interface AuthorizationFeature { * @return the supported object type, required to be not null */ String[] getSupportedTypes(); -} \ No newline at end of file +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/SubmitFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/SubmitFeature.java new file mode 100644 index 0000000000..dce3ead8be --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/SubmitFeature.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.CollectionRest; +import org.dspace.app.rest.utils.Utils; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.Collection; +import org.dspace.content.service.CollectionService; +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; + +@Component +@AuthorizationFeatureDocumentation(name = SubmitFeature.NAME, + description = "It can be used to verify if a user has rights to submit anything.") +public class SubmitFeature implements AuthorizationFeature { + public static final String NAME = "canSubmit"; + + @Autowired + AuthorizeService authService; + + @Autowired + CollectionService collectionService; + + @Autowired + Utils utils; + + @Override + public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException, SearchServiceException { + if (object == null) { + // Check whether the user has permission to add to any collection + return collectionService.countCollectionsWithSubmit("", context, null) > 0; + } else if (object instanceof CollectionRest) { + // Check whether the user has permission to add to the given collection + Collection collection = (Collection) utils.getDSpaceAPIObjectFromRest(context, object); + return authService.authorizeActionBoolean(context, collection, Constants.ADD); + } + return false; + } + + @Override + public String[] getSupportedTypes() { + return new String[0]; + } +} From 6621e57ebd4b1a94170a3510718098cf29c39dbb Mon Sep 17 00:00:00 2001 From: Koen Pauwels Date: Thu, 1 Dec 2022 16:37:03 +0100 Subject: [PATCH 02/16] 97183: Added SolrServiceIndexItemEditorsPlugin Analogous to SolrServiceIndexCollectionSubmittersPlugin, but for Items instead of Collections, and for WRITE rights instead of ADD rights. Also refactored SolrServiceIndexCollectionSubmittersPlugin to share code with the new plugin. --- .../org/dspace/discovery/IndexingUtils.java | 146 ++++++++++++++++++ ...erviceIndexCollectionSubmittersPlugin.java | 40 ++--- .../SolrServiceIndexItemEditorsPlugin.java | 63 ++++++++ .../AuthorizationFeatureService.java | 6 +- .../impl/AuthorizationFeatureServiceImpl.java | 3 +- .../AuthorizationRestRepository.java | 11 +- dspace/solr/search/conf/schema.xml | 3 + 7 files changed, 236 insertions(+), 36 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/discovery/IndexingUtils.java create mode 100644 dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexItemEditorsPlugin.java diff --git a/dspace-api/src/main/java/org/dspace/discovery/IndexingUtils.java b/dspace-api/src/main/java/org/dspace/discovery/IndexingUtils.java new file mode 100644 index 0000000000..35835ec402 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/discovery/IndexingUtils.java @@ -0,0 +1,146 @@ +/** + * 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.discovery; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.stream.Stream; + +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.core.Constants; +import org.dspace.core.Context; + +/** + * Util methods used by indexing. + * + * @author Koen Pauwels (koen.pauwels at atmire dot com) + */ +public abstract class IndexingUtils { + /** + * Retrieve all ancestor communities of a given community, with the first one being the given community and the + * last one being the root. + * + * TODO: can be done in a single SQL query with recursive common table expressions + * TODO: should probably be moved to CommunityService + * + * @param context DSpace context object + * @param community Community for which we search the ancestors + * @return A stream of ancestor communities. + * @throws SQLException if database error + */ + static Stream getAncestorCommunities(Context context, Community community) throws SQLException { + ArrayList communities = new ArrayList<>(); + while (community != null) { + communities.add(community); + community = (Community) ContentServiceFactory.getInstance().getDSpaceObjectService(community) + .getParentObject(context, community); + } + return communities.stream(); + } + + /** + * Retrieve the ids of all groups that have ADMIN rights to the given community, either directly + * (through direct resource policy) or indirectly (through a policy on an ancestor community). + * + * @param context DSpace context object + * @param community Community for which we search the admin group IDs + * @return A stream of admin group IDs + * @throws SQLException if database error + */ + static Stream findTransitiveAdminGroupIds(Context context, Community community) throws SQLException { + return getAncestorCommunities(context, community) + .map(parent -> parent.getAdministrators().getID()); + } + + /** + * Retrieve the ids of all groups that have ADMIN rights to the given collection, either directly + * (through direct resource policy) or indirectly (through a policy on its community, or one of + * its ancestor communities). + * + * @param context DSpace context object + * @param collection Collection for which we search the admin group IDs + * @return A stream of admin group IDs + * @throws SQLException if database error + */ + static Stream findTransitiveAdminGroupIds(Context context, Collection collection) throws SQLException { + UUID directAdminGroupId = collection.getAdministrators().getID(); + List> subResults = Arrays.asList(Stream.of(directAdminGroupId)); + for (Community community : collection.getCommunities()) { + subResults.add(findTransitiveAdminGroupIds(context, community)); + } + return sequence(subResults); + } + + /** + * Retrieve the ids of all groups that have ADMIN rights on the given item, either directly + * (through direct resource policy) or indirectly (through a policy on the owning collection, or on + * the owning collection's community, or on any of that community's ancestor communities). + * + * @param authService + * @param context + * @param item + * @return + * @throws SQLException + * + * @param authService The authentication service + * @param context DSpace context object + * @param item Item for which we search the admin group IDs + * @return A stream of admin group IDs + * @throws SQLException if database error + */ + static Stream findTransitiveAdminGroupIds(AuthorizeService authService, Context context, Item item) + throws SQLException { + Stream directAdminGroupIds = authService.getPoliciesActionFilter(context, item, Constants.ADMIN) + .stream() + .filter(policy -> policy.getGroup() != null) + .map(policy -> policy.getGroup().getID()); + List> subResults = Arrays.asList(directAdminGroupIds); + for (Collection coll : item.getCollections()) { + subResults.add(findTransitiveAdminGroupIds(context, coll)); + } + return sequence(subResults); + } + + /** + * Retrieve group and eperson IDs for all groups and eperson who have _any_ of the given authorizations + * on the given DSpaceObject. The resulting IDs are prefixed with "e" in the case of an eperson ID, and "g" in the + * case of a group ID. + * + * @param authService The authentication service + * @param context DSpace context object + * @param obj DSpaceObject for which we search the admin group IDs + * @return A stream of admin group IDs as Strings, prefixed with either "e" or "g", depending on whether it is a + * group or eperson ID. + * @throws SQLException if database error + */ + static Stream findDirectAuthorizedGroupsAndEPersonsPrefixedIds(AuthorizeService authService, + Context context, DSpaceObject obj, int[] authorizations) throws SQLException { + + ArrayList> subResults = new ArrayList<>(); + for (int auth : authorizations) { + Stream subResult = authService.getPoliciesActionFilter(context, obj, auth).stream() + .map(policy -> policy.getGroup() == null ? "e" + policy.getEPerson().getID() + : "g" + policy.getGroup().getID()); + subResults.add(subResult); + // TODO: context.uncacheEntitiy(policy); + } + return sequence(subResults); + } + + private static Stream sequence(List> subResults) { + return subResults.stream().flatMap(x -> x); + } +} diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexCollectionSubmittersPlugin.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexCollectionSubmittersPlugin.java index 00b70f93d5..5b97c1edfe 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexCollectionSubmittersPlugin.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexCollectionSubmittersPlugin.java @@ -8,15 +8,11 @@ package org.dspace.discovery; import java.sql.SQLException; -import java.util.List; import org.apache.logging.log4j.Logger; import org.apache.solr.common.SolrInputDocument; -import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.Collection; -import org.dspace.content.Community; -import org.dspace.content.factory.ContentServiceFactory; import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.core.LogHelper; @@ -42,30 +38,18 @@ public class SolrServiceIndexCollectionSubmittersPlugin implements SolrServiceIn Collection col = ((IndexableCollection) idxObj).getIndexedObject(); if (col != null) { try { - String fieldValue = null; - Community parent = (Community) ContentServiceFactory.getInstance().getDSpaceObjectService(col) - .getParentObject(context, col); - while (parent != null) { - if (parent.getAdministrators() != null) { - fieldValue = "g" + parent.getAdministrators().getID(); - document.addField("submit", fieldValue); - } - parent = (Community) ContentServiceFactory.getInstance().getDSpaceObjectService(parent) - .getParentObject(context, parent); - } - List policies = authorizeService.getPoliciesActionFilter(context,col,Constants.ADD); - policies.addAll(authorizeService.getPoliciesActionFilter(context, col, Constants.ADMIN)); + // Index groups with ADMIN rights on the Collection, on + // Communities containing those Collections, and recursively on any Community containing such a + // Community. + // TODO: Strictly speaking we should also check for epersons who received admin rights directly, + // without being part of the admin group. Finding them may be a lot slower though. + IndexingUtils.findTransitiveAdminGroupIds(context, col) + .forEach(unprefixedId -> document.addField("submit", "g" + unprefixedId)); - for (ResourcePolicy resourcePolicy : policies) { - if (resourcePolicy.getGroup() != null) { - fieldValue = "g" + resourcePolicy.getGroup().getID(); - } else { - fieldValue = "e" + resourcePolicy.getEPerson().getID(); - - } - document.addField("submit", fieldValue); - context.uncacheEntity(resourcePolicy); - } + // Index groups and epersons with ADD rights on the Collection. + IndexingUtils.findDirectAuthorizedGroupsAndEPersonsPrefixedIds( + authorizeService, context, col, new int[] {Constants.ADD} + ).forEach(prefixedId -> document.addField("submit", prefixedId)); } catch (SQLException e) { log.error(LogHelper.getHeader(context, "Error while indexing resource policies", "Collection: (id " + col.getID() + " type " + col.getName() + ")" )); @@ -74,4 +58,4 @@ public class SolrServiceIndexCollectionSubmittersPlugin implements SolrServiceIn } } -} \ No newline at end of file +} diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexItemEditorsPlugin.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexItemEditorsPlugin.java new file mode 100644 index 0000000000..d4b2538519 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexItemEditorsPlugin.java @@ -0,0 +1,63 @@ +/** + * 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.discovery; + +import static org.dspace.discovery.IndexingUtils.findDirectAuthorizedGroupsAndEPersonsPrefixedIds; +import static org.dspace.discovery.IndexingUtils.findTransitiveAdminGroupIds; + +import java.sql.SQLException; + +import org.apache.logging.log4j.Logger; +import org.apache.solr.common.SolrInputDocument; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.Item; +import org.dspace.core.Constants; +import org.dspace.core.Context; +import org.dspace.core.LogHelper; +import org.dspace.discovery.indexobject.IndexableItem; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Indexes policies that yield write access to items. + * + * @author Koen Pauwels at atmire.com + */ +public class SolrServiceIndexItemEditorsPlugin implements SolrServiceIndexPlugin { + private static final Logger log = org.apache.logging.log4j.LogManager + .getLogger(SolrServiceIndexCollectionSubmittersPlugin.class); + + @Autowired(required = true) + protected AuthorizeService authorizeService; + + @Override + public void additionalIndex(Context context, IndexableObject idxObj, SolrInputDocument document) { + if (idxObj instanceof IndexableItem) { + Item item = ((IndexableItem) idxObj).getIndexedObject(); + if (item != null) { + try { + // Index groups with ADMIN rights on the Item, on Collections containing the Item, on + // Communities containing those Collections, and recursively on any Community containing ssuch a + // Community. + // TODO: Strictly speaking we should also check for epersons who received admin rights directly, + // without being part of the admin group. Finding them may be a lot slower though. + findTransitiveAdminGroupIds(authorizeService, context, item) + .forEach(unprefixedId -> document.addField("edit", "g" + unprefixedId)); + + // Index groups and epersons with WRITE rights on the Item. + findDirectAuthorizedGroupsAndEPersonsPrefixedIds( + authorizeService, context, item, new int[] {Constants.WRITE} + ).forEach(prefixedId -> document.addField("edit", prefixedId)); + + } catch (SQLException e) { + log.error(LogHelper.getHeader(context, "Error while indexing resource policies", + "Item: (id " + item.getID() + " name " + item.getName() + ")" )); + } + } + } + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/AuthorizationFeatureService.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/AuthorizationFeatureService.java index 2b04bb983c..941c614fcb 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/AuthorizationFeatureService.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/AuthorizationFeatureService.java @@ -13,6 +13,7 @@ import java.util.List; import org.dspace.app.rest.model.BaseObjectRest; import org.dspace.app.rest.model.SiteRest; import org.dspace.core.Context; +import org.dspace.discovery.SearchServiceException; /** * This service provides access to the Authorization Features and check if the feature is allowed or not in a specific @@ -34,7 +35,8 @@ public interface AuthorizationFeatureService { * feature pass the {@link SiteRest} object * @return true if the user associated with the context has access to the feature */ - boolean isAuthorized(Context context, AuthorizationFeature feature, BaseObjectRest object) throws SQLException; + boolean isAuthorized(Context context, AuthorizationFeature feature, BaseObjectRest object) + throws SQLException, SearchServiceException; /** * Get all the authorization features defined in the system @@ -60,4 +62,4 @@ public interface AuthorizationFeatureService { * @return */ List findByResourceType(String categoryDotModel); -} \ No newline at end of file +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/AuthorizationFeatureServiceImpl.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/AuthorizationFeatureServiceImpl.java index 01385d4435..766204364c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/AuthorizationFeatureServiceImpl.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/AuthorizationFeatureServiceImpl.java @@ -18,6 +18,7 @@ import org.dspace.app.rest.authorization.AuthorizationFeatureService; import org.dspace.app.rest.model.BaseObjectRest; import org.dspace.app.rest.utils.Utils; import org.dspace.core.Context; +import org.dspace.discovery.SearchServiceException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -38,7 +39,7 @@ public class AuthorizationFeatureServiceImpl implements AuthorizationFeatureServ @Override public boolean isAuthorized(Context context, AuthorizationFeature feature, BaseObjectRest object) - throws SQLException { + throws SQLException, SearchServiceException { if (object == null) { // the authorization interface require that the object is not null return false; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AuthorizationRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AuthorizationRestRepository.java index 97e6866073..1379528669 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AuthorizationRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AuthorizationRestRepository.java @@ -31,6 +31,7 @@ import org.dspace.app.rest.model.BaseObjectRest; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.service.AuthorizeService; import org.dspace.core.Context; +import org.dspace.discovery.SearchServiceException; import org.dspace.eperson.EPerson; import org.dspace.eperson.service.EPersonService; import org.slf4j.Logger; @@ -124,7 +125,7 @@ public class AuthorizationRestRepository extends DSpaceRestRepository findByObject(@Parameter(value = "uri", required = true) String uri, @Parameter(value = "eperson") UUID epersonUuid, @Parameter(value = "feature") String featureName, - Pageable pageable) throws AuthorizeException, SQLException { + Pageable pageable) throws AuthorizeException, SQLException, SearchServiceException { Context context = obtainContext(); @@ -234,7 +235,7 @@ public class AuthorizationRestRepository extends DSpaceRestRepository(); @@ -269,7 +270,7 @@ public class AuthorizationRestRepository extends DSpaceRestRepository findByObjectAndFeature( Context context, EPerson user, BaseObjectRest obj, String featureName - ) throws SQLException { + ) throws SQLException, SearchServiceException { AuthorizationFeature feature = authorizationFeatureService.find(featureName); diff --git a/dspace/solr/search/conf/schema.xml b/dspace/solr/search/conf/schema.xml index f9c8eb9760..f8878b37b2 100644 --- a/dspace/solr/search/conf/schema.xml +++ b/dspace/solr/search/conf/schema.xml @@ -264,6 +264,9 @@ + + + From 31cb37b4a5db81894f0b754550082141910d233c Mon Sep 17 00:00:00 2001 From: Koen Pauwels Date: Fri, 2 Dec 2022 09:49:06 +0100 Subject: [PATCH 03/16] 97183 Minor fixes to IndexingUtils#findDirectlyAuthorizedGroupAndEPersonPrefixedIds --- .../org/dspace/discovery/IndexingUtils.java | 29 +++++++++---------- ...erviceIndexCollectionSubmittersPlugin.java | 14 ++++++--- .../SolrServiceIndexItemEditorsPlugin.java | 11 ++++--- 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/discovery/IndexingUtils.java b/dspace-api/src/main/java/org/dspace/discovery/IndexingUtils.java index 35835ec402..496cfd8c83 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/IndexingUtils.java +++ b/dspace-api/src/main/java/org/dspace/discovery/IndexingUtils.java @@ -14,6 +14,7 @@ import java.util.List; import java.util.UUID; import java.util.stream.Stream; +import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.Collection; import org.dspace.content.Community; @@ -89,12 +90,6 @@ public abstract class IndexingUtils { * (through direct resource policy) or indirectly (through a policy on the owning collection, or on * the owning collection's community, or on any of that community's ancestor communities). * - * @param authService - * @param context - * @param item - * @return - * @throws SQLException - * * @param authService The authentication service * @param context DSpace context object * @param item Item for which we search the admin group IDs @@ -126,18 +121,20 @@ public abstract class IndexingUtils { * group or eperson ID. * @throws SQLException if database error */ - static Stream findDirectAuthorizedGroupsAndEPersonsPrefixedIds(AuthorizeService authService, - Context context, DSpaceObject obj, int[] authorizations) throws SQLException { - - ArrayList> subResults = new ArrayList<>(); + static List findDirectlyAuthorizedGroupAndEPersonPrefixedIds( + AuthorizeService authService, Context context, DSpaceObject obj, int[] authorizations) + throws SQLException { + ArrayList prefixedIds = new ArrayList<>(); for (int auth : authorizations) { - Stream subResult = authService.getPoliciesActionFilter(context, obj, auth).stream() - .map(policy -> policy.getGroup() == null ? "e" + policy.getEPerson().getID() - : "g" + policy.getGroup().getID()); - subResults.add(subResult); - // TODO: context.uncacheEntitiy(policy); + for (ResourcePolicy policy : authService.getPoliciesActionFilter(context, obj, auth)) { + String prefixedId = policy.getGroup() == null + ? "e" + policy.getEPerson().getID() + : "g" + policy.getGroup().getID(); + prefixedIds.add(prefixedId); + context.uncacheEntity(policy); + } } - return sequence(subResults); + return prefixedIds; } private static Stream sequence(List> subResults) { diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexCollectionSubmittersPlugin.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexCollectionSubmittersPlugin.java index 5b97c1edfe..bcd0f24cd3 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexCollectionSubmittersPlugin.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexCollectionSubmittersPlugin.java @@ -7,7 +7,11 @@ */ package org.dspace.discovery; +import static org.dspace.discovery.IndexingUtils.findDirectlyAuthorizedGroupAndEPersonPrefixedIds; +import static org.dspace.discovery.IndexingUtils.findTransitiveAdminGroupIds; + import java.sql.SQLException; +import java.util.List; import org.apache.logging.log4j.Logger; import org.apache.solr.common.SolrInputDocument; @@ -43,13 +47,16 @@ public class SolrServiceIndexCollectionSubmittersPlugin implements SolrServiceIn // Community. // TODO: Strictly speaking we should also check for epersons who received admin rights directly, // without being part of the admin group. Finding them may be a lot slower though. - IndexingUtils.findTransitiveAdminGroupIds(context, col) + findTransitiveAdminGroupIds(context, col) .forEach(unprefixedId -> document.addField("submit", "g" + unprefixedId)); // Index groups and epersons with ADD rights on the Collection. - IndexingUtils.findDirectAuthorizedGroupsAndEPersonsPrefixedIds( + List prefixedIds = findDirectlyAuthorizedGroupAndEPersonPrefixedIds( authorizeService, context, col, new int[] {Constants.ADD} - ).forEach(prefixedId -> document.addField("submit", prefixedId)); + ); + for (String prefixedId : prefixedIds) { + document.addField("submit", prefixedId); + } } catch (SQLException e) { log.error(LogHelper.getHeader(context, "Error while indexing resource policies", "Collection: (id " + col.getID() + " type " + col.getName() + ")" )); @@ -57,5 +64,4 @@ public class SolrServiceIndexCollectionSubmittersPlugin implements SolrServiceIn } } } - } diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexItemEditorsPlugin.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexItemEditorsPlugin.java index d4b2538519..b404c647c5 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexItemEditorsPlugin.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexItemEditorsPlugin.java @@ -7,10 +7,11 @@ */ package org.dspace.discovery; -import static org.dspace.discovery.IndexingUtils.findDirectAuthorizedGroupsAndEPersonsPrefixedIds; +import static org.dspace.discovery.IndexingUtils.findDirectlyAuthorizedGroupAndEPersonPrefixedIds; import static org.dspace.discovery.IndexingUtils.findTransitiveAdminGroupIds; import java.sql.SQLException; +import java.util.List; import org.apache.logging.log4j.Logger; import org.apache.solr.common.SolrInputDocument; @@ -49,10 +50,12 @@ public class SolrServiceIndexItemEditorsPlugin implements SolrServiceIndexPlugin .forEach(unprefixedId -> document.addField("edit", "g" + unprefixedId)); // Index groups and epersons with WRITE rights on the Item. - findDirectAuthorizedGroupsAndEPersonsPrefixedIds( + List prefixedIds = findDirectlyAuthorizedGroupAndEPersonPrefixedIds( authorizeService, context, item, new int[] {Constants.WRITE} - ).forEach(prefixedId -> document.addField("edit", prefixedId)); - + ); + for (String prefixedId : prefixedIds) { + document.addField("edit", prefixedId); + } } catch (SQLException e) { log.error(LogHelper.getHeader(context, "Error while indexing resource policies", "Item: (id " + item.getID() + " name " + item.getName() + ")" )); From f99e8763276ac0cbe46f373ed78b59a8aef0bbb9 Mon Sep 17 00:00:00 2001 From: Koen Pauwels Date: Fri, 2 Dec 2022 10:50:24 +0100 Subject: [PATCH 04/16] 97183 IndexingUtils: Changed interface to Lists instead of Streams --- .../org/dspace/discovery/IndexingUtils.java | 79 +++++++------------ ...erviceIndexCollectionSubmittersPlugin.java | 10 ++- .../SolrServiceIndexItemEditorsPlugin.java | 15 ++-- 3 files changed, 43 insertions(+), 61 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/discovery/IndexingUtils.java b/dspace-api/src/main/java/org/dspace/discovery/IndexingUtils.java index 496cfd8c83..2127c7557d 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/IndexingUtils.java +++ b/dspace-api/src/main/java/org/dspace/discovery/IndexingUtils.java @@ -12,16 +12,14 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.UUID; -import java.util.stream.Stream; +import java.util.stream.Collectors; import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.DSpaceObject; -import org.dspace.content.Item; import org.dspace.content.factory.ContentServiceFactory; -import org.dspace.core.Constants; import org.dspace.core.Context; /** @@ -29,27 +27,30 @@ import org.dspace.core.Context; * * @author Koen Pauwels (koen.pauwels at atmire dot com) */ -public abstract class IndexingUtils { +public class IndexingUtils { + private IndexingUtils() { + } + /** * Retrieve all ancestor communities of a given community, with the first one being the given community and the * last one being the root. - * + *

* TODO: can be done in a single SQL query with recursive common table expressions * TODO: should probably be moved to CommunityService * * @param context DSpace context object * @param community Community for which we search the ancestors - * @return A stream of ancestor communities. + * @return A list of ancestor communities. * @throws SQLException if database error */ - static Stream getAncestorCommunities(Context context, Community community) throws SQLException { + static List getAncestorCommunities(Context context, Community community) throws SQLException { ArrayList communities = new ArrayList<>(); while (community != null) { communities.add(community); community = (Community) ContentServiceFactory.getInstance().getDSpaceObjectService(community) .getParentObject(context, community); } - return communities.stream(); + return communities; } /** @@ -58,12 +59,13 @@ public abstract class IndexingUtils { * * @param context DSpace context object * @param community Community for which we search the admin group IDs - * @return A stream of admin group IDs + * @return A list of admin group IDs * @throws SQLException if database error */ - static Stream findTransitiveAdminGroupIds(Context context, Community community) throws SQLException { - return getAncestorCommunities(context, community) - .map(parent -> parent.getAdministrators().getID()); + static List findTransitiveAdminGroupIds(Context context, Community community) throws SQLException { + return getAncestorCommunities(context, community).stream() + .map(parent -> parent.getAdministrators().getID()) + .collect(Collectors.toList()); } /** @@ -71,42 +73,19 @@ public abstract class IndexingUtils { * (through direct resource policy) or indirectly (through a policy on its community, or one of * its ancestor communities). * - * @param context DSpace context object + * @param context DSpace context object * @param collection Collection for which we search the admin group IDs - * @return A stream of admin group IDs + * @return A list of admin group IDs * @throws SQLException if database error */ - static Stream findTransitiveAdminGroupIds(Context context, Collection collection) throws SQLException { - UUID directAdminGroupId = collection.getAdministrators().getID(); - List> subResults = Arrays.asList(Stream.of(directAdminGroupId)); + static List findTransitiveAdminGroupIds(Context context, Collection collection) throws SQLException { + List ids = Arrays.asList(collection.getAdministrators().getID()); for (Community community : collection.getCommunities()) { - subResults.add(findTransitiveAdminGroupIds(context, community)); + for (UUID id : findTransitiveAdminGroupIds(context, community)) { + ids.add(id); + } } - return sequence(subResults); - } - - /** - * Retrieve the ids of all groups that have ADMIN rights on the given item, either directly - * (through direct resource policy) or indirectly (through a policy on the owning collection, or on - * the owning collection's community, or on any of that community's ancestor communities). - * - * @param authService The authentication service - * @param context DSpace context object - * @param item Item for which we search the admin group IDs - * @return A stream of admin group IDs - * @throws SQLException if database error - */ - static Stream findTransitiveAdminGroupIds(AuthorizeService authService, Context context, Item item) - throws SQLException { - Stream directAdminGroupIds = authService.getPoliciesActionFilter(context, item, Constants.ADMIN) - .stream() - .filter(policy -> policy.getGroup() != null) - .map(policy -> policy.getGroup().getID()); - List> subResults = Arrays.asList(directAdminGroupIds); - for (Collection coll : item.getCollections()) { - subResults.add(findTransitiveAdminGroupIds(context, coll)); - } - return sequence(subResults); + return ids; } /** @@ -114,11 +93,11 @@ public abstract class IndexingUtils { * on the given DSpaceObject. The resulting IDs are prefixed with "e" in the case of an eperson ID, and "g" in the * case of a group ID. * - * @param authService The authentication service - * @param context DSpace context object - * @param obj DSpaceObject for which we search the admin group IDs - * @return A stream of admin group IDs as Strings, prefixed with either "e" or "g", depending on whether it is a - * group or eperson ID. + * @param authService The authentication service + * @param context DSpace context object + * @param obj DSpaceObject for which we search the admin group IDs + * @return A stream of admin group IDs as Strings, prefixed with either "e" or "g", depending on whether it is a + * group or eperson ID. * @throws SQLException if database error */ static List findDirectlyAuthorizedGroupAndEPersonPrefixedIds( @@ -136,8 +115,4 @@ public abstract class IndexingUtils { } return prefixedIds; } - - private static Stream sequence(List> subResults) { - return subResults.stream().flatMap(x -> x); - } } diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexCollectionSubmittersPlugin.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexCollectionSubmittersPlugin.java index bcd0f24cd3..ee93f954a5 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexCollectionSubmittersPlugin.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexCollectionSubmittersPlugin.java @@ -12,6 +12,7 @@ import static org.dspace.discovery.IndexingUtils.findTransitiveAdminGroupIds; import java.sql.SQLException; import java.util.List; +import java.util.UUID; import org.apache.logging.log4j.Logger; import org.apache.solr.common.SolrInputDocument; @@ -47,12 +48,13 @@ public class SolrServiceIndexCollectionSubmittersPlugin implements SolrServiceIn // Community. // TODO: Strictly speaking we should also check for epersons who received admin rights directly, // without being part of the admin group. Finding them may be a lot slower though. - findTransitiveAdminGroupIds(context, col) - .forEach(unprefixedId -> document.addField("submit", "g" + unprefixedId)); + for (UUID unprefixedId : findTransitiveAdminGroupIds(context, col)) { + document.addField("submit", "g" + unprefixedId); + } - // Index groups and epersons with ADD rights on the Collection. + // Index groups and epersons with ADD or ADMIN rights on the Collection. List prefixedIds = findDirectlyAuthorizedGroupAndEPersonPrefixedIds( - authorizeService, context, col, new int[] {Constants.ADD} + authorizeService, context, col, new int[] {Constants.ADD, Constants.ADMIN} ); for (String prefixedId : prefixedIds) { document.addField("submit", prefixedId); diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexItemEditorsPlugin.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexItemEditorsPlugin.java index b404c647c5..b10d2d72a3 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexItemEditorsPlugin.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexItemEditorsPlugin.java @@ -12,10 +12,12 @@ import static org.dspace.discovery.IndexingUtils.findTransitiveAdminGroupIds; import java.sql.SQLException; import java.util.List; +import java.util.UUID; import org.apache.logging.log4j.Logger; import org.apache.solr.common.SolrInputDocument; import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.Collection; import org.dspace.content.Item; import org.dspace.core.Constants; import org.dspace.core.Context; @@ -41,17 +43,20 @@ public class SolrServiceIndexItemEditorsPlugin implements SolrServiceIndexPlugin Item item = ((IndexableItem) idxObj).getIndexedObject(); if (item != null) { try { - // Index groups with ADMIN rights on the Item, on Collections containing the Item, on + // Index groups with ADMIN rights on Collections containing the Item, on // Communities containing those Collections, and recursively on any Community containing ssuch a // Community. // TODO: Strictly speaking we should also check for epersons who received admin rights directly, // without being part of the admin group. Finding them may be a lot slower though. - findTransitiveAdminGroupIds(authorizeService, context, item) - .forEach(unprefixedId -> document.addField("edit", "g" + unprefixedId)); + for (Collection collection : item.getCollections()) { + for (UUID unprefixedId : findTransitiveAdminGroupIds(context, collection)) { + document.addField("edit", "g" + unprefixedId); + } + } - // Index groups and epersons with WRITE rights on the Item. + // Index groups and epersons with WRITE or direct ADMIN rights on the Item. List prefixedIds = findDirectlyAuthorizedGroupAndEPersonPrefixedIds( - authorizeService, context, item, new int[] {Constants.WRITE} + authorizeService, context, item, new int[] {Constants.WRITE, Constants.ADMIN} ); for (String prefixedId : prefixedIds) { document.addField("edit", prefixedId); From 6cbb1630e3280995242e9c49858e6ccb7876018f Mon Sep 17 00:00:00 2001 From: Koen Pauwels Date: Fri, 2 Dec 2022 16:08:35 +0100 Subject: [PATCH 05/16] 97183 ItemService: added methods to search the index for items for which the current user has editing rights --- .../org/dspace/content/ItemServiceImpl.java | 55 +++++++++++++++++++ .../dspace/content/service/ItemService.java | 22 ++++++++ .../org/dspace/discovery/IndexingUtils.java | 7 ++- dspace/config/spring/api/discovery.xml | 1 + 4 files changed, 83 insertions(+), 2 deletions(-) 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 27d0ba189c..156c8b2712 100644 --- a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java @@ -51,8 +51,14 @@ import org.dspace.content.virtual.VirtualMetadataPopulator; import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.core.LogHelper; +import org.dspace.discovery.DiscoverQuery; +import org.dspace.discovery.DiscoverResult; +import org.dspace.discovery.SearchService; +import org.dspace.discovery.SearchServiceException; +import org.dspace.discovery.indexobject.IndexableItem; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; +import org.dspace.eperson.service.GroupService; import org.dspace.event.Event; import org.dspace.harvest.HarvestedItem; import org.dspace.harvest.service.HarvestedItemService; @@ -93,6 +99,8 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl implements It @Autowired(required = true) protected CommunityService communityService; @Autowired(required = true) + protected GroupService groupService; + @Autowired(required = true) protected AuthorizeService authorizeService; @Autowired(required = true) protected BundleService bundleService; @@ -105,6 +113,8 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl implements It @Autowired(required = true) protected InstallItemService installItemService; @Autowired(required = true) + protected SearchService searchService; + @Autowired(required = true) protected ResourcePolicyService resourcePolicyService; @Autowired(required = true) protected CollectionService collectionService; @@ -1065,6 +1075,51 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl implements It return collectionService.canEditBoolean(context, item.getOwningCollection(), false); } + /** + * Finds all Indexed Items where the current user has submit rights. If the user is an Admin, + * this is all Indexed Items. Otherwise, it includes those Items where + * an indexed "submit" policy lists either the eperson or one of the eperson's groups + * + * @param context DSpace context + * @param discoverQuery + * @return discovery search result objects + * @throws SQLException if something goes wrong + * @throws SearchServiceException if search error + */ + private DiscoverResult retrieveItemsWithEdit(Context context, DiscoverQuery discoverQuery) + throws SQLException, SearchServiceException { + EPerson currentUser = context.getCurrentUser(); + if (!authorizeService.isAdmin(context)) { + String userId = currentUser != null ? "e" + currentUser.getID().toString() : "e"; + Stream groupIds = groupService.allMemberGroupsSet(context, currentUser).stream() + .map(group -> "g" + group.getID()); + String query = Stream.concat(Stream.of(userId), groupIds) + .collect(Collectors.joining(" OR ", "edit:(", ")")); + discoverQuery.addFilterQueries(query); + } + return searchService.search(context, discoverQuery); + } + + public List findItemsWithEdit(Context context, int offset, int limit) + throws SQLException, SearchServiceException { + DiscoverQuery discoverQuery = new DiscoverQuery(); + discoverQuery.setDSpaceObjectFilter(IndexableItem.TYPE); + discoverQuery.setStart(offset); + discoverQuery.setMaxResults(limit); + DiscoverResult resp = retrieveItemsWithEdit(context, discoverQuery); + return resp.getIndexableObjects().stream() + .map(solrItems -> ((IndexableItem) solrItems).getIndexedObject()) + .collect(Collectors.toList()); + } + + public int countItemsWithEdit(Context context) throws SQLException, SearchServiceException { + DiscoverQuery discoverQuery = new DiscoverQuery(); + discoverQuery.setMaxResults(0); + discoverQuery.setDSpaceObjectFilter(IndexableItem.TYPE); + DiscoverResult resp = retrieveItemsWithEdit(context, discoverQuery); + return (int) resp.getTotalSearchResults(); + } + /** * Check if the item is an inprogress submission * 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 8b7badf223..b7a479469b 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 @@ -28,6 +28,7 @@ import org.dspace.content.MetadataValue; import org.dspace.content.Thumbnail; import org.dspace.content.WorkspaceItem; import org.dspace.core.Context; +import org.dspace.discovery.SearchServiceException; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; @@ -768,6 +769,27 @@ public interface ItemService */ int countWithdrawnItems(Context context) throws SQLException; + /** + * finds all items for which the current user has editing rights + * @param context DSpace context object + * @param offset page offset + * @param limit page size limit + * @return list of items for which the current user has editing rights + * @throws SQLException + * @throws SearchServiceException + */ + public List findItemsWithEdit(Context context, int offset, int limit) + throws SQLException, SearchServiceException; + + /** + * counts all items for which the current user has editing rights + * @param context DSpace context object + * @return list of items for which the current user has editing rights + * @throws SQLException + * @throws SearchServiceException + */ + public int countItemsWithEdit(Context context) throws SQLException, SearchServiceException; + /** * Check if the supplied item is an inprogress submission * diff --git a/dspace-api/src/main/java/org/dspace/discovery/IndexingUtils.java b/dspace-api/src/main/java/org/dspace/discovery/IndexingUtils.java index 2127c7557d..c4848c7432 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/IndexingUtils.java +++ b/dspace-api/src/main/java/org/dspace/discovery/IndexingUtils.java @@ -9,7 +9,6 @@ package org.dspace.discovery; import java.sql.SQLException; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.UUID; import java.util.stream.Collectors; @@ -64,6 +63,7 @@ public class IndexingUtils { */ static List findTransitiveAdminGroupIds(Context context, Community community) throws SQLException { return getAncestorCommunities(context, community).stream() + .filter(parent -> parent.getAdministrators() != null) .map(parent -> parent.getAdministrators().getID()) .collect(Collectors.toList()); } @@ -79,7 +79,10 @@ public class IndexingUtils { * @throws SQLException if database error */ static List findTransitiveAdminGroupIds(Context context, Collection collection) throws SQLException { - List ids = Arrays.asList(collection.getAdministrators().getID()); + List ids = new ArrayList<>(); + if (collection.getAdministrators() != null) { + ids.add(collection.getAdministrators().getID()); + } for (Community community : collection.getCommunities()) { for (UUID id : findTransitiveAdminGroupIds(context, community)) { ids.add(id); diff --git a/dspace/config/spring/api/discovery.xml b/dspace/config/spring/api/discovery.xml index 57f4c07aee..188f417059 100644 --- a/dspace/config/spring/api/discovery.xml +++ b/dspace/config/spring/api/discovery.xml @@ -31,6 +31,7 @@ + From 3fe6491975341eac6d5e22bae5db2b39e12a8d65 Mon Sep 17 00:00:00 2001 From: Koen Pauwels Date: Mon, 5 Dec 2022 16:27:51 +0100 Subject: [PATCH 06/16] 97183 Integration tests and fixes for SubmitFeature --- .../authorization/impl/SubmitFeature.java | 8 +- .../rest/authorization/SubmitFeatureIT.java | 328 ++++++++++++++++++ 2 files changed, 334 insertions(+), 2 deletions(-) create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/SubmitFeatureIT.java diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/SubmitFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/SubmitFeature.java index dce3ead8be..3793928fb0 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/SubmitFeature.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/SubmitFeature.java @@ -13,6 +13,7 @@ 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.SiteRest; import org.dspace.app.rest.utils.Utils; import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.Collection; @@ -40,7 +41,7 @@ public class SubmitFeature implements AuthorizationFeature { @Override public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException, SearchServiceException { - if (object == null) { + if (object instanceof SiteRest) { // Check whether the user has permission to add to any collection return collectionService.countCollectionsWithSubmit("", context, null) > 0; } else if (object instanceof CollectionRest) { @@ -53,6 +54,9 @@ public class SubmitFeature implements AuthorizationFeature { @Override public String[] getSupportedTypes() { - return new String[0]; + return new String[] { + CollectionRest.CATEGORY + "." + CollectionRest.NAME, + SiteRest.CATEGORY + "." + SiteRest.NAME + }; } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/SubmitFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/SubmitFeatureIT.java new file mode 100644 index 0000000000..9404fb6620 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/SubmitFeatureIT.java @@ -0,0 +1,328 @@ +/** + * 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 java.util.concurrent.Callable; + +import org.dspace.app.rest.authorization.impl.SubmitFeature; +import org.dspace.app.rest.converter.CollectionConverter; +import org.dspace.app.rest.converter.SiteConverter; +import org.dspace.app.rest.model.CollectionRest; +import org.dspace.app.rest.model.SiteRest; +import org.dspace.app.rest.projection.DefaultProjection; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.app.rest.utils.Utils; +import org.dspace.authorize.ResourcePolicy; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.GroupBuilder; +import org.dspace.builder.ResourcePolicyBuilder; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.Site; +import org.dspace.content.service.SiteService; +import org.dspace.core.Constants; +import org.dspace.eperson.Group; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.servlet.ResultActions; + +public class SubmitFeatureIT extends AbstractControllerIntegrationTest { + @Autowired + private AuthorizationFeatureService authorizationFeatureService; + @Autowired + private SiteService siteService; + @Autowired + private CollectionConverter collectionConverter; + @Autowired + private SiteConverter siteConverter; + @Autowired + private Utils utils; + + private Group group; + private String siteUri; + private String epersonToken; + private AuthorizationFeature submitFeature; + private Community communityA; + + private Collection collectionA1; + private Collection collectionA2; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + + withSuppressedAuthorization(() -> { + communityA = CommunityBuilder.createCommunity(context).withName("Community A").build(); + collectionA1 = CollectionBuilder.createCollection(context, communityA).withName("Collection A1").build(); + collectionA2 = CollectionBuilder.createCollection(context, communityA).withName("Collection A2").build(); + group = GroupBuilder.createGroup(context) + .withName("group") + .addMember(eperson) + .build(); + return null; + }); + submitFeature = authorizationFeatureService.find(SubmitFeature.NAME); + + Site site = siteService.findSite(context); + SiteRest siteRest = siteConverter.convert(site, DefaultProjection.DEFAULT); + siteUri = utils.linkToSingleResource(siteRest, "self").getHref(); + } + + @Test + public void testNoRights() throws Exception { + expectZeroResults(requestSitewideSubmitFeature()); + } + + @Test + public void testDirectEPersonAddPolicy() throws Exception { + ResourcePolicy rp = ResourcePolicyBuilder.createResourcePolicy(context) + .withUser(eperson) + .withDspaceObject(collectionA1) + .withAction(Constants.ADD) + .build(); + expectSomeResults(requestSitewideSubmitFeature()); + } + + @Test + public void testDirectGroupAddPolicy() throws Exception { + ResourcePolicy rp = ResourcePolicyBuilder.createResourcePolicy(context) + .withGroup(group) + .withDspaceObject(collectionA1) + .withAction(Constants.ADD) + .build(); + expectSomeResults(requestSitewideSubmitFeature()); + } + + @Test + public void testDirectEPersonAdminPolicy() throws Exception { + ResourcePolicy rp = ResourcePolicyBuilder.createResourcePolicy(context) + .withUser(eperson) + .withDspaceObject(collectionA1) + .withAction(Constants.ADMIN) + .build(); + expectSomeResults(requestSitewideSubmitFeature()); + } + + @Test + public void testDirectGroupAdminPolicy() throws Exception { + ResourcePolicy rp = ResourcePolicyBuilder.createResourcePolicy(context) + .withGroup(group) + .withDspaceObject(collectionA1) + .withAction(Constants.ADMIN) + .build(); + expectSomeResults(requestSitewideSubmitFeature()); + } + + @Test + public void testCollectionAdmin() throws Exception { + withSuppressedAuthorization(() -> { + Collection col = CollectionBuilder + .createCollection(context, communityA) + .withName("this is another test collection") + .withAdminGroup(eperson) + .build(); + return null; + }); + expectSomeResults(requestSitewideSubmitFeature()); + } + + @Test + public void testCommunityWithoutCollectionsAdmin() throws Exception { + withSuppressedAuthorization(() -> { + Community comm = CommunityBuilder + .createCommunity(context) + .withName("This community contains no collections") + .withAdminGroup(eperson) + .build(); + return null; + }); + expectZeroResults(requestSitewideSubmitFeature()); + } + + @Test + public void testCommunityWithCollectionsAdmin() throws Exception { + withSuppressedAuthorization(() -> { + Community comm = CommunityBuilder + .createCommunity(context) + .withName("This community contains a collection") + .withAdminGroup(eperson) + .build(); + Collection coll = CollectionBuilder + .createCollection(context, comm) + .withName("Contained collection") + .build(); + return null; + }); + expectSomeResults(requestSitewideSubmitFeature()); + } + + @Test + public void testCommunityWithSubCommunityWithCollectionsAdmin() throws Exception { + withSuppressedAuthorization(() -> { + Community parent = CommunityBuilder + .createCommunity(context) + .withName("This community contains no collections") + .withAdminGroup(eperson) + .build(); + Community child = CommunityBuilder + .createSubCommunity(context, parent) + .withName("This community contains a collection") + .build(); + Collection coll = CollectionBuilder + .createCollection(context, child) + .withName("Contained collection") + .build(); + return null; + }); + expectSomeResults(requestSitewideSubmitFeature()); + } + + @Test + public void testNoRightsOnCollection() throws Exception { + expectZeroResults(requestSubmitFeature(getCollectionUri(collectionA1))); + expectZeroResults(requestSubmitFeature(getCollectionUri(collectionA2))); + } + + @Test + public void testDirectEPersonWritePolicyOnCollection() throws Exception { + ResourcePolicy rp = ResourcePolicyBuilder.createResourcePolicy(context) + .withUser(eperson) + .withDspaceObject(collectionA1) + .withAction(Constants.ADD) + .build(); + expectSomeResults(requestSubmitFeature(getCollectionUri(collectionA1))); + expectZeroResults(requestSubmitFeature(getCollectionUri(collectionA2))); + } + + @Test + public void testDirectGroupWritePolicyOnCollection() throws Exception { + ResourcePolicy rp = ResourcePolicyBuilder.createResourcePolicy(context) + .withGroup(group) + .withDspaceObject(collectionA1) + .withAction(Constants.ADD) + .build(); + expectSomeResults(requestSubmitFeature(getCollectionUri(collectionA1))); + expectZeroResults(requestSubmitFeature(getCollectionUri(collectionA2))); + } + + @Test + public void testDirectEPersonAdminPolicyOnCollection() throws Exception { + ResourcePolicy rp = ResourcePolicyBuilder.createResourcePolicy(context) + .withUser(eperson) + .withDspaceObject(collectionA1) + .withAction(Constants.ADMIN) + .build(); + expectSomeResults(requestSubmitFeature(getCollectionUri(collectionA1))); + expectZeroResults(requestSubmitFeature(getCollectionUri(collectionA2))); + } + + @Test + public void testDirectGroupAdminPolicyOnCollection() throws Exception { + ResourcePolicy rp = ResourcePolicyBuilder.createResourcePolicy(context) + .withGroup(group) + .withDspaceObject(collectionA1) + .withAction(Constants.ADMIN) + .build(); + expectSomeResults(requestSubmitFeature(getCollectionUri(collectionA1))); + expectZeroResults(requestSubmitFeature(getCollectionUri(collectionA2))); + } + + @Test + public void testCollectionAdminOnCollection() throws Exception { + Collection col = withSuppressedAuthorization(() -> { + return CollectionBuilder + .createCollection(context, communityA) + .withName("this is another test collection") + .withAdminGroup(eperson) + .build(); + }); + expectSomeResults(requestSubmitFeature(getCollectionUri(col))); + expectZeroResults(requestSubmitFeature(getCollectionUri(collectionA1))); + } + + @Test + public void testCommunityWithCollectionsAdminOnCollection() throws Exception { + Collection coll = withSuppressedAuthorization(() -> { + Community comm = CommunityBuilder + .createCommunity(context) + .withName("This community contains a collection") + .withAdminGroup(eperson) + .build(); + return CollectionBuilder + .createCollection(context, comm) + .withName("Contained collection") + .build(); + }); + expectSomeResults(requestSubmitFeature(getCollectionUri(coll))); + } + + @Test + public void testCommunityWithSubCommunityWithCollectionsAdminOnCollection() throws Exception { + Collection coll = withSuppressedAuthorization(() -> { + Community parent = CommunityBuilder + .createCommunity(context) + .withName("This community contains no collections") + .withAdminGroup(eperson) + .build(); + Community child = CommunityBuilder + .createSubCommunity(context, parent) + .withName("This community contains a collection") + .build(); + return CollectionBuilder + .createCollection(context, child) + .withName("Contained collection") + .build(); + }); + expectSomeResults(requestSubmitFeature(getCollectionUri(coll))); + } + + private ResultActions requestSitewideSubmitFeature() throws Exception { + return requestSubmitFeature(siteUri); + } + + private ResultActions requestSubmitFeature(String uri) throws Exception { + epersonToken = getAuthToken(eperson.getEmail(), password); + return getClient(epersonToken).perform(get("/api/authz/authorizations/search/object?") + .param("uri", uri) + .param("feature", submitFeature.getName()) + .param("embed", "feature")); + } + + private ResultActions expectSomeResults(ResultActions actions) throws Exception { + return actions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", greaterThan(0))); + } + + private ResultActions expectZeroResults(ResultActions actions) throws Exception { + return actions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + } + + private T withSuppressedAuthorization(Callable fn) throws Exception { + context.turnOffAuthorisationSystem(); + T result = fn.call(); + context.restoreAuthSystemState(); + return result; + } + + private String getCollectionUri(Collection collection) { + CollectionRest collectionRest = collectionConverter.convert(collection, DefaultProjection.DEFAULT); + return utils.linkToSingleResource(collectionRest, "self").getHref(); + } +} From 36e9f0d4f710d39dc9a78d0cc30d97d9f1ba1468 Mon Sep 17 00:00:00 2001 From: Koen Pauwels Date: Mon, 5 Dec 2022 16:42:26 +0100 Subject: [PATCH 07/16] 97183 SubmitFeatureIT: renamed some tests --- .../org/dspace/app/rest/authorization/SubmitFeatureIT.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/SubmitFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/SubmitFeatureIT.java index 9404fb6620..bba45ecd22 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/SubmitFeatureIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/SubmitFeatureIT.java @@ -198,7 +198,7 @@ public class SubmitFeatureIT extends AbstractControllerIntegrationTest { } @Test - public void testDirectEPersonWritePolicyOnCollection() throws Exception { + public void testDirectEPersonAddPolicyOnCollection() throws Exception { ResourcePolicy rp = ResourcePolicyBuilder.createResourcePolicy(context) .withUser(eperson) .withDspaceObject(collectionA1) @@ -209,7 +209,7 @@ public class SubmitFeatureIT extends AbstractControllerIntegrationTest { } @Test - public void testDirectGroupWritePolicyOnCollection() throws Exception { + public void testDirectGroupAddPolicyOnCollection() throws Exception { ResourcePolicy rp = ResourcePolicyBuilder.createResourcePolicy(context) .withGroup(group) .withDspaceObject(collectionA1) From ada0b8c4d1c3e4b2df45ee22f52046b54805b420 Mon Sep 17 00:00:00 2001 From: Koen Pauwels Date: Mon, 5 Dec 2022 16:49:47 +0100 Subject: [PATCH 08/16] 97183 Added EditItemFeature --- .../authorization/impl/EditItemFeature.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EditItemFeature.java diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EditItemFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EditItemFeature.java new file mode 100644 index 0000000000..04f95d13e1 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EditItemFeature.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.model.SiteRest; +import org.dspace.app.rest.utils.Utils; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.Item; +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; + +@Component +@AuthorizationFeatureDocumentation(name = EditItemFeature.NAME, + description = "It can be used to verify if a user has rights to edit any item.") +public class EditItemFeature implements AuthorizationFeature { + public static final String NAME = "canEdit"; + @Autowired + AuthorizeService authService; + @Autowired + ItemService itemService; + + @Autowired + Utils utils; + + @Override + public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException, SearchServiceException { + if (object instanceof SiteRest) { + return itemService.countItemsWithEdit(context) > 0; + } else if (object instanceof ItemRest) { + Item item = (Item) utils.getDSpaceAPIObjectFromRest(context, object); + return authService.authorizeActionBoolean(context, item, Constants.WRITE); + } + return false; + } + + @Override + public String[] getSupportedTypes() { + return new String[] { + ItemRest.CATEGORY + "." + ItemRest.NAME, + SiteRest.CATEGORY + "." + SiteRest.NAME + }; + } +} From ebccac79f7ddd9c74e0d46b2b59dbc362c225c8e Mon Sep 17 00:00:00 2001 From: Koen Pauwels Date: Tue, 6 Dec 2022 09:09:39 +0100 Subject: [PATCH 09/16] 97183 EditItemFeature integration tests --- .../rest/authorization/EditItemFeatureIT.java | 279 ++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EditItemFeatureIT.java diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EditItemFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EditItemFeatureIT.java new file mode 100644 index 0000000000..4bdc7743b5 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EditItemFeatureIT.java @@ -0,0 +1,279 @@ +/** + * 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 java.util.concurrent.Callable; + +import org.dspace.app.rest.authorization.impl.EditItemFeature; +import org.dspace.app.rest.converter.ItemConverter; +import org.dspace.app.rest.converter.SiteConverter; +import org.dspace.app.rest.model.ItemRest; +import org.dspace.app.rest.model.SiteRest; +import org.dspace.app.rest.projection.DefaultProjection; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.app.rest.utils.Utils; +import org.dspace.authorize.ResourcePolicy; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.GroupBuilder; +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.content.Site; +import org.dspace.content.service.SiteService; +import org.dspace.core.Constants; +import org.dspace.eperson.Group; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.servlet.ResultActions; + +public class EditItemFeatureIT extends AbstractControllerIntegrationTest { + @Autowired + private AuthorizationFeatureService authorizationFeatureService; + @Autowired + private SiteService siteService; + @Autowired + private ItemConverter itemConverter; + @Autowired + private SiteConverter siteConverter; + @Autowired + private Utils utils; + + private Group group; + private String siteUri; + private String epersonToken; + private AuthorizationFeature editItemFeature; + private Community communityA; + + private Collection collectionA1; + private Collection collectionA2; + + private Item itemA1X; + private Item itemA2X; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + + withSuppressedAuthorization(() -> { + communityA = CommunityBuilder.createCommunity(context).withName("Community A").build(); + collectionA1 = CollectionBuilder.createCollection(context, communityA).withName("Collection A1").build(); + collectionA2 = CollectionBuilder.createCollection(context, communityA).withName("Collection A2").build(); + itemA1X = ItemBuilder.createItem(context, collectionA1).withTitle("Item A1X").build(); + itemA2X = ItemBuilder.createItem(context, collectionA2).withTitle("Item A2X").build(); + group = GroupBuilder.createGroup(context) + .withName("group") + .addMember(eperson) + .build(); + return null; + }); + editItemFeature = authorizationFeatureService.find(EditItemFeature.NAME); + + Site site = siteService.findSite(context); + SiteRest siteRest = siteConverter.convert(site, DefaultProjection.DEFAULT); + siteUri = utils.linkToSingleResource(siteRest, "self").getHref(); + } + + @Test + public void testNoRights() throws Exception { + expectZeroResults(requestSitewideEditItemFeature()); + } + + @Test + public void testDirectEPersonWritePolicy() throws Exception { + ResourcePolicy rp = ResourcePolicyBuilder.createResourcePolicy(context) + .withUser(eperson) + .withDspaceObject(itemA1X) + .withAction(Constants.WRITE) + .build(); + expectSomeResults(requestSitewideEditItemFeature()); + expectSomeResults(requestEditItemFeature(itemA1X)); + expectZeroResults(requestEditItemFeature(itemA2X)); + } + + @Test + public void testDirectGroupWritePolicy() throws Exception { + ResourcePolicy rp = ResourcePolicyBuilder.createResourcePolicy(context) + .withGroup(group) + .withDspaceObject(itemA1X) + .withAction(Constants.WRITE) + .build(); + expectSomeResults(requestSitewideEditItemFeature()); + expectSomeResults(requestEditItemFeature(itemA1X)); + expectZeroResults(requestEditItemFeature(itemA2X)); + } + + @Test + public void testDirectEPersonAdminPolicy() throws Exception { + ResourcePolicy rp = ResourcePolicyBuilder.createResourcePolicy(context) + .withUser(eperson) + .withDspaceObject(itemA1X) + .withAction(Constants.ADMIN) + .build(); + expectSomeResults(requestSitewideEditItemFeature()); + expectSomeResults(requestEditItemFeature(itemA1X)); + expectZeroResults(requestEditItemFeature(itemA2X)); + } + + @Test + public void testDirectGroupAdminPolicy() throws Exception { + ResourcePolicy rp = ResourcePolicyBuilder.createResourcePolicy(context) + .withGroup(group) + .withDspaceObject(itemA1X) + .withAction(Constants.ADMIN) + .build(); + expectSomeResults(requestSitewideEditItemFeature()); + expectSomeResults(requestEditItemFeature(itemA1X)); + expectZeroResults(requestEditItemFeature(itemA2X)); + } + + @Test + public void testNonemptyCollectionAdmin() throws Exception { + Item item = withSuppressedAuthorization(() -> { + Collection col = CollectionBuilder + .createCollection(context, communityA) + .withName("nonempty collection") + .withAdminGroup(eperson) + .build(); + return ItemBuilder + .createItem(context, col) + .withTitle("item in nonempty collection") + .build(); + }); + expectSomeResults(requestSitewideEditItemFeature()); + expectSomeResults(requestEditItemFeature(item)); + expectZeroResults(requestEditItemFeature(itemA1X)); + expectZeroResults(requestEditItemFeature(itemA2X)); + } + + @Test + public void testEmptyCollectionAdmin() throws Exception { + withSuppressedAuthorization(() -> { + Collection col = CollectionBuilder + .createCollection(context, communityA) + .withName("nonempty collection") + .withAdminGroup(eperson) + .build(); + return null; + }); + expectZeroResults(requestSitewideEditItemFeature()); + } + + @Test + public void testCommunityWithEmptyCollectionAdmin() throws Exception { + withSuppressedAuthorization(() -> { + Community comm = CommunityBuilder + .createCommunity(context) + .withName("This community contains a collection") + .withAdminGroup(eperson) + .build(); + Collection coll = CollectionBuilder + .createCollection(context, comm) + .withName("This collection contains no items") + .build(); + return null; + }); + expectZeroResults(requestSitewideEditItemFeature()); + } + + @Test + public void testCommunityWithNonemptyCollectionAdmin() throws Exception { + Item item = withSuppressedAuthorization(() -> { + Community comm = CommunityBuilder + .createCommunity(context) + .withName("This community contains a collection") + .withAdminGroup(eperson) + .build(); + Collection coll = CollectionBuilder + .createCollection(context, comm) + .withName("This collection contains an item") + .build(); + return ItemBuilder + .createItem(context, coll) + .withTitle("This is an item") + .build(); + }); + expectSomeResults(requestSitewideEditItemFeature()); + expectSomeResults(requestEditItemFeature(item)); + } + + @Test + public void testNestedCommunitiesWithNonemptyCollectionAdmin() throws Exception { + Item item = withSuppressedAuthorization(() -> { + Community parent = CommunityBuilder + .createCommunity(context) + .withName("parent community") + .withAdminGroup(eperson) + .build(); + Community child = CommunityBuilder + .createSubCommunity(context, parent) + .withName("child community") + .withAdminGroup(eperson) + .build(); + Collection coll = CollectionBuilder + .createCollection(context, child) + .withName("This collection contains an item") + .build(); + return ItemBuilder + .createItem(context, coll) + .withTitle("This is an item") + .build(); + }); + expectSomeResults(requestSitewideEditItemFeature()); + expectSomeResults(requestEditItemFeature(item)); + } + + private ResultActions requestSitewideEditItemFeature() throws Exception { + return requestEditItemFeature(siteUri); + } + + private ResultActions requestEditItemFeature(Item item) throws Exception { + return requestEditItemFeature(getItemUri(item)); + } + private ResultActions requestEditItemFeature(String uri) throws Exception { + epersonToken = getAuthToken(eperson.getEmail(), password); + return getClient(epersonToken).perform(get("/api/authz/authorizations/search/object?") + .param("uri", uri) + .param("feature", editItemFeature.getName()) + .param("embed", "feature")); + } + + private ResultActions expectSomeResults(ResultActions actions) throws Exception { + return actions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", greaterThan(0))); + } + + private ResultActions expectZeroResults(ResultActions actions) throws Exception { + return actions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + } + + private T withSuppressedAuthorization(Callable fn) throws Exception { + context.turnOffAuthorisationSystem(); + T result = fn.call(); + context.restoreAuthSystemState(); + return result; + } + + private String getItemUri(Item item) { + ItemRest itemRest = itemConverter.convert(item, DefaultProjection.DEFAULT); + return utils.linkToSingleResource(itemRest, "self").getHref(); + } +} From f928ec71f60543c04ab9c206faaeb7c5a12969c7 Mon Sep 17 00:00:00 2001 From: Koen Pauwels Date: Tue, 6 Dec 2022 16:12:38 +0100 Subject: [PATCH 10/16] 97183 Renamed "canEditItem" feature --- .../org/dspace/app/rest/authorization/impl/EditItemFeature.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EditItemFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EditItemFeature.java index 04f95d13e1..5c605daaf4 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EditItemFeature.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EditItemFeature.java @@ -28,7 +28,7 @@ import org.springframework.stereotype.Component; @AuthorizationFeatureDocumentation(name = EditItemFeature.NAME, description = "It can be used to verify if a user has rights to edit any item.") public class EditItemFeature implements AuthorizationFeature { - public static final String NAME = "canEdit"; + public static final String NAME = "canEditItem"; @Autowired AuthorizeService authService; @Autowired From 1252a982406670fac319923ad33bdd4b0c52b10c Mon Sep 17 00:00:00 2001 From: Koen Pauwels Date: Thu, 8 Dec 2022 10:33:31 +0100 Subject: [PATCH 11/16] 97183 SolrServiceIndexItemEditorsPlugin: fixed logger type --- .../org/dspace/discovery/SolrServiceIndexItemEditorsPlugin.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexItemEditorsPlugin.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexItemEditorsPlugin.java index b10d2d72a3..09308be759 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexItemEditorsPlugin.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceIndexItemEditorsPlugin.java @@ -32,7 +32,7 @@ import org.springframework.beans.factory.annotation.Autowired; */ public class SolrServiceIndexItemEditorsPlugin implements SolrServiceIndexPlugin { private static final Logger log = org.apache.logging.log4j.LogManager - .getLogger(SolrServiceIndexCollectionSubmittersPlugin.class); + .getLogger(SolrServiceIndexItemEditorsPlugin.class); @Autowired(required = true) protected AuthorizeService authorizeService; From ec483fef5b9b856056df4307e314fb18f92738b5 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 9 Nov 2022 11:05:09 -0600 Subject: [PATCH 12/16] Update BCEL and add correct scope --- dspace-api/pom.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 6850051600..b277bf0584 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -806,10 +806,11 @@ test - + org.apache.bcel bcel - 6.4.0 + 6.6.0 + test From 7ee46e0cba582a299a3f0a74e99d251f7409742f Mon Sep 17 00:00:00 2001 From: Koen Pauwels Date: Mon, 2 Jan 2023 09:58:31 +0100 Subject: [PATCH 13/16] 97183 Processing PR review feedback --- .../src/main/java/org/dspace/content/ItemServiceImpl.java | 2 ++ .../src/main/java/org/dspace/discovery/IndexingUtils.java | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) 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 156c8b2712..380941b776 100644 --- a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java @@ -1100,6 +1100,7 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl implements It return searchService.search(context, discoverQuery); } + @Override public List findItemsWithEdit(Context context, int offset, int limit) throws SQLException, SearchServiceException { DiscoverQuery discoverQuery = new DiscoverQuery(); @@ -1112,6 +1113,7 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl implements It .collect(Collectors.toList()); } + @Override public int countItemsWithEdit(Context context) throws SQLException, SearchServiceException { DiscoverQuery discoverQuery = new DiscoverQuery(); discoverQuery.setMaxResults(0); diff --git a/dspace-api/src/main/java/org/dspace/discovery/IndexingUtils.java b/dspace-api/src/main/java/org/dspace/discovery/IndexingUtils.java index c4848c7432..8dd02f5d44 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/IndexingUtils.java +++ b/dspace-api/src/main/java/org/dspace/discovery/IndexingUtils.java @@ -34,8 +34,6 @@ public class IndexingUtils { * Retrieve all ancestor communities of a given community, with the first one being the given community and the * last one being the root. *

- * TODO: can be done in a single SQL query with recursive common table expressions - * TODO: should probably be moved to CommunityService * * @param context DSpace context object * @param community Community for which we search the ancestors From 35790266949bc8bae6303d0a16435678dee286c9 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Thu, 12 Jan 2023 13:10:55 +0100 Subject: [PATCH 14/16] 98462: Use discovery configurations in browse --- .../java/org/dspace/browse/BrowseDAO.java | 18 +- .../java/org/dspace/browse/BrowseEngine.java | 12 +- .../java/org/dspace/browse/SolrBrowseDAO.java | 24 +- .../app/rest/BrowsesResourceControllerIT.java | 383 ++++++++++++++++++ 4 files changed, 412 insertions(+), 25 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/browse/BrowseDAO.java b/dspace-api/src/main/java/org/dspace/browse/BrowseDAO.java index 22cf02fe13..d65c1d2a12 100644 --- a/dspace-api/src/main/java/org/dspace/browse/BrowseDAO.java +++ b/dspace-api/src/main/java/org/dspace/browse/BrowseDAO.java @@ -8,8 +8,8 @@ package org.dspace.browse; import java.util.List; -import java.util.UUID; +import org.dspace.content.DSpaceObject; import org.dspace.content.Item; /** @@ -140,21 +140,21 @@ public interface BrowseDAO { public void setAscending(boolean ascending); /** - * Get the database ID of the container object. The container object will be a + * Get the container object. The container object will be a * Community or a Collection. * - * @return the database id of the container, or -1 if none is set + * @return the container, or null if none is set */ - public UUID getContainerID(); + public DSpaceObject getContainer(); /** - * Set the database id of the container object. This should be the id of a - * Community or Collection. This will constrain the results of the browse - * to only items or values within items that appear in the given container. + * Set the container object. This should be a Community or Collection. + * This will constrain the results of the browse to only items or values within items that appear in the given + * container and add the related configuration default filters. * - * @param containerID community/collection internal ID (UUID) + * @param container community/collection */ - public void setContainerID(UUID containerID); + public void setContainer(DSpaceObject container); /** * get the name of the field in which to look for the container id. This is diff --git a/dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java b/dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java index 7454c8e82b..72b75ff2a8 100644 --- a/dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java +++ b/dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java @@ -140,12 +140,12 @@ public class BrowseEngine { Collection col = (Collection) scope.getBrowseContainer(); dao.setContainerTable("collection2item"); dao.setContainerIDField("collection_id"); - dao.setContainerID(col.getID()); + dao.setContainer(col); } else if (scope.inCommunity()) { Community com = (Community) scope.getBrowseContainer(); dao.setContainerTable("communities2item"); dao.setContainerIDField("community_id"); - dao.setContainerID(com.getID()); + dao.setContainer(com); } } @@ -239,12 +239,12 @@ public class BrowseEngine { Collection col = (Collection) scope.getBrowseContainer(); dao.setContainerTable("collection2item"); dao.setContainerIDField("collection_id"); - dao.setContainerID(col.getID()); + dao.setContainer(col); } else if (scope.inCommunity()) { Community com = (Community) scope.getBrowseContainer(); dao.setContainerTable("communities2item"); dao.setContainerIDField("community_id"); - dao.setContainerID(com.getID()); + dao.setContainer(com); } } @@ -408,12 +408,12 @@ public class BrowseEngine { Collection col = (Collection) scope.getBrowseContainer(); dao.setContainerTable("collection2item"); dao.setContainerIDField("collection_id"); - dao.setContainerID(col.getID()); + dao.setContainer(col); } else if (scope.inCommunity()) { Community com = (Community) scope.getBrowseContainer(); dao.setContainerTable("communities2item"); dao.setContainerIDField("community_id"); - dao.setContainerID(com.getID()); + dao.setContainer(com); } } diff --git a/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java b/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java index 6a960e8d75..c42db988de 100644 --- a/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java +++ b/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java @@ -13,12 +13,12 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; -import java.util.UUID; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.dspace.authorize.factory.AuthorizeServiceFactory; import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.DSpaceObject; import org.dspace.content.Item; import org.dspace.core.Context; import org.dspace.discovery.DiscoverFacetField; @@ -30,6 +30,8 @@ import org.dspace.discovery.DiscoverResult.SearchDocument; import org.dspace.discovery.IndexableObject; import org.dspace.discovery.SearchService; import org.dspace.discovery.SearchServiceException; +import org.dspace.discovery.SearchUtils; +import org.dspace.discovery.configuration.DiscoveryConfiguration; import org.dspace.discovery.configuration.DiscoveryConfigurationParameters; import org.dspace.discovery.indexobject.IndexableItem; import org.dspace.services.factory.DSpaceServicesFactory; @@ -123,9 +125,9 @@ public class SolrBrowseDAO implements BrowseDAO { private String containerIDField = null; /** - * the database id of the container we are constraining to + * the container we are constraining to */ - private UUID containerID = null; + private DSpaceObject container = null; /** * the column that we are sorting results by @@ -235,13 +237,15 @@ public class SolrBrowseDAO implements BrowseDAO { } private void addLocationScopeFilter(DiscoverQuery query) { - if (containerID != null) { + if (container != null) { if (containerIDField.startsWith("collection")) { - query.addFilterQueries("location.coll:" + containerID); + query.addFilterQueries("location.coll:" + container.getID()); } else if (containerIDField.startsWith("community")) { - query.addFilterQueries("location.comm:" + containerID); + query.addFilterQueries("location.comm:" + container.getID()); } } + DiscoveryConfiguration discoveryConfiguration = SearchUtils.getDiscoveryConfiguration(container); + discoveryConfiguration.getDefaultFilterQueries().forEach(query::addFilterQueries); } @Override @@ -393,8 +397,8 @@ public class SolrBrowseDAO implements BrowseDAO { * @see org.dspace.browse.BrowseDAO#getContainerID() */ @Override - public UUID getContainerID() { - return containerID; + public DSpaceObject getContainer() { + return container; } /* @@ -556,8 +560,8 @@ public class SolrBrowseDAO implements BrowseDAO { * @see org.dspace.browse.BrowseDAO#setContainerID(int) */ @Override - public void setContainerID(UUID containerID) { - this.containerID = containerID; + public void setContainer(DSpaceObject container) { + this.container = container; } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java index 5bb3ce6078..adce4099f1 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java @@ -176,6 +176,24 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe .withSubject("AnotherTest").withSubject("TestingForMore") .withSubject("ExtraEntry") .build(); + Item withdrawnItem1 = ItemBuilder.createItem(context, col2) + .withTitle("Withdrawn item 1") + .withIssueDate("2016-02-13") + .withAuthor("Smith, Maria").withAuthor("Doe, Jane") + .withSubject("AnotherTest").withSubject("TestingForMore") + .withSubject("ExtraEntry").withSubject("WithdrawnEntry") + .withdrawn() + .build(); + Item privateItem1 = ItemBuilder.createItem(context, col2) + .withTitle("Private item 1") + .withIssueDate("2016-02-13") + .withAuthor("Smith, Maria").withAuthor("Doe, Jane") + .withSubject("AnotherTest").withSubject("TestingForMore") + .withSubject("ExtraEntry").withSubject("PrivateEntry") + .makeUnDiscoverable() + .build(); + + context.restoreAuthSystemState(); @@ -369,6 +387,23 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe .withSubject("AnotherTest") .build(); + Item withdrawnItem1 = ItemBuilder.createItem(context, col2) + .withTitle("Withdrawn item 1") + .withIssueDate("2016-02-13") + .withAuthor("Smith, Maria").withAuthor("Doe, Jane") + .withSubject("AnotherTest").withSubject("TestingForMore") + .withSubject("ExtraEntry").withSubject("WithdrawnEntry") + .withdrawn() + .build(); + Item privateItem1 = ItemBuilder.createItem(context, col2) + .withTitle("Private item 1") + .withIssueDate("2016-02-13") + .withAuthor("Smith, Maria").withAuthor("Doe, Jane") + .withSubject("AnotherTest").withSubject("TestingForMore") + .withSubject("ExtraEntry").withSubject("PrivateEntry") + .makeUnDiscoverable() + .build(); + context.restoreAuthSystemState(); //** WHEN ** @@ -407,6 +442,152 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe ItemMatcher.matchItemWithTitleAndDateIssued(publicItem1, "zPublic item more", "2017-10-17") ))); + //** WHEN ** + //An anonymous user browses the items that correspond with the PrivateEntry subject query + getClient().perform(get("/api/discover/browses/subject/items") + .param("filterValue", "PrivateEntry")) + //** THEN ** + //The status has to be 200 + .andExpect(status().isOk()) + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + //We expect there to be no elements because the item is private + .andExpect(jsonPath("$.page.totalElements", is(0))) + .andExpect(jsonPath("$.page.size", is(20))); + + //** WHEN ** + //An anonymous user browses the items that correspond with the WithdrawnEntry subject query + getClient().perform(get("/api/discover/browses/subject/items") + .param("filterValue", "WithdrawnEntry")) + //** THEN ** + //The status has to be 200 + .andExpect(status().isOk()) + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + //We expect there to be no elements because the item is withdrawn + .andExpect(jsonPath("$.page.totalElements", is(0))) + .andExpect(jsonPath("$.page.size", is(20))); + } + + @Test + public void findBrowseBySubjectItemsWithScope() throws Exception { + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and two collections. + 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(); + + //2. Two public items with the same subject and another public item that contains that same subject, but also + // another one + // All of the items are readable by an Anonymous user + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("zPublic item more") + .withIssueDate("2017-10-17") + .withAuthor("Smith, Donald").withAuthor("Doe, John") + .withSubject("ExtraEntry").withSubject("AnotherTest") + .build(); + + Item publicItem2 = ItemBuilder.createItem(context, col2) + .withTitle("Public item 2") + .withIssueDate("2016-02-13") + .withAuthor("Smith, Maria").withAuthor("Doe, Jane") + .withSubject("AnotherTest") + .build(); + + Item publicItem3 = ItemBuilder.createItem(context, col2) + .withTitle("Public item 3") + .withIssueDate("2016-02-14") + .withAuthor("Smith, Maria").withAuthor("Doe, Jane") + .withSubject("AnotherTest") + .build(); + + Item withdrawnItem1 = ItemBuilder.createItem(context, col2) + .withTitle("Withdrawn item 1") + .withIssueDate("2016-02-13") + .withAuthor("Smith, Maria").withAuthor("Doe, Jane") + .withSubject("AnotherTest").withSubject("TestingForMore") + .withSubject("ExtraEntry").withSubject("WithdrawnEntry") + .withdrawn() + .build(); + Item privateItem1 = ItemBuilder.createItem(context, col2) + .withTitle("Private item 1") + .withIssueDate("2016-02-13") + .withAuthor("Smith, Maria").withAuthor("Doe, Jane") + .withSubject("AnotherTest").withSubject("TestingForMore") + .withSubject("ExtraEntry").withSubject("PrivateEntry") + .makeUnDiscoverable() + .build(); + + context.restoreAuthSystemState(); + + //** WHEN ** + //An anonymous user browses the items that correspond with the ExtraEntry subject query + getClient().perform(get("/api/discover/browses/subject/items") + .param("scope", String.valueOf(col2.getID())) + .param("filterValue", "ExtraEntry")) + //** THEN ** + //The status has to be 200 + .andExpect(status().isOk()) + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + //We expect there to be no elements in collection 2 + .andExpect(jsonPath("$.page.totalElements", is(0))) + .andExpect(jsonPath("$.page.size", is(20))); + + //** WHEN ** + //An anonymous user browses the items that correspond with the AnotherTest subject query + getClient().perform(get("/api/discover/browses/subject/items") + .param("scope", String.valueOf(col2.getID())) + .param("filterValue", "AnotherTest")) + //** THEN ** + //The status has to be 200 + .andExpect(status().isOk()) + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + //We expect there to be only two elements, the ones that we've added with the requested subject + // in collection 2 + .andExpect(jsonPath("$.page.totalElements", is(2))) + .andExpect(jsonPath("$.page.size", is(20))) + //Verify that the title of the public and embargoed items are present and sorted descending + .andExpect(jsonPath("$._embedded.items", contains( + ItemMatcher.matchItemWithTitleAndDateIssued(publicItem2, "Public item 2", "2016-02-13"), + ItemMatcher.matchItemWithTitleAndDateIssued(publicItem3, "Public item 3", "2016-02-14") + ))); + + //** WHEN ** + //An anonymous user browses the items that correspond with the PrivateEntry subject query + getClient().perform(get("/api/discover/browses/subject/items") + .param("scope", String.valueOf(col2.getID())) + .param("filterValue", "PrivateEntry")) + //** THEN ** + //The status has to be 200 + .andExpect(status().isOk()) + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + //We expect there to be no elements because the item is private + .andExpect(jsonPath("$.page.totalElements", is(0))) + .andExpect(jsonPath("$.page.size", is(20))); + + //** WHEN ** + //An anonymous user browses the items that correspond with the WithdrawnEntry subject query + getClient().perform(get("/api/discover/browses/subject/items") + .param("scope", String.valueOf(col2.getID())) + .param("filterValue", "WithdrawnEntry")) + //** THEN ** + //The status has to be 200 + .andExpect(status().isOk()) + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + //We expect there to be no elements because the item is withdrawn + .andExpect(jsonPath("$.page.totalElements", is(0))) + .andExpect(jsonPath("$.page.size", is(20))); } @Test @@ -503,6 +684,98 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe not(matchMetadata("dc.title", "Internal publication"))))); } + @Test + public void findBrowseByTitleItemsWithScope() throws Exception { + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and two collections. + 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(); + + //2. Two public items that are readable by Anonymous + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Public item 1") + .withIssueDate("2017-10-17") + .withAuthor("Smith, Donald").withAuthor("Doe, John") + .withSubject("Java").withSubject("Unit Testing") + .build(); + + Item publicItem2 = ItemBuilder.createItem(context, col2) + .withTitle("Public item 2") + .withIssueDate("2016-02-13") + .withAuthor("Smith, Maria").withAuthor("Doe, Jane") + .withSubject("Angular").withSubject("Unit Testing") + .build(); + + //3. An item that has been made private + Item privateItem = ItemBuilder.createItem(context, col1) + .withTitle("This is a private item") + .withIssueDate("2015-03-12") + .withAuthor("Duck, Donald") + .withSubject("Cartoons").withSubject("Ducks") + .makeUnDiscoverable() + .build(); + + //4. An item with an item-level embargo + Item embargoedItem = ItemBuilder.createItem(context, col2) + .withTitle("An embargoed publication") + .withIssueDate("2017-08-10") + .withAuthor("Mouse, Mickey") + .withSubject("Cartoons").withSubject("Mice") + .withEmbargoPeriod("12 months") + .build(); + + //5. An item that is only readable for an internal groups + Group internalGroup = GroupBuilder.createGroup(context) + .withName("Internal Group") + .build(); + + Item internalItem = ItemBuilder.createItem(context, col2) + .withTitle("Internal publication") + .withIssueDate("2016-09-19") + .withAuthor("Doe, John") + .withSubject("Unknown") + .withReaderGroup(internalGroup) + .build(); + + context.restoreAuthSystemState(); + + //** WHEN ** + //An anonymous user browses the items in the Browse by item endpoint + //sorted descending by tile + getClient().perform(get("/api/discover/browses/title/items") + .param("scope", String.valueOf(col2.getID())) + .param("sort", "title,desc")) + + //** THEN ** + //The status has to be 200 OK + .andExpect(status().isOk()) + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(1))) + .andExpect(jsonPath("$.page.totalPages", is(1))) + .andExpect(jsonPath("$.page.number", is(0))) + + .andExpect(jsonPath("$._embedded.items", + contains(ItemMatcher.matchItemWithTitleAndDateIssued(publicItem2, + "Public item 2", + "2016-02-13")))) + + //The private and internal items must not be present + .andExpect(jsonPath("$._embedded.items[*].metadata", Matchers.allOf( + not(matchMetadata("dc.title", "This is a private item")), + not(matchMetadata("dc.title", "Internal publication"))))); + } + @Test /** * This test was introduced to reproduce the bug DS-4269 Pagination links must be consistent also when there is not @@ -623,6 +896,18 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe .withIssueDate("2016-01-12") .build(); + Item withdrawnItem1 = ItemBuilder.createItem(context, col2) + .withTitle("Withdrawn item 1") + .withIssueDate("2016-02-13") + .withdrawn() + .build(); + + Item privateItem1 = ItemBuilder.createItem(context, col2) + .withTitle("Private item 1") + .makeUnDiscoverable() + .build(); + + context.restoreAuthSystemState(); //** WHEN ** @@ -684,6 +969,104 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe ))); } + @Test + public void testPaginationBrowseByDateIssuedItemsWithScope() throws Exception { + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and two collections. + 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(); + + //2. 7 public items that are readable by Anonymous + Item item1 = ItemBuilder.createItem(context, col1) + .withTitle("Item 1") + .withIssueDate("2017-10-17") + .build(); + + Item item2 = ItemBuilder.createItem(context, col2) + .withTitle("Item 2") + .withIssueDate("2016-02-13") + .build(); + + Item item3 = ItemBuilder.createItem(context, col1) + .withTitle("Item 3") + .withIssueDate("2016-02-12") + .build(); + + Item item4 = ItemBuilder.createItem(context, col2) + .withTitle("Item 4") + .withIssueDate("2016-02-11") + .build(); + + Item item5 = ItemBuilder.createItem(context, col1) + .withTitle("Item 5") + .withIssueDate("2016-02-10") + .build(); + + Item item6 = ItemBuilder.createItem(context, col2) + .withTitle("Item 6") + .withIssueDate("2016-01-13") + .build(); + + Item item7 = ItemBuilder.createItem(context, col1) + .withTitle("Item 7") + .withIssueDate("2016-01-12") + .build(); + + Item withdrawnItem1 = ItemBuilder.createItem(context, col2) + .withTitle("Withdrawn item 1") + .withIssueDate("2016-02-13") + .withdrawn() + .build(); + + Item privateItem1 = ItemBuilder.createItem(context, col2) + .withTitle("Private item 1") + .makeUnDiscoverable() + .build(); + + + context.restoreAuthSystemState(); + + //** WHEN ** + //An anonymous user browses the items in the Browse by date issued endpoint + //sorted ascending by tile with a page size of 5 + getClient().perform(get("/api/discover/browses/dateissued/items") + .param("scope", String.valueOf(col2.getID())) + .param("sort", "title,asc") + .param("size", "5")) + + //** THEN ** + //The status has to be 200 OK + .andExpect(status().isOk()) + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + + //We expect only the first five items to be present + .andExpect(jsonPath("$.page.size", is(5))) + .andExpect(jsonPath("$.page.totalElements", is(3))) + .andExpect(jsonPath("$.page.totalPages", is(1))) + .andExpect(jsonPath("$.page.number", is(0))) + + //Verify that the title and date of the items match and that they are sorted ascending + .andExpect(jsonPath("$._embedded.items", + contains( + ItemMatcher.matchItemWithTitleAndDateIssued(item2, + "Item 2", "2016-02-13"), + ItemMatcher.matchItemWithTitleAndDateIssued(item4, + "Item 4", "2016-02-11"), + ItemMatcher.matchItemWithTitleAndDateIssued(item6, + "Item 6", "2016-01-13") + ))); + + } + @Test public void testBrowseByEntriesStartsWith() throws Exception { context.turnOffAuthorisationSystem(); From d18120919c19c6c7e34ab0181d79dfdea718aa0f Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Thu, 12 Jan 2023 16:32:51 +0100 Subject: [PATCH 15/16] 98462: Refactor change and add admin tests --- .../java/org/dspace/browse/SolrBrowseDAO.java | 5 + .../app/rest/BrowsesResourceControllerIT.java | 257 +++++++++++++++++- 2 files changed, 261 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java b/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java index c42db988de..9a4d4869aa 100644 --- a/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java +++ b/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java @@ -177,6 +177,7 @@ public class SolrBrowseDAO implements BrowseDAO { if (sResponse == null) { DiscoverQuery query = new DiscoverQuery(); addLocationScopeFilter(query); + addDefaultFilterQueries(query); addStatusFilter(query); if (distinct) { DiscoverFacetField dff; @@ -244,6 +245,9 @@ public class SolrBrowseDAO implements BrowseDAO { query.addFilterQueries("location.comm:" + container.getID()); } } + } + + private void addDefaultFilterQueries(DiscoverQuery query) { DiscoveryConfiguration discoveryConfiguration = SearchUtils.getDiscoveryConfiguration(container); discoveryConfiguration.getDefaultFilterQueries().forEach(query::addFilterQueries); } @@ -336,6 +340,7 @@ public class SolrBrowseDAO implements BrowseDAO { throws BrowseException { DiscoverQuery query = new DiscoverQuery(); addLocationScopeFilter(query); + addDefaultFilterQueries(query); addStatusFilter(query); query.setMaxResults(0); query.addFilterQueries("search.resourcetype:" + IndexableItem.TYPE); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java index adce4099f1..626b819fc8 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java @@ -590,6 +590,130 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe .andExpect(jsonPath("$.page.size", is(20))); } + @Test + public void findBrowseBySubjectItemsWithScopeAsAdmin() throws Exception { + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and two collections. + 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(); + + //2. Two public items with the same subject and another public item that contains that same subject, but also + // another one + // All of the items are readable by an Anonymous user + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("zPublic item more") + .withIssueDate("2017-10-17") + .withAuthor("Smith, Donald").withAuthor("Doe, John") + .withSubject("ExtraEntry").withSubject("AnotherTest") + .build(); + + Item publicItem2 = ItemBuilder.createItem(context, col2) + .withTitle("Public item 2") + .withIssueDate("2016-02-13") + .withAuthor("Smith, Maria").withAuthor("Doe, Jane") + .withSubject("AnotherTest") + .build(); + + Item publicItem3 = ItemBuilder.createItem(context, col2) + .withTitle("Public item 3") + .withIssueDate("2016-02-14") + .withAuthor("Smith, Maria").withAuthor("Doe, Jane") + .withSubject("AnotherTest") + .build(); + + Item withdrawnItem1 = ItemBuilder.createItem(context, col2) + .withTitle("Withdrawn item 1") + .withIssueDate("2016-02-13") + .withAuthor("Smith, Maria").withAuthor("Doe, Jane") + .withSubject("AnotherTest").withSubject("TestingForMore") + .withSubject("ExtraEntry").withSubject("WithdrawnEntry") + .withdrawn() + .build(); + Item privateItem1 = ItemBuilder.createItem(context, col2) + .withTitle("Private item 1") + .withIssueDate("2016-02-13") + .withAuthor("Smith, Maria").withAuthor("Doe, Jane") + .withSubject("AnotherTest").withSubject("TestingForMore") + .withSubject("ExtraEntry").withSubject("PrivateEntry") + .makeUnDiscoverable() + .build(); + + context.restoreAuthSystemState(); + + String adminToken = getAuthToken(admin.getEmail(), password); + + + //** WHEN ** + //An admin user browses the items that correspond with the ExtraEntry subject query + getClient(adminToken).perform(get("/api/discover/browses/subject/items") + .param("scope", String.valueOf(col2.getID())) + .param("filterValue", "ExtraEntry")) + //** THEN ** + //The status has to be 200 + .andExpect(status().isOk()) + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + //We expect there to be no elements in collection 2 + .andExpect(jsonPath("$.page.totalElements", is(0))) + .andExpect(jsonPath("$.page.size", is(20))); + + //** WHEN ** + //An admin user browses the items that correspond with the AnotherTest subject query + getClient(adminToken).perform(get("/api/discover/browses/subject/items") + .param("scope", String.valueOf(col2.getID())) + .param("filterValue", "AnotherTest")) + //** THEN ** + //The status has to be 200 + .andExpect(status().isOk()) + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + //We expect there to be only two elements, the ones that we've added with the requested subject + // in collection 2 + .andExpect(jsonPath("$.page.totalElements", is(2))) + .andExpect(jsonPath("$.page.size", is(20))) + //Verify that the title of the public and embargoed items are present and sorted descending + .andExpect(jsonPath("$._embedded.items", contains( + ItemMatcher.matchItemWithTitleAndDateIssued(publicItem2, "Public item 2", "2016-02-13"), + ItemMatcher.matchItemWithTitleAndDateIssued(publicItem3, "Public item 3", "2016-02-14") + ))); + + //** WHEN ** + //An admin user browses the items that correspond with the PrivateEntry subject query + getClient(adminToken).perform(get("/api/discover/browses/subject/items") + .param("scope", String.valueOf(col2.getID())) + .param("filterValue", "PrivateEntry")) + //** THEN ** + //The status has to be 200 + .andExpect(status().isOk()) + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + //We expect there to be no elements because the item is private + .andExpect(jsonPath("$.page.totalElements", is(0))) + .andExpect(jsonPath("$.page.size", is(20))); + + //** WHEN ** + //An admin user browses the items that correspond with the WithdrawnEntry subject query + getClient(adminToken).perform(get("/api/discover/browses/subject/items") + .param("scope", String.valueOf(col2.getID())) + .param("filterValue", "WithdrawnEntry")) + //** THEN ** + //The status has to be 200 + .andExpect(status().isOk()) + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + //We expect there to be no elements because the item is withdrawn + .andExpect(jsonPath("$.page.totalElements", is(0))) + .andExpect(jsonPath("$.page.size", is(20))); + } + @Test public void findBrowseByTitleItems() throws Exception { context.turnOffAuthorisationSystem(); @@ -682,6 +806,45 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe .andExpect(jsonPath("$._embedded.items[*].metadata", Matchers.allOf( not(matchMetadata("dc.title", "This is a private item")), not(matchMetadata("dc.title", "Internal publication"))))); + + String adminToken = getAuthToken(admin.getEmail(), password); + + //** WHEN ** + //An anonymous user browses the items in the Browse by item endpoint + //sorted descending by tile + getClient(adminToken).perform(get("/api/discover/browses/title/items") + .param("sort", "title,desc")) + + //** THEN ** + //The status has to be 200 OK + .andExpect(status().isOk()) + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(4))) + .andExpect(jsonPath("$.page.totalPages", is(1))) + .andExpect(jsonPath("$.page.number", is(0))) + + .andExpect(jsonPath("$._embedded.items", + contains(ItemMatcher.matchItemWithTitleAndDateIssued(publicItem2, + "Public item 2", + "2016-02-13"), + ItemMatcher.matchItemWithTitleAndDateIssued(publicItem1, + "Public item 1", + "2017-10-17"), + ItemMatcher.matchItemWithTitleAndDateIssued(internalItem, + "Internal publication", + "2016-09-19"), + ItemMatcher.matchItemWithTitleAndDateIssued(embargoedItem, + "An embargoed publication", + "2017-08-10") + ))) + + //The private and internal items must not be present + .andExpect(jsonPath("$._embedded.items[*].metadata", Matchers.allOf( + not(matchMetadata("dc.title", "This is a private item")), + not(matchMetadata("dc.title", "Internal publication"))))); } @Test @@ -715,7 +878,7 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe .build(); //3. An item that has been made private - Item privateItem = ItemBuilder.createItem(context, col1) + Item privateItem = ItemBuilder.createItem(context, col2) .withTitle("This is a private item") .withIssueDate("2015-03-12") .withAuthor("Duck, Donald") @@ -774,6 +937,43 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe .andExpect(jsonPath("$._embedded.items[*].metadata", Matchers.allOf( not(matchMetadata("dc.title", "This is a private item")), not(matchMetadata("dc.title", "Internal publication"))))); + + String adminToken = getAuthToken(admin.getEmail(), password); + //** WHEN ** + //An admin user browses the items in the Browse by item endpoint + //sorted descending by tile + getClient(adminToken).perform(get("/api/discover/browses/title/items") + .param("scope", String.valueOf(col2.getID())) + .param("sort", "title,desc")) + + //** THEN ** + //The status has to be 200 OK + .andExpect(status().isOk()) + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(3))) + .andExpect(jsonPath("$.page.totalPages", is(1))) + .andExpect(jsonPath("$.page.number", is(0))) + .andExpect(jsonPath("$._embedded.items", contains( + ItemMatcher.matchItemWithTitleAndDateIssued(publicItem2, + "Public item 2", + "2016-02-13"), + ItemMatcher.matchItemWithTitleAndDateIssued(internalItem, + "Internal publication", + "2016-09-19"), + ItemMatcher.matchItemWithTitleAndDateIssued(embargoedItem, + "An embargoed publication", + "2017-08-10") + + ))) + + + //The private and internal items must not be present + .andExpect(jsonPath("$._embedded.items[*].metadata", Matchers.allOf( + not(matchMetadata("dc.title", "This is a private item")) + ))); } @Test @@ -960,6 +1160,32 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe .andExpect(jsonPath("$.page.totalPages", is(2))) .andExpect(jsonPath("$.page.number", is(1))) + //Verify that the title and date of the items match and that they are sorted ascending + .andExpect(jsonPath("$._embedded.items", + contains(ItemMatcher.matchItemWithTitleAndDateIssued(item6, + "Item 6", "2016-01-13"), + ItemMatcher.matchItemWithTitleAndDateIssued(item7, + "Item 7", "2016-01-12") + ))); + + String adminToken = getAuthToken(admin.getEmail(), password); + //The next page gives us the last two items + getClient(adminToken).perform(get("/api/discover/browses/dateissued/items") + .param("sort", "title,asc") + .param("size", "5") + .param("page", "1")) + + //The status has to be 200 OK + .andExpect(status().isOk()) + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + + //We expect only the first five items to be present + .andExpect(jsonPath("$.page.size", is(5))) + .andExpect(jsonPath("$.page.totalElements", is(7))) + .andExpect(jsonPath("$.page.totalPages", is(2))) + .andExpect(jsonPath("$.page.number", is(1))) + //Verify that the title and date of the items match and that they are sorted ascending .andExpect(jsonPath("$._embedded.items", contains(ItemMatcher.matchItemWithTitleAndDateIssued(item6, @@ -1065,6 +1291,35 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe "Item 6", "2016-01-13") ))); + String adminToken = getAuthToken(admin.getEmail(), password); + getClient(adminToken).perform(get("/api/discover/browses/dateissued/items") + .param("scope", String.valueOf(col2.getID())) + .param("sort", "title,asc") + .param("size", "5")) + + //** THEN ** + //The status has to be 200 OK + .andExpect(status().isOk()) + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + + //We expect only the first five items to be present + .andExpect(jsonPath("$.page.size", is(5))) + .andExpect(jsonPath("$.page.totalElements", is(3))) + .andExpect(jsonPath("$.page.totalPages", is(1))) + .andExpect(jsonPath("$.page.number", is(0))) + + //Verify that the title and date of the items match and that they are sorted ascending + .andExpect(jsonPath("$._embedded.items", + contains( + ItemMatcher.matchItemWithTitleAndDateIssued(item2, + "Item 2", "2016-02-13"), + ItemMatcher.matchItemWithTitleAndDateIssued(item4, + "Item 4", "2016-02-11"), + ItemMatcher.matchItemWithTitleAndDateIssued(item6, + "Item 6", "2016-01-13") + ))); + } @Test From 9534b7893e02fe226aab3ea6f36ece8d8dafc42b Mon Sep 17 00:00:00 2001 From: Koen Pauwels Date: Tue, 17 Jan 2023 16:14:42 +0100 Subject: [PATCH 16/16] 97183 Tests for findItemsWithEdit and countItemsWithEdit methods of ItemServiceImpl --- .../org/dspace/content/ItemServiceImpl.java | 4 +- .../content/service/ItemServiceTest.java | 81 +++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) 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 380941b776..ec4b964c8d 100644 --- a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java @@ -1076,9 +1076,9 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl implements It } /** - * Finds all Indexed Items where the current user has submit rights. If the user is an Admin, + * Finds all Indexed Items where the current user has edit rights. If the user is an Admin, * this is all Indexed Items. Otherwise, it includes those Items where - * an indexed "submit" policy lists either the eperson or one of the eperson's groups + * an indexed "edit" policy lists either the eperson or one of the eperson's groups * * @param context DSpace context * @param discoverQuery diff --git a/dspace-api/src/test/java/org/dspace/content/service/ItemServiceTest.java b/dspace-api/src/test/java/org/dspace/content/service/ItemServiceTest.java index 267d66ac2f..f63e05ae3f 100644 --- a/dspace-api/src/test/java/org/dspace/content/service/ItemServiceTest.java +++ b/dspace-api/src/test/java/org/dspace/content/service/ItemServiceTest.java @@ -20,12 +20,15 @@ import java.util.stream.Collectors; import org.apache.logging.log4j.Logger; import org.dspace.AbstractIntegrationTestWithDatabase; import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.ResourcePolicy; import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CommunityBuilder; import org.dspace.builder.EntityTypeBuilder; +import org.dspace.builder.GroupBuilder; import org.dspace.builder.ItemBuilder; import org.dspace.builder.RelationshipBuilder; import org.dspace.builder.RelationshipTypeBuilder; +import org.dspace.builder.ResourcePolicyBuilder; import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.EntityType; @@ -35,6 +38,8 @@ import org.dspace.content.Relationship; import org.dspace.content.RelationshipType; import org.dspace.content.WorkspaceItem; import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.core.Constants; +import org.dspace.eperson.Group; import org.dspace.versioning.Version; import org.dspace.versioning.factory.VersionServiceFactory; import org.dspace.versioning.service.VersioningService; @@ -473,6 +478,82 @@ public class ItemServiceTest extends AbstractIntegrationTestWithDatabase { context.restoreAuthSystemState(); } + @Test + public void testFindItemsWithEditNoRights() throws Exception { + context.setCurrentUser(eperson); + List result = itemService.findItemsWithEdit(context, 0, 10); + int count = itemService.countItemsWithEdit(context); + assertThat(result.size(), equalTo(0)); + assertThat(count, equalTo(0)); + } + + @Test + public void testFindAndCountItemsWithEditEPerson() throws Exception { + ResourcePolicy rp = ResourcePolicyBuilder.createResourcePolicy(context) + .withUser(eperson) + .withDspaceObject(item) + .withAction(Constants.WRITE) + .build(); + context.setCurrentUser(eperson); + List result = itemService.findItemsWithEdit(context, 0, 10); + int count = itemService.countItemsWithEdit(context); + assertThat(result.size(), equalTo(1)); + assertThat(count, equalTo(1)); + } + + @Test + public void testFindAndCountItemsWithAdminEPerson() throws Exception { + ResourcePolicy rp = ResourcePolicyBuilder.createResourcePolicy(context) + .withUser(eperson) + .withDspaceObject(item) + .withAction(Constants.ADMIN) + .build(); + context.setCurrentUser(eperson); + List result = itemService.findItemsWithEdit(context, 0, 10); + int count = itemService.countItemsWithEdit(context); + assertThat(result.size(), equalTo(1)); + assertThat(count, equalTo(1)); + } + + @Test + public void testFindAndCountItemsWithEditGroup() throws Exception { + context.turnOffAuthorisationSystem(); + Group group = GroupBuilder.createGroup(context) + .addMember(eperson) + .build(); + context.restoreAuthSystemState(); + + ResourcePolicy rp = ResourcePolicyBuilder.createResourcePolicy(context) + .withGroup(group) + .withDspaceObject(item) + .withAction(Constants.WRITE) + .build(); + context.setCurrentUser(eperson); + List result = itemService.findItemsWithEdit(context, 0, 10); + int count = itemService.countItemsWithEdit(context); + assertThat(result.size(), equalTo(1)); + assertThat(count, equalTo(1)); + } + + @Test + public void testFindAndCountItemsWithAdminGroup() throws Exception { + context.turnOffAuthorisationSystem(); + Group group = GroupBuilder.createGroup(context) + .addMember(eperson) + .build(); + context.restoreAuthSystemState(); + + ResourcePolicy rp = ResourcePolicyBuilder.createResourcePolicy(context) + .withGroup(group) + .withDspaceObject(item) + .withAction(Constants.ADMIN) + .build(); + context.setCurrentUser(eperson); + List result = itemService.findItemsWithEdit(context, 0, 10); + int count = itemService.countItemsWithEdit(context); + assertThat(result.size(), equalTo(1)); + assertThat(count, equalTo(1)); + } private void assertMetadataValue(String authorQualifier, String contributorElement, String dcSchema, String value, String authority, int place, MetadataValue metadataValue) { assertThat(metadataValue.getValue(), equalTo(value));