mirror of
https://github.com/elastic/elasticsearch.git
synced 2025-06-28 09:28:55 -04:00
Support dynamic credentials in S3HttpFixture
(#117458)
Rephrase the authorization check in `S3HttpFixture` in terms of a predicate provided by the caller so that there's no need for a separate subclass that handles session tokens, and so that it can support auto-generated credentials more naturally. Also adapts `Ec2ImdsHttpFixture` to dynamically generate credentials this way. Also extracts the STS fixture in `S3HttpFixtureWithSTS` into a separate service, similarly to #117324, and adapts this new fixture to dynamically generate credentials too. Relates ES-9984
This commit is contained in:
parent
ed33bea30c
commit
b13e0d25c0
17 changed files with 553 additions and 155 deletions
|
@ -46,6 +46,7 @@ dependencies {
|
|||
yamlRestTestImplementation project(":test:framework")
|
||||
yamlRestTestImplementation project(':test:fixtures:s3-fixture')
|
||||
yamlRestTestImplementation project(':test:fixtures:ec2-imds-fixture')
|
||||
yamlRestTestImplementation project(':test:fixtures:aws-sts-fixture')
|
||||
yamlRestTestImplementation project(':test:fixtures:minio-fixture')
|
||||
internalClusterTestImplementation project(':test:fixtures:minio-fixture')
|
||||
|
||||
|
|
|
@ -35,7 +35,14 @@ public class RepositoryS3RestReloadCredentialsIT extends ESRestTestCase {
|
|||
private static final String BUCKET = "RepositoryS3RestReloadCredentialsIT-bucket-" + HASHED_SEED;
|
||||
private static final String BASE_PATH = "RepositoryS3RestReloadCredentialsIT-base-path-" + HASHED_SEED;
|
||||
|
||||
public static final S3HttpFixture s3Fixture = new S3HttpFixture(true, BUCKET, BASE_PATH, "ignored");
|
||||
private static volatile String repositoryAccessKey;
|
||||
|
||||
public static final S3HttpFixture s3Fixture = new S3HttpFixture(
|
||||
true,
|
||||
BUCKET,
|
||||
BASE_PATH,
|
||||
S3HttpFixture.mutableAccessKey(() -> repositoryAccessKey)
|
||||
);
|
||||
|
||||
private static final MutableSettingsProvider keystoreSettings = new MutableSettingsProvider();
|
||||
|
||||
|
@ -68,7 +75,7 @@ public class RepositoryS3RestReloadCredentialsIT extends ESRestTestCase {
|
|||
|
||||
// Set up initial credentials
|
||||
final var accessKey1 = randomIdentifier();
|
||||
s3Fixture.setAccessKey(accessKey1);
|
||||
repositoryAccessKey = accessKey1;
|
||||
keystoreSettings.put("s3.client.default.access_key", accessKey1);
|
||||
keystoreSettings.put("s3.client.default.secret_key", randomIdentifier());
|
||||
cluster.updateStoredSecureSettings();
|
||||
|
@ -79,14 +86,14 @@ public class RepositoryS3RestReloadCredentialsIT extends ESRestTestCase {
|
|||
|
||||
// Rotate credentials in blob store
|
||||
final var accessKey2 = randomValueOtherThan(accessKey1, ESTestCase::randomIdentifier);
|
||||
s3Fixture.setAccessKey(accessKey2);
|
||||
repositoryAccessKey = accessKey2;
|
||||
|
||||
// Ensure that initial credentials now invalid
|
||||
final var accessDeniedException2 = expectThrows(ResponseException.class, () -> client().performRequest(verifyRequest));
|
||||
assertThat(accessDeniedException2.getResponse().getStatusLine().getStatusCode(), equalTo(500));
|
||||
assertThat(
|
||||
accessDeniedException2.getMessage(),
|
||||
allOf(containsString("Bad access key"), containsString("Status Code: 403"), containsString("Error Code: AccessDenied"))
|
||||
allOf(containsString("Access denied"), containsString("Status Code: 403"), containsString("Error Code: AccessDenied"))
|
||||
);
|
||||
|
||||
// Set up refreshed credentials
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
package org.elasticsearch.repositories.s3;
|
||||
|
||||
import fixture.aws.imds.Ec2ImdsHttpFixture;
|
||||
import fixture.s3.DynamicS3Credentials;
|
||||
import fixture.s3.S3HttpFixture;
|
||||
import fixture.s3.S3HttpFixtureWithSessionToken;
|
||||
|
||||
import com.carrotsearch.randomizedtesting.annotations.Name;
|
||||
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
|
||||
|
@ -34,27 +34,30 @@ public class RepositoryS3ClientYamlTestSuiteIT extends AbstractRepositoryS3Clien
|
|||
|
||||
private static final String HASHED_SEED = Integer.toString(Murmur3HashFunction.hash(System.getProperty("tests.seed")));
|
||||
private static final String TEMPORARY_SESSION_TOKEN = "session_token-" + HASHED_SEED;
|
||||
private static final String IMDS_ACCESS_KEY = "imds-access-key-" + HASHED_SEED;
|
||||
private static final String IMDS_SESSION_TOKEN = "imds-session-token-" + HASHED_SEED;
|
||||
|
||||
private static final S3HttpFixture s3Fixture = new S3HttpFixture();
|
||||
|
||||
private static final S3HttpFixtureWithSessionToken s3HttpFixtureWithSessionToken = new S3HttpFixtureWithSessionToken(
|
||||
private static final S3HttpFixture s3HttpFixtureWithSessionToken = new S3HttpFixture(
|
||||
true,
|
||||
"session_token_bucket",
|
||||
"session_token_base_path_integration_tests",
|
||||
System.getProperty("s3TemporaryAccessKey"),
|
||||
TEMPORARY_SESSION_TOKEN
|
||||
S3HttpFixture.fixedAccessKeyAndToken(System.getProperty("s3TemporaryAccessKey"), TEMPORARY_SESSION_TOKEN)
|
||||
);
|
||||
|
||||
private static final S3HttpFixtureWithSessionToken s3HttpFixtureWithImdsSessionToken = new S3HttpFixtureWithSessionToken(
|
||||
private static final DynamicS3Credentials dynamicS3Credentials = new DynamicS3Credentials();
|
||||
|
||||
private static final Ec2ImdsHttpFixture ec2ImdsHttpFixture = new Ec2ImdsHttpFixture(
|
||||
dynamicS3Credentials::addValidCredentials,
|
||||
Set.of()
|
||||
);
|
||||
|
||||
private static final S3HttpFixture s3HttpFixtureWithImdsSessionToken = new S3HttpFixture(
|
||||
true,
|
||||
"ec2_bucket",
|
||||
"ec2_base_path",
|
||||
IMDS_ACCESS_KEY,
|
||||
IMDS_SESSION_TOKEN
|
||||
dynamicS3Credentials::isAuthorized
|
||||
);
|
||||
|
||||
private static final Ec2ImdsHttpFixture ec2ImdsHttpFixture = new Ec2ImdsHttpFixture(IMDS_ACCESS_KEY, IMDS_SESSION_TOKEN, Set.of());
|
||||
|
||||
public static ElasticsearchCluster cluster = ElasticsearchCluster.local()
|
||||
.module("repository-s3")
|
||||
.keystore("s3.client.integration_test_permanent.access_key", System.getProperty("s3PermanentAccessKey"))
|
||||
|
|
|
@ -10,12 +10,12 @@
|
|||
package org.elasticsearch.repositories.s3;
|
||||
|
||||
import fixture.aws.imds.Ec2ImdsHttpFixture;
|
||||
import fixture.s3.S3HttpFixtureWithSessionToken;
|
||||
import fixture.s3.DynamicS3Credentials;
|
||||
import fixture.s3.S3HttpFixture;
|
||||
|
||||
import com.carrotsearch.randomizedtesting.annotations.Name;
|
||||
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
|
||||
|
||||
import org.elasticsearch.cluster.routing.Murmur3HashFunction;
|
||||
import org.elasticsearch.test.cluster.ElasticsearchCluster;
|
||||
import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate;
|
||||
import org.junit.ClassRule;
|
||||
|
@ -26,23 +26,20 @@ import java.util.Set;
|
|||
|
||||
public class RepositoryS3EcsClientYamlTestSuiteIT extends AbstractRepositoryS3ClientYamlTestSuiteIT {
|
||||
|
||||
private static final String HASHED_SEED = Integer.toString(Murmur3HashFunction.hash(System.getProperty("tests.seed")));
|
||||
private static final String ECS_ACCESS_KEY = "ecs-access-key-" + HASHED_SEED;
|
||||
private static final String ECS_SESSION_TOKEN = "ecs-session-token-" + HASHED_SEED;
|
||||
|
||||
private static final S3HttpFixtureWithSessionToken s3Fixture = new S3HttpFixtureWithSessionToken(
|
||||
"ecs_bucket",
|
||||
"ecs_base_path",
|
||||
ECS_ACCESS_KEY,
|
||||
ECS_SESSION_TOKEN
|
||||
);
|
||||
private static final DynamicS3Credentials dynamicS3Credentials = new DynamicS3Credentials();
|
||||
|
||||
private static final Ec2ImdsHttpFixture ec2ImdsHttpFixture = new Ec2ImdsHttpFixture(
|
||||
ECS_ACCESS_KEY,
|
||||
ECS_SESSION_TOKEN,
|
||||
dynamicS3Credentials::addValidCredentials,
|
||||
Set.of("/ecs_credentials_endpoint")
|
||||
);
|
||||
|
||||
private static final S3HttpFixture s3Fixture = new S3HttpFixture(
|
||||
true,
|
||||
"ecs_bucket",
|
||||
"ecs_base_path",
|
||||
dynamicS3Credentials::isAuthorized
|
||||
);
|
||||
|
||||
public static ElasticsearchCluster cluster = ElasticsearchCluster.local()
|
||||
.module("repository-s3")
|
||||
.setting("s3.client.integration_test_ecs.endpoint", s3Fixture::getAddress)
|
||||
|
|
|
@ -9,8 +9,9 @@
|
|||
|
||||
package org.elasticsearch.repositories.s3;
|
||||
|
||||
import fixture.aws.sts.AwsStsHttpFixture;
|
||||
import fixture.s3.DynamicS3Credentials;
|
||||
import fixture.s3.S3HttpFixture;
|
||||
import fixture.s3.S3HttpFixtureWithSTS;
|
||||
|
||||
import com.carrotsearch.randomizedtesting.annotations.Name;
|
||||
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
|
||||
|
@ -24,13 +25,27 @@ import org.junit.rules.TestRule;
|
|||
|
||||
public class RepositoryS3StsClientYamlTestSuiteIT extends AbstractRepositoryS3ClientYamlTestSuiteIT {
|
||||
|
||||
public static final S3HttpFixture s3Fixture = new S3HttpFixture();
|
||||
private static final S3HttpFixtureWithSTS s3Sts = new S3HttpFixtureWithSTS();
|
||||
private static final DynamicS3Credentials dynamicS3Credentials = new DynamicS3Credentials();
|
||||
|
||||
private static final S3HttpFixture s3HttpFixture = new S3HttpFixture(
|
||||
true,
|
||||
"sts_bucket",
|
||||
"sts_base_path",
|
||||
dynamicS3Credentials::isAuthorized
|
||||
);
|
||||
|
||||
private static final AwsStsHttpFixture stsHttpFixture = new AwsStsHttpFixture(dynamicS3Credentials::addValidCredentials, """
|
||||
Atza|IQEBLjAsAhRFiXuWpUXuRvQ9PZL3GMFcYevydwIUFAHZwXZXXXXXXXXJnrulxKDHwy87oGKPznh0D6bEQZTSCzyoCtL_8S07pLpr0zMbn6w1lfVZKNTBdDans\
|
||||
FBmtGnIsIapjI6xKR02Yc_2bQ8LZbUXSGm6Ry6_BG7PrtLZtj_dfCTj92xNGed-CrKqjG7nPBjNIL016GGvuS5gSvPRUxWES3VYfm1wl7WTI7jn-Pcb6M-buCgHhFO\
|
||||
zTQxod27L9CqnOLio7N3gZAGpsp6n1-AJBOCJckcyXe2c6uD0srOJeZlKUm2eTDVMf8IehDVI0r1QOnTV6KzzAI3OY87Vd_cVMQ""");
|
||||
|
||||
public static ElasticsearchCluster cluster = ElasticsearchCluster.local()
|
||||
.module("repository-s3")
|
||||
.setting("s3.client.integration_test_sts.endpoint", s3Sts::getAddress)
|
||||
.systemProperty("com.amazonaws.sdk.stsMetadataServiceEndpointOverride", () -> s3Sts.getAddress() + "/assume-role-with-web-identity")
|
||||
.setting("s3.client.integration_test_sts.endpoint", s3HttpFixture::getAddress)
|
||||
.systemProperty(
|
||||
"com.amazonaws.sdk.stsMetadataServiceEndpointOverride",
|
||||
() -> stsHttpFixture.getAddress() + "/assume-role-with-web-identity"
|
||||
)
|
||||
.configFile("repository-s3/aws-web-identity-token-file", Resource.fromClasspath("aws-web-identity-token-file"))
|
||||
.environment("AWS_WEB_IDENTITY_TOKEN_FILE", System.getProperty("awsWebIdentityTokenExternalLocation"))
|
||||
// // The AWS STS SDK requires the role and session names to be set. We can verify that they are sent to S3S in the
|
||||
|
@ -40,7 +55,7 @@ public class RepositoryS3StsClientYamlTestSuiteIT extends AbstractRepositoryS3Cl
|
|||
.build();
|
||||
|
||||
@ClassRule
|
||||
public static TestRule ruleChain = RuleChain.outerRule(s3Fixture).around(s3Sts).around(cluster);
|
||||
public static TestRule ruleChain = RuleChain.outerRule(s3HttpFixture).around(stsHttpFixture).around(cluster);
|
||||
|
||||
@ParametersFactory
|
||||
public static Iterable<Object[]> parameters() throws Exception {
|
||||
|
|
|
@ -86,6 +86,7 @@ List projects = [
|
|||
'distribution:tools:ansi-console',
|
||||
'server',
|
||||
'test:framework',
|
||||
'test:fixtures:aws-sts-fixture',
|
||||
'test:fixtures:azure-fixture',
|
||||
'test:fixtures:ec2-imds-fixture',
|
||||
'test:fixtures:gcs-fixture',
|
||||
|
|
19
test/fixtures/aws-sts-fixture/build.gradle
vendored
Normal file
19
test/fixtures/aws-sts-fixture/build.gradle
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
apply plugin: 'elasticsearch.java'
|
||||
|
||||
description = 'Fixture for emulating the Security Token Service (STS) running in AWS'
|
||||
|
||||
dependencies {
|
||||
api project(':server')
|
||||
api("junit:junit:${versions.junit}") {
|
||||
transitive = false
|
||||
}
|
||||
api project(':test:framework')
|
||||
}
|
64
test/fixtures/aws-sts-fixture/src/main/java/fixture/aws/sts/AwsStsHttpFixture.java
vendored
Normal file
64
test/fixtures/aws-sts-fixture/src/main/java/fixture/aws/sts/AwsStsHttpFixture.java
vendored
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
package fixture.aws.sts;
|
||||
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
|
||||
import org.junit.rules.ExternalResource;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
public class AwsStsHttpFixture extends ExternalResource {
|
||||
|
||||
private HttpServer server;
|
||||
|
||||
private final BiConsumer<String, String> newCredentialsConsumer;
|
||||
private final String webIdentityToken;
|
||||
|
||||
public AwsStsHttpFixture(BiConsumer<String, String> newCredentialsConsumer, String webIdentityToken) {
|
||||
this.newCredentialsConsumer = Objects.requireNonNull(newCredentialsConsumer);
|
||||
this.webIdentityToken = Objects.requireNonNull(webIdentityToken);
|
||||
}
|
||||
|
||||
protected HttpHandler createHandler() {
|
||||
return new AwsStsHttpHandler(newCredentialsConsumer, webIdentityToken);
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return "http://" + server.getAddress().getHostString() + ":" + server.getAddress().getPort();
|
||||
}
|
||||
|
||||
public void stop(int delay) {
|
||||
server.stop(delay);
|
||||
}
|
||||
|
||||
protected void before() throws Throwable {
|
||||
server = HttpServer.create(resolveAddress(), 0);
|
||||
server.createContext("/", Objects.requireNonNull(createHandler()));
|
||||
server.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void after() {
|
||||
stop(0);
|
||||
}
|
||||
|
||||
private static InetSocketAddress resolveAddress() {
|
||||
try {
|
||||
return new InetSocketAddress(InetAddress.getByName("localhost"), 0);
|
||||
} catch (UnknownHostException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,12 +6,16 @@
|
|||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
package fixture.s3;
|
||||
package fixture.aws.sts;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
|
||||
import org.elasticsearch.ExceptionsHelper;
|
||||
import org.elasticsearch.core.SuppressForbidden;
|
||||
import org.elasticsearch.rest.RestStatus;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.ZonedDateTime;
|
||||
|
@ -19,53 +23,39 @@ import java.time.format.DateTimeFormatter;
|
|||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class S3HttpFixtureWithSTS extends S3HttpFixture {
|
||||
import static org.elasticsearch.test.ESTestCase.randomIdentifier;
|
||||
|
||||
private static final String ROLE_ARN = "arn:aws:iam::123456789012:role/FederatedWebIdentityRole";
|
||||
private static final String ROLE_NAME = "sts-fixture-test";
|
||||
private final String sessionToken;
|
||||
/**
|
||||
* Minimal HTTP handler that emulates the AWS STS server
|
||||
*/
|
||||
@SuppressForbidden(reason = "this test uses a HttpServer to emulate the AWS STS endpoint")
|
||||
public class AwsStsHttpHandler implements HttpHandler {
|
||||
|
||||
static final String ROLE_ARN = "arn:aws:iam::123456789012:role/FederatedWebIdentityRole";
|
||||
static final String ROLE_NAME = "sts-fixture-test";
|
||||
|
||||
private final BiConsumer<String, String> newCredentialsConsumer;
|
||||
private final String webIdentityToken;
|
||||
|
||||
public S3HttpFixtureWithSTS() {
|
||||
this(true);
|
||||
}
|
||||
|
||||
public S3HttpFixtureWithSTS(boolean enabled) {
|
||||
this(
|
||||
enabled,
|
||||
"sts_bucket",
|
||||
"sts_base_path",
|
||||
"sts_access_key",
|
||||
"sts_session_token",
|
||||
"Atza|IQEBLjAsAhRFiXuWpUXuRvQ9PZL3GMFcYevydwIUFAHZwXZXXXXXXXXJnrulxKDHwy87oGKPznh0D6bEQZTSCzyoCtL_8S07pLpr0zMbn6w1lfVZKNTBdDansFBmtGnIsIapjI6xKR02Yc_2bQ8LZbUXSGm6Ry6_BG7PrtLZtj_dfCTj92xNGed-CrKqjG7nPBjNIL016GGvuS5gSvPRUxWES3VYfm1wl7WTI7jn-Pcb6M-buCgHhFOzTQxod27L9CqnOLio7N3gZAGpsp6n1-AJBOCJckcyXe2c6uD0srOJeZlKUm2eTDVMf8IehDVI0r1QOnTV6KzzAI3OY87Vd_cVMQ"
|
||||
);
|
||||
}
|
||||
|
||||
public S3HttpFixtureWithSTS(
|
||||
boolean enabled,
|
||||
String bucket,
|
||||
String basePath,
|
||||
String accessKey,
|
||||
String sessionToken,
|
||||
String webIdentityToken
|
||||
) {
|
||||
super(enabled, bucket, basePath, accessKey);
|
||||
this.sessionToken = sessionToken;
|
||||
this.webIdentityToken = webIdentityToken;
|
||||
public AwsStsHttpHandler(BiConsumer<String, String> newCredentialsConsumer, String webIdentityToken) {
|
||||
this.newCredentialsConsumer = Objects.requireNonNull(newCredentialsConsumer);
|
||||
this.webIdentityToken = Objects.requireNonNull(webIdentityToken);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpHandler createHandler() {
|
||||
final HttpHandler delegate = super.createHandler();
|
||||
public void handle(final HttpExchange exchange) throws IOException {
|
||||
// https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html
|
||||
|
||||
try (exchange) {
|
||||
final var requestMethod = exchange.getRequestMethod();
|
||||
final var path = exchange.getRequestURI().getPath();
|
||||
|
||||
if ("POST".equals(requestMethod) && "/assume-role-with-web-identity/".equals(path)) {
|
||||
|
||||
return exchange -> {
|
||||
// https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html
|
||||
// It's run as a separate service, but we emulate it under the `assume-role-with-web-identity` endpoint
|
||||
// of the S3 serve for the simplicity sake
|
||||
if ("POST".equals(exchange.getRequestMethod())
|
||||
&& exchange.getRequestURI().getPath().startsWith("/assume-role-with-web-identity")) {
|
||||
String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
|
||||
Map<String, String> params = Arrays.stream(body.split("&"))
|
||||
.map(e -> e.split("="))
|
||||
|
@ -82,6 +72,9 @@ public class S3HttpFixtureWithSTS extends S3HttpFixture {
|
|||
exchange.close();
|
||||
return;
|
||||
}
|
||||
final var accessKey = randomIdentifier();
|
||||
final var sessionToken = randomIdentifier();
|
||||
newCredentialsConsumer.accept(accessKey, sessionToken);
|
||||
final byte[] response = String.format(
|
||||
Locale.ROOT,
|
||||
"""
|
||||
|
@ -95,7 +88,7 @@ public class S3HttpFixtureWithSTS extends S3HttpFixture {
|
|||
</AssumedRoleUser>
|
||||
<Credentials>
|
||||
<SessionToken>%s</SessionToken>
|
||||
<SecretAccessKey>secret_access_key</SecretAccessKey>
|
||||
<SecretAccessKey>%s</SecretAccessKey>
|
||||
<Expiration>%s</Expiration>
|
||||
<AccessKeyId>%s</AccessKeyId>
|
||||
</Credentials>
|
||||
|
@ -109,6 +102,7 @@ public class S3HttpFixtureWithSTS extends S3HttpFixture {
|
|||
ROLE_ARN,
|
||||
ROLE_NAME,
|
||||
sessionToken,
|
||||
randomIdentifier(),
|
||||
ZonedDateTime.now().plusDays(1L).format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ")),
|
||||
accessKey
|
||||
).getBytes(StandardCharsets.UTF_8);
|
||||
|
@ -118,7 +112,8 @@ public class S3HttpFixtureWithSTS extends S3HttpFixture {
|
|||
exchange.close();
|
||||
return;
|
||||
}
|
||||
delegate.handle(exchange);
|
||||
};
|
||||
|
||||
ExceptionsHelper.maybeDieOnAnotherThread(new AssertionError("not supported: " + requestMethod + " " + path));
|
||||
}
|
||||
}
|
||||
}
|
268
test/fixtures/aws-sts-fixture/src/test/java/fixture/aws/sts/AwsStsHttpHandlerTests.java
vendored
Normal file
268
test/fixtures/aws-sts-fixture/src/test/java/fixture/aws/sts/AwsStsHttpHandlerTests.java
vendored
Normal file
|
@ -0,0 +1,268 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
package fixture.aws.sts;
|
||||
|
||||
import com.sun.net.httpserver.Headers;
|
||||
import com.sun.net.httpserver.HttpContext;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpPrincipal;
|
||||
|
||||
import org.elasticsearch.common.bytes.BytesArray;
|
||||
import org.elasticsearch.common.bytes.BytesReference;
|
||||
import org.elasticsearch.common.io.stream.BytesStreamOutput;
|
||||
import org.elasticsearch.rest.RestStatus;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.hamcrest.Matchers.aMapWithSize;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
|
||||
public class AwsStsHttpHandlerTests extends ESTestCase {
|
||||
|
||||
public void testGenerateCredentials() {
|
||||
final Map<String, String> generatedCredentials = new HashMap<>();
|
||||
|
||||
final var webIdentityToken = randomUnicodeOfLength(10);
|
||||
final var handler = new AwsStsHttpHandler(generatedCredentials::put, webIdentityToken);
|
||||
|
||||
final var response = handleRequest(
|
||||
handler,
|
||||
Map.of(
|
||||
"Action",
|
||||
"AssumeRoleWithWebIdentity",
|
||||
"RoleSessionName",
|
||||
AwsStsHttpHandler.ROLE_NAME,
|
||||
"RoleArn",
|
||||
AwsStsHttpHandler.ROLE_ARN,
|
||||
"WebIdentityToken",
|
||||
webIdentityToken
|
||||
)
|
||||
);
|
||||
assertEquals(RestStatus.OK, response.status());
|
||||
|
||||
assertThat(generatedCredentials, aMapWithSize(1));
|
||||
final var accessKey = generatedCredentials.keySet().iterator().next();
|
||||
final var sessionToken = generatedCredentials.values().iterator().next();
|
||||
|
||||
final var responseBody = response.body().utf8ToString();
|
||||
assertThat(responseBody, containsString("<AccessKeyId>" + accessKey + "</AccessKeyId>"));
|
||||
assertThat(responseBody, containsString("<SessionToken>" + sessionToken + "</SessionToken>"));
|
||||
}
|
||||
|
||||
public void testInvalidAction() {
|
||||
final var handler = new AwsStsHttpHandler((key, token) -> fail(), randomUnicodeOfLength(10));
|
||||
final var response = handleRequest(handler, Map.of("Action", "Unsupported"));
|
||||
assertEquals(RestStatus.BAD_REQUEST, response.status());
|
||||
}
|
||||
|
||||
public void testInvalidRole() {
|
||||
final var webIdentityToken = randomUnicodeOfLength(10);
|
||||
final var handler = new AwsStsHttpHandler((key, token) -> fail(), webIdentityToken);
|
||||
final var response = handleRequest(
|
||||
handler,
|
||||
Map.of(
|
||||
"Action",
|
||||
"AssumeRoleWithWebIdentity",
|
||||
"RoleSessionName",
|
||||
randomValueOtherThan(AwsStsHttpHandler.ROLE_NAME, ESTestCase::randomIdentifier),
|
||||
"RoleArn",
|
||||
AwsStsHttpHandler.ROLE_ARN,
|
||||
"WebIdentityToken",
|
||||
webIdentityToken
|
||||
)
|
||||
);
|
||||
assertEquals(RestStatus.UNAUTHORIZED, response.status());
|
||||
}
|
||||
|
||||
public void testInvalidToken() {
|
||||
final var webIdentityToken = randomUnicodeOfLength(10);
|
||||
final var handler = new AwsStsHttpHandler((key, token) -> fail(), webIdentityToken);
|
||||
final var response = handleRequest(
|
||||
handler,
|
||||
Map.of(
|
||||
"Action",
|
||||
"AssumeRoleWithWebIdentity",
|
||||
"RoleSessionName",
|
||||
AwsStsHttpHandler.ROLE_NAME,
|
||||
"RoleArn",
|
||||
AwsStsHttpHandler.ROLE_ARN,
|
||||
"WebIdentityToken",
|
||||
randomValueOtherThan(webIdentityToken, () -> randomUnicodeOfLength(10))
|
||||
)
|
||||
);
|
||||
assertEquals(RestStatus.UNAUTHORIZED, response.status());
|
||||
}
|
||||
|
||||
public void testInvalidARN() {
|
||||
final var webIdentityToken = randomUnicodeOfLength(10);
|
||||
final var handler = new AwsStsHttpHandler((key, token) -> fail(), webIdentityToken);
|
||||
final var response = handleRequest(
|
||||
handler,
|
||||
Map.of(
|
||||
"Action",
|
||||
"AssumeRoleWithWebIdentity",
|
||||
"RoleSessionName",
|
||||
AwsStsHttpHandler.ROLE_NAME,
|
||||
"RoleArn",
|
||||
randomValueOtherThan(AwsStsHttpHandler.ROLE_ARN, ESTestCase::randomIdentifier),
|
||||
"WebIdentityToken",
|
||||
webIdentityToken
|
||||
)
|
||||
);
|
||||
assertEquals(RestStatus.UNAUTHORIZED, response.status());
|
||||
}
|
||||
|
||||
private record TestHttpResponse(RestStatus status, BytesReference body) {}
|
||||
|
||||
private static TestHttpResponse handleRequest(AwsStsHttpHandler handler, Map<String, String> body) {
|
||||
final var httpExchange = new TestHttpExchange(
|
||||
"POST",
|
||||
"/assume-role-with-web-identity/",
|
||||
new BytesArray(
|
||||
body.entrySet()
|
||||
.stream()
|
||||
.map(e -> e.getKey() + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
|
||||
.collect(Collectors.joining("&"))
|
||||
),
|
||||
TestHttpExchange.EMPTY_HEADERS
|
||||
);
|
||||
try {
|
||||
handler.handle(httpExchange);
|
||||
} catch (IOException e) {
|
||||
fail(e);
|
||||
}
|
||||
assertNotEquals(0, httpExchange.getResponseCode());
|
||||
return new TestHttpResponse(RestStatus.fromCode(httpExchange.getResponseCode()), httpExchange.getResponseBodyContents());
|
||||
}
|
||||
|
||||
private static class TestHttpExchange extends HttpExchange {
|
||||
|
||||
private static final Headers EMPTY_HEADERS = new Headers();
|
||||
|
||||
private final String method;
|
||||
private final URI uri;
|
||||
private final BytesReference requestBody;
|
||||
private final Headers requestHeaders;
|
||||
|
||||
private final Headers responseHeaders = new Headers();
|
||||
private final BytesStreamOutput responseBody = new BytesStreamOutput();
|
||||
private int responseCode;
|
||||
|
||||
TestHttpExchange(String method, String uri, BytesReference requestBody, Headers requestHeaders) {
|
||||
this.method = method;
|
||||
this.uri = URI.create(uri);
|
||||
this.requestBody = requestBody;
|
||||
this.requestHeaders = requestHeaders;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Headers getRequestHeaders() {
|
||||
return requestHeaders;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Headers getResponseHeaders() {
|
||||
return responseHeaders;
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getRequestURI() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRequestMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpContext getHttpContext() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {}
|
||||
|
||||
@Override
|
||||
public InputStream getRequestBody() {
|
||||
try {
|
||||
return requestBody.streamInput();
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getResponseBody() {
|
||||
return responseBody;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendResponseHeaders(int rCode, long responseLength) {
|
||||
this.responseCode = rCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetSocketAddress getRemoteAddress() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getResponseCode() {
|
||||
return responseCode;
|
||||
}
|
||||
|
||||
public BytesReference getResponseBodyContents() {
|
||||
return responseBody.bytes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetSocketAddress getLocalAddress() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProtocol() {
|
||||
return "HTTP/1.1";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getAttribute(String name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String name, Object value) {
|
||||
fail("setAttribute not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStreams(InputStream i, OutputStream o) {
|
||||
fail("setStreams not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpPrincipal getPrincipal() {
|
||||
fail("getPrincipal not implemented");
|
||||
throw new UnsupportedOperationException("getPrincipal not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -18,23 +18,22 @@ import java.net.InetSocketAddress;
|
|||
import java.net.UnknownHostException;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
public class Ec2ImdsHttpFixture extends ExternalResource {
|
||||
|
||||
private HttpServer server;
|
||||
|
||||
private final String accessKey;
|
||||
private final String sessionToken;
|
||||
private final BiConsumer<String, String> newCredentialsConsumer;
|
||||
private final Set<String> alternativeCredentialsEndpoints;
|
||||
|
||||
public Ec2ImdsHttpFixture(String accessKey, String sessionToken, Set<String> alternativeCredentialsEndpoints) {
|
||||
this.accessKey = accessKey;
|
||||
this.sessionToken = sessionToken;
|
||||
this.alternativeCredentialsEndpoints = alternativeCredentialsEndpoints;
|
||||
public Ec2ImdsHttpFixture(BiConsumer<String, String> newCredentialsConsumer, Set<String> alternativeCredentialsEndpoints) {
|
||||
this.newCredentialsConsumer = Objects.requireNonNull(newCredentialsConsumer);
|
||||
this.alternativeCredentialsEndpoints = Objects.requireNonNull(alternativeCredentialsEndpoints);
|
||||
}
|
||||
|
||||
protected HttpHandler createHandler() {
|
||||
return new Ec2ImdsHttpHandler(accessKey, sessionToken, alternativeCredentialsEndpoints);
|
||||
return new Ec2ImdsHttpHandler(newCredentialsConsumer, alternativeCredentialsEndpoints);
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
|
|
|
@ -25,6 +25,7 @@ import java.time.format.DateTimeFormatter;
|
|||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import static org.elasticsearch.test.ESTestCase.randomIdentifier;
|
||||
|
||||
|
@ -36,13 +37,11 @@ public class Ec2ImdsHttpHandler implements HttpHandler {
|
|||
|
||||
private static final String IMDS_SECURITY_CREDENTIALS_PATH = "/latest/meta-data/iam/security-credentials/";
|
||||
|
||||
private final String accessKey;
|
||||
private final String sessionToken;
|
||||
private final BiConsumer<String, String> newCredentialsConsumer;
|
||||
private final Set<String> validCredentialsEndpoints = ConcurrentCollections.newConcurrentSet();
|
||||
|
||||
public Ec2ImdsHttpHandler(String accessKey, String sessionToken, Collection<String> alternativeCredentialsEndpoints) {
|
||||
this.accessKey = Objects.requireNonNull(accessKey);
|
||||
this.sessionToken = Objects.requireNonNull(sessionToken);
|
||||
public Ec2ImdsHttpHandler(BiConsumer<String, String> newCredentialsConsumer, Collection<String> alternativeCredentialsEndpoints) {
|
||||
this.newCredentialsConsumer = Objects.requireNonNull(newCredentialsConsumer);
|
||||
this.validCredentialsEndpoints.addAll(alternativeCredentialsEndpoints);
|
||||
}
|
||||
|
||||
|
@ -70,6 +69,9 @@ public class Ec2ImdsHttpHandler implements HttpHandler {
|
|||
exchange.getResponseBody().write(response);
|
||||
return;
|
||||
} else if (validCredentialsEndpoints.contains(path)) {
|
||||
final String accessKey = randomIdentifier();
|
||||
final String sessionToken = randomIdentifier();
|
||||
newCredentialsConsumer.accept(accessKey, sessionToken);
|
||||
final byte[] response = Strings.format(
|
||||
"""
|
||||
{
|
||||
|
|
|
@ -28,15 +28,18 @@ import java.io.InputStream;
|
|||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.hamcrest.Matchers.aMapWithSize;
|
||||
|
||||
public class Ec2ImdsHttpHandlerTests extends ESTestCase {
|
||||
|
||||
public void testImdsV1() throws IOException {
|
||||
final var accessKey = randomIdentifier();
|
||||
final var sessionToken = randomIdentifier();
|
||||
final Map<String, String> generatedCredentials = new HashMap<>();
|
||||
|
||||
final var handler = new Ec2ImdsHttpHandler(accessKey, sessionToken, Set.of());
|
||||
final var handler = new Ec2ImdsHttpHandler(generatedCredentials::put, Set.of());
|
||||
|
||||
final var roleResponse = handleRequest(handler, "GET", "/latest/meta-data/iam/security-credentials/");
|
||||
assertEquals(RestStatus.OK, roleResponse.status());
|
||||
|
@ -46,6 +49,10 @@ public class Ec2ImdsHttpHandlerTests extends ESTestCase {
|
|||
final var credentialsResponse = handleRequest(handler, "GET", "/latest/meta-data/iam/security-credentials/" + profileName);
|
||||
assertEquals(RestStatus.OK, credentialsResponse.status());
|
||||
|
||||
assertThat(generatedCredentials, aMapWithSize(1));
|
||||
final var accessKey = generatedCredentials.keySet().iterator().next();
|
||||
final var sessionToken = generatedCredentials.values().iterator().next();
|
||||
|
||||
final var responseMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), credentialsResponse.body().streamInput(), false);
|
||||
assertEquals(Set.of("AccessKeyId", "Expiration", "RoleArn", "SecretAccessKey", "Token"), responseMap.keySet());
|
||||
assertEquals(accessKey, responseMap.get("AccessKeyId"));
|
||||
|
@ -55,7 +62,7 @@ public class Ec2ImdsHttpHandlerTests extends ESTestCase {
|
|||
public void testImdsV2Disabled() {
|
||||
assertEquals(
|
||||
RestStatus.METHOD_NOT_ALLOWED,
|
||||
handleRequest(new Ec2ImdsHttpHandler(randomIdentifier(), randomIdentifier(), Set.of()), "PUT", "/latest/api/token").status()
|
||||
handleRequest(new Ec2ImdsHttpHandler((accessKey, sessionToken) -> fail(), Set.of()), "PUT", "/latest/api/token").status()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
39
test/fixtures/s3-fixture/src/main/java/fixture/s3/DynamicS3Credentials.java
vendored
Normal file
39
test/fixtures/s3-fixture/src/main/java/fixture/s3/DynamicS3Credentials.java
vendored
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
package fixture.s3;
|
||||
|
||||
import org.elasticsearch.common.util.concurrent.ConcurrentCollections;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Allows dynamic creation of access-key/session-token credentials for accessing AWS services such as S3. Typically there's one service
|
||||
* (e.g. IMDS or STS) which creates credentials dynamically and registers them here using {@link #addValidCredentials}, and then the
|
||||
* {@link S3HttpFixture} uses {@link #isAuthorized} to validate the credentials it receives corresponds with some previously-generated
|
||||
* credentials.
|
||||
*/
|
||||
public class DynamicS3Credentials {
|
||||
private final Map<String, Set<String>> validCredentialsMap = ConcurrentCollections.newConcurrentMap();
|
||||
|
||||
public boolean isAuthorized(String authorizationHeader, String sessionTokenHeader) {
|
||||
return authorizationHeader != null
|
||||
&& sessionTokenHeader != null
|
||||
&& validCredentialsMap.getOrDefault(sessionTokenHeader, Set.of()).stream().anyMatch(authorizationHeader::contains);
|
||||
}
|
||||
|
||||
public void addValidCredentials(String accessKey, String sessionToken) {
|
||||
validCredentialsMap.computeIfAbsent(
|
||||
Objects.requireNonNull(sessionToken, "sessionToken"),
|
||||
t -> ConcurrentCollections.newConcurrentSet()
|
||||
).add(Objects.requireNonNull(accessKey, "accessKey"));
|
||||
}
|
||||
}
|
|
@ -21,6 +21,8 @@ import java.net.InetAddress;
|
|||
import java.net.InetSocketAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiPredicate;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class S3HttpFixture extends ExternalResource {
|
||||
|
||||
|
@ -29,21 +31,21 @@ public class S3HttpFixture extends ExternalResource {
|
|||
private final boolean enabled;
|
||||
private final String bucket;
|
||||
private final String basePath;
|
||||
protected volatile String accessKey;
|
||||
private final BiPredicate<String, String> authorizationPredicate;
|
||||
|
||||
public S3HttpFixture() {
|
||||
this(true);
|
||||
}
|
||||
|
||||
public S3HttpFixture(boolean enabled) {
|
||||
this(enabled, "bucket", "base_path_integration_tests", "s3_test_access_key");
|
||||
this(enabled, "bucket", "base_path_integration_tests", fixedAccessKey("s3_test_access_key"));
|
||||
}
|
||||
|
||||
public S3HttpFixture(boolean enabled, String bucket, String basePath, String accessKey) {
|
||||
public S3HttpFixture(boolean enabled, String bucket, String basePath, BiPredicate<String, String> authorizationPredicate) {
|
||||
this.enabled = enabled;
|
||||
this.bucket = bucket;
|
||||
this.basePath = basePath;
|
||||
this.accessKey = accessKey;
|
||||
this.authorizationPredicate = authorizationPredicate;
|
||||
}
|
||||
|
||||
protected HttpHandler createHandler() {
|
||||
|
@ -51,9 +53,11 @@ public class S3HttpFixture extends ExternalResource {
|
|||
@Override
|
||||
public void handle(final HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
final String authorization = exchange.getRequestHeaders().getFirst("Authorization");
|
||||
if (authorization == null || authorization.contains(accessKey) == false) {
|
||||
sendError(exchange, RestStatus.FORBIDDEN, "AccessDenied", "Bad access key");
|
||||
if (authorizationPredicate.test(
|
||||
exchange.getRequestHeaders().getFirst("Authorization"),
|
||||
exchange.getRequestHeaders().getFirst("x-amz-security-token")
|
||||
) == false) {
|
||||
sendError(exchange, RestStatus.FORBIDDEN, "AccessDenied", "Access denied by " + authorizationPredicate);
|
||||
return;
|
||||
}
|
||||
super.handle(exchange);
|
||||
|
@ -76,7 +80,7 @@ public class S3HttpFixture extends ExternalResource {
|
|||
|
||||
protected void before() throws Throwable {
|
||||
if (enabled) {
|
||||
InetSocketAddress inetSocketAddress = resolveAddress("localhost", 0);
|
||||
InetSocketAddress inetSocketAddress = resolveAddress();
|
||||
this.server = HttpServer.create(inetSocketAddress, 0);
|
||||
HttpHandler handler = createHandler();
|
||||
this.server.createContext("/", Objects.requireNonNull(handler));
|
||||
|
@ -91,15 +95,27 @@ public class S3HttpFixture extends ExternalResource {
|
|||
}
|
||||
}
|
||||
|
||||
private static InetSocketAddress resolveAddress(String address, int port) {
|
||||
private static InetSocketAddress resolveAddress() {
|
||||
try {
|
||||
return new InetSocketAddress(InetAddress.getByName(address), port);
|
||||
return new InetSocketAddress(InetAddress.getByName("localhost"), 0);
|
||||
} catch (UnknownHostException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void setAccessKey(String accessKey) {
|
||||
this.accessKey = accessKey;
|
||||
public static BiPredicate<String, String> fixedAccessKey(String accessKey) {
|
||||
return mutableAccessKey(() -> accessKey);
|
||||
}
|
||||
|
||||
public static BiPredicate<String, String> mutableAccessKey(Supplier<String> accessKeySupplier) {
|
||||
return (authorizationHeader, sessionTokenHeader) -> authorizationHeader != null
|
||||
&& authorizationHeader.contains(accessKeySupplier.get());
|
||||
}
|
||||
|
||||
public static BiPredicate<String, String> fixedAccessKeyAndToken(String accessKey, String sessionToken) {
|
||||
Objects.requireNonNull(sessionToken);
|
||||
final var accessKeyPredicate = fixedAccessKey(accessKey);
|
||||
return (authorizationHeader, sessionTokenHeader) -> accessKeyPredicate.test(authorizationHeader, sessionTokenHeader)
|
||||
&& sessionToken.equals(sessionTokenHeader);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
package fixture.s3;
|
||||
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
|
||||
import org.elasticsearch.rest.RestStatus;
|
||||
|
||||
import static fixture.s3.S3HttpHandler.sendError;
|
||||
|
||||
public class S3HttpFixtureWithSessionToken extends S3HttpFixture {
|
||||
|
||||
protected final String sessionToken;
|
||||
|
||||
public S3HttpFixtureWithSessionToken(String bucket, String basePath, String accessKey, String sessionToken) {
|
||||
super(true, bucket, basePath, accessKey);
|
||||
this.sessionToken = sessionToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpHandler createHandler() {
|
||||
final HttpHandler delegate = super.createHandler();
|
||||
return exchange -> {
|
||||
final String securityToken = exchange.getRequestHeaders().getFirst("x-amz-security-token");
|
||||
if (securityToken == null) {
|
||||
sendError(exchange, RestStatus.FORBIDDEN, "AccessDenied", "No session token");
|
||||
return;
|
||||
}
|
||||
if (securityToken.equals(sessionToken) == false) {
|
||||
sendError(exchange, RestStatus.FORBIDDEN, "AccessDenied", "Bad session token");
|
||||
return;
|
||||
}
|
||||
delegate.handle(exchange);
|
||||
};
|
||||
}
|
||||
}
|
|
@ -44,7 +44,14 @@ public class S3SearchableSnapshotsCredentialsReloadIT extends ESRestTestCase {
|
|||
private static final String BUCKET = "S3SearchableSnapshotsCredentialsReloadIT-bucket";
|
||||
private static final String BASE_PATH = "S3SearchableSnapshotsCredentialsReloadIT-base-path";
|
||||
|
||||
public static final S3HttpFixture s3Fixture = new S3HttpFixture(true, BUCKET, BASE_PATH, "ignored");
|
||||
private static volatile String repositoryAccessKey;
|
||||
|
||||
public static final S3HttpFixture s3Fixture = new S3HttpFixture(
|
||||
true,
|
||||
BUCKET,
|
||||
BASE_PATH,
|
||||
S3HttpFixture.mutableAccessKey(() -> repositoryAccessKey)
|
||||
);
|
||||
|
||||
private static final MutableSettingsProvider keystoreSettings = new MutableSettingsProvider();
|
||||
|
||||
|
@ -78,7 +85,7 @@ public class S3SearchableSnapshotsCredentialsReloadIT extends ESRestTestCase {
|
|||
|
||||
// Set up initial credentials
|
||||
final String accessKey1 = randomIdentifier();
|
||||
s3Fixture.setAccessKey(accessKey1);
|
||||
repositoryAccessKey = accessKey1;
|
||||
keystoreSettings.put("s3.client.default.access_key", accessKey1);
|
||||
keystoreSettings.put("s3.client.default.secret_key", randomIdentifier());
|
||||
cluster.updateStoredSecureSettings();
|
||||
|
@ -92,7 +99,7 @@ public class S3SearchableSnapshotsCredentialsReloadIT extends ESRestTestCase {
|
|||
// Rotate credentials in blob store
|
||||
logger.info("--> rotate credentials");
|
||||
final String accessKey2 = randomValueOtherThan(accessKey1, ESTestCase::randomIdentifier);
|
||||
s3Fixture.setAccessKey(accessKey2);
|
||||
repositoryAccessKey = accessKey2;
|
||||
|
||||
// Ensure searchable snapshot now does not work due to invalid credentials
|
||||
logger.info("--> expect failure");
|
||||
|
@ -118,7 +125,7 @@ public class S3SearchableSnapshotsCredentialsReloadIT extends ESRestTestCase {
|
|||
final String accessKey2 = randomValueOtherThan(accessKey1, ESTestCase::randomIdentifier);
|
||||
final String alternativeClient = randomValueOtherThan("default", ESTestCase::randomIdentifier);
|
||||
|
||||
s3Fixture.setAccessKey(accessKey1);
|
||||
repositoryAccessKey = accessKey1;
|
||||
keystoreSettings.put("s3.client.default.access_key", accessKey1);
|
||||
keystoreSettings.put("s3.client.default.secret_key", randomIdentifier());
|
||||
keystoreSettings.put("s3.client." + alternativeClient + ".access_key", accessKey2);
|
||||
|
@ -133,7 +140,7 @@ public class S3SearchableSnapshotsCredentialsReloadIT extends ESRestTestCase {
|
|||
|
||||
// Rotate credentials in blob store
|
||||
logger.info("--> rotate credentials");
|
||||
s3Fixture.setAccessKey(accessKey2);
|
||||
repositoryAccessKey = accessKey2;
|
||||
|
||||
// Ensure searchable snapshot now does not work due to invalid credentials
|
||||
logger.info("--> expect failure");
|
||||
|
@ -157,7 +164,7 @@ public class S3SearchableSnapshotsCredentialsReloadIT extends ESRestTestCase {
|
|||
final String accessKey2 = randomValueOtherThan(accessKey1, ESTestCase::randomIdentifier);
|
||||
|
||||
testHarness.putRepository(b -> b.put("access_key", accessKey1).put("secret_key", randomIdentifier()));
|
||||
s3Fixture.setAccessKey(accessKey1);
|
||||
repositoryAccessKey = accessKey1;
|
||||
|
||||
testHarness.createFrozenSearchableSnapshotIndex();
|
||||
|
||||
|
@ -166,7 +173,7 @@ public class S3SearchableSnapshotsCredentialsReloadIT extends ESRestTestCase {
|
|||
|
||||
// Rotate credentials in blob store
|
||||
logger.info("--> rotate credentials");
|
||||
s3Fixture.setAccessKey(accessKey2);
|
||||
repositoryAccessKey = accessKey2;
|
||||
|
||||
// Ensure searchable snapshot now does not work due to invalid credentials
|
||||
logger.info("--> expect failure");
|
||||
|
@ -269,7 +276,7 @@ public class S3SearchableSnapshotsCredentialsReloadIT extends ESRestTestCase {
|
|||
assertThat(
|
||||
expectThrows(ResponseException.class, () -> client().performRequest(searchRequest)).getMessage(),
|
||||
allOf(
|
||||
containsString("Bad access key"),
|
||||
containsString("Access denied"),
|
||||
containsString("Status Code: 403"),
|
||||
containsString("Error Code: AccessDenied"),
|
||||
containsString("failed to read data from cache")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue