Merge branch 'rebase-for-publish' of https://github.com/sstiglitz/spring-cloud-config into sstiglitz-rebase-for-publish

This commit is contained in:
Ryan Baxter
2019-11-06 11:45:26 -05:00
parent c0dd3aa538
commit 91a6301bde
21 changed files with 766 additions and 11 deletions

View File

@@ -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.

View File

@@ -70,6 +70,10 @@
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
<dependency>
<groupId>org.tmatesoft.svnkit</groupId>
<artifactId>svnkit</artifactId>

View File

@@ -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 {
}

View File

@@ -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<String, ResourceEncryptor> 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;
}

View File

@@ -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;
}
}
}

View File

@@ -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<String, ResourceEncryptor> resourceEncryptors() {
Map<String, ResourceEncryptor> resourceEncryptorMap = new HashMap<>();
addSupportedExtensionsToMap(resourceEncryptorMap,
new CipherResourceJsonEncryptor(encryptor));
addSupportedExtensionsToMap(resourceEncryptorMap,
new CipherResourcePropertiesEncryptor(encryptor));
addSupportedExtensionsToMap(resourceEncryptorMap,
new CipherResourceYamlEncryptor(encryptor));
return resourceEncryptorMap;
}
private void addSupportedExtensionsToMap(
Map<String, ResourceEncryptor> resourceEncryptorMap,
ResourceEncryptor resourceEncryptor) {
for (String ext : resourceEncryptor.getSupportedExtensions()) {
resourceEncryptorMap.put(ext, resourceEncryptor);
}
}
}

View File

@@ -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<String> 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<String> valsToDecrpyt = new HashSet<String>();
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));
}
}

View File

@@ -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<String> SUPPORTED_EXTENSIONS = Arrays.asList("json");
private final JsonFactory factory;
public CipherResourceJsonEncryptor(TextEncryptorLocator encryptor) {
super(encryptor);
this.factory = new JsonFactory();
}
@Override
public List<String> getSupportedExtensions() {
return SUPPORTED_EXTENSIONS;
}
@Override
public String decrypt(String text, Environment environment) throws IOException {
return decryptWithJacksonParser(text, environment.getName(),
environment.getProfiles(), factory);
}
}

View File

@@ -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<String> SUPPORTED_EXTENSIONS = Arrays.asList("properties");
public CipherResourcePropertiesEncryptor(TextEncryptorLocator encryptor) {
super(encryptor);
}
@Override
public List<String> getSupportedExtensions() {
return SUPPORTED_EXTENSIONS;
}
@Override
public String decrypt(String text, Environment environment) throws IOException {
Set<String> valsToDecrpyt = new HashSet<String>();
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;
}
}

View File

@@ -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<String> SUPPORTED_EXTENSIONS = Arrays.asList("yml", "yaml");
private final YAMLFactory factory;
public CipherResourceYamlEncryptor(TextEncryptorLocator encryptor) {
super(encryptor);
this.factory = new YAMLFactory();
}
@Override
public List<String> getSupportedExtensions() {
return SUPPORTED_EXTENSIONS;
}
@Override
public String decrypt(String text, Environment environment) throws IOException {
return decryptWithJacksonParser(text, environment.getName(),
environment.getProfiles(), factory);
}
}

View File

@@ -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<String> getSupportedExtensions();
String decrypt(String text, Environment environment) throws IOException;
}

View File

@@ -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<String, ResourceEncryptor> resourceEncryptorMap = new HashMap<>();
private UrlPathHelper helper = new UrlPathHelper();
private boolean encryptEnabled = false;
private boolean plainTextEncryptEnabled = false;
public ResourceController(ResourceRepository resourceRepository,
EnvironmentRepository environmentRepository,
Map<String, ResourceEncryptor> 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;
}
}

View File

@@ -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<String, String> 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();
}
}

View File

@@ -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<String, String> 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();
}
}

View File

@@ -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<String, String> 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();
}
}

View File

@@ -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<String, ResourceEncryptor> 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);
}
}

View File

@@ -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<String, ResourceEncryptor> 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");
}
}

View File

@@ -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"
}
}

View File

@@ -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

View File

@@ -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'