diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceAdministrativeSearchRestrictionPlugin.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceAdministrativeSearchRestrictionPlugin.java new file mode 100644 index 00000000000..e17d0f37238 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceAdministrativeSearchRestrictionPlugin.java @@ -0,0 +1,90 @@ +/** + * 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.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.Logger; +import org.apache.solr.client.solrj.SolrQuery; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.core.Context; +import org.dspace.core.LogHelper; +import org.dspace.eperson.service.GroupService; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Plugin that filters out non-administered items from administrative searches for collections and communities admins. + * + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + */ +public class SolrServiceAdministrativeSearchRestrictionPlugin implements SolrServiceSearchPlugin { + + private static final Logger log = + org.apache.logging.log4j.LogManager.getLogger(SolrServiceAdministrativeSearchRestrictionPlugin.class); + public static final String SEARCH_CONFIGURATION_PREFIX = "administrative"; + + @Autowired + protected AuthorizeService authorizeService; + @Autowired + protected GroupService groupService; + + private static boolean isAdministrativeConfiguration(DiscoverQuery discoveryQuery) { + return discoveryQuery != null && + StringUtils.isNotBlank(discoveryQuery.getDiscoveryConfigurationName()) && + discoveryQuery.getDiscoveryConfigurationName().startsWith(SEARCH_CONFIGURATION_PREFIX); + } + + @Override + public void additionalSearchParameters(Context context, DiscoverQuery discoveryQuery, SolrQuery solrQuery) { + try { + + // Only apply this plugin to administrative searches + if (!isAdministrativeConfiguration(discoveryQuery)) { + return; + } + + // Only apply this plugin to non-administrators + if (isAdmin(context)) { + return; + } + + // Only apply this plugin to community / collection administrators + if (!isCommunityCollAdmin(context)) { + return; + } + + // Applies filter query to restrict search results to only those that are administrate by the current user + solrQuery.addFilterQuery( + Stream.concat( + groupService.allMemberGroupsSet(context, context.getCurrentUser()) + .stream() + .map(group -> "g" + group.getID()), + Stream.of(context.getCurrentUser()) + .filter(Objects::nonNull) + .map(eperson -> String.valueOf(eperson.getID())) + ) + .collect(Collectors.joining(" OR ", "admin:(", ")")) + ); + } catch (SQLException e) { + log.error(LogHelper.getHeader(context, "Error while adding resource policy information to query", ""), e); + } + } + + private boolean isCommunityCollAdmin(Context context) throws SQLException { + return this.authorizeService.isCollectionAdmin(context) || this.authorizeService.isCommunityAdmin(context); + } + + private boolean isAdmin(Context context) throws SQLException { + return authorizeService.isAdmin(context); + } + +} diff --git a/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/test-discovery.xml b/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/test-discovery.xml index 3c09125b0fd..49a1fc6e060 100644 --- a/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/test-discovery.xml +++ b/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/test-discovery.xml @@ -52,6 +52,7 @@ + diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryRestControllerIT.java index 43aecb96e3a..ae5982e9fff 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryRestControllerIT.java @@ -5506,6 +5506,201 @@ public void discoverSearchObjectsTestForAdministrativeViewAdmin() throws Excepti .andExpect(jsonPath("$._links.self.href", containsString("/api/discover/search/objects"))); } + @Test + public void discoverSearchObjectsTestForAdministrativeViewCollCommAdministrators() throws Exception { + + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + + //1. A community-collection structure with one parent community with sub-community and two collections. + + EPerson commAdmin = + EPersonBuilder.createEPerson(context) + .withEmail("community-admin@4science.com") + .withPassword(password) + .withNameInMetadata("Community", "Admin") + .withCanLogin(true) + .build(); + + EPerson subCommAdmin = + EPersonBuilder.createEPerson(context) + .withEmail("sub-community-admin@4science.com") + .withPassword(password) + .withNameInMetadata("SubCommunity", "Admin") + .withCanLogin(true) + .build(); + + EPerson collAdmin = + EPersonBuilder.createEPerson(context) + .withEmail("collection-admin@4science.com") + .withPassword(password) + .withNameInMetadata("Collection", "Admin") + .withCanLogin(true) + .build(); + + parentCommunity = CommunityBuilder + .createCommunity(context) + .withName("Parent Community") + .withAdminGroup(commAdmin) + .build(); + Community child1 = CommunityBuilder + .createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .withAdminGroup(subCommAdmin) + .build(); + Collection col1 = CollectionBuilder + .createCollection(context, child1) + .withName("Collection 1") + .withAdminGroup(collAdmin) + .build(); + Collection col2 = CollectionBuilder + .createCollection(context, child1) + .withName("Collection 2") + .build(); + Collection col3 = CollectionBuilder + .createCollection(context, parentCommunity) + .withName("Collection 3") + .build(); + + //2. One public item, one private, one withdrawn. + + ItemBuilder.createItem(context, col1) + .withTitle("COL1 Test Item") + .withIssueDate("2010-10-17") + .withAuthor("Smith, Donald") + .withSubject("ExtraEntry") + .build(); + + ItemBuilder.createItem(context, col2) + .withTitle("COL2 Test Item") + .withIssueDate("2024-09-16") + .withAuthor("Smith, Maria") + .withAuthor("Doe, Jane") + .build(); + + ItemBuilder.createItem(context, col2) + .withTitle("COL2-1 Test Item") + .withIssueDate("2024-09-16") + .withAuthor("Smith, Maria") + .withAuthor("Doe, Jane") + .build(); + + ItemBuilder.createItem(context, col3) + .withTitle("COL3 Test Item") + .withIssueDate("2024-09-16") + .withAuthor("Smith, Maria") + .withAuthor("Doe, Jane") + .build(); + + context.restoreAuthSystemState(); + + String adminToken = getAuthToken(admin.getEmail(), password); + + getClient(adminToken).perform(get("/api/discover/search/objects") + .param("configuration", "administrativeView") + .param("query", "Test")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._embedded.searchResult.page", is( + PageMatcher.pageEntryWithTotalPagesAndElements(0, 20, 1, 4) + ))) + .andExpect(jsonPath("$._embedded.searchResult._embedded.objects", + Matchers.containsInAnyOrder( + SearchResultMatcher.matchOnItemName( + "item", "items", "COL1 Test Item" + ), + SearchResultMatcher.matchOnItemName( + "item", "items", "COL2 Test Item" + ), + SearchResultMatcher.matchOnItemName( + "item", "items", "COL2-1 Test Item" + ), + SearchResultMatcher.matchOnItemName( + "item", "items", "COL3 Test Item" + ) + ) + )) + .andExpect(jsonPath("$._links.self.href", containsString("/api/discover/search/objects"))); + + String commAdminToken = getAuthToken(commAdmin.getEmail(), password); + + getClient(commAdminToken).perform(get("/api/discover/search/objects") + .param("configuration", "administrativeView") + .param("query", "Test")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._embedded.searchResult.page", is( + PageMatcher.pageEntryWithTotalPagesAndElements(0, 20, 1, 4) + ))) + .andExpect(jsonPath("$._embedded.searchResult._embedded.objects", + Matchers.containsInAnyOrder( + SearchResultMatcher.matchOnItemName( + "item", "items", "COL1 Test Item" + ), + SearchResultMatcher.matchOnItemName( + "item", "items", "COL2 Test Item" + ), + SearchResultMatcher.matchOnItemName( + "item", "items", "COL2-1 Test Item" + ), + SearchResultMatcher.matchOnItemName( + "item", "items", "COL3 Test Item" + ) + ) + )) + .andExpect(jsonPath("$._links.self.href", containsString("/api/discover/search/objects"))); + + String collAdminToken = getAuthToken(collAdmin.getEmail(), password); + + getClient(collAdminToken).perform(get("/api/discover/search/objects") + .param("configuration", "administrativeView") + .param("query", "Test")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._embedded.searchResult.page", is( + PageMatcher.pageEntryWithTotalPagesAndElements(0, 20, 1, 1) + ))) + .andExpect(jsonPath("$._embedded.searchResult._embedded.objects", + Matchers.containsInAnyOrder( + SearchResultMatcher.matchOnItemName( + "item", "items", "COL1 Test Item" + ) + ) + )) + .andExpect(jsonPath("$._links.self.href", + containsString("/api/discover/search/objects")) + ); + + String subCommAdminToken = getAuthToken(subCommAdmin.getEmail(), password); + + getClient(subCommAdminToken).perform(get("/api/discover/search/objects") + .param("configuration", "administrativeView") + .param("query", "Test")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._embedded.searchResult.page", is( + PageMatcher.pageEntryWithTotalPagesAndElements(0, 20, 1, 3) + ))) + .andExpect(jsonPath("$._embedded.searchResult._embedded.objects", + Matchers.containsInAnyOrder( + SearchResultMatcher.matchOnItemName( + "item", "items", "COL1 Test Item" + ), + SearchResultMatcher.matchOnItemName( + "item", "items", "COL2 Test Item" + ), + SearchResultMatcher.matchOnItemName( + "item", "items", "COL2-1 Test Item" + ) + ) + )) + .andExpect(jsonPath("$._links.self.href", + containsString("/api/discover/search/objects")) + ); + } + @Test public void discoverSearchObjectsTestForAdministrativeViewWithFilters() throws Exception { diff --git a/dspace/config/spring/api/discovery.xml b/dspace/config/spring/api/discovery.xml index a556ba4849c..080edb2e4ad 100644 --- a/dspace/config/spring/api/discovery.xml +++ b/dspace/config/spring/api/discovery.xml @@ -74,6 +74,7 @@ + dc.contributor.author