feat(mesh): per-cluster PSK provider + central interceptor

This commit is contained in:
Joseph Doherty
2026-08-06 03:43:05 -04:00
parent fbda015c6e
commit 68a416e4fc
6 changed files with 1000 additions and 5 deletions
+19 -5
View File
@@ -18,6 +18,7 @@ using ZB.MOM.WW.OtOpcUa.Commons.Interfaces;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Services;
using ZB.MOM.WW.OtOpcUa.ControlPlane;
using ZB.MOM.WW.OtOpcUa.ControlPlane.Communication;
using ZB.MOM.WW.OtOpcUa.ControlPlane.Redundancy;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
@@ -526,6 +527,13 @@ if (hasAdmin)
// hosted service has built the system).
builder.Services.TryAddSingleton<Func<ActorSystem>>(sp => () => sp.GetRequiredService<ActorSystem>());
builder.Services.AddSingleton<IManualFailoverService, ManualFailoverService>();
// Per-cluster mesh PSKs (ClusterClient→gRPC). Admin-only, and singleton because its whole job is
// to hold a cache: central talks to N clusters and would otherwise re-read the secret store on
// every inbound ack. A node needs nothing of this — it holds exactly its own MeshTransport:GrpcPsk
// — so registering it fleet-wide would give a driver-only node a lookup it can never use.
// Depends on ISecretResolver, which AddOtOpcUaSecrets registers unconditionally below (DI
// registration order is irrelevant; resolution order is not).
builder.Services.AddSingleton<IMeshPskProvider, MeshPskProvider>();
}
// Registered unconditionally: driver-role nodes resolve Layer-B DriverConfig secrets and have no auth/DP/AdminUI.
@@ -548,11 +556,14 @@ builder.Services.AddOtOpcUaObservability(builder.Configuration);
// Registered whenever either role is present so an admin-only node still serves config and a
// driver-only node still gates sync + telemetry.
//
// MeshCommandAuthInterceptor (ClusterClient→gRPC migration) joins on the same terms: it gates the
// node-side NodeCommandService and is registered NOW, a pure pass-through until Phase 1 maps that
// service — an interceptor whose service is unmapped never sees a matching path. Registering it
// alongside the service it gates would put a security gate's wiring in the same commit as the write
// surface it protects; doing it here means the gate cannot be forgotten when that surface lights up.
// The two mesh-command interceptors (ClusterClient→gRPC migration) join on the same terms:
// MeshCommandAuthInterceptor gates the node-side NodeCommandService (driver-role, one key — this
// node's own), CentralCommandAuthInterceptor gates the central-side CentralCommandService
// (admin-role, one key per cluster via IMeshPskProvider). Both are registered NOW and are pure
// pass-throughs until Phase 1 maps those services — an interceptor whose service is unmapped never
// sees a matching path. Registering them alongside the services they gate would put a security
// gate's wiring in the same commit as the write surface it protects; doing it here means the gate
// cannot be forgotten when that surface lights up.
if (hasDriver || hasAdmin)
{
builder.Services.AddGrpc(o =>
@@ -564,7 +575,10 @@ if (hasDriver || hasAdmin)
o.Interceptors.Add<MeshCommandAuthInterceptor>();
}
if (hasAdmin)
{
o.Interceptors.Add<ConfigServeAuthInterceptor>();
o.Interceptors.Add<CentralCommandAuthInterceptor>();
}
});
}