feat(secrets)!: retire the gRPC secrets hub in ScadaBridge — sites get secrets via Deploy Artifacts; rig central on the shared SQL store

This commit is contained in:
Joseph Doherty
2026-09-28 19:52:26 -04:00
parent 52cd4fb096
commit 2bac13170c
11 changed files with 333 additions and 1181 deletions
@@ -5,12 +5,8 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.LocalDb.Replication;
using ZB.MOM.WW.Secrets.Replicator.Grpc;
using ZB.MOM.WW.Secrets.Replicator.Grpc.DependencyInjection;
namespace ZB.MOM.WW.ScadaBridge.Host.Tests;
@@ -21,14 +17,7 @@ public class MappedGrpcAuthenticationCoverageTests
public async Task EveryMappedGrpcService_IsCoveredIncludingExternalMappings()
{
var builder = WebApplication.CreateBuilder();
builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
["Secrets:Replication:Enabled"] = "true",
["Secrets:Replication:Mode"] = "Grpc",
["Secrets:GrpcHub:BearerToken"] = "coverage-test-token",
});
builder.Services.AddGrpc();
builder.Services.AddZbSecretsGrpcHub(builder.Configuration, "Secrets:GrpcHub");
await using var app = builder.Build();
// Discover the production map calls, not a hand-maintained three-service type list.
@@ -51,17 +40,19 @@ public class MappedGrpcAuthenticationCoverageTests
map.MakeGenericMethod(type).Invoke(null, [app]);
}
// Exercise the production extension mappings too; these packages hide their MapGrpcService calls.
// Exercise the production extension mappings too; this package hides its MapGrpcService call.
Assert.Contains(hostSources.SelectMany(s => s.DescendantNodes().OfType<InvocationExpressionSyntax>()),
n => n.Expression.ToString().EndsWith(".MapZbLocalDbSync", StringComparison.Ordinal));
app.MapZbLocalDbSync();
app.MapScadaBridgeSecretsHub(builder.Configuration);
var metadata = ((IEndpointRouteBuilder)app).DataSources.SelectMany(s => s.Endpoints)
.Select(e => e.Metadata.GetMetadata<GrpcMethodMetadata>()).Where(m => m is not null).ToList();
Assert.NotEmpty(metadata);
var serviceTypes = metadata.Select(m => m!.ServiceType).Distinct().ToList();
Assert.True(serviceTypes.Count >= typeNames.Count + 2, "External LocalDb and Secrets mappings must both be exercised.");
Assert.True(serviceTypes.Count >= typeNames.Count + 1, "The external LocalDb mapping must also be exercised.");
// The secrets hub — which used to install an all-method interceptor on its own service
// options and so needed a separate gate check here — is retired (Task 11): there is no
// longer a mapped gRPC service whose only gate is anything other than one of these prefixes.
var prefixes = ControlPlaneAuthInterceptor.DefaultGatedPrefixes
.Concat(CentralControlAuthInterceptor.DefaultGatedPrefixes)
.Append((string)typeof(LocalDbSyncAuthInterceptor).GetField("ServicePrefix", BindingFlags.Static | BindingFlags.NonPublic)!.GetRawConstantValue()!)
@@ -78,21 +69,14 @@ public class MappedGrpcAuthenticationCoverageTests
Assert.NotEmpty(binder.Methods);
Assert.Equal(metadata.Where(m => m!.ServiceType == type).Select(m => m!.Method.FullName).Order(), binder.Methods.Order());
// Secrets installs an all-method interceptor on its own service options. Inspect actual DI options.
var optionsType = typeof(IOptions<>).MakeGenericType(typeof(GrpcServiceOptions<>).MakeGenericType(type));
var options = app.Services.GetRequiredService(optionsType);
var serviceOptions = (GrpcServiceOptions)optionsType.GetProperty("Value")!.GetValue(options)!;
var hasSecretsGate = serviceOptions.Interceptors.Any(i => i.Type == typeof(SecretsHubAuthInterceptor));
Assert.All(binder.Methods, method => Assert.True(
hasSecretsGate || prefixes.Any(prefix => method.StartsWith(prefix, StringComparison.Ordinal)),
prefixes.Any(prefix => method.StartsWith(prefix, StringComparison.Ordinal)),
$"Mapped service {type.FullName} method {method} has no authentication interceptor gate."));
if (!hasSecretsGate)
{
// Negative control uses the real service's methods: removing its gate must be detected.
var removed = prefixes.First(p => binder.Methods.Any(m => m.StartsWith(p, StringComparison.Ordinal)));
var remaining = prefixes.Where(p => p != removed).ToArray();
Assert.Contains(binder.Methods, method => !remaining.Any(p => method.StartsWith(p, StringComparison.Ordinal)));
}
// Negative control uses the real service's methods: removing its gate must be detected.
var removed = prefixes.First(p => binder.Methods.Any(m => m.StartsWith(p, StringComparison.Ordinal)));
var remaining = prefixes.Where(p => p != removed).ToArray();
Assert.Contains(binder.Methods, method => !remaining.Any(p => method.StartsWith(p, StringComparison.Ordinal)));
}
}