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:
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user