Skip to content

Commit 44e90c8

Browse files
authored
Support dynamic credentials in S3HttpFixture (#117749)
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 Backport of #117458 to 8.x
1 parent f52f5c4 commit 44e90c8

File tree

17 files changed

+552
-154
lines changed

17 files changed

+552
-154
lines changed

modules/repository-s3/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ dependencies {
4747
yamlRestTestImplementation project(":test:framework")
4848
yamlRestTestImplementation project(':test:fixtures:s3-fixture')
4949
yamlRestTestImplementation project(':test:fixtures:ec2-imds-fixture')
50+
yamlRestTestImplementation project(':test:fixtures:aws-sts-fixture')
5051
yamlRestTestImplementation project(':test:fixtures:minio-fixture')
5152
internalClusterTestImplementation project(':test:fixtures:minio-fixture')
5253

modules/repository-s3/src/javaRestTest/java/org/elasticsearch/repositories/s3/RepositoryS3RestReloadCredentialsIT.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,14 @@ public class RepositoryS3RestReloadCredentialsIT extends ESRestTestCase {
3636
private static final String BUCKET = "RepositoryS3RestReloadCredentialsIT-bucket-" + HASHED_SEED;
3737
private static final String BASE_PATH = "RepositoryS3RestReloadCredentialsIT-base-path-" + HASHED_SEED;
3838

39-
public static final S3HttpFixture s3Fixture = new S3HttpFixture(true, BUCKET, BASE_PATH, "ignored");
39+
private static volatile String repositoryAccessKey;
40+
41+
public static final S3HttpFixture s3Fixture = new S3HttpFixture(
42+
true,
43+
BUCKET,
44+
BASE_PATH,
45+
S3HttpFixture.mutableAccessKey(() -> repositoryAccessKey)
46+
);
4047

4148
private static final MutableSettingsProvider keystoreSettings = new MutableSettingsProvider();
4249

@@ -67,7 +74,7 @@ public void testReloadCredentialsFromKeystore() throws IOException {
6774

6875
// Set up initial credentials
6976
final var accessKey1 = randomIdentifier();
70-
s3Fixture.setAccessKey(accessKey1);
77+
repositoryAccessKey = accessKey1;
7178
keystoreSettings.put("s3.client.default.access_key", accessKey1);
7279
keystoreSettings.put("s3.client.default.secret_key", randomSecretKey());
7380
cluster.updateStoredSecureSettings();
@@ -79,14 +86,14 @@ public void testReloadCredentialsFromKeystore() throws IOException {
7986

8087
// Rotate credentials in blob store
8188
final var accessKey2 = randomValueOtherThan(accessKey1, ESTestCase::randomSecretKey);
82-
s3Fixture.setAccessKey(accessKey2);
89+
repositoryAccessKey = accessKey2;
8390

8491
// Ensure that initial credentials now invalid
8592
final var accessDeniedException2 = expectThrows(ResponseException.class, () -> client().performRequest(verifyRequest));
8693
assertThat(accessDeniedException2.getResponse().getStatusLine().getStatusCode(), equalTo(500));
8794
assertThat(
8895
accessDeniedException2.getMessage(),
89-
allOf(containsString("Bad access key"), containsString("Status Code: 403"), containsString("Error Code: AccessDenied"))
96+
allOf(containsString("Access denied"), containsString("Status Code: 403"), containsString("Error Code: AccessDenied"))
9097
);
9198

9299
// Set up refreshed credentials

modules/repository-s3/src/yamlRestTest/java/org/elasticsearch/repositories/s3/RepositoryS3ClientYamlTestSuiteIT.java

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
package org.elasticsearch.repositories.s3;
1111

1212
import fixture.aws.imds.Ec2ImdsHttpFixture;
13+
import fixture.s3.DynamicS3Credentials;
1314
import fixture.s3.S3HttpFixture;
14-
import fixture.s3.S3HttpFixtureWithSessionToken;
1515

1616
import com.carrotsearch.randomizedtesting.annotations.Name;
1717
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
@@ -34,27 +34,30 @@ public class RepositoryS3ClientYamlTestSuiteIT extends AbstractRepositoryS3Clien
3434

3535
private static final String HASHED_SEED = Integer.toString(Murmur3HashFunction.hash(System.getProperty("tests.seed")));
3636
private static final String TEMPORARY_SESSION_TOKEN = "session_token-" + HASHED_SEED;
37-
private static final String IMDS_ACCESS_KEY = "imds-access-key-" + HASHED_SEED;
38-
private static final String IMDS_SESSION_TOKEN = "imds-session-token-" + HASHED_SEED;
3937

4038
private static final S3HttpFixture s3Fixture = new S3HttpFixture();
4139

42-
private static final S3HttpFixtureWithSessionToken s3HttpFixtureWithSessionToken = new S3HttpFixtureWithSessionToken(
40+
private static final S3HttpFixture s3HttpFixtureWithSessionToken = new S3HttpFixture(
41+
true,
4342
"session_token_bucket",
4443
"session_token_base_path_integration_tests",
45-
System.getProperty("s3TemporaryAccessKey"),
46-
TEMPORARY_SESSION_TOKEN
44+
S3HttpFixture.fixedAccessKeyAndToken(System.getProperty("s3TemporaryAccessKey"), TEMPORARY_SESSION_TOKEN)
4745
);
4846

49-
private static final S3HttpFixtureWithSessionToken s3HttpFixtureWithImdsSessionToken = new S3HttpFixtureWithSessionToken(
47+
private static final DynamicS3Credentials dynamicS3Credentials = new DynamicS3Credentials();
48+
49+
private static final Ec2ImdsHttpFixture ec2ImdsHttpFixture = new Ec2ImdsHttpFixture(
50+
dynamicS3Credentials::addValidCredentials,
51+
Set.of()
52+
);
53+
54+
private static final S3HttpFixture s3HttpFixtureWithImdsSessionToken = new S3HttpFixture(
55+
true,
5056
"ec2_bucket",
5157
"ec2_base_path",
52-
IMDS_ACCESS_KEY,
53-
IMDS_SESSION_TOKEN
58+
dynamicS3Credentials::isAuthorized
5459
);
5560

56-
private static final Ec2ImdsHttpFixture ec2ImdsHttpFixture = new Ec2ImdsHttpFixture(IMDS_ACCESS_KEY, IMDS_SESSION_TOKEN, Set.of());
57-
5861
public static ElasticsearchCluster cluster = ElasticsearchCluster.local()
5962
.module("repository-s3")
6063
.keystore("s3.client.integration_test_permanent.access_key", System.getProperty("s3PermanentAccessKey"))

modules/repository-s3/src/yamlRestTest/java/org/elasticsearch/repositories/s3/RepositoryS3EcsClientYamlTestSuiteIT.java

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@
1010
package org.elasticsearch.repositories.s3;
1111

1212
import fixture.aws.imds.Ec2ImdsHttpFixture;
13-
import fixture.s3.S3HttpFixtureWithSessionToken;
13+
import fixture.s3.DynamicS3Credentials;
14+
import fixture.s3.S3HttpFixture;
1415

1516
import com.carrotsearch.randomizedtesting.annotations.Name;
1617
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
1718

18-
import org.elasticsearch.cluster.routing.Murmur3HashFunction;
1919
import org.elasticsearch.test.cluster.ElasticsearchCluster;
2020
import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate;
2121
import org.junit.ClassRule;
@@ -26,23 +26,20 @@
2626

2727
public class RepositoryS3EcsClientYamlTestSuiteIT extends AbstractRepositoryS3ClientYamlTestSuiteIT {
2828

29-
private static final String HASHED_SEED = Integer.toString(Murmur3HashFunction.hash(System.getProperty("tests.seed")));
30-
private static final String ECS_ACCESS_KEY = "ecs-access-key-" + HASHED_SEED;
31-
private static final String ECS_SESSION_TOKEN = "ecs-session-token-" + HASHED_SEED;
32-
33-
private static final S3HttpFixtureWithSessionToken s3Fixture = new S3HttpFixtureWithSessionToken(
34-
"ecs_bucket",
35-
"ecs_base_path",
36-
ECS_ACCESS_KEY,
37-
ECS_SESSION_TOKEN
38-
);
29+
private static final DynamicS3Credentials dynamicS3Credentials = new DynamicS3Credentials();
3930

4031
private static final Ec2ImdsHttpFixture ec2ImdsHttpFixture = new Ec2ImdsHttpFixture(
41-
ECS_ACCESS_KEY,
42-
ECS_SESSION_TOKEN,
32+
dynamicS3Credentials::addValidCredentials,
4333
Set.of("/ecs_credentials_endpoint")
4434
);
4535

36+
private static final S3HttpFixture s3Fixture = new S3HttpFixture(
37+
true,
38+
"ecs_bucket",
39+
"ecs_base_path",
40+
dynamicS3Credentials::isAuthorized
41+
);
42+
4643
public static ElasticsearchCluster cluster = ElasticsearchCluster.local()
4744
.module("repository-s3")
4845
.setting("s3.client.integration_test_ecs.endpoint", s3Fixture::getAddress)

modules/repository-s3/src/yamlRestTest/java/org/elasticsearch/repositories/s3/RepositoryS3StsClientYamlTestSuiteIT.java

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99

1010
package org.elasticsearch.repositories.s3;
1111

12+
import fixture.aws.sts.AwsStsHttpFixture;
13+
import fixture.s3.DynamicS3Credentials;
1214
import fixture.s3.S3HttpFixture;
13-
import fixture.s3.S3HttpFixtureWithSTS;
1415

1516
import com.carrotsearch.randomizedtesting.annotations.Name;
1617
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
@@ -24,13 +25,27 @@
2425

2526
public class RepositoryS3StsClientYamlTestSuiteIT extends AbstractRepositoryS3ClientYamlTestSuiteIT {
2627

27-
public static final S3HttpFixture s3Fixture = new S3HttpFixture();
28-
private static final S3HttpFixtureWithSTS s3Sts = new S3HttpFixtureWithSTS();
28+
private static final DynamicS3Credentials dynamicS3Credentials = new DynamicS3Credentials();
29+
30+
private static final S3HttpFixture s3HttpFixture = new S3HttpFixture(
31+
true,
32+
"sts_bucket",
33+
"sts_base_path",
34+
dynamicS3Credentials::isAuthorized
35+
);
36+
37+
private static final AwsStsHttpFixture stsHttpFixture = new AwsStsHttpFixture(dynamicS3Credentials::addValidCredentials, """
38+
Atza|IQEBLjAsAhRFiXuWpUXuRvQ9PZL3GMFcYevydwIUFAHZwXZXXXXXXXXJnrulxKDHwy87oGKPznh0D6bEQZTSCzyoCtL_8S07pLpr0zMbn6w1lfVZKNTBdDans\
39+
FBmtGnIsIapjI6xKR02Yc_2bQ8LZbUXSGm6Ry6_BG7PrtLZtj_dfCTj92xNGed-CrKqjG7nPBjNIL016GGvuS5gSvPRUxWES3VYfm1wl7WTI7jn-Pcb6M-buCgHhFO\
40+
zTQxod27L9CqnOLio7N3gZAGpsp6n1-AJBOCJckcyXe2c6uD0srOJeZlKUm2eTDVMf8IehDVI0r1QOnTV6KzzAI3OY87Vd_cVMQ""");
2941

3042
public static ElasticsearchCluster cluster = ElasticsearchCluster.local()
3143
.module("repository-s3")
32-
.setting("s3.client.integration_test_sts.endpoint", s3Sts::getAddress)
33-
.systemProperty("com.amazonaws.sdk.stsMetadataServiceEndpointOverride", () -> s3Sts.getAddress() + "/assume-role-with-web-identity")
44+
.setting("s3.client.integration_test_sts.endpoint", s3HttpFixture::getAddress)
45+
.systemProperty(
46+
"com.amazonaws.sdk.stsMetadataServiceEndpointOverride",
47+
() -> stsHttpFixture.getAddress() + "/assume-role-with-web-identity"
48+
)
3449
.configFile("repository-s3/aws-web-identity-token-file", Resource.fromClasspath("aws-web-identity-token-file"))
3550
.environment("AWS_WEB_IDENTITY_TOKEN_FILE", System.getProperty("awsWebIdentityTokenExternalLocation"))
3651
// // 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
4055
.build();
4156

4257
@ClassRule
43-
public static TestRule ruleChain = RuleChain.outerRule(s3Fixture).around(s3Sts).around(cluster);
58+
public static TestRule ruleChain = RuleChain.outerRule(s3HttpFixture).around(stsHttpFixture).around(cluster);
4459

4560
@ParametersFactory
4661
public static Iterable<Object[]> parameters() throws Exception {

settings.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ List projects = [
8888
'distribution:tools:ansi-console',
8989
'server',
9090
'test:framework',
91+
'test:fixtures:aws-sts-fixture',
9192
'test:fixtures:azure-fixture',
9293
'test:fixtures:ec2-imds-fixture',
9394
'test:fixtures:gcs-fixture',
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
apply plugin: 'elasticsearch.java'
10+
11+
description = 'Fixture for emulating the Security Token Service (STS) running in AWS'
12+
13+
dependencies {
14+
api project(':server')
15+
api("junit:junit:${versions.junit}") {
16+
transitive = false
17+
}
18+
api project(':test:framework')
19+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
package fixture.aws.sts;
10+
11+
import com.sun.net.httpserver.HttpHandler;
12+
import com.sun.net.httpserver.HttpServer;
13+
14+
import org.junit.rules.ExternalResource;
15+
16+
import java.net.InetAddress;
17+
import java.net.InetSocketAddress;
18+
import java.net.UnknownHostException;
19+
import java.util.Objects;
20+
import java.util.function.BiConsumer;
21+
22+
public class AwsStsHttpFixture extends ExternalResource {
23+
24+
private HttpServer server;
25+
26+
private final BiConsumer<String, String> newCredentialsConsumer;
27+
private final String webIdentityToken;
28+
29+
public AwsStsHttpFixture(BiConsumer<String, String> newCredentialsConsumer, String webIdentityToken) {
30+
this.newCredentialsConsumer = Objects.requireNonNull(newCredentialsConsumer);
31+
this.webIdentityToken = Objects.requireNonNull(webIdentityToken);
32+
}
33+
34+
protected HttpHandler createHandler() {
35+
return new AwsStsHttpHandler(newCredentialsConsumer, webIdentityToken);
36+
}
37+
38+
public String getAddress() {
39+
return "http://" + server.getAddress().getHostString() + ":" + server.getAddress().getPort();
40+
}
41+
42+
public void stop(int delay) {
43+
server.stop(delay);
44+
}
45+
46+
protected void before() throws Throwable {
47+
server = HttpServer.create(resolveAddress(), 0);
48+
server.createContext("/", Objects.requireNonNull(createHandler()));
49+
server.start();
50+
}
51+
52+
@Override
53+
protected void after() {
54+
stop(0);
55+
}
56+
57+
private static InetSocketAddress resolveAddress() {
58+
try {
59+
return new InetSocketAddress(InetAddress.getByName("localhost"), 0);
60+
} catch (UnknownHostException e) {
61+
throw new RuntimeException(e);
62+
}
63+
}
64+
}

0 commit comments

Comments
 (0)