Add support for Couchbase through vault-plugin-database-couchbase

Original pull request: gh-408.
This commit is contained in:
fhitchen@gmail.com
2020-06-03 14:29:02 -05:00
committed by Mark Paluch
parent 6cc626c884
commit 9161d58cef
8 changed files with 571 additions and 3 deletions

View File

@@ -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.

View File

@@ -232,6 +232,7 @@ Spring Cloud Vault integrates with these backends:
* <<vault.config.backends.database>>
* <<vault.config.backends.cassandra>>
* <<vault.config.backends.couchbase>>
* <<vault.config.backends.elasticsearch>>
* <<vault.config.backends.mongodb>>
* <<vault.config.backends.mysql>>
@@ -336,6 +337,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

View File

@@ -95,6 +95,11 @@
<artifactId>spring-boot-starter-jdbc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-couchbase</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -32,16 +32,17 @@ import org.springframework.vault.core.util.PropertyTransformer;
/**
* 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

View File

@@ -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;
}
}

View File

@@ -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<String, String> 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<String, String> 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);
}
}
}

View File

@@ -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<String, String> 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<String, String> 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);
}
}
}

View File

@@ -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<String, Object> 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<String, String> 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);
}
}
}