diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java index 037904b027..2bd9ee36f9 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java @@ -37,10 +37,16 @@ import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.logging.DeferredLogFactory; +import org.springframework.cloud.bootstrap.encrypt.KeyProperties; +import org.springframework.cloud.bootstrap.encrypt.RsaProperties; +import org.springframework.cloud.bootstrap.encrypt.TextEncryptorUtils; import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.context.encrypt.EncryptorFactory; import org.springframework.core.Ordered; import org.springframework.core.log.LogMessage; import org.springframework.retry.support.RetryTemplate; +import org.springframework.security.crypto.encrypt.TextEncryptor; +import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.web.client.RestTemplate; @@ -54,6 +60,8 @@ public class ConfigServerConfigDataLocationResolver * Prefix for Config Server imports. */ public static final String PREFIX = "configserver:"; + static final boolean RSA_IS_PRESENT = ClassUtils + .isPresent("org.springframework.security.rsa.crypto.RsaSecretEncryptor", null); private final Log log; @@ -66,6 +74,41 @@ public int getOrder() { return -1; } + /* + * Depending on whether encrypt.key is set as an environment or system property we may + * have created a TextEncryptor implementation or just created a + * FailsafeTextEncryptor. This is because when TextEncryptorConfigBootstrapper runs we + * have not yet loaded any configuration files (application.yaml | properties etc). + * However, at this point when the ConfigServerConfigDataLocationResolver is resolving + * configuration we would have resolved the configuration files on the classpath at + * least so we can potentially create a properly configured TextEncryptor. So if the + * FailsafeTextEncryptor is in the context and we can create a TextEncryptor then we + * set the delegate in the FailsafeTextEncryptor so that we can decrypt any encrypted + * properties at this point. + */ + protected void setTextEncryptorDelegate(ConfigDataLocationResolverContext context) { + if (context.getBootstrapContext().isRegistered(TextEncryptor.class)) { + Binder binder = context.getBinder(); + KeyProperties keyProperties = binder.bindOrCreate(KeyProperties.PREFIX, Bindable.of(KeyProperties.class)); + boolean textEncryptorRegistered = context.getBootstrapContext().isRegistered(TextEncryptor.class); + if (TextEncryptorUtils.keysConfigured(keyProperties) && textEncryptorRegistered) { + TextEncryptor textEncryptor = context.getBootstrapContext().get(TextEncryptor.class); + if (textEncryptor instanceof TextEncryptorUtils.FailsafeTextEncryptor failsafeTextEncryptor) { + TextEncryptor delegate; + if (RSA_IS_PRESENT) { + RsaProperties rsaProperties = binder.bindOrCreate(RsaProperties.PREFIX, + Bindable.of(RsaProperties.class)); + delegate = TextEncryptorUtils.createTextEncryptor(keyProperties, rsaProperties); + } + else { + delegate = new EncryptorFactory(keyProperties.getSalt()).create(keyProperties.getKey()); + } + failsafeTextEncryptor.setDelegate(delegate); + } + } + } + } + protected PropertyHolder loadProperties(ConfigDataLocationResolverContext context, String uris) { Binder binder = context.getBinder(); BindHandler bindHandler = getBindHandler(context); @@ -182,6 +225,7 @@ public List resolve(ConfigDataLocationResolverCo public List resolveProfileSpecific( ConfigDataLocationResolverContext resolverContext, ConfigDataLocation location, Profiles profiles) throws ConfigDataLocationNotFoundException { + setTextEncryptorDelegate(resolverContext); String uris = location.getNonPrefixedValue(getPrefix()); PropertyHolder propertyHolder = loadProperties(resolverContext, uris); ConfigClientProperties properties = propertyHolder.properties; @@ -269,14 +313,6 @@ public List getConfigServerInstances(String serviceId) { return locations; } - private class PropertyHolder { - - ConfigClientProperties properties; - - RetryProperties retryProperties; - - } - public static class PropertyResolver { private final Binder binder; @@ -302,4 +338,12 @@ public T resolveOrCreateConfigurationProperties(String prefix, Class type } + private class PropertyHolder { + + ConfigClientProperties properties; + + RetryProperties retryProperties; + + } + } diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolverTests.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolverTests.java index f2f3c82261..b0836f4e34 100644 --- a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolverTests.java +++ b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolverTests.java @@ -21,14 +21,23 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.factory.support.InstanceSupplier; +import org.springframework.boot.BootstrapRegistry; import org.springframework.boot.ConfigurableBootstrapContext; import org.springframework.boot.context.config.ConfigDataLocation; import org.springframework.boot.context.config.ConfigDataLocationResolverContext; import org.springframework.boot.context.config.Profiles; +import org.springframework.boot.context.properties.bind.BindHandler; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.logging.DeferredLog; +import org.springframework.cloud.bootstrap.TextEncryptorBindHandler; +import org.springframework.cloud.bootstrap.encrypt.KeyProperties; +import org.springframework.cloud.bootstrap.encrypt.TextEncryptorUtils; +import org.springframework.cloud.context.encrypt.EncryptorFactory; import org.springframework.mock.env.MockEnvironment; +import org.springframework.security.crypto.encrypt.TextEncryptor; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.eq; @@ -205,6 +214,65 @@ void multipleImportEntriesDoesNotShareSameURIs() { assertThat(resource2.getProperties().getUri()).isEqualTo(new String[] { "http://urlNo2" }); } + @Test + void setFailsafeDelegateKeysNotConfigured() { + ConfigurableBootstrapContext bootstrapContext = mock(ConfigurableBootstrapContext.class); + when(bootstrapContext.isRegistered(eq(ConfigClientProperties.class))).thenReturn(true); + KeyProperties keyProperties = new KeyProperties(); + ConfigClientProperties configClientProperties = new ConfigClientProperties(); + configClientProperties.setUri(new String[] { "http://myuri" }); + when(bootstrapContext.isRegistered(TextEncryptor.class)).thenReturn(true); + when(bootstrapContext.get(TextEncryptor.class)).thenReturn(new TextEncryptorUtils.FailsafeTextEncryptor()); + when(bootstrapContext.get(eq(ConfigClientProperties.class))).thenReturn(configClientProperties); + when(context.getBootstrapContext()).thenReturn(bootstrapContext); + this.resolver.resolve(context, ConfigDataLocation.of("configserver:http://urlNo1")); + TextEncryptor textEncryptor = bootstrapContext.get(TextEncryptor.class); + assertThat(textEncryptor).isInstanceOf(TextEncryptorUtils.FailsafeTextEncryptor.class); + assertThat(((TextEncryptorUtils.FailsafeTextEncryptor) textEncryptor).getDelegate()).isNull(); + } + + @Test + void setFailsafeDelegateKeysConfigured() { + ConfigurableBootstrapContext bootstrapContext = mock(ConfigurableBootstrapContext.class); + when(bootstrapContext.isRegistered(eq(ConfigClientProperties.class))).thenReturn(true); + environment.setProperty("encrypt.key", "mykey"); + + // The value is "password" encrypted with the key "mykey" + environment.setProperty("spring.cloud.config.password", + "{cipher}6defc102cd76752fcf4c78231ed82ead85133a09741d9a1442595b4800e2b3d1"); + ConfigClientProperties configClientProperties = new ConfigClientProperties(); + configClientProperties.setUri(new String[] { "http://myuri" }); + when(bootstrapContext.isRegistered(TextEncryptor.class)).thenReturn(true); + when(bootstrapContext.get(TextEncryptor.class)).thenReturn(new TextEncryptorUtils.FailsafeTextEncryptor()); + when(bootstrapContext.get(eq(ConfigClientProperties.class))).thenReturn(configClientProperties); + when(context.getBootstrapContext()).thenReturn(bootstrapContext); + KeyProperties keyProperties = new KeyProperties(); + keyProperties.setKey("mykey"); + + // Use this TextEncryptor in the BindHandler we return so it will decrypt the + // password when we bind ConfigClientProperties + TextEncryptor bindHandlerTextEncryptor = new EncryptorFactory(keyProperties.getSalt()) + .create(keyProperties.getKey()); + when(context.getBootstrapContext().getOrElse(eq(BindHandler.class), eq(null))) + .thenReturn(new TextEncryptorBindHandler(bindHandlerTextEncryptor, keyProperties)); + + // Call resolve so we can test that the delegate is added to the + // FailsafeTextEncryptor + this.resolver.resolve(context, ConfigDataLocation.of("configserver:http://urlNo1")); + TextEncryptor textEncryptor = bootstrapContext.get(TextEncryptor.class); + + // Capture the ConfigClientProperties we create and register in + // ConfigServerConfigDataLocationResolver.resolveProfileSpecific + // it should have the decrypted passord in it + ArgumentCaptor> captor = ArgumentCaptor + .forClass(BootstrapRegistry.InstanceSupplier.class); + verify(bootstrapContext).register(eq(ConfigClientProperties.class), captor.capture()); + assertThat(captor.getValue().get(bootstrapContext).getPassword()).isEqualTo("password"); + assertThat(textEncryptor).isInstanceOf(TextEncryptorUtils.FailsafeTextEncryptor.class); + assertThat(((TextEncryptorUtils.FailsafeTextEncryptor) textEncryptor).getDelegate()) + .isInstanceOf(TextEncryptor.class); + } + private ConfigServerConfigDataResource testUri(String propertyUri, String locationUri) { this.environment.setProperty(ConfigClientProperties.PREFIX + ".uri", propertyUri); when(context.getBootstrapContext()).thenReturn(mock(ConfigurableBootstrapContext.class));