From 382383ce672aaea5dacac170c202980c85c68262 Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Fri, 18 Oct 2024 17:04:19 +0200 Subject: [PATCH 1/7] Change from Apache Http Client to Reactor Netty. Provide followRedirect to remove Authorization. Fixes #5989 --- .../pom.xml | 4 + ...InsecureS3RequestRedirectStrategyTest.java | 112 ++++++++++ ...OnSignedS3RequestRedirectStrategyTest.java | 2 - .../pom.xml | 4 + .../ContainerImageRestTemplateFactory.java | 115 ++++------ ...rizationHeaderRequestRedirectStrategy.java | 200 ------------------ 6 files changed, 164 insertions(+), 273 deletions(-) create mode 100644 spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnInsecureS3RequestRedirectStrategyTest.java delete mode 100644 spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderRequestRedirectStrategy.java diff --git a/spring-cloud-dataflow-configuration-metadata/pom.xml b/spring-cloud-dataflow-configuration-metadata/pom.xml index 64c96a5128..7082a44ea8 100644 --- a/spring-cloud-dataflow-configuration-metadata/pom.xml +++ b/spring-cloud-dataflow-configuration-metadata/pom.xml @@ -35,6 +35,10 @@ org.springframework spring-web + + io.projectreactor.netty + reactor-netty + com.fasterxml.jackson.core jackson-core diff --git a/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnInsecureS3RequestRedirectStrategyTest.java b/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnInsecureS3RequestRedirectStrategyTest.java new file mode 100644 index 0000000000..d04a30b862 --- /dev/null +++ b/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnInsecureS3RequestRedirectStrategyTest.java @@ -0,0 +1,112 @@ +/* + * Copyright 2020-2020 the original author or authors. + * + * 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 + * + * https://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.springframework.cloud.dataflow.container.registry.authorization; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient; +import org.springframework.cloud.dataflow.configuration.metadata.ApplicationConfigurationMetadataResolverAutoConfiguration; +import org.springframework.cloud.dataflow.configuration.metadata.container.DefaultContainerImageMetadataResolver; +import org.springframework.cloud.dataflow.container.registry.ContainerRegistryAutoConfiguration; +import org.springframework.cloud.dataflow.container.registry.ContainerRegistryConfiguration; +import org.springframework.cloud.dataflow.container.registry.ContainerRegistryProperties; +import org.springframework.cloud.dataflow.container.registry.authorization.support.S3SignedRedirectRequestServerApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; +import org.springframework.test.util.TestSocketUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * @author Adam J. Weigold + * @author Corneil du Plessis + */ +//TODO: Boot3x followup +@Disabled("TODO: Netty HttpClient configuration for both http and https required") +public class DropAuthorizationHeaderOnInsecureS3RequestRedirectStrategyTest { + private final static Logger logger = LoggerFactory.getLogger(DropAuthorizationHeaderOnInsecureS3RequestRedirectStrategyTest.class); + private AnnotationConfigApplicationContext context; + private ConfigurableApplicationContext application; + private static int serverPort; + @BeforeEach + void setup() { + serverPort = TestSocketUtils.findAvailableTcpPort(); + + logger.info("Setting S3 Signed Redirect Server port to " + serverPort); + + this.application = new SpringApplicationBuilder(S3SignedRedirectRequestServerApplication.class).build() + .run("--server.port=" + serverPort); + logger.info("S3 Signed Redirect Server Server is UP! at " + serverPort); + } + @AfterEach + void clean() { + if (context != null) { + context.close(); + } + context = null; + } + + @Test + void redirect() { + context = new AnnotationConfigApplicationContext(TestApplication.class); + + final DefaultContainerImageMetadataResolver imageMetadataResolver = + context.getBean(DefaultContainerImageMetadataResolver.class); + + Map imageLabels = imageMetadataResolver.getImageLabels("localhost:" + + serverPort + "/test/s3-redirect-image:1.0.0"); + + assertThat(imageLabels).containsOnly(entry("foo", "bar")); + } + + @Import({ContainerRegistryAutoConfiguration.class, ApplicationConfigurationMetadataResolverAutoConfiguration.class}) + @AutoConfigureWebClient + static class TestApplication { + @Bean + @Primary + ContainerRegistryProperties containerRegistryProperties() { + ContainerRegistryProperties properties = new ContainerRegistryProperties(); + ContainerRegistryConfiguration registryConfiguration = new ContainerRegistryConfiguration(); + registryConfiguration.setRegistryHost(String.format("localhost:%s", serverPort)); + registryConfiguration.setAuthorizationType(ContainerRegistryConfiguration.AuthorizationType.dockeroauth2); + registryConfiguration.setUser("admin"); + registryConfiguration.setSecret("Harbor12345"); + registryConfiguration.setDisableSslVerification(true); + registryConfiguration.setExtra(Collections.singletonMap( + DockerOAuth2RegistryAuthorizer.DOCKER_REGISTRY_AUTH_URI_KEY, + "http://localhost:" + serverPort + "/service/token")); + properties.setRegistryConfigurations(Collections.singletonMap("goharbor", registryConfiguration)); + + return properties; + } + } +} diff --git a/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnSignedS3RequestRedirectStrategyTest.java b/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnSignedS3RequestRedirectStrategyTest.java index 0b7809b65a..80cfa743e8 100644 --- a/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnSignedS3RequestRedirectStrategyTest.java +++ b/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnSignedS3RequestRedirectStrategyTest.java @@ -42,8 +42,6 @@ * @author Adam J. Weigold * @author Corneil du Plessis */ -//TODO: Boot3x followup -@Disabled("TODO: Boot3x `org.springframework.web.client.HttpClientErrorException$BadRequest: 400 : [no body]` is thrown by REST Template") public class DropAuthorizationHeaderOnSignedS3RequestRedirectStrategyTest { @RegisterExtension public final static S3SignedRedirectRequestServerResource s3SignedRedirectRequestServerResource = diff --git a/spring-cloud-dataflow-container-registry/pom.xml b/spring-cloud-dataflow-container-registry/pom.xml index 8656ce7f43..9bd6153af3 100644 --- a/spring-cloud-dataflow-container-registry/pom.xml +++ b/spring-cloud-dataflow-container-registry/pom.xml @@ -42,6 +42,10 @@ org.springframework spring-web + + io.projectreactor.netty + reactor-netty + com.fasterxml.jackson.core jackson-core diff --git a/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerImageRestTemplateFactory.java b/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerImageRestTemplateFactory.java index 6e6c78099f..4edf496f50 100644 --- a/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerImageRestTemplateFactory.java +++ b/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerImageRestTemplateFactory.java @@ -16,36 +16,26 @@ package org.springframework.cloud.dataflow.container.registry; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; - -import org.apache.hc.client5.http.config.RequestConfig; -import org.apache.hc.client5.http.cookie.StandardCookieSpec; -import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; -import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager; -import org.apache.hc.client5.http.socket.ConnectionSocketFactory; -import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory; -import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; -import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; -import org.apache.hc.core5.http.HttpHost; -import org.apache.hc.core5.http.config.Lookup; -import org.apache.hc.core5.http.config.RegistryBuilder; +import javax.net.ssl.SSLException; + +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import reactor.netty.http.Http11SslContextSpec; +import reactor.netty.http.client.HttpClient; +import reactor.netty.transport.ProxyProvider; import org.springframework.boot.web.client.RestTemplateBuilder; -import org.springframework.cloud.dataflow.container.registry.authorization.DropAuthorizationHeaderRequestRedirectStrategy; import org.springframework.http.MediaType; -import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.ReactorNettyClientRequestFactory; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.client.RestTemplate; @@ -153,67 +143,50 @@ public RestTemplate getContainerRestTemplate(boolean skipSslVerification, boolea } } - private RestTemplate createContainerRestTemplate(boolean skipSslVerification, boolean withHttpProxy, Map extra) - throws NoSuchAlgorithmException, KeyManagementException { + private RestTemplate createContainerRestTemplate(boolean skipSslVerification, boolean withHttpProxy, Map extra) { + HttpClient client = httpClientBuilder(skipSslVerification); + return initRestTemplate(client, withHttpProxy, extra); + } - if (!skipSslVerification) { - // Create a RestTemplate that uses custom request factory - return this.initRestTemplate(HttpClients.custom(), withHttpProxy, extra); + private static void removeAuthorization(HttpHeaders headers) { + for(Map.Entry entry: headers.entries()) { + if(entry.getKey().equalsIgnoreCase(org.springframework.http.HttpHeaders.AUTHORIZATION)) { + headers.remove(entry.getKey()); + break; + } } - - // Trust manager that blindly trusts all SSL certificates. - TrustManager[] trustAllCerts = new TrustManager[] { - new X509TrustManager() { - public java.security.cert.X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0]; - } - - public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) { - } - - public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) { - } - } - }; - SSLContext sslContext = SSLContext.getInstance("SSL"); - // Install trust manager to SSL Context. - sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); - - // Create a RestTemplate that uses custom request factory - return initRestTemplate( - httpClientBuilder(sslContext), - withHttpProxy, - extra); - } - private HttpClientBuilder httpClientBuilder(SSLContext sslContext) { - // Register http/s connection factories - Lookup connSocketFactoryLookup = RegistryBuilder. create() - .register("https", new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE)) - .register("http", new PlainConnectionSocketFactory()) - .build(); - return HttpClients.custom() - .setConnectionManager(new BasicHttpClientConnectionManager(connSocketFactoryLookup)); } - private RestTemplate initRestTemplate(HttpClientBuilder clientBuilder, boolean withHttpProxy, Map extra) { - clientBuilder.setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(StandardCookieSpec.RELAXED).build()); + private HttpClient httpClientBuilder(boolean skipSslVerification) { + + try { + SslContextBuilder builder = skipSslVerification ? SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE) : SslContextBuilder.forClient(); + SslContext sslContext = builder.build(); + HttpClient client = HttpClient.create().secure(sslContextSpec -> sslContextSpec.sslContext(sslContext)); + + return client.followRedirect(true, (entries, httpClientRequest) -> { + HttpHeaders httpHeaders = httpClientRequest.requestHeaders(); + removeAuthorization(httpHeaders); + removeAuthorization(entries); + httpClientRequest.headers(httpHeaders); + }); + } catch (SSLException e) { + throw new RuntimeException(e); + } + + } + private RestTemplate initRestTemplate(HttpClient client, boolean withHttpProxy, Map extra) { // Set the HTTP proxy if configured. if (withHttpProxy) { if (!properties.getHttpProxy().isEnabled()) { throw new ContainerRegistryException("Registry Configuration uses a HttpProxy but non is configured!"); } - HttpHost proxy = new HttpHost(properties.getHttpProxy().getHost(), properties.getHttpProxy().getPort()); - clientBuilder.setProxy(proxy); + ProxyProvider.Builder builder = ProxyProvider.builder().type(ProxyProvider.Proxy.HTTP).host(properties.getHttpProxy().getHost()).port(properties.getHttpProxy().getPort()); + client.proxy(typeSpec -> builder.build()); } - - HttpComponentsClientHttpRequestFactory customRequestFactory = - new HttpComponentsClientHttpRequestFactory( - clientBuilder - .setRedirectStrategy(new DropAuthorizationHeaderRequestRedirectStrategy(extra)) - // Azure redirects may contain double slashes and on default those are normilised - .setDefaultRequestConfig(RequestConfig.custom().build()) - .build()); + // TODO what do we do with extra? + ClientHttpRequestFactory customRequestFactory = new ReactorNettyClientRequestFactory(client); // DockerHub response's media-type is application/octet-stream although the content is in JSON. // Similarly the Github CR response's media-type is always text/plain although the content is in JSON. diff --git a/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderRequestRedirectStrategy.java b/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderRequestRedirectStrategy.java deleted file mode 100644 index e7741b9562..0000000000 --- a/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderRequestRedirectStrategy.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright 2020-2021 the original author or authors. - * - * 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 - * - * https://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.springframework.cloud.dataflow.container.registry.authorization; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Arrays; -import java.util.Map; - -import org.apache.hc.client5.http.classic.methods.HttpGet; -import org.apache.hc.client5.http.classic.methods.HttpHead; -import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; -import org.apache.hc.client5.http.impl.DefaultRedirectStrategy; -import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpException; -import org.apache.hc.core5.http.HttpRequest; -import org.apache.hc.core5.http.HttpResponse; -import org.apache.hc.core5.http.protocol.HttpContext; - -import org.springframework.util.StringUtils; - -/** - * Amazon, Azure and Custom Container Registry services require special treatment for the Authorization headers when the - * HTTP request are forwarded to 3rd party services. - * - * Amazon: - * The Amazon S3 API supports two Authentication Methods (https://amzn.to/2Dg9sga): - * (1) HTTP Authorization header and (2) Query string parameters (often referred to as a pre-signed URL). - * - * But only one auth mechanism is allowed at a time. If the http request contains both an Authorization header and - * an pre-signed URL parameters then an error is thrown. - * - * Container Registries often use AmazonS3 as a backend object store. If HTTP Authorization header - * is used to authenticate with the Container Registry and then this registry redirect the request to a S3 storage - * using pre-signed URL authentication, the redirection will fail. - * - * Solution is to implement a HTTP redirect strategy that removes the original Authorization headers when the request is - * redirected toward an Amazon signed URL. - * - * Azure: - * Azure have same type of issues as S3 so header needs to be dropped as well. - * (https://docs.microsoft.com/en-us/azure/container-registry/container-registry-faq#authentication-information-is-not-given-in-the-correct-format-on-direct-rest-api-calls) - * - * Custom: - * Custom Container Registry may have same type of issues as S3 so header needs to be dropped as well. - * - * @author Adam J. Weigold - * @author Janne Valkealahti - * @author Christian Tzolov - * @author Cheng Guan Poh - */ -public class DropAuthorizationHeaderRequestRedirectStrategy extends DefaultRedirectStrategy { - - private static final String CUSTOM_REGISTRY = "custom-registry"; - - private static final String AMZ_CREDENTIAL = "X-Amz-Credential"; - - private static final String AUTHORIZATION_HEADER = "Authorization"; - - private static final String AZURECR_URI_SUFFIX = "azurecr.io"; - - private static final String BASIC_AUTH = "Basic"; - - /** - * Additional registry specific configuration properties - usually used inside the Registry authorizer - * implementations (eg. the AwsEcrAuthorizer implementation). - */ - private Map extra; - - public DropAuthorizationHeaderRequestRedirectStrategy(Map extra) { - this.extra = extra; - } - - @Override - public URI getLocationURI(final HttpRequest request, final HttpResponse response, - final HttpContext context) throws HttpException { - - URI httpUriRequest = super.getLocationURI(request, response, context); - String query = httpUriRequest.getQuery(); - String method = request.getMethod(); - - // Handle Amazon requests - if (StringUtils.hasText(query) && query.contains(AMZ_CREDENTIAL)) { - if (isHeadOrGetMethod(method)) { - try { - return new DropAuthorizationHeaderHttpRequestBase(httpUriRequest, method).getUri(); - } catch (URISyntaxException e) { - throw new HttpException("Unable to get location URI", e); - } - } - } - - // Handle Azure requests - try { - if (request.getUri().getRawPath().contains(AZURECR_URI_SUFFIX)) { - if (isHeadOrGetMethod(method)) { - return (new DropAuthorizationHeaderHttpRequestBase(httpUriRequest, method) { - // Drop headers only for the Basic Auth and leave unchanged for OAuth2 - @Override - protected boolean isDropHeader(String name, Object value) { - return name.equalsIgnoreCase(AUTHORIZATION_HEADER) && StringUtils.hasText((String) value) && ((String)value).contains(BASIC_AUTH); - } - }).getUri(); - } - } - - - // Handle Custom requests - if (extra.containsKey(CUSTOM_REGISTRY) && request.getUri().getRawPath().contains(extra.get(CUSTOM_REGISTRY))) { - if (isHeadOrGetMethod(method)) { - return new DropAuthorizationHeaderHttpRequestBase(httpUriRequest, method).getUri(); - } - } - } catch (URISyntaxException e) { - throw new HttpException("Unable to get Locaction URI", e); - } - return httpUriRequest; - } - - private boolean isHeadOrGetMethod(String method) { - return StringUtils.hasText(method) - && (method.equalsIgnoreCase(HttpHead.METHOD_NAME) || method.equalsIgnoreCase(HttpGet.METHOD_NAME)); - } - - /** - * Overrides all header setter methods to filter out the Authorization headers. - */ - static class DropAuthorizationHeaderHttpRequestBase extends HttpUriRequestBase { - - private final String method; - - DropAuthorizationHeaderHttpRequestBase(URI uri, String method) { - super(method, uri); - this.method = method; - } - - @Override - public String getMethod() { - return this.method; - } - - @Override - public void addHeader(Header header) { - if (!isDropHeader(header)) { - super.addHeader(header); - } - } - - @Override - public void addHeader(String name, Object value) { - if (!isDropHeader(name, value)) { - super.addHeader(name, value); - } - } - - @Override - public void setHeader(Header header) { - if (!isDropHeader(header)) { - super.setHeader(header); - } - } - - @Override - public void setHeader(String name, Object value) { - if (!isDropHeader(name, value)) { - super.setHeader(name, value); - } - } - - @Override - public void setHeaders(Header[] headers) { - Header[] filteredHeaders = Arrays.stream(headers) - .filter(header -> !isDropHeader(header)) - .toArray(Header[]::new); - super.setHeaders(filteredHeaders); - } - - protected boolean isDropHeader(Header header) { - return isDropHeader(header.getName(), header.getValue()); - } - - protected boolean isDropHeader(String name, Object value) { - return name.equalsIgnoreCase(AUTHORIZATION_HEADER); - } - } -} From a58a39f54944cc7628fa6f36553719ae7623c001 Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Fri, 18 Oct 2024 17:04:38 +0200 Subject: [PATCH 2/7] Change from Apache Http Client to Reactor Netty. Provide followRedirect to remove Authorization. Fixes #5989 --- ...horizationHeaderOnInsecureS3RequestRedirectStrategyTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnInsecureS3RequestRedirectStrategyTest.java b/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnInsecureS3RequestRedirectStrategyTest.java index d04a30b862..4ef3d19ad3 100644 --- a/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnInsecureS3RequestRedirectStrategyTest.java +++ b/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnInsecureS3RequestRedirectStrategyTest.java @@ -23,8 +23,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.RegisterExtension; import org.slf4j.Logger; import org.slf4j.LoggerFactory; From 4c246153b0bc890e091a584ab49c006d1c190b77 Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Mon, 21 Oct 2024 17:01:44 +0200 Subject: [PATCH 3/7] Container Registry Service assumes HTTPS and doesn't require plain HTTP support. --- ...InsecureS3RequestRedirectStrategyTest.java | 110 ------------------ 1 file changed, 110 deletions(-) delete mode 100644 spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnInsecureS3RequestRedirectStrategyTest.java diff --git a/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnInsecureS3RequestRedirectStrategyTest.java b/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnInsecureS3RequestRedirectStrategyTest.java deleted file mode 100644 index 4ef3d19ad3..0000000000 --- a/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnInsecureS3RequestRedirectStrategyTest.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2020-2020 the original author or authors. - * - * 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 - * - * https://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.springframework.cloud.dataflow.container.registry.authorization; - -import java.util.Collections; -import java.util.Map; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient; -import org.springframework.cloud.dataflow.configuration.metadata.ApplicationConfigurationMetadataResolverAutoConfiguration; -import org.springframework.cloud.dataflow.configuration.metadata.container.DefaultContainerImageMetadataResolver; -import org.springframework.cloud.dataflow.container.registry.ContainerRegistryAutoConfiguration; -import org.springframework.cloud.dataflow.container.registry.ContainerRegistryConfiguration; -import org.springframework.cloud.dataflow.container.registry.ContainerRegistryProperties; -import org.springframework.cloud.dataflow.container.registry.authorization.support.S3SignedRedirectRequestServerApplication; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.Primary; -import org.springframework.test.util.TestSocketUtils; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.entry; - -/** - * @author Adam J. Weigold - * @author Corneil du Plessis - */ -//TODO: Boot3x followup -@Disabled("TODO: Netty HttpClient configuration for both http and https required") -public class DropAuthorizationHeaderOnInsecureS3RequestRedirectStrategyTest { - private final static Logger logger = LoggerFactory.getLogger(DropAuthorizationHeaderOnInsecureS3RequestRedirectStrategyTest.class); - private AnnotationConfigApplicationContext context; - private ConfigurableApplicationContext application; - private static int serverPort; - @BeforeEach - void setup() { - serverPort = TestSocketUtils.findAvailableTcpPort(); - - logger.info("Setting S3 Signed Redirect Server port to " + serverPort); - - this.application = new SpringApplicationBuilder(S3SignedRedirectRequestServerApplication.class).build() - .run("--server.port=" + serverPort); - logger.info("S3 Signed Redirect Server Server is UP! at " + serverPort); - } - @AfterEach - void clean() { - if (context != null) { - context.close(); - } - context = null; - } - - @Test - void redirect() { - context = new AnnotationConfigApplicationContext(TestApplication.class); - - final DefaultContainerImageMetadataResolver imageMetadataResolver = - context.getBean(DefaultContainerImageMetadataResolver.class); - - Map imageLabels = imageMetadataResolver.getImageLabels("localhost:" + - serverPort + "/test/s3-redirect-image:1.0.0"); - - assertThat(imageLabels).containsOnly(entry("foo", "bar")); - } - - @Import({ContainerRegistryAutoConfiguration.class, ApplicationConfigurationMetadataResolverAutoConfiguration.class}) - @AutoConfigureWebClient - static class TestApplication { - @Bean - @Primary - ContainerRegistryProperties containerRegistryProperties() { - ContainerRegistryProperties properties = new ContainerRegistryProperties(); - ContainerRegistryConfiguration registryConfiguration = new ContainerRegistryConfiguration(); - registryConfiguration.setRegistryHost(String.format("localhost:%s", serverPort)); - registryConfiguration.setAuthorizationType(ContainerRegistryConfiguration.AuthorizationType.dockeroauth2); - registryConfiguration.setUser("admin"); - registryConfiguration.setSecret("Harbor12345"); - registryConfiguration.setDisableSslVerification(true); - registryConfiguration.setExtra(Collections.singletonMap( - DockerOAuth2RegistryAuthorizer.DOCKER_REGISTRY_AUTH_URI_KEY, - "http://localhost:" + serverPort + "/service/token")); - properties.setRegistryConfigurations(Collections.singletonMap("goharbor", registryConfiguration)); - - return properties; - } - } -} From 4ece24f90e5753d89a8583986b83b5fc084a1151 Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Mon, 28 Oct 2024 14:51:51 +0200 Subject: [PATCH 4/7] Update httpClientBuilder with comment for reason of removing Authorization on redirect. --- .../ContainerImageRestTemplateFactory.java | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerImageRestTemplateFactory.java b/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerImageRestTemplateFactory.java index 4edf496f50..e21e2b4587 100644 --- a/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerImageRestTemplateFactory.java +++ b/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerImageRestTemplateFactory.java @@ -148,15 +148,37 @@ private RestTemplate createContainerRestTemplate(boolean skipSslVerification, bo return initRestTemplate(client, withHttpProxy, extra); } - private static void removeAuthorization(HttpHeaders headers) { - for(Map.Entry entry: headers.entries()) { - if(entry.getKey().equalsIgnoreCase(org.springframework.http.HttpHeaders.AUTHORIZATION)) { - headers.remove(entry.getKey()); - break; - } - } - } - + /** + * Amazon, Azure and Custom Container Registry services require special treatment for the Authorization headers when the + * HTTP request are forwarded to 3rd party services. + * + * Amazon: + * The Amazon S3 API supports two Authentication Methods (https://amzn.to/2Dg9sga): + * (1) HTTP Authorization header and (2) Query string parameters (often referred to as a pre-signed URL). + * + * But only one auth mechanism is allowed at a time. If the http request contains both an Authorization header and + * an pre-signed URL parameters then an error is thrown. + * + * Container Registries often use AmazonS3 as a backend object store. If HTTP Authorization header + * is used to authenticate with the Container Registry and then this registry redirect the request to a S3 storage + * using pre-signed URL authentication, the redirection will fail. + * + * Solution is to implement a HTTP redirect strategy that removes the original Authorization headers when the request is + * redirected toward an Amazon signed URL. + * + * Azure: + * Azure have same type of issues as S3 so header needs to be dropped as well. + * (https://docs.microsoft.com/en-us/azure/container-registry/container-registry-faq#authentication-information-is-not-given-in-the-correct-format-on-direct-rest-api-calls) + * + * Custom: + * Custom Container Registry may have same type of issues as S3 so header needs to be dropped as well. + * + * @author Adam J. Weigold + * @author Janne Valkealahti + * @author Christian Tzolov + * @author Cheng Guan Poh + * @author Corneil du Plessis + */ private HttpClient httpClientBuilder(boolean skipSslVerification) { try { @@ -173,8 +195,17 @@ private HttpClient httpClientBuilder(boolean skipSslVerification) { } catch (SSLException e) { throw new RuntimeException(e); } + } + private static void removeAuthorization(HttpHeaders headers) { + for(Map.Entry entry: headers.entries()) { + if(entry.getKey().equalsIgnoreCase(org.springframework.http.HttpHeaders.AUTHORIZATION)) { + headers.remove(entry.getKey()); + break; + } + } } + private RestTemplate initRestTemplate(HttpClient client, boolean withHttpProxy, Map extra) { // Set the HTTP proxy if configured. From e42c3a2f56e3a1080e330480c4b4ff2c3ae659e8 Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Mon, 28 Oct 2024 18:30:08 +0200 Subject: [PATCH 5/7] Added Manual test that can be used for other registries. --- ...horizationHeaderOnContainerTestManual.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnContainerTestManual.java diff --git a/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnContainerTestManual.java b/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnContainerTestManual.java new file mode 100644 index 0000000000..ee1d49bb56 --- /dev/null +++ b/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnContainerTestManual.java @@ -0,0 +1,82 @@ +/* + * Copyright 2020-2020 the original author or authors. + * + * 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 + * + * https://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.springframework.cloud.dataflow.container.registry.authorization; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient; +import org.springframework.cloud.dataflow.configuration.metadata.ApplicationConfigurationMetadataResolverAutoConfiguration; +import org.springframework.cloud.dataflow.configuration.metadata.container.DefaultContainerImageMetadataResolver; +import org.springframework.cloud.dataflow.container.registry.ContainerRegistryAutoConfiguration; +import org.springframework.cloud.dataflow.container.registry.ContainerRegistryConfiguration; +import org.springframework.cloud.dataflow.container.registry.ContainerRegistryProperties; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Adam J. Weigold + * @author Corneil du Plessis + */ +public class DropAuthorizationHeaderOnContainerTestManual { + + private AnnotationConfigApplicationContext context; + + @AfterEach + void clean() { + if (context != null) { + context.close(); + } + context = null; + } + + @Test + void testContainerImageLabels() { + context = new AnnotationConfigApplicationContext(TestApplication.class); + ContainerRegistryProperties registryProperties = context.getBean(ContainerRegistryProperties.class); + DefaultContainerImageMetadataResolver imageMetadataResolver = context.getBean(DefaultContainerImageMetadataResolver.class); + String imageNameAndTag = "springcloudstream/s3-sink-rabbit:5.0.0"; + Map imageLabels = imageMetadataResolver.getImageLabels(registryProperties.getDefaultRegistryHost() + "/" + imageNameAndTag); + System.out.println("imageLabels:" + imageLabels.keySet()); + assertThat(imageLabels).containsKey("org.springframework.boot.spring-configuration-metadata.json"); + } + + @Import({ContainerRegistryAutoConfiguration.class, ApplicationConfigurationMetadataResolverAutoConfiguration.class}) + @AutoConfigureWebClient + static class TestApplication { + @Bean + @Primary + ContainerRegistryProperties containerRegistryProperties() { + ContainerRegistryProperties properties = new ContainerRegistryProperties(); + ContainerRegistryConfiguration registryConfiguration = new ContainerRegistryConfiguration(); + registryConfiguration.setRegistryHost("registry-1.docker.io"); + registryConfiguration.setAuthorizationType(ContainerRegistryConfiguration.AuthorizationType.dockeroauth2); + registryConfiguration.setUser(""); + registryConfiguration.setSecret(""); + properties.setRegistryConfigurations(Collections.singletonMap("registry-1.docker.io", registryConfiguration)); + + return properties; + } + } +} From 2d22c5feba06f0c5a766ad320f0d15febde71ff2 Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Fri, 1 Nov 2024 10:06:58 +0200 Subject: [PATCH 6/7] Restore logic about how to redirect for specific registries. --- .../ContainerImageRestTemplateFactory.java | 78 +++++++++++++------ 1 file changed, 54 insertions(+), 24 deletions(-) diff --git a/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerImageRestTemplateFactory.java b/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerImageRestTemplateFactory.java index e21e2b4587..f62b4ec4de 100644 --- a/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerImageRestTemplateFactory.java +++ b/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerImageRestTemplateFactory.java @@ -25,11 +25,12 @@ import javax.net.ssl.SSLException; import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; -import reactor.netty.http.Http11SslContextSpec; import reactor.netty.http.client.HttpClient; +import reactor.netty.http.client.HttpClientRequest; import reactor.netty.transport.ProxyProvider; import org.springframework.boot.web.client.RestTemplateBuilder; @@ -39,20 +40,20 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.client.RestTemplate; - /** - * On demand creates a cacheable {@link RestTemplate} instances for the purpose of the Container Registry access. - * Created RestTemplates can be configured to use Http Proxy and/or bypassing the SSL verification. + * On demand creates a cacheable {@link RestTemplate} instances for the purpose of the + * Container Registry access. Created RestTemplates can be configured to use Http Proxy + * and/or bypassing the SSL verification. * - * For configuring a Http Proxy in need to: - * 1. Add http proxy configuration using the spring.cloud.dataflow.container.httpProxy.* properties. - * 2. For every {@link ContainerRegistryConfiguration} that has to interact via the http proxy set the use-http-proxy - * flag to true. For example: - * spring.cloud.dataflow.container.registry-configurations[reg-name].use-http-proxy=ture + * For configuring a Http Proxy in need to: 1. Add http proxy configuration using the + * spring.cloud.dataflow.container.httpProxy.* properties. 2. For every + * {@link ContainerRegistryConfiguration} that has to interact via the http proxy set the + * use-http-proxy flag to true. For example: + * spring.cloud.dataflow.container.registry-configurations[reg-name].use-http-proxy=ture * - * Following example configures the default (e.g. DockerHub) registry to use the HTTP Proxy (my-proxy.test:8080) - * while the dockerhub-mirror and the private-snapshots registry configurations allow direct communication: - * + * Following example configures the default (e.g. DockerHub) registry to use the HTTP + * Proxy (my-proxy.test:8080) while the dockerhub-mirror and the private-snapshots + * registry configurations allow direct communication: * spring: * cloud: * dataflow: @@ -76,6 +77,12 @@ */ public class ContainerImageRestTemplateFactory { + private static final String CUSTOM_REGISTRY = "custom-registry"; + private static final String AMZ_CREDENTIAL = "X-Amz-Credential"; + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String AZURECR_URI_SUFFIX = "azurecr.io"; + private static final String BASIC_AUTH = "Basic"; + private final RestTemplateBuilder restTemplateBuilder; private final ContainerRegistryProperties properties; @@ -144,7 +151,7 @@ public RestTemplate getContainerRestTemplate(boolean skipSslVerification, boolea } private RestTemplate createContainerRestTemplate(boolean skipSslVerification, boolean withHttpProxy, Map extra) { - HttpClient client = httpClientBuilder(skipSslVerification); + HttpClient client = httpClientBuilder(skipSslVerification, extra); return initRestTemplate(client, withHttpProxy, extra); } @@ -179,22 +186,45 @@ private RestTemplate createContainerRestTemplate(boolean skipSslVerification, bo * @author Cheng Guan Poh * @author Corneil du Plessis */ - private HttpClient httpClientBuilder(boolean skipSslVerification) { + private HttpClient httpClientBuilder(boolean skipSslVerification, Map extra) { - try { - SslContextBuilder builder = skipSslVerification ? SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE) : SslContextBuilder.forClient(); + try { + SslContextBuilder builder = skipSslVerification + ? SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE) + : SslContextBuilder.forClient(); SslContext sslContext = builder.build(); HttpClient client = HttpClient.create().secure(sslContextSpec -> sslContextSpec.sslContext(sslContext)); return client.followRedirect(true, (entries, httpClientRequest) -> { - HttpHeaders httpHeaders = httpClientRequest.requestHeaders(); - removeAuthorization(httpHeaders); - removeAuthorization(entries); - httpClientRequest.headers(httpHeaders); - }); - } catch (SSLException e) { - throw new RuntimeException(e); - } + if (shouldRemoveAuthorization(httpClientRequest, extra)) { + HttpHeaders httpHeaders = httpClientRequest.requestHeaders(); + removeAuthorization(httpHeaders); + removeAuthorization(entries); + httpClientRequest.headers(httpHeaders); + } + }); + } + catch (SSLException e) { + throw new RuntimeException(e); + } + } + + private boolean shouldRemoveAuthorization(HttpClientRequest request, Map extra) { + HttpMethod method = request.method(); + if(method.equals(HttpMethod.GET) || method.equals(HttpMethod.HEAD)) { + if (request.uri().contains(AMZ_CREDENTIAL)) { + return true; + } + if (request.uri().contains(AZURECR_URI_SUFFIX)) { + return request.requestHeaders() + .entries() + .stream() + .anyMatch(entry -> entry.getKey().equalsIgnoreCase(AUTHORIZATION_HEADER) + && entry.getValue().contains(BASIC_AUTH)); + } + return extra.containsKey(CUSTOM_REGISTRY) && request.uri().contains(extra.get(CUSTOM_REGISTRY)); + } + return false; } private static void removeAuthorization(HttpHeaders headers) { From 2fb769969abb8ebbf1ff9002bbc9add3ab4dc568 Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Thu, 7 Nov 2024 13:28:20 +0200 Subject: [PATCH 7/7] Updated DropAuthorizationHeaderOnContainerTestManual with documentation on using for a manual test. Updated ContainerImageRestTemplateFactory with documentation on extra and improved formatting. Refactored CacheKey in ContainerImageRestTemplateFactory to be a Java Record and include extra to ensure correct behaviour for RestTemplate obtained. --- ...horizationHeaderOnContainerTestManual.java | 34 +++-- .../ContainerImageRestTemplateFactory.java | 136 ++++++++---------- 2 files changed, 82 insertions(+), 88 deletions(-) diff --git a/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnContainerTestManual.java b/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnContainerTestManual.java index ee1d49bb56..13e6aa32a5 100644 --- a/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnContainerTestManual.java +++ b/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnContainerTestManual.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2020 the original author or authors. + * Copyright 2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,11 +36,26 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * @author Adam J. Weigold + * This test is aimed at performing a manual test against a deployed container registry; + * In order to invoke this test populate the fields of DropAuthorizationHeaderOnContainerTestManual.TestApplication + * named registryDomainName, registryUser, registrySecret and imageNameAndTag + * + * The image should be one built with spring-boot:build-image or paketo so that is has a label named 'org.springframework.boot.version' + * For docker hub use: + * registryDomainName="registry-1.docker.io", + * registryUser="docker user" + * registrySecret="docker access token" + * imageNameAndTag="springcloudstream/s3-sink-rabbit:5.0.0" + * * @author Corneil du Plessis */ public class DropAuthorizationHeaderOnContainerTestManual { + private static final String registryDomainName = "registry-1.docker.io"; + private static final String registryUser = ""; + private static final String registrySecret = ""; + private static final String imageNameAndTag = "springcloudstream/s3-sink-rabbit:5.0.0"; + private AnnotationConfigApplicationContext context; @AfterEach @@ -54,27 +69,26 @@ void clean() { @Test void testContainerImageLabels() { context = new AnnotationConfigApplicationContext(TestApplication.class); - ContainerRegistryProperties registryProperties = context.getBean(ContainerRegistryProperties.class); DefaultContainerImageMetadataResolver imageMetadataResolver = context.getBean(DefaultContainerImageMetadataResolver.class); - String imageNameAndTag = "springcloudstream/s3-sink-rabbit:5.0.0"; - Map imageLabels = imageMetadataResolver.getImageLabels(registryProperties.getDefaultRegistryHost() + "/" + imageNameAndTag); + Map imageLabels = imageMetadataResolver.getImageLabels(registryDomainName + "/" + imageNameAndTag); System.out.println("imageLabels:" + imageLabels.keySet()); - assertThat(imageLabels).containsKey("org.springframework.boot.spring-configuration-metadata.json"); + assertThat(imageLabels).containsKey("org.springframework.boot.version"); } @Import({ContainerRegistryAutoConfiguration.class, ApplicationConfigurationMetadataResolverAutoConfiguration.class}) @AutoConfigureWebClient static class TestApplication { + @Bean @Primary ContainerRegistryProperties containerRegistryProperties() { ContainerRegistryProperties properties = new ContainerRegistryProperties(); ContainerRegistryConfiguration registryConfiguration = new ContainerRegistryConfiguration(); - registryConfiguration.setRegistryHost("registry-1.docker.io"); + registryConfiguration.setRegistryHost(registryDomainName); registryConfiguration.setAuthorizationType(ContainerRegistryConfiguration.AuthorizationType.dockeroauth2); - registryConfiguration.setUser(""); - registryConfiguration.setSecret(""); - properties.setRegistryConfigurations(Collections.singletonMap("registry-1.docker.io", registryConfiguration)); + registryConfiguration.setUser(registryUser); + registryConfiguration.setSecret(registrySecret); + properties.setRegistryConfigurations(Collections.singletonMap(registryDomainName, registryConfiguration)); return properties; } diff --git a/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerImageRestTemplateFactory.java b/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerImageRestTemplateFactory.java index f62b4ec4de..a4c8c21813 100644 --- a/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerImageRestTemplateFactory.java +++ b/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerImageRestTemplateFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,11 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.Map; -import java.util.Objects; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; import javax.net.ssl.SSLException; @@ -41,19 +43,18 @@ import org.springframework.web.client.RestTemplate; /** - * On demand creates a cacheable {@link RestTemplate} instances for the purpose of the - * Container Registry access. Created RestTemplates can be configured to use Http Proxy - * and/or bypassing the SSL verification. + * On demand creates a cacheable {@link RestTemplate} instances for the purpose of the Container Registry access. + * Created RestTemplates can be configured to use Http Proxy and/or bypassing the SSL verification. * - * For configuring a Http Proxy in need to: 1. Add http proxy configuration using the - * spring.cloud.dataflow.container.httpProxy.* properties. 2. For every - * {@link ContainerRegistryConfiguration} that has to interact via the http proxy set the - * use-http-proxy flag to true. For example: - * spring.cloud.dataflow.container.registry-configurations[reg-name].use-http-proxy=ture + * For configuring a Http Proxy in need to: + * 1. Add http proxy configuration using the spring.cloud.dataflow.container.httpProxy.* properties. + * 2. For every {@link ContainerRegistryConfiguration} that has to interact via the http proxy set the use-http-proxy + * flag to true. For example: + * spring.cloud.dataflow.container.registry-configurations[reg-name].use-http-proxy=ture * - * Following example configures the default (e.g. DockerHub) registry to use the HTTP - * Proxy (my-proxy.test:8080) while the dockerhub-mirror and the private-snapshots - * registry configurations allow direct communication: + * Following example configures the default (e.g. DockerHub) registry to use the HTTP Proxy (my-proxy.test:8080) + * while the dockerhub-mirror and the private-snapshots registry configurations allow direct communication: + * * spring: * cloud: * dataflow: @@ -74,6 +75,7 @@ * * @author Christian Tzolov * @author Cheng Guan Poh + * @author Corneil du Plessis */ public class ContainerImageRestTemplateFactory { @@ -88,41 +90,16 @@ public class ContainerImageRestTemplateFactory { private final ContainerRegistryProperties properties; /** - * Depends on the disablesSslVerification and useHttpProxy a 4 different RestTemplate configurations might be + * Depends on the skipSslVerification and withHttpProxy and extra map with multiple configurations might be * used at the same time for interacting with different container registries. - * The cache map allows reusing the RestTemplates for given useHttpProxy and disablesSslVerification combination. + * The cache map allows reusing the RestTemplates for given withHttpProxy and skipSslVerification and extra map combination. */ private final ConcurrentHashMap restTemplateCache; /** - * Unique key for any useHttpProxy and disablesSslVerification combination. + * Unique key for any withHttpProxy and skipSslVerification combination. */ - private static class CacheKey { - private final boolean disablesSslVerification; - private final boolean useHttpProxy; - - public CacheKey(boolean disablesSslVerification, boolean useHttpProxy) { - this.disablesSslVerification = disablesSslVerification; - this.useHttpProxy = useHttpProxy; - } - - static CacheKey of(boolean disablesSslVerification, boolean useHttpProxy) { - return new CacheKey(disablesSslVerification, useHttpProxy); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - CacheKey cacheKey = (CacheKey) o; - return disablesSslVerification == cacheKey.disablesSslVerification && useHttpProxy == cacheKey.useHttpProxy; - } - - @Override - public int hashCode() { - return Objects.hash(disablesSslVerification, useHttpProxy); - } - } + record CacheKey(boolean skipSslVerification, boolean withHttpProxy, Map extra) {}; public ContainerImageRestTemplateFactory(RestTemplateBuilder restTemplateBuilder, ContainerRegistryProperties properties) { this.restTemplateBuilder = restTemplateBuilder; @@ -130,23 +107,30 @@ public ContainerImageRestTemplateFactory(RestTemplateBuilder restTemplateBuilder this.restTemplateCache = new ConcurrentHashMap(); } + /** + * Obtain a configured RestTemplate for interacting with container registry. + * @param skipSslVerification indicates we want to trust all certificates. + * @param withHttpProxy indicates we want to use configured proxy. + * @return A configured RestTemplate with the given ssl and proxy settings. + */ public RestTemplate getContainerRestTemplate(boolean skipSslVerification, boolean withHttpProxy) { return this.getContainerRestTemplate(skipSslVerification, withHttpProxy, Collections.emptyMap()); } + /** + * Obtain a configured RestTemplate for interacting with container registry. + * @param skipSslVerification indicates that we want to trust all certificates. + * @param withHttpProxy indicates we want to use the configure proxy host and port. + * @param extra by adding entry custom-registry=registry-domain we expect to remove Authorization headers. + * @return A configured RestTemplate with the given ssl and proxy and extra settings. + */ public RestTemplate getContainerRestTemplate(boolean skipSslVerification, boolean withHttpProxy, Map extra) { + var cacheKey = new CacheKey(skipSslVerification, withHttpProxy, new HashMap<>(extra)); try { - CacheKey cacheKey = CacheKey.of(skipSslVerification, withHttpProxy); - if (!this.restTemplateCache.containsKey(cacheKey)) { - RestTemplate restTemplate = createContainerRestTemplate(skipSslVerification, withHttpProxy, extra); - this.restTemplateCache.putIfAbsent(cacheKey, restTemplate); - } - return this.restTemplateCache.get(cacheKey); + return this.restTemplateCache.computeIfAbsent(cacheKey, (key) -> createContainerRestTemplate(key.skipSslVerification(), key.withHttpProxy(), key.extra())); } catch (Exception e) { - throw new ContainerRegistryException( - "Failed to create Container Image RestTemplate for disableSsl:" - + skipSslVerification + ", httpProxy:" + withHttpProxy, e); + throw new ContainerRegistryException("Failed to create Container Image RestTemplate for disableSsl:" + skipSslVerification + ", httpProxy:" + withHttpProxy, e); } } @@ -179,12 +163,6 @@ private RestTemplate createContainerRestTemplate(boolean skipSslVerification, bo * * Custom: * Custom Container Registry may have same type of issues as S3 so header needs to be dropped as well. - * - * @author Adam J. Weigold - * @author Janne Valkealahti - * @author Christian Tzolov - * @author Cheng Guan Poh - * @author Corneil du Plessis */ private HttpClient httpClientBuilder(boolean skipSslVerification, Map extra) { @@ -211,29 +189,27 @@ private HttpClient httpClientBuilder(boolean skipSslVerification, Map extra) { HttpMethod method = request.method(); - if(method.equals(HttpMethod.GET) || method.equals(HttpMethod.HEAD)) { - if (request.uri().contains(AMZ_CREDENTIAL)) { - return true; - } - if (request.uri().contains(AZURECR_URI_SUFFIX)) { - return request.requestHeaders() - .entries() - .stream() - .anyMatch(entry -> entry.getKey().equalsIgnoreCase(AUTHORIZATION_HEADER) - && entry.getValue().contains(BASIC_AUTH)); - } - return extra.containsKey(CUSTOM_REGISTRY) && request.uri().contains(extra.get(CUSTOM_REGISTRY)); + if(!method.equals(HttpMethod.GET) && !method.equals(HttpMethod.HEAD)) { + return false; } - return false; + if (request.uri().contains(AMZ_CREDENTIAL)) { + return true; + } + if (request.uri().contains(AZURECR_URI_SUFFIX)) { + return request.requestHeaders() + .entries() + .stream() + .anyMatch(entry -> entry.getKey().equalsIgnoreCase(AUTHORIZATION_HEADER) + && entry.getValue().contains(BASIC_AUTH)); + } + return extra.containsKey(CUSTOM_REGISTRY) && request.uri().contains(extra.get(CUSTOM_REGISTRY)); } private static void removeAuthorization(HttpHeaders headers) { - for(Map.Entry entry: headers.entries()) { - if(entry.getKey().equalsIgnoreCase(org.springframework.http.HttpHeaders.AUTHORIZATION)) { - headers.remove(entry.getKey()); - break; - } - } + Set authHeaders = headers.entries() + .stream() + .filter(entry -> entry.getKey().equalsIgnoreCase(AUTHORIZATION_HEADER)).map(Map.Entry::getKey).collect(Collectors.toSet()); + authHeaders.forEach(authHeader -> headers.remove(authHeader)); } private RestTemplate initRestTemplate(HttpClient client, boolean withHttpProxy, Map extra) { @@ -243,10 +219,13 @@ private RestTemplate initRestTemplate(HttpClient client, boolean withHttpProxy, if (!properties.getHttpProxy().isEnabled()) { throw new ContainerRegistryException("Registry Configuration uses a HttpProxy but non is configured!"); } - ProxyProvider.Builder builder = ProxyProvider.builder().type(ProxyProvider.Proxy.HTTP).host(properties.getHttpProxy().getHost()).port(properties.getHttpProxy().getPort()); + ProxyProvider.Builder builder = ProxyProvider.builder() + .type(ProxyProvider.Proxy.HTTP) + .host(properties.getHttpProxy().getHost()) + .port(properties.getHttpProxy().getPort()); client.proxy(typeSpec -> builder.build()); } - // TODO what do we do with extra? + ClientHttpRequestFactory customRequestFactory = new ReactorNettyClientRequestFactory(client); // DockerHub response's media-type is application/octet-stream although the content is in JSON. @@ -255,6 +234,7 @@ private RestTemplate initRestTemplate(HttpClient client, boolean withHttpProxy, // include application/octet-stream and text/plain. MappingJackson2HttpMessageConverter octetSupportJsonConverter = new MappingJackson2HttpMessageConverter(); ArrayList mediaTypeList = new ArrayList(octetSupportJsonConverter.getSupportedMediaTypes()); + mediaTypeList.add(MediaType.APPLICATION_JSON); mediaTypeList.add(MediaType.APPLICATION_OCTET_STREAM); mediaTypeList.add(MediaType.TEXT_PLAIN); octetSupportJsonConverter.setSupportedMediaTypes(mediaTypeList);