[CST-5669] ORCID Authorizations (REST) .

This commit is contained in:
eskander
2022-04-12 11:06:59 +02:00
parent afa86658c9
commit 035940018f
12 changed files with 909 additions and 19 deletions

View File

@@ -10,6 +10,7 @@ package org.dspace.app.orcid.service;
import java.sql.SQLException;
import org.dspace.app.orcid.model.OrcidTokenResponseDTO;
import org.dspace.app.profile.OrcidProfileDisconnectionMode;
import org.dspace.content.Item;
import org.dspace.core.Context;
@@ -33,4 +34,20 @@ public interface OrcidSynchronizationService {
*/
public void linkProfile(Context context, Item profile, OrcidTokenResponseDTO token) throws SQLException;
/**
* Disconnect the given profile from ORCID.
*
* @param context the relevant DSpace Context.
* @param profile the profile to disconnect
* @throws SQLException if a SQL error occurs during the profile update
*/
public void unlinkProfile(Context context, Item profile) throws SQLException;
/**
* Returns the configuration ORCID profile's disconnection mode. If that mode is
* not configured or the configuration is wrong, the value DISABLED is returned.
*
* @return the disconnection mode
*/
OrcidProfileDisconnectionMode getDisconnectionMode();
}

View File

@@ -16,6 +16,7 @@ import java.util.stream.Stream;
import org.dspace.app.orcid.model.OrcidTokenResponseDTO;
import org.dspace.app.orcid.service.OrcidSynchronizationService;
import org.dspace.app.profile.OrcidProfileDisconnectionMode;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
@@ -75,6 +76,27 @@ public class OrcidSynchronizationServiceImpl implements OrcidSynchronizationServ
}
@Override
public void unlinkProfile(Context context, Item profile) throws SQLException {
itemService.clearMetadata(context, profile, "person", "identifier", "orcid", Item.ANY);
itemService.clearMetadata(context, profile, "dspace", "orcid", "access-token", Item.ANY);
itemService.clearMetadata(context, profile, "dspace", "orcid", "refresh-token", Item.ANY);
itemService.clearMetadata(context, profile, "dspace", "orcid", "scope", Item.ANY);
itemService.clearMetadata(context, profile, "dspace", "orcid", "authenticated", Item.ANY);
updateItem(context, profile);
}
@Override
public OrcidProfileDisconnectionMode getDisconnectionMode() {
String value = configurationService.getProperty("orcid.disconnection.allowed-users");
if (!OrcidProfileDisconnectionMode.isValid(value)) {
return OrcidProfileDisconnectionMode.DISABLED;
}
return OrcidProfileDisconnectionMode.fromString(value);
}
private Stream<MetadataValue> getMetadataValues(Item item, String metadataField) {
return item.getMetadata().stream()
.filter(metadata -> metadataField.equals(metadata.getMetadataField().toString('.')));

View File

@@ -0,0 +1,90 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.profile;
import static java.time.LocalDateTime.now;
import static java.time.format.DateTimeFormatter.ISO_DATE_TIME;
import static org.apache.commons.collections.CollectionUtils.isNotEmpty;
import static org.dspace.content.Item.ANY;
import java.sql.SQLException;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.collections.CollectionUtils;
import org.dspace.app.profile.service.AfterResearcherProfileCreationAction;
import org.dspace.content.Item;
import org.dspace.content.MetadataFieldName;
import org.dspace.content.MetadataValue;
import org.dspace.content.service.ItemService;
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.core.Ordered;
import org.springframework.core.annotation.Order;
/**
* Implementation of {@link AfterResearcherProfileCreationAction} that copy the
* ORCID metadata, if any, from the owner to the researcher profile item.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
@Order(Ordered.HIGHEST_PRECEDENCE)
public class OrcidMetadataCopyingAction implements AfterResearcherProfileCreationAction {
@Autowired
private ItemService itemService;
@Autowired
private EPersonService ePersonService;
@Override
public void perform(Context context, ResearcherProfile researcherProfile, EPerson owner) throws SQLException {
Item item = researcherProfile.getItem();
copyMetadataValues(context, owner, "eperson.orcid", item, "person.identifier.orcid");
copyMetadataValues(context, owner, "eperson.orcid.access-token", item, "dspace.orcid.access-token");
copyMetadataValues(context, owner, "eperson.orcid.refresh-token", item, "dspace.orcid.refresh-token");
copyMetadataValues(context, owner, "eperson.orcid.scope", item, "dspace.orcid.scope");
if (isLinkedToOrcid(owner)) {
String currentDate = ISO_DATE_TIME.format(now());
itemService.setMetadataSingleValue(context, item, "dspace", "orcid", "authenticated", null, currentDate);
}
}
private void copyMetadataValues(Context context, EPerson ePerson, String ePersonMetadataField, Item item,
String itemMetadataField) throws SQLException {
List<String> values = getMetadataValues(ePerson, ePersonMetadataField);
if (CollectionUtils.isEmpty(values)) {
return;
}
MetadataFieldName metadata = new MetadataFieldName(itemMetadataField);
itemService.clearMetadata(context, item, metadata.schema, metadata.element, metadata.qualifier, ANY);
itemService.addMetadata(context, item, metadata.schema, metadata.element, metadata.qualifier, null, values);
}
private boolean isLinkedToOrcid(EPerson ePerson) {
return isNotEmpty(getMetadataValues(ePerson, "eperson.orcid"))
&& isNotEmpty(getMetadataValues(ePerson, "eperson.orcid.access-token"));
}
private List<String> getMetadataValues(EPerson ePerson, String metadataField) {
return ePersonService.getMetadataByMetadataString(ePerson, metadataField).stream()
.map(MetadataValue::getValue)
.collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,49 @@
/**
* 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.apache.commons.lang3.EnumUtils.isValidEnum;
/**
* Enum that models all the available values of the property that which
* determines which users can disconnect a profile from an ORCID account.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public enum OrcidProfileDisconnectionMode {
/**
* The disconnection is disabled.
*/
DISABLED,
/**
* Only the profile's owner can disconnect that profile from ORCID.
*/
ONLY_OWNER,
/**
* Only the admins can disconnect profiles from ORCID.
*/
ONLY_ADMIN,
/**
* Only the admin or the profile's owner can disconnect that profile from ORCID.
*/
ADMIN_AND_OWNER;
public static boolean isValid(String mode) {
return mode != null ? isValidEnum(OrcidProfileDisconnectionMode.class, mode.toUpperCase()) : false;
}
public static OrcidProfileDisconnectionMode fromString(String mode) {
return isValid(mode) ? OrcidProfileDisconnectionMode.valueOf(mode.toUpperCase()) : null;
}
}

