Role name can now be used with cert auth
When using TLS authentication, the Vault API allows the caller to specify a role to use for authorization. Previously, we did not allow this to be specified, which caused Vault to select one of the roles associated with the certificate. See gh-780
This commit is contained in:
committed by
Mark Paluch
parent
c31b5abd0a
commit
1e8753eaca
@@ -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<String, String> 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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<Object>) restOperations -> {
|
||||
vaultOperations.opsForSys().createOrUpdatePolicy("cert-auth1", DEFAULT_POLICY);
|
||||
vaultOperations.opsForSys().createOrUpdatePolicy("cert-auth2", ALTERNATE_POLICY);
|
||||
|
||||
vaultOperations.doWithSession((RestOperationsCallback<Object>) restOperations -> {
|
||||
File workDir = findWorkDir();
|
||||
|
||||
String certificate = Files.contentOf(new File(workDir, "ca/certs/client.cert.pem"),
|
||||
StandardCharsets.US_ASCII);
|
||||
|
||||
Map<String, Object> 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<String> 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<Map<String, Object>> 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<Map<String, Object>>() {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user