diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigClientAutoConfigurationTests.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigClientAutoConfigurationTests.java index 00e3967f..399fd7f9 100644 --- a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigClientAutoConfigurationTests.java +++ b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigClientAutoConfigurationTests.java @@ -1,15 +1,30 @@ +/* + * Copyright 2002-2015 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 + * + * http://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.client; -import static org.junit.Assert.assertEquals; - import org.junit.Test; + import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.config.client.ConfigClientAutoConfiguration; -import org.springframework.cloud.config.client.ConfigClientProperties; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import static org.junit.Assert.assertEquals; + public class ConfigClientAutoConfigurationTests { @Test diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ConfigServerMvcConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ConfigServerMvcConfiguration.java index ac94539b..05b5d890 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ConfigServerMvcConfiguration.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ConfigServerMvcConfiguration.java @@ -17,9 +17,9 @@ package org.springframework.cloud.config.server; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.cloud.config.server.encryption.EnvironmentEncryptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.crypto.encrypt.TextEncryptor; import org.springframework.util.StringUtils; /** @@ -29,19 +29,18 @@ import org.springframework.util.StringUtils; @Configuration @ConditionalOnWebApplication public class ConfigServerMvcConfiguration { - - @Autowired(required = false) - private TextEncryptor encryptor; - @Autowired private EnvironmentRepository repository; @Autowired private ConfigServerProperties server; + @Autowired + private EnvironmentEncryptor environmentEncryptor; + @Bean public EnvironmentController environmentController() { - EnvironmentController controller = new EnvironmentController(repository, encryptionController()); + EnvironmentController controller = new EnvironmentController(repository, environmentEncryptor); controller.setDefaultLabel(getDefaultLabel()); controller.setOverrides(server.getOverrides()); controller.setStripDocumentFromYaml(server.isStripDocumentFromYaml()); @@ -56,13 +55,4 @@ public class ConfigServerMvcConfiguration { return repository.getDefaultLabel(); } } - - @Bean - public EncryptionController encryptionController() { - EncryptionController controller = new EncryptionController(); - if (encryptor!=null) { - controller.setEncryptor(encryptor); - } - return controller; - } } \ No newline at end of file diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ConfigServerProperties.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ConfigServerProperties.java index 6b9b6acc..33e71db2 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ConfigServerProperties.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ConfigServerProperties.java @@ -57,6 +57,17 @@ public class ConfigServerProperties { */ private boolean stripDocumentFromYaml = true; + /** + * Default application name when incoming requests do not have a specific one. + */ + private String defaultApplicationName = "application"; + + /** + * Default application profile when incoming requests do not have a specific one. + */ + private String defaultProfile = "default"; + + public String getDefaultLabel() { return defaultLabel; } @@ -97,4 +108,19 @@ public class ConfigServerProperties { this.stripDocumentFromYaml = stripDocumentFromYaml; } + public String getDefaultApplicationName() { + return defaultApplicationName; + } + + public void setDefaultApplicationName(String defaultApplicationName) { + this.defaultApplicationName = defaultApplicationName; + } + + public String getDefaultProfile() { + return defaultProfile; + } + + public void setDefaultProfile(String defaultProfile) { + this.defaultProfile = defaultProfile; + } } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EnableConfigServer.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EnableConfigServer.java index 47c147e8..3bf27850 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EnableConfigServer.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EnableConfigServer.java @@ -21,6 +21,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.cloud.config.server.encryption.ConfigServerEncryptionConfiguration; import org.springframework.context.annotation.Import; /** @@ -30,7 +31,9 @@ import org.springframework.context.annotation.Import; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented -@Import({ConfigServerConfiguration.class, ConfigServerMvcConfiguration.class}) +@Import({ConfigServerConfiguration.class, + ConfigServerMvcConfiguration.class, + ConfigServerEncryptionConfiguration.class}) public @interface EnableConfigServer { } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EncryptionController.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EncryptionController.java index 7574acc0..22791079 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EncryptionController.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EncryptionController.java @@ -21,16 +21,13 @@ import java.net.URLDecoder; import java.security.KeyPair; import java.util.Collections; import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.Map; -import java.util.Map.Entry; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cloud.config.environment.Environment; -import org.springframework.cloud.config.environment.PropertySource; + +import org.springframework.cloud.config.server.encryption.SingleTextEncryptorLocator; +import org.springframework.cloud.config.server.encryption.TextEncryptorLocator; import org.springframework.cloud.context.encrypt.EncryptorFactory; import org.springframework.cloud.context.encrypt.KeyFormatException; import org.springframework.core.io.ByteArrayResource; @@ -44,6 +41,7 @@ import org.springframework.security.rsa.crypto.KeyStoreKeyFactory; import org.springframework.security.rsa.crypto.RsaKeyHolder; import org.springframework.security.rsa.crypto.RsaSecretEncryptor; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; @@ -63,15 +61,19 @@ public class EncryptionController { private static Log logger = LogFactory.getLog(EncryptionController.class); - private TextEncryptor encryptor; + private final TextEncryptorLocator encryptorLocator; - @Autowired(required = false) - public void setEncryptor(TextEncryptor encryptor) { - this.encryptor = encryptor; + private final ConfigServerProperties properties; + + public EncryptionController(TextEncryptorLocator encryptorLocator, + ConfigServerProperties configServerProperties) { + this.encryptorLocator = encryptorLocator; + this.properties = configServerProperties; } @RequestMapping(value = "/key", method = RequestMethod.GET) public String getPublicKey() { + TextEncryptor encryptor = locateDefaultTextEncryptor(); if (!(encryptor instanceof RsaKeyHolder)) { throw new KeyNotAvailableException(); } @@ -90,8 +92,9 @@ public class EncryptionController { ByteArrayResource resource = new ByteArrayResource(file.getBytes()); KeyPair keyPair = new KeyStoreKeyFactory(resource, password.toCharArray()) .getKeyPair(alias); - encryptor = new RsaSecretEncryptor(keyPair); - body.put("publicKey", ((RsaKeyHolder) encryptor).getPublicKey()); + RsaSecretEncryptor encryptor = new RsaSecretEncryptor(keyPair); + updateEncryptor(encryptor); + body.put("publicKey", encryptor.getPublicKey()); } catch (IOException e) { throw new KeyFormatException(); @@ -102,21 +105,31 @@ public class EncryptionController { } - @RequestMapping(value = "/key", method = RequestMethod.POST, params = { "!password" }) + @RequestMapping(value = "/key", method = RequestMethod.POST, params = { "!password" }) public ResponseEntity> uploadKey(@RequestBody String data, @RequestHeader("Content-Type") MediaType type) { Map body = new HashMap(); body.put("status", "OK"); - encryptor = new EncryptorFactory().create(stripFormData(data, type, false)); - + TextEncryptor encryptor = new EncryptorFactory().create(stripFormData(data, type, false)); + updateEncryptor(encryptor); if (encryptor instanceof RsaKeyHolder) { body.put("publicKey", ((RsaKeyHolder) encryptor).getPublicKey()); } logger.info("Key changed with literal value"); return new ResponseEntity>(body, HttpStatus.CREATED); + } + // this is temporary solution to support existing REST API + // downcasting is only necessary until we introduce some key management abstraction, + // we don't want TextEncryptorLocator to have setEncryptor method + private void updateEncryptor(TextEncryptor encryptor) { + if (encryptorLocator instanceof SingleTextEncryptorLocator) { + ((SingleTextEncryptorLocator) encryptorLocator).setEncryptor(encryptor); + } else { + throw new IncompatibleTextEncryptorLocatorException(); + } } @ExceptionHandler(KeyFormatException.class) @@ -139,39 +152,72 @@ public class EncryptionController { @RequestMapping(value = "encrypt/status", method = RequestMethod.GET) public Map status() { - if (encryptor == null) { - throw new KeyNotInstalledException(); - } + checkEncryptorInstalled(locateDefaultTextEncryptor()); return Collections. singletonMap("status", "OK"); } @RequestMapping(value = "encrypt", method = RequestMethod.POST) - public String encrypt(@RequestBody String data, + public String encrypt( + @RequestBody String data, @RequestHeader("Content-Type") MediaType type) { - if (encryptor == null) { - throw new KeyNotInstalledException(); + + return encrypt(properties.getDefaultApplicationName(), + properties.getDefaultProfile(), data, type); + } + + @RequestMapping(value = "/encrypt/{name}/{profiles}", method = RequestMethod.POST) + public String encrypt( + @PathVariable String name, + @PathVariable String profiles, + @RequestBody String data, + @RequestHeader("Content-Type") MediaType type) { + + try { + TextEncryptor encryptor = checkEncryptorInstalled(encryptorLocator.locate(name, profiles)); + String encrypted = encryptor.encrypt(stripFormData(data, type, false)); + logger.info("Encrypted data"); + return encrypted; + } catch (IllegalArgumentException e) { + throw new InvalidCipherException(); } - data = stripFormData(data, type, false); - String encrypted = encryptor.encrypt(data); - logger.info("Encrypted data"); - return encrypted; } @RequestMapping(value = "decrypt", method = RequestMethod.POST) - public String decrypt(@RequestBody String data, + public String decrypt( + @RequestBody String data, @RequestHeader("Content-Type") MediaType type) { + + return decrypt(properties.getDefaultApplicationName(), + properties.getDefaultProfile(), data, type); + } + + @RequestMapping(value = "/decrypt/{name}/{profiles}", method = RequestMethod.POST) + public String decrypt( + @PathVariable String name, + @PathVariable String profiles, + @RequestBody String data, + @RequestHeader("Content-Type") MediaType type) { + + try { + TextEncryptor encryptor = checkEncryptorInstalled(encryptorLocator.locate(name, profiles)); + String decrypted = encryptor.decrypt(stripFormData(data, type, true)); + logger.info("Decrypted cipher data"); + return decrypted; + } catch (IllegalArgumentException e) { + throw new InvalidCipherException(); + } + } + + private TextEncryptor checkEncryptorInstalled(TextEncryptor encryptor) { if (encryptor == null) { throw new KeyNotInstalledException(); } - try { - data = stripFormData(data, type, true); - String decrypted = encryptor.decrypt(data); - logger.info("Decrypted cipher data"); - return decrypted; - } - catch (IllegalArgumentException e) { - throw new InvalidCipherException(); - } + return encryptor; + } + + private TextEncryptor locateDefaultTextEncryptor() { + return encryptorLocator.locate(properties.getDefaultApplicationName(), + properties.getDefaultProfile()); } private String stripFormData(String data, MediaType type, boolean cipher) { @@ -228,40 +274,6 @@ public class EncryptionController { return new ResponseEntity>(body, HttpStatus.BAD_REQUEST); } - public Environment decrypt(Environment environment) { - Environment result = new Environment(environment.getName(), environment.getProfiles(), - environment.getLabel()); - for (PropertySource source : environment.getPropertySources()) { - Map map = new LinkedHashMap( - source.getSource()); - for (Entry entry : new LinkedHashSet<>(map.entrySet())) { - Object key = entry.getKey(); - String name = key.toString(); - String value = entry.getValue().toString(); - if (value.startsWith("{cipher}")) { - map.remove(key); - if (encryptor == null) { - map.put(name, value); - } - else { - try { - value = value == null ? null : encryptor.decrypt(value - .substring("{cipher}".length())); - } - catch (Exception e) { - value = ""; - name = "invalid." + name; - logger.warn("Cannot decrypt key: " + key + " (" - + e.getClass() + ": " + e.getMessage() + ")"); - } - map.put(name, value); - } - } - } - result.add(new PropertySource(source.getName(), map)); - } - return result; - } } @SuppressWarnings("serial") @@ -275,3 +287,7 @@ class KeyNotAvailableException extends RuntimeException { @SuppressWarnings("serial") class InvalidCipherException extends RuntimeException { } + +@SuppressWarnings("serial") +class IncompatibleTextEncryptorLocatorException extends RuntimeException { +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EnvironmentController.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EnvironmentController.java index e90f55b2..32801364 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EnvironmentController.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EnvironmentController.java @@ -31,6 +31,7 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.boot.bind.PropertiesConfigurationFactory; import org.springframework.cloud.config.environment.Environment; import org.springframework.cloud.config.environment.PropertySource; +import org.springframework.cloud.config.server.encryption.EnvironmentEncryptor; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MutablePropertySources; import org.springframework.http.HttpHeaders; @@ -51,6 +52,9 @@ import org.yaml.snakeyaml.nodes.Tag; * @author Dave Syer * @author Spencer Gibb * @author Roy Clarkson + * @author Bartosz Wojtkiewicz + * @author Rafal Zukowski + * */ @RestController @RequestMapping("${spring.cloud.config.server.prefix:}") @@ -60,7 +64,7 @@ public class EnvironmentController { private EnvironmentRepository repository; - private EncryptionController encryption; + private EnvironmentEncryptor environmentEncryptor; private String defaultLabel; @@ -68,12 +72,11 @@ public class EnvironmentController { private boolean stripDocument = true; - public EnvironmentController(EnvironmentRepository repository, - EncryptionController encryption) { + public EnvironmentController(EnvironmentRepository repository, EnvironmentEncryptor environmentEncryptor) { super(); this.repository = repository; this.defaultLabel = repository.getDefaultLabel(); - this.encryption = encryption; + this.environmentEncryptor = environmentEncryptor; } /** @@ -94,7 +97,7 @@ public class EnvironmentController { @RequestMapping("/{name}/{profiles}/{label:.*}") public Environment labelled(@PathVariable String name, @PathVariable String profiles, @PathVariable String label) { - Environment environment = encryption.decrypt(repository.findOne(name, profiles, + Environment environment = environmentEncryptor.decrypt(repository.findOne(name, profiles, label)); if (!overrides.isEmpty()) { environment.addFirst(new PropertySource("overrides", overrides)); diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/CipherEnvironmentEncryptor.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/CipherEnvironmentEncryptor.java new file mode 100644 index 00000000..b0cd8853 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/CipherEnvironmentEncryptor.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2015 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 + * + * http://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.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.config.environment.Environment; +import org.springframework.cloud.config.environment.PropertySource; +import org.springframework.security.crypto.encrypt.TextEncryptor; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +/** + * EnvironmentEncryptor that can decrypt property values prefixed with {cipher} marker. + * + * @author Dave Syer + * @author Bartosz Wojtkiewicz + * @author Rafal Zukowski + * + */ +@Component +public class CipherEnvironmentEncryptor implements EnvironmentEncryptor { + + private static Log logger = LogFactory.getLog(CipherEnvironmentEncryptor.class); + + private final TextEncryptorLocator encryptorLocator; + + @Autowired + public CipherEnvironmentEncryptor(TextEncryptorLocator encryptorLocator) { + this.encryptorLocator = encryptorLocator; + } + + @Override + public Environment decrypt(Environment environment) { + TextEncryptor encryptor = encryptorLocator.locate(environment.getName(), + StringUtils.arrayToCommaDelimitedString(environment.getProfiles())); + return encryptor != null ? decrypt(environment, encryptor) : environment; + } + + private Environment decrypt(Environment environment, TextEncryptor encryptor) { + Environment result = new Environment(environment.getName(), + environment.getProfiles(), environment.getLabel()); + for (PropertySource source : environment.getPropertySources()) { + Map map = new LinkedHashMap(source.getSource()); + for (Map.Entry entry : new LinkedHashSet<>(map.entrySet())) { + Object key = entry.getKey(); + String name = key.toString(); + String value = entry.getValue().toString(); + if (value.startsWith("{cipher}")) { + map.remove(key); + try { + value = value == null ? null : encryptor.decrypt(value.substring( + "{cipher}".length())); + } catch (Exception e) { + value = ""; + name = "invalid." + name; + logger.warn("Cannot decrypt key: " + key + + " (" + e.getClass() + ": " + e.getMessage() + ")"); + } + map.put(name, value); + } + } + result.add(new PropertySource(source.getName(), map)); + } + return result; + } + +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/ConfigServerEncryptionConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/ConfigServerEncryptionConfiguration.java new file mode 100644 index 00000000..60c5c590 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/ConfigServerEncryptionConfiguration.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2015 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 + * + * http://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 org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cloud.config.server.ConfigServerProperties; +import org.springframework.cloud.config.server.EncryptionController; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.encrypt.TextEncryptor; + +/** + * @author Bartosz Wojtkiewicz + * @author Rafal Zukowski + * + */ +@Configuration +public class ConfigServerEncryptionConfiguration { + + @Autowired(required = false) + private TextEncryptor encryptor; + + @Autowired + private TextEncryptorLocator locator; + + @Autowired + private ConfigServerProperties properties; + + @Bean + public EncryptionController encryptionController() { + return new EncryptionController(locator, properties); + } + + @Bean + public EnvironmentEncryptor environmentEncryptor() { + return new CipherEnvironmentEncryptor(locator); + } + + @Bean + @ConditionalOnMissingBean + public TextEncryptorLocator textEncryptorLocator() { + return new SingleTextEncryptorLocator(encryptor); + } +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/EnvironmentEncryptor.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/EnvironmentEncryptor.java new file mode 100644 index 00000000..623f5b9b --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/EnvironmentEncryptor.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2015 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 + * + * http://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 org.springframework.cloud.config.environment.Environment; + +/** + * Service interface for decrypting properties in Environment object. + * + * @author Bartosz Wojtkiewicz + * @author Rafal Zukowski + * + */ +public interface EnvironmentEncryptor { + Environment decrypt(Environment environment); +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/SingleTextEncryptorLocator.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/SingleTextEncryptorLocator.java new file mode 100644 index 00000000..c7db7471 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/SingleTextEncryptorLocator.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2015 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 + * + * http://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 org.springframework.security.crypto.encrypt.TextEncryptor; + +/** + * Basic implementation of TextEncryptorLocator which uses + * single TextEncryptor for all applications and profiles. + * + * @author Bartosz Wojtkiewicz + * @author Rafal Zukowski + * + */ +public class SingleTextEncryptorLocator implements TextEncryptorLocator { + private TextEncryptor encryptor; + + public SingleTextEncryptorLocator() { + } + + public SingleTextEncryptorLocator(TextEncryptor encryptor) { + this.encryptor = encryptor; + } + + // temporary solution to support EncryptionController REST API + public void setEncryptor(TextEncryptor encryptor) { + this.encryptor = encryptor; + } + + @Override + public TextEncryptor locate(String applicationName, String profiles) { + return encryptor; + } +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/TextEncryptorLocator.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/TextEncryptorLocator.java new file mode 100644 index 00000000..2b2cd456 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/TextEncryptorLocator.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2015 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 + * + * http://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 org.springframework.security.crypto.encrypt.TextEncryptor; + +/** + * Service interface for locating proper TextEncryptor to be used for particular application. + * It can be used to provide config server with application and environment specific encryption. + * + * @author Bartosz Wojtkiewicz + * @author Rafal Zukowski + * + */ +public interface TextEncryptorLocator { + /** + * Returns TextEncryptor to be used for given application and profiles. + * + * @param applicationName application name + * @param profiles comma separated list of profiles + */ + TextEncryptor locate(String applicationName, String profiles); +} diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ConfigClientOffIntegrationTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ConfigClientOffIntegrationTests.java index 4761624d..664937ca 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ConfigClientOffIntegrationTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ConfigClientOffIntegrationTests.java @@ -1,14 +1,12 @@ package org.springframework.cloud.config.server; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - import java.io.IOException; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; + import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -26,6 +24,11 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.anyString; + @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = TestConfiguration.class) @IntegrationTest("server.port:0") @@ -63,7 +66,9 @@ public class ConfigClientOffIntegrationTests { @Bean public EnvironmentRepository environmentRepository() { - return Mockito.mock(EnvironmentRepository.class); + EnvironmentRepository repository = Mockito.mock(EnvironmentRepository.class); + given(repository.findOne(anyString(), anyString(), anyString())).willReturn(new Environment("", "")); + return repository; } } diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ConfigClientOnIntegrationTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ConfigClientOnIntegrationTests.java index 0a0f695f..0787f4a2 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ConfigClientOnIntegrationTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ConfigClientOnIntegrationTests.java @@ -1,14 +1,12 @@ package org.springframework.cloud.config.server; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - import java.io.IOException; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; + import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -26,6 +24,11 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.anyString; + @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = TestConfiguration.class) @IntegrationTest({ "server.port:0", "spring.cloud.config.enabled:true" }) @@ -63,7 +66,9 @@ public class ConfigClientOnIntegrationTests { @Bean public EnvironmentRepository environmentRepository() { - return Mockito.mock(EnvironmentRepository.class); + EnvironmentRepository repository = Mockito.mock(EnvironmentRepository.class); + given(repository.findOne(anyString(), anyString(), anyString())).willReturn(new Environment("", "")); + return repository; } } diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EncryptionControllerTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EncryptionControllerTests.java index dab9283b..025731e8 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EncryptionControllerTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EncryptionControllerTests.java @@ -15,29 +15,26 @@ */ package org.springframework.cloud.config.server; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertTrue; - -import java.util.Collections; - import org.junit.Test; -import org.springframework.cloud.config.environment.Environment; -import org.springframework.cloud.config.environment.PropertySource; -import org.springframework.cloud.config.server.EncryptionController; -import org.springframework.cloud.config.server.InvalidCipherException; -import org.springframework.cloud.config.server.KeyNotInstalledException; + +import org.springframework.cloud.config.server.encryption.SingleTextEncryptorLocator; import org.springframework.cloud.context.encrypt.KeyFormatException; import org.springframework.http.MediaType; import org.springframework.security.rsa.crypto.RsaSecretEncryptor; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + /** * @author Dave Syer * */ public class EncryptionControllerTests { - private EncryptionController controller = new EncryptionController(); + private SingleTextEncryptorLocator textEncryptorLocator = new SingleTextEncryptorLocator(); + private ConfigServerProperties properties = new ConfigServerProperties(); + private EncryptionController controller = new EncryptionController(textEncryptorLocator, properties); @Test(expected = KeyNotInstalledException.class) public void cannotDecryptWithoutKey() { @@ -69,38 +66,27 @@ public class EncryptionControllerTests { @Test public void sunnyDayRsaKey() { - controller.setEncryptor(new RsaSecretEncryptor()); + textEncryptorLocator.setEncryptor(new RsaSecretEncryptor()); String cipher = controller.encrypt("foo", MediaType.TEXT_PLAIN); assertEquals("foo", controller.decrypt(cipher, MediaType.TEXT_PLAIN)); } @Test public void publicKey() { - controller.setEncryptor(new RsaSecretEncryptor()); + textEncryptorLocator.setEncryptor(new RsaSecretEncryptor()); String key = controller.getPublicKey(); assertTrue("Wrong key format: " + key, key.startsWith("ssh-rsa")); } @Test public void formDataIn() { - controller.setEncryptor(new RsaSecretEncryptor()); + textEncryptorLocator.setEncryptor(new RsaSecretEncryptor()); // Add space to input String cipher = controller.encrypt("foo bar=", MediaType.APPLICATION_FORM_URLENCODED); String decrypt = controller.decrypt(cipher + "=", MediaType.APPLICATION_FORM_URLENCODED); assertEquals("Wrong decrypted plaintext: " + decrypt, "foo bar", decrypt); } - @Test - public void decryptEnvironment() { - controller.uploadKey("foo", MediaType.TEXT_PLAIN); - String cipher = controller.encrypt("foo", MediaType.TEXT_PLAIN); - Environment environment = new Environment("foo", "bar"); - environment.add(new PropertySource("spam", Collections - . singletonMap("my", "{cipher}" + cipher))); - Environment result = controller.decrypt(environment); - assertEquals("foo", result.getPropertySources().get(0).getSource().get("my")); - } - @Test public void randomizedCipher() { controller.uploadKey("foo", MediaType.TEXT_PLAIN); diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EnvironmentControllerIntegrationTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EnvironmentControllerIntegrationTests.java index 669d7065..4a98a570 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EnvironmentControllerIntegrationTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EnvironmentControllerIntegrationTests.java @@ -19,7 +19,10 @@ package org.springframework.cloud.config.server; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; + import org.springframework.cloud.config.environment.Environment; +import org.springframework.cloud.config.server.encryption.CipherEnvironmentEncryptor; +import org.springframework.cloud.config.server.encryption.SingleTextEncryptorLocator; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; @@ -38,7 +41,7 @@ public class EnvironmentControllerIntegrationTests { public void init() { Mockito.when(repository.getDefaultLabel()).thenReturn("master"); mvc = MockMvcBuilders.standaloneSetup( - new EnvironmentController(repository, new EncryptionController())) + new EnvironmentController(repository, new CipherEnvironmentEncryptor(new SingleTextEncryptorLocator()))) .build(); } diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EnvironmentControllerTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EnvironmentControllerTests.java index 22100990..edfdadc3 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EnvironmentControllerTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EnvironmentControllerTests.java @@ -28,6 +28,8 @@ import org.mockito.Mockito; import org.springframework.cloud.config.environment.Environment; import org.springframework.cloud.config.environment.PropertySource; +import org.springframework.cloud.config.server.encryption.CipherEnvironmentEncryptor; +import org.springframework.cloud.config.server.encryption.SingleTextEncryptorLocator; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @@ -55,7 +57,7 @@ public class EnvironmentControllerTests { @Before public void init() { Mockito.when(repository.getDefaultLabel()).thenReturn("master"); - this.controller = new EnvironmentController(repository, new EncryptionController()); + this.controller = new EnvironmentController(repository, new CipherEnvironmentEncryptor(new SingleTextEncryptorLocator())); } @Test diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/CipherEnvironmentEncryptorTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/CipherEnvironmentEncryptorTests.java new file mode 100644 index 00000000..c76a94db --- /dev/null +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/CipherEnvironmentEncryptorTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2015 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 + * + * http://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.util.Collections; + +import org.junit.Test; + +import org.springframework.cloud.config.environment.Environment; +import org.springframework.cloud.config.environment.PropertySource; +import org.springframework.cloud.context.encrypt.EncryptorFactory; +import org.springframework.security.crypto.encrypt.TextEncryptor; + +import static java.util.UUID.randomUUID; +import static org.junit.Assert.assertEquals; + +public class CipherEnvironmentEncryptorTests { + TextEncryptor textEncryptor = new EncryptorFactory().create("foo"); + TextEncryptorLocator textEncryptorLocator = new SingleTextEncryptorLocator(textEncryptor); + EnvironmentEncryptor encryptor = new CipherEnvironmentEncryptor(textEncryptorLocator); + + @Test + public void shouldDecryptEnvironment() { + // given + String secret = randomUUID().toString(); + + // when + Environment environment = new Environment("name", "profile", "label"); + environment.add(new PropertySource("a", + Collections.singletonMap(environment.getName(), "{cipher}" + textEncryptor.encrypt(secret)))); + + // then + assertEquals(secret, encryptor.decrypt(environment).getPropertySources().get(0).getSource().get(environment.getName())); + } +}