Skip to content

Commit d28c710

Browse files
committed
[GWC-1363] Support Environment Parametrization for WMSLayer Credentials
This commit enhances security and configurability by enabling dynamic runtime resolution of HTTP Basic Authentication credentials for WMS layers. Credentials can now be injected from environment variables, reducing the need to hardcode sensitive values. This improves code maintainability, supports secure multi- environment deployments, and simplifies testing through dynamic configuration. 1. **Dynamic Environment Parametrization**: - Introduced `GeoWebCacheEnvironment#isAllowEnvParametrization()` to replace the static `ALLOW_ENV_PARAMETRIZATION` field, allowing runtime toggling. 2. **Environment Variable Resolution Refactor**: - Replaced direct static field checks with method calls. - Updated `resolveValue()` and related methods to use environment variables dynamically. 3. **WMS Credentials Management Update**: - Added `getResolvedHttpUsername()` and `getResolvedHttpPassword()` in `WMSHttpHelper`. - Created `setGeoWebCacheEnvironment()` for dependency injection. 4. **Testing Enhancements**: - Integrated the `system-rules` library for environment variable manipulation. - Added tests to cover default, custom, and parameterized credentials. 5. **Code Improvements**: - Replaced unsafe casts in `resolveValue()`. - Improved exception handling by switching from `Throwable` to `RuntimeException`. - Added better logging and documentation for credential handling.
1 parent 9c767e3 commit d28c710

File tree

12 files changed

+567
-43
lines changed

12 files changed

+567
-43
lines changed

geowebcache/core/pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,13 @@
205205
<artifactId>awaitility</artifactId>
206206
<scope>test</scope>
207207
</dependency>
208+
<dependency>
209+
<!-- used for tests that require environment variables -->
210+
<groupId>com.github.stefanbirkner</groupId>
211+
<artifactId>system-rules</artifactId>
212+
<version>1.19.0</version>
213+
<scope>test</scope>
214+
</dependency>
208215

209216
<!-- Thijs Brentjens: for security fixes, OWASP library-->
210217
<dependency>

geowebcache/core/src/main/java/org/geowebcache/GeoWebCacheEnvironment.java

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import org.springframework.beans.factory.config.PlaceholderConfigurerSupport;
2424
import org.springframework.core.Constants;
2525
import org.springframework.util.PropertyPlaceholderHelper;
26-
import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
2726

2827
/**
2928
* Utility class uses to process GeoWebCache configuration workflow through external environment variables.
@@ -46,7 +45,7 @@
4645
public class GeoWebCacheEnvironment {
4746

4847
/** logger */
49-
public static Logger LOGGER = Logging.getLogger(GeoWebCacheEnvironment.class.getName());
48+
public static final Logger LOGGER = Logging.getLogger(GeoWebCacheEnvironment.class.getName());
5049

5150
private static final Constants constants = new Constants(PlaceholderConfigurerSupport.class);
5251

