diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index 3067273e..83644689 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -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. diff --git a/docs/src/main/asciidoc/new-features.adoc b/docs/src/main/asciidoc/new-features.adoc index ede4b102..0684ccc8 100644 --- a/docs/src/main/asciidoc/new-features.adoc +++ b/docs/src/main/asciidoc/new-features.adoc @@ -9,3 +9,4 @@ This section briefly covers items that are new and noteworthy in the latest rele * Support for the <> 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 <>. diff --git a/docs/src/main/asciidoc/secret-backends.adoc b/docs/src/main/asciidoc/secret-backends.adoc index 19df2b6a..a004b6fa 100644 --- a/docs/src/main/asciidoc/secret-backends.adoc +++ b/docs/src/main/asciidoc/secret-backends.adoc @@ -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 +---- +==== + +* `` descriptive name of the database configuration. +* `.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. Make sure to use unique property names to avoid property shadowing. +* `.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] diff --git a/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/VaultConfigDatabaseBootstrapConfiguration.java b/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/VaultConfigDatabaseBootstrapConfiguration.java index 864bb21f..278971e4 100644 --- a/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/VaultConfigDatabaseBootstrapConfiguration.java +++ b/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/VaultConfigDatabaseBootstrapConfiguration.java @@ -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() { diff --git a/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/VaultMultipleDatabaseProperties.java b/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/VaultDatabasesProperties.java similarity index 54% rename from spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/VaultMultipleDatabaseProperties.java rename to spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/VaultDatabasesProperties.java index fa2c8536..a48dddfd 100644 --- a/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/VaultMultipleDatabaseProperties.java +++ b/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/VaultDatabasesProperties.java @@ -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 databases = new ArrayList<>(); + private Map databases = new HashMap<>(); - public List getDatabases() { - return databases; + public Map getDatabases() { + return this.databases; } - public void setDatabases(List databases) { + public void setDatabases(Map databases) { this.databases = databases; } + @Override + public Collection create() { + return getDatabases().values(); + } + } diff --git a/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/VaultMySqlProperties.java b/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/VaultMySqlProperties.java index 05ccfcdb..4696e002 100644 --- a/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/VaultMySqlProperties.java +++ b/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/VaultMySqlProperties.java @@ -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 diff --git a/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/VaultPostgreSqlProperties.java b/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/VaultPostgreSqlProperties.java index aa1b1ecd..93c08212 100644 --- a/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/VaultPostgreSqlProperties.java +++ b/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/VaultPostgreSqlProperties.java @@ -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 diff --git a/spring-cloud-vault-config-databases/src/main/resources/META-INF/spring.factories b/spring-cloud-vault-config-databases/src/main/resources/META-INF/spring.factories index 59d5e158..6a44ee66 100644 --- a/spring-cloud-vault-config-databases/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-vault-config-databases/src/main/resources/META-INF/spring.factories @@ -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 diff --git a/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/MySqlDatabaseSecretIntegrationTests.java b/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/MySqlDatabaseSecretIntegrationTests.java index 3803c047..b5dd3c94 100644 --- a/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/MySqlDatabaseSecretIntegrationTests.java +++ b/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/MySqlDatabaseSecretIntegrationTests.java @@ -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 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 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 diff --git a/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/MySqlFixtures.java b/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/MySqlFixtures.java new file mode 100644 index 00000000..f267753d --- /dev/null +++ b/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/MySqlFixtures.java @@ -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 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 body = new HashMap<>(); + body.put("db_name", "mysql"); + body.put("creation_statements", CREATE_USER_AND_GRANT_SQL); + + vaultOperations.write("database/roles/readonly", body); + } + +} diff --git a/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/VaultConfigDatabaseBootstrapConfigurationTests.java b/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/VaultConfigDatabaseBootstrapConfigurationTests.java index ca2fe7af..e223f50c 100644 --- a/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/VaultConfigDatabaseBootstrapConfigurationTests.java +++ b/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/VaultConfigDatabaseBootstrapConfigurationTests.java @@ -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") diff --git a/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/VaultConfigMySqlDatabaseTests.java b/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/VaultConfigMySqlDatabaseTests.java index c3cb10cd..625ac84b 100644 --- a/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/VaultConfigMySqlDatabaseTests.java +++ b/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/VaultConfigMySqlDatabaseTests.java @@ -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 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 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 diff --git a/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/VaultConfigMySqlDatabasesBootstrapTests.java b/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/VaultConfigMySqlDatabasesBootstrapTests.java new file mode 100644 index 00000000..0904974c --- /dev/null +++ b/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/VaultConfigMySqlDatabasesBootstrapTests.java @@ -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 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 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); + } + + } + +} diff --git a/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/VaultConfigMySqlDatabasesTests.java b/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/VaultConfigMySqlDatabasesTests.java new file mode 100644 index 00000000..909702cf --- /dev/null +++ b/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/VaultConfigMySqlDatabasesTests.java @@ -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); + } + + } + +} diff --git a/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/VaultConfigMySqlTests.java b/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/VaultConfigMySqlTests.java index b76785f4..b1f9f457 100644 --- a/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/VaultConfigMySqlTests.java +++ b/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/VaultConfigMySqlTests.java @@ -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 diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapPropertySourceConfiguration.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapPropertySourceConfiguration.java index 2d1e1a32..0c4a9cad 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapPropertySourceConfiguration.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapPropertySourceConfiguration.java @@ -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 vaultSecretBackendDescriptors; + @Nullable + private Collection vaultSecretBackendDescriptorFactories; + @Nullable private Collection> 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 vaultConfigurers = this.applicationContext.getBeansOfType(VaultConfigurer.class) .values(); + + List 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); diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigDataLocationResolver.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigDataLocationResolver.java index 72b27cae..5cc02ce1 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigDataLocationResolver.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigDataLocationResolver.java @@ -201,21 +201,35 @@ public class VaultConfigDataLocationResolver implements ConfigDataLocationResolv private static List findDescriptors(Binder binder) { - List descriptorClasses = SpringFactoriesLoader.loadFactoryNames(VaultSecretBackendDescriptor.class, - VaultConfigDataLocationResolver.class.getClassLoader()); + List descriptorClasses = new ArrayList<>(); + descriptorClasses.addAll(SpringFactoriesLoader.loadFactoryNames(VaultSecretBackendDescriptor.class, + VaultConfigDataLocationResolver.class.getClassLoader())); + descriptorClasses.addAll(SpringFactoriesLoader.loadFactoryNames(VaultSecretBackendDescriptorFactory.class, + VaultConfigDataLocationResolver.class.getClassLoader())); List descriptors = new ArrayList<>(descriptorClasses.size()); for (String className : descriptorClasses) { - Class 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 loadClass(String className) { + private static Class loadClass(String className) { try { - return (Class) ClassUtils.forName(className, - VaultConfigDataLocationResolver.class.getClassLoader()); + return ClassUtils.forName(className, VaultConfigDataLocationResolver.class.getClassLoader()); } catch (ReflectiveOperationException e) { ReflectionUtils.rethrowRuntimeException(e); diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultSecretBackendDescriptorFactory.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultSecretBackendDescriptorFactory.java new file mode 100644 index 00000000..2e2bdf7a --- /dev/null +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultSecretBackendDescriptorFactory.java @@ -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 create(); + +}