Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/sphinx-guides/source/installation/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3951,8 +3951,10 @@ Linked Data Notifications (LDN) Allowed Hosts
+++++++++++++++++++++++++++++++++++++++++++++

Dataverse supports receiving LDN notifications via the /api/inbox endpoint. The dataverse.ldn.allowed-hosts allows you to specify the list of host IP addresses from which LDN notifications can be received, or ``*`` to receive messages from anywhere.
Note that since the Inbox endpoint does not require authentication, allowing un-trusted hosts via ``*`` is not recommended for production.

Example: ``dataverse.ldn.allowed-hosts=*``
Example: ``dataverse.ldn.allowed-hosts=172.16.234.56,172.16.234.57``

COAR Notify Relationship Announcement Notify Superusers Only
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
import edu.harvard.iq.dataverse.UserNotificationServiceBean;
import edu.harvard.iq.dataverse.authorization.Permission;
import edu.harvard.iq.dataverse.pidproviders.PidProvider;
import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider;
import edu.harvard.iq.dataverse.pidproviders.doi.UnmanagedDOIProvider;
import edu.harvard.iq.dataverse.settings.JvmSettings;
import edu.harvard.iq.dataverse.util.json.JsonLDNamespace;
import edu.harvard.iq.dataverse.util.json.JsonLDTerm;
import edu.harvard.iq.dataverse.util.json.JsonUtil;

Expand Down Expand Up @@ -70,7 +71,6 @@ public COARNotifyRelationshipAnnouncement(
* Process a COAR Notify Relationship Announcement message.
*
* @param msgObject The JSON-LD message object
* @return true if the message was successfully processed, false otherwise
*/
public void processMessage(JsonObject msgObject) {
// Extract subject, object, and relationship from the message
Expand Down Expand Up @@ -168,7 +168,11 @@ private ResourceMetadata retrieveResourceMetadata(String subjectId) {

// Step 3: Retrieve and parse DataCite XML
if (dataciteXmlUrl != null) {
parseDataCiteXml(dataciteXmlUrl, client, metadata);
if (isTrustedDataCiteUrl(dataciteXmlUrl)) {
parseDataCiteXml(dataciteXmlUrl, client, metadata);
} else {
logger.warning("DataCite XML URL is not from a trusted source: " + dataciteXmlUrl);
}
} else {
logger.fine("No DataCite XML URL found in Signposting links");
}
Expand All @@ -189,10 +193,6 @@ private ResourceMetadata retrieveResourceMetadata(String subjectId) {
return metadata;
}

/**
* Extract DataCite XML URL from Signposting Link headers.
*/

/**
* Extract DataCite XML URL from Signposting Link headers.
*/
Expand Down Expand Up @@ -232,6 +232,33 @@ private String extractDataCiteXmlUrl(CloseableHttpResponse headResponse) {
return null;
}

/**
* Validate that the URL is a trusted source for DataCite XML.
* Supports standard DOI resolvers and DataCite API.
*/
boolean isTrustedDataCiteUrl(String url) {
if (url == null || url.isBlank()) {
return false;
}
url = url.toLowerCase();

String doiPart = null;
if (url.startsWith(AbstractDOIProvider.DOI_RESOLVER_URL)) {
doiPart = url.substring(AbstractDOIProvider.DOI_RESOLVER_URL.length());
} else if (url.startsWith(AbstractDOIProvider.HTTP_DOI_RESOLVER_URL)) {
doiPart = url.substring(AbstractDOIProvider.HTTP_DOI_RESOLVER_URL.length());
} else if (url.startsWith(AbstractDOIProvider.DXDOI_RESOLVER_URL)) {
doiPart = url.substring(AbstractDOIProvider.DXDOI_RESOLVER_URL.length());
} else if (url.startsWith(AbstractDOIProvider.HTTP_DXDOI_RESOLVER_URL)) {
doiPart = url.substring(AbstractDOIProvider.HTTP_DXDOI_RESOLVER_URL.length());
}

if (doiPart != null) {
return (new UnmanagedDOIProvider()).parsePersistentId(AbstractDOIProvider.DOI_PROTOCOL, doiPart) != null;
}
return false;
}

/**
* Parse DataCite XML to extract title and resource type.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package edu.harvard.iq.dataverse.api.ldn;

import edu.harvard.iq.dataverse.DatasetServiceBean;
import edu.harvard.iq.dataverse.DataverseRoleServiceBean;
import edu.harvard.iq.dataverse.RoleAssigneeServiceBean;
import edu.harvard.iq.dataverse.UserNotificationServiceBean;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import static org.junit.jupiter.api.Assertions.*;

public class COARNotifyRelationshipAnnouncementTest {

private COARNotifyRelationshipAnnouncement handler;

@BeforeEach
public void setUp() {
DatasetServiceBean datasetService = Mockito.mock(DatasetServiceBean.class);
UserNotificationServiceBean userNotificationService = Mockito.mock(UserNotificationServiceBean.class);
DataverseRoleServiceBean roleService = Mockito.mock(DataverseRoleServiceBean.class);
RoleAssigneeServiceBean roleAssigneeService = Mockito.mock(RoleAssigneeServiceBean.class);
handler = new COARNotifyRelationshipAnnouncement(datasetService, userNotificationService, roleService, roleAssigneeService);
}

@Test
public void testIsTrustedDataCiteUrl() {
// Trusted DOI resolvers
assertTrue(handler.isTrustedDataCiteUrl("https://doi.org/10.7910/DVN/TJCLKP"));
assertTrue(handler.isTrustedDataCiteUrl("http://doi.org/10.7910/DVN/TJCLKP"));
assertTrue(handler.isTrustedDataCiteUrl("https://dx.doi.org/10.7910/DVN/TJCLKP"));
assertTrue(handler.isTrustedDataCiteUrl("http://dx.doi.org/10.7910/DVN/TJCLKP"));

// Invalid DOI
assertFalse(handler.isTrustedDataCiteUrl("https://doi.org/not-a-doi"));


// Untrusted sources
assertFalse(handler.isTrustedDataCiteUrl("https://example.com/metadata.xml"));
assertFalse(handler.isTrustedDataCiteUrl("https://malicious.org/doi.org/10.1234/5678"));

// Null and empty
assertFalse(handler.isTrustedDataCiteUrl(null));
assertFalse(handler.isTrustedDataCiteUrl(""));
}
}
Loading