diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index e9ad3bdfe24..ddb15e45fbe 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -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 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/src/main/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncement.java b/src/main/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncement.java index 84f5702cf40..cfeb034782b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncement.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncement.java @@ -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; @@ -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 @@ -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"); } @@ -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. */ @@ -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. */ diff --git a/src/test/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncementTest.java b/src/test/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncementTest.java new file mode 100644 index 00000000000..c0f2de888e8 --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncementTest.java @@ -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("")); + } +}