diff --git a/README.adoc b/README.adoc index e18f47803..294770d3f 100644 --- a/README.adoc +++ b/README.adoc @@ -264,7 +264,7 @@ Depending on how you have set up Vault you might need additional configuration l {docs}#vault.config.ssl[SSL] and {docs}#vault.config.authentication[authentication]. -If the application imports the `spring-boot-starter-actuator` project, the status of the vault server will be available via the `/health` endpoint. +If the application imports the `spring-boot-starter-actuator` project, the status of the vault server will be available via the `/actuator/health` endpoint. The vault health indicator can be enabled or disabled through the property `management.health.vault.enabled` (default to `true`). diff --git a/docs/src/main/asciidoc/intro.adoc b/docs/src/main/asciidoc/intro.adoc index 1ac5a77cc..9d6514e64 100644 --- a/docs/src/main/asciidoc/intro.adoc +++ b/docs/src/main/asciidoc/intro.adoc @@ -1,3 +1,3 @@ Spring Cloud Vault Config provides client-side support for externalized configuration in a distributed system. With https://www.vaultproject.io[HashiCorp's Vault] you have a central place to manage external secret properties for applications across all environments. -Vault can manage static and dynamic secrets such as username/password for remote applications/resources and provide credentials for external services such as MySQL, PostgreSQL, Apache Cassandra, MongoDB, Consul, AWS and more. +Vault can manage static and dynamic secrets such as username/password for remote applications/resources and provide credentials for external services such as MySQL, PostgreSQL, Apache Cassandra, Couchbase, MongoDB, Consul, AWS and more. diff --git a/docs/src/main/asciidoc/spring-cloud-vault.adoc b/docs/src/main/asciidoc/spring-cloud-vault.adoc index 830ce3608..3a7ebe634 100644 --- a/docs/src/main/asciidoc/spring-cloud-vault.adoc +++ b/docs/src/main/asciidoc/spring-cloud-vault.adoc @@ -900,6 +900,7 @@ Spring Cloud Vault integrates with these backends: * <> * <> +* <> * <> * <> * <> @@ -1004,6 +1005,40 @@ spring.cloud.vault: See also: https://www.vaultproject.io/docs/secrets/cassandra/index.html[Vault Documentation: Setting up Apache Cassandra with Vault] +[[vault.config.backends.couchbase]] +=== Couchbase Database + +Spring Cloud Vault can obtain credentials for Couchbase. +The integration can be enabled by setting +`spring.cloud.vault.couchbase.enabled=true` (default `false`) and providing the role name with `spring.cloud.vault.couchbase.role=…`. + +Username and password are available from `spring.data.couchbase.username` +and `spring.data.couchbase.password` properties so using Spring Boot will pick up the generated credentials without further configuration. +You can configure the property names by setting +`spring.cloud.vault.couchbase.username-property` and +`spring.cloud.vault.couchbase.password-property`. + +==== +[source,yaml] +---- +spring.cloud.vault: + couchbase: + enabled: true + role: readonly + backend: database + username-property: spring.data.couchbase.username + password-property: spring.data.couchbase.password +---- +==== + +* `enabled` setting this value to `true` enables the Couchbase backend config usage +* `role` sets the role name of the Couchbase role definition +* `backend` sets the path of the Couchbase mount to use +* `username-property` sets the property name in which the Couchbase username is stored +* `password-property` sets the property name in which the Couchbase password is stored + +See also: https://www.vaultproject.io/docs/secrets/couchbase/index.html[Vault Documentation: Setting up Couchbase with Vault] + [[vault.config.backends.elasticsearch]] === Elasticsearch diff --git a/spring-cloud-vault-config-databases/pom.xml b/spring-cloud-vault-config-databases/pom.xml index 0abbfb99e..4b700aeb9 100644 --- a/spring-cloud-vault-config-databases/pom.xml +++ b/spring-cloud-vault-config-databases/pom.xml @@ -95,6 +95,11 @@ spring-boot-starter-jdbc test + + org.springframework.data + spring-data-couchbase + test + 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 700cf5874..b0965f3bc 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 @@ -32,17 +32,18 @@ /** * Bootstrap configuration providing support for the Database secret backends such as - * Database, Apache Cassandra and MongoDB. + * Database, Apache Cassandra, Couchbase and MongoDB. * * @author Mark Paluch * @author Per Abich * @author Sebastien Nahelou + * @author Francis Hitchens */ @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties({ VaultMySqlProperties.class, VaultPostgreSqlProperties.class, VaultCassandraProperties.class, VaultMongoProperties.class, VaultElasticsearchProperties.class, - VaultDatabaseProperties.class }) + VaultDatabaseProperties.class, VaultCouchbaseProperties.class }) public class VaultConfigDatabaseBootstrapConfiguration { @Bean diff --git a/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/VaultCouchbaseProperties.java b/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/VaultCouchbaseProperties.java new file mode 100644 index 000000000..790136d4e --- /dev/null +++ b/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/VaultCouchbaseProperties.java @@ -0,0 +1,121 @@ +/* + * Copyright 2016-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.vault.config.databases; + +import javax.validation.constraints.NotEmpty; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +/** + * Configuration properties for Vault using the Couchbase integration. + * + * @author Francis Hitchens + */ +@ConfigurationProperties("spring.cloud.vault.couchbase") +@Validated +public class VaultCouchbaseProperties implements DatabaseSecretProperties { + + /** + * Enable couchbase backend usage. + */ + private boolean enabled = false; + + /** + * Role name for credentials. + */ + private String role; + + /** + * Enable static role usage. + * + */ + private boolean staticRole = false; + + /** + * Couchbase backend path. + */ + @NotEmpty + private String backend = "database"; + + /** + * Target property for the obtained username. + */ + @NotEmpty + private String usernameProperty = "spring.data.couchbase.username"; + + /** + * Target property for the obtained password. + */ + @NotEmpty + private String passwordProperty = "spring.data.couchbase.password"; + + @Override + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Override + public String getRole() { + return this.role; + } + + public void setRole(String role) { + this.role = role; + } + + @Override + public boolean isStaticRole() { + return this.staticRole; + } + + public void setStaticRole(boolean staticRole) { + this.staticRole = staticRole; + } + + @Override + public String getBackend() { + return this.backend; + } + + public void setBackend(String backend) { + this.backend = backend; + } + + @Override + public String getUsernameProperty() { + return this.usernameProperty; + } + + public void setUsernameProperty(String usernameProperty) { + this.usernameProperty = usernameProperty; + } + + @Override + public String getPasswordProperty() { + return this.passwordProperty; + } + + public void setPasswordProperty(String passwordProperty) { + this.passwordProperty = passwordProperty; + } + +} diff --git a/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/VaultConfigCouchbaseDatabaseStaticTests.java b/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/VaultConfigCouchbaseDatabaseStaticTests.java new file mode 100644 index 000000000..454477eb8 --- /dev/null +++ b/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/VaultConfigCouchbaseDatabaseStaticTests.java @@ -0,0 +1,139 @@ +/* + * Copyright 2017-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.vault.config.databases; + +import java.net.InetSocketAddress; + +import com.couchbase.client.java.Cluster; +import com.couchbase.client.core.error.UnambiguousTimeoutException; + +import java.util.HashMap; +import java.util.Map; +import java.time.Duration; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +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. 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}. + * + * Uses the existing admin user that comes with the couchbase/sandbox-server docker image + * provided by Couchbase. The test will fail if this user does not exits. + * + * @author Francis Hitchens + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = VaultConfigCouchbaseDatabaseStaticTests.TestApplication.class, + properties = { "spring.cloud.vault.couchbase.enabled=true", + "spring.cloud.vault.couchbase.role=staticreadonly", + "spring.cloud.vault.couchbase.staticRole=true", + "spring.data.couchbase.username=foo", + "spring.data.couchbase.password=bar", + "spring.main.allow-bean-definition-overriding=true" }) +public class VaultConfigCouchbaseDatabaseStaticTests { + + private static final int COUCHBASE_PORT = 8091; + + private static final String COUCHBASE_HOST = "localhost"; + + @Value("${spring.data.couchbase.username}") + String username; + + @Value("${spring.data.couchbase.password}") + String password; + + Cluster cluster; + + /** + * Initialize the couchbase secret backend. + */ + @BeforeClass + public static void beforeClass() { + + VaultRule vaultRule = new VaultRule(); + vaultRule.before(); + + assumeTrue(CanConnect.to(new InetSocketAddress(COUCHBASE_HOST, COUCHBASE_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", "couchbase-database-plugin"); + config.put("hosts", "couchbase://localhost"); + config.put("username", "Administrator"); + config.put("password", "password"); + config.put("allowed_roles", "*"); + + vaultOperations.write("database/config/spring-cloud-vault-couchbase", config); + + Map body = new HashMap<>(); + body.put("db_name", "spring-cloud-vault-couchbase"); + body.put("username", "admin"); + body.put("rotation_period", "5m"); + body.put("creation_statements", "[{\"name\":\"ro_admin\"}]"); + + vaultOperations.write("database/static-roles/staticreadonly", body); + } + + @Test + public void shouldConnectConnection() throws UnambiguousTimeoutException { + + this.cluster = Cluster.connect("127.0.0.1", this.username, this.password); + this.cluster.waitUntilReady(Duration.ofSeconds(5)); + this.cluster.disconnect(); + } + + @Test(expected = UnambiguousTimeoutException.class) + public void shouldFailConnectConnection() throws UnambiguousTimeoutException { + + this.cluster = Cluster.connect("127.0.0.1", this.username, "fake.pwd"); + this.cluster.waitUntilReady(Duration.ofSeconds(5)); + this.cluster.disconnect(); + } + + @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/VaultConfigCouchbaseDatabaseTests.java b/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/VaultConfigCouchbaseDatabaseTests.java new file mode 100644 index 000000000..97093a2ab --- /dev/null +++ b/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/VaultConfigCouchbaseDatabaseTests.java @@ -0,0 +1,133 @@ +/* + * Copyright 2017-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.vault.config.databases; + +import java.net.InetSocketAddress; + +import com.couchbase.client.java.Cluster; +import com.couchbase.client.core.error.UnambiguousTimeoutException; + +import java.util.HashMap; +import java.util.Map; +import java.time.Duration; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +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. 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 Francis Hitchens + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = VaultConfigCouchbaseDatabaseTests.TestApplication.class, + properties = { "spring.cloud.vault.couchbase.enabled=true", + "spring.cloud.vault.couchbase.role=readonly", + "spring.data.couchbase.username=foo", + "spring.data.couchbase.password=bar", + "spring.main.allow-bean-definition-overriding=true" }) +public class VaultConfigCouchbaseDatabaseTests { + + private static final int COUCHBASE_PORT = 8091; + + private static final String COUCHBASE_HOST = "localhost"; + + @Value("${spring.data.couchbase.username}") + String username; + + @Value("${spring.data.couchbase.password}") + String password; + + Cluster cluster; + + /** + * Initialize the couchbase secret backend. + */ + @BeforeClass + public static void beforeClass() { + + VaultRule vaultRule = new VaultRule(); + vaultRule.before(); + + assumeTrue(CanConnect.to(new InetSocketAddress(COUCHBASE_HOST, COUCHBASE_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", "couchbase-database-plugin"); + config.put("hosts", "couchbase://localhost"); + config.put("username", "Administrator"); + config.put("password", "password"); + config.put("allowed_roles", "*"); + + vaultOperations.write("database/config/spring-cloud-vault-couchbase", config); + + Map body = new HashMap<>(); + body.put("db_name", "spring-cloud-vault-couchbase"); + body.put("creation_statements", "{\"roles\":[{\"role\":\"ro_admin\"}]}"); + + vaultOperations.write("database/roles/readonly", body); + } + + @Test + public void shouldConnectConnection() throws UnambiguousTimeoutException { + + this.cluster = Cluster.connect("127.0.0.1", this.username, this.password); + this.cluster.waitUntilReady(Duration.ofSeconds(5)); + this.cluster.disconnect(); + } + + @Test(expected = UnambiguousTimeoutException.class) + public void shouldFailConnectConnection() throws UnambiguousTimeoutException { + + this.cluster = Cluster.connect("127.0.0.1", this.username, "fake.pwd"); + this.cluster.waitUntilReady(Duration.ofSeconds(5)); + this.cluster.disconnect(); + } + + @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/VaultConfigCouchbaseTests.java b/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/VaultConfigCouchbaseTests.java new file mode 100644 index 000000000..52f7c5f93 --- /dev/null +++ b/spring-cloud-vault-config-databases/src/test/java/org/springframework/cloud/vault/config/databases/VaultConfigCouchbaseTests.java @@ -0,0 +1,134 @@ +/* + * Copyright 2016-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.vault.config.databases; + +import java.net.InetSocketAddress; +import java.util.HashMap; +import java.util.Map; + +import com.datastax.oss.driver.api.core.CqlSession; +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.test.context.junit4.SpringRunner; +import org.springframework.vault.core.VaultOperations; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assume.assumeTrue; + +/** + * Integration tests using the couchbase secret backend. 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 = VaultConfigCouchbaseTests.TestApplication.class, + properties = { "spring.cloud.vault.couchbase.enabled=true", + "spring.cloud.vault.couchbase.role=readonly", + "spring.data.couchbase.jmx-enabled=false" }) +public class VaultConfigCouchbaseTests { + + private static final String COUCHBASE_HOST = "localhost"; + + private static final int COUCHBASE_PORT = 65535; + + private static final String COUCHBASE_USERNAME = "springvault"; + + private static final String COUCHBASE_PASSWORD = "springvault"; + + private static final String CREATE_USER_AND_GRANT_CQL = "CREATE USER '{{username}}' WITH PASSWORD '{{password}}' NOSUPERUSER;" + + "GRANT SELECT ON ALL KEYSPACES TO {{username}};"; + + @Value("${spring.data.couchbase.username}") + String username; + + @Value("${spring.data.couchbase.password}") + String password; + + @Autowired + CqlSession cqlSession; + + /** + * Initialize the couchbase secret backend. + */ + @BeforeClass + public static void beforeClass() { + + assumeTrue(CanConnect.to(new InetSocketAddress(COUCHBASE_HOST, COUCHBASE_PORT))); + + VaultRule vaultRule = new VaultRule(); + vaultRule.before(); + + if (!vaultRule.prepare().hasSecretBackend("couchbase")) { + vaultRule.prepare().mountSecret("couchbase"); + } + + VaultOperations vaultOperations = vaultRule.prepare().getVaultOperations(); + + Map connection = new HashMap<>(); + connection.put("hosts", COUCHBASE_HOST); + connection.put("username", COUCHBASE_USERNAME); + connection.put("password", COUCHBASE_PASSWORD); + connection.put("protocol_version", 3); + + vaultOperations.write(String.format("%s/config/connection", "couchbase"), + connection); + + Map role = new HashMap<>(); + + role.put("creation_cql", CREATE_USER_AND_GRANT_CQL); + role.put("consistency", "All"); + + vaultOperations.write("couchbase/roles/readonly", role); + } + + @Test + public void shouldUseAuthenticatedSession() { + assertThat(this.cqlSession.getMetadata().getKeyspace("system")).isNotEmpty(); + } + + @Test + public void shouldConnectUsingCouchbaseClient() { + + try (CqlSession session = CqlSession.builder().withLocalDatacenter("dc1") + .addContactPoint(new InetSocketAddress(COUCHBASE_HOST, COUCHBASE_PORT)) + .withAuthCredentials(this.username, this.password).build()) { + assertThat(session.getMetadata().getKeyspace("system")).isNotEmpty(); + } + } + + @SpringBootApplication + public static class TestApplication { + + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } + + } + +}