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:
Andy Lintner
2023-04-12 12:21:57 -04:00
committed by Mark Paluch
parent c31b5abd0a
commit 1e8753eaca
6 changed files with 177 additions and 42 deletions

View File

@@ -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");

View File

@@ -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);
}
}

View File

@@ -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>>() {
});
});
}

View File

@@ -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

View File

@@ -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

View File

@@ -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);