e15c3cb052
SEC-25 (near-term hardening; full per-session EventsHub ACL stays deferred to roadmap item 12): DashboardEventBroadcaster now redacts tag values from a deep CLONE of the event before mirroring to SignalR when Dashboard:ShowTagValues is false (the default). Clears MxEvent.value (covers OnDataChange/OnWriteComplete/ OperationComplete/OnBufferedDataChange — their bodies are empty discriminators, values ride in the top-level field incl. buffered arrays) and the alarm body's current_value/limit_value. Source event never mutated (shared with the gRPC path + replay ring) - verified by a source-not-mutated test. Makes the formerly dead ShowTagValues flag live for the mirror. EventsHub TODO(per-session-acl) kept and tied to roadmap item 12. Tests: DashboardEventBroadcasterTests (3). SEC-30: trim docs/Diagnostics.md to mark opt-in command-value logging as NOT YET IMPLEMENTED (no LogCommandValues knob, RedactCommandValue has no call sites, no values logged); wiring deferred pending secured-bulk redaction coverage (SEC-13). No option/call sites added. Server build clean (0 warnings); Dashboard tests 152/152. Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
94 lines
3.6 KiB
C#
94 lines
3.6 KiB
C#
using Microsoft.AspNetCore.SignalR;
|
|
using Microsoft.Extensions.Options;
|
|
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
using ZB.MOM.WW.MxGateway.Server.Configuration;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Server.Dashboard.Hubs;
|
|
|
|
/// <summary>
|
|
/// Broadcasts MxEvents to <see cref="EventsHub"/> clients subscribed to the
|
|
/// session's group. Fire-and-forget: we hand the send to the hub context
|
|
/// and return immediately so the source gRPC stream is never blocked.
|
|
/// Errors are logged once and dropped — keeping the SignalR mirror best-effort
|
|
/// preserves the gRPC contract that exists today.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// When <c>MxGateway:Dashboard:ShowTagValues</c> is false (the default), tag
|
|
/// values are stripped from a redacted copy of the event before it reaches any
|
|
/// dashboard client. The source <see cref="MxEvent"/> is shared with the gRPC
|
|
/// event path and the reconnect replay ring, so it is never mutated in place —
|
|
/// the redaction is applied to a deep clone. This closes the value-leak seam at
|
|
/// the mirror independently of the still-outstanding per-session hub ACL
|
|
/// (see <see cref="EventsHub"/>).
|
|
/// </remarks>
|
|
public sealed class DashboardEventBroadcaster(
|
|
IHubContext<EventsHub> hubContext,
|
|
IOptions<GatewayOptions> options,
|
|
ILogger<DashboardEventBroadcaster> logger) : IDashboardEventBroadcaster
|
|
{
|
|
private readonly bool _showTagValues = options.Value.Dashboard.ShowTagValues;
|
|
|
|
/// <inheritdoc />
|
|
public void Publish(string sessionId, MxEvent mxEvent)
|
|
{
|
|
if (string.IsNullOrEmpty(sessionId) || mxEvent is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
MxEvent outbound = _showTagValues ? mxEvent : RedactValues(mxEvent);
|
|
|
|
// Wrap the Task acquisition in a try/catch so a hypothetical synchronous throw
|
|
// from SendAsync (e.g. an implementation that throws before returning the Task)
|
|
// cannot escape Publish. The interface contract is never-throw; fire-and-forget.
|
|
Task send;
|
|
try
|
|
{
|
|
send = hubContext.Clients
|
|
.Group(EventsHub.GroupName(sessionId))
|
|
.SendAsync(EventsHub.EventMessage, outbound);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogDebug(ex, "Dashboard event mirror to session {SessionId} threw synchronously.", sessionId);
|
|
return;
|
|
}
|
|
|
|
if (!send.IsCompletedSuccessfully)
|
|
{
|
|
_ = send.ContinueWith(
|
|
t =>
|
|
{
|
|
if (t.Exception is { } ex)
|
|
{
|
|
logger.LogDebug(ex, "Dashboard event mirror to session {SessionId} failed.", sessionId);
|
|
}
|
|
},
|
|
TaskScheduler.Default);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Produces a deep clone of <paramref name="source"/> with every tag-value
|
|
/// field cleared, leaving tag reference, quality, status, and timestamps
|
|
/// intact so the dashboard still renders the event without the value. The
|
|
/// source event is left untouched because it is shared downstream with the
|
|
/// gRPC stream and the replay ring.
|
|
/// </summary>
|
|
/// <param name="source">The source event to redact a copy of.</param>
|
|
/// <returns>A redacted deep clone of the event.</returns>
|
|
private static MxEvent RedactValues(MxEvent source)
|
|
{
|
|
MxEvent redacted = source.Clone();
|
|
redacted.Value = null;
|
|
|
|
if (redacted.BodyCase == MxEvent.BodyOneofCase.OnAlarmTransition)
|
|
{
|
|
redacted.OnAlarmTransition.CurrentValue = null;
|
|
redacted.OnAlarmTransition.LimitValue = null;
|
|
}
|
|
|
|
return redacted;
|
|
}
|
|
}
|