@@ -55,9 +54,13 @@ public class GeoWebCacheEnvironment {
5554
* placeholders translation.
5655
*
5756
* <p>Default to FALSE
57+
*
58+
* @deprecated a static final variable does prevents change during runtime and hinders testing. Use
59+
* {@link #isAllowEnvParametrization()} instead.
5860
*/
61+
@Deprecated(forRemoval = true)
5962
public static final boolean ALLOW_ENV_PARAMETRIZATION =
60-
Boolean.valueOf(GeoWebCacheExtensions.getProperty("ALLOW_ENV_PARAMETRIZATION"));
63+
Boolean.parseBoolean(GeoWebCacheExtensions.getProperty("ALLOW_ENV_PARAMETRIZATION"));
6164

6265
private static final String nullValue = "null";
6366

@@ -67,10 +70,12 @@ public class GeoWebCacheEnvironment {
6770
constants.asString("DEFAULT_VALUE_SEPARATOR"),
6871
true);
6972

70-
private final PlaceholderResolver resolver = placeholderName -> resolvePlaceholder(placeholderName);
71-
7273
private Properties props;
7374

75+
public boolean isAllowEnvParametrization() {
76+
return Boolean.parseBoolean(GeoWebCacheExtensions.getProperty("ALLOW_ENV_PARAMETRIZATION"));
77+
}
78+
7479
/**
7580
* Internal "props" getter method.
7681
*
@@ -107,7 +112,7 @@ protected String resolveSystemProperty(String key) {
107112
value = System.getenv(key);
108113
}
109114
return value;
110-
} catch (Throwable ex) {
115+
} catch (RuntimeException ex) {
111116
if (LOGGER.isLoggable(Level.FINE)) {
112117
LOGGER.fine("Could not access system property '" + key + "': " + ex);
113118
}
@@ -116,7 +121,7 @@ protected String resolveSystemProperty(String key) {
116121
}
117122

118123
protected String resolveStringValue(String strVal) throws BeansException {
119-
String resolved = this.helper.replacePlaceholders(strVal, this.resolver);
124+
String resolved = this.helper.replacePlaceholders(strVal, this::resolvePlaceholder);
120125

121126
return (resolved.equals(nullValue) ? null : resolved);
122127
}
@@ -127,19 +132,17 @@ protected String resolveStringValue(String strVal) throws BeansException {
127132
* <p>The method first looks for System variables which take precedence on local ones, then into internal props
128133
* injected through the applicationContext.
129134
*/
130-
public Object resolveValue(Object value) {
131-
if (value != null) {
132-
if (value instanceof String) {
133-
return resolveStringValue((String) value);
134-
}
135+
@SuppressWarnings("unchecked")
136+
public <T> T resolveValue(T value) {
137+
if (value instanceof String) {
138+
return (T) resolveStringValue((String) value);
135139
}
136140

137141
return value;
138142
}
139143

140144
private String resolveValueIfEnabled(String value) {
141-
if (ALLOW_ENV_PARAMETRIZATION) return (String) resolveValue(value);
142-
else return value;
145+
return isAllowEnvParametrization() ? resolveValue(value) : value;
143146
}
144147

145148
private boolean validateBoolean(String value) {

geowebcache/core/src/main/java/org/geowebcache/config/XMLConfiguration.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import javax.xml.validation.SchemaFactory;
5454
import javax.xml.validation.Validator;
5555
import org.geotools.util.logging.Logging;
56+
import org.geowebcache.GeoWebCacheEnvironment;
5657
import org.geowebcache.GeoWebCacheException;
5758
import org.geowebcache.GeoWebCacheExtensions;
5859
import org.geowebcache.config.ContextualConfigurationProvider.Context;
@@ -282,6 +283,8 @@ public void setDefaultValues(TileLayer layer) {
282283
sourceHelper = new WMSHttpHelper(null, null, proxyUrl);
283284
log.fine("Not using HTTP credentials for " + wl.getName());
284285
}
286+
GeoWebCacheEnvironment gwcEnv = GeoWebCacheExtensions.bean(GeoWebCacheEnvironment.class);
287+
sourceHelper.setGeoWebCacheEnvironment(gwcEnv);
285288

286289
wl.setSourceHelper(sourceHelper);
287290
wl.setLockProvider(getGwcConfig().getLockProvider());

geowebcache/core/src/main/java/org/geowebcache/layer/wms/WMSHttpHelper.java

Lines changed: 93 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,19 @@
2828
import org.apache.commons.io.IOUtils;
2929
import org.apache.http.HttpEntity;
3030
import org.apache.http.HttpResponse;
31+
import org.apache.http.HttpStatus;
3132
import org.apache.http.NameValuePair;
33+
import org.apache.http.client.ClientProtocolException;
3234
import org.apache.http.client.HttpClient;
3335
import org.apache.http.client.methods.HttpGet;
3436
import org.apache.http.client.methods.HttpPost;
3537
import org.apache.http.client.methods.HttpRequestBase;
3638
import org.apache.http.entity.StringEntity;
3739
import org.apache.http.message.BasicNameValuePair;
3840
import org.geotools.util.logging.Logging;
41+
import org.geowebcache.GeoWebCacheEnvironment;
3942
import org.geowebcache.GeoWebCacheException;
43+
import org.geowebcache.GeoWebCacheExtensions;
4044
import org.geowebcache.io.Resource;
4145
import org.geowebcache.layer.TileResponseReceiver;
4246
import org.geowebcache.mime.ErrorMime;
@@ -48,17 +52,39 @@
4852
import org.geowebcache.util.URLs;
4953
import org.springframework.util.Assert;
5054

51-
/** This class is a wrapper for HTTP interaction with WMS backend */
55+
/**
56+
* Helper class for HTTP interactions of {@link WMSLayer} with a WMS backend.
57+
*
58+
* <p>HTTP Basic Auth username and password supplied to the constructor support environment parametrization.
59+
*
60+
* @see GeoWebCacheEnvironment#isAllowEnvParametrization()
61+
* @see GeoWebCacheEnvironment#resolveValue(Object)
62+
*/
5263
public class WMSHttpHelper extends WMSSourceHelper {
5364
private static final Logger log = Logging.getLogger(WMSHttpHelper.class.getName());
5465

66+
/**
67+
* Used by {@link #getResolvedHttpUsername()} and {@link #getResolvedHttpPassword()} to
68+
* {@link GeoWebCacheEnvironment#resolveValue resolve} the actual values against environment variables when
69+
* {@link GeoWebCacheEnvironment#isAllowEnvParametrization() ALLOW_ENV_PARAMETRIZATION} is enabled.
70+
*/
71+
protected GeoWebCacheEnvironment gwcEnv;
72+
5573
private final URL proxyUrl;
5674

75+
/**
76+
* HTTP Basic Auth username, might be de-referenced using environment variable substitution. Always access it
77+
* through {@link #getResolvedHttpUsername()}
78+
*/
5779
private final String httpUsername;
5880

81+
/**
82+
* HTTP Basic Auth password, might be de-referenced using environment variable substitution. Always access it
83+
* through {@link #getResolvedHttpUsername()}
84+
*/
5985
private final String httpPassword;
6086

61-
protected volatile HttpClient client;
87+
protected HttpClient client;
6288

6389
public WMSHttpHelper() {
6490
this(null, null, null);
@@ -71,23 +97,74 @@ public WMSHttpHelper(String httpUsername, String httpPassword, URL proxyUrl) {
7197
this.proxyUrl = proxyUrl;
7298
}
7399

100+
/**
101+
* Used by {@link #executeRequest}
102+
*
103+
* @return the actual http username to use when executing requests
104+
*/
105+
public String getResolvedHttpUsername() {
106+
return resolve(httpUsername);
107+
}
108+
109+
/**
110+
* Used by {@link #executeRequest}
111+
*
112+
* @return the actual http password to use when executing requests
113+
*/
114+
public String getResolvedHttpPassword() {
115+
return resolve(httpPassword);
116+
}
117+
118+
/**
119+
* Assigns the environment variable {@link GeoWebCacheEnvironment#resolveValue resolver} to perform variable
120+
* substitution agains the configured http username and password during {@link #executeRequest}
121+
*
122+
* <p>When unset, a bean of this type will be looked up through {@link GeoWebCacheExtensions#bean(Class)}
123+
*/
124+
public void setGeoWebCacheEnvironment(GeoWebCacheEnvironment gwcEnv) {
125+
this.gwcEnv = gwcEnv;
126+
}
127+
128+
private String resolve(String value) {
129+
GeoWebCacheEnvironment env = getEnvironment();
130+
return env != null && env.isAllowEnvParametrization() ? env.resolveValue(value) : value;
131+
}
132+
133+
private GeoWebCacheEnvironment getEnvironment() {
134+
GeoWebCacheEnvironment env = this.gwcEnv;
135+
if (env == null) {
136+
env = GeoWebCacheExtensions.bean(GeoWebCacheEnvironment.class);
137+
this.gwcEnv = env;
138+
}
139+
return env;
140+
}
141+
74142
HttpClient getHttpClient() {
75143
if (client == null) {
76144
synchronized (this) {
77-
if (client != null) {
78-
return client;
145+
if (client == null) {
146+
int backendTimeout = getBackendTimeout();
147+
String user = getResolvedHttpUsername();
148+
String password = getResolvedHttpPassword();
149+
URL proxy = proxyUrl;
150+
int concurrency = getConcurrency();
151+
client = buildHttpClient(backendTimeout, user, password, proxy, concurrency);
79152
}
80-
81-
HttpClientBuilder builder = new HttpClientBuilder(
82-
null, getBackendTimeout(), httpUsername, httpPassword, proxyUrl, getConcurrency());
83-
84-
client = builder.buildClient();
85153
}
86154
}
87155

88156
return client;
89157
}
90158

159+
HttpClient buildHttpClient(int backendTimeout, String username, String password, URL proxy, int concurrency) {
160+
161+
URL serverUrl = null;
162+
HttpClientBuilder builder =
163+
new HttpClientBuilder(serverUrl, backendTimeout, username, password, proxy, concurrency);
164+
165+
return builder.buildClient();
166+
}
167+
91168
/** Loops over the different backends, tries the request */
92169
@Override
93170
protected void makeRequest(
@@ -235,7 +312,7 @@ private void connectAndCheckHeaders(
235312
}
236313

237314
// Read the actual data
238-
if (responseCode != 204) {
315+
if (responseCode != HttpStatus.SC_NO_CONTENT) {
239316
try (InputStream inStream = method.getEntity().getContent()) {
240317
if (inStream == null) {
241318
log.severe("No response for " + method);
@@ -319,7 +396,12 @@ public HttpResponse executeRequest(
319396
if (log.isLoggable(Level.FINER)) {
320397
log.finer(method.toString());
321398
}
322-
return getHttpClient().execute(method);
399+
HttpClient httpClient = getHttpClient();
400+
return execute(httpClient, method);
401+
}
402+
403+
HttpResponse execute(HttpClient httpClient, HttpRequestBase method) throws IOException, ClientProtocolException {
404+
return httpClient.execute(method);
323405
}
324406

325407
private String processRequestParameters(Map<String, String> parameters) throws UnsupportedEncodingException {

geowebcache/core/src/test/java/org/geowebcache/GeoWebCacheEnvironmentTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public void testEnvironment() {
5858
Assert.assertEquals(1, extensions.size());
5959
Assert.assertTrue(extensions.contains(genv));
6060

61-
Assert.assertTrue(GeoWebCacheEnvironment.ALLOW_ENV_PARAMETRIZATION);
61+
Assert.assertTrue(genv.isAllowEnvParametrization());
6262
}
6363

6464
@Test

0 commit comments

Comments
 (0)