From d8ca94d304c831d6b11240da7d9ddb2df671848b Mon Sep 17 00:00:00 2001 From: Alexander Sulfrian Date: Wed, 27 Mar 2019 17:26:41 +0100 Subject: [PATCH 001/143] IPMatcher: Fix netmask conversion --- .../java/org/dspace/authenticate/IPMatcher.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/authenticate/IPMatcher.java b/dspace-api/src/main/java/org/dspace/authenticate/IPMatcher.java index 955b6c86d3..439e53af1d 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/IPMatcher.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/IPMatcher.java @@ -87,13 +87,16 @@ public class IPMatcher { + ipSpec); } - int maskBytes = maskBits / 8; - for (int i = 0; i < maskBytes; i++) { - netmask[i] = (byte) 0Xff; - } - netmask[maskBytes] = (byte) ((byte) 0Xff << 8 - (maskBits % 8)); // FIXME test! - for (int i = maskBytes + 1; i < (128 / 8); i++) { - netmask[i] = 0; + for (int i = 0; i < netmask.length; i++) { + if (maskBits <= 0) { + netmask[i] = 0; + } else if (maskBits > 8) { + netmask[i] = (byte) 0Xff; + } else { + netmask[i] = (byte) ((byte) 0Xff << 8 - maskBits); + } + + maskBits = maskBits - 8; } break; case 1: // No explicit mask: fill the mask with 1s From 8b1a75d309e0e2e5bddad1eebb8258c1679fd742 Mon Sep 17 00:00:00 2001 From: Alexander Sulfrian Date: Mon, 1 Apr 2019 18:49:27 +0200 Subject: [PATCH 002/143] IPMatcher: Add test to verify parsing of netmask --- .../test/java/org/dspace/authenticate/IPMatcherTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dspace-api/src/test/java/org/dspace/authenticate/IPMatcherTest.java b/dspace-api/src/test/java/org/dspace/authenticate/IPMatcherTest.java index 3f5a08a648..31ca92e02a 100644 --- a/dspace-api/src/test/java/org/dspace/authenticate/IPMatcherTest.java +++ b/dspace-api/src/test/java/org/dspace/authenticate/IPMatcherTest.java @@ -153,6 +153,14 @@ public class IPMatcherTest { assertFalse(ipMatcher.match("0:0:0:0:0:0:0:1")); } + @Test + public void testIPv6FullMaskMatching() throws Exception { + final IPMatcher ipMatcher = new IPMatcher("::2/128"); + + assertTrue(ipMatcher.match("0:0:0:0:0:0:0:2")); + assertFalse(ipMatcher.match("0:0:0:0:0:0:0:1")); + } + @Test public void testAsteriskMatchingSuccess() throws Exception { From 003daf0a054932e600569966be938ef8844bec1b Mon Sep 17 00:00:00 2001 From: xuejiangtao <779480193@qq.com> Date: Wed, 15 May 2019 23:26:02 +0800 Subject: [PATCH 003/143] remove the condition that is always 'true' --- .../src/main/java/org/dspace/app/util/IndexVersion.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/util/IndexVersion.java b/dspace-api/src/main/java/org/dspace/app/util/IndexVersion.java index d8b2d6868a..a01c8af616 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/IndexVersion.java +++ b/dspace-api/src/main/java/org/dspace/app/util/IndexVersion.java @@ -252,10 +252,6 @@ public class IndexVersion { return GREATER_THAN; } else if (firstMinor < secondMinor) { return LESS_THAN; - } else { - // This is an impossible scenario. - // This 'else' should never be triggered since we've checked for equality above already - return EQUAL; } } From 6ad6186375d596014530fc502e8d721eb197d543 Mon Sep 17 00:00:00 2001 From: xuejiangtao <779480193@qq.com> Date: Mon, 20 May 2019 22:54:45 +0800 Subject: [PATCH 004/143] add default return statement --- dspace-api/src/main/java/org/dspace/app/util/IndexVersion.java | 1 + 1 file changed, 1 insertion(+) diff --git a/dspace-api/src/main/java/org/dspace/app/util/IndexVersion.java b/dspace-api/src/main/java/org/dspace/app/util/IndexVersion.java index a01c8af616..0c4e475339 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/IndexVersion.java +++ b/dspace-api/src/main/java/org/dspace/app/util/IndexVersion.java @@ -253,6 +253,7 @@ public class IndexVersion { } else if (firstMinor < secondMinor) { return LESS_THAN; } + return EQUAL; } /** From 9766fe131d241bf55695b7b8768d5a33ac62940b Mon Sep 17 00:00:00 2001 From: xuejiangtao <779480193@qq.com> Date: Tue, 21 May 2019 21:52:36 +0800 Subject: [PATCH 005/143] remove the last if statement to avoid the dead code --- dspace-api/src/main/java/org/dspace/app/util/IndexVersion.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/util/IndexVersion.java b/dspace-api/src/main/java/org/dspace/app/util/IndexVersion.java index 0c4e475339..7bdaa95b5c 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/IndexVersion.java +++ b/dspace-api/src/main/java/org/dspace/app/util/IndexVersion.java @@ -250,10 +250,9 @@ public class IndexVersion { } else if (firstMinor > secondMinor) { // If we get here, major versions must be EQUAL. Now, time to check our minor versions return GREATER_THAN; - } else if (firstMinor < secondMinor) { + } else { return LESS_THAN; } - return EQUAL; } /** From 5d714a174ee9c36e82baa2249df85b46357a0c08 Mon Sep 17 00:00:00 2001 From: nicholas Date: Wed, 1 Apr 2020 09:22:29 -0500 Subject: [PATCH 006/143] include limit and offset when retrieving items for a collection --- .../org/dspace/rest/CollectionsResource.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dspace-rest/src/main/java/org/dspace/rest/CollectionsResource.java b/dspace-rest/src/main/java/org/dspace/rest/CollectionsResource.java index af06792b7b..66919ad5c7 100644 --- a/dspace-rest/src/main/java/org/dspace/rest/CollectionsResource.java +++ b/dspace-rest/src/main/java/org/dspace/rest/CollectionsResource.java @@ -274,16 +274,16 @@ public class CollectionsResource extends Resource { headers, request, context); items = new ArrayList(); - Iterator dspaceItems = itemService.findByCollection(context, dspaceCollection); - for (int i = 0; (dspaceItems.hasNext()) && (i < (limit + offset)); i++) { + Iterator dspaceItems = itemService.findByCollection(context, dspaceCollection, + limit, offset); + + while (dspaceItems.hasNext()) { org.dspace.content.Item dspaceItem = dspaceItems.next(); - if (i >= offset) { - if (itemService.isItemListedForUser(context, dspaceItem)) { - items.add(new Item(dspaceItem, servletContext, expand, context)); - writeStats(dspaceItem, UsageEvent.Action.VIEW, user_ip, user_agent, xforwardedfor, - headers, request, context); - } + if (itemService.isItemListedForUser(context, dspaceItem)) { + items.add(new Item(dspaceItem, servletContext, expand, context)); + writeStats(dspaceItem, UsageEvent.Action.VIEW, user_ip, user_agent, xforwardedfor, + headers, request, context); } } From 6e0396e6ad33cedbe81adddf0e146887abaea4d9 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Wed, 8 Apr 2020 15:14:18 +0200 Subject: [PATCH 007/143] 70213: Initial findAll endpoint & backend --- .../java/org/dspace/license/CCLicense.java | 26 ++- .../license/CCLicenseConnectorService.java | 26 +++ .../CCLicenseConnectorServiceImpl.java | 216 ++++++++++++++++++ .../org/dspace/license/CCLicenseField.java | 25 +- .../dspace/license/CCLicenseFieldEnum.java | 82 +++++++ .../java/org/dspace/license/CCLookup.java | 50 ++-- .../license/CreativeCommonsServiceImpl.java | 68 ++++-- .../service/CreativeCommonsService.java | 17 ++ .../MockCCLicenseConnectorServiceImpl.java | 72 ++++++ .../SubmissionCCLicenseConverter.java | 59 +++++ .../SubmissionCCLicenseFieldConverter.java | 61 +++++ ...SubmissionCCLicenseFieldEnumConverter.java | 45 ++++ .../SubmissionCCLicenseFieldEnumRest.java | 44 ++++ .../model/SubmissionCCLicenseFieldRest.java | 59 +++++ .../rest/model/SubmissionCCLicenseRest.java | 73 ++++++ .../hateoas/SubmissionCCLicenseResource.java | 23 ++ .../SubmissionCCLicenseRestRepository.java | 44 ++++ .../config/spring/api/core-services-mock.xml | 1 + .../SubmissionCCLicenseRestRepositoryIT.java | 46 ++++ .../matcher/SubmissionCCLicenseMatcher.java | 82 +++++++ .../MockCCLicenseConnectorServiceImpl.java | 72 ++++++ dspace/config/spring/api/core-services.xml | 1 + 22 files changed, 1121 insertions(+), 71 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorService.java create mode 100644 dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/license/CCLicenseFieldEnum.java create mode 100644 dspace-api/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseConverter.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseFieldConverter.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseFieldEnumConverter.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseFieldEnumRest.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseFieldRest.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseRest.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SubmissionCCLicenseResource.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionCCLicenseRestRepository.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseRestRepositoryIT.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SubmissionCCLicenseMatcher.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java diff --git a/dspace-api/src/main/java/org/dspace/license/CCLicense.java b/dspace-api/src/main/java/org/dspace/license/CCLicense.java index b015e3a9d3..d5d9fe14a2 100644 --- a/dspace-api/src/main/java/org/dspace/license/CCLicense.java +++ b/dspace-api/src/main/java/org/dspace/license/CCLicense.java @@ -8,6 +8,8 @@ package org.dspace.license; +import java.util.List; + /** * @author wbossons */ @@ -15,17 +17,17 @@ public class CCLicense { private String licenseName; private String licenseId; - private int order = 0; + private List ccLicenseFieldList; public CCLicense() { super(); } - public CCLicense(String licenseId, String licenseName, int order) { + public CCLicense(String licenseId, String licenseName, List ccLicenseFieldList) { super(); this.licenseId = licenseId; this.licenseName = licenseName; - this.order = order; + this.ccLicenseFieldList = ccLicenseFieldList; } public String getLicenseName() { @@ -44,13 +46,19 @@ public class CCLicense { this.licenseId = licenseId; } - public int getOrder() { - return this.order; + /** + * Gets the list of CC License Fields + * @return the list of CC License Fields + */ + public List getCcLicenseFieldList() { + return ccLicenseFieldList; } - public void setOrder(int order) { - this.order = order; + /** + * Sets the list of CC License Fields + * @param ccLicenseFieldList + */ + public void setCcLicenseFieldList(final List ccLicenseFieldList) { + this.ccLicenseFieldList = ccLicenseFieldList; } - - } diff --git a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorService.java b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorService.java new file mode 100644 index 0000000000..52bbf39cc7 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorService.java @@ -0,0 +1,26 @@ +/** + * 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.license; + +import java.util.List; + +/** + * Service interface class for the Creative commons license connector service. + * The implementation of this class is responsible for all the calls to the CC license API and parsing the response + * The service is autowired by spring + */ +public interface CCLicenseConnectorService { + + /** + * Retrieves the CC Licenses for the provided language from the CC License API + * @param language - the language to retrieve the licenses for + * @return a list of licenses obtained for the provided languages + */ + public List retrieveLicenses(String language); + +} diff --git a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java new file mode 100644 index 0000000000..edc9934694 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java @@ -0,0 +1,216 @@ +/** + * 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.license; + +import java.io.IOException; +import java.io.StringReader; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.util.EntityUtils; +import org.apache.logging.log4j.Logger; +import org.dspace.services.ConfigurationService; +import org.jaxen.JaxenException; +import org.jaxen.jdom.JDOMXPath; +import org.jdom.Attribute; +import org.jdom.Element; +import org.jdom.JDOMException; +import org.jdom.input.SAXBuilder; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.xml.sax.InputSource; + +/** + * Implementation for the Creative commons license connector service. + * This class is responsible for all the calls to the CC license API and parsing the response + */ +public class CCLicenseConnectorServiceImpl implements CCLicenseConnectorService, InitializingBean { + + private Logger log = org.apache.logging.log4j.LogManager.getLogger(CCLicenseConnectorServiceImpl.class); + + private CloseableHttpClient client; + private SAXBuilder parser = new SAXBuilder(); + + + @Autowired + private ConfigurationService configurationService; + + @Override + public void afterPropertiesSet() throws Exception { + HttpClientBuilder builder = HttpClientBuilder.create(); + + client = builder + .disableAutomaticRetries() + .setMaxConnTotal(5) + .build(); + } + + /** + * Retrieves the CC Licenses for the provided language from the CC License API + * @param language - the language to retrieve the licenses for + * @return a list of licenses obtained for the provided languages + */ + public List retrieveLicenses(String language) { + String ccLicenseUrl = configurationService.getProperty("cc.api.rooturl"); + + HttpGet httpGet = new HttpGet(ccLicenseUrl + "/?locale=" + language); + + List licenses; + try (CloseableHttpResponse response = client.execute(httpGet)) { + licenses = retrieveLicenses(response); + } catch (JDOMException | JaxenException | IOException e) { + log.error(e); + licenses = Collections.emptyList(); + } + + List ccLicenses = new LinkedList<>(); + + for (String license : licenses) { + + HttpGet licenseHttpGet = new HttpGet(ccLicenseUrl + "/license/" + license); + try (CloseableHttpResponse response = client.execute(licenseHttpGet)) { + CCLicense ccLicense = retrieveLicenseObject(response); + ccLicenses.add(ccLicense); + } catch (JaxenException | JDOMException | IOException e) { + log.error(e); + } + } + + return ccLicenses; + } + + /** + * Retrieve the list of licenses from the response from the CC License API and remove the licenses configured + * to be excluded + * @param response The response from the API + * @return a list of license identifiers for which details need to be retrieved + * @throws IOException + * @throws JaxenException + * @throws JDOMException + */ + private List retrieveLicenses(CloseableHttpResponse response) + throws IOException, JaxenException, JDOMException { + + List domains = new LinkedList<>(); + String[] excludedLicenses = configurationService.getArrayProperty("cc.license.classfilter"); + + + String responseString = EntityUtils.toString(response.getEntity()); + JDOMXPath licenseClassXpath = new JDOMXPath("//licenses/license"); + + + InputSource is = new InputSource(new StringReader(responseString)); + org.jdom.Document classDoc = this.parser.build(is); + + List elements = licenseClassXpath.selectNodes(classDoc); + for (Element element : elements) { + String licenseId = getSingleNodeValue(element, "@id"); + if (StringUtils.isNotBlank(licenseId) && !ArrayUtils.contains(excludedLicenses, licenseId)) { + domains.add(licenseId); + } + } + + return domains; + + } + + /** + * Parse the response for a single CC License and return the corresponding CC License Object + * @param response for a specific CC License response + * @return the corresponding CC License Object + * @throws IOException + * @throws JaxenException + * @throws JDOMException + */ + private CCLicense retrieveLicenseObject(CloseableHttpResponse response) + throws IOException, JaxenException, JDOMException { + + String responseString = EntityUtils.toString(response.getEntity()); + + + JDOMXPath licenseClassXpath = new JDOMXPath("//licenseclass"); + JDOMXPath licenseFieldXpath = new JDOMXPath("field"); + + + InputSource is; + + is = new InputSource(new StringReader(responseString)); + + org.jdom.Document classDoc = this.parser.build(is); + + Object element = licenseClassXpath.selectSingleNode(classDoc); + String licenseId = getSingleNodeValue(element, "@id"); + String licenseLabel = getSingleNodeValue(element, "label"); + + List ccLicenseFields = new LinkedList<>(); + + List licenseFields = licenseFieldXpath.selectNodes(element); + for (Element licenseField : licenseFields) { + CCLicenseField ccLicenseField = parseLicenseField(licenseField); + ccLicenseFields.add(ccLicenseField); + } + + + return new CCLicense(licenseId, licenseLabel, ccLicenseFields); + } + + private CCLicenseField parseLicenseField(final Element licenseField) throws JaxenException { + String id = getSingleNodeValue(licenseField, "@id"); + String label = getSingleNodeValue(licenseField, "label"); + String description = getSingleNodeValue(licenseField, "description"); + + JDOMXPath enumXpath = new JDOMXPath("enum"); + List enums = enumXpath.selectNodes(licenseField); + + List ccLicenseFieldEnumList = new LinkedList<>(); + + for (Element enumElement : enums) { + CCLicenseFieldEnum ccLicenseFieldEnum = parseEnum(enumElement); + ccLicenseFieldEnumList.add(ccLicenseFieldEnum); + } + + return new CCLicenseField(id, label, description, ccLicenseFieldEnumList); + + } + + private CCLicenseFieldEnum parseEnum(final Element enumElement) throws JaxenException { + String id = getSingleNodeValue(enumElement, "@id"); + String label = getSingleNodeValue(enumElement, "label"); + String description = getSingleNodeValue(enumElement, "description"); + + return new CCLicenseFieldEnum(id, label, description); + } + + + private String getNodeValue(final Object el) { + if (el instanceof Element) { + return ((Element) el).getValue(); + } else if (el instanceof Attribute) { + return ((Attribute) el).getValue(); + } else if (el instanceof String) { + return (String) el; + } else { + return null; + } + } + + private String getSingleNodeValue(final Object t, String query) throws JaxenException { + JDOMXPath xpath = new JDOMXPath(query); + Object singleNode = xpath.selectSingleNode(t); + + return getNodeValue(singleNode); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/license/CCLicenseField.java b/dspace-api/src/main/java/org/dspace/license/CCLicenseField.java index 6360249f65..8fb6de5478 100644 --- a/dspace-api/src/main/java/org/dspace/license/CCLicenseField.java +++ b/dspace-api/src/main/java/org/dspace/license/CCLicenseField.java @@ -7,8 +7,7 @@ */ package org.dspace.license; -import java.util.HashMap; -import java.util.Map; +import java.util.List; /** * Wrapper class for representation of a license field declaration. @@ -22,7 +21,7 @@ public class CCLicenseField { private String description = ""; private String type = ""; - private HashMap fieldEnum = null; + private List fieldEnum = null; /** * Construct a new LicenseField class. Note that after construction, @@ -31,13 +30,11 @@ public class CCLicenseField { * @param id The unique identifier for this field; this value will be used in constructing the answers XML. * @param label The label to use when generating the user interface. */ - public CCLicenseField(String id, String label) { - super(); - - this.fieldEnum = new HashMap(); - + public CCLicenseField(String id, String label, String description, List fieldEnum) { this.id = id; this.label = label; + this.description = description; + this.fieldEnum = fieldEnum; } /** @@ -90,16 +87,12 @@ public class CCLicenseField { } /** - * @return Returns an instance implementing the Map interface; - * the instance contains a mapping from identifiers to - * labels for the enumeration values. - * @see Map + * Returns the list of enums of this field + * @return the list of enums of this field */ - public Map getEnum() { - return this.fieldEnum; + public List getFieldEnum() { + return fieldEnum; } - - } diff --git a/dspace-api/src/main/java/org/dspace/license/CCLicenseFieldEnum.java b/dspace-api/src/main/java/org/dspace/license/CCLicenseFieldEnum.java new file mode 100644 index 0000000000..628fcb8354 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/license/CCLicenseFieldEnum.java @@ -0,0 +1,82 @@ +/** + * 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.license; + +import org.apache.commons.lang3.StringUtils; + +/** + * Wrapper class for representation of a license field enum declaration. + * A field enum is a single "answer" to the field question + */ +public class CCLicenseFieldEnum { + + private String id = ""; + private String label = ""; + private String description = ""; + + public CCLicenseFieldEnum(String id, String label, String description) { + if (StringUtils.isNotBlank(id)) { + this.id = id; + } + if (StringUtils.isNotBlank(label)) { + this.label = label; + } + if (StringUtils.isNotBlank(description)) { + this.description = description; + } + + } + + /** + * Get the id of this enum + * @return the id of this enum + */ + public String getId() { + return id; + } + + /** + * Set the id of this enum + * @param id + */ + public void setId(final String id) { + this.id = id; + } + + /** + * Get the label of this enum + * @return the label of this enum + */ + public String getLabel() { + return label; + } + + /** + * Set the label of this enum + * @param label + */ + public void setLabel(final String label) { + this.label = label; + } + + /** + * Get the description of this enum + * @return the description of this enum + */ + public String getDescription() { + return description; + } + + /** + * Set the description of this enum + * @param description + */ + public void setDescription(final String description) { + this.description = description; + } +} diff --git a/dspace-api/src/main/java/org/dspace/license/CCLookup.java b/dspace-api/src/main/java/org/dspace/license/CCLookup.java index c86aa78301..b7ddfa2314 100644 --- a/dspace-api/src/main/java/org/dspace/license/CCLookup.java +++ b/dspace-api/src/main/java/org/dspace/license/CCLookup.java @@ -128,7 +128,7 @@ public class CCLookup { // add if not filtered String liD = ((Attribute) xp_LicenseID.selectSingleNode(license)).getValue(); if (!lcFilter.contains(liD)) { - this.licenses.add(new CCLicense(liD, license.getText(), i)); +// this.licenses.add(new CCLicense(liD, license.getText(), i)); } } } catch (JaxenException jaxen_e) { @@ -213,30 +213,30 @@ public class CCLookup { for (int i = 0; i < results.size(); i++) { Element field = (Element) results.get(i); - try { - // create the field object - CCLicenseField cclicensefield = new CCLicenseField( - ((Attribute) xp_LicenseID.selectSingleNode(field)).getValue(), - ((Element) xp_Label.selectSingleNode(field)).getText()); - - // extract additional properties - cclicensefield.setDescription(((Element) xp_Description.selectSingleNode(field)).getText()); - cclicensefield.setType(((Element) xp_FieldType.selectSingleNode(field)).getText()); - - enumOptions = xp_Enum.selectNodes(field); - - for (int j = 0; j < enumOptions.size(); j++) { - String id = ((Attribute) xp_LicenseID.selectSingleNode(enumOptions.get(j))).getValue(); - String label = ((Element) xp_Label.selectSingleNode(enumOptions.get(j))).getText(); - - cclicensefield.getEnum().put(id, label); - - } // for each enum option - - this.licenseFields.add(cclicensefield); - } catch (JaxenException e) { - return null; - } +// try { +// // create the field object +// CCLicenseField cclicensefield = new CCLicenseField( +// ((Attribute) xp_LicenseID.selectSingleNode(field)).getValue(), +// ((Element) xp_Label.selectSingleNode(field)).getText()); +// +// // extract additional properties +// cclicensefield.setDescription(((Element) xp_Description.selectSingleNode(field)).getText()); +// cclicensefield.setType(((Element) xp_FieldType.selectSingleNode(field)).getText()); +// +// enumOptions = xp_Enum.selectNodes(field); +// +// for (int j = 0; j < enumOptions.size(); j++) { +// String id = ((Attribute) xp_LicenseID.selectSingleNode(enumOptions.get(j))).getValue(); +// String label = ((Element) xp_Label.selectSingleNode(enumOptions.get(j))).getText(); +// +//// cclicensefield.getEnum().put(id, label); +// +// } // for each enum option +// +// this.licenseFields.add(cclicensefield); +// } catch (JaxenException e) { +// return null; +// } } return licenseFields; diff --git a/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java b/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java index 384b82ddc3..fed51a9f0a 100644 --- a/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java @@ -82,9 +82,13 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi protected BundleService bundleService; @Autowired(required = true) protected ItemService itemService; + @Autowired + protected CCLicenseConnectorService ccLicenseConnectorService; protected ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + private List ccLicenses; + protected CreativeCommonsServiceImpl() { } @@ -103,8 +107,8 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi try { templates = TransformerFactory.newInstance().newTemplates( - new StreamSource(CreativeCommonsServiceImpl.class - .getResourceAsStream("CreativeCommons.xsl"))); + new StreamSource(CreativeCommonsServiceImpl.class + .getResourceAsStream("CreativeCommons.xsl"))); } catch (TransformerConfigurationException e) { throw new RuntimeException(e.getMessage(), e); } @@ -120,7 +124,7 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi // create the CC bundle if it doesn't exist // If it does, remove it and create a new one. protected Bundle getCcBundle(Context context, Item item) - throws SQLException, AuthorizeException, IOException { + throws SQLException, AuthorizeException, IOException { List bundles = itemService.getBundles(item, CC_BUNDLE_NAME); if ((bundles.size() > 0) && (bundles.get(0) != null)) { @@ -131,8 +135,8 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi @Override public void setLicenseRDF(Context context, Item item, String licenseRdf) - throws SQLException, IOException, - AuthorizeException { + throws SQLException, IOException, + AuthorizeException { Bundle bundle = getCcBundle(context, item); // set the format BitstreamFormat bs_rdf_format = bitstreamFormatService.findByShortDescription(context, "RDF XML"); @@ -144,7 +148,7 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi @Override public void setLicense(Context context, Item item, InputStream licenseStm, String mimeType) - throws SQLException, IOException, AuthorizeException { + throws SQLException, IOException, AuthorizeException { Bundle bundle = getCcBundle(context, item); // set the format @@ -160,9 +164,9 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi Bitstream bs = bitstreamService.create(context, bundle, licenseStm); bs.setSource(context, CC_BS_SOURCE); bs.setName(context, (mimeType != null && - (mimeType.equalsIgnoreCase("text/xml") || - mimeType.equalsIgnoreCase("text/rdf"))) ? - BSN_LICENSE_RDF : BSN_LICENSE_TEXT); + (mimeType.equalsIgnoreCase("text/xml") || + mimeType.equalsIgnoreCase("text/rdf"))) ? + BSN_LICENSE_RDF : BSN_LICENSE_TEXT); bs.setFormat(context, bs_format); bitstreamService.update(context, bs); } @@ -170,7 +174,7 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi @Override public void removeLicense(Context context, Item item) - throws SQLException, IOException, AuthorizeException { + throws SQLException, IOException, AuthorizeException { // remove CC license bundle if one exists List bundles = itemService.getBundles(item, CC_BUNDLE_NAME); @@ -181,7 +185,7 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi @Override public boolean hasLicense(Context context, Item item) - throws SQLException, IOException { + throws SQLException, IOException { // try to find CC license bundle List bundles = itemService.getBundles(item, CC_BUNDLE_NAME); @@ -203,20 +207,20 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi @Override public String getLicenseRDF(Context context, Item item) throws SQLException, - IOException, AuthorizeException { + IOException, AuthorizeException { return getStringFromBitstream(context, item, BSN_LICENSE_RDF); } @Override public Bitstream getLicenseRdfBitstream(Item item) throws SQLException, - IOException, AuthorizeException { + IOException, AuthorizeException { return getBitstream(item, BSN_LICENSE_RDF); } @Deprecated @Override public Bitstream getLicenseTextBitstream(Item item) throws SQLException, - IOException, AuthorizeException { + IOException, AuthorizeException { return getBitstream(item, BSN_LICENSE_TEXT); } @@ -237,8 +241,8 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi try { templates.newTransformer().transform( - new JDOMSource(license), - new StreamResult(result) + new JDOMSource(license), + new StreamResult(result) ); } catch (TransformerException e) { throw new IllegalStateException(e.getMessage(), e); @@ -267,7 +271,7 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi */ protected void setBitstreamFromBytes(Context context, Item item, Bundle bundle, String bitstream_name, BitstreamFormat format, byte[] bytes) - throws SQLException, IOException, AuthorizeException { + throws SQLException, IOException, AuthorizeException { ByteArrayInputStream bais = new ByteArrayInputStream(bytes); Bitstream bs = bitstreamService.create(context, bundle, bais); @@ -297,7 +301,7 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi */ protected String getStringFromBitstream(Context context, Item item, String bitstream_name) throws SQLException, IOException, - AuthorizeException { + AuthorizeException { byte[] bytes = getBytesFromBitstream(context, item, bitstream_name); if (bytes == null) { @@ -320,7 +324,7 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi * to perform a particular action. */ protected Bitstream getBitstream(Item item, String bitstream_name) - throws SQLException, IOException, AuthorizeException { + throws SQLException, IOException, AuthorizeException { Bundle cc_bundle = null; // look for the CC bundle @@ -342,7 +346,7 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi } protected byte[] getBytesFromBitstream(Context context, Item item, String bitstream_name) - throws SQLException, IOException, AuthorizeException { + throws SQLException, IOException, AuthorizeException { Bitstream bs = getBitstream(item, bitstream_name); // no such bitstream @@ -368,7 +372,7 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi @Override public void removeLicense(Context context, LicenseMetadataValue uriField, LicenseMetadataValue nameField, Item item) - throws AuthorizeException, IOException, SQLException { + throws AuthorizeException, IOException, SQLException { // only remove any previous licenses String licenseUri = uriField.ccItemValue(item); if (licenseUri != null) { @@ -383,4 +387,26 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi } } + /** + * Find all CC Licenses using the default language found in the configuration + * @return A list of available CC Licenses + */ + public List findAllCCLicenses() { + String language = configurationService.getProperty("cc.license.locale", "en"); + return findAllCCLicenses(language); + } + + /** + * Find all CC Licenses for the provided language + * @param language - the language for which to find the CC Licenses + * @return A list of available CC Licenses for the provided language + */ + public List findAllCCLicenses(String language) { + + if (ccLicenses == null || ccLicenses.isEmpty()) { + ccLicenses = ccLicenseConnectorService.retrieveLicenses(language); + } + return ccLicenses; + } + } diff --git a/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java b/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java index c99c38a127..d25f02ff7b 100644 --- a/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java +++ b/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java @@ -10,11 +10,13 @@ package org.dspace.license.service; import java.io.IOException; import java.io.InputStream; import java.sql.SQLException; +import java.util.List; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Bitstream; import org.dspace.content.Item; import org.dspace.core.Context; +import org.dspace.license.CCLicense; import org.dspace.license.LicenseMetadataValue; import org.jdom.Document; @@ -149,4 +151,19 @@ public interface CreativeCommonsService { public void removeLicense(Context context, LicenseMetadataValue uriField, LicenseMetadataValue nameField, Item item) throws AuthorizeException, IOException, SQLException; + + /** + * Find all CC Licenses using the default language found in the configuration + * + * @return A list of available CC Licenses + */ + public List findAllCCLicenses(); + + /** + * Find all CC Licenses for the provided language + * + * @param language - the language for which to find the CC Licenses + * @return A list of available CC Licenses for the provided language + */ + public List findAllCCLicenses(String language); } diff --git a/dspace-api/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java b/dspace-api/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java new file mode 100644 index 0000000000..54219bbaaa --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java @@ -0,0 +1,72 @@ +/** + * 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.license; + +import java.util.LinkedList; +import java.util.List; + +/** + * Mock implementation for the Creative commons license connector service. + * This class will return a structure of CC Licenses similar to the CC License API but without having to contact it + */ +public class MockCCLicenseConnectorServiceImpl extends CCLicenseConnectorServiceImpl { + + /** + * Retrieves mock CC Licenses for the provided language + * @param language - the language + * @return a list of mocked licenses + */ + public List retrieveLicenses(String language) { + List ccLicenses = new LinkedList<>(); + ccLicenses.add(createMockLicense(1, new int[]{3, 2, 3})); + ccLicenses.add(createMockLicense(2, new int[]{2})); + ccLicenses.add(createMockLicense(3, new int[]{})); + + return ccLicenses; + } + + private CCLicense createMockLicense(int count, int[] amountOfFieldsAndEnums) { + String licenseId = "license" + count; + String licenseName = "License " + count + " - Name"; + List mockLicenseFields = createMockLicenseFields(count, amountOfFieldsAndEnums); + return new CCLicense(licenseId, licenseName, mockLicenseFields); + } + + private List createMockLicenseFields(int count, int[] amountOfFieldsAndEnums) { + List ccLicenseFields = new LinkedList<>(); + for (int index = 0; index < amountOfFieldsAndEnums.length; index++) { + String licenseFieldId = "license" + count + "-field" + index; + String licenseFieldLabel = "License " + count + " - Field " + index + " - Label"; + String licenseFieldDescription = "License " + count + " - Field " + index + " - Description"; + List mockLicenseFields = createMockLicenseFields(count, + index, + amountOfFieldsAndEnums[index]); + ccLicenseFields.add(new CCLicenseField(licenseFieldId, + licenseFieldLabel, + licenseFieldDescription, + mockLicenseFields)); + + } + + return ccLicenseFields; + } + + private List createMockLicenseFields(int count, int index, int amountOfEnums) { + List ccLicenseFieldEnumList = new LinkedList<>(); + for (int i = 0; i < amountOfEnums; i++) { + String enumId = "license" + count + "-field" + index + "-enum" + i; + String enumLabel = "License " + count + " - Field " + index + " - Enum " + i + " - Label"; + String enumDescription = "License " + count + " - Field " + index + " - Enum " + i + " - " + + "Description"; + ccLicenseFieldEnumList.add(new CCLicenseFieldEnum(enumId, enumLabel, enumDescription)); + } + return ccLicenseFieldEnumList; + + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseConverter.java new file mode 100644 index 0000000000..bd7e582e75 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseConverter.java @@ -0,0 +1,59 @@ +/** + * 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 java.util.LinkedList; +import java.util.List; + +import org.dspace.app.rest.model.SubmissionCCLicenseFieldRest; +import org.dspace.app.rest.model.SubmissionCCLicenseRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.license.CCLicense; +import org.dspace.license.CCLicenseField; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * This converter is responsible for transforming the model representation of an CCLicense to the REST + * representation of an CCLicense and vice versa + **/ +@Component +public class SubmissionCCLicenseConverter implements DSpaceConverter { + + @Autowired + private ConverterService converter; + + /** + * Convert a CCLicense to its REST representation + * @param modelObject - the CCLicense to convert + * @param projection - the projection + * @return the corresponding SubmissionCCLicenseRest object + */ + @Override + public SubmissionCCLicenseRest convert(final CCLicense modelObject, final Projection projection) { + SubmissionCCLicenseRest submissionCCLicenseRest = new SubmissionCCLicenseRest(); + submissionCCLicenseRest.setProjection(projection); + submissionCCLicenseRest.setId(modelObject.getLicenseId()); + submissionCCLicenseRest.setName(modelObject.getLicenseName()); + + List ccLicenseFieldList = modelObject.getCcLicenseFieldList(); + List submissionCCLicenseFieldRests = new LinkedList<>(); + if (ccLicenseFieldList != null) { + for (CCLicenseField ccLicenseField : ccLicenseFieldList) { + submissionCCLicenseFieldRests.add(converter.toRest(ccLicenseField, projection)); + } + } + submissionCCLicenseRest.setFields(submissionCCLicenseFieldRests); + return submissionCCLicenseRest; + } + + public Class getModelClass() { + return CCLicense.class; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseFieldConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseFieldConverter.java new file mode 100644 index 0000000000..b0418ef4b5 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseFieldConverter.java @@ -0,0 +1,61 @@ +/** + * 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 java.util.LinkedList; +import java.util.List; + +import org.dspace.app.rest.model.SubmissionCCLicenseFieldEnumRest; +import org.dspace.app.rest.model.SubmissionCCLicenseFieldRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.license.CCLicenseField; +import org.dspace.license.CCLicenseFieldEnum; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * This converter is responsible for transforming the model representation of an CCLicenseField to the REST + * representation of an CCLicenseField and vice versa + * The CCLicenseField is a sub component of the CCLicense object + **/ +@Component +public class SubmissionCCLicenseFieldConverter + implements DSpaceConverter { + + @Autowired + private ConverterService converter; + + /** + * Convert a CCLicenseField to its REST representation + * @param modelObject - the CCLicenseField to convert + * @param projection - the projection + * @return the corresponding SubmissionCCLicenseFieldRest object + */ + @Override + public SubmissionCCLicenseFieldRest convert(final CCLicenseField modelObject, final Projection projection) { + SubmissionCCLicenseFieldRest submissionCCLicenseFieldRest = new SubmissionCCLicenseFieldRest(); + submissionCCLicenseFieldRest.setId(modelObject.getId()); + submissionCCLicenseFieldRest.setLabel(modelObject.getLabel()); + submissionCCLicenseFieldRest.setDescription(modelObject.getDescription()); + + List fieldEnum = modelObject.getFieldEnum(); + List submissionCCLicenseFieldEnumRests = new LinkedList<>(); + if (fieldEnum != null) { + for (CCLicenseFieldEnum ccLicenseFieldEnum : fieldEnum) { + submissionCCLicenseFieldEnumRests.add(converter.toRest(ccLicenseFieldEnum, projection)); + } + } + submissionCCLicenseFieldRest.setEnums(submissionCCLicenseFieldEnumRests); + return submissionCCLicenseFieldRest; + } + + public Class getModelClass() { + return CCLicenseField.class; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseFieldEnumConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseFieldEnumConverter.java new file mode 100644 index 0000000000..85ed1c45e4 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseFieldEnumConverter.java @@ -0,0 +1,45 @@ +/** + * 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.rest.model.SubmissionCCLicenseFieldEnumRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.license.CCLicenseFieldEnum; +import org.springframework.stereotype.Component; + +/** + * This converter is responsible for transforming the model representation of an CCLicenseFieldEnum to the REST + * representation of an CCLicenseFieldEnum and vice versa + * The CCLicenseFieldEnum is a sub component of the CCLicenseField object + **/ +@Component +public class SubmissionCCLicenseFieldEnumConverter + implements DSpaceConverter { + + /** + * Convert a CCLicenseFieldEnum to its REST representation + * + * @param modelObject - the CCLicenseField to convert + * @param projection - the projection + * @return the corresponding SubmissionCCLicenseFieldEnumRest object + */ + @Override + public SubmissionCCLicenseFieldEnumRest convert(final CCLicenseFieldEnum modelObject, final Projection projection) { + SubmissionCCLicenseFieldEnumRest submissionCCLicenseFieldEnumRest = new SubmissionCCLicenseFieldEnumRest(); + submissionCCLicenseFieldEnumRest.setId(modelObject.getId()); + submissionCCLicenseFieldEnumRest.setLabel(modelObject.getLabel()); + submissionCCLicenseFieldEnumRest.setDescription(modelObject.getDescription()); + + return submissionCCLicenseFieldEnumRest; + } + + public Class getModelClass() { + return CCLicenseFieldEnum.class; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseFieldEnumRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseFieldEnumRest.java new file mode 100644 index 0000000000..770eb25782 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseFieldEnumRest.java @@ -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.model; + +/** + * This class is the REST representation of the CCLicenseFieldEnum model object and acts as a data sub object + * for the SubmissionCCLicenseFieldRest class. + * Refer to {@link org.dspace.license.CCLicenseFieldEnum} for explanation of the properties + */ +public class SubmissionCCLicenseFieldEnumRest { + + private String id; + private String label; + private String description; + + public String getId() { + return id; + } + + public void setId(final String id) { + this.id = id; + } + + public String getLabel() { + return label; + } + + public void setLabel(final String label) { + this.label = label; + } + + public String getDescription() { + return description; + } + + public void setDescription(final String description) { + this.description = description; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseFieldRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseFieldRest.java new file mode 100644 index 0000000000..bcc90279dc --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseFieldRest.java @@ -0,0 +1,59 @@ +/** + * 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.List; + +/** + * This class is the REST representation of the CCLicenseField model object and acts as a data sub object + * for the SubmissionCCLicenseRest class. + * Refer to {@link org.dspace.license.CCLicenseField} for explanation of the properties + */ +public class SubmissionCCLicenseFieldRest { + + private String id; + + private String label; + + private String description; + + private List enums; + + + public String getId() { + return id; + } + + public void setId(final String id) { + this.id = id; + } + + public String getLabel() { + return label; + } + + public void setLabel(final String label) { + this.label = label; + } + + public String getDescription() { + return description; + } + + public void setDescription(final String description) { + this.description = description; + } + + public List getEnums() { + return enums; + } + + public void setEnums(final List enums) { + this.enums = enums; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseRest.java new file mode 100644 index 0000000000..396e014531 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseRest.java @@ -0,0 +1,73 @@ +/** + * 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.List; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.dspace.app.rest.RestResourceController; + +/** + * This class is the REST representation of the CCLicense model object and acts as a data object + * * for the SubmissionCCLicenseResource class. + * Refer to {@link org.dspace.license.CCLicense} for explanation of the properties + */ +public class SubmissionCCLicenseRest extends BaseObjectRest { + public static final String NAME = "submissioncclicense"; + + public static final String CATEGORY = RestAddressableModel.CONFIGURATION; + + private String id; + + private String name; + + private List fields; + + public String getId() { + return id; + } + + public void setId(final String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(final String name) { + this.name = name; + } + + public List getFields() { + return fields; + } + + public void setFields(final List fields) { + this.fields = fields; + } + + @JsonIgnore + @Override + public String getCategory() { + return CATEGORY; + } + + @Override + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public String getType() { + return NAME; + } + + @Override + @JsonIgnore + public Class getController() { + return RestResourceController.class; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SubmissionCCLicenseResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SubmissionCCLicenseResource.java new file mode 100644 index 0000000000..fb041d2827 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SubmissionCCLicenseResource.java @@ -0,0 +1,23 @@ +/** + * 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.SubmissionCCLicenseRest; +import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource; +import org.dspace.app.rest.utils.Utils; + +/** + * CCLicense HAL Resource. This resource adds the data from the REST object together with embedded objects + * and a set of links if applicable + */ +@RelNameDSpaceResource(SubmissionCCLicenseRest.NAME) +public class SubmissionCCLicenseResource extends DSpaceResource { + public SubmissionCCLicenseResource(SubmissionCCLicenseRest submissionCCLicenseRest, Utils utils) { + super(submissionCCLicenseRest, utils); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionCCLicenseRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionCCLicenseRestRepository.java new file mode 100644 index 0000000000..d3c5236141 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionCCLicenseRestRepository.java @@ -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.repository; + +import java.util.List; + +import org.dspace.app.rest.model.SubmissionCCLicenseRest; +import org.dspace.core.Context; +import org.dspace.license.CCLicense; +import org.dspace.license.service.CreativeCommonsService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +/** + * This is the repository that is responsible to manage CCLicense Rest objects + */ +@Component(SubmissionCCLicenseRest.CATEGORY + "." + SubmissionCCLicenseRest.NAME) +public class SubmissionCCLicenseRestRepository extends DSpaceRestRepository { + + @Autowired + protected CreativeCommonsService creativeCommonsService; + + + public SubmissionCCLicenseRest findOne(final Context context, final String s) { + return null; + } + + public Page findAll(final Context context, final Pageable pageable) { + + List allCCLicenses = creativeCommonsService.findAllCCLicenses(); + return converter.toRestPage(utils.getPage(allCCLicenses, pageable), utils.obtainProjection()); + } + + public Class getDomainClass() { + return null; + } +} diff --git a/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/core-services-mock.xml b/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/core-services-mock.xml index ff13e7f6b4..8010d3e5d6 100644 --- a/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/core-services-mock.xml +++ b/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/core-services-mock.xml @@ -6,4 +6,5 @@ + diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseRestRepositoryIT.java new file mode 100644 index 0000000000..0cf4f2b387 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseRestRepositoryIT.java @@ -0,0 +1,46 @@ +/** + * 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.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +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.matcher.SubmissionCCLicenseMatcher; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.hamcrest.Matchers; +import org.junit.Test; + +/** + * Class to the methods from the SubmissionCCLicenseRestRepository + * Since the CC Licenses are obtained from the CC License API, a mock service has been implemented + * This mock service will return a fixed set of CC Licenses using a similar structure to the ones obtained from the + * CC License API. + * Refer to {@link org.dspace.license.MockCCLicenseConnectorServiceImpl} for more information + */ +public class SubmissionCCLicenseRestRepositoryIT extends AbstractControllerIntegrationTest { + + + /** + * Test the findAll method form the SubmissionCCLicenseRestRepository + * @throws Exception + */ + @Test + public void findAllTest() throws Exception { + + getClient().perform(get("/api/config/submissioncclicenses")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.submissioncclicenses", Matchers.containsInAnyOrder( + SubmissionCCLicenseMatcher.matchLicenseEntry(1, new int[]{3, 2, 3}), + SubmissionCCLicenseMatcher.matchLicenseEntry(2, new int[]{2}), + SubmissionCCLicenseMatcher.matchLicenseEntry(3, new int[]{}) + ))); + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SubmissionCCLicenseMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SubmissionCCLicenseMatcher.java new file mode 100644 index 0000000000..cdf0470b51 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SubmissionCCLicenseMatcher.java @@ -0,0 +1,82 @@ +/** + * 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.matcher; + +import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.is; + +import java.util.LinkedList; +import java.util.List; + +import org.hamcrest.Matcher; + +public class SubmissionCCLicenseMatcher { + + private SubmissionCCLicenseMatcher() { + } + + public static Matcher matchLicenseEntry(int count, int[] amountOfFieldsAndEnums) { + return allOf( + matchLicenseProperties(count), + matchFields(count, amountOfFieldsAndEnums) + ); + } + + private static Matcher matchFields(int count, int[] amountOfFieldsAndEnums) { + List> matchers = new LinkedList<>(); + for (int index = 0; index < amountOfFieldsAndEnums.length; index++) { + matchers.add(matchField(count, index, amountOfFieldsAndEnums[index])); + } + return hasJsonPath("$.fields", containsInAnyOrder(matchers)); + } + + private static Matcher matchField(int count, int fieldIndex, int amountOfEnums) { + return allOf( + matchLicenseFieldProperties(count, fieldIndex), + matchEnums(count, fieldIndex, amountOfEnums) + ); + + } + + private static Matcher matchEnums(int count, int fieldIndex, int amountOfEnums) { + List> matchers = new LinkedList<>(); + for (int index = 0; index < amountOfEnums; index++) { + matchers.add(matchLicenseFieldEnumProperties(count, fieldIndex, index)); + } +// return hasJsonPath("$.enums"); + return hasJsonPath("$.enums", containsInAnyOrder(matchers)); + } + + + public static Matcher matchLicenseProperties(int count) { + return allOf( + hasJsonPath("$.id", is("license" + count)), + hasJsonPath("$.name", is("License " + count + " - Name")) + ); + } + + public static Matcher matchLicenseFieldProperties(int count, int fieldIndex) { + return allOf( + hasJsonPath("$.id", is("license" + count + "-field" + fieldIndex)), + hasJsonPath("$.label", is("License " + count + " - Field " + fieldIndex + " - Label")), + hasJsonPath("$.description", is("License " + count + " - Field " + fieldIndex + " - Description")) + ); + } + + public static Matcher matchLicenseFieldEnumProperties(int count, int fieldIndex, int enumIndex) { + return allOf( + hasJsonPath("$.id", is("license" + count + "-field" + fieldIndex + "-enum" + enumIndex)), + hasJsonPath("$.label", + is("License " + count + " - Field " + fieldIndex + " - Enum " + enumIndex + " - Label")), + hasJsonPath("$.description", + is("License " + count + " - Field " + fieldIndex + " - Enum " + enumIndex + " - " + "Description")) + ); + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java b/dspace-server-webapp/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java new file mode 100644 index 0000000000..54219bbaaa --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java @@ -0,0 +1,72 @@ +/** + * 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.license; + +import java.util.LinkedList; +import java.util.List; + +/** + * Mock implementation for the Creative commons license connector service. + * This class will return a structure of CC Licenses similar to the CC License API but without having to contact it + */ +public class MockCCLicenseConnectorServiceImpl extends CCLicenseConnectorServiceImpl { + + /** + * Retrieves mock CC Licenses for the provided language + * @param language - the language + * @return a list of mocked licenses + */ + public List retrieveLicenses(String language) { + List ccLicenses = new LinkedList<>(); + ccLicenses.add(createMockLicense(1, new int[]{3, 2, 3})); + ccLicenses.add(createMockLicense(2, new int[]{2})); + ccLicenses.add(createMockLicense(3, new int[]{})); + + return ccLicenses; + } + + private CCLicense createMockLicense(int count, int[] amountOfFieldsAndEnums) { + String licenseId = "license" + count; + String licenseName = "License " + count + " - Name"; + List mockLicenseFields = createMockLicenseFields(count, amountOfFieldsAndEnums); + return new CCLicense(licenseId, licenseName, mockLicenseFields); + } + + private List createMockLicenseFields(int count, int[] amountOfFieldsAndEnums) { + List ccLicenseFields = new LinkedList<>(); + for (int index = 0; index < amountOfFieldsAndEnums.length; index++) { + String licenseFieldId = "license" + count + "-field" + index; + String licenseFieldLabel = "License " + count + " - Field " + index + " - Label"; + String licenseFieldDescription = "License " + count + " - Field " + index + " - Description"; + List mockLicenseFields = createMockLicenseFields(count, + index, + amountOfFieldsAndEnums[index]); + ccLicenseFields.add(new CCLicenseField(licenseFieldId, + licenseFieldLabel, + licenseFieldDescription, + mockLicenseFields)); + + } + + return ccLicenseFields; + } + + private List createMockLicenseFields(int count, int index, int amountOfEnums) { + List ccLicenseFieldEnumList = new LinkedList<>(); + for (int i = 0; i < amountOfEnums; i++) { + String enumId = "license" + count + "-field" + index + "-enum" + i; + String enumLabel = "License " + count + " - Field " + index + " - Enum " + i + " - Label"; + String enumDescription = "License " + count + " - Field " + index + " - Enum " + i + " - " + + "Description"; + ccLicenseFieldEnumList.add(new CCLicenseFieldEnum(enumId, enumLabel, enumDescription)); + } + return ccLicenseFieldEnumList; + + } + +} diff --git a/dspace/config/spring/api/core-services.xml b/dspace/config/spring/api/core-services.xml index 316f81fd18..64d8cc7147 100644 --- a/dspace/config/spring/api/core-services.xml +++ b/dspace/config/spring/api/core-services.xml @@ -100,6 +100,7 @@ + From 18c8efd95106f02ec12fe6edc9fb2abaf3735a0e Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Thu, 9 Apr 2020 14:26:39 +0200 Subject: [PATCH 008/143] 70332: Implement feedback --- .../CCLicenseConnectorServiceImpl.java | 62 ++++++++++--------- .../SubmissionCCLicenseConverter.java | 1 + .../SubmissionCCLicenseFieldConverter.java | 1 + ...SubmissionCCLicenseFieldEnumConverter.java | 1 + .../SubmissionCCLicenseRestRepository.java | 6 +- 5 files changed, 41 insertions(+), 30 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java index edc9934694..a0dbd075cf 100644 --- a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java @@ -59,19 +59,21 @@ public class CCLicenseConnectorServiceImpl implements CCLicenseConnectorService, /** * Retrieves the CC Licenses for the provided language from the CC License API + * * @param language - the language to retrieve the licenses for * @return a list of licenses obtained for the provided languages */ public List retrieveLicenses(String language) { String ccLicenseUrl = configurationService.getProperty("cc.api.rooturl"); - HttpGet httpGet = new HttpGet(ccLicenseUrl + "/?locale=" + language); + String uri = ccLicenseUrl + "/?locale=" + language; + HttpGet httpGet = new HttpGet(uri); List licenses; try (CloseableHttpResponse response = client.execute(httpGet)) { licenses = retrieveLicenses(response); } catch (JDOMException | JaxenException | IOException e) { - log.error(e); + log.error("Error while retrieving the license details using url: " + uri, e); licenses = Collections.emptyList(); } @@ -79,12 +81,13 @@ public class CCLicenseConnectorServiceImpl implements CCLicenseConnectorService, for (String license : licenses) { - HttpGet licenseHttpGet = new HttpGet(ccLicenseUrl + "/license/" + license); + String licenseUri = ccLicenseUrl + "/license/" + license; + HttpGet licenseHttpGet = new HttpGet(licenseUri); try (CloseableHttpResponse response = client.execute(licenseHttpGet)) { - CCLicense ccLicense = retrieveLicenseObject(response); + CCLicense ccLicense = retrieveLicenseObject(license, response); ccLicenses.add(ccLicense); } catch (JaxenException | JDOMException | IOException e) { - log.error(e); + log.error("Error while retrieving the license details using url: " + licenseUri, e); } } @@ -94,6 +97,7 @@ public class CCLicenseConnectorServiceImpl implements CCLicenseConnectorService, /** * Retrieve the list of licenses from the response from the CC License API and remove the licenses configured * to be excluded + * * @param response The response from the API * @return a list of license identifiers for which details need to be retrieved * @throws IOException @@ -111,14 +115,16 @@ public class CCLicenseConnectorServiceImpl implements CCLicenseConnectorService, JDOMXPath licenseClassXpath = new JDOMXPath("//licenses/license"); - InputSource is = new InputSource(new StringReader(responseString)); - org.jdom.Document classDoc = this.parser.build(is); + try (StringReader stringReader = new StringReader(responseString)) { + InputSource is = new InputSource(stringReader); + org.jdom.Document classDoc = this.parser.build(is); - List elements = licenseClassXpath.selectNodes(classDoc); - for (Element element : elements) { - String licenseId = getSingleNodeValue(element, "@id"); - if (StringUtils.isNotBlank(licenseId) && !ArrayUtils.contains(excludedLicenses, licenseId)) { - domains.add(licenseId); + List elements = licenseClassXpath.selectNodes(classDoc); + for (Element element : elements) { + String licenseId = getSingleNodeValue(element, "@id"); + if (StringUtils.isNotBlank(licenseId) && !ArrayUtils.contains(excludedLicenses, licenseId)) { + domains.add(licenseId); + } } } @@ -128,13 +134,15 @@ public class CCLicenseConnectorServiceImpl implements CCLicenseConnectorService, /** * Parse the response for a single CC License and return the corresponding CC License Object - * @param response for a specific CC License response + * + * @param licenseId the license id of the CC License to retrieve + * @param response for a specific CC License response * @return the corresponding CC License Object * @throws IOException * @throws JaxenException * @throws JDOMException */ - private CCLicense retrieveLicenseObject(CloseableHttpResponse response) + private CCLicense retrieveLicenseObject(final String licenseId, CloseableHttpResponse response) throws IOException, JaxenException, JDOMException { String responseString = EntityUtils.toString(response.getEntity()); @@ -144,26 +152,24 @@ public class CCLicenseConnectorServiceImpl implements CCLicenseConnectorService, JDOMXPath licenseFieldXpath = new JDOMXPath("field"); - InputSource is; + try (StringReader stringReader = new StringReader(responseString)) { + InputSource is = new InputSource(stringReader); - is = new InputSource(new StringReader(responseString)); + org.jdom.Document classDoc = this.parser.build(is); - org.jdom.Document classDoc = this.parser.build(is); + Object element = licenseClassXpath.selectSingleNode(classDoc); + String licenseLabel = getSingleNodeValue(element, "label"); - Object element = licenseClassXpath.selectSingleNode(classDoc); - String licenseId = getSingleNodeValue(element, "@id"); - String licenseLabel = getSingleNodeValue(element, "label"); + List ccLicenseFields = new LinkedList<>(); - List ccLicenseFields = new LinkedList<>(); + List licenseFields = licenseFieldXpath.selectNodes(element); + for (Element licenseField : licenseFields) { + CCLicenseField ccLicenseField = parseLicenseField(licenseField); + ccLicenseFields.add(ccLicenseField); + } - List licenseFields = licenseFieldXpath.selectNodes(element); - for (Element licenseField : licenseFields) { - CCLicenseField ccLicenseField = parseLicenseField(licenseField); - ccLicenseFields.add(ccLicenseField); + return new CCLicense(licenseId, licenseLabel, ccLicenseFields); } - - - return new CCLicense(licenseId, licenseLabel, ccLicenseFields); } private CCLicenseField parseLicenseField(final Element licenseField) throws JaxenException { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseConverter.java index bd7e582e75..bf6b92a618 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseConverter.java @@ -52,6 +52,7 @@ public class SubmissionCCLicenseConverter implements DSpaceConverter getModelClass() { return CCLicense.class; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseFieldConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseFieldConverter.java index b0418ef4b5..782056dc1c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseFieldConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseFieldConverter.java @@ -54,6 +54,7 @@ public class SubmissionCCLicenseFieldConverter return submissionCCLicenseFieldRest; } + @Override public Class getModelClass() { return CCLicenseField.class; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseFieldEnumConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseFieldEnumConverter.java index 85ed1c45e4..6c8993905f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseFieldEnumConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseFieldEnumConverter.java @@ -38,6 +38,7 @@ public class SubmissionCCLicenseFieldEnumConverter return submissionCCLicenseFieldEnumRest; } + @Override public Class getModelClass() { return CCLicenseFieldEnum.class; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionCCLicenseRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionCCLicenseRestRepository.java index d3c5236141..d8ee6bec15 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionCCLicenseRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionCCLicenseRestRepository.java @@ -27,18 +27,20 @@ public class SubmissionCCLicenseRestRepository extends DSpaceRestRepository findAll(final Context context, final Pageable pageable) { List allCCLicenses = creativeCommonsService.findAllCCLicenses(); return converter.toRestPage(utils.getPage(allCCLicenses, pageable), utils.obtainProjection()); } + @Override public Class getDomainClass() { - return null; + return SubmissionCCLicenseRest.class; } } From f51a12d0106281d32885e1006d91f0c48110de05 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Fri, 10 Apr 2020 10:27:32 +0200 Subject: [PATCH 009/143] 70334: CC license (REST): Initial fineOne endpoint --- .../license/CCLicenseConnectorService.java | 7 ++- .../CCLicenseConnectorServiceImpl.java | 10 ++-- .../license/CreativeCommonsServiceImpl.java | 57 +++++++++++++++++-- .../service/CreativeCommonsService.java | 18 ++++++ .../MockCCLicenseConnectorServiceImpl.java | 18 ++++-- .../SubmissionCCLicenseRestRepository.java | 9 ++- .../SubmissionCCLicenseRestRepositoryIT.java | 16 ++++++ .../MockCCLicenseConnectorServiceImpl.java | 18 ++++-- 8 files changed, 127 insertions(+), 26 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorService.java b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorService.java index 52bbf39cc7..caf079d230 100644 --- a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorService.java +++ b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorService.java @@ -7,7 +7,7 @@ */ package org.dspace.license; -import java.util.List; +import java.util.Map; /** * Service interface class for the Creative commons license connector service. @@ -18,9 +18,10 @@ public interface CCLicenseConnectorService { /** * Retrieves the CC Licenses for the provided language from the CC License API + * * @param language - the language to retrieve the licenses for - * @return a list of licenses obtained for the provided languages + * @return a map of licenses with the id and the license for the provided language */ - public List retrieveLicenses(String language); + public Map retrieveLicenses(String language); } diff --git a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java index a0dbd075cf..4cb6d74b0f 100644 --- a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java @@ -10,8 +10,10 @@ package org.dspace.license; import java.io.IOException; import java.io.StringReader; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; @@ -61,9 +63,9 @@ public class CCLicenseConnectorServiceImpl implements CCLicenseConnectorService, * Retrieves the CC Licenses for the provided language from the CC License API * * @param language - the language to retrieve the licenses for - * @return a list of licenses obtained for the provided languages + * @return a map of licenses with the id and the license for the provided language */ - public List retrieveLicenses(String language) { + public Map retrieveLicenses(String language) { String ccLicenseUrl = configurationService.getProperty("cc.api.rooturl"); String uri = ccLicenseUrl + "/?locale=" + language; @@ -77,7 +79,7 @@ public class CCLicenseConnectorServiceImpl implements CCLicenseConnectorService, licenses = Collections.emptyList(); } - List ccLicenses = new LinkedList<>(); + Map ccLicenses = new HashMap<>(); for (String license : licenses) { @@ -85,7 +87,7 @@ public class CCLicenseConnectorServiceImpl implements CCLicenseConnectorService, HttpGet licenseHttpGet = new HttpGet(licenseUri); try (CloseableHttpResponse response = client.execute(licenseHttpGet)) { CCLicense ccLicense = retrieveLicenseObject(license, response); - ccLicenses.add(ccLicense); + ccLicenses.put(ccLicense.getLicenseId(), ccLicense); } catch (JaxenException | JDOMException | IOException e) { log.error("Error while retrieving the license details using url: " + licenseUri, e); } diff --git a/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java b/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java index fed51a9f0a..1c08d297dc 100644 --- a/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java @@ -13,7 +13,10 @@ import java.io.IOException; import java.io.InputStream; import java.io.StringWriter; import java.sql.SQLException; +import java.util.HashMap; +import java.util.LinkedList; import java.util.List; +import java.util.Map; import javax.xml.transform.Templates; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; @@ -87,7 +90,7 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi protected ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); - private List ccLicenses; + private Map> ccLicenses; protected CreativeCommonsServiceImpl() { @@ -105,6 +108,9 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi System.setProperty("http.proxyPort", proxyPort); } + ccLicenses = new HashMap<>(); + + try { templates = TransformerFactory.newInstance().newTemplates( new StreamSource(CreativeCommonsServiceImpl.class @@ -389,6 +395,7 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi /** * Find all CC Licenses using the default language found in the configuration + * * @return A list of available CC Licenses */ public List findAllCCLicenses() { @@ -398,15 +405,55 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi /** * Find all CC Licenses for the provided language - * @param language - the language for which to find the CC Licenses + * + * @param language - the language for which to find the CC Licenses * @return A list of available CC Licenses for the provided language */ public List findAllCCLicenses(String language) { - if (ccLicenses == null || ccLicenses.isEmpty()) { - ccLicenses = ccLicenseConnectorService.retrieveLicenses(language); + if (!ccLicenses.containsKey(language)) { + initLicenses(language); } - return ccLicenses; + return new LinkedList<>(ccLicenses.get(language).values()); + } + + /** + * Find the CC License corresponding to the provided ID using the default language found in the configuration + * + * @param id - the ID of the license to be found + * @return the corresponding license if found or null when not found + */ + public CCLicense findOne(String id) { + String language = configurationService.getProperty("cc.license.locale", "en"); + return findOne(id, language); + } + + /** + * Find the CC License corresponding to the provided ID and provided language + * + * @param id - the ID of the license to be found + * @param language - the language for which to find the CC License + * @return the corresponding license if found or null when not found + */ + public CCLicense findOne(String id, String language) { + if (!ccLicenses.containsKey(language)) { + initLicenses(language); + } + Map licenseMap = ccLicenses.get(language); + if (licenseMap.containsKey(id)) { + return licenseMap.get(id); + } + return null; + } + + /** + * Retrieves the licenses for a specific language and cache them in this service + * + * @param language - the language for which to find the CC Licenses + */ + private void initLicenses(final String language) { + Map licenseMap = ccLicenseConnectorService.retrieveLicenses(language); + ccLicenses.put(language, licenseMap); } } diff --git a/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java b/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java index d25f02ff7b..edb9410f7e 100644 --- a/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java +++ b/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java @@ -166,4 +166,22 @@ public interface CreativeCommonsService { * @return A list of available CC Licenses for the provided language */ public List findAllCCLicenses(String language); + + /** + * Find the CC License corresponding to the provided ID using the default language found in the configuration + * + * @param id - the ID of the license to be found + * @return the corresponding license if found or null when not found + */ + public CCLicense findOne(String id); + + /** + * Find the CC License corresponding to the provided ID and provided language + * + * @param id - the ID of the license to be found + * @param language - the language for which to find the CC License + * @return the corresponding license if found or null when not found + */ + public CCLicense findOne(String id, String language); + } diff --git a/dspace-api/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java b/dspace-api/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java index 54219bbaaa..df934312d1 100644 --- a/dspace-api/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java +++ b/dspace-api/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java @@ -7,8 +7,10 @@ */ package org.dspace.license; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; /** * Mock implementation for the Creative commons license connector service. @@ -19,13 +21,17 @@ public class MockCCLicenseConnectorServiceImpl extends CCLicenseConnectorService /** * Retrieves mock CC Licenses for the provided language * @param language - the language - * @return a list of mocked licenses + * @return a map of mocked licenses with the id and the license */ - public List retrieveLicenses(String language) { - List ccLicenses = new LinkedList<>(); - ccLicenses.add(createMockLicense(1, new int[]{3, 2, 3})); - ccLicenses.add(createMockLicense(2, new int[]{2})); - ccLicenses.add(createMockLicense(3, new int[]{})); + public Map retrieveLicenses(String language) { + Map ccLicenses = new HashMap<>(); + CCLicense mockLicense1 = createMockLicense(1, new int[]{3, 2, 3}); + CCLicense mockLicense2 = createMockLicense(2, new int[]{2}); + CCLicense mockLicense3 = createMockLicense(3, new int[]{}); + + ccLicenses.put(mockLicense1.getLicenseId(), mockLicense1); + ccLicenses.put(mockLicense2.getLicenseId(), mockLicense2); + ccLicenses.put(mockLicense3.getLicenseId(), mockLicense3); return ccLicenses; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionCCLicenseRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionCCLicenseRestRepository.java index d8ee6bec15..88ba438639 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionCCLicenseRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionCCLicenseRestRepository.java @@ -16,6 +16,7 @@ import org.dspace.license.service.CreativeCommonsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; import org.springframework.stereotype.Component; /** @@ -28,8 +29,12 @@ public class SubmissionCCLicenseRestRepository extends DSpaceRestRepository retrieveLicenses(String language) { - List ccLicenses = new LinkedList<>(); - ccLicenses.add(createMockLicense(1, new int[]{3, 2, 3})); - ccLicenses.add(createMockLicense(2, new int[]{2})); - ccLicenses.add(createMockLicense(3, new int[]{})); + public Map retrieveLicenses(String language) { + Map ccLicenses = new HashMap<>(); + CCLicense mockLicense1 = createMockLicense(1, new int[]{3, 2, 3}); + CCLicense mockLicense2 = createMockLicense(2, new int[]{2}); + CCLicense mockLicense3 = createMockLicense(3, new int[]{}); + + ccLicenses.put(mockLicense1.getLicenseId(), mockLicense1); + ccLicenses.put(mockLicense2.getLicenseId(), mockLicense2); + ccLicenses.put(mockLicense3.getLicenseId(), mockLicense3); return ccLicenses; } From 44d3d2bcc3c39c57cea29ed1ae9b158e0769f2be Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Tue, 14 Apr 2020 11:58:50 +0200 Subject: [PATCH 010/143] [Task 70273] added POST endpoint for new registration creation and IT --- .../dspace/eperson/AccountServiceImpl.java | 6 ++ .../app/rest/RegistrationRestController.java | 86 +++++++++++++++++++ .../app/rest/model/RegistrationRest.java | 72 ++++++++++++++++ .../rest/RegistrationRestControllerIT.java | 75 ++++++++++++++++ 4 files changed, 239 insertions(+) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java diff --git a/dspace-api/src/main/java/org/dspace/eperson/AccountServiceImpl.java b/dspace-api/src/main/java/org/dspace/eperson/AccountServiceImpl.java index e00a9568e3..9282fc116e 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/AccountServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/eperson/AccountServiceImpl.java @@ -22,6 +22,7 @@ import org.dspace.core.Utils; import org.dspace.eperson.service.AccountService; import org.dspace.eperson.service.EPersonService; import org.dspace.eperson.service.RegistrationDataService; +import org.dspace.services.ConfigurationService; import org.springframework.beans.factory.annotation.Autowired; /** @@ -47,6 +48,8 @@ public class AccountServiceImpl implements AccountService { protected EPersonService ePersonService; @Autowired(required = true) protected RegistrationDataService registrationDataService; + @Autowired + private ConfigurationService configurationService; protected AccountServiceImpl() { @@ -67,6 +70,9 @@ public class AccountServiceImpl implements AccountService { public void sendRegistrationInfo(Context context, String email) throws SQLException, IOException, MessagingException, AuthorizeException { + if (!configurationService.getBooleanProperty("user.registration", true)) { + throw new IllegalStateException("The user.registration parameter was set to false"); + } sendInfo(context, email, true, true); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java new file mode 100644 index 0000000000..f11bed240f --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java @@ -0,0 +1,86 @@ +/** + * 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 java.io.IOException; +import java.sql.SQLException; +import javax.mail.MessagingException; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.RegistrationRest; +import org.dspace.app.rest.utils.ContextUtil; +import org.dspace.authorize.AuthorizeException; +import org.dspace.core.Context; +import org.dspace.eperson.service.AccountService; +import org.dspace.eperson.service.EPersonService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.rest.webmvc.ControllerUtils; +import org.springframework.hateoas.ResourceSupport; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/" + RegistrationRest.CATEGORY + "/" + RegistrationRest.NAME_PLURAL) +public class RegistrationRestController { + +// @Autowired +// private AuthorizationFeatureService authorizationFeatureService; +// +// @Autowired +// private SiteService siteService; +// +// @Autowired +// private ConverterService converterService; + + @Autowired + private AccountService accountService; + + @Autowired + private EPersonService ePersonService; + + @RequestMapping(method = RequestMethod.POST) + public ResponseEntity register(HttpServletRequest request, HttpServletResponse response) + throws SQLException, IOException, MessagingException, AuthorizeException { + + Context context = ContextUtil.obtainContext(request); +// AuthorizationFeature epersonRegistration = authorizationFeatureService.find("epersonRegistration"); +// Site site = siteService.findSite(context); +// SiteRest siteRest = converterService.toRest(site, Projection.DEFAULT); +// if (!authorizationFeatureService.isAuthorized(context, epersonRegistration, siteRest)) { +// throw new AccessDeniedException("Registration is disabled, you are not authorized to create +// a new Authorization"); +// } + ObjectMapper mapper = new ObjectMapper(); + RegistrationRest registrationRest; + try { + ServletInputStream input = request.getInputStream(); + registrationRest = mapper.readValue(input, RegistrationRest.class); + } catch (IOException e1) { + throw new UnprocessableEntityException("Error parsing request body.", e1); + } + if (StringUtils.isBlank(registrationRest.getEmail())) { + throw new UnprocessableEntityException("The email cannot be omitted from the Registration endpoint"); + } + if (ePersonService.findByEmail(context, registrationRest.getEmail()) != null) { + accountService.sendForgotPasswordInfo(context, registrationRest.getEmail()); + } else { + accountService.sendRegistrationInfo(context, registrationRest.getEmail()); + } + context.complete(); + return ControllerUtils.toEmptyResponse(HttpStatus.CREATED); + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java new file mode 100644 index 0000000000..0d7f2e30bd --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java @@ -0,0 +1,72 @@ +/** + * 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 com.fasterxml.jackson.annotation.JsonProperty; +import org.dspace.app.rest.RegistrationRestController; + + +public class RegistrationRest extends BaseObjectRest { + + public static final String NAME = "registration"; + public static final String NAME_PLURAL = "registrations"; + public static final String CATEGORY = EPERSON; + + private String email; + private UUID user; + + /** + * Generic getter for the email + * @return the email value of this RegisterRest + */ + public String getEmail() { + return email; + } + + /** + * Generic setter for the email + * @param email The email to be set on this RegisterRest + */ + public void setEmail(String email) { + this.email = email; + } + + /** + * Generic getter for the user + * @return the user value of this RegisterRest + */ + public UUID getUser() { + return user; + } + + /** + * Generic setter for the user + * @param user The user to be set on this RegisterRest + */ + public void setUser(UUID user) { + this.user = user; + } + + @Override + public String getCategory() { + return CATEGORY; + } + + @Override + public Class getController() { + return RegistrationRestController.class; + } + + @Override + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public String getType() { + return NAME; + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java new file mode 100644 index 0000000000..16135d2c68 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java @@ -0,0 +1,75 @@ +/** + * 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.junit.Assert.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.dspace.app.rest.model.RegistrationRest; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.eperson.RegistrationData; +import org.dspace.eperson.dao.RegistrationDataDAO; +import org.dspace.services.ConfigurationService; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +public class RegistrationRestControllerIT extends AbstractControllerIntegrationTest { + + @Autowired + private RegistrationDataDAO registrationDataDAO; + + @Autowired + private ConfigurationService configurationService; + + @Test + public void registrationFlowTest() throws Exception { + List registrationData = registrationDataDAO.findAll(context, RegistrationData.class); + assertTrue(registrationData.isEmpty()); + + String token = getAuthToken(eperson.getEmail(), password); + String t = ";;"; + ObjectMapper mapper = new ObjectMapper(); + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(eperson.getEmail()); + getClient().perform(post("/api/eperson/registrations") + .content(mapper.writeValueAsBytes(registrationRest)) + .contentType(contentType)) + .andExpect(status().isCreated()); + registrationData = registrationDataDAO.findAll(context, RegistrationData.class); + assertTrue(registrationData.size() == 1); + assertTrue(StringUtils.equalsIgnoreCase(registrationData.get(0).getEmail(), eperson.getEmail())); + + String newEmail = "newEPersonTest@gmail.com"; + registrationRest.setEmail(newEmail); + getClient().perform(post("/api/eperson/registrations") + .content(mapper.writeValueAsBytes(registrationRest)) + .contentType(contentType)) + .andExpect(status().isCreated()); + registrationData = registrationDataDAO.findAll(context, RegistrationData.class); + assertTrue(registrationData.size() == 2); + assertTrue(StringUtils.equalsIgnoreCase(registrationData.get(0).getEmail(), newEmail) || + StringUtils.equalsIgnoreCase(registrationData.get(1).getEmail(), newEmail)); + configurationService.setProperty("user.registration", false); + + newEmail = "newEPersonTestTwo@gmail.com"; + registrationRest.setEmail(newEmail); + getClient().perform(post("/api/eperson/registrations") + .content(mapper.writeValueAsBytes(registrationRest)) + .contentType(contentType)) + .andExpect(status().is(500)); + + assertTrue(registrationData.size() == 2); + assertTrue(!StringUtils.equalsIgnoreCase(registrationData.get(0).getEmail(), newEmail) && + !StringUtils.equalsIgnoreCase(registrationData.get(1).getEmail(), newEmail)); + } +} From 794ee9fb9fc723c787709a35ba172ce522ca133c Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Tue, 14 Apr 2020 15:59:21 +0200 Subject: [PATCH 011/143] 70337: Search CC License --- .../license/CCLicenseConnectorService.java | 12 ++ .../CCLicenseConnectorServiceImpl.java | 94 +++++++++++++ .../license/CreativeCommonsServiceImpl.java | 131 +++++++++++++++++- .../service/CreativeCommonsService.java | 87 ++++++++++-- .../MockCCLicenseConnectorServiceImpl.java | 15 ++ .../SubmissionCCLicenseSearchController.java | 95 +++++++++++++ .../rest/model/SubmissionCCLicenseRest.java | 1 + .../SubmissionCCLicenseRestRepositoryIT.java | 1 + ...SubmissionCCLicenseSearchControllerIT.java | 68 +++++++++ .../MockCCLicenseConnectorServiceImpl.java | 14 ++ 10 files changed, 501 insertions(+), 17 deletions(-) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseSearchController.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseSearchControllerIT.java diff --git a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorService.java b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorService.java index caf079d230..48ed5f7200 100644 --- a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorService.java +++ b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorService.java @@ -24,4 +24,16 @@ public interface CCLicenseConnectorService { */ public Map retrieveLicenses(String language); + /** + * Retrieve the CC License URI based on the provided license id, language and answers to the field questions from + * the CC License API + * @param licenseId - the ID of the license + * @param language - the language for which to retrieve the full answerMap + * @param answerMap - the answers to the different field questions + * @return the CC License URI + */ + public String retrieveRightsByQuestion(String licenseId, + String language, + Map answerMap); + } diff --git a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java index 4cb6d74b0f..7fedc7e2e1 100644 --- a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java @@ -9,6 +9,7 @@ package org.dspace.license; import java.io.IOException; import java.io.StringReader; +import java.text.MessageFormat; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; @@ -17,8 +18,11 @@ import java.util.Map; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; @@ -45,6 +49,15 @@ public class CCLicenseConnectorServiceImpl implements CCLicenseConnectorService, private CloseableHttpClient client; private SAXBuilder parser = new SAXBuilder(); + private String postArgument = "answers"; + private String postAnswerFormat = + " " + + "{1}" + + "" + + "{2}" + + "" + + ""; + @Autowired private ConfigurationService configurationService; @@ -221,4 +234,85 @@ public class CCLicenseConnectorServiceImpl implements CCLicenseConnectorService, return getNodeValue(singleNode); } + /** + * Retrieve the CC License URI based on the provided license id, language and answers to the field questions from + * the CC License API + * @param licenseId - the ID of the license + * @param language - the language for which to retrieve the full answerMap + * @param answerMap - the answers to the different field questions + * @return the CC License URI + */ + public String retrieveRightsByQuestion(String licenseId, + String language, + Map answerMap) { + + String ccLicenseUrl = configurationService.getProperty("cc.api.rooturl"); + + + HttpPost httpPost = new HttpPost(ccLicenseUrl + "/license/" + licenseId + "/issue"); + + + String answers = createAnswerString(answerMap); + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + String text = MessageFormat.format(postAnswerFormat, licenseId, language, answers); + builder.addTextBody(postArgument, text); + + HttpEntity multipart = builder.build(); + + httpPost.setEntity(multipart); + + try (CloseableHttpResponse response = client.execute(httpPost)) { + return retrieveLicenseUri(response); + } catch (JDOMException | JaxenException | IOException e) { + log.error("Error while retrieving the license uri for license : " + licenseId + " with answers " + + answerMap.toString(), e); + } + return null; + } + + /** + * Parse the response for the CC License URI request and return the corresponding CC License URI + * + * @param response for a specific CC License URI response + * @return the corresponding CC License URI as a string + * @throws IOException + * @throws JaxenException + * @throws JDOMException + */ + private String retrieveLicenseUri(final CloseableHttpResponse response) + throws IOException, JaxenException, JDOMException { + + String responseString = EntityUtils.toString(response.getEntity()); + JDOMXPath licenseClassXpath = new JDOMXPath("//result/license-uri"); + + + try (StringReader stringReader = new StringReader(responseString)) { + InputSource is = new InputSource(stringReader); + org.jdom.Document classDoc = this.parser.build(is); + + Object node = licenseClassXpath.selectSingleNode(classDoc); + String nodeValue = getNodeValue(node); + + if (StringUtils.isNotBlank(nodeValue)) { + return nodeValue; + } + } + return null; + } + + private String createAnswerString(final Map parameterMap) { + StringBuilder sb = new StringBuilder(); + for (String key : parameterMap.keySet()) { + sb.append("<"); + sb.append(key); + sb.append(">"); + sb.append(parameterMap.get(key)); + sb.append(""); + } + return sb.toString(); + } + + } diff --git a/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java b/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java index 1c08d297dc..c0c190307b 100644 --- a/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java @@ -90,6 +90,9 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi protected ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + private String defaultLanguage; + + private Map> ccLicenses; protected CreativeCommonsServiceImpl() { @@ -109,7 +112,7 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi } ccLicenses = new HashMap<>(); - + defaultLanguage = configurationService.getProperty("cc.license.locale", "en"); try { templates = TransformerFactory.newInstance().newTemplates( @@ -399,8 +402,7 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi * @return A list of available CC Licenses */ public List findAllCCLicenses() { - String language = configurationService.getProperty("cc.license.locale", "en"); - return findAllCCLicenses(language); + return findAllCCLicenses(defaultLanguage); } /** @@ -424,8 +426,7 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi * @return the corresponding license if found or null when not found */ public CCLicense findOne(String id) { - String language = configurationService.getProperty("cc.license.locale", "en"); - return findOne(id, language); + return findOne(id, defaultLanguage); } /** @@ -456,4 +457,124 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi ccLicenses.put(language, licenseMap); } + /** + * Retrieve the CC License URI for the provided license ID, based on the provided answers, using the default + * language found in the configuration + * + * @param licenseId - the ID of the license + * @param answerMap - the answers to the different field questions + * @return the corresponding license URI + */ + public String retrieveLicenseUri(String licenseId, Map answerMap) { + return retrieveLicenseUri(licenseId, defaultLanguage, answerMap); + + } + + /** + * Retrieve the CC License URI for the provided license ID and language based on the provided answers + * + * @param licenseId - the ID of the license + * @param language - the language for which to find the CC License URI + * @param answerMap - the answers to the different field questions + * @return the corresponding license URI + */ + public String retrieveLicenseUri(String licenseId, String language, Map answerMap) { + return ccLicenseConnectorService.retrieveRightsByQuestion(licenseId, language, answerMap); + + } + + /** + * Verify whether the answer map contains a valid response to all field questions and no answers that don't have a + * corresponding question in the license, using the default language found in the config to check the license + * + * @param licenseId - the ID of the license + * @param fullAnswerMap - the answers to the different field questions + * @return whether the information is valid + */ + public boolean verifyLicenseInformation(String licenseId, Map fullAnswerMap) { + return verifyLicenseInformation(licenseId, defaultLanguage, fullAnswerMap); + } + + /** + * Verify whether the answer map contains a valid response to all field questions and no answers that don't have a + * corresponding question in the license, using the provided language to check the license + * + * @param licenseId - the ID of the license + * @param language - the language for which to retrieve the full answerMap + * @param fullAnswerMap - the answers to the different field questions + * @return whether the information is valid + */ + public boolean verifyLicenseInformation(String licenseId, String language, Map fullAnswerMap) { + CCLicense ccLicense = findOne(licenseId, language); + + List ccLicenseFieldList = ccLicense.getCcLicenseFieldList(); + + for (String field : fullAnswerMap.keySet()) { + CCLicenseField ccLicenseField = findCCLicenseField(field, ccLicenseFieldList); + if (ccLicenseField == null) { + return false; + } + if (!containsAnswerEnum(fullAnswerMap.get(field), ccLicenseField)) { + return false; + } + } + return true; + } + + /** + * Retrieve the full answer map containing empty values when an answer for a field was not provided in the + * answerMap, using the default language found in the configuration + * + * @param licenseId - the ID of the license + * @param answerMap - the answers to the different field questions + * @return the answerMap supplemented with all other license fields with a blank answer + */ + public Map retrieveFullAnswerMap(String licenseId, Map answerMap) { + return retrieveFullAnswerMap(licenseId, defaultLanguage, answerMap); + } + + /** + * Retrieve the full answer map for a provided language, containing empty values when an answer for a field was not + * provided in the answerMap. + * + * @param licenseId - the ID of the license + * @param language - the language for which to retrieve the full answerMap + * @param answerMap - the answers to the different field questions + * @return the answerMap supplemented with all other license fields with a blank answer for the provided language + */ + public Map retrieveFullAnswerMap(String licenseId, String language, Map answerMap) { + CCLicense ccLicense = findOne(licenseId, language); + if (ccLicense == null) { + return null; + } + Map fullParamMap = new HashMap<>(answerMap); + List ccLicenseFieldList = ccLicense.getCcLicenseFieldList(); + for (CCLicenseField ccLicenseField : ccLicenseFieldList) { + if (!fullParamMap.containsKey(ccLicenseField.getId())) { + fullParamMap.put(ccLicenseField.getId(), ""); + } + } + return fullParamMap; + } + + private boolean containsAnswerEnum(final String enumAnswer, final CCLicenseField ccLicenseField) { + List fieldEnums = ccLicenseField.getFieldEnum(); + for (CCLicenseFieldEnum fieldEnum : fieldEnums) { + if (StringUtils.equals(fieldEnum.getId(), enumAnswer)) { + return true; + } + } + return false; + } + + private CCLicenseField findCCLicenseField(final String field, final List ccLicenseFieldList) { + for (CCLicenseField ccLicenseField : ccLicenseFieldList) { + if (StringUtils.equals(ccLicenseField.getId(), field)) { + return ccLicenseField; + } + } + + return null; + } + } diff --git a/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java b/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java index edb9410f7e..cda546246f 100644 --- a/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java +++ b/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java @@ -11,6 +11,7 @@ import java.io.IOException; import java.io.InputStream; import java.sql.SQLException; import java.util.List; +import java.util.Map; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Bitstream; @@ -52,7 +53,7 @@ public interface CreativeCommonsService { * to perform a particular action. */ public void setLicenseRDF(Context context, Item item, String licenseRdf) - throws SQLException, IOException, AuthorizeException; + throws SQLException, IOException, AuthorizeException; /** @@ -74,19 +75,19 @@ public interface CreativeCommonsService { */ public void setLicense(Context context, Item item, InputStream licenseStm, String mimeType) - throws SQLException, IOException, AuthorizeException; + throws SQLException, IOException, AuthorizeException; public void removeLicense(Context context, Item item) - throws SQLException, IOException, AuthorizeException; + throws SQLException, IOException, AuthorizeException; public boolean hasLicense(Context context, Item item) - throws SQLException, IOException; + throws SQLException, IOException; public String getLicenseURL(Context context, Item item) - throws SQLException, IOException, AuthorizeException; + throws SQLException, IOException, AuthorizeException; public String getLicenseRDF(Context context, Item item) - throws SQLException, IOException, AuthorizeException; + throws SQLException, IOException, AuthorizeException; /** * Get Creative Commons license RDF, returning Bitstream object. @@ -99,7 +100,7 @@ public interface CreativeCommonsService { * to perform a particular action. */ public Bitstream getLicenseRdfBitstream(Item item) - throws SQLException, IOException, AuthorizeException; + throws SQLException, IOException, AuthorizeException; /** * Get Creative Commons license Text, returning Bitstream object. @@ -114,7 +115,7 @@ public interface CreativeCommonsService { * is no longer stored (see https://jira.duraspace.org/browse/DS-2604) */ public Bitstream getLicenseTextBitstream(Item item) - throws SQLException, IOException, AuthorizeException; + throws SQLException, IOException, AuthorizeException; /** * Get a few license-specific properties. We expect these to be cached at @@ -150,7 +151,7 @@ public interface CreativeCommonsService { */ public void removeLicense(Context context, LicenseMetadataValue uriField, LicenseMetadataValue nameField, Item item) - throws AuthorizeException, IOException, SQLException; + throws AuthorizeException, IOException, SQLException; /** * Find all CC Licenses using the default language found in the configuration @@ -170,7 +171,7 @@ public interface CreativeCommonsService { /** * Find the CC License corresponding to the provided ID using the default language found in the configuration * - * @param id - the ID of the license to be found + * @param id - the ID of the license to be found * @return the corresponding license if found or null when not found */ public CCLicense findOne(String id); @@ -178,10 +179,72 @@ public interface CreativeCommonsService { /** * Find the CC License corresponding to the provided ID and provided language * - * @param id - the ID of the license to be found - * @param language - the language for which to find the CC License + * @param id - the ID of the license to be found + * @param language - the language for which to find the CC License * @return the corresponding license if found or null when not found */ public CCLicense findOne(String id, String language); + /** + * Retrieve the CC License URI for the provided license ID, based on the provided answers, using the default + * language found in the configuration + * + * @param licenseId - the ID of the license + * @param answerMap - the answers to the different field questions + * @return the corresponding license URI + */ + public String retrieveLicenseUri(String licenseId, Map answerMap); + + /** + * Retrieve the CC License URI for the provided license ID and language based on the provided answers + * + * @param licenseId - the ID of the license + * @param language - the language for which to find the CC License URI + * @param answerMap - the answers to the different field questions + * @return the corresponding license URI + */ + public String retrieveLicenseUri(String licenseId, String language, Map answerMap); + + /** + * Retrieve the full answer map containing empty values when an answer for a field was not provided in the + * answerMap, using the default language found in the configuration + * + * @param licenseId - the ID of the license + * @param answerMap - the answers to the different field questions + * @return the answerMap supplemented with all other license fields with a blank answer + */ + public Map retrieveFullAnswerMap(String licenseId, Map answerMap); + + /** + * Retrieve the full answer map for a provided language, containing empty values when an answer for a field was not + * provided in the answerMap. + * + * @param licenseId - the ID of the license + * @param language - the language for which to retrieve the full answerMap + * @param answerMap - the answers to the different field questions + * @return the answerMap supplemented with all other license fields with a blank answer for the provided language + */ + public Map retrieveFullAnswerMap(String licenseId, String language, Map answerMap); + + /** + * Verify whether the answer map contains a valid response to all field questions and no answers that don't have a + * corresponding question in the license, using the default language found in the config to check the license + * + * @param licenseId - the ID of the license + * @param fullAnswerMap - the answers to the different field questions + * @return whether the information is valid + */ + public boolean verifyLicenseInformation(String licenseId, Map fullAnswerMap); + + /** + * Verify whether the answer map contains a valid response to all field questions and no answers that don't have a + * corresponding question in the license, using the provided language to check the license + * + * @param licenseId - the ID of the license + * @param language - the language for which to retrieve the full answerMap + * @param fullAnswerMap - the answers to the different field questions + * @return whether the information is valid + */ + public boolean verifyLicenseInformation(String licenseId, String language, Map fullAnswerMap); + } diff --git a/dspace-api/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java b/dspace-api/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java index df934312d1..226e2aa77b 100644 --- a/dspace-api/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java +++ b/dspace-api/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java @@ -20,6 +20,7 @@ public class MockCCLicenseConnectorServiceImpl extends CCLicenseConnectorService /** * Retrieves mock CC Licenses for the provided language + * * @param language - the language * @return a map of mocked licenses with the id and the license */ @@ -75,4 +76,18 @@ public class MockCCLicenseConnectorServiceImpl extends CCLicenseConnectorService } + /** + * Retrieve a mock CC License URI + * + * @param licenseId - the ID of the license + * @param language - the language for which to retrieve the full answerMap + * @param answerMap - the answers to the different field questions + * @return the CC License URI + */ + public String retrieveRightsByQuestion(final String licenseId, + final String language, + final Map answerMap) { + + return "mock-license-uri"; + } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseSearchController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseSearchController.java new file mode 100644 index 0000000000..185fb7e8eb --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseSearchController.java @@ -0,0 +1,95 @@ +/** + * 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 java.util.HashMap; +import java.util.Map; +import javax.servlet.ServletRequest; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.model.SubmissionCCLicenseRest; +import org.dspace.app.rest.utils.Utils; +import org.dspace.license.service.CreativeCommonsService; +import org.dspace.services.RequestService; +import org.dspace.utils.DSpace; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * This controller is responsible for searching the CC License URI + */ +@RestController +@RequestMapping("/api/" + SubmissionCCLicenseRest.CATEGORY + "/" + SubmissionCCLicenseRest.PLURAL + "/search" + + "/rightsByQuestions") +public class SubmissionCCLicenseSearchController { + + private static final Logger log = LogManager.getLogger(); + + @Autowired + protected Utils utils; + + @Autowired + protected CreativeCommonsService creativeCommonsService; + + protected RequestService requestService = new DSpace().getRequestService(); + + /** + * Retrieves the CC License URI based on the license ID and answers in the field questions, provided as parameters + * to this request + * + * @return the CC License URI as a string + */ + @RequestMapping(method = RequestMethod.GET) + @ResponseBody + public String findByRightsByQuestions() { + ServletRequest servletRequest = requestService.getCurrentRequest() + .getServletRequest(); + Map requestParameterMap = servletRequest + .getParameterMap(); + Map parameterMap = new HashMap<>(); + String licenseId = servletRequest.getParameter("license"); + if (StringUtils.isBlank(licenseId)) { + throw new DSpaceBadRequestException( + "A \"license\" parameter needs to be provided."); + } + for (String parameter : requestParameterMap.keySet()) { + if (StringUtils.startsWith(parameter, "answer_")) { + String field = StringUtils.substringAfter(parameter, "answer_"); + String answer = ""; + if (requestParameterMap.get(parameter).length > 0) { + answer = requestParameterMap.get(parameter)[0]; + } + parameterMap.put(field, answer); + } + } + + Map fullParamMap = creativeCommonsService.retrieveFullAnswerMap(licenseId, parameterMap); + if (fullParamMap == null) { + throw new ResourceNotFoundException("No CC License could be matched on the provided ID: " + licenseId); + } + boolean licenseContainsCorrectInfo = creativeCommonsService.verifyLicenseInformation(licenseId, fullParamMap); + if (!licenseContainsCorrectInfo) { + throw new DSpaceBadRequestException( + "The provided answers do not match the required fields for the provided license."); + } + + String licenseUri = creativeCommonsService.retrieveLicenseUri(licenseId, fullParamMap); + + if (StringUtils.isBlank(licenseUri)) { + throw new ResourceNotFoundException("No CC License URI could be found for ID: " + licenseId); + } + return licenseUri; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseRest.java index 396e014531..611d532039 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseRest.java @@ -20,6 +20,7 @@ import org.dspace.app.rest.RestResourceController; */ public class SubmissionCCLicenseRest extends BaseObjectRest { public static final String NAME = "submissioncclicense"; + public static final String PLURAL = "submissioncclicenses"; public static final String CATEGORY = RestAddressableModel.CONFIGURATION; diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseRestRepositoryIT.java index 1a2b1b0839..5fa22470fe 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseRestRepositoryIT.java @@ -29,6 +29,7 @@ public class SubmissionCCLicenseRestRepositoryIT extends AbstractControllerInteg /** * Test the findAll method form the SubmissionCCLicenseRestRepository + * * @throws Exception */ @Test diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseSearchControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseSearchControllerIT.java new file mode 100644 index 0000000000..402f4c3a69 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseSearchControllerIT.java @@ -0,0 +1,68 @@ +/** + * 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.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.junit.Test; + +/** + * Class to the methods from the SubmissionCCLicenseSearchController + * Since the CC Licenses and the corresponding URIs are obtained from the CC License API, a mock service has been + * implemented. + * This mock service will return a fixed set of CC Licenses using a similar structure to the ones obtained from the + * CC License API. + * Refer to {@link org.dspace.license.MockCCLicenseConnectorServiceImpl} for more information + */ +public class SubmissionCCLicenseSearchControllerIT extends AbstractControllerIntegrationTest { + + + @Test + public void searchRightsByQuestionsTest() throws Exception { + getClient().perform(get( + "/api/config/submissioncclicenses/search/rightsByQuestions?license=license2&answer_license2-field0" + + "=license2-field0-enum1")) + .andExpect(status().isOk()) + .andExpect(content().string("mock-license-uri")); + } + + @Test + public void searchRightsByQuestionsTestLicenseWithoutFields() throws Exception { + getClient().perform(get("/api/config/submissioncclicenses/search/rightsByQuestions?license=license3")) + .andExpect(status().isOk()) + .andExpect(content().string("mock-license-uri")); + } + + @Test + public void searchRightsByQuestionsNonExistingLicense() throws Exception { + getClient().perform(get( + "/api/config/submissioncclicenses/search/rightsByQuestions?license=nonexisting-license" + + "&answer_license2-field0=license2-field0-enum1")) + .andExpect(status().isNotFound()); + } + + @Test + public void searchRightsByQuestionsMissingRequiredAnswer() throws Exception { + getClient().perform(get( + "/api/config/submissioncclicenses/search/rightsByQuestions?license=license1&answer_license1field0" + + "=license1field0enum1")) + .andExpect(status().isBadRequest()); + } + + @Test + public void searchRightsByQuestionsAdditionalNonExistingAnswer() throws Exception { + getClient().perform(get( + "/api/config/submissioncclicenses/search/rightsByQuestions?license=license2" + + "&answer_license2field0=license2field0enum1&answer_nonexisting=test")) + .andExpect(status().isBadRequest()); + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java b/dspace-server-webapp/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java index df934312d1..23d395ec2b 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java +++ b/dspace-server-webapp/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java @@ -75,4 +75,18 @@ public class MockCCLicenseConnectorServiceImpl extends CCLicenseConnectorService } + /** + * Retrieve a mock CC License URI + * + * @param licenseId - the ID of the license + * @param language - the language for which to retrieve the full answerMap + * @param answerMap - the answers to the different field questions + * @return the CC License URI + */ + public String retrieveRightsByQuestion(final String licenseId, + final String language, + final Map answerMap) { + + return "mock-license-uri"; + } } From f07c3824412919b1760ee50acb966e8e3d4b037a Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Wed, 15 Apr 2020 10:31:29 +0200 Subject: [PATCH 012/143] [Task 70382] added the search findByToken method for Registrations and added ITs --- .../app/rest/model/RegistrationRest.java | 2 +- .../model/hateoas/RegistrationResource.java | 18 ++++ .../RegistrationRestRepository.java | 68 ++++++++++++++ .../rest/RegistrationRestControllerIT.java | 33 ++++--- .../rest/RegistrationRestRepositoryIT.java | 90 +++++++++++++++++++ .../app/rest/matcher/RegistrationMatcher.java | 29 ++++++ 6 files changed, 225 insertions(+), 15 deletions(-) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/RegistrationResource.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RegistrationRestRepository.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestRepositoryIT.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/RegistrationMatcher.java diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java index 0d7f2e30bd..b84699f140 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java @@ -13,7 +13,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.dspace.app.rest.RegistrationRestController; -public class RegistrationRest extends BaseObjectRest { +public class RegistrationRest extends RestAddressableModel { public static final String NAME = "registration"; public static final String NAME_PLURAL = "registrations"; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/RegistrationResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/RegistrationResource.java new file mode 100644 index 0000000000..0d92b0d41e --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/RegistrationResource.java @@ -0,0 +1,18 @@ +/** + * 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.RegistrationRest; +import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource; + +@RelNameDSpaceResource(RegistrationRest.NAME) +public class RegistrationResource extends HALResource { + public RegistrationResource(RegistrationRest content) { + super(content); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RegistrationRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RegistrationRestRepository.java new file mode 100644 index 0000000000..f9ff623163 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RegistrationRestRepository.java @@ -0,0 +1,68 @@ +/** + * 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 org.dspace.app.rest.Parameter; +import org.dspace.app.rest.SearchRestMethod; +import org.dspace.app.rest.exception.RepositoryMethodNotImplementedException; +import org.dspace.app.rest.model.RegistrationRest; +import org.dspace.authorize.AuthorizeException; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.RegistrationData; +import org.dspace.eperson.service.AccountService; +import org.dspace.eperson.service.RegistrationDataService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import org.springframework.stereotype.Component; + +@Component(RegistrationRest.CATEGORY + "." + RegistrationRest.NAME) +public class RegistrationRestRepository extends DSpaceRestRepository { + + @Autowired + private AccountService accountService; + + @Autowired + private RegistrationDataService registrationDataService; + + @Override + public RegistrationRest findOne(Context context, Integer integer) { + throw new RepositoryMethodNotImplementedException("No implementation found; Method not allowed!", ""); + } + + @Override + public Page findAll(Context context, Pageable pageable) { + throw new RepositoryMethodNotImplementedException("No implementation found; Method not allowed!", ""); + } + + @Override + public Class getDomainClass() { + return RegistrationRest.class; + } + + @SearchRestMethod(name = "findByToken") + public RegistrationRest findByToken(@Parameter(value = "token", required = true) String token) + throws SQLException, AuthorizeException { + Context context = obtainContext(); + RegistrationData registrationData = registrationDataService.findByToken(context, token); + if (registrationData == null) { + throw new ResourceNotFoundException("The token: " + token + " couldn't be found"); + } + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(registrationData.getEmail()); + EPerson ePerson = accountService.getEPerson(context, token); + if (ePerson != null) { + registrationRest.setUser(ePerson.getID()); + } + return registrationRest; + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java index 16135d2c68..26abcb3075 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java @@ -11,6 +11,7 @@ import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.Iterator; import java.util.List; import com.fasterxml.jackson.databind.ObjectMapper; @@ -33,11 +34,9 @@ public class RegistrationRestControllerIT extends AbstractControllerIntegrationT @Test public void registrationFlowTest() throws Exception { - List registrationData = registrationDataDAO.findAll(context, RegistrationData.class); - assertTrue(registrationData.isEmpty()); + List registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); + assertTrue(registrationDataList.isEmpty()); - String token = getAuthToken(eperson.getEmail(), password); - String t = ";;"; ObjectMapper mapper = new ObjectMapper(); RegistrationRest registrationRest = new RegistrationRest(); registrationRest.setEmail(eperson.getEmail()); @@ -45,9 +44,9 @@ public class RegistrationRestControllerIT extends AbstractControllerIntegrationT .content(mapper.writeValueAsBytes(registrationRest)) .contentType(contentType)) .andExpect(status().isCreated()); - registrationData = registrationDataDAO.findAll(context, RegistrationData.class); - assertTrue(registrationData.size() == 1); - assertTrue(StringUtils.equalsIgnoreCase(registrationData.get(0).getEmail(), eperson.getEmail())); + registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); + assertTrue(registrationDataList.size() == 1); + assertTrue(StringUtils.equalsIgnoreCase(registrationDataList.get(0).getEmail(), eperson.getEmail())); String newEmail = "newEPersonTest@gmail.com"; registrationRest.setEmail(newEmail); @@ -55,10 +54,10 @@ public class RegistrationRestControllerIT extends AbstractControllerIntegrationT .content(mapper.writeValueAsBytes(registrationRest)) .contentType(contentType)) .andExpect(status().isCreated()); - registrationData = registrationDataDAO.findAll(context, RegistrationData.class); - assertTrue(registrationData.size() == 2); - assertTrue(StringUtils.equalsIgnoreCase(registrationData.get(0).getEmail(), newEmail) || - StringUtils.equalsIgnoreCase(registrationData.get(1).getEmail(), newEmail)); + registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); + assertTrue(registrationDataList.size() == 2); + assertTrue(StringUtils.equalsIgnoreCase(registrationDataList.get(0).getEmail(), newEmail) || + StringUtils.equalsIgnoreCase(registrationDataList.get(1).getEmail(), newEmail)); configurationService.setProperty("user.registration", false); newEmail = "newEPersonTestTwo@gmail.com"; @@ -68,8 +67,14 @@ public class RegistrationRestControllerIT extends AbstractControllerIntegrationT .contentType(contentType)) .andExpect(status().is(500)); - assertTrue(registrationData.size() == 2); - assertTrue(!StringUtils.equalsIgnoreCase(registrationData.get(0).getEmail(), newEmail) && - !StringUtils.equalsIgnoreCase(registrationData.get(1).getEmail(), newEmail)); + assertTrue(registrationDataList.size() == 2); + assertTrue(!StringUtils.equalsIgnoreCase(registrationDataList.get(0).getEmail(), newEmail) && + !StringUtils.equalsIgnoreCase(registrationDataList.get(1).getEmail(), newEmail)); + + Iterator iterator = registrationDataList.iterator(); + while (iterator.hasNext()) { + RegistrationData registrationData = iterator.next(); + registrationDataDAO.delete(context, registrationData); + } } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestRepositoryIT.java new file mode 100644 index 0000000000..88feebb286 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestRepositoryIT.java @@ -0,0 +1,90 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.dspace.app.rest.matcher.RegistrationMatcher; +import org.dspace.app.rest.model.RegistrationRest; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.eperson.RegistrationData; +import org.dspace.eperson.dao.RegistrationDataDAO; +import org.hamcrest.Matchers; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +public class RegistrationRestRepositoryIT extends AbstractControllerIntegrationTest { + + @Autowired + private RegistrationDataDAO registrationDataDAO; + + @Test + public void findByTokenTestExistingUserTest() throws Exception { + String email = eperson.getEmail(); + createTokenForEmail(email); + RegistrationData registrationData = registrationDataDAO.findByEmail(context, email); + + getClient().perform(get("/api/eperson/registrations/search/findByToken") + .param("token", registrationData.getToken())) + .andExpect(status().isOk()) + .andExpect( + jsonPath("$", Matchers.is(RegistrationMatcher.matchRegistration(email, eperson.getID())))); + + email = "newUser@testnewuser.com"; + createTokenForEmail(email); + registrationData = registrationDataDAO.findByEmail(context, email); + + getClient().perform(get("/api/eperson/registrations/search/findByToken") + .param("token", registrationData.getToken())) + .andExpect(status().isOk()) + .andExpect( + jsonPath("$", Matchers.is(RegistrationMatcher.matchRegistration(email, null)))); + + registrationDataDAO.delete(context, registrationData); + + } + + @Test + public void findByTokenTestNewUserTest() throws Exception { + String email = "newUser@testnewuser.com"; + createTokenForEmail(email); + RegistrationData registrationData = registrationDataDAO.findByEmail(context, email); + + getClient().perform(get("/api/eperson/registrations/search/findByToken") + .param("token", registrationData.getToken())) + .andExpect(status().isOk()) + .andExpect( + jsonPath("$", Matchers.is(RegistrationMatcher.matchRegistration(email, null)))); + + registrationDataDAO.delete(context, registrationData); + } + + @Test + public void findByTokenNotExistingTokenTest() throws Exception { + getClient().perform(get("/api/eperson/registration/search/findByToken") + .param("token", "ThisTokenDoesNotExist")) + .andExpect(status().isNotFound()); + } + + private void createTokenForEmail(String email) throws Exception { + List registrationDatas; + ObjectMapper mapper = new ObjectMapper(); + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(email); + getClient().perform(post("/api/eperson/registrations") + .content(mapper.writeValueAsBytes(registrationRest)) + .contentType(contentType)) + .andExpect(status().isCreated()); + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/RegistrationMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/RegistrationMatcher.java new file mode 100644 index 0000000000..a154091a2e --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/RegistrationMatcher.java @@ -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.matcher; + +import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.is; + +import java.util.UUID; + +import org.hamcrest.Matcher; + +public class RegistrationMatcher { + + private RegistrationMatcher(){} + + public static Matcher matchRegistration(String email, UUID epersonUuid) { + return allOf( + hasJsonPath("$.email", is(email)), + hasJsonPath("$.user", is(epersonUuid == null ? null : String.valueOf(epersonUuid))) + ); + + } +} From 7da7ff03786802a4c707e2fb68e5c99079fa2bae Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Wed, 15 Apr 2020 12:57:51 +0200 Subject: [PATCH 013/143] [Task 70397] added EPersonRegistrationFeature and added ITs --- .../app/rest/RegistrationRestController.java | 38 ++++-- .../impl/EPersonRegistrationFeature.java | 55 ++++++++ .../rest/RegistrationRestControllerIT.java | 2 +- .../EPersonRegistrationFeatureIT.java | 128 ++++++++++++++++++ 4 files changed, 207 insertions(+), 16 deletions(-) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EPersonRegistrationFeature.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EPersonRegistrationFeatureIT.java diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java index f11bed240f..b03eda36f0 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java @@ -16,10 +16,17 @@ import javax.servlet.http.HttpServletResponse; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.StringUtils; +import org.dspace.app.rest.authorization.AuthorizationFeature; +import org.dspace.app.rest.authorization.AuthorizationFeatureService; +import org.dspace.app.rest.converter.ConverterService; import org.dspace.app.rest.exception.UnprocessableEntityException; import org.dspace.app.rest.model.RegistrationRest; +import org.dspace.app.rest.model.SiteRest; +import org.dspace.app.rest.projection.Projection; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.authorize.AuthorizeException; +import org.dspace.content.Site; +import org.dspace.content.service.SiteService; import org.dspace.core.Context; import org.dspace.eperson.service.AccountService; import org.dspace.eperson.service.EPersonService; @@ -28,6 +35,7 @@ import org.springframework.data.rest.webmvc.ControllerUtils; import org.springframework.hateoas.ResourceSupport; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @@ -36,14 +44,14 @@ import org.springframework.web.bind.annotation.RestController; @RequestMapping("/api/" + RegistrationRest.CATEGORY + "/" + RegistrationRest.NAME_PLURAL) public class RegistrationRestController { -// @Autowired -// private AuthorizationFeatureService authorizationFeatureService; -// -// @Autowired -// private SiteService siteService; -// -// @Autowired -// private ConverterService converterService; + @Autowired + private AuthorizationFeatureService authorizationFeatureService; + + @Autowired + private SiteService siteService; + + @Autowired + private ConverterService converterService; @Autowired private AccountService accountService; @@ -56,13 +64,13 @@ public class RegistrationRestController { throws SQLException, IOException, MessagingException, AuthorizeException { Context context = ContextUtil.obtainContext(request); -// AuthorizationFeature epersonRegistration = authorizationFeatureService.find("epersonRegistration"); -// Site site = siteService.findSite(context); -// SiteRest siteRest = converterService.toRest(site, Projection.DEFAULT); -// if (!authorizationFeatureService.isAuthorized(context, epersonRegistration, siteRest)) { -// throw new AccessDeniedException("Registration is disabled, you are not authorized to create -// a new Authorization"); -// } + AuthorizationFeature epersonRegistration = authorizationFeatureService.find("epersonRegistration"); + Site site = siteService.findSite(context); + SiteRest siteRest = converterService.toRest(site, Projection.DEFAULT); + if (!authorizationFeatureService.isAuthorized(context, epersonRegistration, siteRest)) { + throw new AccessDeniedException( + "Registration is disabled, you are not authorized to create a new Authorization"); + } ObjectMapper mapper = new ObjectMapper(); RegistrationRest registrationRest; try { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EPersonRegistrationFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EPersonRegistrationFeature.java new file mode 100644 index 0000000000..44de9ce6d7 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EPersonRegistrationFeature.java @@ -0,0 +1,55 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.authorization.impl; + +import java.sql.SQLException; + +import org.dspace.app.rest.authorization.AuthorizationFeature; +import org.dspace.app.rest.authorization.AuthorizationFeatureDocumentation; +import org.dspace.app.rest.model.BaseObjectRest; +import org.dspace.app.rest.model.SiteRest; +import org.dspace.authenticate.service.AuthenticationService; +import org.dspace.core.Context; +import org.dspace.services.ConfigurationService; +import org.dspace.services.RequestService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +@AuthorizationFeatureDocumentation(name = EPersonRegistrationFeature.NAME, + description = "It can be used to register an eperson") +public class EPersonRegistrationFeature implements AuthorizationFeature { + + public static final String NAME = "epersonRegistration"; + + @Autowired + private ConfigurationService configurationService; + + @Autowired + private AuthenticationService authenticationService; + + @Autowired + private RequestService requestService; + + @Override + public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException { + if (!(object instanceof SiteRest)) { + return false; + } + if (configurationService.getBooleanProperty("user.registration", true)) { + return authenticationService + .allowSetPassword(context, requestService.getCurrentRequest().getHttpServletRequest(), null); + } + return false; + } + + @Override + public String[] getSupportedTypes() { + return new String[] {SiteRest.CATEGORY + "." + SiteRest.NAME}; + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java index 26abcb3075..0201eb284f 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java @@ -65,7 +65,7 @@ public class RegistrationRestControllerIT extends AbstractControllerIntegrationT getClient().perform(post("/api/eperson/registrations") .content(mapper.writeValueAsBytes(registrationRest)) .contentType(contentType)) - .andExpect(status().is(500)); + .andExpect(status().is(401)); assertTrue(registrationDataList.size() == 2); assertTrue(!StringUtils.equalsIgnoreCase(registrationDataList.get(0).getEmail(), newEmail) && diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EPersonRegistrationFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EPersonRegistrationFeatureIT.java new file mode 100644 index 0000000000..a9bbdce3db --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EPersonRegistrationFeatureIT.java @@ -0,0 +1,128 @@ +/** + * 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.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.dspace.app.rest.authorization.impl.EPersonRegistrationFeature; +import org.dspace.app.rest.converter.ConverterService; +import org.dspace.app.rest.converter.SiteConverter; +import org.dspace.app.rest.model.SiteRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.app.rest.utils.Utils; +import org.dspace.content.Site; +import org.dspace.content.service.SiteService; +import org.dspace.services.ConfigurationService; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +public class EPersonRegistrationFeatureIT extends AbstractControllerIntegrationTest { + + @Autowired + private AuthorizationFeatureService authorizationFeatureService; + + @Autowired + private ConverterService converterService; + + @Autowired + private ConfigurationService configurationService; + + @Autowired + private SiteService siteService; + + @Autowired + private SiteConverter siteConverter; + + @Autowired + private Utils utils; + + private AuthorizationFeature epersonRegistrationFeature; + + public static final String[] SHIB_ONLY = {"org.dspace.authenticate.ShibAuthentication"}; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + epersonRegistrationFeature = authorizationFeatureService.find(EPersonRegistrationFeature.NAME); + } + + @Test + public void userRegistrationEnabledSuccessTest() throws Exception { + + Site site = siteService.findSite(context); + SiteRest SiteRest = siteConverter.convert(site, Projection.DEFAULT); + String siteUri = utils.linkToSingleResource(SiteRest, "self").getHref(); + + // access the authorization for the admin user + String adminToken = getAuthToken(admin.getEmail(), password); + + getClient(adminToken).perform(get("/api/authz/authorizations/search/objectAndFeature") + .param("uri", siteUri) + .param("eperson", admin.getID().toString()) + .param("feature", epersonRegistrationFeature.getName())) + .andExpect(status().isOk()); + } + + @Test + public void userRegistrationDisabledUnAuthorizedTest() throws Exception { + + Site site = siteService.findSite(context); + SiteRest SiteRest = siteConverter.convert(site, Projection.DEFAULT); + String siteUri = utils.linkToSingleResource(SiteRest, "self").getHref(); + + context.turnOffAuthorisationSystem(); + configurationService.setProperty("user.registration", false); + context.restoreAuthSystemState(); + // access the authorization for the admin user + String adminToken = getAuthToken(admin.getEmail(), password); + + getClient(adminToken).perform(get("/api/authz/authorizations/search/objectAndFeature") + .param("uri", siteUri) + .param("eperson", admin.getID().toString()) + .param("feature", epersonRegistrationFeature.getName())) + .andExpect(status().isNoContent()); + + context.turnOffAuthorisationSystem(); + configurationService.setProperty("user.registration", true); + context.restoreAuthSystemState(); + } + + + @Test + public void userRegistrationEnabledShibTest() throws Exception { + + Site site = siteService.findSite(context); + SiteRest SiteRest = siteConverter.convert(site, Projection.DEFAULT); + String siteUri = utils.linkToSingleResource(SiteRest, "self").getHref(); + + // access the authorization for the admin user + String adminToken = getAuthToken(admin.getEmail(), password); + + getClient(adminToken).perform(get("/api/authz/authorizations/search/objectAndFeature") + .param("uri", siteUri) + .param("eperson", admin.getID().toString()) + .param("feature", epersonRegistrationFeature.getName())) + .andExpect(status().isOk()); + + context.turnOffAuthorisationSystem(); + //Enable Shibboleth and password login + configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", SHIB_ONLY); + context.restoreAuthSystemState(); + + getClient(adminToken).perform(get("/api/authz/authorizations/search/objectAndFeature") + .param("uri", siteUri) + .param("eperson", admin.getID().toString()) + .param("feature", epersonRegistrationFeature.getName())) + .andExpect(status().isNoContent()); + + } +} From b30f9d9338b9008e47442bf43e28f9313d58bdaa Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Thu, 16 Apr 2020 16:51:02 +0200 Subject: [PATCH 014/143] 70403: CC license (REST): New submission section --- .../license/CreativeCommonsServiceImpl.java | 54 +++++++++++++++++-- .../dspace/license/LicenseMetadataValue.java | 4 ++ .../service/CreativeCommonsService.java | 20 ++++++- .../app/rest/model/step/DataCCLicense.java | 44 +++++++++++++++ .../submit/AbstractRestProcessingStep.java | 1 + .../app/rest/submit/SubmissionService.java | 52 ++++++++++++------ .../app/rest/submit/step/CCLicenseStep.java | 44 +++++++++++++++ dspace/config/item-submission.xml | 8 +-- 8 files changed, 202 insertions(+), 25 deletions(-) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/model/step/DataCCLicense.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/CCLicenseStep.java diff --git a/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java b/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java index c0c190307b..928e4b6891 100644 --- a/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java @@ -235,15 +235,51 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi @Override public String getLicenseURL(Context context, Item item) throws SQLException, IOException, AuthorizeException { - String licenseUri = getCCField("uri").ccItemValue(item); + String licenseUri = getCCField("uri"); if (StringUtils.isNotBlank(licenseUri)) { - return licenseUri; + return getLicenseURI(item); } // JSPUI backward compatibility see https://jira.duraspace.org/browse/DS-2604 return getStringFromBitstream(context, item, BSN_LICENSE_URL); } + /** + * Returns the stored license uri of the item + * + * @param item - The item for which to retrieve the stored license uri + * @return the stored license uri of the item + */ + @Override + public String getLicenseURI(Item item) { + String licenseUriField = getCCField("uri"); + if (StringUtils.isNotBlank(licenseUriField)) { + String metadata = itemService.getMetadata(item, licenseUriField); + if (StringUtils.isNotBlank(metadata)) { + return metadata; + } + } + return null; + } + + /** + * Returns the stored license name of the item + * + * @param item - The item for which to retrieve the stored license name + * @return the stored license name of the item + */ + @Override + public String getLicenseName( Item item) { + String licenseNameField = getCCField("name"); + if (StringUtils.isNotBlank(licenseNameField)) { + String metadata = itemService.getMetadata(item, licenseNameField); + if (StringUtils.isNotBlank(metadata)) { + return metadata; + } + } + return null; + } + @Override public String fetchLicenseRDF(Document license) { StringWriter result = new StringWriter(); @@ -374,8 +410,8 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi * Returns a metadata field handle for given field Id */ @Override - public LicenseMetadataValue getCCField(String fieldId) { - return new LicenseMetadataValue(configurationService.getProperty("cc.license." + fieldId)); + public String getCCField(String fieldId) { + return configurationService.getProperty("cc.license." + fieldId); } @Override @@ -401,6 +437,7 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi * * @return A list of available CC Licenses */ + @Override public List findAllCCLicenses() { return findAllCCLicenses(defaultLanguage); } @@ -411,6 +448,7 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi * @param language - the language for which to find the CC Licenses * @return A list of available CC Licenses for the provided language */ + @Override public List findAllCCLicenses(String language) { if (!ccLicenses.containsKey(language)) { @@ -425,6 +463,7 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi * @param id - the ID of the license to be found * @return the corresponding license if found or null when not found */ + @Override public CCLicense findOne(String id) { return findOne(id, defaultLanguage); } @@ -436,6 +475,7 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi * @param language - the language for which to find the CC License * @return the corresponding license if found or null when not found */ + @Override public CCLicense findOne(String id, String language) { if (!ccLicenses.containsKey(language)) { initLicenses(language); @@ -465,6 +505,7 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi * @param answerMap - the answers to the different field questions * @return the corresponding license URI */ + @Override public String retrieveLicenseUri(String licenseId, Map answerMap) { return retrieveLicenseUri(licenseId, defaultLanguage, answerMap); @@ -478,6 +519,7 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi * @param answerMap - the answers to the different field questions * @return the corresponding license URI */ + @Override public String retrieveLicenseUri(String licenseId, String language, Map answerMap) { return ccLicenseConnectorService.retrieveRightsByQuestion(licenseId, language, answerMap); @@ -491,6 +533,7 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi * @param fullAnswerMap - the answers to the different field questions * @return whether the information is valid */ + @Override public boolean verifyLicenseInformation(String licenseId, Map fullAnswerMap) { return verifyLicenseInformation(licenseId, defaultLanguage, fullAnswerMap); } @@ -504,6 +547,7 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi * @param fullAnswerMap - the answers to the different field questions * @return whether the information is valid */ + @Override public boolean verifyLicenseInformation(String licenseId, String language, Map fullAnswerMap) { CCLicense ccLicense = findOne(licenseId, language); @@ -529,6 +573,7 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi * @param answerMap - the answers to the different field questions * @return the answerMap supplemented with all other license fields with a blank answer */ + @Override public Map retrieveFullAnswerMap(String licenseId, Map answerMap) { return retrieveFullAnswerMap(licenseId, defaultLanguage, answerMap); } @@ -542,6 +587,7 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi * @param answerMap - the answers to the different field questions * @return the answerMap supplemented with all other license fields with a blank answer for the provided language */ + @Override public Map retrieveFullAnswerMap(String licenseId, String language, Map answerMap) { CCLicense ccLicense = findOne(licenseId, language); if (ccLicense == null) { diff --git a/dspace-api/src/main/java/org/dspace/license/LicenseMetadataValue.java b/dspace-api/src/main/java/org/dspace/license/LicenseMetadataValue.java index ec5c9e447b..6c1d27d381 100644 --- a/dspace-api/src/main/java/org/dspace/license/LicenseMetadataValue.java +++ b/dspace-api/src/main/java/org/dspace/license/LicenseMetadataValue.java @@ -18,6 +18,8 @@ import org.dspace.content.MetadataValue; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.ItemService; import org.dspace.core.Context; +import org.dspace.license.factory.LicenseServiceFactory; +import org.dspace.license.service.CreativeCommonsService; /** * Helper class for using CC-related Metadata fields @@ -27,6 +29,7 @@ import org.dspace.core.Context; public class LicenseMetadataValue { protected final ItemService itemService; + protected final CreativeCommonsService creativeCommonsService; // Shibboleth for Creative Commons license data - i.e. characters that reliably indicate CC in a URI protected static final String ccShib = "creativecommons"; @@ -41,6 +44,7 @@ public class LicenseMetadataValue { params[3] = Item.ANY; } itemService = ContentServiceFactory.getInstance().getItemService(); + creativeCommonsService = LicenseServiceFactory.getInstance().getCreativeCommonsService(); } /** diff --git a/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java b/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java index cda546246f..3225393248 100644 --- a/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java +++ b/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java @@ -86,6 +86,24 @@ public interface CreativeCommonsService { public String getLicenseURL(Context context, Item item) throws SQLException, IOException, AuthorizeException; + + /** + * Returns the stored license uri of the item + * + * @param item - The item for which to retrieve the stored license uri + * @return the stored license uri of the item + */ + public String getLicenseURI(Item item); + + /** + * Returns the stored license name of the item + * + * @param item - The item for which to retrieve the stored license name + * @return the stored license name of the item + */ + public String getLicenseName(Item item); + + public String getLicenseRDF(Context context, Item item) throws SQLException, IOException, AuthorizeException; @@ -124,7 +142,7 @@ public interface CreativeCommonsService { * @param fieldId name of the property. * @return its value. */ - public LicenseMetadataValue getCCField(String fieldId); + public String getCCField(String fieldId); /** * Apply same transformation on the document to retrieve only the most diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/step/DataCCLicense.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/step/DataCCLicense.java new file mode 100644 index 0000000000..a8cfaf64f5 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/step/DataCCLicense.java @@ -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.model.step; + +import org.dspace.app.rest.model.BitstreamRest; + + +public class DataCCLicense implements SectionData { + + private String uri; + + private String rights; + + private BitstreamRest file; + + public String getUri() { + return uri; + } + + public void setUri(final String uri) { + this.uri = uri; + } + + public String getRights() { + return rights; + } + + public void setRights(final String rights) { + this.rights = rights; + } + + public BitstreamRest getFile() { + return file; + } + + public void setFile(final BitstreamRest file) { + this.file = file; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/AbstractRestProcessingStep.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/AbstractRestProcessingStep.java index 58bfaba27b..9989f6ca07 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/AbstractRestProcessingStep.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/AbstractRestProcessingStep.java @@ -35,6 +35,7 @@ public interface AbstractRestProcessingStep extends ListenerProcessingStep { public static final String UPLOAD_STEP_MOVE_OPERATION_ENTRY = "bitstreammove"; public static final String UPLOAD_STEP_ACCESSCONDITIONS_OPERATION_ENTRY = "accessConditions"; public static final String LICENSE_STEP_OPERATION_ENTRY = "granted"; + public static final String CCLICENSE_STEP_OPERATION_ENTRY = "ccLicense"; public static final String UPLOAD_STEP_METADATA_PATH = "metadata"; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/SubmissionService.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/SubmissionService.java index 2840035a7a..660c360f33 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/SubmissionService.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/SubmissionService.java @@ -25,6 +25,7 @@ import org.dspace.app.rest.model.CheckSumRest; import org.dspace.app.rest.model.MetadataValueRest; import org.dspace.app.rest.model.UploadBitstreamAccessConditionDTO; import org.dspace.app.rest.model.WorkspaceItemRest; +import org.dspace.app.rest.model.step.DataCCLicense; import org.dspace.app.rest.model.step.DataUpload; import org.dspace.app.rest.model.step.UploadBitstreamRest; import org.dspace.app.rest.projection.Projection; @@ -33,6 +34,8 @@ import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.ResourcePolicy; import org.dspace.content.Bitstream; import org.dspace.content.Collection; +import org.dspace.content.InProgressSubmission; +import org.dspace.content.Item; import org.dspace.content.MetadataValue; import org.dspace.content.WorkspaceItem; import org.dspace.content.service.CollectionService; @@ -41,6 +44,7 @@ import org.dspace.content.service.WorkspaceItemService; import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.core.Utils; +import org.dspace.license.service.CreativeCommonsService; import org.dspace.services.ConfigurationService; import org.dspace.services.RequestService; import org.dspace.services.model.Request; @@ -75,6 +79,8 @@ public class SubmissionService { @Autowired protected WorkflowService workflowService; @Autowired + protected CreativeCommonsService creativeCommonsService; + @Autowired private RequestService requestService; @Autowired private ConverterService converter; @@ -84,10 +90,8 @@ public class SubmissionService { /** * Create a workspaceitem using the information in the request * - * @param context - * the dspace context - * @param request - * the request containing the details about the workspace to create + * @param context the dspace context + * @param request the request containing the details about the workspace to create * @return * @throws SQLException * @throws AuthorizeException @@ -136,19 +140,19 @@ public class SubmissionService { } } -/** - * Build the rest representation of a bitstream as used in the upload section - * ({@link DataUpload}. It contains all its metadata and the list of applied - * access conditions (@link {@link UploadBitstreamAccessConditionDTO} - * - * @param configurationService the DSpace ConfigurationService - * @param source the bitstream to translate in its rest submission - * representation - * @return - * @throws SQLException - */ + /** + * Build the rest representation of a bitstream as used in the upload section + * ({@link DataUpload}. It contains all its metadata and the list of applied + * access conditions (@link {@link UploadBitstreamAccessConditionDTO} + * + * @param configurationService the DSpace ConfigurationService + * @param source the bitstream to translate in its rest submission + * representation + * @return + * @throws SQLException + */ public UploadBitstreamRest buildUploadBitstream(ConfigurationService configurationService, Bitstream source) - throws SQLException { + throws SQLException { UploadBitstreamRest data = new UploadBitstreamRest(); for (MetadataValue md : source.getMetadata()) { @@ -242,7 +246,7 @@ public class SubmissionService { wi = workflowService.start(context, wsi); } catch (IOException e) { throw new RuntimeException("The workflow could not be started for workspaceItem with" + - "id: " + id); + "id: " + id); } return wi; @@ -268,4 +272,18 @@ public class SubmissionService { public void saveWorkflowItem(Context context, XmlWorkflowItem source) throws SQLException, AuthorizeException { workflowItemService.update(context, source); } + + public DataCCLicense getDataCCLicense(InProgressSubmission obj) + throws SQLException, IOException, AuthorizeException { + DataCCLicense result = new DataCCLicense(); + Item item = obj.getItem(); + + result.setUri(creativeCommonsService.getLicenseURI(item)); + result.setRights(creativeCommonsService.getLicenseName(item)); + + Bitstream licenseRdfBitstream = creativeCommonsService.getLicenseRdfBitstream(item); + result.setFile(converter.toRest(licenseRdfBitstream, Projection.DEFAULT)); + + return result; + } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/CCLicenseStep.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/CCLicenseStep.java new file mode 100644 index 0000000000..4dcc319a3d --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/CCLicenseStep.java @@ -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.submit.step; + +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.model.step.DataCCLicense; +import org.dspace.app.rest.submit.AbstractRestProcessingStep; +import org.dspace.app.rest.submit.SubmissionService; +import org.dspace.app.rest.submit.factory.PatchOperationFactory; +import org.dspace.app.rest.submit.factory.impl.PatchOperation; +import org.dspace.app.util.SubmissionStepConfig; +import org.dspace.content.InProgressSubmission; +import org.dspace.core.Context; +import org.dspace.services.model.Request; + + +public class CCLicenseStep extends org.dspace.submit.step.CCLicenseStep implements AbstractRestProcessingStep { + + @Override + public DataCCLicense getData(SubmissionService submissionService, InProgressSubmission obj, + SubmissionStepConfig config) + throws Exception { + return submissionService.getDataCCLicense(obj); + } + + + @Override + public void doPatchProcessing(Context context, Request currentRequest, InProgressSubmission source, Operation op) + throws Exception { + + if (op.getPath().endsWith(CCLICENSE_STEP_OPERATION_ENTRY)) { + + PatchOperation patchOperation = new PatchOperationFactory() + .instanceOf(CCLICENSE_STEP_OPERATION_ENTRY, op.getOp()); + patchOperation.perform(context, currentRequest, source, op); + + } + } +} diff --git a/dspace/config/item-submission.xml b/dspace/config/item-submission.xml index af707162ae..d1f755d293 100644 --- a/dspace/config/item-submission.xml +++ b/dspace/config/item-submission.xml @@ -115,9 +115,9 @@ - + submit.progressbar.CClicense + org.dspace.app.rest.submit.step.CCLicenseStep + cclicense + + From 5800bef325d38876b383fad6941a273e38069c4c Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Fri, 17 Apr 2020 13:13:40 +0200 Subject: [PATCH 015/143] 70403: Add javadocs, undo line adjustments, LicenseMetadataValue changes --- .../dspace/license/LicenseMetadataValue.java | 4 ---- .../app/rest/model/step/DataCCLicense.java | 4 +++- .../app/rest/submit/SubmissionService.java | 15 ++++++++++-- .../app/rest/submit/step/CCLicenseStep.java | 23 ++++++++++++++++++- 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/license/LicenseMetadataValue.java b/dspace-api/src/main/java/org/dspace/license/LicenseMetadataValue.java index 6c1d27d381..ec5c9e447b 100644 --- a/dspace-api/src/main/java/org/dspace/license/LicenseMetadataValue.java +++ b/dspace-api/src/main/java/org/dspace/license/LicenseMetadataValue.java @@ -18,8 +18,6 @@ import org.dspace.content.MetadataValue; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.ItemService; import org.dspace.core.Context; -import org.dspace.license.factory.LicenseServiceFactory; -import org.dspace.license.service.CreativeCommonsService; /** * Helper class for using CC-related Metadata fields @@ -29,7 +27,6 @@ import org.dspace.license.service.CreativeCommonsService; public class LicenseMetadataValue { protected final ItemService itemService; - protected final CreativeCommonsService creativeCommonsService; // Shibboleth for Creative Commons license data - i.e. characters that reliably indicate CC in a URI protected static final String ccShib = "creativecommons"; @@ -44,7 +41,6 @@ public class LicenseMetadataValue { params[3] = Item.ANY; } itemService = ContentServiceFactory.getInstance().getItemService(); - creativeCommonsService = LicenseServiceFactory.getInstance().getCreativeCommonsService(); } /** diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/step/DataCCLicense.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/step/DataCCLicense.java index a8cfaf64f5..32b3710d7c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/step/DataCCLicense.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/step/DataCCLicense.java @@ -9,7 +9,9 @@ package org.dspace.app.rest.model.step; import org.dspace.app.rest.model.BitstreamRest; - +/** + * Java Bean to expose the section creativecommons representing the CC License during in progress submission. + */ public class DataCCLicense implements SectionData { private String uri; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/SubmissionService.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/SubmissionService.java index 660c360f33..0ac468448b 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/SubmissionService.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/SubmissionService.java @@ -90,8 +90,10 @@ public class SubmissionService { /** * Create a workspaceitem using the information in the request * - * @param context the dspace context - * @param request the request containing the details about the workspace to create + * @param context + * the dspace context + * @param request + * the request containing the details about the workspace to create * @return * @throws SQLException * @throws AuthorizeException @@ -273,6 +275,15 @@ public class SubmissionService { workflowItemService.update(context, source); } + /** + * Builds the CC License data of an inprogress submission based on the cc license info present in the metadata + * + * @param obj - the in progress submission + * @return an object representing the CC License data + * @throws SQLException + * @throws IOException + * @throws AuthorizeException + */ public DataCCLicense getDataCCLicense(InProgressSubmission obj) throws SQLException, IOException, AuthorizeException { DataCCLicense result = new DataCCLicense(); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/CCLicenseStep.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/CCLicenseStep.java index 4dcc319a3d..75f1949116 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/CCLicenseStep.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/CCLicenseStep.java @@ -18,9 +18,21 @@ import org.dspace.content.InProgressSubmission; import org.dspace.core.Context; import org.dspace.services.model.Request; - +/** + * CC License step for DSpace Spring Rest. Expose the creative commons license information about the in progress + * submission. + */ public class CCLicenseStep extends org.dspace.submit.step.CCLicenseStep implements AbstractRestProcessingStep { + /** + * Retrieves the CC License data of the in progress submission + * + * @param submissionService the submission service + * @param obj the in progress submission + * @param config the submission step configuration + * @return the CC License data of the in progress submission + * @throws Exception + */ @Override public DataCCLicense getData(SubmissionService submissionService, InProgressSubmission obj, SubmissionStepConfig config) @@ -29,6 +41,15 @@ public class CCLicenseStep extends org.dspace.submit.step.CCLicenseStep implemen } + /** + * Processes a patch for the CC License data + * + * @param context the DSpace context + * @param currentRequest the http request + * @param source the in progress submission + * @param op the json patch operation + * @throws Exception + */ @Override public void doPatchProcessing(Context context, Request currentRequest, InProgressSubmission source, Operation op) throws Exception { From fee24e3f96dba9f2a21b97aabae29485915a3fca Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Fri, 17 Apr 2020 14:14:25 +0200 Subject: [PATCH 016/143] [Task 70398] implementing change password with token patch and added ITs for the functionality --- .../repository/EPersonRestRepository.java | 14 ++ .../EPersonPasswordReplaceOperation.java | 39 ++++ .../app/rest/EPersonRestRepositoryIT.java | 213 ++++++++++++++++++ 3 files changed, 266 insertions(+) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java index 073d1b25bd..aa2526c0fe 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java @@ -19,6 +19,7 @@ import org.dspace.app.rest.Parameter; import org.dspace.app.rest.SearchRestMethod; import org.dspace.app.rest.exception.UnprocessableEntityException; import org.dspace.app.rest.model.EPersonRest; +import org.dspace.app.rest.model.patch.Operation; import org.dspace.app.rest.model.patch.Patch; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.service.AuthorizeService; @@ -28,6 +29,7 @@ import org.dspace.eperson.service.EPersonService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Component; @@ -167,6 +169,18 @@ public class EPersonRestRepository extends DSpaceObjectRestRepository extends PatchOperation { + private static final Logger log = org.apache.logging.log4j.LogManager + .getLogger(EPersonPasswordReplaceOperation.class); + /** * Path in json body of patch that uses this operation */ public static final String OPERATION_PASSWORD_CHANGE = "/password"; protected EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService(); + @Autowired + private RequestService requestService; + + @Autowired + private AccountService accountService; + @Override public R perform(Context context, R object, Operation operation) { checkOperationValue(operation.getValue()); if (supports(object, operation)) { EPerson eperson = (EPerson) object; + String token = requestService.getCurrentRequest().getHttpServletRequest().getParameter("token"); checkModelForExistingValue(eperson); + if (StringUtils.isNotBlank(token)) { + patchWithToken(context,eperson, token, operation); + } ePersonService.setPassword(eperson, (String) operation.getValue()); return object; } else { @@ -46,6 +68,23 @@ public class EPersonPasswordReplaceOperation extends PatchOperation { } } + private void patchWithToken(Context context, EPerson eperson, String token, Operation operation) { + try { + EPerson ePersonFromToken = accountService.getEPerson(context, token); + if (ePersonFromToken == null) { + throw new AccessDeniedException("The token in the parameter: " + token + " couldn't" + + " be associated with an EPerson"); + } + if (!ePersonFromToken.getID().equals(eperson.getID())) { + throw new AccessDeniedException("The token in the parameter belongs to a different EPerson" + + " than the uri indicates"); + } + accountService.deleteToken(context, token); + } catch (SQLException | AuthorizeException e) { + log.error(e.getMessage(), e); + } + } + /** * Checks whether the ePerson has a password via the ePersonService to checking if it has a non null password hash * throws a DSpaceBadRequestException if not pw hash was present diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java index 9610287273..7b6513f766 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java @@ -14,6 +14,8 @@ import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; @@ -28,6 +30,7 @@ import java.util.UUID; import javax.ws.rs.core.MediaType; import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; import org.dspace.app.rest.builder.CollectionBuilder; import org.dspace.app.rest.builder.CommunityBuilder; import org.dspace.app.rest.builder.EPersonBuilder; @@ -39,6 +42,7 @@ import org.dspace.app.rest.matcher.HalMatcher; import org.dspace.app.rest.model.EPersonRest; import org.dspace.app.rest.model.MetadataRest; import org.dspace.app.rest.model.MetadataValueRest; +import org.dspace.app.rest.model.RegistrationRest; import org.dspace.app.rest.model.patch.Operation; import org.dspace.app.rest.model.patch.ReplaceOperation; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; @@ -47,12 +51,25 @@ import org.dspace.content.Collection; import org.dspace.content.Item; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; +import org.dspace.eperson.PasswordHash; +import org.dspace.eperson.service.AccountService; +import org.dspace.eperson.service.EPersonService; +import org.dspace.eperson.service.RegistrationDataService; import org.hamcrest.Matchers; import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { + @Autowired + private AccountService accountService; + + @Autowired + private RegistrationDataService registrationDataService; + + @Autowired + private EPersonService ePersonService; @Test public void createTest() throws Exception { @@ -1590,4 +1607,200 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { ); } + + @Test + public void patchReplacePasswordWithToken() throws Exception { + context.turnOffAuthorisationSystem(); + + EPerson ePerson = EPersonBuilder.createEPerson(context) + .withNameInMetadata("John", "Doe") + .withEmail("Johndoe@fake-email.com") + .withPassword(password) + .build(); + + String newPassword = "newpassword"; + + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + ReplaceOperation replaceOperation = new ReplaceOperation("/password", newPassword); + ops.add(replaceOperation); + String patchBody = getPatchContent(ops); + accountService.sendRegistrationInfo(context, ePerson.getEmail()); + String tokenForEPerson = registrationDataService.findByEmail(context, ePerson.getEmail()).getToken(); + String token = getAuthToken(admin.getEmail(), password); + PasswordHash oldPassword = ePersonService.getPasswordHash(ePerson); + // updates password + getClient(token).perform(patch("/api/eperson/epersons/" + ePerson.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON) + .param("token", tokenForEPerson)) + .andExpect(status().isOk()); + + PasswordHash newPasswordHash = ePersonService.getPasswordHash(ePerson); + assertFalse(oldPassword.equals(newPasswordHash)); + assertTrue(registrationDataService.findByEmail(context, ePerson.getEmail()) == null); + } + + + @Test + public void patchReplacePasswordWithRandomTokenPatchFail() throws Exception { + context.turnOffAuthorisationSystem(); + + EPerson ePerson = EPersonBuilder.createEPerson(context) + .withNameInMetadata("John", "Doe") + .withEmail("Johndoe@fake-email.com") + .withPassword(password) + .build(); + + String newPassword = "newpassword"; + + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + ReplaceOperation replaceOperation = new ReplaceOperation("/password", newPassword); + ops.add(replaceOperation); + String patchBody = getPatchContent(ops); + accountService.sendRegistrationInfo(context, ePerson.getEmail()); + String tokenForEPerson = registrationDataService.findByEmail(context, ePerson.getEmail()).getToken(); + String token = getAuthToken(admin.getEmail(), password); + PasswordHash oldPassword = ePersonService.getPasswordHash(ePerson); + // updates password + getClient(token).perform(patch("/api/eperson/epersons/" + ePerson.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON) + .param("token", "RandomToken")) + .andExpect(status().isForbidden()); + + PasswordHash newPasswordHash = ePersonService.getPasswordHash(ePerson); + assertTrue(StringUtils.equalsIgnoreCase(oldPassword.getHashString(),newPasswordHash.getHashString())); + assertFalse(registrationDataService.findByEmail(context, ePerson.getEmail()) == null); + assertTrue(StringUtils.equals(registrationDataService.findByEmail(context, ePerson.getEmail()).getToken(), + tokenForEPerson)); + } + + @Test + public void patchReplacePasswordWithOtherUserTokenFail() throws Exception { + context.turnOffAuthorisationSystem(); + + EPerson ePerson = EPersonBuilder.createEPerson(context) + .withNameInMetadata("John", "Doe") + .withEmail("Johndoe@fake-email.com") + .withPassword(password) + .build(); + + + EPerson ePersonTwo = EPersonBuilder.createEPerson(context) + .withNameInMetadata("Smith", "Donald") + .withEmail("donaldSmith@fake-email.com") + .withPassword(password) + .build(); + + String newPassword = "newpassword"; + + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + ReplaceOperation replaceOperation = new ReplaceOperation("/password", newPassword); + ops.add(replaceOperation); + String patchBody = getPatchContent(ops); + accountService.sendRegistrationInfo(context, ePerson.getEmail()); + accountService.sendRegistrationInfo(context, ePersonTwo.getEmail()); + String tokenForEPerson = registrationDataService.findByEmail(context, ePerson.getEmail()).getToken(); + String tokenForEPersonTwo = registrationDataService.findByEmail(context, ePersonTwo.getEmail()).getToken(); + + String token = getAuthToken(admin.getEmail(), password); + PasswordHash oldPassword = ePersonService.getPasswordHash(ePerson); + // updates password + getClient(token).perform(patch("/api/eperson/epersons/" + ePerson.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON) + .param("token", tokenForEPersonTwo)) + .andExpect(status().isForbidden()); + + PasswordHash newPasswordHash = ePersonService.getPasswordHash(ePerson); + assertTrue(StringUtils.equalsIgnoreCase(oldPassword.getHashString(),newPasswordHash.getHashString())); + assertFalse(registrationDataService.findByEmail(context, ePerson.getEmail()) == null); + } + + @Test + public void patchReplaceEmailWithTokenFail() throws Exception { + context.turnOffAuthorisationSystem(); + + String originalEmail = "Johndoe@fake-email.com"; + EPerson ePerson = EPersonBuilder.createEPerson(context) + .withNameInMetadata("John", "Doe") + .withEmail(originalEmail) + .withPassword(password) + .build(); + + String newEmail = "johnyandmaria@fake-email.com"; + + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + ReplaceOperation replaceOperation = new ReplaceOperation("/email", newEmail); + ops.add(replaceOperation); + String patchBody = getPatchContent(ops); + accountService.sendRegistrationInfo(context, ePerson.getEmail()); + String tokenForEPerson = registrationDataService.findByEmail(context, ePerson.getEmail()).getToken(); + String token = getAuthToken(admin.getEmail(), password); + PasswordHash oldPassword = ePersonService.getPasswordHash(ePerson); + // updates password + getClient(token).perform(patch("/api/eperson/epersons/" + ePerson.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON) + .param("token", tokenForEPerson)) + .andExpect(status().isForbidden()); + + PasswordHash newPasswordHash = ePersonService.getPasswordHash(ePerson); + assertTrue(StringUtils.equalsIgnoreCase(oldPassword.getHashString(),newPasswordHash.getHashString())); + assertFalse(registrationDataService.findByEmail(context, ePerson.getEmail()) == null); + assertTrue(StringUtils.equalsIgnoreCase(ePerson.getEmail(), originalEmail)); + } + + @Test + public void registerNewAccountPatchUpdatePasswordRandomUserUuidFail() throws Exception { + context.turnOffAuthorisationSystem(); + + ObjectMapper mapper = new ObjectMapper(); + String newRegisterEmail = "new-register@fake-email.com"; + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(newRegisterEmail); + getClient().perform(post("/api/eperson/registrations") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsBytes(registrationRest))) + .andExpect(status().isCreated()); + + EPerson ePerson = EPersonBuilder.createEPerson(context) + .withNameInMetadata("John", "Doe") + .withEmail("Johndoe@fake-email.com") + .withPassword(password) + .build(); + + String newPassword = "newpassword"; + + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + ReplaceOperation replaceOperation = new ReplaceOperation("/password", newPassword); + ops.add(replaceOperation); + String patchBody = getPatchContent(ops); + accountService.sendRegistrationInfo(context, ePerson.getEmail()); + String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); + String token = getAuthToken(admin.getEmail(), password); + PasswordHash oldPassword = ePersonService.getPasswordHash(ePerson); + // updates password + getClient(token).perform(patch("/api/eperson/epersons/" + ePerson.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON) + .param("token", newRegisterToken)) + .andExpect(status().isForbidden()); + + PasswordHash newPasswordHash = ePersonService.getPasswordHash(ePerson); + assertTrue(StringUtils.equalsIgnoreCase(oldPassword.getHashString(),newPasswordHash.getHashString())); + assertFalse(registrationDataService.findByEmail(context, ePerson.getEmail()) == null); + assertFalse(registrationDataService.findByEmail(context, newRegisterEmail) == null); + } + } From cb8b5c8efc3baaa535b1545f31313c8a3364e02f Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Mon, 20 Apr 2020 11:31:34 +0200 Subject: [PATCH 017/143] [Task 70399] created the POST EPerson endpoint with token and added tests --- .../dspace/app/rest/model/EPersonRest.java | 6 +- .../repository/EPersonRestRepository.java | 95 ++++- .../app/rest/EPersonRestRepositoryIT.java | 365 ++++++++++++++++++ 3 files changed, 462 insertions(+), 4 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/EPersonRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/EPersonRest.java index 00881b9fd1..7b4c683322 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/EPersonRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/EPersonRest.java @@ -41,7 +41,7 @@ public class EPersonRest extends DSpaceObjectRest { private boolean requireCertificate = false; - private boolean selfRegistered = false; + private Boolean selfRegistered; @JsonProperty(access = Access.WRITE_ONLY) private String password; @@ -92,11 +92,11 @@ public class EPersonRest extends DSpaceObjectRest { this.requireCertificate = requireCertificate; } - public boolean isSelfRegistered() { + public Boolean isSelfRegistered() { return selfRegistered; } - public void setSelfRegistered(boolean selfRegistered) { + public void setSelfRegistered(Boolean selfRegistered) { this.selfRegistered = selfRegistered; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java index aa2526c0fe..1309e11713 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java @@ -15,17 +15,29 @@ import javax.servlet.http.HttpServletRequest; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.StringUtils; +import org.apache.log4j.Logger; import org.dspace.app.rest.Parameter; import org.dspace.app.rest.SearchRestMethod; +import org.dspace.app.rest.authorization.AuthorizationFeature; +import org.dspace.app.rest.authorization.AuthorizationFeatureService; import org.dspace.app.rest.exception.UnprocessableEntityException; import org.dspace.app.rest.model.EPersonRest; +import org.dspace.app.rest.model.MetadataRest; +import org.dspace.app.rest.model.MetadataValueRest; +import org.dspace.app.rest.model.SiteRest; import org.dspace.app.rest.model.patch.Operation; import org.dspace.app.rest.model.patch.Patch; +import org.dspace.app.rest.projection.Projection; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.Site; +import org.dspace.content.service.SiteService; import org.dspace.core.Context; import org.dspace.eperson.EPerson; +import org.dspace.eperson.RegistrationData; +import org.dspace.eperson.service.AccountService; import org.dspace.eperson.service.EPersonService; +import org.dspace.eperson.service.RegistrationDataService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -43,9 +55,23 @@ import org.springframework.stereotype.Component; @Component(EPersonRest.CATEGORY + "." + EPersonRest.NAME) public class EPersonRestRepository extends DSpaceObjectRestRepository { + private static final Logger log = Logger.getLogger(EPersonRestRepository.class); + @Autowired AuthorizeService authorizeService; + @Autowired + private AccountService accountService; + + @Autowired + private AuthorizationFeatureService authorizationFeatureService; + + @Autowired + private SiteService siteService; + + @Autowired + private RegistrationDataService registrationDataService; + private final EPersonService es; @@ -66,7 +92,21 @@ public class EPersonRestRepository extends DSpaceObjectRestRepository epersonFirstName = metadataRest.getMap().get("eperson.firstname"); + List epersonLastName = metadataRest.getMap().get("eperson.lastname"); + if (epersonFirstName == null || epersonLastName == null || + epersonFirstName.isEmpty() || epersonLastName.isEmpty()) { + throw new AccessDeniedException("The eperson.firstname and eperson.lastname values need to be " + + "filled in"); + } + } + String password = epersonRest.getPassword(); + if (StringUtils.isBlank(password)) { + throw new AccessDeniedException("the password cannot be left blank"); + } } @Override diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java index 7b6513f766..5a1d47680c 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java @@ -15,6 +15,8 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -26,6 +28,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.UUID; import javax.ws.rs.core.MediaType; @@ -58,6 +61,7 @@ import org.dspace.eperson.service.RegistrationDataService; import org.hamcrest.Matchers; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.servlet.MvcResult; public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { @@ -1803,4 +1807,365 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { assertFalse(registrationDataService.findByEmail(context, newRegisterEmail) == null); } + @Test + public void postEPersonWithTokenWithoutEmailProperty() throws Exception { + + ObjectMapper mapper = new ObjectMapper(); + + String newRegisterEmail = "new-register@fake-email.com"; + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(newRegisterEmail); + getClient().perform(post("/api/eperson/registrations") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsBytes(registrationRest))) + .andExpect(status().isCreated()); + String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); + + String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]," + + "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"password\":\"somePassword\"," + + "\"type\":\"eperson\"}"; + + String token = getAuthToken(admin.getEmail(), password); + MvcResult mvcResult = getClient(token).perform(post("/api/eperson/epersons") + .param("token", newRegisterToken) + .content(json) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$", Matchers.allOf( + hasJsonPath("$.uuid", not(empty())), + // is it what you expect? EPerson.getName() returns the email... + //hasJsonPath("$.name", is("Doe John")), + hasJsonPath("$.type", is("eperson")), + hasJsonPath("$._links.self.href", not(empty())), + hasJsonPath("$.metadata", Matchers.allOf( + matchMetadata("eperson.firstname", "John"), + matchMetadata("eperson.lastname", "Doe") + ))))).andReturn(); + + String content = mvcResult.getResponse().getContentAsString(); + Map map = mapper.readValue(content, Map.class); + String epersonUuid = String.valueOf(map.get("uuid")); + EPerson createdEPerson = ePersonService.find(context, UUID.fromString(epersonUuid)); + assertTrue(ePersonService.checkPassword(context, createdEPerson, "somePassword")); + + assertNull(registrationDataService.findByToken(context, newRegisterToken)); + context.turnOffAuthorisationSystem(); + ePersonService.delete(context, createdEPerson); + context.restoreAuthSystemState(); + } + + @Test + public void postEPersonWithTokenWithEmailProperty() throws Exception { + + ObjectMapper mapper = new ObjectMapper(); + + String newRegisterEmail = "new-register@fake-email.com"; + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(newRegisterEmail); + getClient().perform(post("/api/eperson/registrations") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsBytes(registrationRest))) + .andExpect(status().isCreated()); + String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); + + String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]," + + "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"email\":\"" + newRegisterEmail + + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; + + String token = getAuthToken(admin.getEmail(), password); + MvcResult mvcResult = getClient(token).perform(post("/api/eperson/epersons") + .param("token", newRegisterToken) + .content(json) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$", Matchers.allOf( + hasJsonPath("$.uuid", not(empty())), + // is it what you expect? EPerson.getName() returns the email... + //hasJsonPath("$.name", is("Doe John")), + hasJsonPath("$.email", is(newRegisterEmail)), + hasJsonPath("$.type", is("eperson")), + hasJsonPath("$._links.self.href", not(empty())), + hasJsonPath("$.metadata", Matchers.allOf( + matchMetadata("eperson.firstname", "John"), + matchMetadata("eperson.lastname", "Doe") + ))))).andReturn(); + + String content = mvcResult.getResponse().getContentAsString(); + Map map = mapper.readValue(content, Map.class); + String epersonUuid = String.valueOf(map.get("uuid")); + EPerson createdEPerson = ePersonService.find(context, UUID.fromString(epersonUuid)); + assertTrue(ePersonService.checkPassword(context, createdEPerson, "somePassword")); + assertNull(registrationDataService.findByToken(context, newRegisterToken)); + + context.turnOffAuthorisationSystem(); + ePersonService.delete(context, createdEPerson); + context.restoreAuthSystemState(); + } + + @Test + public void postEPersonWithTokenWithEmailAndSelfRegisteredProperty() throws Exception { + + ObjectMapper mapper = new ObjectMapper(); + + String newRegisterEmail = "new-register@fake-email.com"; + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(newRegisterEmail); + getClient().perform(post("/api/eperson/registrations") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsBytes(registrationRest))) + .andExpect(status().isCreated()); + String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); + + String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]," + + "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"selfRegistered\":true,\"email\":\"" + newRegisterEmail + + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; + + String token = getAuthToken(admin.getEmail(), password); + MvcResult mvcResult = getClient(token).perform(post("/api/eperson/epersons") + .param("token", newRegisterToken) + .content(json) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$", Matchers.allOf( + hasJsonPath("$.uuid", not(empty())), + // is it what you expect? EPerson.getName() returns the email... + //hasJsonPath("$.name", is("Doe John")), + hasJsonPath("$.email", is(newRegisterEmail)), + hasJsonPath("$.type", is("eperson")), + hasJsonPath("$._links.self.href", not(empty())), + hasJsonPath("$.metadata", Matchers.allOf( + matchMetadata("eperson.firstname", "John"), + matchMetadata("eperson.lastname", "Doe") + ))))).andReturn(); + + String content = mvcResult.getResponse().getContentAsString(); + Map map = mapper.readValue(content, Map.class); + String epersonUuid = String.valueOf(map.get("uuid")); + EPerson createdEPerson = ePersonService.find(context, UUID.fromString(epersonUuid)); + assertTrue(ePersonService.checkPassword(context, createdEPerson, "somePassword")); + assertNull(registrationDataService.findByToken(context, newRegisterToken)); + + context.turnOffAuthorisationSystem(); + ePersonService.delete(context, createdEPerson); + context.restoreAuthSystemState(); + } + + @Test + public void postEPersonWithTokenWithTwoTokensDifferentEmailProperty() throws Exception { + + ObjectMapper mapper = new ObjectMapper(); + + String newRegisterEmail = "new-register@fake-email.com"; + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(newRegisterEmail); + getClient().perform(post("/api/eperson/registrations") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsBytes(registrationRest))) + .andExpect(status().isCreated()); + String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); + + String newRegisterEmailTwo = "new-register-two@fake-email.com"; + RegistrationRest registrationRestTwo = new RegistrationRest(); + registrationRestTwo.setEmail(newRegisterEmailTwo); + getClient().perform(post("/api/eperson/registrations") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsBytes(registrationRestTwo))) + .andExpect(status().isCreated()); + String newRegisterTokenTwo = registrationDataService.findByEmail(context, newRegisterEmailTwo).getToken(); + + + String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]," + + "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"email\":\"" + newRegisterEmailTwo + + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; + + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(post("/api/eperson/epersons") + .param("token", newRegisterToken) + .content(json) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + + EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmailTwo); + assertNull(createdEPerson); + assertNotNull(registrationDataService.findByToken(context, newRegisterToken)); + assertNotNull(registrationDataService.findByToken(context, newRegisterTokenTwo)); + } + + @Test + public void postEPersonWithRandomTokenWithEmailProperty() throws Exception { + + ObjectMapper mapper = new ObjectMapper(); + + String newRegisterEmail = "new-register@fake-email.com"; + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(newRegisterEmail); + getClient().perform(post("/api/eperson/registrations") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsBytes(registrationRest))) + .andExpect(status().isCreated()); + String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); + + String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]," + + "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"email\":\"" + newRegisterEmail + + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; + + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(post("/api/eperson/epersons") + .param("token", "randomToken") + .content(json) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + + EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail); + assertNull(createdEPerson); + assertNotNull(registrationDataService.findByToken(context, newRegisterToken)); + } + + @Test + public void postEPersonWithTokenWithEmailAndSelfRegisteredFalseProperty() throws Exception { + + ObjectMapper mapper = new ObjectMapper(); + + String newRegisterEmail = "new-register@fake-email.com"; + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(newRegisterEmail); + getClient().perform(post("/api/eperson/registrations") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsBytes(registrationRest))) + .andExpect(status().isCreated()); + String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); + + String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]," + + "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"selfRegistered\":false,\"email\":\"" + newRegisterEmail + + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; + + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(post("/api/eperson/epersons") + .param("token", newRegisterToken) + .content(json) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + + EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail); + assertNull(createdEPerson); + assertNotNull(registrationDataService.findByToken(context, newRegisterToken)); + } + + @Test + public void postEPersonWithTokenWithoutLastNameProperty() throws Exception { + + ObjectMapper mapper = new ObjectMapper(); + + String newRegisterEmail = "new-register@fake-email.com"; + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(newRegisterEmail); + getClient().perform(post("/api/eperson/registrations") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsBytes(registrationRest))) + .andExpect(status().isCreated()); + String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); + + String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]},\"selfRegistered\":true," + + "\"email\":\"" + newRegisterEmail + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; + + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(post("/api/eperson/epersons") + .param("token", newRegisterToken) + .content(json) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + + EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail); + assertNull(createdEPerson); + assertNotNull(registrationDataService.findByToken(context, newRegisterToken)); + } + + @Test + public void postEPersonWithTokenWithoutFirstNameProperty() throws Exception { + + ObjectMapper mapper = new ObjectMapper(); + + String newRegisterEmail = "new-register@fake-email.com"; + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(newRegisterEmail); + getClient().perform(post("/api/eperson/registrations") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsBytes(registrationRest))) + .andExpect(status().isCreated()); + String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); + + String json = "{\"metadata\":{\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"selfRegistered\":true," + + "\"email\":\"" + newRegisterEmail + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; + + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(post("/api/eperson/epersons") + .param("token", newRegisterToken) + .content(json) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + + EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail); + assertNull(createdEPerson); + assertNotNull(registrationDataService.findByToken(context, newRegisterToken)); + } + + @Test + public void postEPersonWithTokenWithoutPasswordProperty() throws Exception { + + ObjectMapper mapper = new ObjectMapper(); + + String newRegisterEmail = "new-register@fake-email.com"; + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(newRegisterEmail); + getClient().perform(post("/api/eperson/registrations") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsBytes(registrationRest))) + .andExpect(status().isCreated()); + String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); + + String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]," + + "\"eperson.lastname\":[{\"value\":\"Doe\"}]}," + + "\"type\":\"eperson\"}"; + + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(post("/api/eperson/epersons") + .param("token", newRegisterToken) + .content(json) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + + EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail); + assertNull(createdEPerson); + assertNotNull(registrationDataService.findByToken(context, newRegisterToken)); + } + + @Test + public void postEPersonWithWrongToken() throws Exception { + + ObjectMapper mapper = new ObjectMapper(); + String newEmail = "new-email@fake-email.com"; + + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(eperson.getEmail()); + getClient().perform(post("/api/eperson/registrations") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsBytes(registrationRest))) + .andExpect(status().isCreated()); + String forgotPasswordToken = registrationDataService.findByEmail(context, eperson.getEmail()).getToken(); + + String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]," + + "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"selfRegistered\":true,\"password\":\"somePassword\"," + + "\"type\":\"eperson\"}"; + + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(post("/api/eperson/epersons") + .param("token", forgotPasswordToken) + .content(json) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + + EPerson createdEPerson = ePersonService.findByEmail(context, newEmail); + assertNull(createdEPerson); + assertNotNull(registrationDataService.findByToken(context, forgotPasswordToken)); + } } From 0920de7b21e7f5ccb6d88ae2128e6b533e0910bd Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Mon, 20 Apr 2020 12:57:49 +0200 Subject: [PATCH 018/143] 70404: CC license (REST): Patch submission (Add) --- .../license/CCLicenseConnectorService.java | 21 ++++ .../CCLicenseConnectorServiceImpl.java | 55 +++++++- .../license/CreativeCommonsServiceImpl.java | 118 ++++++++++++++++-- .../service/CreativeCommonsService.java | 44 ++++++- .../submit/AbstractRestProcessingStep.java | 2 +- .../impl/CCLicenseAddPatchOperation.java | 66 ++++++++++ .../spring/spring-dspace-core-services.xml | 4 + dspace/config/item-submission.xml | 4 +- 8 files changed, 296 insertions(+), 18 deletions(-) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/CCLicenseAddPatchOperation.java diff --git a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorService.java b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorService.java index 48ed5f7200..0c061d2d64 100644 --- a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorService.java +++ b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorService.java @@ -7,8 +7,11 @@ */ package org.dspace.license; +import java.io.IOException; import java.util.Map; +import org.jdom.Document; + /** * Service interface class for the Creative commons license connector service. * The implementation of this class is responsible for all the calls to the CC license API and parsing the response @@ -27,6 +30,7 @@ public interface CCLicenseConnectorService { /** * Retrieve the CC License URI based on the provided license id, language and answers to the field questions from * the CC License API + * * @param licenseId - the ID of the license * @param language - the language for which to retrieve the full answerMap * @param answerMap - the answers to the different field questions @@ -36,4 +40,21 @@ public interface CCLicenseConnectorService { String language, Map answerMap); + /** + * Retrieve the license RDF document based on the license URI + * + * @param licenseURI - The license URI for which to retrieve the license RDF document + * @return the license RDF document + * @throws IOException + */ + public Document retrieveLicenseRDFDoc(String licenseURI) throws IOException; + + /** + * Retrieve the license Name from the license document + * + * @param doc - The license document from which to retrieve the license name + * @return the license name + */ + public String retrieveLicenseName(final Document doc); + } diff --git a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java index 7fedc7e2e1..9813a5c31b 100644 --- a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java @@ -8,7 +8,11 @@ package org.dspace.license; import java.io.IOException; +import java.io.InputStream; import java.io.StringReader; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; import java.text.MessageFormat; import java.util.Collections; import java.util.HashMap; @@ -31,6 +35,7 @@ import org.dspace.services.ConfigurationService; import org.jaxen.JaxenException; import org.jaxen.jdom.JDOMXPath; import org.jdom.Attribute; +import org.jdom.Document; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.input.SAXBuilder; @@ -237,6 +242,7 @@ public class CCLicenseConnectorServiceImpl implements CCLicenseConnectorService, /** * Retrieve the CC License URI based on the provided license id, language and answers to the field questions from * the CC License API + * * @param licenseId - the ID of the license * @param language - the language for which to retrieve the full answerMap * @param answerMap - the answers to the different field questions @@ -273,7 +279,7 @@ public class CCLicenseConnectorServiceImpl implements CCLicenseConnectorService, /** * Parse the response for the CC License URI request and return the corresponding CC License URI * - * @param response for a specific CC License URI response + * @param response for a specific CC License URI response * @return the corresponding CC License URI as a string * @throws IOException * @throws JaxenException @@ -314,5 +320,52 @@ public class CCLicenseConnectorServiceImpl implements CCLicenseConnectorService, return sb.toString(); } + /** + * Retrieve the license RDF document based on the license URI + * + * @param licenseURI - The license URI for which to retrieve the license RDF document + * @return the license RDF document + * @throws IOException + */ + @Override + public Document retrieveLicenseRDFDoc(String licenseURI) throws IOException { + String ccLicenseUrl = configurationService.getProperty("cc.api.rooturl"); + + String issueUrl = ccLicenseUrl + "/details?license-uri=" + licenseURI; + + URL request_url; + try { + request_url = new URL(issueUrl); + } catch (MalformedURLException e) { + return null; + } + URLConnection connection = request_url.openConnection(); + connection.setDoOutput(true); + try { + // parsing document from input stream + InputStream stream = connection.getInputStream(); + Document doc = parser.build(stream); + return doc; + + } catch (Exception e) { + log.error("Error while retrieving the license document for URI: " + licenseURI, e); + } + return null; + } + + /** + * Retrieve the license Name from the license document + * + * @param doc - The license document from which to retrieve the license name + * @return the license name + */ + public String retrieveLicenseName(final Document doc) { + try { + return getSingleNodeValue(doc, "//result/license-uri"); + } catch (JaxenException e) { + log.error("Error while retrieving the license name from the license document", e); + } + return null; + } } diff --git a/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java b/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java index 928e4b6891..a1077af1ef 100644 --- a/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java @@ -181,8 +181,17 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi } + /** + * Removes the license file from the item + * + * @param context - The relevant DSpace Context + * @param item - The item from which the license file needs to be removed + * @throws SQLException + * @throws IOException + * @throws AuthorizeException + */ @Override - public void removeLicense(Context context, Item item) + public void removeLicenseFile(Context context, Item item) throws SQLException, IOException, AuthorizeException { // remove CC license bundle if one exists List bundles = itemService.getBundles(item, CC_BUNDLE_NAME); @@ -414,24 +423,49 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi return configurationService.getProperty("cc.license." + fieldId); } + /** + * Remove license information, delete also the bitstream + * + * @param context - DSpace Context + * @param item - the item + * @throws AuthorizeException Exception indicating the current user of the context does not have permission + * to perform a particular action. + * @throws IOException A general class of exceptions produced by failed or interrupted I/O operations. + * @throws SQLException An exception that provides information on a database access error or other errors. + */ @Override - public void removeLicense(Context context, LicenseMetadataValue uriField, - LicenseMetadataValue nameField, Item item) + public void removeLicense(Context context, Item item) throws AuthorizeException, IOException, SQLException { + + String uriField = getCCField("uri"); + String nameField = getCCField("name"); + + String licenseUri = itemService.getMetadata(item, uriField); + // only remove any previous licenses - String licenseUri = uriField.ccItemValue(item); if (licenseUri != null) { - uriField.removeItemValue(context, item, licenseUri); + removeLicenseField(context, item, uriField); if (configurationService.getBooleanProperty("cc.submit.setname")) { - String licenseName = nameField.keyedItemValue(item, licenseUri); - nameField.removeItemValue(context, item, licenseName); + removeLicenseField(context, item, nameField); } if (configurationService.getBooleanProperty("cc.submit.addbitstream")) { - removeLicense(context, item); + removeLicenseFile(context, item); } } } + private void removeLicenseField(Context context, Item item, String field) throws SQLException { + String[] params = splitField(field); + itemService.clearMetadata(context, item, params[0], params[1], params[2], params[3]); + + } + + private void addLicenseField(Context context, Item item, String field, String value) throws SQLException { + String[] params = splitField(field); + itemService.addMetadata(context, item, params[0], params[1], params[2], params[3], value); + + } + /** * Find all CC Licenses using the default language found in the configuration * @@ -623,4 +657,72 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi return null; } + /** + * Update the license of the item with a new one based on the provided license URI + * + * @param context - The relevant DSpace context + * @param licenseUri - The license URI to be used in the update + * @param item - The item for which to update the license + * @return true when the update was successful, false when not + * @throws AuthorizeException + * @throws SQLException + */ + @Override + public boolean updateLicense(final Context context, final String licenseUri, final Item item) + throws AuthorizeException, SQLException { + try { + Document doc = ccLicenseConnectorService.retrieveLicenseRDFDoc(licenseUri); + String licenseName = ccLicenseConnectorService.retrieveLicenseName(doc); + if (StringUtils.isBlank(licenseName)) { + return false; + } + + removeLicense(context, item); + addLicense(context, item, licenseUri, licenseName, doc); + + return true; + + } catch (IOException e) { + log.error("Error while updating the license of item: " + item.getID(), e); + } + return false; + } + + /** + * Add a new license to the item + * + * @param context - The relevant Dspace context + * @param item - The item to which the license will be added + * @param licenseUri - The license URI to add + * @param licenseName - The license name to add + * @param doc - The license to document to add + * @throws SQLException + * @throws IOException + * @throws AuthorizeException + */ + @Override + public void addLicense(Context context, Item item, String licenseUri, String licenseName, Document doc) + throws SQLException, IOException, AuthorizeException { + String uriField = getCCField("uri"); + String nameField = getCCField("name"); + + addLicenseField(context, item, uriField, licenseUri); + if (configurationService.getBooleanProperty("cc.submit.addbitstream")) { + setLicenseRDF(context, item, fetchLicenseRDF(doc)); + } + if (configurationService.getBooleanProperty("cc.submit.setname")) { + addLicenseField(context, item, nameField, licenseName); + } + } + + private String[] splitField(String fieldName) { + String[] params = new String[4]; + String[] fParams = fieldName.split("\\."); + for (int i = 0; i < fParams.length; i++) { + params[i] = fParams[i]; + } + params[3] = Item.ANY; + return params; + } + } diff --git a/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java b/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java index 3225393248..c5ea6475bb 100644 --- a/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java +++ b/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java @@ -18,7 +18,6 @@ import org.dspace.content.Bitstream; import org.dspace.content.Item; import org.dspace.core.Context; import org.dspace.license.CCLicense; -import org.dspace.license.LicenseMetadataValue; import org.jdom.Document; /** @@ -77,7 +76,16 @@ public interface CreativeCommonsService { InputStream licenseStm, String mimeType) throws SQLException, IOException, AuthorizeException; - public void removeLicense(Context context, Item item) + /** + * Removes the license file from the item + * + * @param context - The relevant DSpace Context + * @param item - The item from which the license file needs to be removed + * @throws SQLException + * @throws IOException + * @throws AuthorizeException + */ + public void removeLicenseFile(Context context, Item item) throws SQLException, IOException, AuthorizeException; public boolean hasLicense(Context context, Item item) @@ -159,16 +167,13 @@ public interface CreativeCommonsService { * Remove license information, delete also the bitstream * * @param context - DSpace Context - * @param uriField - the metadata field for license uri - * @param nameField - the metadata field for license name * @param item - the item * @throws AuthorizeException Exception indicating the current user of the context does not have permission * to perform a particular action. * @throws IOException A general class of exceptions produced by failed or interrupted I/O operations. * @throws SQLException An exception that provides information on a database access error or other errors. */ - public void removeLicense(Context context, LicenseMetadataValue uriField, - LicenseMetadataValue nameField, Item item) + public void removeLicense(Context context, Item item) throws AuthorizeException, IOException, SQLException; /** @@ -265,4 +270,31 @@ public interface CreativeCommonsService { */ public boolean verifyLicenseInformation(String licenseId, String language, Map fullAnswerMap); + /** + * Update the license of the item with a new one based on the provided license URI + * + * @param context - The relevant DSpace context + * @param licenseUri - The license URI to be used in the update + * @param item - The item for which to update the license + * @return true when the update was successful, false when not + * @throws AuthorizeException + * @throws SQLException + */ + public boolean updateLicense(final Context context, String licenseUri, final Item item) + throws AuthorizeException, SQLException; + + /** + * Add a new license to the item + * + * @param context - The relevant Dspace context + * @param item - The item to which the license will be added + * @param licenseUri - The license URI to add + * @param licenseName - The license name to add + * @param doc - The license to document to add + * @throws SQLException + * @throws IOException + * @throws AuthorizeException + */ + public void addLicense(Context context, Item item, String licenseUri, String licenseName, Document doc) + throws SQLException, IOException, AuthorizeException; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/AbstractRestProcessingStep.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/AbstractRestProcessingStep.java index 9989f6ca07..2dea6a2ce5 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/AbstractRestProcessingStep.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/AbstractRestProcessingStep.java @@ -35,7 +35,7 @@ public interface AbstractRestProcessingStep extends ListenerProcessingStep { public static final String UPLOAD_STEP_MOVE_OPERATION_ENTRY = "bitstreammove"; public static final String UPLOAD_STEP_ACCESSCONDITIONS_OPERATION_ENTRY = "accessConditions"; public static final String LICENSE_STEP_OPERATION_ENTRY = "granted"; - public static final String CCLICENSE_STEP_OPERATION_ENTRY = "ccLicense"; + public static final String CCLICENSE_STEP_OPERATION_ENTRY = "cclicense/uri"; public static final String UPLOAD_STEP_METADATA_PATH = "metadata"; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/CCLicenseAddPatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/CCLicenseAddPatchOperation.java new file mode 100644 index 0000000000..46a0f5ce95 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/CCLicenseAddPatchOperation.java @@ -0,0 +1,66 @@ +/** + * 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.submit.factory.impl; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.content.InProgressSubmission; +import org.dspace.content.Item; +import org.dspace.core.Context; +import org.dspace.license.service.CreativeCommonsService; +import org.dspace.services.model.Request; +import org.springframework.beans.factory.annotation.Autowired; + + +/** + * Submission "add" PATCH operation + * + * To add or update the Creative Commons License of a workspace item. + * When the item already has a Creative Commons License, the license will be replaced with a new one. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/submission/workspaceitems/31599 -H "Content-Type: + * application/json" -d '[{ "op": "add", "path": "/sections/cclicense/uri", + * "value":"http://creativecommons.org/licenses/by-nc-sa/3.0/us/"}]' + * + * + */ +public class CCLicenseAddPatchOperation extends AddPatchOperation { + + @Autowired + CreativeCommonsService creativeCommonsService; + + @Override + protected Class getArrayClassForEvaluation() { + return String[].class; + } + + @Override + protected Class getClassForEvaluation() { + return String.class; + } + + @Override + void add(Context context, Request currentRequest, InProgressSubmission source, String path, Object value) + throws Exception { + + + String licenseUri = null; + if (value instanceof String) { + licenseUri = (String) value; + } + + if (StringUtils.isBlank(licenseUri)) { + throw new IllegalArgumentException( + "Value is not a valid license URI"); + } + + Item item = source.getItem(); + creativeCommonsService.updateLicense(context, licenseUri, item); + } + +} diff --git a/dspace-server-webapp/src/main/resources/spring/spring-dspace-core-services.xml b/dspace-server-webapp/src/main/resources/spring/spring-dspace-core-services.xml index 61459f11d6..2b37ea5bdd 100644 --- a/dspace-server-webapp/src/main/resources/spring/spring-dspace-core-services.xml +++ b/dspace-server-webapp/src/main/resources/spring/spring-dspace-core-services.xml @@ -52,6 +52,10 @@ + + + diff --git a/dspace/config/item-submission.xml b/dspace/config/item-submission.xml index d1f755d293..481b508176 100644 --- a/dspace/config/item-submission.xml +++ b/dspace/config/item-submission.xml @@ -115,7 +115,7 @@ - submit.progressbar.CClicense + submit.progressbar.CClicense org.dspace.app.rest.submit.step.CCLicenseStep cclicense @@ -203,7 +203,7 @@ - + From 09c0f62561795f3fefd22cac929d1cc9e6036f21 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Wed, 22 Apr 2020 14:39:53 +0200 Subject: [PATCH 019/143] 70505: Patch submission (Add): Bugfixing & tests --- .../CCLicenseConnectorServiceImpl.java | 4 +- .../license/CreativeCommonsServiceImpl.java | 3 + .../dspaceFolder/config/item-submission.xml | 8 +- .../MockCCLicenseConnectorServiceImpl.java | 32 +++++- .../impl/CCLicenseAddPatchOperation.java | 11 ++- .../rest/CCLicenseAddPatchOperationIT.java | 99 +++++++++++++++++++ .../SubmissionDefinitionsControllerIT.java | 2 +- .../MockCCLicenseConnectorServiceImpl.java | 31 ++++++ .../org/dspace/license/cc-license-rdf.xml | 31 ++++++ 9 files changed, 209 insertions(+), 12 deletions(-) create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseAddPatchOperationIT.java create mode 100644 dspace-server-webapp/src/test/resources/org/dspace/license/cc-license-rdf.xml diff --git a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java index 9813a5c31b..a237a91984 100644 --- a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java @@ -52,7 +52,7 @@ public class CCLicenseConnectorServiceImpl implements CCLicenseConnectorService, private Logger log = org.apache.logging.log4j.LogManager.getLogger(CCLicenseConnectorServiceImpl.class); private CloseableHttpClient client; - private SAXBuilder parser = new SAXBuilder(); + protected SAXBuilder parser = new SAXBuilder(); private String postArgument = "answers"; private String postAnswerFormat = @@ -361,7 +361,7 @@ public class CCLicenseConnectorServiceImpl implements CCLicenseConnectorService, */ public String retrieveLicenseName(final Document doc) { try { - return getSingleNodeValue(doc, "//result/license-uri"); + return getSingleNodeValue(doc, "//result/license-name"); } catch (JaxenException e) { log.error("Error while retrieving the license name from the license document", e); } diff --git a/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java b/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java index a1077af1ef..67a9e4e06d 100644 --- a/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java @@ -672,6 +672,9 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi throws AuthorizeException, SQLException { try { Document doc = ccLicenseConnectorService.retrieveLicenseRDFDoc(licenseUri); + if (doc == null) { + return false; + } String licenseName = ccLicenseConnectorService.retrieveLicenseName(doc); if (StringUtils.isBlank(licenseName)) { return false; diff --git a/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml b/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml index de19ef7287..d78b14c437 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml @@ -82,9 +82,9 @@ - + submit.progressbar.CClicense + org.dspace.app.rest.submit.step.CCLicenseStep + cclicense - + diff --git a/dspace-api/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java b/dspace-api/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java index 226e2aa77b..41cd9805f6 100644 --- a/dspace-api/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java +++ b/dspace-api/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java @@ -7,11 +7,17 @@ */ package org.dspace.license; +import java.io.IOException; +import java.io.InputStream; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.jdom.Document; +import org.jdom.JDOMException; + /** * Mock implementation for the Creative commons license connector service. * This class will return a structure of CC Licenses similar to the CC License API but without having to contact it @@ -20,7 +26,6 @@ public class MockCCLicenseConnectorServiceImpl extends CCLicenseConnectorService /** * Retrieves mock CC Licenses for the provided language - * * @param language - the language * @return a map of mocked licenses with the id and the license */ @@ -90,4 +95,29 @@ public class MockCCLicenseConnectorServiceImpl extends CCLicenseConnectorService return "mock-license-uri"; } + + /** + * Retrieve a mock license RDF document. + * When the uri contains "invalid", null will be returned to simulate that no document was found for the provided + * URI + * + * @param licenseURI - The license URI for which to retrieve the license RDF document + * @return a mock license RDF document or null when the URI contains invalid + * @throws IOException + */ + public Document retrieveLicenseRDFDoc(String licenseURI) throws IOException { + if (!StringUtils.contains(licenseURI, "invalid")) { + try { + + InputStream cclicense = getClass().getResourceAsStream("cc-license-rdf.xml"); + + Document doc = parser.build(cclicense); + return doc; + } catch (JDOMException e) { + e.printStackTrace(); + } + } + return null; + } + } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/CCLicenseAddPatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/CCLicenseAddPatchOperation.java index 46a0f5ce95..e3286551d5 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/CCLicenseAddPatchOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/CCLicenseAddPatchOperation.java @@ -27,7 +27,6 @@ import org.springframework.beans.factory.annotation.Autowired; * application/json" -d '[{ "op": "add", "path": "/sections/cclicense/uri", * "value":"http://creativecommons.org/licenses/by-nc-sa/3.0/us/"}]' * - * */ public class CCLicenseAddPatchOperation extends AddPatchOperation { @@ -46,7 +45,7 @@ public class CCLicenseAddPatchOperation extends AddPatchOperation { @Override void add(Context context, Request currentRequest, InProgressSubmission source, String path, Object value) - throws Exception { + throws Exception { String licenseUri = null; @@ -56,11 +55,15 @@ public class CCLicenseAddPatchOperation extends AddPatchOperation { if (StringUtils.isBlank(licenseUri)) { throw new IllegalArgumentException( - "Value is not a valid license URI"); + "Value is not a valid license URI"); } Item item = source.getItem(); - creativeCommonsService.updateLicense(context, licenseUri, item); + boolean updateLicense = creativeCommonsService.updateLicense(context, licenseUri, item); + if (!updateLicense) { + throw new IllegalArgumentException("The license uri: " + licenseUri + ", could not be resolved to a " + + "CC license"); + } } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseAddPatchOperationIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseAddPatchOperationIT.java new file mode 100644 index 0000000000..ba58e5f489 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseAddPatchOperationIT.java @@ -0,0 +1,99 @@ +/** + * 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.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.ArrayList; +import java.util.List; +import javax.ws.rs.core.MediaType; + +import org.dspace.app.rest.builder.CollectionBuilder; +import org.dspace.app.rest.builder.CommunityBuilder; +import org.dspace.app.rest.builder.WorkspaceItemBuilder; +import org.dspace.app.rest.model.patch.AddOperation; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.WorkspaceItem; +import org.junit.Test; + +/** + * Class to the methods from the CCLicenseAddPatchOperation + * Since the CC Licenses are obtained from the CC License API, a mock service has been implemented + * This mock service will return a fixed set of CC Licenses using a similar structure to the ones obtained from the + * CC License API. + * Refer to {@link org.dspace.license.MockCCLicenseConnectorServiceImpl} for more information + */ +public class CCLicenseAddPatchOperationIT extends AbstractControllerIntegrationTest { + + + @Test + public void patchSubmissionCCLicense() throws Exception { + context.turnOffAuthorisationSystem(); + + Community community = CommunityBuilder.createCommunity(context) + .withName("Community") + .build(); + + Collection collection = CollectionBuilder.createCollection(context, community) + .withName("Collection") + .build(); + + WorkspaceItem workspaceItem = WorkspaceItemBuilder.createWorkspaceItem(context, collection) + .withTitle("Workspace Item") + .build(); + + String adminToken = getAuthToken(admin.getEmail(), password); + + List ops = new ArrayList(); + AddOperation addOperation = new AddOperation("/sections/cclicense/uri", "license-uri"); + + ops.add(addOperation); + String patchBody = getPatchContent(ops); + + + getClient(adminToken).perform(patch("/api/submission/workspaceitems/" + workspaceItem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()); + } + + @Test + public void patchSubmissionCCLicenseInvalid() throws Exception { + context.turnOffAuthorisationSystem(); + + Community community = CommunityBuilder.createCommunity(context) + .withName("Community") + .build(); + + Collection collection = CollectionBuilder.createCollection(context, community) + .withName("Collection") + .build(); + + WorkspaceItem workspaceItem = WorkspaceItemBuilder.createWorkspaceItem(context, collection) + .withTitle("Workspace Item") + .build(); + + String adminToken = getAuthToken(admin.getEmail(), password); + + List ops = new ArrayList(); + AddOperation addOperation = new AddOperation("/sections/cclicense/uri", "invalid-license-uri"); + + ops.add(addOperation); + String patchBody = getPatchContent(ops); + + + getClient(adminToken).perform(patch("/api/submission/workspaceitems/" + workspaceItem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isInternalServerError()); + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java index 38d22fdf04..dbee267740 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java @@ -204,7 +204,7 @@ public class SubmissionDefinitionsControllerIT extends AbstractControllerIntegra // We expect the content type to be "application/hal+json;charset=UTF-8" .andExpect(content().contentType(contentType)) // Match only that a section exists with a submission configuration behind - .andExpect(jsonPath("$._embedded.submissionsections", hasSize(5))) + .andExpect(jsonPath("$._embedded.submissionsections", hasSize(6))) .andExpect(jsonPath("$._embedded.submissionsections", Matchers.hasItem( allOf( diff --git a/dspace-server-webapp/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java b/dspace-server-webapp/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java index 23d395ec2b..41cd9805f6 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java +++ b/dspace-server-webapp/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java @@ -7,11 +7,17 @@ */ package org.dspace.license; +import java.io.IOException; +import java.io.InputStream; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.jdom.Document; +import org.jdom.JDOMException; + /** * Mock implementation for the Creative commons license connector service. * This class will return a structure of CC Licenses similar to the CC License API but without having to contact it @@ -89,4 +95,29 @@ public class MockCCLicenseConnectorServiceImpl extends CCLicenseConnectorService return "mock-license-uri"; } + + /** + * Retrieve a mock license RDF document. + * When the uri contains "invalid", null will be returned to simulate that no document was found for the provided + * URI + * + * @param licenseURI - The license URI for which to retrieve the license RDF document + * @return a mock license RDF document or null when the URI contains invalid + * @throws IOException + */ + public Document retrieveLicenseRDFDoc(String licenseURI) throws IOException { + if (!StringUtils.contains(licenseURI, "invalid")) { + try { + + InputStream cclicense = getClass().getResourceAsStream("cc-license-rdf.xml"); + + Document doc = parser.build(cclicense); + return doc; + } catch (JDOMException e) { + e.printStackTrace(); + } + } + return null; + } + } diff --git a/dspace-server-webapp/src/test/resources/org/dspace/license/cc-license-rdf.xml b/dspace-server-webapp/src/test/resources/org/dspace/license/cc-license-rdf.xml new file mode 100644 index 0000000000..5ff75ee4c7 --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/license/cc-license-rdf.xml @@ -0,0 +1,31 @@ + + + http://creativecommons.org/licenses/by-nc-sa/4.0/ + Attribution-NonCommercial-ShareAlike 4.0 International + false + + + + + + + + + + + + + + + + + + + + + + + + + Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. +
From a27b64c88f98732b3c4db7b62567ef6078f82ba5 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Wed, 22 Apr 2020 18:00:43 +0200 Subject: [PATCH 020/143] 70506: CC license (REST): Patch submission (Remove) --- .../impl/CCLicenseRemovePatchOperation.java | 50 ++++++++++ .../spring/spring-dspace-core-services.xml | 4 + .../rest/CCLicenseAddPatchOperationIT.java | 17 +++- .../rest/CCLicenseRemovePatchOperationIT.java | 99 +++++++++++++++++++ 4 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/CCLicenseRemovePatchOperation.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseRemovePatchOperationIT.java diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/CCLicenseRemovePatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/CCLicenseRemovePatchOperation.java new file mode 100644 index 0000000000..19229a4f72 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/CCLicenseRemovePatchOperation.java @@ -0,0 +1,50 @@ +/** + * 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.submit.factory.impl; + +import org.dspace.content.InProgressSubmission; +import org.dspace.content.Item; +import org.dspace.core.Context; +import org.dspace.license.service.CreativeCommonsService; +import org.dspace.services.model.Request; +import org.springframework.beans.factory.annotation.Autowired; + + +/** + * Submission "remove" PATCH operation + * + * To remove the Creative Commons License of a workspace item. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/submission/workspaceitems/31599 -H "Content-Type: + * application/json" -d '[{ "op": "remove", "path": "/sections/cclicense/uri"}]' + * + */ +public class CCLicenseRemovePatchOperation extends RemovePatchOperation { + + @Autowired + CreativeCommonsService creativeCommonsService; + + @Override + protected Class getArrayClassForEvaluation() { + return String[].class; + } + + @Override + protected Class getClassForEvaluation() { + return String.class; + } + + @Override + void remove(Context context, Request currentRequest, InProgressSubmission source, String path, Object value) + throws Exception { + Item item = source.getItem(); + creativeCommonsService.removeLicense(context, item); + } + +} diff --git a/dspace-server-webapp/src/main/resources/spring/spring-dspace-core-services.xml b/dspace-server-webapp/src/main/resources/spring/spring-dspace-core-services.xml index 2b37ea5bdd..9faad91985 100644 --- a/dspace-server-webapp/src/main/resources/spring/spring-dspace-core-services.xml +++ b/dspace-server-webapp/src/main/resources/spring/spring-dspace-core-services.xml @@ -81,6 +81,10 @@
+ + + diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseAddPatchOperationIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseAddPatchOperationIT.java index ba58e5f489..76b260f194 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseAddPatchOperationIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseAddPatchOperationIT.java @@ -7,7 +7,11 @@ */ package org.dspace.app.rest; +import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.is; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.util.ArrayList; @@ -26,7 +30,7 @@ import org.dspace.content.WorkspaceItem; import org.junit.Test; /** - * Class to the methods from the CCLicenseAddPatchOperation + * Class to test the methods from the CCLicenseAddPatchOperation * Since the CC Licenses are obtained from the CC License API, a mock service has been implemented * This mock service will return a fixed set of CC Licenses using a similar structure to the ones obtained from the * CC License API. @@ -54,7 +58,8 @@ public class CCLicenseAddPatchOperationIT extends AbstractControllerIntegrationT String adminToken = getAuthToken(admin.getEmail(), password); List ops = new ArrayList(); - AddOperation addOperation = new AddOperation("/sections/cclicense/uri", "license-uri"); + AddOperation addOperation = new AddOperation("/sections/cclicense/uri", + "http://creativecommons.org/licenses/by-nc-sa/4.0/"); ops.add(addOperation); String patchBody = getPatchContent(ops); @@ -63,7 +68,13 @@ public class CCLicenseAddPatchOperationIT extends AbstractControllerIntegrationT getClient(adminToken).perform(patch("/api/submission/workspaceitems/" + workspaceItem.getID()) .content(patchBody) .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) - .andExpect(status().isOk()); + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections.cclicense", allOf( + hasJsonPath("$.uri", is("http://creativecommons.org/licenses/by-nc-sa/4.0/")), + hasJsonPath("$.rights", + is("Attribution-NonCommercial-ShareAlike 4.0 International")), + hasJsonPath("$.file.name", is("license_rdf")) + ))); } @Test diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseRemovePatchOperationIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseRemovePatchOperationIT.java new file mode 100644 index 0000000000..b908b7acd7 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseRemovePatchOperationIT.java @@ -0,0 +1,99 @@ +/** + * 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 com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.ArrayList; +import java.util.List; +import javax.ws.rs.core.MediaType; + +import org.dspace.app.rest.builder.CollectionBuilder; +import org.dspace.app.rest.builder.CommunityBuilder; +import org.dspace.app.rest.builder.WorkspaceItemBuilder; +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.test.AbstractControllerIntegrationTest; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.WorkspaceItem; +import org.junit.Test; + +/** + * Class to test the methods from the CCLicenseRemovePatchOperation + * Since the CC Licenses are obtained from the CC License API, a mock service has been implemented + * This mock service will return a fixed set of CC Licenses using a similar structure to the ones obtained from the + * CC License API. + * Refer to {@link org.dspace.license.MockCCLicenseConnectorServiceImpl} for more information + */ +public class CCLicenseRemovePatchOperationIT extends AbstractControllerIntegrationTest { + + + @Test + public void patchRemoveSubmissionCCLicense() throws Exception { + context.turnOffAuthorisationSystem(); + + Community community = CommunityBuilder.createCommunity(context) + .withName("Community") + .build(); + + Collection collection = CollectionBuilder.createCollection(context, community) + .withName("Collection") + .build(); + + WorkspaceItem workspaceItem = WorkspaceItemBuilder.createWorkspaceItem(context, collection) + .withTitle("Workspace Item") + .build(); + + String adminToken = getAuthToken(admin.getEmail(), password); + + // First add a license and verify it is added + List ops = new ArrayList(); + AddOperation addOperation = new AddOperation("/sections/cclicense/uri", + "http://creativecommons.org/licenses/by-nc-sa/4.0/"); + + ops.add(addOperation); + String patchBody = getPatchContent(ops); + + + getClient(adminToken).perform(patch("/api/submission/workspaceitems/" + workspaceItem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections.cclicense", allOf( + hasJsonPath("$.uri", is("http://creativecommons.org/licenses/by-nc-sa/4.0/")), + hasJsonPath("$.rights", + is("Attribution-NonCommercial-ShareAlike 4.0 International")), + hasJsonPath("$.file.name", is("license_rdf")) + ))); + + + + // Remove the license again and verify it is removed + + List removeOps = new ArrayList(); + RemoveOperation removeOperation = new RemoveOperation("/sections/cclicense/uri"); + + removeOps.add(removeOperation); + String removePatch = getPatchContent(removeOps); + + + getClient(adminToken).perform(patch("/api/submission/workspaceitems/" + workspaceItem.getID()) + .content(removePatch) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections", not(hasJsonPath("cclicense")))); + } +} From 36264cca50439249f97b7a3ebbdc3459f8c07f8a Mon Sep 17 00:00:00 2001 From: Kevin Van de Velde Date: Thu, 23 Apr 2020 14:57:03 +0200 Subject: [PATCH 021/143] Submission CreativeCommons license rest evaluator plugin addition so that it plays nicely with the access restrictions --- ...ubmissionCCLicenseRestEvaluatorPlugin.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SubmissionCCLicenseRestEvaluatorPlugin.java diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SubmissionCCLicenseRestEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SubmissionCCLicenseRestEvaluatorPlugin.java new file mode 100644 index 0000000000..251717d799 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SubmissionCCLicenseRestEvaluatorPlugin.java @@ -0,0 +1,21 @@ +package org.dspace.app.rest.security; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.app.rest.model.SubmissionCCLicenseRest; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import java.io.Serializable; + +@Component +public class SubmissionCCLicenseRestEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { + + @Override + public boolean hasDSpacePermission(Authentication authentication, Serializable targetId, String targetType, + DSpaceRestPermission restPermission) { + if (!StringUtils.equalsIgnoreCase(SubmissionCCLicenseRest.NAME, targetType)) { + return false; + } + return true; + } +} From 94d9f623587bd0f48fb64ee8c62706dfd8031ac1 Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Mon, 27 Apr 2020 14:52:00 +0200 Subject: [PATCH 022/143] [Task 70399] added context authorizations modifications for eperson post with token and added its to verify functionality --- .../repository/EPersonRestRepository.java | 2 + .../app/rest/EPersonRestRepositoryIT.java | 75 +++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java index 1309e11713..fa69d15253 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java @@ -159,7 +159,9 @@ public class EPersonRestRepository extends DSpaceObjectRestRepository map = mapper.readValue(content, Map.class); + String epersonUuid = String.valueOf(map.get("uuid")); + EPerson createdEPerson = ePersonService.find(context, UUID.fromString(epersonUuid)); + assertTrue(ePersonService.checkPassword(context, createdEPerson, "somePassword")); + assertNull(registrationDataService.findByToken(context, newRegisterToken)); + + context.turnOffAuthorisationSystem(); + ePersonService.delete(context, createdEPerson); + context.restoreAuthSystemState(); + } } From b271ae662130405aa7b24c5a0cb6dbcec25aba68 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Tue, 28 Apr 2020 17:17:48 +0200 Subject: [PATCH 023/143] 70338: Cleanup CC license code --- .../java/org/dspace/license/CCLookup.java | 435 ------------------ .../license/CreativeCommonsServiceImpl.java | 33 -- .../dspace/license/LicenseMetadataValue.java | 129 ------ .../service/CreativeCommonsService.java | 13 - .../MockCCLicenseConnectorServiceImpl.java | 10 +- ...ubmissionCCLicenseRestEvaluatorPlugin.java | 11 +- .../rest/CCLicenseAddPatchOperationIT.java | 15 + .../rest/CCLicenseRemovePatchOperationIT.java | 3 +- .../MockCCLicenseConnectorServiceImpl.java | 10 +- 9 files changed, 40 insertions(+), 619 deletions(-) delete mode 100644 dspace-api/src/main/java/org/dspace/license/CCLookup.java delete mode 100644 dspace-api/src/main/java/org/dspace/license/LicenseMetadataValue.java diff --git a/dspace-api/src/main/java/org/dspace/license/CCLookup.java b/dspace-api/src/main/java/org/dspace/license/CCLookup.java deleted file mode 100644 index b7ddfa2314..0000000000 --- a/dspace-api/src/main/java/org/dspace/license/CCLookup.java +++ /dev/null @@ -1,435 +0,0 @@ -/** - * 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.license; - -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.UnsupportedEncodingException; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLConnection; -import java.net.URLEncoder; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; - -import org.apache.logging.log4j.Logger; -import org.dspace.license.factory.LicenseServiceFactory; -import org.dspace.license.service.CreativeCommonsService; -import org.dspace.services.ConfigurationService; -import org.dspace.services.factory.DSpaceServicesFactory; -import org.jaxen.JaxenException; -import org.jaxen.jdom.JDOMXPath; -import org.jdom.Attribute; -import org.jdom.Document; -import org.jdom.Element; -import org.jdom.JDOMException; -import org.jdom.input.SAXBuilder; - - -/** - * A wrapper around Creative Commons REST web services. - * - * @author Wendy Bossons - */ -public class CCLookup { - - /** - * log4j logger - */ - private static Logger log = org.apache.logging.log4j.LogManager.getLogger(CCLookup.class); - - private String cc_root; - private String jurisdiction; - private List lcFilter = new ArrayList(); - - private Document license_doc = null; - private String rdfString = null; - private String errorMessage = null; - private boolean success = false; - - private SAXBuilder parser = new SAXBuilder(); - private List licenses = new ArrayList(); - private List licenseFields = new ArrayList(); - - protected CreativeCommonsService creativeCommonsService = LicenseServiceFactory.getInstance() - .getCreativeCommonsService(); - - /** - * Constructs a new instance with the default web services root. - */ - public CCLookup() { - super(); - - ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); - - cc_root = configurationService.getProperty("cc.api.rooturl"); - - String jurisProp = configurationService.getProperty("cc.license.jurisdiction"); - jurisdiction = (jurisProp != null) ? jurisProp : ""; - - String[] filters = configurationService.getArrayProperty("cc.license.classfilter"); - if (filters != null) { - for (String name : filters) { - lcFilter.add(name.trim()); - } - } - } - - /** - * Returns the id for a particular CCLicense label. Returns an - * empty string if no match is found. - * - * @param class_label The CCLicense label to find. - * @return Returns a String containing the License class ID if the label - * is found; if not found, returns an empty string. - * @see CCLicense - */ - public String getLicenseId(String class_label) { - for (int i = 0; i < this.licenses.size(); i++) { - if (((CCLicense) this.licenses.get(i)).getLicenseName().equals(class_label)) { - return ((CCLicense) this.licenses.get(i)).getLicenseId(); - } - } - - return ""; - } - - /** - * Queries the web service for the available licenses. - * - * @param language The language to request labels and description strings in. - * @return Returns a Map of CCLicense objects. - * @see Map - * @see CCLicense - */ - public Collection getLicenses(String language) { - - // create XPath expressions - try { - JDOMXPath xp_Licenses = new JDOMXPath("//licenses/license"); - JDOMXPath xp_LicenseID = new JDOMXPath("@id"); - URL classUrl = new URL(this.cc_root + "/?locale=" + language); - Document classDoc = this.parser.build(classUrl); - // extract the identifiers and labels using XPath - List results = xp_Licenses.selectNodes(classDoc); - // populate licenses container - this.licenses.clear(); - for (int i = 0; i < results.size(); i++) { - Element license = results.get(i); - // add if not filtered - String liD = ((Attribute) xp_LicenseID.selectSingleNode(license)).getValue(); - if (!lcFilter.contains(liD)) { -// this.licenses.add(new CCLicense(liD, license.getText(), i)); - } - } - } catch (JaxenException jaxen_e) { - return null; - } catch (JDOMException jdom_e) { - return null; - } catch (IOException io_e) { - return null; - } catch (Exception e) { - // do nothing... but we should - return null; - } - - return licenses; - } - - - /** - * Queries the web service for a set of licenseFields for a particular license class. - * - * @param license A String specifying the CCLicense identifier to - * retrieve fields for. - * @param language the locale string - * @return A Collection of LicenseField objects. - * @see CCLicense - */ - public Collection getLicenseFields(String license, String language) { - - JDOMXPath xp_LicenseField; - JDOMXPath xp_LicenseID; - JDOMXPath xp_FieldType; - JDOMXPath xp_Description; - JDOMXPath xp_Label; - JDOMXPath xp_Enum; - - Document fieldDoc; - - URL classUrl; - List results = null; - List enumOptions = null; - - // create XPath expressions - try { - xp_LicenseField = new JDOMXPath("//field"); - xp_LicenseID = new JDOMXPath("@id"); - xp_Description = new JDOMXPath("description"); - xp_Label = new JDOMXPath("label"); - xp_FieldType = new JDOMXPath("type"); - xp_Enum = new JDOMXPath("enum"); - - } catch (JaxenException e) { - return null; - } - - // retrieve and parse the license class document - try { - classUrl = new URL(this.cc_root + "/license/" + license + "?locale=" + language); - } catch (Exception err) { - // do nothing... but we should - return null; - } - - // parse the licenses document - try { - fieldDoc = this.parser.build(classUrl); - } catch (JDOMException e) { - return null; - } catch (IOException e) { - return null; - } - - // reset the field definition container - this.licenseFields.clear(); - - // extract the identifiers and labels using XPath - try { - results = xp_LicenseField.selectNodes(fieldDoc); - } catch (JaxenException e) { - return null; - } - - for (int i = 0; i < results.size(); i++) { - Element field = (Element) results.get(i); - -// try { -// // create the field object -// CCLicenseField cclicensefield = new CCLicenseField( -// ((Attribute) xp_LicenseID.selectSingleNode(field)).getValue(), -// ((Element) xp_Label.selectSingleNode(field)).getText()); -// -// // extract additional properties -// cclicensefield.setDescription(((Element) xp_Description.selectSingleNode(field)).getText()); -// cclicensefield.setType(((Element) xp_FieldType.selectSingleNode(field)).getText()); -// -// enumOptions = xp_Enum.selectNodes(field); -// -// for (int j = 0; j < enumOptions.size(); j++) { -// String id = ((Attribute) xp_LicenseID.selectSingleNode(enumOptions.get(j))).getValue(); -// String label = ((Element) xp_Label.selectSingleNode(enumOptions.get(j))).getText(); -// -//// cclicensefield.getEnum().put(id, label); -// -// } // for each enum option -// -// this.licenseFields.add(cclicensefield); -// } catch (JaxenException e) { -// return null; -// } - } - - return licenseFields; - } // licenseFields - - /** - * Passes a set of "answers" to the web service and retrieves a license. - * - * @param licenseId The identifier of the license class being requested. - * @param answers A Map containing the answers to the license fields; - * each key is the identifier of a LicenseField, with the value - * containing the user-supplied answer. - * @param lang The language to request localized elements in. - * @throws IOException if IO error - * @see CCLicense - * @see Map - */ - public void issue(String licenseId, Map answers, String lang) - throws IOException { - - // Determine the issue URL - String issueUrl = this.cc_root + "/license/" + licenseId + "/issue"; - // Assemble the "answers" document - String answer_doc = "\n" + lang + "\n" + "\n"; - Iterator keys = answers.keySet().iterator(); - - try { - String current = (String) keys.next(); - - while (true) { - answer_doc += "<" + current + ">" + (String) answers.get(current) + "\n"; - current = (String) keys.next(); - } - - - } catch (NoSuchElementException e) { - // exception indicates we've iterated through the - // entire collection; just swallow and continue - } - // answer_doc += "\n"; FAILS with jurisdiction argument - answer_doc += "\n\n"; - String post_data; - - try { - post_data = URLEncoder.encode("answers", "UTF-8") + "=" + URLEncoder.encode(answer_doc, "UTF-8"); - } catch (UnsupportedEncodingException e) { - return; - } - - URL post_url; - try { - post_url = new URL(issueUrl); - } catch (MalformedURLException e) { - return; - } - URLConnection connection = post_url.openConnection(); - // this will not be needed after I'm done TODO: remove - connection.setDoOutput(true); - OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream()); - writer.write(post_data); - writer.flush(); - // end TODO - try { - // parsing document from input stream - java.io.InputStream stream = connection.getInputStream(); - this.license_doc = this.parser.build(stream); - } catch (JDOMException jde) { - log.warn(jde.getMessage()); - } catch (Exception e) { - log.warn(e.getCause()); - } - return; - } // issue - - /** - * Passes a set of "answers" to the web service and retrieves a license. - * - * @param licenseURI The uri of the license. - * - * Note: does not support localization in 1.5 -- not yet - * @throws IOException if IO error - * @see CCLicense - * @see Map - */ - public void issue(String licenseURI) - throws IOException { - - // Determine the issue URL - // Example: http://api.creativecommons.org/rest/1.5/details? - // license-uri=http://creativecommons.org/licenses/by-nc-sa/3.0/ - String issueUrl = cc_root + "/details?license-uri=" + licenseURI; - - URL request_url; - try { - request_url = new URL(issueUrl); - } catch (MalformedURLException e) { - return; - } - URLConnection connection = request_url.openConnection(); - // this will not be needed after I'm done TODO: remove - connection.setDoOutput(true); - try { - // parsing document from input stream - java.io.InputStream stream = connection.getInputStream(); - license_doc = this.parser.build(stream); - } catch (JDOMException jde) { - log.warn(jde.getMessage()); - } catch (Exception e) { - log.warn(e.getCause()); - } - return; - } // issue - - /** - * Retrieves the URI for the license issued. - * - * @return A String containing the URI for the license issued. - */ - public String getLicenseUrl() { - String text = null; - try { - JDOMXPath xp_LicenseName = new JDOMXPath("//result/license-uri"); - text = ((Element) xp_LicenseName.selectSingleNode(this.license_doc)).getText(); - } catch (Exception e) { - log.warn(e.getMessage()); - setSuccess(false); - text = "An error occurred getting the license - uri."; - } finally { - return text; - } - } // getLicenseUrl - - /** - * Retrieves the human readable name for the license issued. - * - * @return A String containing the license name. - */ - public String getLicenseName() { - String text = null; - try { - JDOMXPath xp_LicenseName = new JDOMXPath("//result/license-name"); - text = ((Element) xp_LicenseName.selectSingleNode(this.license_doc)).getText(); - } catch (Exception e) { - log.warn(e.getMessage()); - setSuccess(false); - text = "An error occurred on the license name."; - } finally { - return text; - } - } // getLicenseName - - - public org.jdom.Document getLicenseDocument() { - return this.license_doc; - } - - public String getRdf() - throws IOException { - String result = ""; - try { - result = creativeCommonsService.fetchLicenseRDF(license_doc); - } catch (Exception e) { - log.warn("An error occurred getting the rdf . . ." + e.getMessage()); - setSuccess(false); - } - return result; - } - - public boolean isSuccess() { - setSuccess(false); - JDOMXPath xp_Success; - String text = null; - try { - xp_Success = new JDOMXPath("//message"); - text = ((Element) xp_Success.selectSingleNode(this.license_doc)).getText(); - setErrorMessage(text); - } catch (Exception e) { - log.warn("There was an issue . . . " + text); - setSuccess(true); - } - return this.success; - } - - private void setSuccess(boolean success) { - this.success = success; - } - - public String getErrorMessage() { - return this.errorMessage; - } - - private void setErrorMessage(String errorMessage) { - this.errorMessage = errorMessage; - } - -} diff --git a/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java b/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java index 67a9e4e06d..7a48b6f03b 100644 --- a/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java @@ -125,11 +125,6 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi } - @Override - public boolean isEnabled() { - return true; - } - // create the CC bundle if it doesn't exist // If it does, remove it and create a new one. protected Bundle getCcBundle(Context context, Item item) @@ -201,34 +196,6 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi } } - @Override - public boolean hasLicense(Context context, Item item) - throws SQLException, IOException { - // try to find CC license bundle - List bundles = itemService.getBundles(item, CC_BUNDLE_NAME); - - if (bundles.size() == 0) { - return false; - } - - // verify it has correct contents - try { - if ((getLicenseURL(context, item) == null)) { - return false; - } - } catch (AuthorizeException ae) { - return false; - } - - return true; - } - - @Override - public String getLicenseRDF(Context context, Item item) throws SQLException, - IOException, AuthorizeException { - return getStringFromBitstream(context, item, BSN_LICENSE_RDF); - } - @Override public Bitstream getLicenseRdfBitstream(Item item) throws SQLException, IOException, AuthorizeException { diff --git a/dspace-api/src/main/java/org/dspace/license/LicenseMetadataValue.java b/dspace-api/src/main/java/org/dspace/license/LicenseMetadataValue.java deleted file mode 100644 index ec5c9e447b..0000000000 --- a/dspace-api/src/main/java/org/dspace/license/LicenseMetadataValue.java +++ /dev/null @@ -1,129 +0,0 @@ -/** - * 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.license; - -import java.io.IOException; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; - -import org.dspace.authorize.AuthorizeException; -import org.dspace.content.Item; -import org.dspace.content.MetadataValue; -import org.dspace.content.factory.ContentServiceFactory; -import org.dspace.content.service.ItemService; -import org.dspace.core.Context; - -/** - * Helper class for using CC-related Metadata fields - * - * @author kevinvandevelde at atmire.com - */ -public class LicenseMetadataValue { - - protected final ItemService itemService; - // Shibboleth for Creative Commons license data - i.e. characters that reliably indicate CC in a URI - protected static final String ccShib = "creativecommons"; - - private String[] params = new String[4]; - - public LicenseMetadataValue(String fieldName) { - if (fieldName != null && fieldName.length() > 0) { - String[] fParams = fieldName.split("\\."); - for (int i = 0; i < fParams.length; i++) { - params[i] = fParams[i]; - } - params[3] = Item.ANY; - } - itemService = ContentServiceFactory.getInstance().getItemService(); - } - - /** - * Returns first value that matches Creative Commons 'shibboleth', - * or null if no matching values. - * NB: this method will succeed only for metadata fields holding CC URIs - * - * @param item - the item to read - * @return value - the first CC-matched value, or null if no such value - */ - public String ccItemValue(Item item) { - List dcvalues = itemService.getMetadata(item, params[0], params[1], params[2], params[3]); - for (MetadataValue dcvalue : dcvalues) { - if ((dcvalue.getValue()).indexOf(ccShib) != -1) { - // return first value that matches the shib - return dcvalue.getValue(); - } - } - return null; - } - - /** - * Returns the value that matches the value mapped to the passed key if any. - * NB: this only delivers a license name (if present in field) given a license URI - * - * @param item - the item to read - * @param key - the key for desired value - * @return value - the value associated with key or null if no such value - * @throws IOException A general class of exceptions produced by failed or interrupted I/O operations. - * @throws SQLException An exception that provides information on a database access error or other errors. - * @throws AuthorizeException Exception indicating the current user of the context does not have permission - * to perform a particular action. - */ - public String keyedItemValue(Item item, String key) - throws AuthorizeException, IOException, SQLException { - CCLookup ccLookup = new CCLookup(); - ccLookup.issue(key); - String matchValue = ccLookup.getLicenseName(); - List dcvalues = itemService.getMetadata(item, params[0], params[1], params[2], params[3]); - for (MetadataValue dcvalue : dcvalues) { - if (dcvalue.getValue().equals(matchValue)) { - return dcvalue.getValue(); - } - } - return null; - } - - /** - * Removes the passed value from the set of values for the field in passed item. - * - * @param context The relevant DSpace Context. - * @param item - the item to update - * @param value - the value to remove - * @throws IOException A general class of exceptions produced by failed or interrupted I/O operations. - * @throws SQLException An exception that provides information on a database access error or other errors. - * @throws AuthorizeException Exception indicating the current user of the context does not have permission - * to perform a particular action. - */ - public void removeItemValue(Context context, Item item, String value) - throws AuthorizeException, IOException, SQLException { - if (value != null) { - List dcvalues = itemService.getMetadata(item, params[0], params[1], params[2], params[3]); - ArrayList arrayList = new ArrayList(); - for (MetadataValue dcvalue : dcvalues) { - if (!dcvalue.getValue().equals(value)) { - arrayList.add(dcvalue.getValue()); - } - } - itemService.clearMetadata(context, item, params[0], params[1], params[2], params[3]); - itemService.addMetadata(context, item, params[0], params[1], params[2], params[3], arrayList); - } - } - - /** - * Adds passed value to the set of values for the field in passed item. - * - * @param context The relevant DSpace Context. - * @param item - the item to update - * @param value - the value to add in this field - * @throws SQLException An exception that provides information on a database access error or other errors. - */ - public void addItemValue(Context context, Item item, String value) throws SQLException { - itemService.addMetadata(context, item, params[0], params[1], params[2], params[3], value); - } - -} diff --git a/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java b/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java index c5ea6475bb..fa32cb75ca 100644 --- a/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java +++ b/dspace-api/src/main/java/org/dspace/license/service/CreativeCommonsService.java @@ -31,13 +31,6 @@ public interface CreativeCommonsService { public static final String CC_BUNDLE_NAME = "CC-LICENSE"; - /** - * Simple accessor for enabling of CC - * - * @return is CC enabled? - */ - public boolean isEnabled(); - /** * setLicenseRDF * @@ -88,8 +81,6 @@ public interface CreativeCommonsService { public void removeLicenseFile(Context context, Item item) throws SQLException, IOException, AuthorizeException; - public boolean hasLicense(Context context, Item item) - throws SQLException, IOException; public String getLicenseURL(Context context, Item item) throws SQLException, IOException, AuthorizeException; @@ -111,10 +102,6 @@ public interface CreativeCommonsService { */ public String getLicenseName(Item item); - - public String getLicenseRDF(Context context, Item item) - throws SQLException, IOException, AuthorizeException; - /** * Get Creative Commons license RDF, returning Bitstream object. * diff --git a/dspace-api/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java b/dspace-api/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java index 41cd9805f6..bb443ab4a4 100644 --- a/dspace-api/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java +++ b/dspace-api/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java @@ -107,14 +107,18 @@ public class MockCCLicenseConnectorServiceImpl extends CCLicenseConnectorService */ public Document retrieveLicenseRDFDoc(String licenseURI) throws IOException { if (!StringUtils.contains(licenseURI, "invalid")) { + InputStream cclicense = null; try { - - InputStream cclicense = getClass().getResourceAsStream("cc-license-rdf.xml"); + cclicense = getClass().getResourceAsStream("cc-license-rdf.xml"); Document doc = parser.build(cclicense); return doc; } catch (JDOMException e) { - e.printStackTrace(); + throw new RuntimeException(e); + } finally { + if (cclicense != null) { + cclicense.close(); + } } } return null; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SubmissionCCLicenseRestEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SubmissionCCLicenseRestEvaluatorPlugin.java index 251717d799..ae925fe8b1 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SubmissionCCLicenseRestEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SubmissionCCLicenseRestEvaluatorPlugin.java @@ -1,12 +1,19 @@ +/** + * 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 java.io.Serializable; + import org.apache.commons.lang3.StringUtils; import org.dspace.app.rest.model.SubmissionCCLicenseRest; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; -import java.io.Serializable; - @Component public class SubmissionCCLicenseRestEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseAddPatchOperationIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseAddPatchOperationIT.java index 76b260f194..4f9c753047 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseAddPatchOperationIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseAddPatchOperationIT.java @@ -10,6 +10,8 @@ package org.dspace.app.rest; import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -55,6 +57,8 @@ public class CCLicenseAddPatchOperationIT extends AbstractControllerIntegrationT .withTitle("Workspace Item") .build(); + context.restoreAuthSystemState(); + String adminToken = getAuthToken(admin.getEmail(), password); List ops = new ArrayList(); @@ -93,6 +97,9 @@ public class CCLicenseAddPatchOperationIT extends AbstractControllerIntegrationT .withTitle("Workspace Item") .build(); + context.restoreAuthSystemState(); + + String adminToken = getAuthToken(admin.getEmail(), password); List ops = new ArrayList(); @@ -106,5 +113,13 @@ public class CCLicenseAddPatchOperationIT extends AbstractControllerIntegrationT .content(patchBody) .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) .andExpect(status().isInternalServerError()); + + getClient(adminToken).perform(get("/api/submission/workspaceitems/" + workspaceItem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections", not(hasJsonPath("cclicense")))); + + } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseRemovePatchOperationIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseRemovePatchOperationIT.java index b908b7acd7..afee0aa882 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseRemovePatchOperationIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseRemovePatchOperationIT.java @@ -57,6 +57,8 @@ public class CCLicenseRemovePatchOperationIT extends AbstractControllerIntegrati .withTitle("Workspace Item") .build(); + context.restoreAuthSystemState(); + String adminToken = getAuthToken(admin.getEmail(), password); // First add a license and verify it is added @@ -80,7 +82,6 @@ public class CCLicenseRemovePatchOperationIT extends AbstractControllerIntegrati ))); - // Remove the license again and verify it is removed List removeOps = new ArrayList(); diff --git a/dspace-server-webapp/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java b/dspace-server-webapp/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java index 41cd9805f6..bb443ab4a4 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java +++ b/dspace-server-webapp/src/test/java/org/dspace/license/MockCCLicenseConnectorServiceImpl.java @@ -107,14 +107,18 @@ public class MockCCLicenseConnectorServiceImpl extends CCLicenseConnectorService */ public Document retrieveLicenseRDFDoc(String licenseURI) throws IOException { if (!StringUtils.contains(licenseURI, "invalid")) { + InputStream cclicense = null; try { - - InputStream cclicense = getClass().getResourceAsStream("cc-license-rdf.xml"); + cclicense = getClass().getResourceAsStream("cc-license-rdf.xml"); Document doc = parser.build(cclicense); return doc; } catch (JDOMException e) { - e.printStackTrace(); + throw new RuntimeException(e); + } finally { + if (cclicense != null) { + cclicense.close(); + } } } return null; From 8e0b22ad6ca165a16fc608f8d424c66d2b69100d Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Wed, 29 Apr 2020 11:17:17 +0200 Subject: [PATCH 024/143] 70415: jurisdiction --- .../dspace/license/CreativeCommonsServiceImpl.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java b/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java index 7a48b6f03b..40e727d9df 100644 --- a/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/license/CreativeCommonsServiceImpl.java @@ -91,6 +91,8 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi protected ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); private String defaultLanguage; + private String jurisdiction; + private static final String JURISDICTION_KEY = "jurisdiction"; private Map> ccLicenses; @@ -113,6 +115,7 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi ccLicenses = new HashMap<>(); defaultLanguage = configurationService.getProperty("cc.license.locale", "en"); + jurisdiction = configurationService.getProperty("cc.license.jurisdiction", ""); try { templates = TransformerFactory.newInstance().newTemplates( @@ -601,9 +604,18 @@ public class CreativeCommonsServiceImpl implements CreativeCommonsService, Initi fullParamMap.put(ccLicenseField.getId(), ""); } } + + updateJurisdiction(fullParamMap); + return fullParamMap; } + private void updateJurisdiction(final Map fullParamMap) { + if (fullParamMap.containsKey(JURISDICTION_KEY)) { + fullParamMap.put(JURISDICTION_KEY, jurisdiction); + } + } + private boolean containsAnswerEnum(final String enumAnswer, final CCLicenseField ccLicenseField) { List fieldEnums = ccLicenseField.getFieldEnum(); for (CCLicenseFieldEnum fieldEnum : fieldEnums) { From b59aeecf0e09f5d215fd8b583d27659bbe66a80a Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Wed, 29 Apr 2020 14:03:03 +0200 Subject: [PATCH 025/143] Added extra cleanup to the EPersonRestRepositoryIT and optimized test in RegistrationRestControllerIT --- .../app/rest/EPersonRestRepositoryIT.java | 76 +++++++++++++++++++ .../rest/RegistrationRestControllerIT.java | 7 +- 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java index 53a054e2f0..43a790b8af 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java @@ -14,6 +14,7 @@ import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -55,6 +56,8 @@ import org.dspace.content.Item; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.eperson.PasswordHash; +import org.dspace.eperson.RegistrationData; +import org.dspace.eperson.dao.RegistrationDataDAO; import org.dspace.eperson.service.AccountService; import org.dspace.eperson.service.EPersonService; import org.dspace.eperson.service.RegistrationDataService; @@ -75,6 +78,9 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { @Autowired private EPersonService ePersonService; + @Autowired + private RegistrationDataDAO registrationDataDAO; + @Test public void createTest() throws Exception { context.turnOffAuthorisationSystem(); @@ -1672,6 +1678,10 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { PasswordHash newPasswordHash = ePersonService.getPasswordHash(ePerson); assertFalse(oldPassword.equals(newPasswordHash)); assertTrue(registrationDataService.findByEmail(context, ePerson.getEmail()) == null); + + context.turnOffAuthorisationSystem(); + registrationDataService.deleteByToken(context, tokenForEPerson); + context.restoreAuthSystemState(); } @@ -1709,6 +1719,10 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { assertFalse(registrationDataService.findByEmail(context, ePerson.getEmail()) == null); assertTrue(StringUtils.equals(registrationDataService.findByEmail(context, ePerson.getEmail()).getToken(), tokenForEPerson)); + + context.turnOffAuthorisationSystem(); + registrationDataService.deleteByToken(context, tokenForEPerson); + context.restoreAuthSystemState(); } @Test @@ -1753,6 +1767,11 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { PasswordHash newPasswordHash = ePersonService.getPasswordHash(ePerson); assertTrue(StringUtils.equalsIgnoreCase(oldPassword.getHashString(),newPasswordHash.getHashString())); assertFalse(registrationDataService.findByEmail(context, ePerson.getEmail()) == null); + + context.turnOffAuthorisationSystem(); + registrationDataService.deleteByToken(context, tokenForEPerson); + registrationDataService.deleteByToken(context, tokenForEPersonTwo); + context.restoreAuthSystemState(); } @Test @@ -1789,6 +1808,12 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { assertTrue(StringUtils.equalsIgnoreCase(oldPassword.getHashString(),newPasswordHash.getHashString())); assertFalse(registrationDataService.findByEmail(context, ePerson.getEmail()) == null); assertTrue(StringUtils.equalsIgnoreCase(ePerson.getEmail(), originalEmail)); + + context.turnOffAuthorisationSystem(); + registrationDataService.delete(context, registrationDataService.findByEmail(context, ePerson.getEmail())); + registrationDataService.deleteByToken(context, tokenForEPerson); + context.restoreAuthSystemState(); + } @Test @@ -1833,6 +1858,11 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { assertTrue(StringUtils.equalsIgnoreCase(oldPassword.getHashString(),newPasswordHash.getHashString())); assertFalse(registrationDataService.findByEmail(context, ePerson.getEmail()) == null); assertFalse(registrationDataService.findByEmail(context, newRegisterEmail) == null); + + context.turnOffAuthorisationSystem(); + registrationDataService.delete(context, registrationDataService.findByEmail(context, ePerson.getEmail())); + registrationDataService.deleteByToken(context, newRegisterToken); + context.restoreAuthSystemState(); } @Test @@ -1880,6 +1910,10 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { context.turnOffAuthorisationSystem(); ePersonService.delete(context, createdEPerson); context.restoreAuthSystemState(); + + context.turnOffAuthorisationSystem(); + registrationDataService.deleteByToken(context, newRegisterToken); + context.restoreAuthSystemState(); } @Test @@ -1928,6 +1962,11 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { context.turnOffAuthorisationSystem(); ePersonService.delete(context, createdEPerson); context.restoreAuthSystemState(); + + context.turnOffAuthorisationSystem(); + registrationDataService.deleteByToken(context, newRegisterToken); + context.restoreAuthSystemState(); + } @Test @@ -1976,6 +2015,11 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { context.turnOffAuthorisationSystem(); ePersonService.delete(context, createdEPerson); context.restoreAuthSystemState(); + + context.turnOffAuthorisationSystem(); + registrationDataService.deleteByToken(context, newRegisterToken); + context.restoreAuthSystemState(); + } @Test @@ -2017,6 +2061,11 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { assertNull(createdEPerson); assertNotNull(registrationDataService.findByToken(context, newRegisterToken)); assertNotNull(registrationDataService.findByToken(context, newRegisterTokenTwo)); + + context.turnOffAuthorisationSystem(); + registrationDataService.deleteByToken(context, newRegisterToken); + registrationDataService.deleteByToken(context, newRegisterTokenTwo); + context.restoreAuthSystemState(); } @Test @@ -2047,6 +2096,10 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail); assertNull(createdEPerson); assertNotNull(registrationDataService.findByToken(context, newRegisterToken)); + + context.turnOffAuthorisationSystem(); + registrationDataService.deleteByToken(context, newRegisterToken); + context.restoreAuthSystemState(); } @Test @@ -2077,6 +2130,10 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail); assertNull(createdEPerson); assertNotNull(registrationDataService.findByToken(context, newRegisterToken)); + + context.turnOffAuthorisationSystem(); + registrationDataService.deleteByToken(context, newRegisterToken); + context.restoreAuthSystemState(); } @Test @@ -2106,6 +2163,10 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail); assertNull(createdEPerson); assertNotNull(registrationDataService.findByToken(context, newRegisterToken)); + + context.turnOffAuthorisationSystem(); + registrationDataService.deleteByToken(context, newRegisterToken); + context.restoreAuthSystemState(); } @Test @@ -2135,6 +2196,10 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail); assertNull(createdEPerson); assertNotNull(registrationDataService.findByToken(context, newRegisterToken)); + + context.turnOffAuthorisationSystem(); + registrationDataService.deleteByToken(context, newRegisterToken); + context.restoreAuthSystemState(); } @Test @@ -2165,6 +2230,11 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail); assertNull(createdEPerson); assertNotNull(registrationDataService.findByToken(context, newRegisterToken)); + + context.turnOffAuthorisationSystem(); + registrationDataService.deleteByToken(context, newRegisterToken); + context.restoreAuthSystemState(); + } @Test @@ -2195,6 +2265,11 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { EPerson createdEPerson = ePersonService.findByEmail(context, newEmail); assertNull(createdEPerson); assertNotNull(registrationDataService.findByToken(context, forgotPasswordToken)); + + context.turnOffAuthorisationSystem(); + registrationDataService.deleteByToken(context, forgotPasswordToken); + context.restoreAuthSystemState(); + } @Test @@ -2241,6 +2316,7 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { context.turnOffAuthorisationSystem(); ePersonService.delete(context, createdEPerson); + registrationDataService.deleteByToken(context, newRegisterToken); context.restoreAuthSystemState(); } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java index 0201eb284f..d3147d4611 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java @@ -7,6 +7,7 @@ */ package org.dspace.app.rest; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -35,7 +36,7 @@ public class RegistrationRestControllerIT extends AbstractControllerIntegrationT @Test public void registrationFlowTest() throws Exception { List registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); - assertTrue(registrationDataList.isEmpty()); + assertEquals(0, registrationDataList.size()); ObjectMapper mapper = new ObjectMapper(); RegistrationRest registrationRest = new RegistrationRest(); @@ -45,7 +46,7 @@ public class RegistrationRestControllerIT extends AbstractControllerIntegrationT .contentType(contentType)) .andExpect(status().isCreated()); registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); - assertTrue(registrationDataList.size() == 1); + assertEquals(1, registrationDataList.size()); assertTrue(StringUtils.equalsIgnoreCase(registrationDataList.get(0).getEmail(), eperson.getEmail())); String newEmail = "newEPersonTest@gmail.com"; @@ -67,7 +68,7 @@ public class RegistrationRestControllerIT extends AbstractControllerIntegrationT .contentType(contentType)) .andExpect(status().is(401)); - assertTrue(registrationDataList.size() == 2); + assertEquals(2, registrationDataList.size()); assertTrue(!StringUtils.equalsIgnoreCase(registrationDataList.get(0).getEmail(), newEmail) && !StringUtils.equalsIgnoreCase(registrationDataList.get(1).getEmail(), newEmail)); From 91d70d85cec585cae299aac972d2f1c7181957bf Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Thu, 30 Apr 2020 17:40:29 +0200 Subject: [PATCH 026/143] added ITs to prove that patch request with wrong path or metadata must return 422 --- .../rest/WorkspaceItemRestRepositoryIT.java | 337 ++++++++++++++++++ 1 file changed, 337 insertions(+) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java index 35b4956846..b2f4de1d0e 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java @@ -3160,4 +3160,341 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration .andExpect(status().isUnauthorized()); } + @Test + public void patchAddMetadataOnSectionNotExistentTest() throws Exception { + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, child1) + .withName("Collection 1") + .build(); + + WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, col1) + .withIssueDate("2019-04-25") + .withSubject("ExtraEntry") + .build(); + + //disable file upload mandatory + configurationService.setProperty("webui.submit.upload.required", false); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(eperson.getEmail(), password); + + List addTitle = new ArrayList(); + List> values = new ArrayList>(); + Map value = new HashMap(); + value.put("value", "New Title"); + values.add(value); + addTitle.add(new AddOperation("/sections/not-existing-section/dc.title", values)); + + String patchBody = getPatchContent(addTitle); + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void patchAddMetadataWrongAttributeTest() throws Exception { + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, child1) + .withName("Collection 1") + .build(); + + WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, col1) + .withIssueDate("2019-04-25") + .withSubject("ExtraEntry") + .build(); + + //disable file upload mandatory + configurationService.setProperty("webui.submit.upload.required", false); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(eperson.getEmail(), password); + + List addTitle = new ArrayList(); + List> values = new ArrayList>(); + Map value = new HashMap(); + value.put("value", "New Title"); + values.add(value); + addTitle.add(new AddOperation("/sections/traditionalpageone/dc.not.existing", values)); + + String patchBody = getPatchContent(addTitle); + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + // try to add Title on tradiotionalpagetwo, but attribute title is placed on tradiotionalpageone + public void patchAddTitleOnSectionThatNotContainAttributeTitleTest() throws Exception { + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, child1) + .withName("Collection 1") + .build(); + + WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, col1) + .withIssueDate("2019-11-21") + .withSubject("ExtraEntry") + .build(); + + //disable file upload mandatory + configurationService.setProperty("webui.submit.upload.required", false); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(eperson.getEmail(), password); + + List addTitle = new ArrayList(); + List> values = new ArrayList>(); + Map value = new HashMap(); + value.put("value", "New Title"); + values.add(value); + addTitle.add(new AddOperation("/sections/traditionalpagetwo/dc.title", values)); + + String patchBody = getPatchContent(addTitle); + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void patchAcceptLicenseWrontPathTest() throws Exception { + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, child1) + .withName("Collection 1") + .build(); + + WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, col1) + .withTitle("Example Title") + .withIssueDate("2019-11-21") + .withSubject("ExtraEntry") + .build(); + + //disable file upload mandatory + configurationService.setProperty("webui.submit.upload.required", false); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(eperson.getEmail(), password); + + List replaceGrant = new ArrayList(); + replaceGrant.add(new ReplaceOperation("/sections/license/not-existing", true)); + + String patchBody = getPatchContent(replaceGrant); + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void patchAcceptLicenseTryToChangeLicenseUrlTest() throws Exception { + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, child1) + .withName("Collection 1") + .build(); + + WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, col1) + .withTitle("Example Title") + .withIssueDate("2019-11-21") + .withSubject("ExtraEntry") + .build(); + + //disable file upload mandatory + configurationService.setProperty("webui.submit.upload.required", false); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(eperson.getEmail(), password); + + List replaceGrant = new ArrayList(); + replaceGrant.add(new ReplaceOperation("/sections/license/granted", true)); + + String patchBody = getPatchContent(replaceGrant); + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()); + + List replaceLicenseUrl = new ArrayList(); + replaceLicenseUrl.add(new ReplaceOperation("/sections/license/url", "not.existing")); + + patchBody = getPatchContent(replaceLicenseUrl); + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void patchAcceptLicenseTryToChangeDateAccepteLicenseTest() throws Exception { + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, child1) + .withName("Collection 1") + .build(); + + WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, col1) + .withTitle("Example Title") + .withIssueDate("2019-11-21") + .withSubject("ExtraEntry") + .build(); + + //disable file upload mandatory + configurationService.setProperty("webui.submit.upload.required", false); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(eperson.getEmail(), password); + + List replaceGrant = new ArrayList(); + replaceGrant.add(new ReplaceOperation("/sections/license/granted", true)); + + String patchBody = getPatchContent(replaceGrant); + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()); + + List replaceLicenseUrl = new ArrayList(); + replaceLicenseUrl.add(new ReplaceOperation("/sections/license/acceptanceDate", "2020-02-14")); + + patchBody = getPatchContent(replaceLicenseUrl); + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void patchUploadMetadatoNotExistTest() throws Exception { + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1) + .withName("Collection 1") + .build(); + + InputStream pdf = getClass().getResourceAsStream("simple-article.pdf"); + + WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, col1) + .withTitle("Test WorkspaceItem") + .withIssueDate("2019-10-25") + .withFulltext("simple-article.pdf", "/local/path/simple-article.pdf", pdf) + .build(); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(eperson.getEmail(), password); + + List addOpts = new ArrayList(); + Map value = new HashMap(); + value.put("value", "some text"); + addOpts.add(new AddOperation("/sections/upload/files/0/metadata/dc.not.existing", value)); + + String patchBody = getPatchContent(addOpts); + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void patchUploadWrongPathTest() throws Exception { + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1) + .withName("Collection 1") + .build(); + + InputStream pdf = getClass().getResourceAsStream("simple-article.pdf"); + + WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, col1) + .withTitle("Test WorkspaceItem") + .withIssueDate("2017-10-17") + .withFulltext("simple-article.pdf", "/local/path/simple-article.pdf", pdf) + .build(); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(eperson.getEmail(), password); + + List addOpts = new ArrayList(); + Map value = new HashMap(); + value.put("value", "2020-01-25"); + addOpts.add(new AddOperation("/sections/upload/files/0/metadata/dc.date.issued", value)); + + String patchBody = getPatchContent(addOpts); + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()); + } } From 676648568dd07360b29489210fc2df659833db52 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Thu, 30 Apr 2020 18:58:52 +0200 Subject: [PATCH 027/143] fix return http status for wrong patch request to the submission --- .../WorkspaceItemRestRepository.java | 13 ++++++- .../submit/AbstractRestProcessingStep.java | 4 +- .../app/rest/submit/step/CollectionStep.java | 4 +- .../app/rest/submit/step/DescribeStep.java | 17 ++++++--- .../app/rest/submit/step/LicenseStep.java | 7 +++- .../app/rest/submit/step/UploadStep.java | 37 +++++++++++++++++-- 6 files changed, 67 insertions(+), 15 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkspaceItemRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkspaceItemRestRepository.java index 91d6f0c652..0b52174050 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkspaceItemRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkspaceItemRestRepository.java @@ -27,6 +27,7 @@ import org.dspace.app.rest.SearchRestMethod; import org.dspace.app.rest.converter.WorkspaceItemConverter; 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.ErrorRest; import org.dspace.app.rest.model.WorkspaceItemRest; import org.dspace.app.rest.model.patch.Operation; @@ -294,6 +295,7 @@ public class WorkspaceItemRestRepository extends DSpaceRestRepository!"); } + } catch (UnprocessableEntityException e) { + log.error(e.getMessage(), e); + throw new UnprocessableEntityException("Error processing the patch request", e); } catch (Exception e) { log.error(e.getMessage(), e); throw new PatchException("Error processing the patch request", e); } } } + if (!sectionExist) { + throw new UnprocessableEntityException("The section with name " + section + + " does not exist in this submission!"); + } } @PreAuthorize("hasPermission(#id, 'WORKSPACEITEM', 'DELETE')") diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/AbstractRestProcessingStep.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/AbstractRestProcessingStep.java index 58bfaba27b..49f4d818a3 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/AbstractRestProcessingStep.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/AbstractRestProcessingStep.java @@ -94,7 +94,7 @@ public interface AbstractRestProcessingStep extends ListenerProcessingStep { * the json patch operation * @throws Exception */ - public void doPatchProcessing(Context context, Request currentRequest, InProgressSubmission source, Operation op) - throws Exception; + public void doPatchProcessing(Context context, Request currentRequest, InProgressSubmission source, Operation op, + SubmissionStepConfig stepConf) throws Exception; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/CollectionStep.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/CollectionStep.java index 450c214d79..0835f4cde3 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/CollectionStep.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/CollectionStep.java @@ -36,8 +36,8 @@ public class CollectionStep extends org.dspace.submit.step.SelectCollectionStep } @Override - public void doPatchProcessing(Context context, Request currentRequest, InProgressSubmission source, Operation op) - throws Exception { + public void doPatchProcessing(Context context, Request currentRequest, InProgressSubmission source, Operation op, + SubmissionStepConfig stepConf) throws Exception { PatchOperation patchOperation = new PatchOperationFactory() .instanceOf(COLLECTION_STEP_OPERATION_ENTRY, op.getOp()); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/DescribeStep.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/DescribeStep.java index 2fa8e264c7..b0134973a9 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/DescribeStep.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/DescribeStep.java @@ -11,6 +11,7 @@ import java.util.ArrayList; import java.util.List; import org.apache.logging.log4j.Logger; +import org.dspace.app.rest.exception.UnprocessableEntityException; import org.dspace.app.rest.model.MetadataValueRest; import org.dspace.app.rest.model.patch.Operation; import org.dspace.app.rest.model.step.DataDescribe; @@ -110,13 +111,19 @@ public class DescribeStep extends org.dspace.submit.step.DescribeStep implements } @Override - public void doPatchProcessing(Context context, Request currentRequest, InProgressSubmission source, Operation op) - throws Exception { + public void doPatchProcessing(Context context, Request currentRequest, InProgressSubmission source, Operation op, + SubmissionStepConfig stepConf) throws Exception { PatchOperation patchOperation = new PatchOperationFactory() - .instanceOf(DESCRIBE_STEP_METADATA_OPERATION_ENTRY, op.getOp()); - patchOperation.perform(context, currentRequest, source, op); - + .instanceOf(DESCRIBE_STEP_METADATA_OPERATION_ENTRY, op.getOp()); + DCInputSet inputConfig = inputReader.getInputsByFormName(stepConf.getId()); + String[] split = patchOperation.getAbsolutePath(op.getPath()).split("/"); + if (inputConfig.isFieldPresent(split[0])) { + patchOperation.perform(context, currentRequest, source, op); + } else { + throw new UnprocessableEntityException("The attribute " + split[0] + " does not present in section " + + inputConfig.getFormName()); + } } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/LicenseStep.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/LicenseStep.java index 7ed0ffd746..a57951d09a 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/LicenseStep.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/LicenseStep.java @@ -8,6 +8,7 @@ package org.dspace.app.rest.submit.step; import org.atteo.evo.inflector.English; +import org.dspace.app.rest.exception.UnprocessableEntityException; import org.dspace.app.rest.model.BitstreamRest; import org.dspace.app.rest.model.patch.Operation; import org.dspace.app.rest.model.step.DataLicense; @@ -50,8 +51,8 @@ public class LicenseStep extends org.dspace.submit.step.LicenseStep implements A } @Override - public void doPatchProcessing(Context context, Request currentRequest, InProgressSubmission source, Operation op) - throws Exception { + public void doPatchProcessing(Context context, Request currentRequest, InProgressSubmission source, Operation op, + SubmissionStepConfig stepConf) throws Exception { if (op.getPath().endsWith(LICENSE_STEP_OPERATION_ENTRY)) { @@ -59,6 +60,8 @@ public class LicenseStep extends org.dspace.submit.step.LicenseStep implements A .instanceOf(LICENSE_STEP_OPERATION_ENTRY, op.getOp()); patchOperation.perform(context, currentRequest, source, op); + } else { + throw new UnprocessableEntityException("This path : " + op.getPath() + " can not to be replaced"); } } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/UploadStep.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/UploadStep.java index ca46e5608b..f32d686efe 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/UploadStep.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/UploadStep.java @@ -12,6 +12,7 @@ import java.io.InputStream; import java.util.List; import org.apache.logging.log4j.Logger; +import org.dspace.app.rest.exception.UnprocessableEntityException; import org.dspace.app.rest.model.ErrorRest; import org.dspace.app.rest.model.patch.Operation; import org.dspace.app.rest.model.step.DataUpload; @@ -23,6 +24,9 @@ import org.dspace.app.rest.submit.UploadableStep; import org.dspace.app.rest.submit.factory.PatchOperationFactory; import org.dspace.app.rest.submit.factory.impl.PatchOperation; import org.dspace.app.rest.utils.Utils; +import org.dspace.app.util.DCInputSet; +import org.dspace.app.util.DCInputsReader; +import org.dspace.app.util.DCInputsReaderException; import org.dspace.app.util.SubmissionStepConfig; import org.dspace.content.Bitstream; import org.dspace.content.BitstreamFormat; @@ -45,6 +49,13 @@ public class UploadStep extends org.dspace.submit.step.UploadStep private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(UploadStep.class); + public static final String UPLOAD_STEP_METADATA_SECTION = "bitstream-metadata"; + + private DCInputsReader inputReader; + + public UploadStep() throws DCInputsReaderException { + inputReader = new DCInputsReader(); + } @Override public DataUpload getData(SubmissionService submissionService, InProgressSubmission obj, SubmissionStepConfig config) throws Exception { @@ -61,8 +72,8 @@ public class UploadStep extends org.dspace.submit.step.UploadStep } @Override - public void doPatchProcessing(Context context, Request currentRequest, InProgressSubmission source, Operation op) - throws Exception { + public void doPatchProcessing(Context context, Request currentRequest, InProgressSubmission source, Operation op, + SubmissionStepConfig stepConf) throws Exception { String instance = ""; if ("remove".equals(op.getOp())) { @@ -87,8 +98,28 @@ public class UploadStep extends org.dspace.submit.step.UploadStep } } PatchOperation patchOperation = new PatchOperationFactory().instanceOf(instance, op.getOp()); - patchOperation.perform(context, currentRequest, source, op); + if (instance.equals(AbstractRestProcessingStep.UPLOAD_STEP_METADATA_OPERATION_ENTRY)) { + DCInputSet inputConfig = inputReader.getInputsByFormName(UploadStep.UPLOAD_STEP_METADATA_SECTION); + String[] split = patchOperation.getAbsolutePath(op.getPath()).split("/"); + String metadata = findMetadata(split); + if (inputConfig.isFieldPresent(metadata)) { + patchOperation.perform(context, currentRequest, source, op); + } else { + throw new UnprocessableEntityException("The attribute " + metadata + " does not present in section " + + UploadStep.UPLOAD_STEP_METADATA_SECTION); + } + } else { + patchOperation.perform(context, currentRequest, source, op); + } + } + private String findMetadata(String[] metadata) { + for (String s : metadata) { + if (s.contains("dc.")) { + return s; + } + } + return null; } @Override From 6ef11e491962dc5dbf2281752b13ca67ef9a923d Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Mon, 4 May 2020 09:18:22 +0200 Subject: [PATCH 028/143] added the missing parameter --- .../dspace/app/rest/repository/WorkflowItemRestRepository.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkflowItemRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkflowItemRestRepository.java index d2fdb09280..dca3bd354b 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkflowItemRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkflowItemRestRepository.java @@ -292,7 +292,8 @@ public class WorkflowItemRestRepository extends DSpaceRestRepository Date: Tue, 5 May 2020 11:59:05 +0200 Subject: [PATCH 029/143] Fixed checkstyle in the EPersonRestRepositoryIT class --- .../test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java index 43a790b8af..8f48ac4a90 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java @@ -14,7 +14,6 @@ import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -56,7 +55,6 @@ import org.dspace.content.Item; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.eperson.PasswordHash; -import org.dspace.eperson.RegistrationData; import org.dspace.eperson.dao.RegistrationDataDAO; import org.dspace.eperson.service.AccountService; import org.dspace.eperson.service.EPersonService; From b5bbc72eceb1fcf3b323007dfb57affe32109524 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Fri, 8 May 2020 12:57:07 +0200 Subject: [PATCH 030/143] 70815: Angular feedback --- .../SubmissionCCLicenseSearchController.java | 12 +++---- .../app/rest/model/PlainTextValueRest.java | 36 +++++++++++++++++++ ...SubmissionCCLicenseSearchControllerIT.java | 9 +++-- 3 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/model/PlainTextValueRest.java diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseSearchController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseSearchController.java index 185fb7e8eb..26e52c8933 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseSearchController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseSearchController.java @@ -12,9 +12,8 @@ import java.util.Map; import javax.servlet.ServletRequest; import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.model.PlainTextValueRest; import org.dspace.app.rest.model.SubmissionCCLicenseRest; import org.dspace.app.rest.utils.Utils; import org.dspace.license.service.CreativeCommonsService; @@ -24,7 +23,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.rest.webmvc.ResourceNotFoundException; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; /** @@ -35,8 +33,6 @@ import org.springframework.web.bind.annotation.RestController; "/rightsByQuestions") public class SubmissionCCLicenseSearchController { - private static final Logger log = LogManager.getLogger(); - @Autowired protected Utils utils; @@ -52,8 +48,7 @@ public class SubmissionCCLicenseSearchController { * @return the CC License URI as a string */ @RequestMapping(method = RequestMethod.GET) - @ResponseBody - public String findByRightsByQuestions() { + public PlainTextValueRest findByRightsByQuestions() { ServletRequest servletRequest = requestService.getCurrentRequest() .getServletRequest(); Map requestParameterMap = servletRequest @@ -90,6 +85,7 @@ public class SubmissionCCLicenseSearchController { if (StringUtils.isBlank(licenseUri)) { throw new ResourceNotFoundException("No CC License URI could be found for ID: " + licenseId); } - return licenseUri; + PlainTextValueRest plainTextValueRest = new PlainTextValueRest(licenseUri); + return plainTextValueRest; } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/PlainTextValueRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/PlainTextValueRest.java new file mode 100644 index 0000000000..6f02aa9286 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/PlainTextValueRest.java @@ -0,0 +1,36 @@ +/** + * 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; + +/** + * Rest object used to represent a plain text value + */ +public class PlainTextValueRest { + public static final String TYPE = "plaintextvalue"; + + private String value; + + public PlainTextValueRest() { + } + + public PlainTextValueRest(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(final String value) { + this.value = value; + } + + public String getType() { + return TYPE; + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseSearchControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseSearchControllerIT.java index 402f4c3a69..5c35bb5acf 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseSearchControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseSearchControllerIT.java @@ -8,8 +8,9 @@ package org.dspace.app.rest; +import static org.hamcrest.Matchers.is; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +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.test.AbstractControllerIntegrationTest; @@ -32,14 +33,16 @@ public class SubmissionCCLicenseSearchControllerIT extends AbstractControllerInt "/api/config/submissioncclicenses/search/rightsByQuestions?license=license2&answer_license2-field0" + "=license2-field0-enum1")) .andExpect(status().isOk()) - .andExpect(content().string("mock-license-uri")); + .andExpect(jsonPath("$.value", is("mock-license-uri"))) + .andExpect(jsonPath("$.type", is("plaintextvalue"))); } @Test public void searchRightsByQuestionsTestLicenseWithoutFields() throws Exception { getClient().perform(get("/api/config/submissioncclicenses/search/rightsByQuestions?license=license3")) .andExpect(status().isOk()) - .andExpect(content().string("mock-license-uri")); + .andExpect(jsonPath("$.value", is("mock-license-uri"))) + .andExpect(jsonPath("$.type", is("plaintextvalue"))); } @Test From 3dc9f511d4a8797079a8d08fa84b93a949401127 Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Fri, 8 May 2020 15:43:18 +0200 Subject: [PATCH 031/143] [Task 70808] changed the link the mail to include the token in the url instead of as a parameter --- .../src/main/java/org/dspace/eperson/AccountServiceImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/eperson/AccountServiceImpl.java b/dspace-api/src/main/java/org/dspace/eperson/AccountServiceImpl.java index 9282fc116e..2121cd669d 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/AccountServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/eperson/AccountServiceImpl.java @@ -239,8 +239,8 @@ public class AccountServiceImpl implements AccountService { // Note change from "key=" to "token=" String specialLink = new StringBuffer().append(base).append( base.endsWith("/") ? "" : "/").append( - isRegister ? "register" : "forgot").append("?") - .append("token=").append(rd.getToken()) + isRegister ? "register" : "forgot").append("/") + .append(rd.getToken()) .toString(); Locale locale = context.getCurrentLocale(); Email bean = Email.getEmail(I18nUtil.getEmailFilename(locale, isRegister ? "register" From b18b52a621f9b75d92417c4a33730e56343a19b4 Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Mon, 11 May 2020 08:59:47 +0200 Subject: [PATCH 032/143] [Task 70808] added javadocs --- .../app/rest/RegistrationRestController.java | 19 +++++++++++++++++++ .../impl/EPersonRegistrationFeature.java | 4 ++++ .../app/rest/model/RegistrationRest.java | 5 +++++ .../model/hateoas/RegistrationResource.java | 4 ++++ .../repository/EPersonRestRepository.java | 4 ++++ .../RegistrationRestRepository.java | 10 ++++++++++ 6 files changed, 46 insertions(+) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java index b03eda36f0..f24b723e36 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java @@ -40,6 +40,9 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; +/** + * This will be the Controller class that handles calls to the /api/eperson/registrations endpoints + */ @RestController @RequestMapping("/api/" + RegistrationRest.CATEGORY + "/" + RegistrationRest.NAME_PLURAL) public class RegistrationRestController { @@ -59,6 +62,22 @@ public class RegistrationRestController { @Autowired private EPersonService ePersonService; + /** + * This method will be used to either register a new user or to send forgotten password info in a mail. + * It can be called by doing a POST request to the /api/eperson/registrations endpoint. + * It'll create a RegistrationRest object from the inputstream in the request and it'll check whether the email + * defined in that object is in the DB or not. + * If it is in the db then we'll send the forgotten password info, if it wasn't in the database then we'll send + * registration info. + * + * @param request The current request + * @param response The current response + * @return An empty response containing a 201 status code + * @throws SQLException If something goes wrong + * @throws IOException If something goes wrong + * @throws MessagingException If something goes wrong + * @throws AuthorizeException If something goes wrong + */ @RequestMapping(method = RequestMethod.POST) public ResponseEntity register(HttpServletRequest request, HttpServletResponse response) throws SQLException, IOException, MessagingException, AuthorizeException { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EPersonRegistrationFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EPersonRegistrationFeature.java index 44de9ce6d7..d230d2ef97 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EPersonRegistrationFeature.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EPersonRegistrationFeature.java @@ -20,6 +20,10 @@ import org.dspace.services.RequestService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +/** + * The EPerson Registration feature. It's able to be used on site objects if the user.registration property is set to + * true. If it's set to true, it'll check if the current context is allowed to set the password. + */ @Component @AuthorizationFeatureDocumentation(name = EPersonRegistrationFeature.NAME, description = "It can be used to register an eperson") diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java index b84699f140..69ed1d5653 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java @@ -13,6 +13,11 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.dspace.app.rest.RegistrationRestController; +/** + * This class acts as the REST representation of the RegistrationData model class. + * This class acts as a data holder for the RegistrationResource + * Refer to {@link org.dspace.eperson.RegistrationData} for explanation about the properties + */ public class RegistrationRest extends RestAddressableModel { public static final String NAME = "registration"; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/RegistrationResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/RegistrationResource.java index 0d92b0d41e..da53b680d6 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/RegistrationResource.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/RegistrationResource.java @@ -10,6 +10,10 @@ package org.dspace.app.rest.model.hateoas; import org.dspace.app.rest.model.RegistrationRest; import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource; +/** + * Registration HAL Resource. This resource adds the data from the REST object together with embedded objects + * and a set of links if applicable + */ @RelNameDSpaceResource(RegistrationRest.NAME) public class RegistrationResource extends HALResource { public RegistrationResource(RegistrationRest content) { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java index fa69d15253..43e46a85c9 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java @@ -93,6 +93,7 @@ public class EPersonRestRepository extends DSpaceObjectRestRepository { @@ -49,6 +52,13 @@ public class RegistrationRestRepository extends DSpaceRestRepository Date: Mon, 11 May 2020 11:49:57 +0200 Subject: [PATCH 033/143] [Task 70808] applied feedback to the registration implementation --- .../org/dspace/app/util/AuthorizeUtil.java | 44 +++++++++++++++++++ .../dspace/eperson/AccountServiceImpl.java | 12 +++++ .../eperson/service/AccountService.java | 2 + .../app/rest/RegistrationRestController.java | 25 ++++++----- .../impl/EPersonRegistrationFeature.java | 17 +++---- .../repository/EPersonRestRepository.java | 32 ++++++-------- .../EPersonPasswordReplaceOperation.java | 5 +++ .../app/rest/EPersonRestRepositoryIT.java | 44 +++++++------------ .../rest/RegistrationRestControllerIT.java | 26 +++++++++++ .../EPersonRegistrationFeatureIT.java | 18 ++------ 10 files changed, 142 insertions(+), 83 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/util/AuthorizeUtil.java b/dspace-api/src/main/java/org/dspace/app/util/AuthorizeUtil.java index 6c4271e1f2..d5dbf5a514 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/AuthorizeUtil.java +++ b/dspace-api/src/main/java/org/dspace/app/util/AuthorizeUtil.java @@ -9,7 +9,10 @@ package org.dspace.app.util; import java.sql.SQLException; import java.util.List; +import javax.servlet.http.HttpServletRequest; +import org.apache.logging.log4j.Logger; +import org.dspace.authenticate.factory.AuthenticateServiceFactory; import org.dspace.authorize.AuthorizeConfiguration; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.ResourcePolicy; @@ -25,6 +28,8 @@ import org.dspace.content.service.CollectionService; import org.dspace.content.service.ItemService; import org.dspace.core.Constants; import org.dspace.core.Context; +import org.dspace.eperson.factory.EPersonServiceFactory; +import org.dspace.services.factory.DSpaceServicesFactory; /** * This class is an addition to the AuthorizeManager that perform authorization @@ -34,6 +39,7 @@ import org.dspace.core.Context; */ public class AuthorizeUtil { + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(AuthorizeUtil.class); /** * Default constructor */ @@ -525,4 +531,42 @@ public class AuthorizeUtil { } } } + + /** + * This method will return a boolean indicating whether the current user is allowed to register a new + * account or not + * @param context The relevant DSpace context + * @param request The current request + * @return A boolean indicating whether the current user can register a new account or not + * @throws SQLException If something goes wrong + */ + public static boolean authorizeNewAccountRegistration(Context context, HttpServletRequest request) + throws SQLException { + if (DSpaceServicesFactory.getInstance().getConfigurationService() + .getBooleanProperty("user.registration", true)) { + return AuthenticateServiceFactory.getInstance().getAuthenticationService() + .allowSetPassword(context, request, null); + } + return false; + } + + /** + * This method will return a boolean indicating whether it's allowed to update the password for the EPerson + * with the given email and canLogin property + * @param context The relevant DSpace context + * @param email The email to be checked + * @param canLogin The boolean canLogin property + * @return A boolean indicating if the password can be updated or not + */ + public static boolean authorizeUpdatePassword(Context context, String email, boolean canLogin) { + try { + if (EPersonServiceFactory.getInstance().getEPersonService().findByEmail(context, email) != null + && canLogin) { + return true; + } + } catch (SQLException e) { + log.error("Something went wrong trying to retrieve EPerson for email: " + email, e); + } + return false; + } } diff --git a/dspace-api/src/main/java/org/dspace/eperson/AccountServiceImpl.java b/dspace-api/src/main/java/org/dspace/eperson/AccountServiceImpl.java index 2121cd669d..1aeaeaef8e 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/AccountServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/eperson/AccountServiceImpl.java @@ -12,6 +12,7 @@ import java.sql.SQLException; import java.util.Locale; import javax.mail.MessagingException; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.dspace.authorize.AuthorizeException; import org.dspace.core.ConfigurationManager; @@ -161,6 +162,17 @@ public class AccountServiceImpl implements AccountService { registrationDataService.deleteByToken(context, token); } + @Override + public boolean verifyPasswordStructure(String password) { + if (StringUtils.isBlank(password)) { + return false; + } + if (StringUtils.length(password) < 6) { + return false; + } + return true; + } + /** * THIS IS AN INTERNAL METHOD. THE SEND PARAMETER ALLOWS IT TO BE USED FOR * TESTING PURPOSES. diff --git a/dspace-api/src/main/java/org/dspace/eperson/service/AccountService.java b/dspace-api/src/main/java/org/dspace/eperson/service/AccountService.java index c8ecb0cc67..23d46ea00a 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/service/AccountService.java +++ b/dspace-api/src/main/java/org/dspace/eperson/service/AccountService.java @@ -46,4 +46,6 @@ public interface AccountService { public void deleteToken(Context context, String token) throws SQLException; + + public boolean verifyPasswordStructure(String password); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java index f24b723e36..741efa995b 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java @@ -16,18 +16,17 @@ import javax.servlet.http.HttpServletResponse; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.StringUtils; -import org.dspace.app.rest.authorization.AuthorizationFeature; import org.dspace.app.rest.authorization.AuthorizationFeatureService; import org.dspace.app.rest.converter.ConverterService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; import org.dspace.app.rest.exception.UnprocessableEntityException; import org.dspace.app.rest.model.RegistrationRest; -import org.dspace.app.rest.model.SiteRest; -import org.dspace.app.rest.projection.Projection; import org.dspace.app.rest.utils.ContextUtil; +import org.dspace.app.util.AuthorizeUtil; import org.dspace.authorize.AuthorizeException; -import org.dspace.content.Site; import org.dspace.content.service.SiteService; import org.dspace.core.Context; +import org.dspace.eperson.EPerson; import org.dspace.eperson.service.AccountService; import org.dspace.eperson.service.EPersonService; import org.springframework.beans.factory.annotation.Autowired; @@ -83,13 +82,6 @@ public class RegistrationRestController { throws SQLException, IOException, MessagingException, AuthorizeException { Context context = ContextUtil.obtainContext(request); - AuthorizationFeature epersonRegistration = authorizationFeatureService.find("epersonRegistration"); - Site site = siteService.findSite(context); - SiteRest siteRest = converterService.toRest(site, Projection.DEFAULT); - if (!authorizationFeatureService.isAuthorized(context, epersonRegistration, siteRest)) { - throw new AccessDeniedException( - "Registration is disabled, you are not authorized to create a new Authorization"); - } ObjectMapper mapper = new ObjectMapper(); RegistrationRest registrationRest; try { @@ -101,9 +93,18 @@ public class RegistrationRestController { if (StringUtils.isBlank(registrationRest.getEmail())) { throw new UnprocessableEntityException("The email cannot be omitted from the Registration endpoint"); } - if (ePersonService.findByEmail(context, registrationRest.getEmail()) != null) { + EPerson eperson = ePersonService.findByEmail(context, registrationRest.getEmail()); + if (eperson != null) { + if (!AuthorizeUtil.authorizeUpdatePassword(context, eperson.getEmail(), eperson.canLogIn())) { + throw new DSpaceBadRequestException("Password cannot be updated for the given EPerson with email: " + + eperson.getEmail()); + } accountService.sendForgotPasswordInfo(context, registrationRest.getEmail()); } else { + if (!AuthorizeUtil.authorizeNewAccountRegistration(context, request)) { + throw new AccessDeniedException( + "Registration is disabled, you are not authorized to create a new Authorization"); + } accountService.sendRegistrationInfo(context, registrationRest.getEmail()); } context.complete(); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EPersonRegistrationFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EPersonRegistrationFeature.java index d230d2ef97..a03d68fcc9 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EPersonRegistrationFeature.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EPersonRegistrationFeature.java @@ -13,9 +13,8 @@ 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.SiteRest; -import org.dspace.authenticate.service.AuthenticationService; +import org.dspace.app.util.AuthorizeUtil; import org.dspace.core.Context; -import org.dspace.services.ConfigurationService; import org.dspace.services.RequestService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -31,12 +30,6 @@ public class EPersonRegistrationFeature implements AuthorizationFeature { public static final String NAME = "epersonRegistration"; - @Autowired - private ConfigurationService configurationService; - - @Autowired - private AuthenticationService authenticationService; - @Autowired private RequestService requestService; @@ -45,11 +38,11 @@ public class EPersonRegistrationFeature implements AuthorizationFeature { if (!(object instanceof SiteRest)) { return false; } - if (configurationService.getBooleanProperty("user.registration", true)) { - return authenticationService - .allowSetPassword(context, requestService.getCurrentRequest().getHttpServletRequest(), null); + if (!AuthorizeUtil.authorizeNewAccountRegistration(context, + requestService.getCurrentRequest().getHttpServletRequest())) { + return false; } - return false; + return true; } @Override diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java index 43e46a85c9..4226ff6ab4 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java @@ -18,19 +18,17 @@ import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; import org.dspace.app.rest.Parameter; import org.dspace.app.rest.SearchRestMethod; -import org.dspace.app.rest.authorization.AuthorizationFeature; import org.dspace.app.rest.authorization.AuthorizationFeatureService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; import org.dspace.app.rest.exception.UnprocessableEntityException; import org.dspace.app.rest.model.EPersonRest; import org.dspace.app.rest.model.MetadataRest; import org.dspace.app.rest.model.MetadataValueRest; -import org.dspace.app.rest.model.SiteRest; import org.dspace.app.rest.model.patch.Operation; import org.dspace.app.rest.model.patch.Patch; -import org.dspace.app.rest.projection.Projection; +import org.dspace.app.util.AuthorizeUtil; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.service.AuthorizeService; -import org.dspace.content.Site; import org.dspace.content.service.SiteService; import org.dspace.core.Context; import org.dspace.eperson.EPerson; @@ -133,32 +131,30 @@ public class EPersonRestRepository extends DSpaceObjectRestRepository epersonLastName = metadataRest.getMap().get("eperson.lastname"); if (epersonFirstName == null || epersonLastName == null || epersonFirstName.isEmpty() || epersonLastName.isEmpty()) { - throw new AccessDeniedException("The eperson.firstname and eperson.lastname values need to be " + + throw new DSpaceBadRequestException("The eperson.firstname and eperson.lastname values need to be " + "filled in"); } } String password = epersonRest.getPassword(); - if (StringUtils.isBlank(password)) { - throw new AccessDeniedException("the password cannot be left blank"); + if (!accountService.verifyPasswordStructure(password)) { + throw new DSpaceBadRequestException("the password cannot be left blank"); } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/EPersonPasswordReplaceOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/EPersonPasswordReplaceOperation.java index 387721915d..5bac19ba96 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/EPersonPasswordReplaceOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/EPersonPasswordReplaceOperation.java @@ -13,6 +13,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.dspace.app.rest.exception.DSpaceBadRequestException; import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.util.AuthorizeUtil; import org.dspace.authorize.AuthorizeException; import org.dspace.core.Context; import org.dspace.eperson.EPerson; @@ -61,6 +62,10 @@ public class EPersonPasswordReplaceOperation extends PatchOperation { if (StringUtils.isNotBlank(token)) { patchWithToken(context,eperson, token, operation); } + if (!AuthorizeUtil.authorizeUpdatePassword(context, eperson.getEmail(), eperson.canLogIn())) { + throw new DSpaceBadRequestException("Password cannot be updated for the given EPerson with email: " + + eperson.getEmail()); + } ePersonService.setPassword(eperson, (String) operation.getValue()); return object; } else { diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java index 8f48ac4a90..24ecd00b22 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java @@ -1881,8 +1881,7 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"password\":\"somePassword\"," + "\"type\":\"eperson\"}"; - String token = getAuthToken(admin.getEmail(), password); - MvcResult mvcResult = getClient(token).perform(post("/api/eperson/epersons") + MvcResult mvcResult = getClient().perform(post("/api/eperson/epersons") .param("token", newRegisterToken) .content(json) .contentType(MediaType.APPLICATION_JSON)) @@ -1932,8 +1931,7 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"email\":\"" + newRegisterEmail + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; - String token = getAuthToken(admin.getEmail(), password); - MvcResult mvcResult = getClient(token).perform(post("/api/eperson/epersons") + MvcResult mvcResult = getClient().perform(post("/api/eperson/epersons") .param("token", newRegisterToken) .content(json) .contentType(MediaType.APPLICATION_JSON)) @@ -1985,8 +1983,7 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"selfRegistered\":true,\"email\":\"" + newRegisterEmail + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; - String token = getAuthToken(admin.getEmail(), password); - MvcResult mvcResult = getClient(token).perform(post("/api/eperson/epersons") + MvcResult mvcResult = getClient().perform(post("/api/eperson/epersons") .param("token", newRegisterToken) .content(json) .contentType(MediaType.APPLICATION_JSON)) @@ -2048,12 +2045,11 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"email\":\"" + newRegisterEmailTwo + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; - String token = getAuthToken(admin.getEmail(), password); - getClient(token).perform(post("/api/eperson/epersons") + getClient().perform(post("/api/eperson/epersons") .param("token", newRegisterToken) .content(json) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isForbidden()); + .andExpect(status().isBadRequest()); EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmailTwo); assertNull(createdEPerson); @@ -2084,12 +2080,11 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"email\":\"" + newRegisterEmail + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; - String token = getAuthToken(admin.getEmail(), password); - getClient(token).perform(post("/api/eperson/epersons") + getClient().perform(post("/api/eperson/epersons") .param("token", "randomToken") .content(json) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isForbidden()); + .andExpect(status().isBadRequest()); EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail); assertNull(createdEPerson); @@ -2118,12 +2113,11 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"selfRegistered\":false,\"email\":\"" + newRegisterEmail + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; - String token = getAuthToken(admin.getEmail(), password); - getClient(token).perform(post("/api/eperson/epersons") + getClient().perform(post("/api/eperson/epersons") .param("token", newRegisterToken) .content(json) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isForbidden()); + .andExpect(status().isBadRequest()); EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail); assertNull(createdEPerson); @@ -2151,12 +2145,11 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]},\"selfRegistered\":true," + "\"email\":\"" + newRegisterEmail + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; - String token = getAuthToken(admin.getEmail(), password); - getClient(token).perform(post("/api/eperson/epersons") + getClient().perform(post("/api/eperson/epersons") .param("token", newRegisterToken) .content(json) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isForbidden()); + .andExpect(status().isBadRequest()); EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail); assertNull(createdEPerson); @@ -2184,12 +2177,11 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { String json = "{\"metadata\":{\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"selfRegistered\":true," + "\"email\":\"" + newRegisterEmail + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; - String token = getAuthToken(admin.getEmail(), password); - getClient(token).perform(post("/api/eperson/epersons") + getClient().perform(post("/api/eperson/epersons") .param("token", newRegisterToken) .content(json) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isForbidden()); + .andExpect(status().isBadRequest()); EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail); assertNull(createdEPerson); @@ -2218,12 +2210,11 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { "\"eperson.lastname\":[{\"value\":\"Doe\"}]}," + "\"type\":\"eperson\"}"; - String token = getAuthToken(admin.getEmail(), password); - getClient(token).perform(post("/api/eperson/epersons") + getClient().perform(post("/api/eperson/epersons") .param("token", newRegisterToken) .content(json) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isForbidden()); + .andExpect(status().isBadRequest()); EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail); assertNull(createdEPerson); @@ -2253,12 +2244,11 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"selfRegistered\":true,\"password\":\"somePassword\"," + "\"type\":\"eperson\"}"; - String token = getAuthToken(admin.getEmail(), password); - getClient(token).perform(post("/api/eperson/epersons") + getClient().perform(post("/api/eperson/epersons") .param("token", forgotPasswordToken) .content(json) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isForbidden()); + .andExpect(status().isBadRequest()); EPerson createdEPerson = ePersonService.findByEmail(context, newEmail); assertNull(createdEPerson); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java index d3147d4611..f50b4c9d65 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java @@ -78,4 +78,30 @@ public class RegistrationRestControllerIT extends AbstractControllerIntegrationT registrationDataDAO.delete(context, registrationData); } } + + @Test + public void forgotPasswordTest() throws Exception { + context.turnOffAuthorisationSystem(); + configurationService.setProperty("user.registration", false); + context.restoreAuthSystemState(); + + List registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); + assertEquals(0, registrationDataList.size()); + + ObjectMapper mapper = new ObjectMapper(); + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(eperson.getEmail()); + getClient().perform(post("/api/eperson/registrations") + .content(mapper.writeValueAsBytes(registrationRest)) + .contentType(contentType)) + .andExpect(status().isCreated()); + registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); + assertEquals(1, registrationDataList.size()); + assertTrue(StringUtils.equalsIgnoreCase(registrationDataList.get(0).getEmail(), eperson.getEmail())); + Iterator iterator = registrationDataList.iterator(); + while (iterator.hasNext()) { + RegistrationData registrationData = iterator.next(); + registrationDataDAO.delete(context, registrationData); + } + } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EPersonRegistrationFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EPersonRegistrationFeatureIT.java index a9bbdce3db..754804edf4 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EPersonRegistrationFeatureIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EPersonRegistrationFeatureIT.java @@ -62,12 +62,9 @@ public class EPersonRegistrationFeatureIT extends AbstractControllerIntegrationT SiteRest SiteRest = siteConverter.convert(site, Projection.DEFAULT); String siteUri = utils.linkToSingleResource(SiteRest, "self").getHref(); - // access the authorization for the admin user - String adminToken = getAuthToken(admin.getEmail(), password); - getClient(adminToken).perform(get("/api/authz/authorizations/search/objectAndFeature") + getClient().perform(get("/api/authz/authorizations/search/objectAndFeature") .param("uri", siteUri) - .param("eperson", admin.getID().toString()) .param("feature", epersonRegistrationFeature.getName())) .andExpect(status().isOk()); } @@ -82,12 +79,9 @@ public class EPersonRegistrationFeatureIT extends AbstractControllerIntegrationT context.turnOffAuthorisationSystem(); configurationService.setProperty("user.registration", false); context.restoreAuthSystemState(); - // access the authorization for the admin user - String adminToken = getAuthToken(admin.getEmail(), password); - getClient(adminToken).perform(get("/api/authz/authorizations/search/objectAndFeature") + getClient().perform(get("/api/authz/authorizations/search/objectAndFeature") .param("uri", siteUri) - .param("eperson", admin.getID().toString()) .param("feature", epersonRegistrationFeature.getName())) .andExpect(status().isNoContent()); @@ -104,12 +98,9 @@ public class EPersonRegistrationFeatureIT extends AbstractControllerIntegrationT SiteRest SiteRest = siteConverter.convert(site, Projection.DEFAULT); String siteUri = utils.linkToSingleResource(SiteRest, "self").getHref(); - // access the authorization for the admin user - String adminToken = getAuthToken(admin.getEmail(), password); - getClient(adminToken).perform(get("/api/authz/authorizations/search/objectAndFeature") + getClient().perform(get("/api/authz/authorizations/search/objectAndFeature") .param("uri", siteUri) - .param("eperson", admin.getID().toString()) .param("feature", epersonRegistrationFeature.getName())) .andExpect(status().isOk()); @@ -118,9 +109,8 @@ public class EPersonRegistrationFeatureIT extends AbstractControllerIntegrationT configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", SHIB_ONLY); context.restoreAuthSystemState(); - getClient(adminToken).perform(get("/api/authz/authorizations/search/objectAndFeature") + getClient().perform(get("/api/authz/authorizations/search/objectAndFeature") .param("uri", siteUri) - .param("eperson", admin.getID().toString()) .param("feature", epersonRegistrationFeature.getName())) .andExpect(status().isNoContent()); From 8ff77a9e6d289e06e133d6e0dfd694337140f4a0 Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Mon, 11 May 2020 11:51:38 +0200 Subject: [PATCH 034/143] [Task 70808] added javadoc --- .../main/java/org/dspace/eperson/service/AccountService.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dspace-api/src/main/java/org/dspace/eperson/service/AccountService.java b/dspace-api/src/main/java/org/dspace/eperson/service/AccountService.java index 23d46ea00a..45fa6d26b1 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/service/AccountService.java +++ b/dspace-api/src/main/java/org/dspace/eperson/service/AccountService.java @@ -47,5 +47,10 @@ public interface AccountService { public void deleteToken(Context context, String token) throws SQLException; + /** + * This method verifies that a certain String adheres to the password rules for DSpace + * @param password The String to be checked + * @return A boolean indicating whether or not the given String adheres to the password rules + */ public boolean verifyPasswordStructure(String password); } From 6fbc15486d14a0584c64dc51ce7e635d920f8289 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Mon, 11 May 2020 14:52:02 +0200 Subject: [PATCH 035/143] dc.source metadata is not present in the upload section, so we con not apply patch remove request --- .../dspace/app/rest/WorkspaceItemRestRepositoryIT.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java index b2f4de1d0e..d77818cdfb 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java @@ -2422,9 +2422,8 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration is("Description"))) ; - // try to remove the description and the source now + // try to remove the description List removeOpts = new ArrayList(); - removeOpts.add(new RemoveOperation("/sections/upload/files/0/metadata/dc.source/0")); removeOpts.add(new RemoveOperation("/sections/upload/files/0/metadata/dc.description")); patchBody = getPatchContent(removeOpts); @@ -2432,8 +2431,6 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration .content(patchBody) .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) .andExpect(status().isOk()) - // check the removed source - .andExpect(jsonPath("$.sections.upload.files[0].metadata['dc.source']").doesNotExist()) // check the filename still here .andExpect(jsonPath("$.sections.upload.files[0].metadata['dc.title'][0].value", is("newfilename.pdf"))) @@ -2444,7 +2441,8 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration // check that changes persist getClient(authToken).perform(get("/api/submission/workspaceitems/" + witem.getID())) .andExpect(status().isOk()) - .andExpect(jsonPath("$.sections.upload.files[0].metadata['dc.source']").doesNotExist()) + .andExpect(jsonPath("$.sections.upload.files[0].metadata['dc.source'][0].value", + is("/local/path/simple-article.pdf"))) .andExpect(jsonPath("$.sections.upload.files[0].metadata['dc.title'][0].value", is("newfilename.pdf"))) .andExpect(jsonPath("$.sections.upload.files[0].metadata['dc.description']").doesNotExist()) ; From 7b7e7cd14e79c074ae6e739b609070388d28f83e Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Thu, 14 May 2020 10:44:14 +0200 Subject: [PATCH 036/143] added IT for patch request on metadata with qualdrop_value --- .../rest/WorkspaceItemRestRepositoryIT.java | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java index d77818cdfb..bcb0879a5d 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java @@ -1650,15 +1650,20 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration context.restoreAuthSystemState(); // try to add the title - List addTitle = new ArrayList(); + List operations = new ArrayList(); // create a list of values to use in add operation - List> values = new ArrayList>(); + List> titelValues = new ArrayList>(); + List> uriValues = new ArrayList>(); Map value = new HashMap(); + Map value2 = new HashMap(); value.put("value", "New Title"); - values.add(value); - addTitle.add(new AddOperation("/sections/traditionalpageone/dc.title", values)); + value2.put("value", "https://www.dspace.org"); + titelValues.add(value); + uriValues.add(value2); + operations.add(new AddOperation("/sections/traditionalpageone/dc.title", titelValues)); + operations.add(new AddOperation("/sections/traditionalpageone/dc.identifier.uri", uriValues)); - String patchBody = getPatchContent(addTitle); + String patchBody = getPatchContent(operations); getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) .content(patchBody) .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) @@ -1667,7 +1672,10 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration .andExpect(jsonPath("$", // check if the new title if back and the other values untouched Matchers.is(WorkspaceItemMatcher.matchItemWithTitleAndDateIssuedAndSubject(witem, - "New Title", "2017-10-17", "ExtraEntry")))); + "New Title", "2017-10-17", "ExtraEntry")))) + .andExpect(jsonPath("$", Matchers.allOf( + hasJsonPath("$.sections.traditionalpageone['dc.identifier.uri'][0].value", + is("https://www.dspace.org"))))); // verify that the patch changes have been persisted getClient(authToken).perform(get("/api/submission/workspaceitems/" + witem.getID())) @@ -1676,6 +1684,9 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration .andExpect(jsonPath("$", Matchers.is(WorkspaceItemMatcher.matchItemWithTitleAndDateIssuedAndSubject(witem, "New Title", "2017-10-17", "ExtraEntry")))) + .andExpect(jsonPath("$", Matchers.allOf( + hasJsonPath("$.sections.traditionalpageone['dc.identifier.uri'][0].value", + is("https://www.dspace.org"))))) ; } From 3a1982656a0dc418e2903fc44eab7b2034e390e4 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Thu, 14 May 2020 10:48:15 +0200 Subject: [PATCH 037/143] this method should also check metadata with qualdrop_value --- .../java/org/dspace/app/util/DCInputSet.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/util/DCInputSet.java b/dspace-api/src/main/java/org/dspace/app/util/DCInputSet.java index faa3fb7190..e6a88895fd 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/DCInputSet.java +++ b/dspace-api/src/main/java/org/dspace/app/util/DCInputSet.java @@ -10,6 +10,7 @@ package org.dspace.app.util; import java.util.List; import java.util.Map; +import org.dspace.core.Utils; /** * Class representing all DC inputs required for a submission, organized into pages * @@ -107,9 +108,20 @@ public class DCInputSet { for (int i = 0; i < inputs.length; i++) { for (int j = 0; j < inputs[i].length; j++) { DCInput field = inputs[i][j]; - String fullName = field.getFieldName(); - if (fullName.equals(fieldName)) { - return true; + if (field.getInputType().equals("qualdrop_value")) { + List pairs = field.getPairs(); + for (int k = 0; k < pairs.size(); k += 2) { + String qualifier = pairs.get(k + 1); + String fullName = Utils.standardize(field.getSchema(), field.getElement(), qualifier, "."); + if (fullName.equals(fieldName)) { + return true; + } + } + } else { + String fullName = field.getFieldName(); + if (fullName.equals(fieldName)) { + return true; + } } } } From d3cc506b260470125c4690e73bd3f1fbca86f139 Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Tue, 19 May 2020 13:42:24 +0200 Subject: [PATCH 038/143] [Task 70937] removed the canLogin parameter from the AuthorizeUtil#authorizeUpdatePassword method --- .../src/main/java/org/dspace/app/util/AuthorizeUtil.java | 8 ++++---- .../org/dspace/app/rest/RegistrationRestController.java | 2 +- .../patch/operation/EPersonPasswordReplaceOperation.java | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/util/AuthorizeUtil.java b/dspace-api/src/main/java/org/dspace/app/util/AuthorizeUtil.java index d5dbf5a514..92f4fc9311 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/AuthorizeUtil.java +++ b/dspace-api/src/main/java/org/dspace/app/util/AuthorizeUtil.java @@ -28,6 +28,7 @@ import org.dspace.content.service.CollectionService; import org.dspace.content.service.ItemService; import org.dspace.core.Constants; import org.dspace.core.Context; +import org.dspace.eperson.EPerson; import org.dspace.eperson.factory.EPersonServiceFactory; import org.dspace.services.factory.DSpaceServicesFactory; @@ -555,13 +556,12 @@ public class AuthorizeUtil { * with the given email and canLogin property * @param context The relevant DSpace context * @param email The email to be checked - * @param canLogin The boolean canLogin property * @return A boolean indicating if the password can be updated or not */ - public static boolean authorizeUpdatePassword(Context context, String email, boolean canLogin) { + public static boolean authorizeUpdatePassword(Context context, String email) { try { - if (EPersonServiceFactory.getInstance().getEPersonService().findByEmail(context, email) != null - && canLogin) { + EPerson eperson = EPersonServiceFactory.getInstance().getEPersonService().findByEmail(context, email); + if (eperson != null && eperson.canLogIn()) { return true; } } catch (SQLException e) { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java index 741efa995b..34c5a8055c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java @@ -95,7 +95,7 @@ public class RegistrationRestController { } EPerson eperson = ePersonService.findByEmail(context, registrationRest.getEmail()); if (eperson != null) { - if (!AuthorizeUtil.authorizeUpdatePassword(context, eperson.getEmail(), eperson.canLogIn())) { + if (!AuthorizeUtil.authorizeUpdatePassword(context, eperson.getEmail())) { throw new DSpaceBadRequestException("Password cannot be updated for the given EPerson with email: " + eperson.getEmail()); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/EPersonPasswordReplaceOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/EPersonPasswordReplaceOperation.java index 5bac19ba96..ae02200d98 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/EPersonPasswordReplaceOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/EPersonPasswordReplaceOperation.java @@ -62,7 +62,7 @@ public class EPersonPasswordReplaceOperation extends PatchOperation { if (StringUtils.isNotBlank(token)) { patchWithToken(context,eperson, token, operation); } - if (!AuthorizeUtil.authorizeUpdatePassword(context, eperson.getEmail(), eperson.canLogIn())) { + if (!AuthorizeUtil.authorizeUpdatePassword(context, eperson.getEmail())) { throw new DSpaceBadRequestException("Password cannot be updated for the given EPerson with email: " + eperson.getEmail()); } From 2a0971d8fbb4f27e07efe73476243c3e3c03133c Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Tue, 19 May 2020 14:27:40 +0200 Subject: [PATCH 039/143] [Task 70937] fixes after merge --- .../src/main/java/org/dspace/app/util/AuthorizeUtil.java | 3 +-- .../java/org/dspace/app/rest/RegistrationRestController.java | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/util/AuthorizeUtil.java b/dspace-api/src/main/java/org/dspace/app/util/AuthorizeUtil.java index d2089d6170..521eff6fc1 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/AuthorizeUtil.java +++ b/dspace-api/src/main/java/org/dspace/app/util/AuthorizeUtil.java @@ -30,11 +30,10 @@ import org.dspace.content.service.ItemService; import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.eperson.EPerson; -import org.dspace.eperson.factory.EPersonServiceFactory; -import org.dspace.services.factory.DSpaceServicesFactory; import org.dspace.eperson.Group; import org.dspace.eperson.factory.EPersonServiceFactory; import org.dspace.eperson.service.GroupService; +import org.dspace.services.factory.DSpaceServicesFactory; import org.dspace.xmlworkflow.factory.XmlWorkflowServiceFactory; import org.dspace.xmlworkflow.storedcomponents.CollectionRole; import org.dspace.xmlworkflow.storedcomponents.service.CollectionRoleService; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java index 34c5a8055c..c79c2a0969 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java @@ -31,7 +31,7 @@ import org.dspace.eperson.service.AccountService; import org.dspace.eperson.service.EPersonService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.rest.webmvc.ControllerUtils; -import org.springframework.hateoas.ResourceSupport; +import org.springframework.hateoas.RepresentationModel; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; @@ -78,7 +78,7 @@ public class RegistrationRestController { * @throws AuthorizeException If something goes wrong */ @RequestMapping(method = RequestMethod.POST) - public ResponseEntity register(HttpServletRequest request, HttpServletResponse response) + public ResponseEntity> register(HttpServletRequest request, HttpServletResponse response) throws SQLException, IOException, MessagingException, AuthorizeException { Context context = ContextUtil.obtainContext(request); From f33bfac1958d9195674244d038f13af91fd37a71 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Wed, 20 May 2020 12:54:08 +0200 Subject: [PATCH 040/143] added ITs for upload patch request --- .../rest/WorkspaceItemRestRepositoryIT.java | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java index bcb0879a5d..ac6a617773 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java @@ -3506,4 +3506,130 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) .andExpect(status().isUnprocessableEntity()); } + + @Test + public void patchUploadMissingFieldTest() throws Exception { + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1) + .withName("Collection 1") + .build(); + + InputStream pdf = getClass().getResourceAsStream("simple-article.pdf"); + + WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, col1) + .withTitle("Test WorkspaceItem") + .withIssueDate("2017-10-17") + .withFulltext("simple-article.pdf", "/local/path/simple-article.pdf", pdf) + .build(); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(eperson.getEmail(), password); + + List addOpts = new ArrayList(); + Map value = new HashMap(); + value.put("value", "test text"); + + addOpts.add(new AddOperation("/sections/upload/files/0/metadata", value)); + + String patchBody = getPatchContent(addOpts); + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void patchUploadNotExistingPropertyTest() throws Exception { + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1) + .withName("Collection 1") + .build(); + + InputStream pdf = getClass().getResourceAsStream("simple-article.pdf"); + + WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, col1) + .withTitle("Test WorkspaceItem") + .withIssueDate("2017-10-17") + .withFulltext("simple-article.pdf", "/local/path/simple-article.pdf", pdf) + .build(); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(eperson.getEmail(), password); + + List addOpts = new ArrayList(); + Map value = new HashMap(); + value.put("value", "test text"); + + addOpts.add(new AddOperation("/sections/upload/files/0/not-existing-property/dc.title", value)); + + String patchBody = getPatchContent(addOpts); + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void patchUploadWithWrongPathTest() throws Exception { + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1) + .withName("Collection 1") + .build(); + + InputStream pdf = getClass().getResourceAsStream("simple-article.pdf"); + + WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, col1) + .withTitle("Test WorkspaceItem") + .withIssueDate("2017-10-17") + .withFulltext("simple-article.pdf", "/local/path/simple-article.pdf", pdf) + .build(); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(eperson.getEmail(), password); + + List addOpts = new ArrayList(); + Map value = new HashMap(); + value.put("value", "test text"); + + addOpts.add(new AddOperation("/sections/upload/files/0", value)); + + String patchBody = getPatchContent(addOpts); + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()); + + addOpts.add(new AddOperation("/sections/upload/files", value)); + patchBody = getPatchContent(addOpts); + + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()); + } + } From 212985bed68611f7818cba689755af69fbc4c8a7 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Wed, 20 May 2020 13:02:08 +0200 Subject: [PATCH 041/143] refactoring doPatchProcessing of UploadStep --- ...tstreamMetadataValueAddPatchOperation.java | 8 ++- ...streamMetadataValueMovePatchOperation.java | 8 ++- ...reamMetadataValueRemovePatchOperation.java | 8 ++- ...eamMetadataValueReplacePatchOperation.java | 8 ++- .../app/rest/submit/step/UploadStep.java | 39 +++------------ .../BitstreamMetadataValuePathUtils.java | 50 +++++++++++++++++++ dspace/config/spring/api/core-services.xml | 1 + 7 files changed, 86 insertions(+), 36 deletions(-) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamMetadataValuePathUtils.java diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueAddPatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueAddPatchOperation.java index 466207ca57..c7531e9810 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueAddPatchOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueAddPatchOperation.java @@ -11,6 +11,7 @@ import java.util.List; import org.dspace.app.rest.model.MetadataValueRest; import org.dspace.app.rest.model.patch.LateObjectEvaluator; +import org.dspace.app.rest.utils.BitstreamMetadataValuePathUtils; import org.dspace.content.Bitstream; import org.dspace.content.Bundle; import org.dspace.content.InProgressSubmission; @@ -39,12 +40,17 @@ public class BitstreamMetadataValueAddPatchOperation extends MetadataValueAddPat @Autowired ItemService itemService; + @Autowired + BitstreamMetadataValuePathUtils bitstreamMetadataValuePathUtils; + @Override void add(Context context, Request currentRequest, InProgressSubmission source, String path, Object value) throws Exception { //"path": "/sections/upload/files/0/metadata/dc.title/2" //"abspath": "/files/0/metadata/dc.title/2" - String[] split = getAbsolutePath(path).split("/"); + String absolutePath = getAbsolutePath(path); + String[] split = absolutePath.split("/"); + bitstreamMetadataValuePathUtils.validate(absolutePath); Item item = source.getItem(); List bundle = itemService.getBundles(item, Constants.CONTENT_BUNDLE_NAME); ; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueMovePatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueMovePatchOperation.java index d5ade7b9c2..d03d47c91e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueMovePatchOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueMovePatchOperation.java @@ -9,6 +9,7 @@ package org.dspace.app.rest.submit.factory.impl; import java.util.List; +import org.dspace.app.rest.utils.BitstreamMetadataValuePathUtils; import org.dspace.content.Bitstream; import org.dspace.content.Bundle; import org.dspace.content.InProgressSubmission; @@ -35,12 +36,17 @@ public class BitstreamMetadataValueMovePatchOperation extends MetadataValueMoveP @Autowired ItemService itemService; + @Autowired + BitstreamMetadataValuePathUtils bitstreamMetadataValuePathUtils; + @Override void move(Context context, Request currentRequest, InProgressSubmission source, String path, String from) throws Exception { //"path": "/sections/upload/files/0/metadata/dc.title/2" //"abspath": "/files/0/metadata/dc.title/2" - String[] splitTo = getAbsolutePath(path).split("/"); + String absolutePath = getAbsolutePath(path); + String[] splitTo = absolutePath.split("/"); + bitstreamMetadataValuePathUtils.validate(absolutePath); Item item = source.getItem(); List bundle = itemService.getBundles(item, Constants.CONTENT_BUNDLE_NAME); for (Bundle bb : bundle) { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueRemovePatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueRemovePatchOperation.java index 1f811aeaea..0341192b21 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueRemovePatchOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueRemovePatchOperation.java @@ -9,6 +9,7 @@ package org.dspace.app.rest.submit.factory.impl; import java.util.List; +import org.dspace.app.rest.utils.BitstreamMetadataValuePathUtils; import org.dspace.content.Bitstream; import org.dspace.content.Bundle; import org.dspace.content.InProgressSubmission; @@ -35,12 +36,17 @@ public class BitstreamMetadataValueRemovePatchOperation extends MetadataValueRem @Autowired ItemService itemService; + @Autowired + BitstreamMetadataValuePathUtils bitstreamMetadataValuePathUtils; + @Override void remove(Context context, Request currentRequest, InProgressSubmission source, String path, Object value) throws Exception { //"path": "/sections/upload/files/0/metadata/dc.title/2" //"abspath": "/files/0/metadata/dc.title/2" - String[] split = getAbsolutePath(path).split("/"); + String absolutePath = getAbsolutePath(path); + String[] split = absolutePath.split("/"); + bitstreamMetadataValuePathUtils.validate(absolutePath); Item item = source.getItem(); List bundle = itemService.getBundles(item, Constants.CONTENT_BUNDLE_NAME); ; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueReplacePatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueReplacePatchOperation.java index de22891822..fd5b395157 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueReplacePatchOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueReplacePatchOperation.java @@ -12,6 +12,7 @@ import java.util.List; import org.dspace.app.rest.model.MetadataValueRest; import org.dspace.app.rest.model.patch.LateObjectEvaluator; +import org.dspace.app.rest.utils.BitstreamMetadataValuePathUtils; import org.dspace.content.Bitstream; import org.dspace.content.Bundle; import org.dspace.content.InProgressSubmission; @@ -38,12 +39,17 @@ public class BitstreamMetadataValueReplacePatchOperation extends MetadataValueRe @Autowired ItemService itemService; + @Autowired + BitstreamMetadataValuePathUtils bitstreamMetadataValuePathUtils; + @Override void replace(Context context, Request currentRequest, InProgressSubmission source, String path, Object value) throws Exception { //"path": "/sections/upload/files/0/metadata/dc.title/2" //"abspath": "/files/0/metadata/dc.title/2" - String[] split = getAbsolutePath(path).split("/"); + String absolutePath = getAbsolutePath(path); + String[] split = absolutePath.split("/"); + bitstreamMetadataValuePathUtils.validate(absolutePath); Item item = source.getItem(); List bundle = itemService.getBundles(item, Constants.CONTENT_BUNDLE_NAME); for (Bundle bb : bundle) { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/UploadStep.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/UploadStep.java index f32d686efe..11fd36935c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/UploadStep.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/UploadStep.java @@ -24,9 +24,6 @@ import org.dspace.app.rest.submit.UploadableStep; import org.dspace.app.rest.submit.factory.PatchOperationFactory; import org.dspace.app.rest.submit.factory.impl.PatchOperation; import org.dspace.app.rest.utils.Utils; -import org.dspace.app.util.DCInputSet; -import org.dspace.app.util.DCInputsReader; -import org.dspace.app.util.DCInputsReaderException; import org.dspace.app.util.SubmissionStepConfig; import org.dspace.content.Bitstream; import org.dspace.content.BitstreamFormat; @@ -51,11 +48,6 @@ public class UploadStep extends org.dspace.submit.step.UploadStep public static final String UPLOAD_STEP_METADATA_SECTION = "bitstream-metadata"; - private DCInputsReader inputReader; - - public UploadStep() throws DCInputsReaderException { - inputReader = new DCInputsReader(); - } @Override public DataUpload getData(SubmissionService submissionService, InProgressSubmission obj, SubmissionStepConfig config) throws Exception { @@ -75,7 +67,7 @@ public class UploadStep extends org.dspace.submit.step.UploadStep public void doPatchProcessing(Context context, Request currentRequest, InProgressSubmission source, Operation op, SubmissionStepConfig stepConf) throws Exception { - String instance = ""; + String instance = null; if ("remove".equals(op.getOp())) { if (op.getPath().contains(UPLOAD_STEP_METADATA_PATH)) { instance = UPLOAD_STEP_METADATA_OPERATION_ENTRY; @@ -93,33 +85,16 @@ public class UploadStep extends org.dspace.submit.step.UploadStep } else { if (op.getPath().contains(UPLOAD_STEP_ACCESSCONDITIONS_OPERATION_ENTRY)) { instance = UPLOAD_STEP_ACCESSCONDITIONS_OPERATION_ENTRY; - } else { + } else if (op.getPath().contains(UPLOAD_STEP_METADATA_PATH)) { instance = UPLOAD_STEP_METADATA_OPERATION_ENTRY; } } + if (instance == null) { + throw new UnprocessableEntityException("The path " + op.getPath() + " is not supported by the operation " + + op.getOp()); + } PatchOperation patchOperation = new PatchOperationFactory().instanceOf(instance, op.getOp()); - if (instance.equals(AbstractRestProcessingStep.UPLOAD_STEP_METADATA_OPERATION_ENTRY)) { - DCInputSet inputConfig = inputReader.getInputsByFormName(UploadStep.UPLOAD_STEP_METADATA_SECTION); - String[] split = patchOperation.getAbsolutePath(op.getPath()).split("/"); - String metadata = findMetadata(split); - if (inputConfig.isFieldPresent(metadata)) { - patchOperation.perform(context, currentRequest, source, op); - } else { - throw new UnprocessableEntityException("The attribute " + metadata + " does not present in section " - + UploadStep.UPLOAD_STEP_METADATA_SECTION); - } - } else { - patchOperation.perform(context, currentRequest, source, op); - } - } - - private String findMetadata(String[] metadata) { - for (String s : metadata) { - if (s.contains("dc.")) { - return s; - } - } - return null; + patchOperation.perform(context, currentRequest, source, op); } @Override diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamMetadataValuePathUtils.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamMetadataValuePathUtils.java new file mode 100644 index 0000000000..d856538328 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamMetadataValuePathUtils.java @@ -0,0 +1,50 @@ +/** + * 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.utils; + +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.submit.step.UploadStep; +import org.dspace.app.util.DCInputSet; +import org.dspace.app.util.DCInputsReader; +import org.dspace.app.util.DCInputsReaderException; + +/** + * Utils class offering methods to validate patch operations for bitstream metadata in the submission + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.it) + */ +public class BitstreamMetadataValuePathUtils { + + private DCInputsReader inputReader; + + BitstreamMetadataValuePathUtils() throws DCInputsReaderException { + inputReader = new DCInputsReader(); + } + + /** + * Method to verify that the path included in the patch operation is supported + * by the submission configuration of the upload section + * + * @param absolutePath the path in the json patch operation + * @throws DCInputsReaderException if an error occurs reading the + * submission configuration + * @throws UnprocessableEntityException if the path is invalid + */ + public void validate(String absolutePath) throws DCInputsReaderException { + String[] split = absolutePath.split("/"); + DCInputSet inputConfig = inputReader.getInputsByFormName(UploadStep.UPLOAD_STEP_METADATA_SECTION); + if (split.length >= 4) { + if (!inputConfig.isFieldPresent(split[3])) { + throw new UnprocessableEntityException("The field " + split[3] + " is not present in section " + + UploadStep.UPLOAD_STEP_METADATA_SECTION); + } + } else { + throw new UnprocessableEntityException("The path " + split.toString() + " cannot be patched "); + } + } +} diff --git a/dspace/config/spring/api/core-services.xml b/dspace/config/spring/api/core-services.xml index 316f81fd18..455d57c521 100644 --- a/dspace/config/spring/api/core-services.xml +++ b/dspace/config/spring/api/core-services.xml @@ -62,6 +62,7 @@ + From bfa83b75993edd7d428c6e5d25aa49586b754394 Mon Sep 17 00:00:00 2001 From: Kevin Van de Velde Date: Wed, 20 May 2020 13:09:33 +0200 Subject: [PATCH 042/143] Disabling the CC License step in the default configuration --- .../dspaceFolder/config/item-submission.xml | 282 ++++++++++++++++++ dspace/config/item-submission.xml | 8 +- 2 files changed, 287 insertions(+), 3 deletions(-) create mode 100644 dspace-server-webapp/src/test/data/dspaceFolder/config/item-submission.xml diff --git a/dspace-server-webapp/src/test/data/dspaceFolder/config/item-submission.xml b/dspace-server-webapp/src/test/data/dspaceFolder/config/item-submission.xml new file mode 100644 index 0000000000..481b508176 --- /dev/null +++ b/dspace-server-webapp/src/test/data/dspaceFolder/config/item-submission.xml @@ -0,0 +1,282 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.dspace.app.rest.submit.step.CollectionStep + collection + submission + + + submit.progressbar.describe.stepone + org.dspace.app.rest.submit.step.DescribeStep + submission-form + + + submit.progressbar.describe.steptwo + org.dspace.app.rest.submit.step.DescribeStep + submission-form + + + + submit.progressbar.describe.stepone + org.dspace.app.rest.submit.step.DescribeStep + submission-form + + + submit.progressbar.describe.stepone + org.dspace.app.rest.submit.step.DescribeStep + submission-form + + + submit.progressbar.describe.stepone + org.dspace.app.rest.submit.step.DescribeStep + submission-form + + + submit.progressbar.describe.stepone + org.dspace.app.rest.submit.step.DescribeStep + submission-form + + + submit.progressbar.describe.stepone + org.dspace.app.rest.submit.step.DescribeStep + submission-form + + + submit.progressbar.describe.stepone + org.dspace.app.rest.submit.step.DescribeStep + submission-form + + + + submit.progressbar.upload + org.dspace.app.rest.submit.step.UploadStep + upload + + + submit.progressbar.license + org.dspace.app.rest.submit.step.LicenseStep + license + submission + + + + + + + + submit.progressbar.CClicense + org.dspace.app.rest.submit.step.CCLicenseStep + cclicense + + + + + + + + + + + + + submit.progressbar.describe.stepone + org.dspace.app.rest.submit.step.DescribeStep + submission-form + + + submit.progressbar.describe.stepone + org.dspace.app.rest.submit.step.DescribeStep + submission-form + + + submit.progressbar.describe.stepone + org.dspace.app.rest.submit.step.DescribeStep + submission-form + + + submit.progressbar.describe.stepone + org.dspace.app.rest.submit.step.DescribeStep + submission-form + + + submit.progressbar.describe.stepone + org.dspace.app.rest.submit.step.DescribeStep + submission-form + + + + + Sample + org.dspace.submit.step.SampleStep + sample + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dspace/config/item-submission.xml b/dspace/config/item-submission.xml index 481b508176..25162341e4 100644 --- a/dspace/config/item-submission.xml +++ b/dspace/config/item-submission.xml @@ -115,9 +115,9 @@ - submit.progressbar.CClicense + - + + + From 98f2447cbb938e08ae3b805443747b8ef10555cb Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Wed, 20 May 2020 13:13:36 +0200 Subject: [PATCH 043/143] Implement community feedbacks --- dspace-api/src/main/java/org/dspace/app/util/DCInputSet.java | 1 + .../main/java/org/dspace/app/rest/submit/step/LicenseStep.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/app/util/DCInputSet.java b/dspace-api/src/main/java/org/dspace/app/util/DCInputSet.java index e6a88895fd..0f2333004c 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/DCInputSet.java +++ b/dspace-api/src/main/java/org/dspace/app/util/DCInputSet.java @@ -108,6 +108,7 @@ public class DCInputSet { for (int i = 0; i < inputs.length; i++) { for (int j = 0; j < inputs[i].length; j++) { DCInput field = inputs[i][j]; + // If this is a "qualdrop_value" field, then the full field name is the field + dropdown qualifier if (field.getInputType().equals("qualdrop_value")) { List pairs = field.getPairs(); for (int k = 0; k < pairs.size(); k += 2) { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/LicenseStep.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/LicenseStep.java index a57951d09a..fd4ccc6420 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/LicenseStep.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/LicenseStep.java @@ -61,7 +61,7 @@ public class LicenseStep extends org.dspace.submit.step.LicenseStep implements A patchOperation.perform(context, currentRequest, source, op); } else { - throw new UnprocessableEntityException("This path : " + op.getPath() + " can not to be replaced"); + throw new UnprocessableEntityException("The path " + op.getPath() + " cannot be patched"); } } } From ab0aec46984d15b466a19cc582f9a1468a79817e Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Wed, 20 May 2020 15:30:53 +0200 Subject: [PATCH 044/143] move bean to the right file --- .../src/main/resources/spring/spring-dspace-core-services.xml | 1 + dspace/config/spring/api/core-services.xml | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/resources/spring/spring-dspace-core-services.xml b/dspace-server-webapp/src/main/resources/spring/spring-dspace-core-services.xml index 61459f11d6..0358049b18 100644 --- a/dspace-server-webapp/src/main/resources/spring/spring-dspace-core-services.xml +++ b/dspace-server-webapp/src/main/resources/spring/spring-dspace-core-services.xml @@ -108,4 +108,5 @@ + diff --git a/dspace/config/spring/api/core-services.xml b/dspace/config/spring/api/core-services.xml index 455d57c521..316f81fd18 100644 --- a/dspace/config/spring/api/core-services.xml +++ b/dspace/config/spring/api/core-services.xml @@ -62,7 +62,6 @@ - From 9404e24c4e8d8f4774e7eb0b667deee47351bb3b Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Wed, 20 May 2020 15:51:12 +0200 Subject: [PATCH 045/143] fix: wrong calling toString --- .../dspace/app/rest/utils/BitstreamMetadataValuePathUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamMetadataValuePathUtils.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamMetadataValuePathUtils.java index d856538328..4ff42d70cd 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamMetadataValuePathUtils.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamMetadataValuePathUtils.java @@ -44,7 +44,7 @@ public class BitstreamMetadataValuePathUtils { + UploadStep.UPLOAD_STEP_METADATA_SECTION); } } else { - throw new UnprocessableEntityException("The path " + split.toString() + " cannot be patched "); + throw new UnprocessableEntityException("The path " + absolutePath + " cannot be patched "); } } } From ccfbbfce9957cd15a2cdde5a281ee49b908026c0 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Wed, 20 May 2020 15:55:13 +0200 Subject: [PATCH 046/143] added IT patchDeleteSection --- .../rest/WorkspaceItemRestRepositoryIT.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java index ac6a617773..e503488912 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java @@ -3632,4 +3632,44 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration .andExpect(status().isUnprocessableEntity()); } + @Test + public void patchDeleteSectionTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1) + .withName("Collection 1") + .build(); + WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, col1) + .withTitle("Test WorkspaceItem") + .withIssueDate("2020-01-21") + .withSubject("Subject 1") + .withSubject("Subject 2") + .build(); + + context.restoreAuthSystemState(); + + List operations = new ArrayList(); + operations.add(new RemoveOperation("/sections/traditionalpagetwo")); + String patchBody = getPatchContent(operations); + + String authToken = getAuthToken(eperson.getEmail(), password); + + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections.traditionalpagetwo['dc.subject']").doesNotExist()) + .andExpect(jsonPath("$.sections.traditionalpagetwo['dc.description.abstract']").doesNotExist()); + + // verify that the patch changes have been persisted + getClient(authToken).perform(get("/api/submission/workspaceitems/" + witem.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections.traditionalpagetwo['dc.subject']").doesNotExist()) + .andExpect(jsonPath("$.sections.traditionalpagetwo['dc.description.abstract']").doesNotExist()); + } } From 76ff785b076cbb10e38892c7a60f885520534e9d Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Wed, 20 May 2020 16:02:32 +0200 Subject: [PATCH 047/143] added implementation for delete section --- .../app/rest/submit/step/DescribeStep.java | 53 ++++++++++++++++--- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/DescribeStep.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/DescribeStep.java index b0134973a9..26e904f044 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/DescribeStep.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/DescribeStep.java @@ -10,10 +10,12 @@ package org.dspace.app.rest.submit.step; import java.util.ArrayList; import java.util.List; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.dspace.app.rest.exception.UnprocessableEntityException; import org.dspace.app.rest.model.MetadataValueRest; import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.model.patch.RemoveOperation; import org.dspace.app.rest.model.step.DataDescribe; import org.dspace.app.rest.submit.AbstractRestProcessingStep; import org.dspace.app.rest.submit.SubmissionService; @@ -114,16 +116,53 @@ public class DescribeStep extends org.dspace.submit.step.DescribeStep implements public void doPatchProcessing(Context context, Request currentRequest, InProgressSubmission source, Operation op, SubmissionStepConfig stepConf) throws Exception { - PatchOperation patchOperation = new PatchOperationFactory() - .instanceOf(DESCRIBE_STEP_METADATA_OPERATION_ENTRY, op.getOp()); + String[] pathParts = op.getPath().substring(1).split("/"); DCInputSet inputConfig = inputReader.getInputsByFormName(stepConf.getId()); - String[] split = patchOperation.getAbsolutePath(op.getPath()).split("/"); - if (inputConfig.isFieldPresent(split[0])) { - patchOperation.perform(context, currentRequest, source, op); + if ("remove".equals(op.getOp()) && pathParts.length < 3) { + // manage delete all step fields + String[] path = op.getPath().substring(1).split("/", 3); + String configId = path[1]; + List fieldsName = getInputFieldsName(inputConfig, configId); + for (String fieldName : fieldsName) { + String fieldPath = op.getPath() + "/" + fieldName; + Operation fieldRemoveOp = new RemoveOperation(fieldPath); + PatchOperation patchOperation = new PatchOperationFactory() + .instanceOf(DESCRIBE_STEP_METADATA_OPERATION_ENTRY, fieldRemoveOp.getOp()); + patchOperation.perform(context, currentRequest, source, fieldRemoveOp); + } } else { - throw new UnprocessableEntityException("The attribute " + split[0] + " does not present in section " - + inputConfig.getFormName()); + PatchOperation patchOperation = new PatchOperationFactory() + .instanceOf(DESCRIBE_STEP_METADATA_OPERATION_ENTRY, op.getOp()); + String[] split = patchOperation.getAbsolutePath(op.getPath()).split("/"); + if (inputConfig.isFieldPresent(split[0])) { + patchOperation.perform(context, currentRequest, source, op); + } else { + throw new UnprocessableEntityException("The field " + split[0] + " is not present in section " + + inputConfig.getFormName()); + } } } + private List getInputFieldsName(DCInputSet inputConfig, String configId) throws DCInputsReaderException { + List fieldsName = new ArrayList(); + for (DCInput[] row : inputConfig.getFields()) { + for (DCInput input : row) { + if (input.isQualdropValue()) { + for (Object qualifier : input.getPairs()) { + fieldsName.add(input.getFieldName() + "." + (String) qualifier); + } + } else if (StringUtils.equalsIgnoreCase(input.getInputType(), "group") || + StringUtils.equalsIgnoreCase(input.getInputType(), "inline-group")) { + log.info("Called child form:" + configId + "-" + + Utils.standardize(input.getSchema(), input.getElement(), input.getQualifier(), "-")); + DCInputSet inputConfigChild = inputReader.getInputsByFormName(configId + "-" + Utils + .standardize(input.getSchema(), input.getElement(), input.getQualifier(), "-")); + fieldsName.addAll(getInputFieldsName(inputConfigChild, configId)); + } else { + fieldsName.add(input.getFieldName()); + } + } + } + return fieldsName; + } } From ffce2e4299595055ba4d0d5e8b0d9e7719c02de8 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Wed, 27 May 2020 13:25:56 +0200 Subject: [PATCH 048/143] 70815: Angular feedback - add self link --- .../SubmissionCCLicenseSearchController.java | 17 +++-- .../SubmissionCCLicenseUrlConverter.java | 41 ++++++++++++ ...ionCCLicenseUrlResourceHalLinkFactory.java | 64 +++++++++++++++++++ .../rest/model/SubmissionCCLicenseRest.java | 2 +- .../model/SubmissionCCLicenseUrlRest.java | 52 +++++++++++++++ .../SubmissionCCLicenseUrlResource.java | 23 +++++++ ...SubmissionCCLicenseSearchControllerIT.java | 15 +++-- 7 files changed, 204 insertions(+), 10 deletions(-) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseUrlConverter.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/link/process/SubmissionCCLicenseUrlResourceHalLinkFactory.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseUrlRest.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SubmissionCCLicenseUrlResource.java diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseSearchController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseSearchController.java index 26e52c8933..f47376757c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseSearchController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseSearchController.java @@ -12,9 +12,11 @@ import java.util.Map; import javax.servlet.ServletRequest; import org.apache.commons.lang3.StringUtils; +import org.dspace.app.rest.converter.ConverterService; import org.dspace.app.rest.exception.DSpaceBadRequestException; -import org.dspace.app.rest.model.PlainTextValueRest; import org.dspace.app.rest.model.SubmissionCCLicenseRest; +import org.dspace.app.rest.model.SubmissionCCLicenseUrlRest; +import org.dspace.app.rest.model.hateoas.SubmissionCCLicenseUrlResource; import org.dspace.app.rest.utils.Utils; import org.dspace.license.service.CreativeCommonsService; import org.dspace.services.RequestService; @@ -39,16 +41,19 @@ public class SubmissionCCLicenseSearchController { @Autowired protected CreativeCommonsService creativeCommonsService; + @Autowired + protected ConverterService converter; + protected RequestService requestService = new DSpace().getRequestService(); /** * Retrieves the CC License URI based on the license ID and answers in the field questions, provided as parameters * to this request * - * @return the CC License URI as a string + * @return the CC License URI as a SubmissionCCLicenseUrlResource */ @RequestMapping(method = RequestMethod.GET) - public PlainTextValueRest findByRightsByQuestions() { + public SubmissionCCLicenseUrlResource findByRightsByQuestions() { ServletRequest servletRequest = requestService.getCurrentRequest() .getServletRequest(); Map requestParameterMap = servletRequest @@ -85,7 +90,9 @@ public class SubmissionCCLicenseSearchController { if (StringUtils.isBlank(licenseUri)) { throw new ResourceNotFoundException("No CC License URI could be found for ID: " + licenseId); } - PlainTextValueRest plainTextValueRest = new PlainTextValueRest(licenseUri); - return plainTextValueRest; + + SubmissionCCLicenseUrlRest submissionCCLicenseUrlRest = converter.toRest(licenseUri, utils.obtainProjection()); + return converter.toResource(submissionCCLicenseUrlRest); + } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseUrlConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseUrlConverter.java new file mode 100644 index 0000000000..5411ac306c --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseUrlConverter.java @@ -0,0 +1,41 @@ +/** + * 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.rest.model.SubmissionCCLicenseUrlRest; +import org.dspace.app.rest.projection.Projection; +import org.springframework.stereotype.Component; + + +/** + * This converter is responsible for transforming a Submission CC License Url String to the REST + * representation SubmissionCCLicenseUrlRest and vice versa + */ +@Component +public class SubmissionCCLicenseUrlConverter implements DSpaceConverter { + + /** + * Convert a Submission CC License Url String to its REST representation + * @param modelObject - the CC License Url String to convert + * @param projection - the projection + * @return the corresponding SubmissionCCLicenseUrlRest object + */ + @Override + public SubmissionCCLicenseUrlRest convert(final String modelObject, final Projection projection) { + SubmissionCCLicenseUrlRest submissionCCLicenseUrlRest = new SubmissionCCLicenseUrlRest(); + submissionCCLicenseUrlRest.setUrl(modelObject); + + return submissionCCLicenseUrlRest; + } + + @Override + public Class getModelClass() { + return String.class; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/process/SubmissionCCLicenseUrlResourceHalLinkFactory.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/process/SubmissionCCLicenseUrlResourceHalLinkFactory.java new file mode 100644 index 0000000000..39911faad0 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/process/SubmissionCCLicenseUrlResourceHalLinkFactory.java @@ -0,0 +1,64 @@ +/** + * 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.link.process; + +import java.util.LinkedList; +import java.util.Map; + +import org.dspace.app.rest.SubmissionCCLicenseSearchController; +import org.dspace.app.rest.link.HalLinkFactory; +import org.dspace.app.rest.model.hateoas.SubmissionCCLicenseUrlResource; +import org.dspace.services.RequestService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.hateoas.Link; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * This class will provide the SubmissionCCLicenseUrlResource with links + */ +@Component +public class SubmissionCCLicenseUrlResourceHalLinkFactory + extends HalLinkFactory { + + @Autowired + RequestService requestService; + + /** + * Add a self link based on the search parameters + * @param halResource - The halResource + * @param pageable - The page information + * @param list - The list of present links + * @throws Exception + */ + protected void addLinks(SubmissionCCLicenseUrlResource halResource, final Pageable pageable, + LinkedList list) + throws Exception { + + halResource.removeLinks(); + Map parameterMap = requestService.getCurrentRequest().getHttpServletRequest() + .getParameterMap(); + + UriComponentsBuilder uriComponentsBuilder = uriBuilder(getMethodOn().findByRightsByQuestions()); + for (String key : parameterMap.keySet()) { + uriComponentsBuilder.queryParam(key, parameterMap.get(key)); + } + + list.add(buildLink("self", uriComponentsBuilder.build().toUriString())); + } + + + protected Class getControllerClass() { + return SubmissionCCLicenseSearchController.class; + } + + protected Class getResourceClass() { + return SubmissionCCLicenseUrlResource.class; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseRest.java index 611d532039..23589d5a46 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseRest.java @@ -15,7 +15,7 @@ import org.dspace.app.rest.RestResourceController; /** * This class is the REST representation of the CCLicense model object and acts as a data object - * * for the SubmissionCCLicenseResource class. + * for the SubmissionCCLicenseResource class. * Refer to {@link org.dspace.license.CCLicense} for explanation of the properties */ public class SubmissionCCLicenseRest extends BaseObjectRest { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseUrlRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseUrlRest.java new file mode 100644 index 0000000000..14d430b228 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseUrlRest.java @@ -0,0 +1,52 @@ +/** + * 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 com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.dspace.app.rest.SubmissionCCLicenseSearchController; + +/** + * This class is the REST representation of the CCLicense URL String object and acts as a data object + * for the SubmissionCCLicenseUrlRest class. + */ +public class SubmissionCCLicenseUrlRest extends BaseObjectRest { + public static final String NAME = "submissioncclicenseUrl"; + + private String url; + + @JsonIgnore + @Override + public String getId() { + return id; + } + + public String getUrl() { + return url; + } + + public void setUrl(final String url) { + this.url = url; + } + + @Override + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public String getType() { + return NAME; + } + + public String getCategory() { + return SubmissionCCLicenseRest.CATEGORY; + } + + @Override + @JsonIgnore + public Class getController() { + return SubmissionCCLicenseSearchController.class; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SubmissionCCLicenseUrlResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SubmissionCCLicenseUrlResource.java new file mode 100644 index 0000000000..29ce7cf669 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SubmissionCCLicenseUrlResource.java @@ -0,0 +1,23 @@ +/** + * 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.SubmissionCCLicenseUrlRest; +import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource; +import org.dspace.app.rest.utils.Utils; + +/** + * SubmissionCCLicenseUrl HAL Resource. This resource adds the data from the REST object together with embedded objects + * and a set of links if applicable + */ +@RelNameDSpaceResource(SubmissionCCLicenseUrlRest.NAME) +public class SubmissionCCLicenseUrlResource extends DSpaceResource { + public SubmissionCCLicenseUrlResource(SubmissionCCLicenseUrlRest submissionCCLicenseUrlRest, Utils utils) { + super(submissionCCLicenseUrlRest, utils); + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseSearchControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseSearchControllerIT.java index 5c35bb5acf..30734c8cac 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseSearchControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseSearchControllerIT.java @@ -33,16 +33,23 @@ public class SubmissionCCLicenseSearchControllerIT extends AbstractControllerInt "/api/config/submissioncclicenses/search/rightsByQuestions?license=license2&answer_license2-field0" + "=license2-field0-enum1")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.value", is("mock-license-uri"))) - .andExpect(jsonPath("$.type", is("plaintextvalue"))); + .andExpect(jsonPath("$.url", is("mock-license-uri"))) + .andExpect(jsonPath("$.type", is("submissioncclicenseUrl"))) + .andExpect(jsonPath("$._links.self.href", + is("http://localhost/api/config/submissioncclicenses/search/rightsByQuestions" + + "?license=license2" + + "&answer_license2-field0=license2-field0-enum1"))); } @Test public void searchRightsByQuestionsTestLicenseWithoutFields() throws Exception { getClient().perform(get("/api/config/submissioncclicenses/search/rightsByQuestions?license=license3")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.value", is("mock-license-uri"))) - .andExpect(jsonPath("$.type", is("plaintextvalue"))); + .andExpect(jsonPath("$.url", is("mock-license-uri"))) + .andExpect(jsonPath("$.type", is("submissioncclicenseUrl"))) + .andExpect(jsonPath("$._links.self.href", + is("http://localhost/api/config/submissioncclicenses/search/rightsByQuestions" + + "?license=license3"))); } @Test From df01297539dd6dd0d8e81dacb96eb25b75c58a74 Mon Sep 17 00:00:00 2001 From: Kevin Van de Velde Date: Wed, 27 May 2020 17:11:52 +0200 Subject: [PATCH 049/143] Submission CC license: Adding overrides & removing unused class --- ...ionCCLicenseUrlResourceHalLinkFactory.java | 3 ++ .../app/rest/model/PlainTextValueRest.java | 36 ------------------- .../model/SubmissionCCLicenseUrlRest.java | 1 + 3 files changed, 4 insertions(+), 36 deletions(-) delete mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/model/PlainTextValueRest.java diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/process/SubmissionCCLicenseUrlResourceHalLinkFactory.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/process/SubmissionCCLicenseUrlResourceHalLinkFactory.java index 39911faad0..cb44d68e73 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/process/SubmissionCCLicenseUrlResourceHalLinkFactory.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/process/SubmissionCCLicenseUrlResourceHalLinkFactory.java @@ -37,6 +37,7 @@ public class SubmissionCCLicenseUrlResourceHalLinkFactory * @param list - The list of present links * @throws Exception */ + @Override protected void addLinks(SubmissionCCLicenseUrlResource halResource, final Pageable pageable, LinkedList list) throws Exception { @@ -54,10 +55,12 @@ public class SubmissionCCLicenseUrlResourceHalLinkFactory } + @Override protected Class getControllerClass() { return SubmissionCCLicenseSearchController.class; } + @Override protected Class getResourceClass() { return SubmissionCCLicenseUrlResource.class; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/PlainTextValueRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/PlainTextValueRest.java deleted file mode 100644 index 6f02aa9286..0000000000 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/PlainTextValueRest.java +++ /dev/null @@ -1,36 +0,0 @@ -/** - * 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; - -/** - * Rest object used to represent a plain text value - */ -public class PlainTextValueRest { - public static final String TYPE = "plaintextvalue"; - - private String value; - - public PlainTextValueRest() { - } - - public PlainTextValueRest(String value) { - this.value = value; - } - - public String getValue() { - return value; - } - - public void setValue(final String value) { - this.value = value; - } - - public String getType() { - return TYPE; - } -} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseUrlRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseUrlRest.java index 14d430b228..5a54a8078f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseUrlRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseUrlRest.java @@ -40,6 +40,7 @@ public class SubmissionCCLicenseUrlRest extends BaseObjectRest { return NAME; } + @Override public String getCategory() { return SubmissionCCLicenseRest.CATEGORY; } From 614ad3d054e09046112caec89fd6bb68b3ffacee Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Fri, 29 May 2020 11:51:26 +0200 Subject: [PATCH 050/143] 71199: Fix PR issues --- .../SubmissionCCLicenseSearchController.java | 2 ++ .../SubmissionCCLicenseUrlConverter.java | 1 + .../model/SubmissionCCLicenseUrlRest.java | 4 +++ .../SubmissionCCLicenseRestRepository.java | 5 ++- ...censeUrlRestPermissionEvaluatorPlugin.java | 32 +++++++++++++++++++ 5 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SubmissionCCLicenseUrlRestPermissionEvaluatorPlugin.java diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseSearchController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseSearchController.java index f47376757c..17116884cf 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseSearchController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseSearchController.java @@ -23,6 +23,7 @@ import org.dspace.services.RequestService; import org.dspace.utils.DSpace; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @@ -33,6 +34,7 @@ import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/" + SubmissionCCLicenseRest.CATEGORY + "/" + SubmissionCCLicenseRest.PLURAL + "/search" + "/rightsByQuestions") +@PreAuthorize("permitAll()") public class SubmissionCCLicenseSearchController { @Autowired diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseUrlConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseUrlConverter.java index 5411ac306c..99945661c5 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseUrlConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCCLicenseUrlConverter.java @@ -29,6 +29,7 @@ public class SubmissionCCLicenseUrlConverter implements DSpaceConverter { return id; } + public void setId(String id) { + this.id = id; + } + public String getUrl() { return url; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionCCLicenseRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionCCLicenseRestRepository.java index 88ba438639..6d3cf03237 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionCCLicenseRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionCCLicenseRestRepository.java @@ -17,6 +17,7 @@ import org.springframework.beans.factory.annotation.Autowired; 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.stereotype.Component; /** @@ -29,6 +30,7 @@ public class SubmissionCCLicenseRestRepository extends DSpaceRestRepository findAll(final Context context, final Pageable pageable) { List allCCLicenses = creativeCommonsService.findAllCCLicenses(); - return converter.toRestPage(utils.getPage(allCCLicenses, pageable), utils.obtainProjection()); + return converter.toRestPage(allCCLicenses, pageable, utils.obtainProjection()); } @Override diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SubmissionCCLicenseUrlRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SubmissionCCLicenseUrlRestPermissionEvaluatorPlugin.java new file mode 100644 index 0000000000..284333e020 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SubmissionCCLicenseUrlRestPermissionEvaluatorPlugin.java @@ -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.rest.security; + +import java.io.Serializable; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.app.rest.model.DiscoveryResultsRest; +import org.dspace.app.rest.model.SubmissionCCLicenseUrlRest; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +/** + * This class will handle calls made to SubmissionCCLicenseUrlRest endpoints. + * It will return true because access can be granted anytime it's linked from another resource + */ +@Component +public class SubmissionCCLicenseUrlRestPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { + @Override + public boolean hasDSpacePermission(Authentication authentication, Serializable targetId, String targetType, + DSpaceRestPermission restPermission) { + if (!StringUtils.equalsIgnoreCase(SubmissionCCLicenseUrlRest.NAME, targetType)) { + return false; + } + return true; + } +} From 72ac35025054b6f2ea4da4ca15c7e877ba4506c7 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Fri, 29 May 2020 13:19:20 +0200 Subject: [PATCH 051/143] Remove unused import --- .../SubmissionCCLicenseUrlRestPermissionEvaluatorPlugin.java | 1 - 1 file changed, 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SubmissionCCLicenseUrlRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SubmissionCCLicenseUrlRestPermissionEvaluatorPlugin.java index 284333e020..2fd8c7647f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SubmissionCCLicenseUrlRestPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SubmissionCCLicenseUrlRestPermissionEvaluatorPlugin.java @@ -10,7 +10,6 @@ package org.dspace.app.rest.security; import java.io.Serializable; import org.apache.commons.lang3.StringUtils; -import org.dspace.app.rest.model.DiscoveryResultsRest; import org.dspace.app.rest.model.SubmissionCCLicenseUrlRest; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; From 4ea4f9112948d28858fb9b8f727da75fe4312032 Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Wed, 3 Jun 2020 14:42:40 +0200 Subject: [PATCH 052/143] [Task 71213] applied feedback to the new registration creation functionality --- .../org/dspace/app/util/AuthorizeUtil.java | 6 +- .../dspace/eperson/AccountServiceImpl.java | 3 - .../app/rest/RegistrationRestController.java | 114 ------------------ .../app/rest/model/RegistrationRest.java | 4 +- .../repository/EPersonRestRepository.java | 35 ++++-- .../RegistrationRestRepository.java | 67 ++++++++++ .../EPersonPasswordReplaceOperation.java | 14 +-- .../app/rest/EPersonRestRepositoryIT.java | 63 +++++++--- 8 files changed, 150 insertions(+), 156 deletions(-) delete mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java diff --git a/dspace-api/src/main/java/org/dspace/app/util/AuthorizeUtil.java b/dspace-api/src/main/java/org/dspace/app/util/AuthorizeUtil.java index 521eff6fc1..56891f961b 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/AuthorizeUtil.java +++ b/dspace-api/src/main/java/org/dspace/app/util/AuthorizeUtil.java @@ -34,6 +34,7 @@ import org.dspace.eperson.Group; import org.dspace.eperson.factory.EPersonServiceFactory; import org.dspace.eperson.service.GroupService; import org.dspace.services.factory.DSpaceServicesFactory; +import org.dspace.utils.DSpace; import org.dspace.xmlworkflow.factory.XmlWorkflowServiceFactory; import org.dspace.xmlworkflow.storedcomponents.CollectionRole; import org.dspace.xmlworkflow.storedcomponents.service.CollectionRoleService; @@ -637,7 +638,10 @@ public class AuthorizeUtil { try { EPerson eperson = EPersonServiceFactory.getInstance().getEPersonService().findByEmail(context, email); if (eperson != null && eperson.canLogIn()) { - return true; + HttpServletRequest request = new DSpace().getRequestService().getCurrentRequest() + .getHttpServletRequest(); + return AuthenticateServiceFactory.getInstance().getAuthenticationService() + .allowSetPassword(context, request, null); } } catch (SQLException e) { log.error("Something went wrong trying to retrieve EPerson for email: " + email, e); diff --git a/dspace-api/src/main/java/org/dspace/eperson/AccountServiceImpl.java b/dspace-api/src/main/java/org/dspace/eperson/AccountServiceImpl.java index 1aeaeaef8e..40da31a0f9 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/AccountServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/eperson/AccountServiceImpl.java @@ -164,9 +164,6 @@ public class AccountServiceImpl implements AccountService { @Override public boolean verifyPasswordStructure(String password) { - if (StringUtils.isBlank(password)) { - return false; - } if (StringUtils.length(password) < 6) { return false; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java deleted file mode 100644 index c79c2a0969..0000000000 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RegistrationRestController.java +++ /dev/null @@ -1,114 +0,0 @@ -/** - * 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 java.io.IOException; -import java.sql.SQLException; -import javax.mail.MessagingException; -import javax.servlet.ServletInputStream; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.lang3.StringUtils; -import org.dspace.app.rest.authorization.AuthorizationFeatureService; -import org.dspace.app.rest.converter.ConverterService; -import org.dspace.app.rest.exception.DSpaceBadRequestException; -import org.dspace.app.rest.exception.UnprocessableEntityException; -import org.dspace.app.rest.model.RegistrationRest; -import org.dspace.app.rest.utils.ContextUtil; -import org.dspace.app.util.AuthorizeUtil; -import org.dspace.authorize.AuthorizeException; -import org.dspace.content.service.SiteService; -import org.dspace.core.Context; -import org.dspace.eperson.EPerson; -import org.dspace.eperson.service.AccountService; -import org.dspace.eperson.service.EPersonService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.rest.webmvc.ControllerUtils; -import org.springframework.hateoas.RepresentationModel; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; - -/** - * This will be the Controller class that handles calls to the /api/eperson/registrations endpoints - */ -@RestController -@RequestMapping("/api/" + RegistrationRest.CATEGORY + "/" + RegistrationRest.NAME_PLURAL) -public class RegistrationRestController { - - @Autowired - private AuthorizationFeatureService authorizationFeatureService; - - @Autowired - private SiteService siteService; - - @Autowired - private ConverterService converterService; - - @Autowired - private AccountService accountService; - - @Autowired - private EPersonService ePersonService; - - /** - * This method will be used to either register a new user or to send forgotten password info in a mail. - * It can be called by doing a POST request to the /api/eperson/registrations endpoint. - * It'll create a RegistrationRest object from the inputstream in the request and it'll check whether the email - * defined in that object is in the DB or not. - * If it is in the db then we'll send the forgotten password info, if it wasn't in the database then we'll send - * registration info. - * - * @param request The current request - * @param response The current response - * @return An empty response containing a 201 status code - * @throws SQLException If something goes wrong - * @throws IOException If something goes wrong - * @throws MessagingException If something goes wrong - * @throws AuthorizeException If something goes wrong - */ - @RequestMapping(method = RequestMethod.POST) - public ResponseEntity> register(HttpServletRequest request, HttpServletResponse response) - throws SQLException, IOException, MessagingException, AuthorizeException { - - Context context = ContextUtil.obtainContext(request); - ObjectMapper mapper = new ObjectMapper(); - RegistrationRest registrationRest; - try { - ServletInputStream input = request.getInputStream(); - registrationRest = mapper.readValue(input, RegistrationRest.class); - } catch (IOException e1) { - throw new UnprocessableEntityException("Error parsing request body.", e1); - } - if (StringUtils.isBlank(registrationRest.getEmail())) { - throw new UnprocessableEntityException("The email cannot be omitted from the Registration endpoint"); - } - EPerson eperson = ePersonService.findByEmail(context, registrationRest.getEmail()); - if (eperson != null) { - if (!AuthorizeUtil.authorizeUpdatePassword(context, eperson.getEmail())) { - throw new DSpaceBadRequestException("Password cannot be updated for the given EPerson with email: " + - eperson.getEmail()); - } - accountService.sendForgotPasswordInfo(context, registrationRest.getEmail()); - } else { - if (!AuthorizeUtil.authorizeNewAccountRegistration(context, request)) { - throw new AccessDeniedException( - "Registration is disabled, you are not authorized to create a new Authorization"); - } - accountService.sendRegistrationInfo(context, registrationRest.getEmail()); - } - context.complete(); - return ControllerUtils.toEmptyResponse(HttpStatus.CREATED); - } - -} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java index 69ed1d5653..e8397f8ca7 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java @@ -10,7 +10,7 @@ package org.dspace.app.rest.model; import java.util.UUID; import com.fasterxml.jackson.annotation.JsonProperty; -import org.dspace.app.rest.RegistrationRestController; +import org.dspace.app.rest.RestResourceController; /** @@ -66,7 +66,7 @@ public class RegistrationRest extends RestAddressableModel { @Override public Class getController() { - return RegistrationRestController.class; + return RestResourceController.class; } @Override diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java index 4226ff6ab4..6d5e51f961 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java @@ -96,8 +96,8 @@ public class EPersonRestRepository extends DSpaceObjectRestRepository { + private static Logger log = org.apache.logging.log4j.LogManager.getLogger(RegistrationRestRepository.class); + + @Autowired + private EPersonService ePersonService; + @Autowired private AccountService accountService; + @Autowired + private RequestService requestService; + @Autowired private RegistrationDataService registrationDataService; @@ -47,6 +68,52 @@ public class RegistrationRestRepository extends DSpaceRestRepository getDomainClass() { return RegistrationRest.class; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/EPersonPasswordReplaceOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/EPersonPasswordReplaceOperation.java index ae02200d98..1a3201abdd 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/EPersonPasswordReplaceOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/EPersonPasswordReplaceOperation.java @@ -57,15 +57,15 @@ public class EPersonPasswordReplaceOperation extends PatchOperation { checkOperationValue(operation.getValue()); if (supports(object, operation)) { EPerson eperson = (EPerson) object; - String token = requestService.getCurrentRequest().getHttpServletRequest().getParameter("token"); - checkModelForExistingValue(eperson); - if (StringUtils.isNotBlank(token)) { - patchWithToken(context,eperson, token, operation); - } if (!AuthorizeUtil.authorizeUpdatePassword(context, eperson.getEmail())) { throw new DSpaceBadRequestException("Password cannot be updated for the given EPerson with email: " + eperson.getEmail()); } + String token = requestService.getCurrentRequest().getHttpServletRequest().getParameter("token"); + checkModelForExistingValue(eperson); + if (StringUtils.isNotBlank(token)) { + verifyAndDeleteToken(context, eperson, token, operation); + } ePersonService.setPassword(eperson, (String) operation.getValue()); return object; } else { @@ -73,7 +73,7 @@ public class EPersonPasswordReplaceOperation extends PatchOperation { } } - private void patchWithToken(Context context, EPerson eperson, String token, Operation operation) { + private void verifyAndDeleteToken(Context context, EPerson eperson, String token, Operation operation) { try { EPerson ePersonFromToken = accountService.getEPerson(context, token); if (ePersonFromToken == null) { @@ -86,7 +86,7 @@ public class EPersonPasswordReplaceOperation extends PatchOperation { } accountService.deleteToken(context, token); } catch (SQLException | AuthorizeException e) { - log.error(e.getMessage(), e); + log.error("Failed to verify or delete the token for an EPerson patch", e); } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java index 21b886ec45..ba81312f3e 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java @@ -15,7 +15,9 @@ import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -156,11 +158,16 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { dataFull.setCanLogIn(true); dataFull.setMetadata(metadataRest); + context.restoreAuthSystemState(); + getClient().perform(post("/api/eperson/epersons") .content(mapper.writeValueAsBytes(data)) .contentType(contentType) .param("projection", "full")) .andExpect(status().isUnauthorized()); + getClient().perform(get("/api/eperson/epersons/search/byEmail") + .param("email", data.getEmail())) + .andExpect(status().isNoContent()); } @Test @@ -1857,12 +1864,10 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isOk()); PasswordHash newPasswordHash = ePersonService.getPasswordHash(ePerson); - assertFalse(oldPassword.equals(newPasswordHash)); + assertNotEquals(oldPassword, newPasswordHash); assertTrue(registrationDataService.findByEmail(context, ePerson.getEmail()) == null); - context.turnOffAuthorisationSystem(); - registrationDataService.deleteByToken(context, tokenForEPerson); - context.restoreAuthSystemState(); + assertNull(registrationDataService.findByToken(context, tokenForEPerson)); } @@ -1886,7 +1891,7 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { String patchBody = getPatchContent(ops); accountService.sendRegistrationInfo(context, ePerson.getEmail()); String tokenForEPerson = registrationDataService.findByEmail(context, ePerson.getEmail()).getToken(); - String token = getAuthToken(admin.getEmail(), password); + String token = getAuthToken(eperson.getEmail(), password); PasswordHash oldPassword = ePersonService.getPasswordHash(ePerson); // updates password getClient(token).perform(patch("/api/eperson/epersons/" + ePerson.getID()) @@ -1896,10 +1901,9 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isForbidden()); PasswordHash newPasswordHash = ePersonService.getPasswordHash(ePerson); - assertTrue(StringUtils.equalsIgnoreCase(oldPassword.getHashString(),newPasswordHash.getHashString())); - assertFalse(registrationDataService.findByEmail(context, ePerson.getEmail()) == null); - assertTrue(StringUtils.equals(registrationDataService.findByEmail(context, ePerson.getEmail()).getToken(), - tokenForEPerson)); + assertEquals(oldPassword.getHashString(),newPasswordHash.getHashString()); + assertNotNull(registrationDataService.findByEmail(context, ePerson.getEmail())); + assertEquals(registrationDataService.findByEmail(context, ePerson.getEmail()).getToken(), tokenForEPerson); context.turnOffAuthorisationSystem(); registrationDataService.deleteByToken(context, tokenForEPerson); @@ -1936,7 +1940,7 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { String tokenForEPerson = registrationDataService.findByEmail(context, ePerson.getEmail()).getToken(); String tokenForEPersonTwo = registrationDataService.findByEmail(context, ePersonTwo.getEmail()).getToken(); - String token = getAuthToken(admin.getEmail(), password); + String token = getAuthToken(eperson.getEmail(), password); PasswordHash oldPassword = ePersonService.getPasswordHash(ePerson); // updates password getClient(token).perform(patch("/api/eperson/epersons/" + ePerson.getID()) @@ -1946,8 +1950,8 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isForbidden()); PasswordHash newPasswordHash = ePersonService.getPasswordHash(ePerson); - assertTrue(StringUtils.equalsIgnoreCase(oldPassword.getHashString(),newPasswordHash.getHashString())); - assertFalse(registrationDataService.findByEmail(context, ePerson.getEmail()) == null); + assertEquals(oldPassword.getHashString(),newPasswordHash.getHashString()); + assertNotNull(registrationDataService.findByEmail(context, ePerson.getEmail())); context.turnOffAuthorisationSystem(); registrationDataService.deleteByToken(context, tokenForEPerson); @@ -1959,7 +1963,7 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { public void patchReplaceEmailWithTokenFail() throws Exception { context.turnOffAuthorisationSystem(); - String originalEmail = "Johndoe@fake-email.com"; + String originalEmail = "johndoe@fake-email.com"; EPerson ePerson = EPersonBuilder.createEPerson(context) .withNameInMetadata("John", "Doe") .withEmail(originalEmail) @@ -1976,7 +1980,7 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { String patchBody = getPatchContent(ops); accountService.sendRegistrationInfo(context, ePerson.getEmail()); String tokenForEPerson = registrationDataService.findByEmail(context, ePerson.getEmail()).getToken(); - String token = getAuthToken(admin.getEmail(), password); + String token = getAuthToken(eperson.getEmail(), password); PasswordHash oldPassword = ePersonService.getPasswordHash(ePerson); // updates password getClient(token).perform(patch("/api/eperson/epersons/" + ePerson.getID()) @@ -1986,9 +1990,9 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isForbidden()); PasswordHash newPasswordHash = ePersonService.getPasswordHash(ePerson); - assertTrue(StringUtils.equalsIgnoreCase(oldPassword.getHashString(),newPasswordHash.getHashString())); - assertFalse(registrationDataService.findByEmail(context, ePerson.getEmail()) == null); - assertTrue(StringUtils.equalsIgnoreCase(ePerson.getEmail(), originalEmail)); + assertEquals(oldPassword.getHashString(),newPasswordHash.getHashString()); + assertNotNull(registrationDataService.findByEmail(context, ePerson.getEmail())); + assertEquals(ePerson.getEmail(), originalEmail); context.turnOffAuthorisationSystem(); registrationDataService.delete(context, registrationDataService.findByEmail(context, ePerson.getEmail())); @@ -2026,7 +2030,7 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { String patchBody = getPatchContent(ops); accountService.sendRegistrationInfo(context, ePerson.getEmail()); String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); - String token = getAuthToken(admin.getEmail(), password); + String token = getAuthToken(eperson.getEmail(), password); PasswordHash oldPassword = ePersonService.getPasswordHash(ePerson); // updates password getClient(token).perform(patch("/api/eperson/epersons/" + ePerson.getID()) @@ -2060,6 +2064,8 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isCreated()); String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); + // We need to create this json manually to support actually setting the password to a value. + // When using the mapper to write an EPersonRest object to JSON to pass it along, the password gets lost String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]," + "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"password\":\"somePassword\"," + "\"type\":\"eperson\"}"; @@ -2110,6 +2116,8 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isCreated()); String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); + // We need to create this json manually to support actually setting the password to a value. + // When using the mapper to write an EPersonRest object to JSON to pass it along, the password gets lost String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]," + "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"email\":\"" + newRegisterEmail + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; @@ -2162,6 +2170,8 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isCreated()); String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); + // We need to create this json manually to support actually setting the password to a value. + // When using the mapper to write an EPersonRest object to JSON to pass it along, the password gets lost String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]," + "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"selfRegistered\":true,\"email\":\"" + newRegisterEmail + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; @@ -2223,7 +2233,8 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isCreated()); String newRegisterTokenTwo = registrationDataService.findByEmail(context, newRegisterEmailTwo).getToken(); - + // We need to create this json manually to support actually setting the password to a value. + // When using the mapper to write an EPersonRest object to JSON to pass it along, the password gets lost String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]," + "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"email\":\"" + newRegisterEmailTwo + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; @@ -2259,6 +2270,8 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isCreated()); String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); + // We need to create this json manually to support actually setting the password to a value. + // When using the mapper to write an EPersonRest object to JSON to pass it along, the password gets lost String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]," + "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"email\":\"" + newRegisterEmail + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; @@ -2292,6 +2305,8 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isCreated()); String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); + // We need to create this json manually to support actually setting the password to a value. + // When using the mapper to write an EPersonRest object to JSON to pass it along, the password gets lost String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]," + "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"selfRegistered\":false,\"email\":\"" + newRegisterEmail + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; @@ -2325,6 +2340,8 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isCreated()); String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); + // We need to create this json manually to support actually setting the password to a value. + // When using the mapper to write an EPersonRest object to JSON to pass it along, the password gets lost String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]},\"selfRegistered\":true," + "\"email\":\"" + newRegisterEmail + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; @@ -2357,6 +2374,8 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isCreated()); String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); + // We need to create this json manually to support actually setting the password to a value. + // When using the mapper to write an EPersonRest object to JSON to pass it along, the password gets lost String json = "{\"metadata\":{\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"selfRegistered\":true," + "\"email\":\"" + newRegisterEmail + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; @@ -2389,6 +2408,8 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isCreated()); String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); + // We need to create this json manually to support actually setting the password to a value. + // When using the mapper to write an EPersonRest object to JSON to pass it along, the password gets lost String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]," + "\"eperson.lastname\":[{\"value\":\"Doe\"}]}," + "\"type\":\"eperson\"}"; @@ -2423,6 +2444,8 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isCreated()); String forgotPasswordToken = registrationDataService.findByEmail(context, eperson.getEmail()).getToken(); + // We need to create this json manually to support actually setting the password to a value. + // When using the mapper to write an EPersonRest object to JSON to pass it along, the password gets lost String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]," + "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"selfRegistered\":true,\"password\":\"somePassword\"," + "\"type\":\"eperson\"}"; @@ -2457,6 +2480,8 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isCreated()); String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); + // We need to create this json manually to support actually setting the password to a value. + // When using the mapper to write an EPersonRest object to JSON to pass it along, the password gets lost String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]," + "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"email\":\"" + newRegisterEmail + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; From a338526583e21d10058e5a75edda81f1c4681196 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Wed, 3 Jun 2020 15:39:42 +0200 Subject: [PATCH 053/143] Implement community feedbacks --- .../rest/utils/BitstreamMetadataValuePathUtils.java | 1 + .../dspace/app/rest/WorkspaceItemRestRepositoryIT.java | 10 ++++++++-- .../dspace/app/rest/builder/WorkspaceItemBuilder.java | 3 +++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamMetadataValuePathUtils.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamMetadataValuePathUtils.java index 4ff42d70cd..dda661e491 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamMetadataValuePathUtils.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamMetadataValuePathUtils.java @@ -38,6 +38,7 @@ public class BitstreamMetadataValuePathUtils { public void validate(String absolutePath) throws DCInputsReaderException { String[] split = absolutePath.split("/"); DCInputSet inputConfig = inputReader.getInputsByFormName(UploadStep.UPLOAD_STEP_METADATA_SECTION); + // according to the rest contract the absolute path must be something like files/:idx/metadata/dc.title if (split.length >= 4) { if (!inputConfig.isFieldPresent(split[3])) { throw new UnprocessableEntityException("The field " + split[3] + " is not present in section " diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java index 4e848eb256..e18f06c1e4 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java @@ -3685,16 +3685,22 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration .withIssueDate("2020-01-21") .withSubject("Subject 1") .withSubject("Subject 2") + .withAbstract("Test description abstract") .build(); context.restoreAuthSystemState(); + String authToken = getAuthToken(eperson.getEmail(), password); + + getClient(authToken).perform(get("/api/submission/workspaceitems/" + witem.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections.traditionalpagetwo['dc.subject']").isNotEmpty()) + .andExpect(jsonPath("$.sections.traditionalpagetwo['dc.description.abstract']").isNotEmpty()); + List operations = new ArrayList(); operations.add(new RemoveOperation("/sections/traditionalpagetwo")); String patchBody = getPatchContent(operations); - String authToken = getAuthToken(eperson.getEmail(), password); - getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) .content(patchBody) .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/builder/WorkspaceItemBuilder.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/builder/WorkspaceItemBuilder.java index 5716667703..0c328a79e9 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/builder/WorkspaceItemBuilder.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/builder/WorkspaceItemBuilder.java @@ -163,6 +163,9 @@ public class WorkspaceItemBuilder extends AbstractBuilder Date: Thu, 4 Jun 2020 16:38:37 +0200 Subject: [PATCH 054/143] 71266: Community feedback #1 --- .../SubmissionCCLicenseSearchController.java | 2 +- .../SubmissionCCLicenseRestRepository.java | 4 +- .../rest/CCLicenseRemovePatchOperationIT.java | 42 +++++++++++++++++-- .../SubmissionCCLicenseRestRepositoryIT.java | 25 +++++++++-- ...SubmissionCCLicenseSearchControllerIT.java | 31 +++++++++++--- 5 files changed, 90 insertions(+), 14 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseSearchController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseSearchController.java index 17116884cf..76ed1021d3 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseSearchController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseSearchController.java @@ -34,7 +34,7 @@ import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/" + SubmissionCCLicenseRest.CATEGORY + "/" + SubmissionCCLicenseRest.PLURAL + "/search" + "/rightsByQuestions") -@PreAuthorize("permitAll()") +@PreAuthorize("hasAuthority('AUTHENTICATED')") public class SubmissionCCLicenseSearchController { @Autowired diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionCCLicenseRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionCCLicenseRestRepository.java index 6d3cf03237..0dab42f9bd 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionCCLicenseRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionCCLicenseRestRepository.java @@ -30,7 +30,7 @@ public class SubmissionCCLicenseRestRepository extends DSpaceRestRepository findAll(final Context context, final Pageable pageable) { List allCCLicenses = creativeCommonsService.findAllCCLicenses(); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseRemovePatchOperationIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseRemovePatchOperationIT.java index afee0aa882..40828a7667 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseRemovePatchOperationIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseRemovePatchOperationIT.java @@ -59,7 +59,7 @@ public class CCLicenseRemovePatchOperationIT extends AbstractControllerIntegrati context.restoreAuthSystemState(); - String adminToken = getAuthToken(admin.getEmail(), password); + String epersonToken = getAuthToken(eperson.getEmail(), password); // First add a license and verify it is added List ops = new ArrayList(); @@ -70,7 +70,7 @@ public class CCLicenseRemovePatchOperationIT extends AbstractControllerIntegrati String patchBody = getPatchContent(ops); - getClient(adminToken).perform(patch("/api/submission/workspaceitems/" + workspaceItem.getID()) + getClient(epersonToken).perform(patch("/api/submission/workspaceitems/" + workspaceItem.getID()) .content(patchBody) .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) .andExpect(status().isOk()) @@ -91,10 +91,46 @@ public class CCLicenseRemovePatchOperationIT extends AbstractControllerIntegrati String removePatch = getPatchContent(removeOps); - getClient(adminToken).perform(patch("/api/submission/workspaceitems/" + workspaceItem.getID()) + getClient(epersonToken).perform(patch("/api/submission/workspaceitems/" + workspaceItem.getID()) .content(removePatch) .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.sections", not(hasJsonPath("cclicense")))); } + + + @Test + public void patchRemoveSubmissionCCLicenseNonExisting() throws Exception { + context.turnOffAuthorisationSystem(); + + Community community = CommunityBuilder.createCommunity(context) + .withName("Community") + .build(); + + Collection collection = CollectionBuilder.createCollection(context, community) + .withName("Collection") + .build(); + + WorkspaceItem workspaceItem = WorkspaceItemBuilder.createWorkspaceItem(context, collection) + .withTitle("Workspace Item") + .build(); + + context.restoreAuthSystemState(); + + String epersonToken = getAuthToken(eperson.getEmail(), password); + + + List removeOps = new ArrayList(); + RemoveOperation removeOperation = new RemoveOperation("/sections/cclicense/uri"); + + removeOps.add(removeOperation); + String removePatch = getPatchContent(removeOps); + + + getClient(epersonToken).perform(patch("/api/submission/workspaceitems/" + workspaceItem.getID()) + .content(removePatch) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections", not(hasJsonPath("cclicense")))); + } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseRestRepositoryIT.java index 5fa22470fe..9267cef6c5 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseRestRepositoryIT.java @@ -34,8 +34,9 @@ public class SubmissionCCLicenseRestRepositoryIT extends AbstractControllerInteg */ @Test public void findAllTest() throws Exception { + String epersonToken = getAuthToken(eperson.getEmail(), password); - getClient().perform(get("/api/config/submissioncclicenses")) + getClient(epersonToken).perform(get("/api/config/submissioncclicenses")) .andExpect(status().isOk()) .andExpect(content().contentType(contentType)) .andExpect(jsonPath("$._embedded.submissioncclicenses", Matchers.containsInAnyOrder( @@ -47,7 +48,9 @@ public class SubmissionCCLicenseRestRepositoryIT extends AbstractControllerInteg @Test public void findOneTest() throws Exception { - getClient().perform(get("/api/config/submissioncclicenses/license1")) + String epersonToken = getAuthToken(eperson.getEmail(), password); + + getClient(epersonToken).perform(get("/api/config/submissioncclicenses/license1")) .andExpect(status().isOk()) .andExpect(content().contentType(contentType)) .andExpect(jsonPath("$", Matchers.is( @@ -57,7 +60,23 @@ public class SubmissionCCLicenseRestRepositoryIT extends AbstractControllerInteg @Test public void findOneTestNonExistingLicense() throws Exception { - getClient().perform(get("/api/config/submissioncclicenses/non-existing-license")) + String epersonToken = getAuthToken(eperson.getEmail(), password); + + getClient(epersonToken).perform(get("/api/config/submissioncclicenses/non-existing-license")) .andExpect(status().isNotFound()); } + + @Test + public void findAllTestUnAuthorized() throws Exception { + getClient().perform(get("/api/config/submissioncclicenses")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findOneTestUnAuthorized() throws Exception { + + getClient().perform(get("/api/config/submissioncclicenses/license1")) + .andExpect(status().isUnauthorized()); + } + } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseSearchControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseSearchControllerIT.java index 30734c8cac..8a93e18a8e 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseSearchControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseSearchControllerIT.java @@ -29,7 +29,9 @@ public class SubmissionCCLicenseSearchControllerIT extends AbstractControllerInt @Test public void searchRightsByQuestionsTest() throws Exception { - getClient().perform(get( + String epersonToken = getAuthToken(eperson.getEmail(), password); + + getClient(epersonToken).perform(get( "/api/config/submissioncclicenses/search/rightsByQuestions?license=license2&answer_license2-field0" + "=license2-field0-enum1")) .andExpect(status().isOk()) @@ -43,7 +45,10 @@ public class SubmissionCCLicenseSearchControllerIT extends AbstractControllerInt @Test public void searchRightsByQuestionsTestLicenseWithoutFields() throws Exception { - getClient().perform(get("/api/config/submissioncclicenses/search/rightsByQuestions?license=license3")) + String epersonToken = getAuthToken(eperson.getEmail(), password); + + getClient(epersonToken) + .perform(get("/api/config/submissioncclicenses/search/rightsByQuestions?license=license3")) .andExpect(status().isOk()) .andExpect(jsonPath("$.url", is("mock-license-uri"))) .andExpect(jsonPath("$.type", is("submissioncclicenseUrl"))) @@ -54,7 +59,9 @@ public class SubmissionCCLicenseSearchControllerIT extends AbstractControllerInt @Test public void searchRightsByQuestionsNonExistingLicense() throws Exception { - getClient().perform(get( + String epersonToken = getAuthToken(eperson.getEmail(), password); + + getClient(epersonToken).perform(get( "/api/config/submissioncclicenses/search/rightsByQuestions?license=nonexisting-license" + "&answer_license2-field0=license2-field0-enum1")) .andExpect(status().isNotFound()); @@ -62,7 +69,9 @@ public class SubmissionCCLicenseSearchControllerIT extends AbstractControllerInt @Test public void searchRightsByQuestionsMissingRequiredAnswer() throws Exception { - getClient().perform(get( + String epersonToken = getAuthToken(eperson.getEmail(), password); + + getClient(epersonToken).perform(get( "/api/config/submissioncclicenses/search/rightsByQuestions?license=license1&answer_license1field0" + "=license1field0enum1")) .andExpect(status().isBadRequest()); @@ -70,9 +79,21 @@ public class SubmissionCCLicenseSearchControllerIT extends AbstractControllerInt @Test public void searchRightsByQuestionsAdditionalNonExistingAnswer() throws Exception { - getClient().perform(get( + String epersonToken = getAuthToken(eperson.getEmail(), password); + + getClient(epersonToken).perform(get( "/api/config/submissioncclicenses/search/rightsByQuestions?license=license2" + "&answer_license2field0=license2field0enum1&answer_nonexisting=test")) .andExpect(status().isBadRequest()); } + + @Test + public void searchRightsByQuestionsAdditionalUnAuthorized() throws Exception { + + getClient().perform(get( + "/api/config/submissioncclicenses/search/rightsByQuestions?license=license2&answer_license2-field0" + + "=license2-field0-enum1")) + .andExpect(status().isUnauthorized()); + + } } From fdaddd934fe140c60a3db77a1c79a363b369ecb6 Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Mon, 8 Jun 2020 16:24:19 +0200 Subject: [PATCH 055/143] [Task 71271] applied fixes after merge and applied community feedback to the new registration endpoints --- .../org/dspace/app/util/AuthorizeUtil.java | 4 + .../repository/EPersonRestRepository.java | 5 +- .../EPersonRestPermissionEvaluatorPlugin.java | 29 +-- .../app/rest/EPersonRestRepositoryIT.java | 235 +++++++++--------- .../app/rest/ProcessRestRepositoryIT.java | 7 + .../rest/RegistrationRestControllerIT.java | 18 +- 6 files changed, 166 insertions(+), 132 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/util/AuthorizeUtil.java b/dspace-api/src/main/java/org/dspace/app/util/AuthorizeUtil.java index 56891f961b..70a0d7e2e9 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/AuthorizeUtil.java +++ b/dspace-api/src/main/java/org/dspace/app/util/AuthorizeUtil.java @@ -621,6 +621,10 @@ public class AuthorizeUtil { throws SQLException { if (DSpaceServicesFactory.getInstance().getConfigurationService() .getBooleanProperty("user.registration", true)) { + // This allowSetPassword is currently the only mthod that would return true only when it's + // actually expected to be returning true. + // For example the LDAP canSelfRegister will return true due to auto-register, while that + // does not imply a new user can register explicitly return AuthenticateServiceFactory.getInstance().getAuthenticationService() .allowSetPassword(context, request, null); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java index 5486c54fdb..ed878374b7 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java @@ -42,8 +42,8 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.security.access.AccessDeniedException; import org.springframework.hateoas.Link; +import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Component; @@ -186,6 +186,9 @@ public class EPersonRestRepository extends DSpaceObjectRestRepository idRef = new AtomicReference(); - String content = mvcResult.getResponse().getContentAsString(); - Map map = mapper.readValue(content, Map.class); - String epersonUuid = String.valueOf(map.get("uuid")); - EPerson createdEPerson = ePersonService.find(context, UUID.fromString(epersonUuid)); - assertTrue(ePersonService.checkPassword(context, createdEPerson, "somePassword")); + try { + getClient().perform(post("/api/eperson/epersons") + .param("token", newRegisterToken) + .content(json) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$", Matchers.allOf( + hasJsonPath("$.uuid", not(empty())), + // is it what you expect? EPerson.getName() returns the email... + //hasJsonPath("$.name", is("Doe John")), + hasJsonPath("$.type", is("eperson")), + hasJsonPath("$._links.self.href", not(empty())), + hasJsonPath("$.metadata", Matchers.allOf( + matchMetadata("eperson.firstname", "John"), + matchMetadata("eperson.lastname", "Doe") + ))))) + .andDo(result -> idRef + .set(UUID.fromString(read(result.getResponse().getContentAsString(), "$.id")))); - assertNull(registrationDataService.findByToken(context, newRegisterToken)); - context.turnOffAuthorisationSystem(); - ePersonService.delete(context, createdEPerson); - context.restoreAuthSystemState(); - context.turnOffAuthorisationSystem(); - registrationDataService.deleteByToken(context, newRegisterToken); - context.restoreAuthSystemState(); + + String epersonUuid = String.valueOf(idRef.get()); + EPerson createdEPerson = ePersonService.find(context, UUID.fromString(epersonUuid)); + assertTrue(ePersonService.checkPassword(context, createdEPerson, "somePassword")); + + assertNull(registrationDataService.findByToken(context, newRegisterToken)); + + context.turnOffAuthorisationSystem(); + registrationDataService.deleteByToken(context, newRegisterToken); + context.restoreAuthSystemState(); + } finally { + EPersonBuilder.deleteEPerson(idRef.get()); + } } @Test @@ -2135,37 +2138,38 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"email\":\"" + newRegisterEmail + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; - MvcResult mvcResult = getClient().perform(post("/api/eperson/epersons") - .param("token", newRegisterToken) - .content(json) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()) - .andExpect(jsonPath("$", Matchers.allOf( - hasJsonPath("$.uuid", not(empty())), - // is it what you expect? EPerson.getName() returns the email... - //hasJsonPath("$.name", is("Doe John")), - hasJsonPath("$.email", is(newRegisterEmail)), - hasJsonPath("$.type", is("eperson")), - hasJsonPath("$._links.self.href", not(empty())), - hasJsonPath("$.metadata", Matchers.allOf( - matchMetadata("eperson.firstname", "John"), - matchMetadata("eperson.lastname", "Doe") - ))))).andReturn(); + AtomicReference idRef = new AtomicReference(); - String content = mvcResult.getResponse().getContentAsString(); - Map map = mapper.readValue(content, Map.class); - String epersonUuid = String.valueOf(map.get("uuid")); - EPerson createdEPerson = ePersonService.find(context, UUID.fromString(epersonUuid)); - assertTrue(ePersonService.checkPassword(context, createdEPerson, "somePassword")); - assertNull(registrationDataService.findByToken(context, newRegisterToken)); + try { + getClient().perform(post("/api/eperson/epersons") + .param("token", newRegisterToken) + .content(json) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$", Matchers.allOf( + hasJsonPath("$.uuid", not(empty())), + // is it what you expect? EPerson.getName() returns the email... + //hasJsonPath("$.name", is("Doe John")), + hasJsonPath("$.email", is(newRegisterEmail)), + hasJsonPath("$.type", is("eperson")), + hasJsonPath("$._links.self.href", not(empty())), + hasJsonPath("$.metadata", Matchers.allOf( + matchMetadata("eperson.firstname", "John"), + matchMetadata("eperson.lastname", "Doe") + ))))).andDo(result -> idRef + .set(UUID.fromString(read(result.getResponse().getContentAsString(), "$.id")))); - context.turnOffAuthorisationSystem(); - ePersonService.delete(context, createdEPerson); - context.restoreAuthSystemState(); + String epersonUuid = String.valueOf(idRef.get()); + EPerson createdEPerson = ePersonService.find(context, UUID.fromString(epersonUuid)); + assertTrue(ePersonService.checkPassword(context, createdEPerson, "somePassword")); + assertNull(registrationDataService.findByToken(context, newRegisterToken)); - context.turnOffAuthorisationSystem(); - registrationDataService.deleteByToken(context, newRegisterToken); - context.restoreAuthSystemState(); + context.turnOffAuthorisationSystem(); + registrationDataService.deleteByToken(context, newRegisterToken); + context.restoreAuthSystemState(); + } finally { + EPersonBuilder.deleteEPerson(idRef.get()); + } } @@ -2189,37 +2193,42 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"selfRegistered\":true,\"email\":\"" + newRegisterEmail + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; - MvcResult mvcResult = getClient().perform(post("/api/eperson/epersons") - .param("token", newRegisterToken) - .content(json) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()) - .andExpect(jsonPath("$", Matchers.allOf( - hasJsonPath("$.uuid", not(empty())), - // is it what you expect? EPerson.getName() returns the email... - //hasJsonPath("$.name", is("Doe John")), - hasJsonPath("$.email", is(newRegisterEmail)), - hasJsonPath("$.type", is("eperson")), - hasJsonPath("$._links.self.href", not(empty())), - hasJsonPath("$.metadata", Matchers.allOf( - matchMetadata("eperson.firstname", "John"), - matchMetadata("eperson.lastname", "Doe") - ))))).andReturn(); + AtomicReference idRef = new AtomicReference(); - String content = mvcResult.getResponse().getContentAsString(); - Map map = mapper.readValue(content, Map.class); - String epersonUuid = String.valueOf(map.get("uuid")); - EPerson createdEPerson = ePersonService.find(context, UUID.fromString(epersonUuid)); - assertTrue(ePersonService.checkPassword(context, createdEPerson, "somePassword")); - assertNull(registrationDataService.findByToken(context, newRegisterToken)); + try { + getClient().perform(post("/api/eperson/epersons") + .param("token", newRegisterToken) + .content(json) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$", Matchers.allOf( + hasJsonPath("$.uuid", not(empty())), + // is it what you expect? EPerson.getName() returns the email... + //hasJsonPath("$.name", is("Doe John")), + hasJsonPath("$.email", is(newRegisterEmail)), + hasJsonPath("$.type", is("eperson")), + hasJsonPath("$._links.self.href", not(empty())), + hasJsonPath("$.metadata", Matchers.allOf( + matchMetadata("eperson.firstname", "John"), + matchMetadata("eperson.lastname", "Doe") + ))))).andDo(result -> idRef + .set(UUID.fromString(read(result.getResponse().getContentAsString(), "$.id")))); - context.turnOffAuthorisationSystem(); - ePersonService.delete(context, createdEPerson); - context.restoreAuthSystemState(); - context.turnOffAuthorisationSystem(); - registrationDataService.deleteByToken(context, newRegisterToken); - context.restoreAuthSystemState(); + String epersonUuid = String.valueOf(idRef.get()); + EPerson createdEPerson = ePersonService.find(context, UUID.fromString(epersonUuid)); + assertTrue(ePersonService.checkPassword(context, createdEPerson, "somePassword")); + assertNull(registrationDataService.findByToken(context, newRegisterToken)); + + context.turnOffAuthorisationSystem(); + context.restoreAuthSystemState(); + + context.turnOffAuthorisationSystem(); + registrationDataService.deleteByToken(context, newRegisterToken); + context.restoreAuthSystemState(); + } finally { + EPersonBuilder.deleteEPerson(idRef.get()); + } } @@ -2499,34 +2508,38 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"email\":\"" + newRegisterEmail + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; - MvcResult mvcResult = getClient().perform(post("/api/eperson/epersons") - .param("token", newRegisterToken) - .content(json) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()) - .andExpect(jsonPath("$", Matchers.allOf( - hasJsonPath("$.uuid", not(empty())), - // is it what you expect? EPerson.getName() returns the email... - //hasJsonPath("$.name", is("Doe John")), - hasJsonPath("$.email", is(newRegisterEmail)), - hasJsonPath("$.type", is("eperson")), - hasJsonPath("$._links.self.href", not(empty())), - hasJsonPath("$.metadata", Matchers.allOf( - matchMetadata("eperson.firstname", "John"), - matchMetadata("eperson.lastname", "Doe") - ))))).andReturn(); + AtomicReference idRef = new AtomicReference(); - String content = mvcResult.getResponse().getContentAsString(); - Map map = mapper.readValue(content, Map.class); - String epersonUuid = String.valueOf(map.get("uuid")); - EPerson createdEPerson = ePersonService.find(context, UUID.fromString(epersonUuid)); - assertTrue(ePersonService.checkPassword(context, createdEPerson, "somePassword")); - assertNull(registrationDataService.findByToken(context, newRegisterToken)); + try { + getClient().perform(post("/api/eperson/epersons") + .param("token", newRegisterToken) + .content(json) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$", Matchers.allOf( + hasJsonPath("$.uuid", not(empty())), + // is it what you expect? EPerson.getName() returns the email... + //hasJsonPath("$.name", is("Doe John")), + hasJsonPath("$.email", is(newRegisterEmail)), + hasJsonPath("$.type", is("eperson")), + hasJsonPath("$._links.self.href", not(empty())), + hasJsonPath("$.metadata", Matchers.allOf( + matchMetadata("eperson.firstname", "John"), + matchMetadata("eperson.lastname", "Doe") + ))))).andDo(result -> idRef + .set(UUID.fromString(read(result.getResponse().getContentAsString(), "$.id")))); - context.turnOffAuthorisationSystem(); - ePersonService.delete(context, createdEPerson); - registrationDataService.deleteByToken(context, newRegisterToken); - context.restoreAuthSystemState(); + String epersonUuid = String.valueOf(idRef.get()); + EPerson createdEPerson = ePersonService.find(context, UUID.fromString(epersonUuid)); + assertTrue(ePersonService.checkPassword(context, createdEPerson, "somePassword")); + assertNull(registrationDataService.findByToken(context, newRegisterToken)); + + context.turnOffAuthorisationSystem(); + registrationDataService.deleteByToken(context, newRegisterToken); + context.restoreAuthSystemState(); + } finally { + EPersonBuilder.deleteEPerson(idRef.get()); + } } @Test diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ProcessRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ProcessRestRepositoryIT.java index 92c37007b1..de63aaf6cf 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ProcessRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ProcessRestRepositoryIT.java @@ -43,6 +43,13 @@ public class ProcessRestRepositoryIT extends AbstractControllerIntegrationTest { @Before public void setup() throws SQLException { + CollectionUtils.emptyIfNull(processService.findAll(context)).stream().forEach(process -> { + try { + processService.delete(context, process); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); parameters.add(new DSpaceCommandLineParameter("-r", "test")); parameters.add(new DSpaceCommandLineParameter("-i", null)); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java index f50b4c9d65..da6f7a94d8 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java @@ -12,16 +12,19 @@ import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.sql.SQLException; import java.util.Iterator; import java.util.List; import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.dspace.app.rest.model.RegistrationRest; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.eperson.RegistrationData; import org.dspace.eperson.dao.RegistrationDataDAO; import org.dspace.services.ConfigurationService; +import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -33,6 +36,19 @@ public class RegistrationRestControllerIT extends AbstractControllerIntegrationT @Autowired private ConfigurationService configurationService; + + @Before + public void setup() throws SQLException { + CollectionUtils.emptyIfNull(registrationDataDAO.findAll(context, RegistrationData.class)).stream() + .forEach(registrationData -> { + try { + registrationDataDAO.delete(context, registrationData); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + } + @Test public void registrationFlowTest() throws Exception { List registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); @@ -44,7 +60,7 @@ public class RegistrationRestControllerIT extends AbstractControllerIntegrationT getClient().perform(post("/api/eperson/registrations") .content(mapper.writeValueAsBytes(registrationRest)) .contentType(contentType)) - .andExpect(status().isCreated()); + .andExpect(status().isCreated()); registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); assertEquals(1, registrationDataList.size()); assertTrue(StringUtils.equalsIgnoreCase(registrationDataList.get(0).getEmail(), eperson.getEmail())); From 80f7bad4944924b6644d02cfeb9e99e4529cce5b Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Thu, 11 Jun 2020 10:23:50 +0200 Subject: [PATCH 056/143] [Task 71348] fixed the patch replace password for eperson with a token to now be available to anonymous calls --- .../EPersonPasswordReplaceOperation.java | 1 + .../EPersonRestPermissionEvaluatorPlugin.java | 14 ++++++++++- .../app/rest/EPersonRestRepositoryIT.java | 23 ++++++++----------- ...rsonRestPermissionEvaluatorPluginTest.java | 19 ++++++++++++--- 4 files changed, 39 insertions(+), 18 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/EPersonPasswordReplaceOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/EPersonPasswordReplaceOperation.java index 1a3201abdd..5a30f26fc1 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/EPersonPasswordReplaceOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/EPersonPasswordReplaceOperation.java @@ -84,6 +84,7 @@ public class EPersonPasswordReplaceOperation extends PatchOperation { throw new AccessDeniedException("The token in the parameter belongs to a different EPerson" + " than the uri indicates"); } + context.setCurrentUser(ePersonFromToken); accountService.deleteToken(context, token); } catch (SQLException | AuthorizeException e) { log.error("Failed to verify or delete the token for an EPerson patch", e); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestPermissionEvaluatorPlugin.java index 92b0e433fb..6d670772ac 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestPermissionEvaluatorPlugin.java @@ -10,7 +10,9 @@ package org.dspace.app.rest.security; import java.io.Serializable; import java.util.List; import java.util.UUID; +import javax.servlet.http.HttpServletRequest; +import org.apache.commons.lang3.StringUtils; import org.dspace.app.rest.model.patch.Operation; import org.dspace.app.rest.model.patch.Patch; import org.dspace.app.rest.repository.patch.operation.DSpaceObjectMetadataPatchUtils; @@ -82,6 +84,17 @@ public class EPersonRestPermissionEvaluatorPlugin extends RestObjectPermissionEv public boolean hasPatchPermission(Authentication authentication, Serializable targetId, String targetType, Patch patch) { + List operations = patch.getOperations(); + // If it's a password replace action, we can allow anon through provided that there's a token present + Request currentRequest = requestService.getCurrentRequest(); + if (currentRequest != null) { + HttpServletRequest httpServletRequest = currentRequest.getHttpServletRequest(); + if (operations.size() > 0 && StringUtils.equalsIgnoreCase(operations.get(0).getOp(), "replace") + && StringUtils.equalsIgnoreCase(operations.get(0).getPath(), "/password") + && StringUtils.isNotBlank(httpServletRequest.getParameter("token"))) { + return true; + } + } /** * First verify that the user has write permission on the eperson. */ @@ -89,7 +102,6 @@ public class EPersonRestPermissionEvaluatorPlugin extends RestObjectPermissionEv return false; } - List operations = patch.getOperations(); /** * The entire Patch request should be denied if it contains operations that are diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java index fbd0f071ec..ec2167915b 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java @@ -1865,10 +1865,9 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { String patchBody = getPatchContent(ops); accountService.sendRegistrationInfo(context, ePerson.getEmail()); String tokenForEPerson = registrationDataService.findByEmail(context, ePerson.getEmail()).getToken(); - String token = getAuthToken(admin.getEmail(), password); PasswordHash oldPassword = ePersonService.getPasswordHash(ePerson); // updates password - getClient(token).perform(patch("/api/eperson/epersons/" + ePerson.getID()) + getClient().perform(patch("/api/eperson/epersons/" + ePerson.getID()) .content(patchBody) .contentType(MediaType.APPLICATION_JSON_PATCH_JSON) .param("token", tokenForEPerson)) @@ -1902,14 +1901,13 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { String patchBody = getPatchContent(ops); accountService.sendRegistrationInfo(context, ePerson.getEmail()); String tokenForEPerson = registrationDataService.findByEmail(context, ePerson.getEmail()).getToken(); - String token = getAuthToken(eperson.getEmail(), password); PasswordHash oldPassword = ePersonService.getPasswordHash(ePerson); // updates password - getClient(token).perform(patch("/api/eperson/epersons/" + ePerson.getID()) + getClient().perform(patch("/api/eperson/epersons/" + ePerson.getID()) .content(patchBody) .contentType(MediaType.APPLICATION_JSON_PATCH_JSON) .param("token", "RandomToken")) - .andExpect(status().isForbidden()); + .andExpect(status().isUnauthorized()); PasswordHash newPasswordHash = ePersonService.getPasswordHash(ePerson); assertEquals(oldPassword.getHashString(),newPasswordHash.getHashString()); @@ -1951,14 +1949,13 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { String tokenForEPerson = registrationDataService.findByEmail(context, ePerson.getEmail()).getToken(); String tokenForEPersonTwo = registrationDataService.findByEmail(context, ePersonTwo.getEmail()).getToken(); - String token = getAuthToken(eperson.getEmail(), password); PasswordHash oldPassword = ePersonService.getPasswordHash(ePerson); // updates password - getClient(token).perform(patch("/api/eperson/epersons/" + ePerson.getID()) + getClient().perform(patch("/api/eperson/epersons/" + ePerson.getID()) .content(patchBody) .contentType(MediaType.APPLICATION_JSON_PATCH_JSON) .param("token", tokenForEPersonTwo)) - .andExpect(status().isForbidden()); + .andExpect(status().isUnauthorized()); PasswordHash newPasswordHash = ePersonService.getPasswordHash(ePerson); assertEquals(oldPassword.getHashString(),newPasswordHash.getHashString()); @@ -1991,14 +1988,13 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { String patchBody = getPatchContent(ops); accountService.sendRegistrationInfo(context, ePerson.getEmail()); String tokenForEPerson = registrationDataService.findByEmail(context, ePerson.getEmail()).getToken(); - String token = getAuthToken(eperson.getEmail(), password); PasswordHash oldPassword = ePersonService.getPasswordHash(ePerson); // updates password - getClient(token).perform(patch("/api/eperson/epersons/" + ePerson.getID()) + getClient().perform(patch("/api/eperson/epersons/" + ePerson.getID()) .content(patchBody) .contentType(MediaType.APPLICATION_JSON_PATCH_JSON) .param("token", tokenForEPerson)) - .andExpect(status().isForbidden()); + .andExpect(status().isUnauthorized()); PasswordHash newPasswordHash = ePersonService.getPasswordHash(ePerson); assertEquals(oldPassword.getHashString(),newPasswordHash.getHashString()); @@ -2041,14 +2037,13 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { String patchBody = getPatchContent(ops); accountService.sendRegistrationInfo(context, ePerson.getEmail()); String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); - String token = getAuthToken(eperson.getEmail(), password); PasswordHash oldPassword = ePersonService.getPasswordHash(ePerson); // updates password - getClient(token).perform(patch("/api/eperson/epersons/" + ePerson.getID()) + getClient().perform(patch("/api/eperson/epersons/" + ePerson.getID()) .content(patchBody) .contentType(MediaType.APPLICATION_JSON_PATCH_JSON) .param("token", newRegisterToken)) - .andExpect(status().isForbidden()); + .andExpect(status().isUnauthorized()); PasswordHash newPasswordHash = ePersonService.getPasswordHash(ePerson); assertTrue(StringUtils.equalsIgnoreCase(oldPassword.getHashString(),newPasswordHash.getHashString())); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/EPersonRestPermissionEvaluatorPluginTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/EPersonRestPermissionEvaluatorPluginTest.java index 21fcd60b38..9ff264a1c7 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/EPersonRestPermissionEvaluatorPluginTest.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/EPersonRestPermissionEvaluatorPluginTest.java @@ -19,27 +19,40 @@ import java.util.List; import org.dspace.app.rest.model.patch.Operation; import org.dspace.app.rest.model.patch.Patch; import org.dspace.app.rest.model.patch.ReplaceOperation; +import org.dspace.services.RequestService; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; import org.springframework.security.core.Authentication; +import org.springframework.test.util.ReflectionTestUtils; /** * This class verifies that {@link EPersonRestPermissionEvaluatorPlugin} properly * evaluates Patch requests. */ +@RunWith(MockitoJUnitRunner.class) public class EPersonRestPermissionEvaluatorPluginTest { + @InjectMocks private EPersonRestPermissionEvaluatorPlugin ePersonRestPermissionEvaluatorPlugin; private Authentication authentication; + @Mock + private RequestService requestService; + @Before public void setUp() throws Exception { ePersonRestPermissionEvaluatorPlugin = spy(EPersonRestPermissionEvaluatorPlugin.class); authentication = mock(Authentication.class); DSpaceRestPermission restPermission = DSpaceRestPermission.convert("WRITE"); when(ePersonRestPermissionEvaluatorPlugin - .hasDSpacePermission(authentication, null, null, restPermission)).thenReturn(true); + .hasDSpacePermission(authentication, null, null, restPermission)).thenReturn(true); + ReflectionTestUtils.setField(ePersonRestPermissionEvaluatorPlugin, "requestService", requestService); + when(requestService.getCurrentRequest()).thenReturn(null); } @Test @@ -52,7 +65,7 @@ public class EPersonRestPermissionEvaluatorPluginTest { ops.add(canLoginOperation); Patch patch = new Patch(ops); assertFalse(ePersonRestPermissionEvaluatorPlugin - .hasPatchPermission(authentication, null, null, patch)); + .hasPatchPermission(authentication, null, null, patch)); } @@ -64,7 +77,7 @@ public class EPersonRestPermissionEvaluatorPluginTest { ops.add(passwordOperation); Patch patch = new Patch(ops); assertTrue(ePersonRestPermissionEvaluatorPlugin - .hasPatchPermission(authentication, null, null, patch)); + .hasPatchPermission(authentication, null, null, patch)); } From aa15d658178601ed121f527395edc7b22a9bda07 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Thu, 11 Jun 2020 10:47:37 +0200 Subject: [PATCH 057/143] Implement community feedbacks --- .../WorkspaceItemRestRepository.java | 3 +- .../rest/WorkspaceItemRestRepositoryIT.java | 55 ++++++++++++++++++- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkspaceItemRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkspaceItemRestRepository.java index 0b52174050..a8b514ba0c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkspaceItemRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkspaceItemRestRepository.java @@ -331,8 +331,7 @@ public class WorkspaceItemRestRepository extends DSpaceRestRepository operations = new ArrayList(); + operations.add(new RemoveOperation("/sections/traditionalpageone/dc.not.existing/0")); + + String patchBody = getPatchContent(operations); + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()); + } } From 3aba1b7545546a068f311adf7f71695699df84d9 Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Thu, 11 Jun 2020 15:08:03 +0200 Subject: [PATCH 058/143] Added a way to ignore write only properties in REST objects for tests and rewrote EPersonRestRepositoryIT tests to use this --- .../app/rest/EPersonRestRepositoryIT.java | 216 +++++++++++++----- .../jackson/IgnoreJacksonWriteOnlyAccess.java | 24 ++ 2 files changed, 180 insertions(+), 60 deletions(-) create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/jackson/IgnoreJacksonWriteOnlyAccess.java diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java index ec2167915b..ebbb26ad7a 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java @@ -43,6 +43,7 @@ import org.dspace.app.rest.builder.CommunityBuilder; import org.dspace.app.rest.builder.EPersonBuilder; import org.dspace.app.rest.builder.GroupBuilder; import org.dspace.app.rest.builder.ItemBuilder; +import org.dspace.app.rest.jackson.IgnoreJacksonWriteOnlyAccess; import org.dspace.app.rest.matcher.EPersonMatcher; import org.dspace.app.rest.matcher.GroupMatcher; import org.dspace.app.rest.matcher.HalMatcher; @@ -2127,18 +2128,25 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isCreated()); String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); - // We need to create this json manually to support actually setting the password to a value. - // When using the mapper to write an EPersonRest object to JSON to pass it along, the password gets lost - String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]," + - "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"email\":\"" + newRegisterEmail + - "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; - + EPersonRest ePersonRest = new EPersonRest(); + MetadataRest metadataRest = new MetadataRest(); + ePersonRest.setEmail(newRegisterEmail); + ePersonRest.setCanLogIn(true); + MetadataValueRest surname = new MetadataValueRest(); + surname.setValue("Doe"); + metadataRest.put("eperson.lastname", surname); + MetadataValueRest firstname = new MetadataValueRest(); + firstname.setValue("John"); + metadataRest.put("eperson.firstname", firstname); + ePersonRest.setMetadata(metadataRest); + ePersonRest.setPassword("somePassword"); AtomicReference idRef = new AtomicReference(); + mapper.setAnnotationIntrospector(new IgnoreJacksonWriteOnlyAccess()); try { getClient().perform(post("/api/eperson/epersons") .param("token", newRegisterToken) - .content(json) + .content(mapper.writeValueAsBytes(ePersonRest)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isCreated()) .andExpect(jsonPath("$", Matchers.allOf( @@ -2182,18 +2190,28 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isCreated()); String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); - // We need to create this json manually to support actually setting the password to a value. - // When using the mapper to write an EPersonRest object to JSON to pass it along, the password gets lost - String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]," + - "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"selfRegistered\":true,\"email\":\"" + newRegisterEmail + - "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; - + EPersonRest ePersonRest = new EPersonRest(); + MetadataRest metadataRest = new MetadataRest(); + ePersonRest.setEmail(newRegisterEmail); + ePersonRest.setCanLogIn(true); + MetadataValueRest surname = new MetadataValueRest(); + surname.setValue("Doe"); + metadataRest.put("eperson.lastname", surname); + MetadataValueRest firstname = new MetadataValueRest(); + firstname.setValue("John"); + metadataRest.put("eperson.firstname", firstname); + ePersonRest.setMetadata(metadataRest); + ePersonRest.setPassword("somePassword"); + ePersonRest.setSelfRegistered(true); AtomicReference idRef = new AtomicReference(); + mapper.setAnnotationIntrospector(new IgnoreJacksonWriteOnlyAccess()); + + try { getClient().perform(post("/api/eperson/epersons") .param("token", newRegisterToken) - .content(json) + .content(mapper.writeValueAsBytes(ePersonRest)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isCreated()) .andExpect(jsonPath("$", Matchers.allOf( @@ -2250,15 +2268,25 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isCreated()); String newRegisterTokenTwo = registrationDataService.findByEmail(context, newRegisterEmailTwo).getToken(); - // We need to create this json manually to support actually setting the password to a value. - // When using the mapper to write an EPersonRest object to JSON to pass it along, the password gets lost - String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]," + - "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"email\":\"" + newRegisterEmailTwo + - "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; + + EPersonRest ePersonRest = new EPersonRest(); + MetadataRest metadataRest = new MetadataRest(); + ePersonRest.setEmail(newRegisterEmailTwo); + ePersonRest.setCanLogIn(true); + MetadataValueRest surname = new MetadataValueRest(); + surname.setValue("Doe"); + metadataRest.put("eperson.lastname", surname); + MetadataValueRest firstname = new MetadataValueRest(); + firstname.setValue("John"); + metadataRest.put("eperson.firstname", firstname); + ePersonRest.setMetadata(metadataRest); + ePersonRest.setPassword("somePassword"); + + mapper.setAnnotationIntrospector(new IgnoreJacksonWriteOnlyAccess()); getClient().perform(post("/api/eperson/epersons") .param("token", newRegisterToken) - .content(json) + .content(mapper.writeValueAsBytes(ePersonRest)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isBadRequest()); @@ -2287,15 +2315,25 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isCreated()); String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); - // We need to create this json manually to support actually setting the password to a value. - // When using the mapper to write an EPersonRest object to JSON to pass it along, the password gets lost - String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]," + - "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"email\":\"" + newRegisterEmail + - "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; + + EPersonRest ePersonRest = new EPersonRest(); + MetadataRest metadataRest = new MetadataRest(); + ePersonRest.setEmail(newRegisterEmail); + ePersonRest.setCanLogIn(true); + MetadataValueRest surname = new MetadataValueRest(); + surname.setValue("Doe"); + metadataRest.put("eperson.lastname", surname); + MetadataValueRest firstname = new MetadataValueRest(); + firstname.setValue("John"); + metadataRest.put("eperson.firstname", firstname); + ePersonRest.setMetadata(metadataRest); + ePersonRest.setPassword("somePassword"); + + mapper.setAnnotationIntrospector(new IgnoreJacksonWriteOnlyAccess()); getClient().perform(post("/api/eperson/epersons") .param("token", "randomToken") - .content(json) + .content(mapper.writeValueAsBytes(ePersonRest)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isBadRequest()); @@ -2322,15 +2360,26 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isCreated()); String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); - // We need to create this json manually to support actually setting the password to a value. - // When using the mapper to write an EPersonRest object to JSON to pass it along, the password gets lost - String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]," + - "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"selfRegistered\":false,\"email\":\"" + newRegisterEmail + - "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; + + EPersonRest ePersonRest = new EPersonRest(); + MetadataRest metadataRest = new MetadataRest(); + ePersonRest.setEmail(newRegisterEmail); + ePersonRest.setCanLogIn(true); + MetadataValueRest surname = new MetadataValueRest(); + surname.setValue("Doe"); + metadataRest.put("eperson.lastname", surname); + MetadataValueRest firstname = new MetadataValueRest(); + firstname.setValue("John"); + metadataRest.put("eperson.firstname", firstname); + ePersonRest.setMetadata(metadataRest); + ePersonRest.setPassword("somePassword"); + ePersonRest.setSelfRegistered(false); + + mapper.setAnnotationIntrospector(new IgnoreJacksonWriteOnlyAccess()); getClient().perform(post("/api/eperson/epersons") .param("token", newRegisterToken) - .content(json) + .content(mapper.writeValueAsBytes(ePersonRest)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isBadRequest()); @@ -2357,14 +2406,23 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isCreated()); String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); - // We need to create this json manually to support actually setting the password to a value. - // When using the mapper to write an EPersonRest object to JSON to pass it along, the password gets lost - String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]},\"selfRegistered\":true," + - "\"email\":\"" + newRegisterEmail + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; + + EPersonRest ePersonRest = new EPersonRest(); + MetadataRest metadataRest = new MetadataRest(); + ePersonRest.setEmail(newRegisterEmail); + ePersonRest.setCanLogIn(true); + MetadataValueRest firstname = new MetadataValueRest(); + firstname.setValue("John"); + metadataRest.put("eperson.firstname", firstname); + ePersonRest.setMetadata(metadataRest); + ePersonRest.setPassword("somePassword"); + ePersonRest.setSelfRegistered(true); + + mapper.setAnnotationIntrospector(new IgnoreJacksonWriteOnlyAccess()); getClient().perform(post("/api/eperson/epersons") .param("token", newRegisterToken) - .content(json) + .content(mapper.writeValueAsBytes(ePersonRest)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isBadRequest()); @@ -2391,14 +2449,23 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isCreated()); String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); - // We need to create this json manually to support actually setting the password to a value. - // When using the mapper to write an EPersonRest object to JSON to pass it along, the password gets lost - String json = "{\"metadata\":{\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"selfRegistered\":true," + - "\"email\":\"" + newRegisterEmail + "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; + + EPersonRest ePersonRest = new EPersonRest(); + MetadataRest metadataRest = new MetadataRest(); + ePersonRest.setEmail(newRegisterEmail); + ePersonRest.setCanLogIn(true); + MetadataValueRest surname = new MetadataValueRest(); + surname.setValue("Doe"); + metadataRest.put("eperson.lastname", surname); + ePersonRest.setMetadata(metadataRest); + ePersonRest.setPassword("somePassword"); + ePersonRest.setSelfRegistered(true); + + mapper.setAnnotationIntrospector(new IgnoreJacksonWriteOnlyAccess()); getClient().perform(post("/api/eperson/epersons") .param("token", newRegisterToken) - .content(json) + .content(mapper.writeValueAsBytes(ePersonRest)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isBadRequest()); @@ -2425,15 +2492,24 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isCreated()); String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); - // We need to create this json manually to support actually setting the password to a value. - // When using the mapper to write an EPersonRest object to JSON to pass it along, the password gets lost - String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]," + - "\"eperson.lastname\":[{\"value\":\"Doe\"}]}," + - "\"type\":\"eperson\"}"; + + EPersonRest ePersonRest = new EPersonRest(); + MetadataRest metadataRest = new MetadataRest(); + ePersonRest.setEmail(newRegisterEmail); + ePersonRest.setCanLogIn(true); + MetadataValueRest surname = new MetadataValueRest(); + surname.setValue("Doe"); + metadataRest.put("eperson.lastname", surname); + MetadataValueRest firstname = new MetadataValueRest(); + firstname.setValue("John"); + metadataRest.put("eperson.firstname", firstname); + ePersonRest.setMetadata(metadataRest); + + mapper.setAnnotationIntrospector(new IgnoreJacksonWriteOnlyAccess()); getClient().perform(post("/api/eperson/epersons") .param("token", newRegisterToken) - .content(json) + .content(mapper.writeValueAsBytes(ePersonRest)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isBadRequest()); @@ -2461,15 +2537,25 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isCreated()); String forgotPasswordToken = registrationDataService.findByEmail(context, eperson.getEmail()).getToken(); - // We need to create this json manually to support actually setting the password to a value. - // When using the mapper to write an EPersonRest object to JSON to pass it along, the password gets lost - String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]," + - "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"selfRegistered\":true,\"password\":\"somePassword\"," + - "\"type\":\"eperson\"}"; + + EPersonRest ePersonRest = new EPersonRest(); + MetadataRest metadataRest = new MetadataRest(); + ePersonRest.setCanLogIn(true); + MetadataValueRest surname = new MetadataValueRest(); + surname.setValue("Doe"); + metadataRest.put("eperson.lastname", surname); + MetadataValueRest firstname = new MetadataValueRest(); + firstname.setValue("John"); + metadataRest.put("eperson.firstname", firstname); + ePersonRest.setMetadata(metadataRest); + ePersonRest.setPassword("somePassword"); + ePersonRest.setSelfRegistered(true); + + mapper.setAnnotationIntrospector(new IgnoreJacksonWriteOnlyAccess()); getClient().perform(post("/api/eperson/epersons") .param("token", forgotPasswordToken) - .content(json) + .content(mapper.writeValueAsBytes(ePersonRest)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isBadRequest()); @@ -2497,18 +2583,28 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isCreated()); String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); - // We need to create this json manually to support actually setting the password to a value. - // When using the mapper to write an EPersonRest object to JSON to pass it along, the password gets lost - String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]," + - "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"email\":\"" + newRegisterEmail + - "\",\"password\":\"somePassword\",\"type\":\"eperson\"}"; + + EPersonRest ePersonRest = new EPersonRest(); + MetadataRest metadataRest = new MetadataRest(); + ePersonRest.setEmail(newRegisterEmail); + ePersonRest.setCanLogIn(true); + MetadataValueRest surname = new MetadataValueRest(); + surname.setValue("Doe"); + metadataRest.put("eperson.lastname", surname); + MetadataValueRest firstname = new MetadataValueRest(); + firstname.setValue("John"); + metadataRest.put("eperson.firstname", firstname); + ePersonRest.setMetadata(metadataRest); + ePersonRest.setPassword("somePassword"); + + mapper.setAnnotationIntrospector(new IgnoreJacksonWriteOnlyAccess()); AtomicReference idRef = new AtomicReference(); try { getClient().perform(post("/api/eperson/epersons") .param("token", newRegisterToken) - .content(json) + .content(mapper.writeValueAsBytes(ePersonRest)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isCreated()) .andExpect(jsonPath("$", Matchers.allOf( diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/jackson/IgnoreJacksonWriteOnlyAccess.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/jackson/IgnoreJacksonWriteOnlyAccess.java new file mode 100644 index 0000000000..577ed6e4d5 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/jackson/IgnoreJacksonWriteOnlyAccess.java @@ -0,0 +1,24 @@ +/** + * 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.jackson; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; + +public class IgnoreJacksonWriteOnlyAccess extends JacksonAnnotationIntrospector { + + @Override + public JsonProperty.Access findPropertyAccess(Annotated m) { + JsonProperty.Access access = super.findPropertyAccess(m); + if (access == JsonProperty.Access.WRITE_ONLY) { + return JsonProperty.Access.AUTO; + } + return access; + } +} From 4ff01ae963a4bb765b3464600c005b4aa51d15c0 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Fri, 12 Jun 2020 01:07:08 +0200 Subject: [PATCH 059/143] added test --- .../rest/WorkspaceItemRestRepositoryIT.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java index b17c76f2f3..c4a64c6de1 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java @@ -3765,4 +3765,42 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) .andExpect(status().isUnprocessableEntity()); } + + @Test + public void patchDeleteMetadataWrongSectionTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1) + .withName("Collection 1") + .build(); + WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, col1) + .withTitle("Test title") + .withIssueDate("2019-04-25") + .withSubject("ExtraEntry") + .build(); + //disable file upload mandatory + configurationService.setProperty("webui.submit.upload.required", false); + context.restoreAuthSystemState(); + + String authToken = getAuthToken(eperson.getEmail(), password); + List operations = new ArrayList(); + operations.add(new RemoveOperation("/sections/traditionalpagetwo/dc.title/0")); + String patchBody = getPatchContent(operations); + + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()); + + getClient(authToken).perform(get("/api/submission/workspaceitems/" + witem.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", + Matchers.is(WorkspaceItemMatcher.matchItemWithTitleAndDateIssuedAndSubject(witem, + "Test title", "2019-04-25", "ExtraEntry")))); + } } From 48a86399023fa6c1afa4f5382346ec6eccf99b9a Mon Sep 17 00:00:00 2001 From: Peter Nijs Date: Fri, 12 Jun 2020 13:56:38 +0200 Subject: [PATCH 060/143] 71342: Authorization for Downloads of restricted Bitstreams #1 --- .../rest/AuthenticationRestController.java | 27 ++++++++ .../AuthenticationTokenConverter.java | 30 +++++++++ .../AuthenticationTokenHalLinkFactory.java | 42 +++++++++++++ .../rest/model/AuthenticationTokenRest.java | 44 +++++++++++++ .../hateoas/AuthenticationTokenResource.java | 20 ++++++ .../rest/security/CustomLogoutHandler.java | 4 +- .../security/RestAuthenticationService.java | 2 + .../rest/security/jwt/JWTTokenHandler.java | 61 ++++++++++++++++--- ...JWTTokenRestAuthenticationServiceImpl.java | 34 +++++++++-- .../security/jwt/SessionJWTTokenHandler.java | 47 ++++++++++++++ .../jwt/ShortLivedJWTTokenHandler.java | 47 ++++++++++++++ .../rest/AuthenticationRestControllerIT.java | 17 ++++++ .../security/jwt/JWTTokenHandlerTest.java | 28 ++++----- dspace/config/modules/authentication.cfg | 44 ++++++++++--- 14 files changed, 410 insertions(+), 37 deletions(-) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/AuthenticationTokenConverter.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/link/AuthenticationTokenHalLinkFactory.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthenticationTokenRest.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/AuthenticationTokenResource.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/SessionJWTTokenHandler.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java index 68f9085e21..f1f8d495c3 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java @@ -16,9 +16,11 @@ import org.dspace.app.rest.converter.ConverterService; import org.dspace.app.rest.converter.EPersonConverter; import org.dspace.app.rest.link.HalLinkService; import org.dspace.app.rest.model.AuthenticationStatusRest; +import org.dspace.app.rest.model.AuthenticationTokenRest; import org.dspace.app.rest.model.AuthnRest; import org.dspace.app.rest.model.EPersonRest; import org.dspace.app.rest.model.hateoas.AuthenticationStatusResource; +import org.dspace.app.rest.model.hateoas.AuthenticationTokenResource; import org.dspace.app.rest.model.hateoas.AuthnResource; import org.dspace.app.rest.projection.Projection; import org.dspace.app.rest.security.RestAuthenticationService; @@ -32,6 +34,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.hateoas.Link; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; @@ -118,6 +121,30 @@ public class AuthenticationRestController implements InitializingBean { "valid."); } + /** + * This method will generate a short lived token to be used for bitstream downloads. + * + * curl -v -X POST https://{dspace-server.url}/api/authn/shortlivedtokens -H "Authorization: Bearer eyJhbG...COdbo" + * + * Example: + *
+     * {@code
+     * curl -v -X POST https://{dspace-server.url}/api/authn/shortlivedtokens -H "Authorization: Bearer eyJhbG...COdbo"
+     * }
+     * 
+ * @param request The StandardMultipartHttpServletRequest + * @return The created short lived token + */ + @PreAuthorize("hasAuthority('AUTHENTICATED')") + @RequestMapping(value = "/shortlivedtokens", method = RequestMethod.POST) + public AuthenticationTokenResource shortLivedLogin(HttpServletRequest request) { + Projection projection = utils.obtainProjection(); + String shortLivedToken = + restAuthenticationService.getShortLivedAuthenticationToken(ContextUtil.obtainContext(request), request); + AuthenticationTokenRest authenticationTokenRest = converter.toRest(shortLivedToken, projection); + return converter.toResource(authenticationTokenRest); + } + @RequestMapping(value = "/login", method = { RequestMethod.GET, RequestMethod.PUT, RequestMethod.PATCH, RequestMethod.DELETE }) public ResponseEntity login() { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/AuthenticationTokenConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/AuthenticationTokenConverter.java new file mode 100644 index 0000000000..a9f9d7208b --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/AuthenticationTokenConverter.java @@ -0,0 +1,30 @@ +/** + * 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.rest.model.AuthenticationTokenRest; +import org.dspace.app.rest.projection.Projection; +import org.springframework.stereotype.Component; + +/** + * This is the converter from the AuthenticationToken string to tge REST data model + */ +@Component +public class AuthenticationTokenConverter implements DSpaceConverter { + @Override + public AuthenticationTokenRest convert(String modelObject, Projection projection) { + AuthenticationTokenRest token = new AuthenticationTokenRest(); + token.setToken(modelObject); + return token; + } + + @Override + public Class getModelClass() { + return String.class; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/AuthenticationTokenHalLinkFactory.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/AuthenticationTokenHalLinkFactory.java new file mode 100644 index 0000000000..180a3b7e47 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/AuthenticationTokenHalLinkFactory.java @@ -0,0 +1,42 @@ +/** + * 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.link; + +import java.util.LinkedList; + +import org.dspace.app.rest.AuthenticationRestController; +import org.dspace.app.rest.model.hateoas.AuthenticationTokenResource; +import org.springframework.data.domain.Pageable; +import org.springframework.hateoas.IanaLinkRelations; +import org.springframework.hateoas.Link; +import org.springframework.stereotype.Component; + +/** + * This class adds the self link to the AuthenticationTokenResource. + */ +@Component +public class AuthenticationTokenHalLinkFactory + extends HalLinkFactory { + + @Override + protected void addLinks(AuthenticationTokenResource halResource, Pageable pageable, LinkedList list) + throws Exception { + + list.add(buildLink(IanaLinkRelations.SELF.value(), getMethodOn().shortLivedLogin(null))); + } + + @Override + protected Class getControllerClass() { + return AuthenticationRestController.class; + } + + @Override + protected Class getResourceClass() { + return AuthenticationTokenResource.class; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthenticationTokenRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthenticationTokenRest.java new file mode 100644 index 0000000000..130b5cc162 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthenticationTokenRest.java @@ -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.model; + +import org.dspace.app.rest.RestResourceController; + +/** + * The authentication token REST HAL Resource. The HAL Resource wraps the REST Resource + * adding support for the links and embedded resources + */ +public class AuthenticationTokenRest extends RestAddressableModel { + public static final String NAME = "shortlivedtoken"; + public static final String CATEGORY = "authn"; + + private String token; + + @Override + public String getCategory() { + return CATEGORY; + } + + @Override + public Class getController() { + return RestResourceController.class; + } + + @Override + public String getType() { + return NAME; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/AuthenticationTokenResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/AuthenticationTokenResource.java new file mode 100644 index 0000000000..e46831b2f7 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/AuthenticationTokenResource.java @@ -0,0 +1,20 @@ +/** + * 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.AuthenticationTokenRest; + +/** + * Token resource, wraps the AuthenticationToken object + */ +public class AuthenticationTokenResource extends HALResource { + + public AuthenticationTokenResource(AuthenticationTokenRest content) { + super(content); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/CustomLogoutHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/CustomLogoutHandler.java index 204eda62dc..19b3e2f4f0 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/CustomLogoutHandler.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/CustomLogoutHandler.java @@ -10,7 +10,7 @@ package org.dspace.app.rest.security; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.dspace.app.rest.security.jwt.JWTTokenHandler; +import org.dspace.app.rest.security.jwt.SessionJWTTokenHandler; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.core.Context; import org.slf4j.Logger; @@ -29,7 +29,7 @@ import org.springframework.stereotype.Component; @Component public class CustomLogoutHandler implements LogoutHandler { - private static final Logger log = LoggerFactory.getLogger(JWTTokenHandler.class); + private static final Logger log = LoggerFactory.getLogger(SessionJWTTokenHandler.class); @Autowired private RestAuthenticationService restAuthenticationService; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/RestAuthenticationService.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/RestAuthenticationService.java index b1a47336ba..622070e77c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/RestAuthenticationService.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/RestAuthenticationService.java @@ -28,6 +28,8 @@ public interface RestAuthenticationService { void addAuthenticationDataForUser(HttpServletRequest request, HttpServletResponse response, DSpaceAuthentication authentication, boolean addCookie) throws IOException; + String getShortLivedAuthenticationToken(Context context, HttpServletRequest request); + EPerson getAuthenticatedEPerson(HttpServletRequest request, Context context); boolean hasAuthenticationData(HttpServletRequest request); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java index 2f55653a79..fa090df7ed 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java @@ -46,17 +46,16 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.keygen.BytesKeyGenerator; import org.springframework.security.crypto.keygen.KeyGenerators; -import org.springframework.stereotype.Component; /** * Class responsible for creating and parsing JSON Web Tokens (JWTs), supports both JWS and JWE - * https://jwt.io/ + * https://jwt.io/ . This abstract class needs to be extended with a class providing the + * configuration keys for the particular type of token. * * @author Frederic Van Reet (frederic dot vanreet at atmire dot com) * @author Tom Desair (tom dot desair at atmire dot com) */ -@Component -public class JWTTokenHandler implements InitializingBean { +public abstract class JWTTokenHandler implements InitializingBean { private static final int MAX_CLOCK_SKEW_SECONDS = 60; private static final Logger log = LoggerFactory.getLogger(JWTTokenHandler.class); @@ -86,15 +85,57 @@ public class JWTTokenHandler implements InitializingBean { @Override public void afterPropertiesSet() throws Exception { - this.jwtKey = getSecret("jwt.token.secret"); - this.encryptionKey = getSecret("jwt.encryption.secret").getBytes(); + this.jwtKey = + getSecret(getTokenSecretConfigurationKey()); + this.encryptionKey = + getSecret(getEncryptionSecretConfigurationKey()).getBytes(); - this.expirationTime = configurationService.getLongProperty("jwt.token.expiration", 30) * 60 * 1000; - this.includeIP = configurationService.getBooleanProperty("jwt.token.include.ip", true); - this.encryptionEnabled = configurationService.getBooleanProperty("jwt.encryption.enabled", false); - this.compressionEnabled = configurationService.getBooleanProperty("jwt.compression.enabled", false); + this.expirationTime = + configurationService.getLongProperty(getTokenExpirationConfigurationKey(), 30); + this.includeIP = + configurationService.getBooleanProperty(getTokenIncludeIPConfigurationKey(), true); + this.encryptionEnabled = + configurationService.getBooleanProperty(getEncryptionEnabledConfigurationKey(), false); + this.compressionEnabled = + configurationService.getBooleanProperty(getCompressionEnabledConfigurationKey(), false); } + /** + * Get the configuration property key for the token secret. + * @return the configuration property key + */ + protected abstract String getTokenSecretConfigurationKey(); + + /** + * Get the configuration property key for the encryption secret. + * @return the configuration property key + */ + protected abstract String getEncryptionSecretConfigurationKey(); + + /** + * Get the configuration property key for the expiration time. + * @return the configuration property key + */ + protected abstract String getTokenExpirationConfigurationKey(); + + /** + * Get the configuration property key for the include ip. + * @return the configuration property key + */ + protected abstract String getTokenIncludeIPConfigurationKey(); + + /** + * Get the configuration property key for the encryption enable setting. + * @return the configuration property key + */ + protected abstract String getEncryptionEnabledConfigurationKey(); + + /** + * Get the configuration property key for the compression enable setting. + * @return the configuration property key + */ + protected abstract String getCompressionEnabledConfigurationKey(); + /** * Retrieve EPerson from a JSON Web Token (JWT) * diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java index 3dbab09174..e480154781 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java @@ -49,7 +49,10 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication private static final String AUTHORIZATION_TYPE = "Bearer"; @Autowired - private JWTTokenHandler jwtTokenHandler; + private SessionJWTTokenHandler sessionJWTTokenHandler; + + @Autowired + private ShortLivedJWTTokenHandler shortLivedJWTTokenHandler; @Autowired private EPersonService ePersonService; @@ -71,7 +74,7 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication List groups = authenticationService.getSpecialGroups(context, request); - String token = jwtTokenHandler.createTokenForEPerson(context, request, + String token = sessionJWTTokenHandler.createTokenForEPerson(context, request, authentication.getPreviousLoginDate(), groups); addTokenToResponse(response, token, addCookie); @@ -84,11 +87,34 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication } } + /** + * Create a short-lived token for bitstream downloads + * @param context The context for which to create the token + * @param request The request for which to create the token + * @return The token with a short lifespan + */ + @Override + public String getShortLivedAuthenticationToken(Context context, HttpServletRequest request) { + String token = null; + try { + List groups = authenticationService.getSpecialGroups(context, request); + token = shortLivedJWTTokenHandler.createTokenForEPerson(context, request, null, groups); + context.commit(); + return token; + } catch (JOSEException e) { + log.error("JOSE Exception", e); + } catch (SQLException e) { + log.error("SQL error when adding authentication", e); + } + + return token; + } + @Override public EPerson getAuthenticatedEPerson(HttpServletRequest request, Context context) { String token = getToken(request); try { - EPerson ePerson = jwtTokenHandler.parseEPersonFromToken(token, request, context); + EPerson ePerson = sessionJWTTokenHandler.parseEPersonFromToken(token, request, context); return ePerson; } catch (JOSEException e) { log.error("Jose error", e); @@ -111,7 +137,7 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication Context context) throws Exception { String token = getToken(request); invalidateAuthenticationCookie(response); - jwtTokenHandler.invalidateToken(token, request, context); + sessionJWTTokenHandler.invalidateToken(token, request, context); } @Override diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/SessionJWTTokenHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/SessionJWTTokenHandler.java new file mode 100644 index 0000000000..4d70c4b10d --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/SessionJWTTokenHandler.java @@ -0,0 +1,47 @@ +/** + * 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.jwt; + +import org.springframework.stereotype.Component; + +/** + * Class responsible for creating and parsing JSON Web Tokens (JWTs), supports both JWS and JWE + * https://jwt.io/ + */ +@Component +public class SessionJWTTokenHandler extends JWTTokenHandler { + @Override + protected String getTokenSecretConfigurationKey() { + return "jwt.session.token.secret"; + } + + @Override + protected String getEncryptionSecretConfigurationKey() { + return "jwt.session.encryption.secret"; + } + + @Override + protected String getTokenExpirationConfigurationKey() { + return "jwt.session.token.expiration"; + } + + @Override + protected String getTokenIncludeIPConfigurationKey() { + return "jwt.session.token.include.ip"; + } + + @Override + protected String getEncryptionEnabledConfigurationKey() { + return "jwt.session.encryption.enabled"; + } + + @Override + protected String getCompressionEnabledConfigurationKey() { + return "jwt.session.compression.enabled"; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java new file mode 100644 index 0000000000..6610d6f50a --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java @@ -0,0 +1,47 @@ +/** + * 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.jwt; + +import org.springframework.stereotype.Component; + +/** + * Class responsible for creating and parsing JSON Web Tokens (JWTs) used for bitstream + * dowloads, supports both JWS and JWE https://jwt.io/ . + */ +@Component +public class ShortLivedJWTTokenHandler extends JWTTokenHandler { + @Override + protected String getTokenSecretConfigurationKey() { + return "jwt.shortLived.token.secret"; + } + + @Override + protected String getEncryptionSecretConfigurationKey() { + return "jwt.shortLived.encryption.secret"; + } + + @Override + protected String getTokenExpirationConfigurationKey() { + return "jwt.shortLived.token.expiration"; + } + + @Override + protected String getTokenIncludeIPConfigurationKey() { + return "jwt.shortLived.token.include.ip"; + } + + @Override + protected String getEncryptionEnabledConfigurationKey() { + return "jwt.shortLived.encryption.enabled"; + } + + @Override + protected String getCompressionEnabledConfigurationKey() { + return "jwt.shortLived.compression.enabled"; + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java index 5a65447858..41d397f61b 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java @@ -11,6 +11,7 @@ import static java.lang.Thread.sleep; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.startsWith; import static org.junit.Assert.assertNotEquals; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -30,6 +31,7 @@ import org.dspace.app.rest.matcher.HalMatcher; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.eperson.Group; import org.dspace.services.ConfigurationService; +import org.hamcrest.Matchers; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; @@ -757,4 +759,19 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio .andExpect(status().isUnauthorized()); } + + @Test + public void testShortLivedToken() throws Exception { + String token = getAuthToken(eperson.getEmail(), password); + getClient(token).perform(post("/api/authn/shortlivedtokens")) + .andExpect(jsonPath("$.token", notNullValue())) + .andExpect(jsonPath("$.type", is("shortlivedtoken"))) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/authn/shortlivedtokens"))); + } + + @Test + public void testShortLivedTokenNotAuthenticated() throws Exception { + getClient().perform(post("/api/authn/shortlivedtokens")) + .andExpect(status().isUnauthorized()); + } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/JWTTokenHandlerTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/JWTTokenHandlerTest.java index aeda2d0e18..94fb653e6c 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/JWTTokenHandlerTest.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/JWTTokenHandlerTest.java @@ -46,7 +46,7 @@ public class JWTTokenHandlerTest { @InjectMocks @Spy - JWTTokenHandler jwtTokenHandler; + SessionJWTTokenHandler sessionJWTTokenHandler; @Mock private Context context; @@ -87,7 +87,7 @@ public class JWTTokenHandlerTest { @Test public void testJWTNoEncryption() throws Exception { Date previous = new Date(System.currentTimeMillis() - 10000000000L); - String token = jwtTokenHandler + String token = sessionJWTTokenHandler .createTokenForEPerson(context, new MockHttpServletRequest(), previous, new ArrayList<>()); SignedJWT signedJWT = SignedJWT.parse(token); String personId = (String) signedJWT.getJWTClaimsSet().getClaim(EPersonClaimProvider.EPERSON_ID); @@ -96,11 +96,11 @@ public class JWTTokenHandlerTest { @Test(expected = ParseException.class) public void testJWTEncrypted() throws Exception { - when(jwtTokenHandler.isEncryptionEnabled()).thenReturn(true); + when(sessionJWTTokenHandler.isEncryptionEnabled()).thenReturn(true); Date previous = new Date(System.currentTimeMillis() - 10000000000L); StringKeyGenerator keyGenerator = KeyGenerators.string(); - when(jwtTokenHandler.getEncryptionKey()).thenReturn(keyGenerator.generateKey().getBytes()); - String token = jwtTokenHandler + when(sessionJWTTokenHandler.getEncryptionKey()).thenReturn(keyGenerator.generateKey().getBytes()); + String token = sessionJWTTokenHandler .createTokenForEPerson(context, new MockHttpServletRequest(), previous, new ArrayList<>()); SignedJWT signedJWT = SignedJWT.parse(token); } @@ -108,12 +108,12 @@ public class JWTTokenHandlerTest { //temporary set a negative expiration time so the token is invalid immediately @Test public void testExpiredToken() throws Exception { - when(jwtTokenHandler.getExpirationPeriod()).thenReturn(-99999999L); + when(sessionJWTTokenHandler.getExpirationPeriod()).thenReturn(-99999999L); when(ePersonClaimProvider.getEPerson(any(Context.class), any(JWTClaimsSet.class))).thenReturn(ePerson); Date previous = new Date(new Date().getTime() - 10000000000L); - String token = jwtTokenHandler + String token = sessionJWTTokenHandler .createTokenForEPerson(context, new MockHttpServletRequest(), previous, new ArrayList<>()); - EPerson parsed = jwtTokenHandler.parseEPersonFromToken(token, httpServletRequest, context); + EPerson parsed = sessionJWTTokenHandler.parseEPersonFromToken(token, httpServletRequest, context); assertEquals(null, parsed); } @@ -121,17 +121,17 @@ public class JWTTokenHandlerTest { //Try if we can change the expiration date @Test public void testTokenTampering() throws Exception { - when(jwtTokenHandler.getExpirationPeriod()).thenReturn(-99999999L); + when(sessionJWTTokenHandler.getExpirationPeriod()).thenReturn(-99999999L); when(ePersonClaimProvider.getEPerson(any(Context.class), any(JWTClaimsSet.class))).thenReturn(ePerson); Date previous = new Date(new Date().getTime() - 10000000000L); - String token = jwtTokenHandler + String token = sessionJWTTokenHandler .createTokenForEPerson(context, new MockHttpServletRequest(), previous, new ArrayList<>()); JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().claim("eid", "epersonID").expirationTime( new Date(System.currentTimeMillis() + 99999999)).build(); String tamperedPayload = new String(Base64.getUrlEncoder().encode(jwtClaimsSet.toString().getBytes())); String[] splitToken = token.split("\\."); String tamperedToken = splitToken[0] + "." + tamperedPayload + "." + splitToken[2]; - EPerson parsed = jwtTokenHandler.parseEPersonFromToken(tamperedToken, httpServletRequest, context); + EPerson parsed = sessionJWTTokenHandler.parseEPersonFromToken(tamperedToken, httpServletRequest, context); assertEquals(null, parsed); } @@ -139,12 +139,12 @@ public class JWTTokenHandlerTest { public void testInvalidatedToken() throws Exception { Date previous = new Date(System.currentTimeMillis() - 10000000000L); // create a new token - String token = jwtTokenHandler + String token = sessionJWTTokenHandler .createTokenForEPerson(context, new MockHttpServletRequest(), previous, new ArrayList<>()); // immediately invalidate it - jwtTokenHandler.invalidateToken(token, new MockHttpServletRequest(), context); + sessionJWTTokenHandler.invalidateToken(token, new MockHttpServletRequest(), context); // Check if it is still valid by trying to parse the EPerson from it (should return null) - EPerson parsed = jwtTokenHandler.parseEPersonFromToken(token, httpServletRequest, context); + EPerson parsed = sessionJWTTokenHandler.parseEPersonFromToken(token, httpServletRequest, context); assertEquals(null, parsed); } diff --git a/dspace/config/modules/authentication.cfg b/dspace/config/modules/authentication.cfg index d5d010af6d..3a5e618721 100644 --- a/dspace/config/modules/authentication.cfg +++ b/dspace/config/modules/authentication.cfg @@ -57,26 +57,56 @@ plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authen # Server key part that is a part of the key used to sign the authentication tokens. # If this property is not set or empty, DSpace will generate a random key on startup. # IF YOU ARE RUNNING DSPACE IN A CLUSTER, you need to set a value for this property here or as an environment variable -# jwt.token.secret = +# jwt.session.token.secret = # This property enables/disables encryption of the payload in a stateless token. Enabling this makes the data encrypted # and unreadable by the receiver, but makes the token larger in size. false by default -jwt.encryption.enabled = false +jwt.session.encryption.enabled = false # Encryption key to use when JWT token encryption is enabled (JWE). Note that encrypting tokens might required additional # configuration in the REST clients -# jwt.encryption.secret = +# jwt.session.encryption.secret = # This enables compression of the payload of a jwt, enabling this will make the jwt token a little smaller at the cost # of some performance, this setting WILL ONLY BE used when encrypting the jwt. -jwt.compression.enabled = true +jwt.session.compression.enabled = true -# Expiration time of a token in minutes -jwt.token.expiration = 30 +# Expiration time of a token in milliseconds +jwt.session.token.expiration = 1800000 # Restrict tokens to a specific ip-address to prevent theft/session hijacking. This is achieved by making the ip-address # a part of the JWT siging key. If this property is set to false then the ip-address won't be used as part of # the signing key of a jwt token and tokens can be shared over multiple ip-addresses. # For security reasons, this defaults to true -jwt.token.include.ip = true +jwt.session.token.include.ip = true + +#---------------------------------------------------------------# +#---Stateless JWT Authentication for downloads of bitstreams----# +#---------------------------------------------------------------# + +# Server key part that is a part of the key used to sign the authentication tokens. +# If this property is not set or empty, DSpace will generate a random key on startup. +# IF YOU ARE RUNNING DSPACE IN A CLUSTER, you need to set a value for this property here or as an environment variable +# jwt.shortLived.token.secret = + +# This property enables/disables encryption of the payload in a stateless token. Enabling this makes the data encrypted +# and unreadable by the receiver, but makes the token larger in size. false by default +jwt.shortLived.encryption.enabled = false + +# Encryption key to use when JWT token encryption is enabled (JWE). Note that encrypting tokens might required additional +# configuration in the REST clients +# jwt.shortLived.encryption.secret = + +# This enables compression of the payload of a jwt, enabling this will make the jwt token a little smaller at the cost +# of some performance, this setting WILL ONLY BE used when encrypting the jwt. +jwt.shortLived.compression.enabled = true + +# Expiration time of a token in milliseconds +jwt.shortLived.token.expiration = 2000 + +# Restrict tokens to a specific ip-address to prevent theft/session hijacking. This is achieved by making the ip-address +# a part of the JWT siging key. If this property is set to false then the ip-address won't be used as part of +# the signing key of a jwt token and tokens can be shared over multiple ip-addresses. +# For security reasons, this defaults to true +jwt.shortLived.token.include.ip = true From 908f4c761aeecf6ccde03230551bd548f98da8b3 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Thu, 16 Apr 2020 12:52:19 +0200 Subject: [PATCH 061/143] added initial implementantions for languages support --- .../AbstractDSpaceRestRepository.java | 30 ++++++++++++- .../SubmissionFormRestRepository.java | 42 ++++++++++++++----- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AbstractDSpaceRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AbstractDSpaceRestRepository.java index a64f8af5df..7f815ea8f2 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AbstractDSpaceRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AbstractDSpaceRestRepository.java @@ -7,10 +7,14 @@ */ package org.dspace.app.rest.repository; +import java.util.Locale; + +import org.apache.commons.lang3.StringUtils; import org.dspace.app.rest.converter.ConverterService; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.app.rest.utils.Utils; import org.dspace.core.Context; +import org.dspace.core.I18nUtil; import org.dspace.services.RequestService; import org.dspace.services.model.Request; import org.dspace.utils.DSpace; @@ -33,11 +37,35 @@ public abstract class AbstractDSpaceRestRepository { protected RequestService requestService = new DSpace().getRequestService(); protected Context obtainContext() { + Context context = null; Request currentRequest = requestService.getCurrentRequest(); - return ContextUtil.obtainContext(currentRequest.getServletRequest()); + context = ContextUtil.obtainContext(currentRequest.getServletRequest()); + Locale currentLocale = getLocal(context, currentRequest); + context.setCurrentLocale(currentLocale); + return context; } public RequestService getRequestService() { return requestService; } + + private Locale getLocal(Context context, Request request) { + Locale userLocale = null; + Locale supportedLocale = null; + if (context.getCurrentUser() != null) { + String userLanguage = context.getCurrentUser().getLanguage(); + if (userLanguage != null) { + userLocale = new Locale(userLanguage); + } + } + String locale = request.getHttpServletRequest().getHeader("Accept-Language"); + if (StringUtils.isNotBlank(locale)) { + userLocale = new Locale(locale); + } + if (userLocale == null) { + return I18nUtil.getDefaultLocale(); + } + supportedLocale = I18nUtil.getSupportedLocale(userLocale); + return supportedLocale; + } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionFormRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionFormRestRepository.java index 48856aa163..4552d7ddd5 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionFormRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionFormRestRepository.java @@ -7,13 +7,17 @@ */ package org.dspace.app.rest.repository; +import java.util.HashMap; import java.util.List; +import java.util.Locale; +import java.util.Map; import org.dspace.app.rest.model.SubmissionFormRest; import org.dspace.app.util.DCInputSet; import org.dspace.app.util.DCInputsReader; import org.dspace.app.util.DCInputsReaderException; import org.dspace.core.Context; +import org.dspace.core.I18nUtil; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.security.access.prepost.PreAuthorize; @@ -26,32 +30,50 @@ import org.springframework.stereotype.Component; */ @Component(SubmissionFormRest.CATEGORY + "." + SubmissionFormRest.NAME) public class SubmissionFormRestRepository extends DSpaceRestRepository { - - private DCInputsReader inputReader; + private Map inputReaders; + private DCInputsReader defaultInputReader; public SubmissionFormRestRepository() throws DCInputsReaderException { - inputReader = new DCInputsReader(); + defaultInputReader = new DCInputsReader(); + Locale[] locales = I18nUtil.getSupportedLocales(); + inputReaders = new HashMap(); + for (Locale locale : locales) { + inputReaders.put(locale, new DCInputsReader(I18nUtil.getInputFormsFileName(locale))); + } } @PreAuthorize("hasAuthority('AUTHENTICATED')") @Override - public SubmissionFormRest findOne(Context context, String submitName) { - DCInputSet inputConfig; + public SubmissionFormRest findOne(Context context, String submitName) { try { - inputConfig = inputReader.getInputsByFormName(submitName); + Locale currentLocale = context.getCurrentLocale(); + DCInputsReader inputReader; + if (currentLocale != null) { + inputReader = inputReaders.get(currentLocale); + } else { + inputReader = defaultInputReader; + } + DCInputSet subConfs = inputReader.getInputsByFormName(submitName); + if (subConfs == null) { + return null; + } + return converter.toRest(subConfs, utils.obtainProjection()); } catch (DCInputsReaderException e) { throw new IllegalStateException(e.getMessage(), e); } - if (inputConfig == null) { - return null; - } - return converter.toRest(inputConfig, utils.obtainProjection()); } @PreAuthorize("hasAuthority('AUTHENTICATED')") @Override public Page findAll(Context context, Pageable pageable) { try { + Locale currentLocale = context.getCurrentLocale(); + DCInputsReader inputReader; + if (currentLocale != null) { + inputReader = inputReaders.get(currentLocale); + } else { + inputReader = defaultInputReader; + } long total = inputReader.countInputs(); List subConfs = inputReader.getAllInputs(pageable.getPageSize(), Math.toIntExact(pageable.getOffset())); From 36ef4224cd5cf8c5233dca4c15642e0d43caaee2 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Thu, 16 Apr 2020 12:57:17 +0200 Subject: [PATCH 062/143] added configurations and submissions forms in italian and --- .../dspaceFolder/config/item-submission.xml | 11 ++ .../config/submission-forms_it.xml | 171 ++++++++++++++++++ .../config/submission-forms_uk.xml | 169 +++++++++++++++++ 3 files changed, 351 insertions(+) create mode 100644 dspace-api/src/test/data/dspaceFolder/config/submission-forms_it.xml create mode 100644 dspace-api/src/test/data/dspaceFolder/config/submission-forms_uk.xml diff --git a/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml b/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml index de19ef7287..a01cdc1cbc 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml @@ -18,6 +18,7 @@ + @@ -108,6 +109,12 @@ org.dspace.submit.step.SampleStep sample
+ + + submit.progressbar.describe.stepone + org.dspace.app.rest.submit.step.DescribeStep + submission-form + @@ -149,6 +156,10 @@ + + + + diff --git a/dspace-api/src/test/data/dspaceFolder/config/submission-forms_it.xml b/dspace-api/src/test/data/dspaceFolder/config/submission-forms_it.xml new file mode 100644 index 0000000000..1514824796 --- /dev/null +++ b/dspace-api/src/test/data/dspaceFolder/config/submission-forms_it.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + dc + title + + false + + onebox + Inserisci nome del file + È necessario inserire un titolo principale per questo item + + + + + dc + description + true + + textarea + Inserisci descrizione per questo file + + + +
+ +
+ + + isAuthorOfPublication + person + true + + Aggiungi un autore + + dc + contributor + author + name + + È richiesto almeno un autore + + + + + dc + title + + false + + onebox + Inserisci titolo principale di questo item + È necessario inserire un titolo principale per questo item + + + + + + + + dc + language + iso + false + + dropdown + Selezionare la lingua del contenuto principale dell'item. Se la lingua non compare + nell'elenco, selezionare "Altro". Se il contenuto non ha davvero una lingua (ad esempio, + se è un set di dati o un'immagine) selezionare "N/A". + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + N/A + + + + Inglese (USA) + en_US + + + Inglese + en + + + Spagnolo + es + + + Tedesco + de + + + Francese + fr + + + Italiano + it + + + Giapponese + ja + + + Cinese + zh + + + Portogallo + pt + + + Ucraino + uk + + + (Altro) + other + + + + +
\ No newline at end of file diff --git a/dspace-api/src/test/data/dspaceFolder/config/submission-forms_uk.xml b/dspace-api/src/test/data/dspaceFolder/config/submission-forms_uk.xml new file mode 100644 index 0000000000..88941c34ac --- /dev/null +++ b/dspace-api/src/test/data/dspaceFolder/config/submission-forms_uk.xml @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + dc + title + + false + + onebox + Ввести основний заголовок файла. + Заговолок файла обов'язковий ! + + + + + dc + description + true + + textarea + Ввести опис для цього файла + + + +
+ +
+ + + isAuthorOfPublication + person + true + + Додати автора + + dc + contributor + author + name + + Потрібно ввести хочаб одного автора! + + + + + dc + title + + false + + onebox + Ввести основний заголовок файла + Заговолок файла обов'язковий ! + + + + + + + dc + language + iso + false + + dropdown + Виберiть мову головного змiсту файлу, як що мови немає у списку, + вибрати "Iнша". Як що вмiст вайлу не є текстовим, наприклад + є фотографiєю, тодi вибрати "N/A" + + + + +
+
+ + + + + + + + + + + + + + + + + + + + N/A + + + + Американська (USA) + en_US + + + Англiйська + en + + + Iспанська + es + + + Нiмецька + de + + + Французька + fr + + + Iталiйська + it + + + Японська + ja + + + Китайська + zh + + + Португальська + pt + + + Турецька + tr + + + (Iнша) + other + + + + +
\ No newline at end of file From ec59b7d45e98c6031abe89d82f3aed6eb0ecf14a Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Sat, 18 Apr 2020 18:17:25 +0200 Subject: [PATCH 063/143] added Filter for set Content-Language Header of the response --- .../ContentLanguageHeaderResponseFilter.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/filter/ContentLanguageHeaderResponseFilter.java diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/filter/ContentLanguageHeaderResponseFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/filter/ContentLanguageHeaderResponseFilter.java new file mode 100644 index 0000000000..e5bda29c84 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/filter/ContentLanguageHeaderResponseFilter.java @@ -0,0 +1,52 @@ +package org.dspace.app.rest.filter; + +import java.io.IOException; +import java.util.Locale; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; + +import org.dspace.core.I18nUtil; +import org.springframework.stereotype.Component; + +/** + * This filter assures that when the dspace instance supports multiple languages + * they are noted in the Content-Language Header of the response + * + * @author Mykhaylo Boychuk (at 4science.it) + */ +@Component +public class ContentLanguageHeaderResponseFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + chain.doFilter(request, response); + + HttpServletResponse httpServletResponse = (HttpServletResponse) response; + if (!httpServletResponse.containsHeader("Content-Language")) { + Locale[] locales = I18nUtil.getSupportedLocales(); + StringBuilder locsStr = new StringBuilder(); + for (Locale locale : locales) { + if (locsStr.length() > 0) { + locsStr.append(","); + } + locsStr.append(locale.getLanguage()); + } + httpServletResponse.setHeader("Content-Language", locsStr.toString()); + } + } + + @Override + public void destroy() { + } + +} From 0a37c5906a200b08d53d2584d403b74dae297280 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Sat, 18 Apr 2020 18:24:16 +0200 Subject: [PATCH 064/143] refactoring --- .../org/dspace/administer/CreateAdministrator.java | 2 +- .../src/main/java/org/dspace/core/Context.java | 2 +- .../src/main/java/org/dspace/core/I18nUtil.java | 13 +++++-------- .../src/test/java/org/dspace/core/ContextTest.java | 2 +- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/administer/CreateAdministrator.java b/dspace-api/src/main/java/org/dspace/administer/CreateAdministrator.java index a58691e251..983038c812 100644 --- a/dspace-api/src/main/java/org/dspace/administer/CreateAdministrator.java +++ b/dspace-api/src/main/java/org/dspace/administer/CreateAdministrator.java @@ -115,7 +115,7 @@ public final class CreateAdministrator { String lastName = null; char[] password1 = null; char[] password2 = null; - String language = I18nUtil.DEFAULTLOCALE.getLanguage(); + String language = I18nUtil.getDefaultLocale().getLanguage(); while (!dataOK) { System.out.print("E-mail address: "); diff --git a/dspace-api/src/main/java/org/dspace/core/Context.java b/dspace-api/src/main/java/org/dspace/core/Context.java index ecfc29d29d..11e388c727 100644 --- a/dspace-api/src/main/java/org/dspace/core/Context.java +++ b/dspace-api/src/main/java/org/dspace/core/Context.java @@ -179,7 +179,7 @@ public class Context implements AutoCloseable { } currentUser = null; - currentLocale = I18nUtil.DEFAULTLOCALE; + currentLocale = I18nUtil.getDefaultLocale(); extraLogInfo = ""; ignoreAuth = false; diff --git a/dspace-api/src/main/java/org/dspace/core/I18nUtil.java b/dspace-api/src/main/java/org/dspace/core/I18nUtil.java index 37e48c4a4f..0d5e95d048 100644 --- a/dspace-api/src/main/java/org/dspace/core/I18nUtil.java +++ b/dspace-api/src/main/java/org/dspace/core/I18nUtil.java @@ -37,9 +37,6 @@ import org.dspace.services.factory.DSpaceServicesFactory; public class I18nUtil { private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(I18nUtil.class); - // the default Locale of this DSpace Instance - public static final Locale DEFAULTLOCALE = getDefaultLocale(); - // delimiters between elements of UNIX/POSIX locale spec, e.g. en_US.UTF-8 private static final String LOCALE_DELIMITERS = " _."; @@ -127,7 +124,7 @@ public class I18nUtil { return parseLocales(locales); } else { Locale[] availableLocales = new Locale[1]; - availableLocales[0] = DEFAULTLOCALE; + availableLocales[0] = getDefaultLocale(); return availableLocales; } } @@ -148,7 +145,7 @@ public class I18nUtil { Locale supportedLocale = null; String testLocale = ""; if (availableLocales == null) { - supportedLocale = DEFAULTLOCALE; + supportedLocale = getDefaultLocale(); } else { if (!locale.getVariant().equals("")) { testLocale = locale.toString(); @@ -188,7 +185,7 @@ public class I18nUtil { } } if (!isSupported) { - supportedLocale = DEFAULTLOCALE; + supportedLocale = getDefaultLocale(); } } return supportedLocale; @@ -220,7 +217,7 @@ public class I18nUtil { * String of the message */ public static String getMessage(String key) { - return getMessage(key.trim(), DEFAULTLOCALE); + return getMessage(key.trim(), getDefaultLocale()); } /** @@ -233,7 +230,7 @@ public class I18nUtil { */ public static String getMessage(String key, Locale locale) { if (locale == null) { - locale = DEFAULTLOCALE; + locale = getDefaultLocale(); } ResourceBundle.Control control = ResourceBundle.Control.getNoFallbackControl( diff --git a/dspace-api/src/test/java/org/dspace/core/ContextTest.java b/dspace-api/src/test/java/org/dspace/core/ContextTest.java index f5697a72dc..0c29e053ec 100644 --- a/dspace-api/src/test/java/org/dspace/core/ContextTest.java +++ b/dspace-api/src/test/java/org/dspace/core/ContextTest.java @@ -130,7 +130,7 @@ public class ContextTest extends AbstractUnitTest { public void testGetCurrentLocale() { //NOTE: CurrentLocale is not initialized in AbstractUnitTest. So it should be DEFAULTLOCALE assertThat("testGetCurrentLocale 0", context.getCurrentLocale(), notNullValue()); - assertThat("testGetCurrentLocale 1", context.getCurrentLocale(), equalTo(I18nUtil.DEFAULTLOCALE)); + assertThat("testGetCurrentLocale 1", context.getCurrentLocale(), equalTo(I18nUtil.getDefaultLocale())); } /** From efcefef914c20684bdf229f609d4bcf8544267a3 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Sat, 18 Apr 2020 18:43:06 +0200 Subject: [PATCH 065/143] added ITs for language support --- .../config/submission-forms_uk.xml | 7 +- .../SubmissionFormRestRepository.java | 9 + .../app/rest/SubmissionFormsControllerIT.java | 230 ++++++++++++++++++ 3 files changed, 241 insertions(+), 5 deletions(-) diff --git a/dspace-api/src/test/data/dspaceFolder/config/submission-forms_uk.xml b/dspace-api/src/test/data/dspaceFolder/config/submission-forms_uk.xml index 88941c34ac..49a2ccc1a9 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/submission-forms_uk.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/submission-forms_uk.xml @@ -47,7 +47,7 @@ -
+ isAuthorOfPublication @@ -86,10 +86,7 @@ false dropdown - Виберiть мову головного змiсту файлу, як що мови немає у списку, - вибрати "Iнша". Як що вмiст вайлу не є текстовим, наприклад - є фотографiєю, тодi вибрати "N/A" - + Виберiть мову головного змiсту файлу, як що мови немає у списку, вибрати (Iнша). Як що вмiст вайлу не є текстовим, наприклад є фотографiєю, тодi вибрати (N/A) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionFormRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionFormRestRepository.java index 4552d7ddd5..434a8dc0fd 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionFormRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionFormRestRepository.java @@ -87,4 +87,13 @@ public class SubmissionFormRestRepository extends DSpaceRestRepository getDomainClass() { return SubmissionFormRest.class; } + + public void reload() throws DCInputsReaderException { + this.defaultInputReader = new DCInputsReader(); + Locale[] locales = I18nUtil.getSupportedLocales(); + this.inputReaders = new HashMap(); + for (Locale locale : locales) { + inputReaders.put(locale, new DCInputsReader(I18nUtil.getInputFormsFileName(locale))); + } + } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionFormsControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionFormsControllerIT.java index b17ea47c2d..235764eccd 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionFormsControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionFormsControllerIT.java @@ -16,10 +16,18 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.Locale; + +import org.dspace.app.rest.builder.EPersonBuilder; import org.dspace.app.rest.matcher.SubmissionFormFieldMatcher; +import org.dspace.app.rest.repository.SubmissionFormRestRepository; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.app.util.DCInputsReaderException; +import org.dspace.eperson.EPerson; +import org.dspace.services.ConfigurationService; import org.hamcrest.Matchers; import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; /** * Integration test to test the /api/config/submissionforms endpoint @@ -27,6 +35,11 @@ import org.junit.Test; */ public class SubmissionFormsControllerIT extends AbstractControllerIntegrationTest { + @Autowired + private ConfigurationService configurationService; + @Autowired + private SubmissionFormRestRepository submissionFormRestRepository; + @Test public void findAll() throws Exception { //When we call the root endpoint as anonymous user @@ -185,4 +198,221 @@ public class SubmissionFormsControllerIT extends AbstractControllerIntegrationTe "creativework.publisher:somepublishername", "periodical", false)))) ; } + + @Test + public void languageSupportTest() throws Exception { + context.turnOffAuthorisationSystem(); + String[] supportedLanguage = {"it","uk"}; + configurationService.setProperty("webui.supported.locales",supportedLanguage); + submissionFormRestRepository.reload(); + + Locale uk = new Locale("uk"); + Locale it = new Locale("it"); + + context.restoreAuthSystemState(); + + String tokenEperson = getAuthToken(eperson.getEmail(), password); + + // user select italian language + getClient(tokenEperson).perform(get("/api/config/submissionforms/languagetest").locale(it)) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.id", is("languagetest"))) + .andExpect(jsonPath("$.name", is("languagetest"))) + .andExpect(jsonPath("$.type", is("submissionform"))) + .andExpect(jsonPath("$._links.self.href", Matchers + .startsWith(REST_SERVER_URL + "config/submissionforms/languagetest"))) + .andExpect(jsonPath("$.rows[0].fields", contains(SubmissionFormFieldMatcher + .matchFormFieldDefinition("name", "Autore", "\u00C8" + " richiesto almeno un autore", true, + "Aggiungi un autore", "dc.contributor.author")))) + .andExpect(jsonPath("$.rows[1].fields", contains(SubmissionFormFieldMatcher + .matchFormFieldDefinition("onebox", "Titolo", + "\u00C8" + " necessario inserire un titolo principale per questo item", false, + "Inserisci titolo principale di questo item", "dc.title")))) + .andExpect(jsonPath("$.rows[2].fields", contains(SubmissionFormFieldMatcher + .matchFormFieldDefinition("dropdown", "Lingua", null, false, + "Selezionare la lingua del contenuto principale dell'item." + + " Se la lingua non compare nell'elenco, selezionare (Altro)." + + " Se il contenuto non ha davvero una lingua" + + " (ad esempio, se è un set di dati o un'immagine) selezionare (N/A)", "dc.language.iso")))); + + // user select ukranian language + getClient(tokenEperson).perform(get("/api/config/submissionforms/languagetest").locale(uk)) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.id", is("languagetest"))) + .andExpect(jsonPath("$.name", is("languagetest"))) + .andExpect(jsonPath("$.type", is("submissionform"))) + .andExpect(jsonPath("$._links.self.href", Matchers + .startsWith(REST_SERVER_URL + "config/submissionforms/languagetest"))) + .andExpect(jsonPath("$.rows[0].fields", contains(SubmissionFormFieldMatcher + .matchFormFieldDefinition("name", "Автор", "Потрібно ввести хочаб одного автора!", + true, "Додати автора", "dc.contributor.author")))) + .andExpect(jsonPath("$.rows[1].fields", contains(SubmissionFormFieldMatcher + .matchFormFieldDefinition("onebox", "Заголовок", + "Заговолок файла обов'язковий !", false, + "Ввести основний заголовок файла", "dc.title")))) + .andExpect(jsonPath("$.rows[2].fields", contains(SubmissionFormFieldMatcher + .matchFormFieldDefinition("dropdown", "Мова", null, false, + "Виберiть мову головного змiсту файлу, як що мови немає у списку, вибрати (Iнша)." + + " Як що вмiст вайлу не є текстовим, наприклад є фотографiєю, тодi вибрати (N/A)", + "dc.language.iso")))); + + resetPropertyFile(); + } + + @Test + public void preferLanguageTest() throws Exception { + context.turnOffAuthorisationSystem(); + + String[] supportedLanguage = {"it","uk"}; + configurationService.setProperty("webui.supported.locales",supportedLanguage); + submissionFormRestRepository.reload(); + + EPerson epersonIT = EPersonBuilder.createEPerson(context) + .withEmail("epersonIT@example.com") + .withPassword(password) + .withLanguage("it") + .build(); + + EPerson epersonUK = EPersonBuilder.createEPerson(context) + .withEmail("epersonUK@example.com") + .withPassword(password) + .withLanguage("uk") + .build(); + + context.restoreAuthSystemState(); + + String tokenEpersonIT = getAuthToken(epersonIT.getEmail(), password); + String tokenEpersonUK = getAuthToken(epersonUK.getEmail(), password); + + // user with italian prefer language + getClient(tokenEpersonIT).perform(get("/api/config/submissionforms/languagetest")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.id", is("languagetest"))) + .andExpect(jsonPath("$.name", is("languagetest"))) + .andExpect(jsonPath("$.type", is("submissionform"))) + .andExpect(jsonPath("$._links.self.href", Matchers + .startsWith(REST_SERVER_URL + "config/submissionforms/languagetest"))) + .andExpect(jsonPath("$.rows[0].fields", contains(SubmissionFormFieldMatcher + .matchFormFieldDefinition("name", "Autore", "\u00C8" + " richiesto almeno un autore", true, + "Aggiungi un autore", "dc.contributor.author")))) + .andExpect(jsonPath("$.rows[1].fields", contains(SubmissionFormFieldMatcher + .matchFormFieldDefinition("onebox", "Titolo", + "\u00C8" + " necessario inserire un titolo principale per questo item", false, + "Inserisci titolo principale di questo item", "dc.title")))) + .andExpect(jsonPath("$.rows[2].fields", contains(SubmissionFormFieldMatcher + .matchFormFieldDefinition("dropdown", "Lingua", null, false, + "Selezionare la lingua del contenuto principale dell'item." + + " Se la lingua non compare nell'elenco, selezionare (Altro)." + + " Se il contenuto non ha davvero una lingua" + + " (ad esempio, se è un set di dati o un'immagine) selezionare (N/A)", "dc.language.iso")))); + + // user with ukranian prefer language + getClient(tokenEpersonUK).perform(get("/api/config/submissionforms/languagetest")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.id", is("languagetest"))) + .andExpect(jsonPath("$.name", is("languagetest"))) + .andExpect(jsonPath("$.type", is("submissionform"))) + .andExpect(jsonPath("$._links.self.href", Matchers + .startsWith(REST_SERVER_URL + "config/submissionforms/languagetest"))) + .andExpect(jsonPath("$.rows[0].fields", contains(SubmissionFormFieldMatcher + .matchFormFieldDefinition("name", "Автор", "Потрібно ввести хочаб одного автора!", + true, "Додати автора", "dc.contributor.author")))) + .andExpect(jsonPath("$.rows[1].fields", contains(SubmissionFormFieldMatcher + .matchFormFieldDefinition("onebox", "Заголовок", + "Заговолок файла обов'язковий !", false, + "Ввести основний заголовок файла", "dc.title")))) + .andExpect(jsonPath("$.rows[2].fields", contains(SubmissionFormFieldMatcher + .matchFormFieldDefinition("dropdown", "Мова", null, false, + "Виберiть мову головного змiсту файлу, як що мови немає у списку, вибрати (Iнша)." + + " Як що вмiст вайлу не є текстовим, наприклад є фотографiєю, тодi вибрати (N/A)", + "dc.language.iso")))); + + resetPropertyFile(); + } + + @Test + public void userChoiceAnotherLanguageTest() throws Exception { + context.turnOffAuthorisationSystem(); + + String[] supportedLanguage = {"it","uk"}; + configurationService.setProperty("webui.supported.locales",supportedLanguage); + submissionFormRestRepository.reload(); + + Locale it = new Locale("it"); + + EPerson epersonUK = EPersonBuilder.createEPerson(context) + .withEmail("epersonUK@example.com") + .withPassword(password) + .withLanguage("uk") + .build(); + + context.restoreAuthSystemState(); + + String tokenEpersonUK = getAuthToken(epersonUK.getEmail(), password); + + // user prefer ukranian but choice italian language + getClient(tokenEpersonUK).perform(get("/api/config/submissionforms/languagetest").locale(it)) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.id", is("languagetest"))) + .andExpect(jsonPath("$.name", is("languagetest"))) + .andExpect(jsonPath("$.type", is("submissionform"))) + .andExpect(jsonPath("$._links.self.href", Matchers + .startsWith(REST_SERVER_URL + "config/submissionforms/languagetest"))) + .andExpect(jsonPath("$.rows[0].fields", contains(SubmissionFormFieldMatcher + .matchFormFieldDefinition("name", "Autore", "\u00C8" + " richiesto almeno un autore", true, + "Aggiungi un autore", "dc.contributor.author")))) + .andExpect(jsonPath("$.rows[1].fields", contains(SubmissionFormFieldMatcher + .matchFormFieldDefinition("onebox", "Titolo", + "\u00C8" + " necessario inserire un titolo principale per questo item", false, + "Inserisci titolo principale di questo item", "dc.title")))) + .andExpect(jsonPath("$.rows[2].fields", contains(SubmissionFormFieldMatcher + .matchFormFieldDefinition("dropdown", "Lingua", null, false, + "Selezionare la lingua del contenuto principale dell'item." + + " Se la lingua non compare nell'elenco, selezionare (Altro)." + + " Se il contenuto non ha davvero una lingua" + + " (ad esempio, se è un set di dati o un'immagine) selezionare (N/A)", "dc.language.iso")))); + + resetPropertyFile(); + } + + @Test + public void defaultLanguageTest() throws Exception { + context.turnOffAuthorisationSystem(); + + String[] supportedLanguage = {"it","uk"}; + configurationService.setProperty("default.locale","it"); + configurationService.setProperty("webui.supported.locales",supportedLanguage); + submissionFormRestRepository.reload(); + + context.restoreAuthSystemState(); + + String tokenEperson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEperson).perform(get("/api/config/submissionforms/languagetest")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.id", is("languagetest"))) + .andExpect(jsonPath("$.name", is("languagetest"))) + .andExpect(jsonPath("$.type", is("submissionform"))) + .andExpect(jsonPath("$._links.self.href", Matchers + .startsWith(REST_SERVER_URL + "config/submissionforms/languagetest"))) + .andExpect(jsonPath("$.rows[0].fields", contains(SubmissionFormFieldMatcher + .matchFormFieldDefinition("name", "Autore", "\u00C8 richiesto almeno un autore", true, + "Aggiungi un autore", "dc.contributor.author")))) + .andExpect(jsonPath("$.rows[1].fields", contains(SubmissionFormFieldMatcher + .matchFormFieldDefinition("onebox", "Titolo", + "\u00C8 necessario inserire un titolo principale per questo item", false, + "Inserisci titolo principale di questo item", "dc.title")))); + + resetPropertyFile(); + } + + private void resetPropertyFile() throws DCInputsReaderException { + configurationService.setProperty("webui.supported.locales",null); + submissionFormRestRepository.reload(); + } } From e2cf32dea33ab26b1f33224400a3b13bf081f972 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Sun, 19 Apr 2020 13:27:16 +0200 Subject: [PATCH 066/143] added license --- .../test/data/dspaceFolder/config/submission-forms_it.xml | 4 +--- .../rest/filter/ContentLanguageHeaderResponseFilter.java | 7 +++++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/dspace-api/src/test/data/dspaceFolder/config/submission-forms_it.xml b/dspace-api/src/test/data/dspaceFolder/config/submission-forms_it.xml index 1514824796..66ed4a926c 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/submission-forms_it.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/submission-forms_it.xml @@ -87,9 +87,7 @@ false dropdown - Selezionare la lingua del contenuto principale dell'item. Se la lingua non compare - nell'elenco, selezionare "Altro". Se il contenuto non ha davvero una lingua (ad esempio, - se è un set di dati o un'immagine) selezionare "N/A". + Selezionare la lingua del contenuto principale dell'item. Se la lingua non compare nell'elenco, selezionare (Altro). Se il contenuto non ha davvero una lingua (ad esempio, se è un set di dati o un'immagine) selezionare (N/A). diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/filter/ContentLanguageHeaderResponseFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/filter/ContentLanguageHeaderResponseFilter.java index e5bda29c84..6552ae4f7f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/filter/ContentLanguageHeaderResponseFilter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/filter/ContentLanguageHeaderResponseFilter.java @@ -1,3 +1,10 @@ +/** + * 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.filter; import java.io.IOException; From 60240b19ca4b5b495c5ff26ab06403380c219570 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Wed, 22 Apr 2020 12:43:13 +0200 Subject: [PATCH 067/143] added ITs for check Content-Language header --- .../dspace/app/rest/LanguageSupportIT.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/LanguageSupportIT.java diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/LanguageSupportIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/LanguageSupportIT.java new file mode 100644 index 0000000000..2dc06eca8b --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/LanguageSupportIT.java @@ -0,0 +1,84 @@ +/** + * 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.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; + +import java.util.Locale; + +import org.dspace.app.rest.builder.EPersonBuilder; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.content.authority.ChoiceAuthorityServiceImpl; +import org.dspace.core.LegacyPluginServiceImpl; +import org.dspace.eperson.EPerson; +import org.dspace.services.ConfigurationService; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Integration test class for supported languages + * + * @author Mykhaylo Boychuk (at 4science) + */ +public class LanguageSupportIT extends AbstractControllerIntegrationTest { + + @Autowired + private ConfigurationService configurationService; + @Autowired + private LegacyPluginServiceImpl legacyPluginService; + @Autowired + private ChoiceAuthorityServiceImpl choiceAuthorityServiceImpl; + + @Test + public void checkDefaultLanguageAnonymousTest() throws Exception { + getClient().perform(get("/api")) + .andExpect(header().stringValues("Content-Language","en")); + } + + @Test + public void checkEnabledMultipleLanguageSupportTest() throws Exception { + context.turnOffAuthorisationSystem(); + String[] supportedLanguage = {"uk","it"}; + configurationService.setProperty("webui.supported.locales",supportedLanguage); + legacyPluginService.clearNamedPluginClasses(); + choiceAuthorityServiceImpl.clearCache(); + + Locale it = new Locale("it"); + + EPerson epersonUK = EPersonBuilder.createEPerson(context) + .withEmail("epersonUK@example.com") + .withPassword(password) + .withLanguage("uk") + .build(); + + EPerson epersonFR = EPersonBuilder.createEPerson(context) + .withEmail("epersonFR@example.com") + .withPassword(password) + .withLanguage("fr") + .build(); + + context.restoreAuthSystemState(); + + String tokenEPersonUK = getAuthToken(epersonUK.getEmail(), password); + String tokenEPersonFR = getAuthToken(epersonFR.getEmail(), password); + + getClient(tokenEPersonUK).perform(get("/api")) + .andExpect(header().stringValues("Content-Language","uk, it")); + + getClient(tokenEPersonUK).perform(get("/api").locale(it)) + .andExpect(header().stringValues("Content-Language","uk, it")); + + getClient(tokenEPersonFR).perform(get("/api").locale(it)) + .andExpect(header().stringValues("Content-Language","en")); + + configurationService.setProperty("webui.supported.locales",null); + legacyPluginService.clearNamedPluginClasses(); + choiceAuthorityServiceImpl.clearCache(); + } +} From e7ef7d3c5ec75c75b4cb5575e47c464c9539d666 Mon Sep 17 00:00:00 2001 From: Peter Nijs Date: Wed, 17 Jun 2020 16:23:54 +0200 Subject: [PATCH 068/143] 71343: Authorization for Downloads of restricted Bitstreams #2 --- .../rest/security/jwt/JWTTokenHandler.java | 101 +++++++++-------- ...JWTTokenRestAuthenticationServiceImpl.java | 27 ++++- .../jwt/ShortLivedJWTTokenHandler.java | 40 +++++++ .../rest/AuthenticationRestControllerIT.java | 102 ++++++++++++++++++ .../security/jwt/JWTTokenHandlerTest.java | 8 +- 5 files changed, 220 insertions(+), 58 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java index fa090df7ed..47a869105d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java @@ -42,7 +42,6 @@ import org.dspace.service.ClientInfoService; import org.dspace.services.ConfigurationService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.keygen.BytesKeyGenerator; import org.springframework.security.crypto.keygen.KeyGenerators; @@ -55,7 +54,7 @@ import org.springframework.security.crypto.keygen.KeyGenerators; * @author Frederic Van Reet (frederic dot vanreet at atmire dot com) * @author Tom Desair (tom dot desair at atmire dot com) */ -public abstract class JWTTokenHandler implements InitializingBean { +public abstract class JWTTokenHandler { private static final int MAX_CLOCK_SKEW_SECONDS = 60; private static final Logger log = LoggerFactory.getLogger(JWTTokenHandler.class); @@ -75,30 +74,8 @@ public abstract class JWTTokenHandler implements InitializingBean { @Autowired private ClientInfoService clientInfoService; - private String jwtKey; - private long expirationTime; - private boolean includeIP; - private boolean encryptionEnabled; - private boolean compressionEnabled; - private byte[] encryptionKey; - - - @Override - public void afterPropertiesSet() throws Exception { - this.jwtKey = - getSecret(getTokenSecretConfigurationKey()); - this.encryptionKey = - getSecret(getEncryptionSecretConfigurationKey()).getBytes(); - - this.expirationTime = - configurationService.getLongProperty(getTokenExpirationConfigurationKey(), 30); - this.includeIP = - configurationService.getBooleanProperty(getTokenIncludeIPConfigurationKey(), true); - this.encryptionEnabled = - configurationService.getBooleanProperty(getEncryptionEnabledConfigurationKey(), false); - this.compressionEnabled = - configurationService.getBooleanProperty(getCompressionEnabledConfigurationKey(), false); - } + private String generatedJwtKey; + private String generatedEncryptionKey; /** * Get the configuration property key for the token secret. @@ -225,17 +202,54 @@ public abstract class JWTTokenHandler implements InitializingBean { } } - public long getExpirationPeriod() { - return expirationTime; + /** + * Retrieve the token secret key from configuration. If not specified, generate and cache a random 32 byte key + * @return configuration value or random 32 byte key + */ + public String getJwtKey() { + String secret = configurationService.getProperty(getTokenSecretConfigurationKey()); + + if (StringUtils.isBlank(secret)) { + if (StringUtils.isBlank(generatedJwtKey)) { + generatedJwtKey = generateRandomKey(); + } + secret = generatedJwtKey; + } + + return secret; } + public boolean getIncludeIP() { + return configurationService.getBooleanProperty(getTokenIncludeIPConfigurationKey(), true); + } + + public long getExpirationPeriod() { + return configurationService.getLongProperty(getTokenExpirationConfigurationKey(), 30); + } public boolean isEncryptionEnabled() { - return encryptionEnabled; + return configurationService.getBooleanProperty(getEncryptionEnabledConfigurationKey(), false); } + public boolean getCompressionEnabled() { + return configurationService.getBooleanProperty(getCompressionEnabledConfigurationKey(), false); + } + + /** + * Retrieve the encryption secret key from configuration. If not specified, generate and cache a random 32 byte key + * @return configuration value or random 32 byte key + */ public byte[] getEncryptionKey() { - return encryptionKey; + String secretString = configurationService.getProperty(getEncryptionSecretConfigurationKey()); + + if (StringUtils.isBlank(secretString)) { + if (StringUtils.isBlank(generatedEncryptionKey)) { + generatedEncryptionKey = generateRandomKey(); + } + secretString = generatedEncryptionKey; + } + + return secretString.getBytes(); } private JWEObject encryptJWT(SignedJWT signedJWT) throws JOSEException { @@ -261,7 +275,7 @@ public abstract class JWTTokenHandler implements InitializingBean { * @return true if valid, false otherwise * @throws JOSEException */ - private boolean isValidToken(HttpServletRequest request, SignedJWT signedJWT, JWTClaimsSet jwtClaimsSet, + protected boolean isValidToken(HttpServletRequest request, SignedJWT signedJWT, JWTClaimsSet jwtClaimsSet, EPerson ePerson) throws JOSEException { if (ePerson == null || StringUtils.isBlank(ePerson.getSessionSalt())) { return false; @@ -351,7 +365,7 @@ public abstract class JWTTokenHandler implements InitializingBean { //This method makes compression configurable private JWEHeader.Builder compression(JWEHeader.Builder builder) { - if (compressionEnabled) { + if (getCompressionEnabled()) { return builder.compressionAlgorithm(CompressionAlgorithm.DEF); } return builder; @@ -367,12 +381,12 @@ public abstract class JWTTokenHandler implements InitializingBean { * @param ePerson * @return */ - private String buildSigningKey(HttpServletRequest request, EPerson ePerson) { + protected String buildSigningKey(HttpServletRequest request, EPerson ePerson) { String ipAddress = ""; - if (includeIP) { + if (getIncludeIP()) { ipAddress = getIpAddress(request); } - return jwtKey + ePerson.getSessionSalt() + ipAddress; + return getJwtKey() + ePerson.getSessionSalt() + ipAddress; } private String getIpAddress(HttpServletRequest request) { @@ -399,7 +413,7 @@ public abstract class JWTTokenHandler implements InitializingBean { //This allows a user to login on multiple devices/browsers at the same time. if (StringUtils.isBlank(ePerson.getSessionSalt()) || previousLoginDate == null - || (ePerson.getLastActive().getTime() - previousLoginDate.getTime() > expirationTime)) { + || (ePerson.getLastActive().getTime() - previousLoginDate.getTime() > getExpirationPeriod())) { ePerson.setSessionSalt(generateRandomKey()); ePersonService.update(context, ePerson); @@ -412,21 +426,6 @@ public abstract class JWTTokenHandler implements InitializingBean { return ePerson; } - /** - * Retrieve the given secret key from configuration. If not specified, generate a random 32 byte key - * @param property configuration property to check for - * @return configuration value or random 32 byte key - */ - private String getSecret(String property) { - String secret = configurationService.getProperty(property); - - if (StringUtils.isBlank(secret)) { - secret = generateRandomKey(); - } - - return secret; - } - /** * Generate a random 32 bytes key */ diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java index e480154781..f32e221adf 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java @@ -47,6 +47,7 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication private static final String AUTHORIZATION_COOKIE = "Authorization-cookie"; private static final String AUTHORIZATION_HEADER = "Authorization"; private static final String AUTHORIZATION_TYPE = "Bearer"; + private static final String AUTHORIZATION_TOKEN_PARAMETER = "token"; @Autowired private SessionJWTTokenHandler sessionJWTTokenHandler; @@ -112,9 +113,15 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication @Override public EPerson getAuthenticatedEPerson(HttpServletRequest request, Context context) { - String token = getToken(request); try { - EPerson ePerson = sessionJWTTokenHandler.parseEPersonFromToken(token, request, context); + String token = getSessionToken(request); + EPerson ePerson = null; + if (token == null) { + token = getShortLivedToken(request); + ePerson = shortLivedJWTTokenHandler.parseEPersonFromToken(token, request, context); + } else { + ePerson = sessionJWTTokenHandler.parseEPersonFromToken(token, request, context); + } return ePerson; } catch (JOSEException e) { log.error("Jose error", e); @@ -129,13 +136,14 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication @Override public boolean hasAuthenticationData(HttpServletRequest request) { return StringUtils.isNotBlank(request.getHeader(AUTHORIZATION_HEADER)) - || StringUtils.isNotBlank(getAuthorizationCookie(request)); + || StringUtils.isNotBlank(getAuthorizationCookie(request)) + || StringUtils.isNotBlank(request.getParameter(AUTHORIZATION_TOKEN_PARAMETER)); } @Override public void invalidateAuthenticationData(HttpServletRequest request, HttpServletResponse response, Context context) throws Exception { - String token = getToken(request); + String token = getSessionToken(request); invalidateAuthenticationCookie(response); sessionJWTTokenHandler.invalidateToken(token, request, context); } @@ -192,7 +200,7 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication response.setHeader(AUTHORIZATION_HEADER, String.format("%s %s", AUTHORIZATION_TYPE, token)); } - private String getToken(HttpServletRequest request) { + private String getSessionToken(HttpServletRequest request) { String tokenValue = null; String authHeader = request.getHeader(AUTHORIZATION_HEADER); String authCookie = getAuthorizationCookie(request); @@ -205,6 +213,15 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication return tokenValue; } + private String getShortLivedToken(HttpServletRequest request) { + String tokenValue = null; + if (StringUtils.isNotBlank(request.getParameter(AUTHORIZATION_TOKEN_PARAMETER))) { + tokenValue = request.getParameter(AUTHORIZATION_TOKEN_PARAMETER); + } + + return tokenValue; + } + private String getAuthorizationCookie(HttpServletRequest request) { String authCookie = ""; Cookie[] cookies = request.getCookies(); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java index 6610d6f50a..95fd52530c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java @@ -7,6 +7,17 @@ */ package org.dspace.app.rest.security.jwt; +import java.util.Date; +import javax.servlet.http.HttpServletRequest; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jose.crypto.MACVerifier; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import com.nimbusds.jwt.util.DateUtils; +import org.apache.commons.lang3.StringUtils; +import org.dspace.eperson.EPerson; import org.springframework.stereotype.Component; /** @@ -15,6 +26,35 @@ import org.springframework.stereotype.Component; */ @Component public class ShortLivedJWTTokenHandler extends JWTTokenHandler { + + /** + * Determine if current JWT is valid for the given EPerson object. + * To be valid, current JWT *must* have been signed by the EPerson and not be expired. + * If EPerson is null or does not have a known active session, false is returned immediately. + * @param request current request + * @param signedJWT current signed JWT + * @param jwtClaimsSet claims set of current JWT + * @param ePerson EPerson parsed from current signed JWT + * @return true if valid, false otherwise + * @throws JOSEException + */ + @Override + protected boolean isValidToken(HttpServletRequest request, SignedJWT signedJWT, JWTClaimsSet jwtClaimsSet, + EPerson ePerson) throws JOSEException { + if (ePerson == null || StringUtils.isBlank(ePerson.getSessionSalt())) { + return false; + } else { + JWSVerifier verifier = new MACVerifier(buildSigningKey(request, ePerson)); + + //If token is valid and not expired return eperson in token + Date expirationTime = jwtClaimsSet.getExpirationTime(); + return signedJWT.verify(verifier) + && expirationTime != null + //Ensure expiration timestamp is after the current time, with a minute of acceptable clock skew. + && DateUtils.isAfter(expirationTime, new Date(), 0); + } + } + @Override protected String getTokenSecretConfigurationKey() { return "jwt.shortLived.token.secret"; diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java index 41d397f61b..e69f859983 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java @@ -21,14 +21,29 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.io.InputStream; import java.util.Base64; +import java.util.Map; import javax.servlet.http.Cookie; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.codec.CharEncoding; +import org.apache.commons.io.IOUtils; +import org.dspace.app.rest.builder.BitstreamBuilder; +import org.dspace.app.rest.builder.BundleBuilder; +import org.dspace.app.rest.builder.CollectionBuilder; +import org.dspace.app.rest.builder.CommunityBuilder; import org.dspace.app.rest.builder.GroupBuilder; +import org.dspace.app.rest.builder.ItemBuilder; import org.dspace.app.rest.matcher.AuthenticationStatusMatcher; import org.dspace.app.rest.matcher.EPersonMatcher; import org.dspace.app.rest.matcher.HalMatcher; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.content.Bitstream; +import org.dspace.content.Bundle; +import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.services.ConfigurationService; import org.hamcrest.Matchers; @@ -36,6 +51,7 @@ import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; /** @@ -774,4 +790,90 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio getClient().perform(post("/api/authn/shortlivedtokens")) .andExpect(status().isUnauthorized()); } + + @Test + public void testShortLivedTokenToDowloadBitstream() throws Exception { + Bitstream bitstream = createPrivateBitstream(); + String shortLivedToken = getShortLivedToken(eperson); + + getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content?token=" + shortLivedToken)) + .andExpect(status().isOk()); + } + + @Test + public void testSessionTokenToDowloadBitstream() throws Exception { + Bitstream bitstream = createPrivateBitstream(); + + String sessionToken = getAuthToken(eperson.getEmail(), password); + getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content?token=" + sessionToken)) + .andExpect(status().isForbidden()); + } + + @Test + public void testExpiredShortLivedTokenToDowloadBitstream() throws Exception { + Bitstream bitstream = createPrivateBitstream(); + configurationService.setProperty("jwt.shortLived.token.expiration", "1"); + String shortLivedToken = getShortLivedToken(eperson); + Thread.sleep(1); + getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content?token=" + shortLivedToken)) + .andExpect(status().isForbidden()); + } + + private String getShortLivedToken(EPerson ePerson) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + + String token = getAuthToken(eperson.getEmail(), password); + MvcResult mvcResult = getClient(token).perform(post("/api/authn/shortlivedtokens")) + .andReturn(); + + String content = mvcResult.getResponse().getContentAsString(); + Map map = mapper.readValue(content, Map.class); + return String.valueOf(map.get("token")); + } + + private Bitstream createPrivateBitstream() throws Exception { + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + + //2. One public items that is readable by Anonymous + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Test") + .withIssueDate("2010-10-17") + .withAuthor("Smith, Donald") + .withSubject("ExtraEntry") + .build(); + + Bundle bundle1 = BundleBuilder.createBundle(context, publicItem1) + .withName("TEST BUNDLE") + .build(); + + //2. An item restricted to a specific internal group + Group staffGroup = GroupBuilder.createGroup(context) + .withName("Staff") + .addMember(eperson) + .build(); + + String bitstreamContent = "ThisIsSomeDummyText"; + Bitstream bitstream = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) { + bitstream = BitstreamBuilder. + createBitstream(context, bundle1, is) + .withName("Bitstream") + .withDescription("description") + .withMimeType("text/plain") + .withReaderGroup(staffGroup) + .build(); + } + + context.restoreAuthSystemState(); + + return bitstream; + } } + diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/JWTTokenHandlerTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/JWTTokenHandlerTest.java index 94fb653e6c..6ae4af8293 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/JWTTokenHandlerTest.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/JWTTokenHandlerTest.java @@ -24,6 +24,7 @@ import org.dspace.core.Context; import org.dspace.eperson.EPerson; import org.dspace.eperson.service.EPersonService; import org.dspace.service.ClientInfoService; +import org.dspace.services.ConfigurationService; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -48,6 +49,9 @@ public class JWTTokenHandlerTest { @Spy SessionJWTTokenHandler sessionJWTTokenHandler; + @Mock + private ConfigurationService configurationService; + @Mock private Context context; @@ -99,7 +103,7 @@ public class JWTTokenHandlerTest { when(sessionJWTTokenHandler.isEncryptionEnabled()).thenReturn(true); Date previous = new Date(System.currentTimeMillis() - 10000000000L); StringKeyGenerator keyGenerator = KeyGenerators.string(); - when(sessionJWTTokenHandler.getEncryptionKey()).thenReturn(keyGenerator.generateKey().getBytes()); + when(configurationService.getProperty("jwt.session.encryption.secret")).thenReturn(keyGenerator.generateKey()); String token = sessionJWTTokenHandler .createTokenForEPerson(context, new MockHttpServletRequest(), previous, new ArrayList<>()); SignedJWT signedJWT = SignedJWT.parse(token); @@ -108,7 +112,7 @@ public class JWTTokenHandlerTest { //temporary set a negative expiration time so the token is invalid immediately @Test public void testExpiredToken() throws Exception { - when(sessionJWTTokenHandler.getExpirationPeriod()).thenReturn(-99999999L); + when(configurationService.getLongProperty("jwt.session.token.expiration", 30)).thenReturn(-99999999L); when(ePersonClaimProvider.getEPerson(any(Context.class), any(JWTClaimsSet.class))).thenReturn(ePerson); Date previous = new Date(new Date().getTime() - 10000000000L); String token = sessionJWTTokenHandler From 9b2451754676740535841001593b6be431e8c969 Mon Sep 17 00:00:00 2001 From: Andrea Bollini Date: Thu, 18 Jun 2020 23:49:25 +0200 Subject: [PATCH 069/143] Fix obviously wrong implementation --- .../SubmissionUploadRestRepository.java | 38 ++++++------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionUploadRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionUploadRestRepository.java index 3ea5989f5a..a359a7ec4f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionUploadRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionUploadRestRepository.java @@ -8,6 +8,7 @@ package org.dspace.app.rest.repository; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import org.apache.commons.lang3.StringUtils; @@ -16,10 +17,6 @@ import org.dspace.app.rest.model.AccessConditionOptionRest; import org.dspace.app.rest.model.SubmissionUploadRest; import org.dspace.app.rest.projection.Projection; import org.dspace.app.rest.utils.DateMathParser; -import org.dspace.app.util.SubmissionConfig; -import org.dspace.app.util.SubmissionConfigReader; -import org.dspace.app.util.SubmissionConfigReaderException; -import org.dspace.app.util.SubmissionStepConfig; import org.dspace.core.Context; import org.dspace.eperson.Group; import org.dspace.eperson.service.GroupService; @@ -28,7 +25,6 @@ import org.dspace.submit.model.UploadConfiguration; import org.dspace.submit.model.UploadConfigurationService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Component; @@ -45,8 +41,6 @@ public class SubmissionUploadRestRepository extends DSpaceRestRepository findAll(Context context, Pageable pageable) { - List subConfs = new ArrayList(); - subConfs = submissionConfigReader.getAllSubmissionConfigs(pageable.getPageSize(), - Math.toIntExact(pageable.getOffset())); + Collection uploadConfigs = uploadConfigurationService.getMap().values(); Projection projection = utils.obtainProjection(); List results = new ArrayList<>(); - for (SubmissionConfig config : subConfs) { - for (int i = 0; i < config.getNumberOfSteps(); i++) { - SubmissionStepConfig step = config.getStep(i); - if (SubmissionStepConfig.UPLOAD_STEP_NAME.equals(step.getType())) { - UploadConfiguration uploadConfig = uploadConfigurationService.getMap().get(step.getId()); - if (uploadConfig != null) { - try { - results.add(convert(context, uploadConfig, projection)); - } catch (Exception e) { - log.error(e.getMessage(), e); - } - } + List configNames = new ArrayList(); + for (UploadConfiguration uploadConfig : uploadConfigs) { + if (!configNames.contains(uploadConfig.getName())) { + configNames.add(uploadConfig.getName()); + try { + results.add(convert(context, uploadConfig, projection)); + } catch (Exception e) { + log.error(e.getMessage(), e); } } } - return new PageImpl(results, pageable, results.size()); + return utils.getPage(results, pageable); } @Override From e4d61191e61ddf7f994c6fea4c4b699ead688488 Mon Sep 17 00:00:00 2001 From: Andrea Bollini Date: Thu, 18 Jun 2020 23:52:49 +0200 Subject: [PATCH 070/143] Disable test failing due to bug in the Mock library --- .../ContentLanguageHeaderResponseFilter.java | 23 +++++++++---------- .../dspace/app/rest/LanguageSupportIT.java | 5 +++- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/filter/ContentLanguageHeaderResponseFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/filter/ContentLanguageHeaderResponseFilter.java index 6552ae4f7f..74ffd73ad4 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/filter/ContentLanguageHeaderResponseFilter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/filter/ContentLanguageHeaderResponseFilter.java @@ -22,7 +22,9 @@ import org.springframework.stereotype.Component; /** * This filter assures that when the dspace instance supports multiple languages - * they are noted in the Content-Language Header of the response + * they are noted in the Content-Language Header of the response. Where + * appropriate the single endpoint can set the Content-Language header directly + * to note that the response is specific for a language * * @author Mykhaylo Boychuk (at 4science.it) */ @@ -36,20 +38,17 @@ public class ContentLanguageHeaderResponseFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - chain.doFilter(request, response); - HttpServletResponse httpServletResponse = (HttpServletResponse) response; - if (!httpServletResponse.containsHeader("Content-Language")) { - Locale[] locales = I18nUtil.getSupportedLocales(); - StringBuilder locsStr = new StringBuilder(); - for (Locale locale : locales) { - if (locsStr.length() > 0) { - locsStr.append(","); - } - locsStr.append(locale.getLanguage()); + Locale[] locales = I18nUtil.getSupportedLocales(); + StringBuilder locsStr = new StringBuilder(); + for (Locale locale : locales) { + if (locsStr.length() > 0) { + locsStr.append(","); } - httpServletResponse.setHeader("Content-Language", locsStr.toString()); + locsStr.append(locale.getLanguage()); } + httpServletResponse.setHeader("Content-Language", locsStr.toString()); + chain.doFilter(request, response); } @Override diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/LanguageSupportIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/LanguageSupportIT.java index 2dc06eca8b..cc8af92e2b 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/LanguageSupportIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/LanguageSupportIT.java @@ -18,6 +18,7 @@ import org.dspace.content.authority.ChoiceAuthorityServiceImpl; import org.dspace.core.LegacyPluginServiceImpl; import org.dspace.eperson.EPerson; import org.dspace.services.ConfigurationService; +import org.junit.Ignore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -42,6 +43,8 @@ public class LanguageSupportIT extends AbstractControllerIntegrationTest { } @Test + @Ignore("This test fails due to a bug in the MockHttpResponseServlet," + + " see https://github.com/spring-projects/spring-framework/issues/25281") public void checkEnabledMultipleLanguageSupportTest() throws Exception { context.turnOffAuthorisationSystem(); String[] supportedLanguage = {"uk","it"}; @@ -75,7 +78,7 @@ public class LanguageSupportIT extends AbstractControllerIntegrationTest { .andExpect(header().stringValues("Content-Language","uk, it")); getClient(tokenEPersonFR).perform(get("/api").locale(it)) - .andExpect(header().stringValues("Content-Language","en")); + .andExpect(header().stringValues("Content-Language","uk, it")); configurationService.setProperty("webui.supported.locales",null); legacyPluginService.clearNamedPluginClasses(); From c23a5b64de597dc78212522b775289e530470ea2 Mon Sep 17 00:00:00 2001 From: Andrea Bollini Date: Fri, 19 Jun 2020 09:35:40 +0200 Subject: [PATCH 071/143] Improve error handling and test env clenaup --- .../SubmissionUploadRestRepository.java | 35 +++++++++++++++---- .../app/rest/SubmissionFormsControllerIT.java | 11 +++--- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionUploadRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionUploadRestRepository.java index a359a7ec4f..25ac640d49 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionUploadRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionUploadRestRepository.java @@ -7,6 +7,8 @@ */ package org.dspace.app.rest.repository; +import java.sql.SQLException; +import java.text.ParseException; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -89,20 +91,31 @@ public class SubmissionUploadRestRepository extends DSpaceRestRepository Date: Fri, 19 Jun 2020 09:56:34 +0200 Subject: [PATCH 072/143] [Task 71442] applied feedback to the new registration endpoint --- .../app/rest/EPersonRestRepositoryIT.java | 24 +++++++++++-------- .../EPersonRegistrationFeatureIT.java | 7 ------ .../jackson/IgnoreJacksonWriteOnlyAccess.java | 11 +++++++++ 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java index ebbb26ad7a..b904429e1f 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java @@ -2071,18 +2071,25 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isCreated()); String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); - // We need to create this json manually to support actually setting the password to a value. - // When using the mapper to write an EPersonRest object to JSON to pass it along, the password gets lost - String json = "{\"metadata\":{\"eperson.firstname\":[{\"value\":\"John\"}]," + - "\"eperson.lastname\":[{\"value\":\"Doe\"}]},\"password\":\"somePassword\"," + - "\"type\":\"eperson\"}"; - + EPersonRest ePersonRest = new EPersonRest(); + MetadataRest metadataRest = new MetadataRest(); + ePersonRest.setCanLogIn(true); + MetadataValueRest surname = new MetadataValueRest(); + surname.setValue("Doe"); + metadataRest.put("eperson.lastname", surname); + MetadataValueRest firstname = new MetadataValueRest(); + firstname.setValue("John"); + metadataRest.put("eperson.firstname", firstname); + ePersonRest.setMetadata(metadataRest); + ePersonRest.setPassword("somePassword"); AtomicReference idRef = new AtomicReference(); + mapper.setAnnotationIntrospector(new IgnoreJacksonWriteOnlyAccess()); + try { getClient().perform(post("/api/eperson/epersons") .param("token", newRegisterToken) - .content(json) + .content(mapper.writeValueAsBytes(ePersonRest)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isCreated()) .andExpect(jsonPath("$", Matchers.allOf( @@ -2233,9 +2240,6 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { assertTrue(ePersonService.checkPassword(context, createdEPerson, "somePassword")); assertNull(registrationDataService.findByToken(context, newRegisterToken)); - context.turnOffAuthorisationSystem(); - context.restoreAuthSystemState(); - context.turnOffAuthorisationSystem(); registrationDataService.deleteByToken(context, newRegisterToken); context.restoreAuthSystemState(); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EPersonRegistrationFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EPersonRegistrationFeatureIT.java index 754804edf4..528ab24eb2 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EPersonRegistrationFeatureIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EPersonRegistrationFeatureIT.java @@ -76,18 +76,13 @@ public class EPersonRegistrationFeatureIT extends AbstractControllerIntegrationT SiteRest SiteRest = siteConverter.convert(site, Projection.DEFAULT); String siteUri = utils.linkToSingleResource(SiteRest, "self").getHref(); - context.turnOffAuthorisationSystem(); configurationService.setProperty("user.registration", false); - context.restoreAuthSystemState(); getClient().perform(get("/api/authz/authorizations/search/objectAndFeature") .param("uri", siteUri) .param("feature", epersonRegistrationFeature.getName())) .andExpect(status().isNoContent()); - context.turnOffAuthorisationSystem(); - configurationService.setProperty("user.registration", true); - context.restoreAuthSystemState(); } @@ -104,10 +99,8 @@ public class EPersonRegistrationFeatureIT extends AbstractControllerIntegrationT .param("feature", epersonRegistrationFeature.getName())) .andExpect(status().isOk()); - context.turnOffAuthorisationSystem(); //Enable Shibboleth and password login configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", SHIB_ONLY); - context.restoreAuthSystemState(); getClient().perform(get("/api/authz/authorizations/search/objectAndFeature") .param("uri", siteUri) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/jackson/IgnoreJacksonWriteOnlyAccess.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/jackson/IgnoreJacksonWriteOnlyAccess.java index 577ed6e4d5..df68ce8ab3 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/jackson/IgnoreJacksonWriteOnlyAccess.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/jackson/IgnoreJacksonWriteOnlyAccess.java @@ -11,6 +11,17 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.introspect.Annotated; import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; +/** + * This is a custom JacksonAnnotationIntrospector which allows us to ignore `@JsonProperty(access = Access + * .WRITE_ONLY)` annotations in our tests. + * Normally, this annotation allows the property to be written to (during deserialization), + * but does NOT allow it to be read (during serialization). + * In some tests, we need to ignore this annotation so that the test can use/verify the property + * during both serialization & deserialization. + * + * In order to use this class in a test, assign it the the current mapper like this: + * mapper.setAnnotationIntrospector(new IgnoreJacksonWriteOnlyAccess()); + */ public class IgnoreJacksonWriteOnlyAccess extends JacksonAnnotationIntrospector { @Override From de6bc7d8d024b88d9b69da5032c0ba7edb9c854c Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Fri, 19 Jun 2020 10:18:28 +0200 Subject: [PATCH 073/143] [Task 71442] fixed compile error after merge --- .../EPersonRestPermissionEvaluatorPlugin.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestPermissionEvaluatorPlugin.java index 14bde8e666..50f209cedc 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestPermissionEvaluatorPlugin.java @@ -8,6 +8,7 @@ package org.dspace.app.rest.security; import java.io.Serializable; +import java.sql.SQLException; import java.util.List; import java.util.UUID; import javax.servlet.http.HttpServletRequest; @@ -69,16 +70,20 @@ public class EPersonRestPermissionEvaluatorPlugin extends RestObjectPermissionEv UUID dsoId = UUID.fromString(targetId.toString()); // anonymous user - if (ePerson == null) { - return false; + try { + if (ePerson == null) { + return false; } else if (dsoId.equals(ePerson.getID())) { return true; } else if (authorizeService.isCommunityAdmin(context, ePerson) - && AuthorizeUtil.canCommunityAdminManageAccounts()) { + && AuthorizeUtil.canCommunityAdminManageAccounts()) { return true; } else if (authorizeService.isCollectionAdmin(context, ePerson) - && AuthorizeUtil.canCollectionAdminManageAccounts()) { - return true; + && AuthorizeUtil.canCollectionAdminManageAccounts()) { + return true; + } + } catch (SQLException e) { + log.error(e.getMessage(), e); } From b4b56750fd95146dae01f322ac8ae174b5a106d4 Mon Sep 17 00:00:00 2001 From: Peter Nijs Date: Fri, 19 Jun 2020 10:25:17 +0200 Subject: [PATCH 074/143] 71410: Authorization for Downloads of restricted Bitstreams - Internal feedback --- .../rest/AuthenticationRestController.java | 7 +- .../AuthenticationTokenConverter.java | 13 ++- .../model/wrapper/AuthenticationToken.java | 28 +++++ .../rest/security/CustomLogoutHandler.java | 3 +- .../security/RestAuthenticationService.java | 3 +- .../rest/security/jwt/JWTTokenHandler.java | 2 +- ...JWTTokenRestAuthenticationServiceImpl.java | 11 +- .../jwt/ShortLivedJWTTokenHandler.java | 2 +- .../security/jwt/JWTTokenHandlerTest.java | 20 ++-- .../jwt/ShortLivedJWTTokenHandlerTest.java | 106 ++++++++++++++++++ dspace/config/modules/authentication.cfg | 1 + 11 files changed, 167 insertions(+), 29 deletions(-) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/model/wrapper/AuthenticationToken.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandlerTest.java diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java index f1f8d495c3..0b60fa1cd4 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java @@ -22,6 +22,7 @@ import org.dspace.app.rest.model.EPersonRest; import org.dspace.app.rest.model.hateoas.AuthenticationStatusResource; import org.dspace.app.rest.model.hateoas.AuthenticationTokenResource; import org.dspace.app.rest.model.hateoas.AuthnResource; +import org.dspace.app.rest.model.wrapper.AuthenticationToken; import org.dspace.app.rest.projection.Projection; import org.dspace.app.rest.security.RestAuthenticationService; import org.dspace.app.rest.utils.ContextUtil; @@ -122,7 +123,7 @@ public class AuthenticationRestController implements InitializingBean { } /** - * This method will generate a short lived token to be used for bitstream downloads. + * This method will generate a short lived token to be used for bitstream downloads among other things. * * curl -v -X POST https://{dspace-server.url}/api/authn/shortlivedtokens -H "Authorization: Bearer eyJhbG...COdbo" * @@ -139,9 +140,9 @@ public class AuthenticationRestController implements InitializingBean { @RequestMapping(value = "/shortlivedtokens", method = RequestMethod.POST) public AuthenticationTokenResource shortLivedLogin(HttpServletRequest request) { Projection projection = utils.obtainProjection(); - String shortLivedToken = + AuthenticationToken shortLivedToken = restAuthenticationService.getShortLivedAuthenticationToken(ContextUtil.obtainContext(request), request); - AuthenticationTokenRest authenticationTokenRest = converter.toRest(shortLivedToken, projection); + AuthenticationTokenRest authenticationTokenRest = converter.toRest(shortLivedToken, projection); return converter.toResource(authenticationTokenRest); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/AuthenticationTokenConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/AuthenticationTokenConverter.java index a9f9d7208b..ea64bc8bc8 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/AuthenticationTokenConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/AuthenticationTokenConverter.java @@ -8,23 +8,24 @@ package org.dspace.app.rest.converter; import org.dspace.app.rest.model.AuthenticationTokenRest; +import org.dspace.app.rest.model.wrapper.AuthenticationToken; import org.dspace.app.rest.projection.Projection; import org.springframework.stereotype.Component; /** - * This is the converter from the AuthenticationToken string to tge REST data model + * This is the converter from the AuthenticationToken to the REST data model */ @Component -public class AuthenticationTokenConverter implements DSpaceConverter { +public class AuthenticationTokenConverter implements DSpaceConverter { @Override - public AuthenticationTokenRest convert(String modelObject, Projection projection) { + public AuthenticationTokenRest convert(AuthenticationToken modelObject, Projection projection) { AuthenticationTokenRest token = new AuthenticationTokenRest(); - token.setToken(modelObject); + token.setToken(modelObject.getToken()); return token; } @Override - public Class getModelClass() { - return String.class; + public Class getModelClass() { + return AuthenticationToken.class; } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/wrapper/AuthenticationToken.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/wrapper/AuthenticationToken.java new file mode 100644 index 0000000000..30301ffd77 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/wrapper/AuthenticationToken.java @@ -0,0 +1,28 @@ +/** + * 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.wrapper; + +/** + * This class represents an authentication token. It acts as a wrapper for a String object to differentiate between + * actual Strings and AuthenticationToken + */ +public class AuthenticationToken { + private String token; + + public AuthenticationToken(String token) { + this.token = token; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/CustomLogoutHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/CustomLogoutHandler.java index 19b3e2f4f0..b3f4a00d37 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/CustomLogoutHandler.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/CustomLogoutHandler.java @@ -10,7 +10,6 @@ package org.dspace.app.rest.security; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.dspace.app.rest.security.jwt.SessionJWTTokenHandler; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.core.Context; import org.slf4j.Logger; @@ -29,7 +28,7 @@ import org.springframework.stereotype.Component; @Component public class CustomLogoutHandler implements LogoutHandler { - private static final Logger log = LoggerFactory.getLogger(SessionJWTTokenHandler.class); + private static final Logger log = LoggerFactory.getLogger(CustomLogoutHandler.class); @Autowired private RestAuthenticationService restAuthenticationService; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/RestAuthenticationService.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/RestAuthenticationService.java index 622070e77c..6270fa2851 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/RestAuthenticationService.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/RestAuthenticationService.java @@ -11,6 +11,7 @@ import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.dspace.app.rest.model.wrapper.AuthenticationToken; import org.dspace.authenticate.service.AuthenticationService; import org.dspace.core.Context; import org.dspace.eperson.EPerson; @@ -28,7 +29,7 @@ public interface RestAuthenticationService { void addAuthenticationDataForUser(HttpServletRequest request, HttpServletResponse response, DSpaceAuthentication authentication, boolean addCookie) throws IOException; - String getShortLivedAuthenticationToken(Context context, HttpServletRequest request); + AuthenticationToken getShortLivedAuthenticationToken(Context context, HttpServletRequest request); EPerson getAuthenticatedEPerson(HttpServletRequest request, Context context); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java index 47a869105d..bec41c52ed 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java @@ -224,7 +224,7 @@ public abstract class JWTTokenHandler { } public long getExpirationPeriod() { - return configurationService.getLongProperty(getTokenExpirationConfigurationKey(), 30); + return configurationService.getLongProperty(getTokenExpirationConfigurationKey(), 1800000); } public boolean isEncryptionEnabled() { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java index f32e221adf..123f96f89b 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java @@ -18,6 +18,7 @@ import javax.servlet.http.HttpServletResponse; import com.nimbusds.jose.JOSEException; import org.apache.commons.lang3.StringUtils; +import org.dspace.app.rest.model.wrapper.AuthenticationToken; import org.dspace.app.rest.security.DSpaceAuthentication; import org.dspace.app.rest.security.RestAuthenticationService; import org.dspace.app.rest.utils.ContextUtil; @@ -89,26 +90,26 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication } /** - * Create a short-lived token for bitstream downloads + * Create a short-lived token for bitstream downloads among other things * @param context The context for which to create the token * @param request The request for which to create the token * @return The token with a short lifespan */ @Override - public String getShortLivedAuthenticationToken(Context context, HttpServletRequest request) { - String token = null; + public AuthenticationToken getShortLivedAuthenticationToken(Context context, HttpServletRequest request) { try { + String token; List groups = authenticationService.getSpecialGroups(context, request); token = shortLivedJWTTokenHandler.createTokenForEPerson(context, request, null, groups); context.commit(); - return token; + return new AuthenticationToken(token); } catch (JOSEException e) { log.error("JOSE Exception", e); } catch (SQLException e) { log.error("SQL error when adding authentication", e); } - return token; + return null; } @Override diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java index 95fd52530c..de8e851118 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java @@ -22,7 +22,7 @@ import org.springframework.stereotype.Component; /** * Class responsible for creating and parsing JSON Web Tokens (JWTs) used for bitstream - * dowloads, supports both JWS and JWE https://jwt.io/ . + * downloads among other things, supports both JWS and JWE https://jwt.io/ . */ @Component public class ShortLivedJWTTokenHandler extends JWTTokenHandler { diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/JWTTokenHandlerTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/JWTTokenHandlerTest.java index 6ae4af8293..1a33ae7484 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/JWTTokenHandlerTest.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/JWTTokenHandlerTest.java @@ -47,31 +47,31 @@ public class JWTTokenHandlerTest { @InjectMocks @Spy - SessionJWTTokenHandler sessionJWTTokenHandler; + private SessionJWTTokenHandler sessionJWTTokenHandler; @Mock - private ConfigurationService configurationService; + protected ConfigurationService configurationService; @Mock - private Context context; + protected Context context; @Mock - private EPerson ePerson; + protected EPerson ePerson; @Mock - private HttpServletRequest httpServletRequest; + protected HttpServletRequest httpServletRequest; @Mock - private EPersonService ePersonService; + protected EPersonService ePersonService; @Mock - private EPersonClaimProvider ePersonClaimProvider; + protected EPersonClaimProvider ePersonClaimProvider; @Mock - private ClientInfoService clientInfoService; + protected ClientInfoService clientInfoService; @Spy - private List jwtClaimProviders = new ArrayList<>(); + protected List jwtClaimProviders = new ArrayList<>(); @Before public void setUp() throws Exception { @@ -112,7 +112,7 @@ public class JWTTokenHandlerTest { //temporary set a negative expiration time so the token is invalid immediately @Test public void testExpiredToken() throws Exception { - when(configurationService.getLongProperty("jwt.session.token.expiration", 30)).thenReturn(-99999999L); + when(configurationService.getLongProperty("jwt.session.token.expiration", 1800000)).thenReturn(-99999999L); when(ePersonClaimProvider.getEPerson(any(Context.class), any(JWTClaimsSet.class))).thenReturn(ePerson); Date previous = new Date(new Date().getTime() - 10000000000L); String token = sessionJWTTokenHandler diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandlerTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandlerTest.java new file mode 100644 index 0000000000..38176ba57c --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandlerTest.java @@ -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.security.jwt; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Date; + +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.crypto.keygen.KeyGenerators; +import org.springframework.security.crypto.keygen.StringKeyGenerator; + +/** + * Test suite for the short lived authentication token + */ +@RunWith(MockitoJUnitRunner.class) +public class ShortLivedJWTTokenHandlerTest extends JWTTokenHandlerTest { + @InjectMocks + @Spy + private ShortLivedJWTTokenHandler shortLivedJWTTokenHandler; + + @Test + public void testJWTNoEncryption() throws Exception { + Date previous = new Date(System.currentTimeMillis() - 10000000000L); + String token = shortLivedJWTTokenHandler + .createTokenForEPerson(context, new MockHttpServletRequest(), previous, new ArrayList<>()); + SignedJWT signedJWT = SignedJWT.parse(token); + String personId = (String) signedJWT.getJWTClaimsSet().getClaim(EPersonClaimProvider.EPERSON_ID); + assertEquals("epersonID", personId); + } + + @Test(expected = ParseException.class) + public void testJWTEncrypted() throws Exception { + when(shortLivedJWTTokenHandler.isEncryptionEnabled()).thenReturn(true); + Date previous = new Date(System.currentTimeMillis() - 10000000000L); + StringKeyGenerator keyGenerator = KeyGenerators.string(); + when(configurationService.getProperty("jwt.shortLived.encryption.secret")) + .thenReturn(keyGenerator.generateKey()); + String token = shortLivedJWTTokenHandler + .createTokenForEPerson(context, new MockHttpServletRequest(), previous, new ArrayList<>()); + SignedJWT signedJWT = SignedJWT.parse(token); + } + + //temporary set a negative expiration time so the token is invalid immediately + @Test + public void testExpiredToken() throws Exception { + when(configurationService.getLongProperty("jwt.shortLived.token.expiration", 1800000)) + .thenReturn(-99999999L); + when(ePersonClaimProvider.getEPerson(any(Context.class), any(JWTClaimsSet.class))).thenReturn(ePerson); + Date previous = new Date(new Date().getTime() - 10000000000L); + String token = shortLivedJWTTokenHandler + .createTokenForEPerson(context, new MockHttpServletRequest(), previous, new ArrayList<>()); + EPerson parsed = shortLivedJWTTokenHandler.parseEPersonFromToken(token, httpServletRequest, context); + assertEquals(null, parsed); + + } + + //Try if we can change the expiration date + @Test + public void testTokenTampering() throws Exception { + when(shortLivedJWTTokenHandler.getExpirationPeriod()).thenReturn(-99999999L); + when(ePersonClaimProvider.getEPerson(any(Context.class), any(JWTClaimsSet.class))).thenReturn(ePerson); + Date previous = new Date(new Date().getTime() - 10000000000L); + String token = shortLivedJWTTokenHandler + .createTokenForEPerson(context, new MockHttpServletRequest(), previous, new ArrayList<>()); + JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().claim("eid", "epersonID").expirationTime( + new Date(System.currentTimeMillis() + 99999999)).build(); + String tamperedPayload = new String(Base64.getUrlEncoder().encode(jwtClaimsSet.toString().getBytes())); + String[] splitToken = token.split("\\."); + String tamperedToken = splitToken[0] + "." + tamperedPayload + "." + splitToken[2]; + EPerson parsed = shortLivedJWTTokenHandler.parseEPersonFromToken(tamperedToken, httpServletRequest, context); + assertEquals(null, parsed); + } + + @Test + public void testInvalidatedToken() throws Exception { + Date previous = new Date(System.currentTimeMillis() - 10000000000L); + // create a new token + String token = shortLivedJWTTokenHandler + .createTokenForEPerson(context, new MockHttpServletRequest(), previous, new ArrayList<>()); + // immediately invalidate it + shortLivedJWTTokenHandler.invalidateToken(token, new MockHttpServletRequest(), context); + // Check if it is still valid by trying to parse the EPerson from it (should return null) + EPerson parsed = shortLivedJWTTokenHandler.parseEPersonFromToken(token, httpServletRequest, context); + assertEquals(null, parsed); + } +} diff --git a/dspace/config/modules/authentication.cfg b/dspace/config/modules/authentication.cfg index 3a5e618721..e3077f4a57 100644 --- a/dspace/config/modules/authentication.cfg +++ b/dspace/config/modules/authentication.cfg @@ -83,6 +83,7 @@ jwt.session.token.include.ip = true #---------------------------------------------------------------# #---Stateless JWT Authentication for downloads of bitstreams----# +#----------------------among other things-----------------------# #---------------------------------------------------------------# # Server key part that is a part of the key used to sign the authentication tokens. From 6c91e5acf05be11a60b8fa62d99b49099afbaf91 Mon Sep 17 00:00:00 2001 From: Kevin Van de Velde Date: Fri, 19 Jun 2020 12:03:42 +0200 Subject: [PATCH 075/143] Authorization for file downloads: Small comment changes --- .../dspace/app/rest/security/RestAuthenticationService.java | 6 ++++++ .../app/rest/security/jwt/ShortLivedJWTTokenHandler.java | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/RestAuthenticationService.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/RestAuthenticationService.java index 6270fa2851..88b1d26524 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/RestAuthenticationService.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/RestAuthenticationService.java @@ -29,6 +29,12 @@ public interface RestAuthenticationService { void addAuthenticationDataForUser(HttpServletRequest request, HttpServletResponse response, DSpaceAuthentication authentication, boolean addCookie) throws IOException; + /** + * Retrieve a short lived authentication token, this can be used (among other things) for file downloads + * @param context the DSpace context + * @param request The current client request + * @return An AuthenticationToken that contains a string with the token + */ AuthenticationToken getShortLivedAuthenticationToken(Context context, HttpServletRequest request); EPerson getAuthenticatedEPerson(HttpServletRequest request, Context context); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java index de8e851118..375bfe4ae5 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java @@ -50,7 +50,7 @@ public class ShortLivedJWTTokenHandler extends JWTTokenHandler { Date expirationTime = jwtClaimsSet.getExpirationTime(); return signedJWT.verify(verifier) && expirationTime != null - //Ensure expiration timestamp is after the current time, with a minute of acceptable clock skew. + //Ensure expiration timestamp is after the current time && DateUtils.isAfter(expirationTime, new Date(), 0); } } From 08abaf8b039a5c9a152ae296b1e1961a2af6d983 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Fri, 19 Jun 2020 14:05:14 +0200 Subject: [PATCH 076/143] 71440: Implement feedback --- ... => SubmissionCCLicenseUrlRepository.java} | 53 +++++++++++++------ ...ionCCLicenseUrlResourceHalLinkFactory.java | 22 +++++--- .../model/SubmissionCCLicenseUrlRest.java | 9 ++-- .../impl/CCLicenseRemovePatchOperation.java | 11 +++- .../rest/CCLicenseRemovePatchOperationIT.java | 3 +- ...> SubmissionCCLicenseUrlRepositoryIT.java} | 22 ++++---- 6 files changed, 79 insertions(+), 41 deletions(-) rename dspace-server-webapp/src/main/java/org/dspace/app/rest/{SubmissionCCLicenseSearchController.java => SubmissionCCLicenseUrlRepository.java} (65%) rename dspace-server-webapp/src/test/java/org/dspace/app/rest/{SubmissionCCLicenseSearchControllerIT.java => SubmissionCCLicenseUrlRepositoryIT.java} (77%) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseSearchController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseUrlRepository.java similarity index 65% rename from dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseSearchController.java rename to dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseUrlRepository.java index 76ed1021d3..ff76183506 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseSearchController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseUrlRepository.java @@ -14,28 +14,28 @@ import javax.servlet.ServletRequest; import org.apache.commons.lang3.StringUtils; import org.dspace.app.rest.converter.ConverterService; import org.dspace.app.rest.exception.DSpaceBadRequestException; -import org.dspace.app.rest.model.SubmissionCCLicenseRest; +import org.dspace.app.rest.exception.RepositoryMethodNotImplementedException; import org.dspace.app.rest.model.SubmissionCCLicenseUrlRest; -import org.dspace.app.rest.model.hateoas.SubmissionCCLicenseUrlResource; +import org.dspace.app.rest.repository.DSpaceRestRepository; import org.dspace.app.rest.utils.Utils; +import org.dspace.core.Context; import org.dspace.license.service.CreativeCommonsService; import org.dspace.services.RequestService; import org.dspace.utils.DSpace; import org.springframework.beans.factory.annotation.Autowired; +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.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.stereotype.Component; /** - * This controller is responsible for searching the CC License URI + * This Repository is responsible for handling the CC License URIs. + * It only supports a search method */ -@RestController -@RequestMapping("/api/" + SubmissionCCLicenseRest.CATEGORY + "/" + SubmissionCCLicenseRest.PLURAL + "/search" + - "/rightsByQuestions") -@PreAuthorize("hasAuthority('AUTHENTICATED')") -public class SubmissionCCLicenseSearchController { + +@Component(SubmissionCCLicenseUrlRest.CATEGORY + "." + SubmissionCCLicenseUrlRest.NAME) +public class SubmissionCCLicenseUrlRepository extends DSpaceRestRepository { @Autowired protected Utils utils; @@ -52,10 +52,11 @@ public class SubmissionCCLicenseSearchController { * Retrieves the CC License URI based on the license ID and answers in the field questions, provided as parameters * to this request * - * @return the CC License URI as a SubmissionCCLicenseUrlResource + * @return the CC License URI as a SubmissionCCLicenseUrlRest */ - @RequestMapping(method = RequestMethod.GET) - public SubmissionCCLicenseUrlResource findByRightsByQuestions() { + @PreAuthorize("hasAuthority('AUTHENTICATED')") + @SearchRestMethod(name = "rightsByQuestions") + public SubmissionCCLicenseUrlRest findByRightsByQuestions() { ServletRequest servletRequest = requestService.getCurrentRequest() .getServletRequest(); Map requestParameterMap = servletRequest @@ -66,6 +67,9 @@ public class SubmissionCCLicenseSearchController { throw new DSpaceBadRequestException( "A \"license\" parameter needs to be provided."); } + + // Loop through parameters to find answer parameters, adding them to the parameterMap. Zero or more answers + // may exist, as some CC licenses do not require answers for (String parameter : requestParameterMap.keySet()) { if (StringUtils.startsWith(parameter, "answer_")) { String field = StringUtils.substringAfter(parameter, "answer_"); @@ -93,8 +97,25 @@ public class SubmissionCCLicenseSearchController { throw new ResourceNotFoundException("No CC License URI could be found for ID: " + licenseId); } - SubmissionCCLicenseUrlRest submissionCCLicenseUrlRest = converter.toRest(licenseUri, utils.obtainProjection()); - return converter.toResource(submissionCCLicenseUrlRest); + return converter.toRest(licenseUri, utils.obtainProjection()); } + + /** + * The findOne method is not supported in this repository + */ + public SubmissionCCLicenseUrlRest findOne(final Context context, final String s) { + throw new RepositoryMethodNotImplementedException(SubmissionCCLicenseUrlRest.NAME, "findOne"); + } + + /** + * The findAll method is not supported in this repository + */ + public Page findAll(final Context context, final Pageable pageable) { + throw new RepositoryMethodNotImplementedException(SubmissionCCLicenseUrlRest.NAME, "findAll"); + } + + public Class getDomainClass() { + return SubmissionCCLicenseUrlRest.class; + } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/process/SubmissionCCLicenseUrlResourceHalLinkFactory.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/process/SubmissionCCLicenseUrlResourceHalLinkFactory.java index cb44d68e73..07d5e46c61 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/process/SubmissionCCLicenseUrlResourceHalLinkFactory.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/process/SubmissionCCLicenseUrlResourceHalLinkFactory.java @@ -10,14 +10,16 @@ package org.dspace.app.rest.link.process; import java.util.LinkedList; import java.util.Map; -import org.dspace.app.rest.SubmissionCCLicenseSearchController; +import org.dspace.app.rest.RestResourceController; import org.dspace.app.rest.link.HalLinkFactory; +import org.dspace.app.rest.model.SubmissionCCLicenseUrlRest; import org.dspace.app.rest.model.hateoas.SubmissionCCLicenseUrlResource; import org.dspace.services.RequestService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Pageable; import org.springframework.hateoas.Link; import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.util.UriComponentsBuilder; /** @@ -25,16 +27,17 @@ import org.springframework.web.util.UriComponentsBuilder; */ @Component public class SubmissionCCLicenseUrlResourceHalLinkFactory - extends HalLinkFactory { + extends HalLinkFactory { @Autowired RequestService requestService; /** * Add a self link based on the search parameters - * @param halResource - The halResource - * @param pageable - The page information - * @param list - The list of present links + * + * @param halResource - The halResource + * @param pageable - The page information + * @param list - The list of present links * @throws Exception */ @Override @@ -46,7 +49,10 @@ public class SubmissionCCLicenseUrlResourceHalLinkFactory Map parameterMap = requestService.getCurrentRequest().getHttpServletRequest() .getParameterMap(); - UriComponentsBuilder uriComponentsBuilder = uriBuilder(getMethodOn().findByRightsByQuestions()); + + UriComponentsBuilder uriComponentsBuilder = uriBuilder(getMethodOn().executeSearchMethods( + SubmissionCCLicenseUrlRest.CATEGORY, SubmissionCCLicenseUrlRest.PLURAL, "rightsByQuestions", null, null, + null, null, new LinkedMultiValueMap<>())); for (String key : parameterMap.keySet()) { uriComponentsBuilder.queryParam(key, parameterMap.get(key)); } @@ -56,8 +62,8 @@ public class SubmissionCCLicenseUrlResourceHalLinkFactory @Override - protected Class getControllerClass() { - return SubmissionCCLicenseSearchController.class; + protected Class getControllerClass() { + return RestResourceController.class; } @Override diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseUrlRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseUrlRest.java index 46aa9bc705..77263ba317 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseUrlRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCCLicenseUrlRest.java @@ -9,7 +9,7 @@ package org.dspace.app.rest.model; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import org.dspace.app.rest.SubmissionCCLicenseSearchController; +import org.dspace.app.rest.RestResourceController; /** * This class is the REST representation of the CCLicense URL String object and acts as a data object @@ -17,6 +17,9 @@ import org.dspace.app.rest.SubmissionCCLicenseSearchController; */ public class SubmissionCCLicenseUrlRest extends BaseObjectRest { public static final String NAME = "submissioncclicenseUrl"; + public static final String PLURAL = "submissioncclicenseUrls"; + public static final String CATEGORY = RestAddressableModel.CONFIGURATION; + private String url; @@ -46,12 +49,12 @@ public class SubmissionCCLicenseUrlRest extends BaseObjectRest { @Override public String getCategory() { - return SubmissionCCLicenseRest.CATEGORY; + return SubmissionCCLicenseUrlRest.CATEGORY; } @Override @JsonIgnore public Class getController() { - return SubmissionCCLicenseSearchController.class; + return RestResourceController.class; } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/CCLicenseRemovePatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/CCLicenseRemovePatchOperation.java index 19229a4f72..add819b7a4 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/CCLicenseRemovePatchOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/CCLicenseRemovePatchOperation.java @@ -7,6 +7,7 @@ */ package org.dspace.app.rest.submit.factory.impl; +import org.apache.commons.lang3.StringUtils; import org.dspace.content.InProgressSubmission; import org.dspace.content.Item; import org.dspace.core.Context; @@ -44,7 +45,15 @@ public class CCLicenseRemovePatchOperation extends RemovePatchOperation void remove(Context context, Request currentRequest, InProgressSubmission source, String path, Object value) throws Exception { Item item = source.getItem(); - creativeCommonsService.removeLicense(context, item); + + + if (StringUtils.isNotBlank(creativeCommonsService.getLicenseName(item))) { + creativeCommonsService.removeLicense(context, item); + } else { + throw new IllegalArgumentException("No CC license can be removed since none is present on submission: " + + source.getID()); + } + } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseRemovePatchOperationIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseRemovePatchOperationIT.java index 40828a7667..3b05621f08 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseRemovePatchOperationIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CCLicenseRemovePatchOperationIT.java @@ -130,7 +130,6 @@ public class CCLicenseRemovePatchOperationIT extends AbstractControllerIntegrati getClient(epersonToken).perform(patch("/api/submission/workspaceitems/" + workspaceItem.getID()) .content(removePatch) .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.sections", not(hasJsonPath("cclicense")))); + .andExpect(status().isInternalServerError()); } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseSearchControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseUrlRepositoryIT.java similarity index 77% rename from dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseSearchControllerIT.java rename to dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseUrlRepositoryIT.java index 8a93e18a8e..84fe06ce19 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseSearchControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCCLicenseUrlRepositoryIT.java @@ -17,14 +17,14 @@ import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.junit.Test; /** - * Class to the methods from the SubmissionCCLicenseSearchController + * Class to the methods from the SubmissionCCLicenseUrlRepository * Since the CC Licenses and the corresponding URIs are obtained from the CC License API, a mock service has been * implemented. * This mock service will return a fixed set of CC Licenses using a similar structure to the ones obtained from the * CC License API. * Refer to {@link org.dspace.license.MockCCLicenseConnectorServiceImpl} for more information */ -public class SubmissionCCLicenseSearchControllerIT extends AbstractControllerIntegrationTest { +public class SubmissionCCLicenseUrlRepositoryIT extends AbstractControllerIntegrationTest { @Test @@ -32,28 +32,28 @@ public class SubmissionCCLicenseSearchControllerIT extends AbstractControllerInt String epersonToken = getAuthToken(eperson.getEmail(), password); getClient(epersonToken).perform(get( - "/api/config/submissioncclicenses/search/rightsByQuestions?license=license2&answer_license2-field0" + + "/api/config/submissioncclicenseUrls/search/rightsByQuestions?license=license2&answer_license2-field0" + "=license2-field0-enum1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.url", is("mock-license-uri"))) .andExpect(jsonPath("$.type", is("submissioncclicenseUrl"))) .andExpect(jsonPath("$._links.self.href", - is("http://localhost/api/config/submissioncclicenses/search/rightsByQuestions" + + is("http://localhost/api/config/submissioncclicenseUrls/search/rightsByQuestions" + "?license=license2" + "&answer_license2-field0=license2-field0-enum1"))); } @Test - public void searchRightsByQuestionsTestLicenseWithoutFields() throws Exception { + public void searchRightsByQuestionsTestLicenseForLicenseWithoutQuestions() throws Exception { String epersonToken = getAuthToken(eperson.getEmail(), password); getClient(epersonToken) - .perform(get("/api/config/submissioncclicenses/search/rightsByQuestions?license=license3")) + .perform(get("/api/config/submissioncclicenseUrls/search/rightsByQuestions?license=license3")) .andExpect(status().isOk()) .andExpect(jsonPath("$.url", is("mock-license-uri"))) .andExpect(jsonPath("$.type", is("submissioncclicenseUrl"))) .andExpect(jsonPath("$._links.self.href", - is("http://localhost/api/config/submissioncclicenses/search/rightsByQuestions" + + is("http://localhost/api/config/submissioncclicenseUrls/search/rightsByQuestions" + "?license=license3"))); } @@ -62,7 +62,7 @@ public class SubmissionCCLicenseSearchControllerIT extends AbstractControllerInt String epersonToken = getAuthToken(eperson.getEmail(), password); getClient(epersonToken).perform(get( - "/api/config/submissioncclicenses/search/rightsByQuestions?license=nonexisting-license" + + "/api/config/submissioncclicenseUrls/search/rightsByQuestions?license=nonexisting-license" + "&answer_license2-field0=license2-field0-enum1")) .andExpect(status().isNotFound()); } @@ -72,7 +72,7 @@ public class SubmissionCCLicenseSearchControllerIT extends AbstractControllerInt String epersonToken = getAuthToken(eperson.getEmail(), password); getClient(epersonToken).perform(get( - "/api/config/submissioncclicenses/search/rightsByQuestions?license=license1&answer_license1field0" + + "/api/config/submissioncclicenseUrls/search/rightsByQuestions?license=license1&answer_license1field0" + "=license1field0enum1")) .andExpect(status().isBadRequest()); } @@ -82,7 +82,7 @@ public class SubmissionCCLicenseSearchControllerIT extends AbstractControllerInt String epersonToken = getAuthToken(eperson.getEmail(), password); getClient(epersonToken).perform(get( - "/api/config/submissioncclicenses/search/rightsByQuestions?license=license2" + + "/api/config/submissioncclicenseUrls/search/rightsByQuestions?license=license2" + "&answer_license2field0=license2field0enum1&answer_nonexisting=test")) .andExpect(status().isBadRequest()); } @@ -91,7 +91,7 @@ public class SubmissionCCLicenseSearchControllerIT extends AbstractControllerInt public void searchRightsByQuestionsAdditionalUnAuthorized() throws Exception { getClient().perform(get( - "/api/config/submissioncclicenses/search/rightsByQuestions?license=license2&answer_license2-field0" + + "/api/config/submissioncclicenseUrls/search/rightsByQuestions?license=license2&answer_license2-field0" + "=license2-field0-enum1")) .andExpect(status().isUnauthorized()); From 891ab3f3e261e8a78321091b8835fab413937899 Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Fri, 19 Jun 2020 14:47:53 +0200 Subject: [PATCH 077/143] [Task 71442] fixed the tests without the additional cleanup required --- .../app/rest/RegistrationRestControllerIT.java | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java index da6f7a94d8..d02dceea20 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java @@ -12,19 +12,16 @@ import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import java.sql.SQLException; import java.util.Iterator; import java.util.List; import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.dspace.app.rest.model.RegistrationRest; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.eperson.RegistrationData; import org.dspace.eperson.dao.RegistrationDataDAO; import org.dspace.services.ConfigurationService; -import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -36,19 +33,6 @@ public class RegistrationRestControllerIT extends AbstractControllerIntegrationT @Autowired private ConfigurationService configurationService; - - @Before - public void setup() throws SQLException { - CollectionUtils.emptyIfNull(registrationDataDAO.findAll(context, RegistrationData.class)).stream() - .forEach(registrationData -> { - try { - registrationDataDAO.delete(context, registrationData); - } catch (SQLException e) { - throw new RuntimeException(e); - } - }); - } - @Test public void registrationFlowTest() throws Exception { List registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); @@ -119,5 +103,6 @@ public class RegistrationRestControllerIT extends AbstractControllerIntegrationT RegistrationData registrationData = iterator.next(); registrationDataDAO.delete(context, registrationData); } + context.complete(); } } From ffad77b43bbd07699c25e48ce1d39e3971c4bd5d Mon Sep 17 00:00:00 2001 From: Danilo Di Nuzzo Date: Mon, 22 Jun 2020 17:20:22 +0200 Subject: [PATCH 078/143] [CST-2877] fix submission multi-lang --- .../main/java/org/dspace/core/Context.java | 23 +++++++++++++++++++ .../AbstractDSpaceRestRepository.java | 22 ++++++++++++++---- .../SubmissionFormRestRepository.java | 14 +++++++---- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/core/Context.java b/dspace-api/src/main/java/org/dspace/core/Context.java index 11e388c727..3ede4ab5a0 100644 --- a/dspace-api/src/main/java/org/dspace/core/Context.java +++ b/dspace-api/src/main/java/org/dspace/core/Context.java @@ -68,6 +68,13 @@ public class Context implements AutoCloseable { */ private Locale currentLocale; + /** + * List of locale object indicating the locales that are + * acceptable to the client based on the Accept-Language header. + * The Locales are in decreasing order starting with the preferred locale. + */ + private List clientLocales; + /** * Extra log info */ @@ -876,4 +883,20 @@ public class Context implements AutoCloseable { private void reloadContextBoundEntities() throws SQLException { currentUser = reloadEntity(currentUser); } + + /** + * Returns a list of locale object indicating the locales that are + * acceptable to the client based on the Accept-Language header. + * The Locales are in decreasing order starting with the preferred locale. + * @return List + */ + public List getClientLocales() { + return clientLocales; + } + + + public void setClientLocales(List clientLocales) { + this.clientLocales = clientLocales; + } + } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AbstractDSpaceRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AbstractDSpaceRestRepository.java index 7f815ea8f2..57650fa905 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AbstractDSpaceRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AbstractDSpaceRestRepository.java @@ -7,9 +7,11 @@ */ package org.dspace.app.rest.repository; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; import java.util.Locale; -import org.apache.commons.lang3.StringUtils; import org.dspace.app.rest.converter.ConverterService; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.app.rest.utils.Utils; @@ -42,6 +44,7 @@ public abstract class AbstractDSpaceRestRepository { context = ContextUtil.obtainContext(currentRequest.getServletRequest()); Locale currentLocale = getLocal(context, currentRequest); context.setCurrentLocale(currentLocale); + context.setClientLocales(getLocalesFromRequest(currentRequest)); return context; } @@ -58,9 +61,9 @@ public abstract class AbstractDSpaceRestRepository { userLocale = new Locale(userLanguage); } } - String locale = request.getHttpServletRequest().getHeader("Accept-Language"); - if (StringUtils.isNotBlank(locale)) { - userLocale = new Locale(locale); + Enumeration locales = request.getHttpServletRequest().getLocales(); + if (locales != null) { + userLocale = locales.nextElement(); } if (userLocale == null) { return I18nUtil.getDefaultLocale(); @@ -68,4 +71,15 @@ public abstract class AbstractDSpaceRestRepository { supportedLocale = I18nUtil.getSupportedLocale(userLocale); return supportedLocale; } + + private List getLocalesFromRequest(Request request) { + List locales = new ArrayList<>(); + Enumeration reqLocales = request.getHttpServletRequest().getLocales(); + if (reqLocales != null) { + while (reqLocales.hasMoreElements()) { + locales.add(reqLocales.nextElement()); + } + } + return locales; + } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionFormRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionFormRestRepository.java index 434a8dc0fd..1070848917 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionFormRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionFormRestRepository.java @@ -46,11 +46,15 @@ public class SubmissionFormRestRepository extends DSpaceRestRepository clientLocales = context.getClientLocales(); + DCInputsReader inputReader = null; + for (Locale locale: clientLocales) { + inputReader = inputReaders.get(locale); + if (inputReader != null) { + break; + } + } + if (inputReader == null) { inputReader = defaultInputReader; } DCInputSet subConfs = inputReader.getInputsByFormName(submitName); From 8407a2f6888658f01f8485462aff4cf2ea2641ab Mon Sep 17 00:00:00 2001 From: Danilo Di Nuzzo Date: Tue, 23 Jun 2020 11:54:48 +0200 Subject: [PATCH 079/143] [CST-2877] fix --- .../main/java/org/dspace/core/Context.java | 22 ------------------- .../main/java/org/dspace/core/I18nUtil.java | 19 ++++++++++++++++ .../AbstractDSpaceRestRepository.java | 22 +++++++------------ .../SubmissionFormRestRepository.java | 10 ++------- 4 files changed, 29 insertions(+), 44 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/core/Context.java b/dspace-api/src/main/java/org/dspace/core/Context.java index 3ede4ab5a0..4ea314e108 100644 --- a/dspace-api/src/main/java/org/dspace/core/Context.java +++ b/dspace-api/src/main/java/org/dspace/core/Context.java @@ -68,13 +68,6 @@ public class Context implements AutoCloseable { */ private Locale currentLocale; - /** - * List of locale object indicating the locales that are - * acceptable to the client based on the Accept-Language header. - * The Locales are in decreasing order starting with the preferred locale. - */ - private List clientLocales; - /** * Extra log info */ @@ -884,19 +877,4 @@ public class Context implements AutoCloseable { currentUser = reloadEntity(currentUser); } - /** - * Returns a list of locale object indicating the locales that are - * acceptable to the client based on the Accept-Language header. - * The Locales are in decreasing order starting with the preferred locale. - * @return List - */ - public List getClientLocales() { - return clientLocales; - } - - - public void setClientLocales(List clientLocales) { - this.clientLocales = clientLocales; - } - } diff --git a/dspace-api/src/main/java/org/dspace/core/I18nUtil.java b/dspace-api/src/main/java/org/dspace/core/I18nUtil.java index 0d5e95d048..d06b1e9bc8 100644 --- a/dspace-api/src/main/java/org/dspace/core/I18nUtil.java +++ b/dspace-api/src/main/java/org/dspace/core/I18nUtil.java @@ -381,4 +381,23 @@ public class I18nUtil { } return resultList.toArray(new Locale[resultList.size()]); } + + /** + * Check if the input locale is in the list of supported locales + * @param locale + * @return true if locale is supported, false otherwise + */ + public static boolean isSupportedLocale(Locale locale) { + boolean isSupported = false; + Locale[] supportedLocales = getSupportedLocales(); + if (supportedLocales != null) { + for (Locale sLocale: supportedLocales) { + if (locale.getLanguage().equals(sLocale.getLanguage()) ) { + isSupported = true; + break; + } + } + } + return isSupported; + } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AbstractDSpaceRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AbstractDSpaceRestRepository.java index 57650fa905..6460424f1c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AbstractDSpaceRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AbstractDSpaceRestRepository.java @@ -7,9 +7,7 @@ */ package org.dspace.app.rest.repository; -import java.util.ArrayList; import java.util.Enumeration; -import java.util.List; import java.util.Locale; import org.dspace.app.rest.converter.ConverterService; @@ -44,7 +42,6 @@ public abstract class AbstractDSpaceRestRepository { context = ContextUtil.obtainContext(currentRequest.getServletRequest()); Locale currentLocale = getLocal(context, currentRequest); context.setCurrentLocale(currentLocale); - context.setClientLocales(getLocalesFromRequest(currentRequest)); return context; } @@ -61,9 +58,16 @@ public abstract class AbstractDSpaceRestRepository { userLocale = new Locale(userLanguage); } } + // Locales requested from client Enumeration locales = request.getHttpServletRequest().getLocales(); if (locales != null) { - userLocale = locales.nextElement(); + while (locales.hasMoreElements()) { + Locale current = locales.nextElement(); + if (I18nUtil.isSupportedLocale(current)) { + userLocale = current; + break; + } + } } if (userLocale == null) { return I18nUtil.getDefaultLocale(); @@ -72,14 +76,4 @@ public abstract class AbstractDSpaceRestRepository { return supportedLocale; } - private List getLocalesFromRequest(Request request) { - List locales = new ArrayList<>(); - Enumeration reqLocales = request.getHttpServletRequest().getLocales(); - if (reqLocales != null) { - while (reqLocales.hasMoreElements()) { - locales.add(reqLocales.nextElement()); - } - } - return locales; - } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionFormRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionFormRestRepository.java index 1070848917..770438e183 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionFormRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionFormRestRepository.java @@ -46,14 +46,8 @@ public class SubmissionFormRestRepository extends DSpaceRestRepository clientLocales = context.getClientLocales(); - DCInputsReader inputReader = null; - for (Locale locale: clientLocales) { - inputReader = inputReaders.get(locale); - if (inputReader != null) { - break; - } - } + Locale currentLocale = context.getCurrentLocale(); + DCInputsReader inputReader = inputReaders.get(currentLocale); if (inputReader == null) { inputReader = defaultInputReader; } From 95110d2b5a202aadba7395e2f598be7bbceb82db Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Tue, 23 Jun 2020 14:23:50 +0200 Subject: [PATCH 080/143] [Task 71440] changed the ModelObject for SubmissionCCLicenseUrlRest from String to SubmissionCCLicenseUrl --- .../SubmissionCCLicenseUrlRepository.java | 4 +- .../SubmissionCCLicenseUrlConverter.java | 15 +++--- .../model/wrapper/SubmissionCCLicenseUrl.java | 51 +++++++++++++++++++ 3 files changed, 62 insertions(+), 8 deletions(-) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/model/wrapper/SubmissionCCLicenseUrl.java diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseUrlRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseUrlRepository.java index ff76183506..62cd48d56e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseUrlRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseUrlRepository.java @@ -16,6 +16,7 @@ import org.dspace.app.rest.converter.ConverterService; import org.dspace.app.rest.exception.DSpaceBadRequestException; import org.dspace.app.rest.exception.RepositoryMethodNotImplementedException; import org.dspace.app.rest.model.SubmissionCCLicenseUrlRest; +import org.dspace.app.rest.model.wrapper.SubmissionCCLicenseUrl; import org.dspace.app.rest.repository.DSpaceRestRepository; import org.dspace.app.rest.utils.Utils; import org.dspace.core.Context; @@ -93,11 +94,12 @@ public class SubmissionCCLicenseUrlRepository extends DSpaceRestRepository { +public class SubmissionCCLicenseUrlConverter implements DSpaceConverter { /** * Convert a Submission CC License Url String to its REST representation - * @param modelObject - the CC License Url String to convert + * @param modelObject - the CC License Url object to convert * @param projection - the projection * @return the corresponding SubmissionCCLicenseUrlRest object */ @Override - public SubmissionCCLicenseUrlRest convert(final String modelObject, final Projection projection) { + public SubmissionCCLicenseUrlRest convert(SubmissionCCLicenseUrl modelObject, Projection projection) { SubmissionCCLicenseUrlRest submissionCCLicenseUrlRest = new SubmissionCCLicenseUrlRest(); - submissionCCLicenseUrlRest.setUrl(modelObject); - submissionCCLicenseUrlRest.setId(modelObject); + submissionCCLicenseUrlRest.setUrl(modelObject.getUrl()); + submissionCCLicenseUrlRest.setId(modelObject.getId()); return submissionCCLicenseUrlRest; } @Override - public Class getModelClass() { - return String.class; + public Class getModelClass() { + return SubmissionCCLicenseUrl.class; } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/wrapper/SubmissionCCLicenseUrl.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/wrapper/SubmissionCCLicenseUrl.java new file mode 100644 index 0000000000..4178e67c49 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/wrapper/SubmissionCCLicenseUrl.java @@ -0,0 +1,51 @@ +/** + * 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.wrapper; + +public class SubmissionCCLicenseUrl { + + private String url; + private String id; + + public SubmissionCCLicenseUrl(String url, String id) { + this.url = url; + this.id = id; + } + + /** + * Generic getter for the url + * @return the url value of this SubmissionCCLicenseUrl + */ + public String getUrl() { + return url; + } + + /** + * Generic setter for the url + * @param url The url to be set on this SubmissionCCLicenseUrl + */ + public void setUrl(String url) { + this.url = url; + } + + /** + * Generic getter for the id + * @return the id value of this SubmissionCCLicenseUrl + */ + public String getId() { + return id; + } + + /** + * Generic setter for the id + * @param id The id to be set on this SubmissionCCLicenseUrl + */ + public void setId(String id) { + this.id = id; + } +} From 9f9fe26549fc8d0dd17059b4f6d84bff6451badf Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Tue, 23 Jun 2020 15:09:45 +0200 Subject: [PATCH 081/143] [Task 71440] fixed checkstyle and added preAuthorize to the SubmissionCCLicenseUrlRepository --- .../org/dspace/app/rest/SubmissionCCLicenseUrlRepository.java | 1 + .../app/rest/converter/SubmissionCCLicenseUrlConverter.java | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseUrlRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseUrlRepository.java index 62cd48d56e..26703f320d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseUrlRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseUrlRepository.java @@ -106,6 +106,7 @@ public class SubmissionCCLicenseUrlRepository extends DSpaceRestRepository { +public class SubmissionCCLicenseUrlConverter + implements DSpaceConverter { /** * Convert a Submission CC License Url String to its REST representation From 6a88ef51b37338cd2f7ca28f3db54ee35cc54517 Mon Sep 17 00:00:00 2001 From: Peter Nijs Date: Tue, 23 Jun 2020 18:08:39 +0200 Subject: [PATCH 082/143] 71410: Authorization for Downloads of restricted Bitstreams - Don't update ePerson session salt when requesting a short lived token --- .../app/rest/security/jwt/JWTTokenHandler.java | 2 +- .../security/jwt/ShortLivedJWTTokenHandler.java | 12 ++++++++++++ .../jwt/ShortLivedJWTTokenHandlerTest.java | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java index bec41c52ed..ab24a7cbae 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java @@ -403,7 +403,7 @@ public abstract class JWTTokenHandler { * @return EPerson object of current user, with an updated session salt * @throws SQLException */ - private EPerson updateSessionSalt(final Context context, final Date previousLoginDate) throws SQLException { + protected EPerson updateSessionSalt(final Context context, final Date previousLoginDate) throws SQLException { EPerson ePerson; try { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java index 375bfe4ae5..902e391c30 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java @@ -17,6 +17,7 @@ import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; import com.nimbusds.jwt.util.DateUtils; import org.apache.commons.lang3.StringUtils; +import org.dspace.core.Context; import org.dspace.eperson.EPerson; import org.springframework.stereotype.Component; @@ -55,6 +56,17 @@ public class ShortLivedJWTTokenHandler extends JWTTokenHandler { } } + /** + * The session salt doesn't need to be updated for short lived tokens. + * @param context current DSpace Context + * @param previousLoginDate date of last login (prior to this one) + * @return EPerson object of current user, with an updated session salt + */ + @Override + protected EPerson updateSessionSalt(final Context context, final Date previousLoginDate) { + return context.getCurrentUser(); + } + @Override protected String getTokenSecretConfigurationKey() { return "jwt.shortLived.token.secret"; diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandlerTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandlerTest.java index 38176ba57c..795694b202 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandlerTest.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandlerTest.java @@ -15,14 +15,17 @@ import java.text.ParseException; import java.util.ArrayList; import java.util.Base64; import java.util.Date; +import javax.servlet.http.HttpServletRequest; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; import org.dspace.core.Context; import org.dspace.eperson.EPerson; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; +import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.mock.web.MockHttpServletRequest; @@ -38,6 +41,17 @@ public class ShortLivedJWTTokenHandlerTest extends JWTTokenHandlerTest { @Spy private ShortLivedJWTTokenHandler shortLivedJWTTokenHandler; + @Before + @Override + public void setUp() throws Exception { + when(ePerson.getSessionSalt()).thenReturn("01234567890123456789012345678901"); + when(context.getCurrentUser()).thenReturn(ePerson); + when(clientInfoService.getClientIp(any())).thenReturn("123.123.123.123"); + when(ePersonClaimProvider.getKey()).thenReturn("eid"); + when(ePersonClaimProvider.getValue(any(), Mockito.any(HttpServletRequest.class))).thenReturn("epersonID"); + jwtClaimProviders.add(ePersonClaimProvider); + } + @Test public void testJWTNoEncryption() throws Exception { Date previous = new Date(System.currentTimeMillis() - 10000000000L); From 2b6f19f6beb1f54b727b1a3d83c69b7179a55972 Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Wed, 24 Jun 2020 11:14:40 +0200 Subject: [PATCH 083/143] [Task 71440] added javadocs to SubmissionCCLicenseUrl --- .../model/wrapper/SubmissionCCLicenseUrl.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/wrapper/SubmissionCCLicenseUrl.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/wrapper/SubmissionCCLicenseUrl.java index 4178e67c49..68ff1166b4 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/wrapper/SubmissionCCLicenseUrl.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/wrapper/SubmissionCCLicenseUrl.java @@ -7,11 +7,28 @@ */ package org.dspace.app.rest.model.wrapper; +/** + * This class represents a model implementation for {@link org.dspace.app.rest.model.SubmissionCCLicenseUrlRest} + * This will simply store a url and an id. it'll be used to create an object with these variables out of information + * that came from the back-end. This object will then be used in the + * {@link org.dspace.app.rest.converter.SubmissionCCLicenseUrlConverter} to turn it into its REST object + */ public class SubmissionCCLicenseUrl { + /** + * The url for ths object + */ private String url; + /** + * The id for this object + */ private String id; + /** + * Default constructor with two parameters, url and id + * @param url The url of this object + * @param id The id of this object + */ public SubmissionCCLicenseUrl(String url, String id) { this.url = url; this.id = id; From 9f77864cea295b834fed3b244b965440679d5f1d Mon Sep 17 00:00:00 2001 From: Peter Nijs Date: Thu, 25 Jun 2020 09:15:30 +0200 Subject: [PATCH 084/143] 71410: Authorization for Downloads of restricted Bitstreams - Add test that the ePerson session salt isn't updated when requesting a short lived token --- .../dspace/app/rest/AuthenticationRestControllerIT.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java index e69f859983..c3834f37ed 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java @@ -13,6 +13,7 @@ import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -779,10 +780,16 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio @Test public void testShortLivedToken() throws Exception { String token = getAuthToken(eperson.getEmail(), password); + + // Verify the main session salt doesn't change + String salt = eperson.getSessionSalt(); + getClient(token).perform(post("/api/authn/shortlivedtokens")) .andExpect(jsonPath("$.token", notNullValue())) .andExpect(jsonPath("$.type", is("shortlivedtoken"))) .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/authn/shortlivedtokens"))); + + assertEquals(salt, eperson.getSessionSalt()); } @Test From 8ca6064c88af564865e837aaf40970aaa4048a77 Mon Sep 17 00:00:00 2001 From: Kevin Van de Velde Date: Thu, 25 Jun 2020 10:16:46 +0200 Subject: [PATCH 085/143] [CC License] Adding search link to HAL output --- .../rest/SubmissionCCLicenseUrlRepository.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseUrlRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseUrlRepository.java index 26703f320d..cbfbd347b2 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseUrlRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseUrlRepository.java @@ -7,6 +7,7 @@ */ package org.dspace.app.rest; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import javax.servlet.ServletRequest; @@ -23,10 +24,12 @@ import org.dspace.core.Context; import org.dspace.license.service.CreativeCommonsService; import org.dspace.services.RequestService; import org.dspace.utils.DSpace; +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import org.springframework.hateoas.Link; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Component; @@ -36,7 +39,8 @@ import org.springframework.stereotype.Component; */ @Component(SubmissionCCLicenseUrlRest.CATEGORY + "." + SubmissionCCLicenseUrlRest.NAME) -public class SubmissionCCLicenseUrlRepository extends DSpaceRestRepository { +public class SubmissionCCLicenseUrlRepository extends DSpaceRestRepository + implements InitializingBean { @Autowired protected Utils utils; @@ -49,6 +53,9 @@ public class SubmissionCCLicenseUrlRepository extends DSpaceRestRepository getDomainClass() { return SubmissionCCLicenseUrlRest.class; } + + @Override + public void afterPropertiesSet() { + discoverableEndpointsService.register(this, Arrays.asList( + new Link("/api/" + SubmissionCCLicenseUrlRest.CATEGORY + "/" + SubmissionCCLicenseUrlRest.NAME + "/search", + SubmissionCCLicenseUrlRest.NAME + "-search"))); + } + } From 8cadd105468132611f42c23f94b813ad1b22e90c Mon Sep 17 00:00:00 2001 From: Kevin Van de Velde Date: Thu, 25 Jun 2020 10:56:20 +0200 Subject: [PATCH 086/143] [CC License] Fixing checkstyle issues --- .../org/dspace/app/rest/SubmissionCCLicenseUrlRepository.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseUrlRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseUrlRepository.java index cbfbd347b2..957484319c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseUrlRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/SubmissionCCLicenseUrlRepository.java @@ -132,7 +132,8 @@ public class SubmissionCCLicenseUrlRepository extends DSpaceRestRepository Date: Thu, 25 Jun 2020 12:33:37 +0200 Subject: [PATCH 087/143] Fix injection to avoid issue with docker/double references --- ...tstreamMetadataValueAddPatchOperation.java | 6 ++++- ...streamMetadataValueMovePatchOperation.java | 5 ++++- ...reamMetadataValueRemovePatchOperation.java | 5 ++++- ...eamMetadataValueReplacePatchOperation.java | 6 ++++- .../spring/spring-dspace-core-services.xml | 22 +++++++++++++------ 5 files changed, 33 insertions(+), 11 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueAddPatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueAddPatchOperation.java index c7531e9810..447f715c12 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueAddPatchOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueAddPatchOperation.java @@ -40,7 +40,7 @@ public class BitstreamMetadataValueAddPatchOperation extends MetadataValueAddPat @Autowired ItemService itemService; - @Autowired + // this is wired in the pring-dspace-core-services.xml BitstreamMetadataValuePathUtils bitstreamMetadataValuePathUtils; @Override @@ -103,4 +103,8 @@ public class BitstreamMetadataValueAddPatchOperation extends MetadataValueAddPat protected BitstreamService getDSpaceObjectService() { return bitstreamService; } + + public void setBitstreamMetadataValuePathUtils(BitstreamMetadataValuePathUtils bitstreamMetadataValuePathUtils) { + this.bitstreamMetadataValuePathUtils = bitstreamMetadataValuePathUtils; + } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueMovePatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueMovePatchOperation.java index d03d47c91e..f0e764f970 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueMovePatchOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueMovePatchOperation.java @@ -36,7 +36,7 @@ public class BitstreamMetadataValueMovePatchOperation extends MetadataValueMoveP @Autowired ItemService itemService; - @Autowired + // this is wired in the pring-dspace-core-services.xml BitstreamMetadataValuePathUtils bitstreamMetadataValuePathUtils; @Override @@ -78,4 +78,7 @@ public class BitstreamMetadataValueMovePatchOperation extends MetadataValueMoveP return bitstreamService; } + public void setBitstreamMetadataValuePathUtils(BitstreamMetadataValuePathUtils bitstreamMetadataValuePathUtils) { + this.bitstreamMetadataValuePathUtils = bitstreamMetadataValuePathUtils; + } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueRemovePatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueRemovePatchOperation.java index 0341192b21..17de738637 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueRemovePatchOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueRemovePatchOperation.java @@ -36,7 +36,7 @@ public class BitstreamMetadataValueRemovePatchOperation extends MetadataValueRem @Autowired ItemService itemService; - @Autowired + // this is wired in the pring-dspace-core-services.xml BitstreamMetadataValuePathUtils bitstreamMetadataValuePathUtils; @Override @@ -73,4 +73,7 @@ public class BitstreamMetadataValueRemovePatchOperation extends MetadataValueRem return bitstreamService; } + public void setBitstreamMetadataValuePathUtils(BitstreamMetadataValuePathUtils bitstreamMetadataValuePathUtils) { + this.bitstreamMetadataValuePathUtils = bitstreamMetadataValuePathUtils; + } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueReplacePatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueReplacePatchOperation.java index fd5b395157..d9bcba5ef1 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueReplacePatchOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueReplacePatchOperation.java @@ -39,7 +39,7 @@ public class BitstreamMetadataValueReplacePatchOperation extends MetadataValueRe @Autowired ItemService itemService; - @Autowired + // this is wired in the pring-dspace-core-services.xml BitstreamMetadataValuePathUtils bitstreamMetadataValuePathUtils; @Override @@ -86,4 +86,8 @@ public class BitstreamMetadataValueReplacePatchOperation extends MetadataValueRe protected BitstreamService getDSpaceObjectService() { return bitstreamService; } + + public void setBitstreamMetadataValuePathUtils(BitstreamMetadataValuePathUtils bitstreamMetadataValuePathUtils) { + this.bitstreamMetadataValuePathUtils = bitstreamMetadataValuePathUtils; + } } diff --git a/dspace-server-webapp/src/main/resources/spring/spring-dspace-core-services.xml b/dspace-server-webapp/src/main/resources/spring/spring-dspace-core-services.xml index 0358049b18..fb09e305be 100644 --- a/dspace-server-webapp/src/main/resources/spring/spring-dspace-core-services.xml +++ b/dspace-server-webapp/src/main/resources/spring/spring-dspace-core-services.xml @@ -25,7 +25,9 @@ + class="org.dspace.app.rest.submit.factory.impl.BitstreamMetadataValueMovePatchOperation"> + + + class="org.dspace.app.rest.submit.factory.impl.BitstreamMetadataValueAddPatchOperation"> + + + class="org.dspace.app.rest.submit.factory.impl.ItemMetadataValueRemovePatchOperation" /> + class="org.dspace.app.rest.submit.factory.impl.BitstreamMetadataValueRemovePatchOperation"> + + + class="org.dspace.app.rest.submit.factory.impl.BitstreamRemovePatchOperation" /> + class="org.dspace.app.rest.submit.factory.impl.BitstreamMetadataValueReplacePatchOperation"> + + - + From 06f9eb521c98fad98c5c7a6e1f4d31226e6c49e4 Mon Sep 17 00:00:00 2001 From: Andrea Bollini Date: Thu, 25 Jun 2020 14:02:00 +0200 Subject: [PATCH 088/143] Fix checkstyle issue --- .../factory/impl/BitstreamMetadataValueAddPatchOperation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueAddPatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueAddPatchOperation.java index 447f715c12..4df55c7d60 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueAddPatchOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamMetadataValueAddPatchOperation.java @@ -103,7 +103,7 @@ public class BitstreamMetadataValueAddPatchOperation extends MetadataValueAddPat protected BitstreamService getDSpaceObjectService() { return bitstreamService; } - + public void setBitstreamMetadataValuePathUtils(BitstreamMetadataValuePathUtils bitstreamMetadataValuePathUtils) { this.bitstreamMetadataValuePathUtils = bitstreamMetadataValuePathUtils; } From 32a03eed36a9fbe08e44d0bc6a945a4b5853a8d6 Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Thu, 25 Jun 2020 16:17:36 +0200 Subject: [PATCH 089/143] [Task 71604] moved RegistrationRestControllerIT tests to RegistrationRestRepositoryIT and added extra cleanup --- .../rest/RegistrationRestControllerIT.java | 108 ------------------ .../rest/RegistrationRestRepositoryIT.java | 81 +++++++++++++ 2 files changed, 81 insertions(+), 108 deletions(-) delete mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java deleted file mode 100644 index d02dceea20..0000000000 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestControllerIT.java +++ /dev/null @@ -1,108 +0,0 @@ -/** - * 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.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import java.util.Iterator; -import java.util.List; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.lang3.StringUtils; -import org.dspace.app.rest.model.RegistrationRest; -import org.dspace.app.rest.test.AbstractControllerIntegrationTest; -import org.dspace.eperson.RegistrationData; -import org.dspace.eperson.dao.RegistrationDataDAO; -import org.dspace.services.ConfigurationService; -import org.junit.Test; -import org.springframework.beans.factory.annotation.Autowired; - -public class RegistrationRestControllerIT extends AbstractControllerIntegrationTest { - - @Autowired - private RegistrationDataDAO registrationDataDAO; - - @Autowired - private ConfigurationService configurationService; - - @Test - public void registrationFlowTest() throws Exception { - List registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); - assertEquals(0, registrationDataList.size()); - - ObjectMapper mapper = new ObjectMapper(); - RegistrationRest registrationRest = new RegistrationRest(); - registrationRest.setEmail(eperson.getEmail()); - getClient().perform(post("/api/eperson/registrations") - .content(mapper.writeValueAsBytes(registrationRest)) - .contentType(contentType)) - .andExpect(status().isCreated()); - registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); - assertEquals(1, registrationDataList.size()); - assertTrue(StringUtils.equalsIgnoreCase(registrationDataList.get(0).getEmail(), eperson.getEmail())); - - String newEmail = "newEPersonTest@gmail.com"; - registrationRest.setEmail(newEmail); - getClient().perform(post("/api/eperson/registrations") - .content(mapper.writeValueAsBytes(registrationRest)) - .contentType(contentType)) - .andExpect(status().isCreated()); - registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); - assertTrue(registrationDataList.size() == 2); - assertTrue(StringUtils.equalsIgnoreCase(registrationDataList.get(0).getEmail(), newEmail) || - StringUtils.equalsIgnoreCase(registrationDataList.get(1).getEmail(), newEmail)); - configurationService.setProperty("user.registration", false); - - newEmail = "newEPersonTestTwo@gmail.com"; - registrationRest.setEmail(newEmail); - getClient().perform(post("/api/eperson/registrations") - .content(mapper.writeValueAsBytes(registrationRest)) - .contentType(contentType)) - .andExpect(status().is(401)); - - assertEquals(2, registrationDataList.size()); - assertTrue(!StringUtils.equalsIgnoreCase(registrationDataList.get(0).getEmail(), newEmail) && - !StringUtils.equalsIgnoreCase(registrationDataList.get(1).getEmail(), newEmail)); - - Iterator iterator = registrationDataList.iterator(); - while (iterator.hasNext()) { - RegistrationData registrationData = iterator.next(); - registrationDataDAO.delete(context, registrationData); - } - } - - @Test - public void forgotPasswordTest() throws Exception { - context.turnOffAuthorisationSystem(); - configurationService.setProperty("user.registration", false); - context.restoreAuthSystemState(); - - List registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); - assertEquals(0, registrationDataList.size()); - - ObjectMapper mapper = new ObjectMapper(); - RegistrationRest registrationRest = new RegistrationRest(); - registrationRest.setEmail(eperson.getEmail()); - getClient().perform(post("/api/eperson/registrations") - .content(mapper.writeValueAsBytes(registrationRest)) - .contentType(contentType)) - .andExpect(status().isCreated()); - registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); - assertEquals(1, registrationDataList.size()); - assertTrue(StringUtils.equalsIgnoreCase(registrationDataList.get(0).getEmail(), eperson.getEmail())); - Iterator iterator = registrationDataList.iterator(); - while (iterator.hasNext()) { - RegistrationData registrationData = iterator.next(); - registrationDataDAO.delete(context, registrationData); - } - context.complete(); - } -} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestRepositoryIT.java index 88feebb286..2709042cbd 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestRepositoryIT.java @@ -7,19 +7,24 @@ */ package org.dspace.app.rest; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.Iterator; import java.util.List; import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; import org.dspace.app.rest.matcher.RegistrationMatcher; import org.dspace.app.rest.model.RegistrationRest; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.eperson.RegistrationData; import org.dspace.eperson.dao.RegistrationDataDAO; +import org.dspace.services.ConfigurationService; import org.hamcrest.Matchers; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -29,6 +34,9 @@ public class RegistrationRestRepositoryIT extends AbstractControllerIntegrationT @Autowired private RegistrationDataDAO registrationDataDAO; + @Autowired + private ConfigurationService configurationService; + @Test public void findByTokenTestExistingUserTest() throws Exception { String email = eperson.getEmail(); @@ -41,6 +49,8 @@ public class RegistrationRestRepositoryIT extends AbstractControllerIntegrationT .andExpect( jsonPath("$", Matchers.is(RegistrationMatcher.matchRegistration(email, eperson.getID())))); + registrationDataDAO.delete(context, registrationData); + email = "newUser@testnewuser.com"; createTokenForEmail(email); registrationData = registrationDataDAO.findByEmail(context, email); @@ -87,4 +97,75 @@ public class RegistrationRestRepositoryIT extends AbstractControllerIntegrationT .contentType(contentType)) .andExpect(status().isCreated()); } + + @Test + public void registrationFlowTest() throws Exception { + List registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); + assertEquals(0, registrationDataList.size()); + + ObjectMapper mapper = new ObjectMapper(); + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(eperson.getEmail()); + getClient().perform(post("/api/eperson/registrations") + .content(mapper.writeValueAsBytes(registrationRest)) + .contentType(contentType)) + .andExpect(status().isCreated()); + registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); + assertEquals(1, registrationDataList.size()); + assertTrue(StringUtils.equalsIgnoreCase(registrationDataList.get(0).getEmail(), eperson.getEmail())); + + String newEmail = "newEPersonTest@gmail.com"; + registrationRest.setEmail(newEmail); + getClient().perform(post("/api/eperson/registrations") + .content(mapper.writeValueAsBytes(registrationRest)) + .contentType(contentType)) + .andExpect(status().isCreated()); + registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); + assertTrue(registrationDataList.size() == 2); + assertTrue(StringUtils.equalsIgnoreCase(registrationDataList.get(0).getEmail(), newEmail) || + StringUtils.equalsIgnoreCase(registrationDataList.get(1).getEmail(), newEmail)); + configurationService.setProperty("user.registration", false); + + newEmail = "newEPersonTestTwo@gmail.com"; + registrationRest.setEmail(newEmail); + getClient().perform(post("/api/eperson/registrations") + .content(mapper.writeValueAsBytes(registrationRest)) + .contentType(contentType)) + .andExpect(status().is(401)); + + assertEquals(2, registrationDataList.size()); + assertTrue(!StringUtils.equalsIgnoreCase(registrationDataList.get(0).getEmail(), newEmail) && + !StringUtils.equalsIgnoreCase(registrationDataList.get(1).getEmail(), newEmail)); + + Iterator iterator = registrationDataList.iterator(); + while (iterator.hasNext()) { + RegistrationData registrationData = iterator.next(); + registrationDataDAO.delete(context, registrationData); + } + } + + @Test + public void forgotPasswordTest() throws Exception { + configurationService.setProperty("user.registration", false); + + List registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); + assertEquals(0, registrationDataList.size()); + + ObjectMapper mapper = new ObjectMapper(); + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(eperson.getEmail()); + getClient().perform(post("/api/eperson/registrations") + .content(mapper.writeValueAsBytes(registrationRest)) + .contentType(contentType)) + .andExpect(status().isCreated()); + registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); + assertEquals(1, registrationDataList.size()); + assertTrue(StringUtils.equalsIgnoreCase(registrationDataList.get(0).getEmail(), eperson.getEmail())); + Iterator iterator = registrationDataList.iterator(); + while (iterator.hasNext()) { + RegistrationData registrationData = iterator.next(); + registrationDataDAO.delete(context, registrationData); + } + } + } From 21e96f937c77bba576db67bb19fd93acabb8399a Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Thu, 25 Jun 2020 19:09:25 +0200 Subject: [PATCH 090/143] added missing parameter --- .../java/org/dspace/app/rest/submit/step/CCLicenseStep.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/CCLicenseStep.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/CCLicenseStep.java index 75f1949116..3f8ed6a22f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/CCLicenseStep.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/CCLicenseStep.java @@ -51,8 +51,8 @@ public class CCLicenseStep extends org.dspace.submit.step.CCLicenseStep implemen * @throws Exception */ @Override - public void doPatchProcessing(Context context, Request currentRequest, InProgressSubmission source, Operation op) - throws Exception { + public void doPatchProcessing(Context context, Request currentRequest, InProgressSubmission source, Operation op, + SubmissionStepConfig stepConf) throws Exception { if (op.getPath().endsWith(CCLICENSE_STEP_OPERATION_ENTRY)) { From 27e733c1e8c557284bb756f32bc09c6a937bc4ee Mon Sep 17 00:00:00 2001 From: Peter Nijs Date: Fri, 26 Jun 2020 14:20:06 +0200 Subject: [PATCH 091/143] 71410: Authorization for Downloads of restricted Bitstreams - Add test that the ePerson session salt isn't updated when requesting a short lived token --- .../rest/AuthenticationRestController.java | 2 +- .../AuthenticationTokenHalLinkFactory.java | 2 +- .../rest/model/AuthenticationStatusRest.java | 2 +- .../rest/model/AuthenticationTokenRest.java | 2 +- .../org/dspace/app/rest/model/AuthnRest.java | 2 +- .../org/dspace/app/rest/model/RestModel.java | 1 + ...JWTTokenRestAuthenticationServiceImpl.java | 14 +++---- ...Handler.java => LoginJWTTokenHandler.java} | 14 +++---- .../rest/AuthenticationRestControllerIT.java | 39 ++++++++++++++++--- .../security/jwt/JWTTokenHandlerTest.java | 28 ++++++------- dspace/config/modules/authentication.cfg | 12 +++--- 11 files changed, 74 insertions(+), 44 deletions(-) rename dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/{SessionJWTTokenHandler.java => LoginJWTTokenHandler.java} (72%) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java index 0b60fa1cd4..3038011009 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java @@ -138,7 +138,7 @@ public class AuthenticationRestController implements InitializingBean { */ @PreAuthorize("hasAuthority('AUTHENTICATED')") @RequestMapping(value = "/shortlivedtokens", method = RequestMethod.POST) - public AuthenticationTokenResource shortLivedLogin(HttpServletRequest request) { + public AuthenticationTokenResource shortLivedToken(HttpServletRequest request) { Projection projection = utils.obtainProjection(); AuthenticationToken shortLivedToken = restAuthenticationService.getShortLivedAuthenticationToken(ContextUtil.obtainContext(request), request); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/AuthenticationTokenHalLinkFactory.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/AuthenticationTokenHalLinkFactory.java index 180a3b7e47..ea70f08923 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/AuthenticationTokenHalLinkFactory.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/AuthenticationTokenHalLinkFactory.java @@ -27,7 +27,7 @@ public class AuthenticationTokenHalLinkFactory protected void addLinks(AuthenticationTokenResource halResource, Pageable pageable, LinkedList list) throws Exception { - list.add(buildLink(IanaLinkRelations.SELF.value(), getMethodOn().shortLivedLogin(null))); + list.add(buildLink(IanaLinkRelations.SELF.value(), getMethodOn().shortLivedToken(null))); } @Override diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthenticationStatusRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthenticationStatusRest.java index cddfe34a22..a137620e6b 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthenticationStatusRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthenticationStatusRest.java @@ -18,7 +18,7 @@ public class AuthenticationStatusRest extends BaseObjectRest { private boolean authenticated; public static final String NAME = "status"; - public static final String CATEGORY = "authn"; + public static final String CATEGORY = RestAddressableModel.AUTHENTICATION; @Override public String getCategory() { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthenticationTokenRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthenticationTokenRest.java index 130b5cc162..0599e09565 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthenticationTokenRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthenticationTokenRest.java @@ -15,7 +15,7 @@ import org.dspace.app.rest.RestResourceController; */ public class AuthenticationTokenRest extends RestAddressableModel { public static final String NAME = "shortlivedtoken"; - public static final String CATEGORY = "authn"; + public static final String CATEGORY = RestAddressableModel.AUTHENTICATION; private String token; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthnRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthnRest.java index dd225de1c7..fade90fe4d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthnRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthnRest.java @@ -18,7 +18,7 @@ import org.dspace.app.rest.AuthenticationRestController; public class AuthnRest extends BaseObjectRest { public static final String NAME = "authn"; - public static final String CATEGORY = "authn"; + public static final String CATEGORY = RestAddressableModel.AUTHENTICATION; public String getCategory() { return CATEGORY; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RestModel.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RestModel.java index 940f4a4deb..0b32aedf92 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RestModel.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RestModel.java @@ -32,6 +32,7 @@ public interface RestModel extends Serializable { public static final String WORKFLOW = "workflow"; public static final String AUTHORIZATION = "authz"; public static final String VERSIONING = "versioning"; + public static final String AUTHENTICATION = "authn"; public String getType(); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java index 123f96f89b..bce2724c8d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java @@ -51,7 +51,7 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication private static final String AUTHORIZATION_TOKEN_PARAMETER = "token"; @Autowired - private SessionJWTTokenHandler sessionJWTTokenHandler; + private LoginJWTTokenHandler loginJWTTokenHandler; @Autowired private ShortLivedJWTTokenHandler shortLivedJWTTokenHandler; @@ -76,7 +76,7 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication List groups = authenticationService.getSpecialGroups(context, request); - String token = sessionJWTTokenHandler.createTokenForEPerson(context, request, + String token = loginJWTTokenHandler.createTokenForEPerson(context, request, authentication.getPreviousLoginDate(), groups); addTokenToResponse(response, token, addCookie); @@ -115,13 +115,13 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication @Override public EPerson getAuthenticatedEPerson(HttpServletRequest request, Context context) { try { - String token = getSessionToken(request); + String token = getLoginToken(request); EPerson ePerson = null; if (token == null) { token = getShortLivedToken(request); ePerson = shortLivedJWTTokenHandler.parseEPersonFromToken(token, request, context); } else { - ePerson = sessionJWTTokenHandler.parseEPersonFromToken(token, request, context); + ePerson = loginJWTTokenHandler.parseEPersonFromToken(token, request, context); } return ePerson; } catch (JOSEException e) { @@ -144,9 +144,9 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication @Override public void invalidateAuthenticationData(HttpServletRequest request, HttpServletResponse response, Context context) throws Exception { - String token = getSessionToken(request); + String token = getLoginToken(request); invalidateAuthenticationCookie(response); - sessionJWTTokenHandler.invalidateToken(token, request, context); + loginJWTTokenHandler.invalidateToken(token, request, context); } @Override @@ -201,7 +201,7 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication response.setHeader(AUTHORIZATION_HEADER, String.format("%s %s", AUTHORIZATION_TYPE, token)); } - private String getSessionToken(HttpServletRequest request) { + private String getLoginToken(HttpServletRequest request) { String tokenValue = null; String authHeader = request.getHeader(AUTHORIZATION_HEADER); String authCookie = getAuthorizationCookie(request); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/SessionJWTTokenHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/LoginJWTTokenHandler.java similarity index 72% rename from dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/SessionJWTTokenHandler.java rename to dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/LoginJWTTokenHandler.java index 4d70c4b10d..9446834519 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/SessionJWTTokenHandler.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/LoginJWTTokenHandler.java @@ -14,34 +14,34 @@ import org.springframework.stereotype.Component; * https://jwt.io/ */ @Component -public class SessionJWTTokenHandler extends JWTTokenHandler { +public class LoginJWTTokenHandler extends JWTTokenHandler { @Override protected String getTokenSecretConfigurationKey() { - return "jwt.session.token.secret"; + return "jwt.login.token.secret"; } @Override protected String getEncryptionSecretConfigurationKey() { - return "jwt.session.encryption.secret"; + return "jwt.login.encryption.secret"; } @Override protected String getTokenExpirationConfigurationKey() { - return "jwt.session.token.expiration"; + return "jwt.login.token.expiration"; } @Override protected String getTokenIncludeIPConfigurationKey() { - return "jwt.session.token.include.ip"; + return "jwt.login.token.include.ip"; } @Override protected String getEncryptionEnabledConfigurationKey() { - return "jwt.session.encryption.enabled"; + return "jwt.login.encryption.enabled"; } @Override protected String getCompressionEnabledConfigurationKey() { - return "jwt.session.compression.enabled"; + return "jwt.login.compression.enabled"; } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java index c3834f37ed..b04c99b0e8 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java @@ -34,6 +34,7 @@ import org.dspace.app.rest.builder.BitstreamBuilder; import org.dspace.app.rest.builder.BundleBuilder; import org.dspace.app.rest.builder.CollectionBuilder; import org.dspace.app.rest.builder.CommunityBuilder; +import org.dspace.app.rest.builder.EPersonBuilder; import org.dspace.app.rest.builder.GroupBuilder; import org.dspace.app.rest.builder.ItemBuilder; import org.dspace.app.rest.matcher.AuthenticationStatusMatcher; @@ -808,11 +809,28 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio } @Test - public void testSessionTokenToDowloadBitstream() throws Exception { + public void testShortLivedTokenToDowloadBitstreamUnauthorized() throws Exception { Bitstream bitstream = createPrivateBitstream(); - String sessionToken = getAuthToken(eperson.getEmail(), password); - getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content?token=" + sessionToken)) + context.turnOffAuthorisationSystem(); + EPerson testEPerson = EPersonBuilder.createEPerson(context) + .withNameInMetadata("John", "Doe") + .withEmail("UnauthorizedUser@example.com") + .withPassword(password) + .build(); + context.restoreAuthSystemState(); + + String shortLivedToken = getShortLivedToken(testEPerson); + getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content?token=" + shortLivedToken)) + .andExpect(status().isForbidden()); + } + + @Test + public void testLoginTokenToDowloadBitstream() throws Exception { + Bitstream bitstream = createPrivateBitstream(); + + String loginToken = getAuthToken(eperson.getEmail(), password); + getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content?token=" + loginToken)) .andExpect(status().isForbidden()); } @@ -826,10 +844,21 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio .andExpect(status().isForbidden()); } - private String getShortLivedToken(EPerson ePerson) throws Exception { - ObjectMapper mapper = new ObjectMapper(); + @Test + public void testShortLivedAndLoginTokenSeparation() throws Exception { + configurationService.setProperty("jwt.shortLived.token.expiration", "1"); String token = getAuthToken(eperson.getEmail(), password); + Thread.sleep(2); + getClient(token).perform(get("/api/authn/status").param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.authenticated", is(true))); + } + + private String getShortLivedToken(EPerson requestUser) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + + String token = getAuthToken(requestUser.getEmail(), password); MvcResult mvcResult = getClient(token).perform(post("/api/authn/shortlivedtokens")) .andReturn(); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/JWTTokenHandlerTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/JWTTokenHandlerTest.java index 1a33ae7484..3b0eb84793 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/JWTTokenHandlerTest.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/JWTTokenHandlerTest.java @@ -47,7 +47,7 @@ public class JWTTokenHandlerTest { @InjectMocks @Spy - private SessionJWTTokenHandler sessionJWTTokenHandler; + private LoginJWTTokenHandler loginJWTTokenHandler; @Mock protected ConfigurationService configurationService; @@ -91,7 +91,7 @@ public class JWTTokenHandlerTest { @Test public void testJWTNoEncryption() throws Exception { Date previous = new Date(System.currentTimeMillis() - 10000000000L); - String token = sessionJWTTokenHandler + String token = loginJWTTokenHandler .createTokenForEPerson(context, new MockHttpServletRequest(), previous, new ArrayList<>()); SignedJWT signedJWT = SignedJWT.parse(token); String personId = (String) signedJWT.getJWTClaimsSet().getClaim(EPersonClaimProvider.EPERSON_ID); @@ -100,11 +100,11 @@ public class JWTTokenHandlerTest { @Test(expected = ParseException.class) public void testJWTEncrypted() throws Exception { - when(sessionJWTTokenHandler.isEncryptionEnabled()).thenReturn(true); + when(loginJWTTokenHandler.isEncryptionEnabled()).thenReturn(true); Date previous = new Date(System.currentTimeMillis() - 10000000000L); StringKeyGenerator keyGenerator = KeyGenerators.string(); - when(configurationService.getProperty("jwt.session.encryption.secret")).thenReturn(keyGenerator.generateKey()); - String token = sessionJWTTokenHandler + when(configurationService.getProperty("jwt.login.encryption.secret")).thenReturn(keyGenerator.generateKey()); + String token = loginJWTTokenHandler .createTokenForEPerson(context, new MockHttpServletRequest(), previous, new ArrayList<>()); SignedJWT signedJWT = SignedJWT.parse(token); } @@ -112,12 +112,12 @@ public class JWTTokenHandlerTest { //temporary set a negative expiration time so the token is invalid immediately @Test public void testExpiredToken() throws Exception { - when(configurationService.getLongProperty("jwt.session.token.expiration", 1800000)).thenReturn(-99999999L); + when(configurationService.getLongProperty("jwt.login.token.expiration", 1800000)).thenReturn(-99999999L); when(ePersonClaimProvider.getEPerson(any(Context.class), any(JWTClaimsSet.class))).thenReturn(ePerson); Date previous = new Date(new Date().getTime() - 10000000000L); - String token = sessionJWTTokenHandler + String token = loginJWTTokenHandler .createTokenForEPerson(context, new MockHttpServletRequest(), previous, new ArrayList<>()); - EPerson parsed = sessionJWTTokenHandler.parseEPersonFromToken(token, httpServletRequest, context); + EPerson parsed = loginJWTTokenHandler.parseEPersonFromToken(token, httpServletRequest, context); assertEquals(null, parsed); } @@ -125,17 +125,17 @@ public class JWTTokenHandlerTest { //Try if we can change the expiration date @Test public void testTokenTampering() throws Exception { - when(sessionJWTTokenHandler.getExpirationPeriod()).thenReturn(-99999999L); + when(loginJWTTokenHandler.getExpirationPeriod()).thenReturn(-99999999L); when(ePersonClaimProvider.getEPerson(any(Context.class), any(JWTClaimsSet.class))).thenReturn(ePerson); Date previous = new Date(new Date().getTime() - 10000000000L); - String token = sessionJWTTokenHandler + String token = loginJWTTokenHandler .createTokenForEPerson(context, new MockHttpServletRequest(), previous, new ArrayList<>()); JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().claim("eid", "epersonID").expirationTime( new Date(System.currentTimeMillis() + 99999999)).build(); String tamperedPayload = new String(Base64.getUrlEncoder().encode(jwtClaimsSet.toString().getBytes())); String[] splitToken = token.split("\\."); String tamperedToken = splitToken[0] + "." + tamperedPayload + "." + splitToken[2]; - EPerson parsed = sessionJWTTokenHandler.parseEPersonFromToken(tamperedToken, httpServletRequest, context); + EPerson parsed = loginJWTTokenHandler.parseEPersonFromToken(tamperedToken, httpServletRequest, context); assertEquals(null, parsed); } @@ -143,12 +143,12 @@ public class JWTTokenHandlerTest { public void testInvalidatedToken() throws Exception { Date previous = new Date(System.currentTimeMillis() - 10000000000L); // create a new token - String token = sessionJWTTokenHandler + String token = loginJWTTokenHandler .createTokenForEPerson(context, new MockHttpServletRequest(), previous, new ArrayList<>()); // immediately invalidate it - sessionJWTTokenHandler.invalidateToken(token, new MockHttpServletRequest(), context); + loginJWTTokenHandler.invalidateToken(token, new MockHttpServletRequest(), context); // Check if it is still valid by trying to parse the EPerson from it (should return null) - EPerson parsed = sessionJWTTokenHandler.parseEPersonFromToken(token, httpServletRequest, context); + EPerson parsed = loginJWTTokenHandler.parseEPersonFromToken(token, httpServletRequest, context); assertEquals(null, parsed); } diff --git a/dspace/config/modules/authentication.cfg b/dspace/config/modules/authentication.cfg index e3077f4a57..f22e2eaf19 100644 --- a/dspace/config/modules/authentication.cfg +++ b/dspace/config/modules/authentication.cfg @@ -57,28 +57,28 @@ plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authen # Server key part that is a part of the key used to sign the authentication tokens. # If this property is not set or empty, DSpace will generate a random key on startup. # IF YOU ARE RUNNING DSPACE IN A CLUSTER, you need to set a value for this property here or as an environment variable -# jwt.session.token.secret = +# jwt.login.token.secret = # This property enables/disables encryption of the payload in a stateless token. Enabling this makes the data encrypted # and unreadable by the receiver, but makes the token larger in size. false by default -jwt.session.encryption.enabled = false +jwt.login.encryption.enabled = false # Encryption key to use when JWT token encryption is enabled (JWE). Note that encrypting tokens might required additional # configuration in the REST clients -# jwt.session.encryption.secret = +# jwt.login.encryption.secret = # This enables compression of the payload of a jwt, enabling this will make the jwt token a little smaller at the cost # of some performance, this setting WILL ONLY BE used when encrypting the jwt. -jwt.session.compression.enabled = true +jwt.login.compression.enabled = true # Expiration time of a token in milliseconds -jwt.session.token.expiration = 1800000 +jwt.login.token.expiration = 1800000 # Restrict tokens to a specific ip-address to prevent theft/session hijacking. This is achieved by making the ip-address # a part of the JWT siging key. If this property is set to false then the ip-address won't be used as part of # the signing key of a jwt token and tokens can be shared over multiple ip-addresses. # For security reasons, this defaults to true -jwt.session.token.include.ip = true +jwt.login.token.include.ip = true #---------------------------------------------------------------# From 22678a7ef54984198e9ee08623ccdfc84ff96887 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 26 Jun 2020 09:41:50 -0500 Subject: [PATCH 092/143] Configure LGTM --- .lgtm.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .lgtm.yml diff --git a/.lgtm.yml b/.lgtm.yml new file mode 100644 index 0000000000..132de8a6de --- /dev/null +++ b/.lgtm.yml @@ -0,0 +1,9 @@ +# LGTM Settings (https://lgtm.com/) +# For reference, see https://lgtm.com/help/lgtm/lgtm.yml-configuration-file +# or template at https://lgtm.com/static/downloads/lgtm.template.yml + +extraction: + java: + index: + # Specify the Java version required to build the project + java_version: 11 From 2271237bc6729ad6508f7482e7fe6f67c5919d6e Mon Sep 17 00:00:00 2001 From: Andrea Bollini Date: Mon, 29 Jun 2020 23:48:37 +0200 Subject: [PATCH 093/143] Add javadoc for the reload method --- .../app/rest/repository/SubmissionFormRestRepository.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionFormRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionFormRestRepository.java index 770438e183..76d680cf27 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionFormRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionFormRestRepository.java @@ -86,6 +86,13 @@ public class SubmissionFormRestRepository extends DSpaceRestRepository Date: Tue, 30 Jun 2020 08:30:01 +0200 Subject: [PATCH 094/143] [Task 71627] addressed feedback on the new registration functionaliity --- .../repository/EPersonRestRepository.java | 2 +- .../app/rest/EPersonRestRepositoryIT.java | 221 ++++++++++-------- .../rest/RegistrationRestRepositoryIT.java | 147 ++++++------ 3 files changed, 202 insertions(+), 168 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java index b57df5f73d..e4a1ca1389 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java @@ -199,7 +199,7 @@ public class EPersonRestRepository extends DSpaceObjectRestRepository epersonLastName = metadataRest.getMap().get("eperson.lastname"); if (epersonFirstName == null || epersonLastName == null || epersonFirstName.isEmpty() || epersonLastName.isEmpty()) { - throw new DSpaceBadRequestException("The eperson.firstname and eperson.lastname values need to be " + + throw new UnprocessableEntityException("The eperson.firstname and eperson.lastname values need to be " + "filled in"); } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java index b681d5fe0d..d2b599e477 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java @@ -2044,22 +2044,24 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { accountService.sendRegistrationInfo(context, ePerson.getEmail()); String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); PasswordHash oldPassword = ePersonService.getPasswordHash(ePerson); - // updates password - getClient().perform(patch("/api/eperson/epersons/" + ePerson.getID()) - .content(patchBody) - .contentType(MediaType.APPLICATION_JSON_PATCH_JSON) - .param("token", newRegisterToken)) - .andExpect(status().isUnauthorized()); + try { + // updates password + getClient().perform(patch("/api/eperson/epersons/" + ePerson.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON) + .param("token", newRegisterToken)) + .andExpect(status().isUnauthorized()); - PasswordHash newPasswordHash = ePersonService.getPasswordHash(ePerson); - assertTrue(StringUtils.equalsIgnoreCase(oldPassword.getHashString(),newPasswordHash.getHashString())); - assertFalse(registrationDataService.findByEmail(context, ePerson.getEmail()) == null); - assertFalse(registrationDataService.findByEmail(context, newRegisterEmail) == null); - - context.turnOffAuthorisationSystem(); - registrationDataService.delete(context, registrationDataService.findByEmail(context, ePerson.getEmail())); - registrationDataService.deleteByToken(context, newRegisterToken); - context.restoreAuthSystemState(); + PasswordHash newPasswordHash = ePersonService.getPasswordHash(ePerson); + assertTrue(StringUtils.equalsIgnoreCase(oldPassword.getHashString(),newPasswordHash.getHashString())); + assertFalse(registrationDataService.findByEmail(context, ePerson.getEmail()) == null); + assertFalse(registrationDataService.findByEmail(context, newRegisterEmail) == null); + } finally { + context.turnOffAuthorisationSystem(); + registrationDataService.delete(context, registrationDataService.findByEmail(context, ePerson.getEmail())); + registrationDataService.deleteByToken(context, newRegisterToken); + context.restoreAuthSystemState(); + } } @Test @@ -2118,10 +2120,10 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { assertNull(registrationDataService.findByToken(context, newRegisterToken)); + } finally { context.turnOffAuthorisationSystem(); registrationDataService.deleteByToken(context, newRegisterToken); context.restoreAuthSystemState(); - } finally { EPersonBuilder.deleteEPerson(idRef.get()); } } @@ -2179,10 +2181,10 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { assertTrue(ePersonService.checkPassword(context, createdEPerson, "somePassword")); assertNull(registrationDataService.findByToken(context, newRegisterToken)); + } finally { context.turnOffAuthorisationSystem(); registrationDataService.deleteByToken(context, newRegisterToken); context.restoreAuthSystemState(); - } finally { EPersonBuilder.deleteEPerson(idRef.get()); } @@ -2245,10 +2247,10 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { assertTrue(ePersonService.checkPassword(context, createdEPerson, "somePassword")); assertNull(registrationDataService.findByToken(context, newRegisterToken)); + } finally { context.turnOffAuthorisationSystem(); registrationDataService.deleteByToken(context, newRegisterToken); context.restoreAuthSystemState(); - } finally { EPersonBuilder.deleteEPerson(idRef.get()); } @@ -2293,21 +2295,24 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { mapper.setAnnotationIntrospector(new IgnoreJacksonWriteOnlyAccess()); - getClient().perform(post("/api/eperson/epersons") - .param("token", newRegisterToken) - .content(mapper.writeValueAsBytes(ePersonRest)) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); + try { + getClient().perform(post("/api/eperson/epersons") + .param("token", newRegisterToken) + .content(mapper.writeValueAsBytes(ePersonRest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); - EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmailTwo); - assertNull(createdEPerson); - assertNotNull(registrationDataService.findByToken(context, newRegisterToken)); - assertNotNull(registrationDataService.findByToken(context, newRegisterTokenTwo)); + EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmailTwo); + assertNull(createdEPerson); + assertNotNull(registrationDataService.findByToken(context, newRegisterToken)); + assertNotNull(registrationDataService.findByToken(context, newRegisterTokenTwo)); + } finally { + context.turnOffAuthorisationSystem(); + registrationDataService.deleteByToken(context, newRegisterToken); + registrationDataService.deleteByToken(context, newRegisterTokenTwo); + context.restoreAuthSystemState(); - context.turnOffAuthorisationSystem(); - registrationDataService.deleteByToken(context, newRegisterToken); - registrationDataService.deleteByToken(context, newRegisterTokenTwo); - context.restoreAuthSystemState(); + } } @Test @@ -2340,19 +2345,22 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { mapper.setAnnotationIntrospector(new IgnoreJacksonWriteOnlyAccess()); - getClient().perform(post("/api/eperson/epersons") - .param("token", "randomToken") - .content(mapper.writeValueAsBytes(ePersonRest)) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); + try { + getClient().perform(post("/api/eperson/epersons") + .param("token", "randomToken") + .content(mapper.writeValueAsBytes(ePersonRest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); - EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail); - assertNull(createdEPerson); - assertNotNull(registrationDataService.findByToken(context, newRegisterToken)); + EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail); + assertNull(createdEPerson); + assertNotNull(registrationDataService.findByToken(context, newRegisterToken)); + } finally { + context.turnOffAuthorisationSystem(); + registrationDataService.deleteByToken(context, newRegisterToken); + context.restoreAuthSystemState(); + } - context.turnOffAuthorisationSystem(); - registrationDataService.deleteByToken(context, newRegisterToken); - context.restoreAuthSystemState(); } @Test @@ -2386,19 +2394,22 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { mapper.setAnnotationIntrospector(new IgnoreJacksonWriteOnlyAccess()); - getClient().perform(post("/api/eperson/epersons") - .param("token", newRegisterToken) - .content(mapper.writeValueAsBytes(ePersonRest)) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); + try { + getClient().perform(post("/api/eperson/epersons") + .param("token", newRegisterToken) + .content(mapper.writeValueAsBytes(ePersonRest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); - EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail); - assertNull(createdEPerson); - assertNotNull(registrationDataService.findByToken(context, newRegisterToken)); + EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail); + assertNull(createdEPerson); + assertNotNull(registrationDataService.findByToken(context, newRegisterToken)); + } finally { + context.turnOffAuthorisationSystem(); + registrationDataService.deleteByToken(context, newRegisterToken); + context.restoreAuthSystemState(); + } - context.turnOffAuthorisationSystem(); - registrationDataService.deleteByToken(context, newRegisterToken); - context.restoreAuthSystemState(); } @Test @@ -2429,19 +2440,22 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { mapper.setAnnotationIntrospector(new IgnoreJacksonWriteOnlyAccess()); - getClient().perform(post("/api/eperson/epersons") - .param("token", newRegisterToken) - .content(mapper.writeValueAsBytes(ePersonRest)) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); + try { + getClient().perform(post("/api/eperson/epersons") + .param("token", newRegisterToken) + .content(mapper.writeValueAsBytes(ePersonRest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isUnprocessableEntity()); - EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail); - assertNull(createdEPerson); - assertNotNull(registrationDataService.findByToken(context, newRegisterToken)); + EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail); + assertNull(createdEPerson); + assertNotNull(registrationDataService.findByToken(context, newRegisterToken)); + } finally { + context.turnOffAuthorisationSystem(); + registrationDataService.deleteByToken(context, newRegisterToken); + context.restoreAuthSystemState(); + } - context.turnOffAuthorisationSystem(); - registrationDataService.deleteByToken(context, newRegisterToken); - context.restoreAuthSystemState(); } @Test @@ -2472,19 +2486,22 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { mapper.setAnnotationIntrospector(new IgnoreJacksonWriteOnlyAccess()); - getClient().perform(post("/api/eperson/epersons") - .param("token", newRegisterToken) - .content(mapper.writeValueAsBytes(ePersonRest)) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); + try { + getClient().perform(post("/api/eperson/epersons") + .param("token", newRegisterToken) + .content(mapper.writeValueAsBytes(ePersonRest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isUnprocessableEntity()); - EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail); - assertNull(createdEPerson); - assertNotNull(registrationDataService.findByToken(context, newRegisterToken)); + EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail); + assertNull(createdEPerson); + assertNotNull(registrationDataService.findByToken(context, newRegisterToken)); + } finally { + context.turnOffAuthorisationSystem(); + registrationDataService.deleteByToken(context, newRegisterToken); + context.restoreAuthSystemState(); + } - context.turnOffAuthorisationSystem(); - registrationDataService.deleteByToken(context, newRegisterToken); - context.restoreAuthSystemState(); } @Test @@ -2516,19 +2533,21 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { mapper.setAnnotationIntrospector(new IgnoreJacksonWriteOnlyAccess()); - getClient().perform(post("/api/eperson/epersons") - .param("token", newRegisterToken) - .content(mapper.writeValueAsBytes(ePersonRest)) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); + try { + getClient().perform(post("/api/eperson/epersons") + .param("token", newRegisterToken) + .content(mapper.writeValueAsBytes(ePersonRest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); - EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail); - assertNull(createdEPerson); - assertNotNull(registrationDataService.findByToken(context, newRegisterToken)); - - context.turnOffAuthorisationSystem(); - registrationDataService.deleteByToken(context, newRegisterToken); - context.restoreAuthSystemState(); + EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail); + assertNull(createdEPerson); + assertNotNull(registrationDataService.findByToken(context, newRegisterToken)); + } finally { + context.turnOffAuthorisationSystem(); + registrationDataService.deleteByToken(context, newRegisterToken); + context.restoreAuthSystemState(); + } } @@ -2562,19 +2581,22 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { mapper.setAnnotationIntrospector(new IgnoreJacksonWriteOnlyAccess()); - getClient().perform(post("/api/eperson/epersons") - .param("token", forgotPasswordToken) - .content(mapper.writeValueAsBytes(ePersonRest)) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); + try { + getClient().perform(post("/api/eperson/epersons") + .param("token", forgotPasswordToken) + .content(mapper.writeValueAsBytes(ePersonRest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); - EPerson createdEPerson = ePersonService.findByEmail(context, newEmail); - assertNull(createdEPerson); - assertNotNull(registrationDataService.findByToken(context, forgotPasswordToken)); + EPerson createdEPerson = ePersonService.findByEmail(context, newEmail); + assertNull(createdEPerson); + assertNotNull(registrationDataService.findByToken(context, forgotPasswordToken)); + } finally { + context.turnOffAuthorisationSystem(); + registrationDataService.deleteByToken(context, forgotPasswordToken); + context.restoreAuthSystemState(); + } - context.turnOffAuthorisationSystem(); - registrationDataService.deleteByToken(context, forgotPasswordToken); - context.restoreAuthSystemState(); } @@ -2633,11 +2655,10 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { EPerson createdEPerson = ePersonService.find(context, UUID.fromString(epersonUuid)); assertTrue(ePersonService.checkPassword(context, createdEPerson, "somePassword")); assertNull(registrationDataService.findByToken(context, newRegisterToken)); - + } finally { context.turnOffAuthorisationSystem(); registrationDataService.deleteByToken(context, newRegisterToken); context.restoreAuthSystemState(); - } finally { EPersonBuilder.deleteEPerson(idRef.get()); } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestRepositoryIT.java index 2709042cbd..1c3ae58374 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestRepositoryIT.java @@ -16,6 +16,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import java.util.Iterator; import java.util.List; +import javax.servlet.http.HttpServletResponse; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.StringUtils; @@ -43,25 +44,28 @@ public class RegistrationRestRepositoryIT extends AbstractControllerIntegrationT createTokenForEmail(email); RegistrationData registrationData = registrationDataDAO.findByEmail(context, email); - getClient().perform(get("/api/eperson/registrations/search/findByToken") - .param("token", registrationData.getToken())) - .andExpect(status().isOk()) - .andExpect( - jsonPath("$", Matchers.is(RegistrationMatcher.matchRegistration(email, eperson.getID())))); + try { + getClient().perform(get("/api/eperson/registrations/search/findByToken") + .param("token", registrationData.getToken())) + .andExpect(status().isOk()) + .andExpect( + jsonPath("$", Matchers.is(RegistrationMatcher.matchRegistration(email, eperson.getID())))); - registrationDataDAO.delete(context, registrationData); + registrationDataDAO.delete(context, registrationData); - email = "newUser@testnewuser.com"; - createTokenForEmail(email); - registrationData = registrationDataDAO.findByEmail(context, email); + email = "newUser@testnewuser.com"; + createTokenForEmail(email); + registrationData = registrationDataDAO.findByEmail(context, email); - getClient().perform(get("/api/eperson/registrations/search/findByToken") - .param("token", registrationData.getToken())) - .andExpect(status().isOk()) - .andExpect( - jsonPath("$", Matchers.is(RegistrationMatcher.matchRegistration(email, null)))); + getClient().perform(get("/api/eperson/registrations/search/findByToken") + .param("token", registrationData.getToken())) + .andExpect(status().isOk()) + .andExpect( + jsonPath("$", Matchers.is(RegistrationMatcher.matchRegistration(email, null)))); + } finally { + registrationDataDAO.delete(context, registrationData); + } - registrationDataDAO.delete(context, registrationData); } @@ -71,13 +75,16 @@ public class RegistrationRestRepositoryIT extends AbstractControllerIntegrationT createTokenForEmail(email); RegistrationData registrationData = registrationDataDAO.findByEmail(context, email); - getClient().perform(get("/api/eperson/registrations/search/findByToken") - .param("token", registrationData.getToken())) - .andExpect(status().isOk()) - .andExpect( - jsonPath("$", Matchers.is(RegistrationMatcher.matchRegistration(email, null)))); + try { + getClient().perform(get("/api/eperson/registrations/search/findByToken") + .param("token", registrationData.getToken())) + .andExpect(status().isOk()) + .andExpect( + jsonPath("$", Matchers.is(RegistrationMatcher.matchRegistration(email, null)))); + } finally { + registrationDataDAO.delete(context, registrationData); + } - registrationDataDAO.delete(context, registrationData); } @Test @@ -106,41 +113,44 @@ public class RegistrationRestRepositoryIT extends AbstractControllerIntegrationT ObjectMapper mapper = new ObjectMapper(); RegistrationRest registrationRest = new RegistrationRest(); registrationRest.setEmail(eperson.getEmail()); - getClient().perform(post("/api/eperson/registrations") - .content(mapper.writeValueAsBytes(registrationRest)) - .contentType(contentType)) - .andExpect(status().isCreated()); - registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); - assertEquals(1, registrationDataList.size()); - assertTrue(StringUtils.equalsIgnoreCase(registrationDataList.get(0).getEmail(), eperson.getEmail())); - String newEmail = "newEPersonTest@gmail.com"; - registrationRest.setEmail(newEmail); - getClient().perform(post("/api/eperson/registrations") - .content(mapper.writeValueAsBytes(registrationRest)) - .contentType(contentType)) - .andExpect(status().isCreated()); - registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); - assertTrue(registrationDataList.size() == 2); - assertTrue(StringUtils.equalsIgnoreCase(registrationDataList.get(0).getEmail(), newEmail) || - StringUtils.equalsIgnoreCase(registrationDataList.get(1).getEmail(), newEmail)); - configurationService.setProperty("user.registration", false); + try { + getClient().perform(post("/api/eperson/registrations") + .content(mapper.writeValueAsBytes(registrationRest)) + .contentType(contentType)) + .andExpect(status().isCreated()); + registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); + assertEquals(1, registrationDataList.size()); + assertTrue(StringUtils.equalsIgnoreCase(registrationDataList.get(0).getEmail(), eperson.getEmail())); - newEmail = "newEPersonTestTwo@gmail.com"; - registrationRest.setEmail(newEmail); - getClient().perform(post("/api/eperson/registrations") - .content(mapper.writeValueAsBytes(registrationRest)) - .contentType(contentType)) - .andExpect(status().is(401)); + String newEmail = "newEPersonTest@gmail.com"; + registrationRest.setEmail(newEmail); + getClient().perform(post("/api/eperson/registrations") + .content(mapper.writeValueAsBytes(registrationRest)) + .contentType(contentType)) + .andExpect(status().isCreated()); + registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); + assertTrue(registrationDataList.size() == 2); + assertTrue(StringUtils.equalsIgnoreCase(registrationDataList.get(0).getEmail(), newEmail) || + StringUtils.equalsIgnoreCase(registrationDataList.get(1).getEmail(), newEmail)); + configurationService.setProperty("user.registration", false); - assertEquals(2, registrationDataList.size()); - assertTrue(!StringUtils.equalsIgnoreCase(registrationDataList.get(0).getEmail(), newEmail) && - !StringUtils.equalsIgnoreCase(registrationDataList.get(1).getEmail(), newEmail)); + newEmail = "newEPersonTestTwo@gmail.com"; + registrationRest.setEmail(newEmail); + getClient().perform(post("/api/eperson/registrations") + .content(mapper.writeValueAsBytes(registrationRest)) + .contentType(contentType)) + .andExpect(status().is(HttpServletResponse.SC_UNAUTHORIZED)); - Iterator iterator = registrationDataList.iterator(); - while (iterator.hasNext()) { - RegistrationData registrationData = iterator.next(); - registrationDataDAO.delete(context, registrationData); + assertEquals(2, registrationDataList.size()); + assertTrue(!StringUtils.equalsIgnoreCase(registrationDataList.get(0).getEmail(), newEmail) && + !StringUtils.equalsIgnoreCase(registrationDataList.get(1).getEmail(), newEmail)); + } finally { + Iterator iterator = registrationDataList.iterator(); + while (iterator.hasNext()) { + RegistrationData registrationData = iterator.next(); + registrationDataDAO.delete(context, registrationData); + } } } @@ -149,22 +159,25 @@ public class RegistrationRestRepositoryIT extends AbstractControllerIntegrationT configurationService.setProperty("user.registration", false); List registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); - assertEquals(0, registrationDataList.size()); + try { + assertEquals(0, registrationDataList.size()); - ObjectMapper mapper = new ObjectMapper(); - RegistrationRest registrationRest = new RegistrationRest(); - registrationRest.setEmail(eperson.getEmail()); - getClient().perform(post("/api/eperson/registrations") - .content(mapper.writeValueAsBytes(registrationRest)) - .contentType(contentType)) - .andExpect(status().isCreated()); - registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); - assertEquals(1, registrationDataList.size()); - assertTrue(StringUtils.equalsIgnoreCase(registrationDataList.get(0).getEmail(), eperson.getEmail())); - Iterator iterator = registrationDataList.iterator(); - while (iterator.hasNext()) { - RegistrationData registrationData = iterator.next(); - registrationDataDAO.delete(context, registrationData); + ObjectMapper mapper = new ObjectMapper(); + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(eperson.getEmail()); + getClient().perform(post("/api/eperson/registrations") + .content(mapper.writeValueAsBytes(registrationRest)) + .contentType(contentType)) + .andExpect(status().isCreated()); + registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); + assertEquals(1, registrationDataList.size()); + assertTrue(StringUtils.equalsIgnoreCase(registrationDataList.get(0).getEmail(), eperson.getEmail())); + } finally { + Iterator iterator = registrationDataList.iterator(); + while (iterator.hasNext()) { + RegistrationData registrationData = iterator.next(); + registrationDataDAO.delete(context, registrationData); + } } } From 2a6731297cc8c7c7f1387cf214800e42a9672071 Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Tue, 30 Jun 2020 10:27:47 +0200 Subject: [PATCH 095/143] Fixed LGTM issue with an error String --- .../org/dspace/app/rest/repository/EPersonRestRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java index e4a1ca1389..a312fd4f28 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java @@ -164,7 +164,7 @@ public class EPersonRestRepository extends DSpaceObjectRestRepository Date: Tue, 30 Jun 2020 18:20:06 +0200 Subject: [PATCH 096/143] 71654: Authorization for Downloads of restricted Bitstreams - Short lived tokens can't be used to login, or generate other tokens --- .../rest/security/jwt/JWTTokenHandler.java | 8 ++++++++ .../rest/AuthenticationRestControllerIT.java | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java index ab24a7cbae..6edd471e36 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java @@ -43,6 +43,7 @@ import org.dspace.services.ConfigurationService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.AccessDeniedException; import org.springframework.security.crypto.keygen.BytesKeyGenerator; import org.springframework.security.crypto.keygen.KeyGenerators; @@ -57,6 +58,8 @@ import org.springframework.security.crypto.keygen.KeyGenerators; public abstract class JWTTokenHandler { private static final int MAX_CLOCK_SKEW_SECONDS = 60; + private static final String AUTHORIZATION_TOKEN_PARAMETER = "token"; + private static final Logger log = LoggerFactory.getLogger(JWTTokenHandler.class); @Autowired @@ -165,6 +168,11 @@ public abstract class JWTTokenHandler { public String createTokenForEPerson(Context context, HttpServletRequest request, Date previousLoginDate, List groups) throws JOSEException, SQLException { + // Verify that the user isn't trying to use a short lived token to generate another token + if (StringUtils.isNotBlank(request.getParameter(AUTHORIZATION_TOKEN_PARAMETER))) { + throw new AccessDeniedException("Short lived tokens can't be used to generate other tokens"); + } + // Update the saved session salt for the currently logged in user, returning the user object EPerson ePerson = updateSessionSalt(context, previousLoginDate); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java index b04c99b0e8..7361dd9d6a 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java @@ -855,6 +855,25 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio .andExpect(jsonPath("$.authenticated", is(true))); } + // TODO: fix the exception. For now we want to verify a short lived token can't be used to login + @Test(expected = Exception.class) + public void testLoginWithShortLivedToken() throws Exception { + String shortLivedToken = getShortLivedToken(eperson); + + getClient().perform(post("/api/authn/login?token=" + shortLivedToken)) + .andExpect(status().isInternalServerError()); + // TODO: This internal server error needs to be fixed. This should actually produce a forbidden status + //.andExpect(status().isForbidden()); + } + + @Test + public void testGenerateShortLivedTokenWithShortLivedToken() throws Exception { + String shortLivedToken = getShortLivedToken(eperson); + + getClient().perform(post("/api/authn/shortlivedtokens?token=" + shortLivedToken)) + .andExpect(status().isForbidden()); + } + private String getShortLivedToken(EPerson requestUser) throws Exception { ObjectMapper mapper = new ObjectMapper(); From be89cc815975530e06489af79b0a4b6962f7bfd9 Mon Sep 17 00:00:00 2001 From: Peter Nijs Date: Wed, 1 Jul 2020 10:59:18 +0200 Subject: [PATCH 097/143] 71672: Authorization for Downloads of restricted Bitstreams - Rename url parameter "token" to "authentication-token" --- .../app/rest/security/jwt/JWTTokenHandler.java | 2 +- .../jwt/JWTTokenRestAuthenticationServiceImpl.java | 2 +- .../app/rest/AuthenticationRestControllerIT.java | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java index 6edd471e36..dcfb364fdd 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java @@ -58,7 +58,7 @@ import org.springframework.security.crypto.keygen.KeyGenerators; public abstract class JWTTokenHandler { private static final int MAX_CLOCK_SKEW_SECONDS = 60; - private static final String AUTHORIZATION_TOKEN_PARAMETER = "token"; + private static final String AUTHORIZATION_TOKEN_PARAMETER = "authentication-token"; private static final Logger log = LoggerFactory.getLogger(JWTTokenHandler.class); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java index bce2724c8d..1b5ee6a0c5 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java @@ -48,7 +48,7 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication private static final String AUTHORIZATION_COOKIE = "Authorization-cookie"; private static final String AUTHORIZATION_HEADER = "Authorization"; private static final String AUTHORIZATION_TYPE = "Bearer"; - private static final String AUTHORIZATION_TOKEN_PARAMETER = "token"; + private static final String AUTHORIZATION_TOKEN_PARAMETER = "authentication-token"; @Autowired private LoginJWTTokenHandler loginJWTTokenHandler; diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java index 7361dd9d6a..6976bbf350 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java @@ -804,7 +804,7 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio Bitstream bitstream = createPrivateBitstream(); String shortLivedToken = getShortLivedToken(eperson); - getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content?token=" + shortLivedToken)) + getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content?authentication-token=" + shortLivedToken)) .andExpect(status().isOk()); } @@ -821,7 +821,7 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio context.restoreAuthSystemState(); String shortLivedToken = getShortLivedToken(testEPerson); - getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content?token=" + shortLivedToken)) + getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content?authentication-token=" + shortLivedToken)) .andExpect(status().isForbidden()); } @@ -830,7 +830,7 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio Bitstream bitstream = createPrivateBitstream(); String loginToken = getAuthToken(eperson.getEmail(), password); - getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content?token=" + loginToken)) + getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content?authentication-token=" + loginToken)) .andExpect(status().isForbidden()); } @@ -840,7 +840,7 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio configurationService.setProperty("jwt.shortLived.token.expiration", "1"); String shortLivedToken = getShortLivedToken(eperson); Thread.sleep(1); - getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content?token=" + shortLivedToken)) + getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content?authentication-token=" + shortLivedToken)) .andExpect(status().isForbidden()); } @@ -860,7 +860,7 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio public void testLoginWithShortLivedToken() throws Exception { String shortLivedToken = getShortLivedToken(eperson); - getClient().perform(post("/api/authn/login?token=" + shortLivedToken)) + getClient().perform(post("/api/authn/login?authentication-token=" + shortLivedToken)) .andExpect(status().isInternalServerError()); // TODO: This internal server error needs to be fixed. This should actually produce a forbidden status //.andExpect(status().isForbidden()); @@ -870,7 +870,7 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio public void testGenerateShortLivedTokenWithShortLivedToken() throws Exception { String shortLivedToken = getShortLivedToken(eperson); - getClient().perform(post("/api/authn/shortlivedtokens?token=" + shortLivedToken)) + getClient().perform(post("/api/authn/shortlivedtokens?authentication-token=" + shortLivedToken)) .andExpect(status().isForbidden()); } From 9044daf50eb1b4044b6b4d80ce12b32e0a399712 Mon Sep 17 00:00:00 2001 From: Peter Nijs Date: Wed, 1 Jul 2020 11:50:57 +0200 Subject: [PATCH 098/143] 71672: Authorization for Downloads of restricted Bitstreams - Fix checkstyle --- .../app/rest/AuthenticationRestControllerIT.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java index 6976bbf350..513ca5ebfd 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java @@ -804,7 +804,8 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio Bitstream bitstream = createPrivateBitstream(); String shortLivedToken = getShortLivedToken(eperson); - getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content?authentication-token=" + shortLivedToken)) + getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + + "/content?authentication-token=" + shortLivedToken)) .andExpect(status().isOk()); } @@ -821,7 +822,8 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio context.restoreAuthSystemState(); String shortLivedToken = getShortLivedToken(testEPerson); - getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content?authentication-token=" + shortLivedToken)) + getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + + "/content?authentication-token=" + shortLivedToken)) .andExpect(status().isForbidden()); } @@ -830,7 +832,8 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio Bitstream bitstream = createPrivateBitstream(); String loginToken = getAuthToken(eperson.getEmail(), password); - getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content?authentication-token=" + loginToken)) + getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + + "/content?authentication-token=" + loginToken)) .andExpect(status().isForbidden()); } @@ -840,7 +843,8 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio configurationService.setProperty("jwt.shortLived.token.expiration", "1"); String shortLivedToken = getShortLivedToken(eperson); Thread.sleep(1); - getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content?authentication-token=" + shortLivedToken)) + getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + + "/content?authentication-token=" + shortLivedToken)) .andExpect(status().isForbidden()); } From 8c35f6d8a36d199b0b16f6843f2071ec6497e1c6 Mon Sep 17 00:00:00 2001 From: Andrea Bollini Date: Wed, 1 Jul 2020 12:32:32 +0200 Subject: [PATCH 099/143] DS-3679 improved test to show the bug with auth session --- .../services/email/EmailServiceImplTest.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/dspace-services/src/test/java/org/dspace/services/email/EmailServiceImplTest.java b/dspace-services/src/test/java/org/dspace/services/email/EmailServiceImplTest.java index 0be0bfe81e..958e4f8106 100644 --- a/dspace-services/src/test/java/org/dspace/services/email/EmailServiceImplTest.java +++ b/dspace-services/src/test/java/org/dspace/services/email/EmailServiceImplTest.java @@ -10,6 +10,7 @@ package org.dspace.services.email; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import javax.mail.MessagingException; import javax.mail.PasswordAuthentication; @@ -65,11 +66,42 @@ public class EmailServiceImplTest // Try to get a Session session = instance.getSession(); assertNotNull(" getSession returned null", session); + assertNull(" getSession returned authenticated session", + session.getProperties().getProperty("mail.smtp.auth")); } private static final String CFG_USERNAME = "mail.server.username"; private static final String CFG_PASSWORD = "mail.server.password"; + /** + * Test of testGetSession method, of class EmailServiceImpl when an smtp + * username is provided. + */ + @Test + public void testGetAuthenticatedInstance() { + System.out.println("getSession"); + ConfigurationService cfg = getKernel().getConfigurationService(); + + // Save existing values. + String oldUsername = cfg.getProperty(CFG_USERNAME); + String oldPassword = cfg.getProperty(CFG_PASSWORD); + + // Set known values. + cfg.setProperty(CFG_USERNAME, USERNAME); + cfg.setProperty(CFG_PASSWORD, PASSWORD); + + EmailServiceImpl instance = (EmailServiceImpl) getService(EmailServiceImpl.class); + instance.reset(); + assertNotNull(" getSession returned null", instance); + assertEquals(" authenticated session ", "true", + instance.getSession().getProperties().getProperty("mail.smtp.auth")); + + // Restore old values, if any. + cfg.setProperty(CFG_USERNAME, oldUsername); + cfg.setProperty(CFG_PASSWORD, oldPassword); + instance.reset(); + } + /** * Test of getPasswordAuthentication method, of class EmailServiceImpl. */ From fe051662528f0f186a5f88c21a1ef01bca4bbb2d Mon Sep 17 00:00:00 2001 From: Andrea Bollini Date: Wed, 1 Jul 2020 12:34:09 +0200 Subject: [PATCH 100/143] DS-3679 fix check for blank username --- .../org/dspace/services/email/EmailServiceImpl.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/dspace-services/src/main/java/org/dspace/services/email/EmailServiceImpl.java b/dspace-services/src/main/java/org/dspace/services/email/EmailServiceImpl.java index f20458f51a..d4f7851425 100644 --- a/dspace-services/src/main/java/org/dspace/services/email/EmailServiceImpl.java +++ b/dspace-services/src/main/java/org/dspace/services/email/EmailServiceImpl.java @@ -16,6 +16,7 @@ import javax.naming.NameNotFoundException; import javax.naming.NamingException; import javax.naming.NoInitialContextException; +import org.apache.commons.lang3.StringUtils; import org.dspace.kernel.mixins.InitializedService; import org.dspace.services.ConfigurationService; import org.dspace.services.EmailService; @@ -106,7 +107,7 @@ public class EmailServiceImpl props.put(key, value); } } - if (null == cfg.getProperty("mail.server.username")) { + if (StringUtils.isBlank(cfg.getProperty("mail.server.username"))) { session = Session.getInstance(props); } else { props.put("mail.smtp.auth", "true"); @@ -125,4 +126,12 @@ public class EmailServiceImpl cfg.getProperty("mail.server.username"), cfg.getProperty("mail.server.password")); } + + /** + * Force a new initialization of the session, useful for testing purpose + */ + public void reset() { + session = null; + init(); + } } From 861170a179591ee0cc78e75c430caab1cd6219c3 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Thu, 2 Jul 2020 19:55:53 +0200 Subject: [PATCH 101/143] added test and fix getLocale --- .../AbstractDSpaceRestRepository.java | 33 +++++++++++-------- .../app/rest/SubmissionFormsControllerIT.java | 31 +++++++++++++++++ 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AbstractDSpaceRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AbstractDSpaceRestRepository.java index 6460424f1c..f5ef703700 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AbstractDSpaceRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AbstractDSpaceRestRepository.java @@ -10,6 +10,7 @@ package org.dspace.app.rest.repository; import java.util.Enumeration; import java.util.Locale; +import org.apache.commons.lang3.StringUtils; import org.dspace.app.rest.converter.ConverterService; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.app.rest.utils.Utils; @@ -40,7 +41,7 @@ public abstract class AbstractDSpaceRestRepository { Context context = null; Request currentRequest = requestService.getCurrentRequest(); context = ContextUtil.obtainContext(currentRequest.getServletRequest()); - Locale currentLocale = getLocal(context, currentRequest); + Locale currentLocale = getLocale(context, currentRequest); context.setCurrentLocale(currentLocale); return context; } @@ -49,26 +50,30 @@ public abstract class AbstractDSpaceRestRepository { return requestService; } - private Locale getLocal(Context context, Request request) { + private Locale getLocale(Context context, Request request) { Locale userLocale = null; Locale supportedLocale = null; - if (context.getCurrentUser() != null) { + + // Locales requested from client + String locale = request.getHttpServletRequest().getHeader("Accept-Language"); + if (StringUtils.isNotBlank(locale)) { + Enumeration locales = request.getHttpServletRequest().getLocales(); + if (locales != null) { + while (locales.hasMoreElements()) { + Locale current = locales.nextElement(); + if (I18nUtil.isSupportedLocale(current)) { + userLocale = current; + break; + } + } + } + } + if (userLocale == null && context.getCurrentUser() != null) { String userLanguage = context.getCurrentUser().getLanguage(); if (userLanguage != null) { userLocale = new Locale(userLanguage); } } - // Locales requested from client - Enumeration locales = request.getHttpServletRequest().getLocales(); - if (locales != null) { - while (locales.hasMoreElements()) { - Locale current = locales.nextElement(); - if (I18nUtil.isSupportedLocale(current)) { - userLocale = current; - break; - } - } - } if (userLocale == null) { return I18nUtil.getDefaultLocale(); } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionFormsControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionFormsControllerIT.java index 53c41c62d4..4eee3579ee 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionFormsControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionFormsControllerIT.java @@ -411,6 +411,37 @@ public class SubmissionFormsControllerIT extends AbstractControllerIntegrationTe resetLocalesConfiguration(); } + @Test + public void supportLanguageUsingMultipleLocaleTest() throws Exception { + context.turnOffAuthorisationSystem(); + String[] supportedLanguage = {"it","uk","en"}; + configurationService.setProperty("default.locale","en"); + configurationService.setProperty("webui.supported.locales",supportedLanguage); + submissionFormRestRepository.reload(); + + context.restoreAuthSystemState(); + + String tokenEperson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEperson).perform(get("/api/config/submissionforms/languagetest") + .header("Accept-Language", "fr;q=1, it;q=0.9")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.id", is("languagetest"))) + .andExpect(jsonPath("$.name", is("languagetest"))) + .andExpect(jsonPath("$.type", is("submissionform"))) + .andExpect(jsonPath("$._links.self.href", Matchers + .startsWith(REST_SERVER_URL + "config/submissionforms/languagetest"))) + .andExpect(jsonPath("$.rows[0].fields", contains(SubmissionFormFieldMatcher + .matchFormFieldDefinition("name", "Autore", "\u00C8 richiesto almeno un autore", true, + "Aggiungi un autore", "dc.contributor.author")))) + .andExpect(jsonPath("$.rows[1].fields", contains(SubmissionFormFieldMatcher + .matchFormFieldDefinition("onebox", "Titolo", + "\u00C8 necessario inserire un titolo principale per questo item", false, + "Inserisci titolo principale di questo item", "dc.title")))); + + resetLocalesConfiguration(); + } + private void resetLocalesConfiguration() throws DCInputsReaderException { configurationService.setProperty("default.locale","en"); configurationService.setProperty("webui.supported.locales",null); From dc2151209a18c5ad112c48301d5cd68267708b8e Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Thu, 2 Jul 2020 14:21:56 -0500 Subject: [PATCH 102/143] [maven-release-plugin] prepare release dspace-7.0-beta3 --- dspace-api/pom.xml | 2 +- dspace-oai/pom.xml | 2 +- dspace-rdf/pom.xml | 2 +- dspace-rest/pom.xml | 4 ++-- dspace-server-webapp/pom.xml | 2 +- dspace-services/pom.xml | 2 +- dspace-sword/pom.xml | 2 +- dspace-swordv2/pom.xml | 2 +- dspace/modules/additions/pom.xml | 4 ++-- dspace/modules/pom.xml | 2 +- dspace/modules/rest/pom.xml | 2 +- dspace/modules/server/pom.xml | 4 ++-- dspace/pom.xml | 2 +- pom.xml | 26 +++++++++++++------------- 14 files changed, 29 insertions(+), 29 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index a0714c04ee..6571b5f6b1 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -12,7 +12,7 @@ org.dspace dspace-parent - 7.0-beta3-SNAPSHOT + 7.0-beta3 .. diff --git a/dspace-oai/pom.xml b/dspace-oai/pom.xml index d5a129c90a..137c990969 100644 --- a/dspace-oai/pom.xml +++ b/dspace-oai/pom.xml @@ -8,7 +8,7 @@ dspace-parent org.dspace - 7.0-beta3-SNAPSHOT + 7.0-beta3 .. diff --git a/dspace-rdf/pom.xml b/dspace-rdf/pom.xml index 47fc5cd204..3a3bfe833c 100644 --- a/dspace-rdf/pom.xml +++ b/dspace-rdf/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 7.0-beta3-SNAPSHOT + 7.0-beta3 .. diff --git a/dspace-rest/pom.xml b/dspace-rest/pom.xml index 1038617b49..45694cd2d4 100644 --- a/dspace-rest/pom.xml +++ b/dspace-rest/pom.xml @@ -3,7 +3,7 @@ org.dspace dspace-rest war - 7.0-beta3-SNAPSHOT + 7.0-beta3 DSpace (Deprecated) REST Webapp DSpace RESTful Web Services API. NOTE: this REST API is DEPRECATED. Please consider using the REST API in the dspace-server-webapp instead! @@ -12,7 +12,7 @@ org.dspace dspace-parent - 7.0-beta3-SNAPSHOT + 7.0-beta3 .. diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index d8cc6a7587..6f04013c31 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.0-beta3-SNAPSHOT + 7.0-beta3 .. diff --git a/dspace-services/pom.xml b/dspace-services/pom.xml index 1670c8454f..b1d92238e1 100644 --- a/dspace-services/pom.xml +++ b/dspace-services/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 7.0-beta3-SNAPSHOT + 7.0-beta3 diff --git a/dspace-sword/pom.xml b/dspace-sword/pom.xml index 7bb709932a..3d56f3541e 100644 --- a/dspace-sword/pom.xml +++ b/dspace-sword/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.0-beta3-SNAPSHOT + 7.0-beta3 .. diff --git a/dspace-swordv2/pom.xml b/dspace-swordv2/pom.xml index 934eae3682..cb92dcab91 100644 --- a/dspace-swordv2/pom.xml +++ b/dspace-swordv2/pom.xml @@ -13,7 +13,7 @@ org.dspace dspace-parent - 7.0-beta3-SNAPSHOT + 7.0-beta3 .. diff --git a/dspace/modules/additions/pom.xml b/dspace/modules/additions/pom.xml index 0c05de84a4..e34a740642 100644 --- a/dspace/modules/additions/pom.xml +++ b/dspace/modules/additions/pom.xml @@ -17,7 +17,7 @@ org.dspace modules - 7.0-beta3-SNAPSHOT + 7.0-beta3 .. @@ -158,7 +158,7 @@ org.dspace dspace-api - 7.0-beta3-SNAPSHOT + 7.0-beta3 test-jar test diff --git a/dspace/modules/pom.xml b/dspace/modules/pom.xml index 84c685a981..c5951c13d3 100644 --- a/dspace/modules/pom.xml +++ b/dspace/modules/pom.xml @@ -11,7 +11,7 @@ org.dspace dspace-parent - 7.0-beta3-SNAPSHOT + 7.0-beta3 ../../pom.xml diff --git a/dspace/modules/rest/pom.xml b/dspace/modules/rest/pom.xml index a17ff70f80..ba0dd1b45d 100644 --- a/dspace/modules/rest/pom.xml +++ b/dspace/modules/rest/pom.xml @@ -13,7 +13,7 @@ org.dspace modules - 7.0-beta3-SNAPSHOT + 7.0-beta3 .. diff --git a/dspace/modules/server/pom.xml b/dspace/modules/server/pom.xml index 06d466f522..78600f0f0c 100644 --- a/dspace/modules/server/pom.xml +++ b/dspace/modules/server/pom.xml @@ -13,7 +13,7 @@ just adding new jar in the classloader modules org.dspace - 7.0-beta3-SNAPSHOT + 7.0-beta3 .. @@ -194,7 +194,7 @@ just adding new jar in the classloader org.dspace dspace-server-webapp - 7.0-beta3-SNAPSHOT + 7.0-beta3 test-jar test diff --git a/dspace/pom.xml b/dspace/pom.xml index 6c693eacec..48e1f0ceb6 100644 --- a/dspace/pom.xml +++ b/dspace/pom.xml @@ -16,7 +16,7 @@ org.dspace dspace-parent - 7.0-beta3-SNAPSHOT + 7.0-beta3 .. diff --git a/pom.xml b/pom.xml index aa8a8b15dd..a68760ba88 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.dspace dspace-parent pom - 7.0-beta3-SNAPSHOT + 7.0-beta3 DSpace Parent Project DSpace open source software is a turnkey institutional repository application. @@ -848,14 +848,14 @@ org.dspace dspace-rest - 7.0-beta3-SNAPSHOT + 7.0-beta3 jar classes org.dspace dspace-rest - 7.0-beta3-SNAPSHOT + 7.0-beta3 war @@ -1008,50 +1008,50 @@ org.dspace dspace-api - 7.0-beta3-SNAPSHOT + 7.0-beta3 org.dspace.modules additions - 7.0-beta3-SNAPSHOT + 7.0-beta3 org.dspace dspace-sword - 7.0-beta3-SNAPSHOT + 7.0-beta3 org.dspace dspace-swordv2 - 7.0-beta3-SNAPSHOT + 7.0-beta3 org.dspace dspace-oai - 7.0-beta3-SNAPSHOT + 7.0-beta3 org.dspace dspace-services - 7.0-beta3-SNAPSHOT + 7.0-beta3 org.dspace dspace-rdf - 7.0-beta3-SNAPSHOT + 7.0-beta3 org.dspace dspace-server-webapp - 7.0-beta3-SNAPSHOT + 7.0-beta3 jar classes org.dspace dspace-server-webapp - 7.0-beta3-SNAPSHOT + 7.0-beta3 war @@ -1812,7 +1812,7 @@ scm:git:git@github.com:DSpace/DSpace.git scm:git:git@github.com:DSpace/DSpace.git git@github.com:DSpace/DSpace.git - HEAD + dspace-7.0-beta3 From cc7a6c30fd83d153fcd7d5b9af5f4a5454f0c342 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Thu, 2 Jul 2020 14:22:06 -0500 Subject: [PATCH 103/143] [maven-release-plugin] prepare for next development iteration --- dspace-api/pom.xml | 2 +- dspace-oai/pom.xml | 2 +- dspace-rdf/pom.xml | 2 +- dspace-rest/pom.xml | 4 ++-- dspace-server-webapp/pom.xml | 2 +- dspace-services/pom.xml | 2 +- dspace-sword/pom.xml | 2 +- dspace-swordv2/pom.xml | 2 +- dspace/modules/additions/pom.xml | 4 ++-- dspace/modules/pom.xml | 2 +- dspace/modules/rest/pom.xml | 2 +- dspace/modules/server/pom.xml | 4 ++-- dspace/pom.xml | 2 +- pom.xml | 26 +++++++++++++------------- 14 files changed, 29 insertions(+), 29 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 6571b5f6b1..49ca062aa6 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -12,7 +12,7 @@ org.dspace dspace-parent - 7.0-beta3 + 7.0-beta4-SNAPSHOT .. diff --git a/dspace-oai/pom.xml b/dspace-oai/pom.xml index 137c990969..4036348be0 100644 --- a/dspace-oai/pom.xml +++ b/dspace-oai/pom.xml @@ -8,7 +8,7 @@ dspace-parent org.dspace - 7.0-beta3 + 7.0-beta4-SNAPSHOT .. diff --git a/dspace-rdf/pom.xml b/dspace-rdf/pom.xml index 3a3bfe833c..4287188c8e 100644 --- a/dspace-rdf/pom.xml +++ b/dspace-rdf/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 7.0-beta3 + 7.0-beta4-SNAPSHOT .. diff --git a/dspace-rest/pom.xml b/dspace-rest/pom.xml index 45694cd2d4..744ee24203 100644 --- a/dspace-rest/pom.xml +++ b/dspace-rest/pom.xml @@ -3,7 +3,7 @@ org.dspace dspace-rest war - 7.0-beta3 + 7.0-beta4-SNAPSHOT DSpace (Deprecated) REST Webapp DSpace RESTful Web Services API. NOTE: this REST API is DEPRECATED. Please consider using the REST API in the dspace-server-webapp instead! @@ -12,7 +12,7 @@ org.dspace dspace-parent - 7.0-beta3 + 7.0-beta4-SNAPSHOT .. diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 6f04013c31..d1f54fd9d6 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.0-beta3 + 7.0-beta4-SNAPSHOT .. diff --git a/dspace-services/pom.xml b/dspace-services/pom.xml index b1d92238e1..95ab75f52e 100644 --- a/dspace-services/pom.xml +++ b/dspace-services/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 7.0-beta3 + 7.0-beta4-SNAPSHOT diff --git a/dspace-sword/pom.xml b/dspace-sword/pom.xml index 3d56f3541e..4b28f091db 100644 --- a/dspace-sword/pom.xml +++ b/dspace-sword/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.0-beta3 + 7.0-beta4-SNAPSHOT .. diff --git a/dspace-swordv2/pom.xml b/dspace-swordv2/pom.xml index cb92dcab91..e0642eaa0b 100644 --- a/dspace-swordv2/pom.xml +++ b/dspace-swordv2/pom.xml @@ -13,7 +13,7 @@ org.dspace dspace-parent - 7.0-beta3 + 7.0-beta4-SNAPSHOT .. diff --git a/dspace/modules/additions/pom.xml b/dspace/modules/additions/pom.xml index e34a740642..59ba279c3f 100644 --- a/dspace/modules/additions/pom.xml +++ b/dspace/modules/additions/pom.xml @@ -17,7 +17,7 @@ org.dspace modules - 7.0-beta3 + 7.0-beta4-SNAPSHOT .. @@ -158,7 +158,7 @@ org.dspace dspace-api - 7.0-beta3 + 7.0-beta4-SNAPSHOT test-jar test diff --git a/dspace/modules/pom.xml b/dspace/modules/pom.xml index c5951c13d3..a875a17ca9 100644 --- a/dspace/modules/pom.xml +++ b/dspace/modules/pom.xml @@ -11,7 +11,7 @@ org.dspace dspace-parent - 7.0-beta3 + 7.0-beta4-SNAPSHOT ../../pom.xml diff --git a/dspace/modules/rest/pom.xml b/dspace/modules/rest/pom.xml index ba0dd1b45d..2f1addb2f9 100644 --- a/dspace/modules/rest/pom.xml +++ b/dspace/modules/rest/pom.xml @@ -13,7 +13,7 @@ org.dspace modules - 7.0-beta3 + 7.0-beta4-SNAPSHOT .. diff --git a/dspace/modules/server/pom.xml b/dspace/modules/server/pom.xml index 78600f0f0c..4ecf2cf5b2 100644 --- a/dspace/modules/server/pom.xml +++ b/dspace/modules/server/pom.xml @@ -13,7 +13,7 @@ just adding new jar in the classloader modules org.dspace - 7.0-beta3 + 7.0-beta4-SNAPSHOT .. @@ -194,7 +194,7 @@ just adding new jar in the classloader org.dspace dspace-server-webapp - 7.0-beta3 + 7.0-beta4-SNAPSHOT test-jar test diff --git a/dspace/pom.xml b/dspace/pom.xml index 48e1f0ceb6..f468746b62 100644 --- a/dspace/pom.xml +++ b/dspace/pom.xml @@ -16,7 +16,7 @@ org.dspace dspace-parent - 7.0-beta3 + 7.0-beta4-SNAPSHOT .. diff --git a/pom.xml b/pom.xml index a68760ba88..a3b4b30c98 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.dspace dspace-parent pom - 7.0-beta3 + 7.0-beta4-SNAPSHOT DSpace Parent Project DSpace open source software is a turnkey institutional repository application. @@ -848,14 +848,14 @@ org.dspace dspace-rest - 7.0-beta3 + 7.0-beta4-SNAPSHOT jar classes org.dspace dspace-rest - 7.0-beta3 + 7.0-beta4-SNAPSHOT war @@ -1008,50 +1008,50 @@ org.dspace dspace-api - 7.0-beta3 + 7.0-beta4-SNAPSHOT org.dspace.modules additions - 7.0-beta3 + 7.0-beta4-SNAPSHOT org.dspace dspace-sword - 7.0-beta3 + 7.0-beta4-SNAPSHOT org.dspace dspace-swordv2 - 7.0-beta3 + 7.0-beta4-SNAPSHOT org.dspace dspace-oai - 7.0-beta3 + 7.0-beta4-SNAPSHOT org.dspace dspace-services - 7.0-beta3 + 7.0-beta4-SNAPSHOT org.dspace dspace-rdf - 7.0-beta3 + 7.0-beta4-SNAPSHOT org.dspace dspace-server-webapp - 7.0-beta3 + 7.0-beta4-SNAPSHOT jar classes org.dspace dspace-server-webapp - 7.0-beta3 + 7.0-beta4-SNAPSHOT war @@ -1812,7 +1812,7 @@ scm:git:git@github.com:DSpace/DSpace.git scm:git:git@github.com:DSpace/DSpace.git git@github.com:DSpace/DSpace.git - dspace-7.0-beta3 + HEAD From b524c36a53b5d6625fba2c21bc2e3adcd9d28a32 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2020 19:22:27 +0000 Subject: [PATCH 104/143] Bump commons-beanutils from 1.9.3 to 1.9.4 Bumps commons-beanutils from 1.9.3 to 1.9.4. Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a3b4b30c98..b74e76da5c 100644 --- a/pom.xml +++ b/pom.xml @@ -1261,7 +1261,7 @@ commons-beanutils commons-beanutils - 1.9.3 + 1.9.4 commons-cli From fcdee503343aa7eac61f0f3689391047fbd3304e Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 3 Jul 2020 10:55:52 -0500 Subject: [PATCH 105/143] Fixes to error handling Log all errors & provide generic messages in response --- .../DSpaceApiExceptionControllerAdvice.java | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java index d255b6fe27..52c691723f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java @@ -14,6 +14,8 @@ import java.sql.SQLException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.security.RestAuthenticationService; import org.dspace.authorize.AuthorizeException; import org.springframework.beans.TypeMismatchException; @@ -41,6 +43,7 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExcep */ @ControllerAdvice public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionHandler { + private static final Logger log = LogManager.getLogger(DSpaceApiExceptionControllerAdvice.class); @Autowired private RestAuthenticationService restAuthenticationService; @@ -49,16 +52,16 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH protected void handleAuthorizeException(HttpServletRequest request, HttpServletResponse response, Exception ex) throws IOException { if (restAuthenticationService.hasAuthenticationData(request)) { - sendErrorResponse(request, response, ex, ex.getMessage(), HttpServletResponse.SC_FORBIDDEN); + sendErrorResponse(request, response, ex, "Access is denied", HttpServletResponse.SC_FORBIDDEN); } else { - sendErrorResponse(request, response, ex, ex.getMessage(), HttpServletResponse.SC_UNAUTHORIZED); + sendErrorResponse(request, response, ex, "Authentication is required", HttpServletResponse.SC_UNAUTHORIZED); } } @ExceptionHandler({IllegalArgumentException.class, MultipartException.class}) protected void handleWrongRequestException(HttpServletRequest request, HttpServletResponse response, Exception ex) throws IOException { - sendErrorResponse(request, response, ex, ex.getMessage(), HttpServletResponse.SC_BAD_REQUEST); + sendErrorResponse(request, response, ex, "Bad or invalid request", HttpServletResponse.SC_BAD_REQUEST); } @ExceptionHandler(SQLException.class) @@ -72,24 +75,23 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH protected void handleIOException(HttpServletRequest request, HttpServletResponse response, Exception ex) throws IOException { sendErrorResponse(request, response, ex, - "An internal read or write operation failed (IO Exception)", + "An internal read or write operation failed", HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } @ExceptionHandler(MethodNotAllowedException.class) protected void methodNotAllowedException(HttpServletRequest request, HttpServletResponse response, Exception ex) throws IOException { - sendErrorResponse(request, response, ex, ex.getMessage(), HttpServletResponse.SC_METHOD_NOT_ALLOWED); + sendErrorResponse(request, response, ex, "Method is not allowed or supported", HttpServletResponse.SC_METHOD_NOT_ALLOWED); } @ExceptionHandler( {UnprocessableEntityException.class}) protected void handleUnprocessableEntityException(HttpServletRequest request, HttpServletResponse response, Exception ex) throws IOException { - //422 is not defined in HttpServletResponse. Its meaning is "Unprocessable Entity". //Using the value from HttpStatus. sendErrorResponse(request, response, null, - ex.getMessage(), + "Unprocessable or invalid entity", HttpStatus.UNPROCESSABLE_ENTITY.value()); } @@ -98,7 +100,7 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH throws IOException { // we want the 400 status for missing parameters, see https://jira.lyrasis.org/browse/DS-4428 sendErrorResponse(request, response, null, - ex.getMessage(), + "Required parameters are invalid", HttpStatus.BAD_REQUEST.value()); } @@ -107,7 +109,7 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH throws IOException { // we want the 400 status for missing parameters, see https://jira.lyrasis.org/browse/DS-4428 sendErrorResponse(request, response, null, - ex.getMessage(), + "Required parameters are missing", HttpStatus.BAD_REQUEST.value()); } @@ -137,7 +139,7 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH } else { returnCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR; } - sendErrorResponse(request, response, ex, "An Exception has occured", returnCode); + sendErrorResponse(request, response, ex, "An Exception has occurred", returnCode); } @@ -147,6 +149,9 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH //Make sure Spring picks up this exception request.setAttribute(EXCEPTION_ATTRIBUTE, ex); + //Log the full error and status code + log.error(message + " (status={})", statusCode, ex); + //Exception properties will be set by org.springframework.boot.web.support.ErrorPageFilter response.sendError(statusCode, message); } From 41dbbfedd43ca4adb1e2760eff79f53d86f5b368 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 3 Jul 2020 15:16:45 -0500 Subject: [PATCH 106/143] Fix bugs and failed test --- .../DSpaceApiExceptionControllerAdvice.java | 13 +++++++------ .../rest/WorkflowDefinitionRestRepositoryIT.java | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java index 52c691723f..6cf297ebf6 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java @@ -61,7 +61,7 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH @ExceptionHandler({IllegalArgumentException.class, MultipartException.class}) protected void handleWrongRequestException(HttpServletRequest request, HttpServletResponse response, Exception ex) throws IOException { - sendErrorResponse(request, response, ex, "Bad or invalid request", HttpServletResponse.SC_BAD_REQUEST); + sendErrorResponse(request, response, ex, "Request is invalid or incorrect", HttpServletResponse.SC_BAD_REQUEST); } @ExceptionHandler(SQLException.class) @@ -82,7 +82,8 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH @ExceptionHandler(MethodNotAllowedException.class) protected void methodNotAllowedException(HttpServletRequest request, HttpServletResponse response, Exception ex) throws IOException { - sendErrorResponse(request, response, ex, "Method is not allowed or supported", HttpServletResponse.SC_METHOD_NOT_ALLOWED); + sendErrorResponse(request, response, ex, "Method is not allowed or supported", + HttpServletResponse.SC_METHOD_NOT_ALLOWED); } @ExceptionHandler( {UnprocessableEntityException.class}) @@ -100,7 +101,7 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH throws IOException { // we want the 400 status for missing parameters, see https://jira.lyrasis.org/browse/DS-4428 sendErrorResponse(request, response, null, - "Required parameters are invalid", + "A required parameter is invalid", HttpStatus.BAD_REQUEST.value()); } @@ -109,7 +110,7 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH throws IOException { // we want the 400 status for missing parameters, see https://jira.lyrasis.org/browse/DS-4428 sendErrorResponse(request, response, null, - "Required parameters are missing", + "A required parameter is missing", HttpStatus.BAD_REQUEST.value()); } @@ -139,7 +140,7 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH } else { returnCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR; } - sendErrorResponse(request, response, ex, "An Exception has occurred", returnCode); + sendErrorResponse(request, response, ex, "An exception has occurred", returnCode); } @@ -150,7 +151,7 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH request.setAttribute(EXCEPTION_ATTRIBUTE, ex); //Log the full error and status code - log.error(message + " (status={})", statusCode, ex); + log.error("{} (status:{})", message, statusCode, ex); //Exception properties will be set by org.springframework.boot.web.support.ErrorPageFilter response.sendError(statusCode, message); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowDefinitionRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowDefinitionRestRepositoryIT.java index 4a877825b9..7d8b7f7065 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowDefinitionRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowDefinitionRestRepositoryIT.java @@ -240,7 +240,7 @@ public class WorkflowDefinitionRestRepositoryIT extends AbstractControllerIntegr getClient(token).perform(get(WORKFLOW_DEFINITIONS_ENDPOINT + "/search/findByCollection?uuid=" + nonValidUUID)) //We expect a 400 Illegal Argument Exception (Bad Request) cannot convert UUID .andExpect(status().isBadRequest()) - .andExpect(status().reason(containsString("Failed to convert " + nonValidUUID))); + .andExpect(status().reason(containsString("A required parameter is invalid"))); } @Test From e52e88004565525c4e501bf943dd5b0fb542911a Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Tue, 7 Jul 2020 14:39:40 +0200 Subject: [PATCH 107/143] [Task 71690] implemented the IPAuthenticationFilter --- .../rest/security/IPAuthenticationFilter.java | 67 +++++++++++++++++++ .../security/WebSecurityConfiguration.java | 7 +- dspace/config/modules/authentication.cfg | 2 +- 3 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/security/IPAuthenticationFilter.java diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/IPAuthenticationFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/IPAuthenticationFilter.java new file mode 100644 index 0000000000..e0f4d189c4 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/IPAuthenticationFilter.java @@ -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.security; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.List; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.log4j.Logger; +import org.dspace.app.rest.utils.ContextUtil; +import org.dspace.authenticate.service.AuthenticationService; +import org.dspace.core.Context; +import org.dspace.eperson.Group; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; + +/** + * This is a Filter class that'll fetch special groups from the {@link AuthenticationService} and set these in the + * current DSpace Context. This will allow us to set a specific Group to a specific IP so that any request from that + * IP is always treated as being a part of the configured group. + * The configuration for the authentication through ip can be fined in authentication-ip.cfg + * This can be enabled by uncommenting the IPAuhentication plugin in authentication.cfg + */ +public class IPAuthenticationFilter extends BasicAuthenticationFilter { + + private static final Logger log = Logger.getLogger(IPAuthenticationFilter.class); + + private AuthenticationService authenticationService; + + /** + * Constructor for the class + * @param authenticationManager The relevant AuthenticationManager + * @param authenticationService The autowired AuthenticationService + */ + public IPAuthenticationFilter(AuthenticationManager authenticationManager, + AuthenticationService authenticationService) { + super(authenticationManager); + this.authenticationService = authenticationService; + } + + @Override + protected void doFilterInternal(HttpServletRequest req, + HttpServletResponse res, + FilterChain chain) throws IOException, ServletException { + + Context context = ContextUtil.obtainContext(req); + try { + List groups = authenticationService.getSpecialGroups(context, req); + for (Group group : groups) { + context.setSpecialGroup(group.getID()); + } + } catch (SQLException e) { + log.error("Something went wrong trying to fetch groups in IPAuthenticationFilter", e); + } + chain.doFilter(req, res); + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java index 32c0cdda00..c5b5dff39d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java @@ -7,6 +7,7 @@ */ package org.dspace.app.rest.security; +import org.dspace.authenticate.service.AuthenticationService; import org.dspace.services.RequestService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.security.SecurityProperties; @@ -53,6 +54,9 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired private CustomLogoutHandler customLogoutHandler; + @Autowired + private AuthenticationService authenticationService; + @Override public void configure(WebSecurity webSecurity) throws Exception { webSecurity @@ -103,7 +107,8 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { //Everyone can call GET on the status endpoint .antMatchers(HttpMethod.GET, "/api/authn/status").permitAll() .and() - + .addFilterBefore(new IPAuthenticationFilter(authenticationManager(), authenticationService), + StatelessAuthenticationFilter.class) //Add a filter before our login endpoints to do the authentication based on the data in the HTTP request .addFilterBefore(new StatelessLoginFilter("/api/authn/login", authenticationManager(), restAuthenticationService), diff --git a/dspace/config/modules/authentication.cfg b/dspace/config/modules/authentication.cfg index f22e2eaf19..fab1f577d2 100644 --- a/dspace/config/modules/authentication.cfg +++ b/dspace/config/modules/authentication.cfg @@ -34,7 +34,7 @@ # defining a new order in local.cfg. # IP-based authentication/authorization. See authentication-ip.cfg for default configuration. -#plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.IPAuthentication +plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.IPAuthentication # LDAP authentication/authorization. See authentication-ldap.cfg for default configuration. #plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.LDAPAuthentication From f34f44d7e00f5258ef875b3e2ddd2ac88dde5920 Mon Sep 17 00:00:00 2001 From: Ian Little Date: Tue, 7 Jul 2020 09:52:38 -0400 Subject: [PATCH 108/143] Bump Handle to version 9.3.0-SNAPSHOT, now coming form CNRI maven repository. Requires bumping Jetty version also. --- dspace-api/pom.xml | 63 +++++++++++++++++++- dspace-server-webapp/pom.xml | 8 +++ dspace/config/log4j-handle-plugin.properties | 4 +- dspace/modules/server/pom.xml | 8 +++ pom.xml | 17 ++++-- 5 files changed, 92 insertions(+), 8 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 49ca062aa6..7a8662bcff 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -291,9 +291,20 @@ - org.dspace + net.handle handle + + net.cnri + cnri-servlet-container + + + + org.ow2.asm + asm-commons + + + org.eclipse.jetty @@ -471,11 +482,59 @@ solr-cell ${solr.client.version} - + org.ow2.asm asm-commons + + org.bouncycastle + bcpkix-jdk15on + + + org.bouncycastle + bcprov-jdk15on + + + org.eclipse.jetty + jetty-xml + + + org.eclipse.jetty + jetty-http + + + org.eclipse.jetty + jetty-servlet + + + org.eclipse.jetty + jetty-webapp + + + org.eclipse.jetty + jetty-util + + + org.eclipse.jetty + jetty-deploy + + + org.eclipse.jetty + jetty-continuation + + + org.eclipse.jetty + jetty-servlets + + + org.eclipse.jetty + jetty-io + + + org.eclipse.jetty + jetty-security + diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index d1f54fd9d6..ccb78dde49 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -460,6 +460,14 @@ solr-cell test + + org.bouncycastle + bcpkix-jdk15on + + + org.bouncycastle + bcprov-jdk15on + org.eclipse.jetty jetty-continuation diff --git a/dspace/config/log4j-handle-plugin.properties b/dspace/config/log4j-handle-plugin.properties index 72381a698c..44d39fb1bd 100644 --- a/dspace/config/log4j-handle-plugin.properties +++ b/dspace/config/log4j-handle-plugin.properties @@ -20,12 +20,12 @@ log.dir=${dspace.dir}/log log4j.rootCategory=INFO, A1 # A1 is set to be a DailyRollingFileAppender. -log4j.appender.A1=org.apache.logging.log4j.DailyRollingFileAppender +log4j.appender.A1=org.apache.log4j.DailyRollingFileAppender log4j.appender.A1.File=${log.dir}/handle-plugin.log log4j.appender.A1.DatePattern='.'yyyy-MM-dd # A1 uses PatternLayout. -log4j.appender.A1.layout=org.apache.logging.log4j.PatternLayout +log4j.appender.A1.layout=org.apache.log4j.PatternLayout log4j.appender.A1.layout.ConversionPattern=%d %-5p %c @ %m%n diff --git a/dspace/modules/server/pom.xml b/dspace/modules/server/pom.xml index 4ecf2cf5b2..174d6eb019 100644 --- a/dspace/modules/server/pom.xml +++ b/dspace/modules/server/pom.xml @@ -266,6 +266,14 @@ just adding new jar in the classloader solr-cell test + + org.bouncycastle + bcpkix-jdk15on + + + org.bouncycastle + bcprov-jdk15on + org.eclipse.jetty jetty-continuation diff --git a/pom.xml b/pom.xml index a3b4b30c98..5f50f577bb 100644 --- a/pom.xml +++ b/pom.xml @@ -36,7 +36,7 @@ 2.3.1 2.3.1 - 9.4.8.v20171121 + 9.4.15.v20190215 2.11.2 2.0.15 3.17 @@ -1236,9 +1236,14 @@ - org.dspace + net.handle handle - 9.1.0.v20190416 + 9.3.0-SNAPSHOT + + + net.cnri + cnri-servlet-container + 3.0.0-SNAPSHOT @@ -1590,7 +1595,7 @@ com.google.code.gson gson - 2.6.1 + 2.8.6 compile @@ -1842,6 +1847,10 @@ true + + handle.net + https://handle.net/maven + From 4e129cf841e7c103b77e488858f46d3a9744f1c3 Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Wed, 8 Jul 2020 10:23:08 +0200 Subject: [PATCH 109/143] [Task 71753] implemented feedback and added tests for the ip authentication functionality --- .../test/data/dspaceFolder/config/local.cfg | 3 + ...onymousAdditionalAuthorizationFilter.java} | 12 +- .../security/WebSecurityConfiguration.java | 2 +- ...nymousAdditionalAuthorizationFilterIT.java | 144 ++++++++++++++++++ .../AbstractControllerIntegrationTest.java | 17 +++ dspace/config/modules/authentication.cfg | 2 +- 6 files changed, 173 insertions(+), 7 deletions(-) rename dspace-server-webapp/src/main/java/org/dspace/app/rest/security/{IPAuthenticationFilter.java => AnonymousAdditionalAuthorizationFilter.java} (78%) create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/AnonymousAdditionalAuthorizationFilterIT.java diff --git a/dspace-api/src/test/data/dspaceFolder/config/local.cfg b/dspace-api/src/test/data/dspaceFolder/config/local.cfg index 51ce1a0165..2455c68cfd 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/local.cfg +++ b/dspace-api/src/test/data/dspaceFolder/config/local.cfg @@ -120,3 +120,6 @@ rest.properties.exposed = configuration.not.existing configuration.not.exposed = secret_value configuration.exposed.single.value = public_value configuration.exposed.array.value = public_value_1, public_value_2 + +authentication-ip.Staff = 5.5.5.5 +authentication-ip.Student = 6.6.6.6 diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/IPAuthenticationFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AnonymousAdditionalAuthorizationFilter.java similarity index 78% rename from dspace-server-webapp/src/main/java/org/dspace/app/rest/security/IPAuthenticationFilter.java rename to dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AnonymousAdditionalAuthorizationFilter.java index e0f4d189c4..ef040daf12 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/IPAuthenticationFilter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AnonymousAdditionalAuthorizationFilter.java @@ -25,14 +25,16 @@ import org.springframework.security.web.authentication.www.BasicAuthenticationFi /** * This is a Filter class that'll fetch special groups from the {@link AuthenticationService} and set these in the - * current DSpace Context. This will allow us to set a specific Group to a specific IP so that any request from that + * current DSpace Context. It'll do extra processing on anonymous requests to see which authorizations they + * can implicitly have and adds those + * This will allow us to for example set a specific Group to a specific IP so that any request from that * IP is always treated as being a part of the configured group. * The configuration for the authentication through ip can be fined in authentication-ip.cfg * This can be enabled by uncommenting the IPAuhentication plugin in authentication.cfg */ -public class IPAuthenticationFilter extends BasicAuthenticationFilter { +public class AnonymousAdditionalAuthorizationFilter extends BasicAuthenticationFilter { - private static final Logger log = Logger.getLogger(IPAuthenticationFilter.class); + private static final Logger log = Logger.getLogger(AnonymousAdditionalAuthorizationFilter.class); private AuthenticationService authenticationService; @@ -41,8 +43,8 @@ public class IPAuthenticationFilter extends BasicAuthenticationFilter { * @param authenticationManager The relevant AuthenticationManager * @param authenticationService The autowired AuthenticationService */ - public IPAuthenticationFilter(AuthenticationManager authenticationManager, - AuthenticationService authenticationService) { + public AnonymousAdditionalAuthorizationFilter(AuthenticationManager authenticationManager, + AuthenticationService authenticationService) { super(authenticationManager); this.authenticationService = authenticationService; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java index c5b5dff39d..4471262ef6 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java @@ -107,7 +107,7 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { //Everyone can call GET on the status endpoint .antMatchers(HttpMethod.GET, "/api/authn/status").permitAll() .and() - .addFilterBefore(new IPAuthenticationFilter(authenticationManager(), authenticationService), + .addFilterBefore(new AnonymousAdditionalAuthorizationFilter(authenticationManager(), authenticationService), StatelessAuthenticationFilter.class) //Add a filter before our login endpoints to do the authentication based on the data in the HTTP request .addFilterBefore(new StatelessLoginFilter("/api/authn/login", authenticationManager(), diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AnonymousAdditionalAuthorizationFilterIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AnonymousAdditionalAuthorizationFilterIT.java new file mode 100644 index 0000000000..779bbabee8 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AnonymousAdditionalAuthorizationFilterIT.java @@ -0,0 +1,144 @@ +/** + * 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.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.dspace.app.rest.builder.CollectionBuilder; +import org.dspace.app.rest.builder.CommunityBuilder; +import org.dspace.app.rest.builder.GroupBuilder; +import org.dspace.app.rest.builder.ItemBuilder; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.Item; +import org.dspace.eperson.Group; +import org.dspace.eperson.service.GroupService; +import org.dspace.services.ConfigurationService; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +public class AnonymousAdditionalAuthorizationFilterIT extends AbstractControllerIntegrationTest { + + @Autowired + private ConfigurationService configurationService; + + @Autowired + private GroupService groupService; + + public static final String[] IP = {"org.dspace.authenticate.IPAuthentication"}; + public static final String[] IP_AND_PASS = + {"org.dspace.authenticate.IPAuthentication", + "org.dspace.authenticate.PasswordAuthentication"}; + public static final String[] PASS = {"org.dspace.authenticate.PasswordAuthentication"}; + + + Item publicItem1; + Group staff; + + @Before + public void setup() { + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and two collections. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build(); + + staff = GroupBuilder.createGroup(context).withName("Staff").build(); + + //2. Three public items that are readable by Anonymous with different subjects + publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Public item 1") + .withIssueDate("2017-10-17") + .withAuthor("Smith, Donald").withAuthor("Doe, John") + .withSubject("ExtraEntry") + .withReaderGroup(staff) + .build(); + + } + + @Test + public void verifyIPAuthentication() throws Exception { + configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", IP); + + getClient().perform(get("/api/core/items/" + publicItem1.getID())) + .andExpect(status().isUnauthorized()); + + getClient().perform(get("/api/core/items/" + publicItem1.getID()) + .header("X-Forwarded-For", "5.5.5.5")) + .andExpect(status().isOk()); + + getClient().perform(get("/api/core/items/" + publicItem1.getID()) + .header("X-FORWARDED-FOR", "6.6.6.6")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void verifyIPAndPasswordAuthentication() throws Exception { + configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", IP_AND_PASS); + + groupService.addMember(context, staff, eperson); + + getClient().perform(get("/api/core/items/" + publicItem1.getID())) + .andExpect(status().isUnauthorized()); + + getClient().perform(get("/api/core/items/" + publicItem1.getID()) + .header("X-Forwarded-For", "5.5.5.5")) + .andExpect(status().isOk()); + + getClient().perform(get("/api/core/items/" + publicItem1.getID()) + .header("X-Forwarded-For", "6.6.6.6")) + .andExpect(status().isUnauthorized()); + + String token = getAuthToken(eperson.getEmail(), password); + + getClient(token).perform(get("/api/core/items/" + publicItem1.getID())) + .andExpect(status().isOk()); + + getClient(getAuthTokenWithXForwardedForHeader(eperson.getEmail(), password, "6.6.6.6")) + .perform(get("/api/core/items/" + publicItem1.getID()) + .header("X-Forwarded-For", "6.6.6.6")) + .andExpect(status().isOk()); + } + + @Test + public void verifyPasswordAuthentication() throws Exception { + configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", PASS); + + groupService.addMember(context, staff, eperson); + + getClient().perform(get("/api/core/items/" + publicItem1.getID())) + .andExpect(status().isUnauthorized()); + + getClient().perform(get("/api/core/items/" + publicItem1.getID()) + .header("X-Forwarded-For", "5.5.5.5")) + .andExpect(status().isUnauthorized()); + + getClient().perform(get("/api/core/items/" + publicItem1.getID()) + .header("X-Forwarded-For", "6.6.6.6")) + .andExpect(status().isUnauthorized()); + + String token = getAuthToken(eperson.getEmail(), password); + + getClient(token).perform(get("/api/core/items/" + publicItem1.getID())) + .andExpect(status().isOk()); + + getClient(getAuthTokenWithXForwardedForHeader(eperson.getEmail(), password, "6.6.6.6")) + .perform(get("/api/core/items/" + publicItem1.getID()) + .header("X-Forwarded-For", "6.6.6.6")) + .andExpect(status().isOk()); + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/AbstractControllerIntegrationTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/AbstractControllerIntegrationTest.java index de9003b2fe..c470e8a045 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/AbstractControllerIntegrationTest.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/AbstractControllerIntegrationTest.java @@ -134,12 +134,29 @@ public class AbstractControllerIntegrationTest extends AbstractIntegrationTestWi .andReturn().getResponse(); } + public MockHttpServletResponse getAuthResponseWithXForwardedForHeader(String user, String password, + String xForwardedFor) throws Exception { + return getClient().perform(post("/api/authn/login") + .param("user", user) + .param("password", password) + .header("X-Forwarded-For", xForwardedFor)) + .andReturn().getResponse(); + } + + public String getAuthToken(String user, String password) throws Exception { return StringUtils.substringAfter( getAuthResponse(user, password).getHeader(AUTHORIZATION_HEADER), AUTHORIZATION_TYPE); } + public String getAuthTokenWithXForwardedForHeader(String user, String password, String xForwardedFor) + throws Exception { + return StringUtils.substringAfter( + getAuthResponseWithXForwardedForHeader(user, password, xForwardedFor).getHeader(AUTHORIZATION_HEADER), + AUTHORIZATION_TYPE); + } + public String getPatchContent(List ops) { ObjectMapper objectMapper = new ObjectMapper(); try { diff --git a/dspace/config/modules/authentication.cfg b/dspace/config/modules/authentication.cfg index fab1f577d2..f22e2eaf19 100644 --- a/dspace/config/modules/authentication.cfg +++ b/dspace/config/modules/authentication.cfg @@ -34,7 +34,7 @@ # defining a new order in local.cfg. # IP-based authentication/authorization. See authentication-ip.cfg for default configuration. -plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.IPAuthentication +#plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.IPAuthentication # LDAP authentication/authorization. See authentication-ldap.cfg for default configuration. #plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.LDAPAuthentication From 9184189bb09d132dd9d5763943041691553996e2 Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Wed, 8 Jul 2020 10:32:53 +0200 Subject: [PATCH 110/143] [Task 71690] added javadoc for the tests and config --- .../test/data/dspaceFolder/config/local.cfg | 1 + ...nymousAdditionalAuthorizationFilterIT.java | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/dspace-api/src/test/data/dspaceFolder/config/local.cfg b/dspace-api/src/test/data/dspaceFolder/config/local.cfg index 2455c68cfd..093e1742bb 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/local.cfg +++ b/dspace-api/src/test/data/dspaceFolder/config/local.cfg @@ -121,5 +121,6 @@ configuration.not.exposed = secret_value configuration.exposed.single.value = public_value configuration.exposed.array.value = public_value_1, public_value_2 +# Test config for the authentication ip functionality authentication-ip.Staff = 5.5.5.5 authentication-ip.Student = 6.6.6.6 diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AnonymousAdditionalAuthorizationFilterIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AnonymousAdditionalAuthorizationFilterIT.java index 779bbabee8..f520df05f4 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AnonymousAdditionalAuthorizationFilterIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AnonymousAdditionalAuthorizationFilterIT.java @@ -25,6 +25,9 @@ import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +/** + * Testing class for the {@link org.dspace.app.rest.security.AnonymousAdditionalAuthorizationFilter} filter + */ public class AnonymousAdditionalAuthorizationFilterIT extends AbstractControllerIntegrationTest { @Autowired @@ -74,13 +77,16 @@ public class AnonymousAdditionalAuthorizationFilterIT extends AbstractController public void verifyIPAuthentication() throws Exception { configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", IP); + // Make sure that the item is not accessible for anonymous getClient().perform(get("/api/core/items/" + publicItem1.getID())) .andExpect(status().isUnauthorized()); + // Test that we can access the item using the IP that's configured for the Staff group getClient().perform(get("/api/core/items/" + publicItem1.getID()) .header("X-Forwarded-For", "5.5.5.5")) .andExpect(status().isOk()); + // Test that we can't access the item using the IP that's configured for the Students group getClient().perform(get("/api/core/items/" + publicItem1.getID()) .header("X-FORWARDED-FOR", "6.6.6.6")) .andExpect(status().isUnauthorized()); @@ -92,22 +98,28 @@ public class AnonymousAdditionalAuthorizationFilterIT extends AbstractController groupService.addMember(context, staff, eperson); + // Make sure that the item is not accessible for anonymous getClient().perform(get("/api/core/items/" + publicItem1.getID())) .andExpect(status().isUnauthorized()); + // Test that we can access the item using the IP that's configured for the Staff group getClient().perform(get("/api/core/items/" + publicItem1.getID()) .header("X-Forwarded-For", "5.5.5.5")) .andExpect(status().isOk()); + // Test that we can't access the item using the IP that's configured for the Students group getClient().perform(get("/api/core/items/" + publicItem1.getID()) .header("X-Forwarded-For", "6.6.6.6")) .andExpect(status().isUnauthorized()); String token = getAuthToken(eperson.getEmail(), password); + // Test that the user in the Staff group can access the Item with the normal password authentication getClient(token).perform(get("/api/core/items/" + publicItem1.getID())) .andExpect(status().isOk()); + // Test that the user in the Staff group can access the Item with the normal password authentication even + // when it's IP is configured to be part of the students group getClient(getAuthTokenWithXForwardedForHeader(eperson.getEmail(), password, "6.6.6.6")) .perform(get("/api/core/items/" + publicItem1.getID()) .header("X-Forwarded-For", "6.6.6.6")) @@ -120,22 +132,30 @@ public class AnonymousAdditionalAuthorizationFilterIT extends AbstractController groupService.addMember(context, staff, eperson); + // Make sure that the item is not accessible for anonymous getClient().perform(get("/api/core/items/" + publicItem1.getID())) .andExpect(status().isUnauthorized()); + // Test that the Item can't be accessed with the IP for the Staff group if the config is turned off and only + // allows password authentication getClient().perform(get("/api/core/items/" + publicItem1.getID()) .header("X-Forwarded-For", "5.5.5.5")) .andExpect(status().isUnauthorized()); + // Test that the Item can't be accessed with the IP for the Students group if the config is turned off and only + // allows password authentication getClient().perform(get("/api/core/items/" + publicItem1.getID()) .header("X-Forwarded-For", "6.6.6.6")) .andExpect(status().isUnauthorized()); String token = getAuthToken(eperson.getEmail(), password); + // Test that the Item is accessible for a user in the Staff group by password login getClient(token).perform(get("/api/core/items/" + publicItem1.getID())) .andExpect(status().isOk()); + // Test that the Item is accessible for a user in the Staff group by password Login when the request + // is coming from the IP that's configured to be for the Student group getClient(getAuthTokenWithXForwardedForHeader(eperson.getEmail(), password, "6.6.6.6")) .perform(get("/api/core/items/" + publicItem1.getID()) .header("X-Forwarded-For", "6.6.6.6")) From dfe33d273e2c1189b4eae54041e8c45914e07c17 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 7 Jul 2020 17:43:36 +0200 Subject: [PATCH 111/143] 71701: item link on BundleRest + embedded parent tests for bundle and bitstream --- .../org/dspace/app/rest/model/BundleRest.java | 5 ++ .../repository/BundleItemLinkRepository.java | 59 +++++++++++++++++++ .../app/rest/BitstreamRestRepositoryIT.java | 53 +++++++++++++++++ .../app/rest/BundleRestRepositoryIT.java | 17 ++++++ .../app/rest/matcher/BundleMatcher.java | 4 +- 5 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BundleItemLinkRepository.java diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/BundleRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/BundleRest.java index 71f05c8333..dd4a80d488 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/BundleRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/BundleRest.java @@ -16,6 +16,10 @@ import com.fasterxml.jackson.annotation.JsonProperty; * @author Jelle Pelgrims (jelle.pelgrims at atmire.com) */ @LinksRest(links = { + @LinkRest( + name = BundleRest.ITEM, + method = "getItem" + ), @LinkRest( name = BundleRest.BITSTREAMS, method = "getBitstreams" @@ -30,6 +34,7 @@ public class BundleRest extends DSpaceObjectRest { public static final String PLURAL_NAME = "bundles"; public static final String CATEGORY = RestAddressableModel.CORE; + public static final String ITEM = "item"; public static final String BITSTREAMS = "bitstreams"; public static final String PRIMARY_BITSTREAM = "primaryBitstream"; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BundleItemLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BundleItemLinkRepository.java new file mode 100644 index 0000000000..9309829085 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BundleItemLinkRepository.java @@ -0,0 +1,59 @@ +/** + * 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.rest.model.BundleRest; +import org.dspace.app.rest.model.ItemRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.content.Bundle; +import org.dspace.content.Item; +import org.dspace.content.service.BundleService; +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 bundle. + */ +@Component(BundleRest.CATEGORY + "." + BundleRest.NAME + "." + BundleRest.ITEM) +public class BundleItemLinkRepository extends AbstractDSpaceRestRepository + implements LinkRestRepository { + + @Autowired + BundleService bundleService; + + @PreAuthorize("hasPermission(#bundleId, 'BUNDLE', 'READ')") + public ItemRest getItem(@Nullable HttpServletRequest request, + UUID bundleId, + @Nullable Pageable optionalPageable, + Projection projection) { + try { + Context context = obtainContext(); + Bundle bundle = bundleService.find(context, bundleId); + if (bundle == null) { + throw new ResourceNotFoundException("No such bundle: " + bundleId); + } + Item item = bundle.getItems().get(0); + if (item == null) { + return null; + } + return converter.toRest(item, projection); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java index 34bb56cbec..e609f02d7e 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java @@ -30,11 +30,13 @@ import org.dspace.app.rest.builder.ItemBuilder; import org.dspace.app.rest.builder.ResourcePolicyBuilder; import org.dspace.app.rest.matcher.BitstreamFormatMatcher; import org.dspace.app.rest.matcher.BitstreamMatcher; +import org.dspace.app.rest.matcher.BundleMatcher; import org.dspace.app.rest.matcher.HalMatcher; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.app.rest.test.MetadataPatchSuite; import org.dspace.authorize.service.ResourcePolicyService; import org.dspace.content.Bitstream; +import org.dspace.content.Bundle; import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.Item; @@ -1143,5 +1145,56 @@ public class BitstreamRestRepositoryIT extends AbstractControllerIntegrationTest } + @Test + public void getEmbeddedBundleForBitstream() throws Exception { + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build(); + + //2. One public items that is readable by Anonymous + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Test") + .withIssueDate("2010-10-17") + .withAuthor("Smith, Donald") + .withSubject("ExtraEntry") + .build(); + + String bitstreamContent = "ThisIsSomeDummyText"; + + //Add a bitstream to an item + Bitstream bitstream = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) { + bitstream = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream") + .withDescription("Description") + .withMimeType("text/plain") + .build(); + } + + Bundle bundle = bitstream.getBundles().get(0); + + //Get the bitstream with embedded bundle + getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "?embed=bundle")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.bundle", + BundleMatcher.matchProperties( + bundle.getName(), + bundle.getID(), + bundle.getHandle(), + bundle.getType() + ) + )); + } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleRestRepositoryIT.java index 07d6645c00..bdf3447124 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleRestRepositoryIT.java @@ -37,6 +37,7 @@ import org.dspace.app.rest.builder.ResourcePolicyBuilder; import org.dspace.app.rest.matcher.BitstreamMatcher; import org.dspace.app.rest.matcher.BundleMatcher; import org.dspace.app.rest.matcher.HalMatcher; +import org.dspace.app.rest.matcher.ItemMatcher; import org.dspace.app.rest.matcher.MetadataMatcher; import org.dspace.app.rest.model.BundleRest; import org.dspace.app.rest.model.MetadataRest; @@ -636,4 +637,20 @@ public class BundleRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isOk()); } + @Test + public void getEmbeddedItemForBundle() throws Exception { + context.turnOffAuthorisationSystem(); + + bundle1 = BundleBuilder.createBundle(context, item) + .withName("testname") + .build(); + + context.restoreAuthSystemState(); + + getClient().perform(get("/api/core/bundles/" + bundle1.getID() + "?embed=item")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.item", ItemMatcher.matchItemWithTitleAndDateIssued(item, "Public item 1", "2017-10-17"))); + } + } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/BundleMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/BundleMatcher.java index 4fb5606293..812daabf58 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/BundleMatcher.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/BundleMatcher.java @@ -48,7 +48,8 @@ public class BundleMatcher { public static Matcher matchFullEmbeds() { return matchEmbeds( "bitstreams[]", - "primaryBitstream" + "primaryBitstream", + "item" ); } @@ -57,6 +58,7 @@ public class BundleMatcher { */ public static Matcher matchLinks(UUID uuid) { return HalMatcher.matchLinks(REST_SERVER_URL + "core/bundles/" + uuid, + "item", "bitstreams", "primaryBitstream", "self" From 3243f19127f61f793598fe8e6dab19c522b0f5ec Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 8 Jul 2020 11:42:36 +0200 Subject: [PATCH 112/143] 71701: Additional parent link tests for Bitstream and Bundle + JavaDocs --- .../repository/BundleItemLinkRepository.java | 3 + .../app/rest/BitstreamRestRepositoryIT.java | 78 +++++++++++++++++++ .../app/rest/BundleRestRepositoryIT.java | 29 +++++++ 3 files changed, 110 insertions(+) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BundleItemLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BundleItemLinkRepository.java index 9309829085..3d3e5226f3 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BundleItemLinkRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BundleItemLinkRepository.java @@ -35,6 +35,9 @@ public class BundleItemLinkRepository extends AbstractDSpaceRestRepository @Autowired BundleService bundleService; + /** + * Get the first item the provided bundle resides in + */ @PreAuthorize("hasPermission(#bundleId, 'BUNDLE', 'READ')") public ItemRest getItem(@Nullable HttpServletRequest request, UUID bundleId, diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java index e609f02d7e..9cbb5363b6 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java @@ -24,6 +24,7 @@ import java.util.UUID; import org.apache.commons.codec.CharEncoding; import org.apache.commons.io.IOUtils; import org.dspace.app.rest.builder.BitstreamBuilder; +import org.dspace.app.rest.builder.BundleBuilder; import org.dspace.app.rest.builder.CollectionBuilder; import org.dspace.app.rest.builder.CommunityBuilder; import org.dspace.app.rest.builder.ItemBuilder; @@ -1197,4 +1198,81 @@ public class BitstreamRestRepositoryIT extends AbstractControllerIntegrationTest )); } + @Test + public void linksToFirstBundleWhenMultipleBundles() throws Exception { + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build(); + + //2. One public items that is readable by Anonymous + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Test") + .withIssueDate("2010-10-17") + .withAuthor("Smith, Donald") + .withSubject("ExtraEntry") + .build(); + + String bitstreamContent = "ThisIsSomeDummyText"; + + //Add a bitstream to an item + Bitstream bitstream = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) { + bitstream = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream") + .withDescription("Description") + .withMimeType("text/plain") + .build(); + } + + Bundle secondBundle = BundleBuilder.createBundle(context, publicItem1) + .withName("second bundle") + .withBitstream(bitstream).build(); + + Bundle bundle = bitstream.getBundles().get(0); + + //Get bundle should contain the first bundle in the list + getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/bundle")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", + BundleMatcher.matchProperties( + bundle.getName(), + bundle.getID(), + bundle.getHandle(), + bundle.getType() + ) + )); + } + + @Test + public void linksToEmptyWhenNoBundle() throws Exception { + // We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + // ** GIVEN ** + // 1. A community with a logo + parentCommunity = CommunityBuilder.createCommunity(context).withName("Community").withLogo("logo_community") + .build(); + + // 2. A collection with a logo + Collection col = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection") + .withLogo("logo_collection").build(); + + Bitstream bitstream = parentCommunity.getLogo(); + + //Get bundle should contain an empty response + getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/bundle")) + .andExpect(status().isNoContent()); + } + } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleRestRepositoryIT.java index bdf3447124..7e3dcead5a 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleRestRepositoryIT.java @@ -51,6 +51,7 @@ import org.dspace.content.Bitstream; import org.dspace.content.Bundle; import org.dspace.content.Collection; import org.dspace.content.Item; +import org.dspace.content.service.ItemService; import org.dspace.core.Constants; import org.dspace.eperson.EPerson; import org.hamcrest.Matchers; @@ -64,6 +65,9 @@ public class BundleRestRepositoryIT extends AbstractControllerIntegrationTest { @Autowired ResourcePolicyService resourcePolicyService; + @Autowired + ItemService itemService; + private Collection collection; private Item item; private Bundle bundle1; @@ -653,4 +657,29 @@ public class BundleRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(jsonPath("$._embedded.item", ItemMatcher.matchItemWithTitleAndDateIssued(item, "Public item 1", "2017-10-17"))); } + @Test + public void linksToFirstItemWhenMultipleItems() throws Exception { + context.turnOffAuthorisationSystem(); + + bundle1 = BundleBuilder.createBundle(context, item) + .withName("testname") + .build(); + + Item item2 = ItemBuilder.createItem(context, collection) + .withTitle("Public item 2") + .withIssueDate("2020-07-08") + .withAuthor("Smith, Donald").withAuthor("Doe, John") + .withSubject("SecondEntry") + .build(); + + itemService.addBundle(context, item2, bundle1); + + context.restoreAuthSystemState(); + + getClient().perform(get("/api/core/bundles/" + bundle1.getID() + "/item")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", ItemMatcher.matchItemWithTitleAndDateIssued(item, "Public item 1", "2017-10-17"))); + } + } From 3e33a866c6af5cd885770aba53aae92fdc323f6c Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 8 Jul 2020 12:47:35 +0200 Subject: [PATCH 113/143] 71701: Checkstyle fixes --- .../java/org/dspace/app/rest/BundleRestRepositoryIT.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleRestRepositoryIT.java index 7e3dcead5a..20590a1992 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleRestRepositoryIT.java @@ -654,7 +654,9 @@ public class BundleRestRepositoryIT extends AbstractControllerIntegrationTest { getClient().perform(get("/api/core/bundles/" + bundle1.getID() + "?embed=item")) .andExpect(status().isOk()) .andExpect(content().contentType(contentType)) - .andExpect(jsonPath("$._embedded.item", ItemMatcher.matchItemWithTitleAndDateIssued(item, "Public item 1", "2017-10-17"))); + .andExpect(jsonPath("$._embedded.item", + ItemMatcher.matchItemWithTitleAndDateIssued(item, "Public item 1", "2017-10-17") + )); } @Test @@ -679,7 +681,9 @@ public class BundleRestRepositoryIT extends AbstractControllerIntegrationTest { getClient().perform(get("/api/core/bundles/" + bundle1.getID() + "/item")) .andExpect(status().isOk()) .andExpect(content().contentType(contentType)) - .andExpect(jsonPath("$", ItemMatcher.matchItemWithTitleAndDateIssued(item, "Public item 1", "2017-10-17"))); + .andExpect(jsonPath("$", + ItemMatcher.matchItemWithTitleAndDateIssued(item, "Public item 1", "2017-10-17") + )); } } From 62cfd2d5bd98bcc688395a3c7ba89e77df003fb6 Mon Sep 17 00:00:00 2001 From: Raf Ponsaerts Date: Wed, 8 Jul 2020 13:40:47 +0200 Subject: [PATCH 114/143] [Task 71761] applied feedback to the AnonymousAdditionalAuthorizationFilter and IT --- ...nonymousAdditionalAuthorizationFilter.java | 2 +- ...nymousAdditionalAuthorizationFilterIT.java | 30 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AnonymousAdditionalAuthorizationFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AnonymousAdditionalAuthorizationFilter.java index ef040daf12..3087a5850b 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AnonymousAdditionalAuthorizationFilter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AnonymousAdditionalAuthorizationFilter.java @@ -29,7 +29,7 @@ import org.springframework.security.web.authentication.www.BasicAuthenticationFi * can implicitly have and adds those * This will allow us to for example set a specific Group to a specific IP so that any request from that * IP is always treated as being a part of the configured group. - * The configuration for the authentication through ip can be fined in authentication-ip.cfg + * The configuration for the authentication through ip can be found in authentication-ip.cfg * This can be enabled by uncommenting the IPAuhentication plugin in authentication.cfg */ public class AnonymousAdditionalAuthorizationFilter extends BasicAuthenticationFilter { diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AnonymousAdditionalAuthorizationFilterIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AnonymousAdditionalAuthorizationFilterIT.java index f520df05f4..3202bd37c2 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AnonymousAdditionalAuthorizationFilterIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AnonymousAdditionalAuthorizationFilterIT.java @@ -43,7 +43,7 @@ public class AnonymousAdditionalAuthorizationFilterIT extends AbstractController public static final String[] PASS = {"org.dspace.authenticate.PasswordAuthentication"}; - Item publicItem1; + Item staffAccessItem1; Group staff; @Before @@ -63,7 +63,7 @@ public class AnonymousAdditionalAuthorizationFilterIT extends AbstractController staff = GroupBuilder.createGroup(context).withName("Staff").build(); //2. Three public items that are readable by Anonymous with different subjects - publicItem1 = ItemBuilder.createItem(context, col1) + staffAccessItem1 = ItemBuilder.createItem(context, col1) .withTitle("Public item 1") .withIssueDate("2017-10-17") .withAuthor("Smith, Donald").withAuthor("Doe, John") @@ -78,16 +78,16 @@ public class AnonymousAdditionalAuthorizationFilterIT extends AbstractController configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", IP); // Make sure that the item is not accessible for anonymous - getClient().perform(get("/api/core/items/" + publicItem1.getID())) + getClient().perform(get("/api/core/items/" + staffAccessItem1.getID())) .andExpect(status().isUnauthorized()); // Test that we can access the item using the IP that's configured for the Staff group - getClient().perform(get("/api/core/items/" + publicItem1.getID()) + getClient().perform(get("/api/core/items/" + staffAccessItem1.getID()) .header("X-Forwarded-For", "5.5.5.5")) .andExpect(status().isOk()); // Test that we can't access the item using the IP that's configured for the Students group - getClient().perform(get("/api/core/items/" + publicItem1.getID()) + getClient().perform(get("/api/core/items/" + staffAccessItem1.getID()) .header("X-FORWARDED-FOR", "6.6.6.6")) .andExpect(status().isUnauthorized()); } @@ -99,29 +99,29 @@ public class AnonymousAdditionalAuthorizationFilterIT extends AbstractController groupService.addMember(context, staff, eperson); // Make sure that the item is not accessible for anonymous - getClient().perform(get("/api/core/items/" + publicItem1.getID())) + getClient().perform(get("/api/core/items/" + staffAccessItem1.getID())) .andExpect(status().isUnauthorized()); // Test that we can access the item using the IP that's configured for the Staff group - getClient().perform(get("/api/core/items/" + publicItem1.getID()) + getClient().perform(get("/api/core/items/" + staffAccessItem1.getID()) .header("X-Forwarded-For", "5.5.5.5")) .andExpect(status().isOk()); // Test that we can't access the item using the IP that's configured for the Students group - getClient().perform(get("/api/core/items/" + publicItem1.getID()) + getClient().perform(get("/api/core/items/" + staffAccessItem1.getID()) .header("X-Forwarded-For", "6.6.6.6")) .andExpect(status().isUnauthorized()); String token = getAuthToken(eperson.getEmail(), password); // Test that the user in the Staff group can access the Item with the normal password authentication - getClient(token).perform(get("/api/core/items/" + publicItem1.getID())) + getClient(token).perform(get("/api/core/items/" + staffAccessItem1.getID())) .andExpect(status().isOk()); // Test that the user in the Staff group can access the Item with the normal password authentication even // when it's IP is configured to be part of the students group getClient(getAuthTokenWithXForwardedForHeader(eperson.getEmail(), password, "6.6.6.6")) - .perform(get("/api/core/items/" + publicItem1.getID()) + .perform(get("/api/core/items/" + staffAccessItem1.getID()) .header("X-Forwarded-For", "6.6.6.6")) .andExpect(status().isOk()); } @@ -133,31 +133,31 @@ public class AnonymousAdditionalAuthorizationFilterIT extends AbstractController groupService.addMember(context, staff, eperson); // Make sure that the item is not accessible for anonymous - getClient().perform(get("/api/core/items/" + publicItem1.getID())) + getClient().perform(get("/api/core/items/" + staffAccessItem1.getID())) .andExpect(status().isUnauthorized()); // Test that the Item can't be accessed with the IP for the Staff group if the config is turned off and only // allows password authentication - getClient().perform(get("/api/core/items/" + publicItem1.getID()) + getClient().perform(get("/api/core/items/" + staffAccessItem1.getID()) .header("X-Forwarded-For", "5.5.5.5")) .andExpect(status().isUnauthorized()); // Test that the Item can't be accessed with the IP for the Students group if the config is turned off and only // allows password authentication - getClient().perform(get("/api/core/items/" + publicItem1.getID()) + getClient().perform(get("/api/core/items/" + staffAccessItem1.getID()) .header("X-Forwarded-For", "6.6.6.6")) .andExpect(status().isUnauthorized()); String token = getAuthToken(eperson.getEmail(), password); // Test that the Item is accessible for a user in the Staff group by password login - getClient(token).perform(get("/api/core/items/" + publicItem1.getID())) + getClient(token).perform(get("/api/core/items/" + staffAccessItem1.getID())) .andExpect(status().isOk()); // Test that the Item is accessible for a user in the Staff group by password Login when the request // is coming from the IP that's configured to be for the Student group getClient(getAuthTokenWithXForwardedForHeader(eperson.getEmail(), password, "6.6.6.6")) - .perform(get("/api/core/items/" + publicItem1.getID()) + .perform(get("/api/core/items/" + staffAccessItem1.getID()) .header("X-Forwarded-For", "6.6.6.6")) .andExpect(status().isOk()); } From 61bed89a0c97e0e71c85ef16ff400c6c30310eae Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Tue, 14 Jul 2020 09:44:14 -0500 Subject: [PATCH 115/143] Rename master to main. Rename whitelist to allow list. --- .github/pull_request_template.md | 2 +- Dockerfile | 2 +- Dockerfile.cli | 2 +- Dockerfile.test | 2 +- README.md | 36 +++++++++---------- .../service/CitationDocumentService.java | 2 +- .../SimpleXpathMetadatumContributor.java | 2 +- .../webapp/WEB-INF/applicationContext.xml | 2 +- .../java/org/dspace/app/rest/Application.java | 4 +-- .../java/org/dspace/app/rest/utils/Utils.java | 2 +- .../crosswalks/google-metadata.properties | 14 ++++---- dspace/src/main/docker-compose/cli.ingest.yml | 2 +- dspace/src/main/docker/README.md | 6 ++-- pom.xml | 2 +- 14 files changed, 40 insertions(+), 40 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 3605531adb..6542242af7 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -23,5 +23,5 @@ _This checklist provides a reminder of what we are going to look for when review - [ ] My PR passes Checkstyle validation based on the [Code Style Guide](https://wiki.lyrasis.org/display/DSPACE/Code+Style+Guide). - [ ] My PR includes Javadoc for _all new (or modified) public methods and classes_. It also includes Javadoc for large or complex private methods. - [ ] My PR passes all tests and includes new/updated Unit or Integration Tests based on the [Code Testing Guide](https://wiki.lyrasis.org/display/DSPACE/Code+Testing+Guide). -- [ ] If my PR includes new, third-party dependencies (in any `pom.xml`), I've made sure their licenses align with the [DSpace BSD License](https://github.com/DSpace/DSpace/blob/master/LICENSE) based on the [Licensing of Contributions](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions) documentation. +- [ ] If my PR includes new, third-party dependencies (in any `pom.xml`), I've made sure their licenses align with the [DSpace BSD License](https://github.com/DSpace/DSpace/blob/main/LICENSE) based on the [Licensing of Contributions](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions) documentation. - [ ] If my PR modifies the REST API, I've linked to the REST Contract page (or open PR) related to this change. diff --git a/Dockerfile b/Dockerfile index 006f32f28e..2dc3ee9bda 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # This image will be published as dspace/dspace -# See https://github.com/DSpace/DSpace/tree/master/dspace/src/main/docker for usage details +# See https://github.com/DSpace/DSpace/tree/main/dspace/src/main/docker for usage details # # This version is JDK11 compatible # - tomcat:8-jdk11 diff --git a/Dockerfile.cli b/Dockerfile.cli index 116b251f2d..d4204ebdd0 100644 --- a/Dockerfile.cli +++ b/Dockerfile.cli @@ -1,5 +1,5 @@ # This image will be published as dspace/dspace-cli -# See https://github.com/DSpace/DSpace/tree/master/dspace/src/main/docker for usage details +# See https://github.com/DSpace/DSpace/tree/main/dspace/src/main/docker for usage details # # This version is JDK11 compatible # - openjdk:11 diff --git a/Dockerfile.test b/Dockerfile.test index 090f714e28..82ffdef177 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -1,5 +1,5 @@ # This image will be published as dspace/dspace -# See https://github.com/DSpace/DSpace/tree/master/dspace/src/main/docker for usage details +# See https://github.com/DSpace/DSpace/tree/main/dspace/src/main/docker for usage details # # This version is JDK11 compatible # - tomcat:8-jdk11 diff --git a/README.md b/README.md index 49f3814b49..1b28c958d4 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,24 @@ # DSpace -[![Build Status](https://travis-ci.org/DSpace/DSpace.png?branch=master)](https://travis-ci.org/DSpace/DSpace) +[![Build Status](https://travis-ci.com/DSpace/DSpace.png?branch=main)](https://travis-ci.com/DSpace/DSpace) -[DSpace Documentation](https://wiki.duraspace.org/display/DSDOC/) | +[DSpace Documentation](https://wiki.lyrasis.org/display/DSDOC/) | [DSpace Releases](https://github.com/DSpace/DSpace/releases) | -[DSpace Wiki](https://wiki.duraspace.org/display/DSPACE/Home) | -[Support](https://wiki.duraspace.org/display/DSPACE/Support) +[DSpace Wiki](https://wiki.lyrasis.org/display/DSPACE/Home) | +[Support](https://wiki.lyrasis.org/display/DSPACE/Support) DSpace open source software is a turnkey repository application used by more than 2,000 organizations and institutions worldwide to provide durable access to digital resources. For more information, visit http://www.dspace.org/ *** -:warning: **Work on DSpace 7 has begun on our `master` branch.** This means that there is temporarily NO user interface on this `master` branch. DSpace 7 will feature a new, unified [Angular](https://angular.io/) user interface, along with an enhanced, rebuilt REST API. The latest status of this work can be found on the [DSpace 7 UI Working Group](https://wiki.duraspace.org/display/DSPACE/DSpace+7+UI+Working+Group) page. Additionally, the codebases can be found in the following places: - * DSpace 7 REST API work is occurring on the [`master` branch](https://github.com/DSpace/DSpace/tree/master/dspace-server-webapp) of this repository. - * The REST Contract is being documented at https://github.com/DSpace/Rest7Contract +:warning: **Work on DSpace 7 has begun on our `main` branch.** This means that there is NO user interface on this `main` branch. DSpace 7 will feature a new, unified [Angular](https://angular.io/) user interface, along with an enhanced, rebuilt REST API. The latest status of this work can be found on the [DSpace 7 Working Group](https://wiki.lyrasis.org/display/DSPACE/DSpace+7+Working+Group) page. Additionally, the codebases can be found in the following places: + * DSpace 7 REST API work is occurring on the [`main` branch](https://github.com/DSpace/DSpace/tree/main/dspace-server-webapp) of this repository. + * The REST Contract is at https://github.com/DSpace/Rest7Contract * DSpace 7 Angular UI work is occurring at https://github.com/DSpace/dspace-angular -**If you would like to get involved in our DSpace 7 development effort, we welcome new contributors.** Just join one of our meetings or get in touch via Slack. See the [DSpace 7 UI Working Group](https://wiki.duraspace.org/display/DSPACE/DSpace+7+UI+Working+Group) wiki page for more info. +**If you would like to get involved in our DSpace 7 development effort, we welcome new contributors.** Just join one of our meetings or get in touch via Slack. See the [DSpace 7 Working Group](https://wiki.lyrasis.org/display/DSPACE/DSpace+7+Working+Group) wiki page for more info. **If you are looking for the ongoing maintenance work for DSpace 6 (or prior releases)**, you can find that work on the corresponding maintenance branch (e.g. [`dspace-6_x`](https://github.com/DSpace/DSpace/tree/dspace-6_x)) in this repository. *** @@ -31,10 +31,10 @@ Past releases are all available via GitHub at https://github.com/DSpace/DSpace/r ## Documentation / Installation -Documentation for each release may be viewed online or downloaded via our [Documentation Wiki](https://wiki.duraspace.org/display/DSDOC/). +Documentation for each release may be viewed online or downloaded via our [Documentation Wiki](https://wiki.lyrasis.org/display/DSDOC/). The latest DSpace Installation instructions are available at: -https://wiki.duraspace.org/display/DSDOC6x/Installing+DSpace +https://wiki.lyrasis.org/display/DSDOC6x/Installing+DSpace Please be aware that, as a Java web application, DSpace requires a database (PostgreSQL or Oracle) and a servlet container (usually Tomcat) in order to function. @@ -49,11 +49,11 @@ DSpace is a community built and supported project. We do not have a centralized but have a dedicated group of volunteers who help us improve the software, documentation, resources, etc. We welcome contributions of any type. Here's a few basic guides that provide suggestions for contributing to DSpace: -* [How to Contribute to DSpace](https://wiki.duraspace.org/display/DSPACE/How+to+Contribute+to+DSpace): How to contribute in general (via code, documentation, bug reports, expertise, etc) -* [Code Contribution Guidelines](https://wiki.duraspace.org/display/DSPACE/Code+Contribution+Guidelines): How to give back code or contribute features, bug fixes, etc. -* [DSpace Community Advisory Team (DCAT)](https://wiki.duraspace.org/display/cmtygp/DSpace+Community+Advisory+Team): If you are not a developer, we also have an interest group specifically for repository managers. The DCAT group meets virtually, once a month, and sends open invitations to join their meetings via the [DCAT mailing list](https://groups.google.com/d/forum/DSpaceCommunityAdvisoryTeam). +* [How to Contribute to DSpace](https://wiki.lyrasis.org/display/DSPACE/How+to+Contribute+to+DSpace): How to contribute in general (via code, documentation, bug reports, expertise, etc) +* [Code Contribution Guidelines](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines): How to give back code or contribute features, bug fixes, etc. +* [DSpace Community Advisory Team (DCAT)](https://wiki.lyrasis.org/display/cmtygp/DSpace+Community+Advisory+Team): If you are not a developer, we also have an interest group specifically for repository managers. The DCAT group meets virtually, once a month, and sends open invitations to join their meetings via the [DCAT mailing list](https://groups.google.com/d/forum/DSpaceCommunityAdvisoryTeam). -We also encourage GitHub Pull Requests (PRs) at any time. Please see our [Development with Git](https://wiki.duraspace.org/display/DSPACE/Development+with+Git) guide for more info. +We also encourage GitHub Pull Requests (PRs) at any time. Please see our [Development with Git](https://wiki.lyrasis.org/display/DSPACE/Development+with+Git) guide for more info. In addition, a listing of all known contributors to DSpace software can be found online at: https://wiki.duraspace.org/display/DSPACE/DSpaceContributors @@ -64,12 +64,12 @@ DSpace provides public mailing lists where you can post questions or raise topic We welcome everyone to participate in these lists: * [dspace-community@googlegroups.com](https://groups.google.com/d/forum/dspace-community) : General discussion about DSpace platform, announcements, sharing of best practices -* [dspace-tech@googlegroups.com](https://groups.google.com/d/forum/dspace-tech) : Technical support mailing list. See also our guide for [How to troubleshoot an error](https://wiki.duraspace.org/display/DSPACE/Troubleshoot+an+error). +* [dspace-tech@googlegroups.com](https://groups.google.com/d/forum/dspace-tech) : Technical support mailing list. See also our guide for [How to troubleshoot an error](https://wiki.lyrasis.org/display/DSPACE/Troubleshoot+an+error). * [dspace-devel@googlegroups.com](https://groups.google.com/d/forum/dspace-devel) : Developers / Development mailing list Great Q&A is also available under the [DSpace tag on Stackoverflow](http://stackoverflow.com/questions/tagged/dspace) -Additional support options are listed at https://wiki.duraspace.org/display/DSPACE/Support +Additional support options are at https://wiki.lyrasis.org/display/DSPACE/Support DSpace also has an active service provider network. If you'd rather hire a service provider to install, upgrade, customize or host DSpace, then we recommend getting in touch with one of our @@ -84,7 +84,7 @@ The DSpace Issue Tracker can be found at: https://jira.duraspace.org/projects/DS ### Running Tests By default, in DSpace, Unit Tests and Integration Tests are disabled. However, they are -run automatically by [Travis CI](https://travis-ci.org/DSpace/DSpace/) for all Pull Requests and code commits. +run automatically by [Travis CI](https://travis-ci.com/DSpace/DSpace/) for all Pull Requests and code commits. * How to run both Unit Tests (via `maven-surefire-plugin`) and Integration Tests (via `maven-failsafe-plugin`): ``` @@ -130,4 +130,4 @@ run automatically by [Travis CI](https://travis-ci.org/DSpace/DSpace/) for all P ## License DSpace source code is freely available under a standard [BSD 3-Clause license](https://opensource.org/licenses/BSD-3-Clause). -The full license is available at http://www.dspace.org/license/ +The full license is available in the [LICENSE](LICENSE) file or online at http://www.dspace.org/license/ diff --git a/dspace-api/src/main/java/org/dspace/disseminate/service/CitationDocumentService.java b/dspace-api/src/main/java/org/dspace/disseminate/service/CitationDocumentService.java index d6c7935a86..4a59de3f5f 100644 --- a/dspace-api/src/main/java/org/dspace/disseminate/service/CitationDocumentService.java +++ b/dspace-api/src/main/java/org/dspace/disseminate/service/CitationDocumentService.java @@ -38,7 +38,7 @@ public interface CitationDocumentService { * Citation enabled globally (all citable bitstreams will get "watermarked") modules/disseminate-citation: * enable_globally * OR - * The container is this object is whitelist enabled. + * The container is this object is "allow list" enabled. * - community: modules/disseminate-citation: enabled_communities * - collection: modules/disseminate-citation: enabled_collections * AND diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java index ba5afceb5f..ef2571acc6 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java @@ -79,7 +79,7 @@ public class SimpleXpathMetadatumContributor implements MetadataContributorMetadataFieldConfig + * MetadataFieldConfig */ public SimpleXpathMetadatumContributor(String query, Map prefixToNamespaceMapping, MetadataFieldConfig field) { diff --git a/dspace-rest/src/main/webapp/WEB-INF/applicationContext.xml b/dspace-rest/src/main/webapp/WEB-INF/applicationContext.xml index 62b660b86b..ec892fbaa4 100644 --- a/dspace-rest/src/main/webapp/WEB-INF/applicationContext.xml +++ b/dspace-rest/src/main/webapp/WEB-INF/applicationContext.xml @@ -28,7 +28,7 @@ + 9.4.15.v20190215 2.11.2 2.0.15 @@ -1240,6 +1240,7 @@ handle 9.3.0-SNAPSHOT + net.cnri cnri-servlet-container @@ -1847,6 +1848,7 @@ true + handle.net https://handle.net/maven From 836876508f42f44583b8aadd22d4859473b3b9d9 Mon Sep 17 00:00:00 2001 From: jonas-atmire Date: Thu, 23 Jul 2020 08:59:10 +0200 Subject: [PATCH 128/143] Addition of explanation to tests + Small comment update --- .../dspace/app/rest/repository/BundleItemLinkRepository.java | 2 +- .../java/org/dspace/app/rest/BitstreamRestRepositoryIT.java | 5 +++++ .../java/org/dspace/app/rest/BundleRestRepositoryIT.java | 5 +++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BundleItemLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BundleItemLinkRepository.java index 3d3e5226f3..4df81d5054 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BundleItemLinkRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BundleItemLinkRepository.java @@ -36,7 +36,7 @@ public class BundleItemLinkRepository extends AbstractDSpaceRestRepository BundleService bundleService; /** - * Get the first item the provided bundle resides in + * Get the item where the provided bundle resides in */ @PreAuthorize("hasPermission(#bundleId, 'BUNDLE', 'READ')") public ItemRest getItem(@Nullable HttpServletRequest request, diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java index 9cbb5363b6..691bb7d07d 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java @@ -1199,6 +1199,11 @@ public class BitstreamRestRepositoryIT extends AbstractControllerIntegrationTest } @Test + /** + * This test proves that, if a bitstream is linked to multiple bundles, we only ever return the first bundle. + * **NOTE: DSpace does NOT support or expect to have a bitstream linked to multiple bundles**. + * But, because the database does allow for it, this test simply proves the REST API will respond without an error. + */ public void linksToFirstBundleWhenMultipleBundles() throws Exception { //We turn off the authorization system in order to create the structure as defined below context.turnOffAuthorisationSystem(); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleRestRepositoryIT.java index 20590a1992..4aa25946ff 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleRestRepositoryIT.java @@ -660,6 +660,11 @@ public class BundleRestRepositoryIT extends AbstractControllerIntegrationTest { } @Test + /** + * This test proves that, if a bundle is linked to multiple items, we only ever return the first item. + * **NOTE: DSpace does NOT support or expect to have a bundle linked to multiple items**. + * But, because the database does allow for it, this test simply proves the REST API will respond without an error + */ public void linksToFirstItemWhenMultipleItems() throws Exception { context.turnOffAuthorisationSystem(); From eb8f9375954a47fb8f55b09776b489521474a672 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 24 Jul 2020 15:27:01 -0500 Subject: [PATCH 129/143] Limit the errors we log --- .../exception/DSpaceApiExceptionControllerAdvice.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java index 6cf297ebf6..4cba9153d3 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java @@ -150,8 +150,12 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH //Make sure Spring picks up this exception request.setAttribute(EXCEPTION_ATTRIBUTE, ex); - //Log the full error and status code - log.error("{} (status:{})", message, statusCode, ex); + // For now, just logging server errors. + // We don't want to fill logs with bad/invalid REST API requests. + if (statusCode == HttpServletResponse.SC_INTERNAL_SERVER_ERROR) { + // Log the full error and status code + log.error("{} (status:{})", message, statusCode, ex); + } //Exception properties will be set by org.springframework.boot.web.support.ErrorPageFilter response.sendError(statusCode, message); From c3c683e23bf470a26dc62dc44d2b6aaf86d53567 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 24 Jul 2020 16:32:31 -0500 Subject: [PATCH 130/143] Fix two other info exposure through stack trace errors --- .../app/rest/security/DSpace401AuthenticationEntryPoint.java | 3 +-- .../java/org/purl/sword/server/ServiceDocumentServlet.java | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/DSpace401AuthenticationEntryPoint.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/DSpace401AuthenticationEntryPoint.java index 68aea6a526..b70931336e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/DSpace401AuthenticationEntryPoint.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/DSpace401AuthenticationEntryPoint.java @@ -35,7 +35,6 @@ public class DSpace401AuthenticationEntryPoint implements AuthenticationEntryPoi response.setHeader("WWW-Authenticate", restAuthenticationService.getWwwAuthenticateHeaderValue(request, response)); - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, - authException.getMessage()); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication is required"); } } diff --git a/dspace-sword/src/main/java/org/purl/sword/server/ServiceDocumentServlet.java b/dspace-sword/src/main/java/org/purl/sword/server/ServiceDocumentServlet.java index 28c309b2ca..494cbc9db4 100644 --- a/dspace-sword/src/main/java/org/purl/sword/server/ServiceDocumentServlet.java +++ b/dspace-sword/src/main/java/org/purl/sword/server/ServiceDocumentServlet.java @@ -164,7 +164,8 @@ public class ServiceDocumentServlet extends HttpServlet { } catch (SWORDException se) { log.error("Internal error", se); // Throw a HTTP 500 - response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, se.getMessage()); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + "Internal error (check logs for more information)"); } } From 5c54554cd9fe816d8ce49fb31ff2df19c302f7fb Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 24 Jul 2020 16:33:07 -0500 Subject: [PATCH 131/143] Fix improper AuthorizeException & more info exposure errors --- .../StatelessAuthenticationFilter.java | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessAuthenticationFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessAuthenticationFilter.java index 4ab9fb5371..573fd90a52 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessAuthenticationFilter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessAuthenticationFilter.java @@ -17,7 +17,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.dspace.app.rest.utils.ContextUtil; -import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.factory.AuthorizeServiceFactory; import org.dspace.authorize.service.AuthorizeService; import org.dspace.core.Context; @@ -77,20 +76,17 @@ public class StatelessAuthenticationFilter extends BasicAuthenticationFilter { HttpServletResponse res, FilterChain chain) throws IOException, ServletException { - Authentication authentication = null; + Authentication authentication; try { authentication = getAuthentication(req, res); - } catch (AuthorizeException e) { - res.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage()); - log.error(e.getMessage(), e); - return; } catch (IllegalArgumentException | SQLException e) { - res.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage()); - log.error(e.getMessage(), e); + res.sendError(HttpServletResponse.SC_BAD_REQUEST, "Authentication request is invalid or incorrect"); + log.error("Authentication request is invalid or incorrect (status:{})", + HttpServletResponse.SC_BAD_REQUEST, e); return; } catch (AccessDeniedException e) { - res.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage()); - log.error(e.getMessage(), e); + res.sendError(HttpServletResponse.SC_FORBIDDEN, "Access is denied"); + log.error("Access is denied (status:{})", HttpServletResponse.SC_FORBIDDEN, e); return; } if (authentication != null) { @@ -115,7 +111,7 @@ public class StatelessAuthenticationFilter extends BasicAuthenticationFilter { * @throws IOException If something goes wrong */ private Authentication getAuthentication(HttpServletRequest request, HttpServletResponse res) - throws AuthorizeException, SQLException { + throws AccessDeniedException, SQLException { if (restAuthenticationService.hasAuthenticationData(request)) { // parse the token. @@ -146,7 +142,7 @@ public class StatelessAuthenticationFilter extends BasicAuthenticationFilter { } } else { if (request.getHeader(ON_BEHALF_OF_REQUEST_PARAM) != null) { - throw new AuthorizeException("Only admins are allowed to use the login as feature"); + throw new AccessDeniedException("Only admins are allowed to use the login as feature"); } } From 9ae124bef0dce09fc4c7f1930c21d0a4eee8b167 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 24 Jul 2020 17:09:41 -0500 Subject: [PATCH 132/143] Revert changes to StatelessAuthenticationFilter. It is supposed to be a 401 error! --- .../rest/security/StatelessAuthenticationFilter.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessAuthenticationFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessAuthenticationFilter.java index 573fd90a52..ff845b00f2 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessAuthenticationFilter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessAuthenticationFilter.java @@ -17,6 +17,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.dspace.app.rest.utils.ContextUtil; +import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.factory.AuthorizeServiceFactory; import org.dspace.authorize.service.AuthorizeService; import org.dspace.core.Context; @@ -79,6 +80,10 @@ public class StatelessAuthenticationFilter extends BasicAuthenticationFilter { Authentication authentication; try { authentication = getAuthentication(req, res); + } catch (AuthorizeException e) { + // just return an error, but do not log + res.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication is required"); + return; } catch (IllegalArgumentException | SQLException e) { res.sendError(HttpServletResponse.SC_BAD_REQUEST, "Authentication request is invalid or incorrect"); log.error("Authentication request is invalid or incorrect (status:{})", @@ -111,7 +116,7 @@ public class StatelessAuthenticationFilter extends BasicAuthenticationFilter { * @throws IOException If something goes wrong */ private Authentication getAuthentication(HttpServletRequest request, HttpServletResponse res) - throws AccessDeniedException, SQLException { + throws AuthorizeException, SQLException { if (restAuthenticationService.hasAuthenticationData(request)) { // parse the token. @@ -130,7 +135,7 @@ public class StatelessAuthenticationFilter extends BasicAuthenticationFilter { if (configurationService.getBooleanProperty("webui.user.assumelogin")) { return getOnBehalfOfAuthentication(context, onBehalfOfParameterValue, res); } else { - throw new IllegalArgumentException("The login as feature is not allowed" + + throw new IllegalArgumentException("The 'login as' feature is not allowed" + " due to the current configuration"); } } @@ -142,7 +147,7 @@ public class StatelessAuthenticationFilter extends BasicAuthenticationFilter { } } else { if (request.getHeader(ON_BEHALF_OF_REQUEST_PARAM) != null) { - throw new AccessDeniedException("Only admins are allowed to use the login as feature"); + throw new AuthorizeException("Must be logged in (as an admin) to use the 'login as' feature"); } } From 667a362b25b9f852f2d0e8533ade8644a0a39f00 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 27 Jul 2020 10:44:07 -0500 Subject: [PATCH 133/143] Add keyword "fixes" as a reminder. Remove JIRA --- .github/pull_request_template.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 6542242af7..6799d875f4 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,8 +1,7 @@ ## References -_Add references/links to any related tickets or PRs. These may include:_ -* Link to [JIRA](https://jira.lyrasis.org/projects/DS/summary) ticket(s), if any -* Link to [REST Contract](https://github.com/DSpace/Rest7Contract) or an open REST Contract PR, if any -* Link to [Angular issue or PR](https://github.com/DSpace/dspace-angular/issues) related to this PR, if any +_Add references/links to any related issues or PRs. These may include:_ +* Related to [REST Contract](https://github.com/DSpace/Rest7Contract) or an open REST Contract PR, if any +* Fixes [GitHub issue](https://github.com/DSpace/DSpace/issues), if any ## Description Short summary of changes (1-2 sentences). From dcc651945dd9c4567a0a25028f6f36b39b2d8745 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 27 Jul 2020 11:09:26 -0500 Subject: [PATCH 134/143] Throw errors when possible "zip slip" detected. --- .../app/itemimport/ItemImportServiceImpl.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java index 12fcd84d04..13aa236f54 100644 --- a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java @@ -1519,6 +1519,12 @@ public class ItemImportServiceImpl implements ItemImportService, InitializingBea if (!dir.exists() && !dir.mkdirs()) { log.error("Unable to create directory: " + dir.getAbsolutePath()); } + // Verify that the directory the entry is using is a subpath of zipDir (and not somewhere else!) + if (!dir.toPath().normalize().startsWith(zipDir)) { + throw new IOException("Bad zip entry: '" + entry.getName() + + "' in file '" + zipfile.getAbsolutePath() + "'!" + + " Cannot process this file."); + } //Entries could have too many directories, and we need to adjust the sourcedir // file1.zip (SimpleArchiveFormat / item1 / contents|dublin_core|... @@ -1539,9 +1545,16 @@ public class ItemImportServiceImpl implements ItemImportService, InitializingBea } byte[] buffer = new byte[1024]; int len; + File outFile = new File(zipDir + entry.getName()); + // Verify that this file will be created in our zipDir (and not somewhere else!) + if (!outFile.toPath().normalize().startsWith(zipDir)) { + throw new IOException("Bad zip entry: '" + entry.getName() + + "' in file '" + zipfile.getAbsolutePath() + "'!" + + " Cannot process this file."); + } InputStream in = zf.getInputStream(entry); BufferedOutputStream out = new BufferedOutputStream( - new FileOutputStream(zipDir + entry.getName())); + new FileOutputStream(outFile)); while ((len = in.read(buffer)) >= 0) { out.write(buffer, 0, len); } From cf7daef43125e902ba6eb47506d4a8b5868b41fb Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 27 Jul 2020 11:12:18 -0500 Subject: [PATCH 135/143] Set invalidation cookie to "secure" just to avoid LGTM warning --- .../rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java index 1b5ee6a0c5..8aff9cc884 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java @@ -154,6 +154,7 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication Cookie cookie = new Cookie(AUTHORIZATION_COOKIE, ""); cookie.setHttpOnly(true); cookie.setMaxAge(0); + cookie.setSecure(true); response.addCookie(cookie); } From 294bb02193b74b15f9193e6ef6af3958ca406f31 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 27 Jul 2020 12:00:06 -0500 Subject: [PATCH 136/143] Fix HTTP Response splitting by validating URL --- .../dspace/rdf/negotiation/Negotiator.java | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/rdf/negotiation/Negotiator.java b/dspace-api/src/main/java/org/dspace/rdf/negotiation/Negotiator.java index c28b9ec1e6..d011d305b1 100644 --- a/dspace-api/src/main/java/org/dspace/rdf/negotiation/Negotiator.java +++ b/dspace-api/src/main/java/org/dspace/rdf/negotiation/Negotiator.java @@ -15,6 +15,7 @@ import java.util.Iterator; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.validator.routines.UrlValidator; import org.apache.logging.log4j.Logger; import org.dspace.rdf.RDFUtil; import org.dspace.services.factory.DSpaceServicesFactory; @@ -197,6 +198,7 @@ public class Negotiator { if (extraPathInfo == null) { extraPathInfo = ""; } + UrlValidator urlValidator = new UrlValidator(UrlValidator.ALLOW_LOCAL_URLS); StringBuilder urlBuilder = new StringBuilder(); String lang = null; @@ -256,12 +258,15 @@ public class Negotiator { urlBuilder.append(handle).append("/").append(extraPathInfo); } String url = urlBuilder.toString(); - - log.debug("Will forward to '" + url + "'."); - response.setStatus(HttpServletResponse.SC_SEE_OTHER); - response.setHeader("Location", url); - response.flushBuffer(); - return true; + if (urlValidator.isValid(url)) { + log.debug("Will forward to '" + url + "'."); + response.setStatus(HttpServletResponse.SC_SEE_OTHER); + response.setHeader("Location", url); + response.flushBuffer(); + return true; + } else { + throw new IOException("Invalid URL '" + url + "', cannot redirect."); + } } // currently we cannot serve statistics as rdf @@ -287,10 +292,14 @@ public class Negotiator { urlBuilder.append("/handle/").append(handle); urlBuilder.append("/").append(lang); String url = urlBuilder.toString(); - log.debug("Will forward to '" + url + "'."); - response.setStatus(HttpServletResponse.SC_SEE_OTHER); - response.setHeader("Location", url); - response.flushBuffer(); - return true; + if (urlValidator.isValid(url)) { + log.debug("Will forward to '" + url + "'."); + response.setStatus(HttpServletResponse.SC_SEE_OTHER); + response.setHeader("Location", url); + response.flushBuffer(); + return true; + } else { + throw new IOException("Invalid URL '" + url + "', cannot redirect."); + } } } From b006b84039850be461611687c75e7e25267d5236 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 27 Jul 2020 12:08:47 -0500 Subject: [PATCH 137/143] Use object's reported handle instead of user provided handle --- .../org/dspace/rdf/providing/LocalURIRedirectionServlet.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dspace-rdf/src/main/java/org/dspace/rdf/providing/LocalURIRedirectionServlet.java b/dspace-rdf/src/main/java/org/dspace/rdf/providing/LocalURIRedirectionServlet.java index b6a6854938..7224bb9bfb 100644 --- a/dspace-rdf/src/main/java/org/dspace/rdf/providing/LocalURIRedirectionServlet.java +++ b/dspace-rdf/src/main/java/org/dspace/rdf/providing/LocalURIRedirectionServlet.java @@ -86,7 +86,8 @@ public class LocalURIRedirectionServlet extends HttpServlet { response.sendError(HttpServletResponse.SC_NOT_FOUND); return; } - + // use object's reported handle for redirect (just in case user provided handle had odd characters) + handle = dso.getHandle(); // close the context and send forward. context.abort(); Negotiator.sendRedirect(response, handle, "", requestedMimeType, true); From 53df0814221a7bc36ad91299c48db65dce1621e0 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 27 Jul 2020 13:13:25 -0500 Subject: [PATCH 138/143] Fix possible XXE attacks by disabling DTD parsing for external service requests. --- .../java/org/dspace/app/sherpa/SHERPAResponse.java | 3 +++ .../org/dspace/content/packager/METSManifest.java | 14 +++++++++----- .../dspace/ctask/general/MetadataWebService.java | 3 +++ .../license/CCLicenseConnectorServiceImpl.java | 4 ++++ .../org/dspace/submit/lookup/ArXivService.java | 3 +++ .../org/dspace/submit/lookup/CiNiiService.java | 6 ++++++ .../org/dspace/submit/lookup/CrossRefService.java | 3 +++ .../org/dspace/submit/lookup/PubmedService.java | 9 +++++++++ 8 files changed, 40 insertions(+), 5 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/sherpa/SHERPAResponse.java b/dspace-api/src/main/java/org/dspace/app/sherpa/SHERPAResponse.java index c5b8bbebf3..bd2909c0c1 100644 --- a/dspace-api/src/main/java/org/dspace/app/sherpa/SHERPAResponse.java +++ b/dspace-api/src/main/java/org/dspace/app/sherpa/SHERPAResponse.java @@ -48,6 +48,9 @@ public class SHERPAResponse { factory.setValidating(false); factory.setIgnoringComments(true); factory.setIgnoringElementContentWhitespace(true); + // disallow DTD parsing to ensure no XXE attacks can occur. + // See https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); DocumentBuilder db = factory.newDocumentBuilder(); Document inDoc = db.parse(xmlData); diff --git a/dspace-api/src/main/java/org/dspace/content/packager/METSManifest.java b/dspace-api/src/main/java/org/dspace/content/packager/METSManifest.java index 53a8678df2..ed15037c11 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/METSManifest.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/METSManifest.java @@ -272,12 +272,16 @@ public class METSManifest { // Set validation feature if (validate) { builder.setFeature("http://apache.org/xml/features/validation/schema", true); - } - // Tell the parser where local copies of schemas are, to speed up - // validation. Local XSDs are identified in the configuration file. - if (localSchemas.length() > 0) { - builder.setProperty("http://apache.org/xml/properties/schema/external-schemaLocation", localSchemas); + // Tell the parser where local copies of schemas are, to speed up + // validation & avoid XXE attacks from remote schemas. Local XSDs are identified in the configuration file. + if (localSchemas.length() > 0) { + builder.setProperty("http://apache.org/xml/properties/schema/external-schemaLocation", localSchemas); + } + } else { + // disallow DTD parsing to ensure no XXE attacks can occur. + // See https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html + builder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); } // Parse the METS file diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java b/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java index 2b6c52d0d6..754f3b4ab3 100644 --- a/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java +++ b/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java @@ -199,6 +199,9 @@ public class MetadataWebService extends AbstractCurationTask implements Namespac DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); try { + // disallow DTD parsing to ensure no XXE attacks can occur. + // See https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); docBuilder = factory.newDocumentBuilder(); } catch (ParserConfigurationException pcE) { log.error("caught exception: " + pcE); diff --git a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java index a237a91984..792c25d629 100644 --- a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java @@ -75,6 +75,10 @@ public class CCLicenseConnectorServiceImpl implements CCLicenseConnectorService, .disableAutomaticRetries() .setMaxConnTotal(5) .build(); + + // disallow DTD parsing to ensure no XXE attacks can occur. + // See https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html + parser.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); } /** diff --git a/dspace-api/src/main/java/org/dspace/submit/lookup/ArXivService.java b/dspace-api/src/main/java/org/dspace/submit/lookup/ArXivService.java index 0a32871758..337fb4175a 100644 --- a/dspace-api/src/main/java/org/dspace/submit/lookup/ArXivService.java +++ b/dspace-api/src/main/java/org/dspace/submit/lookup/ArXivService.java @@ -113,6 +113,9 @@ public class ArXivService { factory.setValidating(false); factory.setIgnoringComments(true); factory.setIgnoringElementContentWhitespace(true); + // disallow DTD parsing to ensure no XXE attacks can occur. + // See https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); DocumentBuilder db = factory.newDocumentBuilder(); Document inDoc = db.parse(response.getEntity().getContent()); diff --git a/dspace-api/src/main/java/org/dspace/submit/lookup/CiNiiService.java b/dspace-api/src/main/java/org/dspace/submit/lookup/CiNiiService.java index 23026353fd..bb59043e52 100644 --- a/dspace-api/src/main/java/org/dspace/submit/lookup/CiNiiService.java +++ b/dspace-api/src/main/java/org/dspace/submit/lookup/CiNiiService.java @@ -102,6 +102,9 @@ public class CiNiiService { factory.setValidating(false); factory.setIgnoringComments(true); factory.setIgnoringElementContentWhitespace(true); + // disallow DTD parsing to ensure no XXE attacks can occur. + // See https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); DocumentBuilder db = factory.newDocumentBuilder(); Document inDoc = db.parse(response.getEntity().getContent()); @@ -178,6 +181,9 @@ public class CiNiiService { factory.setValidating(false); factory.setIgnoringComments(true); factory.setIgnoringElementContentWhitespace(true); + // disallow DTD parsing to ensure no XXE attacks can occur. + // See https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); DocumentBuilder db = factory.newDocumentBuilder(); Document inDoc = db.parse(response.getEntity().getContent()); diff --git a/dspace-api/src/main/java/org/dspace/submit/lookup/CrossRefService.java b/dspace-api/src/main/java/org/dspace/submit/lookup/CrossRefService.java index f73e9c0352..4b99cf1f8b 100644 --- a/dspace-api/src/main/java/org/dspace/submit/lookup/CrossRefService.java +++ b/dspace-api/src/main/java/org/dspace/submit/lookup/CrossRefService.java @@ -99,6 +99,9 @@ public class CrossRefService { factory.setValidating(false); factory.setIgnoringComments(true); factory.setIgnoringElementContentWhitespace(true); + // disallow DTD parsing to ensure no XXE attacks can occur. + // See https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); DocumentBuilder db = factory .newDocumentBuilder(); diff --git a/dspace-api/src/main/java/org/dspace/submit/lookup/PubmedService.java b/dspace-api/src/main/java/org/dspace/submit/lookup/PubmedService.java index fa30ee8ea5..a5e74322f5 100644 --- a/dspace-api/src/main/java/org/dspace/submit/lookup/PubmedService.java +++ b/dspace-api/src/main/java/org/dspace/submit/lookup/PubmedService.java @@ -119,6 +119,9 @@ public class PubmedService { factory.setValidating(false); factory.setIgnoringComments(true); factory.setIgnoringElementContentWhitespace(true); + // disallow DTD parsing to ensure no XXE attacks can occur. + // See https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); DocumentBuilder builder; try { @@ -156,6 +159,9 @@ public class PubmedService { factory.setValidating(false); factory.setIgnoringComments(true); factory.setIgnoringElementContentWhitespace(true); + // disallow DTD parsing to ensure no XXE attacks can occur. + // See https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); DocumentBuilder builder = factory.newDocumentBuilder(); Document inDoc = builder.parse(stream); @@ -216,6 +222,9 @@ public class PubmedService { factory.setValidating(false); factory.setIgnoringComments(true); factory.setIgnoringElementContentWhitespace(true); + // disallow DTD parsing to ensure no XXE attacks can occur. + // See https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); DocumentBuilder builder = factory.newDocumentBuilder(); Document inDoc = builder From c323b989d2b3751fbb4a0b10aaac97423da47917 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 27 Jul 2020 14:42:56 -0500 Subject: [PATCH 139/143] Escape any HTML in user provided param. --- .../main/java/org/dspace/app/rest/OpenSearchController.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/OpenSearchController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/OpenSearchController.java index 42ad173f2e..62c6a9c573 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/OpenSearchController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/OpenSearchController.java @@ -34,6 +34,7 @@ import org.dspace.content.service.CollectionService; import org.dspace.content.service.CommunityService; import org.dspace.core.Context; import org.dspace.core.LogManager; +import org.dspace.core.Utils; import org.dspace.discovery.DiscoverQuery; import org.dspace.discovery.DiscoverResult; import org.dspace.discovery.IndexableObject; @@ -103,7 +104,8 @@ public class OpenSearchController { // do some sanity checking if (!openSearchService.getFormats().contains(format)) { - String err = "Format " + format + " is not supported."; + // Since we are returning error response as HTML, escape any HTML in "format" param + String err = "Format " + Utils.addEntities(format) + " is not supported."; response.setContentType("text/html"); response.setContentLength(err.length()); response.getWriter().write(err); From 79e4e3b497374e889161c15343faf461b250a454 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 27 Jul 2020 15:08:05 -0500 Subject: [PATCH 140/143] Escape special chars in filename which includes user input --- dspace-sword/src/main/java/org/dspace/sword/DepositManager.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dspace-sword/src/main/java/org/dspace/sword/DepositManager.java b/dspace-sword/src/main/java/org/dspace/sword/DepositManager.java index 7535302139..4491f876cc 100644 --- a/dspace-sword/src/main/java/org/dspace/sword/DepositManager.java +++ b/dspace-sword/src/main/java/org/dspace/sword/DepositManager.java @@ -245,6 +245,8 @@ public class DepositManager { String filenameBase = "sword-" + deposit.getUsername() + "-" + (new Date()).getTime(); + // No dots or slashes allowed in filename + filenameBase = filenameBase.replaceAll("\\.", "").replaceAll("/", ""). replaceAll("\\\\", ""); File packageFile = new File(path, filenameBase); File headersFile = new File(path, filenameBase + "-headers"); From 93903de1c599ffc54c5327cb1f202e44737053aa Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 27 Jul 2020 15:23:43 -0500 Subject: [PATCH 141/143] Fix possible int overflow by converting to long before multiplication --- .../src/main/java/org/dspace/harvest/HarvestScheduler.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/harvest/HarvestScheduler.java b/dspace-api/src/main/java/org/dspace/harvest/HarvestScheduler.java index d668b09bc4..5d0545845c 100644 --- a/dspace-api/src/main/java/org/dspace/harvest/HarvestScheduler.java +++ b/dspace-api/src/main/java/org/dspace/harvest/HarvestScheduler.java @@ -134,11 +134,13 @@ public class HarvestScheduler implements Runnable { if (maxActiveThreads == 0) { maxActiveThreads = 3; } - minHeartbeat = ConfigurationManager.getIntProperty("oai", "harvester.minHeartbeat") * 1000; + minHeartbeat = ConfigurationManager.getIntProperty("oai", "harvester.minHeartbeat"); + minHeartbeat = minHeartbeat * 1000; // multiple by 1000 to turn seconds to ms if (minHeartbeat == 0) { minHeartbeat = 30000; } - maxHeartbeat = ConfigurationManager.getIntProperty("oai", "harvester.maxHeartbeat") * 1000; + maxHeartbeat = ConfigurationManager.getIntProperty("oai", "harvester.maxHeartbeat"); + maxHeartbeat = maxHeartbeat * 1000; // multiple by 1000 to turn seconds to ms if (maxHeartbeat == 0) { maxHeartbeat = 3600000; } From 645a1800bb0f54af01269dd8456114efff142f83 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 27 Jul 2020 15:27:51 -0500 Subject: [PATCH 142/143] Fix potentially unsafe external link --- .../main/webapp/static/reports/restReport.js | 153 +++++++++--------- 1 file changed, 77 insertions(+), 76 deletions(-) diff --git a/dspace-rest/src/main/webapp/static/reports/restReport.js b/dspace-rest/src/main/webapp/static/reports/restReport.js index 3efc67e85e..8abe17be06 100644 --- a/dspace-rest/src/main/webapp/static/reports/restReport.js +++ b/dspace-rest/src/main/webapp/static/reports/restReport.js @@ -15,7 +15,7 @@ var Report = function() { this.ROOTPATH = "/xmlui/handle/" //this.ROOTPATH = "/jspui/handle/" //this.ROOTPATH = "/handle/" - + //Indicate if Password Authentication is supported this.makeAuthLink = function(){return false;}; @@ -27,34 +27,34 @@ var Report = function() { this.getId = function(obj) { return obj.uuid; } - + //Override this method is sortable.js has been included this.hasSorttable = function() { return false; } - + this.getDefaultParameters = function(){ return {}; } this.getCurrentParameters = function(){ return {}; } - + this.saveUrl = function() { this.myReportParameters.saveAsUrl(this.getCurrentParameters()); } - + this.getLoginPayload = function() { //Placeholder to allow a customized report to prompt for email/password //If not enabled, the authenticaton callback will be called immediately var email = $("#restemail").val(); var pass = $("#restpass").val(); if (email == "" || pass == "") { - return undefined; + return undefined; } else if (email == null || pass == null) { - return undefined; + return undefined; } else { - return {email: email, password: pass}; + return {email: email, password: pass}; } } this.getLangSuffix = function(){ @@ -82,15 +82,15 @@ var Report = function() { className: 'spinner', // The CSS class to assign to the spinner zIndex: 2e9, // The z-index (defaults to 2000000000) top: '400px', // Top position relative to parent - left: '600px' // Left position relative to parent + left: '600px' // Left position relative to parent }); - + this.displayItems = function(itemsTitle, offset, limit, total, funcdec, funcinc) { var count = $("#itemtable tr.data").length; - + var last = offset + limit; var suff = ""; - + if (total == null) { last = offset + count; suff = (count == limit) ? " of " + last + "+ " : " of " + last; @@ -102,7 +102,7 @@ var Report = function() { suff = " of " + total; } suff += " unfiltered; displaying " + count + " filtered" ; - + itemsTitle += " (" + (offset+1) + " - " + last + suff + ")"; $("#prev,#next").attr("disabled",true); $("#itemdiv h3").text(itemsTitle); @@ -110,34 +110,34 @@ var Report = function() { if (offset > 0) $("#prev").attr("disabled", false); $("#prev").off("click").on("click", funcdec); //in case of filters, always allow next - + if (total == null) { - $("#next").attr("disabled", false); + $("#next").attr("disabled", false); } else if (offset + limit < total) { - $("#next").attr("disabled", false); + $("#next").attr("disabled", false); $("#exlimit").addClass("red"); } else if (limit == total) { //total may only be accurate to one page - $("#next").attr("disabled", false); + $("#next").attr("disabled", false); $("#exlimit").addClass("red"); } $("#next").off("click").on("click", funcinc); } - + this.myReportParameters = undefined; this.myFilters = undefined; this.myMetadataFields = undefined; - + this.initMetadataFields = function() { this.myMetadataFields = new MetadataFields(self); - this.myMetadataFields.load(); + this.myMetadataFields.load(); } - + this.initBitstreamFields = function() { this.myBitstreamFields = new BitstreamFields(self); - this.myBitstreamFields.load(); + this.myBitstreamFields.load(); } - + this.baseInit = function() { this.myReportParameters = new ReportParameters( this.getDefaultParameters(), @@ -173,13 +173,13 @@ var Report = function() { }); return itemdata; } - + this.export = function(rows) { var itemdata = "data:text/csv;charset=utf-8," + this.makeCsv(rows); var encodedUri = encodeURI(itemdata); - window.open(encodedUri); + window.open(encodedUri); } - + //this is meant to be overridden for each report this.exportCol = function(colnum, col) { var data = ""; @@ -187,7 +187,7 @@ var Report = function() { data += self.exportCell(col); return data; } - + this.exportCell = function(col) { data = "\""; $(col).contents().each(function(i, node){ @@ -198,16 +198,16 @@ var Report = function() { if ($(node).is("div:not(:last-child)")) { data += "||"; } - } + } }); data += "\""; return data; } - + this.init = function() { - this.baseInit(); + this.baseInit(); } - + } var Auth = function(report) { @@ -242,17 +242,17 @@ var Auth = function(report) { self.authStat(); self.callback(); } - }); + }); } this.verifyShibLogin = function() { var self = this; $.ajax({ - url: "/rest/shibboleth-login", + url: "/rest/shibboleth-login", success: self.authStat }); } - + this.authStat = function() { var self = this; $.ajax({ @@ -264,7 +264,7 @@ var Auth = function(report) { success: function(data) { var user = ""; if (data.email != undefined) { - user = data.email; + user = data.email; } else { user = "You are not logged in. Some items may be excluded from reports."; } @@ -279,10 +279,10 @@ var Auth = function(report) { if (data.email == undefined && self.report.makeShibLink()) { self.verifyShibLogin(); } - } - }); + } + }); } - + this.logout = function() { var self = this; $.ajax({ @@ -293,7 +293,7 @@ var Auth = function(report) { complete: function(xhr, status) { self.authStat(); } - }); + }); } this.getHeaders = function() { var HEADERS = {}; @@ -314,14 +314,14 @@ var ReportParameters = function(defaultParams, prmstr) { var field = tmparr[0]; var val = decodeURIComponent(tmparr[1]); var pval = this.params[field]; - + if ($.isArray(pval)) { - pval[pval.length] = val; + pval[pval.length] = val; } else { this.params[field] = val; } } - $("#limit").val(this.params.limit); + $("#limit").val(this.params.limit); $("#offset").val(this.params.offset); this.limit = this.params.limit; this.offset = this.params.offset; @@ -350,11 +350,11 @@ var ReportParameters = function(defaultParams, prmstr) { var lim = $("#limit").val(); if ($.isNumeric(val) && $.isNumeric(lim)) { if (increment) { - $("#offset").val(this.getNextOffset()); + $("#offset").val(this.getNextOffset()); } else { - $("#offset").val(this.getPrevOffset()); + $("#offset").val(this.getPrevOffset()); } - } + } } this.saveAsUrl = function(params) { @@ -381,7 +381,7 @@ var Filters = function() { $("#filter-reload").attr("disabled", false); } ); - + $.getJSON( "/rest/filters", function(data){ @@ -444,13 +444,13 @@ var Filters = function() { list = "none"; } return list; - } + } } var MetadataFields = function(report) { this.metadataSchemas = undefined; var self = this; - + this.load = function(){ $.ajax({ url: "/rest/registries/schema", @@ -463,15 +463,15 @@ var MetadataFields = function(report) { }, complete: function(xhr, status) { } - }); + }); } - + this.initFields = function(data, report) { var params = report.myReportParameters.params; self.metadataSchemas = data; self.drawShowFields(params["show_fields[]"]); } - + this.getShowFields = function(){ var val = $("#show-fields select").val(); return val == null ? Array() : val; @@ -497,7 +497,7 @@ var MetadataFields = function(report) { }); }); } - + this.initQueries = function(){}; } @@ -508,15 +508,15 @@ var BitstreamFields = function(report) { } this.map = [ { - key: "original-file-names", - name: "Original File Names", + key: "original-file-names", + name: "Original File Names", ftest: self.isOriginal, fval: function(bit) { return bit.name; } }, { - key: "mime-type", + key: "mime-type", name: "Mime Type", ftest: self.isOriginal, fval: function(bit) { @@ -524,7 +524,7 @@ var BitstreamFields = function(report) { } }, { - key: "bitstream-format", + key: "bitstream-format", name: "Bitstream Format", ftest: self.isOriginal, fval: function(bit) { @@ -532,7 +532,7 @@ var BitstreamFields = function(report) { } }, { - key: "bitstream-description", + key: "bitstream-description", name: "Bitstream Description", ftest: self.isOriginal, fval: function(bit) { @@ -540,7 +540,7 @@ var BitstreamFields = function(report) { } }, { - key: "bitstream-size", + key: "bitstream-size", name: "Bitstream Size", ftest: self.isOriginal, fval: function(bit) { @@ -548,18 +548,18 @@ var BitstreamFields = function(report) { } }, { - key: "bitstream-checksum", + key: "bitstream-checksum", name: "MD5 Checksum", ftest: self.isOriginal, fval: function(bit) { if (bit.checkSum.checkSumAlgorithm === "MD5") { - return bit.checkSum.value; + return bit.checkSum.value; } return ""; } }, ]; - + this.load = function(){ self.initFields(report); } @@ -568,7 +568,7 @@ var BitstreamFields = function(report) { var params = report.myReportParameters.params; self.drawShowFieldsBits(params["show_fields_bits[]"]); }; - + this.hasBitstreamFields = function() { return self.getShowFieldsBits() != null; } @@ -576,20 +576,20 @@ var BitstreamFields = function(report) { var val = $("#show-fields-bits select").val(); return val == null ? Array() : val; } - + this.drawShowFieldsBits = function(pfieldsBits) { var sel = $("