From d7f58148231af7be7390486665e3c0a56b7d3041 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 31 Jul 2024 21:18:46 +0200 Subject: [PATCH 1/4] [DSC-1860] Add DSpace property to specify addition rest url --- dspace/config/dspace.cfg | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index fab3a307267..0feeaa3a8eb 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -28,6 +28,10 @@ csvexport.dir = ${dspace.dir}/exports # and is usually "synced" with the "rest" section in the DSpace User Interface's config.*.yml. # It corresponds to the URL that you would type into your browser to access the REST API. dspace.server.url = http://localhost:8080/server +# Additional URL of DSpace backend which could be used by DSpace frontend during SSR execution. +# May require a port number if not using standard ports (80 or 443) +# DO NOT end it with '/'. +dspace.server.ssr.url = # Public URL of DSpace frontend (Angular UI). May require a port number if not using standard ports (80 or 443) # DO NOT end it with '/'. From 1d8fc4967a9eb8b8f1b5df3755d8eb1cc42ea394 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 31 Jul 2024 21:22:03 +0200 Subject: [PATCH 2/4] [DSC-1860] Add check to allow base object uri containing dspace.server.ssr.uri --- .../java/org/dspace/app/rest/utils/Utils.java | 15 ++- .../rest/AuthorizationRestRepositoryIT.java | 127 ++++++++++++++++++ 2 files changed, 140 insertions(+), 2 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java index fb148b8c4eb..b00934081b4 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java @@ -1018,21 +1018,26 @@ public Object getDSpaceAPIObjectFromRest(Context context, BaseObjectRest restObj */ public BaseObjectRest getBaseObjectRestFromUri(Context context, String uri) throws SQLException { String dspaceUrl = configurationService.getProperty("dspace.server.url"); + String dspaceSSRUrl = configurationService.getProperty("dspace.server.ssr.url", ""); // Convert strings to URL objects. // Do this early to check that inputs are well-formed. URL dspaceUrlObject; + URL dspaceUrlSSRObject = null; URL requestUrlObject; try { dspaceUrlObject = new URL(dspaceUrl); requestUrlObject = new URL(uri); + if (StringUtils.isNoneBlank(dspaceSSRUrl)) { + dspaceUrlSSRObject = new URL(dspaceSSRUrl); + } } catch (MalformedURLException ex) { throw new IllegalArgumentException( String.format("Configuration '%s' or request '%s' is malformed", dspaceUrl, uri)); } // Check whether the URI could be valid. - if (!urlIsPrefixOf(dspaceUrl, uri)) { + if (!urlIsPrefixOf(dspaceUrl, uri) && (StringUtils.isBlank(dspaceSSRUrl) || !urlIsPrefixOf(dspaceSSRUrl, uri))) { throw new IllegalArgumentException("the supplied uri is not ours: " + uri); } @@ -1042,10 +1047,16 @@ public BaseObjectRest getBaseObjectRestFromUri(Context context, String uri) thro String[] requestPath = StringUtils.split(requestUrlObject.getPath(), '/'); String[] uriParts = Arrays.copyOfRange(requestPath, dspacePathLength, requestPath.length); + String[] uriSSRParts = new String[0]; + if (StringUtils.isNoneBlank(dspaceSSRUrl) && !Objects.isNull(dspaceUrlSSRObject)) { + int dspaceSSRPathLength = StringUtils.split(dspaceUrlSSRObject.getPath(), '/').length; + uriSSRParts = Arrays.copyOfRange(requestPath, dspaceSSRPathLength, + requestPath.length); + } if ("api".equalsIgnoreCase(uriParts[0])) { uriParts = Arrays.copyOfRange(uriParts, 1, uriParts.length); } - if (uriParts.length != 3) { + if (uriParts.length != 3 && uriSSRParts.length != 3) { throw new IllegalArgumentException("the supplied uri lacks required path elements: " + uri); } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthorizationRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthorizationRestRepositoryIT.java index 6a51649b5bc..10763605295 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthorizationRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthorizationRestRepositoryIT.java @@ -884,6 +884,133 @@ public void findByObjectBadRequestTest() throws Exception { .andExpect(status().isBadRequest()); } + @Test + /** + * Verify that the findByObject return the 400 Bad Request response for invalid or missing URI (required parameter) + * + * @throws Exception + */ + public void findByObjectBadRequestSSRTest() throws Exception { + Site site = siteService.findSite(context); + SiteRest siteRest = siteConverter.convert(site, DefaultProjection.DEFAULT); + String siteUri = "http://ssr.example.com/api/core/sites/" + siteRest.getId(); + String[] invalidUris = new String[] { + "invalid-uri", + "", + "http://localhost/api/wrongcategory/wrongmodel/1", + "http://localhost/api/core/sites/this-is-not-an-uuid" + }; + + // disarm the alwaysThrowExceptionFeature + configurationService.setProperty("org.dspace.app.rest.authorization.AlwaysThrowExceptionFeature.turnoff", true); + configurationService.setProperty("dspace.server.ssr.url", "http://ssr.example.com/api"); + + String adminToken = getAuthToken(admin.getEmail(), password); + String epersonToken = getAuthToken(eperson.getEmail(), password); + + // verify that it works for administrator users + getClient(adminToken).perform(get("/api/authz/authorizations/search/object") + .param("uri", siteUri)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.authorizations", Matchers.hasSize(greaterThanOrEqualTo(1)))) + .andExpect(jsonPath("$._embedded.authorizations", Matchers.everyItem( + Matchers.anyOf( + JsonPathMatchers.hasJsonPath("$.type", is("authorization")), + JsonPathMatchers.hasJsonPath("$._embedded.feature", + Matchers.allOf( + is(alwaysTrue.getName()) + )), + JsonPathMatchers.hasJsonPath("$._embedded.feature", + Matchers.not(Matchers.anyOf( + is(alwaysFalse.getName()), + is(alwaysException.getName()), + is(trueForTestUsers.getName()), + is(trueForAdmins.getName()) + ) + )), + JsonPathMatchers.hasJsonPath("$._embedded.feature.resourcetypes", + Matchers.hasItem(is("authorization"))), + JsonPathMatchers.hasJsonPath("$.id", + Matchers.anyOf( + Matchers.startsWith(eperson.getID().toString()), + Matchers.endsWith(siteRest.getUniqueType() + "_" + siteRest.getId())))) + ) + ) + ) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/authz/authorizations/search/object"))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", greaterThanOrEqualTo(1))); + + // verify that it works for authenticated users + getClient(epersonToken).perform(get("/api/authz/authorizations/search/object") + .param("uri", siteUri)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.authorizations", Matchers.hasSize(greaterThanOrEqualTo(1)))) + .andExpect(jsonPath("$._embedded.authorizations", Matchers.everyItem( + Matchers.anyOf( + JsonPathMatchers.hasJsonPath("$.type", is("authorization")), + JsonPathMatchers.hasJsonPath("$._embedded.feature", + Matchers.allOf( + is(alwaysTrue.getName()) + )), + JsonPathMatchers.hasJsonPath("$._embedded.feature", + Matchers.not(Matchers.anyOf( + is(alwaysFalse.getName()), + is(alwaysException.getName()), + is(trueForTestUsers.getName()), + is(trueForAdmins.getName()) + ) + )), + JsonPathMatchers.hasJsonPath("$._embedded.feature.resourcetypes", + Matchers.hasItem(is("authorization"))), + JsonPathMatchers.hasJsonPath("$.id", + Matchers.anyOf( + Matchers.startsWith(eperson.getID().toString()), + Matchers.endsWith(siteRest.getUniqueType() + "_" + siteRest.getId())))) + ) + ) + ) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/authz/authorizations/search/object"))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", greaterThanOrEqualTo(1))); + + // verify that it works for anonymous users + getClient(epersonToken).perform(get("/api/authz/authorizations/search/object") + .param("uri", siteUri)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.authorizations", Matchers.hasSize(greaterThanOrEqualTo(1)))) + .andExpect(jsonPath("$._embedded.authorizations", Matchers.everyItem( + Matchers.anyOf( + JsonPathMatchers.hasJsonPath("$.type", is("authorization")), + JsonPathMatchers.hasJsonPath("$._embedded.feature", + Matchers.allOf( + is(alwaysTrue.getName()) + )), + JsonPathMatchers.hasJsonPath("$._embedded.feature", + Matchers.not(Matchers.anyOf( + is(alwaysFalse.getName()), + is(alwaysException.getName()), + is(trueForTestUsers.getName()), + is(trueForAdmins.getName()) + ) + )), + JsonPathMatchers.hasJsonPath("$._embedded.feature.resourcetypes", + Matchers.hasItem(is("authorization"))), + JsonPathMatchers.hasJsonPath("$.id", + Matchers.anyOf( + Matchers.startsWith(eperson.getID().toString()), + Matchers.endsWith(siteRest.getUniqueType() + "_" + siteRest.getId())))) + ) + ) + ) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/authz/authorizations/search/object"))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", greaterThanOrEqualTo(1))); + } + @Test /** * Verify that the findByObject return the 401 Unauthorized response when an eperson is involved From 349445c286c76b2d750fbb6febc0de7866050eee Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 1 Aug 2024 09:47:49 +0200 Subject: [PATCH 3/4] [DSC-1860] fix checkstyle --- .../src/main/java/org/dspace/app/rest/utils/Utils.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java index b00934081b4..c48bf14c58a 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java @@ -1037,7 +1037,8 @@ public BaseObjectRest getBaseObjectRestFromUri(Context context, String uri) thro } // Check whether the URI could be valid. - if (!urlIsPrefixOf(dspaceUrl, uri) && (StringUtils.isBlank(dspaceSSRUrl) || !urlIsPrefixOf(dspaceSSRUrl, uri))) { + if (!urlIsPrefixOf(dspaceUrl, uri) && (StringUtils.isBlank(dspaceSSRUrl) || + !urlIsPrefixOf(dspaceSSRUrl, uri))) { throw new IllegalArgumentException("the supplied uri is not ours: " + uri); } From 283f30d39c715167d3ad47c85288b196cb54c5f4 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 1 Aug 2024 15:12:20 +0200 Subject: [PATCH 4/4] [DSC-1860] Add dspace.server.ssr.uri in the local.cfg.EXAMPLE --- dspace/config/local.cfg.EXAMPLE | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dspace/config/local.cfg.EXAMPLE b/dspace/config/local.cfg.EXAMPLE index ba46f939dbd..4cdf4a04eeb 100644 --- a/dspace/config/local.cfg.EXAMPLE +++ b/dspace/config/local.cfg.EXAMPLE @@ -40,6 +40,11 @@ dspace.dir=/dspace # It corresponds to the URL that you would type into your browser to access the REST API. dspace.server.url = http://localhost:8080/server +# Additional URL of DSpace backend which could be used by DSpace frontend during SSR execution. +# May require a port number if not using standard ports (80 or 443) +# DO NOT end it with '/'. +dspace.server.ssr.url = + # Public URL of DSpace frontend (Angular UI). May require a port number if not using standard ports (80 or 443) # DO NOT end it with '/'. # This is used by the backend to provide links in emails, RSS feeds, Sitemaps, etc.