Polishing.

Introduce VaultSecretBackendDescriptorFactory as abstraction for descriptor factories that can produce multiple SecretBackendDescriptors. Consider VaultSecretBackendDescriptorFactory in Boostrap and Config Data API mechanisms. Add tests.

Original pull request: gh-587
See gh-459
This commit is contained in:
Mark Paluch
2021-11-24 09:37:45 +01:00
parent 2ddb9d1e66
commit ed977bf664
18 changed files with 449 additions and 138 deletions

View File

@@ -59,6 +59,7 @@
|spring.cloud.vault.database.role | | Role name for credentials.
|spring.cloud.vault.database.static-role | `false` | Enable static role usage.
|spring.cloud.vault.database.username-property | `spring.datasource.username` | Target property for the obtained username.
|spring.cloud.vault.databases | |
|spring.cloud.vault.discovery.enabled | `false` | Flag to indicate that Vault server discovery is enabled (vault server URL will be looked up via discovery).
|spring.cloud.vault.discovery.service-id | `vault` | Service id to locate Vault.
|spring.cloud.vault.elasticsearch.backend | `database` | Database backend path.

View File

@@ -9,3 +9,4 @@ This section briefly covers items that are new and noteworthy in the latest rele
* Support for the <<vault.config.backends.couchbase>> backend.
* Configuration of keystore/truststore types through `spring.cloud.vault.ssl.key-store-type=…`/`spring.cloud.vault.ssl.trust-store-type=…` including PEM support.
* Support for `ReactiveDiscoveryClient` by configuring a `ReactiveVaultEndpointProvider`.
* Support to configure <<vault.config.backends.databases>>.

View File

@@ -203,10 +203,10 @@ Supported AWS credential Types:
* federation_token (STS)
The access key and secret key are stored in `cloud.aws.credentials.accessKey`
and `cloud.aws.credentials.secretKey`. So using Spring Cloud AWS will pick up the generated credentials without further configuration.
and `cloud.aws.credentials.secretKey`. So using Spring Cloud AWS will pick up the generated credentials without further configuration.
You can configure the property names by setting `spring.cloud.vault.aws.access-key-property` and
`spring.cloud.vault.aws.secret-key-property`.
`spring.cloud.vault.aws.secret-key-property`.
For STS security token, you can configure the property name by setting `spring.cloud.vault.aws.session-token-key-property`. The security token is stored under `cloud.aws.credentials.sessionToken` (defaults).
@@ -248,7 +248,7 @@ spring.cloud.vault:
* `backend` sets the path of the AWS mount to use
* `access-key-property` sets the property name in which the AWS access key is stored
* `secret-key-property` sets the property name in which the AWS secret key is stored
* `session-token-key-property` sets the property name in which the AWS STS security token is stored.
* `session-token-key-property` sets the property name in which the AWS STS security token is stored.
* `credential-type` sets the aws credential type to use for this backend. Defaults to `iam_user`
* `ttl` sets the ttl for the STS token when using `assumed_role` or `federation_token`. Defaults to the ttl specified by the vault role. Min/Max values are also limited to what AWS would support for STS.
* `role-arn` sets the IAM role to assume if more than one are configured for the vault role when using `assumed_role`.
@@ -323,11 +323,40 @@ spring.cloud.vault:
----
====
* `enabled` setting this value to `true` enables the Database backend config usage
* `role` sets the role name of the Database role definition
* `backend` sets the path of the Database mount to use
* `username-property` sets the property name in which the Database username is stored
* `password-property` sets the property name in which the Database password is stored
[[vault.config.backends.databases]]
=== Multiple Databases
Sometimes, credentials for a single database isn't sufficient because an application might connect to two or more databases of the same kind.
Beginning with version 3.0.5, Spring Vault supports the configuration of multiple database secret backends under the `spring.cloud.vault.databases.*` namespace.
The configuration accepts multiple database backends to materialize credentials into the specified properties. Make sure to configure `username-property` and `password-property` appropriately.
====
[source,yaml]
----
spring.cloud.vault:
databases:
primary:
enabled: true
role: readwrite
backend: database
username-property: spring.primary-datasource.username
password-property: spring.primary-datasource.password
other-database:
enabled: true
role: readonly
backend: database
username-property: spring.secondary-datasource.username
password-property: spring.secondary-datasource.password
----
====
* `<name>` descriptive name of the database configuration.
* `<name>.enabled` setting this value to `true` enables the Database backend config usage
* `<name>.role` sets the role name of the Database role definition
* `<name>.backend` sets the path of the Database mount to use
* `<name>.username-property` sets the property name in which the Database username is stored. Make sure to use unique property names to avoid property shadowing.
* `<name>.password-property` sets the property name in which the Database password is stored Make sure to use unique property names to avoid property shadowing.
See also: https://www.vaultproject.io/docs/secrets/databases/index.html[Vault Documentation: Database Secrets backend]

