EnvironmentDecryptApplicationInitializerTests.java
/*
* Copyright 2013-present 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.bootstrap.encrypt;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.boot.test.util.TestPropertyValues.Type;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.security.crypto.encrypt.Encryptors;
import org.springframework.security.crypto.encrypt.TextEncryptor;
import static org.assertj.core.api.BDDAssertions.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.cloud.bootstrap.BootstrapApplicationListener.BOOTSTRAP_PROPERTY_SOURCE_NAME;
import static org.springframework.cloud.bootstrap.encrypt.EnvironmentDecryptApplicationInitializer.DECRYPTED_BOOTSTRAP_PROPERTY_SOURCE_NAME;
import static org.springframework.cloud.bootstrap.encrypt.EnvironmentDecryptApplicationInitializer.DECRYPTED_PROPERTY_SOURCE_NAME;
/**
* @author Dave Syer
* @author Biju Kunjummen
* @author Tim Ysewyn
*/
@ExtendWith(OutputCaptureExtension.class)
public class EnvironmentDecryptApplicationInitializerTests {
private EnvironmentDecryptApplicationInitializer listener = new EnvironmentDecryptApplicationInitializer(
Encryptors.noOpText());
@Test
public void decryptCipherKey() {
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext();
TestPropertyValues.of("spring.cloud.bootstrap.enabled=true", "foo: {cipher}bar").applyTo(context);
this.listener.initialize(context);
then(context.getEnvironment().getProperty("foo")).isEqualTo("bar");
}
@Test
public void relaxedBinding() {
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext();
TestPropertyValues.of("spring.cloud.bootstrap.enabled=true", "FOO_TEXT: {cipher}bar")
.applyTo(context.getEnvironment(), TestPropertyValues.Type.SYSTEM_ENVIRONMENT);
this.listener.initialize(context);
then(context.getEnvironment().getProperty("foo.text")).isEqualTo("bar");
}
@Test
public void propertySourcesOrderedCorrectly() {
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext();
TestPropertyValues.of("spring.cloud.bootstrap.enabled=true", "foo: {cipher}bar").applyTo(context);
context.getEnvironment()
.getPropertySources()
.addFirst(new MapPropertySource("test_override", Collections.singletonMap("foo", "{cipher}spam")));
this.listener.initialize(context);
then(context.getEnvironment().getProperty("foo")).isEqualTo("spam");
}
@Test
public void errorOnDecrypt(CapturedOutput output) {
this.listener = new EnvironmentDecryptApplicationInitializer(Encryptors.text("deadbeef", "AFFE37"));
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext();
TestPropertyValues.of("spring.cloud.bootstrap.enabled=true", "foo: {cipher}bar").applyTo(context);
// catch IllegalStateException and verify
try {
this.listener.initialize(context);
}
catch (Exception e) {
then(e).isInstanceOf(IllegalStateException.class);
}
// Assert logs contain warning even when exception thrown
String sysOutput = output.toString();
then(sysOutput).contains("Cannot decrypt: key=foo");
}
@Test
public void errorOnDecryptWithEmpty(CapturedOutput output) {
this.listener = new EnvironmentDecryptApplicationInitializer(Encryptors.text("deadbeef", "AFFE37"));
this.listener.setFailOnError(false);
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext();
TestPropertyValues.of("spring.cloud.bootstrap.enabled=true", "foo: {cipher}bar").applyTo(context);
this.listener.initialize(context);
// Assert logs contain warning
String sysOutput = output.toString();
then(sysOutput).contains("Cannot decrypt: key=foo");
// Empty is safest fallback for undecryptable cipher
then(context.getEnvironment().getProperty("foo")).isEqualTo("");
}
@Test
@SuppressWarnings("unchecked")
public void indexedPropertiesCopied() {
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext();
// tests that collections in another property source don't get copied into
// "decrypted" property source
TestPropertyValues
.of("spring.cloud.bootstrap.enabled=true", "yours[0].someValue: yourFoo", "yours[1].someValue: yourBar")
.applyTo(context);
// collection with some encrypted keys and some not encrypted
TestPropertyValues
.of("mine[0].someValue: Foo", "mine[0].someKey: {cipher}Foo0", "mine[1].someValue: Bar",
"mine[1].someKey: {cipher}Bar1", "nonindexed: nonindexval")
.applyTo(context.getEnvironment(), Type.MAP, "combinedTest");
this.listener.initialize(context);
then(context.getEnvironment().getProperty("mine[0].someValue")).isEqualTo("Foo");
then(context.getEnvironment().getProperty("mine[0].someKey")).isEqualTo("Foo0");
then(context.getEnvironment().getProperty("mine[1].someValue")).isEqualTo("Bar");
then(context.getEnvironment().getProperty("mine[1].someKey")).isEqualTo("Bar1");
then(context.getEnvironment().getProperty("yours[0].someValue")).isEqualTo("yourFoo");
then(context.getEnvironment().getProperty("yours[1].someValue")).isEqualTo("yourBar");
MutablePropertySources propertySources = context.getEnvironment().getPropertySources();
PropertySource<Map<?, ?>> decrypted = (PropertySource<Map<?, ?>>) propertySources
.get(DECRYPTED_PROPERTY_SOURCE_NAME);
then(decrypted.getSource().size()).as("decrypted property source had wrong size").isEqualTo(4);
}
@Test
public void testDecryptNonStandardParent() {
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext();
EnvironmentDecryptApplicationInitializer initializer = new EnvironmentDecryptApplicationInitializer(
Encryptors.noOpText());
TestPropertyValues.of("spring.cloud.bootstrap.enabled=true", "key:{cipher}value").applyTo(ctx);
ApplicationContext ctxParent = mock(ApplicationContext.class);
when(ctxParent.getEnvironment()).thenReturn(mock(Environment.class));
ctx.setParent(ctxParent);
initializer.initialize(ctx);
then(ctx.getEnvironment().getProperty("key")).isEqualTo("value");
}
@Test
public void testDecryptCompositePropertySource() {
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext();
TestPropertyValues.of("spring.cloud.bootstrap.enabled=true").applyTo(ctx);
EnvironmentDecryptApplicationInitializer initializer = new EnvironmentDecryptApplicationInitializer(
Encryptors.noOpText());
MapPropertySource devProfile = new MapPropertySource("dev-profile",
Collections.singletonMap("key", "{cipher}value1"));
MapPropertySource defaultProfile = new MapPropertySource("default-profile",
Collections.singletonMap("key", "{cipher}value2"));
CompositePropertySource cps = mock(CompositePropertySource.class);
when(cps.getName()).thenReturn("mock-composite-source");
when(cps.getPropertyNames()).thenReturn(devProfile.getPropertyNames());
when(cps.getPropertySources()).thenReturn(Arrays.asList(devProfile, defaultProfile));
ctx.getEnvironment().getPropertySources().addLast(cps);
initializer.initialize(ctx);
then(ctx.getEnvironment().getProperty("key")).isEqualTo("value1");
}
@Test
public void propertySourcesOrderedCorrectlyWithUnencryptedOverrides() {
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext();
TestPropertyValues.of("spring.cloud.bootstrap.enabled=true", "foo: {cipher}bar").applyTo(context);
context.getEnvironment()
.getPropertySources()
.addFirst(new MapPropertySource("test_override", Collections.singletonMap("foo", "spam")));
this.listener.initialize(context);
then(context.getEnvironment().getProperty("foo")).isEqualTo("spam");
}
@Test
public void doNotDecryptBootstrapTwice() {
TextEncryptor encryptor = mock(TextEncryptor.class);
when(encryptor.decrypt("bar")).thenReturn("bar");
when(encryptor.decrypt("bar2")).thenReturn("bar2");
when(encryptor.decrypt("bar3")).thenReturn("bar3");
when(encryptor.decrypt("baz")).thenReturn("baz");
EnvironmentDecryptApplicationInitializer initializer = new EnvironmentDecryptApplicationInitializer(encryptor);
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext();
TestPropertyValues.of("spring.cloud.bootstrap.enabled=true").applyTo(context);
CompositePropertySource bootstrap = new CompositePropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME);
bootstrap
.addPropertySource(new MapPropertySource("configService", Collections.singletonMap("foo", "{cipher}bar")));
context.getEnvironment().getPropertySources().addFirst(bootstrap);
Map<String, Object> props = new HashMap<>();
props.put("foo2", "{cipher}bar2");
props.put("bar", "{cipher}baz");
context.getEnvironment()
.getPropertySources()
.addAfter(BOOTSTRAP_PROPERTY_SOURCE_NAME, new MapPropertySource("remote", props));
initializer.initialize(context);
// Simulate retrieval of new properties via Spring Cloud Config
props.put("foo2", "{cipher}bar3");
context.getEnvironment().getPropertySources().replace("remote", new MapPropertySource("remote", props));
initializer.initialize(context);
verify(encryptor).decrypt("bar");
verify(encryptor).decrypt("bar2");
verify(encryptor).decrypt("bar3");
verify(encryptor, times(2)).decrypt("baz");
// Check if all encrypted properties are still decrypted
PropertySource<?> decryptedBootstrap = context.getEnvironment()
.getPropertySources()
.get(DECRYPTED_BOOTSTRAP_PROPERTY_SOURCE_NAME);
then(decryptedBootstrap.getProperty("foo")).isEqualTo("bar");
PropertySource<?> decrypted = context.getEnvironment().getPropertySources().get(DECRYPTED_PROPERTY_SOURCE_NAME);
then(decrypted.getProperty("foo2")).isEqualTo("bar3");
then(decrypted.getProperty("bar")).isEqualTo("baz");
}
@Test
public void testOnlyDecryptIfNotOverridden() {
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext();
TextEncryptor encryptor = mock(TextEncryptor.class);
when(encryptor.decrypt("bar2")).thenReturn("bar2");
EnvironmentDecryptApplicationInitializer initializer = new EnvironmentDecryptApplicationInitializer(encryptor);
TestPropertyValues.of("spring.cloud.bootstrap.enabled=true", "foo: {cipher}bar", "foo2: {cipher}bar2")
.applyTo(context);
context.getEnvironment()
.getPropertySources()
.addFirst(new MapPropertySource("test_override", Collections.singletonMap("foo", "spam")));
initializer.initialize(context);
then(context.getEnvironment().getProperty("foo")).isEqualTo("spam");
then(context.getEnvironment().getProperty("foo2")).isEqualTo("bar2");
verify(encryptor).decrypt("bar2");
verifyNoMoreInteractions(encryptor);
}
}