fix(logging): fail-closed bearer redaction; hoist per-request logger creation

This commit is contained in:
Joseph Doherty
2026-08-15 12:17:27 -04:00
parent e2ac5d117a
commit ca34a2d65d
4 changed files with 182 additions and 73 deletions
@@ -24,6 +24,67 @@ public sealed class GatewayLogRedactorTests
Assert.DoesNotContain("super_secret_value", redacted);
}
/// <summary>
/// Verifies that a bearer credential the gateway does not issue is redacted too. A client that
/// pastes a JWT (or any other token) into the authorization header must not have it logged.
/// </summary>
[Fact]
public void RedactClientIdentity_RedactsNonGatewayBearerCredential()
{
const string token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIn0.c2lnbmF0dXJl";
string? redacted = GatewayLogRedactor.RedactClientIdentity($"Bearer {token}");
Assert.Equal("Bearer [redacted]", redacted);
Assert.DoesNotContain(token, redacted, StringComparison.Ordinal);
Assert.DoesNotContain("eyJ", redacted, StringComparison.Ordinal);
}
/// <summary>Verifies that a gateway API key keeps its key-id shape so an operator can still tell keys apart.</summary>
[Fact]
public void RedactClientIdentity_PreservesGatewayKeyIdShape()
{
string? redacted = GatewayLogRedactor.RedactClientIdentity("Bearer mxgw_operator01_super-secret");
Assert.Equal("Bearer mxgw_operator01_[redacted]", redacted);
Assert.DoesNotContain("super-secret", redacted, StringComparison.Ordinal);
}
/// <summary>
/// Verifies that anything not recognized as a gateway API key fails closed: the scheme word
/// survives only when it looks like an auth scheme, and the credential never does.
/// </summary>
/// <param name="clientIdentity">The raw client identity value.</param>
/// <param name="expected">The expected redacted value.</param>
[Theory]
[InlineData("Bearer", "[redacted]")]
[InlineData("Bearer ", "[redacted]")]
[InlineData("Basic dXNlcjpwYXNzd29yZA==", "Basic [redacted]")]
[InlineData("Negotiate YIIJvwYGKwYBBQUCoIIJ", "Negotiate [redacted]")]
[InlineData("mxgw_operator01_super-secret", "[redacted]")]
[InlineData("mxgw_operator01_super-secret trailing", "[redacted]")]
[InlineData("Bearer mxgw_operator01", "Bearer mxgw_[redacted]")]
[InlineData("Bearer mxgw_", "Bearer mxgw_[redacted]")]
[InlineData("Bearer mxgw__super-secret", "Bearer mxgw_[redacted]")]
[InlineData("bearer mxgw_operator01_super-secret", "bearer mxgw_operator01_[redacted]")]
[InlineData("anonymous", "[redacted]")]
[InlineData("some random junk", "[redacted]")]
public void RedactClientIdentity_FailsClosedForUnrecognizedCredentials(string clientIdentity, string expected)
{
Assert.Equal(expected, GatewayLogRedactor.RedactClientIdentity(clientIdentity));
}
/// <summary>Verifies that a blank client identity is passed through — there is nothing to redact.</summary>
/// <param name="clientIdentity">The raw client identity value.</param>
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void RedactClientIdentity_PassesThroughBlankValues(string? clientIdentity)
{
Assert.Equal(clientIdentity, GatewayLogRedactor.RedactClientIdentity(clientIdentity));
}
/// <summary>Verifies that IsCredentialBearingCommand identifies credential-bearing MXAccess commands.</summary>
/// <param name="commandMethod">Name of the MXAccess command method.</param>
[Theory]