From 9907a97cb084a1a181b2f5bf8b0035684f07b26f Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 19 Oct 2026 19:42:58 -0400 Subject: [PATCH] fix(java-client): redact operator password on async acknowledge-alarm failures Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT --- .../ww/mxgateway/client/MxGatewayClient.java | 16 ++++++- .../client/MxGatewayCredentialReplyTests.java | 46 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/clients/java/zb-mom-ww-mxgateway-client/src/main/java/com/zb/mom/ww/mxgateway/client/MxGatewayClient.java b/clients/java/zb-mom-ww-mxgateway-client/src/main/java/com/zb/mom/ww/mxgateway/client/MxGatewayClient.java index aa23f6a..59798cb 100644 --- a/clients/java/zb-mom-ww-mxgateway-client/src/main/java/com/zb/mom/ww/mxgateway/client/MxGatewayClient.java +++ b/clients/java/zb-mom-ww-mxgateway-client/src/main/java/com/zb/mom/ww/mxgateway/client/MxGatewayClient.java @@ -308,10 +308,24 @@ public final class MxGatewayClient implements AutoCloseable { */ public CompletableFuture acknowledgeAlarmAsync(AcknowledgeAlarmRequest request) { CompletableFuture future = toCompletable(rawFutureStub().acknowledgeAlarm(request)); - return future.thenApply(reply -> { + String secret = request.getOperatorPassword(); + CompletableFuture checked = future.thenApply(reply -> { MxGatewayErrors.ensureProtocolSuccess("acknowledge alarm", reply.getProtocolStatus(), null); return reply; }); + if (secret.isEmpty()) { + return checked; + } + // Scrub the operator password from any failure surfaced through the future. + return checked.exceptionally(error -> { + Throwable cause = error instanceof java.util.concurrent.CompletionException && error.getCause() != null + ? error.getCause() + : error; + if (cause instanceof MxGatewayException gatewayError) { + throw new java.util.concurrent.CompletionException(MxGatewayErrors.redacted(gatewayError, secret)); + } + throw new java.util.concurrent.CompletionException(cause); + }); } /** diff --git a/clients/java/zb-mom-ww-mxgateway-client/src/test/java/com/zb/mom/ww/mxgateway/client/MxGatewayCredentialReplyTests.java b/clients/java/zb-mom-ww-mxgateway-client/src/test/java/com/zb/mom/ww/mxgateway/client/MxGatewayCredentialReplyTests.java index 1657a84..e8a99f5 100644 --- a/clients/java/zb-mom-ww-mxgateway-client/src/test/java/com/zb/mom/ww/mxgateway/client/MxGatewayCredentialReplyTests.java +++ b/clients/java/zb-mom-ww-mxgateway-client/src/test/java/com/zb/mom/ww/mxgateway/client/MxGatewayCredentialReplyTests.java @@ -74,6 +74,52 @@ final class MxGatewayCredentialReplyTests { } } + @Test + void acknowledgeAlarmAsyncRedactsOperatorPasswordFromFailure() throws Exception { + String serverName = "mxgw-java-ack-" + UUID.randomUUID(); + MxAccessGatewayGrpc.MxAccessGatewayImplBase service = new MxAccessGatewayGrpc.MxAccessGatewayImplBase() { + @Override + public void acknowledgeAlarm( + mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest request, + StreamObserver observer) { + observer.onNext(mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply.newBuilder() + .setProtocolStatus(ProtocolStatus.newBuilder() + .setCode(ProtocolStatusCode.PROTOCOL_STATUS_CODE_MXACCESS_FAILURE) + .setMessage("auth failed for operator1 password " + CREDENTIAL)) + .build()); + observer.onCompleted(); + } + }; + Server server = InProcessServerBuilder.forName(serverName) + .directExecutor().addService(service).build().start(); + ManagedChannel channel = InProcessChannelBuilder.forName(serverName).directExecutor().build(); + try (MxGatewayClient client = new MxGatewayClient( + channel, + MxGatewayClientOptions.builder() + .endpoint("in-process").apiKey("").plaintext(true) + .callTimeout(Duration.ofSeconds(5)).build())) { + var request = mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest.newBuilder() + .setAlarmFullReference("Tank01.Level.HiHi") + .setOperatorUser("operator1") + .setOperatorPassword(CREDENTIAL) + .build(); + + java.util.concurrent.ExecutionException async = assertThrows( + java.util.concurrent.ExecutionException.class, + () -> client.acknowledgeAlarmAsync(request).get()); + assertTrue(async.getCause() instanceof MxGatewayException, String.valueOf(async.getCause())); + assertFalse(async.getCause().getMessage().contains(CREDENTIAL)); + assertFalse(((MxGatewayCommandException) async.getCause()).protocolStatus().getMessage() + .contains(CREDENTIAL)); + + MxGatewayException sync = assertThrows(MxGatewayException.class, () -> client.acknowledgeAlarm(request)); + assertFalse(sync.getMessage().contains(CREDENTIAL)); + } finally { + channel.shutdownNow(); + server.shutdownNow(); + } + } + @Test void authenticateUserMissingPayloadThrowsMalformedReply() throws Exception { MxCommandReply reply = loadReply("authenticate-user.missing-payload.reply.json");