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 f75356d1..5c586a54 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 @@ -25,6 +25,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.cloud.vault.config.LeasingSecretBackendMetadata; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.vault.core.lease.SecretLeaseContainer; import org.springframework.vault.core.lease.domain.RequestedSecret; import org.springframework.vault.core.lease.event.SecretLeaseCreatedEvent; @@ -33,7 +34,7 @@ import org.springframework.vault.core.util.PropertyTransformer; /** * @author Mark Paluch */ -class ConsulBackendMetadata implements LeasingSecretBackendMetadata { +class ConsulBackendMetadata implements LeasingSecretBackendMetadata, ApplicationEventPublisherAware { private final Log log = LogFactory.getLog(getClass()); @@ -41,7 +42,7 @@ class ConsulBackendMetadata implements LeasingSecretBackendMetadata { private final PropertyTransformer transformer; - private final ApplicationEventPublisher eventPublisher; + private ApplicationEventPublisher eventPublisher; ConsulBackendMetadata(VaultConsulProperties properties, PropertyTransformer transformer, ApplicationEventPublisher eventPublisher) { @@ -50,6 +51,11 @@ class ConsulBackendMetadata implements LeasingSecretBackendMetadata { this.eventPublisher = eventPublisher; } + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.eventPublisher = applicationEventPublisher; + } + @Override public String getName() { return String.format("%s with Role %s", this.properties.getBackend(), this.properties.getRole()); 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 3754b604..9cbd52d2 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 @@ -16,9 +16,7 @@ package org.springframework.cloud.vault.config.consul; -import java.net.InetSocketAddress; import java.util.Collections; -import java.util.HashMap; import java.util.Map; import org.junit.Before; @@ -28,21 +26,12 @@ 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; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; import org.springframework.util.Base64Utils; import org.springframework.vault.core.VaultOperations; -import org.springframework.web.client.HttpStatusCodeException; -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; /** @@ -53,34 +42,21 @@ import static org.junit.Assume.assumeTrue; */ public class ConsulSecretIntegrationTests extends IntegrationTestSupport { - private static final String CONSUL_HOST = "localhost"; - - private static final int CONSUL_PORT = 8500; - - private static final String CONNECTION_URL = String.format("%s:%d", CONSUL_HOST, CONSUL_PORT); - private static final String POLICY = "key \"\" { policy = \"read\" }"; - private static final String CONSUL_ACL_MASTER_TOKEN = "consul-master-token"; - - private static final ParameterizedTypeReference> STRING_MAP = new ParameterizedTypeReference>() { - }; - private VaultProperties vaultProperties = Settings.createVaultProperties(); private VaultConfigOperations configOperations; private VaultConsulProperties consul = new VaultConsulProperties(); - private RestTemplate restTemplate = new RestTemplate(); - /** * Initialize the consul secret backend. */ @Before public void setUp() { - assumeTrue(CanConnect.to(new InetSocketAddress(CONSUL_HOST, CONSUL_PORT))); + assumeTrue(SetupConsul.isConsulAvailable()); this.consul.setEnabled(true); this.consul.setRole("readonly"); @@ -91,29 +67,7 @@ public class ConsulSecretIntegrationTests extends IntegrationTestSupport { VaultOperations vaultOperations = this.vaultRule.prepare().getVaultOperations(); - HttpHeaders headers = new HttpHeaders(); - headers.add("X-Consul-Token", CONSUL_ACL_MASTER_TOKEN); - HttpEntity requestEntity = new HttpEntity<>("{\"Name\": \"sample\", \"Type\": \"management\"}", - headers); - - try { - ResponseEntity> tokenResponse = this.restTemplate.exchange( - "http://{host}:{port}/v1/acl/create", HttpMethod.PUT, requestEntity, STRING_MAP, CONSUL_HOST, - CONSUL_PORT); - - Map consulAccess = new HashMap<>(); - consulAccess.put("address", CONNECTION_URL); - consulAccess.put("token", tokenResponse.getBody().get("ID")); - - vaultOperations.write(String.format("%s/config/access", this.consul.getBackend()), consulAccess); - } - catch (HttpStatusCodeException e) { - - assumeFalse("Skipping because Consul is not configured as we expect it to be", - e.getStatusCode().is4xxClientError()); - - throw e; - } + SetupConsul.setupConsul(vaultOperations, this.consul.getBackend()); vaultOperations.write(String.format("%s/roles/%s", this.consul.getBackend(), this.consul.getRole()), Collections.singletonMap("policy", Base64Utils.encodeToString(POLICY.getBytes()))); diff --git a/spring-cloud-vault-config-consul/src/test/java/org/springframework/cloud/vault/config/consul/SetupConsul.java b/spring-cloud-vault-config-consul/src/test/java/org/springframework/cloud/vault/config/consul/SetupConsul.java new file mode 100644 index 00000000..c5728dfe --- /dev/null +++ b/spring-cloud-vault-config-consul/src/test/java/org/springframework/cloud/vault/config/consul/SetupConsul.java @@ -0,0 +1,86 @@ +/* + * Copyright 2020-2021 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.net.InetSocketAddress; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.cloud.vault.util.CanConnect; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.vault.core.VaultOperations; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestTemplate; + +import static org.junit.Assume.assumeFalse; + +/** + * Utility to setup Consul. + * + * @author Mark Paluch + */ +class SetupConsul { + + static final String CONSUL_HOST = "localhost"; + + static final int CONSUL_PORT = 8500; + + private static final String CONNECTION_URL = String.format("%s:%d", CONSUL_HOST, CONSUL_PORT); + + private static final ParameterizedTypeReference> STRING_MAP = new ParameterizedTypeReference>() { + }; + + private static final String CONSUL_ACL_MASTER_TOKEN = "consul-master-token"; + + static void setupConsul(VaultOperations vaultOperations, String consulBackend) { + + RestTemplate restTemplate = new RestTemplate(); + + HttpHeaders headers = new HttpHeaders(); + headers.add("X-Consul-Token", CONSUL_ACL_MASTER_TOKEN); + HttpEntity requestEntity = new HttpEntity<>("{\"Name\": \"sample\", \"Type\": \"management\"}", + headers); + + try { + ResponseEntity> tokenResponse = restTemplate.exchange( + "http://{host}:{port}/v1/acl/create", HttpMethod.PUT, requestEntity, STRING_MAP, CONSUL_HOST, + CONSUL_PORT); + + Map consulAccess = new HashMap<>(); + consulAccess.put("address", CONNECTION_URL); + consulAccess.put("token", tokenResponse.getBody().get("ID")); + + vaultOperations.write(String.format("%s/config/access", consulBackend), consulAccess); + } + catch (HttpStatusCodeException e) { + + assumeFalse("Skipping because Consul is not configured as we expect it to be", + e.getStatusCode().is4xxClientError()); + + throw e; + } + } + + static boolean isConsulAvailable() { + return CanConnect.to(new InetSocketAddress(CONSUL_HOST, CONSUL_PORT)); + } + +} diff --git a/spring-cloud-vault-config-consul/src/test/java/org/springframework/cloud/vault/config/consul/VaultConfigConsulConfigDataTests.java b/spring-cloud-vault-config-consul/src/test/java/org/springframework/cloud/vault/config/consul/VaultConfigConsulConfigDataTests.java new file mode 100644 index 00000000..996bf606 --- /dev/null +++ b/spring-cloud-vault-config-consul/src/test/java/org/springframework/cloud/vault/config/consul/VaultConfigConsulConfigDataTests.java @@ -0,0 +1,118 @@ +/* + * Copyright 2017-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.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.cloud.vault.util.IntegrationTestSupport; +import org.springframework.cloud.vault.util.Settings; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.event.EventListener; +import org.springframework.util.Base64Utils; +import org.springframework.vault.core.VaultOperations; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assume.assumeTrue; + +/** + * Tests for {@link VaultConfigConsulConfigDataTests}. + * + * @author Mark Paluch + */ +public class VaultConfigConsulConfigDataTests extends IntegrationTestSupport { + + private static final String POLICY = "key \"\" { policy = \"read\" }"; + + private ConfigurableApplicationContext context; + + @Before + public void before() { + + assumeTrue(SetupConsul.isConsulAvailable()); + + VaultOperations vaultOperations = this.vaultRule.prepare().getVaultOperations(); + + if (!prepare().hasSecretBackend("consul")) { + prepare().mountSecret("consul"); + } + + SetupConsul.setupConsul(vaultOperations, "consul"); + + Map role = new LinkedHashMap<>(); + role.put("policy", Base64Utils.encodeToString(POLICY.getBytes())); + role.put("ttl", "3s"); + role.put("max_ttl", "3s"); + vaultOperations.write(String.format("%s/roles/%s", "consul", "short-readonly"), role); + + this.vaultRule.prepare().getVaultOperations().write("secret/VaultConfigConsulConfigDataTests", + Collections.singletonMap("default-key", "default")); + + SpringApplication application = new SpringApplication(VaultConfigConsulConfigDataTests.Config.class); + application.setWebApplicationType(WebApplicationType.NONE); + + this.context = application.run("--spring.application.name=VaultConfigConsulConfigDataTests", + "--spring.config.import=vault:", "--spring.cloud.vault.kv.enabled=false", + "--spring.cloud.vault.config.lifecycle.min-renewal=2s", "--spring.cloud.vault.consul.enabled=true", + "--spring.cloud.vault.consul.role=short-readonly", + "--spring.cloud.vault.token=" + Settings.token().getToken()); + } + + @Test + public void shouldApplyConfigurer() throws InterruptedException { + + Config config = this.context.getBean(Config.class); + + assertThat(config.events).isEmpty(); + assertThat(this.context.getEnvironment().getProperty("spring.cloud.consul.config.acl-token")).isNotNull(); + + Thread.sleep(5_000); + assertThat(config.events).isNotEmpty(); + } + + @After + public void after() { + if (this.context != null) { + this.context.close(); + } + } + + @SpringBootConfiguration(proxyBeanMethods = false) + @EnableAutoConfiguration + static class Config { + + final BlockingQueue events = new LinkedBlockingQueue<>(); + + @EventListener + public void onRebind(ConsulBackendMetadata.RebindConsulEvent rebindConsulEvent) { + this.events.add(rebindConsulEvent); + } + + } + +} diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigDataLoader.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigDataLoader.java index 79757112..f3c1e4be 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigDataLoader.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigDataLoader.java @@ -33,6 +33,7 @@ import org.springframework.boot.context.config.ConfigDataLoader; import org.springframework.boot.context.config.ConfigDataLoaderContext; import org.springframework.boot.context.config.ConfigDataLocationNotFoundException; import org.springframework.cloud.vault.config.VaultAutoConfiguration.TaskSchedulerWrapper; +import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.env.PropertySource; @@ -121,6 +122,14 @@ public class VaultConfigDataLoader implements ConfigDataLoader { + ((ApplicationEventPublisherAware) location.getSecretBackendMetadata()) + .setApplicationEventPublisher(event.getApplicationContext()); + }); + } + if (vaultProperties.getConfig().getLifecycle().isEnabled()) { RequestedSecret secret = getRequestedSecret(location.getSecretBackendMetadata());