fix(auditlog): pull clients attach the site PSK inside TLS on https endpoints (ScadaBridge#108)

GrpcPullAuditEventsInvoker, GrpcPullSiteCallsInvoker and
GrpcPullTraceEventsInvoker called WithSiteCredentials without the endpoint,
so they composed the PSK over insecure channel credentials and threw at
channel construction for any https site address. They now pass the endpoint
(same change as 9138f3ed): https composes over SecureSsl, http is unchanged.

An internal ConfigureHandler seam lets tests trust a generated certificate;
production leaves it null.

Tests: real Kestrel HTTP/2-over-TLS with the real ControlPlaneAuthInterceptor
for all three pulls (correct PSK accepted, wrong PSK PermissionDenied,
untrusted certificate fails). Reverting the fix turns all five red.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 17:24:22 -05:00
parent 7912d793e2
commit 1ae4c7a4cb
5 changed files with 235 additions and 21 deletions
@@ -337,13 +337,26 @@ public sealed class GrpcPullAuditEventsInvoker
private GrpcChannel CreateChannel(string siteId, string endpoint) =>
GrpcChannel.ForAddress(endpoint, new GrpcChannelOptions
{
HttpHandler = new SocketsHttpHandler
{
KeepAlivePingDelay = _options.GrpcKeepAlivePingDelay,
KeepAlivePingTimeout = _options.GrpcKeepAlivePingTimeout,
KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always,
},
}.WithMessageLimits().WithSiteCredentials(_pskProvider, siteId));
HttpHandler = CreateHandler(),
}.WithMessageLimits().WithSiteCredentials(_pskProvider, siteId, endpoint));
/// <summary>
/// Test seam (ScadaBridge#108): lets a test trust a generated server certificate on the
/// handler. Production leaves it null, so the OS trust store applies.
/// </summary>
internal Action<SocketsHttpHandler>? ConfigureHandler { get; init; }
private SocketsHttpHandler CreateHandler()
{
var handler = new SocketsHttpHandler
{
KeepAlivePingDelay = _options.GrpcKeepAlivePingDelay,
KeepAlivePingTimeout = _options.GrpcKeepAlivePingTimeout,
KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always,
};
ConfigureHandler?.Invoke(handler);
return handler;
}
/// <inheritdoc />
public void RetainOnly(IReadOnlyList<SiteEntry> liveSites) =>
@@ -354,13 +354,26 @@ public sealed class GrpcPullSiteCallsInvoker
private GrpcChannel CreateChannel(string siteId, string endpoint) =>
GrpcChannel.ForAddress(endpoint, new GrpcChannelOptions
{
HttpHandler = new SocketsHttpHandler
{
KeepAlivePingDelay = _options.GrpcKeepAlivePingDelay,
KeepAlivePingTimeout = _options.GrpcKeepAlivePingTimeout,
KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always,
},
}.WithMessageLimits().WithSiteCredentials(_pskProvider, siteId));
HttpHandler = CreateHandler(),
}.WithMessageLimits().WithSiteCredentials(_pskProvider, siteId, endpoint));
/// <summary>
/// Test seam (ScadaBridge#108): lets a test trust a generated server certificate on the
/// handler. Production leaves it null, so the OS trust store applies.
/// </summary>
internal Action<SocketsHttpHandler>? ConfigureHandler { get; init; }
private SocketsHttpHandler CreateHandler()
{
var handler = new SocketsHttpHandler
{
KeepAlivePingDelay = _options.GrpcKeepAlivePingDelay,
KeepAlivePingTimeout = _options.GrpcKeepAlivePingTimeout,
KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always,
};
ConfigureHandler?.Invoke(handler);
return handler;
}
/// <inheritdoc />
public void RetainOnly(IReadOnlyList<SiteEntry> liveSites) =>
@@ -216,13 +216,26 @@ public sealed class GrpcPullTraceEventsInvoker : GrpcPullTraceEventsClient.IPull
private GrpcChannel CreateChannel(string siteId, string endpoint) =>
GrpcChannel.ForAddress(endpoint, new GrpcChannelOptions
{
HttpHandler = new SocketsHttpHandler
{
KeepAlivePingDelay = _options.GrpcKeepAlivePingDelay,
KeepAlivePingTimeout = _options.GrpcKeepAlivePingTimeout,
KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always,
},
}.WithMessageLimits().WithSiteCredentials(_pskProvider, siteId));
HttpHandler = CreateHandler(),
}.WithMessageLimits().WithSiteCredentials(_pskProvider, siteId, endpoint));
/// <summary>
/// Test seam (ScadaBridge#108): lets a test trust a generated server certificate on the
/// handler. Production leaves it null, so the OS trust store applies.
/// </summary>
internal Action<SocketsHttpHandler>? ConfigureHandler { get; init; }
private SocketsHttpHandler CreateHandler()
{
var handler = new SocketsHttpHandler
{
KeepAlivePingDelay = _options.GrpcKeepAlivePingDelay,
KeepAlivePingTimeout = _options.GrpcKeepAlivePingTimeout,
KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always,
};
ConfigureHandler?.Invoke(handler);
return handler;
}
/// <inheritdoc />
public void RetainOnly(IReadOnlyList<SiteEntry> liveSites) =>
@@ -33,6 +33,7 @@
<ItemGroup>
<InternalsVisibleTo Include="ZB.MOM.WW.ScadaBridge.AuditLog.Tests" />
<InternalsVisibleTo Include="ZB.MOM.WW.ScadaBridge.Host.Tests" />
</ItemGroup>
</Project>