fix(gateway): preserve raw client correlation id in denial audit DetailsJson + add wiring test (§1.2)
This commit is contained in:
@@ -121,8 +121,10 @@ public sealed class ConstraintEnforcer(
|
|||||||
/// <param name="target">The target being accessed (tag address or handle).</param>
|
/// <param name="target">The target being accessed (tag address or handle).</param>
|
||||||
/// <param name="failure">The constraint failure details.</param>
|
/// <param name="failure">The constraint failure details.</param>
|
||||||
/// <param name="correlationId">
|
/// <param name="correlationId">
|
||||||
/// The per-request client correlation id, if any. Persisted as the audit record's
|
/// The per-request client correlation id, if any. Persisted as the audit record's typed
|
||||||
/// <c>CorrelationId</c> when it parses as a GUID; a non-GUID value is dropped (left null).
|
/// <c>CorrelationId</c> when it parses as a GUID; a non-GUID value leaves that column null.
|
||||||
|
/// The raw string is always preserved in <c>DetailsJson["clientCorrelationId"]</c> so a
|
||||||
|
/// non-GUID id (e.g. from Rust/Python/Java clients) is never silently lost.
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <param name="cancellationToken">Token to observe for cancellation.</param>
|
/// <param name="cancellationToken">Token to observe for cancellation.</param>
|
||||||
public async Task RecordDenialAsync(
|
public async Task RecordDenialAsync(
|
||||||
@@ -153,6 +155,11 @@ public sealed class ConstraintEnforcer(
|
|||||||
["message"] = failure.Message,
|
["message"] = failure.Message,
|
||||||
["commandKind"] = commandKind,
|
["commandKind"] = commandKind,
|
||||||
["target"] = target,
|
["target"] = target,
|
||||||
|
// Always preserve the raw client correlation id here so it is never silently
|
||||||
|
// lost: the typed CorrelationId column only retains GUID-parseable ids, but
|
||||||
|
// clients (Rust/Python/Java) commonly send non-GUID or empty trace ids. The
|
||||||
|
// raw id is a client trace id, not a secret, so storing it is fine.
|
||||||
|
["clientCorrelationId"] = correlationId ?? "",
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -548,6 +548,33 @@ public sealed class MxAccessGatewayServiceConstraintTests
|
|||||||
Assert.Equal("42", enforcer.RecordedDenials[0].Target);
|
Assert.Equal("42", enforcer.RecordedDenials[0].Target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// End-to-end wiring (M-2): the per-request <c>ClientCorrelationId</c> must propagate
|
||||||
|
/// all the way through <c>Invoke</c> -> <c>ApplyConstraintsAsync</c> -> the unary write
|
||||||
|
/// enforce helper -> <c>RecordDenialAsync</c>, so the recorded denial carries the exact
|
||||||
|
/// id the client sent (including non-GUID trace ids used by Rust/Python/Java clients).
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task Invoke_Write_WithDeniedHandle_ThreadsClientCorrelationIdIntoRecordedDenial()
|
||||||
|
{
|
||||||
|
const string CorrelationId = "rust-client-Write-7";
|
||||||
|
PredicateConstraintEnforcer enforcer = new()
|
||||||
|
{
|
||||||
|
DenyWriteHandle = (serverHandle, itemHandle) => serverHandle == 7 && itemHandle == 42,
|
||||||
|
};
|
||||||
|
FakeSessionManager sessionManager = CreateSessionManagerWithSeed();
|
||||||
|
MxAccessGatewayService service = CreateService(sessionManager, enforcer);
|
||||||
|
|
||||||
|
MxCommandRequest request = CreateWriteRequest(serverHandle: 7, itemHandle: 42);
|
||||||
|
request.ClientCorrelationId = CorrelationId;
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<RpcException>(
|
||||||
|
async () => await service.Invoke(request, new TestServerCallContext()));
|
||||||
|
|
||||||
|
Assert.Single(enforcer.RecordedDenials);
|
||||||
|
Assert.Equal(CorrelationId, enforcer.RecordedDenials[0].CorrelationId);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unary <c>WriteSecured</c> against a denied handle takes the same enforce path
|
/// Unary <c>WriteSecured</c> against a denied handle takes the same enforce path
|
||||||
/// and rejects identically — proving the four-arm switch in
|
/// and rejects identically — proving the four-arm switch in
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Text.Json;
|
||||||
using ZB.MOM.WW.Audit;
|
using ZB.MOM.WW.Audit;
|
||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
|
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
|
||||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
||||||
@@ -102,9 +103,12 @@ public sealed class ConstraintEnforcerTests
|
|||||||
Assert.Equal(correlationId, auditEvent.CorrelationId);
|
Assert.Equal(correlationId, auditEvent.CorrelationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>A denial with a non-GUID correlation id leaves the audit correlation id null.</summary>
|
/// <summary>
|
||||||
|
/// A denial with a non-GUID correlation id leaves the typed audit correlation id null but
|
||||||
|
/// still preserves the raw client correlation id in DetailsJson so it is not lost.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RecordDenialAsync_WithNonGuidCorrelationId_LeavesCorrelationIdNull()
|
public async Task RecordDenialAsync_WithNonGuidCorrelationId_LeavesCorrelationIdNullButPreservesRawInDetails()
|
||||||
{
|
{
|
||||||
ConstraintEnforcer enforcer = CreateEnforcer(out FakeAuditWriter auditWriter);
|
ConstraintEnforcer enforcer = CreateEnforcer(out FakeAuditWriter auditWriter);
|
||||||
|
|
||||||
@@ -113,11 +117,17 @@ public sealed class ConstraintEnforcerTests
|
|||||||
"Read",
|
"Read",
|
||||||
"Secret.Tag",
|
"Secret.Tag",
|
||||||
new ConstraintFailure("read_scope", "Tag is outside the API key read scope."),
|
new ConstraintFailure("read_scope", "Tag is outside the API key read scope."),
|
||||||
"cli-xyz",
|
"rust-client-Write-7",
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
AuditEvent auditEvent = Assert.Single(auditWriter.Events);
|
AuditEvent auditEvent = Assert.Single(auditWriter.Events);
|
||||||
Assert.Null(auditEvent.CorrelationId);
|
Assert.Null(auditEvent.CorrelationId);
|
||||||
|
Assert.NotNull(auditEvent.DetailsJson);
|
||||||
|
|
||||||
|
Dictionary<string, string>? details =
|
||||||
|
JsonSerializer.Deserialize<Dictionary<string, string>>(auditEvent.DetailsJson);
|
||||||
|
Assert.NotNull(details);
|
||||||
|
Assert.Equal("rust-client-Write-7", details["clientCorrelationId"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>A denial with no identity records the canonical "anonymous" actor.</summary>
|
/// <summary>A denial with no identity records the canonical "anonymous" actor.</summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user