Skip to content

Commit cf200e5

Browse files
Merge pull request #65 from aspnet/57_Key-Preprocessing
57 key preprocessing
2 parents 32d71a0 + 97367f0 commit cf200e5

4 files changed

Lines changed: 170 additions & 7 deletions

File tree

src/Azure/AzureKeyVaultConfigBuilder.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,14 +143,33 @@ public override ICollection<KeyValuePair<string, string>> GetAllValues(string pr
143143
return d;
144144
}
145145

146+
/// <summary>
147+
/// Transform the given key to an intermediate format that will be used to look up values in backing store.
148+
/// </summary>
149+
/// <param name="key">The string to be mapped.</param>
150+
/// <returns>The key string to be used while looking up config values..</returns>
151+
public override string MapKey(string key)
152+
{
153+
if (String.IsNullOrEmpty(key))
154+
return key;
155+
156+
// Colons and underscores are common in appSettings keys, but not allowed in key vault key names.
157+
// It's likely that apps will want to lookup config values with these characters in their name in
158+
// key vault. More extensive key mapping can be done with subclasses. But let's handle the most
159+
// most common case here.
160+
key = key.Replace(':', '-');
161+
key = key.Replace('_', '-');
162+
return key;
163+
}
164+
146165
/// <summary>
147166
/// Makes a determination about whether the input key is valid for this builder and backing store.
148167
/// </summary>
149168
/// <param name="key">The string to be validated. May be partial.</param>
150169
/// <returns>True if the string is valid. False if the string is not a valid key.</returns>
151170
public override bool ValidateKey(string key)
152171
{
153-
// Key Vault only allows alphanumerics and '-'. This builder also allows for one '/'.
172+
// Key Vault only allows alphanumerics and '-'. This builder also allows for one '/' for versioning.
154173
return Regex.IsMatch(key, "^[a-zA-Z0-9-]+(/?[a-zA-Z0-9-]+)?$");
155174
}
156175

src/AzureAppConfig/AzureAppConfigurationBuilder.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Collections.Specialized;
77
using System.Linq;
8+
using System.Text.RegularExpressions;
89
using System.Threading;
910
using System.Threading.Tasks;
1011
using Microsoft.Azure.AppConfiguration.Azconfig;
@@ -102,8 +103,21 @@ protected override void LazyInitialize(string name, NameValueCollection config)
102103
/// <returns>True if the string is valid. False if the string is not a valid key.</returns>
103104
public override bool ValidateKey(string key)
104105
{
105-
// Azure App Config does not restrict key names, although a couple characters have special meaning if not escaped.
106-
// We may want to restrict using those characters unescaped in a key name in the future.
106+
// From - https://docs.microsoft.com/en-us/azure/azure-app-configuration/concept-key-value
107+
// You can use any unicode character in key names entered into App Configuration except for *, ,, and \. These characters are
108+
// reserved.If you need to include a reserved character, you must escape it by using \{ Reserved Character}.
109+
if (String.IsNullOrWhiteSpace(key))
110+
return false;
111+
112+
if (key.Contains('*') || key.Contains(','))
113+
return false;
114+
115+
// We don't want to completely disallow '\' since it is used for escaping. But writing a full parser for someone elses
116+
// naming format could be error prone. If we see a '\' followed by a '{', just call it good. Don't bother with the Regex
117+
// if there aren't any backslashes though.
118+
if (key.Contains('\\'))
119+
return !Regex.IsMatch(key, @"\\[^{]");
120+
107121
return true;
108122
}
109123

src/Base/KeyValueConfigBuilder.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ public abstract class KeyValueConfigBuilder : ConfigurationBuilder
7575
/// <returns>A collection of key/value pairs.</returns>
7676
public abstract ICollection<KeyValuePair<string, string>> GetAllValues(string prefix);
7777

