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'