Skip to content

Commit faffaa9

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

File tree

8 files changed

+81
-32
lines changed

8 files changed

+81
-32
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/build.gradle

+3-1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ String azureAccount = System.getenv("azure_storage_account")
7676
String azureKey = System.getenv("azure_storage_key")
7777
String azureContainer = System.getenv("azure_storage_container")
7878
String azureBasePath = System.getenv("azure_storage_base_path")
79+
String azureSasToken = System.getenv("azure_storage_sas_token")
7980

8081
test {
8182
exclude '**/AzureStorageCleanupThirdPartyTests.class'
@@ -85,10 +86,11 @@ task thirdPartyTest(type: Test) {
8586
include '**/AzureStorageCleanupThirdPartyTests.class'
8687
systemProperty 'test.azure.account', azureAccount ? azureAccount : ""
8788
systemProperty 'test.azure.key', azureKey ? azureKey : ""
89+
systemProperty 'test.azure.sas_token', azureSasToken ? azureSasToken : ""
8890
systemProperty 'test.azure.container', azureContainer ? azureContainer : ""
8991
systemProperty 'test.azure.base', azureBasePath ? azureBasePath : ""
9092
}
9193

92-
if (azureAccount || azureKey || azureContainer || azureBasePath) {
94+
if (azureAccount || azureKey || azureContainer || azureBasePath || azureSasToken) {
9395
check.dependsOn(thirdPartyTest)
9496
}

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

+11-2
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@ String azureAccount = System.getenv("azure_storage_account")
2929
String azureKey = System.getenv("azure_storage_key")
3030
String azureContainer = System.getenv("azure_storage_container")
3131
String azureBasePath = System.getenv("azure_storage_base_path")
32+
String azureSasToken = System.getenv("azure_storage_sas_token")
3233

33-
if (!azureAccount && !azureKey && !azureContainer && !azureBasePath) {
34+
if (!azureAccount && !azureKey && !azureContainer && !azureBasePath && !azureSasToken) {
3435
azureAccount = 'azure_integration_test_account'
3536
azureKey = 'YXp1cmVfaW50ZWdyYXRpb25fdGVzdF9rZXk=' // The key is "azure_integration_test_key" encoded using base64
3637
azureContainer = 'container_test'
3738
azureBasePath = 'integration_test'
39+
azureSasToken = ''
3840
useFixture = true
3941
}
4042

@@ -63,7 +65,14 @@ integTest {
6365
testClusters.integTest {
6466
plugin file(project(':plugins:repository-azure').bundlePlugin.archiveFile)
6567
keystore 'azure.client.integration_test.account', azureAccount
66-
keystore 'azure.client.integration_test.key', azureKey
68+
if (azureKey != null && azureKey.isEmpty() == false) {
69+
println "Using access key in external service tests."
70+
keystore 'azure.client.integration_test.key', azureKey
71+
}
72+
if (azureSasToken != null && azureSasToken.isEmpty() == false) {
73+
println "Using SAS token in external service tests."
74+
keystore 'azure.client.integration_test.sas_token', azureSasToken
75+
}
6776

6877
if (useFixture) {
6978
tasks.integTest.dependsOn azureStorageFixture

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

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public List<Setting<?>> getSettings() {
6565
return Arrays.asList(
6666
AzureStorageSettings.ACCOUNT_SETTING,
6767
AzureStorageSettings.KEY_SETTING,
68+
AzureStorageSettings.SAS_TOKEN_SETTING,
6869
AzureStorageSettings.ENDPOINT_SUFFIX_SETTING,
6970
AzureStorageSettings.TIMEOUT_SETTING,
7071
AzureStorageSettings.MAX_RETRIES_SETTING,

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ private static CloudBlobClient buildClient(AzureStorageSettings azureStorageSett
121121
}
122122

123123
private static CloudBlobClient createClient(AzureStorageSettings azureStorageSettings) throws InvalidKeyException, URISyntaxException {
124-
final String connectionString = azureStorageSettings.buildConnectionString();
124+
final String connectionString = azureStorageSettings.getConnectString();
125125
return CloudStorageAccount.parse(connectionString).createCloudBlobClient();
126126
}
127127

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

+37-20
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.settings.SecureSetting;
2627
import org.elasticsearch.common.settings.SecureString;
@@ -53,6 +54,10 @@ final class AzureStorageSettings {
5354
public static final AffixSetting<SecureString> KEY_SETTING = Setting.affixKeySetting(AZURE_CLIENT_PREFIX_KEY, "key",
5455
key -> SecureSetting.secureString(key, null));
5556

57+
/** Azure SAS token */
58+
public static final AffixSetting<SecureString> SAS_TOKEN_SETTING = Setting.affixKeySetting(AZURE_CLIENT_PREFIX_KEY, "sas_token",
59+
key -> SecureSetting.secureString(key, null));
60+
5661
/** max_retries: Number of retries in case of Azure errors. Defaults to 3 (RetryPolicy.DEFAULT_CLIENT_RETRY_COUNT). */
5762
public static final Setting<Integer> MAX_RETRIES_SETTING =
5863
Setting.affixKeySetting(AZURE_CLIENT_PREFIX_KEY, "max_retries",
@@ -82,29 +87,29 @@ final class AzureStorageSettings {
8287
PROXY_HOST_SETTING);
8388

8489
private final String account;
85-
private final String key;
90+
private final String connectString;
8691
private final String endpointSuffix;
8792
private final TimeValue timeout;
8893
private final int maxRetries;
8994
private final Proxy proxy;
9095
private final LocationMode locationMode;
9196

9297
// copy-constructor
93-
private AzureStorageSettings(String account, String key, String endpointSuffix, TimeValue timeout, int maxRetries, Proxy proxy,
94-
LocationMode locationMode) {
98+
private AzureStorageSettings(String account, String connectString, String endpointSuffix, TimeValue timeout, int maxRetries,
99+
Proxy proxy, LocationMode locationMode) {
95100
this.account = account;
96-
this.key = key;
101+
this.connectString = connectString;
97102
this.endpointSuffix = endpointSuffix;
98103
this.timeout = timeout;
99104
this.maxRetries = maxRetries;
100105
this.proxy = proxy;
101106
this.locationMode = locationMode;
102107
}
103108

104-
AzureStorageSettings(String account, String key, String endpointSuffix, TimeValue timeout, int maxRetries,
105-
Proxy.Type proxyType, String proxyHost, Integer proxyPort) {
109+
private AzureStorageSettings(String account, String key, String sasToken, String endpointSuffix, TimeValue timeout, int maxRetries,
110+
Proxy.Type proxyType, String proxyHost, Integer proxyPort) {
106111
this.account = account;
107-
this.key = key;
112+
this.connectString = buildConnectString(account, key, sasToken, endpointSuffix);
108113
this.endpointSuffix = endpointSuffix;
109114
this.timeout = timeout;
110115
this.maxRetries = maxRetries;
@@ -145,13 +150,26 @@ public Proxy getProxy() {
145150
return proxy;
146151
}
147152

148-
public String buildConnectionString() {
153+
public String getConnectString() {
154+
return connectString;
155+
}
156+
157+
private static String buildConnectString(String account, @Nullable String key, @Nullable String sasToken, String endpointSuffix) {
158+
final boolean hasSasToken = Strings.hasText(sasToken);
159+
final boolean hasKey = Strings.hasText(key);
160+
if (hasSasToken == false && hasKey == false) {
161+
throw new SettingsException("Neither a secret key nor a shared access token was set.");
162+
}
163+
if (hasSasToken && hasKey) {
164+
throw new SettingsException("Both a secret as well as a shared access token were set.");
165+
}
149166
final StringBuilder connectionStringBuilder = new StringBuilder();
150-
connectionStringBuilder.append("DefaultEndpointsProtocol=https")
151-
.append(";AccountName=")
152-
.append(account)
153-
.append(";AccountKey=")
154-
.append(key);
167+
connectionStringBuilder.append("DefaultEndpointsProtocol=https").append(";AccountName=").append(account);
168+
if (hasKey) {
169+
connectionStringBuilder.append(";AccountKey=").append(key);
170+
} else {
171+
connectionStringBuilder.append(";SharedAccessSignature=").append(sasToken);
172+
}
155173
if (Strings.hasText(endpointSuffix)) {
156174
connectionStringBuilder.append(";EndpointSuffix=").append(endpointSuffix);
157175
}
@@ -166,7 +184,6 @@ public LocationMode getLocationMode() {
166184
public String toString() {
167185
final StringBuilder sb = new StringBuilder("AzureStorageSettings{");
168186
sb.append("account='").append(account).append('\'');
169-
sb.append(", key='").append(key).append('\'');
170187
sb.append(", timeout=").append(timeout);
171188
sb.append(", endpointSuffix='").append(endpointSuffix).append('\'');
172189
sb.append(", maxRetries=").append(maxRetries);
@@ -201,8 +218,9 @@ public static Map<String, AzureStorageSettings> load(Settings settings) {
201218
/** Parse settings for a single client. */
202219
private static AzureStorageSettings getClientSettings(Settings settings, String clientName) {
203220
try (SecureString account = getConfigValue(settings, clientName, ACCOUNT_SETTING);
204-
SecureString key = getConfigValue(settings, clientName, KEY_SETTING)) {
205-
return new AzureStorageSettings(account.toString(), key.toString(),
221+
SecureString key = getConfigValue(settings, clientName, KEY_SETTING);
222+
SecureString sasToken = getConfigValue(settings, clientName, SAS_TOKEN_SETTING)) {
223+
return new AzureStorageSettings(account.toString(), key.toString(), sasToken.toString(),
206224
getValue(settings, clientName, ENDPOINT_SUFFIX_SETTING),
207225
getValue(settings, clientName, TIMEOUT_SETTING),
208226
getValue(settings, clientName, MAX_RETRIES_SETTING),
@@ -228,10 +246,9 @@ static Map<String, AzureStorageSettings> overrideLocationMode(Map<String, AzureS
228246
LocationMode locationMode) {
229247
final var map = new HashMap<String, AzureStorageSettings>();
230248
for (final Map.Entry<String, AzureStorageSettings> entry : clientsSettings.entrySet()) {
231-
final AzureStorageSettings azureSettings = new AzureStorageSettings(entry.getValue().account, entry.getValue().key,
232-
entry.getValue().endpointSuffix, entry.getValue().timeout, entry.getValue().maxRetries, entry.getValue().proxy,
233-
locationMode);
234-
map.put(entry.getKey(), azureSettings);
249+
map.put(entry.getKey(),
250+
new AzureStorageSettings(entry.getValue().account, entry.getValue().connectString, entry.getValue().endpointSuffix,
251+
entry.getValue().timeout, entry.getValue().maxRetries, entry.getValue().proxy, locationMode));
235252
}
236253
return Map.copyOf(map);
237254
}

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

+12-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package org.elasticsearch.repositories.azure;
2121

2222
import org.elasticsearch.action.support.master.AcknowledgedResponse;
23+
import org.elasticsearch.common.Strings;
2324
import org.elasticsearch.common.settings.MockSecureSettings;
2425
import org.elasticsearch.common.settings.SecureSettings;
2526
import org.elasticsearch.common.settings.Settings;
@@ -42,13 +43,22 @@ protected Collection<Class<? extends Plugin>> getPlugins() {
4243
@Override
4344
protected SecureSettings credentials() {
4445
assertThat(System.getProperty("test.azure.account"), not(blankOrNullString()));
45-
assertThat(System.getProperty("test.azure.key"), not(blankOrNullString()));
46+
final boolean hasSasToken = Strings.hasText(System.getProperty("test.azure.sas_token"));
47+
if (hasSasToken == false) {
48+
assertThat(System.getProperty("test.azure.key"), not(blankOrNullString()));
49+
} else {
50+
assertThat(System.getProperty("test.azure.key"), blankOrNullString());
51+
}
4652
assertThat(System.getProperty("test.azure.container"), not(blankOrNullString()));
4753
assertThat(System.getProperty("test.azure.base"), not(blankOrNullString()));
4854

4955
MockSecureSettings secureSettings = new MockSecureSettings();
5056
secureSettings.setString("azure.client.default.account", System.getProperty("test.azure.account"));
51-
secureSettings.setString("azure.client.default.key", System.getProperty("test.azure.key"));
57+
if (hasSasToken) {
58+
secureSettings.setString("azure.client.default.sas_token", System.getProperty("test.azure.sas_token"));
59+
} else {
60+
secureSettings.setString("azure.client.default.key", System.getProperty("test.azure.key"));
61+
}
5262
return secureSettings;
5363
}
5464

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

+9-3
Original file line numberDiff line numberDiff line change
@@ -158,15 +158,21 @@ public void testReinitClientWrongSettings() throws IOException {
158158
secureSettings2.setString("azure.client.azure1.account", "myaccount1");
159159
// missing key
160160
final Settings settings2 = Settings.builder().setSecureSettings(secureSettings2).build();
161+
final MockSecureSettings secureSettings3 = new MockSecureSettings();
162+
secureSettings3.setString("azure.client.azure1.account", "myaccount3");
163+
secureSettings3.setString("azure.client.azure1.key", encodeKey("mykey33"));
164+
secureSettings3.setString("azure.client.azure1.sas_token", encodeKey("mysasToken33"));
165+
final Settings settings3 = Settings.builder().setSecureSettings(secureSettings3).build();
161166
try (AzureRepositoryPlugin plugin = pluginWithSettingsValidation(settings1)) {
162167
final AzureStorageService azureStorageService = plugin.azureStoreService;
163168
final CloudBlobClient client11 = azureStorageService.client("azure1").v1();
164169
assertThat(client11.getEndpoint().toString(), equalTo("https://myaccount1.blob.core.windows.net"));
165-
plugin.reload(settings2);
170+
final SettingsException e1 = expectThrows(SettingsException.class, () -> plugin.reload(settings2));
171+
assertThat(e1.getMessage(), is("Neither a secret key nor a shared access token was set."));
172+
final SettingsException e2 = expectThrows(SettingsException.class, () -> plugin.reload(settings3));
173+
assertThat(e2.getMessage(), is("Both a secret as well as a shared access token were set."));
166174
// existing client untouched
167175
assertThat(client11.getEndpoint().toString(), equalTo("https://myaccount1.blob.core.windows.net"));
168-
final SettingsException e = expectThrows(SettingsException.class, () -> azureStorageService.client("azure1"));
169-
assertThat(e.getMessage(), is("Invalid azure client settings with name [azure1]"));
170176
}
171177
}
172178

0 commit comments

Comments
 (0)