diff --git a/README.md b/README.md index 1dfd0f1..1863f30 100644 --- a/README.md +++ b/README.md @@ -267,6 +267,46 @@ Disable Property: `org.springframework.cloud.bindings.boot.sqlserver.enable` | `spring.r2dbc.password` | `{secret/password}` | `spring.r2dbc.username` | `{secret/username}` + +### Vault +Kind: `Vault` +Disable Property: `org.springframework.cloud.bindings.boot.vault.enable` + +Any Provider: +| Property | Value +| -------- | ------------------ +| `spring.cloud.vault.uri` | `{secret/uri}` +| `spring.cloud.vault.namespace` | `{secret/namespace}` + +Provider: `approle` +| Property | Value +| -------- | ------------------ +| `spring.cloud.vault.authentication` | `APPROLE` +| `spring.cloud.vault.app-role.role-id` | `{secret/role-id}` +| `spring.cloud.vault.app-role.secret-id` | `{secret/secret-id}` +| `spring.cloud.vault.app-role.role` | `{secret/role}` +| `spring.cloud.vault.app-role.app-role-path` | `{secret/app-role-path}` + +Provider: `cert` +| Property | Value +| -------- | ------------------ +| `spring.cloud.vault.authentication` | `CERT` +| `spring.cloud.vault.ssl.key-store` | `${CNB_BINDINGS}/{name}/secret/keystore.jks` +| `spring.cloud.vault.ssl.key-store-password` | `{secret/key-store-password}` +| `spring.cloud.vault.ssl.cert-auth-path` | `{secret/cert-auth-path}` + +Provider: `cubbyhole` +| Property | Value +| -------- | ------------------ +| `spring.cloud.vault.authentication` | `CUBBYHOLE` +| `spring.cloud.vault.token` | `{secret/token}` + +Provider: `token` +| Property | Value +| -------- | ------------------ +| `spring.cloud.vault.authentication` | `TOKEN` +| `spring.cloud.vault.token` | `{secret/token}` + ### Wavefront Kind: `Wavefront` diff --git a/src/main/java/org/springframework/cloud/bindings/boot/VaultBindingsPropertiesProcessor.java b/src/main/java/org/springframework/cloud/bindings/boot/VaultBindingsPropertiesProcessor.java new file mode 100644 index 0000000..6a25318 --- /dev/null +++ b/src/main/java/org/springframework/cloud/bindings/boot/VaultBindingsPropertiesProcessor.java @@ -0,0 +1,73 @@ +/* + * Copyright 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 + * + * 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.bindings.boot; + +import org.springframework.cloud.bindings.Binding; +import org.springframework.cloud.bindings.Bindings; +import org.springframework.core.env.Environment; + +import java.util.Map; + +import static org.springframework.cloud.bindings.boot.Guards.isKindEnabled; + +/** + * An implementation of {@link BindingsPropertiesProcessor} that detects {@link Binding}s of kind: {@value KIND}. + */ +public final class VaultBindingsPropertiesProcessor implements BindingsPropertiesProcessor { + + /** + * The {@link Binding} kind that this processor is interested in: {@value}. + **/ + public static final String KIND = "Vault"; + + @Override + public void process(Environment environment, Bindings bindings, Map properties) { + if (!isKindEnabled(environment, KIND)) { + return; + } + + bindings.filterBindings(KIND).forEach(binding -> { + MapMapper map = new MapMapper(binding.getSecret(), properties); + map.from("uri").to("spring.cloud.vault.uri"); + map.from("namespace").to("spring.cloud.vault.namespace"); // vault enterprise feature + + String provider = binding.getProvider(); + if (provider == null) { + return; + } + String authentication = provider.toUpperCase(); + properties.put("spring.cloud.vault.authentication", authentication); + switch (authentication) { + case "TOKEN": + case "CUBBYHOLE": + map.from("token").to("spring.cloud.vault.token"); + break; + case "APPROLE": + map.from("role-id").to("spring.cloud.vault.app-role.role-id"); + map.from("secret-id").to("spring.cloud.vault.app-role.secret-id"); + map.from("role").to("spring.cloud.vault.app-role.role"); + map.from("app-role-path").to("spring.cloud.vault.app-role.app-role-path"); + break; + case "CERT": + properties.put("spring.cloud.vault.ssl.key-store", binding.getSecretFilePath("keystore.jks").toString()); + map.from("key-store-password").to("spring.cloud.vault.ssl.key-store-password"); + map.from("cert-auth-path").to("spring.cloud.vault.ssl.cert-auth-path"); + } + }); + } + +} diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories index 0f3c20f..ee9aa7b 100644 --- a/src/main/resources/META-INF/spring.factories +++ b/src/main/resources/META-INF/spring.factories @@ -23,4 +23,5 @@ org.springframework.cloud.bindings.boot.BindingsPropertiesProcessor=\ org.springframework.cloud.bindings.boot.RedisBindingsPropertiesProcessor, \ org.springframework.cloud.bindings.boot.SpringSecurityOAuth2BindingsPropertiesProcessor, \ org.springframework.cloud.bindings.boot.SqlServerBindingsPropertiesProcessor, \ + org.springframework.cloud.bindings.boot.VaultBindingsPropertiesProcessor, \ org.springframework.cloud.bindings.boot.WavefrontBindingsPropertiesProcessor \ No newline at end of file diff --git a/src/test/java/org/springframework/cloud/bindings/boot/BindingSpecificEnvironmentPostProcessorTest.java b/src/test/java/org/springframework/cloud/bindings/boot/BindingSpecificEnvironmentPostProcessorTest.java index bc1eb60..cc04d7b 100644 --- a/src/test/java/org/springframework/cloud/bindings/boot/BindingSpecificEnvironmentPostProcessorTest.java +++ b/src/test/java/org/springframework/cloud/bindings/boot/BindingSpecificEnvironmentPostProcessorTest.java @@ -98,7 +98,7 @@ final class BindingSpecificEnvironmentPostProcessorTest { @Test @DisplayName("included implementations are registered") void includedImplementations() { - assertThat(new BindingSpecificEnvironmentPostProcessor().processors).hasSize(18); + assertThat(new BindingSpecificEnvironmentPostProcessor().processors).hasSize(19); } } diff --git a/src/test/java/org/springframework/cloud/bindings/boot/VaultPropertiesProcessorTest.java b/src/test/java/org/springframework/cloud/bindings/boot/VaultPropertiesProcessorTest.java new file mode 100644 index 0000000..e957acf --- /dev/null +++ b/src/test/java/org/springframework/cloud/bindings/boot/VaultPropertiesProcessorTest.java @@ -0,0 +1,163 @@ +/* + * Copyright 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 + * + * 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.bindings.boot; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.bindings.Binding; +import org.springframework.cloud.bindings.Bindings; +import org.springframework.cloud.bindings.FluentMap; +import org.springframework.mock.env.MockEnvironment; + +import java.nio.file.Paths; +import java.util.Collections; +import java.util.HashMap; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.cloud.bindings.boot.VaultBindingsPropertiesProcessor.KIND; + +@DisplayName("Vault BindingsPropertiesProcessor") +final class VaultPropertiesProcessorTest { + + private FluentMap baseSecret() { + return new FluentMap() + .withEntry("uri", "test-uri") + .withEntry("namespace", "test-namespace"); + } + + private FluentMap baseMetadata() { + return new FluentMap() + .withEntry("kind", KIND); + } + + private final Binding tokenBinding = new Binding( + "test-name", Paths.get("test-path"), + baseMetadata().withEntry("provider", "token"), + baseSecret().withEntry("token", "test-token") + ); + + private final Binding appRoleBinding = new Binding( + "test-name", Paths.get("test-path"), + baseMetadata().withEntry("provider", "approle"), + baseSecret() + .withEntry("role-id", "test-role-id") + .withEntry("secret-id", "test-secret-id") + .withEntry("role", "test-role") + .withEntry("app-role-path", "test-app-role-path") + ); + + private final Binding cubbyholeBinding = new Binding( + "test-name", Paths.get("test-path"), + baseMetadata().withEntry("provider", "cubbyhole"), + baseSecret().withEntry("token", "test-token") + ); + + private final Binding certBinding = new Binding( + "test-name", Paths.get("test-path"), + baseMetadata().withEntry("provider", "cert"), + baseSecret() + .withEntry("keystore.jks", "key store contents!") + .withEntry("key-store-password", "test-key-store-password") + .withEntry("cert-auth-path", "test-cert-auth-path") + ); + + private final MockEnvironment environment = new MockEnvironment(); + + private final HashMap properties = new HashMap<>(); + + @Test + @DisplayName("Supports token authentication") + void testTokenAuthentication() { + new VaultBindingsPropertiesProcessor().process(environment, new Bindings(tokenBinding), properties); + assertThat(properties) + .containsEntry("spring.cloud.vault.uri", "test-uri") + .containsEntry("spring.cloud.vault.namespace", "test-namespace") + .containsEntry("spring.cloud.vault.authentication", "TOKEN") + .containsEntry("spring.cloud.vault.token", "test-token"); + } + + @Test + @DisplayName("Supports AppRole authentication") + void testAppRoleAuthentication() { + new VaultBindingsPropertiesProcessor().process(environment, new Bindings(appRoleBinding), properties); + assertThat(properties) + .containsEntry("spring.cloud.vault.uri", "test-uri") + .containsEntry("spring.cloud.vault.namespace", "test-namespace") + .containsEntry("spring.cloud.vault.authentication", "APPROLE") + .containsEntry("spring.cloud.vault.app-role.role-id", "test-role-id") + .containsEntry("spring.cloud.vault.app-role.secret-id", "test-secret-id") + .containsEntry("spring.cloud.vault.app-role.role", "test-role") + .containsEntry("spring.cloud.vault.app-role.app-role-path", "test-app-role-path"); + } + + @Test + @DisplayName("Supports cubbyhole authentication") + void testCubbyholeAuthentication() { + new VaultBindingsPropertiesProcessor().process(environment, new Bindings(cubbyholeBinding), properties); + assertThat(properties) + .containsEntry("spring.cloud.vault.uri", "test-uri") + .containsEntry("spring.cloud.vault.namespace", "test-namespace") + .containsEntry("spring.cloud.vault.authentication", "CUBBYHOLE") + .containsEntry("spring.cloud.vault.token", "test-token"); + } + + @Test + @DisplayName("Supports TLS certificate authentication") + void testCertAuthentication() { + new VaultBindingsPropertiesProcessor().process(environment, new Bindings(certBinding), properties); + assertThat(properties) + .containsEntry("spring.cloud.vault.uri", "test-uri") + .containsEntry("spring.cloud.vault.namespace", "test-namespace") + .containsEntry("spring.cloud.vault.authentication", "CERT") + .containsEntry("spring.cloud.vault.ssl.key-store", "test-path/secret/keystore.jks") + .containsEntry("spring.cloud.vault.ssl.key-store-password", "test-key-store-password") + .containsEntry("spring.cloud.vault.ssl.cert-auth-path", "test-cert-auth-path"); + } + + @Test + @DisplayName("Handles missing provider") + void testMissingProvider() { + new VaultBindingsPropertiesProcessor().process(environment, new Bindings(new Binding( + "test-name", + Paths.get("test-path"), + baseMetadata(), + baseSecret() + )), properties); + assertThat(properties) + .containsEntry("spring.cloud.vault.uri", "test-uri") + .containsEntry("spring.cloud.vault.namespace", "test-namespace") + .doesNotContainKey("spring.cloud.vault.authentication"); + } + + //Vault agent authentication can be configured using a sidecar and should not require a binding + + @Test + @DisplayName("can be disabled") + void disabled() { + environment.setProperty("org.springframework.cloud.bindings.boot.vault.enable", "false"); + + new VaultBindingsPropertiesProcessor().process(environment, new Bindings( + tokenBinding, + appRoleBinding, + cubbyholeBinding, + certBinding + ), properties); + + assertThat(properties).isEmpty(); + } + +}