diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageEntity.java b/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageEntity.java index b9833ff7ee30..5d96dd3f956c 100644 --- a/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageEntity.java +++ b/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageEntity.java @@ -34,7 +34,12 @@ public class LDNMessageEntity implements ReloadableEntity { * LDN messages interact with a fictitious queue. Scheduled tasks manage the queue. */ - /** + /** + * Message must not be processed. + */ + public static final Integer QUEUE_STATUS_UNTRUSTED_IP = 0; + + /** * Message queued, it has to be elaborated. */ public static final Integer QUEUE_STATUS_QUEUED = 1; @@ -113,6 +118,9 @@ public class LDNMessageEntity implements ReloadableEntity { @Column(name = "coar_notify_type") private String coarNotifyType; + @Column(name = "source_ip") + private String sourceIp; + protected LDNMessageEntity() { } @@ -255,6 +263,14 @@ public void setQueueTimeout(Date queueTimeout) { this.queueTimeout = queueTimeout; } + public String getSourceIp() { + return sourceIp; + } + + public void setSourceIp(String sourceIp) { + this.sourceIp = sourceIp; + } + @Override public String toString() { return "LDNMessage id:" + this.getID() + " typed:" + this.getType(); diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/NotifyServiceEntity.java b/dspace-api/src/main/java/org/dspace/app/ldn/NotifyServiceEntity.java index 1aeb18947392..206ed16fa00e 100644 --- a/dspace-api/src/main/java/org/dspace/app/ldn/NotifyServiceEntity.java +++ b/dspace-api/src/main/java/org/dspace/app/ldn/NotifyServiceEntity.java @@ -57,6 +57,12 @@ public class NotifyServiceEntity implements ReloadableEntity { @Column(name = "score") private BigDecimal score; + @Column(name = "lower_ip") + private String lowerIp; + + @Column(name = "upper_ip") + private String upperIp; + public void setId(Integer id) { this.id = id; } @@ -130,4 +136,21 @@ public BigDecimal getScore() { public void setScore(BigDecimal score) { this.score = score; } + + public String getLowerIp() { + return lowerIp; + } + + public void setLowerIp(String lowerIp) { + this.lowerIp = lowerIp; + } + + public String getUpperIp() { + return upperIp; + } + + public void setUpperIp(String upperIp) { + this.upperIp = upperIp; + } + } diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/service/LDNMessageService.java b/dspace-api/src/main/java/org/dspace/app/ldn/service/LDNMessageService.java index 9520a47461ec..b1d86edb8e19 100644 --- a/dspace-api/src/main/java/org/dspace/app/ldn/service/LDNMessageService.java +++ b/dspace-api/src/main/java/org/dspace/app/ldn/service/LDNMessageService.java @@ -59,10 +59,11 @@ public interface LDNMessageService { * * @param context The DSpace context * @param notification the requested notification + * @param sourceIp the source ip * @return the created LDN Message * @throws SQLException If something goes wrong in the database */ - public LDNMessageEntity create(Context context, Notification notification) throws SQLException; + public LDNMessageEntity create(Context context, Notification notification, String sourceIp) throws SQLException; /** * Update the provided LDNMessage diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/LDNMessageServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/LDNMessageServiceImpl.java index 35490f6697e1..f2d44237edfb 100644 --- a/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/LDNMessageServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/LDNMessageServiceImpl.java @@ -7,6 +7,8 @@ */ package org.dspace.app.ldn.service.impl; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collections; @@ -85,7 +87,7 @@ public LDNMessageEntity create(Context context, String id) throws SQLException { } @Override - public LDNMessageEntity create(Context context, Notification notification) throws SQLException { + public LDNMessageEntity create(Context context, Notification notification, String sourceIp) throws SQLException { LDNMessageEntity ldnMessage = create(context, notification.getId()); ldnMessage.setObject(findDspaceObjectByUrl(context, notification.getObject().getId())); if (null != notification.getContext()) { @@ -124,11 +126,55 @@ public LDNMessageEntity create(Context context, Notification notification) throw ldnMessage.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_UNTRUSTED); } ldnMessage.setQueueTimeout(new Date()); + ldnMessage.setSourceIp(sourceIp); + + if (!isValidIp(ldnMessage)) { + ldnMessage.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_UNTRUSTED_IP); + } update(context, ldnMessage); return ldnMessage; } + private boolean isValidIp(LDNMessageEntity message) { + + boolean enabled = configurationService.getBooleanProperty("coar-notify.ip-range.enabled", true); + + if (!enabled) { + return true; + } + + NotifyServiceEntity notifyService = + message.getOrigin() == null ? message.getTarget() : message.getOrigin(); + + String lowerIp = notifyService.getLowerIp(); + String upperIp = notifyService.getUpperIp(); + + try { + InetAddress ip = InetAddress.getByName(message.getSourceIp()); + InetAddress lowerBoundAddress = InetAddress.getByName(lowerIp); + InetAddress upperBoundAddress = InetAddress.getByName(upperIp); + + long ipLong = ipToLong(ip); + long lowerBoundLong = ipToLong(lowerBoundAddress); + long upperBoundLong = ipToLong(upperBoundAddress); + + return ipLong >= lowerBoundLong && ipLong <= upperBoundLong; + } catch (UnknownHostException e) { + return false; + } + } + + private long ipToLong(InetAddress ip) { + byte[] octets = ip.getAddress(); + long result = 0; + for (byte octet : octets) { + result <<= 8; + result |= octet & 0xff; + } + return result; + } + @Override public void update(Context context, LDNMessageEntity ldnMessage) throws SQLException { //CST-12126 then LDNMessageService.update() when the origin is set != null, diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V8.0_2024.01.12__notifyservice_table_ip_range_coulmns.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V8.0_2024.01.12__notifyservice_table_ip_range_coulmns.sql new file mode 100644 index 000000000000..740bd525b8cf --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V8.0_2024.01.12__notifyservice_table_ip_range_coulmns.sql @@ -0,0 +1,15 @@ +-- +-- 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/ +-- + +-------------------------------------------------------------------------- +-- ADD IP Range columns to notifyservice table +-------------------------------------------------------------------------- + +ALTER TABLE notifyservice ADD COLUMN lower_ip VARCHAR(45); + +ALTER TABLE notifyservice ADD COLUMN upper_ip VARCHAR(45); \ No newline at end of file diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V8.0_2024.01.13__ldn_message_table_source_ip_coulmn.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V8.0_2024.01.13__ldn_message_table_source_ip_coulmn.sql new file mode 100644 index 000000000000..5ddfc767de88 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V8.0_2024.01.13__ldn_message_table_source_ip_coulmn.sql @@ -0,0 +1,13 @@ +-- +-- 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/ +-- + +-------------------------------------------------------------------------- +-- ADD source_ip columns to ldn_message table +-------------------------------------------------------------------------- + +ALTER TABLE ldn_message ADD COLUMN source_ip VARCHAR(45); \ No newline at end of file diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V8.0_2024.01.12__notifyservice_table_ip_range_coulmns.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V8.0_2024.01.12__notifyservice_table_ip_range_coulmns.sql new file mode 100644 index 000000000000..740bd525b8cf --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V8.0_2024.01.12__notifyservice_table_ip_range_coulmns.sql @@ -0,0 +1,15 @@ +-- +-- 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/ +-- + +-------------------------------------------------------------------------- +-- ADD IP Range columns to notifyservice table +-------------------------------------------------------------------------- + +ALTER TABLE notifyservice ADD COLUMN lower_ip VARCHAR(45); + +ALTER TABLE notifyservice ADD COLUMN upper_ip VARCHAR(45); \ No newline at end of file diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V8.0_2024.01.13__ldn_message_table_source_ip_coulmn.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V8.0_2024.01.13__ldn_message_table_source_ip_coulmn.sql new file mode 100644 index 000000000000..5ddfc767de88 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V8.0_2024.01.13__ldn_message_table_source_ip_coulmn.sql @@ -0,0 +1,13 @@ +-- +-- 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/ +-- + +-------------------------------------------------------------------------- +-- ADD source_ip columns to ldn_message table +-------------------------------------------------------------------------- + +ALTER TABLE ldn_message ADD COLUMN source_ip VARCHAR(45); \ No newline at end of file diff --git a/dspace-api/src/test/java/org/dspace/builder/NotifyServiceBuilder.java b/dspace-api/src/test/java/org/dspace/builder/NotifyServiceBuilder.java index a7886ebe5149..9c8de62d8bcc 100644 --- a/dspace-api/src/test/java/org/dspace/builder/NotifyServiceBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/NotifyServiceBuilder.java @@ -136,4 +136,14 @@ public NotifyServiceBuilder isEnabled(boolean enabled) { return this; } + public NotifyServiceBuilder withLowerIp(String lowerIp) { + notifyServiceEntity.setLowerIp(lowerIp); + return this; + } + + public NotifyServiceBuilder withUpperIp(String upperIp) { + notifyServiceEntity.setUpperIp(upperIp); + return this; + } + } \ No newline at end of file diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/LDNInboxController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/LDNInboxController.java index 4dec7b5b2639..a86264381445 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/LDNInboxController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/LDNInboxController.java @@ -8,6 +8,7 @@ package org.dspace.app.rest; import java.util.regex.Pattern; +import javax.servlet.http.HttpServletRequest; import org.apache.commons.validator.routines.UrlValidator; import org.apache.logging.log4j.Logger; @@ -51,12 +52,14 @@ public class LDNInboxController { * @throws Exception */ @PostMapping(value = "/inbox", consumes = "application/ld+json") - public ResponseEntity inbox(@RequestBody Notification notification) throws Exception { + public ResponseEntity inbox(HttpServletRequest request, @RequestBody Notification notification) + throws Exception { + Context context = ContextUtil.obtainCurrentRequestContext(); validate(notification); log.info("stored notification {} {}", notification.getId(), notification.getType()); - LDNMessageEntity ldnMsgEntity = ldnMessageService.create(context, notification); + LDNMessageEntity ldnMsgEntity = ldnMessageService.create(context, notification, request.getRemoteAddr()); log.info("stored ldn message {}", ldnMsgEntity); context.commit(); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/NotifyServiceConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/NotifyServiceConverter.java index ed2295ad2a51..8e0225f43fca 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/NotifyServiceConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/NotifyServiceConverter.java @@ -37,6 +37,8 @@ public NotifyServiceRest convert(NotifyServiceEntity obj, Projection projection) notifyServiceRest.setLdnUrl(obj.getLdnUrl()); notifyServiceRest.setEnabled(obj.isEnabled()); notifyServiceRest.setScore(obj.getScore()); + notifyServiceRest.setLowerIp(obj.getLowerIp()); + notifyServiceRest.setUpperIp(obj.getUpperIp()); if (obj.getInboundPatterns() != null) { notifyServiceRest.setNotifyServiceInboundPatterns( diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/NotifyServiceRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/NotifyServiceRest.java index e827c81a763b..2cfad037d266 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/NotifyServiceRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/NotifyServiceRest.java @@ -29,6 +29,8 @@ public class NotifyServiceRest extends BaseObjectRest { private String ldnUrl; private boolean enabled; private BigDecimal score; + private String lowerIp; + private String upperIp; private List notifyServiceInboundPatterns; @@ -103,4 +105,20 @@ public void setScore(BigDecimal score) { this.score = score; } + public String getLowerIp() { + return lowerIp; + } + + public void setLowerIp(String lowerIp) { + this.lowerIp = lowerIp; + } + + public String getUpperIp() { + return upperIp; + } + + public void setUpperIp(String upperIp) { + this.upperIp = upperIp; + } + } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/NotifyServiceRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/NotifyServiceRestRepository.java index 4c1be6cc4573..2034a0751224 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/NotifyServiceRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/NotifyServiceRestRepository.java @@ -112,6 +112,8 @@ protected NotifyServiceRest createAndReturn(Context context) throws AuthorizeExc notifyServiceEntity.setUrl(notifyServiceRest.getUrl()); notifyServiceEntity.setLdnUrl(notifyServiceRest.getLdnUrl()); notifyServiceEntity.setEnabled(notifyServiceRest.isEnabled()); + notifyServiceEntity.setLowerIp(notifyServiceRest.getLowerIp()); + notifyServiceEntity.setUpperIp(notifyServiceRest.getUpperIp()); if (notifyServiceRest.getNotifyServiceInboundPatterns() != null) { appendNotifyServiceInboundPatterns(context, notifyServiceEntity, diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceLowerIpReplaceOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceLowerIpReplaceOperation.java new file mode 100644 index 000000000000..be605f94d834 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceLowerIpReplaceOperation.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.repository.patch.operation.ldn; + +import java.sql.SQLException; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.service.NotifyService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService lowerIp Replace patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "replace", + * "path": "/lowerIp", + * "value": "lowerIp value" + * }]' + * + */ +@Component +public class NotifyServiceLowerIpReplaceOperation extends PatchOperation { + + @Autowired + private NotifyService notifyService; + + private static final String OPERATION_PATH = "/lowerip"; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) + throws SQLException { + checkOperationValue(operation.getValue()); + + Object lowerIp = operation.getValue(); + if (lowerIp == null | !(lowerIp instanceof String)) { + throw new UnprocessableEntityException("The /lowerIp value must be a string"); + } + + checkModelForExistingValue(notifyServiceEntity); + notifyServiceEntity.setLowerIp((String) lowerIp); + notifyService.update(context, notifyServiceEntity); + return notifyServiceEntity; + } + + /** + * Checks whether the lowerIp of notifyServiceEntity has an existing value to replace + * @param notifyServiceEntity Object on which patch is being done + */ + private void checkModelForExistingValue(NotifyServiceEntity notifyServiceEntity) { + if (notifyServiceEntity.getLowerIp() == null) { + throw new DSpaceBadRequestException("Attempting to replace a non-existent value (lowerIp)."); + } + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_REPLACE) && + operation.getPath().trim().toLowerCase().equalsIgnoreCase(OPERATION_PATH)); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceLowerOrUpperRemoveOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceLowerOrUpperRemoveOperation.java new file mode 100644 index 000000000000..a7425a002796 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceLowerOrUpperRemoveOperation.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.repository.patch.operation.ldn; + +import java.sql.SQLException; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService LowerIp Or UpperIp Remove patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "remove", + * "path": "/lowerIp" + * }]' + * + */ +@Component +public class NotifyServiceLowerOrUpperRemoveOperation extends PatchOperation { + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) + throws SQLException { + throw new UnprocessableEntityException("/lowerIp or /upperIp are mandatory and can't be removed"); + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_REMOVE) && + (operation.getPath().trim().toLowerCase().equalsIgnoreCase("/lowerip") || + operation.getPath().trim().toLowerCase().equalsIgnoreCase("/upperip"))); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceUpperIpReplaceOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceUpperIpReplaceOperation.java new file mode 100644 index 000000000000..7ee362cf97d0 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceUpperIpReplaceOperation.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.repository.patch.operation.ldn; + +import java.sql.SQLException; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.service.NotifyService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService upperIp Replace patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "replace", + * "path": "/upperIp", + * "value": "upperIp value" + * }]' + * + */ +@Component +public class NotifyServiceUpperIpReplaceOperation extends PatchOperation { + + @Autowired + private NotifyService notifyService; + + private static final String OPERATION_PATH = "/upperip"; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) + throws SQLException { + checkOperationValue(operation.getValue()); + + Object upperIp = operation.getValue(); + if (upperIp == null | !(upperIp instanceof String)) { + throw new UnprocessableEntityException("The /upperIp value must be a string"); + } + + checkModelForExistingValue(notifyServiceEntity); + notifyServiceEntity.setUpperIp((String) upperIp); + notifyService.update(context, notifyServiceEntity); + return notifyServiceEntity; + } + + /** + * Checks whether the upperIp of notifyServiceEntity has an existing value to replace + * @param notifyServiceEntity Object on which patch is being done + */ + private void checkModelForExistingValue(NotifyServiceEntity notifyServiceEntity) { + if (notifyServiceEntity.getUpperIp() == null) { + throw new DSpaceBadRequestException("Attempting to replace a non-existent value (upperIp)."); + } + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_REPLACE) && + operation.getPath().trim().toLowerCase().equalsIgnoreCase(OPERATION_PATH)); + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/LDNInboxControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/LDNInboxControllerIT.java index ddce490e96b8..30f27004abe0 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/LDNInboxControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/LDNInboxControllerIT.java @@ -76,6 +76,8 @@ public void ldnInboxAnnounceEndorsementTest() throws Exception { .withDescription("service description") .withUrl("service url") .withLdnUrl("https://overlay-journal.com/inbox/") + .withLowerIp("127.0.0.1") + .withUpperIp("127.0.0.3") .build(); context.restoreAuthSystemState(); @@ -111,6 +113,8 @@ public void ldnInboxAnnounceReviewTest() throws Exception { .withUrl("https://review-service.com/inbox/about/") .withLdnUrl("https://review-service.com/inbox/") .withScore(BigDecimal.valueOf(0.6d)) + .withLowerIp("127.0.0.1") + .withUpperIp("127.0.0.3") .build(); String announceReview = IOUtils.toString(announceReviewStream, Charset.defaultCharset()); announceReviewStream.close(); @@ -163,6 +167,8 @@ public void ldnInboxOfferReviewAndACKTest() throws Exception { .withUrl("https://review-service.com/inbox/about/") .withLdnUrl("https://review-service.com/inbox/") .withScore(BigDecimal.valueOf(0.6d)) + .withLowerIp("127.0.0.1") + .withUpperIp("127.0.0.3") .build(); InputStream offerReviewStream = getClass().getResourceAsStream("ldn_offer_review.json"); String announceReview = IOUtils.toString(offerReviewStream, Charset.defaultCharset()); @@ -218,6 +224,8 @@ public void ldnInboxAnnounceReleaseTest() throws Exception { .withUrl("https://review-service.com/inbox/about/") .withLdnUrl("https://review-service.com/inbox/") .withScore(BigDecimal.valueOf(0.6d)) + .withLowerIp("127.0.0.1") + .withUpperIp("127.0.0.3") .build(); String announceRelationship = IOUtils.toString(announceRelationshipStream, Charset.defaultCharset()); announceRelationshipStream.close(); @@ -265,6 +273,46 @@ private void checkStoredLDNMessage(Notification notification, LDNMessageEntity l assertEquals(notification.getType(), storedMessage.getType()); } + @Test + public void ldnInboxAnnounceEndorsementInvalidIpTest() throws Exception { + context.turnOffAuthorisationSystem(); + Community community = CommunityBuilder.createCommunity(context).withName("community").build(); + Collection collection = CollectionBuilder.createCollection(context, community).build(); + Item item = ItemBuilder.createItem(context, collection).build(); + String object = configurationService.getProperty("dspace.ui.url") + "/handle/" + item.getHandle(); + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("service url") + .withLdnUrl("https://overlay-journal.com/inbox/") + .withLowerIp("127.0.0.2") + .withUpperIp("127.0.0.5") + .build(); + context.restoreAuthSystemState(); + + InputStream announceEndorsementStream = getClass().getResourceAsStream("ldn_announce_endorsement.json"); + String announceEndorsement = IOUtils.toString(announceEndorsementStream, Charset.defaultCharset()); + announceEndorsementStream.close(); + String message = announceEndorsement.replaceAll("<>", object); + message = message.replaceAll("<>", object); + + ObjectMapper mapper = new ObjectMapper(); + Notification notification = mapper.readValue(message, Notification.class); + getClient() + .perform(post("/ldn/inbox") + .contentType("application/ld+json") + .content(message)) + .andExpect(status().isAccepted()); + + int processed = ldnMessageService.extractAndProcessMessageFromQueue(context); + assertEquals(processed, 0); + + LDNMessageEntity ldnMessage = ldnMessageService.find(context, notification.getId()); + checkStoredLDNMessage(notification, ldnMessage, object); + assertEquals(ldnMessage.getQueueStatus(), LDNMessageEntity.QUEUE_STATUS_UNTRUSTED_IP); + } + @Override @After public void destroy() throws Exception { diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/NotifyServiceRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/NotifyServiceRestRepositoryIT.java index 226cf1242eb5..984b9b573e5c 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/NotifyServiceRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/NotifyServiceRestRepositoryIT.java @@ -210,6 +210,8 @@ public void createTest() throws Exception { notifyServiceRest.setLdnUrl("https://service.ldn.org/inbox"); notifyServiceRest.setNotifyServiceInboundPatterns(List.of(inboundPatternRestOne, inboundPatternRestTwo)); notifyServiceRest.setEnabled(false); + notifyServiceRest.setLowerIp("192.168.0.1"); + notifyServiceRest.setUpperIp("192.168.0.5"); AtomicReference idRef = new AtomicReference(); String authToken = getAuthToken(admin.getEmail(), password); @@ -218,7 +220,8 @@ public void createTest() throws Exception { .contentType(contentType)) .andExpect(status().isCreated()) .andExpect(jsonPath("$", matchNotifyService("service name", "service description", - "https://service.ldn.org/about", "https://service.ldn.org/inbox", false))) + "https://service.ldn.org/about", "https://service.ldn.org/inbox", false, + "192.168.0.1", "192.168.0.5"))) .andDo(result -> idRef.set((read(result.getResponse().getContentAsString(), "$.id")))); @@ -228,7 +231,8 @@ public void createTest() throws Exception { .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) .andExpect(jsonPath("$", allOf( matchNotifyService(idRef.get(), "service name", "service description", - "https://service.ldn.org/about", "https://service.ldn.org/inbox", false), + "https://service.ldn.org/about", "https://service.ldn.org/inbox", false, + "192.168.0.1", "192.168.0.5"), hasJsonPath("$.notifyServiceInboundPatterns", containsInAnyOrder( matchNotifyServicePattern("patternA", "itemFilterA", true), matchNotifyServicePattern("patternB", null, false) @@ -2304,4 +2308,185 @@ public void destroy() throws Exception { super.destroy(); } + + @Test + public void notifyServiceLowerIpReplaceOperationBadRequestTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withUrl("https://service.ldn.org/about") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + ReplaceOperation operation = new ReplaceOperation("/lowerIp", "192.168.0.1"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + public void notifyServiceLowerIpReplaceOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .withLowerIp("192.168.0.1") + .withUpperIp("192.168.0.5") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + ReplaceOperation operation = new ReplaceOperation("/lowerIp", "192.168.0.2"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", matchNotifyService(notifyServiceEntity.getID(), "service name", + "service description", "https://service.ldn.org/about", "https://service.ldn.org/inbox", + false, "192.168.0.2", "192.168.0.5")) + ); + } + + @Test + public void notifyServiceLowerIpRemoveOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .withLowerIp("192.168.0.1") + .withUpperIp("192.168.0.5") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + RemoveOperation operation = new RemoveOperation("/lowerIp"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void notifyServiceUpperIpReplaceOperationBadRequestTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withUrl("https://service.ldn.org/about") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + ReplaceOperation operation = new ReplaceOperation("/lowerIp", "192.168.0.8"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + public void notifyServiceUpperIpReplaceOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .withLowerIp("192.168.0.1") + .withUpperIp("192.168.0.5") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + ReplaceOperation operation = new ReplaceOperation("/upperIp", "192.168.0.8"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", matchNotifyService(notifyServiceEntity.getID(), "service name", + "service description", "https://service.ldn.org/about", "https://service.ldn.org/inbox", + false, "192.168.0.1", "192.168.0.8")) + ); + } + + @Test + public void notifyServiceUpperIpRemoveOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .withLowerIp("192.168.0.1") + .withUpperIp("192.168.0.5") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + RemoveOperation operation = new RemoveOperation("/upperIp"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()); + } + } \ No newline at end of file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/NotifyServiceMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/NotifyServiceMatcher.java index 857054aa5259..377ed043e8c9 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/NotifyServiceMatcher.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/NotifyServiceMatcher.java @@ -59,6 +59,21 @@ public static Matcher matchNotifyService(String name, String des ); } + public static Matcher matchNotifyService(String name, String description, String url, + String ldnUrl, boolean enabled, + String lowerIp, String upperIp) { + return allOf( + hasJsonPath("$.name", is(name)), + hasJsonPath("$.description", is(description)), + hasJsonPath("$.url", is(url)), + hasJsonPath("$.ldnUrl", is(ldnUrl)), + hasJsonPath("$.enabled", is(enabled)), + hasJsonPath("$.lowerIp", is(lowerIp)), + hasJsonPath("$.upperIp", is(upperIp)), + hasJsonPath("$._links.self.href", containsString("/api/ldn/ldnservices/")) + ); + } + public static Matcher matchNotifyService(int id, String name, String description, String url, String ldnUrl) { return allOf( @@ -79,6 +94,17 @@ public static Matcher matchNotifyService(int id, String name, St ); } + public static Matcher matchNotifyService(int id, String name, String description, + String url, String ldnUrl, boolean enabled, + String lowerIp, String upperIp) { + return allOf( + hasJsonPath("$.id", is(id)), + matchNotifyService(name, description, url, ldnUrl, enabled, lowerIp, upperIp), + hasJsonPath("$._links.self.href", startsWith(REST_SERVER_URL)), + hasJsonPath("$._links.self.href", endsWith("/api/ldn/ldnservices/" + id)) + ); + } + public static Matcher matchNotifyServiceWithoutLinks( int id, String name, String description, String url, String ldnUrl) { return allOf( diff --git a/dspace/config/modules/coar-notify-ldn.cfg b/dspace/config/modules/coar-notify-ldn.cfg index 3cf3afa02828..11f9abdc8c61 100644 --- a/dspace/config/modules/coar-notify-ldn.cfg +++ b/dspace/config/modules/coar-notify-ldn.cfg @@ -7,3 +7,6 @@ #Boolean to determine if Coar Notify is enabled globally for entire site. #default => true coar-notify.enabled = true + +# For debugging purposes only, skip the check on the IP range. +#coar-notify.ip-range.enabled = false