diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/ClientCertificateAuthentication.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/ClientCertificateAuthentication.java index 3d0ccb07..18742152 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/ClientCertificateAuthentication.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/ClientCertificateAuthentication.java @@ -15,19 +15,19 @@ */ package org.springframework.vault.authentication; +import static org.springframework.vault.authentication.AuthenticationSteps.HttpRequestBuilder.post; + import java.util.Collections; +import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.springframework.util.Assert; import org.springframework.vault.support.VaultResponse; import org.springframework.vault.support.VaultToken; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestOperations; -import static org.springframework.vault.authentication.AuthenticationSteps.HttpRequestBuilder.post; - /** * TLS Client Certificate {@link ClientAuthentication}. * @@ -81,11 +81,13 @@ public class ClientCertificateAuthentication implements ClientAuthentication, Au * @since 2.3 */ public static AuthenticationSteps createAuthenticationSteps(ClientCertificateAuthenticationOptions options) { - Assert.notNull(options, "ClientCertificateAuthenticationOptions must not be null"); - return AuthenticationSteps - .just(post(AuthenticationUtil.getLoginPath(options.getPath())).as(VaultResponse.class)); + String name = options.getName(); + Map body = name != null ? Collections.singletonMap("name", name) : Collections.emptyMap(); + + return AuthenticationSteps.fromSupplier(() -> body) + .login(post(AuthenticationUtil.getLoginPath(options.getPath())).as(VaultResponse.class)); } @Override @@ -101,8 +103,11 @@ public class ClientCertificateAuthentication implements ClientAuthentication, Au private VaultToken createTokenUsingTlsCertAuthentication() { try { + String name = this.options.getName(); + VaultResponse response = this.restOperations.postForObject( - AuthenticationUtil.getLoginPath(this.options.getPath()), Collections.emptyMap(), + AuthenticationUtil.getLoginPath(this.options.getPath()), + name != null ? Collections.singletonMap("name", name) : Collections.emptyMap(), VaultResponse.class); Assert.state(response.getAuth() != null, "Auth field must not be null"); diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/ClientCertificateAuthenticationOptions.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/ClientCertificateAuthenticationOptions.java index 27a48b14..630f3b9f 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/ClientCertificateAuthenticationOptions.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/ClientCertificateAuthenticationOptions.java @@ -15,6 +15,7 @@ */ package org.springframework.vault.authentication; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -38,8 +39,15 @@ public class ClientCertificateAuthenticationOptions { */ private final String path; - private ClientCertificateAuthenticationOptions(String path) { + /** + * Optional named certificate role to authenticate against. + */ + @Nullable + private final String name; + + private ClientCertificateAuthenticationOptions(String path, String name) { this.path = path; + this.name = name; } /** @@ -56,6 +64,14 @@ public class ClientCertificateAuthenticationOptions { return this.path; } + /** + * @return the optional named certificate role to authenticate against. + */ + @Nullable + public String getName() { + return this.name; + } + /** * Builder for {@link ClientCertificateAuthenticationOptions}. */ @@ -63,6 +79,9 @@ public class ClientCertificateAuthenticationOptions { private String path = DEFAULT_CERT_PATH; + @Nullable + private String name; + ClientCertificateAuthenticationOptionsBuilder() { } @@ -79,12 +98,25 @@ public class ClientCertificateAuthenticationOptions { return this; } + /** + * Configure the named certificate role to authenticate against. + * @param name must not be empty or {@literal null}. + * @return {@code this} {@link ClientCertificateAuthenticationOptionsBuilder}. + */ + public ClientCertificateAuthenticationOptionsBuilder name(String name) { + + Assert.hasText(name, "Name must not be empty"); + + this.name = name; + return this; + } + /** * Build a new {@link ClientCertificateAuthenticationOptions} instance. * @return a new {@link ClientCertificateAuthenticationOptions}. */ public ClientCertificateAuthenticationOptions build() { - return new ClientCertificateAuthenticationOptions(this.path); + return new ClientCertificateAuthenticationOptions(this.path, this.name); } } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTestBase.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTestBase.java index 19659c94..24b4a145 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTestBase.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTestBase.java @@ -15,24 +15,30 @@ */ package org.springframework.vault.authentication; +import static org.assertj.core.api.Assertions.as; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.vault.util.Settings.createSslConfiguration; +import static org.springframework.vault.util.Settings.findWorkDir; + import java.io.File; import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; import java.util.Map; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.assertj.core.api.ListAssert; import org.assertj.core.util.Files; import org.junit.jupiter.api.BeforeEach; - +import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.io.FileSystemResource; +import org.springframework.http.*; +import org.springframework.vault.client.VaultHttpHeaders; import org.springframework.vault.core.RestOperationsCallback; -import org.springframework.vault.support.Policy; -import org.springframework.vault.support.SslConfiguration; +import org.springframework.vault.core.VaultOperations; +import org.springframework.vault.support.*; import org.springframework.vault.support.SslConfiguration.KeyStoreConfiguration; import org.springframework.vault.util.IntegrationTestSupport; -import static org.springframework.vault.util.Settings.createSslConfiguration; -import static org.springframework.vault.util.Settings.findWorkDir; - /** * Integration test base class for {@link ClientCertificateAuthentication} tests. * @@ -40,12 +46,18 @@ import static org.springframework.vault.util.Settings.findWorkDir; */ public abstract class ClientCertificateAuthenticationIntegrationTestBase extends IntegrationTestSupport { - static final Policy POLICY = Policy.of(Policy.Rule.builder() - .path("/*") + static final Policy DEFAULT_POLICY = Policy.of(Policy.Rule.builder() + .path("/default/*") .capabilities(Policy.BuiltinCapabilities.READ, Policy.BuiltinCapabilities.CREATE, Policy.BuiltinCapabilities.UPDATE) .build()); + static final Policy ALTERNATE_POLICY = Policy + .of(Policy.Rule.builder().path("/alternate/*").capabilities(Policy.BuiltinCapabilities.READ, + Policy.BuiltinCapabilities.CREATE, Policy.BuiltinCapabilities.UPDATE).build()); + + VaultOperations vaultOperations; + @BeforeEach public void before() { @@ -53,19 +65,44 @@ public abstract class ClientCertificateAuthenticationIntegrationTestBase extends prepare().mountAuth("cert"); } - prepare().getVaultOperations().opsForSys().createOrUpdatePolicy("cert-auth", POLICY); + vaultOperations = prepare().getVaultOperations(); - prepare().getVaultOperations().doWithSession((RestOperationsCallback) restOperations -> { + vaultOperations.opsForSys().createOrUpdatePolicy("cert-auth1", DEFAULT_POLICY); + vaultOperations.opsForSys().createOrUpdatePolicy("cert-auth2", ALTERNATE_POLICY); + + vaultOperations.doWithSession((RestOperationsCallback) restOperations -> { File workDir = findWorkDir(); String certificate = Files.contentOf(new File(workDir, "ca/certs/client.cert.pem"), StandardCharsets.US_ASCII); Map role = new LinkedHashMap<>(); - role.put("token_policies", "cert-auth"); + role.put("token_policies", "cert-auth1"); role.put("certificate", certificate); - return restOperations.postForEntity("auth/cert/certs/my-role", role, Map.class); + restOperations.postForEntity("auth/cert/certs/my-default-role", role, Map.class); + + role.put("token_policies", "cert-auth2"); + restOperations.postForEntity("auth/cert/certs/my-alternate-role", role, Map.class); + return true; + }); + } + + ListAssert assertThatPolicies(final VaultToken token) { + return assertThat(lookupSelf(token).getBody()).isNotNull() + .extracting("data", as(InstanceOfAssertFactories.map(String.class, Object.class))).isNotNull() + .extracting("policies", as(InstanceOfAssertFactories.list(String.class))).isNotNull(); + } + + ResponseEntity> lookupSelf(final VaultToken token) { + + return vaultOperations.doWithVault(restOperations -> { + HttpHeaders headers = new HttpHeaders(); + headers.add(VaultHttpHeaders.VAULT_TOKEN, token.getToken()); + + return restOperations.exchange("auth/token/lookup-self", HttpMethod.GET, new HttpEntity<>(headers), + new ParameterizedTypeReference>() { + }); }); } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTests.java index 62a22e28..37aca5e5 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTests.java @@ -15,23 +15,18 @@ */ package org.springframework.vault.authentication; -import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.Test; import org.springframework.core.NestedRuntimeException; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.vault.client.ClientHttpRequestFactoryFactory; import org.springframework.vault.client.VaultClients; -import org.springframework.vault.support.ClientOptions; -import org.springframework.vault.support.SslConfiguration; -import org.springframework.vault.support.VaultToken; +import org.springframework.vault.support.*; import org.springframework.vault.util.Settings; import org.springframework.vault.util.TestRestTemplateFactory; import org.springframework.web.client.RestTemplate; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; - /** * Integration tests for {@link ClientCertificateAuthentication}. * @@ -87,6 +82,38 @@ class ClientCertificateAuthenticationIntegrationTests extends ClientCertificateA prepareCertAuthenticationMethod(SslConfiguration.KeyConfiguration.of("wrong".toCharArray(), "1")))); } + @Test + void shouldSelectRoleOne() { + ClientHttpRequestFactory clientHttpRequestFactory = ClientHttpRequestFactoryFactory.create(new ClientOptions(), + prepareCertAuthenticationMethod()); + + RestTemplate restTemplate = VaultClients.createRestTemplate(TestRestTemplateFactory.TEST_VAULT_ENDPOINT, + clientHttpRequestFactory); + ClientCertificateAuthentication authentication = new ClientCertificateAuthentication( + ClientCertificateAuthenticationOptions.builder().name("my-default-role").build(), restTemplate); + VaultToken login = authentication.login(); + + assertThat(login.getToken()).isNotEmpty(); + assertThatPolicies(login).contains("cert-auth1") // + .doesNotContain("cert-auth2"); + } + + @Test + void shouldSelectRoleTwo() { + ClientHttpRequestFactory clientHttpRequestFactory = ClientHttpRequestFactoryFactory.create(new ClientOptions(), + prepareCertAuthenticationMethod()); + + RestTemplate restTemplate = VaultClients.createRestTemplate(TestRestTemplateFactory.TEST_VAULT_ENDPOINT, + clientHttpRequestFactory); + ClientCertificateAuthentication authentication = new ClientCertificateAuthentication( + ClientCertificateAuthenticationOptions.builder().name("my-alternate-role").build(), restTemplate); + VaultToken login = authentication.login(); + + assertThat(login.getToken()).isNotEmpty(); + assertThatPolicies(login).contains("cert-auth2") // + .doesNotContain("cert-auth1"); + } + // Compatibility for Vault 0.6.0 and below. Vault 0.6.1 fixed that issue and we // receive a VaultException here. @Test diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationOperatorIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationOperatorIntegrationTests.java index acbb2b07..e02e7550 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationOperatorIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationOperatorIntegrationTests.java @@ -15,14 +15,14 @@ */ package org.springframework.vault.authentication; -import org.junit.jupiter.api.Test; -import reactor.test.StepVerifier; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import org.junit.jupiter.api.Test; import org.springframework.vault.support.SslConfiguration; import org.springframework.vault.util.TestWebClientFactory; import org.springframework.web.reactive.function.client.WebClient; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import reactor.test.StepVerifier; /** * Integration tests for {@link ClientCertificateAuthentication} using @@ -76,6 +76,40 @@ class ClientCertificateAuthenticationOperatorIntegrationTests .verifyError(VaultLoginException.class); } + @Test + void shouldSelectRoleOne() { + + WebClient webClient = TestWebClientFactory.create(prepareCertAuthenticationMethod()); + + AuthenticationStepsOperator operator = new AuthenticationStepsOperator( + ClientCertificateAuthentication.createAuthenticationSteps( + ClientCertificateAuthenticationOptions.builder().name("my-default-role").build()), + webClient); + + operator.getVaultToken() // + .as(StepVerifier::create) // + .assertNext(token -> assertThatPolicies(token).contains("cert-auth1") // + .doesNotContain("cert-auth2")) // + .verifyComplete(); + } + + @Test + void shouldSelectRoleTwo() { + + WebClient webClient = TestWebClientFactory.create(prepareCertAuthenticationMethod()); + + AuthenticationStepsOperator operator = new AuthenticationStepsOperator( + ClientCertificateAuthentication.createAuthenticationSteps( + ClientCertificateAuthenticationOptions.builder().name("my-alternate-role").build()), + webClient); + + operator.getVaultToken() // + .as(StepVerifier::create) // + .assertNext(token -> assertThatPolicies(token).contains("cert-auth2") // + .doesNotContain("cert-auth1")) // + .verifyComplete(); + } + @Test void shouldProvideInvalidKeyPassword() { assertThatIllegalStateException().isThrownBy(() -> TestWebClientFactory diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationUnitTests.java index e8b64fc2..2f48d34b 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationUnitTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationUnitTests.java @@ -15,11 +15,16 @@ */ package org.springframework.vault.authentication; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.*; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withServerError; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + import java.time.Duration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.test.web.client.MockRestServiceServer; @@ -28,13 +33,6 @@ import org.springframework.vault.client.VaultClients; import org.springframework.vault.support.VaultToken; import org.springframework.web.client.RestTemplate; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; -import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; -import static org.springframework.test.web.client.response.MockRestResponseCreators.withServerError; -import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; - /** * Unit tests for {@link ClientCertificateAuthentication}. * @@ -61,12 +59,14 @@ class ClientCertificateAuthenticationUnitTests { this.mockRest.expect(requestTo("/auth/my/path/login")) .andExpect(method(HttpMethod.POST)) - .andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON) + .andExpect(content().json("{\"name\": \"my-default-role\"}")) + .andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON) .body("{" + "\"auth\":{\"client_token\":\"my-token\", \"renewable\": true, \"lease_duration\": 10}" + "}")); ClientCertificateAuthenticationOptions options = ClientCertificateAuthenticationOptions.builder() - .path("my/path") + .name("my-default-role") // + .path("my/path") .build(); ClientCertificateAuthentication sut = new ClientCertificateAuthentication(options, this.restTemplate);