dc7fd16dd5
CLI-40: port the exact-secret credential scrub to Rust/Java/.NET (Go/Python
already did it). AuthenticateUser/WriteSecured(2) helpers now redact the exact
caller-supplied secret from any surfaced error, as defense-in-depth on top of the
by-construction guarantee. Rust hand-writes a redacting Debug (derived Debug would
leak the reply); Java/.NET rebuild the same exception type with the redacted
message and do not carry the secret-bearing original forward (so ToString/stack
traces stay clean too).
CLI-41: uniform malformed-reply contract for AuthenticateUser/ArchestrAUserToId/
AddBufferedItem across all five clients — typed payload, else a present int32
return_value, else a typed malformed-reply error. Fixes Go/Java silent-0, .NET
NRE, and Rust's own internal inconsistency.
CLI-44: the Go event goroutine's Recv-error path now uses a non-blocking
sendTerminalEventResult on the reserved slot, so a genuine terminal stream error
is reported as itself instead of being mislabeled ErrSlowConsumer under overflow.
Riders from the CLI-37/38 review: (a) .NET ToDiagnosticSummary and Python
_mxaccess_message surface the raw success member (diagnostics-only parity with
Rust); (b) the status-conversion fixture carries an independent wantSuccess
boolean and the Go/.NET fixture tests assert against it instead of recomputing
the formula under test.
Shared fixtures (authenticate-user.{echoed-credential,missing-payload,
return-value-only}.reply.json) + manifest + ClientBehaviorFixtures.md +
ClientLibrariesDesign.md updated in the same change. Tracking: CLI-40/41/44 -> Done.
85 lines
4.2 KiB
C#
85 lines
4.2 KiB
C#
namespace ZB.MOM.WW.MxGateway.Client;
|
|
|
|
/// <summary>
|
|
/// Scrubs exact secret substrings out of diagnostic text before it leaves the client on an
|
|
/// exception path. MXAccess can echo a submitted credential or secured value back inside a
|
|
/// failure diagnostic (protocol message, MXSTATUS_PROXY diagnostic text, HRESULT description);
|
|
/// this helper replaces any such verbatim occurrence with <c><redacted></c> so the raw
|
|
/// request payload never reaches a caught exception's message. The marker matches the Go, Rust,
|
|
/// and Java clients.
|
|
/// </summary>
|
|
internal static class MxGatewaySecretRedaction
|
|
{
|
|
private const string Marker = "<redacted>";
|
|
|
|
/// <summary>
|
|
/// Replaces every non-null, non-empty secret in <paramref name="secrets"/> with the
|
|
/// redaction marker (ordinal comparison). Returns the message unchanged when it is null or
|
|
/// empty, or when no usable secret is supplied.
|
|
/// </summary>
|
|
/// <param name="message">The diagnostic message to scrub.</param>
|
|
/// <param name="secrets">The secret values to remove from the message.</param>
|
|
/// <returns>The scrubbed message.</returns>
|
|
internal static string Redact(string message, params string?[] secrets)
|
|
{
|
|
if (string.IsNullOrEmpty(message) || secrets is null)
|
|
{
|
|
return message;
|
|
}
|
|
|
|
string result = message;
|
|
foreach (string? secret in secrets)
|
|
{
|
|
if (!string.IsNullOrEmpty(secret))
|
|
{
|
|
result = result.Replace(secret, Marker, StringComparison.Ordinal);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns an exception equivalent to <paramref name="ex"/> but with any verbatim secret
|
|
/// scrubbed from its message. When nothing changes, the original exception is returned
|
|
/// unchanged; otherwise a new exception of the same concrete runtime type is built and the
|
|
/// original reply/status context is preserved. The secret-bearing original is deliberately
|
|
/// <b>not</b> chained as the inner exception — doing so would let its unredacted message
|
|
/// re-surface through <see cref="Exception.ToString"/> (which logging frameworks call). The
|
|
/// original's own inner cause (a transport error, never the request payload) is carried
|
|
/// forward instead.
|
|
/// </summary>
|
|
/// <param name="ex">The exception to redact.</param>
|
|
/// <param name="secrets">The secret values to remove from the message.</param>
|
|
/// <returns>The redacted exception, or the original when no change was needed.</returns>
|
|
internal static MxGatewayException Redacted(MxGatewayException ex, params string?[] secrets)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(ex);
|
|
|
|
string redacted = Redact(ex.Message, secrets);
|
|
if (string.Equals(redacted, ex.Message, StringComparison.Ordinal))
|
|
{
|
|
return ex;
|
|
}
|
|
|
|
Exception? cause = ex.InnerException;
|
|
return ex switch
|
|
{
|
|
MxAccessException access => new MxAccessException(redacted, access.Reply, cause),
|
|
MxGatewaySessionException => new MxGatewaySessionException(
|
|
redacted, ex.SessionId, ex.CorrelationId, ex.ProtocolStatus, ex.HResultCode, ex.Statuses, cause),
|
|
MxGatewayWorkerException => new MxGatewayWorkerException(
|
|
redacted, ex.SessionId, ex.CorrelationId, ex.ProtocolStatus, ex.HResultCode, ex.Statuses, cause),
|
|
MxGatewayAuthenticationException => new MxGatewayAuthenticationException(
|
|
redacted, ex.SessionId, ex.CorrelationId, ex.ProtocolStatus, ex.HResultCode, ex.Statuses, cause),
|
|
MxGatewayAuthorizationException => new MxGatewayAuthorizationException(
|
|
redacted, ex.SessionId, ex.CorrelationId, ex.ProtocolStatus, ex.HResultCode, ex.Statuses, cause),
|
|
MxGatewayMalformedReplyException => new MxGatewayMalformedReplyException(
|
|
redacted, ex.SessionId, ex.CorrelationId, ex.ProtocolStatus, ex.HResultCode, ex.Statuses, cause),
|
|
MxGatewayCommandException => new MxGatewayCommandException(
|
|
redacted, ex.SessionId, ex.CorrelationId, ex.ProtocolStatus, ex.HResultCode, ex.Statuses, cause),
|
|
_ => new MxGatewayException(redacted, cause),
|
|
};
|
|
}
|
|
}
|