78+
/// <summary>
79+
/// Transform the given key to an intermediate format that will be used to look up values in backing store.
80+
/// </summary>
81+
/// <param name="key">The string to be mapped.</param>
82+
/// <returns>The key string to be used while looking up config values..</returns>
83+
public virtual string MapKey(string key) { return key; }
7884
/// <summary>
7985
/// Makes a determination about whether the input key is valid for this builder and backing store.
8086
/// </summary>
@@ -252,13 +258,14 @@ private void EnsureGreedyInitialized()
252258
{
253259
// In Greedy mode, we need to know all the key/value pairs from this config source. So we
254260
// can't 'cache' them as we go along. Slurp them all up now. But only once. ;)
255-
if (!_greedyInitialized && (String.IsNullOrEmpty(KeyPrefix) || ValidateKey(KeyPrefix)))
261+
if (!_greedyInitialized)
256262
{
263+
string prefix = MapKey(KeyPrefix); // Do this outside the lock. It ensures _cachedValues is initialized.
257264
lock (_cachedValues)
258265
{
259-
if (!_greedyInitialized)
266+
if (!_greedyInitialized && (String.IsNullOrEmpty(prefix) || ValidateKey(prefix)))
260267
{
261-
foreach (KeyValuePair<string, string> kvp in GetAllValues(KeyPrefix))
268+
foreach (KeyValuePair<string, string> kvp in GetAllValues(prefix))
262269
{
263270
_cachedValues.Add(kvp);
264271
}
@@ -308,7 +315,7 @@ private string GetValueInternal(string key)
308315

309316
// Stripping Prefix in strict mode means from the source key. The static config file will have a prefix-less key to match.
310317
// ie <add key="MySetting" /> should only match the key/value (KeyPrefix + "MySetting") from the source.
311-
string sourceKey = (StripPrefix) ? KeyPrefix + key : key;
318+
string sourceKey = MapKey((StripPrefix) ? KeyPrefix + key : key);
312319

313320
if (!ValidateKey(sourceKey))
314321
return null;

test/Microsoft.Configuration.ConfigurationBuilders.Test/BaseTests.cs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,113 @@ public void BaseBehavior_Greedy()
494494
Assert.Equal("Prefix_TestKey1Value", newSettings.Settings["Prefix_TestKey1"]?.Value);
495495
}
496496

497+
// ======================================================================
498+
// Extension Points
499+
// ======================================================================
500+
[Fact]
501+
public void Ext_KeyMapping()
502+
{
503+
// Strict
504+
var builder = new FakeKeyMappingConfigBuilder();
505+
builder.Initialize("test", new System.Collections.Specialized.NameValueCollection());
506+
AppSettingsSection newSettings = (AppSettingsSection)builder.ProcessConfigurationSection(GetAppSettings());
507+
Assert.Equal("TestKey1Value", newSettings.Settings["TestKey1"]?.Value);
508+
Assert.Equal("${TestKey1}", newSettings.Settings["test1"]?.Value);
509+
Assert.Equal("expandTestValue", newSettings.Settings["${TestKey1}"]?.Value);
510+
Assert.Equal("PrefixTest1", newSettings.Settings["TestKey"]?.Value);
511+
Assert.Null(newSettings.Settings["Prefix_TestKey"]?.Value);
512+
Assert.Equal("Prefix_TestKeyValue", newSettings.Settings["Prefix#TestKey"]?.Value);
513+
Assert.Equal("${Prefix_TestKey1}", newSettings.Settings["PreTest2"]?.Value);
514+
Assert.Equal("MappingTest1", newSettings.Settings["Prefix_Alt_Token"]?.Value);
515+
Assert.Null(newSettings.Settings["Alt#Token"]?.Value);
516+
Assert.Null(newSettings.Settings["Alt_Token"]?.Value);
517+
Assert.Equal("ThisWasADifferentAlternateTokenPattern", newSettings.Settings["Alt:Token"]?.Value);
518+
519+
// Strict with prefix
520+
builder = new FakeKeyMappingConfigBuilder();
521+
builder.Initialize("test", new System.Collections.Specialized.NameValueCollection() { { "prefix", "Prefix_" } });
522+
newSettings = (AppSettingsSection)builder.ProcessConfigurationSection(GetAppSettings());
523+
Assert.Equal("val1", newSettings.Settings["TestKey1"]?.Value);
524+
Assert.Equal("${TestKey1}", newSettings.Settings["test1"]?.Value);
525+
Assert.Equal("expandTestValue", newSettings.Settings["${TestKey1}"]?.Value);
526+
Assert.Equal("PrefixTest1", newSettings.Settings["TestKey"]?.Value);
527+
Assert.Null(newSettings.Settings["Prefix_TestKey"]?.Value);
528+
Assert.Equal("Prefix_TestKeyValue", newSettings.Settings["Prefix#TestKey"]?.Value);
529+
Assert.Equal("${Prefix_TestKey1}", newSettings.Settings["PreTest2"]?.Value);
530+
Assert.Equal("MappingTest1", newSettings.Settings["Prefix_Alt_Token"]?.Value);
531+
Assert.Null(newSettings.Settings["Alt#Token"]?.Value);
532+
Assert.Null(newSettings.Settings["Alt_Token"]?.Value);
533+
Assert.Equal("MappingTest2", newSettings.Settings["Alt:Token"]?.Value);
534+
535+
// Greedy
536+
builder = new FakeKeyMappingConfigBuilder();
537+
builder.Initialize("test", new System.Collections.Specialized.NameValueCollection() { { "mode", "Greedy" } });
538+
newSettings = (AppSettingsSection)builder.ProcessConfigurationSection(GetAppSettings());
539+
Assert.Equal("TestKey1Value", newSettings.Settings["TestKey1"]?.Value);
540+
Assert.Equal("TestKey2Value", newSettings.Settings["TestKey2"]?.Value);
541+
Assert.Equal("${TestKey1}", newSettings.Settings["test1"]?.Value);
542+
Assert.Equal("expandTestValue", newSettings.Settings["${TestKey1}"]?.Value);
543+
Assert.Equal("PrefixTest1", newSettings.Settings["TestKey"]?.Value);
544+
Assert.Null(newSettings.Settings["Prefix_TestKey"]?.Value);
545+
Assert.Equal("Prefix_TestKeyValue", newSettings.Settings["Prefix#TestKey"]?.Value);
546+
Assert.Null(newSettings.Settings["Prefix_TestKey1"]?.Value);
547+
Assert.Equal("Prefix_TestKey1Value", newSettings.Settings["Prefix#TestKey1"]?.Value);
548+
Assert.Equal("${Prefix_TestKey1}", newSettings.Settings["PreTest2"]?.Value);
549+
Assert.Equal("MappingTest1", newSettings.Settings["Prefix_Alt_Token"]?.Value);
550+
Assert.Equal("ThisWasAnAltTokenPatternWithPrefix", newSettings.Settings["Prefix#Alt:Token"]?.Value);
551+
Assert.Null(newSettings.Settings["Prefix_Alt:Token"]?.Value);
552+
Assert.Equal("ThisWasADifferentAlternateTokenPattern", newSettings.Settings["Alt#Token"]?.Value);
553+
Assert.Null(newSettings.Settings["Alt_Token"]?.Value);
554+
Assert.Equal("ThisWasAnAlternateTokenPattern", newSettings.Settings["Alt:Token"]?.Value);
555+
556+
// Greedy with prefix and stripping
557+
builder = new FakeKeyMappingConfigBuilder();
558+
builder.Initialize("test", new System.Collections.Specialized.NameValueCollection() { { "mode", "Greedy" }, { "prefix", "Prefix_" }, { "stripPrefix", "TRUE" } });
559+
newSettings = (AppSettingsSection)builder.ProcessConfigurationSection(GetAppSettings());
560+
Assert.Equal("Prefix_TestKey1Value", newSettings.Settings["TestKey1"]?.Value);
561+
Assert.Null(newSettings.Settings["TestKey2"]?.Value);
562+
Assert.Equal("${TestKey1}", newSettings.Settings["test1"]?.Value);
563+
Assert.Equal("expandTestValue", newSettings.Settings["${TestKey1}"]?.Value);
564+
Assert.Equal("Prefix_TestKeyValue", newSettings.Settings["TestKey"]?.Value);
565+
Assert.Equal("PrefixTest2", newSettings.Settings["Prefix_TestKey"]?.Value);
566+
Assert.Null(newSettings.Settings["Prefix#TestKey"]?.Value);
567+
Assert.Null(newSettings.Settings["Prefix_TestKey1"]?.Value);
568+
Assert.Null(newSettings.Settings["Prefix#TestKey1"]?.Value);
569+
Assert.Equal("${Prefix_TestKey1}", newSettings.Settings["PreTest2"]?.Value);
570+
Assert.Equal("MappingTest1", newSettings.Settings["Prefix_Alt_Token"]?.Value);
571+
Assert.Equal("ThisWasAnAltTokenPatternWithPrefix", newSettings.Settings["Alt:Token"]?.Value);
572+
Assert.Null(newSettings.Settings["Alt#Token"]?.Value);
573+
Assert.Null(newSettings.Settings["Alt_Token"]?.Value);
574+
575+
// Greedy with interesting prefix
576+
builder = new FakeKeyMappingConfigBuilder();
577+
builder.Initialize("test", new System.Collections.Specialized.NameValueCollection() { { "mode", "Greedy" }, { "prefix", "Prefix:" } });
578+
newSettings = (AppSettingsSection)builder.ProcessConfigurationSection(GetAppSettings());
579+
Assert.Equal("val1", newSettings.Settings["TestKey1"]?.Value);
580+
Assert.Null(newSettings.Settings["TestKey2"]?.Value);
581+
Assert.Equal("${TestKey1}", newSettings.Settings["test1"]?.Value);
582+
Assert.Equal("expandTestValue", newSettings.Settings["${TestKey1}"]?.Value);
583+
Assert.Equal("PrefixTest1", newSettings.Settings["TestKey"]?.Value);
584+
Assert.Null(newSettings.Settings["Prefix_TestKey"]?.Value);
585+
Assert.Null(newSettings.Settings["Prefix:TestKey"]?.Value);
586+
Assert.Equal("Prefix_TestKeyValue", newSettings.Settings["Prefix#TestKey"]?.Value);
587+
Assert.Null(newSettings.Settings["Prefix_TestKey1"]?.Value);
588+
Assert.Null(newSettings.Settings["Prefix:TestKey1"]?.Value);
589+
Assert.Equal("Prefix_TestKey1Value", newSettings.Settings["Prefix#TestKey1"]?.Value);
590+
Assert.Equal("${Prefix_TestKey1}", newSettings.Settings["PreTest2"]?.Value);
591+
Assert.Equal("MappingTest1", newSettings.Settings["Prefix_Alt_Token"]?.Value);
592+
Assert.Null(newSettings.Settings["Prefix_Alt:Token"]?.Value);
593+
Assert.Null(newSettings.Settings["Prefix_Alt#Token"]?.Value);
594+
Assert.Null(newSettings.Settings["Prefix#Alt_Token"]?.Value);
595+
Assert.Equal("ThisWasAnAltTokenPatternWithPrefix", newSettings.Settings["Prefix#Alt:Token"]?.Value);
596+
Assert.Null(newSettings.Settings["Prefix#Alt#Token"]?.Value);
597+
Assert.Null(newSettings.Settings["Prefix:Alt_Token"]?.Value);
598+
Assert.Null(newSettings.Settings["Prefix:Alt#Token"]?.Value);
599+
Assert.Null(newSettings.Settings["Prefix:Alt:Token"]?.Value);
600+
Assert.Equal("MappingTest2", newSettings.Settings["Alt:Token"]?.Value);
601+
Assert.Null(newSettings.Settings["Alt#Token"]?.Value);
602+
Assert.Null(newSettings.Settings["Alt_Token"]?.Value);
603+
}
497604

498605
// ======================================================================
499606
// Errors
@@ -605,6 +712,8 @@ AppSettingsSection GetAppSettings()
605712
appSettings.Settings.Add("TestKey", "PrefixTest1");
606713
appSettings.Settings.Add("Prefix_TestKey", "PrefixTest2");
607714
appSettings.Settings.Add("PreTest2", "${Prefix_TestKey1}");
715+
appSettings.Settings.Add("Prefix_Alt_Token", "MappingTest1");
716+
appSettings.Settings.Add("Alt:Token", "MappingTest2");
608717
return appSettings;
609718
}
610719

@@ -628,6 +737,7 @@ class FakeConfigBuilder : KeyValueConfigBuilder
628737
{ "Prefix_TestKey", "Prefix_TestKeyValue" },
629738
{ "Prefix_TestKey1", "Prefix_TestKey1Value" },
630739
{ "Alt:Token", "ThisWasAnAlternateTokenPattern" },
740+
{ "Alt_Token", "ThisWasADifferentAlternateTokenPattern" },
631741
{ "Prefix_Alt:Token", "ThisWasAnAltTokenPatternWithPrefix" }
632742
};
633743

@@ -673,4 +783,17 @@ public void SetTokenPattern(string newPattern)
673783
this.TokenPattern = newPattern;
674784
}
675785
}
786+
787+
class FakeKeyMappingConfigBuilder : FakeConfigBuilder
788+
{
789+
public override string MapKey(string key)
790+
{
791+
return key.Replace(":", "_");
792+
}
793+
794+
public override string UpdateKey(string rawKey)
795+
{
796+
return rawKey.Replace("_", "#");
797+
}
798+
}
676799
}

0 commit comments

Comments
 (0)