Skip to content

Commit ec170d2

Browse files
Add SAS Token Authentication Support to Azure Repo Plugin (#43841)
* Add SAS Token Authentication Support to Azure Repo Plugin (#42982) (#43618) * Added setting for SAS token * Added support for the token in tests * Relates #42117
1 parent 83eef2c commit ec170d2

File tree

7 files changed

+69
-38
lines changed

7 files changed

+69
-38
lines changed

docs/plugins/repository-azure.asciidoc

+7-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ bin/elasticsearch-keystore add azure.client.default.account
1919
bin/elasticsearch-keystore add azure.client.default.key
2020
----------------------------------------------------------------
2121

22-
Where `account` is the azure account name and `key` the azure secret key.
22+
Where `account` is the azure account name and `key` the azure secret key. Instead of an azure secret key under `key`, you can alternatively
23+
define a shared access signatures (SAS) token under `sas_token` to use for authentication instead. When using an SAS token instead of an
24+
account key, the SAS token must have read (r), write (w), list (l), and delete (d) permissions for the repository base path and
25+
all its contents. These permissions need to be granted for the blob service (b) and apply to resource types service (s), container (c), and
26+
object (o).
2327
These settings are used by the repository's internal azure client.
2428

2529
Note that you can also define more than one account:
@@ -29,14 +33,14 @@ Note that you can also define more than one account:
2933
bin/elasticsearch-keystore add azure.client.default.account
3034
bin/elasticsearch-keystore add azure.client.default.key
3135
bin/elasticsearch-keystore add azure.client.secondary.account
32-
bin/elasticsearch-keystore add azure.client.secondary.key
36+
bin/elasticsearch-keystore add azure.client.secondary.sas_token
3337
----------------------------------------------------------------
3438

3539
`default` is the default account name which will be used by a repository,
3640
unless you set an explicit one in the
3741
<<repository-azure-repository-settings, repository settings>>.
3842

39-
Both `account` and `key` storage settings are
43+
The `account`, `key`, and `sas_token` storage settings are
4044
{ref}/secure-settings.html#reloadable-secure-settings[reloadable]. After you
4145
reload the settings, the internal azure clients, which are used to transfer the
4246
snapshot, will utilize the latest settings from the keystore.

plugins/repository-azure/qa/microsoft-azure-storage/build.gradle

+11-2
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,14 @@ String azureAccount = System.getenv("azure_storage_account")
3333
String azureKey = System.getenv("azure_storage_key")
3434
String azureContainer = System.getenv("azure_storage_container")
3535
String azureBasePath = System.getenv("azure_storage_base_path")
36+
String azureSasToken = System.getenv("azure_storage_sas_token")
3637

37-
if (!azureAccount && !azureKey && !azureContainer && !azureBasePath) {
38+
if (!azureAccount && !azureKey && !azureContainer && !azureBasePath && !azureSasToken) {
3839
azureAccount = 'azure_integration_test_account'
3940
azureKey = 'YXp1cmVfaW50ZWdyYXRpb25fdGVzdF9rZXk=' // The key is "azure_integration_test_key" encoded using base64
4041
azureContainer = 'container_test'
4142
azureBasePath = 'integration_test'
43+
azureSasToken = ''
4244
useFixture = true
4345
}
4446

@@ -62,8 +64,15 @@ processTestResources {
6264

6365
integTestCluster {
6466
keystoreSetting 'azure.client.integration_test.account', azureAccount
65-
keystoreSetting 'azure.client.integration_test.key', azureKey
6667

68+
if (azureKey != null && azureKey.isEmpty() == false) {
69+
println "Using access key in external service tests."
70+
keystoreSetting 'azure.client.integration_test.key', azureKey
71+
}
72+
if (azureSasToken != null && azureSasToken.isEmpty() == false) {
73+
println "Using SAS token in external service tests."
74+
keystoreSetting 'azure.client.integration_test.sas_token', azureSasToken
75+
}
6776
if (useFixture) {
6877
dependsOn azureStorageFixture
6978
// Use a closure on the string to delay evaluation until tests are executed. The endpoint_suffix is used

plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepositoryPlugin.java

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public List<Setting<?>> getSettings() {
5959
AzureStorageSettings.Storage.STORAGE_ACCOUNTS,
6060
AzureStorageSettings.ACCOUNT_SETTING,
6161
AzureStorageSettings.KEY_SETTING,
62+
AzureStorageSettings.SAS_TOKEN_SETTING,
6263
AzureStorageSettings.ENDPOINT_SUFFIX_SETTING,
6364
AzureStorageSettings.TIMEOUT_SETTING,
6465
AzureStorageSettings.MAX_RETRIES_SETTING,

plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureStorageService.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ protected CloudBlobClient buildClient(AzureStorageSettings azureStorageSettings)
112112
return client;
113113
}
114114

115-
protected CloudBlobClient createClient(AzureStorageSettings azureStorageSettings) throws InvalidKeyException, URISyntaxException {
116-
final String connectionString = azureStorageSettings.buildConnectionString();
115+
private static CloudBlobClient createClient(AzureStorageSettings azureStorageSettings) throws InvalidKeyException, URISyntaxException {
116+
final String connectionString = azureStorageSettings.getConnectString();
117117
return CloudStorageAccount.parse(connectionString).createCloudBlobClient();
118118
}
119119

plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureStorageSettings.java

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

2222
import com.microsoft.azure.storage.LocationMode;
2323
import com.microsoft.azure.storage.RetryPolicy;
24+
import org.elasticsearch.common.Nullable;
2425
import org.elasticsearch.common.Strings;
2526
import org.elasticsearch.common.collect.MapBuilder;
2627
import org.elasticsearch.common.collect.Tuple;
@@ -57,6 +58,10 @@ public final class AzureStorageSettings {
5758
public static final AffixSetting<SecureString> KEY_SETTING = Setting.affixKeySetting(AZURE_CLIENT_PREFIX_KEY, "key",
5859
key -> SecureSetting.secureString(key, null));
5960

61+
/** Azure SAS token */
62+
public static final AffixSetting<SecureString> SAS_TOKEN_SETTING = Setting.affixKeySetting(AZURE_CLIENT_PREFIX_KEY, "sas_token",
63+
key -> SecureSetting.secureString(key, null));
64+
6065
/** max_retries: Number of retries in case of Azure errors. Defaults to 3 (RetryPolicy.DEFAULT_CLIENT_RETRY_COUNT). */
6166
public static final Setting<Integer> MAX_RETRIES_SETTING =
6267
Setting.affixKeySetting(AZURE_CLIENT_PREFIX_KEY, "max_retries",
@@ -118,7 +123,7 @@ public interface Storage {
118123
@Deprecated
119124
private final String name;
120125
private final String account;
121-
private final String key;
126+
private final String connectString;
122127
private final String endpointSuffix;
123128
private final TimeValue timeout;
124129
@Deprecated
@@ -128,11 +133,11 @@ public interface Storage {
128133
private final LocationMode locationMode;
129134

130135
// copy-constructor
131-
private AzureStorageSettings(String name, String account, String key, String endpointSuffix, TimeValue timeout, boolean activeByDefault,
132-
int maxRetries, Proxy proxy, LocationMode locationMode) {
136+
private AzureStorageSettings(String name, String account, String connectString, String endpointSuffix, TimeValue timeout,
137+
boolean activeByDefault, int maxRetries, Proxy proxy, LocationMode locationMode) {
133138
this.name = name;
134139
this.account = account;
135-
this.key = key;
140+
this.connectString = connectString;
136141
this.endpointSuffix = endpointSuffix;
137142
this.timeout = timeout;
138143
this.activeByDefault = activeByDefault;
@@ -145,7 +150,7 @@ private AzureStorageSettings(String name, String account, String key, String end
145150
public AzureStorageSettings(String name, String account, String key, TimeValue timeout, boolean activeByDefault, int maxRetries) {
146151
this.name = name;
147152
this.account = account;
148-
this.key = key;
153+
this.connectString = buildConnectString(account, key, null, null);
149154
this.endpointSuffix = null;
150155
this.timeout = timeout;
151156
this.activeByDefault = activeByDefault;
@@ -154,11 +159,11 @@ public AzureStorageSettings(String name, String account, String key, TimeValue t
154159
this.locationMode = LocationMode.PRIMARY_ONLY;
155160
}
156161

157-
AzureStorageSettings(String account, String key, String endpointSuffix, TimeValue timeout, int maxRetries,
158-
Proxy.Type proxyType, String proxyHost, Integer proxyPort) {
162+
private AzureStorageSettings(String account, String key, String sasToken, String endpointSuffix, TimeValue timeout, int maxRetries,
163+
Proxy.Type proxyType, String proxyHost, Integer proxyPort) {
159164
this.name = null;
160165
this.account = account;
161-
this.key = key;
166+
this.connectString = buildConnectString(account, key, sasToken, endpointSuffix);
162167
this.endpointSuffix = endpointSuffix;
163168
this.timeout = timeout;
164169
this.activeByDefault = false;
@@ -189,10 +194,6 @@ public String getName() {
189194
return name;
190195
}
191196

192-
public String getKey() {
193-
return key;
194-
}
195-
196197
public String getAccount() {
197198
return account;
198199
}
@@ -218,13 +219,26 @@ public Proxy getProxy() {
218219
return proxy;
219220
}
220221

221-
public String buildConnectionString() {
222+
public String getConnectString() {
223+
return connectString;
224+
}
225+
226+
private static String buildConnectString(String account, @Nullable String key, @Nullable String sasToken, String endpointSuffix) {
227+
final boolean hasSasToken = Strings.hasText(sasToken);
228+
final boolean hasKey = Strings.hasText(key);
229+
if (hasSasToken == false && hasKey == false) {
230+
throw new SettingsException("Neither a secret key nor a shared access token was set.");
231+
}
232+
if (hasSasToken && hasKey) {
233+
throw new SettingsException("Both a secret as well as a shared access token were set.");
234+
}
222235
final StringBuilder connectionStringBuilder = new StringBuilder();
223-
connectionStringBuilder.append("DefaultEndpointsProtocol=https")
224-
.append(";AccountName=")
225-
.append(account)
226-
.append(";AccountKey=")
227-
.append(key);
236+
connectionStringBuilder.append("DefaultEndpointsProtocol=https").append(";AccountName=").append(account);
237+
if (hasKey) {
238+
connectionStringBuilder.append(";AccountKey=").append(key);
239+
} else {
240+
connectionStringBuilder.append(";SharedAccessSignature=").append(sasToken);
241+
}
228242
if (Strings.hasText(endpointSuffix)) {
229243
connectionStringBuilder.append(";EndpointSuffix=").append(endpointSuffix);
230244
}
@@ -239,7 +253,6 @@ public LocationMode getLocationMode() {
239253
public String toString() {
240254
final StringBuilder sb = new StringBuilder("AzureStorageSettings{");
241255
sb.append("account='").append(account).append('\'');
242-
sb.append(", key='").append(key).append('\'');
243256
sb.append(", activeByDefault='").append(activeByDefault).append('\'');
244257
sb.append(", timeout=").append(timeout);
245258
sb.append(", endpointSuffix='").append(endpointSuffix).append('\'');
@@ -309,8 +322,9 @@ static Map<String, AzureStorageSettings> loadRegular(Settings settings) {
309322
/** Parse settings for a single client. */
310323
static AzureStorageSettings getClientSettings(Settings settings, String clientName) {
311324
try (SecureString account = getConfigValue(settings, clientName, ACCOUNT_SETTING);
312-
SecureString key = getConfigValue(settings, clientName, KEY_SETTING)) {
313-
return new AzureStorageSettings(account.toString(), key.toString(),
325+
SecureString key = getConfigValue(settings, clientName, KEY_SETTING);
326+
SecureString sasToken = getConfigValue(settings, clientName, SAS_TOKEN_SETTING)) {
327+
return new AzureStorageSettings(account.toString(), key.toString(), sasToken.toString(),
314328
getValue(settings, clientName, ENDPOINT_SUFFIX_SETTING),
315329
getValue(settings, clientName, TIMEOUT_SETTING),
316330
getValue(settings, clientName, MAX_RETRIES_SETTING),
@@ -358,8 +372,8 @@ private static AzureStorageSettings getPrimary(List<AzureStorageSettings> settin
358372
} else if (settings.size() == 1) {
359373
// the only storage settings belong (implicitly) to the default primary storage
360374
AzureStorageSettings storage = settings.get(0);
361-
return new AzureStorageSettings(storage.getName(), storage.getAccount(), storage.getKey(), storage.getTimeout(), true,
362-
storage.getMaxRetries());
375+
return new AzureStorageSettings(storage.getName(), storage.getAccount(), storage.connectString, null, storage.getTimeout(),
376+
true, storage.getMaxRetries(), null, LocationMode.PRIMARY_ONLY);
363377
} else {
364378
AzureStorageSettings primary = null;
365379
for (AzureStorageSettings setting : settings) {
@@ -398,8 +412,8 @@ public static Map<String, AzureStorageSettings> overrideLocationMode(Map<String,
398412
final MapBuilder<String, AzureStorageSettings> mapBuilder = new MapBuilder<>();
399413
for (final Map.Entry<String, AzureStorageSettings> entry : clientsSettings.entrySet()) {
400414
final AzureStorageSettings azureSettings = new AzureStorageSettings(entry.getValue().name, entry.getValue().account,
401-
entry.getValue().key, entry.getValue().endpointSuffix, entry.getValue().timeout, entry.getValue().activeByDefault,
402-
entry.getValue().maxRetries, entry.getValue().proxy, locationMode);
415+
entry.getValue().connectString, entry.getValue().endpointSuffix, entry.getValue().timeout,
416+
entry.getValue().activeByDefault, entry.getValue().maxRetries, entry.getValue().proxy, locationMode);
403417
mapBuilder.put(entry.getKey(), azureSettings);
404418
}
405419
return mapBuilder.immutableMap();

plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureSettingsParserTests.java

-3
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,9 @@ public void testParseTwoSettingsExplicitDefault() {
4949
Tuple<AzureStorageSettings, Map<String, AzureStorageSettings>> tuple = AzureStorageSettings.loadLegacy(settings);
5050
assertThat(tuple.v1(), notNullValue());
5151
assertThat(tuple.v1().getAccount(), is("myaccount1"));
52-
assertThat(tuple.v1().getKey(), is("mykey1"));
5352
assertThat(tuple.v2().keySet(), hasSize(1));
5453
assertThat(tuple.v2().get("azure2"), notNullValue());
5554
assertThat(tuple.v2().get("azure2").getAccount(), is("myaccount2"));
56-
assertThat(tuple.v2().get("azure2").getKey(), is("mykey2"));
5755
assertSettingDeprecationsAndWarnings(new Setting<?>[]{
5856
getConcreteSetting(DEPRECATED_ACCOUNT_SETTING, "azure1"),
5957
getConcreteSetting(DEPRECATED_KEY_SETTING, "azure1"),
@@ -72,7 +70,6 @@ public void testParseUniqueSettings() {
7270
Tuple<AzureStorageSettings, Map<String, AzureStorageSettings>> tuple = AzureStorageSettings.loadLegacy(settings);
7371
assertThat(tuple.v1(), notNullValue());
7472
assertThat(tuple.v1().getAccount(), is("myaccount1"));
75-
assertThat(tuple.v1().getKey(), is("mykey1"));
7673
assertThat(tuple.v2().keySet(), hasSize(0));
7774
assertSettingDeprecationsAndWarnings(new Setting<?>[]{
7875
getConcreteSetting(DEPRECATED_ACCOUNT_SETTING, "azure1"),

plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureStorageServiceTests.java

+9-3
Original file line numberDiff line numberDiff line change
@@ -176,15 +176,21 @@ public void testReinitClientWrongSettings() throws IOException {
176176
secureSettings2.setString("azure.client.azure1.account", "myaccount1");
177177
// missing key
178178
final Settings settings2 = Settings.builder().setSecureSettings(secureSettings2).build();
179+
final MockSecureSettings secureSettings3 = new MockSecureSettings();
180+
secureSettings3.setString("azure.client.azure1.account", "myaccount3");
181+
secureSettings3.setString("azure.client.azure1.key", encodeKey("mykey33"));
182+
secureSettings3.setString("azure.client.azure1.sas_token", encodeKey("mysasToken33"));
183+
final Settings settings3 = Settings.builder().setSecureSettings(secureSettings3).build();
179184
try (AzureRepositoryPlugin plugin = pluginWithSettingsValidation(settings1)) {
180185
final AzureStorageService azureStorageService = plugin.azureStoreService;
181186
final CloudBlobClient client11 = azureStorageService.client("azure1").v1();
182187
assertThat(client11.getEndpoint().toString(), equalTo("https://myaccount1.blob.core.windows.net"));
183-
plugin.reload(settings2);
188+
final SettingsException e1 = expectThrows(SettingsException.class, () -> plugin.reload(settings2));
189+
assertThat(e1.getMessage(), is("Neither a secret key nor a shared access token was set."));
190+
final SettingsException e2 = expectThrows(SettingsException.class, () -> plugin.reload(settings3));
191+
assertThat(e2.getMessage(), is("Both a secret as well as a shared access token were set."));
184192
// existing client untouched
185193
assertThat(client11.getEndpoint().toString(), equalTo("https://myaccount1.blob.core.windows.net"));
186-
final SettingsException e = expectThrows(SettingsException.class, () -> azureStorageService.client("azure1"));
187-
assertThat(e.getMessage(), is("Invalid azure client settings with name [azure1]"));
188194
}
189195
}
190196

0 commit comments

Comments
 (0)