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:
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
||||
using ZB.MOM.WW.MxGateway.Worker.Conversion;
|
||||
using ZB.MOM.WW.MxGateway.Worker.Sta;
|
||||
@@ -912,30 +913,25 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
|
||||
$"AcknowledgeAlarm.alarm_guid is not a valid canonical GUID: '{payload.AlarmGuid}'.");
|
||||
}
|
||||
|
||||
// operator_password is a raw credential: it goes into AlarmAckOperator for
|
||||
// MXAccess AuthenticateUser and nowhere else. Never format the payload
|
||||
// (its generated ToString renders every field) or the exception text.
|
||||
try
|
||||
{
|
||||
int rc = alarmCommandHandler.Acknowledge(
|
||||
AlarmAckResult result = alarmCommandHandler.Acknowledge(
|
||||
alarmGuid,
|
||||
payload.Comment,
|
||||
payload.OperatorUser,
|
||||
payload.OperatorNode,
|
||||
payload.OperatorDomain,
|
||||
payload.OperatorFullName);
|
||||
MxCommandReply reply = CreateOkReply(command);
|
||||
reply.Hresult = rc;
|
||||
reply.AcknowledgeAlarm = new AcknowledgeAlarmReplyPayload
|
||||
{
|
||||
NativeStatus = rc,
|
||||
};
|
||||
if (rc != 0)
|
||||
{
|
||||
reply.DiagnosticMessage = $"AVEVA AlarmAckByGUID returned non-zero status {rc}.";
|
||||
}
|
||||
return reply;
|
||||
new AlarmAckOperator
|
||||
{
|
||||
UserName = payload.OperatorUser,
|
||||
Domain = payload.OperatorDomain,
|
||||
Password = payload.OperatorPassword,
|
||||
});
|
||||
return CreateAcknowledgeReply(command, result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return CreateAlarmFailureReply(command, ex);
|
||||
return CreateAcknowledgeFailureReply(command, ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -958,35 +954,80 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
|
||||
return CreateInvalidRequestReply(command, "AcknowledgeAlarmByName.alarm_name is required.");
|
||||
}
|
||||
|
||||
// See ExecuteAcknowledgeAlarm: operator_password never leaves AlarmAckOperator.
|
||||
try
|
||||
{
|
||||
int rc = alarmCommandHandler.AcknowledgeByName(
|
||||
AlarmAckResult result = alarmCommandHandler.AcknowledgeByName(
|
||||
payload.AlarmName,
|
||||
payload.ProviderName,
|
||||
payload.GroupName,
|
||||
payload.Comment,
|
||||
payload.OperatorUser,
|
||||
payload.OperatorNode,
|
||||
payload.OperatorDomain,
|
||||
payload.OperatorFullName);
|
||||
MxCommandReply reply = CreateOkReply(command);
|
||||
reply.Hresult = rc;
|
||||
reply.AcknowledgeAlarm = new AcknowledgeAlarmReplyPayload
|
||||
{
|
||||
NativeStatus = rc,
|
||||
};
|
||||
if (rc != 0)
|
||||
{
|
||||
reply.DiagnosticMessage = $"AVEVA AlarmAckByName returned non-zero status {rc}.";
|
||||
}
|
||||
return reply;
|
||||
new AlarmAckOperator
|
||||
{
|
||||
UserName = payload.OperatorUser,
|
||||
Domain = payload.OperatorDomain,
|
||||
Password = payload.OperatorPassword,
|
||||
});
|
||||
return CreateAcknowledgeReply(command, result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return CreateAlarmFailureReply(command, ex);
|
||||
return CreateAcknowledgeFailureReply(command, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static MxCommandReply CreateAcknowledgeReply(StaCommand command, AlarmAckResult result)
|
||||
{
|
||||
MxCommandReply reply = CreateOkReply(command);
|
||||
reply.Hresult = result.NativeStatus;
|
||||
reply.AcknowledgeAlarm = new AcknowledgeAlarmReplyPayload { NativeStatus = result.NativeStatus };
|
||||
reply.Statuses.Add(result.Statuses);
|
||||
reply.DiagnosticMessage = result.Diagnostic;
|
||||
switch (result.Outcome)
|
||||
{
|
||||
case AlarmAckOutcome.AlarmNotFound:
|
||||
case AlarmAckOutcome.NotAckable:
|
||||
reply.ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.InvalidRequest, Message = result.Diagnostic };
|
||||
break;
|
||||
case AlarmAckOutcome.AuthenticationFailed:
|
||||
case AlarmAckOutcome.WriteRejected:
|
||||
reply.ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.MxaccessFailure, Message = result.Diagnostic };
|
||||
break;
|
||||
}
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps an exception escaping an acknowledgement (e.g. the ack writer's
|
||||
/// MXAccess Register failing, or no active subscription) to an
|
||||
/// <see cref="ProtocolStatusCode.MxaccessFailure"/> reply. Only the
|
||||
/// exception type and HRESULT are reported: exception text on this path
|
||||
/// is not under our control and must not be able to echo operator input.
|
||||
/// </summary>
|
||||
private static MxCommandReply CreateAcknowledgeFailureReply(StaCommand command, Exception exception)
|
||||
{
|
||||
string diagnostic = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"Alarm acknowledgement failed: {0} (HRESULT 0x{1:X8}).",
|
||||
exception.GetType().FullName,
|
||||
exception.HResult);
|
||||
return new MxCommandReply
|
||||
{
|
||||
SessionId = command.SessionId,
|
||||
CorrelationId = command.CorrelationId,
|
||||
Kind = command.Kind,
|
||||
Hresult = exception.HResult,
|
||||
ProtocolStatus = new ProtocolStatus
|
||||
{
|
||||
Code = ProtocolStatusCode.MxaccessFailure,
|
||||
Message = diagnostic,
|
||||
},
|
||||
DiagnosticMessage = diagnostic,
|
||||
AcknowledgeAlarm = new AcknowledgeAlarmReplyPayload { NativeStatus = exception.HResult },
|
||||
};
|
||||
}
|
||||
|
||||
private MxCommandReply ExecuteQueryActiveAlarms(StaCommand command)
|
||||
{
|
||||
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.QueryActiveAlarmsCommand)
|
||||
|
||||
Reference in New Issue
Block a user