View File

@@ -15,14 +15,17 @@ import java.io.IOException;
import java.net.URI;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import javax.annotation.PostConstruct;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.dspace.app.exception.ResourceConflictException;
import org.dspace.app.profile.service.AfterResearcherProfileCreationAction;
import org.dspace.app.profile.service.ResearcherProfileService;
import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.service.AuthorizeService;
@@ -85,6 +88,18 @@ public class ResearcherProfileServiceImpl implements ResearcherProfileService {
@Autowired
private AuthorizeService authorizeService;
@Autowired(required = false)
private List<AfterResearcherProfileCreationAction> afterCreationActions;
@PostConstruct
public void postConstruct() {
if (afterCreationActions == null) {
afterCreationActions = Collections.emptyList();
}
}
@Override
public ResearcherProfile findById(Context context, UUID id) throws SQLException, AuthorizeException {
Assert.notNull(id, "An id must be provided to find a researcher profile");
@@ -118,6 +133,10 @@ public class ResearcherProfileServiceImpl implements ResearcherProfileService {
ResearcherProfile researcherProfile = new ResearcherProfile(item);
for (AfterResearcherProfileCreationAction afterCreationAction : afterCreationActions) {
afterCreationAction.perform(context, researcherProfile, ePerson);
}
return researcherProfile;
}

View File

@@ -0,0 +1,35 @@
/**
* 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.sql.SQLException;
import org.dspace.app.profile.ResearcherProfile;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
/**
* Interface to mark classes that allow to perform additional logic on created
* researcher profile.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public interface AfterResearcherProfileCreationAction {
/**
* Perform some actions on the given researcher profile and returns the updated
* profile.
*
* @param context the DSpace context
* @param researcherProfile the created researcher profile
* @param owner the EPerson that is owner of the given profile
* @throws SQLException if a SQL error occurs
*/
void perform(Context context, ResearcherProfile researcherProfile, EPerson owner) throws SQLException;
}

View File

@@ -129,6 +129,26 @@ public class EPersonBuilder extends AbstractDSpaceObjectBuilder<EPerson> {
return this;
}
public EPersonBuilder withOrcid(final String orcid) {
setMetadataSingleValue(ePerson, "eperson", "orcid", null, orcid);
return this;
}
public EPersonBuilder withOrcidAccessToken(final String accessToken) {
setMetadataSingleValue(ePerson, "eperson", "orcid", "access-token", accessToken);
return this;
}
public EPersonBuilder withOrcidRefreshToken(final String refreshToken) {
setMetadataSingleValue(ePerson, "eperson", "orcid", "refresh-token", refreshToken);
return this;
}
public EPersonBuilder withOrcidScope(final String scope) {
addMetadataValue(ePerson, "eperson", "orcid", "scope", scope);
return this;
}
public static void deleteEPerson(UUID uuid) throws SQLException, IOException {
try (Context c = new Context()) {
c.turnOffAuthorisationSystem();

View File

@@ -0,0 +1,106 @@
/**
* 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 static org.dspace.app.profile.OrcidProfileDisconnectionMode.ADMIN_AND_OWNER;
import static org.dspace.app.profile.OrcidProfileDisconnectionMode.DISABLED;
import static org.dspace.app.profile.OrcidProfileDisconnectionMode.ONLY_ADMIN;
import static org.dspace.app.profile.OrcidProfileDisconnectionMode.ONLY_OWNER;
import java.sql.SQLException;
import org.dspace.app.orcid.service.OrcidSynchronizationService;
import org.dspace.app.profile.OrcidProfileDisconnectionMode;
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.model.patch.Operation;
import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Implementation for ResearcherProfile ORCID disconnection.
*
* Example: <code><br/>
* curl -X PATCH http://${dspace.server.url}/api/cris/profiles/<:id-eperson> -H "
* Content-Type: application/json" -d '[{ "op": "remove", "path": "/orcid" }]'
* </code>
*/
@Component
public class ResearcherProfileRemoveOrcidOperation extends PatchOperation<ResearcherProfile> {
private static final String OPERATION_ORCID = "/orcid";
@Autowired
private ResearcherProfileService profileService;
@Autowired
private OrcidSynchronizationService synchronizationService;
@Autowired
private AuthorizeService authorizeService;
@Override
public ResearcherProfile perform(Context context, ResearcherProfile profile, Operation operation)
throws SQLException {
checkProfileDisconnectionPermissions(context, profile);
synchronizationService.unlinkProfile(context, profile.getItem());
try {
return profileService.findById(context, profile.getId());
} catch (AuthorizeException e) {
throw new RESTAuthorizationException(e);
}
}
private void checkProfileDisconnectionPermissions(Context context, ResearcherProfile profile) throws SQLException {
OrcidProfileDisconnectionMode mode = synchronizationService.getDisconnectionMode();
if (mode == ADMIN_AND_OWNER) {
return;
}
if (mode == DISABLED) {
throw new RESTAuthorizationException("Profile disconnection from ORCID is disabled");
}
if (mode == ONLY_OWNER && isNotOwner(context, profile)) {
throw new RESTAuthorizationException("Only the profile's owner can perform the ORCID disconnection");
}
if (mode == ONLY_ADMIN && isNotAdmin(context)) {
throw new RESTAuthorizationException("Only admins can perform the profile disconnection from ORCID");
}
}
private boolean isNotAdmin(Context context) throws SQLException {
return !authorizeService.isAdmin(context);
}
private boolean isNotOwner(Context context, ResearcherProfile profile) {
EPerson currentUser = context.getCurrentUser();
return currentUser == null || !currentUser.getID().equals(profile.getId());
}
@Override
public boolean supports(Object objectToMatch, Operation operation) {
return objectToMatch instanceof ResearcherProfile
&& operation.getOp().trim().equalsIgnoreCase(OPERATION_REMOVE)
&& operation.getPath().trim().toLowerCase().startsWith(OPERATION_ORCID);
}
}

View File

@@ -15,7 +15,10 @@ import static org.dspace.app.rest.matcher.HalMatcher.matchLinks;
import static org.dspace.app.rest.matcher.MetadataMatcher.matchMetadata;
import static org.dspace.app.rest.matcher.MetadataMatcher.matchMetadataDoesNotExist;
import static org.dspace.app.rest.matcher.MetadataMatcher.matchMetadataNotEmpty;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
@@ -26,6 +29,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.io.UnsupportedEncodingException;
import java.sql.SQLException;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
@@ -34,6 +38,7 @@ import com.jayway.jsonpath.JsonPath;
import org.dspace.app.rest.model.MetadataValueRest;
import org.dspace.app.rest.model.patch.AddOperation;
import org.dspace.app.rest.model.patch.Operation;
import org.dspace.app.rest.model.patch.RemoveOperation;
import org.dspace.app.rest.model.patch.ReplaceOperation;
import org.dspace.app.rest.repository.ResearcherProfileRestRepository;
import org.dspace.app.rest.test.AbstractControllerIntegrationTest;
@@ -43,6 +48,8 @@ import org.dspace.builder.EPersonBuilder;
import org.dspace.builder.ItemBuilder;
import org.dspace.content.Collection;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.content.service.ItemService;
import org.dspace.eperson.EPerson;
import org.dspace.services.ConfigurationService;
import org.junit.Test;
@@ -61,6 +68,9 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra
@Autowired
private ConfigurationService configurationService;
@Autowired
private ItemService itemService;
private EPerson user;
private EPerson anotherUser;
@@ -1035,15 +1045,525 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra
.andExpect(status().isConflict());
}
private String getItemIdByProfileId(String token, String id) throws Exception {
MvcResult result = getClient(token).perform(get("/api/eperson/profiles/{id}/item", id))
@Test
public void testOwnerPatchToDisconnectProfileFromOrcidWithDisabledConfiguration() throws Exception {
configurationService.setProperty("orcid.disconnection.allowed-users", "disabled");
context.turnOffAuthorisationSystem();
EPerson ePerson = EPersonBuilder.createEPerson(context)
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withOrcidAccessToken("3de2e370-8aa9-4bbe-8d7e-f5b1577bdad4")
.withOrcidRefreshToken("6b29a03d-f494-4690-889f-2c0ddf26b82d")
.withOrcidScope("/read")
.withOrcidScope("/write")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.build();
Item profile = createProfile(ePerson);
context.restoreAuthSystemState();
getClient(getAuthToken(ePerson.getEmail(), password))
.perform(patch("/api/eperson/profiles/{id}", ePerson.getID().toString())
.content(getPatchContent(asList(new RemoveOperation("/orcid"))))
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isForbidden());
profile = context.reloadEntity(profile);
assertThat(getMetadataValues(profile, "person.identifier.orcid"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.access-token"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.refresh-token"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.scope"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.authenticated"), not(empty()));
}
@Test
public void testAdminPatchToDisconnectProfileFromOrcidWithDisabledConfiguration() throws Exception {
configurationService.setProperty("orcid.disconnection.allowed-users", null);
context.turnOffAuthorisationSystem();
EPerson ePerson = EPersonBuilder.createEPerson(context)
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withOrcidAccessToken("3de2e370-8aa9-4bbe-8d7e-f5b1577bdad4")
.withOrcidRefreshToken("6b29a03d-f494-4690-889f-2c0ddf26b82d")
.withOrcidScope("/read")
.withOrcidScope("/write")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.build();
Item profile = createProfile(ePerson);
context.restoreAuthSystemState();
getClient(getAuthToken(admin.getEmail(), password))
.perform(patch("/api/eperson/profiles/{id}", ePerson.getID().toString())
.content(getPatchContent(asList(new RemoveOperation("/orcid"))))
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isForbidden());
profile = context.reloadEntity(profile);
assertThat(getMetadataValues(profile, "person.identifier.orcid"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.access-token"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.refresh-token"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.scope"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.authenticated"), not(empty()));
}
@Test
public void testAnotherUserPatchToDisconnectProfileFromOrcidWithDisabledConfiguration() throws Exception {
configurationService.setProperty("orcid.disconnection.allowed-users", "");
context.turnOffAuthorisationSystem();
EPerson ePerson = EPersonBuilder.createEPerson(context)
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withOrcidAccessToken("3de2e370-8aa9-4bbe-8d7e-f5b1577bdad4")
.withOrcidRefreshToken("6b29a03d-f494-4690-889f-2c0ddf26b82d")
.withOrcidScope("/read")
.withOrcidScope("/write")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.build();
EPerson anotherUser = EPersonBuilder.createEPerson(context)
.withCanLogin(true)
.withEmail("user@email.it")
.withPassword(password)
.withNameInMetadata("Another", "User")
.build();
Item profile = createProfile(ePerson);
context.restoreAuthSystemState();
getClient(getAuthToken(anotherUser.getEmail(), password))
.perform(patch("/api/eperson/profiles/{id}", ePerson.getID().toString())
.content(getPatchContent(asList(new RemoveOperation("/orcid"))))
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isForbidden());
profile = context.reloadEntity(profile);
assertThat(getMetadataValues(profile, "person.identifier.orcid"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.access-token"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.refresh-token"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.scope"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.authenticated"), not(empty()));
}
@Test
public void testOwnerPatchToDisconnectProfileFromOrcidWithOnlyOwnerConfiguration() throws Exception {
configurationService.setProperty("orcid.disconnection.allowed-users", "only_owner");
context.turnOffAuthorisationSystem();
EPerson ePerson = EPersonBuilder.createEPerson(context)
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withOrcidAccessToken("3de2e370-8aa9-4bbe-8d7e-f5b1577bdad4")
.withOrcidRefreshToken("6b29a03d-f494-4690-889f-2c0ddf26b82d")
.withOrcidScope("/read")
.withOrcidScope("/write")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.build();
Item profile = createProfile(ePerson);
context.restoreAuthSystemState();
getClient(getAuthToken(ePerson.getEmail(), password))
.perform(patch("/api/eperson/profiles/{id}", ePerson.getID().toString())
.content(getPatchContent(asList(new RemoveOperation("/orcid"))))
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isOk())
.andReturn();
.andExpect(jsonPath("$.id", is(ePerson.getID().toString())))
.andExpect(jsonPath("$.visible", is(false)))
.andExpect(jsonPath("$.type", is("profile")))
.andExpect(jsonPath("$.orcid").doesNotExist())
.andExpect(jsonPath("$.orcidSynchronization").doesNotExist());
profile = context.reloadEntity(profile);
assertThat(getMetadataValues(profile, "person.identifier.orcid"), empty());
assertThat(getMetadataValues(profile, "dspace.orcid.access-token"), empty());
assertThat(getMetadataValues(profile, "dspace.orcid.refresh-token"), empty());
assertThat(getMetadataValues(profile, "dspace.orcid.scope"), empty());
assertThat(getMetadataValues(profile, "dspace.orcid.authenticated"), empty());
}
@Test
public void testAdminPatchToDisconnectProfileFromOrcidWithOnlyOwnerConfiguration() throws Exception {
configurationService.setProperty("orcid.disconnection.allowed-users", "only_owner");
context.turnOffAuthorisationSystem();
EPerson ePerson = EPersonBuilder.createEPerson(context)
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withOrcidAccessToken("3de2e370-8aa9-4bbe-8d7e-f5b1577bdad4")
.withOrcidRefreshToken("6b29a03d-f494-4690-889f-2c0ddf26b82d")
.withOrcidScope("/read")
.withOrcidScope("/write")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.build();
Item profile = createProfile(ePerson);
context.restoreAuthSystemState();
getClient(getAuthToken(admin.getEmail(), password))
.perform(patch("/api/eperson/profiles/{id}", ePerson.getID().toString())
.content(getPatchContent(asList(new RemoveOperation("/orcid"))))
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isForbidden());
profile = context.reloadEntity(profile);
assertThat(getMetadataValues(profile, "person.identifier.orcid"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.access-token"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.refresh-token"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.scope"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.authenticated"), not(empty()));
}
@Test
public void testAnotherUserPatchToDisconnectProfileFromOrcidWithOnlyOwnerConfiguration() throws Exception {
configurationService.setProperty("orcid.disconnection.allowed-users", "admin_and_owner");
context.turnOffAuthorisationSystem();
EPerson ePerson = EPersonBuilder.createEPerson(context)
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withOrcidAccessToken("3de2e370-8aa9-4bbe-8d7e-f5b1577bdad4")
.withOrcidRefreshToken("6b29a03d-f494-4690-889f-2c0ddf26b82d")
.withOrcidScope("/read")
.withOrcidScope("/write")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.build();
Item profile = createProfile(ePerson);
context.restoreAuthSystemState();
getClient(getAuthToken(anotherUser.getEmail(), password))
.perform(patch("/api/eperson/profiles/{id}", ePerson.getID().toString())
.content(getPatchContent(asList(new RemoveOperation("/orcid"))))
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isForbidden());
profile = context.reloadEntity(profile);
assertThat(getMetadataValues(profile, "person.identifier.orcid"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.access-token"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.refresh-token"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.scope"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.authenticated"), not(empty()));
}
@Test
public void testOwnerPatchToDisconnectProfileFromOrcidWithOnlyAdminConfiguration() throws Exception {
configurationService.setProperty("orcid.disconnection.allowed-users", "only_admin");
context.turnOffAuthorisationSystem();
EPerson ePerson = EPersonBuilder.createEPerson(context)
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withOrcidAccessToken("3de2e370-8aa9-4bbe-8d7e-f5b1577bdad4")
.withOrcidRefreshToken("6b29a03d-f494-4690-889f-2c0ddf26b82d")
.withOrcidScope("/read")
.withOrcidScope("/write")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.build();
Item profile = createProfile(ePerson);
context.restoreAuthSystemState();
getClient(getAuthToken(ePerson.getEmail(), password))
.perform(patch("/api/eperson/profiles/{id}", ePerson.getID().toString())
.content(getPatchContent(asList(new RemoveOperation("/orcid"))))
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isForbidden());
profile = context.reloadEntity(profile);
assertThat(getMetadataValues(profile, "person.identifier.orcid"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.access-token"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.refresh-token"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.scope"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.authenticated"), not(empty()));
}
@Test
public void testAdminPatchToDisconnectProfileFromOrcidWithOnlyAdminConfiguration() throws Exception {
configurationService.setProperty("orcid.disconnection.allowed-users", "only_admin");
context.turnOffAuthorisationSystem();
EPerson ePerson = EPersonBuilder.createEPerson(context)
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withOrcidAccessToken("3de2e370-8aa9-4bbe-8d7e-f5b1577bdad4")
.withOrcidRefreshToken("6b29a03d-f494-4690-889f-2c0ddf26b82d")
.withOrcidScope("/read")
.withOrcidScope("/write")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.build();
Item profile = createProfile(ePerson);
context.restoreAuthSystemState();
getClient(getAuthToken(admin.getEmail(), password))
.perform(patch("/api/eperson/profiles/{id}", ePerson.getID().toString())
.content(getPatchContent(asList(new RemoveOperation("/orcid"))))
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id", is(ePerson.getID().toString())))
.andExpect(jsonPath("$.visible", is(false)))
.andExpect(jsonPath("$.type", is("profile")))
.andExpect(jsonPath("$.orcid").doesNotExist())
.andExpect(jsonPath("$.orcidSynchronization").doesNotExist());
profile = context.reloadEntity(profile);
assertThat(getMetadataValues(profile, "person.identifier.orcid"), empty());
assertThat(getMetadataValues(profile, "dspace.orcid.access-token"), empty());
assertThat(getMetadataValues(profile, "dspace.orcid.refresh-token"), empty());
assertThat(getMetadataValues(profile, "dspace.orcid.scope"), empty());
assertThat(getMetadataValues(profile, "dspace.orcid.authenticated"), empty());
}
@Test
public void testAnotherUserPatchToDisconnectProfileFromOrcidWithOnlyAdminConfiguration() throws Exception {
configurationService.setProperty("orcid.disconnection.allowed-users", "only_admin");
context.turnOffAuthorisationSystem();
EPerson ePerson = EPersonBuilder.createEPerson(context)
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withOrcidAccessToken("3de2e370-8aa9-4bbe-8d7e-f5b1577bdad4")
.withOrcidRefreshToken("6b29a03d-f494-4690-889f-2c0ddf26b82d")
.withOrcidScope("/read")
.withOrcidScope("/write")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.build();
Item profile = createProfile(ePerson);
context.restoreAuthSystemState();
getClient(getAuthToken(anotherUser.getEmail(), password))
.perform(patch("/api/eperson/profiles/{id}", ePerson.getID().toString())
.content(getPatchContent(asList(new RemoveOperation("/orcid"))))
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isForbidden());
profile = context.reloadEntity(profile);
assertThat(getMetadataValues(profile, "person.identifier.orcid"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.access-token"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.refresh-token"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.scope"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.authenticated"), not(empty()));
}
@Test
public void testOwnerPatchToDisconnectProfileFromOrcidWithAdminAndOwnerConfiguration() throws Exception {
configurationService.setProperty("orcid.disconnection.allowed-users", "admin_and_owner");
context.turnOffAuthorisationSystem();
EPerson ePerson = EPersonBuilder.createEPerson(context)
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withOrcidAccessToken("3de2e370-8aa9-4bbe-8d7e-f5b1577bdad4")
.withOrcidRefreshToken("6b29a03d-f494-4690-889f-2c0ddf26b82d")
.withOrcidScope("/read")
.withOrcidScope("/write")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.build();
Item profile = createProfile(ePerson);
context.restoreAuthSystemState();
getClient(getAuthToken(ePerson.getEmail(), password))
.perform(patch("/api/eperson/profiles/{id}", ePerson.getID().toString())
.content(getPatchContent(asList(new RemoveOperation("/orcid"))))
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id", is(ePerson.getID().toString())))
.andExpect(jsonPath("$.visible", is(false)))
.andExpect(jsonPath("$.type", is("profile")))
.andExpect(jsonPath("$.orcid").doesNotExist())
.andExpect(jsonPath("$.orcidSynchronization").doesNotExist());
profile = context.reloadEntity(profile);
assertThat(getMetadataValues(profile, "person.identifier.orcid"), empty());
assertThat(getMetadataValues(profile, "dspace.orcid.access-token"), empty());
assertThat(getMetadataValues(profile, "dspace.orcid.refresh-token"), empty());
assertThat(getMetadataValues(profile, "dspace.orcid.scope"), empty());
assertThat(getMetadataValues(profile, "dspace.orcid.authenticated"), empty());
}
@Test
public void testAdminPatchToDisconnectProfileFromOrcidWithAdminAndOwnerConfiguration() throws Exception {
configurationService.setProperty("orcid.disconnection.allowed-users", "admin_and_owner");
context.turnOffAuthorisationSystem();
EPerson ePerson = EPersonBuilder.createEPerson(context)
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withOrcidAccessToken("3de2e370-8aa9-4bbe-8d7e-f5b1577bdad4")
.withOrcidRefreshToken("6b29a03d-f494-4690-889f-2c0ddf26b82d")
.withOrcidScope("/read")
.withOrcidScope("/write")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.build();
Item profile = createProfile(ePerson);
context.restoreAuthSystemState();
getClient(getAuthToken(admin.getEmail(), password))
.perform(patch("/api/eperson/profiles/{id}", ePerson.getID().toString())
.content(getPatchContent(asList(new RemoveOperation("/orcid"))))
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id", is(ePerson.getID().toString())))
.andExpect(jsonPath("$.visible", is(false)))
.andExpect(jsonPath("$.type", is("profile")))
.andExpect(jsonPath("$.orcid").doesNotExist())
.andExpect(jsonPath("$.orcidSynchronization").doesNotExist());
profile = context.reloadEntity(profile);
assertThat(getMetadataValues(profile, "person.identifier.orcid"), empty());
assertThat(getMetadataValues(profile, "dspace.orcid.access-token"), empty());
assertThat(getMetadataValues(profile, "dspace.orcid.refresh-token"), empty());
assertThat(getMetadataValues(profile, "dspace.orcid.scope"), empty());
assertThat(getMetadataValues(profile, "dspace.orcid.authenticated"), empty());
}
@Test
public void testAnotherUserPatchToDisconnectProfileFromOrcidWithAdminAndOwnerConfiguration() throws Exception {
configurationService.setProperty("orcid.disconnection.allowed-users", "admin_and_owner");
context.turnOffAuthorisationSystem();
EPerson ePerson = EPersonBuilder.createEPerson(context)
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withOrcidAccessToken("3de2e370-8aa9-4bbe-8d7e-f5b1577bdad4")
.withOrcidRefreshToken("6b29a03d-f494-4690-889f-2c0ddf26b82d")
.withOrcidScope("/read")
.withOrcidScope("/write")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.build();
Item profile = createProfile(ePerson);
context.restoreAuthSystemState();
getClient(getAuthToken(anotherUser.getEmail(), password))
.perform(patch("/api/eperson/profiles/{id}", ePerson.getID().toString())
.content(getPatchContent(asList(new RemoveOperation("/orcid"))))
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isForbidden());
profile = context.reloadEntity(profile);
assertThat(getMetadataValues(profile, "person.identifier.orcid"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.access-token"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.refresh-token"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.scope"), not(empty()));
assertThat(getMetadataValues(profile, "dspace.orcid.authenticated"), not(empty()));
}
private Item createProfile(EPerson ePerson) throws Exception {
String authToken = getAuthToken(ePerson.getEmail(), password);
AtomicReference<UUID> ePersonIdRef = new AtomicReference<UUID>();
AtomicReference<UUID> itemIdRef = new AtomicReference<UUID>();
getClient(authToken).perform(post("/api/eperson/profiles/")
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isCreated())
.andDo(result -> ePersonIdRef.set(fromString(read(result.getResponse().getContentAsString(),
"$.id"))));
getClient(authToken).perform(get("/api/eperson/profiles/{id}/item", ePersonIdRef.get())
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isOk())
.andDo(result -> itemIdRef.set(fromString(read(result.getResponse().getContentAsString(),
"$.id"))));
return itemService.find(context, itemIdRef.get());
}
private String getItemIdByProfileId(String token, String id) throws SQLException, Exception {
MvcResult result = getClient(token).perform(get("/api/eperson/profiles/{id}/item", id))
.andExpect(status().isOk())
.andReturn();
return readAttributeFromResponse(result, "$.id");
}
private List<MetadataValue> getMetadataValues(Item item, String metadataField) {
return itemService.getMetadataByMetadataString(item, metadataField);
}
private <T> T readAttributeFromResponse(MvcResult result, String attribute) throws UnsupportedEncodingException {
return JsonPath.read(result.getResponse().getContentAsString(), attribute);
}
}

View File

@@ -1586,21 +1586,6 @@ request.item.helpdesk.override = false
# ${dspace.dir}/config/ directory.
module_dir = modules
#------------------------------------------------------------------#
#--------------------ORCID CONFIGURATIONS--------------------------#
#------------------------------------------------------------------#
orcid.domain-url= https://sandbox.orcid.org
orcid.authorize-url = ${orcid.domain-url}/oauth/authorize
orcid.token-url = ${orcid.domain-url}/oauth/token
orcid.api-url = https://api.sandbox.orcid.org/v3.0
orcid.redirect-url = ${dspace.server.url}/api/authn/orcid
orcid.application-client-id =
orcid.application-client-secret =
orcid.scope = /authenticate
orcid.scope = /read-limited
orcid.scope = /activities/update
orcid.scope = /person/update
# Load default module configs
# ----------------------------
@@ -1644,4 +1629,5 @@ include = ${module_dir}/translator.cfg
include = ${module_dir}/usage-statistics.cfg
include = ${module_dir}/versioning.cfg
include = ${module_dir}/workflow.cfg
include = ${module_dir}/authority.cfg
include = ${module_dir}/authority.cfg
include = ${module_dir}/orcid.cfg

View File

@@ -0,0 +1,24 @@
#------------------------------------------------------------------#
#--------------------ORCID GENERIC CONFIGURATIONS------------------#
#------------------------------------------------------------------#
#Allowed values are disabled, only_admin, only_owner or admin_and_owner
orcid.disconnection.allowed-users = admin_and_owner
#------------------------------------------------------------------#
#--------------------ORCID CLIENT CONFIGURATIONS-------------------#
#------------------------------------------------------------------#
orcid.domain-url= https://sandbox.orcid.org
orcid.authorize-url = ${orcid.domain-url}/oauth/authorize
orcid.token-url = ${orcid.domain-url}/oauth/token
orcid.api-url = https://api.sandbox.orcid.org/v3.0
orcid.public-url = https://pub.sandbox.orcid.org/v3.0
orcid.redirect-url = ${dspace.server.url}/api/authn/orcid
orcid.webhook-url = https://api.sandbox.orcid.org/
orcid.application-client-id =
orcid.application-client-secret =
orcid.scope = /authenticate
orcid.scope = /read-limited
orcid.scope = /activities/update
orcid.scope = /person/update

View File

@@ -65,6 +65,8 @@
<bean class="org.dspace.app.profile.ResearcherProfileServiceImpl"/>
<bean class="org.dspace.app.profile.OrcidMetadataCopyingAction"/>
<bean class='org.dspace.service.impl.HttpConnectionPoolService'
id='solrHttpConnectionPoolService'
scope='singleton'