22// Licensed under the MIT license. See the License.txt file in the project root for full license information.
33
44using System ;
5+ using System . Collections . Concurrent ;
56using System . Collections . Generic ;
67using System . Collections . Specialized ;
8+ using System . Configuration ;
79using System . Linq ;
10+ using System . Linq . Expressions ;
811using System . Text . RegularExpressions ;
912using System . Threading . Tasks ;
13+
1014using Azure ;
1115using Azure . Data . AppConfiguration ;
1216using Azure . Identity ;
17+ using Azure . Security . KeyVault . Secrets ;
18+ using Newtonsoft . Json ;
1319
1420namespace Microsoft . Configuration . ConfigurationBuilders
1521{
@@ -18,20 +24,24 @@ namespace Microsoft.Configuration.ConfigurationBuilders
1824 /// </summary>
1925 public class AzureAppConfigurationBuilder : KeyValueConfigBuilder
2026 {
27+ private const string KeyVaultContentType = "application/vnd.microsoft.appconfig.keyvaultref+json" ;
28+
2129 #pragma warning disable CS1591 // No xml comments for tag literals.
2230 public const string endpointTag = "endpoint" ;
2331 public const string connectionStringTag = "connectionString" ;
2432 public const string keyFilterTag = "keyFilter" ;
2533 public const string labelFilterTag = "labelFilter" ;
2634 public const string dateTimeFilterTag = "acceptDateTime" ;
35+ public const string useKeyVaultTag = "useAzureKeyVault" ;
2736 #pragma warning restore CS1591 // No xml comments for tag literals.
2837
2938 private Uri _endpoint ;
3039 private string _connectionString ;
3140 private string _keyFilter ;
3241 private string _labelFilter ;
3342 private DateTimeOffset _dateTimeFilter ;
34-
43+ private bool _useKeyVault = false ;
44+ private ConcurrentDictionary < Uri , SecretClient > _kvClientCache ;
3545 private ConfigurationClient _client ;
3646
3747 /// <summary>
@@ -68,6 +78,10 @@ protected override void LazyInitialize(string name, NameValueCollection config)
6878 // acceptDateTime
6979 _dateTimeFilter = DateTimeOffset . TryParse ( UpdateConfigSettingWithAppSettings ( dateTimeFilterTag ) , out _dateTimeFilter ) ? _dateTimeFilter : DateTimeOffset . MinValue ;
7080
81+ // Azure Key Vault Integration
82+ _useKeyVault = ( UpdateConfigSettingWithAppSettings ( useKeyVaultTag ) != null ) ? Boolean . Parse ( config [ useKeyVaultTag ] ) : _useKeyVault ;
83+ if ( _useKeyVault )
84+ _kvClientCache = new ConcurrentDictionary < Uri , SecretClient > ( EqualityComparer < Uri > . Default ) ;
7185
7286 // Always allow 'connectionString' to override black magic. But we expect this to be null most of the time.
7387 _connectionString = UpdateConfigSettingWithAppSettings ( connectionStringTag ) ;
@@ -186,7 +200,8 @@ private async Task<string> GetValueAsync(string key)
186200 return null ;
187201
188202 SettingSelector selector = new SettingSelector ( key , _labelFilter ) ;
189- selector . Fields = SettingFields . Key | SettingFields . Value ;
203+ // TODO: Reduce bandwidth by limiting the fields we retrieve.
204+ //selector.Fields = SettingFields.Key | SettingFields.Value | SettingFields.ContentType;
190205 if ( _dateTimeFilter > DateTimeOffset . MinValue )
191206 {
192207 selector . AcceptDateTime = _dateTimeFilter ;
@@ -201,7 +216,27 @@ private async Task<string> GetValueAsync(string key)
201216 {
202217 // There should only be one result. If there's more, we're only returning the fisrt.
203218 await enumerator . MoveNextAsync ( ) ;
204- return enumerator . Current ? . Value ;
219+ ConfigurationSetting current = enumerator . Current ;
220+ if ( current == null )
221+ return null ;
222+
223+ if ( _useKeyVault && IsKeyVaultReference ( current ) )
224+ {
225+ try
226+ {
227+ return await GetKeyVaultValue ( current ) ;
228+ }
229+ catch ( Exception )
230+ {
231+ // 'Optional' plays a double role with this provider. Being optional means it is
232+ // ok for us to fail to resolve a keyvault reference. If we are not optional though,
233+ // we want to make some noise when a reference fails to resolve.
234+ if ( ! Optional )
235+ throw ;
236+ }
237+ }
238+
239+ return current . Value ;
205240 }
206241 finally
207242 {
@@ -221,7 +256,8 @@ private async Task<ICollection<KeyValuePair<string, string>>> GetAllValuesAsync(
221256 return data ;
222257
223258 SettingSelector selector = new SettingSelector ( _keyFilter , _labelFilter ) ;
224- selector . Fields = SettingFields . Key | SettingFields . Value ;
259+ // TODO: Reduce bandwidth by limiting the fields we retrieve.
260+ //selector.Fields = SettingFields.Key | SettingFields.Value | SettingFields.ContentType;
225261 if ( _dateTimeFilter > DateTimeOffset . MinValue )
226262 {
227263 selector . AcceptDateTime = _dateTimeFilter ;
@@ -239,8 +275,27 @@ private async Task<ICollection<KeyValuePair<string, string>>> GetAllValuesAsync(
239275 while ( await enumerator . MoveNextAsync ( ) )
240276 {
241277 ConfigurationSetting setting = enumerator . Current ;
278+ string configValue = setting . Value ;
279+
280+ // If it's a key vault reference, go fetch the value from key vault
281+ if ( _useKeyVault && IsKeyVaultReference ( setting ) )
282+ {
283+ try
284+ {
285+ configValue = await GetKeyVaultValue ( setting ) ;
286+ }
287+ catch ( Exception )
288+ {
289+ // 'Optional' plays a double role with this provider. Being optional means it is
290+ // ok for us to fail to resolve a keyvault reference. If we are not optional though,
291+ // we want to make some noise when a reference fails to resolve.
292+ if ( ! Optional )
293+ throw ;
294+ }
295+ }
296+
242297 if ( ! data . ContainsKey ( setting . Key ) )
243- data [ setting . Key ] = setting . Value ;
298+ data [ setting . Key ] = configValue ;
244299 }
245300 }
246301 finally
@@ -252,5 +307,51 @@ private async Task<ICollection<KeyValuePair<string, string>>> GetAllValuesAsync(
252307
253308 return data ;
254309 }
310+
311+ private bool IsKeyVaultReference ( ConfigurationSetting setting )
312+ {
313+ string contentType = setting . ContentType ? . Split ( ';' ) [ 0 ] . Trim ( ) ;
314+
315+ return String . Equals ( contentType , KeyVaultContentType ) ;
316+ }
317+
318+ private async Task < string > GetKeyVaultValue ( ConfigurationSetting setting )
319+ {
320+ // The key vault reference will be in the form of a Uri wrapped in JSON, like so:
321+ // {"uri":"https://vaultName.vault.azure.net/secrets/secretName"}
322+
323+ // Content validation - will throw JsonReaderException on failure
324+ KeyVaultSecretReference secretRef = JsonConvert . DeserializeObject < KeyVaultSecretReference > ( setting . Value , KeyVaultSecretReference . s_SerializationSettings ) ;
325+
326+ // Uri validation - will throw UriFormatException upon failure
327+ Uri secretUri = new Uri ( secretRef . Uri ) ;
328+ Uri vaultUri = new Uri ( secretUri . GetLeftPart ( UriPartial . Authority ) ) ;
329+
330+ // TODO: Check to see if SecretClient can take the full uri instead of requiring us to parse out the secretID.
331+ SecretClient kvClient = GetSecretClient ( vaultUri ) ;
332+ if ( kvClient == null && ! Optional )
333+ throw new ConfigurationErrorsException ( "Could not connect to Azure Key Vault while retrieving secret. Connection is not optional." ) ;
334+
335+ // Retrieve Value
336+ KeyVaultSecret kvSecret = await kvClient . GetSecretAsync ( secretUri . Segments [ 2 ] . TrimEnd ( new char [ ] { '/' } ) ) ; // ['/', 'secrets/', '{secretID}/']
337+ if ( kvSecret != null && kvSecret . Properties . Enabled . GetValueOrDefault ( ) )
338+ return kvSecret . Value ;
339+
340+ return null ;
341+ }
342+
343+ private SecretClient GetSecretClient ( Uri vaultUri )
344+ {
345+ return _kvClientCache . GetOrAdd ( vaultUri , uri => new SecretClient ( uri , new DefaultAzureCredential ( ) ) ) ;
346+ }
347+
348+ [ JsonObject ( MemberSerialization . OptIn ) ]
349+ private class KeyVaultSecretReference
350+ {
351+ public static JsonSerializerSettings s_SerializationSettings = new JsonSerializerSettings { DateParseHandling = DateParseHandling . None } ;
352+
353+ [ JsonProperty ( "uri" ) ]
354+ public string Uri { get ; set ; }
355+ }
255356 }
256357}
0 commit comments