Skip to content

Commit 1662cd4

Browse files
Add Region and Signer Algorithm Overrides to S3 Repos (#52112) (#52562)
Exposes S3 SDK signing region and algorithm override settings as requested in #51861. Closes #51861
1 parent 0a09e15 commit 1662cd4

File tree

6 files changed

+135
-14
lines changed

6 files changed

+135
-14
lines changed

docs/plugins/repository-s3.asciidoc

+16
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,22 @@ pattern then you should set this setting to `true` when upgrading.
184184
https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3Builder.html#disableChunkedEncoding--[AWS
185185
Java SDK documentation] for details. Defaults to `false`.
186186

187+
`region`::
188+
189+
Allows specifying the signing region to use. Specificing this setting manually should not be necessary for most use cases. Generally,
190+
the SDK will correctly guess the signing region to use. It should be considered an expert level setting to support S3-compatible APIs
191+
that require https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html[v4 signatures] and use a region other than the
192+
default `us-east-1`. Defaults to empty string which means that the SDK will try to automatically determine the correct signing region.
193+
194+
`signer_override`::
195+
196+
Allows specifying the name of the signature algorithm to use for signing requests by the S3 client. Specifying this setting should not
197+
be necessary for most use cases. It should be considered an expert level setting to support S3-compatible APIs that do not support the
198+
signing algorithm that the SDK automatically determines for them.
199+
See the
200+
https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/ClientConfiguration.html#setSignerOverride-java.lang.String-[AWS
201+
Java SDK documentation] for details. Defaults to empty string which means that no signing algorithm override will be used.
202+
187203
[float]
188204
[[repository-s3-compatible-services]]
189205
===== S3-compatible services

plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3ClientSettings.java

+32-6
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.util.Map;
3636
import java.util.Objects;
3737
import java.util.Set;
38+
import java.util.function.Function;
3839

3940
/**
4041
* A container for settings used to create an S3 client.
@@ -103,6 +104,14 @@ final class S3ClientSettings {
103104
static final Setting.AffixSetting<Boolean> DISABLE_CHUNKED_ENCODING = Setting.affixKeySetting(PREFIX, "disable_chunked_encoding",
104105
key -> Setting.boolSetting(key, false, Property.NodeScope));
105106

107+
/** An override for the s3 region to use for signing requests. */
108+
static final Setting.AffixSetting<String> REGION = Setting.affixKeySetting(PREFIX, "region",
109+
key -> new Setting<>(key, "", Function.identity(), Property.NodeScope));
110+
111+
/** An override for the signer to use. */
112+
static final Setting.AffixSetting<String> SIGNER_OVERRIDE = Setting.affixKeySetting(PREFIX, "signer_override",
113+
key -> new Setting<>(key, "", Function.identity(), Property.NodeScope));
114+
106115
/** Credentials to authenticate with s3. */
107116
final S3BasicCredentials credentials;
108117

