Merge branch 'rebase-for-publish' of https://github.com/sstiglitz/spring-cloud-config into sstiglitz-rebase-for-publish
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
hello
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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'
|
||||
Reference in New Issue
Block a user