Skip to content
This repository was archived by the owner on May 14, 2025. It is now read-only.

Commit c3bba95

Browse files
tzolovjvalkeal
authored andcommitted
Add drop auth headers redirect strategy for Azure
- Drop Authorization headers only for Basic auth. - Add support for GitHub Container registry. - Jackson MessageConverter support for text/plain. The Github CR response's media-type is always text/plain although the content is in JSON - Normalize Azure redirect URI - Add Container Registries IT tests - Add SCDF image build configuration - Fix docker compose debug conf for build pack images Resolves #4412
1 parent 472cc4b commit c3bba95

File tree

8 files changed

+145
-23
lines changed

8 files changed

+145
-23
lines changed

spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerImageRestTemplateFactory.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import java.security.NoSuchAlgorithmException;
2121
import java.security.cert.X509Certificate;
2222
import java.util.ArrayList;
23-
import java.util.List;
2423
import java.util.Objects;
2524
import java.util.concurrent.ConcurrentHashMap;
2625

@@ -36,7 +35,7 @@
3635
import org.apache.http.impl.client.HttpClients;
3736

3837
import org.springframework.boot.web.client.RestTemplateBuilder;
39-
import org.springframework.cloud.dataflow.container.registry.authorization.DropAuthorizationHeaderOnSignedS3RequestRedirectStrategy;
38+
import org.springframework.cloud.dataflow.container.registry.authorization.DropAuthorizationHeaderRequestRedirectStrategy;
4039
import org.springframework.http.MediaType;
4140
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
4241
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
@@ -190,14 +189,19 @@ private RestTemplate initRestTemplate(HttpClientBuilder clientBuilder, boolean w
190189
HttpComponentsClientHttpRequestFactory customRequestFactory =
191190
new HttpComponentsClientHttpRequestFactory(
192191
clientBuilder
193-
.setRedirectStrategy(new DropAuthorizationHeaderOnSignedS3RequestRedirectStrategy())
192+
.setRedirectStrategy(new DropAuthorizationHeaderRequestRedirectStrategy())
193+
// Azure redirects may contain double slashes and on default those are normilised
194+
.setDefaultRequestConfig(RequestConfig.custom().setNormalizeUri(false).build())
194195
.build());
195196

196197
// DockerHub response's media-type is application/octet-stream although the content is in JSON.
197-
// Therefore extend the MappingJackson2HttpMessageConverter media-types to include application/octet-stream.
198+
// Similarly the Github CR response's media-type is always text/plain although the content is in JSON.
199+
// Therefore we extend the MappingJackson2HttpMessageConverter media-types to
200+
// include application/octet-stream and text/plain.
198201
MappingJackson2HttpMessageConverter octetSupportJsonConverter = new MappingJackson2HttpMessageConverter();
199-
List<MediaType> mediaTypeList = new ArrayList(octetSupportJsonConverter.getSupportedMediaTypes());
202+
ArrayList<MediaType> mediaTypeList = new ArrayList(octetSupportJsonConverter.getSupportedMediaTypes());
200203
mediaTypeList.add(MediaType.APPLICATION_OCTET_STREAM);
204+
mediaTypeList.add(MediaType.TEXT_PLAIN);
201205
octetSupportJsonConverter.setSupportedMediaTypes(mediaTypeList);
202206

203207
return restTemplateBuilder
Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2020 the original author or authors.
2+
* Copyright 2020-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -32,33 +32,48 @@
3232
import org.apache.http.protocol.HttpContext;
3333

3434
/**
35-
* The Amazon S3 API supports two Authentication Methods (https://amzn.to/2Dg9sga):
36-
* (1) HTTP Authorization header and (2) Query string parameters (often referred to as a pre-signed URL).
35+
* Both Amazon and Azure Container Registry services require special treatment for the Authorization headers when the
36+
* HTTP request are forwarded to 3rd party services.
3737
*
38-
* But only one auth mechanism is allowed at a time. If the http request contains both an Authorization header and
39-
* an pre-signed URL parameters then an error is thrown.
38+
* Amazon:
39+
* The Amazon S3 API supports two Authentication Methods (https://amzn.to/2Dg9sga):
40+
* (1) HTTP Authorization header and (2) Query string parameters (often referred to as a pre-signed URL).
4041
*
41-
* Container Registries often use AmazonS3 as a backend object store. If HTTP Authorization header
42-
* is used to authenticate with the Container Registry and then this registry redirect the request to a S3 storage
43-
* using pre-signed URL authentication, the redirection will fail.
42+
* But only one auth mechanism is allowed at a time. If the http request contains both an Authorization header and
43+
* an pre-signed URL parameters then an error is thrown.
4444
*
45-
* Solution is to implement a HTTP redirect strategy that removes the original Authorization headers when the request is
46-
* redirected toward an Amazon signed URL.
45+
* Container Registries often use AmazonS3 as a backend object store. If HTTP Authorization header
46+
* is used to authenticate with the Container Registry and then this registry redirect the request to a S3 storage
47+
* using pre-signed URL authentication, the redirection will fail.
48+
*
49+
* Solution is to implement a HTTP redirect strategy that removes the original Authorization headers when the request is
50+
* redirected toward an Amazon signed URL.
51+
*
52+
* Azure:
53+
* Azure have same type of issues as S3 so header needs to be dropped as well.
54+
* (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)
4755
*
4856
* @author Adam J. Weigold
57+
* @author Janne Valkealahti
58+
* @author Christian Tzolov
4959
*/
50-
public class DropAuthorizationHeaderOnSignedS3RequestRedirectStrategy extends DefaultRedirectStrategy {
60+
public class DropAuthorizationHeaderRequestRedirectStrategy extends DefaultRedirectStrategy {
5161

5262
private static final String AMZ_CREDENTIAL = "X-Amz-Credential";
5363

5464
private static final String AUTHORIZATION_HEADER = "Authorization";
5565

66+
private static final String AZURECR_URI_SUFFIX = "azurecr.io";
67+
68+
private static final String BASIC_AUTH = "Basic";
69+
5670
@Override
5771
public HttpUriRequest getRedirect(final HttpRequest request, final HttpResponse response,
5872
final HttpContext context) throws ProtocolException {
5973

6074
HttpUriRequest httpUriRequest = super.getRedirect(request, response, context);
6175

76+
// Handle Amazon requests
6277
final String query = httpUriRequest.getURI().getQuery();
6378

6479
if (StringUtils.isNoneEmpty(query) && query.contains(AMZ_CREDENTIAL)) {
@@ -69,6 +84,22 @@ public HttpUriRequest getRedirect(final HttpRequest request, final HttpResponse
6984
}
7085
}
7186

87+
// Handle Azure requests
88+
if (request.getRequestLine().getUri().contains(AZURECR_URI_SUFFIX)) {
89+
final String method = request.getRequestLine().getMethod();
90+
if (StringUtils.isNoneEmpty(method)
91+
&& (method.equalsIgnoreCase(HttpHead.METHOD_NAME) || method.equalsIgnoreCase(HttpGet.METHOD_NAME))) {
92+
return new DropAuthorizationHeaderHttpRequestBase(httpUriRequest.getURI(), method) {
93+
// drop headers only for the Basic Auth and leve them unchanged for OAuth2!
94+
@Override
95+
protected boolean isDropHeader(String name, String value) {
96+
return name.equalsIgnoreCase(AUTHORIZATION_HEADER) && StringUtils.isNoneEmpty(value)
97+
&& value.contains(BASIC_AUTH);
98+
}
99+
};
100+
}
101+
}
102+
72103
return httpUriRequest;
73104
}
74105

@@ -92,38 +123,46 @@ public String getMethod() {
92123

93124
@Override
94125
public void addHeader(Header header) {
95-
if (!header.getName().equalsIgnoreCase(AUTHORIZATION_HEADER)) {
126+
if (!isDropHeader(header)) {
96127
super.addHeader(header);
97128
}
98129
}
99130

100131
@Override
101132
public void addHeader(String name, String value) {
102-
if (!name.equalsIgnoreCase(AUTHORIZATION_HEADER)) {
133+
if (!isDropHeader(name, value)) {
103134
super.addHeader(name, value);
104135
}
105136
}
106137

107138
@Override
108139
public void setHeader(Header header) {
109-
if (!header.getName().equalsIgnoreCase(AUTHORIZATION_HEADER)) {
140+
if (!isDropHeader(header)) {
110141
super.setHeader(header);
111142
}
112143
}
113144

114145
@Override
115146
public void setHeader(String name, String value) {
116-
if (!name.equalsIgnoreCase(AUTHORIZATION_HEADER)) {
147+
if (!isDropHeader(name, value)) {
117148
super.setHeader(name, value);
118149
}
119150
}
120151

121152
@Override
122153
public void setHeaders(Header[] headers) {
123154
Header[] filteredHeaders = Arrays.stream(headers)
124-
.filter(header -> !header.getName().equalsIgnoreCase(AUTHORIZATION_HEADER))
155+
.filter(header -> !isDropHeader(header))
125156
.toArray(Header[]::new);
126157
super.setHeaders(filteredHeaders);
127158
}
159+
160+
protected boolean isDropHeader(Header header) {
161+
return isDropHeader(header.getName(), header.getValue());
162+
}
163+
164+
protected boolean isDropHeader(String name, String value) {
165+
return name.equalsIgnoreCase(AUTHORIZATION_HEADER);
166+
}
128167
}
129168
}

spring-cloud-dataflow-server/pom.xml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,22 @@
165165
</execution>
166166
</executions>
167167
</plugin>
168+
<plugin>
169+
<groupId>org.springframework.boot</groupId>
170+
<artifactId>spring-boot-maven-plugin</artifactId>
171+
<executions>
172+
<execution>
173+
<goals>
174+
<goal>build-image</goal>
175+
</goals>
176+
</execution>
177+
</executions>
178+
<configuration>
179+
<imageName>
180+
springcloud/${project.artifactId}:${project.version}
181+
</imageName>
182+
</configuration>
183+
</plugin>
168184
</plugins>
169185
</build>
170186

spring-cloud-dataflow-server/src/test/java/org/springframework/cloud/dataflow/integration/test/DataFlowIT.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,41 @@ public void applicationMetadataDockerTests() {
288288
dataFlowOperations.appRegistryOperations().unregister("docker-app-with-jar-metadata", ApplicationType.sink);
289289
}
290290

291+
@Test
292+
@EnabledIfSystemProperty(named = "SCDF_CR_TEST", matches="true")
293+
public void githubContainerRegistryTests() {
294+
containerRegistryTests("github-log-sink",
295+
"docker:ghcr.io/tzolov/log-sink-rabbit:3.1.0-SNAPSHOT");
296+
}
297+
298+
@Test
299+
@EnabledIfSystemProperty(named = "SCDF_CR_TEST", matches="true")
300+
public void azureContainerRegistryTests() {
301+
containerRegistryTests("azure-log-sink",
302+
"docker:scdftest.azurecr.io/springcloudstream/log-sink-rabbit:3.1.0-SNAPSHOT");
303+
}
304+
305+
@Test
306+
@EnabledIfSystemProperty(named = "SCDF_CR_TEST", matches="true")
307+
public void harborContainerRegistryTests() {
308+
containerRegistryTests("harbor-log-sink",
309+
"docker:projects.registry.vmware.com/scdf/scdftest/log-sink-rabbit:3.1.0-SNAPSHOT");
310+
}
311+
312+
private void containerRegistryTests(String appName, String appUrl) {
313+
logger.info("application-metadata-" + appName + "-container-registry-test");
314+
315+
// Docker app with container image metadata
316+
dataFlowOperations.appRegistryOperations().register( appName, ApplicationType.sink,
317+
appUrl, null, true);
318+
DetailedAppRegistrationResource dockerAppWithContainerMetadata = dataFlowOperations.appRegistryOperations()
319+
.info(appName, ApplicationType.sink, false);
320+
assertThat(dockerAppWithContainerMetadata.getOptions()).hasSize(3);
321+
322+
// unregister the test apps
323+
dataFlowOperations.appRegistryOperations().unregister(appName, ApplicationType.sink);
324+
}
325+
291326
// -----------------------------------------------------------------------
292327
// PLATFORM TESTS
293328
// -----------------------------------------------------------------------

spring-cloud-dataflow-server/src/test/java/org/springframework/cloud/dataflow/integration/test/util/DockerComposeFactory.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ public class DockerComposeFactory {
110110
.withAdditionalEnvironmentVariable("METADATA_DEFAULT_DOCKERHUB_USER", DockerComposeFactoryProperties.get("METADATA_DEFAULT_DOCKERHUB_USER", ""))
111111
.withAdditionalEnvironmentVariable("METADATA_DEFAULT_DOCKERHUB_PASSWORD", DockerComposeFactoryProperties.get("METADATA_DEFAULT_DOCKERHUB_PASSWORD", ""))
112112
.withAdditionalEnvironmentVariable("COMPOSE_PROJECT_NAME", "scdf")
113+
.withAdditionalEnvironmentVariable("CR_AZURE_USER", DockerComposeFactoryProperties.get("CR_AZURE_USER", ""))
114+
.withAdditionalEnvironmentVariable("CR_AZURE_PASS", DockerComposeFactoryProperties.get("CR_AZURE_PASS", ""))
115+
.withAdditionalEnvironmentVariable("CR_GITHUB_USER", DockerComposeFactoryProperties.get("CR_GITHUB_USER", ""))
116+
.withAdditionalEnvironmentVariable("CR_GITHUB_PASS", DockerComposeFactoryProperties.get("CR_GITHUB_PASS", ""))
117+
.withAdditionalEnvironmentVariable("CR_HARBOR_USER", DockerComposeFactoryProperties.get("CR_HARBOR_USER", ""))
118+
.withAdditionalEnvironmentVariable("CR_HARBOR_PASS", DockerComposeFactoryProperties.get("CR_HARBOR_PASS", ""))
113119
.build();
114120

115121
private static String[] addDockerComposeToPath(String[] dockerComposePaths, String additionalDockerCompose) {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
version: '3'
2+
3+
services:
4+
5+
dataflow-server:
6+
environment:
7+
# AZURE
8+
- spring.cloud.dataflow.container.registry-configurations.azure-registry.registry-host=scdftest.azurecr.io
9+
- spring.cloud.dataflow.container.registry-configurations.azure-registry.authorization-type=basicauth
10+
- spring.cloud.dataflow.container.registry-configurations.azure-registry.user=${CR_AZURE_USER}
11+
- spring.cloud.dataflow.container.registry-configurations.azure-registry.secret=${CR_AZURE_PASS}
12+
# HARBOR
13+
- spring.cloud.dataflow.container.registry-configurations.harbor-registry.registry-host=projects.registry.vmware.com
14+
- spring.cloud.dataflow.container.registry-configurations.harbor-registry.authorization-type=basicauth
15+
# - spring.cloud.dataflow.container.registry-configurations.harbor-registry.authorization-type=dockeroauth2
16+
- spring.cloud.dataflow.container.registry-configurations.harbor-registry.user=${CR_HARBOR_USER}
17+
- spring.cloud.dataflow.container.registry-configurations.harbor-registry.secret=${CR_HARBOR_PASS}
18+
# GITHUB
19+
- spring.cloud.dataflow.container.registry-configurations.github-registry.registry-host=ghcr.io
20+
- spring.cloud.dataflow.container.registry-configurations.github-registry.authorization-type=dockeroauth2
21+
- spring.cloud.dataflow.container.registry-configurations.github-registry.user=${CR_GITHUB_USER}
22+
- spring.cloud.dataflow.container.registry-configurations.github-registry.secret=${CR_GITHUB_PASS}

src/docker-compose/docker-compose-debug-dataflow.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ services:
66
ports:
77
- "5005:5005"
88
environment:
9-
- JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
9+
- JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=0.0.0.0:5005

src/docker-compose/docker-compose-debug-skipper.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ services:
66
ports:
77
- "6006:6006"
88
environment:
9-
- JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:6006
9+
- JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=0.0.0.0:6006

0 commit comments

Comments
 (0)