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:
Mark Paluch
2021-03-10 10:42:30 +01:00
parent 943230aa46
commit 39f5b32e0d
5 changed files with 223 additions and 50 deletions

View File

@@ -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());

View File

@@ -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())));

View File

@@ -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));
}
}

View File

@@ -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);
}
}
}

View File

@@ -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());