diff --git a/docs/src/main/asciidoc/spring-cloud-config.adoc b/docs/src/main/asciidoc/spring-cloud-config.adoc
index be58eab5..139e29bf 100644
--- a/docs/src/main/asciidoc/spring-cloud-config.adoc
+++ b/docs/src/main/asciidoc/spring-cloud-config.adoc
@@ -1335,6 +1335,14 @@ So, if you want a profile-specific file, `/\*/development/*/logback.xml` can be
NOTE: If you do not want to supply the `label` and let the server use the default label, you can supply a `useDefaultLabel` request parameter.
So, the preceding example for the `default` profile could be `/foo/default/nginx.conf?useDefaultLabel`.
+=== Decrpyting Plain Text
+
+By default, encrypted values in plain text files are not decrypted. In order to enable decryption for plain text files, set `spring.cloud.config.server.encrypt.enabled=true` and `spring.cloud.config.server.encrypt.plainTextEncrypt=true` in `bootstrap.[yml|properties]`
+
+NOTE: Decrpyting plain text files is only supported for YAML, JSON, and properties file extensions.
+
+If this feature is enabled, and an unsupported file extention is requested, any encrypted values in the file will not be decrypted.
+
== Embedding the Config Server
The Config Server runs best as a standalone application.
diff --git a/spring-cloud-config-server/pom.xml b/spring-cloud-config-server/pom.xml
index 3d88beff..31b4348d 100644
--- a/spring-cloud-config-server/pom.xml
+++ b/spring-cloud-config-server/pom.xml
@@ -70,6 +70,10 @@
org.yaml
snakeyaml
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-yaml
+
org.tmatesoft.svnkit
svnkit
diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerAutoConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerAutoConfiguration.java
index bc664058..281c2d59 100644
--- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerAutoConfiguration.java
+++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerAutoConfiguration.java
@@ -29,7 +29,7 @@ import org.springframework.context.annotation.Import;
@EnableConfigurationProperties(ConfigServerProperties.class)
@Import({ EnvironmentRepositoryConfiguration.class, CompositeConfiguration.class,
ResourceRepositoryConfiguration.class, ConfigServerEncryptionConfiguration.class,
- ConfigServerMvcConfiguration.class })
+ ConfigServerMvcConfiguration.class, ResourceEncryptorConfiguration.class })
public class ConfigServerAutoConfiguration {
}
diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerMvcConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerMvcConfiguration.java
index e77f7ba6..df6dc78e 100644
--- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerMvcConfiguration.java
+++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerMvcConfiguration.java
@@ -16,12 +16,16 @@
package org.springframework.cloud.config.server.config;
+import java.util.HashMap;
+import java.util.Map;
+
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.cloud.config.server.encryption.EnvironmentEncryptor;
+import org.springframework.cloud.config.server.encryption.ResourceEncryptor;
import org.springframework.cloud.config.server.environment.EnvironmentController;
import org.springframework.cloud.config.server.environment.EnvironmentEncryptorEnvironmentRepository;
import org.springframework.cloud.config.server.environment.EnvironmentRepository;
@@ -49,6 +53,9 @@ public class ConfigServerMvcConfiguration implements WebMvcConfigurer {
@Autowired(required = false)
private ObjectMapper objectMapper = new ObjectMapper();
+ @Autowired(required = false)
+ private Map resourceEncryptorMap = new HashMap<>();
+
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.mediaType("properties", MediaType.valueOf("text/plain"));
@@ -72,14 +79,16 @@ public class ConfigServerMvcConfiguration implements WebMvcConfigurer {
public ResourceController resourceController(ResourceRepository repository,
EnvironmentRepository envRepository, ConfigServerProperties server) {
ResourceController controller = new ResourceController(repository,
- encrypted(envRepository, server));
+ encrypted(envRepository, server), this.resourceEncryptorMap);
+ controller.setEncryptEnabled(server.getEncrypt().isEnabled());
+ controller.setPlainTextEncryptEnabled(server.getEncrypt().isPlainTextEncrypt());
return controller;
}
private EnvironmentRepository encrypted(EnvironmentRepository envRepository,
ConfigServerProperties server) {
EnvironmentEncryptorEnvironmentRepository encrypted = new EnvironmentEncryptorEnvironmentRepository(
- envRepository, this.environmentEncryptor);
+ envRepository, environmentEncryptor);
encrypted.setOverrides(server.getOverrides());
return encrypted;
}
diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerProperties.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerProperties.java
index 4a7dbd0a..309f263d 100644
--- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerProperties.java
+++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerProperties.java
@@ -157,6 +157,12 @@ public class ConfigServerProperties {
*/
private boolean enabled = true;
+ /**
+ * Enable decryption of environment properties served by plain text endpoint
+ * {@link org.springframework.cloud.config.server.resource.ResourceController}.
+ */
+ private boolean plainTextEncrypt = false;
+
public boolean isEnabled() {
return this.enabled;
}
@@ -165,6 +171,14 @@ public class ConfigServerProperties {
this.enabled = enabled;
}
+ public boolean isPlainTextEncrypt() {
+ return plainTextEncrypt;
+ }
+
+ public void setPlainTextEncrypt(boolean plainTextEncrypt) {
+ this.plainTextEncrypt = plainTextEncrypt;
+ }
+
}
}
diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ResourceEncryptorConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ResourceEncryptorConfiguration.java
new file mode 100644
index 00000000..50b31189
--- /dev/null
+++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ResourceEncryptorConfiguration.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2002-2019 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.config.server.config;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
+import org.springframework.cloud.config.server.encryption.CipherResourceJsonEncryptor;
+import org.springframework.cloud.config.server.encryption.CipherResourcePropertiesEncryptor;
+import org.springframework.cloud.config.server.encryption.CipherResourceYamlEncryptor;
+import org.springframework.cloud.config.server.encryption.ResourceEncryptor;
+import org.springframework.cloud.config.server.encryption.TextEncryptorLocator;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Adds configuration to decrypt plain text files served through
+ * {@link org.springframework.cloud.config.server.resource.ResourceController}. Each
+ * supported extension is added as a key with its associated @{link
+ * org.springframework.cloud.config.server.encryption.ResourceEncryptor} implementation as
+ * a value.
+ *
+ * @author Sean Stiglitz
+ */
+@Configuration
+@ConditionalOnExpression("${spring.cloud.config.server.encrypt.enabled:true} && ${spring.cloud.config.server.encrypt.plainTextEncrypt:false}")
+public class ResourceEncryptorConfiguration {
+
+ @Autowired
+ private TextEncryptorLocator encryptor;
+
+ @Bean
+ Map resourceEncryptors() {
+ Map resourceEncryptorMap = new HashMap<>();
+ addSupportedExtensionsToMap(resourceEncryptorMap,
+ new CipherResourceJsonEncryptor(encryptor));
+ addSupportedExtensionsToMap(resourceEncryptorMap,
+ new CipherResourcePropertiesEncryptor(encryptor));
+ addSupportedExtensionsToMap(resourceEncryptorMap,
+ new CipherResourceYamlEncryptor(encryptor));
+ return resourceEncryptorMap;
+ }
+
+ private void addSupportedExtensionsToMap(
+ Map resourceEncryptorMap,
+ ResourceEncryptor resourceEncryptor) {
+ for (String ext : resourceEncryptor.getSupportedExtensions()) {
+ resourceEncryptorMap.put(ext, resourceEncryptor);
+ }
+ }
+
+}
diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/AbstractCipherResourceEncryptor.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/AbstractCipherResourceEncryptor.java
new file mode 100644
index 00000000..7847d312
--- /dev/null
+++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/AbstractCipherResourceEncryptor.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2002-2019 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.config.server.encryption;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+
+import org.springframework.cloud.config.environment.Environment;
+import org.springframework.util.StringUtils;
+
+/**
+ * Abstract base class for any @{link
+ * org.springframework.cloud.config.server.encryption.ResourceEncryptor} implementations.
+ * Meant to house shared configuration and logic.
+ *
+ * @author Sean Stiglitz
+ */
+abstract class AbstractCipherResourceEncryptor implements ResourceEncryptor {
+
+ protected final String CIPHER_MARKER = "{cipher}";
+
+ private final TextEncryptorLocator encryptor;
+
+ private EnvironmentPrefixHelper helper = new EnvironmentPrefixHelper();
+
+ AbstractCipherResourceEncryptor(TextEncryptorLocator encryptor) {
+ this.encryptor = encryptor;
+ }
+
+ @Override
+ public abstract List getSupportedExtensions();
+
+ @Override
+ public abstract String decrypt(String text, Environment environment)
+ throws IOException;
+
+ protected String decryptWithJacksonParser(String text, String name, String[] profiles,
+ JsonFactory factory) throws IOException {
+ Set valsToDecrpyt = new HashSet();
+ JsonParser parser = factory.createParser(text);
+ JsonToken token;
+
+ while ((token = parser.nextToken()) != null) {
+ if (token.equals(JsonToken.VALUE_STRING)
+ && parser.getValueAsString().startsWith(CIPHER_MARKER)) {
+ valsToDecrpyt.add(parser.getValueAsString().trim());
+ }
+ }
+
+ for (String value : valsToDecrpyt) {
+ String decryptedValue = decryptValue(value.replace(CIPHER_MARKER, ""), name,
+ profiles);
+ text = text.replace(value, decryptedValue);
+ }
+
+ return text;
+ }
+
+ protected String decryptValue(String value, String name, String[] profiles) {
+ return encryptor
+ .locate(this.helper.getEncryptorKeys(name,
+ StringUtils.arrayToCommaDelimitedString(profiles), value))
+ .decrypt(this.helper.stripPrefix(value));
+ }
+
+}
diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/CipherResourceJsonEncryptor.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/CipherResourceJsonEncryptor.java
new file mode 100644
index 00000000..456802ea
--- /dev/null
+++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/CipherResourceJsonEncryptor.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2013-2019 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.config.server.encryption;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+import com.fasterxml.jackson.core.JsonFactory;
+
+import org.springframework.cloud.config.environment.Environment;
+import org.springframework.stereotype.Component;
+
+/**
+ * @{link org.springframework.cloud.config.server.encryption.ResourceEncryptor}
+ * implementation that can decrypt property values prefixed with {cipher} marker in a JSON
+ * file.
+ * @author Sean Stiglitz
+ */
+@Component
+public class CipherResourceJsonEncryptor extends AbstractCipherResourceEncryptor
+ implements ResourceEncryptor {
+
+ private static final List SUPPORTED_EXTENSIONS = Arrays.asList("json");
+
+ private final JsonFactory factory;
+
+ public CipherResourceJsonEncryptor(TextEncryptorLocator encryptor) {
+ super(encryptor);
+ this.factory = new JsonFactory();
+ }
+
+ @Override
+ public List getSupportedExtensions() {
+ return SUPPORTED_EXTENSIONS;
+ }
+
+ @Override
+ public String decrypt(String text, Environment environment) throws IOException {
+ return decryptWithJacksonParser(text, environment.getName(),
+ environment.getProfiles(), factory);
+ }
+
+}
diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/CipherResourcePropertiesEncryptor.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/CipherResourcePropertiesEncryptor.java
new file mode 100644
index 00000000..d5f6b95f
--- /dev/null
+++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/CipherResourcePropertiesEncryptor.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2002-2019 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.config.server.encryption;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Properties;
+import java.util.Set;
+
+import org.springframework.cloud.config.environment.Environment;
+import org.springframework.stereotype.Component;
+
+/**
+ * @{link org.springframework.cloud.config.server.encryption.ResourceEncryptor}
+ * implementation that can decrypt property values prefixed with {cipher} marker in a
+ * Properties file.
+ * @author Sean Stiglitz
+ */
+@Component
+public class CipherResourcePropertiesEncryptor extends AbstractCipherResourceEncryptor
+ implements ResourceEncryptor {
+
+ private static final List SUPPORTED_EXTENSIONS = Arrays.asList("properties");
+
+ public CipherResourcePropertiesEncryptor(TextEncryptorLocator encryptor) {
+ super(encryptor);
+ }
+
+ @Override
+ public List getSupportedExtensions() {
+ return SUPPORTED_EXTENSIONS;
+ }
+
+ @Override
+ public String decrypt(String text, Environment environment) throws IOException {
+ Set valsToDecrpyt = new HashSet();
+ Properties properties = new Properties();
+ StringBuffer sb = new StringBuffer();
+ properties.load(new ByteArrayInputStream(text.getBytes()));
+
+ for (Object value : properties.values()) {
+ String valueStr = value.toString();
+ if (valueStr.startsWith(CIPHER_MARKER)) {
+ valsToDecrpyt.add(valueStr);
+ }
+ }
+
+ for (String value : valsToDecrpyt) {
+ String decryptedValue = decryptValue(value.replace(CIPHER_MARKER, ""),
+ environment.getName(), environment.getProfiles());
+ text = text.replace(value, decryptedValue);
+ }
+
+ return text;
+ }
+
+}
diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/CipherResourceYamlEncryptor.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/CipherResourceYamlEncryptor.java
new file mode 100644
index 00000000..c8e6763c
--- /dev/null
+++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/CipherResourceYamlEncryptor.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2002-2019 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.config.server.encryption;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+
+import org.springframework.cloud.config.environment.Environment;
+import org.springframework.stereotype.Component;
+
+/**
+ * @{link org.springframework.cloud.config.server.encryption.ResourceEncryptor}
+ * implementation that can decrypt property values prefixed with {cipher} marker in a YAML
+ * file.
+ * @author Sean Stiglitz
+ */
+@Component
+public class CipherResourceYamlEncryptor extends AbstractCipherResourceEncryptor
+ implements ResourceEncryptor {
+
+ private static final List SUPPORTED_EXTENSIONS = Arrays.asList("yml", "yaml");
+
+ private final YAMLFactory factory;
+
+ public CipherResourceYamlEncryptor(TextEncryptorLocator encryptor) {
+ super(encryptor);
+ this.factory = new YAMLFactory();
+ }
+
+ @Override
+ public List getSupportedExtensions() {
+ return SUPPORTED_EXTENSIONS;
+ }
+
+ @Override
+ public String decrypt(String text, Environment environment) throws IOException {
+ return decryptWithJacksonParser(text, environment.getName(),
+ environment.getProfiles(), factory);
+ }
+
+}
diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/ResourceEncryptor.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/ResourceEncryptor.java
new file mode 100644
index 00000000..84040e4c
--- /dev/null
+++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/ResourceEncryptor.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2002-2019 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.config.server.encryption;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.springframework.cloud.config.environment.Environment;
+
+/**
+ * Interface for decrypting values in plain text files served through
+ * {@link org.springframework.cloud.config.server.resource.ResourceController}.
+ *
+ * @author Sean Stiglitz
+ */
+public interface ResourceEncryptor {
+
+ List getSupportedExtensions();
+
+ String decrypt(String text, Environment environment) throws IOException;
+
+}
diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/resource/ResourceController.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/resource/ResourceController.java
index aa59424a..d228bf44 100644
--- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/resource/ResourceController.java
+++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/resource/ResourceController.java
@@ -19,13 +19,20 @@ package org.springframework.cloud.config.server.resource;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.config.environment.Environment;
+import org.springframework.cloud.config.server.encryption.ResourceEncryptor;
import org.springframework.cloud.config.server.environment.EnvironmentRepository;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.StreamUtils;
+import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -56,12 +63,29 @@ import static org.springframework.cloud.config.server.support.EnvironmentPropert
path = "${spring.cloud.config.server.prefix:}")
public class ResourceController {
+ private static Log logger = LogFactory.getLog(ResourceController.class);
+
private ResourceRepository resourceRepository;
private EnvironmentRepository environmentRepository;
+ private Map resourceEncryptorMap = new HashMap<>();
+
private UrlPathHelper helper = new UrlPathHelper();
+ private boolean encryptEnabled = false;
+
+ private boolean plainTextEncryptEnabled = false;
+
+ public ResourceController(ResourceRepository resourceRepository,
+ EnvironmentRepository environmentRepository,
+ Map resourceEncryptorMap) {
+ this.resourceRepository = resourceRepository;
+ this.environmentRepository = environmentRepository;
+ this.resourceEncryptorMap = resourceEncryptorMap;
+ this.helper.setAlwaysUseFullPath(true);
+ }
+
public ResourceController(ResourceRepository resourceRepository,
EnvironmentRepository environmentRepository) {
this.resourceRepository = resourceRepository;
@@ -69,6 +93,14 @@ public class ResourceController {
this.helper.setAlwaysUseFullPath(true);
}
+ public void setEncryptEnabled(boolean encryptEnabled) {
+ this.encryptEnabled = encryptEnabled;
+ }
+
+ public void setPlainTextEncryptEnabled(boolean plainTextEncryptEnabled) {
+ this.plainTextEncryptEnabled = plainTextEncryptEnabled;
+ }
+
@RequestMapping("/{name}/{profile}/{label}/**")
public String retrieve(@PathVariable String name, @PathVariable String profile,
@PathVariable String label, ServletWebRequest request,
@@ -113,11 +145,22 @@ public class ResourceController {
// ensure InputStream will be closed to prevent file locks on Windows
try (InputStream is = resource.getInputStream()) {
String text = StreamUtils.copyToString(is, Charset.forName("UTF-8"));
+ String ext = StringUtils.getFilenameExtension(resource.getFilename())
+ .toLowerCase();
+ Environment environment = this.environmentRepository.findOne(name, profile,
+ label, false);
if (resolvePlaceholders) {
- Environment environment = this.environmentRepository.findOne(name,
- profile, label, false);
text = resolvePlaceholders(prepareEnvironment(environment), text);
}
+ if (encryptEnabled && plainTextEncryptEnabled) {
+ ResourceEncryptor re = this.resourceEncryptorMap.get(ext);
+ if (re == null) {
+ logger.warn("Cannot decrypt for extension " + ext);
+ }
+ else {
+ text = re.decrypt(text, environment);
+ }
+ }
return text;
}
}
diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/CipherResourceJsonEncryptorTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/CipherResourceJsonEncryptorTests.java
new file mode 100644
index 00000000..de49da71
--- /dev/null
+++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/CipherResourceJsonEncryptorTests.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2013-2019 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.config.server.encryption;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.util.Map;
+
+import org.junit.Test;
+
+import org.springframework.cloud.config.environment.Environment;
+import org.springframework.cloud.context.encrypt.EncryptorFactory;
+import org.springframework.security.crypto.encrypt.TextEncryptor;
+import org.springframework.util.ResourceUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Sean Stiglitz
+ */
+public class CipherResourceJsonEncryptorTests {
+
+ private final String salt = "deadbeef";
+
+ private final String key = "foo";
+
+ private TextEncryptor textEncryptor = new EncryptorFactory(salt).create(key);
+
+ private CipherResourceJsonEncryptor encryptor = new CipherResourceJsonEncryptor(
+ new TextEncryptorLocator() {
+ @Override
+ public TextEncryptor locate(Map keys) {
+ return CipherResourceJsonEncryptorTests.this.textEncryptor;
+ }
+ });
+
+ @Test
+ public void whenDecryptResource_thenAllEncryptedValuesDecrypted() throws Exception {
+ // given
+ Environment environment = new Environment("name", "profile", "label");
+ File file = ResourceUtils.getFile("classpath:resource-encryptor/test.json");
+ String text = new String(Files.readAllBytes(file.toPath()));
+
+ // when
+ String decyptedResource = encryptor.decrypt(text, environment);
+
+ // then
+ assertThat(decyptedResource.contains("{cipher}")).isFalse();
+ }
+
+}
diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/CipherResourcePropertiesEncryptorTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/CipherResourcePropertiesEncryptorTests.java
new file mode 100644
index 00000000..1d9ea051
--- /dev/null
+++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/CipherResourcePropertiesEncryptorTests.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2013-2019 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.config.server.encryption;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.util.Map;
+
+import org.junit.Test;
+
+import org.springframework.cloud.config.environment.Environment;
+import org.springframework.cloud.context.encrypt.EncryptorFactory;
+import org.springframework.security.crypto.encrypt.TextEncryptor;
+import org.springframework.util.ResourceUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Sean Stiglitz
+ */
+public class CipherResourcePropertiesEncryptorTests {
+
+ private final String salt = "deadbeef";
+
+ private final String key = "foo";
+
+ private TextEncryptor textEncryptor = new EncryptorFactory(salt).create(key);
+
+ private CipherResourcePropertiesEncryptor encryptor = new CipherResourcePropertiesEncryptor(
+ new TextEncryptorLocator() {
+ @Override
+ public TextEncryptor locate(Map keys) {
+ return CipherResourcePropertiesEncryptorTests.this.textEncryptor;
+ }
+ });
+
+ @Test
+ public void whenDecryptResource_thenAllEncryptedValuesDecrypted() throws Exception {
+ // given
+ Environment environment = new Environment("name", "profile", "label");
+ File file = ResourceUtils.getFile("classpath:resource-encryptor/test.properties");
+ String text = new String(Files.readAllBytes(file.toPath()));
+
+ // when
+ String decyptedResource = encryptor.decrypt(text, environment);
+
+ // then
+ assertThat(decyptedResource.contains("{cipher}")).isFalse();
+ }
+
+}
diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/CipherResourceYamlEncryptorTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/CipherResourceYamlEncryptorTests.java
new file mode 100644
index 00000000..72c29668
--- /dev/null
+++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/CipherResourceYamlEncryptorTests.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2013-2019 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.config.server.encryption;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.util.Map;
+
+import org.junit.Test;
+
+import org.springframework.cloud.config.environment.Environment;
+import org.springframework.cloud.context.encrypt.EncryptorFactory;
+import org.springframework.security.crypto.encrypt.TextEncryptor;
+import org.springframework.util.ResourceUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Sean Stiglitz
+ */
+public class CipherResourceYamlEncryptorTests {
+
+ private final String salt = "deadbeef";
+
+ private final String key = "foo";
+
+ private TextEncryptor textEncryptor = new EncryptorFactory(salt).create(key);
+
+ private CipherResourceYamlEncryptor encryptor = new CipherResourceYamlEncryptor(
+ new TextEncryptorLocator() {
+ @Override
+ public TextEncryptor locate(Map keys) {
+ return CipherResourceYamlEncryptorTests.this.textEncryptor;
+ }
+ });
+
+ @Test
+ public void whenDecryptResource_thenAllEncryptedValuesDecrypted() throws Exception {
+ // given
+ Environment environment = new Environment("name", "profile", "label");
+ File file = ResourceUtils.getFile("classpath:resource-encryptor/test.yml");
+ String text = new String(Files.readAllBytes(file.toPath()));
+
+ // when
+ String decyptedResource = encryptor.decrypt(text, environment);
+
+ // then
+ assertThat(decyptedResource.contains("{cipher}")).isFalse();
+ }
+
+}
diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/resource/ResourceControllerIntegrationTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/resource/ResourceControllerIntegrationTests.java
index 3168c848..0235d955 100644
--- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/resource/ResourceControllerIntegrationTests.java
+++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/resource/ResourceControllerIntegrationTests.java
@@ -16,6 +16,9 @@
package org.springframework.cloud.config.server.resource;
+import java.util.HashMap;
+import java.util.Map;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -25,13 +28,14 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.config.environment.Environment;
+import org.springframework.cloud.config.server.encryption.ResourceEncryptor;
import org.springframework.cloud.config.server.environment.EnvironmentController;
import org.springframework.cloud.config.server.environment.EnvironmentRepository;
import org.springframework.cloud.config.server.resource.ResourceControllerIntegrationTests.ControllerConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
-import org.springframework.core.io.ByteArrayResource;
+import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpHeaders;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
@@ -78,7 +82,7 @@ public class ResourceControllerIntegrationTests {
when(this.repository.findOne("foo", "default", "master", false))
.thenReturn(new Environment("foo", "default"));
when(this.resources.findOne("foo", "default", "master", "foo.txt"))
- .thenReturn(new ByteArrayResource("hello".getBytes()));
+ .thenReturn(new ClassPathResource("resource-controller/foo.txt"));
this.mvc.perform(MockMvcRequestBuilders.get("/foo/default/master/foo.txt"))
.andExpect(MockMvcResultMatchers.status().isOk());
verify(this.repository).findOne("foo", "default", "master", false);
@@ -90,7 +94,7 @@ public class ResourceControllerIntegrationTests {
when(this.repository.findOne("foo", "default", null, false))
.thenReturn(new Environment("foo", "default", "master"));
when(this.resources.findOne("foo", "default", null, "foo.txt"))
- .thenReturn(new ByteArrayResource("hello".getBytes()));
+ .thenReturn(new ClassPathResource("resource-controller/foo.txt"));
this.mvc.perform(MockMvcRequestBuilders.get("/foo/default/foo.txt")
.param("useDefaultLabel", ""))
.andExpect(MockMvcResultMatchers.status().isOk());
@@ -103,7 +107,7 @@ public class ResourceControllerIntegrationTests {
when(this.repository.findOne("foo", "default", null, false))
.thenReturn(new Environment("foo", "default", "master"));
when(this.resources.findOne("foo", "default", null, "foo.txt"))
- .thenReturn(new ByteArrayResource("hello".getBytes()));
+ .thenReturn(new ClassPathResource("resource-controller/foo.txt"));
this.mvc.perform(MockMvcRequestBuilders.get("/foo/default/foo.txt")
.param("useDefaultLabel", "")
.header(HttpHeaders.ACCEPT, MimeTypeUtils.APPLICATION_OCTET_STREAM_VALUE))
@@ -117,6 +121,9 @@ public class ResourceControllerIntegrationTests {
@Import(PropertyPlaceholderAutoConfiguration.class)
public static class ControllerConfiguration {
+ @Autowired(required = false)
+ private Map resourceEncryptorMap = new HashMap<>();
+
@Bean
public EnvironmentRepository environmentRepository() {
EnvironmentRepository repository = Mockito.mock(EnvironmentRepository.class);
@@ -136,7 +143,8 @@ public class ResourceControllerIntegrationTests {
@Bean
public ResourceController resourceController() {
- return new ResourceController(resourceRepository(), environmentRepository());
+ return new ResourceController(resourceRepository(), environmentRepository(),
+ resourceEncryptorMap);
}
}
diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/resource/ResourceControllerTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/resource/ResourceControllerTests.java
index cff89fd0..acd08695 100644
--- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/resource/ResourceControllerTests.java
+++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/resource/ResourceControllerTests.java
@@ -16,12 +16,16 @@
package org.springframework.cloud.config.server.resource;
+import java.util.Map;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
+import org.mockito.Mockito;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.cloud.config.server.encryption.ResourceEncryptor;
import org.springframework.cloud.config.server.environment.NativeEnvironmentProperties;
import org.springframework.cloud.config.server.environment.NativeEnvironmentRepository;
import org.springframework.cloud.config.server.environment.NativeEnvironmentRepositoryTests;
@@ -31,6 +35,10 @@ import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.context.request.ServletWebRequest;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
/**
* @author Dave Syer
@@ -46,6 +54,9 @@ public class ResourceControllerTests {
private NativeEnvironmentRepository environmentRepository;
+ @SuppressWarnings("unchecked")
+ private Map resourceEncryptorMap = Mockito.mock(Map.class);
+
@After
public void close() {
if (this.context != null) {
@@ -63,7 +74,7 @@ public class ResourceControllerTests {
this.repository = new GenericResourceRepository(this.environmentRepository);
this.repository.setResourceLoader(this.context);
this.controller = new ResourceController(this.repository,
- this.environmentRepository);
+ this.environmentRepository, this.resourceEncryptorMap);
this.context.close();
}
@@ -310,4 +321,44 @@ public class ResourceControllerTests {
assertThat(new String(resource)).isEqualToIgnoringNewLines("foo: dev_bar/spam");
}
+ @Test
+ public void whenSupportedResourceWithDecrpyt_thenSuccess() throws Exception {
+ // given
+ String decryptedStr = "{\"foo\": \"decrypted\"}";
+ ResourceEncryptor resourceEncryptor = mock(ResourceEncryptor.class);
+ when(resourceEncryptor.decrypt(anyString(), any())).thenReturn(decryptedStr);
+ when(resourceEncryptorMap.get("json")).thenReturn(resourceEncryptor);
+
+ this.environmentRepository.setSearchLocations("classpath:/test");
+ this.controller.setEncryptEnabled(true);
+ this.controller.setPlainTextEncryptEnabled(true);
+
+ // when
+ String resource = this.controller.retrieve("foo", "bar", "dev", "template.json",
+ false);
+
+ // then
+ assertThat(resource).isEqualTo(decryptedStr);
+ }
+
+ @Test
+ public void whenUnkownResourceWithDecrpyt_thenNothingChanged() throws Exception {
+ // given
+ String decryptedStr = "{\"foo\": \"decrypted\"}";
+ ResourceEncryptor resourceEncryptor = mock(ResourceEncryptor.class);
+ when(resourceEncryptor.decrypt(anyString(), any())).thenReturn(decryptedStr);
+ when(resourceEncryptorMap.get("json")).thenReturn(resourceEncryptor);
+
+ this.environmentRepository.setSearchLocations("classpath:/test");
+ this.controller.setEncryptEnabled(true);
+ this.controller.setPlainTextEncryptEnabled(true);
+
+ // when
+ String resource = this.controller.retrieve("foo", "bar", "dev", "spam/foo.txt",
+ false);
+
+ // then
+ assertThat(resource).isEqualToIgnoringNewLines("foo: dev_bar/spam");
+ }
+
}
diff --git a/spring-cloud-config-server/src/test/resources/resource-controller/foo.txt b/spring-cloud-config-server/src/test/resources/resource-controller/foo.txt
new file mode 100644
index 00000000..b6fc4c62
--- /dev/null
+++ b/spring-cloud-config-server/src/test/resources/resource-controller/foo.txt
@@ -0,0 +1 @@
+hello
\ No newline at end of file
diff --git a/spring-cloud-config-server/src/test/resources/resource-encryptor/test.json b/spring-cloud-config-server/src/test/resources/resource-encryptor/test.json
new file mode 100644
index 00000000..420f78fb
--- /dev/null
+++ b/spring-cloud-config-server/src/test/resources/resource-encryptor/test.json
@@ -0,0 +1,11 @@
+{
+ "spring": {
+ "profiles": "encrypt"
+ },
+ "config": {
+ "foo": "{cipher}d1b2458ccede07c856ff952bd841638ff4dd12ed1d36812663c3c7262d57bf46",
+ "null-value": null,
+ "array": ["{cipher}d1b2458ccede07c856ff952bd841638ff4dd12ed1d36812663c3c7262d57bf46","{cipher}d1b2458ccede07c856ff952bd841638ff4dd12ed1d36812663c3c7262d57bf46"],
+ "key-password": "{cipher}{key:mytestkey}d1b2458ccede07c856ff952bd841638ff4dd12ed1d36812663c3c7262d57bf46"
+ }
+}
\ No newline at end of file
diff --git a/spring-cloud-config-server/src/test/resources/resource-encryptor/test.properties b/spring-cloud-config-server/src/test/resources/resource-encryptor/test.properties
new file mode 100644
index 00000000..92219d63
--- /dev/null
+++ b/spring-cloud-config-server/src/test/resources/resource-encryptor/test.properties
@@ -0,0 +1,6 @@
+spring.profiles=encrypt
+config.foo={cipher}d1b2458ccede07c856ff952bd841638ff4dd12ed1d36812663c3c7262d57bf46
+config.null-value=null
+config.array.password[0]={cipher}d1b2458ccede07c856ff952bd841638ff4dd12ed1d36812663c3c7262d57bf46
+config.array.password[1]={cipher}d1b2458ccede07c856ff952bd841638ff4dd12ed1d36812663c3c7262d57bf46
+config.key-password={cipher}{key:mytestkey}d1b2458ccede07c856ff952bd841638ff4dd12ed1d36812663c3c7262d57bf46
diff --git a/spring-cloud-config-server/src/test/resources/resource-encryptor/test.yml b/spring-cloud-config-server/src/test/resources/resource-encryptor/test.yml
new file mode 100644
index 00000000..c9c0cd62
--- /dev/null
+++ b/spring-cloud-config-server/src/test/resources/resource-encryptor/test.yml
@@ -0,0 +1,25 @@
+---
+spring:
+ profiles: encrypt
+config:
+ foo: '{cipher}d1b2458ccede07c856ff952bd841638ff4dd12ed1d36812663c3c7262d57bf46'
+ null-value: null
+ array:
+ - first-password: '{cipher}d1b2458ccede07c856ff952bd841638ff4dd12ed1d36812663c3c7262d57bf46'
+ second-password: '{cipher}d1b2458ccede07c856ff952bd841638ff4dd12ed1d36812663c3c7262d57bf46'
+ pipe-block-text-password: |
+ {cipher}d1b2458ccede07c856ff952bd841638ff4dd12ed1d36812663c3c7262d57bf46
+ greater-than-block-text-password: >
+ {cipher}d1b2458ccede07c856ff952bd841638ff4dd12ed1d36812663c3c7262d57bf46
+---
+spring:
+ profiles: encryptkey
+config:
+ foo: '{cipher}{key:mytestkey}d1b2458ccede07c856ff952bd841638ff4dd12ed1d36812663c3c7262d57bf46'
+ array:
+ - first-password: '{cipher}{key:mytestkey}d1b2458ccede07c856ff952bd841638ff4dd12ed1d36812663c3c7262d57bf46'
+ second-password: '{cipher}{key:mytestkey}d1b2458ccede07c856ff952bd841638ff4dd12ed1d36812663c3c7262d57bf46'
+ pipe-block-text-password: |
+ '{cipher}{key:mytestkey}d1b2458ccede07c856ff952bd841638ff4dd12ed1d36812663c3c7262d57bf46'
+ greater-than-block-text-password: >
+ '{cipher}{key:mytestkey}d1b2458ccede07c856ff952bd841638ff4dd12ed1d36812663c3c7262d57bf46'