@@ -141,10 +150,16 @@ final class S3ClientSettings {
141150
/** Whether chunked encoding should be disabled or not. */
142151
final boolean disableChunkedEncoding;
143152

153+
/** Region to use for signing requests or empty string to use default. */
154+
final String region;
155+
156+
/** Signer override to use or empty string to use default. */
157+
final String signerOverride;
158+
144159
private S3ClientSettings(S3BasicCredentials credentials, String endpoint, Protocol protocol,
145160
String proxyHost, int proxyPort, String proxyUsername, String proxyPassword,
146161
int readTimeoutMillis, int maxRetries, boolean throttleRetries,
147-
boolean pathStyleAccess, boolean disableChunkedEncoding) {
162+
boolean pathStyleAccess, boolean disableChunkedEncoding, String region, String signerOverride) {
148163
this.credentials = credentials;
149164
this.endpoint = endpoint;
150165
this.protocol = protocol;
@@ -157,6 +172,8 @@ private S3ClientSettings(S3BasicCredentials credentials, String endpoint, Protoc
157172
this.throttleRetries = throttleRetries;
158173
this.pathStyleAccess = pathStyleAccess;
159174
this.disableChunkedEncoding = disableChunkedEncoding;
175+
this.region = region;
176+
this.signerOverride = signerOverride;
160177
}
161178

162179
/**
@@ -188,10 +205,13 @@ S3ClientSettings refine(RepositoryMetaData metadata) {
188205
} else {
189206
newCredentials = credentials;
190207
}
208+
final String newRegion = getRepoSettingOrDefault(REGION, normalizedSettings, region);
209+
final String newSignerOverride = getRepoSettingOrDefault(SIGNER_OVERRIDE, normalizedSettings, signerOverride);
191210
if (Objects.equals(endpoint, newEndpoint) && protocol == newProtocol && Objects.equals(proxyHost, newProxyHost)
192211
&& proxyPort == newProxyPort && newReadTimeoutMillis == readTimeoutMillis && maxRetries == newMaxRetries
193212
&& newThrottleRetries == throttleRetries && Objects.equals(credentials, newCredentials)
194-
&& newDisableChunkedEncoding == disableChunkedEncoding) {
213+
&& newDisableChunkedEncoding == disableChunkedEncoding
214+
&& Objects.equals(region, newRegion) && Objects.equals(signerOverride, newSignerOverride)) {
195215
return this;
196216
}
197217
return new S3ClientSettings(
@@ -206,7 +226,9 @@ S3ClientSettings refine(RepositoryMetaData metadata) {
206226
newMaxRetries,
207227
newThrottleRetries,
208228
usePathStyleAccess,
209-
newDisableChunkedEncoding
229+
newDisableChunkedEncoding,
230+
newRegion,
231+
newSignerOverride
210232
);
211233
}
212234

@@ -295,7 +317,9 @@ static S3ClientSettings getClientSettings(final Settings settings, final String
295317
getConfigValue(settings, clientName, MAX_RETRIES_SETTING),
296318
getConfigValue(settings, clientName, USE_THROTTLE_RETRIES_SETTING),
297319
getConfigValue(settings, clientName, USE_PATH_STYLE_ACCESS),
298-
getConfigValue(settings, clientName, DISABLE_CHUNKED_ENCODING)
320+
getConfigValue(settings, clientName, DISABLE_CHUNKED_ENCODING),
321+
getConfigValue(settings, clientName, REGION),
322+
getConfigValue(settings, clientName, SIGNER_OVERRIDE)
299323
);
300324
}
301325
}
@@ -319,13 +343,15 @@ public boolean equals(final Object o) {
319343
Objects.equals(proxyHost, that.proxyHost) &&
320344
Objects.equals(proxyUsername, that.proxyUsername) &&
321345
Objects.equals(proxyPassword, that.proxyPassword) &&
322-
Objects.equals(disableChunkedEncoding, that.disableChunkedEncoding);
346+
Objects.equals(disableChunkedEncoding, that.disableChunkedEncoding) &&
347+
Objects.equals(region, that.region) &&
348+
Objects.equals(signerOverride, that.signerOverride);
323349
}
324350

325351
@Override
326352
public int hashCode() {
327353
return Objects.hash(credentials, endpoint, protocol, proxyHost, proxyPort, proxyUsername, proxyPassword,
328-
readTimeoutMillis, maxRetries, throttleRetries, disableChunkedEncoding);
354+
readTimeoutMillis, maxRetries, throttleRetries, disableChunkedEncoding, region, signerOverride);
329355
}
330356

331357
private static <T> T getConfigValue(Settings settings, String clientName,

plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3RepositoryPlugin.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,9 @@ public List<Setting<?>> getSettings() {
107107
S3ClientSettings.USE_THROTTLE_RETRIES_SETTING,
108108
S3ClientSettings.USE_PATH_STYLE_ACCESS,
109109
S3Repository.ACCESS_KEY_SETTING,
110-
S3Repository.SECRET_KEY_SETTING);
110+
S3Repository.SECRET_KEY_SETTING,
111+
S3ClientSettings.SIGNER_OVERRIDE,
112+
S3ClientSettings.REGION);
111113
}
112114

113115
@Override

plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3Service.java

+7-3
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ AmazonS3 buildClient(final S3ClientSettings clientSettings) {
141141
builder.withClientConfiguration(buildConfiguration(clientSettings));
142142

143143
final String endpoint = Strings.hasLength(clientSettings.endpoint) ? clientSettings.endpoint : Constants.S3_HOSTNAME;
144-
logger.debug("using endpoint [{}]", endpoint);
144+
final String region = Strings.hasLength(clientSettings.region) ? clientSettings.region : null;
145+
logger.debug("using endpoint [{}] and region [{}]", endpoint, region);
145146

146147
// If the endpoint configuration isn't set on the builder then the default behaviour is to try
147148
// and work out what region we are in and use an appropriate endpoint - see AwsClientBuilder#setRegion.
@@ -151,7 +152,7 @@ AmazonS3 buildClient(final S3ClientSettings clientSettings) {
151152
//
152153
// We do this because directly constructing the client is deprecated (was already deprecated in 1.1.223 too)
153154
// so this change removes that usage of a deprecated API.
154-
builder.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endpoint, null));
155+
builder.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endpoint, region));
155156
if (clientSettings.pathStyleAccess) {
156157
builder.enablePathStyleAccess();
157158
}
@@ -177,6 +178,10 @@ static ClientConfiguration buildConfiguration(S3ClientSettings clientSettings) {
177178
clientConfiguration.setProxyPassword(clientSettings.proxyPassword);
178179
}
179180

181+
if (Strings.hasLength(clientSettings.signerOverride)) {
182+
clientConfiguration.setSignerOverride(clientSettings.signerOverride);
183+
}
184+
180185
clientConfiguration.setMaxErrorRetry(clientSettings.maxRetries);
181186
clientConfiguration.setUseThrottleRetries(clientSettings.throttleRetries);
182187
clientConfiguration.setSocketTimeout(clientSettings.readTimeoutMillis);
@@ -231,5 +236,4 @@ public void refresh() {
231236
public void close() {
232237
releaseCachedClients();
233238
}
234-
235239
}

plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3BlobStoreRepositoryTests.java

+52-4
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.elasticsearch.snapshots.SnapshotId;
4747
import org.elasticsearch.snapshots.SnapshotsService;
4848
import org.elasticsearch.snapshots.mockstore.BlobStoreWrapper;
49+
import org.elasticsearch.test.ESIntegTestCase;
4950
import org.elasticsearch.threadpool.ThreadPool;
5051

5152
import java.io.IOException;
@@ -56,14 +57,34 @@
5657
import java.util.List;
5758
import java.util.Map;
5859

60+
import static org.hamcrest.Matchers.containsString;
5961
import static org.hamcrest.Matchers.greaterThan;
6062
import static org.hamcrest.Matchers.lessThan;
63+
import static org.hamcrest.Matchers.startsWith;
6164

6265
@SuppressForbidden(reason = "this test uses a HttpServer to emulate an S3 endpoint")
66+
// Need to set up a new cluster for each test because cluster settings use randomized authentication settings
67+
@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST)
6368
public class S3BlobStoreRepositoryTests extends ESMockAPIBasedRepositoryIntegTestCase {
6469

6570
private static final TimeValue TEST_COOLDOWN_PERIOD = TimeValue.timeValueSeconds(5L);
6671

72+
private String region;
73+
private String signerOverride;
74+
75+
@Override
76+
public void setUp() throws Exception {
77+
if (randomBoolean()) {
78+
region = "test-region";
79+
}
80+
if (region != null && randomBoolean()) {
81+
signerOverride = randomFrom("AWS3SignerType", "AWS4SignerType");
82+
} else if (randomBoolean()) {
83+
signerOverride = "AWS3SignerType";
84+
}
85+
super.setUp();
86+
}
87+
6788
@Override
6889
protected String repositoryType() {
6990
return S3Repository.TYPE;
@@ -101,16 +122,23 @@ protected Settings nodeSettings(int nodeOrdinal) {
101122
secureSettings.setString(S3ClientSettings.ACCESS_KEY_SETTING.getConcreteSettingForNamespace("test").getKey(), "access");
102123
secureSettings.setString(S3ClientSettings.SECRET_KEY_SETTING.getConcreteSettingForNamespace("test").getKey(), "secret");
103124

104-
return Settings.builder()
125+
final Settings.Builder builder = Settings.builder()
105126
.put(ThreadPool.ESTIMATED_TIME_INTERVAL_SETTING.getKey(), 0) // We have tests that verify an exact wait time
106127
.put(S3ClientSettings.ENDPOINT_SETTING.getConcreteSettingForNamespace("test").getKey(), httpServerUrl())
107128
// Disable chunked encoding as it simplifies a lot the request parsing on the httpServer side
108129
.put(S3ClientSettings.DISABLE_CHUNKED_ENCODING.getConcreteSettingForNamespace("test").getKey(), true)
109130
// Disable request throttling because some random values in tests might generate too many failures for the S3 client
110131
.put(S3ClientSettings.USE_THROTTLE_RETRIES_SETTING.getConcreteSettingForNamespace("test").getKey(), false)
111132
.put(super.nodeSettings(nodeOrdinal))
112-
.setSecureSettings(secureSettings)
113-
.build();
133+
.setSecureSettings(secureSettings);
134+
135+
if (signerOverride != null) {
136+
builder.put(S3ClientSettings.SIGNER_OVERRIDE.getConcreteSettingForNamespace("test").getKey(), signerOverride);
137+
}
138+
if (region != null) {
139+
builder.put(S3ClientSettings.REGION.getConcreteSettingForNamespace("test").getKey(), region);
140+
}
141+
return builder.build();
114142
}
115143

116144
public void testEnforcedCooldownPeriod() throws IOException {
@@ -192,11 +220,31 @@ void ensureMultiPartUploadSize(long blobSize) {
192220
}
193221

194222
@SuppressForbidden(reason = "this test uses a HttpHandler to emulate an S3 endpoint")
195-
private static class S3BlobStoreHttpHandler extends S3HttpHandler implements BlobStoreHttpHandler {
223+
private class S3BlobStoreHttpHandler extends S3HttpHandler implements BlobStoreHttpHandler {
196224

197225
S3BlobStoreHttpHandler(final String bucket) {
198226
super(bucket);
199227
}
228+
229+
@Override
230+
public void handle(final HttpExchange exchange) throws IOException {
231+
validateAuthHeader(exchange);
232+
super.handle(exchange);
233+
}
234+
235+
private void validateAuthHeader(HttpExchange exchange) {
236+
final String authorizationHeaderV4 = exchange.getRequestHeaders().getFirst("Authorization");
237+
final String authorizationHeaderV3 = exchange.getRequestHeaders().getFirst("X-amzn-authorization");
238+
239+
if ("AWS3SignerType".equals(signerOverride)) {
240+
assertThat(authorizationHeaderV3, startsWith("AWS3"));
241+
} else if ("AWS4SignerType".equals(signerOverride)) {
242+
assertThat(authorizationHeaderV4, containsString("aws4_request"));
243+
}
244+
if (region != null && authorizationHeaderV4 != null) {
245+
assertThat(authorizationHeaderV4, containsString("/" + region + "/s3/"));
246+
}
247+
}
200248
}
201249

202250
/**

plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3ClientSettingsTests.java

+25
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import com.amazonaws.ClientConfiguration;
2323
import com.amazonaws.Protocol;
24+
import com.amazonaws.services.s3.AmazonS3Client;
2425
import org.elasticsearch.cluster.metadata.RepositoryMetaData;
2526
import org.elasticsearch.common.settings.MockSecureSettings;
2627
import org.elasticsearch.common.settings.Settings;
@@ -158,4 +159,28 @@ public void testUseChunkedEncodingCanBeSet() {
158159
assertThat(settings.get("default").disableChunkedEncoding, is(false));
159160
assertThat(settings.get("other").disableChunkedEncoding, is(true));
160161
}
162+
163+
public void testRegionCanBeSet() {
164+
final String region = randomAlphaOfLength(5);
165+
final Map<String, S3ClientSettings> settings = S3ClientSettings.load(
166+
Settings.builder().put("s3.client.other.region", region).build());
167+
assertThat(settings.get("default").region, is(""));
168+
assertThat(settings.get("other").region, is(region));
169+
try (S3Service s3Service = new S3Service()) {
170+
AmazonS3Client other = (AmazonS3Client) s3Service.buildClient(settings.get("other"));
171+
assertThat(other.getSignerRegionOverride(), is(region));
172+
}
173+
}
174+
175+
public void testSignerOverrideCanBeSet() {
176+
final String signerOverride = randomAlphaOfLength(5);
177+
final Map<String, S3ClientSettings> settings = S3ClientSettings.load(
178+
Settings.builder().put("s3.client.other.signer_override", signerOverride).build());
179+
assertThat(settings.get("default").region, is(""));
180+
assertThat(settings.get("other").signerOverride, is(signerOverride));
181+
ClientConfiguration defaultConfiguration = S3Service.buildConfiguration(settings.get("default"));
182+
assertThat(defaultConfiguration.getSignerOverride(), nullValue());
183+
ClientConfiguration configuration = S3Service.buildConfiguration(settings.get("other"));
184+
assertThat(configuration.getSignerOverride(), is(signerOverride));
185+
}
161186
}

0 commit comments

Comments
 (0)