Skip to content

Commit a847c82

Browse files
committed
Fix #29400: allow to customize HTTP headers
1 parent c78cd22 commit a847c82

4 files changed

Lines changed: 167 additions & 18 deletions

File tree

src/main/java/eu/openanalytics/containerproxy/security/WebSecurityConfig.java

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import eu.openanalytics.containerproxy.auth.IAuthenticationBackend;
2525
import eu.openanalytics.containerproxy.auth.UserLogoutHandler;
2626
import eu.openanalytics.containerproxy.util.AppRecoveryFilter;
27+
import eu.openanalytics.containerproxy.util.EnvironmentUtils;
28+
import eu.openanalytics.containerproxy.util.OverridingHeaderWriter;
2729
import org.apache.logging.log4j.LogManager;
2830
import org.apache.logging.log4j.Logger;
2931
import org.springframework.beans.factory.annotation.Autowired;
@@ -43,16 +45,17 @@
4345
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
4446
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
4547
import org.springframework.security.web.csrf.MissingCsrfTokenException;
48+
import org.springframework.security.web.header.Header;
4649
import org.springframework.security.web.header.writers.StaticHeadersWriter;
4750
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
48-
import org.springframework.web.servlet.config.annotation.CorsRegistry;
4951
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
5052

5153
import javax.inject.Inject;
5254
import javax.servlet.ServletException;
5355
import javax.servlet.http.HttpServletRequest;
5456
import javax.servlet.http.HttpServletResponse;
5557
import java.io.IOException;
58+
import java.util.ArrayList;
5659
import java.util.List;
5760

5861
import static eu.openanalytics.containerproxy.ui.TemplateResolverConfig.PROP_CORS_ALLOWED_ORIGINS;
@@ -77,7 +80,12 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
7780

