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 new file mode 100644 index 00000000..75a46312 --- /dev/null +++ b/spring-cloud-vault-config-consul/src/main/java/org/springframework/cloud/vault/config/consul/ConsulBackendMetadata.java @@ -0,0 +1,110 @@ +/* + * Copyright 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 java.util.HashMap; +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.vault.core.lease.SecretLeaseContainer; +import org.springframework.vault.core.lease.domain.RequestedSecret; +import org.springframework.vault.core.lease.event.SecretLeaseCreatedEvent; +import org.springframework.vault.core.util.PropertyTransformer; + +/** + * @author Mark Paluch + */ +class ConsulBackendMetadata implements LeasingSecretBackendMetadata { + + 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) { + this.properties = properties; + this.transformer = transformer; + this.rebinder = rebinder; + } + + @Override + public String getName() { + return String.format("%s with Role %s", this.properties.getBackend(), + this.properties.getRole()); + } + + @Override + public String getPath() { + return String.format("%s/creds/%s", this.properties.getBackend(), + this.properties.getRole()); + } + + @Override + public Map getVariables() { + + Map variables = new HashMap<>(); + + variables.put("backend", this.properties.getBackend()); + variables + .put("key", String.format("creds/%s", this.properties.getRole())); + + return variables; + } + + @Override + public PropertyTransformer getPropertyTransformer() { + return this.transformer; + } + + @Override + public RequestedSecret.Mode getLeaseMode() { + return RequestedSecret.Mode.ROTATE; + } + + @Override + public void afterRegistration(RequestedSecret secret, SecretLeaseContainer container) { + + container.addLeaseListener(leaseEvent -> { + + if (leaseEvent + .getSource() == secret && leaseEvent instanceof SecretLeaseCreatedEvent) { + rebind("consulDiscoveryProperties"); + rebind("consulConfigProperties"); + } + }); + + // initial rebind after requesting these properties + 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 d54c0be5..f1a58357 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,11 +16,12 @@ package org.springframework.cloud.vault.config.consul; -import java.util.HashMap; -import java.util.Map; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; 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; @@ -28,7 +29,6 @@ import org.springframework.cloud.vault.config.VaultSecretBackendDescriptor; 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,8 @@ public class VaultConfigConsulBootstrapConfiguration { @Bean @ConditionalOnMissingBean - public ConsulSecretBackendMetadataFactory consulSecretBackendAccessorFactory() { - return new ConsulSecretBackendMetadataFactory(); + public ConsulSecretBackendMetadataFactory consulSecretBackendAccessorFactory(ConfigurationPropertiesRebinder rebinder) { + return new ConsulSecretBackendMetadataFactory(rebinder); } /** @@ -52,6 +52,12 @@ public class VaultConfigConsulBootstrapConfiguration { public static class ConsulSecretBackendMetadataFactory implements SecretBackendMetadataFactory { + private final ConfigurationPropertiesRebinder rebinder; + + public ConsulSecretBackendMetadataFactory(ConfigurationPropertiesRebinder rebinder) { + this.rebinder = rebinder; + } + /** * Creates a {@link SecretBackendMetadata} for a secret backend using * {@link VaultConsulProperties}. This accessor transforms Vault's token property @@ -59,43 +65,14 @@ public class VaultConfigConsulBootstrapConfiguration { * @param properties must not be {@literal null}. * @return the {@link SecretBackendMetadata} */ - static SecretBackendMetadata forConsul(final VaultConsulProperties properties) { + SecretBackendMetadata forConsul(VaultConsulProperties properties) { Assert.notNull(properties, "VaultConsulProperties must not be null"); PropertyNameTransformer transformer = new PropertyNameTransformer(); transformer.addKeyTransformation("token", properties.getTokenProperty()); - return new SecretBackendMetadata() { - - @Override - public String getName() { - return String.format("%s with Role %s", properties.getBackend(), - properties.getRole()); - } - - @Override - public String getPath() { - return String.format("%s/creds/%s", properties.getBackend(), - properties.getRole()); - } - - @Override - public Map getVariables() { - - Map variables = new HashMap<>(); - - variables.put("backend", properties.getBackend()); - variables.put("key", String.format("creds/%s", properties.getRole())); - - return variables; - } - - @Override - public PropertyTransformer getPropertyTransformer() { - return transformer; - } - }; + return new ConsulBackendMetadata(properties, transformer, this.rebinder); } @Override 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 7905a00b..1601d977 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 @@ -27,6 +27,7 @@ import org.junit.Test; import org.springframework.cloud.vault.config.VaultConfigOperations; import org.springframework.cloud.vault.config.VaultConfigTemplate; import org.springframework.cloud.vault.config.VaultProperties; +import org.springframework.cloud.vault.config.consul.VaultConfigConsulBootstrapConfiguration.ConsulSecretBackendMetadataFactory; import org.springframework.cloud.vault.util.CanConnect; import org.springframework.cloud.vault.util.IntegrationTestSupport; import org.springframework.cloud.vault.util.Settings; @@ -43,7 +44,6 @@ import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; -import static org.springframework.cloud.vault.config.consul.VaultConfigConsulBootstrapConfiguration.ConsulSecretBackendMetadataFactory.forConsul; /** * Integration tests for {@link VaultConfigTemplate} using the consul secret backend. This @@ -131,8 +131,9 @@ public class ConsulSecretIntegrationTests extends IntegrationTestSupport { @Test public void shouldCreateCredentialsCorrectly() { + ConsulSecretBackendMetadataFactory factory = new ConsulSecretBackendMetadataFactory(null); Map secretProperties = this.configOperations - .read(forConsul(this.consul)).getData(); + .read(factory.forConsul(this.consul)).getData(); assertThat(secretProperties).containsKeys("spring.cloud.consul.token"); } 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 dd74a4e5..ed050040 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 @@ -22,6 +22,7 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder; import org.springframework.cloud.vault.config.GenericSecretBackendMetadata; import org.springframework.cloud.vault.config.SecretBackendMetadata; import org.springframework.cloud.vault.config.consul.VaultConfigConsulBootstrapConfiguration.ConsulSecretBackendMetadataFactory; @@ -66,9 +67,9 @@ public class VaultConfigConsulBootstrapConfigurationTests extends IntegrationTes @Bean @ConditionalOnProperty("VaultConfigConsulBootstrapConfigurationTests.custom.config") - ConsulSecretBackendMetadataFactory customFactory() { + ConsulSecretBackendMetadataFactory customFactory(ConfigurationPropertiesRebinder rebinder) { - return new ConsulSecretBackendMetadataFactory() { + return new ConsulSecretBackendMetadataFactory(rebinder) { @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 39f0a012..6c67421c 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 @@ -18,18 +18,20 @@ package org.springframework.cloud.vault.config.consul; import java.net.InetSocketAddress; import java.util.Base64; -import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; 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.discovery.ConsulDiscoveryProperties; import org.springframework.cloud.vault.util.CanConnect; import org.springframework.cloud.vault.util.VaultRule; import org.springframework.core.ParameterizedTypeReference; @@ -56,8 +58,8 @@ 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"}) public class VaultConfigConsulTests { private static final String CONSUL_HOST = "localhost"; @@ -77,6 +79,9 @@ public class VaultConfigConsulTests { @Value("${spring.cloud.consul.token}") String token; + @Autowired + ConsulDiscoveryProperties discoveryProperties; + /** * Initialize the consul secret backend. */ @@ -111,8 +116,12 @@ public class VaultConfigConsulTests { vaultOperations.write("consul/config/access", consulAccess); - vaultOperations.write("consul/roles/readonly", Collections.singletonMap( - "policy", new String(Base64.getEncoder().encode(POLICY.getBytes())))); + Map role = new LinkedHashMap<>(); + role.put("policy", new String(Base64.getEncoder().encode(POLICY.getBytes()))); + role.put("ttl", "15s"); + role.put("max_ttl", "15s"); + + vaultOperations.write("consul/roles/readonly", role); } catch (HttpStatusCodeException e) { @@ -126,6 +135,19 @@ public class VaultConfigConsulTests { @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); + + Thread.sleep(20_000L); + + // TODO: The properties weren't rebound so this test fails. + assertThat(this.discoveryProperties.getAclToken()).isNotEmpty().isNotEqualTo(this.token); } @SpringBootApplication diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/LeasingSecretBackendMetadata.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/LeasingSecretBackendMetadata.java index 25e18cb0..93353628 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/LeasingSecretBackendMetadata.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/LeasingSecretBackendMetadata.java @@ -16,6 +16,8 @@ package org.springframework.cloud.vault.config; +import org.springframework.vault.core.lease.SecretLeaseContainer; +import org.springframework.vault.core.lease.domain.RequestedSecret; import org.springframework.vault.core.lease.domain.RequestedSecret.Mode; /** @@ -37,4 +39,27 @@ public interface LeasingSecretBackendMetadata extends SecretBackendMetadata { */ Mode getLeaseMode(); + /** + * Callback method before registering a {@link RequestedSecret secret} with {@link SecretLeaseContainer}. + * Registering a {@code before} callback allows event consumption before the secrets are visible in the associated property source. + * + * @param secret the requested secret. + * @param container the lease container that was used to request the secret. + * @since 3.0 + */ + default void beforeRegistration(RequestedSecret secret, SecretLeaseContainer container) { + } + + /** + * Callback method after registering a {@link RequestedSecret secret} with {@link SecretLeaseContainer}. + * Registering a {@code after} callback allows event consumption after the secrets are visible in the associated property source. + * Note that this callback does not necessarily guarantee notification of the initial secrets retrieval. + * + * @param secret the requested secret. + * @param container the lease container that was used to request the secret. + * @since 3.0 + */ + default void afterRegistration(RequestedSecret secret, SecretLeaseContainer container) { + } + } diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceLocator.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceLocator.java index 8592b87b..57fc2113 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceLocator.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceLocator.java @@ -147,8 +147,21 @@ class LeasingVaultPropertySourceLocator extends VaultPropertySourceLocatorSuppor private PropertySource createVaultPropertySource(RequestedSecret secret, SecretBackendMetadata accessor) { - return new LeaseAwareVaultPropertySource(accessor.getName(), + if (accessor instanceof LeasingSecretBackendMetadata) { + ((LeasingSecretBackendMetadata) accessor) + .beforeRegistration(secret, this.secretLeaseContainer); + } + + LeaseAwareVaultPropertySource propertySource = new LeaseAwareVaultPropertySource(accessor + .getName(), this.secretLeaseContainer, secret, accessor.getPropertyTransformer()); + + if (accessor instanceof LeasingSecretBackendMetadata) { + ((LeasingSecretBackendMetadata) accessor) + .afterRegistration(secret, this.secretLeaseContainer); + } + + return propertySource; } }