Polishing

Fix property names to spring.couchbase instead of spring.data.couchbase. Fix VaultConfigCouchbaseTests to make it work with Couchbase instead of using Cassandra.

Add CouchbaseSecretIntegrationTests to verify interaction through VaultConfigTemplate. Remove property overrides for username/password so that Vault PropertySources are used. Use unique role name to avoid clashes with other tests.

Enable VaultConfigCouchbaseDatabaseTests for ConfigData API by importing vault://. Update reference documentation.

Resolves gh-408.
This commit is contained in:
Mark Paluch
2020-09-21 13:53:26 +02:00
parent 9161d58cef
commit c65ac19eb7
13 changed files with 165 additions and 208 deletions

1
.gitignore vendored
View File

@@ -25,4 +25,5 @@ pom.xml.releaseBackup
release.properties
.vscode/
.sdkmanrc
plugins/

View File

@@ -6,3 +6,4 @@ This section briefly covers items that are new and noteworthy in the latest rele
=== New in Spring Cloud Vault 3.0
* Migration of `PropertySource` initialization from Spring Cloud's Bootstrap Context to Spring Boot's <<vault.configdata,ConfigData API>>.
* Support for the <<vault.config.backends.couchbase>> backend.

View File

@@ -344,8 +344,8 @@ 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.
Username and password are available from `spring.couchbase.username`
and `spring.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`.
@@ -358,8 +358,8 @@ spring.cloud.vault:
enabled: true
role: readonly
backend: database
username-property: spring.data.couchbase.username
password-property: spring.data.couchbase.password
username-property: spring.couchbase.username
password-property: spring.couchbase.password
----
====
@@ -369,7 +369,7 @@ spring.cloud.vault:
* `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]
See also: https://github.com/hashicorp/vault-plugin-database-couchbase[Couchbase Database Plugin Documentation]
[[vault.config.backends.elasticsearch]]
=== Elasticsearch

View File

@@ -48,6 +48,12 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.couchbase.client</groupId>
<artifactId>java-client</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
@@ -95,11 +101,13 @@
<artifactId>spring-boot-starter-jdbc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-couchbase</artifactId>
<scope>test</scope>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-couchbase</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -41,8 +41,8 @@ import org.springframework.vault.core.util.PropertyTransformer;
*/
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties({ VaultMySqlProperties.class, VaultPostgreSqlProperties.class,
VaultCassandraProperties.class, VaultMongoProperties.class, VaultElasticsearchProperties.class,
VaultDatabaseProperties.class, VaultCouchbaseProperties.class })
VaultCassandraProperties.class, VaultCouchbaseProperties.class, VaultMongoProperties.class,
VaultElasticsearchProperties.class, VaultDatabaseProperties.class })
public class VaultConfigDatabaseBootstrapConfiguration {
@Bean

View File

@@ -56,13 +56,13 @@ public class VaultCouchbaseProperties implements DatabaseSecretProperties {
* Target property for the obtained username.
*/
@NotEmpty
private String usernameProperty = "spring.data.couchbase.username";
private String usernameProperty = "spring.couchbase.username";
/**
* Target property for the obtained password.
*/
@NotEmpty
private String passwordProperty = "spring.data.couchbase.password";
private String passwordProperty = "spring.couchbase.password";
@Override
public boolean isEnabled() {

View File

@@ -6,9 +6,10 @@ org.springframework.cloud.vault.config.SecretBackendMetadataFactory=\
org.springframework.cloud.vault.config.databases.VaultConfigDatabaseBootstrapConfiguration.DatabaseSecretBackendMetadataFactory
org.springframework.cloud.vault.config.VaultSecretBackendDescriptor=\
org.springframework.cloud.vault.config.databases.VaultMySqlProperties,\
org.springframework.cloud.vault.config.databases.VaultPostgreSqlProperties,\
org.springframework.cloud.vault.config.databases.VaultCassandraProperties,\
org.springframework.cloud.vault.config.databases.VaultMongoProperties,\
org.springframework.cloud.vault.config.databases.VaultCouchbaseProperties,\
org.springframework.cloud.vault.config.databases.VaultDatabaseProperties,\
org.springframework.cloud.vault.config.databases.VaultElasticsearchProperties,\
org.springframework.cloud.vault.config.databases.VaultDatabaseProperties
org.springframework.cloud.vault.config.databases.VaultMongoProperties,\
org.springframework.cloud.vault.config.databases.VaultMySqlProperties,\
org.springframework.cloud.vault.config.databases.VaultPostgreSqlProperties

View File

@@ -0,0 +1,102 @@
/*
* 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 org.junit.Before;
import org.junit.Test;
import org.springframework.cloud.vault.config.VaultConfigOperations;
import org.springframework.cloud.vault.config.VaultConfigTemplate;
import org.springframework.cloud.vault.config.VaultProperties;
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;
import static org.springframework.cloud.vault.config.databases.VaultConfigDatabaseBootstrapConfiguration.DatabaseSecretBackendMetadataFactory.forDatabase;
/**
* Integration tests for {@link VaultConfigTemplate} using the couchbase secret backend.
* This test requires a running Couchbase instance, see {@link #COUCHBASE_HOST}.
*
* @author Mark Paluch
*/
public class CouchbaseSecretIntegrationTests extends IntegrationTestSupport {
private static final int COUCHBASE_PORT = 8091;
private static final String COUCHBASE_HOST = "localhost";
private VaultProperties vaultProperties = Settings.createVaultProperties();
private VaultConfigOperations configOperations;
private VaultCouchbaseProperties couchbaseProperties = new VaultCouchbaseProperties();
/**
* Initialize couchbase secret backend.
*/
@Before
public void setUp() {
assumeTrue(CanConnect.to(new InetSocketAddress(COUCHBASE_HOST, COUCHBASE_PORT)));
assumeTrue(this.vaultRule.prepare().getVersion().isGreaterThanOrEqualTo(Version.parse("1.3.0")));
this.couchbaseProperties.setEnabled(true);
this.couchbaseProperties.setRole("couchbase-readonly");
if (!prepare().hasSecretBackend(this.couchbaseProperties.getBackend())) {
prepare().mountSecret(this.couchbaseProperties.getBackend());
}
VaultOperations vaultOperations = this.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/couchbase-readonly", body);
this.configOperations = new VaultConfigTemplate(vaultOperations, this.vaultProperties);
}
@Test
public void shouldCreateCredentialsCorrectly() {
Map<String, Object> secretProperties = this.configOperations.read(forDatabase(this.couchbaseProperties))
.getData();
assertThat(secretProperties).containsKeys("spring.couchbase.username", "spring.couchbase.password");
}
}

View File

@@ -1,139 +0,0 @@
/*
* 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

@@ -51,10 +51,8 @@ import static org.junit.Assume.assumeTrue;
*/
@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",
properties = { "spring.cloud.vault.couchbase.enabled=true", "spring.config.import=vault://",
"spring.cloud.vault.couchbase.role=couchbase-readonly",
"spring.main.allow-bean-definition-overriding=true" })
public class VaultConfigCouchbaseDatabaseTests {
@@ -62,10 +60,10 @@ public class VaultConfigCouchbaseDatabaseTests {
private static final String COUCHBASE_HOST = "localhost";
@Value("${spring.data.couchbase.username}")
@Value("${spring.couchbase.username}")
String username;
@Value("${spring.data.couchbase.password}")
@Value("${spring.couchbase.password}")
String password;
Cluster cluster;
@@ -80,8 +78,7 @@ public class VaultConfigCouchbaseDatabaseTests {
vaultRule.before();
assumeTrue(CanConnect.to(new InetSocketAddress(COUCHBASE_HOST, COUCHBASE_PORT)));
assumeTrue(vaultRule.prepare().getVersion()
.isGreaterThanOrEqualTo(Version.parse("0.7.1")));
assumeTrue(vaultRule.prepare().getVersion().isGreaterThanOrEqualTo(Version.parse("1.3.0")));
if (!vaultRule.prepare().hasSecretBackend("database")) {
vaultRule.prepare().mountSecret("database");
@@ -102,7 +99,7 @@ public class VaultConfigCouchbaseDatabaseTests {
body.put("db_name", "spring-cloud-vault-couchbase");
body.put("creation_statements", "{\"roles\":[{\"role\":\"ro_admin\"}]}");
vaultOperations.write("database/roles/readonly", body);
vaultOperations.write("database/roles/couchbase-readonly", body);
}
@Test

View File

@@ -20,7 +20,7 @@ import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.Map;
import com.datastax.oss.driver.api.core.CqlSession;
import com.couchbase.client.java.Cluster;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -32,6 +32,7 @@ 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;
@@ -48,30 +49,23 @@ import static org.junit.Assume.assumeTrue;
*/
@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" })
properties = { "spring.cloud.vault.couchbase.enabled=true", "spring.config.import=vault://",
"spring.cloud.vault.couchbase.role=couchbase-readonly",
"spring.couchbase.connection-string=couchbase://localhost" })
public class VaultConfigCouchbaseTests {
private static final int COUCHBASE_PORT = 8091;
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}")
@Value("${spring.couchbase.username}")
String username;
@Value("${spring.data.couchbase.password}")
@Value("${spring.couchbase.password}")
String password;
@Autowired
CqlSession cqlSession;
Cluster cluster;
/**
* Initialize the couchbase secret backend.
@@ -79,47 +73,37 @@ public class VaultConfigCouchbaseTests {
@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");
}
assumeTrue(CanConnect.to(new InetSocketAddress(COUCHBASE_HOST, COUCHBASE_PORT)));
assumeTrue(vaultRule.prepare().getVersion().isGreaterThanOrEqualTo(Version.parse("1.3.0")));
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);
if (!vaultRule.prepare().hasSecretBackend("database")) {
vaultRule.prepare().mountSecret("database");
}
vaultOperations.write(String.format("%s/config/connection", "couchbase"),
connection);
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", "*");
Map<String, String> role = new HashMap<>();
vaultOperations.write("database/config/spring-cloud-vault-couchbase", config);
role.put("creation_cql", CREATE_USER_AND_GRANT_CQL);
role.put("consistency", "All");
Map<String, String> body = new HashMap<>();
body.put("db_name", "spring-cloud-vault-couchbase");
body.put("creation_statements", "{\"roles\":[{\"role\":\"ro_admin\"}]}");
vaultOperations.write("couchbase/roles/readonly", role);
vaultOperations.write("database/roles/couchbase-readonly", body);
}
@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();
}
assertThat(this.cluster.buckets().getAllBuckets()).isNotEmpty();
}
@SpringBootApplication

View File

@@ -8,7 +8,7 @@
set -o errexit
EDITION="${EDITION:-oss}"
VAULT_OSS="${VAULT_OSS:-1.3.4}"
VAULT_OSS="${VAULT_OSS:-1.5.0}"
VAULT_ENT="${VAULT_ENT:-0.11.0}"
UNAME=$(uname -s | tr '[:upper:]' '[:lower:]')
VERBOSE=false

View File

@@ -7,4 +7,6 @@ listener "tcp" {
tls_key_file = "work/ca/private/localhost.decrypted.key.pem"
}
plugin_directory = "plugins"
api_addr = "https://127.0.0.1:8200"
disable_mlock = true