diff --git a/dspace-api/src/main/java/org/dspace/app/exception/ResourceConflictException.java b/dspace-api/src/main/java/org/dspace/app/exception/ResourceAlreadyExistsException.java similarity index 54% rename from dspace-api/src/main/java/org/dspace/app/exception/ResourceConflictException.java rename to dspace-api/src/main/java/org/dspace/app/exception/ResourceAlreadyExistsException.java index 14e415aeb8..cd65fbcb74 100644 --- a/dspace-api/src/main/java/org/dspace/app/exception/ResourceConflictException.java +++ b/dspace-api/src/main/java/org/dspace/app/exception/ResourceAlreadyExistsException.java @@ -8,32 +8,26 @@ package org.dspace.app.exception; /** - * This class provides an exception to be used when a conflict on a resource - * occurs. + * This class provides an exception to be used when trying to save a resource + * that already exists. * * @author Luca Giamminonni (luca.giamminonni at 4science.it) * */ -public class ResourceConflictException extends RuntimeException { +public class ResourceAlreadyExistsException extends RuntimeException { private static final long serialVersionUID = 1L; - private final Object resource; - /** - * Create a ResourceConflictException with a message and the conflicting - * resource. + * Create a ResourceAlreadyExistsException with a message and the already + * existing resource. * * @param message the error message * @param resource the resource that caused the conflict */ - public ResourceConflictException(String message, Object resource) { + public ResourceAlreadyExistsException(String message) { super(message); - this.resource = resource; } - public Object getResource() { - return resource; - } } diff --git a/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfile.java b/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfile.java index 21dbe36da6..1daa634658 100644 --- a/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfile.java +++ b/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfile.java @@ -52,6 +52,11 @@ public class ResearcherProfile { return dspaceObjectOwner.getValue(); } + /** + * A profile is considered visible if accessible by anonymous users. This method + * returns true if the given item has a READ policy related to ANONYMOUS group, + * false otherwise. + */ public boolean isVisible() { return item.getResourcePolicies().stream() .filter(policy -> policy.getGroup() != null) diff --git a/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfileServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfileServiceImpl.java index 19c2f03e5e..b1118139a4 100644 --- a/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfileServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/profile/ResearcherProfileServiceImpl.java @@ -9,7 +9,6 @@ package org.dspace.app.profile; import static java.util.Optional.empty; import static java.util.Optional.ofNullable; -import static org.apache.commons.lang3.ArrayUtils.contains; import static org.dspace.content.authority.Choices.CF_ACCEPTED; import static org.dspace.core.Constants.READ; import static org.dspace.eperson.Group.ANONYMOUS; @@ -24,7 +23,7 @@ import java.util.UUID; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang.StringUtils; -import org.dspace.app.exception.ResourceConflictException; +import org.dspace.app.exception.ResourceAlreadyExistsException; import org.dspace.app.profile.service.ResearcherProfileService; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.service.AuthorizeService; @@ -105,8 +104,7 @@ public class ResearcherProfileServiceImpl implements ResearcherProfileService { Item profileItem = findResearcherProfileItemById(context, ePerson.getID()); if (profileItem != null) { - ResearcherProfile profile = new ResearcherProfile(profileItem); - throw new ResourceConflictException("A profile is already linked to the provided User", profile); + throw new ResourceAlreadyExistsException("A profile is already linked to the provided User"); } Collection collection = findProfileCollection(context) @@ -136,7 +134,7 @@ public class ResearcherProfileServiceImpl implements ResearcherProfileService { if (isHardDeleteEnabled()) { deleteItem(context, profileItem); } else { - removeDspaceObjectOwnerMetadata(context, profileItem); + removeOwnerMetadata(context, profileItem); } } @@ -161,47 +159,63 @@ public class ResearcherProfileServiceImpl implements ResearcherProfileService { } @Override - public ResearcherProfile claim(final Context context, final EPerson ePerson, final URI uri) + public ResearcherProfile claim(Context context, EPerson ePerson, URI uri) throws SQLException, AuthorizeException, SearchServiceException { Item profileItem = findResearcherProfileItemById(context, ePerson.getID()); if (profileItem != null) { - ResearcherProfile profile = new ResearcherProfile(profileItem); - throw new ResourceConflictException("A profile is already linked to the provided User", profile); + throw new ResourceAlreadyExistsException("A profile is already linked to the provided User"); } Item item = findItemByURI(context, uri) .orElseThrow(() -> new IllegalArgumentException("No item found by URI " + uri)); - if (!item.isArchived() || item.isWithdrawn() || notClaimableEntityType(item)) { - throw new IllegalArgumentException("Provided uri does not represent a valid Item to be claimed"); + if (!item.isArchived() || item.isWithdrawn()) { + throw new IllegalArgumentException( + "Only archived items can be claimed to create a researcher profile. Item ID: " + item.getID()); + } + + if (!hasProfileType(item)) { + throw new IllegalArgumentException("The provided item has not a profile type. Item ID: " + item.getID()); } String existingOwner = itemService.getMetadataFirstValue(item, "dspace", "object", "owner", Item.ANY); if (StringUtils.isNotBlank(existingOwner)) { - throw new IllegalArgumentException("Item with provided uri has already an owner"); + throw new IllegalArgumentException("Item with provided uri has already an owner - ID: " + existingOwner); } context.turnOffAuthorisationSystem(); itemService.addMetadata(context, item, "dspace", "object", "owner", null, ePerson.getName(), ePerson.getID().toString(), CF_ACCEPTED); - context.restoreAuthSystemState(); + return new ResearcherProfile(item); } + @Override + public boolean hasProfileType(Item item) { + String profileType = getProfileType(); + if (StringUtils.isBlank(profileType)) { + return false; + } + return profileType.equals(itemService.getEntityType(item)); + } + + @Override + public String getProfileType() { + return configurationService.getProperty("researcher-profile.entity-type", "Person"); + } + private Optional findItemByURI(final Context context, final URI uri) throws SQLException { String path = uri.getPath(); UUID uuid = UUIDUtils.fromString(path.substring(path.lastIndexOf("/") + 1)); return ofNullable(itemService.find(context, uuid)); } - private boolean notClaimableEntityType(final Item item) { - String entityType = itemService.getEntityType(item); - return !contains(configurationService.getArrayProperty("claimable.entityType"), entityType); - } - + /** + * Search for an profile item owned by an eperson with the given id. + */ private Item findResearcherProfileItemById(Context context, UUID id) throws SQLException, AuthorizeException { String profileType = getProfileType(); @@ -218,6 +232,10 @@ public class ResearcherProfileServiceImpl implements ResearcherProfileService { return null; } + /** + * Returns a Profile collection based on a configuration or searching for a + * collection of researcher profile type. + */ @SuppressWarnings("rawtypes") private Optional findProfileCollection(Context context) throws SQLException, SearchServiceException { UUID uuid = UUIDUtils.fromString(configurationService.getProperty("researcher-profile.collection.uuid")); @@ -246,6 +264,9 @@ public class ResearcherProfileServiceImpl implements ResearcherProfileService { return ofNullable((Collection) indexableObjects.get(0).getIndexedObject()); } + /** + * Create a new profile item for the given ePerson in the provided collection. + */ private Item createProfileItem(Context context, EPerson ePerson, Collection collection) throws AuthorizeException, SQLException { @@ -259,22 +280,34 @@ public class ResearcherProfileServiceImpl implements ResearcherProfileService { item = installItemService.installItem(context, workspaceItem); - Group anonymous = groupService.findByName(context, ANONYMOUS); - authorizeService.removeGroupPolicies(context, item, anonymous); + if (isNewProfilePrivateByDefault()) { + Group anonymous = groupService.findByName(context, ANONYMOUS); + authorizeService.removeGroupPolicies(context, item, anonymous); + } + authorizeService.addPolicy(context, item, READ, ePerson); - return item; + return reloadItem(context, item); } private boolean isHardDeleteEnabled() { return configurationService.getBooleanProperty("researcher-profile.hard-delete.enabled"); } - private void removeDspaceObjectOwnerMetadata(Context context, Item profileItem) throws SQLException { + private boolean isNewProfilePrivateByDefault() { + return configurationService.getBooleanProperty("researcher-profile.set-new-profile-private"); + } + + private void removeOwnerMetadata(Context context, Item profileItem) throws SQLException { List metadata = itemService.getMetadata(profileItem, "dspace", "object", "owner", Item.ANY); itemService.removeMetadataValues(context, profileItem, metadata); } + private Item reloadItem(Context context, Item item) throws SQLException { + context.uncacheEntity(item); + return context.reloadEntity(item); + } + private void deleteItem(Context context, Item profileItem) throws SQLException, AuthorizeException { try { context.turnOffAuthorisationSystem(); @@ -286,8 +319,4 @@ public class ResearcherProfileServiceImpl implements ResearcherProfileService { } } - private String getProfileType() { - return configurationService.getProperty("researcher-profile.type", "Person"); - } - } diff --git a/dspace-api/src/main/java/org/dspace/app/profile/service/ResearcherProfileService.java b/dspace-api/src/main/java/org/dspace/app/profile/service/ResearcherProfileService.java index 674b440b00..8190b0d98d 100644 --- a/dspace-api/src/main/java/org/dspace/app/profile/service/ResearcherProfileService.java +++ b/dspace-api/src/main/java/org/dspace/app/profile/service/ResearcherProfileService.java @@ -13,6 +13,7 @@ import java.util.UUID; import org.dspace.app.profile.ResearcherProfile; import org.dspace.authorize.AuthorizeException; +import org.dspace.content.Item; import org.dspace.core.Context; import org.dspace.discovery.SearchServiceException; import org.dspace.eperson.EPerson; @@ -52,22 +53,26 @@ public interface ResearcherProfileService { throws AuthorizeException, SQLException, SearchServiceException; /** - * Removes the association between the researcher profile and eperson related to - * the input uuid. + * Delete the profile with the given id. Based on the + * researcher-profile.hard-delete.enabled configuration, this method deletes the + * related item or removes the association between the researcher profile and + * eperson related to the input uuid. * - * @param context the relevant DSpace Context. - * @param id the researcher profile id + * @param context the relevant DSpace Context. + * @param id the researcher profile id * @throws AuthorizeException * @throws SQLException */ public void deleteById(Context context, UUID id) throws SQLException, AuthorizeException; /** - * Changes the visibility of the given profile using the given new visible value + * Changes the visibility of the given profile using the given new visible + * value. * - * @param context the relevant DSpace Context. - * @param profile the researcher profile to update - * @param visible the visible value to set + * @param context the relevant DSpace Context. + * @param profile the researcher profile to update + * @param visible the visible value to set. If true the profile will + * be visible to all users. * @throws SQLException * @throws AuthorizeException */ @@ -76,11 +81,32 @@ public interface ResearcherProfileService { /** * Claims and links an eperson to an existing DSpaceObject - * @param context the relevant DSpace Context. - * @param ePerson the ePerson - * @param uri uri of existing DSpaceObject to be linked to the eperson - * @return + * @param context the relevant DSpace Context. + * @param ePerson the ePerson + * @param uri uri of existing Item to be linked to the + * eperson + * @return the created profile + * @throws IllegalArgumentException if the given uri is not related to an + * archived item or if the item cannot be + * claimed */ ResearcherProfile claim(Context context, EPerson ePerson, URI uri) throws SQLException, AuthorizeException, SearchServiceException; + + /** + * Check if the given item has an entity type compatible with that of the + * researcher profile. If the given item does not have an entity type, the check + * returns false. + * + * @param item the item to check + * @return the check result + */ + boolean hasProfileType(Item item); + + /** + * Returns the profile entity type, if any. + * + * @return the profile type + */ + String getProfileType(); } diff --git a/dspace-api/src/main/java/org/dspace/content/authority/EPersonAuthority.java b/dspace-api/src/main/java/org/dspace/content/authority/EPersonAuthority.java index 7a1510d721..4aa7283a34 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/EPersonAuthority.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/EPersonAuthority.java @@ -12,7 +12,8 @@ import java.util.ArrayList; import java.util.List; import java.util.UUID; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.core.Context; import org.dspace.eperson.EPerson; import org.dspace.eperson.factory.EPersonServiceFactory; @@ -20,11 +21,14 @@ import org.dspace.eperson.service.EPersonService; import org.dspace.util.UUIDUtils; /** + * Implementation of {@link ChoiceAuthority} based on EPerson. Allows you to set + * the id of an eperson as authority. * * @author Mykhaylo Boychuk (4science.it) */ public class EPersonAuthority implements ChoiceAuthority { - private static final Logger log = Logger.getLogger(EPersonAuthority.class); + + private static final Logger log = LogManager.getLogger(EPersonAuthority.class); /** * the name assigned to the specific instance by the PluginService, @see diff --git a/dspace-api/src/test/data/dspaceFolder/config/local.cfg b/dspace-api/src/test/data/dspaceFolder/config/local.cfg index a9f606c180..f05a7af223 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/local.cfg +++ b/dspace-api/src/test/data/dspaceFolder/config/local.cfg @@ -143,6 +143,4 @@ authentication-ip.Student = 6.6.6.6 # NOTE: 127.0.0.1 (localhost) is always a trusted IP, see ClientInfoServiceImpl#parseTrustedProxyRanges useProxies = true proxies.trusted.ipranges = 7.7.7.7 -proxies.trusted.include_ui_ip = true - -researcher-profile.type = Person \ No newline at end of file +proxies.trusted.include_ui_ip = true \ No newline at end of file diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanClaimItemFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanClaimItemFeature.java index f203247c36..bca11b8d9b 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanClaimItemFeature.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanClaimItemFeature.java @@ -23,7 +23,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** - * Checks if the given user can claim the given item. + * Checks if the given user can claim the given item. An item can be claimed + * only if the show claim is enabled for it (see + * {@link org.dspace.app.rest.authorization.impl.ShowClaimItemFeature}). * * @author Luca Giamminonni (luca.giamminonni at 4science.it) * diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/ShowClaimItemFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/ShowClaimItemFeature.java index 1de02ce331..ce25988b69 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/ShowClaimItemFeature.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/ShowClaimItemFeature.java @@ -11,7 +11,6 @@ import java.sql.SQLException; import java.util.Objects; import java.util.UUID; -import org.apache.commons.lang3.ArrayUtils; import org.dspace.app.profile.service.ResearcherProfileService; import org.dspace.app.rest.authorization.AuthorizationFeature; import org.dspace.app.rest.authorization.AuthorizationFeatureDocumentation; @@ -21,7 +20,6 @@ import org.dspace.authorize.AuthorizeException; import org.dspace.content.Item; import org.dspace.content.service.ItemService; import org.dspace.core.Context; -import org.dspace.services.ConfigurationService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -45,15 +43,11 @@ public class ShowClaimItemFeature implements AuthorizationFeature { private final ItemService itemService; private final ResearcherProfileService researcherProfileService; - private final ConfigurationService configurationService; @Autowired - public ShowClaimItemFeature(ItemService itemService, - ResearcherProfileService researcherProfileService, - ConfigurationService configurationService) { + public ShowClaimItemFeature(ItemService itemService, ResearcherProfileService researcherProfileService) { this.itemService = itemService; this.researcherProfileService = researcherProfileService; - this.configurationService = configurationService; } @Override @@ -67,25 +61,18 @@ public class ShowClaimItemFeature implements AuthorizationFeature { String id = ((ItemRest) object).getId(); Item item = itemService.find(context, UUID.fromString(id)); - return claimableEntityType(item) && hasNotAlreadyAProfile(context); + return researcherProfileService.hasProfileType(item) && hasNotAlreadyAProfile(context); } private boolean hasNotAlreadyAProfile(Context context) { try { return researcherProfileService.findById(context, context.getCurrentUser().getID()) == null; } catch (SQLException | AuthorizeException e) { - LOG.warn("Error while checking if eperson has a ResearcherProfileAssociated: {}", - e.getMessage(), e); + LOG.warn("Error while checking if eperson has a ResearcherProfileAssociated: {}", e.getMessage(), e); return false; } } - private boolean claimableEntityType(Item item) { - String[] claimableEntityTypes = configurationService.getArrayProperty("claimable.entityType"); - String entityType = itemService.getEntityType(item); - return ArrayUtils.contains(claimableEntityTypes, entityType); - } - @Override public String[] getSupportedTypes() { return new String[] {ItemRest.CATEGORY + "." + ItemRest.NAME}; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java index c3ebc1ca54..e2e39c820c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java @@ -20,16 +20,12 @@ import javax.servlet.http.HttpServletResponse; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.dspace.app.exception.ResourceConflictException; -import org.dspace.app.rest.converter.ConverterService; -import org.dspace.app.rest.model.RestModel; +import org.dspace.app.exception.ResourceAlreadyExistsException; import org.dspace.app.rest.utils.ContextUtil; -import org.dspace.app.rest.utils.Utils; import org.dspace.authorize.AuthorizeException; import org.dspace.core.Context; import org.dspace.services.ConfigurationService; import org.springframework.beans.TypeMismatchException; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.repository.support.QueryMethodParameterConversionException; import org.springframework.http.HttpHeaders; @@ -72,12 +68,6 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH @Inject private ConfigurationService configurationService; - @Autowired - private ConverterService converterService; - - @Autowired - private Utils utils; - @ExceptionHandler({AuthorizeException.class, RESTAuthorizationException.class, AccessDeniedException.class}) protected void handleAuthorizeException(HttpServletRequest request, HttpServletResponse response, Exception ex) throws IOException { @@ -177,10 +167,10 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH HttpStatus.BAD_REQUEST.value()); } - @ExceptionHandler(ResourceConflictException.class) - protected ResponseEntity resourceConflictException(ResourceConflictException ex) { - RestModel resource = converterService.toRest(ex.getResource(), utils.obtainProjection()); - return new ResponseEntity(resource, HttpStatus.CONFLICT); + @ExceptionHandler(ResourceAlreadyExistsException.class) + protected void resourceConflictException(HttpServletRequest request, HttpServletResponse response, + ResourceAlreadyExistsException ex) throws IOException { + sendErrorResponse(request, response, null, ex.getMessage(), HttpStatus.UNPROCESSABLE_ENTITY.value()); } @ExceptionHandler(MissingParameterException.class) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/login/impl/ResearcherProfileAutomaticClaim.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/login/impl/ResearcherProfileAutomaticClaim.java index 3c3f459fd1..40dd935460 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/login/impl/ResearcherProfileAutomaticClaim.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/login/impl/ResearcherProfileAutomaticClaim.java @@ -8,6 +8,7 @@ package org.dspace.app.rest.login.impl; import static org.apache.commons.collections4.IteratorUtils.toList; +import static org.apache.commons.lang3.StringUtils.isBlank; import static org.dspace.content.authority.Choices.CF_ACCEPTED; import java.sql.SQLException; @@ -53,15 +54,21 @@ public class ResearcherProfileAutomaticClaim implements PostLoggedInAction { @Autowired private EPersonService ePersonService; + /** + * The field of the eperson to search for. + */ private final String ePersonField; - private final String profileFiled; + /** + * The field of the profile item to search. + */ + private final String profileField; public ResearcherProfileAutomaticClaim(String ePersonField, String profileField) { Assert.notNull(ePersonField, "An eperson field is required to perform automatic claim"); Assert.notNull(profileField, "An profile field is required to perform automatic claim"); this.ePersonField = ePersonField; - this.profileFiled = profileField; + this.profileField = profileField; } @Override @@ -72,6 +79,10 @@ public class ResearcherProfileAutomaticClaim implements PostLoggedInAction { return; } + if (isBlank(researcherProfileService.getProfileType())) { + return; + } + try { claimProfile(context, currentUser); } catch (SQLException | AuthorizeException e) { @@ -89,7 +100,7 @@ public class ResearcherProfileAutomaticClaim implements PostLoggedInAction { return; } - Item item = findClaimableItem(context, currentUser); + Item item = findClaimableProfile(context, currentUser); if (item != null) { itemService.addMetadata(context, item, "dspace", "object", "owner", null, fullName, id.toString(), CF_ACCEPTED); @@ -101,16 +112,16 @@ public class ResearcherProfileAutomaticClaim implements PostLoggedInAction { return researcherProfileService.findById(context, context.getCurrentUser().getID()) != null; } - private Item findClaimableItem(Context context, EPerson currentUser) - throws SQLException, AuthorizeException { + private Item findClaimableProfile(Context context, EPerson currentUser) throws SQLException, AuthorizeException { String value = getValueToSearchFor(context, currentUser); if (StringUtils.isEmpty(value)) { return null; } - List items = toList(itemService.findArchivedByMetadataField(context, profileFiled, value)).stream() - .filter(this::hasNotCrisOwner) + List items = toList(itemService.findArchivedByMetadataField(context, profileField, value)).stream() + .filter(this::hasNotOwner) + .filter(researcherProfileService::hasProfileType) .collect(Collectors.toList()); return items.size() == 1 ? items.get(0) : null; @@ -123,7 +134,7 @@ public class ResearcherProfileAutomaticClaim implements PostLoggedInAction { return ePersonService.getMetadataFirstValue(currentUser, new MetadataFieldName(ePersonField), Item.ANY); } - private boolean hasNotCrisOwner(Item item) { + private boolean hasNotOwner(Item item) { return CollectionUtils.isEmpty(itemService.getMetadata(item, "dspace", "object", "owner", Item.ANY)); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResearcherProfileEPersonLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResearcherProfileEPersonLinkRepository.java index ed7b286003..af7729f8f2 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResearcherProfileEPersonLinkRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResearcherProfileEPersonLinkRepository.java @@ -52,7 +52,7 @@ public class ResearcherProfileEPersonLinkRepository extends AbstractDSpaceRestRe * @param projection the projection object * @return the ePerson rest representation */ - @PreAuthorize("hasPermission(#id, 'PROFILE', 'READ')") + @PreAuthorize("hasPermission(#id, 'EPERSON', 'READ')") public EPersonRest getEPerson(@Nullable HttpServletRequest request, UUID id, @Nullable Pageable pageable, Projection projection) { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResearcherProfileRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResearcherProfileRestRepository.java index 25eb3d938a..5f605f5d8c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResearcherProfileRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResearcherProfileRestRepository.java @@ -46,9 +46,7 @@ import org.springframework.stereotype.Component; * */ @Component(ResearcherProfileRest.CATEGORY + "." + ResearcherProfileRest.NAME) -@ConditionalOnProperty( - value = "researcher-profile.type" -) +@ConditionalOnProperty(value = "researcher-profile.entity-type") public class ResearcherProfileRestRepository extends DSpaceRestRepository { public static final String NO_VISIBILITY_CHANGE_MSG = "Refused to perform the Researcher Profile patch based " @@ -136,7 +134,7 @@ public class ResearcherProfileRestRepository extends DSpaceRestRepository curl -X PATCH http://${dspace.server.url}/api/cris/profiles/<:id-eperson> -H " + * curl -X PATCH http://${dspace.server.url}/api/eperson/profiles/<:id-eperson> -H " * Content-Type: application/json" -d '[{ "op": "replace", "path": " * /visible", "value": true]' * diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResearcherProfileRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResearcherProfileRestRepositoryIT.java index 3d03a09912..1fd9b2cd47 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResearcherProfileRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResearcherProfileRestRepositoryIT.java @@ -98,7 +98,6 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra .build(); configurationService.setProperty("researcher-profile.collection.uuid", personCollection.getID().toString()); - configurationService.setProperty("claimable.entityType", "Person"); context.setCurrentUser(user); @@ -257,6 +256,23 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra .andExpect(jsonPath("$.name", is(name))); } + @Test + public void testCreateAndReturnWithPublicProfile() throws Exception { + + configurationService.setProperty("researcher-profile.set-new-profile-private", false); + String id = user.getID().toString(); + + String authToken = getAuthToken(user.getEmail(), password); + + getClient(authToken).perform(post("/api/eperson/profiles/") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id", is(id))) + .andExpect(jsonPath("$.visible", is(true))) + .andExpect(jsonPath("$.type", is("profile"))) + .andExpect(jsonPath("$", matchLinks("http://localhost/api/eperson/profiles/" + id, "item", "eperson"))); + } + /** * Verify that an admin can call the createAndReturn endpoint to store a new * researcher profile related to another user. @@ -345,10 +361,7 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra getClient(authToken).perform(post("/api/eperson/profiles/") .contentType(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(status().isConflict()) - .andExpect(jsonPath("$.id", is(id))) - .andExpect(jsonPath("$.visible", is(false))) - .andExpect(jsonPath("$.type", is("profile"))); + .andExpect(status().isUnprocessableEntity()); } @@ -762,6 +775,37 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra } + @Test + public void testAutomaticProfileClaimByEmailWithRegularEntity() throws Exception { + + String userToken = getAuthToken(user.getEmail(), password); + + context.turnOffAuthorisationSystem(); + + Item itemToBeClaimed = ItemBuilder.createItem(context, personCollection) + .withPersonEmail(user.getEmail()) + .build(); + + context.restoreAuthSystemState(); + + String id = user.getID().toString(); + + getClient(userToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isNotFound()); + + // the automatic claim is done after the user login + String newUserToken = getAuthToken(user.getEmail(), password); + + getClient(newUserToken).perform(get("/api/eperson/profiles/{id}", id)) + .andExpect(status().isOk()); + + // the profile item should be the same + String firstItemId = itemToBeClaimed.getID().toString(); + String secondItemId = getItemIdByProfileId(newUserToken, id); + assertEquals("The item should be the same", firstItemId, secondItemId); + + } + @Test public void testNoAutomaticProfileClaimOccursIfManyClaimableItemsAreFound() throws Exception { @@ -909,7 +953,7 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra getClient(authToken).perform(post("/api/eperson/profiles/") .contentType(TEXT_URI_LIST) .content("http://localhost:8080/server/api/core/items/" + otherPerson.getID().toString())) - .andExpect(status().isConflict()); + .andExpect(status().isUnprocessableEntity()); // other person trying to claim same profile context.turnOffAuthorisationSystem(); @@ -1032,7 +1076,7 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra getClient(authToken) .perform(post("/api/eperson/profiles/").contentType(TEXT_URI_LIST) .content("http://localhost:8080/server/api/core/items/" + id)) - .andExpect(status().isConflict()); + .andExpect(status().isUnprocessableEntity()); } private String getItemIdByProfileId(String token, String id) throws Exception { diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanClaimItemFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanClaimItemFeatureIT.java index 368d0f1a44..0887b29887 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanClaimItemFeatureIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanClaimItemFeatureIT.java @@ -24,7 +24,6 @@ import org.dspace.builder.CommunityBuilder; import org.dspace.builder.ItemBuilder; import org.dspace.content.Collection; import org.dspace.content.Item; -import org.dspace.services.ConfigurationService; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -39,9 +38,6 @@ public class CanClaimItemFeatureIT extends AbstractControllerIntegrationTest { private Item collectionAProfile; private Item collectionBProfile; - @Autowired - private ConfigurationService configurationService; - @Autowired private ItemConverter itemConverter; @@ -73,8 +69,6 @@ public class CanClaimItemFeatureIT extends AbstractControllerIntegrationTest { collectionAProfile = ItemBuilder.createItem(context, personCollection).build(); collectionBProfile = ItemBuilder.createItem(context, claimableCollectionB).build(); - configurationService.addPropertyValue("claimable.entityType", "Person"); - context.restoreAuthSystemState(); canClaimProfileFeature = authorizationFeatureService.find(CanClaimItemFeature.NAME); @@ -173,22 +167,6 @@ public class CanClaimItemFeatureIT extends AbstractControllerIntegrationTest { .andExpect(jsonPath("$.page.totalElements", equalTo(0))); } - @Test - public void testWithoutClaimableEntities() throws Exception { - - configurationService.setProperty("claimable.entityType", null); - - getClient(getAuthToken(context.getCurrentUser().getEmail(), password)) - .perform(get("/api/authz/authorizations/search/object") - .param("uri", uri(collectionAProfile)) - .param("eperson", context.getCurrentUser().getID().toString()) - .param("feature", canClaimProfileFeature.getName())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$._embedded").doesNotExist()) - .andExpect(jsonPath("$.page.totalElements", equalTo(0))); - - } - private String uri(Item item) { ItemRest itemRest = itemConverter.convert(item, Projection.DEFAULT); String itemRestURI = utils.linkToSingleResource(itemRest, "self").getHref(); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/ShowClaimItemFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/ShowClaimItemFeatureIT.java index 563e443efe..684bec7486 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/ShowClaimItemFeatureIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/ShowClaimItemFeatureIT.java @@ -24,7 +24,6 @@ import org.dspace.builder.CommunityBuilder; import org.dspace.builder.ItemBuilder; import org.dspace.content.Collection; import org.dspace.content.Item; -import org.dspace.services.ConfigurationService; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -40,9 +39,6 @@ public class ShowClaimItemFeatureIT extends AbstractControllerIntegrationTest { private Item collectionAProfile; private Item collectionBProfile; - @Autowired - private ConfigurationService configurationService; - @Autowired private ItemConverter itemConverter; @@ -74,8 +70,6 @@ public class ShowClaimItemFeatureIT extends AbstractControllerIntegrationTest { collectionAProfile = ItemBuilder.createItem(context, personCollection).build(); collectionBProfile = ItemBuilder.createItem(context, claimableCollectionB).build(); - configurationService.addPropertyValue("claimable.entityType", "Person"); - context.restoreAuthSystemState(); showClaimProfileFeature = authorizationFeatureService.find(ShowClaimItemFeature.NAME); @@ -174,22 +168,6 @@ public class ShowClaimItemFeatureIT extends AbstractControllerIntegrationTest { .andExpect(jsonPath("$.page.totalElements", equalTo(0))); } - @Test - public void testWithoutClaimableEntities() throws Exception { - - configurationService.setProperty("claimable.entityType", null); - - getClient(getAuthToken(context.getCurrentUser().getEmail(), password)) - .perform(get("/api/authz/authorizations/search/object") - .param("uri", uri(collectionAProfile)) - .param("eperson", context.getCurrentUser().getID().toString()) - .param("feature", showClaimProfileFeature.getName())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$._embedded").doesNotExist()) - .andExpect(jsonPath("$.page.totalElements", equalTo(0))); - - } - private String uri(Item item) { ItemRest itemRest = itemConverter.convert(item, Projection.DEFAULT); String itemRestURI = utils.linkToSingleResource(itemRest, "self").getHref(); diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index fe94ca1ce5..3f454f9e0c 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1379,17 +1379,6 @@ sherpa.romeo.url = https://v2.sherpa.ac.uk/cgi/retrieve # register for a new API key sherpa.romeo.apikey = -##### Authority Control Settings ##### -#plugin.named.org.dspace.content.authority.ChoiceAuthority = \ -# org.dspace.content.authority.SampleAuthority = Sample, \ -# org.dspace.content.authority.SHERPARoMEOPublisher = SRPublisher, \ -# org.dspace.content.authority.SHERPARoMEOJournalTitle = SRJournalTitle, \ -# org.dspace.content.authority.SolrAuthority = SolrAuthorAuthority - -#Uncomment to enable ORCID authority control -#plugin.named.org.dspace.content.authority.ChoiceAuthority = \ -# org.dspace.content.authority.SolrAuthority = SolrAuthorAuthority - # URL of ORCID API # Defaults to using the Public API V3 (pub.orcid.org) orcid.api.url = https://pub.orcid.org/v3.0 @@ -1404,69 +1393,6 @@ orcid.clientsecret = #ORCID JWT Endpoint orcid.oauth.url = https://orcid.org/oauth/token -## The DCInputAuthority plugin is automatically configured with every -## value-pairs element in input-forms.xml, namely: -## common_identifiers, common_types, common_iso_languages -## -## The DSpaceControlledVocabulary plugin is automatically configured -## with every *.xml file in [dspace]/config/controlled-vocabularies, -## and creates a plugin instance for each, using base filename as the name. -## eg: nsi, srsc. -## Each DSpaceControlledVocabulary plugin comes with three configuration options: -# vocabulary.plugin._plugin_.hierarchy.store = # default: true -# vocabulary.plugin._plugin_.hierarchy.suggest = # default: false -# vocabulary.plugin._plugin_.delimiter = "" # default: "::" -## -## An example using "srsc" can be found later in this section - -plugin.selfnamed.org.dspace.content.authority.ChoiceAuthority = \ - org.dspace.content.authority.DCInputAuthority, \ - org.dspace.content.authority.DSpaceControlledVocabulary - -## -## This sets the default lowest confidence level at which a metadata value is included -## in an authority-controlled browse (and search) index. It is a symbolic -## keyword, one of the following values (listed in descending order): -## accepted -## uncertain -## ambiguous -## notfound -## failed -## rejected -## novalue -## unset -## See manual or org.dspace.content.authority.Choices source for descriptions. -authority.minconfidence = ambiguous - -# Configuration settings for ORCID based authority control. -# Uncomment the lines below to enable configuration -#choices.plugin.dc.contributor.author = SolrAuthorAuthority -#choices.presentation.dc.contributor.author = authorLookup -#authority.controlled.dc.contributor.author = true -#authority.author.indexer.field.1=dc.contributor.author - -## -## This sets the lowest confidence level at which a metadata value is included -## in an authority-controlled browse (and search) index. It is a symbolic -## keyword from the same set as for the default "authority.minconfidence" -#authority.minconfidence.dc.contributor.author = accepted - -## demo: subject code autocomplete, using srsc as authority -## (DSpaceControlledVocabulary plugin must be enabled) -## Warning: when enabling this feature any controlled vocabulary configuration in the input-forms.xml for the metadata field will be overridden. -#vocabulary.plugin.srsc.hierarchy.store = true -#vocabulary.plugin.srsc.hierarchy.suggest = true -#vocabulary.plugin.srsc.delimiter = "::" - -# publisher name lookup through SHERPA/RoMEO: -#choices.plugin.dc.publisher = SRPublisher -#choices.presentation.dc.publisher = suggest - -## demo: journal title lookup, with ISSN as authority -#choices.plugin.dc.title.alternative = SRJournalTitle -#choices.presentation.dc.title.alternative = suggest -#authority.controlled.dc.title.alternative = true - ##### Google Scholar Metadata Configuration ##### google-metadata.config = ${dspace.dir}/config/crosswalks/google-metadata.properties google-metadata.enable = true @@ -1632,6 +1558,7 @@ include = ${module_dir}/rest.cfg include = ${module_dir}/iiif.cfg include = ${module_dir}/solr-statistics.cfg include = ${module_dir}/solrauthority.cfg +include = ${module_dir}/researcher-profile.cfg include = ${module_dir}/spring.cfg include = ${module_dir}/submission-curation.cfg include = ${module_dir}/sword-client.cfg diff --git a/dspace/config/modules/authority.cfg b/dspace/config/modules/authority.cfg index 664730a8bf..c9362a1f18 100644 --- a/dspace/config/modules/authority.cfg +++ b/dspace/config/modules/authority.cfg @@ -4,12 +4,83 @@ # These configs are used by the authority framework # #---------------------------------------------------------------# +## The DCInputAuthority plugin is automatically configured with every +## value-pairs element in input-forms.xml, namely: +## common_identifiers, common_types, common_iso_languages +## +## The DSpaceControlledVocabulary plugin is automatically configured +## with every *.xml file in [dspace]/config/controlled-vocabularies, +## and creates a plugin instance for each, using base filename as the name. +## eg: nsi, srsc. +## Each DSpaceControlledVocabulary plugin comes with three configuration options: +# vocabulary.plugin._plugin_.hierarchy.store = # default: true +# vocabulary.plugin._plugin_.hierarchy.suggest = # default: false +# vocabulary.plugin._plugin_.delimiter = "" # default: "::" +## +## An example using "srsc" can be found later in this section + +plugin.selfnamed.org.dspace.content.authority.ChoiceAuthority = \ + org.dspace.content.authority.DCInputAuthority, \ + org.dspace.content.authority.DSpaceControlledVocabulary + + ## +## This sets the default lowest confidence level at which a metadata value is included +## in an authority-controlled browse (and search) index. It is a symbolic +## keyword, one of the following values (listed in descending order): +## accepted +## uncertain +## ambiguous +## notfound +## failed +## rejected +## novalue +## unset +## See manual or org.dspace.content.authority.Choices source for descriptions. +authority.minconfidence = ambiguous + +# Configuration settings for ORCID based authority control. +# Uncomment the lines below to enable configuration +#choices.plugin.dc.contributor.author = SolrAuthorAuthority +#choices.presentation.dc.contributor.author = authorLookup +#authority.controlled.dc.contributor.author = true +#authority.author.indexer.field.1=dc.contributor.author + +## +## This sets the lowest confidence level at which a metadata value is included +## in an authority-controlled browse (and search) index. It is a symbolic +## keyword from the same set as for the default "authority.minconfidence" +#authority.minconfidence.dc.contributor.author = accepted + +## demo: subject code autocomplete, using srsc as authority +## (DSpaceControlledVocabulary plugin must be enabled) +## Warning: when enabling this feature any controlled vocabulary configuration in the input-forms.xml for the metadata field will be overridden. +#vocabulary.plugin.srsc.hierarchy.store = true +#vocabulary.plugin.srsc.hierarchy.suggest = true +#vocabulary.plugin.srsc.delimiter = "::" + +# publisher name lookup through SHERPA/RoMEO: +#choices.plugin.dc.publisher = SRPublisher +#choices.presentation.dc.publisher = suggest + +## demo: journal title lookup, with ISSN as authority +#choices.plugin.dc.title.alternative = SRJournalTitle +#choices.presentation.dc.title.alternative = suggest +#authority.controlled.dc.title.alternative = true + ##### Authority Control Settings ##### +#plugin.named.org.dspace.content.authority.ChoiceAuthority = \ +# org.dspace.content.authority.SampleAuthority = Sample, \ +# org.dspace.content.authority.SHERPARoMEOPublisher = SRPublisher, \ +# org.dspace.content.authority.SHERPARoMEOJournalTitle = SRJournalTitle, \ +# org.dspace.content.authority.SolrAuthority = SolrAuthorAuthority + +#Uncomment to enable ORCID authority control +#plugin.named.org.dspace.content.authority.ChoiceAuthority = \ +# org.dspace.content.authority.SolrAuthority = SolrAuthorAuthority plugin.named.org.dspace.content.authority.ChoiceAuthority = \ org.dspace.content.authority.EPersonAuthority = EPersonAuthority - choices.plugin.dspace.object.owner = EPersonAuthority choices.presentation.dspace.object.owner = suggest authority.controlled.dspace.object.owner = true \ No newline at end of file diff --git a/dspace/config/modules/researcher-profile.cfg b/dspace/config/modules/researcher-profile.cfg new file mode 100644 index 0000000000..03d542c39c --- /dev/null +++ b/dspace/config/modules/researcher-profile.cfg @@ -0,0 +1,15 @@ +#---------------------------------------------------------------# +#------------------- PROFILE CONFIGURATIONS --------------------# +#---------------------------------------------------------------# + +#the entity type of the researcher profile +researcher-profile.entity-type = Person + +# the uuid of the collection where store the researcher profiles created from scratch +researcher-profile.collection.uuid = + +# if true when the profile is deleted even the related item is deleted, if false only the link between profile and item is removed +researcher-profile.hard-delete.enabled = false + +#true if the new profiles should be private (without anonymous read policy), false otherwise +researcher-profile.set-new-profile-private = true \ No newline at end of file diff --git a/dspace/config/registries/dspace-types.xml b/dspace/config/registries/dspace-types.xml index a252fbced3..5a8ddff640 100644 --- a/dspace/config/registries/dspace-types.xml +++ b/dspace/config/registries/dspace-types.xml @@ -48,7 +48,7 @@ dspace object owner - Stores the reference to the eperson that own the item + Used to support researcher profiles