fix(secrets): route the unset-token harness through the real hub composition (review drift-risk)
An internal validateEagerly seam on AddZbSecretsGrpcHub replaces the harness's hand-copied registration fragment, so the deny-all second-layer tests exercise the production wiring and cannot drift from it. Claude-Session: https://claude.ai/code/session_014WNM4vjoVksyyBraTXSZE1
This commit is contained in:
+21
-5
@@ -43,7 +43,20 @@ public static class SecretsGrpcHubServerExtensions
|
|||||||
public static IServiceCollection AddZbSecretsGrpcHub(
|
public static IServiceCollection AddZbSecretsGrpcHub(
|
||||||
this IServiceCollection services,
|
this IServiceCollection services,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
string sectionPath = "Secrets:GrpcHub")
|
string sectionPath = "Secrets:GrpcHub") =>
|
||||||
|
AddZbSecretsGrpcHubCore(services, configuration, sectionPath, validateEagerly: true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The whole composition, with the eager validation optionally skipped. Internal so the test
|
||||||
|
/// harness can exercise the interceptor's deny-all second layer over the wire through the REAL
|
||||||
|
/// registration path — a hand-copied fragment of this method in the harness would silently
|
||||||
|
/// drift when this method changes. Production callers always validate eagerly.
|
||||||
|
/// </summary>
|
||||||
|
internal static IServiceCollection AddZbSecretsGrpcHubCore(
|
||||||
|
IServiceCollection services,
|
||||||
|
IConfiguration configuration,
|
||||||
|
string sectionPath,
|
||||||
|
bool validateEagerly)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(services);
|
ArgumentNullException.ThrowIfNull(services);
|
||||||
ArgumentNullException.ThrowIfNull(configuration);
|
ArgumentNullException.ThrowIfNull(configuration);
|
||||||
@@ -74,10 +87,13 @@ public static class SecretsGrpcHubServerExtensions
|
|||||||
// startup failure and the runtime warning both name the key this host actually reads.
|
// startup failure and the runtime warning both name the key this host actually reads.
|
||||||
services.PostConfigure<SecretsGrpcHubServerOptions>(options => options.SectionPath = sectionPath);
|
services.PostConfigure<SecretsGrpcHubServerOptions>(options => options.SectionPath = sectionPath);
|
||||||
|
|
||||||
var eager = new SecretsGrpcHubServerOptions { SectionPath = sectionPath };
|
if (validateEagerly)
|
||||||
section.Bind(eager);
|
{
|
||||||
eager.SectionPath = sectionPath;
|
var eager = new SecretsGrpcHubServerOptions { SectionPath = sectionPath };
|
||||||
eager.Validate();
|
section.Bind(eager);
|
||||||
|
eager.SectionPath = sectionPath;
|
||||||
|
eager.Validate();
|
||||||
|
}
|
||||||
|
|
||||||
// Singleton so the "unconfigured" warning is emitted once per process rather than once per
|
// Singleton so the "unconfigured" warning is emitted once per process rather than once per
|
||||||
// interceptor instance; the gRPC interceptor activator prefers a registered instance.
|
// interceptor instance; the gRPC interceptor activator prefers a registered instance.
|
||||||
|
|||||||
+15
-14
@@ -216,23 +216,24 @@ internal sealed class SecretsHubTestServer : IAsyncDisposable
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <c>AddZbSecretsGrpcHub</c> refuses this configuration at registration — which is the intended
|
/// <c>AddZbSecretsGrpcHub</c> refuses this configuration at registration — which is the intended
|
||||||
/// first line of defence and is pinned elsewhere — so the pieces are wired here the way that
|
/// first line of defence and is pinned elsewhere — so this path goes through the REAL composition
|
||||||
/// extension wires them, minus the eager validation. That is precisely the case the interceptor's
|
/// via its internal <c>validateEagerly: false</c> seam, changing nothing else. That is precisely
|
||||||
/// deny-all exists for: a host that built the options some other way. Without this path the
|
/// the case the interceptor's deny-all exists for: a host that built the options some other way.
|
||||||
/// second layer could only ever be tested against a hand-constructed interceptor, never over the
|
/// Without this path the second layer could only ever be tested against a hand-constructed
|
||||||
/// wire.
|
/// interceptor, never over the wire.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="store">The store the hub would serve.</param>
|
/// <param name="store">The store the hub would serve.</param>
|
||||||
/// <returns>The running hub.</returns>
|
/// <returns>The running hub.</returns>
|
||||||
public static Task<SecretsHubTestServer> StartWithNoTokenConfiguredAsync(ISecretStore store) =>
|
public static Task<SecretsHubTestServer> StartWithNoTokenConfiguredAsync(ISecretStore store)
|
||||||
StartAsync(store, services =>
|
{
|
||||||
{
|
IConfiguration configuration = new ConfigurationBuilder()
|
||||||
services.Configure<SecretsGrpcHubServerOptions>(options => options.BearerToken = string.Empty);
|
.AddInMemoryCollection(new Dictionary<string, string?>())
|
||||||
services.AddSingleton<SecretsHubAuthInterceptor>();
|
.Build();
|
||||||
services.AddGrpc()
|
|
||||||
.AddServiceOptions<SecretsHubService>(
|
return StartAsync(store, services =>
|
||||||
options => options.Interceptors.Add<SecretsHubAuthInterceptor>());
|
SecretsGrpcHubServerExtensions.AddZbSecretsGrpcHubCore(
|
||||||
});
|
services, configuration, "Secrets:GrpcHub", validateEagerly: false));
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task<SecretsHubTestServer> StartAsync(
|
private static async Task<SecretsHubTestServer> StartAsync(
|
||||||
ISecretStore store, Action<IServiceCollection> registerHub)
|
ISecretStore store, Action<IServiceCollection> registerHub)
|
||||||
|
|||||||
Reference in New Issue
Block a user