7881
@Autowired(required=false)
7982
private List<ICustomSecurityConfig> customConfigs;
80-
83+
84+
public static final String PROP_DISABLE_NO_SNIFF_HEADER = "proxy.api-security.disable-no-sniff-header";
85+
public static final String PROP_DISABLE_HSTS_HEADER = "proxy.api-security.disable-hsts-header";
86+
public static final String PROP_DISABLE_XSS_PROTECTION_HEADER = "proxy.api-security.disable-xss-protection-header";
87+
public static final String PROP_CUSTOM_HEADERS = "proxy.api-security.custom-headers";
88+
8189
@Override
8290
public void configure(WebSecurity web) {
8391
// web
@@ -110,7 +118,7 @@ private void checkForIncorrectConfiguration(HttpServletRequest request) {
110118

111119
@Override
112120
protected void configure(HttpSecurity http) throws Exception {
113-
if (environment.getProperty(PROP_CORS_ALLOWED_ORIGINS + "[0]") != null) {
121+
if (EnvironmentUtils.readList(environment, PROP_CORS_ALLOWED_ORIGINS) != null) {
114122
// enable cors
115123
http.cors();
116124
}
@@ -136,8 +144,25 @@ public void handle(HttpServletRequest request, HttpServletResponse response, Acc
136144
}
137145
});
138146

139-
// Always set header: X-Content-Type-Options=nosniff
140-
http.headers().contentTypeOptions();
147+
if (environment.getProperty(PROP_DISABLE_NO_SNIFF_HEADER, Boolean.class, false)) {
148+
http.headers().contentTypeOptions().disable();
149+
} else {
150+
// set header: X-Content-Type-Options=nosniff
151+
http.headers().contentTypeOptions();
152+
}
153+
154+
if (environment.getProperty(PROP_DISABLE_XSS_PROTECTION_HEADER, Boolean.class, false)) {
155+
http.headers().xssProtection().disable();
156+
} else {
157+
http.headers().xssProtection();
158+
}
159+
160+
if (environment.getProperty(PROP_DISABLE_HSTS_HEADER, Boolean.class, false)) {
161+
http.headers().httpStrictTransportSecurity().disable();
162+
} else {
163+
http.headers().httpStrictTransportSecurity();
164+
}
165+
141166

142167
String frameOptions = environment.getProperty("server.frame-options", "disable");
143168
switch (frameOptions.toUpperCase()) {
@@ -157,15 +182,19 @@ public void handle(HttpServletRequest request, HttpServletResponse response, Acc
157182
.addHeaderWriter(new StaticHeadersWriter("X-Frame-Options", frameOptions));
158183
}
159184
}
160-
185+
186+
List<Header> headers = getCustomHeaders();
187+
if (!headers.isEmpty()) {
188+
http.headers().addHeaderWriter(new OverridingHeaderWriter(headers));
189+
}
190+
161191
// Allow public access to health endpoint
162192
http.authorizeRequests().antMatchers("/actuator/health").permitAll();
163193
http.authorizeRequests().antMatchers("/actuator/health/readiness").permitAll();
164194
http.authorizeRequests().antMatchers("/actuator/health/liveness").permitAll();
165195
http.authorizeRequests().antMatchers("/actuator/prometheus").permitAll();
166196
http.authorizeRequests().antMatchers("/actuator/recyclable").permitAll();
167-
168-
http.authorizeRequests().antMatchers("/saml/metadata").permitAll();
197+
http.authorizeRequests().antMatchers("/saml/metadata").permitAll();
169198

170199
// Note: call early, before http.authorizeRequests().anyRequest().fullyAuthenticated();
171200
if (customConfigs != null) {
@@ -213,4 +242,24 @@ public AuthenticationManager authenticationManagerBean() throws Exception {
213242
return super.authenticationManagerBean();
214243
}
215244

245+
private List<Header> getCustomHeaders() {
246+
List<Header> headers = new ArrayList<>();
247+
248+
int i = 0;
249+
String headerName = environment.getProperty(String.format(PROP_CUSTOM_HEADERS + "[%d].name", i));
250+
while (headerName != null) {
251+
String headerValue = environment.getProperty(String.format(PROP_CUSTOM_HEADERS + "[%d].value", i));
252+
if (headerValue == null) {
253+
logger.warn("Missing header value for header {}", headerName);
254+
i++;
255+
continue;
256+
}
257+
headers.add(new Header(headerName, headerValue));
258+
i++;
259+
headerName = environment.getProperty(String.format(PROP_CUSTOM_HEADERS + "[%d].name", i));
260+
}
261+
262+
return headers;
263+
}
264+
216265
}

src/main/java/eu/openanalytics/containerproxy/ui/TemplateResolverConfig.java

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@
2020
*/
2121
package eu.openanalytics.containerproxy.ui;
2222

23+
import eu.openanalytics.containerproxy.util.EnvironmentUtils;
24+
import org.springframework.beans.factory.annotation.Value;
2325
import org.springframework.context.annotation.Bean;
2426
import org.springframework.context.annotation.Configuration;
27+
import org.springframework.context.annotation.PropertySource;
2528
import org.springframework.core.env.Environment;
2629
import org.springframework.web.servlet.config.annotation.CorsRegistry;
2730
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
@@ -63,16 +66,8 @@ public FileTemplateResolver templateResolver() {
6366

6467
@Override
6568
public void addCorsMappings(@Nonnull CorsRegistry registry) {
66-
List<String> origins = new ArrayList<>();
67-
int i = 0;
68-
String origin = environment.getProperty(String.format(PROP_CORS_ALLOWED_ORIGINS + "[%d]", i));
69-
while (origin != null) {
70-
origins.add(origin);
71-
i++;
72-
origin = environment.getProperty(String.format(PROP_CORS_ALLOWED_ORIGINS + "[%d]", i));
73-
}
74-
75-
if (origins.size() > 0) {
69+
List<String> origins = EnvironmentUtils.readList(environment, PROP_CORS_ALLOWED_ORIGINS);
70+
if (origins != null) {
7671
registry.addMapping("/**")
7772
.allowCredentials(true)
7873
.allowedOrigins(origins.toArray(new String[0]));
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* ContainerProxy
3+
*
4+
* Copyright (C) 2016-2021 Open Analytics
5+
*
6+
* ===========================================================================
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the Apache License as published by
10+
* The Apache Software Foundation, either version 2 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* Apache License for more details.
17+
*
18+
* You should have received a copy of the Apache License
19+
* along with this program. If not, see <http://www.apache.org/licenses/>
20+
*/
21+
package eu.openanalytics.containerproxy.util;
22+
23+
import org.springframework.core.env.Environment;
24+
25+
import java.util.ArrayList;
26+
import java.util.Collections;
27+
import java.util.List;
28+
29+
public class EnvironmentUtils {
30+
31+
public static List<String> readList(Environment environment, String propertyName) {
32+
String singleValue = environment.getProperty(propertyName);
33+
if (singleValue != null) {
34+
return Collections.singletonList(singleValue);
35+
}
36+
37+
List<String> result = new ArrayList<>();
38+
int i = 0;
39+
String value = environment.getProperty(String.format(propertyName + "[%d]", i));
40+
while (value != null) {
41+
result.add(value);
42+
i++;
43+
value = environment.getProperty(String.format(propertyName + "[%d]", i));
44+
}
45+
46+
if (result.size() == 0) {
47+
return null;
48+
}
49+
50+
return result;
51+
}
52+
53+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* ContainerProxy
3+
*
4+
* Copyright (C) 2016-2021 Open Analytics
5+
*
6+
* ===========================================================================
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the Apache License as published by
10+
* The Apache Software Foundation, either version 2 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* Apache License for more details.
17+
*
18+
* You should have received a copy of the Apache License
19+
* along with this program. If not, see <http://www.apache.org/licenses/>
20+
*/
21+
package eu.openanalytics.containerproxy.util;
22+
23+
import org.springframework.security.web.header.Header;
24+
import org.springframework.security.web.header.HeaderWriter;
25+
import org.springframework.util.Assert;
26+
27+
import javax.servlet.http.HttpServletRequest;
28+
import javax.servlet.http.HttpServletResponse;
29+
import java.util.List;
30+
31+
public class OverridingHeaderWriter implements HeaderWriter {
32+
33+
private final List<Header> headers;
34+
35+
public OverridingHeaderWriter(List<Header> headers) {
36+
Assert.notEmpty(headers, "headers cannot be null or empty");
37+
this.headers = headers;
38+
}
39+
40+
@Override
41+
public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
42+
for (Header header : this.headers) {
43+
response.setHeader(header.getName(), header.getValues().get(0));
44+
}
45+
}
46+
47+
@Override
48+
public String toString() {
49+
return getClass().getName() + " [headers=" + this.headers + "]";
50+
}
51+
52+
}

0 commit comments

Comments
 (0)