mirror of
https://github.com/DSpace/DSpace.git
synced 2025-10-07 18:14:26 +00:00
Merge pull request #8232 from 4Science/CST-5306
Researcher profile REST repository
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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.exception;
|
||||
|
||||
/**
|
||||
* 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 ResourceAlreadyExistsException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Create a ResourceAlreadyExistsException with a message and the already
|
||||
* existing resource.
|
||||
*
|
||||
* @param message the error message
|
||||
*/
|
||||
public ResourceAlreadyExistsException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 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.profile;
|
||||
|
||||
import static org.dspace.core.Constants.READ;
|
||||
import static org.dspace.eperson.Group.ANONYMOUS;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.dspace.content.Item;
|
||||
import org.dspace.content.MetadataValue;
|
||||
import org.dspace.util.UUIDUtils;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Object representing a Researcher Profile.
|
||||
*
|
||||
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
|
||||
*
|
||||
*/
|
||||
public class ResearcherProfile {
|
||||
|
||||
private final Item item;
|
||||
|
||||
private final MetadataValue dspaceObjectOwner;
|
||||
|
||||
/**
|
||||
* Create a new ResearcherProfile object from the given item.
|
||||
*
|
||||
* @param item the profile item
|
||||
* @throws IllegalArgumentException if the given item has not a dspace.object.owner
|
||||
* metadata with a valid authority
|
||||
*/
|
||||
public ResearcherProfile(Item item) {
|
||||
Assert.notNull(item, "A researcher profile requires an item");
|
||||
this.item = item;
|
||||
this.dspaceObjectOwner = getDspaceObjectOwnerMetadata(item);
|
||||
}
|
||||
|
||||
public UUID getId() {
|
||||
return UUIDUtils.fromString(dspaceObjectOwner.getAuthority());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
.anyMatch(policy -> READ == policy.getAction() && ANONYMOUS.equals(policy.getGroup().getName()));
|
||||
}
|
||||
|
||||
public Item getItem() {
|
||||
return item;
|
||||
}
|
||||
|
||||
private MetadataValue getDspaceObjectOwnerMetadata(Item item) {
|
||||
return getMetadataValue(item, "dspace.object.owner")
|
||||
.filter(metadata -> UUIDUtils.fromString(metadata.getAuthority()) != null)
|
||||
.orElseThrow(
|
||||
() -> new IllegalArgumentException("A profile item must have a valid dspace.object.owner metadata")
|
||||
);
|
||||
}
|
||||
|
||||
private Optional<MetadataValue> getMetadataValue(Item item, String metadataField) {
|
||||
return getMetadataValues(item, metadataField).findFirst();
|
||||
}
|
||||
|
||||
private Stream<MetadataValue> getMetadataValues(Item item, String metadataField) {
|
||||
return item.getMetadata().stream()
|
||||
.filter(metadata -> metadataField.equals(metadata.getMetadataField().toString('.')));
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,367 @@
|
||||
/**
|
||||
* 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.profile;
|
||||
|
||||
import static java.util.Optional.empty;
|
||||
import static java.util.Optional.of;
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static org.dspace.content.authority.Choices.CF_ACCEPTED;
|
||||
import static org.dspace.core.Constants.READ;
|
||||
import static org.dspace.core.Constants.WRITE;
|
||||
import static org.dspace.eperson.Group.ANONYMOUS;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.dspace.app.exception.ResourceAlreadyExistsException;
|
||||
import org.dspace.app.profile.service.ResearcherProfileService;
|
||||
import org.dspace.authorize.AuthorizeException;
|
||||
import org.dspace.authorize.service.AuthorizeService;
|
||||
import org.dspace.content.Collection;
|
||||
import org.dspace.content.Item;
|
||||
import org.dspace.content.MetadataValue;
|
||||
import org.dspace.content.WorkspaceItem;
|
||||
import org.dspace.content.service.CollectionService;
|
||||
import org.dspace.content.service.InstallItemService;
|
||||
import org.dspace.content.service.ItemService;
|
||||
import org.dspace.content.service.WorkspaceItemService;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.discovery.DiscoverQuery;
|
||||
import org.dspace.discovery.DiscoverResult;
|
||||
import org.dspace.discovery.IndexableObject;
|
||||
import org.dspace.discovery.SearchService;
|
||||
import org.dspace.discovery.SearchServiceException;
|
||||
import org.dspace.discovery.indexobject.IndexableCollection;
|
||||
import org.dspace.eperson.EPerson;
|
||||
import org.dspace.eperson.Group;
|
||||
import org.dspace.eperson.service.GroupService;
|
||||
import org.dspace.services.ConfigurationService;
|
||||
import org.dspace.util.UUIDUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Implementation of {@link ResearcherProfileService}.
|
||||
*
|
||||
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
|
||||
*
|
||||
*/
|
||||
public class ResearcherProfileServiceImpl implements ResearcherProfileService {
|
||||
|
||||
private static Logger log = LoggerFactory.getLogger(ResearcherProfileServiceImpl.class);
|
||||
|
||||
@Autowired
|
||||
private ItemService itemService;
|
||||
|
||||
@Autowired
|
||||
private WorkspaceItemService workspaceItemService;
|
||||
|
||||
@Autowired
|
||||
private InstallItemService installItemService;
|
||||
|
||||
@Autowired
|
||||
private ConfigurationService configurationService;
|
||||
|
||||
@Autowired
|
||||
private CollectionService collectionService;
|
||||
|
||||
@Autowired
|
||||
private SearchService searchService;
|
||||
|
||||
@Autowired
|
||||
private GroupService groupService;
|
||||
|
||||
@Autowired
|
||||
private AuthorizeService authorizeService;
|
||||
|
||||
@Override
|
||||
public ResearcherProfile findById(Context context, UUID id) throws SQLException, AuthorizeException {
|
||||
Assert.notNull(id, "An id must be provided to find a researcher profile");
|
||||
|
||||
Item profileItem = findResearcherProfileItemById(context, id);
|
||||
if (profileItem == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ResearcherProfile(profileItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResearcherProfile createAndReturn(Context context, EPerson ePerson)
|
||||
throws AuthorizeException, SQLException, SearchServiceException {
|
||||
|
||||
Item profileItem = findResearcherProfileItemById(context, ePerson.getID());
|
||||
if (profileItem != null) {
|
||||
throw new ResourceAlreadyExistsException("A profile is already linked to the provided User");
|
||||
}
|
||||
|
||||
Collection collection = findProfileCollection(context)
|
||||
.orElseThrow(() -> new IllegalStateException("No collection found for researcher profiles"));
|
||||
|
||||
context.turnOffAuthorisationSystem();
|
||||
try {
|
||||
|
||||
Item item = createProfileItem(context, ePerson, collection);
|
||||
return new ResearcherProfile(item);
|
||||
|
||||
} finally {
|
||||
context.restoreAuthSystemState();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteById(Context context, UUID id) throws SQLException, AuthorizeException {
|
||||
Assert.notNull(id, "An id must be provided to find a researcher profile");
|
||||
|
||||
Item profileItem = findResearcherProfileItemById(context, id);
|
||||
if (profileItem == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isHardDeleteEnabled()) {
|
||||
deleteItem(context, profileItem);
|
||||
} else {
|
||||
removeOwnerMetadata(context, profileItem);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeVisibility(Context context, ResearcherProfile profile, boolean visible)
|
||||
throws AuthorizeException, SQLException {
|
||||
|
||||
if (profile.isVisible() == visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
Item item = profile.getItem();
|
||||
Group anonymous = groupService.findByName(context, ANONYMOUS);
|
||||
|
||||
if (visible) {
|
||||
authorizeService.addPolicy(context, item, READ, anonymous);
|
||||
} else {
|
||||
authorizeService.removeGroupPolicies(context, item, anonymous);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResearcherProfile claim(Context context, EPerson ePerson, URI uri)
|
||||
throws SQLException, AuthorizeException, SearchServiceException {
|
||||
|
||||
Item profileItem = findResearcherProfileItemById(context, ePerson.getID());
|
||||
if (profileItem != null) {
|
||||
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()) {
|
||||
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());
|
||||
}
|
||||
|
||||
if (haveDifferentEmail(item, ePerson)) {
|
||||
throw new IllegalArgumentException("The provided item is not claimable because it has a different email "
|
||||
+ "than the given user's email. 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 - 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<Item> 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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
Iterator<Item> items = itemService.findByAuthorityValue(context, "dspace", "object", "owner", id.toString());
|
||||
while (items.hasNext()) {
|
||||
Item item = items.next();
|
||||
String entityType = itemService.getEntityType(item);
|
||||
if (profileType.equals(entityType)) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Profile collection based on a configuration or searching for a
|
||||
* collection of researcher profile type.
|
||||
*/
|
||||
private Optional<Collection> findProfileCollection(Context context) throws SQLException, SearchServiceException {
|
||||
return findConfiguredProfileCollection(context)
|
||||
.or(() -> findFirstCollectionByProfileEntityType(context));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
String id = ePerson.getID().toString();
|
||||
String fullName = ePerson.getFullName();
|
||||
|
||||
WorkspaceItem workspaceItem = workspaceItemService.create(context, collection, true);
|
||||
Item item = workspaceItem.getItem();
|
||||
itemService.addMetadata(context, item, "dc", "title", null, null, fullName);
|
||||
itemService.addMetadata(context, item, "person", "email", null, null, ePerson.getEmail());
|
||||
itemService.addMetadata(context, item, "dspace", "object", "owner", null, fullName, id, CF_ACCEPTED);
|
||||
|
||||
item = installItemService.installItem(context, workspaceItem);
|
||||
|
||||
if (isNewProfileNotVisibleByDefault()) {
|
||||
Group anonymous = groupService.findByName(context, ANONYMOUS);
|
||||
authorizeService.removeGroupPolicies(context, item, anonymous);
|
||||
}
|
||||
|
||||
authorizeService.addPolicy(context, item, READ, ePerson);
|
||||
authorizeService.addPolicy(context, item, WRITE, ePerson);
|
||||
|
||||
return reloadItem(context, item);
|
||||
}
|
||||
|
||||
private Optional<Collection> findConfiguredProfileCollection(Context context) throws SQLException {
|
||||
UUID uuid = UUIDUtils.fromString(configurationService.getProperty("researcher-profile.collection.uuid"));
|
||||
if (uuid == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Collection collection = collectionService.find(context, uuid);
|
||||
if (collection == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (isNotProfileCollection(collection)) {
|
||||
log.warn("The configured researcher-profile.collection.uuid "
|
||||
+ "has an invalid entity type, expected " + getProfileType());
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return of(collection);
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private Optional<Collection> findFirstCollectionByProfileEntityType(Context context) {
|
||||
|
||||
String profileType = getProfileType();
|
||||
|
||||
DiscoverQuery discoverQuery = new DiscoverQuery();
|
||||
discoverQuery.setDSpaceObjectFilter(IndexableCollection.TYPE);
|
||||
discoverQuery.addFilterQueries("dspace.entity.type:" + profileType);
|
||||
|
||||
DiscoverResult discoverResult = search(context, discoverQuery);
|
||||
List<IndexableObject> indexableObjects = discoverResult.getIndexableObjects();
|
||||
|
||||
if (CollectionUtils.isEmpty(indexableObjects)) {
|
||||
return empty();
|
||||
}
|
||||
|
||||
return ofNullable((Collection) indexableObjects.get(0).getIndexedObject());
|
||||
}
|
||||
|
||||
private boolean isHardDeleteEnabled() {
|
||||
return configurationService.getBooleanProperty("researcher-profile.hard-delete.enabled");
|
||||
}
|
||||
|
||||
private boolean isNewProfileNotVisibleByDefault() {
|
||||
return !configurationService.getBooleanProperty("researcher-profile.set-new-profile-visible");
|
||||
}
|
||||
|
||||
private boolean isNotProfileCollection(Collection collection) {
|
||||
String entityType = collectionService.getMetadataFirstValue(collection, "dspace", "entity", "type", Item.ANY);
|
||||
return entityType == null || !entityType.equals(getProfileType());
|
||||
}
|
||||
|
||||
private boolean haveDifferentEmail(Item item, EPerson currentUser) {
|
||||
return itemService.getMetadataByMetadataString(item, "person.email").stream()
|
||||
.map(MetadataValue::getValue)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.noneMatch(email -> email.equalsIgnoreCase(currentUser.getEmail()));
|
||||
}
|
||||
|
||||
private void removeOwnerMetadata(Context context, Item profileItem) throws SQLException {
|
||||
List<MetadataValue> 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();
|
||||
itemService.delete(context, profileItem);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
context.restoreAuthSystemState();
|
||||
}
|
||||
}
|
||||
|
||||
private DiscoverResult search(Context context, DiscoverQuery discoverQuery) {
|
||||
try {
|
||||
return searchService.search(context, discoverQuery);
|
||||
} catch (SearchServiceException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* 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.profile.service;
|
||||
|
||||
import java.net.URI;
|
||||
import java.sql.SQLException;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Service interface class for the {@link ResearcherProfile} object. The
|
||||
* implementation of this class is responsible for all business logic calls for
|
||||
* the {@link ResearcherProfile} object.
|
||||
*
|
||||
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
|
||||
*
|
||||
*/
|
||||
public interface ResearcherProfileService {
|
||||
|
||||
/**
|
||||
* Find the ResearcherProfile by UUID.
|
||||
*
|
||||
* @param context the relevant DSpace Context.
|
||||
* @param id the ResearcherProfile id
|
||||
* @return the found ResearcherProfile
|
||||
* @throws SQLException
|
||||
* @throws AuthorizeException
|
||||
*/
|
||||
public ResearcherProfile findById(Context context, UUID id) throws SQLException, AuthorizeException;
|
||||
|
||||
/**
|
||||
* Create a new researcher profile for the given ePerson.
|
||||
*
|
||||
* @param context the relevant DSpace Context.
|
||||
* @param ePerson the ePerson
|
||||
* @return the created profile
|
||||
* @throws SQLException
|
||||
* @throws AuthorizeException
|
||||
* @throws SearchServiceException
|
||||
*/
|
||||
public ResearcherProfile createAndReturn(Context context, EPerson ePerson)
|
||||
throws AuthorizeException, SQLException, SearchServiceException;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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. The visiblity controls whether the Profile is Anonymous READ or not.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
public void changeVisibility(Context context, ResearcherProfile profile, boolean visible)
|
||||
throws AuthorizeException, SQLException;
|
||||
|
||||
/**
|
||||
* Claims and links an eperson to an existing DSpaceObject
|
||||
* @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();
|
||||
}
|
@@ -7,6 +7,9 @@
|
||||
*/
|
||||
package org.dspace.authorize;
|
||||
|
||||
import static org.dspace.app.util.AuthorizeUtil.canCollectionAdminManageAccounts;
|
||||
import static org.dspace.app.util.AuthorizeUtil.canCommunityAdminManageAccounts;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -900,6 +903,16 @@ public class AuthorizeServiceImpl implements AuthorizeService {
|
||||
return discoverResult.getTotalSearchResults();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountManager(Context context) {
|
||||
try {
|
||||
return (canCommunityAdminManageAccounts() && isCommunityAdmin(context)
|
||||
|| canCollectionAdminManageAccounts() && isCollectionAdmin(context));
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean performCheck(Context context, String query) throws SQLException {
|
||||
if (context.getCurrentUser() == null) {
|
||||
return false;
|
||||
|
@@ -592,4 +592,12 @@ public interface AuthorizeService {
|
||||
*/
|
||||
long countAdminAuthorizedCollection(Context context, String query)
|
||||
throws SearchServiceException, SQLException;
|
||||
|
||||
/**
|
||||
* Returns true if the current user can manage accounts.
|
||||
*
|
||||
* @param context context with the current user
|
||||
* @return true if the current user can manage accounts
|
||||
*/
|
||||
boolean isAccountManager(Context context);
|
||||
}
|
||||
|
@@ -1131,6 +1131,50 @@ prevent the generation of resource policy entry values with null dspace_object a
|
||||
return !(hasCustomPolicy && isAnonimousGroup && datesAreNull);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator of Items possessing the passed metadata field, or only
|
||||
* those matching the passed value, if value is not Item.ANY
|
||||
*
|
||||
* @param context DSpace context object
|
||||
* @param schema metadata field schema
|
||||
* @param element metadata field element
|
||||
* @param qualifier metadata field qualifier
|
||||
* @param value field value or Item.ANY to match any value
|
||||
* @return an iterator over the items matching that authority value
|
||||
* @throws SQLException if database error
|
||||
* An exception that provides information on a database access error or other errors.
|
||||
* @throws AuthorizeException if authorization error
|
||||
* Exception indicating the current user of the context does not have permission
|
||||
* to perform a particular action.
|
||||
*/
|
||||
@Override
|
||||
public Iterator<Item> findArchivedByMetadataField(Context context,
|
||||
String schema, String element, String qualifier, String value)
|
||||
throws SQLException, AuthorizeException {
|
||||
MetadataSchema mds = metadataSchemaService.find(context, schema);
|
||||
if (mds == null) {
|
||||
throw new IllegalArgumentException("No such metadata schema: " + schema);
|
||||
}
|
||||
MetadataField mdf = metadataFieldService.findByElement(context, mds, element, qualifier);
|
||||
if (mdf == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"No such metadata field: schema=" + schema + ", element=" + element + ", qualifier=" + qualifier);
|
||||
}
|
||||
|
||||
if (Item.ANY.equals(value)) {
|
||||
return itemDAO.findByMetadataField(context, mdf, null, true);
|
||||
} else {
|
||||
return itemDAO.findByMetadataField(context, mdf, value, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Item> findArchivedByMetadataField(Context context, String metadataField, String value)
|
||||
throws SQLException, AuthorizeException {
|
||||
String[] mdValueByField = getMDValueByField(metadataField);
|
||||
return findArchivedByMetadataField(context, mdValueByField[0], mdValueByField[1], mdValueByField[2], value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator of Items possessing the passed metadata field, or only
|
||||
* those matching the passed value, if value is not Item.ANY
|
||||
@@ -1535,5 +1579,9 @@ prevent the generation of resource policy entry values with null dspace_object a
|
||||
.stream().findFirst().orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEntityType(Item item) {
|
||||
return getMetadataFirstValue(item, new MetadataFieldName("dspace.entity.type"), Item.ANY);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* 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.content.authority;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.dspace.authorize.factory.AuthorizeServiceFactory;
|
||||
import org.dspace.authorize.service.AuthorizeService;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.eperson.EPerson;
|
||||
import org.dspace.eperson.factory.EPersonServiceFactory;
|
||||
import org.dspace.eperson.service.EPersonService;
|
||||
import org.dspace.util.UUIDUtils;
|
||||
import org.dspace.web.ContextUtil;
|
||||
|
||||
/**
|
||||
* 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 = LogManager.getLogger(EPersonAuthority.class);
|
||||
|
||||
/**
|
||||
* the name assigned to the specific instance by the PluginService, @see
|
||||
* {@link NameAwarePlugin}
|
||||
**/
|
||||
private String authorityName;
|
||||
|
||||
private EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService();
|
||||
|
||||
private AuthorizeService authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService();
|
||||
|
||||
@Override
|
||||
public Choices getBestMatch(String text, String locale) {
|
||||
return getMatches(text, 0, 2, locale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Choices getMatches(String text, int start, int limit, String locale) {
|
||||
if (limit <= 0) {
|
||||
limit = 20;
|
||||
}
|
||||
|
||||
Context context = getContext();
|
||||
|
||||
List<EPerson> ePersons = searchEPersons(context, text, start, limit);
|
||||
|
||||
List<Choice> choiceList = new ArrayList<Choice>();
|
||||
for (EPerson eperson : ePersons) {
|
||||
choiceList.add(new Choice(eperson.getID().toString(), eperson.getFullName(), eperson.getFullName()));
|
||||
}
|
||||
Choice[] results = new Choice[choiceList.size()];
|
||||
results = choiceList.toArray(results);
|
||||
return new Choices(results, start, ePersons.size(), Choices.CF_AMBIGUOUS, ePersons.size() > (start + limit), 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLabel(String key, String locale) {
|
||||
|
||||
UUID uuid = UUIDUtils.fromString(key);
|
||||
if (uuid == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Context context = getContext();
|
||||
try {
|
||||
EPerson ePerson = ePersonService.find(context, uuid);
|
||||
return ePerson != null ? ePerson.getFullName() : null;
|
||||
} catch (SQLException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private List<EPerson> searchEPersons(Context context, String text, int start, int limit) {
|
||||
|
||||
if (!isCurrentUserAdminOrAccessGroupManager(context)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
try {
|
||||
return ePersonService.search(context, text, start, limit);
|
||||
} catch (SQLException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private Context getContext() {
|
||||
Context context = ContextUtil.obtainCurrentRequestContext();
|
||||
return context != null ? context : new Context();
|
||||
}
|
||||
|
||||
private boolean isCurrentUserAdminOrAccessGroupManager(Context context) {
|
||||
try {
|
||||
return authorizeService.isAdmin(context) || authorizeService.isAccountManager(context);
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPluginInstanceName() {
|
||||
return authorityName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPluginInstanceName(String name) {
|
||||
this.authorityName = name;
|
||||
}
|
||||
}
|
@@ -579,6 +579,37 @@ public interface ItemService
|
||||
*/
|
||||
public boolean canCreateNewVersion(Context context, Item item) throws SQLException;
|
||||
|
||||
/**
|
||||
* Returns an iterator of in archive items possessing the passed metadata field, or only
|
||||
* those matching the passed value, if value is not Item.ANY
|
||||
*
|
||||
* @param context DSpace context object
|
||||
* @param schema metadata field schema
|
||||
* @param element metadata field element
|
||||
* @param qualifier metadata field qualifier
|
||||
* @param value field value or Item.ANY to match any value
|
||||
* @return an iterator over the items matching that authority value
|
||||
* @throws SQLException if database error
|
||||
* @throws AuthorizeException if authorization error
|
||||
*/
|
||||
public Iterator<Item> findArchivedByMetadataField(Context context, String schema,
|
||||
String element, String qualifier,
|
||||
String value) throws SQLException, AuthorizeException;
|
||||
|
||||
/**
|
||||
* Returns an iterator of in archive items possessing the passed metadata field, or only
|
||||
* those matching the passed value, if value is not Item.ANY
|
||||
*
|
||||
* @param context DSpace context object
|
||||
* @param metadataField metadata
|
||||
* @param value field value or Item.ANY to match any value
|
||||
* @return an iterator over the items matching that authority value
|
||||
* @throws SQLException if database error
|
||||
* @throws AuthorizeException if authorization error
|
||||
*/
|
||||
public Iterator<Item> findArchivedByMetadataField(Context context, String metadataField, String value)
|
||||
throws SQLException, AuthorizeException;
|
||||
|
||||
/**
|
||||
* Returns an iterator of Items possessing the passed metadata field, or only
|
||||
* those matching the passed value, if value is not Item.ANY
|
||||
@@ -618,7 +649,7 @@ public interface ItemService
|
||||
*/
|
||||
public Iterator<Item> findByAuthorityValue(Context context,
|
||||
String schema, String element, String qualifier, String value)
|
||||
throws SQLException, AuthorizeException, IOException;
|
||||
throws SQLException, AuthorizeException;
|
||||
|
||||
|
||||
public Iterator<Item> findByMetadataFieldAuthority(Context context, String mdString, String authority)
|
||||
@@ -782,5 +813,12 @@ public interface ItemService
|
||||
*/
|
||||
public List<MetadataValue> getMetadata(Item item, String schema, String element, String qualifier,
|
||||
String lang, boolean enableVirtualMetadata);
|
||||
/**
|
||||
* Returns the item's entity type, if any.
|
||||
*
|
||||
* @param item the item
|
||||
* @return the entity type as string, if any
|
||||
*/
|
||||
public String getEntityType(Item item);
|
||||
|
||||
}
|
||||
|
@@ -147,3 +147,11 @@ proxies.trusted.include_ui_ip = true
|
||||
|
||||
# For the tests we have to disable this health indicator because there isn't a mock server and the calculated status was DOWN
|
||||
management.health.solrOai.enabled = false
|
||||
|
||||
researcher-profile.entity-type = Person
|
||||
|
||||
# Configuration settings required for Researcher Profiles
|
||||
# These settings ensure "dspace.object.owner" field are indexed by Authority Control
|
||||
choices.plugin.dspace.object.owner = EPersonAuthority
|
||||
choices.presentation.dspace.object.owner = suggest
|
||||
authority.controlled.dspace.object.owner = true
|
@@ -8,6 +8,8 @@
|
||||
package org.dspace.builder;
|
||||
|
||||
import static org.dspace.content.LicenseUtils.getLicenseText;
|
||||
import static org.dspace.content.MetadataSchemaEnum.DC;
|
||||
import static org.dspace.content.authority.Choices.CF_ACCEPTED;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
@@ -76,6 +78,11 @@ public class ItemBuilder extends AbstractDSpaceObjectBuilder<Item> {
|
||||
public ItemBuilder withAuthor(final String authorName) {
|
||||
return addMetadataValue(item, MetadataSchemaEnum.DC.getName(), "contributor", "author", authorName);
|
||||
}
|
||||
|
||||
public ItemBuilder withAuthor(final String authorName, final String authority) {
|
||||
return addMetadataValue(item, DC.getName(), "contributor", "author", null, authorName, authority, 600);
|
||||
}
|
||||
|
||||
public ItemBuilder withAuthor(final String authorName, final String authority, final int confidence) {
|
||||
return addMetadataValue(item, MetadataSchemaEnum.DC.getName(), "contributor", "author",
|
||||
null, authorName, authority, confidence);
|
||||
@@ -147,6 +154,10 @@ public class ItemBuilder extends AbstractDSpaceObjectBuilder<Item> {
|
||||
return addMetadataValue(item, schema, element, qualifier, value);
|
||||
}
|
||||
|
||||
public ItemBuilder withDspaceObjectOwner(String value, String authority) {
|
||||
return addMetadataValue(item, "dspace", "object", "owner", null, value, authority, CF_ACCEPTED);
|
||||
}
|
||||
|
||||
public ItemBuilder makeUnDiscoverable() {
|
||||
item.setDiscoverable(false);
|
||||
return this;
|
||||
@@ -175,7 +186,7 @@ public class ItemBuilder extends AbstractDSpaceObjectBuilder<Item> {
|
||||
/**
|
||||
* Create an admin group for the collection with the specified members
|
||||
*
|
||||
* @param members epersons to add to the admin group
|
||||
* @param ePerson epersons to add to the admin group
|
||||
* @return this builder
|
||||
* @throws SQLException
|
||||
* @throws AuthorizeException
|
||||
@@ -184,6 +195,9 @@ public class ItemBuilder extends AbstractDSpaceObjectBuilder<Item> {
|
||||
return setAdminPermission(item, ePerson, null);
|
||||
}
|
||||
|
||||
public ItemBuilder withPersonEmail(String email) {
|
||||
return addMetadataValue(item, "person", "email", null, email);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Item build() {
|
||||
|
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
package org.dspace.app.rest.authorization.impl;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.dspace.app.profile.service.ResearcherProfileService;
|
||||
import org.dspace.app.rest.authorization.AuthorizationFeature;
|
||||
import org.dspace.app.rest.authorization.AuthorizationFeatureDocumentation;
|
||||
import org.dspace.app.rest.model.BaseObjectRest;
|
||||
import org.dspace.app.rest.model.ItemRest;
|
||||
import org.dspace.authorize.AuthorizeException;
|
||||
import org.dspace.content.Item;
|
||||
import org.dspace.content.MetadataValue;
|
||||
import org.dspace.content.service.ItemService;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.eperson.EPerson;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*
|
||||
*/
|
||||
@Component
|
||||
@AuthorizationFeatureDocumentation(name = CanClaimItemFeature.NAME,
|
||||
description = "Used to verify if the current user is able to claim this item as their profile. "
|
||||
+ "Only available if the current item is not already claimed.")
|
||||
public class CanClaimItemFeature implements AuthorizationFeature {
|
||||
|
||||
public static final String NAME = "canClaimItem";
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CanClaimItemFeature.class);
|
||||
|
||||
@Autowired
|
||||
private ItemService itemService;
|
||||
|
||||
@Autowired
|
||||
private ResearcherProfileService researcherProfileService;
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("rawtypes")
|
||||
public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException {
|
||||
|
||||
if (!(object instanceof ItemRest) || context.getCurrentUser() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String id = ((ItemRest) object).getId();
|
||||
Item item = itemService.find(context, UUID.fromString(id));
|
||||
|
||||
return researcherProfileService.hasProfileType(item)
|
||||
&& hasNotOwner(item)
|
||||
&& hasNotAlreadyAProfile(context)
|
||||
&& haveSameEmail(item, context.getCurrentUser());
|
||||
}
|
||||
|
||||
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);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasNotOwner(Item item) {
|
||||
return StringUtils.isBlank(itemService.getMetadata(item, "dspace.object.owner"));
|
||||
}
|
||||
|
||||
private boolean haveSameEmail(Item item, EPerson currentUser) {
|
||||
return itemService.getMetadataByMetadataString(item, "person.email").stream()
|
||||
.map(MetadataValue::getValue)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.anyMatch(email -> email.equalsIgnoreCase(currentUser.getEmail()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedTypes() {
|
||||
return new String[] { ItemRest.CATEGORY + "." + ItemRest.NAME };
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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.converter;
|
||||
|
||||
import org.dspace.app.profile.ResearcherProfile;
|
||||
import org.dspace.app.rest.model.ResearcherProfileRest;
|
||||
import org.dspace.app.rest.projection.Projection;
|
||||
import org.dspace.content.Item;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* This converter is responsible for transforming an model that represent a
|
||||
* ResearcherProfile to the REST representation of an ResearcherProfile.
|
||||
*
|
||||
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
|
||||
*
|
||||
*/
|
||||
@Component
|
||||
public class ResearcherProfileConverter implements DSpaceConverter<ResearcherProfile, ResearcherProfileRest> {
|
||||
|
||||
@Override
|
||||
public ResearcherProfileRest convert(ResearcherProfile profile, Projection projection) {
|
||||
ResearcherProfileRest researcherProfileRest = new ResearcherProfileRest();
|
||||
|
||||
researcherProfileRest.setVisible(profile.isVisible());
|
||||
researcherProfileRest.setId(profile.getId());
|
||||
researcherProfileRest.setProjection(projection);
|
||||
|
||||
Item item = profile.getItem();
|
||||
|
||||
return researcherProfileRest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<ResearcherProfile> getModelClass() {
|
||||
return ResearcherProfile.class;
|
||||
}
|
||||
|
||||
}
|
@@ -20,6 +20,7 @@ import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.dspace.app.exception.ResourceAlreadyExistsException;
|
||||
import org.dspace.app.rest.utils.ContextUtil;
|
||||
import org.dspace.authorize.AuthorizeException;
|
||||
import org.dspace.core.Context;
|
||||
@@ -115,7 +116,7 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH
|
||||
HttpServletResponse.SC_METHOD_NOT_ALLOWED);
|
||||
}
|
||||
|
||||
@ExceptionHandler( {UnprocessableEntityException.class})
|
||||
@ExceptionHandler({ UnprocessableEntityException.class, ResourceAlreadyExistsException.class })
|
||||
protected void handleUnprocessableEntityException(HttpServletRequest request, HttpServletResponse response,
|
||||
Exception ex) throws IOException {
|
||||
//422 is not defined in HttpServletResponse. Its meaning is "Unprocessable Entity".
|
||||
|
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 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.login;
|
||||
|
||||
import org.dspace.core.Context;
|
||||
|
||||
/**
|
||||
* Interface for classes that need to perform some operations after the user
|
||||
* login.
|
||||
*
|
||||
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
|
||||
*
|
||||
*/
|
||||
public interface PostLoggedInAction {
|
||||
|
||||
/**
|
||||
* Perform some operations after the user login.
|
||||
*
|
||||
* @param context the DSpace context
|
||||
*/
|
||||
public void loggedIn(Context context);
|
||||
}
|
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* 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.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;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.dspace.app.profile.service.ResearcherProfileService;
|
||||
import org.dspace.app.rest.login.PostLoggedInAction;
|
||||
import org.dspace.authorize.AuthorizeException;
|
||||
import org.dspace.content.Item;
|
||||
import org.dspace.content.MetadataFieldName;
|
||||
import org.dspace.content.service.ItemService;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.eperson.EPerson;
|
||||
import org.dspace.eperson.service.EPersonService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Implementation of {@link PostLoggedInAction} that perform an automatic claim
|
||||
* between the logged eperson and possible profiles without eperson present in
|
||||
* the system. This pairing between eperson and profile is done starting from
|
||||
* the configured metadata of the logged in user.
|
||||
*
|
||||
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
|
||||
*
|
||||
*/
|
||||
public class ResearcherProfileAutomaticClaim implements PostLoggedInAction {
|
||||
|
||||
private final static Logger LOGGER = LoggerFactory.getLogger(ResearcherProfileAutomaticClaim.class);
|
||||
|
||||
@Autowired
|
||||
private ResearcherProfileService researcherProfileService;
|
||||
|
||||
@Autowired
|
||||
private ItemService itemService;
|
||||
|
||||
@Autowired
|
||||
private EPersonService ePersonService;
|
||||
|
||||
/**
|
||||
* The field of the eperson to search for.
|
||||
*/
|
||||
private final String ePersonField;
|
||||
|
||||
/**
|
||||
* 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.profileField = profileField;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loggedIn(Context context) {
|
||||
|
||||
if (isBlank(researcherProfileService.getProfileType())) {
|
||||
return;
|
||||
}
|
||||
|
||||
EPerson currentUser = context.getCurrentUser();
|
||||
if (currentUser == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
claimProfile(context, currentUser);
|
||||
} catch (SQLException | AuthorizeException e) {
|
||||
LOGGER.error("An error occurs during the profile claim by email", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void claimProfile(Context context, EPerson currentUser) throws SQLException, AuthorizeException {
|
||||
|
||||
UUID id = currentUser.getID();
|
||||
String fullName = currentUser.getFullName();
|
||||
|
||||
if (currentUserHasAlreadyResearcherProfile(context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Item item = findClaimableProfile(context, currentUser);
|
||||
if (item != null) {
|
||||
itemService.addMetadata(context, item, "dspace", "object", "owner",
|
||||
null, fullName, id.toString(), CF_ACCEPTED);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean currentUserHasAlreadyResearcherProfile(Context context) throws SQLException, AuthorizeException {
|
||||
return researcherProfileService.findById(context, context.getCurrentUser().getID()) != null;
|
||||
}
|
||||
|
||||
private Item findClaimableProfile(Context context, EPerson currentUser) throws SQLException, AuthorizeException {
|
||||
|
||||
String value = getValueToSearchFor(context, currentUser);
|
||||
if (StringUtils.isEmpty(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Item> 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;
|
||||
}
|
||||
|
||||
private String getValueToSearchFor(Context context, EPerson currentUser) {
|
||||
if ("email".equals(ePersonField)) {
|
||||
return currentUser.getEmail();
|
||||
}
|
||||
return ePersonService.getMetadataFirstValue(currentUser, new MetadataFieldName(ePersonField), Item.ANY);
|
||||
}
|
||||
|
||||
private boolean hasNotOwner(Item item) {
|
||||
return CollectionUtils.isEmpty(itemService.getMetadata(item, "dspace", "object", "owner", Item.ANY));
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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.model;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.dspace.app.rest.RestResourceController;
|
||||
|
||||
/**
|
||||
* The Researcher Profile REST resource.
|
||||
*
|
||||
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
|
||||
*
|
||||
*/
|
||||
@LinksRest(links = {
|
||||
@LinkRest(name = ResearcherProfileRest.ITEM, method = "getItem"),
|
||||
@LinkRest(name = ResearcherProfileRest.EPERSON, method = "getEPerson")
|
||||
})
|
||||
public class ResearcherProfileRest extends BaseObjectRest<UUID> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
public static final String CATEGORY = RestModel.EPERSON;
|
||||
public static final String NAME = "profile";
|
||||
|
||||
public static final String ITEM = "item";
|
||||
public static final String EPERSON = "eperson";
|
||||
|
||||
private boolean visible;
|
||||
|
||||
public boolean isVisible() {
|
||||
return visible;
|
||||
}
|
||||
|
||||
public void setVisible(boolean visible) {
|
||||
this.visible = visible;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return CATEGORY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getController() {
|
||||
return RestResourceController.class;
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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.model.hateoas;
|
||||
|
||||
import org.dspace.app.rest.model.ResearcherProfileRest;
|
||||
import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource;
|
||||
import org.dspace.app.rest.utils.Utils;
|
||||
|
||||
/**
|
||||
* This class serves as a wrapper class to wrap the SearchConfigurationRest into
|
||||
* a HAL resource.
|
||||
*
|
||||
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
|
||||
*
|
||||
*/
|
||||
@RelNameDSpaceResource(ResearcherProfileRest.NAME)
|
||||
public class ResearcherProfileResource extends DSpaceResource<ResearcherProfileRest> {
|
||||
|
||||
public ResearcherProfileResource(ResearcherProfileRest data, Utils utils) {
|
||||
super(data, utils);
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 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.repository;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.UUID;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.dspace.app.profile.ResearcherProfile;
|
||||
import org.dspace.app.profile.service.ResearcherProfileService;
|
||||
import org.dspace.app.rest.model.EPersonRest;
|
||||
import org.dspace.app.rest.model.ResearcherProfileRest;
|
||||
import org.dspace.app.rest.projection.Projection;
|
||||
import org.dspace.authorize.AuthorizeException;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.eperson.EPerson;
|
||||
import org.dspace.eperson.service.EPersonService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Link repository for "ePerson" subresource of an individual researcher
|
||||
* profile.
|
||||
*
|
||||
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
|
||||
*
|
||||
*/
|
||||
@Component(ResearcherProfileRest.CATEGORY + "." + ResearcherProfileRest.NAME + "." + ResearcherProfileRest.EPERSON)
|
||||
public class ResearcherProfileEPersonLinkRepository extends AbstractDSpaceRestRepository implements LinkRestRepository {
|
||||
|
||||
@Autowired
|
||||
private EPersonService ePersonService;
|
||||
|
||||
@Autowired
|
||||
private ResearcherProfileService researcherProfileService;
|
||||
|
||||
/**
|
||||
* Returns the ePerson related to the Research profile with the given UUID.
|
||||
*
|
||||
* @param request the http servlet request
|
||||
* @param id the profile UUID
|
||||
* @param pageable the optional pageable
|
||||
* @param projection the projection object
|
||||
* @return the ePerson rest representation
|
||||
*/
|
||||
@PreAuthorize("hasPermission(#id, 'EPERSON', 'READ')")
|
||||
public EPersonRest getEPerson(@Nullable HttpServletRequest request, UUID id,
|
||||
@Nullable Pageable pageable, Projection projection) {
|
||||
|
||||
try {
|
||||
Context context = obtainContext();
|
||||
|
||||
ResearcherProfile profile = researcherProfileService.findById(context, id);
|
||||
if (profile == null) {
|
||||
throw new ResourceNotFoundException("No such profile with UUID: " + id);
|
||||
}
|
||||
|
||||
EPerson ePerson = ePersonService.find(context, id);
|
||||
if (ePerson == null) {
|
||||
throw new ResourceNotFoundException("No such eperson related to a profile with EPerson UUID: " + id);
|
||||
}
|
||||
|
||||
return converter.toRest(ePerson, projection);
|
||||
} catch (SQLException | AuthorizeException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* 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.repository;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.UUID;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.dspace.app.profile.ResearcherProfile;
|
||||
import org.dspace.app.profile.service.ResearcherProfileService;
|
||||
import org.dspace.app.rest.model.ItemRest;
|
||||
import org.dspace.app.rest.model.ResearcherProfileRest;
|
||||
import org.dspace.app.rest.projection.Projection;
|
||||
import org.dspace.authorize.AuthorizeException;
|
||||
import org.dspace.core.Context;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Link repository for "item" subresource of an individual researcher profile.
|
||||
*
|
||||
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
|
||||
*
|
||||
*/
|
||||
@Component(ResearcherProfileRest.CATEGORY + "." + ResearcherProfileRest.NAME + "." + ResearcherProfileRest.ITEM)
|
||||
public class ResearcherProfileItemLinkRepository extends AbstractDSpaceRestRepository implements LinkRestRepository {
|
||||
|
||||
@Autowired
|
||||
private ResearcherProfileService researcherProfileService;
|
||||
|
||||
/**
|
||||
* Returns the item related to the Research profile with the given UUID.
|
||||
*
|
||||
* @param request the http servlet request
|
||||
* @param id the profile UUID
|
||||
* @param pageable the optional pageable
|
||||
* @param projection the projection object
|
||||
* @return the item rest representation
|
||||
*/
|
||||
@PreAuthorize("hasPermission(#id, 'PROFILE', 'READ')")
|
||||
public ItemRest getItem(@Nullable HttpServletRequest request, UUID id,
|
||||
@Nullable Pageable pageable, Projection projection) {
|
||||
|
||||
try {
|
||||
Context context = obtainContext();
|
||||
|
||||
ResearcherProfile profile = researcherProfileService.findById(context, id);
|
||||
if (profile == null) {
|
||||
throw new ResourceNotFoundException("No such item related to a profile with EPerson UUID: " + id);
|
||||
}
|
||||
|
||||
return converter.toRest(profile.getItem(), projection);
|
||||
} catch (SQLException | AuthorizeException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* 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.repository;
|
||||
|
||||
import java.net.URI;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.dspace.app.profile.ResearcherProfile;
|
||||
import org.dspace.app.profile.service.ResearcherProfileService;
|
||||
import org.dspace.app.rest.exception.DSpaceBadRequestException;
|
||||
import org.dspace.app.rest.exception.RepositoryMethodNotImplementedException;
|
||||
import org.dspace.app.rest.exception.UnprocessableEntityException;
|
||||
import org.dspace.app.rest.model.ResearcherProfileRest;
|
||||
import org.dspace.app.rest.model.patch.Patch;
|
||||
import org.dspace.app.rest.repository.patch.ResourcePatch;
|
||||
import org.dspace.app.rest.security.DSpacePermissionEvaluator;
|
||||
import org.dspace.authorize.AuthorizeException;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.discovery.SearchServiceException;
|
||||
import org.dspace.eperson.EPerson;
|
||||
import org.dspace.eperson.service.EPersonService;
|
||||
import org.dspace.util.UUIDUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* This is the repository responsible of exposing researcher profiles.
|
||||
*
|
||||
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
|
||||
*
|
||||
*/
|
||||
@Component(ResearcherProfileRest.CATEGORY + "." + ResearcherProfileRest.NAME)
|
||||
@ConditionalOnProperty(value = "researcher-profile.entity-type")
|
||||
public class ResearcherProfileRestRepository extends DSpaceRestRepository<ResearcherProfileRest, UUID> {
|
||||
|
||||
public static final String NO_VISIBILITY_CHANGE_MSG = "Refused to perform the Researcher Profile patch based "
|
||||
+ "on a token without changing the visibility";
|
||||
|
||||
@Autowired
|
||||
private ResearcherProfileService researcherProfileService;
|
||||
|
||||
@Autowired
|
||||
private DSpacePermissionEvaluator permissionEvaluator;
|
||||
|
||||
@Autowired
|
||||
private EPersonService ePersonService;
|
||||
|
||||
@Autowired
|
||||
private ResourcePatch<ResearcherProfile> resourcePatch;
|
||||
|
||||
@Override
|
||||
@PreAuthorize("hasPermission(#id, 'PROFILE', 'READ')")
|
||||
public ResearcherProfileRest findOne(Context context, UUID id) {
|
||||
try {
|
||||
ResearcherProfile profile = researcherProfileService.findById(context, id);
|
||||
if (profile == null) {
|
||||
return null;
|
||||
}
|
||||
return converter.toRest(profile, utils.obtainProjection());
|
||||
} catch (SQLException | AuthorizeException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new researcher profile from scratch.
|
||||
*/
|
||||
@Override
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
protected ResearcherProfileRest createAndReturn(Context context) throws AuthorizeException, SQLException {
|
||||
|
||||
UUID id = getEPersonIdFromRequest(context);
|
||||
if (isNotAuthorized(id, "WRITE")) {
|
||||
throw new AuthorizeException("User unauthorized to create a new profile for user " + id);
|
||||
}
|
||||
|
||||
EPerson ePerson = ePersonService.find(context, id);
|
||||
if (ePerson == null) {
|
||||
throw new UnprocessableEntityException("No EPerson exists with id: " + id);
|
||||
}
|
||||
|
||||
try {
|
||||
ResearcherProfile newProfile = researcherProfileService.createAndReturn(context, ePerson);
|
||||
return converter.toRest(newProfile, utils.obtainProjection());
|
||||
} catch (SearchServiceException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new researcher profile claiming an already existing item.
|
||||
*/
|
||||
@Override
|
||||
protected ResearcherProfileRest createAndReturn(final Context context, final List<String> list)
|
||||
throws AuthorizeException, SQLException, RepositoryMethodNotImplementedException {
|
||||
if (CollectionUtils.isEmpty(list) || list.size() > 1) {
|
||||
throw new IllegalArgumentException("Uri list must contain exactly one element");
|
||||
}
|
||||
|
||||
|
||||
UUID id = getEPersonIdFromRequest(context);
|
||||
if (isNotAuthorized(id, "WRITE")) {
|
||||
throw new AuthorizeException("User unauthorized to create a new profile for user " + id);
|
||||
}
|
||||
|
||||
EPerson ePerson = ePersonService.find(context, id);
|
||||
if (ePerson == null) {
|
||||
throw new UnprocessableEntityException("No EPerson exists with id: " + id);
|
||||
}
|
||||
|
||||
try {
|
||||
ResearcherProfile newProfile = researcherProfileService
|
||||
.claim(context, ePerson, URI.create(list.get(0)));
|
||||
return converter.toRest(newProfile, utils.obtainProjection());
|
||||
} catch (SearchServiceException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<ResearcherProfileRest> findAll(Context context, Pageable pageable) {
|
||||
throw new RepositoryMethodNotImplementedException("No implementation found; Method not allowed!", "");
|
||||
}
|
||||
|
||||
@Override
|
||||
@PreAuthorize("hasPermission(#id, 'PROFILE', 'DELETE')")
|
||||
protected void delete(Context context, UUID id) {
|
||||
try {
|
||||
researcherProfileService.deleteById(context, id);
|
||||
} catch (SQLException | AuthorizeException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@PreAuthorize("hasPermission(#id, 'PROFILE', #patch)")
|
||||
protected void patch(Context context, HttpServletRequest request, String apiCategory, String model,
|
||||
UUID id, Patch patch) throws SQLException, AuthorizeException {
|
||||
|
||||
ResearcherProfile profile = researcherProfileService.findById(context, id);
|
||||
if (profile == null) {
|
||||
throw new ResourceNotFoundException(apiCategory + "." + model + " with id: " + id + " not found");
|
||||
}
|
||||
|
||||
resourcePatch.patch(context, profile, patch.getOperations());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<ResearcherProfileRest> getDomainClass() {
|
||||
return ResearcherProfileRest.class;
|
||||
}
|
||||
|
||||
|
||||
private UUID getEPersonIdFromRequest(Context context) {
|
||||
HttpServletRequest request = getRequestService().getCurrentRequest().getHttpServletRequest();
|
||||
|
||||
String ePersonId = request.getParameter("eperson");
|
||||
if (ePersonId == null) {
|
||||
return context.getCurrentUser().getID();
|
||||
}
|
||||
|
||||
UUID uuid = UUIDUtils.fromString(ePersonId);
|
||||
if (uuid == null) {
|
||||
throw new DSpaceBadRequestException("The provided eperson parameter is not a valid uuid");
|
||||
}
|
||||
return uuid;
|
||||
}
|
||||
|
||||
private boolean isNotAuthorized(UUID id, String permission) {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
return !permissionEvaluator.hasPermission(authentication, id, "PROFILE", permission);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* 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.repository.patch.operation;
|
||||
|
||||
import java.sql.SQLException;
|
||||
|
||||
import org.dspace.app.profile.ResearcherProfile;
|
||||
import org.dspace.app.profile.service.ResearcherProfileService;
|
||||
import org.dspace.app.rest.exception.RESTAuthorizationException;
|
||||
import org.dspace.app.rest.exception.UnprocessableEntityException;
|
||||
import org.dspace.app.rest.model.patch.Operation;
|
||||
import org.dspace.authorize.AuthorizeException;
|
||||
import org.dspace.core.Context;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Implementation for ResearcherProfile visibility patches.
|
||||
*
|
||||
* Example:
|
||||
* <code> curl -X PATCH http://${dspace.server.url}/api/eperson/profiles/<:id-eperson> -H "
|
||||
* Content-Type: application/json" -d '[{ "op": "replace", "path": "
|
||||
* /visible", "value": true]'
|
||||
* </code>
|
||||
*/
|
||||
@Component
|
||||
public class ResearcherProfileVisibleReplaceOperation extends PatchOperation<ResearcherProfile> {
|
||||
|
||||
@Autowired
|
||||
private ResearcherProfileService researcherProfileService;
|
||||
|
||||
/**
|
||||
* Path in json body of patch that uses this operation.
|
||||
*/
|
||||
public static final String OPERATION_VISIBLE_CHANGE = "/visible";
|
||||
|
||||
@Override
|
||||
public ResearcherProfile perform(Context context, ResearcherProfile profile, Operation operation)
|
||||
throws SQLException {
|
||||
|
||||
Object value = operation.getValue();
|
||||
if (value == null | !(value instanceof Boolean)) {
|
||||
throw new UnprocessableEntityException("The /visible value must be a boolean (true|false)");
|
||||
}
|
||||
|
||||
try {
|
||||
researcherProfileService.changeVisibility(context, profile, (boolean) value);
|
||||
} catch (AuthorizeException e) {
|
||||
throw new RESTAuthorizationException("Unauthorized user for profile visibility change");
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Object objectToMatch, Operation operation) {
|
||||
return (objectToMatch instanceof ResearcherProfile
|
||||
&& operation.getOp().trim().equalsIgnoreCase(OPERATION_REPLACE)
|
||||
&& operation.getPath().trim().equalsIgnoreCase(OPERATION_VISIBLE_CHANGE));
|
||||
}
|
||||
|
||||
}
|
@@ -11,14 +11,16 @@ import static org.dspace.app.rest.security.WebSecurityConfiguration.ADMIN_GRANT;
|
||||
import static org.dspace.app.rest.security.WebSecurityConfiguration.AUTHENTICATED_GRANT;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.dspace.app.rest.login.PostLoggedInAction;
|
||||
import org.dspace.app.rest.utils.ContextUtil;
|
||||
import org.dspace.app.util.AuthorizeUtil;
|
||||
import org.dspace.authenticate.AuthenticationMethod;
|
||||
import org.dspace.authenticate.service.AuthenticationService;
|
||||
import org.dspace.authorize.service.AuthorizeService;
|
||||
@@ -62,6 +64,16 @@ public class EPersonRestAuthenticationProvider implements AuthenticationProvider
|
||||
@Autowired
|
||||
private HttpServletRequest request;
|
||||
|
||||
@Autowired(required = false)
|
||||
private List<PostLoggedInAction> postLoggedInActions;
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct() {
|
||||
if (postLoggedInActions == null) {
|
||||
postLoggedInActions = Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
Context context = ContextUtil.obtainContext(request);
|
||||
@@ -122,6 +134,15 @@ public class EPersonRestAuthenticationProvider implements AuthenticationProvider
|
||||
.getHeader(newContext, "login", "type=explicit"));
|
||||
|
||||
output = createAuthentication(newContext);
|
||||
|
||||
for (PostLoggedInAction action : postLoggedInActions) {
|
||||
try {
|
||||
action.loggedIn(newContext);
|
||||
} catch (Exception ex) {
|
||||
log.error("An error occurs performing post logged in action", ex);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
log.info(LogHelper.getHeader(newContext, "failed_login", "email="
|
||||
+ name + ", result="
|
||||
@@ -176,20 +197,15 @@ public class EPersonRestAuthenticationProvider implements AuthenticationProvider
|
||||
EPerson eperson = context.getCurrentUser();
|
||||
if (eperson != null) {
|
||||
boolean isAdmin = false;
|
||||
boolean isCommunityAdmin = false;
|
||||
boolean isCollectionAdmin = false;
|
||||
try {
|
||||
isAdmin = authorizeService.isAdmin(context, eperson);
|
||||
isCommunityAdmin = authorizeService.isCommunityAdmin(context);
|
||||
isCollectionAdmin = authorizeService.isCollectionAdmin(context);
|
||||
} catch (SQLException e) {
|
||||
log.error("SQL error while checking for admin rights", e);
|
||||
}
|
||||
|
||||
if (isAdmin) {
|
||||
authorities.add(new SimpleGrantedAuthority(ADMIN_GRANT));
|
||||
} else if ((isCommunityAdmin && AuthorizeUtil.canCommunityAdminManageAccounts())
|
||||
|| (isCollectionAdmin && AuthorizeUtil.canCollectionAdminManageAccounts())) {
|
||||
} else if (authorizeService.isAccountManager(context)) {
|
||||
authorities.add(new SimpleGrantedAuthority(MANAGE_ACCESS_GROUP));
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 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.security;
|
||||
|
||||
import static org.dspace.app.rest.security.DSpaceRestPermission.DELETE;
|
||||
import static org.dspace.app.rest.security.DSpaceRestPermission.READ;
|
||||
import static org.dspace.app.rest.security.DSpaceRestPermission.WRITE;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.dspace.app.rest.model.ResearcherProfileRest;
|
||||
import org.dspace.app.rest.utils.ContextUtil;
|
||||
import org.dspace.core.Context;
|
||||
import org.dspace.eperson.EPerson;
|
||||
import org.dspace.services.RequestService;
|
||||
import org.dspace.services.model.Request;
|
||||
import org.dspace.util.UUIDUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
*
|
||||
* An authenticated user is allowed to view, update or delete his or her own
|
||||
* data. This {@link RestPermissionEvaluatorPlugin} implements that requirement.
|
||||
*
|
||||
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
|
||||
*
|
||||
*/
|
||||
@Component
|
||||
public class ResearcherProfileRestPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin {
|
||||
|
||||
@Autowired
|
||||
private RequestService requestService;
|
||||
|
||||
@Override
|
||||
public boolean hasDSpacePermission(Authentication authentication, Serializable targetId, String targetType,
|
||||
DSpaceRestPermission restPermission) {
|
||||
|
||||
if (!READ.equals(restPermission) && !WRITE.equals(restPermission) && !DELETE.equals(restPermission)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!StringUtils.equalsIgnoreCase(targetType, ResearcherProfileRest.NAME)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UUID id = UUIDUtils.fromString(targetId.toString());
|
||||
if (id == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Request request = requestService.getCurrentRequest();
|
||||
Context context = ContextUtil.obtainContext((HttpServletRequest) request.getServletRequest());
|
||||
|
||||
EPerson currentUser = context.getCurrentUser();
|
||||
if (currentUser == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (id.equals(currentUser.getID())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
import static org.dspace.app.rest.matcher.VocabularyMatcher.matchVocabularyEntry;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.empty;
|
||||
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.sql.SQLException;
|
||||
|
||||
import org.dspace.app.rest.test.AbstractControllerIntegrationTest;
|
||||
import org.dspace.builder.EPersonBuilder;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link EPersonAuthority}.
|
||||
*
|
||||
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
|
||||
*
|
||||
*/
|
||||
public class EPersonAuthorityIT extends AbstractControllerIntegrationTest {
|
||||
|
||||
|
||||
@Test
|
||||
public void testEPersonAuthorityWithFirstName() throws Exception {
|
||||
|
||||
context.turnOffAuthorisationSystem();
|
||||
String firstEPersonId = createEPerson("Luca", "Giamminonni");
|
||||
String secondEPersonId = createEPerson("Andrea", "Bollini");
|
||||
String thirdEPersonId = createEPerson("Luca", "Bollini");
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
String token = getAuthToken(admin.getEmail(), password);
|
||||
getClient(token).perform(get("/api/submission/vocabularies/EPersonAuthority/entries")
|
||||
.param("filter", "Luca"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._embedded.entries", containsInAnyOrder(
|
||||
matchVocabularyEntry("Luca Giamminonni", "Luca Giamminonni", "vocabularyEntry", firstEPersonId),
|
||||
matchVocabularyEntry("Luca Bollini", "Luca Bollini", "vocabularyEntry", thirdEPersonId))))
|
||||
.andExpect(jsonPath("$.page.totalElements", Matchers.is(2)));
|
||||
|
||||
getClient(token).perform(get("/api/submission/vocabularies/EPersonAuthority/entries")
|
||||
.param("filter", "Andrea"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._embedded.entries", containsInAnyOrder(
|
||||
matchVocabularyEntry("Andrea Bollini", "Andrea Bollini", "vocabularyEntry", secondEPersonId))))
|
||||
.andExpect(jsonPath("$.page.totalElements", Matchers.is(1)));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEPersonAuthorityWithLastName() throws Exception {
|
||||
|
||||
context.turnOffAuthorisationSystem();
|
||||
String firstEPersonId = createEPerson("Luca", "Giamminonni");
|
||||
String secondEPersonId = createEPerson("Andrea", "Bollini");
|
||||
String thirdEPersonId = createEPerson("Luca", "Bollini");
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
String token = getAuthToken(admin.getEmail(), password);
|
||||
getClient(token).perform(get("/api/submission/vocabularies/EPersonAuthority/entries")
|
||||
.param("filter", "Giamminonni"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._embedded.entries", containsInAnyOrder(
|
||||
matchVocabularyEntry("Luca Giamminonni", "Luca Giamminonni", "vocabularyEntry", firstEPersonId))))
|
||||
.andExpect(jsonPath("$.page.totalElements", Matchers.is(1)));
|
||||
|
||||
getClient(token).perform(get("/api/submission/vocabularies/EPersonAuthority/entries")
|
||||
.param("filter", "Bollini"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._embedded.entries", containsInAnyOrder(
|
||||
matchVocabularyEntry("Andrea Bollini", "Andrea Bollini", "vocabularyEntry", secondEPersonId),
|
||||
matchVocabularyEntry("Luca Bollini", "Luca Bollini", "vocabularyEntry", thirdEPersonId))))
|
||||
.andExpect(jsonPath("$.page.totalElements", Matchers.is(2)));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEPersonAuthorityWithId() throws Exception {
|
||||
|
||||
context.turnOffAuthorisationSystem();
|
||||
String firstEPersonId = createEPerson("Luca", "Giamminonni");
|
||||
String secondEPersonId = createEPerson("Andrea", "Bollini");
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
String token = getAuthToken(admin.getEmail(), password);
|
||||
getClient(token).perform(get("/api/submission/vocabularies/EPersonAuthority/entries")
|
||||
.param("filter", firstEPersonId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._embedded.entries", containsInAnyOrder(
|
||||
matchVocabularyEntry("Luca Giamminonni", "Luca Giamminonni", "vocabularyEntry", firstEPersonId))))
|
||||
.andExpect(jsonPath("$.page.totalElements", Matchers.is(1)));
|
||||
|
||||
getClient(token).perform(get("/api/submission/vocabularies/EPersonAuthority/entries")
|
||||
.param("filter", secondEPersonId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._embedded.entries", containsInAnyOrder(
|
||||
matchVocabularyEntry("Andrea Bollini", "Andrea Bollini", "vocabularyEntry", secondEPersonId))))
|
||||
.andExpect(jsonPath("$.page.totalElements", Matchers.is(1)));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEPersonAuthorityWithAnonymousUser() throws Exception {
|
||||
|
||||
context.turnOffAuthorisationSystem();
|
||||
createEPerson("Luca", "Giamminonni");
|
||||
createEPerson("Andrea", "Bollini");
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
getClient().perform(get("/api/submission/vocabularies/EPersonAuthority/entries")
|
||||
.param("filter", "Luca"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEPersonAuthorityWithNotAdminUser() throws Exception {
|
||||
|
||||
context.turnOffAuthorisationSystem();
|
||||
createEPerson("Luca", "Giamminonni");
|
||||
createEPerson("Andrea", "Bollini");
|
||||
createEPerson("Luca", "Bollini");
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
String token = getAuthToken(eperson.getEmail(), password);
|
||||
getClient(token).perform(get("/api/submission/vocabularies/EPersonAuthority/entries")
|
||||
.param("filter", "Luca"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._embedded.entries", empty()))
|
||||
.andExpect(jsonPath("$.page.totalElements", Matchers.is(0)));
|
||||
|
||||
}
|
||||
|
||||
private String createEPerson(String firstName, String lastName) throws SQLException {
|
||||
return EPersonBuilder.createEPerson(context)
|
||||
.withNameInMetadata(firstName, lastName)
|
||||
.build()
|
||||
.getID()
|
||||
.toString();
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* 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.equalTo;
|
||||
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import org.dspace.app.rest.authorization.impl.CanClaimItemFeature;
|
||||
import org.dspace.app.rest.converter.ItemConverter;
|
||||
import org.dspace.app.rest.model.ItemRest;
|
||||
import org.dspace.app.rest.projection.Projection;
|
||||
import org.dspace.app.rest.test.AbstractControllerIntegrationTest;
|
||||
import org.dspace.app.rest.utils.Utils;
|
||||
import org.dspace.builder.CollectionBuilder;
|
||||
import org.dspace.builder.CommunityBuilder;
|
||||
import org.dspace.builder.ItemBuilder;
|
||||
import org.dspace.content.Collection;
|
||||
import org.dspace.content.Item;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
/**
|
||||
* Test of Profile Claim Authorization Feature implementation.
|
||||
*
|
||||
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
|
||||
*/
|
||||
public class CanClaimItemFeatureIT extends AbstractControllerIntegrationTest {
|
||||
|
||||
private Item profile;
|
||||
|
||||
@Autowired
|
||||
private ItemConverter itemConverter;
|
||||
|
||||
@Autowired
|
||||
private Utils utils;
|
||||
|
||||
@Autowired
|
||||
private AuthorizationFeatureService authorizationFeatureService;
|
||||
|
||||
private AuthorizationFeature canClaimProfileFeature;
|
||||
|
||||
private Collection personCollection;
|
||||
|
||||
private String epersonToken;
|
||||
|
||||
private String adminToken;
|
||||
|
||||
@Override
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
parentCommunity = CommunityBuilder.createCommunity(context).withName("Community").build();
|
||||
|
||||
personCollection = CollectionBuilder.createCollection(context, parentCommunity)
|
||||
.withEntityType("Person")
|
||||
.withName("claimableA")
|
||||
.build();
|
||||
|
||||
epersonToken = getAuthToken(eperson.getEmail(), password);
|
||||
|
||||
adminToken = getAuthToken(admin.getEmail(), password);
|
||||
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
canClaimProfileFeature = authorizationFeatureService.find(CanClaimItemFeature.NAME);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCanClaimAProfile() throws Exception {
|
||||
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
profile = ItemBuilder.createItem(context, personCollection)
|
||||
.withPersonEmail(eperson.getEmail())
|
||||
.build();
|
||||
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
getClient(epersonToken).perform(get("/api/authz/authorizations/search/object")
|
||||
.param("uri", uri(profile))
|
||||
.param("feature", canClaimProfileFeature.getName()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._embedded").exists())
|
||||
.andExpect(jsonPath("$.page.totalElements", greaterThanOrEqualTo(1)));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCanClaimAProfileWithAnonymousUser() throws Exception {
|
||||
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
profile = ItemBuilder.createItem(context, personCollection).build();
|
||||
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
getClient().perform(get("/api/authz/authorizations/search/object")
|
||||
.param("uri", uri(profile))
|
||||
.param("feature", canClaimProfileFeature.getName()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._embedded").doesNotExist())
|
||||
.andExpect(jsonPath("$.page.totalElements", equalTo(0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCanClaimWithAdminUser() throws Exception {
|
||||
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
profile = ItemBuilder.createItem(context, personCollection)
|
||||
.withPersonEmail("myemail@test.it")
|
||||
.withPersonEmail(admin.getEmail())
|
||||
.build();
|
||||
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
getClient(adminToken).perform(get("/api/authz/authorizations/search/object")
|
||||
.param("uri", uri(profile))
|
||||
.param("feature", canClaimProfileFeature.getName()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._embedded").exists())
|
||||
.andExpect(jsonPath("$.page.totalElements", greaterThanOrEqualTo(1)));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotClaimableEntityForDifferentEmail() throws Exception {
|
||||
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
profile = ItemBuilder.createItem(context, personCollection)
|
||||
.withPersonEmail(eperson.getEmail())
|
||||
.build();
|
||||
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
getClient(adminToken).perform(get("/api/authz/authorizations/search/object")
|
||||
.param("uri", uri(profile))
|
||||
.param("feature", canClaimProfileFeature.getName()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._embedded").doesNotExist())
|
||||
.andExpect(jsonPath("$.page.totalElements", equalTo(0)));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotClaimableEntityWithoutEmail() throws Exception {
|
||||
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
profile = ItemBuilder.createItem(context, personCollection)
|
||||
.build();
|
||||
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
getClient(adminToken).perform(get("/api/authz/authorizations/search/object")
|
||||
.param("uri", uri(profile))
|
||||
.param("feature", canClaimProfileFeature.getName()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._embedded").doesNotExist())
|
||||
.andExpect(jsonPath("$.page.totalElements", equalTo(0)));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotClaimableEntity() throws Exception {
|
||||
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
Collection publicationCollection = CollectionBuilder
|
||||
.createCollection(context, context.reloadEntity(parentCommunity))
|
||||
.withEntityType("Publication")
|
||||
.withName("notClaimable")
|
||||
.build();
|
||||
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
Item publication = ItemBuilder.createItem(context, publicationCollection).build();
|
||||
|
||||
getClient(epersonToken).perform(get("/api/authz/authorizations/search/object")
|
||||
.param("uri", uri(publication))
|
||||
.param("feature", canClaimProfileFeature.getName()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._embedded").doesNotExist())
|
||||
.andExpect(jsonPath("$.page.totalElements", equalTo(0)));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testItemAlreadyInARelation() throws Exception {
|
||||
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
Item ownedItem = ItemBuilder.createItem(context, personCollection)
|
||||
.withDspaceObjectOwner("owner", "ownerAuthority")
|
||||
.build();
|
||||
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
getClient(epersonToken).perform(get("/api/authz/authorizations/search/object")
|
||||
.param("uri", uri(ownedItem))
|
||||
.param("feature", canClaimProfileFeature.getName()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._embedded").doesNotExist())
|
||||
.andExpect(jsonPath("$.page.totalElements", equalTo(0)));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserWithProfile() throws Exception {
|
||||
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
profile = ItemBuilder.createItem(context, personCollection)
|
||||
.withPersonEmail(eperson.getEmail())
|
||||
.build();
|
||||
|
||||
ItemBuilder.createItem(context, personCollection)
|
||||
.withTitle("User")
|
||||
.withDspaceObjectOwner("User", eperson.getID().toString())
|
||||
.build();
|
||||
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
getClient(epersonToken).perform(get("/api/authz/authorizations/search/object")
|
||||
.param("uri", uri(profile))
|
||||
.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);
|
||||
return utils.linkToSingleResource(itemRest, "self").getHref();
|
||||
}
|
||||
|
||||
}
|
@@ -15,6 +15,7 @@ import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.hamcrest.core.StringEndsWith;
|
||||
|
||||
/**
|
||||
@@ -67,6 +68,22 @@ public class MetadataMatcher {
|
||||
return hasJsonPath("$.['" + key + "'][" + position + "].value", is(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a matcher to ensure a given value is present at a specific position in
|
||||
* the list of values for a given key.
|
||||
*
|
||||
* @param key the metadata key.
|
||||
* @param value the value that must be present.
|
||||
* @param authority the authority that must be present.
|
||||
* @param position the position it must be present at.
|
||||
* @return the matcher.
|
||||
*/
|
||||
public static Matcher<? super Object> matchMetadata(String key, String value, String authority, int position) {
|
||||
Matcher<Object> hasValue = hasJsonPath("$.['" + key + "'][" + position + "].value", is(value));
|
||||
Matcher<Object> hasAuthority = hasJsonPath("$.['" + key + "'][" + position + "].authority", is(authority));
|
||||
return Matchers.allOf(hasValue, hasAuthority);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a matcher to ensure a given key is not present.
|
||||
*
|
||||
|
@@ -41,4 +41,14 @@ public class VocabularyMatcher {
|
||||
hasJsonPath("$.type", is(type))
|
||||
);
|
||||
}
|
||||
|
||||
public static Matcher<? super Object> matchVocabularyEntry(String display, String value, String type,
|
||||
String authority) {
|
||||
return allOf(
|
||||
hasJsonPath("$.display", is(display)),
|
||||
hasJsonPath("$.value", is(value)),
|
||||
hasJsonPath("$.type", is(type)),
|
||||
hasJsonPath("$.authority", is(authority))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -930,6 +930,7 @@ webui.licence_bundle.show = false
|
||||
# since that usually contains email addresses which ought to be kept
|
||||
# private and is mainly of interest to administrators:
|
||||
metadata.hide.dc.description.provenance = true
|
||||
metadata.hide.person.email = true
|
||||
|
||||
##### Settings for Submission Process #####
|
||||
|
||||
@@ -1390,17 +1391,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
|
||||
@@ -1415,69 +1405,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 = <true|false> # default: true
|
||||
# vocabulary.plugin._plugin_.hierarchy.suggest = <true|false> # default: false
|
||||
# vocabulary.plugin._plugin_.delimiter = "<string>" # 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
|
||||
@@ -1634,6 +1561,7 @@ include = ${module_dir}/authentication-oidc.cfg
|
||||
include = ${module_dir}/authentication-password.cfg
|
||||
include = ${module_dir}/authentication-shibboleth.cfg
|
||||
include = ${module_dir}/authentication-x509.cfg
|
||||
include = ${module_dir}/authority.cfg
|
||||
include = ${module_dir}/bulkedit.cfg
|
||||
include = ${module_dir}/citation-page.cfg
|
||||
include = ${module_dir}/clamav.cfg
|
||||
@@ -1650,6 +1578,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
|
||||
|
88
dspace/config/modules/authority.cfg
Normal file
88
dspace/config/modules/authority.cfg
Normal file
@@ -0,0 +1,88 @@
|
||||
#---------------------------------------------------------------#
|
||||
#----------------- AUTHORITY CONFIGURATIONS --------------------#
|
||||
#---------------------------------------------------------------#
|
||||
# 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 = <true|false> # default: true
|
||||
# vocabulary.plugin._plugin_.hierarchy.suggest = <true|false> # default: false
|
||||
# vocabulary.plugin._plugin_.delimiter = "<string>" # 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
|
||||
|
||||
# Configuration settings required for Researcher Profiles
|
||||
# These settings ensure "dspace.object.owner" field are indexed by Authority Control
|
||||
#choices.plugin.dspace.object.owner = EPersonAuthority
|
||||
#choices.presentation.dspace.object.owner = suggest
|
||||
#authority.controlled.dspace.object.owner = true
|
18
dspace/config/modules/researcher-profile.cfg
Normal file
18
dspace/config/modules/researcher-profile.cfg
Normal file
@@ -0,0 +1,18 @@
|
||||
#---------------------------------------------------------------#
|
||||
#------------------- PROFILE CONFIGURATIONS --------------------#
|
||||
#---------------------------------------------------------------#
|
||||
|
||||
# The Entity Type to use for the Researcher Profile. Defaults to "Person" as this is the recommended Entity Type to use
|
||||
# (Requires reboot of servlet container, e.g. Tomcat, to reload)
|
||||
#researcher-profile.entity-type = Person
|
||||
|
||||
# The UUID of the default Collection where newly created Entities will be stored. If unspecified, the first Collection which supports "entity-type" will be used.
|
||||
#researcher-profile.collection.uuid =
|
||||
|
||||
# Whether or not to delete the Entity (Item) when a Profile is deleted. Default value is "false" which means that when a user deletes their profile,
|
||||
# the Entity remains (retaining its data and relationships). When set to "true", the Entity (and its relationships) will be deleted if a user deletes their Profile.
|
||||
researcher-profile.hard-delete.enabled = false
|
||||
|
||||
# Whether a newly created profile should be visible by default. Default value is "false" which means a newly created profile is not readable to
|
||||
# anonymous users. Setting to "true" means a newly created profile is immediately readable to anonymous users.
|
||||
researcher-profile.set-new-profile-visible = false
|
@@ -34,6 +34,7 @@ rest.properties.exposed = plugin.named.org.dspace.curate.CurationTask
|
||||
rest.properties.exposed = google.analytics.key
|
||||
rest.properties.exposed = websvc.opensearch.enable
|
||||
rest.properties.exposed = versioning.item.history.include.submitter
|
||||
rest.properties.exposed = researcher-profile.entity-type
|
||||
rest.properties.exposed = websvc.opensearch.svccontext
|
||||
rest.properties.exposed = submit.type-bind.field
|
||||
|
||||
|
@@ -43,4 +43,12 @@
|
||||
<qualifier>enabled</qualifier>
|
||||
<scope_note>Stores a boolean text value (true or false) to indicate if the iiif feature is enabled or not for the dspace object. If absent the value is derived from the parent dspace object</scope_note>
|
||||
</dc-type>
|
||||
|
||||
<dc-type>
|
||||
<schema>dspace</schema>
|
||||
<element>object</element>
|
||||
<qualifier>owner</qualifier>
|
||||
<scope_note>Used to support researcher profiles</scope_note>
|
||||
</dc-type>
|
||||
|
||||
</dspace-dc-types>
|
||||
|
@@ -63,6 +63,8 @@
|
||||
<bean class="org.dspace.content.authority.ChoiceAuthorityServiceImpl"/>
|
||||
<bean class="org.dspace.content.authority.MetadataAuthorityServiceImpl" lazy-init="true"/>
|
||||
|
||||
<bean class="org.dspace.app.profile.ResearcherProfileServiceImpl"/>
|
||||
|
||||
<bean class='org.dspace.service.impl.HttpConnectionPoolService'
|
||||
id='solrHttpConnectionPoolService'
|
||||
scope='singleton'
|
||||
|
@@ -75,6 +75,7 @@
|
||||
<entry key="personOrOrgunit" value-ref="personOrOrgunit"/>
|
||||
<!-- OpenAIRE4 guidelines - search for an OrgUnit that have a specific dc.type=FundingOrganization -->
|
||||
<entry key="openAIREFundingAgency" value-ref="openAIREFundingAgency"/>
|
||||
<entry key="eperson_claims" value-ref="eperson_claims"/>
|
||||
</map>
|
||||
</property>
|
||||
<property name="toIgnoreMetadataFields">
|
||||
@@ -1410,6 +1411,42 @@
|
||||
<property name="spellCheckEnabled" value="true"/>
|
||||
</bean>
|
||||
|
||||
<bean id="eperson_claims" class="org.dspace.discovery.configuration.DiscoveryConfiguration" scope="prototype">
|
||||
<property name="id" value="eperson_claims"/>
|
||||
<property name="indexAlways" value="true"/>
|
||||
<!--Which sidebar facets are to be displayed-->
|
||||
<property name="sidebarFacets">
|
||||
<list/>
|
||||
</property>
|
||||
<!--The search filters which can be used on the discovery search page-->
|
||||
<property name="searchFilters">
|
||||
<list/>
|
||||
</property>
|
||||
<!--The sort filters for the discovery search-->
|
||||
<property name="searchSortConfiguration">
|
||||
<bean class="org.dspace.discovery.configuration.DiscoverySortConfiguration">
|
||||
<property name="sortFields">
|
||||
<list>
|
||||
<ref bean="sortTitle"/>
|
||||
</list>
|
||||
</property>
|
||||
</bean>
|
||||
</property>
|
||||
<!--Any default filter queries, these filter queries will be used for all
|
||||
queries done by discovery for this configuration -->
|
||||
<property name="defaultFilterQueries">
|
||||
<list>
|
||||
<!--Only find items into claimable collection defined in cfg-->
|
||||
<value>search.resourcetype:Item</value>
|
||||
<value>search.entitytype:${researcher-profile.entity-type:Person}</value>
|
||||
</list>
|
||||
</property>
|
||||
<!--Default result per page -->
|
||||
<property name="defaultRpp" value="100"/>
|
||||
<!-- When true a "did you mean" example will be displayed, value can be true or false -->
|
||||
<property name="spellCheckEnabled" value="false"/>
|
||||
</bean>
|
||||
|
||||
|
||||
<!--TagCloud configuration bean for homepage discovery configuration-->
|
||||
<bean id="homepageTagCloudFacetConfiguration" class="org.dspace.discovery.configuration.TagCloudFacetConfiguration">
|
||||
|
12
dspace/config/spring/rest/post-logged-in-actions.xml
Normal file
12
dspace/config/spring/rest/post-logged-in-actions.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:util="http://www.springframework.org/schema/util"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
|
||||
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
|
||||
|
||||
<bean id="researcherProfileClaimByEmail" class="org.dspace.app.rest.login.impl.ResearcherProfileAutomaticClaim">
|
||||
<constructor-arg name="ePersonField" value="email" />
|
||||
<constructor-arg name="profileField" value="person.email" />
|
||||
</bean>
|
||||
</beans>
|
Reference in New Issue
Block a user