Skip to content

Commit

Permalink
Introduce aws-fixture-utils (elastic#119319)
Browse files Browse the repository at this point in the history
Extracts some common utils for creating AWS service test fixtures out of
the `s3-fixture` module and into a separate library independent of S3.
  • Loading branch information
DaveCTurner authored Dec 27, 2024
1 parent f333a79 commit a4d4762
Show file tree
Hide file tree
Showing 19 changed files with 197 additions and 107 deletions.
2 changes: 2 additions & 0 deletions modules/repository-s3/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,14 @@ dependencies {
internalClusterTestRuntimeOnly "org.slf4j:slf4j-simple:${versions.slf4j}"

yamlRestTestImplementation project(':modules:repository-s3')
yamlRestTestImplementation project(':test:fixtures:aws-fixture-utils')
yamlRestTestImplementation project(':test:fixtures:s3-fixture')
yamlRestTestImplementation project(':test:fixtures:testcontainer-utils')
yamlRestTestImplementation project(':test:framework')
yamlRestTestRuntimeOnly "org.slf4j:slf4j-simple:${versions.slf4j}"

javaRestTestImplementation project(':modules:repository-s3')
javaRestTestImplementation project(':test:fixtures:aws-fixture-utils')
javaRestTestImplementation project(':test:fixtures:aws-sts-fixture')
javaRestTestImplementation project(':test:fixtures:ec2-imds-fixture')
javaRestTestImplementation project(':test:fixtures:minio-fixture')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;

import static fixture.aws.AwsCredentialsUtils.fixedAccessKey;

@ThreadLeakFilters(filters = { TestContainersThreadFilter.class })
@ThreadLeakScope(ThreadLeakScope.Scope.NONE) // https://github.com/elastic/elasticsearch/issues/102482
public class RepositoryS3BasicCredentialsRestIT extends AbstractRepositoryS3RestTestCase {
Expand All @@ -31,7 +33,7 @@ public class RepositoryS3BasicCredentialsRestIT extends AbstractRepositoryS3Rest
private static final String SECRET_KEY = PREFIX + "secret-key";
private static final String CLIENT = "basic_credentials_client";

private static final S3HttpFixture s3Fixture = new S3HttpFixture(true, BUCKET, BASE_PATH, S3HttpFixture.fixedAccessKey(ACCESS_KEY));
private static final S3HttpFixture s3Fixture = new S3HttpFixture(true, BUCKET, BASE_PATH, fixedAccessKey(ACCESS_KEY));

public static ElasticsearchCluster cluster = ElasticsearchCluster.local()
.module("repository-s3")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@

package org.elasticsearch.repositories.s3;

import fixture.aws.DynamicAwsCredentials;
import fixture.aws.imds.Ec2ImdsHttpFixture;
import fixture.aws.imds.Ec2ImdsServiceBuilder;
import fixture.aws.imds.Ec2ImdsVersion;
import fixture.s3.DynamicS3Credentials;
import fixture.s3.S3HttpFixture;

import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters;
Expand All @@ -35,14 +35,14 @@ public class RepositoryS3EcsCredentialsRestIT extends AbstractRepositoryS3RestTe
private static final String BASE_PATH = PREFIX + "base_path";
private static final String CLIENT = "ecs_credentials_client";

private static final DynamicS3Credentials dynamicS3Credentials = new DynamicS3Credentials();
private static final DynamicAwsCredentials dynamicCredentials = new DynamicAwsCredentials();

private static final Ec2ImdsHttpFixture ec2ImdsHttpFixture = new Ec2ImdsHttpFixture(
new Ec2ImdsServiceBuilder(Ec2ImdsVersion.V1).newCredentialsConsumer(dynamicS3Credentials::addValidCredentials)
new Ec2ImdsServiceBuilder(Ec2ImdsVersion.V1).newCredentialsConsumer(dynamicCredentials::addValidCredentials)
.alternativeCredentialsEndpoints(Set.of("/ecs_credentials_endpoint"))
);

private static final S3HttpFixture s3Fixture = new S3HttpFixture(true, BUCKET, BASE_PATH, dynamicS3Credentials::isAuthorized);
private static final S3HttpFixture s3Fixture = new S3HttpFixture(true, BUCKET, BASE_PATH, dynamicCredentials::isAuthorized);

public static ElasticsearchCluster cluster = ElasticsearchCluster.local()
.module("repository-s3")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@

package org.elasticsearch.repositories.s3;

import fixture.aws.DynamicAwsCredentials;
import fixture.aws.imds.Ec2ImdsHttpFixture;
import fixture.aws.imds.Ec2ImdsServiceBuilder;
import fixture.aws.imds.Ec2ImdsVersion;
import fixture.s3.DynamicS3Credentials;
import fixture.s3.S3HttpFixture;

import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters;
Expand All @@ -33,13 +33,13 @@ public class RepositoryS3ImdsV1CredentialsRestIT extends AbstractRepositoryS3Res
private static final String BASE_PATH = PREFIX + "base_path";
private static final String CLIENT = "imdsv1_credentials_client";

private static final DynamicS3Credentials dynamicS3Credentials = new DynamicS3Credentials();
private static final DynamicAwsCredentials dynamicCredentials = new DynamicAwsCredentials();

private static final Ec2ImdsHttpFixture ec2ImdsHttpFixture = new Ec2ImdsHttpFixture(
new Ec2ImdsServiceBuilder(Ec2ImdsVersion.V1).newCredentialsConsumer(dynamicS3Credentials::addValidCredentials)
new Ec2ImdsServiceBuilder(Ec2ImdsVersion.V1).newCredentialsConsumer(dynamicCredentials::addValidCredentials)
);

private static final S3HttpFixture s3Fixture = new S3HttpFixture(true, BUCKET, BASE_PATH, dynamicS3Credentials::isAuthorized);
private static final S3HttpFixture s3Fixture = new S3HttpFixture(true, BUCKET, BASE_PATH, dynamicCredentials::isAuthorized);

public static ElasticsearchCluster cluster = ElasticsearchCluster.local()
.module("repository-s3")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@

package org.elasticsearch.repositories.s3;

import fixture.aws.DynamicAwsCredentials;
import fixture.aws.imds.Ec2ImdsHttpFixture;
import fixture.aws.imds.Ec2ImdsServiceBuilder;
import fixture.aws.imds.Ec2ImdsVersion;
import fixture.s3.DynamicS3Credentials;
import fixture.s3.S3HttpFixture;

import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters;
Expand All @@ -33,13 +33,13 @@ public class RepositoryS3ImdsV2CredentialsRestIT extends AbstractRepositoryS3Res
private static final String BASE_PATH = PREFIX + "base_path";
private static final String CLIENT = "imdsv2_credentials_client";

private static final DynamicS3Credentials dynamicS3Credentials = new DynamicS3Credentials();
private static final DynamicAwsCredentials dynamicCredentials = new DynamicAwsCredentials();

private static final Ec2ImdsHttpFixture ec2ImdsHttpFixture = new Ec2ImdsHttpFixture(
new Ec2ImdsServiceBuilder(Ec2ImdsVersion.V2).newCredentialsConsumer(dynamicS3Credentials::addValidCredentials)
new Ec2ImdsServiceBuilder(Ec2ImdsVersion.V2).newCredentialsConsumer(dynamicCredentials::addValidCredentials)
);

private static final S3HttpFixture s3Fixture = new S3HttpFixture(true, BUCKET, BASE_PATH, dynamicS3Credentials::isAuthorized);
private static final S3HttpFixture s3Fixture = new S3HttpFixture(true, BUCKET, BASE_PATH, dynamicCredentials::isAuthorized);

public static ElasticsearchCluster cluster = ElasticsearchCluster.local()
.module("repository-s3")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

import java.io.IOException;

import static fixture.aws.AwsCredentialsUtils.mutableAccessKey;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.equalTo;
Expand All @@ -38,12 +39,7 @@ public class RepositoryS3RestReloadCredentialsIT extends ESRestTestCase {

private static volatile String repositoryAccessKey;

public static final S3HttpFixture s3Fixture = new S3HttpFixture(
true,
BUCKET,
BASE_PATH,
S3HttpFixture.mutableAccessKey(() -> repositoryAccessKey)
);
public static final S3HttpFixture s3Fixture = new S3HttpFixture(true, BUCKET, BASE_PATH, mutableAccessKey(() -> repositoryAccessKey));

private static final MutableSettingsProvider keystoreSettings = new MutableSettingsProvider();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;

import static fixture.aws.AwsCredentialsUtils.fixedAccessKeyAndToken;

@ThreadLeakFilters(filters = { TestContainersThreadFilter.class })
@ThreadLeakScope(ThreadLeakScope.Scope.NONE) // https://github.com/elastic/elasticsearch/issues/102482
public class RepositoryS3SessionCredentialsRestIT extends AbstractRepositoryS3RestTestCase {
Expand All @@ -36,7 +38,7 @@ public class RepositoryS3SessionCredentialsRestIT extends AbstractRepositoryS3Re
true,
BUCKET,
BASE_PATH,
S3HttpFixture.fixedAccessKeyAndToken(ACCESS_KEY, SESSION_TOKEN)
fixedAccessKeyAndToken(ACCESS_KEY, SESSION_TOKEN)
);

public static ElasticsearchCluster cluster = ElasticsearchCluster.local()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@

package org.elasticsearch.repositories.s3;

import fixture.aws.DynamicAwsCredentials;
import fixture.aws.sts.AwsStsHttpFixture;
import fixture.s3.DynamicS3Credentials;
import fixture.s3.S3HttpFixture;

import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters;
Expand All @@ -32,17 +32,17 @@ public class RepositoryS3StsCredentialsRestIT extends AbstractRepositoryS3RestTe
private static final String BASE_PATH = PREFIX + "base_path";
private static final String CLIENT = "sts_credentials_client";

private static final DynamicS3Credentials dynamicS3Credentials = new DynamicS3Credentials();
private static final DynamicAwsCredentials dynamicCredentials = new DynamicAwsCredentials();

private static final S3HttpFixture s3HttpFixture = new S3HttpFixture(true, BUCKET, BASE_PATH, dynamicS3Credentials::isAuthorized);
private static final S3HttpFixture s3HttpFixture = new S3HttpFixture(true, BUCKET, BASE_PATH, dynamicCredentials::isAuthorized);

private static final String WEB_IDENTITY_TOKEN_FILE_CONTENTS = """
Atza|IQEBLjAsAhRFiXuWpUXuRvQ9PZL3GMFcYevydwIUFAHZwXZXXXXXXXXJnrulxKDHwy87oGKPznh0D6bEQZTSCzyoCtL_8S07pLpr0zMbn6w1lfVZKNTBdDans\
FBmtGnIsIapjI6xKR02Yc_2bQ8LZbUXSGm6Ry6_BG7PrtLZtj_dfCTj92xNGed-CrKqjG7nPBjNIL016GGvuS5gSvPRUxWES3VYfm1wl7WTI7jn-Pcb6M-buCgHhFO\
zTQxod27L9CqnOLio7N3gZAGpsp6n1-AJBOCJckcyXe2c6uD0srOJeZlKUm2eTDVMf8IehDVI0r1QOnTV6KzzAI3OY87Vd_cVMQ""";

private static final AwsStsHttpFixture stsHttpFixture = new AwsStsHttpFixture(
dynamicS3Credentials::addValidCredentials,
dynamicCredentials::addValidCredentials,
WEB_IDENTITY_TOKEN_FILE_CONTENTS
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;

import static fixture.aws.AwsCredentialsUtils.fixedAccessKey;

@ThreadLeakFilters(filters = { TestContainersThreadFilter.class })
@ThreadLeakScope(ThreadLeakScope.Scope.NONE) // https://github.com/elastic/elasticsearch/issues/102482
public class RepositoryS3ClientYamlTestSuiteIT extends AbstractRepositoryS3ClientYamlTestSuiteIT {
Expand All @@ -34,7 +36,7 @@ public class RepositoryS3ClientYamlTestSuiteIT extends AbstractRepositoryS3Clien
true,
"bucket",
"base_path_integration_tests",
S3HttpFixture.fixedAccessKey(ACCESS_KEY)
fixedAccessKey(ACCESS_KEY)
);

public static ElasticsearchCluster cluster = ElasticsearchCluster.local()
Expand Down
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ List projects = [
'distribution:tools:ansi-console',
'server',
'test:framework',
'test:fixtures:aws-fixture-utils',
'test:fixtures:aws-sts-fixture',
'test:fixtures:azure-fixture',
'test:fixtures:ec2-imds-fixture',
Expand Down
16 changes: 16 additions & 0 deletions test/fixtures/aws-fixture-utils/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* 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 = 'Utils for AWS-related fixtures'

dependencies {
implementation project(':server')
implementation project(':test:framework')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* 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;

import com.sun.net.httpserver.HttpExchange;

import org.elasticsearch.rest.RestStatus;

import java.io.IOException;
import java.util.Objects;
import java.util.function.BiPredicate;
import java.util.function.Supplier;

import static fixture.aws.AwsFixtureUtils.sendError;

public enum AwsCredentialsUtils {
;

/**
* @return an authorization predicate that ensures the access key matches the given values.
*/
public static BiPredicate<String, String> fixedAccessKey(String accessKey) {
return mutableAccessKey(() -> accessKey);
}

/**
* @return an authorization predicate that ensures the access key matches one supplied by the given supplier.
*/
public static BiPredicate<String, String> mutableAccessKey(Supplier<String> accessKeySupplier) {
return (authorizationHeader, sessionTokenHeader) -> authorizationHeader != null
&& authorizationHeader.contains(accessKeySupplier.get());
}

/**
* @return an authorization predicate that ensures the access key and session token both match the given values.
*/
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);
}

/**
* Check the authorization headers of the given {@param exchange} against the given {@param authorizationPredicate}. If they match,
* returns {@code true}. If they do not match, sends a {@code 403 Forbidden} response and returns {@code false}.
*/
public static boolean checkAuthorization(BiPredicate<String, String> authorizationPredicate, HttpExchange exchange) throws IOException {
if (authorizationPredicate.test(
exchange.getRequestHeaders().getFirst("Authorization"),
exchange.getRequestHeaders().getFirst("x-amz-security-token")
)) {
return true;
}

sendError(exchange, RestStatus.FORBIDDEN, "AccessDenied", "Access denied by " + authorizationPredicate);
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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;

import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;

import org.elasticsearch.rest.RestStatus;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;

public enum AwsFixtureUtils {
;

/**
* @return an {@link InetSocketAddress} for a test fixture running on {@code localhost} which binds to any available port.
*/
public static InetSocketAddress getLocalFixtureAddress() {
try {
return new InetSocketAddress(InetAddress.getByName("localhost"), 0);
} catch (UnknownHostException e) {
throw new RuntimeException(e);
}
}

/**
* Send an XML-formatted error response typical of an AWS service.
*/
public static void sendError(final HttpExchange exchange, final RestStatus status, final String errorCode, final String message)
throws IOException {
final Headers headers = exchange.getResponseHeaders();
headers.add("Content-Type", "application/xml");

final String requestId = exchange.getRequestHeaders().getFirst("x-amz-request-id");
if (requestId != null) {
headers.add("x-amz-request-id", requestId);
}

if (errorCode == null || "HEAD".equals(exchange.getRequestMethod())) {
exchange.sendResponseHeaders(status.getStatus(), -1L);
exchange.close();
} else {
final byte[] response = ("<?xml version=\"1.0\" encoding=\"UTF-8\"?><Error>"
+ "<Code>"
+ errorCode
+ "</Code>"
+ "<Message>"
+ message
+ "</Message>"
+ "<RequestId>"
+ requestId
+ "</RequestId>"
+ "</Error>").getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(status.getStatus(), response.length);
exchange.getResponseBody().write(response);
exchange.close();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

package fixture.s3;
package fixture.aws;

import org.elasticsearch.common.util.concurrent.ConcurrentCollections;

Expand All @@ -18,10 +18,9 @@
/**
* 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.
* fixture uses {@link #isAuthorized} to validate the credentials it receives corresponds with some previously-generated credentials.
*/
public class DynamicS3Credentials {
public class DynamicAwsCredentials {
private final Map<String, Set<String>> validCredentialsMap = ConcurrentCollections.newConcurrentMap();

public boolean isAuthorized(String authorizationHeader, String sessionTokenHeader) {
Expand Down
Loading

0 comments on commit a4d4762

Please sign in to comment.