Polishing
Allow SecretBackendMetadata to implement ApplicationEventPublisherAware. Register application event publisher after boostrap with ConsulBackendMetadata. Add integration tests. Closes gh-579 Original pull request: gh-580
This commit is contained in:
@@ -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());
|
||||
|
||||
@@ -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<Map<String, String>> STRING_MAP = new ParameterizedTypeReference<Map<String, String>>() {
|
||||
};
|
||||
|
||||
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<String> requestEntity = new HttpEntity<>("{\"Name\": \"sample\", \"Type\": \"management\"}",
|
||||
headers);
|
||||
|
||||
try {
|
||||
ResponseEntity<Map<String, String>> tokenResponse = this.restTemplate.exchange(
|
||||
"http://{host}:{port}/v1/acl/create", HttpMethod.PUT, requestEntity, STRING_MAP, CONSUL_HOST,
|
||||
CONSUL_PORT);
|
||||
|
||||
Map<String, String> 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())));
|
||||
|
||||
@@ -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<Map<String, String>> STRING_MAP = new ParameterizedTypeReference<Map<String, String>>() {
|
||||
};
|
||||
|
||||
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<String> requestEntity = new HttpEntity<>("{\"Name\": \"sample\", \"Type\": \"management\"}",
|
||||
headers);
|
||||
|
||||
try {
|
||||
ResponseEntity<Map<String, String>> tokenResponse = restTemplate.exchange(
|
||||
"http://{host}:{port}/v1/acl/create", HttpMethod.PUT, requestEntity, STRING_MAP, CONSUL_HOST,
|
||||
CONSUL_PORT);
|
||||
|
||||
Map<String, String> 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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String, Object> 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<ConsulBackendMetadata.RebindConsulEvent> events = new LinkedBlockingQueue<>();
|
||||
|
||||
@EventListener
|
||||
public void onRebind(ConsulBackendMetadata.RebindConsulEvent rebindConsulEvent) {
|
||||
this.events.add(rebindConsulEvent);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<VaultConfigLocati
|
||||
private ConfigData loadConfigData(VaultConfigLocation location, ConfigurableBootstrapContext bootstrap,
|
||||
VaultProperties vaultProperties) {
|
||||
|
||||
if (location.getSecretBackendMetadata() instanceof ApplicationEventPublisherAware) {
|
||||
|
||||
bootstrap.addCloseListener(event -> {
|
||||
((ApplicationEventPublisherAware) location.getSecretBackendMetadata())
|
||||
.setApplicationEventPublisher(event.getApplicationContext());
|
||||
});
|
||||
}
|
||||
|
||||
if (vaultProperties.getConfig().getLifecycle().isEnabled()) {
|
||||
|
||||
RequestedSecret secret = getRequestedSecret(location.getSecretBackendMetadata());
|
||||
|
||||
Reference in New Issue
Block a user