diff --git a/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureCredentials.java b/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureCredentials.java new file mode 100644 index 0000000000000..c32e984ff0292 --- /dev/null +++ b/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureCredentials.java @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.repositories.azure; + +public interface AzureCredentials { + String buildConnectionString(String endpointSuffix); +} + diff --git a/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureKeyCredentials.java b/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureKeyCredentials.java new file mode 100644 index 0000000000000..ef55df55c572a --- /dev/null +++ b/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureKeyCredentials.java @@ -0,0 +1,69 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.repositories.azure; + +import org.elasticsearch.common.Strings; + +import java.util.Objects; + +public class AzureKeyCredentials implements AzureCredentials { + private final String account; + private final String key; + + public AzureKeyCredentials(String account, String key) { + this.account = account; + this.key = key; + } + + public String getAccount() { + return account; + } + + public String getKey() { + return key; + } + + public String buildConnectionString(String endpointSuffix) { + final StringBuilder connectionStringBuilder = new StringBuilder(); + connectionStringBuilder.append("DefaultEndpointsProtocol=https") + .append(";AccountName=") + .append(this.getAccount()) + .append(";AccountKey=") + .append(this.getKey()); + if (Strings.hasText(endpointSuffix)) { + connectionStringBuilder.append(";EndpointSuffix=").append(endpointSuffix); + } + return connectionStringBuilder.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AzureKeyCredentials that = (AzureKeyCredentials) o; + return Objects.equals(account, that.account) && + Objects.equals(key, that.key); + } + + @Override + public int hashCode() { + return Objects.hash(account, key); + } +} diff --git a/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepository.java b/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepository.java index 7c3520918fc58..8fd62d8f1241f 100644 --- a/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepository.java +++ b/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepository.java @@ -139,14 +139,17 @@ protected ByteSizeValue chunkSize() { @Override public void initializeSnapshot(SnapshotId snapshotId, List indices, MetaData clusterMetadata) { - try { - final AzureBlobStore blobStore = (AzureBlobStore) blobStore(); - if (blobStore.containerExist() == false) { - throw new IllegalArgumentException("The bucket [" + blobStore + "] does not exist. Please create it before " - + " creating an azure snapshot repository backed by it."); + // Skip bucket existence check if sas_token is set since it might not hold the required permissions to list buckets + if(!storageService.storageSettings.containsKey("sas_token")) { + try { + final AzureBlobStore blobStore = (AzureBlobStore) blobStore(); + if (blobStore.containerExist() == false) { + throw new IllegalArgumentException("The bucket [" + blobStore + "] does not exist. Please create it before " + + " creating an azure snapshot repository backed by it."); + } + } catch (URISyntaxException | StorageException e) { + throw new SnapshotCreationException(metadata.name(), snapshotId, e); } - } catch (URISyntaxException | StorageException e) { - throw new SnapshotCreationException(metadata.name(), snapshotId, e); } super.initializeSnapshot(snapshotId, indices, clusterMetadata); } diff --git a/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepositoryPlugin.java b/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepositoryPlugin.java index ab48cf1314ec5..21d041a2d7a30 100644 --- a/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepositoryPlugin.java +++ b/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepositoryPlugin.java @@ -60,6 +60,7 @@ public List> getSettings() { return Arrays.asList( AzureStorageSettings.ACCOUNT_SETTING, AzureStorageSettings.KEY_SETTING, + AzureStorageSettings.SAS_TOKEN_SETTING, AzureStorageSettings.ENDPOINT_SUFFIX_SETTING, AzureStorageSettings.TIMEOUT_SETTING, AzureStorageSettings.MAX_RETRIES_SETTING, diff --git a/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureSasCredentials.java b/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureSasCredentials.java new file mode 100644 index 0000000000000..80f50224f388c --- /dev/null +++ b/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureSasCredentials.java @@ -0,0 +1,73 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.repositories.azure; + +import org.elasticsearch.common.Strings; + +import java.util.Objects; + +public class AzureSasCredentials implements AzureCredentials { + private final String account; + private final String sasToken; + + public AzureSasCredentials(String account, String sasToken) { + this.account = account; + this.sasToken = sasToken; + } + + public String getSasToken() { + return sasToken; + } + + public String getAccount() { + return account; + } + + public String buildConnectionString(String endpointSuffix) { + final StringBuilder connectionStringBuilder = new StringBuilder(); + connectionStringBuilder.append("DefaultEndpointsProtocol=https") + .append(";AccountName=") + .append(this.getAccount()) + .append(";SharedAccessSignature=") + .append(this.getSasToken()); + if (Strings.hasText(endpointSuffix)) { + connectionStringBuilder.append(";EndpointSuffix=").append(endpointSuffix); + } + return connectionStringBuilder.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) return false; + AzureSasCredentials that = (AzureSasCredentials) o; + return Objects.equals(sasToken, that.sasToken); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), sasToken); + } +} diff --git a/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureStorageService.java b/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureStorageService.java index 89a78fd8045ee..0b4063cf201ba 100644 --- a/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureStorageService.java +++ b/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureStorageService.java @@ -117,6 +117,7 @@ private static CloudBlobClient buildClient(AzureStorageSettings azureStorageSett private static CloudBlobClient createClient(AzureStorageSettings azureStorageSettings) throws InvalidKeyException, URISyntaxException { final String connectionString = azureStorageSettings.buildConnectionString(); + return CloudStorageAccount.parse(connectionString).createCloudBlobClient(); } diff --git a/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureStorageSettings.java b/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureStorageSettings.java index e57d855cb0ee5..e83a9e8c12317 100644 --- a/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureStorageSettings.java +++ b/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureStorageSettings.java @@ -53,36 +53,38 @@ final class AzureStorageSettings { public static final AffixSetting KEY_SETTING = Setting.affixKeySetting(AZURE_CLIENT_PREFIX_KEY, "key", key -> SecureSetting.secureString(key, null)); + /** Azure SAS token */ + public static final AffixSetting SAS_TOKEN_SETTING = Setting.affixKeySetting(AZURE_CLIENT_PREFIX_KEY, "sas_token", + key -> SecureSetting.secureString(key, null)); + /** max_retries: Number of retries in case of Azure errors. Defaults to 3 (RetryPolicy.DEFAULT_CLIENT_RETRY_COUNT). */ public static final Setting MAX_RETRIES_SETTING = Setting.affixKeySetting(AZURE_CLIENT_PREFIX_KEY, "max_retries", (key) -> Setting.intSetting(key, RetryPolicy.DEFAULT_CLIENT_RETRY_COUNT, Setting.Property.NodeScope), - ACCOUNT_SETTING, KEY_SETTING); + ACCOUNT_SETTING); /** * Azure endpoint suffix. Default to core.windows.net (CloudStorageAccount.DEFAULT_DNS). */ public static final Setting ENDPOINT_SUFFIX_SETTING = Setting.affixKeySetting(AZURE_CLIENT_PREFIX_KEY, "endpoint_suffix", - key -> Setting.simpleString(key, Property.NodeScope), ACCOUNT_SETTING, KEY_SETTING); + key -> Setting.simpleString(key, Property.NodeScope), ACCOUNT_SETTING); public static final AffixSetting TIMEOUT_SETTING = Setting.affixKeySetting(AZURE_CLIENT_PREFIX_KEY, "timeout", - (key) -> Setting.timeSetting(key, TimeValue.timeValueMinutes(-1), Property.NodeScope), ACCOUNT_SETTING, KEY_SETTING); + (key) -> Setting.timeSetting(key, TimeValue.timeValueMinutes(-1), Property.NodeScope), ACCOUNT_SETTING); /** The type of the proxy to connect to azure through. Can be direct (no proxy, default), http or socks */ public static final AffixSetting PROXY_TYPE_SETTING = Setting.affixKeySetting(AZURE_CLIENT_PREFIX_KEY, "proxy.type", - (key) -> new Setting<>(key, "direct", s -> Proxy.Type.valueOf(s.toUpperCase(Locale.ROOT)), Property.NodeScope) - , ACCOUNT_SETTING, KEY_SETTING); + (key) -> new Setting<>(key, "direct", s -> Proxy.Type.valueOf(s.toUpperCase(Locale.ROOT)), Property.NodeScope), ACCOUNT_SETTING); /** The host name of a proxy to connect to azure through. */ public static final AffixSetting PROXY_HOST_SETTING = Setting.affixKeySetting(AZURE_CLIENT_PREFIX_KEY, "proxy.host", - (key) -> Setting.simpleString(key, Property.NodeScope), KEY_SETTING, ACCOUNT_SETTING, PROXY_TYPE_SETTING); + (key) -> Setting.simpleString(key, Property.NodeScope), ACCOUNT_SETTING, PROXY_TYPE_SETTING); /** The port of a proxy to connect to azure through. */ public static final Setting PROXY_PORT_SETTING = Setting.affixKeySetting(AZURE_CLIENT_PREFIX_KEY, "proxy.port", - (key) -> Setting.intSetting(key, 0, 0, 65535, Setting.Property.NodeScope), ACCOUNT_SETTING, KEY_SETTING, PROXY_TYPE_SETTING, + (key) -> Setting.intSetting(key, 0, 0, 65535, Setting.Property.NodeScope), ACCOUNT_SETTING, PROXY_TYPE_SETTING, PROXY_HOST_SETTING); - private final String account; - private final String key; + private final AzureCredentials credentials; private final String endpointSuffix; private final TimeValue timeout; private final int maxRetries; @@ -90,10 +92,9 @@ final class AzureStorageSettings { private final LocationMode locationMode; // copy-constructor - private AzureStorageSettings(String account, String key, String endpointSuffix, TimeValue timeout, int maxRetries, Proxy proxy, + private AzureStorageSettings(AzureCredentials credentials, String endpointSuffix, TimeValue timeout, int maxRetries, Proxy proxy, LocationMode locationMode) { - this.account = account; - this.key = key; + this.credentials = credentials; this.endpointSuffix = endpointSuffix; this.timeout = timeout; this.maxRetries = maxRetries; @@ -101,10 +102,9 @@ private AzureStorageSettings(String account, String key, String endpointSuffix, this.locationMode = locationMode; } - AzureStorageSettings(String account, String key, String endpointSuffix, TimeValue timeout, int maxRetries, + AzureStorageSettings(AzureCredentials credentials, String endpointSuffix, TimeValue timeout, int maxRetries, Proxy.Type proxyType, String proxyHost, Integer proxyPort) { - this.account = account; - this.key = key; + this.credentials = credentials; this.endpointSuffix = endpointSuffix; this.timeout = timeout; this.maxRetries = maxRetries; @@ -146,16 +146,7 @@ public Proxy getProxy() { } public String buildConnectionString() { - final StringBuilder connectionStringBuilder = new StringBuilder(); - connectionStringBuilder.append("DefaultEndpointsProtocol=https") - .append(";AccountName=") - .append(account) - .append(";AccountKey=") - .append(key); - if (Strings.hasText(endpointSuffix)) { - connectionStringBuilder.append(";EndpointSuffix=").append(endpointSuffix); - } - return connectionStringBuilder.toString(); + return credentials.buildConnectionString(endpointSuffix); } public LocationMode getLocationMode() { @@ -165,8 +156,7 @@ public LocationMode getLocationMode() { @Override public String toString() { final StringBuilder sb = new StringBuilder("AzureStorageSettings{"); - sb.append("account='").append(account).append('\''); - sb.append(", key='").append(key).append('\''); + sb.append("credentials='").append(credentials.toString()).append('\''); sb.append(", timeout=").append(timeout); sb.append(", endpointSuffix='").append(endpointSuffix).append('\''); sb.append(", maxRetries=").append(maxRetries); @@ -200,15 +190,34 @@ public static Map load(Settings settings) { // pkg private for tests /** Parse settings for a single client. */ private static AzureStorageSettings getClientSettings(Settings settings, String clientName) { - try (SecureString account = getConfigValue(settings, clientName, ACCOUNT_SETTING); - SecureString key = getConfigValue(settings, clientName, KEY_SETTING)) { - return new AzureStorageSettings(account.toString(), key.toString(), + return new AzureStorageSettings(loadCredentials(settings, clientName), getValue(settings, clientName, ENDPOINT_SUFFIX_SETTING), getValue(settings, clientName, TIMEOUT_SETTING), getValue(settings, clientName, MAX_RETRIES_SETTING), getValue(settings, clientName, PROXY_TYPE_SETTING), getValue(settings, clientName, PROXY_HOST_SETTING), getValue(settings, clientName, PROXY_PORT_SETTING)); + } + + private static AzureCredentials loadCredentials(Settings settings, String clientName) { + try (SecureString account = getConfigValue(settings, clientName, ACCOUNT_SETTING); + SecureString key = getConfigValue(settings, clientName, KEY_SETTING); + SecureString sasToken = getConfigValue(settings, clientName, SAS_TOKEN_SETTING) + ) { + if(key.length() != 0 && sasToken.length() != 0) { + throw new SettingsException("Both key and sas_token are set for [" + clientName + "] - only one must be specified"); + } + if (account.length() != 0) { + if(key.length() != 0) { + return new AzureKeyCredentials(account.toString(), key.toString()); + } else if(sasToken.length() != 0) { + return new AzureSasCredentials(account.toString(), sasToken.toString()); + } else { + throw new SettingsException("Either key or sas_token need to be defined for azure client [" + clientName + "]"); + } + } else { + throw new SettingsException("Missing account name for azure client [" + clientName + "]"); + } } } @@ -228,7 +237,7 @@ static Map overrideLocationMode(Map(); for (final Map.Entry entry : clientsSettings.entrySet()) { - final AzureStorageSettings azureSettings = new AzureStorageSettings(entry.getValue().account, entry.getValue().key, + final AzureStorageSettings azureSettings = new AzureStorageSettings(entry.getValue().credentials, entry.getValue().endpointSuffix, entry.getValue().timeout, entry.getValue().maxRetries, entry.getValue().proxy, locationMode); map.put(entry.getKey(), azureSettings); diff --git a/plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureStorageServiceTests.java b/plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureStorageServiceTests.java index f7b49bd24adf6..18395c46acc20 100644 --- a/plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureStorageServiceTests.java +++ b/plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureStorageServiceTests.java @@ -20,6 +20,7 @@ package org.elasticsearch.repositories.azure; import com.microsoft.azure.storage.RetryExponentialRetry; +import com.microsoft.azure.storage.StorageCredentialsSharedAccessSignature; import com.microsoft.azure.storage.blob.CloudBlobClient; import com.microsoft.azure.storage.core.Base64; import org.elasticsearch.common.settings.MockSecureSettings; @@ -45,7 +46,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.isEmptyString; +import static org.hamcrest.Matchers.emptyString; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; @@ -56,11 +57,12 @@ public void testReadSecuredSettings() { .put("azure.client.azure3.endpoint_suffix", "my_endpoint_suffix").build(); final Map loadedSettings = AzureStorageSettings.load(settings); - assertThat(loadedSettings.keySet(), containsInAnyOrder("azure1","azure2","azure3","default")); + assertThat(loadedSettings.keySet(), containsInAnyOrder("azure1","azure2","azure3","azure4","default")); - assertThat(loadedSettings.get("azure1").getEndpointSuffix(), isEmptyString()); - assertThat(loadedSettings.get("azure2").getEndpointSuffix(), isEmptyString()); + assertThat(loadedSettings.get("azure1").getEndpointSuffix(), is(emptyString())); + assertThat(loadedSettings.get("azure2").getEndpointSuffix(), is(emptyString())); assertThat(loadedSettings.get("azure3").getEndpointSuffix(), equalTo("my_endpoint_suffix")); + assertThat(loadedSettings.get("azure4").getEndpointSuffix(), is(emptyString())); } private AzureRepositoryPlugin pluginWithSettingsValidation(Settings settings) { @@ -89,6 +91,37 @@ public void testCreateClientWithEndpointSuffix() throws IOException { } } + public void testCreateClientWithSasToken() throws IOException { + final Settings settings = Settings.builder().setSecureSettings(buildSecureSettings()).build(); + try (AzureRepositoryPlugin plugin = pluginWithSettingsValidation(settings)) { + final AzureStorageService azureStorageService = plugin.azureStoreService; + final CloudBlobClient client4 = azureStorageService.client("azure4").v1(); + assertThat(client4.getEndpoint().toString(), equalTo("https://myaccount4.blob.core.windows.net")); + assertThat(client4.getCredentials(), instanceOf(com.microsoft.azure.storage.StorageCredentialsSharedAccessSignature.class)); + StorageCredentialsSharedAccessSignature sas = (StorageCredentialsSharedAccessSignature) client4.getCredentials(); + assertThat(sas.getToken(), equalTo(java.util.Base64.getEncoder().encodeToString(("sig=signature4&foo=b%r").getBytes(StandardCharsets.UTF_8)))); + } + } + + public void testConflictingAuthenticationSettings() throws IOException { + final MockSecureSettings secureSettings1 = new MockSecureSettings(); + secureSettings1.setString("azure.client.azure1.account", "myaccount1"); + secureSettings1.setString("azure.client.azure1.key", encodeKey("mykey11")); + final Settings settings1 = Settings.builder().setSecureSettings(secureSettings1).build(); + final MockSecureSettings secureSettings2 = new MockSecureSettings(); + secureSettings2.setString("azure.client.azure1.account", "myaccount1"); + secureSettings2.setString("azure.client.azure1.key", encodeKey("mykey11")); + secureSettings2.setString("azure.client.azure1.sas_token", encodeKey("some=token")); + final Settings settings2 = Settings.builder().setSecureSettings(secureSettings2).build(); + try (AzureRepositoryPlugin plugin = pluginWithSettingsValidation(settings1)) { + final AzureStorageService azureStorageService = plugin.azureStoreService; + final CloudBlobClient client11 = azureStorageService.client("azure1").v1(); + assertThat(client11.getEndpoint().toString(), equalTo("https://myaccount1.blob.core.windows.net")); + final SettingsException e = expectThrows(SettingsException.class, () -> plugin.reload(settings2)); + assertThat(e.getMessage(), is("Both key and sas_token are set for [azure1] - only one must be specified")); + } + } + public void testReinitClientSettings() throws IOException { final MockSecureSettings secureSettings1 = new MockSecureSettings(); secureSettings1.setString("azure.client.azure1.account", "myaccount11"); @@ -162,18 +195,18 @@ public void testReinitClientWrongSettings() throws IOException { final AzureStorageService azureStorageService = plugin.azureStoreService; final CloudBlobClient client11 = azureStorageService.client("azure1").v1(); assertThat(client11.getEndpoint().toString(), equalTo("https://myaccount1.blob.core.windows.net")); - plugin.reload(settings2); + final SettingsException e = expectThrows(SettingsException.class, () -> plugin.reload(settings2)); + azureStorageService.client("azure1"); + assertThat(e.getMessage(), is("Either key or sas_token need to be defined for azure client [azure1]")); // existing client untouched assertThat(client11.getEndpoint().toString(), equalTo("https://myaccount1.blob.core.windows.net")); - final SettingsException e = expectThrows(SettingsException.class, () -> azureStorageService.client("azure1")); - assertThat(e.getMessage(), is("Invalid azure client settings with name [azure1]")); } } public void testGetSelectedClientNonExisting() { final AzureStorageService azureStorageService = storageServiceWithSettingsValidation(buildSettings()); - final SettingsException e = expectThrows(SettingsException.class, () -> azureStorageService.client("azure4")); - assertThat(e.getMessage(), is("Unable to find client with name [azure4]")); + final SettingsException e = expectThrows(SettingsException.class, () -> azureStorageService.client("azure5")); + assertThat(e.getMessage(), is("Unable to find client with name [azure5]")); } public void testGetSelectedClientDefaultTimeout() { @@ -341,6 +374,8 @@ private static MockSecureSettings buildSecureSettings() { secureSettings.setString("azure.client.azure2.key", encodeKey("mykey2")); secureSettings.setString("azure.client.azure3.account", "myaccount3"); secureSettings.setString("azure.client.azure3.key", encodeKey("mykey3")); + secureSettings.setString("azure.client.azure4.account", "myaccount4"); + secureSettings.setString("azure.client.azure4.sas_token", encodeKey("sig=signature4&foo=b%r")); return secureSettings; }