diff --git a/.gitignore b/.gitignore index 3c8daef1..63151953 100644 --- a/.gitignore +++ b/.gitignore @@ -17,5 +17,6 @@ _site/ download/ /vault/ /consul/ +/rabbitmq/ work build/ diff --git a/.travis.yml b/.travis.yml index 2960304d..13d8e01d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ language: java services: - mysql - postgresql + - rabbitmq jdk: - oraclejdk8 @@ -18,6 +19,8 @@ install: - src/test/bash/install_consul.sh - src/test/bash/local_run_vault.sh & - src/test/bash/local_run_consul.sh & + - sudo rabbitmq-plugins enable rabbitmq_management + - sudo service rabbitmq-server restart before_script: - mysql -e "CREATE USER 'spring' IDENTIFIED by 'vault';" diff --git a/docs/src/main/asciidoc/spring-cloud-vault-config.adoc b/docs/src/main/asciidoc/spring-cloud-vault-config.adoc index 3e793cbb..d3b3d3cb 100644 --- a/docs/src/main/asciidoc/spring-cloud-vault-config.adoc +++ b/docs/src/main/asciidoc/spring-cloud-vault-config.adoc @@ -233,6 +233,28 @@ spring.cloud.vault: See also: https://www.vaultproject.io/docs/secrets/consul/index.html[Vault Documentation: Setting up Consul with Vault] +[[vault-client-rabbitmq]] +=== RabbitMQ + +Spring Cloud Vault allows to obtain credentials for RabbitMQ. +The integration can be enabled by setting `spring.cloud.vault.rabbit.enabled=true` +(default `false`). Username and password are stored in `spring.rabbit.username` +and `spring.rabbit.password` so using Spring Boot will pick up the generated +credentials without further configuration. You can configure the property names +by setting `spring.cloud.vault.rabbit.username-property` and +`spring.cloud.vault.rabbit.password-property`. + +[source,yaml] +---- +spring.cloud.vault: + enabled: true + ... + rabbit: + enabled: true +---- + +See also: https://www.vaultproject.io/docs/secrets/rabbit/index.html[Vault Documentation: Setting up RabbitMQ with Vault] + [[vault-client-fail-fast]] == Vault Client Fail Fast diff --git a/spring-cloud-vault-config/pom.xml b/spring-cloud-vault-config/pom.xml index 7291541b..7ecaf34e 100644 --- a/spring-cloud-vault-config/pom.xml +++ b/spring-cloud-vault-config/pom.xml @@ -57,6 +57,11 @@ spring-boot-starter-jdbc test + + org.springframework.boot + spring-boot-starter-amqp + test + org.apache.httpcomponents diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/VaultProperties.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/VaultProperties.java index 0553be82..f26bf1e2 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/VaultProperties.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/VaultProperties.java @@ -102,6 +102,8 @@ public class VaultProperties { private Consul consul = new Consul(); + private Rabbitmq rabbitmq = new Rabbitmq(); + /** * Application name for AppId authentication. */ @@ -279,6 +281,38 @@ public class VaultProperties { private String tokenProperty = "spring.cloud.consul.token"; } + @Data + public static class Rabbitmq implements DatabaseSecretProperties { + + /** + * Enable rabbitmq backend usage. + */ + private boolean enabled = false; + + /** + * Role name for credentials. + */ + private String role; + + /** + * RabbitMQ backend path. + */ + @NotEmpty + private String backend = "rabbitmq"; + + /** + * Target property for the obtained username. + */ + @NotEmpty + private String usernameProperty = "spring.rabbitmq.username"; + + /** + * Target property for the obtained password. + */ + @NotEmpty + private String passwordProperty = "spring.rabbitmq.password"; + } + /** * Configuration properties interface for database secrets. */ diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/VaultPropertySource.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/VaultPropertySource.java index 6d727f56..a22761d8 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/VaultPropertySource.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/VaultPropertySource.java @@ -106,6 +106,11 @@ public class VaultPropertySource extends EnumerablePropertySource { if (consul.isEnabled()) { accessors.add(SecureBackendAccessors.consul(consul)); } + + VaultProperties.Rabbitmq rabbitmq = vaultProperties.getRabbitmq(); + if (rabbitmq.isEnabled()) { + accessors.add(SecureBackendAccessors.database(rabbitmq)); + } return accessors; } diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/RabbitMQSecretIntegrationTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/RabbitMQSecretIntegrationTests.java new file mode 100644 index 00000000..b66f2aa2 --- /dev/null +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/RabbitMQSecretIntegrationTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2016 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 + * + * http://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; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.Assume.*; +import static org.springframework.cloud.vault.SecureBackendAccessors.*; + +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.cloud.vault.util.CanConnect; +import org.springframework.cloud.vault.util.Settings; + +/** + * Integration tests for {@link VaultClient} using the rabbitmq secret backend. This test + * requires a running RabbitMQ instance, see {@link #RABBITMQ_URI}. + * + * @author Mark Paluch + */ +public class RabbitMQSecretIntegrationTests extends AbstractIntegrationTests { + + private final static int RABBITMQ_HTTP_MANAGEMENT_PORT = 15672; + private final static String RABBITMQ_HOST = "localhost"; + + private final static String RABBITMQ_USERNAME = "guest"; + private final static String RABBITMQ_PASSWORD = "guest"; + + private final static String RABBITMQ_URI = String + .format("http://%s:%d", RABBITMQ_HOST, RABBITMQ_HTTP_MANAGEMENT_PORT); + + private final static String VHOSTS_ROLE = "{\"/\":{\"write\": \".*\", \"read\": \".*\"}}"; + + private VaultProperties vaultProperties = Settings.createVaultProperties(); + private VaultClient vaultClient = new VaultClient(vaultProperties); + private VaultProperties.Rabbitmq rabbitmq = vaultProperties.getRabbitmq(); + + /** + * Initialize the mysql secret backend. + * + * @throws Exception + */ + @Before + public void setUp() throws Exception { + + assumeTrue(CanConnect.to(new InetSocketAddress(RABBITMQ_HOST, RABBITMQ_HTTP_MANAGEMENT_PORT))); + + rabbitmq.setEnabled(true); + rabbitmq.setRole("readonly"); + + if (!prepare().hasSecret(rabbitmq.getBackend())) { + prepare().mountSecret(rabbitmq.getBackend()); + } + + Map connection = new HashMap<>(); + connection.put("connection_uri", RABBITMQ_URI); + connection.put("username", RABBITMQ_USERNAME); + connection.put("password", RABBITMQ_PASSWORD); + + prepare().write(String.format("%s/config/connection", rabbitmq.getBackend()), + connection); + + prepare().write(String.format("%s/roles/%s", rabbitmq.getBackend(), rabbitmq.getRole()), + Collections.singletonMap("vhosts", VHOSTS_ROLE)); + + vaultClient.setRest(TestRestTemplateFactory.create(vaultProperties)); + } + + @Test + public void shouldCreateCredentialsCorrectly() throws Exception { + + Map secretProperties = vaultClient.read(database(rabbitmq), + Settings.token()); + + assertThat(secretProperties).containsKeys("spring.rabbitmq.username", + "spring.rabbitmq.password"); + } + +} diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/configclient/VaultRabbitMQTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/configclient/VaultRabbitMQTests.java new file mode 100644 index 00000000..357ce7c7 --- /dev/null +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/configclient/VaultRabbitMQTests.java @@ -0,0 +1,136 @@ +/* + * Copyright 2016 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 + * + * http://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.configclient; + +import static org.junit.Assume.*; + +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +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.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.cloud.vault.util.CanConnect; +import org.springframework.cloud.vault.util.VaultRule; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; + +/** + * Integration tests using the rabbitmq 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(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = VaultRabbitMQTests.TestApplication.class) +@IntegrationTest({ "spring.cloud.vault.rabbitmq.enabled=true", + "spring.cloud.vault.rabbitmq.role=readonly", + "spring.rabbitmq.address=localhost" }) +public class VaultRabbitMQTests { + + private final static int RABBITMQ_HTTP_MANAGEMENT_PORT = 15672; + private final static int RABBITMQ_PORT = 5672; + private final static String RABBITMQ_HOST = "localhost"; + + private final static String RABBITMQ_USERNAME = "guest"; + private final static String RABBITMQ_PASSWORD = "guest"; + + private final static String RABBITMQ_URI = String.format("http://%s:%d", + RABBITMQ_HOST, RABBITMQ_HTTP_MANAGEMENT_PORT); + + private final static String VHOSTS_ROLE = "{\"/\":{\"write\": \".*\", \"read\": \".*\"}}"; + + /** + * Initialize the rabbitmq secret backend. + * + * @throws Exception + */ + @BeforeClass + public static void beforeClass() throws Exception { + + assumeTrue(CanConnect + .to(new InetSocketAddress(RABBITMQ_HOST, RABBITMQ_HTTP_MANAGEMENT_PORT))); + + VaultRule vaultRule = new VaultRule(); + vaultRule.before(); + + if (!vaultRule.prepare().hasSecret("rabbitmq")) { + vaultRule.prepare().mountSecret("rabbitmq"); + } + + Map connection = new HashMap<>(); + connection.put("connection_uri", RABBITMQ_URI); + connection.put("username", RABBITMQ_USERNAME); + connection.put("password", RABBITMQ_PASSWORD); + + vaultRule.prepare().write(String.format("rabbitmq/config/connection"), + connection); + + vaultRule.prepare().write(String.format("rabbitmq/roles/readonly"), + Collections.singletonMap("vhosts", VHOSTS_ROLE)); + } + + @Value("${spring.rabbitmq.username}") + String username; + + @Value("${spring.rabbitmq.password}") + String password; + + @Autowired + org.springframework.amqp.rabbit.connection.ConnectionFactory connectionFactory; + + @Test + public void shouldConnectSpringConnectionFactory() { + connectionFactory.createConnection().close(); + } + + @Test + public void shouldConnectUsingRabbitMQClient() throws Exception { + + ConnectionFactory factory = new ConnectionFactory(); + factory.setHost(RABBITMQ_HOST); + factory.setPort(RABBITMQ_PORT); + factory.setUsername(username); + factory.setPassword(password); + Connection connection = factory.newConnection(); + Channel channel = connection.createChannel(); + + channel.close(); + connection.close(); + } + + @SpringBootApplication + public static class TestApplication { + + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } + } +} diff --git a/src/test/bash/install_rabbitmq.sh b/src/test/bash/install_rabbitmq.sh new file mode 100755 index 00000000..60877747 --- /dev/null +++ b/src/test/bash/install_rabbitmq.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +########################################################################### +# Download and Install RabbitMQ # +# This script is prepared for caching of the download directory # +########################################################################### + + +RABBITMQ_VER="3.6.2" +UNAME=$(uname -s | tr '[:upper:]' '[:lower:]') + +if [[ ${UNAME} == "darwin" ]] ; then + RABBITMQ_ZIP="rabbitmq-server-mac-standalone-${RABBITMQ_VER}.tar.xz" +else + echo "Installation of RabbitMQ on ${UNAME} not supported by this script" + exit 1 +fi +IGNORE_CERTS="${IGNORE_CERTS:-no}" + +# cleanup +mkdir -p rabbitmq + +if [[ ! -f "download/${RABBITMQ_ZIP}" ]] ; then + cd download + # install Vault + if [[ "${IGNORE_CERTS}" == "no" ]] ; then + echo "Downloading RabbitMQ with certs verification" + wget "https://www.rabbitmq.com/releases/rabbitmq-server/v${RABBITMQ_VER}/${RABBITMQ_ZIP}" + else + echo "WARNING... Downloading RabbitMQ WITHOUT certs verification" + wget "https://www.rabbitmq.com/releases/rabbitmq-server/v${RABBITMQ_VER}/${RABBITMQ_ZIP}" --no-check-certificate + fi + + if [[ $? != 0 ]] ; then + echo "Cannot download RabbitMQ" + exit 1 + fi + cd .. +fi + +cd rabbitmq + +if [[ -d ebin ]] ; then + rm -Rf ../rabbitmq/* +fi + +tar xzf ../download/${RABBITMQ_ZIP} diff --git a/src/test/bash/local_run_consul.sh b/src/test/bash/local_run_consul.sh index ee604fe3..0ae7660d 100755 --- a/src/test/bash/local_run_consul.sh +++ b/src/test/bash/local_run_consul.sh @@ -6,7 +6,6 @@ BASEDIR=`dirname $0`/../../.. -#!/bin/bash mkdir -p ${BASEDIR}/consul/config mkdir -p ${BASEDIR}/consul/data diff --git a/src/test/bash/local_run_rabbitmq.sh b/src/test/bash/local_run_rabbitmq.sh new file mode 100755 index 00000000..9146e988 --- /dev/null +++ b/src/test/bash/local_run_rabbitmq.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +########################################################################### +# Start RabbitMQ on localhost # +########################################################################### + +BASEDIR=`dirname $0`/../../.. + +RABBITMQ_VER="3.6.2" + +cd rabbitmq/rabbitmq_server-${RABBITMQ_VER} + +if [[ ! -f etc/rabbitmq/rabbitmq.config ]] ; then + cp etc/rabbitmq/rabbitmq.config.example etc/rabbitmq/rabbitmq.config +fi + +sbin/rabbitmq-plugins enable rabbitmq_management +sbin/rabbitmq-server + +exit $? diff --git a/src/test/bash/local_run_vault.sh b/src/test/bash/local_run_vault.sh index 297ab467..fc00213c 100755 --- a/src/test/bash/local_run_vault.sh +++ b/src/test/bash/local_run_vault.sh @@ -5,5 +5,7 @@ ########################################################################### BASEDIR=`dirname $0`/../../.. + ./vault/vault server -config=${BASEDIR}/spring-cloud-vault-config/src/test/resources/vault.conf + exit $?