fix(CLI-40): scrub the credential from the redacted error's structured reply, route MXACCESS_FAILURE to MxAccess (Rust), fix Go Subscribe terminal-error drop

Code-review follow-up on the CLI-40/41/44 branch.

ISSUE 1 (all five, critical): the message-only scrub still leaked the
server-echoed credential through the redacted error's structured reply accessor
(.NET Reply/Statuses, Java reply()/protocolStatus(), Go MxAccessError.Reply via
errors.As, Rust reply()/into_reply(), Python raw_reply). The redacted error now
carries a scrubbed clone of the reply (protocol_status.message,
diagnostic_message, statuses[].diagnostic_text), with per-language tests asserting
the reply accessor no longer contains the credential.

ISSUE 2 (Rust, critical): ensure_command_success routed MXACCESS_FAILURE to
Error::Command (unlike the other four clients), bypassing attach_secrets and
leaking via derived Debug/Display. MXACCESS_FAILURE now routes to Error::MxAccess,
fixing the cross-client inconsistency.

ISSUE 3 (Go, important): the CLI-44 terminal send was unconditionally
non-blocking, dropping a genuine terminal error under a full buffer on the
never-drop SubscribeEvents path. It is now reserved-slot-non-blocking only for the
cancel-on-overflow path and blocking for the never-drop path.

New shared fixture authenticate-user.echoed-credential-mxaccess-failure.reply.json
wired into all five suites. Minors: whitespace-secret guard on .NET/Java redact
helpers; Java preserves exception subtype on redaction; redaction-helper unit
tests (Go/Java/.NET). Docs (ClientBehaviorFixtures.md, ClientLibrariesDesign.md)
updated to make the structured-field claim true.
This commit is contained in:
Joseph Doherty
2026-08-07 07:04:56 -04:00
parent dc7fd16dd5
commit 0d874f91ee
25 changed files with 1190 additions and 88 deletions
+19 -3
View File
@@ -82,15 +82,31 @@ async def test_add_buffered_item_missing_payload_raises_malformed_reply() -> Non
assert captured.value.raw_reply is reply
@pytest.mark.parametrize(
"fixture",
[
"command-replies/authenticate-user.echoed-credential.reply.json",
"command-replies/authenticate-user.echoed-credential-mxaccess-failure.reply.json",
],
)
@pytest.mark.asyncio
async def test_authenticate_user_echoed_credential_is_scrubbed() -> None:
async def test_authenticate_user_echoed_credential_is_scrubbed(fixture: str) -> None:
credential = "sup3rSecretVerify9f3a2b"
reply = _load_reply("command-replies/authenticate-user.echoed-credential.reply.json")
reply = _load_reply(fixture)
session, _ = await _session_with([reply])
with pytest.raises(MxAccessError) as captured:
await session.authenticate_user(12, "operator", credential)
message = str(captured.value)
exc = captured.value
message = str(exc)
assert credential not in message
assert "[redacted]" in message
# The credential must not survive in the structured protobuf context either:
# a logger dumping raw_reply's fields would otherwise reintroduce the leak.
assert exc.raw_reply is not None
assert credential not in exc.raw_reply.protocol_status.message
assert credential not in exc.raw_reply.diagnostic_message
for status in exc.raw_reply.statuses:
assert credential not in status.diagnostic_text
@@ -140,10 +140,19 @@ async def test_write_secured_surfaces_native_failure_without_prior_authenticate(
with pytest.raises(MxAccessError) as captured:
await session.write_secured(12, 34, secret_value, current_user_id=5, verifier_user_id=6)
# Native failure is surfaced (not "fixed") and the raw reply is preserved...
assert captured.value.raw_reply is failure
# ...but the credential-sensitive value is scrubbed from the surfaced message.
# Native failure is surfaced (not "fixed"): the raw reply's structure is
# preserved so callers still see the native verdict...
raw = captured.value.raw_reply
assert raw is not None
assert raw.kind == pb.MX_COMMAND_KIND_WRITE_SECURED
assert raw.hresult == -2147217407
assert raw.protocol_status.code == pb.PROTOCOL_STATUS_CODE_MXACCESS_FAILURE
# ...but the credential-sensitive value is scrubbed from the surfaced message
# AND from the reply's echoed diagnostics, so a logger dumping raw_reply's
# structured fields cannot reintroduce the leak.
assert secret_value not in str(captured.value)
assert secret_value not in raw.protocol_status.message
assert "[redacted]" in raw.protocol_status.message
command = stub.invoke.requests[0].command
assert command.kind == pb.MX_COMMAND_KIND_WRITE_SECURED
assert command.write_secured.current_user_id == 5