feat(java-client): operator credentials on acknowledge-alarm

Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
This commit is contained in:
Joseph Doherty
2026-10-19 19:42:10 -04:00
parent 417a47bdb5
commit 84e04b6552
6 changed files with 1354 additions and 453 deletions
@@ -273,6 +273,11 @@ public final class MxGatewayClient implements AutoCloseable {
* worker's MXAccess session; the resulting native MxStatus is returned
* in the reply. Acks are idempotent at the MxAccess layer.
*
* <p>{@code operator_user} is required; the gateway authenticates it with
* {@code operator_password} (and optional {@code operator_domain}) and the
* Historian records that operator. An empty password is allowed. The
* password is scrubbed from any exception this method throws.
*
* @param request the {@code AcknowledgeAlarmRequest}
* @return the raw acknowledge reply
* @throws MxGatewayException on transport or protocol failure
@@ -283,10 +288,14 @@ public final class MxGatewayClient implements AutoCloseable {
MxGatewayErrors.ensureProtocolSuccess("acknowledge alarm", reply.getProtocolStatus(), null);
return reply;
} catch (RuntimeException error) {
if (error instanceof MxGatewayException) {
throw error;
MxGatewayException mapped = error instanceof MxGatewayException gatewayError
? gatewayError
: (MxGatewayException) MxGatewayErrors.fromGrpc("acknowledge alarm", error);
String secret = request.getOperatorPassword();
if (secret.isEmpty()) {
throw mapped;
}
throw MxGatewayErrors.fromGrpc("acknowledge alarm", error);
throw MxGatewayErrors.redacted(mapped, secret);
}
}
@@ -28,6 +28,43 @@ final class MxGatewayErrors {
return new MxGatewayException("gateway " + operation + " failed: " + error.getMessage(), error);
}
/**
* Rebuilds a gateway exception with an exact secret scrubbed from its
* message and protocol status. The original is not chained as a cause
* because its message and cause chain may carry the secret.
*/
static MxGatewayException redacted(MxGatewayException error, String secret) {
String message = MxGatewaySecrets.redactExact(error.getMessage(), secret);
if (error instanceof MxAccessException ex) {
return new MxAccessException(message, scrub(ex.protocolStatus(), secret), ex.reply(), null);
}
if (error instanceof MxGatewayCommandException ex) {
return new MxGatewayCommandException(message, scrub(ex.protocolStatus(), secret), ex.reply(), null);
}
if (error instanceof MxGatewaySessionException ex) {
return new MxGatewaySessionException(message, scrub(ex.protocolStatus(), secret), null);
}
if (error instanceof MxGatewayWorkerException ex) {
return new MxGatewayWorkerException(message, scrub(ex.protocolStatus(), secret), null);
}
if (error instanceof MxGatewayAuthenticationException) {
return new MxGatewayAuthenticationException(message, null);
}
if (error instanceof MxGatewayAuthorizationException) {
return new MxGatewayAuthorizationException(message, null);
}
return new MxGatewayException(message);
}
private static ProtocolStatus scrub(ProtocolStatus status, String secret) {
if (status == null) {
return null;
}
return status.toBuilder()
.setMessage(MxGatewaySecrets.redactExact(status.getMessage(), secret))
.build();
}
static void ensureProtocolSuccess(String operation, ProtocolStatus status, MxCommandReply reply) {
if (status == null || status.getCode() == ProtocolStatusCode.PROTOCOL_STATUS_CODE_OK) {
return;