From 9f04806c54e30526e229f4f66fdb8671f8d53097 Mon Sep 17 00:00:00 2001 From: Enrico Vianello Date: Wed, 21 Jun 2023 15:37:34 +0200 Subject: [PATCH] Add WLCG Tape REST API discovery mechanism (#31) --- .../storm-webdav.service.d/storm-webdav.conf | 4 ++ .../ServiceConfigurationProperties.java | 41 +++++++++++++ .../webdav/spring/web/SecurityConfig.java | 2 +- .../tape/WlcgTapeRestApiController.java | 46 ++++++++++++++ .../webdav/tape/model/WlcgTapeRestApi.java | 53 ++++++++++++++++ .../tape/model/WlcgTapeRestApiEndpoint.java | 52 ++++++++++++++++ .../tape/service/WlcgTapeRestApiService.java | 61 +++++++++++++++++++ src/main/resources/application-dev.yml | 5 +- src/main/resources/application.yml | 4 ++ .../test/tape/DisabledEndpointTest.java | 47 ++++++++++++++ .../webdav/test/tape/EnabledEndpointTest.java | 61 +++++++++++++++++++ .../test/tape/MalformedEndpointTest.java | 47 ++++++++++++++ .../malformed-wlcg-tape-rest-api.json | 6 ++ .../well-known/wlcg-tape-rest-api.json | 13 ++++ 14 files changed, 440 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/italiangrid/storm/webdav/tape/WlcgTapeRestApiController.java create mode 100644 src/main/java/org/italiangrid/storm/webdav/tape/model/WlcgTapeRestApi.java create mode 100644 src/main/java/org/italiangrid/storm/webdav/tape/model/WlcgTapeRestApiEndpoint.java create mode 100644 src/main/java/org/italiangrid/storm/webdav/tape/service/WlcgTapeRestApiService.java create mode 100644 src/test/java/org/italiangrid/storm/webdav/test/tape/DisabledEndpointTest.java create mode 100644 src/test/java/org/italiangrid/storm/webdav/test/tape/EnabledEndpointTest.java create mode 100644 src/test/java/org/italiangrid/storm/webdav/test/tape/MalformedEndpointTest.java create mode 100644 src/test/resources/well-known/malformed-wlcg-tape-rest-api.json create mode 100644 src/test/resources/well-known/wlcg-tape-rest-api.json diff --git a/etc/systemd/system/storm-webdav.service.d/storm-webdav.conf b/etc/systemd/system/storm-webdav.service.d/storm-webdav.conf index bd57bef3..f173bd26 100644 --- a/etc/systemd/system/storm-webdav.service.d/storm-webdav.conf +++ b/etc/systemd/system/storm-webdav.service.d/storm-webdav.conf @@ -123,3 +123,7 @@ Environment="STORM_WEBDAV_TPC_MAX_CONNECTIONS_PER_ROUTE=25" # Default: false # Set to 'true' if you want to enable HTTP/2 (and remember to enable conscrypt too!) # Environment="STORM_WEBDAV_ENABLE_HTTP2=false" + +# Source file for the tape REST API well-known endpoint +# Default: '/etc/storm/webdav/wlcg-tape-rest-api.json' +# Environment="STORM_WEBDAV_TAPE_WELLKNOWN_SOURCE=/etc/storm/webdav/wlcg-tape-rest-api.json" diff --git a/src/main/java/org/italiangrid/storm/webdav/config/ServiceConfigurationProperties.java b/src/main/java/org/italiangrid/storm/webdav/config/ServiceConfigurationProperties.java index 964d0515..b6e09321 100644 --- a/src/main/java/org/italiangrid/storm/webdav/config/ServiceConfigurationProperties.java +++ b/src/main/java/org/italiangrid/storm/webdav/config/ServiceConfigurationProperties.java @@ -38,6 +38,35 @@ @Validated public class ServiceConfigurationProperties implements ServiceConfiguration { + @Validated + public static class TapeProperties { + + @Validated + public static class TapeWellKnownProperties { + + @NotEmpty + String source; + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + } + + TapeWellKnownProperties wellKnown; + + public TapeWellKnownProperties getWellKnown() { + return wellKnown; + } + + public void setWellKnown(TapeWellKnownProperties wellKnown) { + this.wellKnown = wellKnown; + } + } + public enum ChecksumStrategy { NO_CHECKSUM, EARLY, LATE } @@ -590,6 +619,8 @@ public void setTrustStore(VOMSTrustStoreProperties trustStore) { private RedirectorProperties redirector; + private TapeProperties tape; + @NotEmpty private List hostnames; @@ -846,4 +877,14 @@ public void setRedirector(RedirectorProperties redirector) { public String getTlsProtocol() { return getTls().getProtocol(); } + + + public TapeProperties getTape() { + return tape; + } + + + public void setTape(TapeProperties tape) { + this.tape = tape; + } } diff --git a/src/main/java/org/italiangrid/storm/webdav/spring/web/SecurityConfig.java b/src/main/java/org/italiangrid/storm/webdav/spring/web/SecurityConfig.java index aa79a20a..1ce8148b 100644 --- a/src/main/java/org/italiangrid/storm/webdav/spring/web/SecurityConfig.java +++ b/src/main/java/org/italiangrid/storm/webdav/spring/web/SecurityConfig.java @@ -164,7 +164,7 @@ SecurityFilterChain filterChain(HttpSecurity http, VOMSAuthenticationProvider vo http.authorizeRequests() .antMatchers(HttpMethod.GET, "/.well-known/oauth-authorization-server", - "/.well-known/openid-configuration") + "/.well-known/openid-configuration", "/.well-known/wlcg-tape-rest-api") .permitAll(); AccessDeniedHandlerImpl handler = new AccessDeniedHandlerImpl(); diff --git a/src/main/java/org/italiangrid/storm/webdav/tape/WlcgTapeRestApiController.java b/src/main/java/org/italiangrid/storm/webdav/tape/WlcgTapeRestApiController.java new file mode 100644 index 00000000..ed355ef9 --- /dev/null +++ b/src/main/java/org/italiangrid/storm/webdav/tape/WlcgTapeRestApiController.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.italiangrid.storm.webdav.tape; + +import static org.springframework.http.HttpStatus.NOT_FOUND; + +import org.italiangrid.storm.webdav.tape.model.WlcgTapeRestApi; +import org.italiangrid.storm.webdav.tape.service.WlcgTapeRestApiService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +@RestController +public class WlcgTapeRestApiController { + + private final WlcgTapeRestApiService service; + + public WlcgTapeRestApiController(WlcgTapeRestApiService service) { + this.service = service; + } + + @GetMapping({".well-known/wlcg-tape-rest-api"}) + public WlcgTapeRestApi getMetadata() { + + WlcgTapeRestApi metadata = service.getMetadata(); + if (metadata == null) { + throw new ResponseStatusException(NOT_FOUND, "Unable to find resource"); + } + return metadata; + } + +} diff --git a/src/main/java/org/italiangrid/storm/webdav/tape/model/WlcgTapeRestApi.java b/src/main/java/org/italiangrid/storm/webdav/tape/model/WlcgTapeRestApi.java new file mode 100644 index 00000000..c3557680 --- /dev/null +++ b/src/main/java/org/italiangrid/storm/webdav/tape/model/WlcgTapeRestApi.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.italiangrid.storm.webdav.tape.model; + +import java.util.List; + +import com.google.common.collect.Lists; + +public class WlcgTapeRestApi { + + private String sitename; + private String description; + private List endpoints = Lists.newArrayList(); + + public String getSitename() { + return sitename; + } + + public void setSitename(String sitename) { + this.sitename = sitename; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public List getEndpoints() { + return endpoints; + } + + public void setEndpoints(List endpoints) { + this.endpoints = endpoints; + } + +} diff --git a/src/main/java/org/italiangrid/storm/webdav/tape/model/WlcgTapeRestApiEndpoint.java b/src/main/java/org/italiangrid/storm/webdav/tape/model/WlcgTapeRestApiEndpoint.java new file mode 100644 index 00000000..1fc5d1d9 --- /dev/null +++ b/src/main/java/org/italiangrid/storm/webdav/tape/model/WlcgTapeRestApiEndpoint.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.italiangrid.storm.webdav.tape.model; + +import java.net.URI; +import java.util.Map; + +public class WlcgTapeRestApiEndpoint { + + private URI uri; + private String version; + private Map metadata; + + public URI getUri() { + return uri; + } + + public void setUri(URI uri) { + this.uri = uri; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + +} \ No newline at end of file diff --git a/src/main/java/org/italiangrid/storm/webdav/tape/service/WlcgTapeRestApiService.java b/src/main/java/org/italiangrid/storm/webdav/tape/service/WlcgTapeRestApiService.java new file mode 100644 index 00000000..44158ba3 --- /dev/null +++ b/src/main/java/org/italiangrid/storm/webdav/tape/service/WlcgTapeRestApiService.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.italiangrid.storm.webdav.tape.service; + +import java.io.File; +import java.io.IOException; + +import org.italiangrid.storm.webdav.config.ServiceConfigurationProperties; +import org.italiangrid.storm.webdav.tape.model.WlcgTapeRestApi; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.databind.ObjectMapper; + +@Component +public class WlcgTapeRestApiService { + + public static final Logger LOG = LoggerFactory.getLogger(WlcgTapeRestApiService.class); + + private static final String LOG_INFO_LOADING = "Loading WLCG Tape REST API well-known endpoint from file '{}' ..."; + private static final String LOG_ERROR_PREFIX = "Error loading WLCG Tape REST API well-known endpoint from file: {}"; + private static final String LOG_INFO_NOFILEFOUND = "No WLCG Tape REST API well-known file found at '{}'"; + + private WlcgTapeRestApi metadata; + + public WlcgTapeRestApiService(ServiceConfigurationProperties props) { + + metadata = null; + File source = new File(props.getTape().getWellKnown().getSource()); + if (source.exists()) { + LOG.info(LOG_INFO_LOADING, source); + try { + metadata = (new ObjectMapper()).readValue(source, WlcgTapeRestApi.class); + } catch (IOException e) { + LOG.error(LOG_ERROR_PREFIX, e.getMessage()); + } + } else { + LOG.info(LOG_INFO_NOFILEFOUND, source); + } + } + + public WlcgTapeRestApi getMetadata() { + return metadata; + } + +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index f0d6c340..6b1c7356 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -18,4 +18,7 @@ storm: enabled: true voms: trust-store: - dir: src/test/resources/vomsdir \ No newline at end of file + dir: src/test/resources/vomsdir + tape: + well-known: + source: src/test/resources/well-known/wlcg-tape-rest-api.json diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 6d8d7b39..6eaaa236 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -155,3 +155,7 @@ storm: cache: enabled: ${STORM_WEBDAV_VOMS_CACHE_ENABLE:true} entry-lifetime-sec: ${STORM_WEBDAV_VOMS_CACHE_ENTRY_LIFETIME_SEC:300} + + tape: + well-known: + source: ${STORM_WEBDAV_TAPE_WELLKNOWN_SOURCE:/etc/storm/webdav/wlcg-tape-rest-api.json} \ No newline at end of file diff --git a/src/test/java/org/italiangrid/storm/webdav/test/tape/DisabledEndpointTest.java b/src/test/java/org/italiangrid/storm/webdav/test/tape/DisabledEndpointTest.java new file mode 100644 index 00000000..c1a955c0 --- /dev/null +++ b/src/test/java/org/italiangrid/storm/webdav/test/tape/DisabledEndpointTest.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.italiangrid.storm.webdav.test.tape; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.test.context.support.WithAnonymousUser; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +@RunWith(SpringRunner.class) +@AutoConfigureMockMvc +@ActiveProfiles({"dev"}) +@SpringBootTest(properties = { "storm.tape.well-known.source=not-a-file" }) +@WithAnonymousUser +class DisabledEndpointTest { + + @Autowired + MockMvc mvc; + + @Test + void testDisabledWellKnown() throws Exception { + mvc.perform(get("/.well-known/wlcg-tape-rest-api")) + .andExpect(status().isNotFound()); + } +} diff --git a/src/test/java/org/italiangrid/storm/webdav/test/tape/EnabledEndpointTest.java b/src/test/java/org/italiangrid/storm/webdav/test/tape/EnabledEndpointTest.java new file mode 100644 index 00000000..cc4cd99f --- /dev/null +++ b/src/test/java/org/italiangrid/storm/webdav/test/tape/EnabledEndpointTest.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.italiangrid.storm.webdav.test.tape; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.test.context.support.WithAnonymousUser; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles({"dev"}) +@WithAnonymousUser +class EnabledEndpointTest { + + @Autowired + MockMvc mvc; + + @Test + void testEnabledWellKnown() throws Exception { + mvc.perform(get("/.well-known/wlcg-tape-rest-api")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sitename").exists()) + .andExpect(jsonPath("$.sitename").value(is("StoRM@CNAF"))) + .andExpect(jsonPath("$.description").exists()) + .andExpect(jsonPath("$.description").value(is("This is the tape REST API endpoint for CNAF-T1"))) + .andExpect(jsonPath("$.endpoints").exists()) + .andExpect(jsonPath("$.endpoints").isArray()) + .andExpect(jsonPath("$.endpoints").isNotEmpty()) + .andExpect(jsonPath("$.endpoints[0].uri").value(is("https://storm-tape.example.org:8443/api/v1"))) + .andExpect(jsonPath("$.endpoints[0].version").value(is("v1"))) + .andExpect(jsonPath("$.endpoints[0].metadata").isMap()) + .andExpect(jsonPath("$.endpoints[0].metadata['test']").exists()) + .andExpect(jsonPath("$.endpoints[0].metadata['test']").value(is("test"))); + } +} diff --git a/src/test/java/org/italiangrid/storm/webdav/test/tape/MalformedEndpointTest.java b/src/test/java/org/italiangrid/storm/webdav/test/tape/MalformedEndpointTest.java new file mode 100644 index 00000000..ab4b96b9 --- /dev/null +++ b/src/test/java/org/italiangrid/storm/webdav/test/tape/MalformedEndpointTest.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.italiangrid.storm.webdav.test.tape; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.test.context.support.WithAnonymousUser; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +@RunWith(SpringRunner.class) +@AutoConfigureMockMvc +@ActiveProfiles({"dev"}) +@SpringBootTest(properties = { "storm.tape.well-known.source=src/test/resources/well-known/malformed-wlcg-tape-rest-api.json" }) +@WithAnonymousUser +class MalformedEndpointTest { + + @Autowired + MockMvc mvc; + + @Test + void testMalformedWellKnown() throws Exception { + mvc.perform(get("/.well-known/wlcg-tape-rest-api")) + .andExpect(status().isNotFound()); + } +} diff --git a/src/test/resources/well-known/malformed-wlcg-tape-rest-api.json b/src/test/resources/well-known/malformed-wlcg-tape-rest-api.json new file mode 100644 index 00000000..1fc72b72 --- /dev/null +++ b/src/test/resources/well-known/malformed-wlcg-tape-rest-api.json @@ -0,0 +1,6 @@ +{ + "site": "StoRM@CNAF", + "desc": "This is the tape REST API endpoint for CNAF-T1", + "endp": [ + ] +} \ No newline at end of file diff --git a/src/test/resources/well-known/wlcg-tape-rest-api.json b/src/test/resources/well-known/wlcg-tape-rest-api.json new file mode 100644 index 00000000..f2e9173d --- /dev/null +++ b/src/test/resources/well-known/wlcg-tape-rest-api.json @@ -0,0 +1,13 @@ +{ + "sitename": "StoRM@CNAF", + "description": "This is the tape REST API endpoint for CNAF-T1", + "endpoints": [ + { + "uri": "https://storm-tape.example.org:8443/api/v1", + "version": "v1", + "metadata": { + "test": "test" + } + } + ] +} \ No newline at end of file