Skip to content

Commit c257b56

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

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
/**
@@ -182,10 +199,13 @@ S3ClientSettings refine(RepositoryMetaData metadata) {
182199
final boolean usePathStyleAccess = getRepoSettingOrDefault(USE_PATH_STYLE_ACCESS, normalizedSettings, pathStyleAccess);
183200
final boolean newDisableChunkedEncoding = getRepoSettingOrDefault(
184201
DISABLE_CHUNKED_ENCODING, normalizedSettings, disableChunkedEncoding);
202+
final String newRegion = getRepoSettingOrDefault(REGION, normalizedSettings, region);
203+
final String newSignerOverride = getRepoSettingOrDefault(SIGNER_OVERRIDE, normalizedSettings, signerOverride);
185204
if (Objects.equals(endpoint, newEndpoint) && protocol == newProtocol && Objects.equals(proxyHost, newProxyHost)
186205
&& proxyPort == newProxyPort && newReadTimeoutMillis == readTimeoutMillis && maxRetries == newMaxRetries
187206
&& newThrottleRetries == throttleRetries
188-
&& newDisableChunkedEncoding == disableChunkedEncoding) {
207+
&& newDisableChunkedEncoding == disableChunkedEncoding
208+
&& Objects.equals(region, newRegion) && Objects.equals(signerOverride, newSignerOverride)) {
189209
return this;
190210
}
191211
return new S3ClientSettings(
@@ -200,7 +220,9 @@ S3ClientSettings refine(RepositoryMetaData metadata) {
200220
newMaxRetries,
201221
newThrottleRetries,
202222
usePathStyleAccess,
203-
newDisableChunkedEncoding
223+
newDisableChunkedEncoding,
224+
newRegion,
225+
newSignerOverride
204226
);
205227
}
206228

@@ -266,7 +288,9 @@ static S3ClientSettings getClientSettings(final Settings settings, final String
266288
getConfigValue(settings, clientName, MAX_RETRIES_SETTING),
267289
getConfigValue(settings, clientName, USE_THROTTLE_RETRIES_SETTING),
268290
getConfigValue(settings, clientName, USE_PATH_STYLE_ACCESS),
269-
getConfigValue(settings, clientName, DISABLE_CHUNKED_ENCODING)
291+
getConfigValue(settings, clientName, DISABLE_CHUNKED_ENCODING),
292+
getConfigValue(settings, clientName, REGION),
293+
getConfigValue(settings, clientName, SIGNER_OVERRIDE)
270294
);
271295
}
272296
}
@@ -290,13 +314,15 @@ public boolean equals(final Object o) {
290314
Objects.equals(proxyHost, that.proxyHost) &&
291315
Objects.equals(proxyUsername, that.proxyUsername) &&
292316
Objects.equals(proxyPassword, that.proxyPassword) &&
293-
Objects.equals(disableChunkedEncoding, that.disableChunkedEncoding);
317+
Objects.equals(disableChunkedEncoding, that.disableChunkedEncoding) &&
318+
Objects.equals(region, that.region) &&
319+
Objects.equals(signerOverride, that.signerOverride);
294320
}
295321

296322
@Override
297323
public int hashCode() {
298324
return Objects.hash(credentials, endpoint, protocol, proxyHost, proxyPort, proxyUsername, proxyPassword,
299-
readTimeoutMillis, maxRetries, throttleRetries, disableChunkedEncoding);
325+
readTimeoutMillis, maxRetries, throttleRetries, disableChunkedEncoding, region, signerOverride);
300326
}
301327

302328
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
@@ -105,7 +105,9 @@ public List<Setting<?>> getSettings() {
105105
S3ClientSettings.READ_TIMEOUT_SETTING,
106106
S3ClientSettings.MAX_RETRIES_SETTING,
107107
S3ClientSettings.USE_THROTTLE_RETRIES_SETTING,
108-
S3ClientSettings.USE_PATH_STYLE_ACCESS);
108+
S3ClientSettings.USE_PATH_STYLE_ACCESS,
109+
S3ClientSettings.SIGNER_OVERRIDE,
110+
S3ClientSettings.REGION);
109111
}
110112

111113
@Override

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

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

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

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

182+
if (Strings.hasLength(clientSettings.signerOverride)) {
183+
clientConfiguration.setSignerOverride(clientSettings.signerOverride);
184+
}
185+
181186
clientConfiguration.setMaxErrorRetry(clientSettings.maxRetries);
182187
clientConfiguration.setUseThrottleRetries(clientSettings.throttleRetries);
183188
clientConfiguration.setSocketTimeout(clientSettings.readTimeoutMillis);
@@ -232,5 +237,4 @@ public void refresh() {
232237
public void close() {
233238
releaseCachedClients();
234239
}
235-
236240
}

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;
@@ -99,16 +120,23 @@ protected Settings nodeSettings(int nodeOrdinal) {
99120
secureSettings.setString(S3ClientSettings.ACCESS_KEY_SETTING.getConcreteSettingForNamespace("test").getKey(), "access");
100121
secureSettings.setString(S3ClientSettings.SECRET_KEY_SETTING.getConcreteSettingForNamespace("test").getKey(), "secret");
101122

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

114142
public void testEnforcedCooldownPeriod() throws IOException {
@@ -190,11 +218,31 @@ void ensureMultiPartUploadSize(long blobSize) {
190218
}
191219

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

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

200248
/**

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)