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:
@@ -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.
|
||||
|
||||
@@ -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>>.
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user