Add support for IMDSv2 on EC2 instances.
Closes gh-865
This commit is contained in:
@@ -342,6 +342,27 @@ public class AuthenticationSteps {
|
||||
return new HttpRequestBuilder(HttpMethod.POST, uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder entry point to {@code PUT} to {@code uriTemplate}.
|
||||
* @param uriTemplate must not be {@literal null} or empty.
|
||||
* @param uriVariables the variables to expand the template.
|
||||
* @return a new {@link HttpRequestBuilder}.
|
||||
* @since 3.2
|
||||
*/
|
||||
public static HttpRequestBuilder put(String uriTemplate, String... uriVariables) {
|
||||
return new HttpRequestBuilder(HttpMethod.PUT, uriTemplate, uriVariables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder entry point to {@code PUT} to {@code uri}.
|
||||
* @param uri must not be {@literal null}.
|
||||
* @return a new {@link HttpRequestBuilder}.
|
||||
* @since 3.2
|
||||
*/
|
||||
public static HttpRequestBuilder put(URI uri) {
|
||||
return new HttpRequestBuilder(HttpMethod.PUT, uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder entry point to use {@link HttpMethod} for {@code uriTemplate}.
|
||||
* @param uriTemplate must not be {@literal null} or empty.
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -48,7 +49,7 @@ import org.springframework.web.client.RestOperations;
|
||||
*/
|
||||
public class AuthenticationStepsExecutor implements ClientAuthentication {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(AppIdAuthentication.class);
|
||||
private static final Log logger = LogFactory.getLog(AuthenticationStepsExecutor.class);
|
||||
|
||||
private final AuthenticationSteps chain;
|
||||
|
||||
@@ -166,9 +167,14 @@ public class AuthenticationStepsExecutor implements ClientAuthentication {
|
||||
|
||||
}
|
||||
|
||||
private static HttpEntity<?> getEntity(@Nullable HttpEntity<?> entity, @Nullable Object state) {
|
||||
static HttpEntity<?> getEntity(@Nullable HttpEntity<?> entity, @Nullable Object state) {
|
||||
|
||||
if (entity == null) {
|
||||
|
||||
if (state instanceof HttpHeaders headers) {
|
||||
return new HttpEntity<>(headers);
|
||||
}
|
||||
|
||||
return state == null ? HttpEntity.EMPTY : new HttpEntity<>(state);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.vault.VaultException;
|
||||
@@ -161,7 +162,7 @@ public class AuthenticationStepsOperator implements VaultTokenSupplier {
|
||||
private Mono<Object> doHttpRequest(HttpRequestNode<Object> step, Object state) {
|
||||
|
||||
HttpRequest<Object> definition = step.getDefinition();
|
||||
HttpEntity<?> entity = getEntity(definition.getEntity(), state);
|
||||
HttpEntity<?> entity = AuthenticationStepsExecutor.getEntity(definition.getEntity(), state);
|
||||
|
||||
RequestBodySpec spec;
|
||||
if (definition.getUri() == null) {
|
||||
@@ -184,19 +185,6 @@ public class AuthenticationStepsOperator implements VaultTokenSupplier {
|
||||
return spec.retrieve().bodyToMono(definition.getResponseType());
|
||||
}
|
||||
|
||||
private static HttpEntity<?> getEntity(@Nullable HttpEntity<?> entity, @Nullable Object state) {
|
||||
|
||||
if (entity == null) {
|
||||
return state == null ? HttpEntity.EMPTY : new HttpEntity<>(state);
|
||||
}
|
||||
|
||||
if (entity.getBody() == null && state != null) {
|
||||
return new HttpEntity<>(state, entity.getHeaders());
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static Object doMapStep(MapStep<Object, Object> o, Object state) {
|
||||
return o.apply(state);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.springframework.vault.authentication;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@@ -24,12 +25,17 @@ import java.util.function.Supplier;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.vault.VaultException;
|
||||
import org.springframework.vault.authentication.AuthenticationSteps.HttpRequestBuilder;
|
||||
import org.springframework.vault.support.VaultResponse;
|
||||
import org.springframework.vault.support.VaultToken;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestOperations;
|
||||
|
||||
@@ -52,6 +58,10 @@ public class AwsEc2Authentication implements ClientAuthentication, Authenticatio
|
||||
|
||||
private static final char[] EMPTY = new char[0];
|
||||
|
||||
private static final String METADATA_TOKEN_TTL_HEADER = "X-aws-ec2-metadata-token-ttl-seconds";
|
||||
|
||||
private static final String METADATA_TOKEN_HEADER = "X-aws-ec2-metadata-token";
|
||||
|
||||
private final AwsEc2AuthenticationOptions options;
|
||||
|
||||
private final RestOperations vaultRestOperations;
|
||||
@@ -107,13 +117,34 @@ public class AwsEc2Authentication implements ClientAuthentication, Authenticatio
|
||||
protected static AuthenticationSteps createAuthenticationSteps(AwsEc2AuthenticationOptions options,
|
||||
AtomicReference<char[]> nonce, Supplier<char[]> nonceSupplier) {
|
||||
|
||||
return AuthenticationSteps
|
||||
.fromHttpRequest(HttpRequestBuilder.get(options.getIdentityDocumentUri().toString()).as(String.class)) //
|
||||
AuthenticationSteps.HttpRequest<String> identityRequest = HttpRequestBuilder
|
||||
.get(options.getIdentityDocumentUri().toString())
|
||||
.as(String.class);
|
||||
AuthenticationSteps.Node<String> identity;
|
||||
|
||||
if (options.getVersion() == AwsEc2AuthenticationOptions.InstanceMetadataServiceVersion.V2) {
|
||||
|
||||
identity = AuthenticationSteps
|
||||
.fromHttpRequest(HttpRequestBuilder.put(options.getMetadataTokenRequestUri())
|
||||
.with(createTokenRequestHeaders(options))
|
||||
.as(String.class))
|
||||
.map(it -> {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add(METADATA_TOKEN_HEADER, it);
|
||||
return headers;
|
||||
})
|
||||
.request(identityRequest);
|
||||
}
|
||||
else {
|
||||
identity = AuthenticationSteps.fromHttpRequest(identityRequest);
|
||||
}
|
||||
|
||||
return identity //
|
||||
.map(pkcs7 -> pkcs7.replaceAll("\\r", "")) //
|
||||
.map(pkcs7 -> pkcs7.replaceAll("\\n", "")) //
|
||||
.map(pkcs7 -> {
|
||||
|
||||
Map<String, String> login = new HashMap<>();
|
||||
Map<String, String> login = new LinkedHashMap<>();
|
||||
|
||||
if (StringUtils.hasText(options.getRole())) {
|
||||
login.put("role", options.getRole());
|
||||
@@ -175,6 +206,11 @@ public class AwsEc2Authentication implements ClientAuthentication, Authenticatio
|
||||
protected Map<String, String> getEc2Login() {
|
||||
|
||||
Map<String, String> login = new HashMap<>();
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
|
||||
if (options.getVersion() == AwsEc2AuthenticationOptions.InstanceMetadataServiceVersion.V2) {
|
||||
headers.add(METADATA_TOKEN_HEADER, createIMDSv2Token());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(this.options.getRole())) {
|
||||
login.put("role", this.options.getRole());
|
||||
@@ -187,8 +223,15 @@ public class AwsEc2Authentication implements ClientAuthentication, Authenticatio
|
||||
login.put("nonce", new String(this.nonce.get()));
|
||||
|
||||
try {
|
||||
String pkcs7 = this.awsMetadataRestOperations.getForObject(this.options.getIdentityDocumentUri(),
|
||||
String.class);
|
||||
|
||||
HttpEntity<Object> entity = new HttpEntity<>(headers);
|
||||
ResponseEntity<String> exchange = this.awsMetadataRestOperations
|
||||
.exchange(this.options.getIdentityDocumentUri(), HttpMethod.GET, entity, String.class);
|
||||
if (!exchange.getStatusCode().is2xxSuccessful()) {
|
||||
throw new HttpClientErrorException(exchange.getStatusCode());
|
||||
}
|
||||
|
||||
String pkcs7 = exchange.getBody();
|
||||
if (StringUtils.hasText(pkcs7)) {
|
||||
login.put("pkcs7", pkcs7.replaceAll("\\r", "").replaceAll("\\n", ""));
|
||||
}
|
||||
@@ -201,6 +244,26 @@ public class AwsEc2Authentication implements ClientAuthentication, Authenticatio
|
||||
}
|
||||
}
|
||||
|
||||
private String createIMDSv2Token() {
|
||||
|
||||
try {
|
||||
|
||||
HttpEntity<Object> entity = new HttpEntity<>(createTokenRequestHeaders(this.options));
|
||||
|
||||
ResponseEntity<String> exchange = this.awsMetadataRestOperations
|
||||
.exchange(this.options.getMetadataTokenRequestUri(), HttpMethod.PUT, entity, String.class);
|
||||
if (!exchange.getStatusCode().is2xxSuccessful()) {
|
||||
throw new HttpClientErrorException(exchange.getStatusCode());
|
||||
}
|
||||
|
||||
return exchange.getBody();
|
||||
}
|
||||
catch (RestClientException e) {
|
||||
throw new VaultLoginException(
|
||||
String.format("Cannot obtain IMDSv2 Token from %s", this.options.getMetadataTokenRequestUri()), e);
|
||||
}
|
||||
}
|
||||
|
||||
protected char[] createNonce() {
|
||||
return doCreateNonce(this.options);
|
||||
}
|
||||
@@ -209,4 +272,10 @@ public class AwsEc2Authentication implements ClientAuthentication, Authenticatio
|
||||
return options.getNonce().getValue();
|
||||
}
|
||||
|
||||
private static HttpHeaders createTokenRequestHeaders(AwsEc2AuthenticationOptions options) {
|
||||
HttpHeaders tokenRequestHeaders = new HttpHeaders();
|
||||
tokenRequestHeaders.add(METADATA_TOKEN_TTL_HEADER, String.valueOf(options.getMetadataTokenTtl().toSeconds()));
|
||||
return tokenRequestHeaders;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.springframework.vault.authentication;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -38,6 +39,11 @@ public class AwsEc2AuthenticationOptions {
|
||||
public static final URI DEFAULT_PKCS7_IDENTITY_DOCUMENT_URI = URI
|
||||
.create("http://169.254.169.254/latest/dynamic/instance-identity/pkcs7");
|
||||
|
||||
/**
|
||||
* @since 3.2
|
||||
*/
|
||||
public static final URI DEFAULT_IMDSV2_TOKEN_URI = URI.create("http://169.254.169.254/latest/api/token");
|
||||
|
||||
public static final String DEFAULT_AWS_AUTHENTICATION_PATH = "aws-ec2";
|
||||
|
||||
/**
|
||||
@@ -68,16 +74,39 @@ public class AwsEc2AuthenticationOptions {
|
||||
*/
|
||||
private final Nonce nonce;
|
||||
|
||||
/**
|
||||
* IMDSv2 token TTL.
|
||||
* @since 3.2
|
||||
*/
|
||||
private final Duration metadataTokenTtl;
|
||||
|
||||
/**
|
||||
* {@link URI} to request a token for the AWS EC2 Instance Metadata service v2.
|
||||
* @since 3.2
|
||||
*/
|
||||
private final URI metadataTokenRequestUri;
|
||||
|
||||
/**
|
||||
* Metadata service version.
|
||||
* @since 3.2
|
||||
*/
|
||||
private final InstanceMetadataServiceVersion version;
|
||||
|
||||
private AwsEc2AuthenticationOptions() {
|
||||
this(DEFAULT_AWS_AUTHENTICATION_PATH, DEFAULT_PKCS7_IDENTITY_DOCUMENT_URI, "", Nonce.generated());
|
||||
this(DEFAULT_AWS_AUTHENTICATION_PATH, DEFAULT_PKCS7_IDENTITY_DOCUMENT_URI, "", Nonce.generated(),
|
||||
Duration.ofMinutes(1), DEFAULT_IMDSV2_TOKEN_URI, InstanceMetadataServiceVersion.V1);
|
||||
}
|
||||
|
||||
private AwsEc2AuthenticationOptions(String path, URI identityDocumentUri, @Nullable String role, Nonce nonce) {
|
||||
private AwsEc2AuthenticationOptions(String path, URI identityDocumentUri, @Nullable String role, Nonce nonce,
|
||||
Duration metadataTokenTtl, URI metadataTokenRequestUri, InstanceMetadataServiceVersion version) {
|
||||
|
||||
this.path = path;
|
||||
this.identityDocumentUri = identityDocumentUri;
|
||||
this.role = role;
|
||||
this.nonce = nonce;
|
||||
this.metadataTokenTtl = metadataTokenTtl;
|
||||
this.metadataTokenRequestUri = metadataTokenRequestUri;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,6 +145,30 @@ public class AwsEc2AuthenticationOptions {
|
||||
return this.nonce;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the configured {@link InstanceMetadataServiceVersion}.
|
||||
* @since 3.2
|
||||
*/
|
||||
public InstanceMetadataServiceVersion getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the configured IMDSv2 token TTL.
|
||||
* @since 3.2
|
||||
*/
|
||||
public Duration getMetadataTokenTtl() {
|
||||
return metadataTokenTtl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the {@link URI} to the AWS EC2 Metadata Service to obtain IMDSv2 tokens.
|
||||
* @since 3.2
|
||||
*/
|
||||
public URI getMetadataTokenRequestUri() {
|
||||
return this.metadataTokenRequestUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for {@link AwsEc2AuthenticationOptionsBuilder}.
|
||||
*/
|
||||
@@ -130,6 +183,12 @@ public class AwsEc2AuthenticationOptions {
|
||||
|
||||
private Nonce nonce = Nonce.generated();
|
||||
|
||||
private Duration metadataTokenTtl = Duration.ofMinutes(1);
|
||||
|
||||
private URI metadataTokenRequestUri = DEFAULT_IMDSV2_TOKEN_URI;
|
||||
|
||||
private InstanceMetadataServiceVersion version = InstanceMetadataServiceVersion.V1;
|
||||
|
||||
AwsEc2AuthenticationOptionsBuilder() {
|
||||
}
|
||||
|
||||
@@ -188,6 +247,51 @@ public class AwsEc2AuthenticationOptions {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the Instance Service Metadata v2 Token TTL. Defaults to 1 minute.
|
||||
* @param ttl must not be {@literal null}.
|
||||
* @return {@code this} {@link AwsEc2AuthenticationOptionsBuilder}.
|
||||
* @since 3.2
|
||||
*/
|
||||
public AwsEc2AuthenticationOptionsBuilder metadataTokenTtl(Duration ttl) {
|
||||
|
||||
Assert.notNull(ttl, "Duration must not be null");
|
||||
Assert.isTrue(!ttl.isNegative() && !ttl.isZero(), "Duration must not be zero or negative");
|
||||
|
||||
this.metadataTokenTtl = ttl;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the Identity Metadata token request {@link URI}.
|
||||
* @param metadataTokenRequestUri must not be {@literal null}.
|
||||
* @return {@code this} {@link AwsEc2AuthenticationOptionsBuilder}.
|
||||
* @since 3.2
|
||||
* @see #DEFAULT_IMDSV2_TOKEN_URI
|
||||
*/
|
||||
public AwsEc2AuthenticationOptionsBuilder metadataTokenRequestUri(URI metadataTokenRequestUri) {
|
||||
|
||||
Assert.notNull(metadataTokenRequestUri, "Metadata token request URI must not be null");
|
||||
|
||||
this.metadataTokenRequestUri = metadataTokenRequestUri;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the Instance Service Metadata {@link InstanceMetadataServiceVersion
|
||||
* version}. Defaults to {@link InstanceMetadataServiceVersion#V1}.
|
||||
* @param version must not be {@literal null}.
|
||||
* @return {@code this} {@link AwsEc2AuthenticationOptionsBuilder}.
|
||||
* @since 3.2
|
||||
*/
|
||||
public AwsEc2AuthenticationOptionsBuilder version(InstanceMetadataServiceVersion version) {
|
||||
|
||||
Assert.notNull(version, "Version must not be null");
|
||||
|
||||
this.version = version;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a new {@link AwsEc2AuthenticationOptions} instance.
|
||||
* @return a new {@link AwsEc2AuthenticationOptions}.
|
||||
@@ -196,7 +300,8 @@ public class AwsEc2AuthenticationOptions {
|
||||
|
||||
Assert.notNull(this.identityDocumentUri, "IdentityDocumentUri must not be null");
|
||||
|
||||
return new AwsEc2AuthenticationOptions(this.path, this.identityDocumentUri, this.role, this.nonce);
|
||||
return new AwsEc2AuthenticationOptions(this.path, this.identityDocumentUri, this.role, this.nonce,
|
||||
this.metadataTokenTtl, this.metadataTokenRequestUri, this.version);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -258,4 +363,24 @@ public class AwsEc2AuthenticationOptions {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumeration for the Instance metadata service version.
|
||||
*
|
||||
* @since 3.2
|
||||
*/
|
||||
public enum InstanceMetadataServiceVersion {
|
||||
|
||||
/**
|
||||
* Request/Response (default) oriented version 1.
|
||||
*/
|
||||
V1,
|
||||
|
||||
/**
|
||||
* Session-oriented version 2.
|
||||
*/
|
||||
|
||||
V2;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
*/
|
||||
package org.springframework.vault.authentication;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.springframework.vault.authentication.AuthenticationSteps.HttpRequestBuilder.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
@@ -24,6 +27,7 @@ import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -37,9 +41,6 @@ import org.springframework.vault.support.VaultResponse;
|
||||
import org.springframework.vault.support.VaultToken;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.vault.authentication.AuthenticationSteps.HttpRequestBuilder.post;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link AuthenticationStepsOperator}.
|
||||
*
|
||||
@@ -123,6 +124,34 @@ class AuthenticationStepsOperatorUnitTests {
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void headersShouldBeConsideredForHttpRequest() {
|
||||
|
||||
ClientHttpRequest request = new MockClientHttpRequest(HttpMethod.POST, "/auth/cert/login");
|
||||
MockClientHttpResponse response = new MockClientHttpResponse(HttpStatus.OK);
|
||||
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
|
||||
response.setBody(
|
||||
"{" + "\"auth\":{\"client_token\":\"my-token\", \"renewable\": true, \"lease_duration\": 10}" + "}");
|
||||
ClientHttpConnector connector = (method, uri, fn) -> {
|
||||
|
||||
return fn.apply(request).doOnSuccess(unused -> {
|
||||
assertThat(request.getHeaders().get("a")).contains("b");
|
||||
}).then(Mono.just(response));
|
||||
};
|
||||
|
||||
WebClient webClient = WebClient.builder().clientConnector(connector).build();
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add("a", "b");
|
||||
|
||||
AuthenticationSteps steps = AuthenticationSteps.fromValue(headers)
|
||||
.login(post("/auth/{path}/login", "cert").as(VaultResponse.class));
|
||||
|
||||
login(steps, webClient).as(StepVerifier::create) //
|
||||
.expectNext(VaultToken.of("my-token")) //
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void justLoginRequestShouldLogin() {
|
||||
|
||||
|
||||
@@ -19,12 +19,22 @@ import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.checkerframework.checker.units.qual.A;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.http.client.reactive.ClientHttpRequest;
|
||||
import org.springframework.mock.http.client.reactive.MockClientHttpRequest;
|
||||
import org.springframework.mock.http.client.reactive.MockClientHttpResponse;
|
||||
import org.springframework.test.web.client.MockRestServiceServer;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder;
|
||||
import org.springframework.vault.VaultException;
|
||||
import org.springframework.vault.authentication.AwsEc2AuthenticationOptions.Nonce;
|
||||
import org.springframework.vault.client.VaultClients;
|
||||
@@ -33,9 +43,7 @@ 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.jsonPath;
|
||||
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.match.MockRestRequestMatchers.*;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withServerError;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
|
||||
|
||||
@@ -72,6 +80,57 @@ class AwsEc2AuthenticationUnitTests {
|
||||
assertThat(authentication.getEc2Login()).containsEntry("pkcs7", "Hello, world").containsKey("nonce").hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldObtainIdentityDocumentV2() {
|
||||
|
||||
AwsEc2AuthenticationOptions options = AwsEc2AuthenticationOptions.builder()
|
||||
.version(AwsEc2AuthenticationOptions.InstanceMetadataServiceVersion.V2)
|
||||
.build();
|
||||
|
||||
this.mockRest.expect(requestTo("http://169.254.169.254/latest/api/token")) //
|
||||
.andExpect(method(HttpMethod.PUT)) //
|
||||
.andExpect(header("X-aws-ec2-metadata-token-ttl-seconds", "60")) //
|
||||
.andRespond(withSuccess().body("my-token"));
|
||||
|
||||
this.mockRest.expect(requestTo("http://169.254.169.254/latest/dynamic/instance-identity/pkcs7")) //
|
||||
.andExpect(method(HttpMethod.GET)) //
|
||||
.andExpect(header("X-aws-ec2-metadata-token", "my-token")) //
|
||||
.andRespond(withSuccess().body("Hello, world"));
|
||||
|
||||
AwsEc2Authentication authentication = new AwsEc2Authentication(options, this.restTemplate, this.restTemplate);
|
||||
|
||||
assertThat(authentication.getEc2Login()).containsEntry("pkcs7", "Hello, world").containsKey("nonce").hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldObtainIdentityDocumentWithImperativeAuthenticationStepsV2() {
|
||||
|
||||
AwsEc2AuthenticationOptions options = AwsEc2AuthenticationOptions.builder()
|
||||
.version(AwsEc2AuthenticationOptions.InstanceMetadataServiceVersion.V2)
|
||||
.build();
|
||||
|
||||
this.mockRest.expect(requestTo("http://169.254.169.254/latest/api/token")) //
|
||||
.andExpect(method(HttpMethod.PUT)) //
|
||||
.andExpect(header("X-aws-ec2-metadata-token-ttl-seconds", "60")) //
|
||||
.andRespond(withSuccess().body("my-token"));
|
||||
|
||||
this.mockRest.expect(requestTo("http://169.254.169.254/latest/dynamic/instance-identity/pkcs7")) //
|
||||
.andExpect(method(HttpMethod.GET)) //
|
||||
.andExpect(header("X-aws-ec2-metadata-token", "my-token")) //
|
||||
.andRespond(withSuccess().body("Hello, world"));
|
||||
|
||||
this.mockRest.expect(requestTo("/auth/aws-ec2/login"))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.pkcs7").value("Hello, world"))
|
||||
.andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON)
|
||||
.body("{" + "\"auth\":{\"client_token\":\"my-token\", \"lease_duration\":20}" + "}"));
|
||||
|
||||
AuthenticationSteps steps = AwsEc2Authentication.createAuthenticationSteps(options);
|
||||
AuthenticationStepsExecutor executor = new AuthenticationStepsExecutor(steps, this.restTemplate);
|
||||
|
||||
assertThat(executor.login()).isEqualTo(LoginToken.of("my-token"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCleanUpIdentityResponse() {
|
||||
|
||||
|
||||
@@ -306,6 +306,9 @@ further investigation.
|
||||
|
||||
The nonce is kept in memory and is lost during application restart.
|
||||
|
||||
Since Spring Vault 3.2, AWS-EC2 authentication supports request/response
|
||||
(IMDSv1) metadata retrieval and the session-based variant (IMDSv2).
|
||||
|
||||
AWS-EC2 authentication roles are optional and default to the AMI.
|
||||
You can configure the authentication role by setting
|
||||
it in `AwsEc2AuthenticationOptions`.
|
||||
|
||||
Reference in New Issue
Block a user