feat(gateway): thread ClientCorrelationId into constraint-denial audit (§1.2)

This commit is contained in:
Joseph Doherty
2026-06-15 09:42:40 -04:00
parent 639e36b1bc
commit 8415f35abd
7 changed files with 84 additions and 15 deletions
@@ -69,7 +69,7 @@ public sealed class ConstraintEnforcerTests
CancellationToken.None);
Assert.NotNull(failure);
await enforcer.RecordDenialAsync(identity, "Write", "42", failure, CancellationToken.None);
await enforcer.RecordDenialAsync(identity, "Write", "42", failure, correlationId: null, CancellationToken.None);
AuditEvent auditEvent = Assert.Single(auditWriter.Events);
Assert.Equal("operator01", auditEvent.Actor);
@@ -83,6 +83,43 @@ public sealed class ConstraintEnforcerTests
Assert.Null(auditEvent.CorrelationId);
}
/// <summary>A denial carrying a parseable correlation id stores it on the audit record.</summary>
[Fact]
public async Task RecordDenialAsync_WithGuidCorrelationId_StoresCorrelationId()
{
ConstraintEnforcer enforcer = CreateEnforcer(out FakeAuditWriter auditWriter);
Guid correlationId = Guid.NewGuid();
await enforcer.RecordDenialAsync(
identity: null,
"Read",
"Secret.Tag",
new ConstraintFailure("read_scope", "Tag is outside the API key read scope."),
correlationId.ToString(),
CancellationToken.None);
AuditEvent auditEvent = Assert.Single(auditWriter.Events);
Assert.Equal(correlationId, auditEvent.CorrelationId);
}
/// <summary>A denial with a non-GUID correlation id leaves the audit correlation id null.</summary>
[Fact]
public async Task RecordDenialAsync_WithNonGuidCorrelationId_LeavesCorrelationIdNull()
{
ConstraintEnforcer enforcer = CreateEnforcer(out FakeAuditWriter auditWriter);
await enforcer.RecordDenialAsync(
identity: null,
"Read",
"Secret.Tag",
new ConstraintFailure("read_scope", "Tag is outside the API key read scope."),
"cli-xyz",
CancellationToken.None);
AuditEvent auditEvent = Assert.Single(auditWriter.Events);
Assert.Null(auditEvent.CorrelationId);
}
/// <summary>A denial with no identity records the canonical "anonymous" actor.</summary>
[Fact]
public async Task RecordDenialAsync_WithoutIdentity_UsesAnonymousActor()
@@ -94,6 +131,7 @@ public sealed class ConstraintEnforcerTests
"Read",
"Secret.Tag",
new ConstraintFailure("read_scope", "Tag is outside the API key read scope."),
correlationId: null,
CancellationToken.None);
AuditEvent auditEvent = Assert.Single(auditWriter.Events);