fix(secrets): ${secret:} expander skips comment keys (_-prefixed leaf) — 0.1.2
Live HistorianGateway boot caught that the expander scanned a '_secretsComment' appsettings
value documenting the ${secret:...} syntax, tried to resolve the literal example token, and
crashed boot. The family uses _*Comment keys pervasively, so skip any config key whose leaf
segment starts with '_' (the _comment convention). Functional keys never lead with '_'.
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<Version>0.1.1</Version>
|
<Version>0.1.2</Version>
|
||||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -83,6 +83,13 @@ public sealed class SecretReferenceExpander
|
|||||||
/// or <c>ValidateOnStart</c> executes; otherwise validators may see the unexpanded placeholder.
|
/// or <c>ValidateOnStart</c> executes; otherwise validators may see the unexpanded placeholder.
|
||||||
/// Fail-closed: a missing referenced secret propagates <see cref="SecretNotFoundException"/> out
|
/// Fail-closed: a missing referenced secret propagates <see cref="SecretNotFoundException"/> out
|
||||||
/// of startup.
|
/// of startup.
|
||||||
|
/// <para>
|
||||||
|
/// Documentation/comment keys are skipped: any entry whose leaf key segment (the part after the
|
||||||
|
/// last <c>:</c>) begins with an underscore — the widely-used <c>"_comment"</c> JSON convention —
|
||||||
|
/// is left untouched. This lets an appsettings file document the <c>${secret:...}</c> syntax in a
|
||||||
|
/// <c>_comment</c> value (even with a literal example token) without that example being treated as
|
||||||
|
/// a real reference to resolve. Functional keys are never named with a leading underscore.
|
||||||
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="config">The loaded configuration root to mutate.</param>
|
/// <param name="config">The loaded configuration root to mutate.</param>
|
||||||
/// <param name="ct">A token to cancel the operation.</param>
|
/// <param name="ct">A token to cancel the operation.</param>
|
||||||
@@ -95,7 +102,9 @@ public sealed class SecretReferenceExpander
|
|||||||
var pending = new List<KeyValuePair<string, string>>();
|
var pending = new List<KeyValuePair<string, string>>();
|
||||||
foreach (KeyValuePair<string, string?> entry in config.AsEnumerable())
|
foreach (KeyValuePair<string, string?> entry in config.AsEnumerable())
|
||||||
{
|
{
|
||||||
if (entry.Value is { } value && value.Contains(TokenMarker, StringComparison.Ordinal))
|
if (entry.Value is { } value
|
||||||
|
&& value.Contains(TokenMarker, StringComparison.Ordinal)
|
||||||
|
&& !IsCommentKey(entry.Key))
|
||||||
{
|
{
|
||||||
pending.Add(new KeyValuePair<string, string>(entry.Key, value));
|
pending.Add(new KeyValuePair<string, string>(entry.Key, value));
|
||||||
}
|
}
|
||||||
@@ -106,4 +115,14 @@ public sealed class SecretReferenceExpander
|
|||||||
config[entry.Key] = await ExpandAsync(entry.Value, ct).ConfigureAwait(false);
|
config[entry.Key] = await ExpandAsync(entry.Value, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A configuration key is a comment/documentation key when its leaf segment (after the last ':')
|
||||||
|
// starts with '_' — the conventional "_comment" marker. Such values may legitimately contain a
|
||||||
|
// literal ${secret:...} example and must not be resolved.
|
||||||
|
private static bool IsCommentKey(string key)
|
||||||
|
{
|
||||||
|
int lastSeparator = key.LastIndexOf(':');
|
||||||
|
string leaf = lastSeparator >= 0 ? key[(lastSeparator + 1)..] : key;
|
||||||
|
return leaf.StartsWith('_');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
@@ -103,4 +103,30 @@ public sealed class SecretReferenceExpanderTests
|
|||||||
await Assert.ThrowsAsync<SecretNotFoundException>(
|
await Assert.ThrowsAsync<SecretNotFoundException>(
|
||||||
() => expander.ExpandConfigurationAsync(config, CancellationToken.None));
|
() => expander.ExpandConfigurationAsync(config, CancellationToken.None));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ExpandConfigurationAsync_SkipsCommentKeys()
|
||||||
|
{
|
||||||
|
// A "_comment" key may document the ${secret:...} syntax with a literal (even malformed or
|
||||||
|
// not-yet-seeded) example token; it must be left untouched — never resolved, never throwing —
|
||||||
|
// while a real sibling key still expands. Regression: an unseeded/ellipsis example in a comment
|
||||||
|
// previously crashed boot (SecretName rejects '...').
|
||||||
|
SecretReferenceExpander expander = Expander(("sql/foo", "hunter2"));
|
||||||
|
IConfigurationRoot config = new ConfigurationBuilder()
|
||||||
|
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||||
|
{
|
||||||
|
["_secretsComment"] = "e.g. Password=${secret:...} or ${secret:sql/not-seeded}",
|
||||||
|
["Nested:_comment"] = "also skipped: ${secret:sql/also-absent}",
|
||||||
|
["ConnectionStrings:Db"] = "Server=.;Password=${secret:sql/foo}",
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
await expander.ExpandConfigurationAsync(config, CancellationToken.None);
|
||||||
|
|
||||||
|
// Comment keys untouched (literal token preserved, no throw despite absent/malformed names).
|
||||||
|
Assert.Equal("e.g. Password=${secret:...} or ${secret:sql/not-seeded}", config["_secretsComment"]);
|
||||||
|
Assert.Contains("${secret:sql/also-absent}", config["Nested:_comment"]);
|
||||||
|
// Real key still expanded.
|
||||||
|
Assert.Equal("Server=.;Password=hunter2", config["ConnectionStrings:Db"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user