diff --git a/README.md b/README.md
index 373725d..0be7b61 100644
--- a/README.md
+++ b/README.md
@@ -180,6 +180,7 @@ and currently exposes the format of the file which, as mentioned above, should b
[@keyFilter="string"]
[@labelFilter="label"]
[@acceptDateTime="DateTimeOffset"]
+ [@useAzureKeyVault="bool"]
type="Microsoft.Configuration.ConfigurationBuilders.AzureAppConfigurationBuilder, Microsoft.Configuration.ConfigurationBuilders.AzureAppConfig" />
```
[AppConfiguration](https://docs.microsoft.com/en-us/azure/azure-app-configuration/overview) is a new offering from Azure, currently in preview. If you
@@ -192,6 +193,9 @@ It is however, __strongly__ encouraged to use `endpoint` with a managed service
* `labelFilter` - Only retrieve configuration values that match a certain label.
* `acceptDateTime` - Instead of versioning ala Azure Key Vault, AppConfiguration uses timestamps. Use this attribute to go back in time
to retrieve configuration values from a past state.
+ * `useAzureKeyVault` - Enable this feature to allow AzureAppConfigurationBuilder to connect to and retrieve secrets from Azure Key Vault for
+ config values that are stored in Key Vault. The same managed service identity that is used for connecting to the AppConfiguration service will
+ be used to connect to Key Vault. Default is `false`.
### AzureKeyVaultConfigBuilder
```xml
diff --git a/samples/SampleWebApp/Web.config b/samples/SampleWebApp/Web.config
index 9be1e15..2d06b8b 100644
--- a/samples/SampleWebApp/Web.config
+++ b/samples/SampleWebApp/Web.config
@@ -22,18 +22,20 @@
For Azure AppConfiguration, imagine this is the config store:
- Key Label Value LastModified
+ Key Label Value LastModified
acTest (none) test1
acTest2 (none) test2
acTest2 beta test2b
acTest2 beta2 test2b2
acTest3 beta test3b
acTest4 beta3 test4b3
- acLMTest (none) oldest T1 (T1,2 before 12/1/19. T3,4,5 after.)
- acLMTest (none) newer T3
- acLMTest2 ga older T2
- acLMTest2 ga newest T4
- acLMTest3 (none) toonew T5
+ acLMTest (none) oldest T1 (T1,2 before 12/1/19. T3,4,5 after.)
+ acLMTest (none) newer T3
+ acLMTest2 ga older T2
+ acLMTest2 ga newest T4
+ acLMTest3 (none) toonew T5
+ acKVTest1 beta V1 from KeyVault
+ acKVTest2 beta V2 from KeyVault
-->
@@ -54,7 +56,7 @@
-
+
@@ -98,6 +100,8 @@
+
+
diff --git a/src/Azure/AzureKeyVaultConfigBuilder.cs b/src/Azure/AzureKeyVaultConfigBuilder.cs
index e3b61ec..532bf5f 100644
--- a/src/Azure/AzureKeyVaultConfigBuilder.cs
+++ b/src/Azure/AzureKeyVaultConfigBuilder.cs
@@ -201,11 +201,15 @@ private async Task GetValueAsync(string key)
if (version != null)
{
KeyVaultSecret versionedSecret = await _kvClient.GetSecretAsync(vKey.Key, version);
- return versionedSecret;
+ if (versionedSecret != null && versionedSecret.Properties.Enabled.GetValueOrDefault())
+ return versionedSecret;
+ return null;
}
KeyVaultSecret secret = await _kvClient.GetSecretAsync(vKey.Key);
- return secret;
+ if (secret != null && secret.Properties.Enabled.GetValueOrDefault())
+ return secret;
+ return null;
}
catch (RequestFailedException rfex)
{
diff --git a/src/AzureAppConfig/AzureAppConfig.csproj b/src/AzureAppConfig/AzureAppConfig.csproj
index 4778c19..373948d 100644
--- a/src/AzureAppConfig/AzureAppConfig.csproj
+++ b/src/AzureAppConfig/AzureAppConfig.csproj
@@ -58,6 +58,12 @@
1.0.0
+
+ 4.0.0
+
+
+ 11.0.1
+
diff --git a/src/AzureAppConfig/AzureAppConfigurationBuilder.cs b/src/AzureAppConfig/AzureAppConfigurationBuilder.cs
index 7dbff15..d64bf3f 100644
--- a/src/AzureAppConfig/AzureAppConfigurationBuilder.cs
+++ b/src/AzureAppConfig/AzureAppConfigurationBuilder.cs
@@ -2,14 +2,20 @@
// Licensed under the MIT license. See the License.txt file in the project root for full license information.
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Specialized;
+using System.Configuration;
using System.Linq;
+using System.Linq.Expressions;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
+
using Azure;
using Azure.Data.AppConfiguration;
using Azure.Identity;
+using Azure.Security.KeyVault.Secrets;
+using Newtonsoft.Json;
namespace Microsoft.Configuration.ConfigurationBuilders
{
@@ -18,12 +24,15 @@ namespace Microsoft.Configuration.ConfigurationBuilders
///
public class AzureAppConfigurationBuilder : KeyValueConfigBuilder
{
+ private const string KeyVaultContentType = "application/vnd.microsoft.appconfig.keyvaultref+json";
+
#pragma warning disable CS1591 // No xml comments for tag literals.
public const string endpointTag = "endpoint";
public const string connectionStringTag = "connectionString";
public const string keyFilterTag = "keyFilter";
public const string labelFilterTag = "labelFilter";
public const string dateTimeFilterTag = "acceptDateTime";
+ public const string useKeyVaultTag = "useAzureKeyVault";
#pragma warning restore CS1591 // No xml comments for tag literals.
private Uri _endpoint;
@@ -31,7 +40,8 @@ public class AzureAppConfigurationBuilder : KeyValueConfigBuilder
private string _keyFilter;
private string _labelFilter;
private DateTimeOffset _dateTimeFilter;
-
+ private bool _useKeyVault = false;
+ private ConcurrentDictionary _kvClientCache;
private ConfigurationClient _client;
///
@@ -68,6 +78,10 @@ protected override void LazyInitialize(string name, NameValueCollection config)
// acceptDateTime
_dateTimeFilter = DateTimeOffset.TryParse(UpdateConfigSettingWithAppSettings(dateTimeFilterTag), out _dateTimeFilter) ? _dateTimeFilter : DateTimeOffset.MinValue;
+ // Azure Key Vault Integration
+ _useKeyVault = (UpdateConfigSettingWithAppSettings(useKeyVaultTag) != null) ? Boolean.Parse(config[useKeyVaultTag]) : _useKeyVault;
+ if (_useKeyVault)
+ _kvClientCache = new ConcurrentDictionary(EqualityComparer.Default);
// Always allow 'connectionString' to override black magic. But we expect this to be null most of the time.
_connectionString = UpdateConfigSettingWithAppSettings(connectionStringTag);
@@ -186,7 +200,8 @@ private async Task GetValueAsync(string key)
return null;
SettingSelector selector = new SettingSelector(key, _labelFilter);
- selector.Fields = SettingFields.Key | SettingFields.Value;
+ // TODO: Reduce bandwidth by limiting the fields we retrieve.
+ //selector.Fields = SettingFields.Key | SettingFields.Value | SettingFields.ContentType;
if (_dateTimeFilter > DateTimeOffset.MinValue)
{
selector.AcceptDateTime = _dateTimeFilter;
@@ -201,7 +216,27 @@ private async Task GetValueAsync(string key)
{
// There should only be one result. If there's more, we're only returning the fisrt.
await enumerator.MoveNextAsync();
- return enumerator.Current?.Value;
+ ConfigurationSetting current = enumerator.Current;
+ if (current == null)
+ return null;
+
+ if (_useKeyVault && IsKeyVaultReference(current))
+ {
+ try
+ {
+ return await GetKeyVaultValue(current);
+ }
+ catch (Exception)
+ {
+ // 'Optional' plays a double role with this provider. Being optional means it is
+ // ok for us to fail to resolve a keyvault reference. If we are not optional though,
+ // we want to make some noise when a reference fails to resolve.
+ if (!Optional)
+ throw;
+ }
+ }
+
+ return current.Value;
}
finally
{
@@ -221,7 +256,8 @@ private async Task>> GetAllValuesAsync(
return data;
SettingSelector selector = new SettingSelector(_keyFilter, _labelFilter);
- selector.Fields = SettingFields.Key | SettingFields.Value;
+ // TODO: Reduce bandwidth by limiting the fields we retrieve.
+ //selector.Fields = SettingFields.Key | SettingFields.Value | SettingFields.ContentType;
if (_dateTimeFilter > DateTimeOffset.MinValue)
{
selector.AcceptDateTime = _dateTimeFilter;
@@ -239,8 +275,27 @@ private async Task>> GetAllValuesAsync(
while (await enumerator.MoveNextAsync())
{
ConfigurationSetting setting = enumerator.Current;
+ string configValue = setting.Value;
+
+ // If it's a key vault reference, go fetch the value from key vault
+ if (_useKeyVault && IsKeyVaultReference(setting))
+ {
+ try
+ {
+ configValue = await GetKeyVaultValue(setting);
+ }
+ catch (Exception)
+ {
+ // 'Optional' plays a double role with this provider. Being optional means it is
+ // ok for us to fail to resolve a keyvault reference. If we are not optional though,
+ // we want to make some noise when a reference fails to resolve.
+ if (!Optional)
+ throw;
+ }
+ }
+
if (!data.ContainsKey(setting.Key))
- data[setting.Key] = setting.Value;
+ data[setting.Key] = configValue;
}
}
finally
@@ -252,5 +307,51 @@ private async Task>> GetAllValuesAsync(
return data;
}
+
+ private bool IsKeyVaultReference(ConfigurationSetting setting)
+ {
+ string contentType = setting.ContentType?.Split(';')[0].Trim();
+
+ return String.Equals(contentType, KeyVaultContentType);
+ }
+
+ private async Task GetKeyVaultValue(ConfigurationSetting setting)
+ {
+ // The key vault reference will be in the form of a Uri wrapped in JSON, like so:
+ // {"uri":"https://vaultName.vault.azure.net/secrets/secretName"}
+
+ // Content validation - will throw JsonReaderException on failure
+ KeyVaultSecretReference secretRef = JsonConvert.DeserializeObject(setting.Value, KeyVaultSecretReference.s_SerializationSettings);
+
+ // Uri validation - will throw UriFormatException upon failure
+ Uri secretUri = new Uri(secretRef.Uri);
+ Uri vaultUri = new Uri(secretUri.GetLeftPart(UriPartial.Authority));
+
+ // TODO: Check to see if SecretClient can take the full uri instead of requiring us to parse out the secretID.
+ SecretClient kvClient = GetSecretClient(vaultUri);
+ if (kvClient == null && !Optional)
+ throw new ConfigurationErrorsException("Could not connect to Azure Key Vault while retrieving secret. Connection is not optional.");
+
+ // Retrieve Value
+ KeyVaultSecret kvSecret = await kvClient.GetSecretAsync(secretUri.Segments[2].TrimEnd(new char[] { '/' })); // ['/', 'secrets/', '{secretID}/']
+ if (kvSecret != null && kvSecret.Properties.Enabled.GetValueOrDefault())
+ return kvSecret.Value;
+
+ return null;
+ }
+
+ private SecretClient GetSecretClient(Uri vaultUri)
+ {
+ return _kvClientCache.GetOrAdd(vaultUri, uri => new SecretClient(uri, new DefaultAzureCredential()));
+ }
+
+ [JsonObject(MemberSerialization.OptIn)]
+ private class KeyVaultSecretReference
+ {
+ public static JsonSerializerSettings s_SerializationSettings = new JsonSerializerSettings { DateParseHandling = DateParseHandling.None };
+
+ [JsonProperty("uri")]
+ public string Uri { get; set; }
+ }
}
}