diff --git a/spring-cloud-vault-config-consul/src/main/java/org/springframework/cloud/vault/config/consul/ConsulBackendMetadata.java b/spring-cloud-vault-config-consul/src/main/java/org/springframework/cloud/vault/config/consul/ConsulBackendMetadata.java index 75a46312..476ee099 100644 --- a/spring-cloud-vault-config-consul/src/main/java/org/springframework/cloud/vault/config/consul/ConsulBackendMetadata.java +++ b/spring-cloud-vault-config-consul/src/main/java/org/springframework/cloud/vault/config/consul/ConsulBackendMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2013-2020 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. @@ -22,9 +22,9 @@ import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder; import org.springframework.cloud.vault.config.LeasingSecretBackendMetadata; -import org.springframework.cloud.vault.config.PropertyNameTransformer; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.vault.core.lease.SecretLeaseContainer; import org.springframework.vault.core.lease.domain.RequestedSecret; import org.springframework.vault.core.lease.event.SecretLeaseCreatedEvent; @@ -35,17 +35,19 @@ import org.springframework.vault.core.util.PropertyTransformer; */ class ConsulBackendMetadata implements LeasingSecretBackendMetadata { - private final Log log = LogFactory - .getLog(getClass()); + private final Log log = LogFactory.getLog(getClass()); private final VaultConsulProperties properties; - private final PropertyNameTransformer transformer; - private final ConfigurationPropertiesRebinder rebinder; - public ConsulBackendMetadata(VaultConsulProperties properties, PropertyNameTransformer transformer, ConfigurationPropertiesRebinder rebinder) { + private final PropertyTransformer transformer; + + private final ApplicationEventPublisher eventPublisher; + + ConsulBackendMetadata(VaultConsulProperties properties, + PropertyTransformer transformer, ApplicationEventPublisher eventPublisher) { this.properties = properties; this.transformer = transformer; - this.rebinder = rebinder; + this.eventPublisher = eventPublisher; } @Override @@ -66,8 +68,7 @@ class ConsulBackendMetadata implements LeasingSecretBackendMetadata { Map variables = new HashMap<>(); variables.put("backend", this.properties.getBackend()); - variables - .put("key", String.format("creds/%s", this.properties.getRole())); + variables.put("key", String.format("creds/%s", this.properties.getRole())); return variables; } @@ -83,28 +84,30 @@ class ConsulBackendMetadata implements LeasingSecretBackendMetadata { } @Override - public void afterRegistration(RequestedSecret secret, SecretLeaseContainer container) { - + public void afterRegistration(RequestedSecret secret, + SecretLeaseContainer container) { container.addLeaseListener(leaseEvent -> { - if (leaseEvent - .getSource() == secret && leaseEvent instanceof SecretLeaseCreatedEvent) { - rebind("consulDiscoveryProperties"); - rebind("consulConfigProperties"); + if (leaseEvent.getSource() == secret + && leaseEvent instanceof SecretLeaseCreatedEvent) { + if (this.eventPublisher != null) { + if (log.isDebugEnabled()) { + log.debug("Publishing a RebindConsulEvent"); + } + this.eventPublisher.publishEvent(new RebindConsulEvent(this)); + } } }); - // initial rebind after requesting these properties - rebind("consulDiscoveryProperties"); - rebind("consulConfigProperties"); + // no need to rebind here since the transformer creats all appropriate properties. } - private void rebind(String bean) { + public static class RebindConsulEvent extends ApplicationEvent { - boolean success = this.rebinder.rebind(bean); - if (this.log.isInfoEnabled()) { - this.log.info(String - .format("Attempted to rebind Consul bean '%s' with updated ACL token from vault, success: %s", bean, success)); + RebindConsulEvent(Object source) { + super(source); } + } + } diff --git a/spring-cloud-vault-config-consul/src/main/java/org/springframework/cloud/vault/config/consul/VaultConfigConsulAutoConfiguration.java b/spring-cloud-vault-config-consul/src/main/java/org/springframework/cloud/vault/config/consul/VaultConfigConsulAutoConfiguration.java new file mode 100644 index 00000000..f25877c5 --- /dev/null +++ b/spring-cloud-vault-config-consul/src/main/java/org/springframework/cloud/vault/config/consul/VaultConfigConsulAutoConfiguration.java @@ -0,0 +1,91 @@ +/* + * Copyright 2016-2020 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.vault.config.consul; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder; +import org.springframework.cloud.vault.config.SecretBackendMetadataFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Bootstrap configuration providing support for the Consul secret backend. + * + * @author Mark Paluch + */ +@Configuration(proxyBeanMethods = false) +public class VaultConfigConsulAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public ConsulSecretRebindListener consulSecretRebindListener( + ConfigurationPropertiesRebinder rebinder, + ConfigurableApplicationContext context) { + // TODO: some other way? Maybe a BootstrapApplicationContextHolder bean + // provided by spring cloud commons + ApplicationContext parent = context.getParent(); + if (parent != null) { + context = (ConfigurableApplicationContext) parent; + } + return new ConsulSecretRebindListener(rebinder, context); + } + + /** + * {@link SecretBackendMetadataFactory} for Consul integration using + * {@link VaultConsulProperties}. + */ + public static class ConsulSecretRebindListener + implements ApplicationListener { + + private final Log log = LogFactory.getLog(getClass()); + + private final ConfigurationPropertiesRebinder rebinder; + + public ConsulSecretRebindListener(ConfigurationPropertiesRebinder rebinder, + ConfigurableApplicationContext context) { + this.rebinder = rebinder; + context.addApplicationListener(this); + } + + @Override + public void onApplicationEvent(ConsulBackendMetadata.RebindConsulEvent event) { + if (log.isDebugEnabled()) { + log.debug("received RebindConsulEvent"); + } + rebind("consulDiscoveryProperties"); + rebind("consulConfigProperties"); + } + + private void rebind(String bean) { + + boolean success = this.rebinder.rebind(bean); + if (this.log.isInfoEnabled()) { + this.log.info(String.format( + "Attempted to rebind Consul bean '%s' with updated ACL token from vault, success: %s", + bean, success)); + } + } + + } + +} diff --git a/spring-cloud-vault-config-consul/src/main/java/org/springframework/cloud/vault/config/consul/VaultConfigConsulBootstrapConfiguration.java b/spring-cloud-vault-config-consul/src/main/java/org/springframework/cloud/vault/config/consul/VaultConfigConsulBootstrapConfiguration.java index f1a58357..a15b932e 100644 --- a/spring-cloud-vault-config-consul/src/main/java/org/springframework/cloud/vault/config/consul/VaultConfigConsulBootstrapConfiguration.java +++ b/spring-cloud-vault-config-consul/src/main/java/org/springframework/cloud/vault/config/consul/VaultConfigConsulBootstrapConfiguration.java @@ -16,19 +16,19 @@ package org.springframework.cloud.vault.config.consul; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; +import java.util.LinkedHashMap; +import java.util.Map; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder; -import org.springframework.cloud.vault.config.PropertyNameTransformer; import org.springframework.cloud.vault.config.SecretBackendMetadata; import org.springframework.cloud.vault.config.SecretBackendMetadataFactory; import org.springframework.cloud.vault.config.VaultSecretBackendDescriptor; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.util.Assert; +import org.springframework.vault.core.util.PropertyTransformer; /** * Bootstrap configuration providing support for the Consul secret backend. @@ -41,8 +41,9 @@ public class VaultConfigConsulBootstrapConfiguration { @Bean @ConditionalOnMissingBean - public ConsulSecretBackendMetadataFactory consulSecretBackendAccessorFactory(ConfigurationPropertiesRebinder rebinder) { - return new ConsulSecretBackendMetadataFactory(rebinder); + public ConsulSecretBackendMetadataFactory consulSecretBackendMetadataFactory( + ApplicationContext context) { + return new ConsulSecretBackendMetadataFactory(context); } /** @@ -52,10 +53,14 @@ public class VaultConfigConsulBootstrapConfiguration { public static class ConsulSecretBackendMetadataFactory implements SecretBackendMetadataFactory { - private final ConfigurationPropertiesRebinder rebinder; + private final ApplicationContext context; - public ConsulSecretBackendMetadataFactory(ConfigurationPropertiesRebinder rebinder) { - this.rebinder = rebinder; + public ConsulSecretBackendMetadataFactory(ApplicationContext context) { + this.context = context; + } + + public ApplicationContext getContext() { + return this.context; } /** @@ -69,10 +74,26 @@ public class VaultConfigConsulBootstrapConfiguration { Assert.notNull(properties, "VaultConsulProperties must not be null"); - PropertyNameTransformer transformer = new PropertyNameTransformer(); - transformer.addKeyTransformation("token", properties.getTokenProperty()); + /* + * PropertyNameTransformer transformer = new PropertyNameTransformer(); + * transformer.addKeyTransformation("token", properties.getTokenProperty()); + */ - return new ConsulBackendMetadata(properties, transformer, this.rebinder); + // spring.cloud.consul.token is a shortcut for initialization + // for this I couldn't get it to work without specifiying each property + // specifically + PropertyTransformer transformer = input -> { + + Map transformed = new LinkedHashMap<>(); + transformed.put("spring.cloud.consul.config.acl-token", + input.get("token")); + transformed.put("spring.cloud.consul.discovery.acl-token", + input.get("token")); + + return transformed; + }; + + return new ConsulBackendMetadata(properties, transformer, context); } @Override diff --git a/spring-cloud-vault-config-consul/src/main/resources/META-INF/spring.factories b/spring-cloud-vault-config-consul/src/main/resources/META-INF/spring.factories index eaef6195..560a27eb 100644 --- a/spring-cloud-vault-config-consul/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-vault-config-consul/src/main/resources/META-INF/spring.factories @@ -1,3 +1,6 @@ # Bootstrap Configuration org.springframework.cloud.bootstrap.BootstrapConfiguration=\ -org.springframework.cloud.vault.config.consul.VaultConfigConsulBootstrapConfiguration \ No newline at end of file +org.springframework.cloud.vault.config.consul.VaultConfigConsulBootstrapConfiguration + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.vault.config.consul.VaultConfigConsulAutoConfiguration \ No newline at end of file diff --git a/spring-cloud-vault-config-consul/src/test/java/org/springframework/cloud/vault/config/consul/ConsulSecretIntegrationTests.java b/spring-cloud-vault-config-consul/src/test/java/org/springframework/cloud/vault/config/consul/ConsulSecretIntegrationTests.java index 1601d977..844a8f75 100644 --- a/spring-cloud-vault-config-consul/src/test/java/org/springframework/cloud/vault/config/consul/ConsulSecretIntegrationTests.java +++ b/spring-cloud-vault-config-consul/src/test/java/org/springframework/cloud/vault/config/consul/ConsulSecretIntegrationTests.java @@ -131,7 +131,8 @@ public class ConsulSecretIntegrationTests extends IntegrationTestSupport { @Test public void shouldCreateCredentialsCorrectly() { - ConsulSecretBackendMetadataFactory factory = new ConsulSecretBackendMetadataFactory(null); + ConsulSecretBackendMetadataFactory factory = new ConsulSecretBackendMetadataFactory( + null); Map secretProperties = this.configOperations .read(factory.forConsul(this.consul)).getData(); diff --git a/spring-cloud-vault-config-consul/src/test/java/org/springframework/cloud/vault/config/consul/VaultConfigConsulBootstrapConfigurationTests.java b/spring-cloud-vault-config-consul/src/test/java/org/springframework/cloud/vault/config/consul/VaultConfigConsulBootstrapConfigurationTests.java index ed050040..895c08e3 100644 --- a/spring-cloud-vault-config-consul/src/test/java/org/springframework/cloud/vault/config/consul/VaultConfigConsulBootstrapConfigurationTests.java +++ b/spring-cloud-vault-config-consul/src/test/java/org/springframework/cloud/vault/config/consul/VaultConfigConsulBootstrapConfigurationTests.java @@ -69,7 +69,7 @@ public class VaultConfigConsulBootstrapConfigurationTests extends IntegrationTes @ConditionalOnProperty("VaultConfigConsulBootstrapConfigurationTests.custom.config") ConsulSecretBackendMetadataFactory customFactory(ConfigurationPropertiesRebinder rebinder) { - return new ConsulSecretBackendMetadataFactory(rebinder) { + return new ConsulSecretBackendMetadataFactory(null) { @Override public SecretBackendMetadata createMetadata( VaultConsulProperties backendDescriptor) { diff --git a/spring-cloud-vault-config-consul/src/test/java/org/springframework/cloud/vault/config/consul/VaultConfigConsulTests.java b/spring-cloud-vault-config-consul/src/test/java/org/springframework/cloud/vault/config/consul/VaultConfigConsulTests.java index 6c67421c..23b15aa2 100644 --- a/spring-cloud-vault-config-consul/src/test/java/org/springframework/cloud/vault/config/consul/VaultConfigConsulTests.java +++ b/spring-cloud-vault-config-consul/src/test/java/org/springframework/cloud/vault/config/consul/VaultConfigConsulTests.java @@ -31,10 +31,12 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.consul.config.ConsulConfigProperties; import org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties; import org.springframework.cloud.vault.util.CanConnect; import org.springframework.cloud.vault.util.VaultRule; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.env.Environment; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -58,8 +60,9 @@ import static org.junit.Assume.assumeTrue; */ @RunWith(SpringRunner.class) @SpringBootTest(classes = VaultConfigConsulTests.TestApplication.class, - properties = {"spring.cloud.vault.consul.enabled=true", - "spring.cloud.vault.consul.role=readonly"}) + properties = { "spring.cloud.vault.consul.enabled=true", + "spring.cloud.vault.consul.role=readonly", + "spring.cloud.consul.discovery.catalog-services-watch.enabled=false" }) public class VaultConfigConsulTests { private static final String CONSUL_HOST = "localhost"; @@ -76,12 +79,21 @@ public class VaultConfigConsulTests { private static final ParameterizedTypeReference> STRING_MAP = new ParameterizedTypeReference>() { }; - @Value("${spring.cloud.consul.token}") - String token; + @Value("${spring.cloud.consul.discovery.acl-token:}") + String discoveryToken; + + @Value("${spring.cloud.consul.config.acl-token:}") + String configToken; + + @Autowired + Environment env; @Autowired ConsulDiscoveryProperties discoveryProperties; + @Autowired + ConsulConfigProperties configProperties; + /** * Initialize the consul secret backend. */ @@ -132,22 +144,25 @@ public class VaultConfigConsulTests { } } - @Test - public void shouldHaveToken() { - assertThat(this.token).isNotEmpty(); - assertThat(this.discoveryProperties.getAclToken()).isEqualTo(this.token); - } + /* + * @Test public void shouldHaveToken() { assertThat(this.token).isNotEmpty(); + * assertThat(this.discoveryProperties.getAclToken()).isEqualTo(this.token); } + */ @Test public void shouldHaveRenewedToken() throws InterruptedException { - - assertThat(this.token).isNotEmpty(); - assertThat(this.discoveryProperties.getAclToken()).isEqualTo(this.token); + assertThat(configToken).isNotEmpty(); + assertThat(discoveryToken).isNotEmpty(); + assertThat(this.configProperties.getAclToken()).isEqualTo(configToken); + assertThat(this.discoveryProperties.getAclToken()).isEqualTo(discoveryToken); Thread.sleep(20_000L); // TODO: The properties weren't rebound so this test fails. - assertThat(this.discoveryProperties.getAclToken()).isNotEmpty().isNotEqualTo(this.token); + assertThat(this.configProperties.getAclToken()).isNotEmpty() + .isNotEqualTo(configToken); + assertThat(this.discoveryProperties.getAclToken()).isNotEmpty() + .isNotEqualTo(discoveryToken); } @SpringBootApplication diff --git a/spring-cloud-vault-config-consul/src/test/resources/META-INF/spring.factories b/spring-cloud-vault-config-consul/src/test/resources/META-INF/spring.factories index 673a40fa..4625be75 100644 --- a/spring-cloud-vault-config-consul/src/test/resources/META-INF/spring.factories +++ b/spring-cloud-vault-config-consul/src/test/resources/META-INF/spring.factories @@ -1,4 +1,3 @@ # Bootstrap Configuration org.springframework.cloud.bootstrap.BootstrapConfiguration=\ -org.springframework.cloud.vault.config.consul.VaultConfigConsulBootstrapConfigurationTests.CustomBootstrapConfiguration,\ -org.springframework.cloud.vault.config.consul.VaultConfigConsulBootstrapConfiguration +org.springframework.cloud.vault.config.consul.VaultConfigConsulBootstrapConfigurationTests.CustomBootstrapConfiguration