feat(worker): route alarm acks through the operator-authenticated AckMsg writer
AlarmCommandHandler -> AlarmDispatcher -> IAlarmAckWriter: the dispatcher resolves the ack item through the consumer and the writer authenticates the operator and writes the comment. The executor builds AlarmAckOperator from operator_user/domain/password, maps AlarmAckOutcome to the reply (Applied/Unconfirmed Ok, AlarmNotFound/NotAckable InvalidRequest, AuthenticationFailed/WriteRejected MxaccessFailure) and reports escaping exceptions by type and HRESULT only, so operator_password cannot leak. Removes the inert managed/WNAL alarm-client ack path. Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
This commit is contained in:
@@ -86,7 +86,12 @@ public sealed class AlarmsLiveSmokeTests
|
||||
new WNWRAPCONSUMERLib.wwAlarmConsumerClass(),
|
||||
maxAlarmsPerFetch: 1024);
|
||||
MxAccessAlarmEventSink sink = new MxAccessAlarmEventSink(queue, new MxAccessEventMapper());
|
||||
using AlarmDispatcher dispatcher = new AlarmDispatcher(consumer, sink, SessionId);
|
||||
using AlarmAckWriter ackWriter = new AlarmAckWriter(
|
||||
new MxAccessComObjectFactory(),
|
||||
() => new ZB.MOM.WW.MxGateway.Worker.Sta.StaMessagePump().PumpPendingMessages(),
|
||||
TimeSpan.FromSeconds(5),
|
||||
AlarmAckWriter.DefaultClientName);
|
||||
using AlarmDispatcher dispatcher = new AlarmDispatcher(consumer, sink, SessionId, ackWriter);
|
||||
|
||||
Log("Constructed consumer + sink + dispatcher.");
|
||||
dispatcher.Subscribe(SubscriptionExpression);
|
||||
@@ -139,26 +144,23 @@ public sealed class AlarmsLiveSmokeTests
|
||||
$"Captured reference '{raiseBody.AlarmFullReference}' did not parse as Provider!Group.Tag.");
|
||||
Log($"Ack target: provider='{provider}' group='{group}' name='{alarmName}'");
|
||||
|
||||
// Try the ack with real Windows identity. AVEVA's AlarmAckByName
|
||||
// may reject synthetic operator strings; using the current process
|
||||
// identity gives the alarm-history a recognizable principal.
|
||||
string realUser = Environment.UserName;
|
||||
string realNode = Environment.MachineName;
|
||||
string realDomain = Environment.UserDomainName ?? string.Empty;
|
||||
Log($"Ack identity: user='{realUser}' node='{realNode}' domain='{realDomain}'");
|
||||
// The ack authenticates a Galaxy operator and writes <tag>.AckMsg.
|
||||
// Operator credentials come from the environment (never logged);
|
||||
// the user defaults to the current process identity.
|
||||
AlarmAckOperator ackOperator = new AlarmAckOperator
|
||||
{
|
||||
UserName = Environment.GetEnvironmentVariable("MXGATEWAY_PROBE_ACK_USER") ?? Environment.UserName,
|
||||
Password = Environment.GetEnvironmentVariable("MXGATEWAY_PROBE_ACK_PASSWORD") ?? string.Empty,
|
||||
};
|
||||
Log($"Ack identity: {ackOperator}");
|
||||
|
||||
int rc = dispatcher.AcknowledgeByName(
|
||||
alarmName: alarmName,
|
||||
providerName: provider,
|
||||
groupName: group,
|
||||
ackComment: "alarms-live-smoke ack",
|
||||
ackOperatorName: realUser,
|
||||
ackOperatorNode: realNode,
|
||||
ackOperatorDomain: realDomain,
|
||||
ackOperatorFullName: realUser);
|
||||
Log($"AcknowledgeByName(real identity) -> rc={rc}");
|
||||
AlarmAckResult result = dispatcher.AcknowledgeByName(
|
||||
alarmName, provider, group, "alarms-live-smoke ack", ackOperator);
|
||||
Log($"AcknowledgeByName -> outcome={result.Outcome} native={result.NativeStatus} diag='{result.Diagnostic}'");
|
||||
|
||||
Assert.Equal(0, rc);
|
||||
Assert.True(
|
||||
result.Outcome == AlarmAckOutcome.Applied || result.Outcome == AlarmAckOutcome.Unconfirmed,
|
||||
$"Ack outcome {result.Outcome}: {result.Diagnostic}");
|
||||
|
||||
// 4. Wait for the post-ack transition. With the alarm flipping every
|
||||
// 10s and the consumer polling every 500ms, the next state
|
||||
|
||||
Reference in New Issue
Block a user