Provide a Vault client API for low-level and higer-order functions

Split functionality from VaultClient and refactor it into VaultConfigOperations/VaultConfigTemplate. The template now holds the authentication state and the property source just uses the template.

Fixes gh-23
This commit is contained in:
Mark Paluch
2016-07-19 12:57:09 +02:00
parent 9b88a0a88b
commit d4f40a1f40
31 changed files with 842 additions and 419 deletions

View File

@@ -19,7 +19,7 @@ import java.util.HashMap;
import java.util.Map;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.vault.SecureBackendAccessor;
import org.springframework.cloud.vault.config.SecureBackendAccessor;
import org.springframework.cloud.vault.VaultSecretBackend;
import org.springframework.cloud.vault.config.SecureBackendAccessorFactory;
import org.springframework.context.annotation.Bean;

View File

@@ -15,23 +15,26 @@
*/
package org.springframework.cloud.vault.config.aws;
import static org.assertj.core.api.Assertions.*;
import static org.junit.Assume.*;
import static org.springframework.cloud.vault.config.aws.VaultConfigAwsBootstrapConfiguration.AwsSecureBackendAccessorFactory.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.cloud.vault.AbstractIntegrationTests;
import org.springframework.cloud.vault.TestRestTemplateFactory;
import org.springframework.cloud.vault.ClientAuthentication;
import org.springframework.cloud.vault.VaultClient;
import org.springframework.cloud.vault.VaultProperties;
import org.springframework.cloud.vault.config.VaultConfigOperations;
import org.springframework.cloud.vault.config.VaultTemplate;
import org.springframework.cloud.vault.util.Settings;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.*;
import static org.junit.Assume.*;
import static org.springframework.cloud.vault.config.aws.VaultConfigAwsBootstrapConfiguration.AwsSecureBackendAccessorFactory.*;
import org.junit.Before;
import org.junit.Test;
/**
* Integration tests for {@link VaultClient} using the aws secret backend. This test
* requires AWS credentials and a region, see {@link #AWS_ACCESS_KEY} and
@@ -48,7 +51,7 @@ public class AwsSecretIntegrationTests extends AbstractIntegrationTests {
private final static String ARN = "arn:aws:iam::aws:policy/ReadOnlyAccess";
private VaultProperties vaultProperties = Settings.createVaultProperties();
private VaultClient vaultClient = new VaultClient(vaultProperties);
private VaultConfigOperations configOperations;
private VaultAwsProperties aws = new VaultAwsProperties();
/**
@@ -79,14 +82,14 @@ public class AwsSecretIntegrationTests extends AbstractIntegrationTests {
prepare().write(String.format("%s/roles/%s", aws.getBackend(), aws.getRole()),
Collections.singletonMap("arn", ARN));
vaultClient.setRest(TestRestTemplateFactory.create(vaultProperties));
configOperations = new VaultTemplate(vaultProperties, prepare().newVaultClient(),
ClientAuthentication.token(vaultProperties)).opsForConfig();
}
@Test
public void shouldCreateCredentialsCorrectly() throws Exception {
Map<String, String> secretProperties = vaultClient.read(forAws(aws),
Settings.token());
Map<String, String> secretProperties = configOperations.read(forAws(aws));
assertThat(secretProperties).containsKeys("cloud.aws.credentials.accessKey",
"cloud.aws.credentials.secretKey");

View File

@@ -19,7 +19,7 @@ import java.util.HashMap;
import java.util.Map;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.vault.SecureBackendAccessor;
import org.springframework.cloud.vault.config.SecureBackendAccessor;
import org.springframework.cloud.vault.VaultSecretBackend;
import org.springframework.cloud.vault.config.SecureBackendAccessorFactory;
import org.springframework.context.annotation.Bean;

View File

@@ -15,22 +15,18 @@
*/
package org.springframework.cloud.vault.config.consul;
import static org.assertj.core.api.Assertions.*;
import static org.junit.Assume.*;
import static org.springframework.cloud.vault.config.consul.VaultConfigConsulBootstrapConfiguration.ConsulSecureBackendAccessorFactory.*;
import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.boot.test.TestRestTemplate;
import org.springframework.cloud.vault.AbstractIntegrationTests;
import org.springframework.cloud.vault.TestRestTemplateFactory;
import org.springframework.cloud.vault.ClientAuthentication;
import org.springframework.cloud.vault.VaultClient;
import org.springframework.cloud.vault.VaultProperties;
import org.springframework.cloud.vault.config.VaultConfigOperations;
import org.springframework.cloud.vault.config.VaultTemplate;
import org.springframework.cloud.vault.util.CanConnect;
import org.springframework.cloud.vault.util.Settings;
import org.springframework.core.ParameterizedTypeReference;
@@ -40,6 +36,13 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Base64Utils;
import static org.assertj.core.api.Assertions.*;
import static org.junit.Assume.*;
import static org.springframework.cloud.vault.config.consul.VaultConfigConsulBootstrapConfiguration.ConsulSecureBackendAccessorFactory.*;
import org.junit.Before;
import org.junit.Test;
/**
* Integration tests for {@link VaultClient} using the consul secret backend. This test
* requires a running Consul instance, see {@link #CONNECTION_URL}.
@@ -61,7 +64,7 @@ public class ConsulSecretIntegrationTests extends AbstractIntegrationTests {
};
private VaultProperties vaultProperties = Settings.createVaultProperties();
private VaultClient vaultClient = new VaultClient(vaultProperties);
private VaultConfigOperations configOperations;
private VaultConsulProperties consul = new VaultConsulProperties();
private TestRestTemplate restTemplate = new TestRestTemplate();
@@ -103,14 +106,14 @@ public class ConsulSecretIntegrationTests extends AbstractIntegrationTests {
Collections.singletonMap("policy",
Base64Utils.encodeToString(POLICY.getBytes())));
vaultClient.setRest(TestRestTemplateFactory.create(vaultProperties));
configOperations = new VaultTemplate(vaultProperties, prepare().newVaultClient(),
ClientAuthentication.token(vaultProperties)).opsForConfig();
}
@Test
public void shouldCreateCredentialsCorrectly() throws Exception {
Map<String, String> secretProperties = vaultClient.read(forConsul(consul),
Settings.token());
Map<String, String> secretProperties = configOperations.read(forConsul(consul));
assertThat(secretProperties).containsKeys("spring.cloud.consul.token");
}

View File

@@ -19,7 +19,7 @@ import java.util.HashMap;
import java.util.Map;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.vault.SecureBackendAccessor;
import org.springframework.cloud.vault.config.SecureBackendAccessor;
import org.springframework.cloud.vault.VaultSecretBackend;
import org.springframework.cloud.vault.config.SecureBackendAccessorFactory;
import org.springframework.context.annotation.Bean;

View File

@@ -15,24 +15,27 @@
*/
package org.springframework.cloud.vault.config.databases;
import static org.assertj.core.api.Assertions.*;
import static org.junit.Assume.*;
import static org.springframework.cloud.vault.config.databases.VaultConfigDatabaseBootstrapConfiguration.DatabaseSecureBackendAccessorFactory.*;
import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.cloud.vault.AbstractIntegrationTests;
import org.springframework.cloud.vault.TestRestTemplateFactory;
import org.springframework.cloud.vault.ClientAuthentication;
import org.springframework.cloud.vault.VaultClient;
import org.springframework.cloud.vault.VaultProperties;
import org.springframework.cloud.vault.config.VaultConfigOperations;
import org.springframework.cloud.vault.config.VaultTemplate;
import org.springframework.cloud.vault.util.CanConnect;
import org.springframework.cloud.vault.util.Settings;
import static org.assertj.core.api.Assertions.*;
import static org.junit.Assume.*;
import static org.springframework.cloud.vault.config.databases.VaultConfigDatabaseBootstrapConfiguration.DatabaseSecureBackendAccessorFactory.*;
import org.junit.Before;
import org.junit.Test;
/**
* Integration tests for {@link VaultClient} using the cassandra secret backend. This test
* requires a running Cassandra instance, see {@link #CASSANDRA_HOST} and other
@@ -52,7 +55,7 @@ public class CassandraSecretIntegrationTests extends AbstractIntegrationTests {
+ "GRANT SELECT ON ALL KEYSPACES TO {{username}};";
private VaultProperties vaultProperties = Settings.createVaultProperties();
private VaultClient vaultClient = new VaultClient(vaultProperties);
private VaultConfigOperations configOperations;
private VaultCassandraProperties cassandra = new VaultCassandraProperties();
/**
@@ -80,18 +83,21 @@ public class CassandraSecretIntegrationTests extends AbstractIntegrationTests {
prepare().write(String.format("%s/config/connection", cassandra.getBackend()),
connection);
prepare().write(
String.format("%s/roles/%s", cassandra.getBackend(), cassandra.getRole()),
Collections.singletonMap("creation_cql", CREATE_USER_AND_GRANT_CQL));
prepare()
.write(String.format("%s/roles/%s", cassandra.getBackend(),
cassandra.getRole()),
Collections.singletonMap("creation_cql",
CREATE_USER_AND_GRANT_CQL));
vaultClient.setRest(TestRestTemplateFactory.create(vaultProperties));
configOperations = new VaultTemplate(vaultProperties, prepare().newVaultClient(),
ClientAuthentication.token(vaultProperties)).opsForConfig();
}
@Test
public void shouldCreateCredentialsCorrectly() throws Exception {
Map<String, String> secretProperties = vaultClient.read(forDatabase(cassandra),
Settings.token());
Map<String, String> secretProperties = configOperations
.read(forDatabase(cassandra));
assertThat(secretProperties).containsKeys("spring.data.cassandra.username",
"spring.data.cassandra.password");

View File

@@ -15,23 +15,26 @@
*/
package org.springframework.cloud.vault.config.databases;
import static org.assertj.core.api.Assertions.*;
import static org.junit.Assume.*;
import static org.springframework.cloud.vault.config.databases.VaultConfigDatabaseBootstrapConfiguration.DatabaseSecureBackendAccessorFactory.forDatabase;
import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.cloud.vault.AbstractIntegrationTests;
import org.springframework.cloud.vault.TestRestTemplateFactory;
import org.springframework.cloud.vault.ClientAuthentication;
import org.springframework.cloud.vault.VaultClient;
import org.springframework.cloud.vault.VaultProperties;
import org.springframework.cloud.vault.config.VaultConfigOperations;
import org.springframework.cloud.vault.config.VaultTemplate;
import org.springframework.cloud.vault.util.CanConnect;
import org.springframework.cloud.vault.util.Settings;
import static org.assertj.core.api.Assertions.*;
import static org.junit.Assume.*;
import static org.springframework.cloud.vault.config.databases.VaultConfigDatabaseBootstrapConfiguration.DatabaseSecureBackendAccessorFactory.*;
import org.junit.Before;
import org.junit.Test;
/**
* Integration tests for {@link VaultClient} using the mysql secret backend. This test
* requires a running MySQL instance, see {@link #ROOT_CREDENTIALS}.
@@ -42,13 +45,13 @@ public class MySqlSecretIntegrationTests extends AbstractIntegrationTests {
private final static int MYSQL_PORT = 3306;
private final static String MYSQL_HOST = "localhost";
private final static String ROOT_CREDENTIALS = String
.format("spring:vault@tcp(%s:%d)/", MYSQL_HOST, MYSQL_PORT);
private final static String ROOT_CREDENTIALS = String.format(
"spring:vault@tcp(%s:%d)/", MYSQL_HOST, MYSQL_PORT);
private final static String CREATE_USER_AND_GRANT_SQL = "CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';"
+ "GRANT SELECT ON *.* TO '{{name}}'@'%';";
private VaultProperties vaultProperties = Settings.createVaultProperties();
private VaultClient vaultClient = new VaultClient(vaultProperties);
private VaultConfigOperations configOperations;
private VaultMySqlProperties mySql = new VaultMySqlProperties();
/**
@@ -71,20 +74,20 @@ public class MySqlSecretIntegrationTests extends AbstractIntegrationTests {
prepare().write(String.format("%s/config/connection", mySql.getBackend()),
Collections.singletonMap("connection_url", ROOT_CREDENTIALS));
prepare().write(String.format("%s/roles/%s", mySql.getBackend(), mySql.getRole()),
prepare().write(
String.format("%s/roles/%s", mySql.getBackend(), mySql.getRole()),
Collections.singletonMap("sql", CREATE_USER_AND_GRANT_SQL));
vaultClient.setRest(TestRestTemplateFactory.create(vaultProperties));
configOperations = new VaultTemplate(vaultProperties, prepare().newVaultClient(),
ClientAuthentication.token(vaultProperties)).opsForConfig();
}
@Test
public void shouldCreateCredentialsCorrectly() throws Exception {
Map<String, String> secretProperties = vaultClient.read(forDatabase(mySql),
Settings.token());
Map<String, String> secretProperties = configOperations.read(forDatabase(mySql));
assertThat(secretProperties).containsKeys("spring.datasource.username",
"spring.datasource.password");
}
}

View File

@@ -15,23 +15,26 @@
*/
package org.springframework.cloud.vault.config.databases;
import static org.assertj.core.api.Assertions.*;
import static org.junit.Assume.*;
import static org.springframework.cloud.vault.config.databases.VaultConfigDatabaseBootstrapConfiguration.DatabaseSecureBackendAccessorFactory.*;
import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.cloud.vault.AbstractIntegrationTests;
import org.springframework.cloud.vault.TestRestTemplateFactory;
import org.springframework.cloud.vault.ClientAuthentication;
import org.springframework.cloud.vault.VaultClient;
import org.springframework.cloud.vault.VaultProperties;
import org.springframework.cloud.vault.config.VaultConfigOperations;
import org.springframework.cloud.vault.config.VaultTemplate;
import org.springframework.cloud.vault.util.CanConnect;
import org.springframework.cloud.vault.util.Settings;
import static org.assertj.core.api.Assertions.*;
import static org.junit.Assume.*;
import static org.springframework.cloud.vault.config.databases.VaultConfigDatabaseBootstrapConfiguration.DatabaseSecureBackendAccessorFactory.*;
import org.junit.Before;
import org.junit.Test;
/**
* Integration tests for {@link VaultClient} using the postgresql secret backend. This
* test requires a running PostgreSQL instance, see {@link #CONNECTION_URL}.
@@ -52,7 +55,7 @@ public class PostgreSqlSecretIntegrationTests extends AbstractIntegrationTests {
+ "GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";";
private VaultProperties vaultProperties = Settings.createVaultProperties();
private VaultClient vaultClient = new VaultClient(vaultProperties);
private VaultConfigOperations configOperations;
private VaultPostgreSqlProperties postgreSql = new VaultPostgreSqlProperties();
/**
@@ -80,14 +83,15 @@ public class PostgreSqlSecretIntegrationTests extends AbstractIntegrationTests {
postgreSql.getRole()),
Collections.singletonMap("sql", CREATE_USER_AND_GRANT_SQL));
vaultClient.setRest(TestRestTemplateFactory.create(vaultProperties));
configOperations = new VaultTemplate(vaultProperties, prepare().newVaultClient(),
ClientAuthentication.token(vaultProperties)).opsForConfig();
}
@Test
public void shouldCreateCredentialsCorrectly() throws Exception {
Map<String, String> secretProperties = vaultClient.read(forDatabase(postgreSql),
Settings.token());
Map<String, String> secretProperties = configOperations
.read(forDatabase(postgreSql));
assertThat(secretProperties).containsKeys("spring.datasource.username",
"spring.datasource.password");

View File

@@ -19,7 +19,7 @@ import java.util.HashMap;
import java.util.Map;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.vault.SecureBackendAccessor;
import org.springframework.cloud.vault.config.SecureBackendAccessor;
import org.springframework.cloud.vault.VaultSecretBackend;
import org.springframework.cloud.vault.config.SecureBackendAccessorFactory;
import org.springframework.context.annotation.Bean;

View File

@@ -15,24 +15,27 @@
*/
package org.springframework.cloud.vault.config.rabbitmq;
import static org.assertj.core.api.Assertions.*;
import static org.junit.Assume.*;
import static org.springframework.cloud.vault.config.rabbitmq.VaultConfigRabbitMqBootstrapConfiguration.RabbitMqSecureBackendAccessorFactory.*;
import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.cloud.vault.AbstractIntegrationTests;
import org.springframework.cloud.vault.TestRestTemplateFactory;
import org.springframework.cloud.vault.ClientAuthentication;
import org.springframework.cloud.vault.VaultClient;
import org.springframework.cloud.vault.VaultProperties;
import org.springframework.cloud.vault.config.VaultConfigOperations;
import org.springframework.cloud.vault.config.VaultTemplate;
import org.springframework.cloud.vault.util.CanConnect;
import org.springframework.cloud.vault.util.Settings;
import static org.assertj.core.api.Assertions.*;
import static org.junit.Assume.*;
import static org.springframework.cloud.vault.config.rabbitmq.VaultConfigRabbitMqBootstrapConfiguration.RabbitMqSecureBackendAccessorFactory.*;
import org.junit.Before;
import org.junit.Test;
/**
* Integration tests for {@link VaultClient} using the rabbitmq secret backend. This test
* requires a running RabbitMQ instance, see {@link #RABBITMQ_URI}.
@@ -53,7 +56,7 @@ public class RabbitMqSecretIntegrationTests extends AbstractIntegrationTests {
private final static String VHOSTS_ROLE = "{\"/\":{\"write\": \".*\", \"read\": \".*\"}}";
private VaultProperties vaultProperties = Settings.createVaultProperties();
private VaultClient vaultClient = new VaultClient(vaultProperties);
private VaultConfigOperations configOperations;
private VaultRabbitMqProperties rabbitmq = new VaultRabbitMqProperties();
/**
@@ -64,8 +67,8 @@ public class RabbitMqSecretIntegrationTests extends AbstractIntegrationTests {
@Before
public void setUp() throws Exception {
assumeTrue(CanConnect
.to(new InetSocketAddress(RABBITMQ_HOST, RABBITMQ_HTTP_MANAGEMENT_PORT)));
assumeTrue(CanConnect.to(new InetSocketAddress(RABBITMQ_HOST,
RABBITMQ_HTTP_MANAGEMENT_PORT)));
rabbitmq.setEnabled(true);
rabbitmq.setRole("readonly");
@@ -86,14 +89,15 @@ public class RabbitMqSecretIntegrationTests extends AbstractIntegrationTests {
String.format("%s/roles/%s", rabbitmq.getBackend(), rabbitmq.getRole()),
Collections.singletonMap("vhosts", VHOSTS_ROLE));
vaultClient.setRest(TestRestTemplateFactory.create(vaultProperties));
configOperations = new VaultTemplate(vaultProperties, prepare().newVaultClient(),
ClientAuthentication.token(vaultProperties)).opsForConfig();
}
@Test
public void shouldCreateCredentialsCorrectly() throws Exception {
Map<String, String> secretProperties = vaultClient.read(forRabbitMq(rabbitmq),
Settings.token());
Map<String, String> secretProperties = configOperations
.read(forRabbitMq(rabbitmq));
assertThat(secretProperties).containsKeys("spring.rabbitmq.username",
"spring.rabbitmq.password");

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.vault;
package org.springframework.cloud.vault.config;
import java.util.Map;

View File

@@ -15,7 +15,6 @@
*/
package org.springframework.cloud.vault.config;
import org.springframework.cloud.vault.SecureBackendAccessor;
import org.springframework.cloud.vault.VaultSecretBackend;
/**

View File

@@ -19,7 +19,6 @@ package org.springframework.cloud.vault.config;
import java.util.HashMap;
import java.util.Map;
import org.springframework.cloud.vault.SecureBackendAccessor;
import org.springframework.util.Assert;
/**

View File

@@ -19,8 +19,6 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.cloud.vault.SecureBackendAccessor;
import lombok.extern.apachecommons.CommonsLog;
import org.springframework.cloud.vault.VaultSecretBackend;

View File

@@ -15,8 +15,6 @@
*/
package org.springframework.cloud.vault.config;
import static org.springframework.cloud.vault.VaultBootstrapConfiguration.*;
import java.util.Collection;
import java.util.Map;
@@ -24,11 +22,11 @@ import javax.annotation.PostConstruct;
import org.springframework.beans.BeansException;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.vault.AppIdUserIdMechanism;
import org.springframework.cloud.vault.ClientAuthentication;
import org.springframework.cloud.vault.SecureBackendAccessor;
import org.springframework.cloud.vault.VaultBootstrapConfiguration;
import org.springframework.cloud.vault.VaultClient;
import org.springframework.cloud.vault.VaultProperties;
@@ -39,6 +37,8 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import static org.springframework.cloud.vault.VaultBootstrapConfiguration.*;
/**
* @author Mark Paluch
*/
@@ -59,19 +59,26 @@ public class VaultConfigBootstrapConfiguration implements ApplicationContextAwar
}
@Bean
public VaultPropertySourceLocator vaultPropertySourceLocator(VaultClient vaultClient,
VaultProperties vaultProperties,
VaultGenericBackendProperties vaultGenericBackendProperties,
ClientFactoryWrapper clientFactoryWrapper) {
@ConditionalOnMissingBean
public VaultTemplate vaultOperations(VaultProperties properties,
ClientFactoryWrapper clientFactoryWrapper, VaultClient client) {
ClientAuthentication clientAuthentication = clientAuthentication(
applicationContext, clientFactoryWrapper, properties);
return new VaultTemplate(properties, client, clientAuthentication);
}
@Bean
public VaultPropertySourceLocator vaultPropertySourceLocator(
VaultOperations operations, VaultProperties vaultProperties,
VaultGenericBackendProperties vaultGenericBackendProperties) {
Collection<SecureBackendAccessor> backendAccessors = SecureBackendFactories
.createBackendAcessors(vaultSecretBackends, factories);
ClientAuthentication clientAuthentication = clientAuthentication(
applicationContext, clientFactoryWrapper, vaultProperties);
return new VaultPropertySourceLocator(vaultClient, clientAuthentication,
vaultProperties, vaultGenericBackendProperties, backendAccessors);
return new VaultPropertySourceLocator(operations.opsForConfig(), vaultProperties,
vaultGenericBackendProperties, backendAccessors);
}
private ClientAuthentication clientAuthentication(
@@ -82,12 +89,10 @@ public class VaultConfigBootstrapConfiguration implements ApplicationContextAwar
clientFactoryWrapper.getClientHttpRequestFactory());
ClientAuthentication clientAuthentication;
if (vaultProperties
.getAuthentication() == VaultProperties.AuthenticationMethod.TOKEN) {
if (vaultProperties.getAuthentication() == VaultProperties.AuthenticationMethod.TOKEN) {
clientAuthentication = ClientAuthentication.token(vaultProperties);
}
else if (vaultProperties
.getAuthentication() == VaultProperties.AuthenticationMethod.APPID) {
else if (vaultProperties.getAuthentication() == VaultProperties.AuthenticationMethod.APPID) {
Map<String, AppIdUserIdMechanism> appIdUserIdMechanisms = applicationContext
.getBeansOfType(AppIdUserIdMechanism.class);
@@ -117,10 +122,9 @@ public class VaultConfigBootstrapConfiguration implements ApplicationContextAwar
@PostConstruct
private void postConstruct() {
this.vaultSecretBackends = applicationContext
.getBeansOfType(VaultSecretBackend.class).values();
this.factories = (Collection) applicationContext
.getBeansOfType(SecureBackendAccessorFactory.class).values();
this.vaultSecretBackends = applicationContext.getBeansOfType(
VaultSecretBackend.class).values();
this.factories = (Collection) applicationContext.getBeansOfType(
SecureBackendAccessorFactory.class).values();
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2016 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
*
* http://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;
import java.net.URI;
import java.util.Map;
import org.springframework.cloud.vault.VaultClientResponse;
import org.springframework.cloud.vault.VaultProperties;
import org.springframework.cloud.vault.VaultToken;
/**
* Interface that specified a basic set of Vault operations, implemented by
* {@link VaultConfigTemplate}.
*
* @author Mark Paluch
*/
public interface VaultConfigOperations {
/**
* Read configuration from a secure backend encapsulated within a
* {@link SecureBackendAccessor}. Reading data using this method is suitable for
* secret backends that do not require a request body.
*
* @param secureBackendAccessor must not be {@literal null}.
* @return the configuration data. May be empty but never {@literal null}.
* @throws IllegalStateException if {@link VaultProperties#isFailFast()} is enabled.
*/
Map<String, String> read(SecureBackendAccessor secureBackendAccessor);
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright 2016 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
*
* http://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;
import java.net.URI;
import java.util.Collections;
import java.util.Map;
import org.springframework.cloud.vault.ClientAuthentication;
import org.springframework.cloud.vault.VaultClient;
import org.springframework.cloud.vault.VaultClientResponse;
import org.springframework.cloud.vault.VaultProperties;
import org.springframework.cloud.vault.config.VaultOperations.SessionCallback;
import org.springframework.cloud.vault.config.VaultOperations.VaultSession;
import org.springframework.http.HttpStatus;
import org.springframework.util.Assert;
import lombok.extern.apachecommons.CommonsLog;
import org.apache.commons.logging.Log;
/**
* Central class to retrieve configuration from Vault.
*
* @author Mark Paluch
* @see VaultClient
* @see ClientAuthentication
*/
@CommonsLog
public class VaultConfigTemplate implements VaultConfigOperations {
private final VaultOperations vaultOperations;
private final VaultProperties properties;
private final VaultConfigSessionCallback callback;
/**
* Creates a new {@link VaultConfigTemplate}.
*
* @param vaultOperations must not be {@literal null}.
* @param properties must not be {@literal null}.
*/
public VaultConfigTemplate(VaultOperations vaultOperations, VaultProperties properties) {
Assert.notNull(vaultOperations, "VaultOperations must not be null!");
Assert.notNull(properties, "VaultProperties must not be null!");
this.vaultOperations = vaultOperations;
this.properties = properties;
this.callback = new VaultConfigSessionCallback(log);
}
@Override
public Map<String, String> read(SecureBackendAccessor secureBackendAccessor) {
Assert.notNull(secureBackendAccessor, "SecureBackendAccessor must not be null!");
VaultClientResponse response = vaultOperations.doWithVault("{backend}/{key}",
secureBackendAccessor.variables(), callback);
if (response.getStatusCode() == HttpStatus.OK) {
return secureBackendAccessor
.transformProperties(response.getBody().getData());
}
if (response.getStatusCode() == HttpStatus.NOT_FOUND) {
log.info(String
.format("Could not locate PropertySource: %s", "key not found"));
}
else if (properties.isFailFast()) {
throw new IllegalStateException(
String.format(
"Could not locate PropertySource and the fail fast property is set, failing Status %d %s",
response.getStatusCode().value(), response.getMessage()));
}
else {
log.warn(String.format("Could not locate PropertySource: Status %d %s",
response.getStatusCode().value(), response.getMessage()));
}
return Collections.emptyMap();
}
static class VaultConfigSessionCallback implements SessionCallback {
private final Log log;
public VaultConfigSessionCallback(Log log) {
this.log = log;
}
@Override
public VaultClientResponse doWithVault(URI uri, VaultSession session) {
log.info(String.format("Fetching config from Vault at: %s", uri));
return session.read(uri);
}
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2016 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
*
* http://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;
import java.net.URI;
import java.util.Map;
import org.springframework.cloud.vault.VaultClientResponse;
/**
* Interface that specified a basic set of Vault operations, implemented by
* {@link VaultTemplate}.
*
* @author Mark Paluch
*/
public interface VaultOperations {
/**
* @return the operations interface to interact with Vault configuration data.
*/
VaultConfigOperations opsForConfig();
/**
* Executes a Vault {@link SessionCallback}. Allows to interact with Vault in an
* authenticated session.
*
* @param path the path of the resource, e.g. {@code transit/encrypt/foo}, must not be
* empty or {@literal null}.
* @param sessionCallback the request.
* @return
*/
<T> T doWithVault(String path, SessionCallback sessionCallback);
/**
* Executes a Vault {@link SessionCallback}. Allows to interact with Vault in an
* authenticated session.
*
* @param pathTemplate the path of the resource, e.g. {@code transit/ key}/foo}, must
* not be empty or {@literal null}. * @param variables the variables for expansion of
* the {@code pathTemplate}, must not be {@literal null}.
* @param sessionCallback the request.
* @return
*/
<T> T doWithVault(String pathTemplate, Map<String, ?> variables,
SessionCallback sessionCallback);
/**
* Callback to execute actions within an authenticated {@link VaultSession}.
*
* @author Mark Paluch
*/
public interface SessionCallback {
/**
* Callback method.
*
* @param uri the URI that is used for the request, must not be {@literal null}.
* @param session session to use, must not be {@literal null}.
* @return
*/
<T> T doWithVault(URI uri, VaultSession session);
}
/**
* An authenticated Vault session.
*
* @author Mark Paluch
*/
public interface VaultSession {
/**
* Read data from the given Vault {@code uri}.
*
* @param uri must not be {@literal null}.
* @return the {@link VaultClientResponse}.
*/
public VaultClientResponse read(URI uri);
/**
* Write data to the given Vault {@code uri}.
*
* @param uri must not be {@literal null}.
* @param entity must not be {@literal null}.
* @return the {@link VaultClientResponse}.
*/
public VaultClientResponse write(URI uri, Object entity);
}
}

View File

@@ -19,57 +19,42 @@ import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.springframework.cloud.vault.ClientAuthentication;
import org.springframework.cloud.vault.SecureBackendAccessor;
import org.springframework.cloud.vault.VaultClient;
import org.springframework.cloud.vault.VaultProperties;
import org.springframework.cloud.vault.VaultProperties.AppIdProperties;
import org.springframework.cloud.vault.VaultProperties.AuthenticationMethod;
import org.springframework.cloud.vault.VaultToken;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.util.Assert;
import lombok.extern.apachecommons.CommonsLog;
/**
* A {@link EnumerablePropertySource} backed by {@link VaultClient}.
* A {@link EnumerablePropertySource} backed by {@link VaultConfigOperations}.
*
* @author Spencer Gibb
* @author Mark Paluch
*/
@CommonsLog
class VaultPropertySource extends EnumerablePropertySource<VaultClient> {
class VaultPropertySource extends EnumerablePropertySource<VaultConfigOperations> {
private final VaultProperties vaultProperties;
private final SecureBackendAccessor secureBackendAccessor;
private final Map<String, String> properties = new LinkedHashMap<>();
private final ClientAuthentication clientAuthentication;
private final transient VaultState vaultState;
/**
* Creates a new {@link VaultPropertySource}.
*
* @param vaultClient must not be {@literal null}.
* @param clientAuthentication mist not be {@literal null}.
* @param operations must not be {@literal null}.
* @param properties must not be {@literal null}.
* @param state shared Vault state, must not be {@literal null}.
* @param secureBackendAccessor must not be {@literal null}.
*/
public VaultPropertySource(VaultClient vaultClient,
ClientAuthentication clientAuthentication, VaultProperties properties,
VaultState state, SecureBackendAccessor secureBackendAccessor) {
public VaultPropertySource(VaultConfigOperations operations,
VaultProperties properties, SecureBackendAccessor secureBackendAccessor) {
super(secureBackendAccessor.getName(), vaultClient);
super(secureBackendAccessor.getName(), operations);
Assert.notNull(vaultClient, "VaultClient must not be null!");
Assert.notNull(operations, "VaultConfigOperations must not be null!");
Assert.notNull(properties, "VaultProperties must not be null!");
Assert.notNull(state, "VaultState must not be null!");
Assert.notNull(secureBackendAccessor, "SecureBackendAccessor must not be null!");
Assert.notNull(clientAuthentication, "ClientAuthentication must not be null!");
this.vaultProperties = properties;
this.clientAuthentication = clientAuthentication;
this.vaultState = state;
this.secureBackendAccessor = secureBackendAccessor;
}
@@ -79,8 +64,7 @@ class VaultPropertySource extends EnumerablePropertySource<VaultClient> {
public void init() {
try {
Map<String, String> values = this.source.read(this.secureBackendAccessor,
obtainToken());
Map<String, String> values = this.source.read(this.secureBackendAccessor);
if (values != null) {
this.properties.putAll(values);
}
@@ -102,42 +86,6 @@ class VaultPropertySource extends EnumerablePropertySource<VaultClient> {
}
}
private VaultToken obtainToken() {
if (vaultState.getToken() != null) {
return vaultState.getToken();
}
if (vaultProperties.getAuthentication() == AuthenticationMethod.TOKEN) {
Assert.hasText(vaultProperties.getToken(), "Vault Token must not be empty");
vaultState.setToken(clientAuthentication.login());
return vaultState.getToken();
}
if (vaultProperties.getAuthentication() == AuthenticationMethod.AWS_EC2) {
vaultState.setToken(clientAuthentication.login());
return vaultState.getToken();
}
if (vaultProperties.getAuthentication() == AuthenticationMethod.APPID) {
AppIdProperties appIdProperties = vaultProperties.getAppId();
Assert.hasText(vaultProperties.getApplicationName(),
"AppId must not be empty");
Assert.hasText(appIdProperties.getAppIdPath(), "AppIdPath must not be empty");
vaultState.setToken(clientAuthentication.login());
return vaultState.getToken();
}
throw new IllegalStateException(
String.format("Authentication method %s not supported",
vaultProperties.getAuthentication()));
}
@Override
public Object getProperty(String name) {
return this.properties.get(name);
@@ -148,4 +96,4 @@ class VaultPropertySource extends EnumerablePropertySource<VaultClient> {
Set<String> strings = this.properties.keySet();
return strings.toArray(new String[strings.size()]);
}
}
}

View File

@@ -16,8 +16,6 @@
package org.springframework.cloud.vault.config;
import static org.springframework.cloud.vault.config.SecureBackendAccessors.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -25,8 +23,6 @@ import java.util.Collections;
import java.util.List;
import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.cloud.vault.ClientAuthentication;
import org.springframework.cloud.vault.SecureBackendAccessor;
import org.springframework.cloud.vault.VaultClient;
import org.springframework.cloud.vault.VaultProperties;
import org.springframework.core.env.CompositePropertySource;
@@ -36,6 +32,8 @@ import org.springframework.core.env.PropertySource;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import static org.springframework.cloud.vault.config.SecureBackendAccessors.*;
/**
* {@link PropertySourceLocator} using {@link VaultClient}.
*
@@ -44,125 +42,124 @@ import org.springframework.util.StringUtils;
*/
class VaultPropertySourceLocator implements PropertySourceLocator {
private final VaultClient vaultClient;
private final ClientAuthentication clientAuthentication;
private final VaultProperties properties;
private final VaultGenericBackendProperties genericBackendProperties;
private final Collection<SecureBackendAccessor> backendAccessors;
private final VaultConfigOperations operations;
private final VaultProperties properties;
private final VaultGenericBackendProperties genericBackendProperties;
private final Collection<SecureBackendAccessor> backendAccessors;
private transient final VaultState vaultState = new VaultState();
/**
* Creates a new {@link VaultPropertySourceLocator}.
*
* @param operations must not be {@literal null}.
* @param properties must not be {@literal null}.
* @param genericBackendProperties must not be {@literal null}.
* @param backendAccessors must not be {@literal null}.
*/
public VaultPropertySourceLocator(VaultConfigOperations operations,
VaultProperties properties,
VaultGenericBackendProperties genericBackendProperties,
Collection<SecureBackendAccessor> backendAccessors) {
/**
* Creates a new {@link VaultPropertySourceLocator}.
*
* @param vaultClient must not be {@literal null}.
* @param clientAuthentication must not be {@literal null}.
* @param properties must not be {@literal null}.
* @param genericBackendProperties must not be {@literal null}.
* @param backendAccessors must not be {@literal null}.
*/
public VaultPropertySourceLocator(VaultClient vaultClient, ClientAuthentication clientAuthentication,
VaultProperties properties, VaultGenericBackendProperties genericBackendProperties,
Collection<SecureBackendAccessor> backendAccessors) {
Assert.notNull(operations, "VaultConfigOperations must not be null");
Assert.notNull(properties, "VaultProperties must not be null");
Assert.notNull(backendAccessors, "BackendAccessors must not be null");
Assert.notNull(genericBackendProperties,
"VaultGenericBackendProperties must not be null");
Assert.notNull(vaultClient, "VaultClient must not be null");
Assert.notNull(clientAuthentication, "ClientAuthentication must not be null");
Assert.notNull(properties, "VaultProperties must not be null");
Assert.notNull(backendAccessors, "BackendAccessors must not be null");
Assert.notNull(genericBackendProperties, "VaultGenericBackendProperties must not be null");
this.operations = operations;
this.properties = properties;
this.backendAccessors = backendAccessors;
this.genericBackendProperties = genericBackendProperties;
}
this.vaultClient = vaultClient;
this.clientAuthentication = clientAuthentication;
this.properties = properties;
this.backendAccessors = backendAccessors;
this.genericBackendProperties = genericBackendProperties;
}
@Override
public PropertySource<?> locate(Environment environment) {
@Override
public PropertySource<?> locate(Environment environment) {
if (environment instanceof ConfigurableEnvironment) {
if (environment instanceof ConfigurableEnvironment) {
CompositePropertySource propertySource = createCompositePropertySource((ConfigurableEnvironment) environment);
initialize(propertySource);
CompositePropertySource propertySource = createCompositePropertySource((ConfigurableEnvironment) environment);
initialize(propertySource);
return propertySource;
}
return null;
}
return propertySource;
}
return null;
}
private List<String> buildContexts(ConfigurableEnvironment env) {
private List<String> buildContexts(ConfigurableEnvironment env) {
String appName = env.getProperty("spring.application.name");
List<String> profiles = Arrays.asList(env.getActiveProfiles());
List<String> contexts = new ArrayList<>();
String appName = env.getProperty("spring.application.name");
List<String> profiles = Arrays.asList(env.getActiveProfiles());
List<String> contexts = new ArrayList<>();
String defaultContext = genericBackendProperties.getDefaultContext();
if (StringUtils.hasText(defaultContext)) {
contexts.add(defaultContext);
}
String defaultContext = genericBackendProperties.getDefaultContext();
if (StringUtils.hasText(defaultContext)) {
contexts.add(defaultContext);
}
addProfiles(contexts, defaultContext, profiles);
addProfiles(contexts, defaultContext, profiles);
if (StringUtils.hasText(appName)) {
if (StringUtils.hasText(appName)) {
if (!contexts.contains(appName)) {
contexts.add(appName);
}
if (!contexts.contains(appName)) {
contexts.add(appName);
}
addProfiles(contexts, appName, profiles);
}
addProfiles(contexts, appName, profiles);
}
Collections.reverse(contexts);
return contexts;
}
Collections.reverse(contexts);
return contexts;
}
protected CompositePropertySource createCompositePropertySource(
ConfigurableEnvironment environment) {
protected CompositePropertySource createCompositePropertySource(ConfigurableEnvironment environment) {
CompositePropertySource propertySource = new CompositePropertySource("vault");
CompositePropertySource propertySource = new CompositePropertySource("vault");
if (genericBackendProperties.isEnabled()) {
if (genericBackendProperties.isEnabled()) {
List<String> contexts = buildContexts(environment);
for (String propertySourceContext : contexts) {
List<String> contexts = buildContexts(environment);
for (String propertySourceContext : contexts) {
if (StringUtils.hasText(propertySourceContext)) {
if (StringUtils.hasText(propertySourceContext)) {
VaultPropertySource vaultPropertySource = createVaultPropertySource(generic(
genericBackendProperties.getBackend(), propertySourceContext));
VaultPropertySource vaultPropertySource = createVaultPropertySource(
generic(genericBackendProperties.getBackend(), propertySourceContext));
propertySource.addPropertySource(vaultPropertySource);
}
}
}
propertySource.addPropertySource(vaultPropertySource);
}
}
}
for (SecureBackendAccessor backendAccessor : backendAccessors) {
for (SecureBackendAccessor backendAccessor : backendAccessors) {
VaultPropertySource vaultPropertySource = createVaultPropertySource(backendAccessor);
propertySource.addPropertySource(vaultPropertySource);
}
return propertySource;
}
VaultPropertySource vaultPropertySource = createVaultPropertySource(backendAccessor);
propertySource.addPropertySource(vaultPropertySource);
}
return propertySource;
}
protected void initialize(CompositePropertySource propertySource) {
protected void initialize(CompositePropertySource propertySource) {
for (PropertySource<?> source : propertySource.getPropertySources()) {
((VaultPropertySource) source).init();
}
}
for (PropertySource<?> source : propertySource.getPropertySources()) {
((VaultPropertySource) source).init();
}
}
private VaultPropertySource createVaultPropertySource(SecureBackendAccessor accessor) {
return new VaultPropertySource(this.operations, this.properties, accessor);
}
private VaultPropertySource createVaultPropertySource(SecureBackendAccessor accessor) {
return new VaultPropertySource(this.vaultClient, this.clientAuthentication, this.properties, this.vaultState, accessor);
}
private void addProfiles(List<String> contexts, String baseContext,
List<String> profiles) {
private void addProfiles(List<String> contexts, String baseContext, List<String> profiles) {
for (String profile : profiles) {
String context = baseContext
+ this.genericBackendProperties.getProfileSeparator() + profile;
for (String profile : profiles) {
String context = baseContext + this.genericBackendProperties.getProfileSeparator() + profile;
if (!contexts.contains(context)) {
contexts.add(context);
}
}
}
}
if (!contexts.contains(context)) {
contexts.add(context);
}
}
}
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2016 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
*
* http://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;
import java.net.URI;
import java.util.Map;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.cloud.vault.ClientAuthentication;
import org.springframework.cloud.vault.VaultClient;
import org.springframework.cloud.vault.VaultClientResponse;
import org.springframework.cloud.vault.VaultProperties;
import org.springframework.cloud.vault.VaultToken;
import org.springframework.util.Assert;
/**
* This class encapsulates main Vault interaction. {@link VaultTemplate} will log into
* Vault on initialization and use the token throughout the whole lifetime.
*
* @author Mark Paluch
*/
public class VaultTemplate implements InitializingBean, VaultOperations {
private final VaultProperties properties;
private final VaultClient client;
private final ClientAuthentication clientAuthentication;
private final transient VaultState vaultState = new VaultState();
private final VaultSession vaultSession;
/**
* Creates a new {@link VaultConfigTemplate} for the given {@link VaultProperties},
* {@link VaultClient} and {@link ClientAuthentication}.
*
* @param properties must not be {@literal null}.
* @param client must not be {@literal null}.
* @param clientAuthentication must not be {@literal null}.
*/
public VaultTemplate(VaultProperties properties, VaultClient client,
ClientAuthentication clientAuthentication) {
Assert.notNull(properties, "VaultProperties must not be null!");
Assert.notNull(client, "VaultClient must not be null!");
Assert.notNull(clientAuthentication, "ClientAuthentication must not be null!");
this.properties = properties;
this.client = client;
this.clientAuthentication = clientAuthentication;
this.vaultSession = new VaultSession() {
@Override
public VaultClientResponse read(URI uri) {
return VaultTemplate.this.client.read(uri, getToken());
}
@Override
public VaultClientResponse write(URI uri, Object entity) {
return VaultTemplate.this.client.write(uri, entity, getToken());
}
};
}
@Override
public void afterPropertiesSet() {
login();
}
private void login() {
vaultState.setToken(clientAuthentication.login());
}
private VaultToken getToken() {
if (vaultState.getToken() == null) {
login();
}
return vaultState.getToken();
}
@Override
public VaultConfigOperations opsForConfig() {
return new VaultConfigTemplate(this, properties);
}
@Override
public <T> T doWithVault(String path, SessionCallback sessionCallback) {
Assert.notNull(sessionCallback, "SessionCallback must not be null!");
URI uri = client.buildUri(properties, path);
return sessionCallback.doWithVault(uri, vaultSession);
}
@Override
public <T> T doWithVault(String pathTemplate, Map<String, ?> variables,
SessionCallback sessionCallback) {
Assert.notNull(sessionCallback, "SessionCallback must not be null!");
URI uri = client.buildUri(properties, pathTemplate, variables);
return sessionCallback.doWithVault(uri, vaultSession);
}
}

View File

@@ -15,15 +15,15 @@
*/
package org.springframework.cloud.vault.config;
import org.junit.Before;
import org.springframework.cloud.vault.ClientAuthentication;
import org.springframework.cloud.vault.IpAddressUserId;
import org.springframework.cloud.vault.TestRestTemplateFactory;
import org.springframework.cloud.vault.VaultClient;
import org.springframework.cloud.vault.VaultProperties.AppIdProperties;
import org.springframework.cloud.vault.VaultProperties.AuthenticationMethod;
import org.springframework.cloud.vault.VaultToken;
import org.springframework.cloud.vault.util.Settings;
import org.junit.Before;
import org.springframework.web.client.RestTemplate;
/**
@@ -33,8 +33,6 @@ import org.springframework.web.client.RestTemplate;
*/
public class AppIdAuthenticationIntegrationTests extends GenericSecretIntegrationTests {
private ClientAuthentication clientAuthentication;
@Before
public void setUp() throws Exception {
@@ -56,17 +54,14 @@ public class AppIdAuthenticationIntegrationTests extends GenericSecretIntegratio
prepare().mapAppId(vaultProperties.getApplicationName());
prepare().mapUserId(vaultProperties.getApplicationName(), userId);
RestTemplate restTemplate = TestRestTemplateFactory.create(vaultProperties);
RestTemplate restTemplate = TestRestTemplateFactory
.create(vaultProperties);
this.clientAuthentication = ClientAuthentication.appId(vaultProperties,
restTemplate, userIdMechanism);
this.vaultClient = new VaultClient(vaultProperties);
this.vaultClient.setRest(TestRestTemplateFactory.create(vaultProperties));
}
ClientAuthentication clientAuthentication = ClientAuthentication.appId(
vaultProperties, restTemplate, userIdMechanism);
@Override
protected VaultToken createToken() {
return clientAuthentication.login();
configOperations = new VaultTemplate(vaultProperties, prepare().newVaultClient(),
clientAuthentication).opsForConfig();
}
private AppIdProperties configureAppIdProperties() {

View File

@@ -16,12 +16,13 @@
package org.springframework.cloud.vault.config;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
import org.junit.Test;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
/**
* Tests for fail fast option.
@@ -32,7 +33,7 @@ import org.springframework.boot.builder.SpringApplicationBuilder;
public class ApplicationFailFastTests {
@Test
public void contextLoads() {
public void contextLoadsWithFailFast() {
try {
new SpringApplicationBuilder().sources(ApplicationFailFastTests.class).run(
"--server.port=0", "--spring.cloud.vault.failFast=true",
@@ -40,7 +41,14 @@ public class ApplicationFailFastTests {
fail("failFast option did not produce an exception");
}
catch (Exception e) {
assertThat(e).hasMessageContaining("fail fast");
assertThat(e.getMessage()).isNotEmpty();
}
}
}
@Test
public void contextLoadsWithoutFailFast() {
new SpringApplicationBuilder().sources(ApplicationFailFastTests.class).run(
"--server.port=0", "--spring.cloud.vault.failFast=false",
"--spring.cloud.vault.port=9999");
}
}

View File

@@ -15,21 +15,21 @@
*/
package org.springframework.cloud.vault.config;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.cloud.vault.config.SecureBackendAccessors.*;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.cloud.vault.AbstractIntegrationTests;
import org.springframework.cloud.vault.TestRestTemplateFactory;
import org.springframework.cloud.vault.ClientAuthentication;
import org.springframework.cloud.vault.VaultClient;
import org.springframework.cloud.vault.VaultProperties;
import org.springframework.cloud.vault.VaultToken;
import org.springframework.cloud.vault.util.Settings;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.cloud.vault.config.SecureBackendAccessors.*;
import org.junit.Before;
import org.junit.Test;
/**
* Integration tests for {@link VaultClient} using the generic secret backend.
*
@@ -38,21 +38,23 @@ import org.springframework.cloud.vault.util.Settings;
public class GenericSecretIntegrationTests extends AbstractIntegrationTests {
protected VaultProperties vaultProperties = Settings.createVaultProperties();
protected VaultClient vaultClient = new VaultClient(vaultProperties);
protected VaultConfigOperations configOperations;
@Before
public void setUp() throws Exception {
vaultProperties.setFailFast(false);
prepare().writeSecret("app-name", (Map) createData());
vaultClient.setRest(TestRestTemplateFactory.create(vaultProperties));
configOperations = new VaultTemplate(vaultProperties, prepare().newVaultClient(),
ClientAuthentication.token(vaultProperties)).opsForConfig();
}
@Test
public void shouldReturnSecretsCorrectly() throws Exception {
Map<String, String> secretProperties = vaultClient
.read(generic("secret", "app-name"), createToken());
Map<String, String> secretProperties = configOperations.read(generic("secret",
"app-name"));
assertThat(secretProperties).containsAllEntriesOf(createExpectedMap());
}
@@ -60,28 +62,12 @@ public class GenericSecretIntegrationTests extends AbstractIntegrationTests {
@Test
public void shouldReturnNullIfNotFound() throws Exception {
Map<String, String> secretProperties = vaultClient
.read(generic("secret", "missing"), createToken());
Map<String, String> secretProperties = configOperations.read(generic("secret",
"missing"));
assertThat(secretProperties).isEmpty();
}
@Test(expected = IllegalStateException.class)
public void shouldFailOnFailFast() throws Exception {
vaultProperties.setFailFast(true);
vaultClient.read(generic("secret", "missing"), createToken());
}
/**
* Can be overridden by subclasses.
*
* @return
*/
protected VaultToken createToken() {
return Settings.token();
}
private Map<String, Object> createData() {
Map<String, Object> data = new HashMap<>();
data.put("string", "value");

View File

@@ -17,6 +17,7 @@ package org.springframework.cloud.vault;
import static org.springframework.cloud.vault.VaultClient.*;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

View File

@@ -51,10 +51,8 @@ public class VaultBootstrapConfiguration {
RestTemplate restTemplate = new RestTemplate(
clientHttpRequestFactoryWrapper().getClientHttpRequestFactory());
VaultProperties vaultProperties = vaultProperties();
VaultClient vaultClient = new VaultClient(vaultProperties);
vaultClient.setRest(restTemplate);
VaultClient vaultClient = new VaultClient();
vaultClient.setRestTemplate(restTemplate);
return vaultClient;
}

View File

@@ -16,13 +16,11 @@
package org.springframework.cloud.vault;
import java.net.URI;
import java.util.Collections;
import java.util.Map;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
@@ -30,120 +28,155 @@ import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestTemplate;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.apachecommons.CommonsLog;
/**
* Vault client. This client reads data from Vault secret backends and can authenticate
* with Vault to obtain an access token.
* Vault client. This client reads data from Vault.
*
* @author Spencer Gibb
* @author Mark Paluch
*/
@CommonsLog
public class VaultClient {
public static final String API_VERSION = "v1";
public static final String VAULT_TOKEN = "X-Vault-Token";
@Setter
private RestTemplate rest = new RestTemplate();
@Getter
private RestTemplate restTemplate;
private final VaultProperties properties;
public VaultClient() {
this(new RestTemplate());
}
public VaultClient(VaultProperties properties) {
Assert.notNull(properties, "VaultProperties must not be null");
this.properties = properties;
public VaultClient(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
/**
* Read secrets using the given {@link SecureBackendAccessor} and {@link VaultToken}.
* Read data from the given Vault {@code uri} using the {@link VaultToken}.
*
* @param secureBackendAccessor must not be {@literal null}.
* @param uri must not be {@literal null}.
* @param vaultToken must not be {@literal null}.
* @return A {@link Map} containing properties.
*/
public Map<String, String> read(SecureBackendAccessor secureBackendAccessor,
VaultToken vaultToken) {
public VaultClientResponse read(URI uri, VaultToken vaultToken) {
Assert.notNull(secureBackendAccessor, "SecureBackendAccessor must not be empty!");
Assert.notNull(uri, "URI must not be empty!");
Assert.notNull(vaultToken, "Vault Token must not be null!");
String url = buildUrl();
return exchange(uri, HttpMethod.GET, new HttpEntity<>(createHeaders(vaultToken)));
}
HttpHeaders headers = createHeaders(vaultToken);
Exception error = null;
String errorBody = null;
HttpStatus status = null;
/**
* Write data to the given Vault {@code uri} using the {@link VaultToken}.
*
* @param uri must not be {@literal null}.
* @param entity must not be {@literal null}.
* @return A {@link Map} containing properties.
*/
public VaultClientResponse write(URI uri, Object entity) {
URI uri = this.rest.getUriTemplateHandler().expand(url,
secureBackendAccessor.variables());
log.info(String.format("Fetching config from server at: %s", uri));
Assert.notNull(uri, "URI must not be empty!");
Assert.notNull(entity, "Entity must not be null!");
return exchange(uri, HttpMethod.POST, new HttpEntity<>(entity));
}
/**
* Write data to the given Vault {@code uri} using the {@link VaultToken}.
*
* @param uri must not be {@literal null}.
* @param entity must not be {@literal null}.
* @param vaultToken must not be {@literal null}.
* @return A {@link Map} containing properties.
*/
public VaultClientResponse write(URI uri, Object entity, VaultToken vaultToken) {
Assert.notNull(uri, "URI must not be empty!");
Assert.notNull(entity, "Vault Token must not be null!");
Assert.notNull(vaultToken, "Vault Token must not be null!");
return exchange(uri, HttpMethod.POST, new HttpEntity<>(entity,
createHeaders(vaultToken)));
}
private VaultClientResponse exchange(URI uri, HttpMethod httpMethod,
HttpEntity<?> httpEntity) {
Assert.notNull(uri, "URI must not be empty!");
try {
ResponseEntity<VaultResponse> response = this.rest.exchange(uri,
HttpMethod.GET, new HttpEntity<>(headers), VaultResponse.class);
ResponseEntity<VaultResponse> response = this.restTemplate.exchange(uri,
httpMethod, httpEntity, VaultResponse.class);
status = response.getStatusCode();
if (status == HttpStatus.OK) {
if (response.getBody().getData() != null) {
return secureBackendAccessor
.transformProperties(response.getBody().getData());
}
}
return VaultClientResponse.of(response.getBody(), response.getStatusCode(),
uri, response.getStatusCode().getReasonPhrase());
}
catch (HttpServerErrorException | HttpClientErrorException e) {
if (MediaType.APPLICATION_JSON
.includes(e.getResponseHeaders().getContentType())) {
errorBody = e.getResponseBodyAsString();
String message = e.getResponseBodyAsString();
if (MediaType.APPLICATION_JSON.includes(e.getResponseHeaders()
.getContentType())) {
message = VaultErrorMessage.getError(message);
}
status = e.getStatusCode();
error = e;
return VaultClientResponse.of(null, e.getStatusCode(), uri, message);
}
catch (Exception e) {
error = e;
}
if (status == HttpStatus.NOT_FOUND) {
log.info(String.format("Could not locate PropertySource: %s",
"key not found"));
}
else if (status != null) {
log.warn(String.format("Could not locate PropertySource: Status %d %s",
status.value(), getErrorMessage(error, errorBody)));
}
else {
log.warn(String.format("Could not locate PropertySource: %s",
(getErrorMessage(error, errorBody))));
}
if (properties.isFailFast()) {
throw new IllegalStateException(
"Could not locate PropertySource and the fail fast property is set, failing",
error);
}
return Collections.emptyMap();
}
private String getErrorMessage(Exception error, String errorBody) {
return errorBody == null ? error == null ? "unknown reason" : error.getMessage()
: VaultErrorMessage.getError(errorBody);
/**
* Build the Vault {@link URI} based on the given {@link VaultProperties} and
* {@code path}.
*
* @param properties must not be {@literal null}.
* @param path must not be empty or {@literal null}.
* @return
*/
public static URI buildUri(VaultProperties properties, String path) {
return URI.create(createBaseUrlWithPath(properties, path));
}
/**
* Build the Vault {@link URI} based on the given {@link VaultProperties} and
* {@code pathTemplate}. URI template variables will be expanded using
* {@code uriVariables}.
*
* @param properties must not be {@literal null}.
* @param pathTemplate must not be empty or {@literal null}.
* @param uriVariables must not be {@literal null}.
* @see org.springframework.web.util.UriComponentsBuilder
* @return
*/
public URI buildUri(VaultProperties properties, String pathTemplate,
Map<String, ?> uriVariables) {
Assert.notNull(properties, "VaultProperties must not be null!");
Assert.hasText(pathTemplate, "Path must not be empty!");
Assert.notNull(properties, "Vault Token must not be null!");
return restTemplate.getUriTemplateHandler().expand(
createBaseUrlWithPath(properties, pathTemplate), uriVariables);
}
private HttpHeaders createHeaders(VaultToken vaultToken) {
Assert.notNull(vaultToken, "Vault Token must not be null!");
HttpHeaders headers = new HttpHeaders();
headers.add(VAULT_TOKEN, vaultToken.getToken());
return headers;
}
private String buildUrl() {
return String.format("%s://%s:%s/%s/{backend}/{key}", this.properties.getScheme(),
this.properties.getHost(), this.properties.getPort(), API_VERSION);
private static String createBaseUrlWithPath(VaultProperties properties, String path) {
Assert.notNull(properties, "VaultProperties must not be null!");
Assert.hasText(path, "Path must not be empty!");
return String.format("%s://%s:%s/%s/%s", properties.getScheme(),
properties.getHost(), properties.getPort(), API_VERSION, path);
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2016 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
*
* http://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;
import java.net.URI;
import org.springframework.http.HttpStatus;
import lombok.Value;
/**
* Encapsulates the client response used in {@link VaultClient}. Consists of the body,
* status code the location and a message. The {@code body} is empty for all
* non-successful results.
*
* This class is immutable.
*
* @author Mark Paluch
*/
@Value(staticConstructor = "of")
public class VaultClientResponse {
private VaultResponse body;
private HttpStatus statusCode;
private URI uri;
private String message;
/**
*
* @return {@literal true} if the request was completed successfully.
*/
public boolean isSuccessful() {
return body != null && statusCode.is2xxSuccessful();
}
}

View File

@@ -15,20 +15,20 @@
*/
package org.springframework.cloud.vault;
import static org.assertj.core.api.Assertions.*;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
import org.springframework.cloud.vault.VaultProperties.AppIdProperties;
import org.springframework.cloud.vault.VaultProperties.AuthenticationMethod;
import org.springframework.cloud.vault.util.Settings;
import static org.assertj.core.api.Assertions.*;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.cloud.vault.VaultProperties.AppIdProperties;
import org.springframework.cloud.vault.VaultProperties.AuthenticationMethod;
import org.springframework.cloud.vault.util.Settings;
import org.springframework.web.client.RestTemplate;
/**
* Integration tests for {@link VaultClient} using various UserIds.
@@ -55,11 +55,10 @@ public class AppIdAuthenticationMethodsIntegrationTests extends AbstractIntegrat
VaultProperties vaultProperties = prepareAppIdAuthenticationMethod(
AppIdProperties.IP_ADDRESS, "myapp");
RestTemplate restTemplate = TestRestTemplateFactory
.create(Settings.createVaultProperties());
ClientAuthentication clientAuthentication = new DefaultClientAuthentication(
vaultProperties, restTemplate, new IpAddressUserId());
vaultProperties, TestRestTemplateFactory.create(vaultProperties),
new IpAddressUserId());
assertThat(clientAuthentication.login()).isNotNull();
}
@@ -70,11 +69,9 @@ public class AppIdAuthenticationMethodsIntegrationTests extends AbstractIntegrat
VaultProperties vaultProperties = prepareAppIdAuthenticationMethod("my-user-id",
"myapp");
RestTemplate restTemplate = TestRestTemplateFactory
.create(Settings.createVaultProperties());
ClientAuthentication clientAuthentication = new DefaultClientAuthentication(
vaultProperties, restTemplate, new StaticUserId(vaultProperties));
vaultProperties, TestRestTemplateFactory.create(vaultProperties),
new StaticUserId(vaultProperties));
assertThat(clientAuthentication.login()).isNotNull();
}
@@ -85,11 +82,9 @@ public class AppIdAuthenticationMethodsIntegrationTests extends AbstractIntegrat
VaultProperties vaultProperties = prepareAppIdAuthenticationMethod(
AppIdProperties.MAC_ADDRESS, "myapp");
RestTemplate restTemplate = TestRestTemplateFactory
.create(Settings.createVaultProperties());
ClientAuthentication clientAuthentication = new DefaultClientAuthentication(
vaultProperties, restTemplate, new MacAddressUserId(vaultProperties));
vaultProperties, TestRestTemplateFactory.create(vaultProperties),
new MacAddressUserId(vaultProperties));
assertThat(clientAuthentication.login()).isNotNull();
}

View File

@@ -21,6 +21,7 @@ import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.test.TestRestTemplate;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.web.client.DefaultResponseErrorHandler;
import lombok.SneakyThrows;
@@ -45,12 +46,13 @@ public class TestRestTemplateFactory {
initializeClientHttpRequestFactory(vaultProperties);
TestRestTemplate testRestTemplate = new TestRestTemplate();
testRestTemplate.setErrorHandler(new DefaultResponseErrorHandler());
testRestTemplate.setRequestFactory(factoryCache.get());
return testRestTemplate;
}
protected static void initializeClientHttpRequestFactory(
private static void initializeClientHttpRequestFactory(
VaultProperties vaultProperties) throws Exception {
if (factoryCache.get() != null) {

View File

@@ -31,6 +31,7 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -76,6 +77,15 @@ public class PrepareVault {
this.restTemplate = restTemplate;
}
/**
* Creates a new {@link VaultClient}.
*
* @return
*/
public VaultClient newVaultClient() {
return new VaultClient(restTemplate);
}
/**
* Initialize Vault and unseal the vault.
*
@@ -97,8 +107,8 @@ public class PrepareVault {
new HttpEntity<>(initializeVault), VaultInitialized.class, parameters);
if (!initResponse.getStatusCode().is2xxSuccessful()) {
throw new IllegalStateException(
"Cannot initialize vault: " + initResponse.toString());
throw new IllegalStateException("Cannot initialize vault: "
+ initResponse.toString());
}
VaultInitialized initialized = initResponse.getBody();
@@ -144,8 +154,8 @@ public class PrepareVault {
parameters);
if (!createTokenResponse.getStatusCode().is2xxSuccessful()) {
throw new IllegalStateException(
"Cannot create token: " + createTokenResponse.toString());
throw new IllegalStateException("Cannot create token: "
+ createTokenResponse.toString());
}
AuthToken authToken = createTokenResponse.getBody().getAuth();
@@ -162,11 +172,19 @@ public class PrepareVault {
Map<String, String> parameters = parameters(vaultProperties);
ResponseEntity<String> exchange = restTemplate
.getForEntity(SEAL_STATUS_URL_TEMPLATE, String.class, parameters);
ResponseEntity<String> exchange = null;
try {
exchange = restTemplate.getForEntity(SEAL_STATUS_URL_TEMPLATE, String.class,
parameters);
if (exchange.getStatusCode().is2xxSuccessful()) {
return true;
if (exchange.getStatusCode().is2xxSuccessful()) {
return true;
}
}
catch (HttpStatusCodeException e) {
if (e.getStatusCode().is4xxClientError()) {
return false;
}
}
if (exchange.getStatusCode().is4xxClientError()) {
@@ -197,8 +215,8 @@ public class PrepareVault {
parameters);
if (!responseEntity.getStatusCode().is2xxSuccessful()) {
throw new IllegalStateException(
"Cannot create mount auth backend: " + responseEntity.toString());
throw new IllegalStateException("Cannot create mount auth backend: "
+ responseEntity.toString());
}
responseEntity.getBody();
@@ -239,8 +257,8 @@ public class PrepareVault {
parameters);
if (!responseEntity.getStatusCode().is2xxSuccessful()) {
throw new IllegalStateException(
"Cannot create mount secret backend: " + responseEntity.toString());
throw new IllegalStateException("Cannot create mount secret backend: "
+ responseEntity.toString());
}
responseEntity.getBody();
@@ -267,8 +285,8 @@ public class PrepareVault {
urlTemplate, HttpMethod.GET, entity, MAP_OF_MAPS_TYPE, parameters);
if (!responseEntity.getStatusCode().is2xxSuccessful()) {
throw new IllegalStateException(
"Cannot enumerate mounts: " + responseEntity.toString());
throw new IllegalStateException("Cannot enumerate mounts: "
+ responseEntity.toString());
}
Map<String, Object> body = responseEntity.getBody();
@@ -332,8 +350,8 @@ public class PrepareVault {
parameters);
if (!exchange.getStatusCode().is2xxSuccessful()) {
throw new IllegalStateException(
String.format("Cannot write to %s: %s", path, exchange.getBody()));
throw new IllegalStateException(String.format("Cannot write to %s: %s", path,
exchange.getBody()));
}
}
@@ -368,8 +386,8 @@ public class PrepareVault {
appIdData.put("value", "root"); // policy
appIdData.put("display_name", "this is my test application");
write(String.format("auth/%s/map/app-id/%s",
vaultProperties.getAppId().getAppIdPath(), appId), appIdData);
write(String.format("auth/%s/map/app-id/%s", vaultProperties.getAppId()
.getAppIdPath(), appId), appIdData);
}
private HttpHeaders authenticatedHeaders() {