Default to FALSE
+ *
+ * @deprecated a static final variable does prevents change during runtime and hinders testing. Use
+ * {@link #isAllowEnvParametrization()} instead.
*/
+ @Deprecated(forRemoval = true)
public static final boolean ALLOW_ENV_PARAMETRIZATION =
- Boolean.valueOf(GeoWebCacheExtensions.getProperty("ALLOW_ENV_PARAMETRIZATION"));
+ Boolean.parseBoolean(GeoWebCacheExtensions.getProperty("ALLOW_ENV_PARAMETRIZATION"));
private static final String nullValue = "null";
@@ -67,10 +70,17 @@ public class GeoWebCacheEnvironment {
constants.asString("DEFAULT_VALUE_SEPARATOR"),
true);
- private final PlaceholderResolver resolver = placeholderName -> resolvePlaceholder(placeholderName);
-
private Properties props;
+ /**
+ * Determines if the {@code ALLOW_ENV_PARAMETRIZATION} environment variable is set to {@code true} and hence
+ * variable variable substitution of configuration parameters using ${}
place holders can be performed
+ * through {@link #resolveValue(Object)}.
+ */
+ public boolean isAllowEnvParametrization() {
+ return Boolean.parseBoolean(GeoWebCacheExtensions.getProperty("ALLOW_ENV_PARAMETRIZATION"));
+ }
+
/**
* Internal "props" getter method.
*
@@ -107,7 +117,7 @@ protected String resolveSystemProperty(String key) {
value = System.getenv(key);
}
return value;
- } catch (Throwable ex) {
+ } catch (RuntimeException ex) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Could not access system property '" + key + "': " + ex);
}
@@ -116,7 +126,7 @@ protected String resolveSystemProperty(String key) {
}
protected String resolveStringValue(String strVal) throws BeansException {
- String resolved = this.helper.replacePlaceholders(strVal, this.resolver);
+ String resolved = this.helper.replacePlaceholders(strVal, this::resolvePlaceholder);
return (resolved.equals(nullValue) ? null : resolved);
}
@@ -127,19 +137,17 @@ protected String resolveStringValue(String strVal) throws BeansException {
*
The method first looks for System variables which take precedence on local ones, then into internal props
* injected through the applicationContext.
*/
- public Object resolveValue(Object value) {
- if (value != null) {
- if (value instanceof String) {
- return resolveStringValue((String) value);
- }
+ @SuppressWarnings("unchecked")
+ public HTTP Basic Auth username and password supplied to the constructor support environment parametrization.
+ *
+ * @see GeoWebCacheEnvironment#isAllowEnvParametrization()
+ * @see GeoWebCacheEnvironment#resolveValue(Object)
+ */
public class WMSHttpHelper extends WMSSourceHelper {
private static final Logger log = Logging.getLogger(WMSHttpHelper.class.getName());
+ /**
+ * Used by {@link #getResolvedHttpUsername()} and {@link #getResolvedHttpPassword()} to
+ * {@link GeoWebCacheEnvironment#resolveValue resolve} the actual values against environment variables when
+ * {@link GeoWebCacheEnvironment#isAllowEnvParametrization() ALLOW_ENV_PARAMETRIZATION} is enabled.
+ */
+ protected GeoWebCacheEnvironment gwcEnv;
+
private final URL proxyUrl;
+ /**
+ * HTTP Basic Auth username, might be de-referenced using environment variable substitution. Always access it
+ * through {@link #getResolvedHttpUsername()}
+ */
private final String httpUsername;
+ /**
+ * HTTP Basic Auth password, might be de-referenced using environment variable substitution. Always access it
+ * through {@link #getResolvedHttpUsername()}
+ */
private final String httpPassword;
- protected volatile HttpClient client;
+ protected HttpClient client;
public WMSHttpHelper() {
this(null, null, null);
@@ -71,23 +97,75 @@ public WMSHttpHelper(String httpUsername, String httpPassword, URL proxyUrl) {
this.proxyUrl = proxyUrl;
}
- HttpClient getHttpClient() {
- if (client == null) {
- synchronized (this) {
- if (client != null) {
- return client;
- }
+ /**
+ * Used by {@link #executeRequest}
+ *
+ * @return the actual http username to use when executing requests
+ */
+ public String getResolvedHttpUsername() {
+ return resolvePlaceHolders(httpUsername);
+ }
+
+ /**
+ * Used by {@link #executeRequest}
+ *
+ * @return the actual http password to use when executing requests
+ */
+ public String getResolvedHttpPassword() {
+ return resolvePlaceHolders(httpPassword);
+ }
+
+ /**
+ * Assigns the environment variable {@link GeoWebCacheEnvironment#resolveValue resolver} to perform variable
+ * substitution against the configured http username and password during {@link #executeRequest}
+ *
+ * When unset, a bean of this type will be looked up through {@link GeoWebCacheExtensions#bean(Class)}
+ */
+ public void setGeoWebCacheEnvironment(GeoWebCacheEnvironment gwcEnv) {
+ this.gwcEnv = gwcEnv;
+ }
- HttpClientBuilder builder = new HttpClientBuilder(
- null, getBackendTimeout(), httpUsername, httpPassword, proxyUrl, getConcurrency());
+ private String resolvePlaceHolders(String value) {
+ GeoWebCacheEnvironment env = getEnvironment();
+ return env != null && env.isAllowEnvParametrization() ? env.resolveValue(value) : value;
+ }
- client = builder.buildClient();
- }
+ private GeoWebCacheEnvironment getEnvironment() {
+ GeoWebCacheEnvironment env = this.gwcEnv;
+ if (env == null) {
+ env = GeoWebCacheExtensions.bean(GeoWebCacheEnvironment.class);
+ this.gwcEnv = env;
}
+ return env;
+ }
+ /**
+ * Note: synchronizing the entire method avoids double-checked locking altogether. Modern JVMs optimize this and
+ * reduce the overhead of synchronization. Otherwise PMD complains with a `Double checked locking is not thread safe
+ * in Java.` error.
+ */
+ synchronized HttpClient getHttpClient() {
+ if (client == null) {
+ int backendTimeout = getBackendTimeout();
+ String user = getResolvedHttpUsername();
+ String password = getResolvedHttpPassword();
+ URL proxy = proxyUrl;
+ int concurrency = getConcurrency();
+ client = buildHttpClient(backendTimeout, user, password, proxy, concurrency);
+ }
return client;
}
+ @VisibleForTesting
+ HttpClient buildHttpClient(int backendTimeout, String username, String password, URL proxy, int concurrency) {
+
+ URL serverUrl = null;
+ HttpClientBuilder builder =
+ new HttpClientBuilder(serverUrl, backendTimeout, username, password, proxy, concurrency);
+
+ return builder.buildClient();
+ }
+
/** Loops over the different backends, tries the request */
@Override
protected void makeRequest(
@@ -319,7 +397,13 @@ public HttpResponse executeRequest(
if (log.isLoggable(Level.FINER)) {
log.finer(method.toString());
}
- return getHttpClient().execute(method);
+ HttpClient httpClient = getHttpClient();
+ return execute(httpClient, method);
+ }
+
+ @VisibleForTesting
+ HttpResponse execute(HttpClient httpClient, HttpRequestBase method) throws IOException, ClientProtocolException {
+ return httpClient.execute(method);
}
private String processRequestParameters(Map This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with this program. If not, see
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with this program. If not, see
+ *
+ *
+ */
+ @Before
+ public void setUp() throws URISyntaxException, GeoWebCacheException {
+
+ setEnvironmentVariablesForCredentials();
+
+ xmlConfig = loadConfig();
+ withDefaultCredentials = getLayer("default_credentials");
+ withEnvVariableCredentials = getLayer("env_var_credentials");
+ withCustomCredentials = getLayer("custom_credentials");
+ customCredentialsWithEnvPrefix = getLayer("custom_credentials_with_env_prefix");
+ }
+
+ private void setEnvironmentVariablesForCredentials() {
+ environmentVariables.set("DEFAULT_USER", "default_user_value");
+ environmentVariables.set("DEFAULT_SECRET", "default_secret_value");
+ environmentVariables.set("CUSTOM_USER", "custom_user_value");
+ environmentVariables.set("CUSTOM_SECRET", "custom_secret_value");
+ }
+
+ private void enableEnvParametrization() {
+ environmentVariables.set("ALLOW_ENV_PARAMETRIZATION", "true");
+ }
+
+ private WMSLayer getLayer(String layerName) {
+ return (WMSLayer) xmlConfig.getLayer(layerName).orElseThrow();
+ }
+
+ private XMLConfiguration loadConfig() throws GeoWebCacheException, URISyntaxException {
+ File cpDirectory =
+ new File(requireNonNull(this.getClass().getResource("./")).toURI());
+ XMLFileResourceProvider resourceProvider = new XMLFileResourceProvider(
+ "geowebcache-config-env.xml", (WebApplicationContext) null, cpDirectory.getAbsolutePath(), null);
+
+ XMLConfiguration config = new XMLConfiguration(null, resourceProvider);
+ config.setGridSetBroker(new GridSetBroker(List.of(new DefaultGridsets(true, true))));
+ config.afterPropertiesSet();
+ return config;
+ }
+
+ @Test
+ public void testLayerWithDefaultCredentialsAllowEnvDisabled() {
+ assertCredentials(withDefaultCredentials, null, null, "layer loading shall not change the layer config");
+
+ xmlConfig.setDefaultValues(withDefaultCredentials);
+ assertCredentials(
+ withDefaultCredentials, null, null, "setting default values shall not change the layer config");
+
+ assertCredentials(
+ withDefaultCredentials.getSourceHelper(),
+ "${DEFAULT_USER}",
+ "${DEFAULT_SECRET}",
+ "sourceHelper should be assigned the default user and password, no env var substitution expected");
+ }
+
+ @Test
+ public void testLayerWithDefaultCredentialsAllowEnvEnabled() {
+ enableEnvParametrization();
+
+ assertCredentials(withDefaultCredentials, null, null, "layer loading shall not change the layer config");
+
+ xmlConfig.setDefaultValues(withDefaultCredentials);
+ assertCredentials(
+ withDefaultCredentials, null, null, "setting default values shall not change the layer config");
+
+ assertCredentials(
+ withDefaultCredentials.getSourceHelper(),
+ "default_user_value",
+ "default_secret_value",
+ "sourceHelper should have resolved the global user and password");
+ }
+
+ @Test
+ public void testLayerWithEnvVarCredentialsAllowEnvDisabled() {
+ assertCredentials(
+ withEnvVariableCredentials,
+ "${CUSTOM_USER}",
+ "${CUSTOM_SECRET}",
+ "layer loading shall not change the layer config");
+
+ xmlConfig.setDefaultValues(withEnvVariableCredentials);
+ assertCredentials(
+ withEnvVariableCredentials,
+ "${CUSTOM_USER}",
+ "${CUSTOM_SECRET}",
+ "setting default values shall not change the layer config");
+
+ assertCredentials(
+ withEnvVariableCredentials.getSourceHelper(),
+ "${CUSTOM_USER}",
+ "${CUSTOM_SECRET}",
+ "sourceHelper should keep the layer user and password, no env var substitution expected");
+ }
+
+ @Test
+ public void testLayerWithEnvVarCredentialsAllowEnvEnabled() {
+ enableEnvParametrization();
+
+ assertCredentials(
+ withEnvVariableCredentials,
+ "${CUSTOM_USER}",
+ "${CUSTOM_SECRET}",
+ "layer loading shall not change the layer config");
+
+ xmlConfig.setDefaultValues(withEnvVariableCredentials);
+ assertCredentials(
+ withEnvVariableCredentials,
+ "${CUSTOM_USER}",
+ "${CUSTOM_SECRET}",
+ "setting default values shall not change the layer config");
+
+ assertCredentials(
+ withEnvVariableCredentials.getSourceHelper(),
+ "custom_user_value",
+ "custom_secret_value",
+ "sourceHelper should have resolved the layer's user and password variables");
+ }
+
+ @Test
+ public void testLayerWithCustomCredentialsIsAllowEnvAgnostic() {
+
+ assertLayerUnchanged(withCustomCredentials, "testuser", "testpass");
+
+ enableEnvParametrization();
+
+ assertLayerUnchanged(withCustomCredentials, "testuser", "testpass");
+ }
+
+ @Test
+ public void testLayerWithCustomCredentialsHavingPlaceholderPrefixIsAllowEnvAgnostic() {
+
+ assertLayerUnchanged(customCredentialsWithEnvPrefix, "${user", "pass${word");
+
+ enableEnvParametrization();
+
+ assertLayerUnchanged(customCredentialsWithEnvPrefix, "${user", "pass${word");
+ }
+
+ public void assertLayerUnchanged(WMSLayer staticCredentials, String user, String password) {
+ assertCredentials(
+ staticCredentials,
+ user,
+ password,
+ "parametrization shouldn't affect a non parametrized layer's credentials");
+
+ xmlConfig.setDefaultValues(staticCredentials);
+ assertCredentials(
+ staticCredentials,
+ user,
+ password,
+ "set default values shouldn't affect a non parametrized layer's credenals");
+
+ assertCredentials(
+ (WMSHttpHelper) staticCredentials.getSourceHelper(),
+ user,
+ password,
+ "set default values shouldn't affect a non parametrized layer's credenals");
+
+ WMSHttpHelper sourceHelper = (WMSHttpHelper) staticCredentials.getSourceHelper();
+ assertCredentials(
+ sourceHelper,
+ user,
+ password,
+ "source helper shouldn't not change credential values on a non parametrized layer");
+ }
+
+ private void assertCredentials(
+ WMSSourceHelper wmsSourceHelper, String expectedUser, String expectedPassword, String message) {
+
+ WMSHttpHelper helper = (WMSHttpHelper) wmsSourceHelper;
+ assertEquals(message, expectedUser, helper.getResolvedHttpUsername());
+ assertEquals(message, expectedPassword, helper.getResolvedHttpPassword());
+ }
+
+ private void assertCredentials(WMSLayer layer, String expectedUser, String expectedPassword, String message) {
+ assertEquals(message, expectedUser, layer.getHttpUsername());
+ assertEquals(message, expectedPassword, layer.getHttpPassword());
+ }
+}
diff --git a/geowebcache/core/src/test/java/org/geowebcache/layer/wms/WMSHttpHelperTest.java b/geowebcache/core/src/test/java/org/geowebcache/layer/wms/WMSHttpHelperTest.java
new file mode 100644
index 000000000..7ca14cfb1
--- /dev/null
+++ b/geowebcache/core/src/test/java/org/geowebcache/layer/wms/WMSHttpHelperTest.java
@@ -0,0 +1,104 @@
+/**
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General
+ * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ *