diff --git a/docs/modules/ROOT/pages/authentication.adoc b/docs/modules/ROOT/pages/authentication.adoc index 9520758a..424ac177 100644 --- a/docs/modules/ROOT/pages/authentication.adoc +++ b/docs/modules/ROOT/pages/authentication.adoc @@ -399,6 +399,7 @@ spring.cloud.vault: key-store-password: changeit key-store-type: JKS cert-auth-path: cert + role: my-dev-role ---- See also: https://www.vaultproject.io/docs/auth/cert.html[Vault Documentation: Using the Cert auth backend] diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/ClientAuthenticationFactory.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/ClientAuthenticationFactory.java index 29193cf4..204e1eee 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/ClientAuthenticationFactory.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/ClientAuthenticationFactory.java @@ -378,11 +378,14 @@ private ClientAuthentication pcfAuthentication(VaultProperties vaultProperties) private ClientAuthentication certificateAuthentication(VaultProperties vaultProperties) { - ClientCertificateAuthenticationOptions options = ClientCertificateAuthenticationOptions.builder() - .path(vaultProperties.getSsl().getCertAuthPath()) - .build(); + ClientCertificateAuthenticationOptions.ClientCertificateAuthenticationOptionsBuilder optionsBuilder = ClientCertificateAuthenticationOptions + .builder(); + optionsBuilder.path(vaultProperties.getSsl().getCertAuthPath()); + if (StringUtils.hasText(vaultProperties.getSsl().getRole())) { + optionsBuilder.role(vaultProperties.getSsl().getRole()); + } - return new ClientCertificateAuthentication(options, this.restOperations); + return new ClientCertificateAuthentication(optionsBuilder.build(), this.restOperations); } private ClientAuthentication tokenAuthentication(VaultProperties vaultProperties) { diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java index b4823e2a..8fdff356 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java @@ -42,6 +42,7 @@ * @author Michal Budzyn * @author Grenville Wilson * @author Mårten Svantesson + * @author Issam El-atif */ @ConfigurationProperties(VaultProperties.PREFIX) public class VaultProperties implements EnvironmentAware { @@ -1152,6 +1153,13 @@ public static class Ssl { */ private String certAuthPath = "cert"; + /** + * Name of the role against which the login is being attempted. + * + * @since 5.0 + */ + private String role = ""; + /** * List of enabled SSL/TLS protocol. * @since 3.0.2 @@ -1226,6 +1234,14 @@ public void setCertAuthPath(String certAuthPath) { this.certAuthPath = certAuthPath; } + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + public List getEnabledProtocols() { return this.enabledProtocols; } diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigTlsCertAuthenticationTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigTlsCertAuthenticationTests.java index ce6f5316..039f8132 100644 --- a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigTlsCertAuthenticationTests.java +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigTlsCertAuthenticationTests.java @@ -23,18 +23,22 @@ import java.util.Map; import org.assertj.core.util.Files; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -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.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.vault.util.Settings; import org.springframework.cloud.vault.util.VaultRule; +import org.springframework.cloud.vault.util.VaultTestContextRunner; import org.springframework.vault.core.VaultOperations; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.springframework.cloud.vault.config.VaultProperties.AuthenticationMethod.CERT; import static org.springframework.cloud.vault.util.Settings.findWorkDir; /** @@ -44,18 +48,21 @@ * referenced with {@code ../work/keystore.jks}. * * @author Mark Paluch + * @author Issam El-atif */ - -@SpringBootTest(classes = VaultConfigTlsCertAuthenticationTests.TestApplication.class, - properties = { "spring.cloud.vault.authentication=cert", - "spring.cloud.vault.ssl.key-store=file:../work/client-cert.jks", - "spring.cloud.vault.ssl.key-store-password=changeit", - "spring.cloud.vault.application-name=VaultConfigTlsCertAuthenticationTests", - "spring.cloud.vault.reactive.enabled=false", "spring.cloud.bootstrap.enabled=true" }) +@ExtendWith(OutputCaptureExtension.class) public class VaultConfigTlsCertAuthenticationTests { - @Value("${vault.value}") - String configValue; + VaultTestContextRunner contextRunner = VaultTestContextRunner.of(CERT) + .testClass(getClass()) + .bootstrap(true) + .reactive(false) + .configurations(TestConfig.class); + + private static VaultOperations vaultOperations; + + private static final String certificate = Files.contentOf(new File(findWorkDir(), "ca/certs/client.cert.pem"), + StandardCharsets.US_ASCII); @BeforeAll public static void beforeClass() { @@ -69,7 +76,7 @@ public static void beforeClass() { vaultRule.prepare().mountAuth(vaultProperties.getSsl().getCertAuthPath()); } - VaultOperations vaultOperations = vaultRule.prepare().getVaultOperations(); + vaultOperations = vaultRule.prepare().getVaultOperations(); String rules = "{ \"name\": \"testpolicy\",\n" // + " \"path\": {\n" // @@ -82,10 +89,6 @@ public static void beforeClass() { vaultOperations.write("secret/" + VaultConfigTlsCertAuthenticationTests.class.getSimpleName(), Collections.singletonMap("vault.value", "foo")); - File workDir = findWorkDir(); - - String certificate = Files.contentOf(new File(workDir, "ca/certs/client.cert.pem"), StandardCharsets.US_ASCII); - Map role = new HashMap<>(); role.put("certificate", certificate); role.put("policies", "testpolicy"); @@ -93,17 +96,42 @@ public static void beforeClass() { vaultOperations.write("auth/cert/certs/my-role", role); } + @AfterEach + void cleanup() { + vaultOperations.delete("auth/cert/certs/another-role"); + } + @Test - public void contextLoads() { - assertThat(this.configValue).isEqualTo("foo"); + void authenticateUsingTlsCertificate() { + this.contextRunner + .run(context -> assertThat(context.getEnvironment().getProperty("vault.value")).isEqualTo("foo")); } - @SpringBootApplication - public static class TestApplication { + @Test + void authenticateUsingNamedCertificateRole() { + Map anotherRole = new HashMap<>(); + anotherRole.put("certificate", certificate); + vaultOperations.write("auth/cert/certs/another-role", anotherRole); - public static void main(String[] args) { - SpringApplication.run(TestApplication.class, args); - } + this.contextRunner.property("spring.cloud.vault.ssl.role", "my-role") + .run(context -> assertThat(context.getEnvironment().getProperty("vault.value")).isEqualTo("foo")); + } + + @Test + void authenticationFailsWhenMultipleCertsAndNoNamedRole(CapturedOutput capturedOutput) { + Map anotherRole = new HashMap<>(); + anotherRole.put("certificate", certificate); + vaultOperations.write("auth/cert/certs/another-role", anotherRole); + + this.contextRunner.run(context -> { + assertThat(capturedOutput.getOut()) + .contains("org.springframework.vault.VaultException: Status 403 Forbidden"); + assertNull(context.getEnvironment().getProperty("vault.value")); + }); + } + + @TestConfiguration + public static class TestConfig { } diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/util/VaultTestContextRunner.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/util/VaultTestContextRunner.java new file mode 100644 index 00000000..d05c5514 --- /dev/null +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/util/VaultTestContextRunner.java @@ -0,0 +1,117 @@ +/* + * Copyright 2025-present 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.util; + +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.vault.config.VaultProperties.AuthenticationMethod; +import org.springframework.context.ConfigurableApplicationContext; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * A utility class built on top of {@link SpringApplicationBuilder} that follows Spring + * Boot’s testing patterns but designed for Spring Cloud Vault integration tests. + * + *