View File

@@ -16,8 +16,6 @@
package org.springframework.cloud.vault.config.databases;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.vault.config.PropertyNameTransformer;
@@ -31,7 +29,6 @@ import org.springframework.core.annotation.Order;
import org.springframework.util.Assert;
import org.springframework.vault.core.util.PropertyTransformer;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
@@ -48,27 +45,10 @@ import java.util.Map;
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties({ VaultMySqlProperties.class, VaultPostgreSqlProperties.class,
VaultCassandraProperties.class, VaultCouchbaseProperties.class, VaultMongoProperties.class,
VaultElasticsearchProperties.class, VaultMultipleDatabaseProperties.class, VaultDatabaseProperties.class })
VaultElasticsearchProperties.class, VaultDatabaseProperties.class, VaultDatabasesProperties.class })
@Order(Ordered.LOWEST_PRECEDENCE - 15)
public class VaultConfigDatabaseBootstrapConfiguration {
@Autowired
private ConfigurableBeanFactory beanFactory;
@Autowired
private VaultMultipleDatabaseProperties multipleDatabaseProperties;
@PostConstruct
public void registerBeans() {
multipleDatabaseProperties.getDatabases().forEach(d -> {
String beanName = String.format("vaultMultipleDatabaseProperties_%s",
multipleDatabaseProperties.getDatabases().indexOf(d));
if (!beanFactory.containsBean(beanName)) {
beanFactory.registerSingleton(beanName, d);
}
});
}
@Bean
@ConditionalOnMissingBean
public DatabaseSecretBackendMetadataFactory databaseSecretBackendMetadataFactory() {

View File

@@ -17,28 +17,37 @@
package org.springframework.cloud.vault.config.databases;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.vault.config.VaultSecretBackendDescriptor;
import org.springframework.cloud.vault.config.VaultSecretBackendDescriptorFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* Configuration properties for multiple database secrets using the Database backend. This
* is configured with the spring.cloud.vault.databases list.
* Configuration properties for multiple database secrets using the {@code database}
* backend. This is configured with the {@code spring.cloud.vault.databases.*} mapping.
*
* @author Quintin Beukes
* @author Mark Paluch
* @since 3.0.3
*/
@ConfigurationProperties("spring.cloud.vault")
public class VaultMultipleDatabaseProperties {
public class VaultDatabasesProperties implements VaultSecretBackendDescriptorFactory {
private List<VaultDatabaseProperties> databases = new ArrayList<>();
private Map<String, VaultDatabaseProperties> databases = new HashMap<>();
public List<VaultDatabaseProperties> getDatabases() {
return databases;
public Map<String, VaultDatabaseProperties> getDatabases() {
return this.databases;
}
public void setDatabases(List<VaultDatabaseProperties> databases) {
public void setDatabases(Map<String, VaultDatabaseProperties> databases) {
this.databases = databases;
}
@Override
public Collection<? extends VaultSecretBackendDescriptor> create() {
return getDatabases().values();
}
}

View File

@@ -25,7 +25,7 @@ import org.springframework.lang.Nullable;
*
* @author Mark Paluch
* @deprecated since 2.0. Use {@link VaultDatabaseProperties} or
* {@link VaultMultipleDatabaseProperties}.
* {@link VaultDatabasesProperties}.
*/
@ConfigurationProperties("spring.cloud.vault.mysql")
@Deprecated

View File

@@ -24,7 +24,7 @@ import org.springframework.lang.Nullable;
*
* @author Mark Paluch
* @deprecated since 2.0. Use {@link VaultDatabaseProperties} or
* {@link VaultMultipleDatabaseProperties}.
* {@link VaultDatabasesProperties}.
*/
@ConfigurationProperties("spring.cloud.vault.postgresql")
@Deprecated

View File

@@ -13,3 +13,6 @@ org.springframework.cloud.vault.config.databases.VaultElasticsearchProperties,\
org.springframework.cloud.vault.config.databases.VaultMongoProperties,\
org.springframework.cloud.vault.config.databases.VaultMySqlProperties,\
org.springframework.cloud.vault.config.databases.VaultPostgreSqlProperties
org.springframework.cloud.vault.config.VaultSecretBackendDescriptorFactory=\
org.springframework.cloud.vault.config.databases.VaultDatabasesProperties

View File

@@ -17,7 +17,6 @@
package org.springframework.cloud.vault.config.databases;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
@@ -30,7 +29,6 @@ import org.springframework.cloud.vault.util.CanConnect;
import org.springframework.cloud.vault.util.IntegrationTestSupport;
import org.springframework.cloud.vault.util.Settings;
import org.springframework.cloud.vault.util.Version;
import org.springframework.vault.core.VaultOperations;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assume.assumeTrue;
@@ -45,16 +43,6 @@ import static org.springframework.cloud.vault.config.databases.VaultConfigDataba
*/
public class MySqlDatabaseSecretIntegrationTests extends IntegrationTestSupport {
private static final int MYSQL_PORT = 3306;
private static final String MYSQL_HOST = "localhost";
private static final String ROOT_CREDENTIALS = String.format("springvault:springvault@tcp(%s:%d)/", MYSQL_HOST,
MYSQL_PORT);
private static final String CREATE_USER_AND_GRANT_SQL = "CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';"
+ "GRANT SELECT ON *.* TO '{{name}}'@'%';";
private VaultProperties vaultProperties = Settings.createVaultProperties();
private VaultConfigOperations configOperations;
@@ -68,33 +56,17 @@ public class MySqlDatabaseSecretIntegrationTests extends IntegrationTestSupport
@Before
public void setUp() {
assumeTrue(CanConnect.to(new InetSocketAddress(MYSQL_HOST, MYSQL_PORT)));
assumeTrue(CanConnect.to(new InetSocketAddress(MySqlFixtures.MYSQL_HOST, MySqlFixtures.MYSQL_PORT)));
assumeTrue(prepare().getVersion().isGreaterThanOrEqualTo(Version.parse("0.7.1")));
this.mySql.setEnabled(true);
this.mySql.setRole("readonly");
this.mySql.setBackend("database");
if (!prepare().hasSecretBackend(this.mySql.getBackend())) {
prepare().mountSecret(this.mySql.getBackend());
}
MySqlFixtures.setupMysql(this.vaultRule);
VaultOperations vaultOperations = this.vaultRule.prepare().getVaultOperations();
Map<String, String> config = new HashMap<>();
config.put("plugin_name", "mysql-legacy-database-plugin");
config.put("connection_url", ROOT_CREDENTIALS);
config.put("allowed_roles", "readonly");
vaultOperations.write(String.format("%s/config/mysql", this.mySql.getBackend()), config);
Map<String, String> body = new HashMap<>();
body.put("db_name", "mysql");
body.put("creation_statements", CREATE_USER_AND_GRANT_SQL);
vaultOperations.write(String.format("%s/roles/%s", this.mySql.getBackend(), this.mySql.getRole()), body);
this.configOperations = new VaultConfigTemplate(vaultOperations, this.vaultProperties);
this.configOperations = new VaultConfigTemplate(this.vaultRule.prepare().getVaultOperations(),
this.vaultProperties);
}
@Test

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2016-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.databases;
import java.util.HashMap;
import java.util.Map;
import org.springframework.cloud.vault.util.VaultRule;
import org.springframework.vault.core.VaultOperations;
/**
* @author Mark Paluch
*/
class MySqlFixtures {
static final int MYSQL_PORT = 3306;
static final String MYSQL_HOST = "localhost";
static final String JDBC_URL = "jdbc:mysql://" + MYSQL_HOST + ":" + MYSQL_PORT
+ "/mysql?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true";
static final String ROOT_CREDENTIALS = String.format("springvault:springvault@tcp(%s:%d)/", MYSQL_HOST, MYSQL_PORT);
static final String CREATE_USER_AND_GRANT_SQL = "CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';"
+ "GRANT SELECT ON *.* TO '{{name}}'@'%';";
public static void setupMysql(VaultRule vaultRule) {
if (!vaultRule.prepare().hasSecretBackend("database")) {
vaultRule.prepare().mountSecret("database");
}
VaultOperations vaultOperations = vaultRule.prepare().getVaultOperations();
Map<String, String> config = new HashMap<>();
config.put("plugin_name", "mysql-legacy-database-plugin");
config.put("connection_url", ROOT_CREDENTIALS);
config.put("allowed_roles", "readonly");
vaultOperations.write("database/config/mysql", config);
Map<String, String> body = new HashMap<>();
body.put("db_name", "mysql");
body.put("creation_statements", CREATE_USER_AND_GRANT_SQL);
vaultOperations.write("database/roles/readonly", body);
}
}

View File

@@ -20,6 +20,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.vault.config.KeyValueSecretBackendMetadata;
@@ -46,6 +47,7 @@ import static org.assertj.core.api.Assertions.assertThat;
public class VaultConfigDatabaseBootstrapConfigurationTests extends IntegrationTestSupport {
@Autowired
@Qualifier("customFactory")
DatabaseSecretBackendMetadataFactory factory;
@SuppressWarnings("deprecation")

View File

@@ -19,8 +19,6 @@ package org.springframework.cloud.vault.config.databases;
import java.net.InetSocketAddress;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
@@ -37,7 +35,6 @@ import org.springframework.cloud.vault.util.CanConnect;
import org.springframework.cloud.vault.util.VaultRule;
import org.springframework.cloud.vault.util.Version;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.vault.core.VaultOperations;
import static org.junit.Assume.assumeTrue;
@@ -52,20 +49,10 @@ import static org.junit.Assume.assumeTrue;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = VaultConfigMySqlDatabaseTests.TestApplication.class,
properties = { "spring.cloud.vault.database.enabled=true", "spring.cloud.vault.database.role=readonly",
"spring.datasource.url=jdbc:mysql://localhost:3306/mysql?useSSL=false&serverTimezone=UTC",
"spring.main.allow-bean-definition-overriding=true", "spring.cloud.bootstrap.enabled=true" })
"spring.datasource.url=" + MySqlFixtures.JDBC_URL, "spring.main.allow-bean-definition-overriding=true",
"spring.cloud.bootstrap.enabled=true" })
public class VaultConfigMySqlDatabaseTests {
private static final int MYSQL_PORT = 3306;
private static final String MYSQL_HOST = "localhost";
private static final String ROOT_CREDENTIALS = String.format("springvault:springvault@tcp(%s:%d)/", MYSQL_HOST,
MYSQL_PORT);
private static final String CREATE_USER_AND_GRANT_SQL = "CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';"
+ "GRANT SELECT ON *.* TO '{{name}}'@'%';";
@Value("${spring.datasource.username}")
String username;
@@ -84,40 +71,20 @@ public class VaultConfigMySqlDatabaseTests {
VaultRule vaultRule = new VaultRule();
vaultRule.before();
assumeTrue(CanConnect.to(new InetSocketAddress(MYSQL_HOST, MYSQL_PORT)));
assumeTrue(CanConnect.to(new InetSocketAddress(MySqlFixtures.MYSQL_HOST, MySqlFixtures.MYSQL_PORT)));
assumeTrue(vaultRule.prepare().getVersion().isGreaterThanOrEqualTo(Version.parse("0.7.1")));
if (!vaultRule.prepare().hasSecretBackend("database")) {
vaultRule.prepare().mountSecret("database");
}
VaultOperations vaultOperations = vaultRule.prepare().getVaultOperations();
Map<String, String> config = new HashMap<>();
config.put("plugin_name", "mysql-legacy-database-plugin");
config.put("connection_url", ROOT_CREDENTIALS);
config.put("allowed_roles", "readonly");
vaultOperations.write("database/config/mysql", config);
Map<String, String> body = new HashMap<>();
body.put("db_name", "mysql");
body.put("creation_statements", CREATE_USER_AND_GRANT_SQL);
vaultOperations.write("database/roles/readonly", body);
MySqlFixtures.setupMysql(vaultRule);
}
@Test
public void shouldConnectUsingDataSource() throws SQLException {
this.dataSource.getConnection().close();
}
@Test
public void shouldConnectUsingJdbcUrlConnection() throws SQLException {
String url = String.format("jdbc:mysql://%s?useSSL=false&serverTimezone=UTC", MYSQL_HOST);
DriverManager.getConnection(url, this.username, this.password).close();
DriverManager.getConnection(MySqlFixtures.JDBC_URL, this.username, this.password).close();
}
@SpringBootApplication

View File

@@ -0,0 +1,133 @@
/*
* Copyright 2017-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.databases;
import java.net.InetSocketAddress;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.vault.util.CanConnect;
import org.springframework.cloud.vault.util.VaultRule;
import org.springframework.cloud.vault.util.Version;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.vault.core.VaultOperations;
import static org.junit.Assume.assumeTrue;
/**
* Integration tests using the database secret backend with multi-database support. In
* case this test should fail because of SSL make sure you run the test within the
* spring-cloud-vault-config/spring-cloud-vault-config directory as the keystore is
* referenced with {@code ../work/keystore.jks}.
*
* @author Mark Paluch
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = VaultConfigMySqlDatabasesBootstrapTests.TestApplication.class, properties = {
"spring.cloud.vault.databases.mysql.enabled=true", "spring.cloud.vault.databases.mysql.role=readonly",
"spring.datasource.url=jdbc:mysql://localhost:3306/mysql?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true",
"spring.main.allow-bean-definition-overriding=true", "spring.cloud.bootstrap.enabled=true" })
public class VaultConfigMySqlDatabasesBootstrapTests {
private static final int MYSQL_PORT = 3306;
private static final String MYSQL_HOST = "localhost";
private static final String ROOT_CREDENTIALS = String.format("root:springvault@tcp(%s:%d)/", MYSQL_HOST,
MYSQL_PORT);
private static final String CREATE_USER_AND_GRANT_SQL = "CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';"
+ "GRANT SELECT ON *.* TO '{{name}}'@'%';";
@Value("${spring.datasource.username}")
String username;
@Value("${spring.datasource.password}")
String password;
@Autowired
DataSource dataSource;
/**
* Initialize the mysql secret backend.
*/
@BeforeClass
public static void beforeClass() {
VaultRule vaultRule = new VaultRule();
vaultRule.before();
assumeTrue(CanConnect.to(new InetSocketAddress(MYSQL_HOST, MYSQL_PORT)));
assumeTrue(vaultRule.prepare().getVersion().isGreaterThanOrEqualTo(Version.parse("0.7.1")));
if (!vaultRule.prepare().hasSecretBackend("database")) {
vaultRule.prepare().mountSecret("database");
}
VaultOperations vaultOperations = vaultRule.prepare().getVaultOperations();
Map<String, String> config = new HashMap<>();
config.put("plugin_name", "mysql-legacy-database-plugin");
config.put("connection_url", ROOT_CREDENTIALS);
config.put("allowed_roles", "readonly");
vaultOperations.write("database/config/mysql", config);
Map<String, String> body = new HashMap<>();
body.put("db_name", "mysql");
body.put("creation_statements", CREATE_USER_AND_GRANT_SQL);
vaultOperations.write("database/roles/readonly", body);
}
@Test
public void shouldConnectUsingDataSource() throws SQLException {
this.dataSource.getConnection().close();
}
@Test
public void shouldConnectUsingJdbcUrlConnection() throws SQLException {
String url = String.format("jdbc:mysql://%s?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true",
MYSQL_HOST);
DriverManager.getConnection(url, this.username, this.password).close();
}
@SpringBootApplication
public static class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright 2017-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.databases;
import java.net.InetSocketAddress;
import java.sql.DriverManager;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.vault.util.CanConnect;
import org.springframework.cloud.vault.util.VaultRule;
import org.springframework.cloud.vault.util.Version;
import org.springframework.test.context.junit4.SpringRunner;
import static org.junit.Assume.assumeTrue;
/**
* Integration tests using the database secret backend with multi-database support. In
* case this test should fail because of SSL make sure you run the test within the
* spring-cloud-vault-config/spring-cloud-vault-config directory as the keystore is
* referenced with {@code ../work/keystore.jks}.
*
* @author Mark Paluch
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = VaultConfigMySqlDatabasesTests.TestApplication.class,
properties = { "spring.cloud.vault.databases.mysql.enabled=true",
"spring.cloud.vault.databases.mysql.role=readonly", "spring.datasource.url=" + MySqlFixtures.JDBC_URL,
"spring.config.import=vault://" })
public class VaultConfigMySqlDatabasesTests {
@Value("${spring.datasource.username}")
String username;
@Value("${spring.datasource.password}")
String password;
@Autowired
DataSource dataSource;
/**
* Initialize the mysql secret backend.
*/
@BeforeClass
public static void beforeClass() {
VaultRule vaultRule = new VaultRule();
vaultRule.before();
assumeTrue(CanConnect.to(new InetSocketAddress(MySqlFixtures.MYSQL_HOST, MySqlFixtures.MYSQL_PORT)));
assumeTrue(vaultRule.prepare().getVersion().isGreaterThanOrEqualTo(Version.parse("0.7.1")));
MySqlFixtures.setupMysql(vaultRule);
}
@Test
public void shouldConnectUsingDataSource() throws SQLException {
this.dataSource.getConnection().close();
}
@Test
public void shouldConnectUsingJdbcUrlConnection() throws SQLException {
DriverManager.getConnection(MySqlFixtures.JDBC_URL, this.username, this.password).close();
}
@SpringBootApplication
public static class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
}

View File

@@ -50,20 +50,10 @@ import static org.junit.Assume.assumeTrue;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = VaultConfigMySqlTests.TestApplication.class,
properties = { "spring.cloud.vault.mysql.enabled=true", "spring.cloud.vault.mysql.role=readonly",
"spring.datasource.url=jdbc:mysql://localhost:3306/mysql?useSSL=false&serverTimezone=UTC",
"spring.main.allow-bean-definition-overriding=true", "spring.cloud.bootstrap.enabled=true" })
"spring.datasource.url=" + MySqlFixtures.JDBC_URL, "spring.main.allow-bean-definition-overriding=true",
"spring.cloud.bootstrap.enabled=true" })
public class VaultConfigMySqlTests {
private static final int MYSQL_PORT = 3306;
private static final String MYSQL_HOST = "localhost";
private static final String ROOT_CREDENTIALS = String.format("springvault:springvault@tcp(%s:%d)/", MYSQL_HOST,
MYSQL_PORT);
private static final String CREATE_USER_AND_GRANT_SQL = "CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';"
+ "GRANT SELECT ON *.* TO '{{name}}'@'%';";
@Value("${spring.datasource.username}")
String username;
@@ -79,7 +69,7 @@ public class VaultConfigMySqlTests {
@BeforeClass
public static void beforeClass() {
assumeTrue(CanConnect.to(new InetSocketAddress(MYSQL_HOST, MYSQL_PORT)));
assumeTrue(CanConnect.to(new InetSocketAddress(MySqlFixtures.MYSQL_HOST, MySqlFixtures.MYSQL_PORT)));
VaultRule vaultRule = new VaultRule();
vaultRule.before();
@@ -90,22 +80,21 @@ public class VaultConfigMySqlTests {
VaultOperations vaultOperations = vaultRule.prepare().getVaultOperations();
vaultOperations.write("mysql/config/connection", Collections.singletonMap("connection_url", ROOT_CREDENTIALS));
vaultOperations.write("mysql/config/connection",
Collections.singletonMap("connection_url", MySqlFixtures.ROOT_CREDENTIALS));
vaultOperations.write("mysql/roles/readonly", Collections.singletonMap("sql", CREATE_USER_AND_GRANT_SQL));
vaultOperations.write("mysql/roles/readonly",
Collections.singletonMap("sql", MySqlFixtures.CREATE_USER_AND_GRANT_SQL));
}
@Test
public void shouldConnectUsingDataSource() throws SQLException {
this.dataSource.getConnection().close();
}
@Test
public void shouldConnectUsingJdbcUrlConnection() throws SQLException {
String url = String.format("jdbc:mysql://%s?useSSL=false&serverTimezone=UTC", MYSQL_HOST);
DriverManager.getConnection(url, this.username, this.password).close();
DriverManager.getConnection(MySqlFixtures.JDBC_URL, this.username, this.password).close();
}
@SpringBootApplication

View File

@@ -16,7 +16,9 @@
package org.springframework.cloud.vault.config;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectFactory;
@@ -61,6 +63,9 @@ public class VaultBootstrapPropertySourceConfiguration implements InitializingBe
@Nullable
private Collection<VaultSecretBackendDescriptor> vaultSecretBackendDescriptors;
@Nullable
private Collection<VaultSecretBackendDescriptorFactory> vaultSecretBackendDescriptorFactories;
@Nullable
private Collection<SecretBackendMetadataFactory<? super VaultSecretBackendDescriptor>> factories;
@@ -77,6 +82,9 @@ public class VaultBootstrapPropertySourceConfiguration implements InitializingBe
this.vaultSecretBackendDescriptors = this.applicationContext.getBeansOfType(VaultSecretBackendDescriptor.class)
.values();
this.vaultSecretBackendDescriptorFactories = this.applicationContext
.getBeansOfType(VaultSecretBackendDescriptorFactory.class).values();
this.factories = (Collection) this.applicationContext.getBeansOfType(SecretBackendMetadataFactory.class)
.values();
}
@@ -93,8 +101,12 @@ public class VaultBootstrapPropertySourceConfiguration implements InitializingBe
Collection<VaultConfigurer> vaultConfigurers = this.applicationContext.getBeansOfType(VaultConfigurer.class)
.values();
List<VaultSecretBackendDescriptor> descriptors = new ArrayList<>(this.vaultSecretBackendDescriptors);
this.vaultSecretBackendDescriptorFactories.forEach(it -> descriptors.addAll(it.create()));
PropertySourceLocatorConfigurationFactory factory = new PropertySourceLocatorConfigurationFactory(
vaultConfigurers, this.vaultSecretBackendDescriptors, this.factories);
vaultConfigurers, descriptors, this.factories);
PropertySourceLocatorConfiguration configuration = factory.getPropertySourceConfiguration(kvBackendProperties);

View File

@@ -201,21 +201,35 @@ public class VaultConfigDataLocationResolver implements ConfigDataLocationResolv
private static List<VaultSecretBackendDescriptor> findDescriptors(Binder binder) {
List<String> descriptorClasses = SpringFactoriesLoader.loadFactoryNames(VaultSecretBackendDescriptor.class,
VaultConfigDataLocationResolver.class.getClassLoader());
List<String> descriptorClasses = new ArrayList<>();
descriptorClasses.addAll(SpringFactoriesLoader.loadFactoryNames(VaultSecretBackendDescriptor.class,
VaultConfigDataLocationResolver.class.getClassLoader()));
descriptorClasses.addAll(SpringFactoriesLoader.loadFactoryNames(VaultSecretBackendDescriptorFactory.class,
VaultConfigDataLocationResolver.class.getClassLoader()));
List<VaultSecretBackendDescriptor> descriptors = new ArrayList<>(descriptorClasses.size());
for (String className : descriptorClasses) {
Class<VaultSecretBackendDescriptor> descriptorClass = loadClass(className);
Class<?> descriptorClass = loadClass(className);
MergedAnnotations annotations = MergedAnnotations.from(descriptorClass);
if (annotations.isPresent(ConfigurationProperties.class)) {
String prefix = annotations.get(ConfigurationProperties.class).getString("prefix");
VaultSecretBackendDescriptor hydratedDescriptor = binder.bindOrCreate(prefix, descriptorClass);
descriptors.add(hydratedDescriptor);
Object hydratedDescriptor = binder.bindOrCreate(prefix, descriptorClass);
if (hydratedDescriptor instanceof VaultSecretBackendDescriptorFactory) {
descriptors.addAll(((VaultSecretBackendDescriptorFactory) hydratedDescriptor).create());
}
else if (hydratedDescriptor instanceof VaultSecretBackendDescriptor) {
descriptors.add((VaultSecretBackendDescriptor) hydratedDescriptor);
}
else {
throw new IllegalStateException(String.format(
"Descriptor %s is neither implements VaultSecretBackendDescriptorFactory nor VaultSecretBackendDescriptor",
className));
}
}
else {
throw new IllegalStateException(String.format(
@@ -233,10 +247,9 @@ public class VaultConfigDataLocationResolver implements ConfigDataLocationResolv
}
@SuppressWarnings("unchecked")
private static Class<VaultSecretBackendDescriptor> loadClass(String className) {
private static Class<?> loadClass(String className) {
try {
return (Class<VaultSecretBackendDescriptor>) ClassUtils.forName(className,
VaultConfigDataLocationResolver.class.getClassLoader());
return ClassUtils.forName(className, VaultConfigDataLocationResolver.class.getClassLoader());
}
catch (ReflectiveOperationException e) {
ReflectionUtils.rethrowRuntimeException(e);

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2016-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;
import java.util.Collection;
/**
* Interface to be implemented by factory objects that produce
* {@link VaultSecretBackendDescriptor}.
*
* @author Mark Paluch
* @since 3.0.5
*/
public interface VaultSecretBackendDescriptorFactory {
/**
* Create a collection of {@link VaultSecretBackendDescriptor}s.
* @return a collection of {@link VaultSecretBackendDescriptor}s.
*/
Collection<? extends VaultSecretBackendDescriptor> create();
}