+ * Its goal is to eliminate repetitive boilerplate particularly when repeatedly + * configuring: + *

+ * + *
    + *
  • Bootstrap mode
  • + *
  • Reactive vs. non-reactive modes
  • + *
  • Environment and test classes
  • + *
  • Vault-specific properties
  • + *
+ * + * @author Issam EL-ATIF + * @since 5.0.1 + * @see SpringApplicationBuilder + * @see ApplicationContextRunner + */ +public class VaultTestContextRunner { + + private final List> configurationClasses = new ArrayList<>(); + + private final Map properties = new LinkedHashMap<>(); + + private VaultTestContextRunner(Map properties) { + this.properties.putAll(properties); + } + + public static VaultTestContextRunner of(AuthenticationMethod authenticationMethod) { + Map authProperties = new LinkedHashMap<>(); + switch (authenticationMethod) { + case CERT -> { + authProperties.put("spring.cloud.vault.authentication", "cert"); + authProperties.put("spring.cloud.vault.ssl.key-store", "file:../work/client-cert.jks"); + authProperties.put("spring.cloud.vault.ssl.key-store-password", "changeit"); + } + case KUBERNETES -> { + authProperties.put("spring.cloud.vault.authentication", "kubernetes"); + authProperties.put("spring.cloud.vault.kubernetes.service-account-token-file", + "../work/minikube/hello-minikube-token"); + } + } + + return new VaultTestContextRunner(authProperties); + } + + public VaultTestContextRunner configurations(Class... configClasses) { + this.configurationClasses.addAll(Arrays.asList(configClasses)); + return this; + } + + public VaultTestContextRunner property(String key, Object value) { + this.properties.put(key, value); + return this; + } + + public VaultTestContextRunner testClass(Class testClass) { + this.properties.put("spring.cloud.vault.application-name", testClass.getSimpleName()); + return this; + } + + public VaultTestContextRunner bootstrap(boolean bootstrap) { + this.properties.put("spring.cloud.bootstrap.enabled", bootstrap); + return this; + } + + public VaultTestContextRunner reactive(boolean reactive) { + this.properties.put("spring.cloud.vault.reactive.enabled", reactive); + return this; + } + + public void run(Consumer consumer) { + SpringApplicationBuilder builder = new SpringApplicationBuilder(); + builder.sources(configurationClasses.toArray(new Class[0])) + .web(WebApplicationType.NONE) + .properties(this.properties); + + try (ConfigurableApplicationContext context = builder.run()) { + consumer.accept(context); + } + } + +}