feat(gateway): thread ClientCorrelationId into constraint-denial audit (§1.2)
This commit is contained in:
@@ -1607,6 +1607,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
|
|||||||
string commandKind,
|
string commandKind,
|
||||||
string target,
|
string target,
|
||||||
ConstraintFailure failure,
|
ConstraintFailure failure,
|
||||||
|
string? correlationId,
|
||||||
CancellationToken cancellationToken) => Task.CompletedTask;
|
CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
BulkConstraintPlan? bulkConstraintPlan = await ApplyConstraintsAsync(
|
BulkConstraintPlan? bulkConstraintPlan = await ApplyConstraintsAsync(
|
||||||
session,
|
session,
|
||||||
command,
|
command,
|
||||||
|
request.ClientCorrelationId,
|
||||||
context.CancellationToken)
|
context.CancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -279,17 +280,18 @@ public sealed class MxAccessGatewayService(
|
|||||||
private async Task<BulkConstraintPlan?> ApplyConstraintsAsync(
|
private async Task<BulkConstraintPlan?> ApplyConstraintsAsync(
|
||||||
GatewaySession session,
|
GatewaySession session,
|
||||||
MxCommand command,
|
MxCommand command,
|
||||||
|
string? correlationId,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
ApiKeyIdentity? identity = identityAccessor.Current;
|
ApiKeyIdentity? identity = identityAccessor.Current;
|
||||||
switch (command.Kind)
|
switch (command.Kind)
|
||||||
{
|
{
|
||||||
case MxCommandKind.AddItem:
|
case MxCommandKind.AddItem:
|
||||||
await EnforceReadTagAsync(identity, command.Kind, command.AddItem.ItemDefinition, cancellationToken)
|
await EnforceReadTagAsync(identity, command.Kind, command.AddItem.ItemDefinition, correlationId, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
return null;
|
return null;
|
||||||
case MxCommandKind.AddItem2:
|
case MxCommandKind.AddItem2:
|
||||||
await EnforceReadTagAsync(identity, command.Kind, command.AddItem2.ItemDefinition, cancellationToken)
|
await EnforceReadTagAsync(identity, command.Kind, command.AddItem2.ItemDefinition, correlationId, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
return null;
|
return null;
|
||||||
case MxCommandKind.AddItemBulk:
|
case MxCommandKind.AddItemBulk:
|
||||||
@@ -298,6 +300,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
command,
|
command,
|
||||||
command.AddItemBulk.ServerHandle,
|
command.AddItemBulk.ServerHandle,
|
||||||
command.AddItemBulk.TagAddresses,
|
command.AddItemBulk.TagAddresses,
|
||||||
|
correlationId,
|
||||||
cancellationToken)
|
cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
case MxCommandKind.SubscribeBulk:
|
case MxCommandKind.SubscribeBulk:
|
||||||
@@ -306,6 +309,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
command,
|
command,
|
||||||
command.SubscribeBulk.ServerHandle,
|
command.SubscribeBulk.ServerHandle,
|
||||||
command.SubscribeBulk.TagAddresses,
|
command.SubscribeBulk.TagAddresses,
|
||||||
|
correlationId,
|
||||||
cancellationToken)
|
cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
case MxCommandKind.AdviseItemBulk:
|
case MxCommandKind.AdviseItemBulk:
|
||||||
@@ -315,6 +319,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
command,
|
command,
|
||||||
command.AdviseItemBulk.ServerHandle,
|
command.AdviseItemBulk.ServerHandle,
|
||||||
command.AdviseItemBulk.ItemHandles,
|
command.AdviseItemBulk.ItemHandles,
|
||||||
|
correlationId,
|
||||||
cancellationToken)
|
cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
case MxCommandKind.ReadBulk:
|
case MxCommandKind.ReadBulk:
|
||||||
@@ -323,6 +328,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
command,
|
command,
|
||||||
command.ReadBulk.ServerHandle,
|
command.ReadBulk.ServerHandle,
|
||||||
command.ReadBulk.TagAddresses,
|
command.ReadBulk.TagAddresses,
|
||||||
|
correlationId,
|
||||||
cancellationToken)
|
cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
case MxCommandKind.WriteBulk:
|
case MxCommandKind.WriteBulk:
|
||||||
@@ -333,6 +339,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
command.WriteBulk.ServerHandle,
|
command.WriteBulk.ServerHandle,
|
||||||
command.WriteBulk.Entries,
|
command.WriteBulk.Entries,
|
||||||
entry => entry.ItemHandle,
|
entry => entry.ItemHandle,
|
||||||
|
correlationId,
|
||||||
cancellationToken)
|
cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
case MxCommandKind.Write2Bulk:
|
case MxCommandKind.Write2Bulk:
|
||||||
@@ -343,6 +350,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
command.Write2Bulk.ServerHandle,
|
command.Write2Bulk.ServerHandle,
|
||||||
command.Write2Bulk.Entries,
|
command.Write2Bulk.Entries,
|
||||||
entry => entry.ItemHandle,
|
entry => entry.ItemHandle,
|
||||||
|
correlationId,
|
||||||
cancellationToken)
|
cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
case MxCommandKind.WriteSecuredBulk:
|
case MxCommandKind.WriteSecuredBulk:
|
||||||
@@ -353,6 +361,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
command.WriteSecuredBulk.ServerHandle,
|
command.WriteSecuredBulk.ServerHandle,
|
||||||
command.WriteSecuredBulk.Entries,
|
command.WriteSecuredBulk.Entries,
|
||||||
entry => entry.ItemHandle,
|
entry => entry.ItemHandle,
|
||||||
|
correlationId,
|
||||||
cancellationToken)
|
cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
case MxCommandKind.WriteSecured2Bulk:
|
case MxCommandKind.WriteSecured2Bulk:
|
||||||
@@ -363,6 +372,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
command.WriteSecured2Bulk.ServerHandle,
|
command.WriteSecured2Bulk.ServerHandle,
|
||||||
command.WriteSecured2Bulk.Entries,
|
command.WriteSecured2Bulk.Entries,
|
||||||
entry => entry.ItemHandle,
|
entry => entry.ItemHandle,
|
||||||
|
correlationId,
|
||||||
cancellationToken)
|
cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
case MxCommandKind.Write:
|
case MxCommandKind.Write:
|
||||||
@@ -372,6 +382,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
command.Kind,
|
command.Kind,
|
||||||
command.Write.ServerHandle,
|
command.Write.ServerHandle,
|
||||||
command.Write.ItemHandle,
|
command.Write.ItemHandle,
|
||||||
|
correlationId,
|
||||||
cancellationToken)
|
cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
return null;
|
return null;
|
||||||
@@ -382,6 +393,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
command.Kind,
|
command.Kind,
|
||||||
command.Write2.ServerHandle,
|
command.Write2.ServerHandle,
|
||||||
command.Write2.ItemHandle,
|
command.Write2.ItemHandle,
|
||||||
|
correlationId,
|
||||||
cancellationToken)
|
cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
return null;
|
return null;
|
||||||
@@ -392,6 +404,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
command.Kind,
|
command.Kind,
|
||||||
command.WriteSecured.ServerHandle,
|
command.WriteSecured.ServerHandle,
|
||||||
command.WriteSecured.ItemHandle,
|
command.WriteSecured.ItemHandle,
|
||||||
|
correlationId,
|
||||||
cancellationToken)
|
cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
return null;
|
return null;
|
||||||
@@ -402,6 +415,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
command.Kind,
|
command.Kind,
|
||||||
command.WriteSecured2.ServerHandle,
|
command.WriteSecured2.ServerHandle,
|
||||||
command.WriteSecured2.ItemHandle,
|
command.WriteSecured2.ItemHandle,
|
||||||
|
correlationId,
|
||||||
cancellationToken)
|
cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
return null;
|
return null;
|
||||||
@@ -414,6 +428,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
ApiKeyIdentity? identity,
|
ApiKeyIdentity? identity,
|
||||||
MxCommandKind commandKind,
|
MxCommandKind commandKind,
|
||||||
string tagAddress,
|
string tagAddress,
|
||||||
|
string? correlationId,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
ConstraintFailure? failure = await constraintEnforcer
|
ConstraintFailure? failure = await constraintEnforcer
|
||||||
@@ -424,7 +439,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await constraintEnforcer.RecordDenialAsync(identity, commandKind.ToString(), tagAddress, failure, cancellationToken)
|
await constraintEnforcer.RecordDenialAsync(identity, commandKind.ToString(), tagAddress, failure, correlationId, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
throw new RpcException(new Status(StatusCode.PermissionDenied, failure.Message));
|
throw new RpcException(new Status(StatusCode.PermissionDenied, failure.Message));
|
||||||
}
|
}
|
||||||
@@ -435,6 +450,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
MxCommandKind commandKind,
|
MxCommandKind commandKind,
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
int itemHandle,
|
int itemHandle,
|
||||||
|
string? correlationId,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
ConstraintFailure? failure = await constraintEnforcer
|
ConstraintFailure? failure = await constraintEnforcer
|
||||||
@@ -445,7 +461,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await constraintEnforcer.RecordDenialAsync(identity, commandKind.ToString(), itemHandle.ToString(System.Globalization.CultureInfo.InvariantCulture), failure, cancellationToken)
|
await constraintEnforcer.RecordDenialAsync(identity, commandKind.ToString(), itemHandle.ToString(System.Globalization.CultureInfo.InvariantCulture), failure, correlationId, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
throw new RpcException(new Status(StatusCode.PermissionDenied, failure.Message));
|
throw new RpcException(new Status(StatusCode.PermissionDenied, failure.Message));
|
||||||
}
|
}
|
||||||
@@ -455,6 +471,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
MxCommand command,
|
MxCommand command,
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
IReadOnlyList<string> tagAddresses,
|
IReadOnlyList<string> tagAddresses,
|
||||||
|
string? correlationId,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Dictionary<int, SubscribeResult> denied = [];
|
Dictionary<int, SubscribeResult> denied = [];
|
||||||
@@ -471,7 +488,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await constraintEnforcer.RecordDenialAsync(identity, command.Kind.ToString(), tagAddress, failure, cancellationToken)
|
await constraintEnforcer.RecordDenialAsync(identity, command.Kind.ToString(), tagAddress, failure, correlationId, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
denied[index] = new SubscribeResult
|
denied[index] = new SubscribeResult
|
||||||
{
|
{
|
||||||
@@ -507,6 +524,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
MxCommand command,
|
MxCommand command,
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
IReadOnlyList<string> tagAddresses,
|
IReadOnlyList<string> tagAddresses,
|
||||||
|
string? correlationId,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Mirrors FilterTagBulkAsync but produces BulkReadResult denial entries
|
// Mirrors FilterTagBulkAsync but produces BulkReadResult denial entries
|
||||||
@@ -526,7 +544,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await constraintEnforcer.RecordDenialAsync(identity, command.Kind.ToString(), tagAddress, failure, cancellationToken)
|
await constraintEnforcer.RecordDenialAsync(identity, command.Kind.ToString(), tagAddress, failure, correlationId, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
denied[index] = new BulkReadResult
|
denied[index] = new BulkReadResult
|
||||||
{
|
{
|
||||||
@@ -557,6 +575,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
int serverHandle,
|
int serverHandle,
|
||||||
Google.Protobuf.Collections.RepeatedField<TEntry> entries,
|
Google.Protobuf.Collections.RepeatedField<TEntry> entries,
|
||||||
Func<TEntry, int> getItemHandle,
|
Func<TEntry, int> getItemHandle,
|
||||||
|
string? correlationId,
|
||||||
CancellationToken cancellationToken) where TEntry : class
|
CancellationToken cancellationToken) where TEntry : class
|
||||||
{
|
{
|
||||||
// The four bulk-write families each carry a different per-entry message
|
// The four bulk-write families each carry a different per-entry message
|
||||||
@@ -586,6 +605,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
command.Kind.ToString(),
|
command.Kind.ToString(),
|
||||||
itemHandle.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
itemHandle.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||||||
failure,
|
failure,
|
||||||
|
correlationId,
|
||||||
cancellationToken)
|
cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
denied[index] = new BulkWriteResult
|
denied[index] = new BulkWriteResult
|
||||||
@@ -637,6 +657,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
MxCommand command,
|
MxCommand command,
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
IReadOnlyList<int> itemHandles,
|
IReadOnlyList<int> itemHandles,
|
||||||
|
string? correlationId,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Dictionary<int, SubscribeResult> denied = [];
|
Dictionary<int, SubscribeResult> denied = [];
|
||||||
@@ -653,7 +674,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await constraintEnforcer.RecordDenialAsync(identity, command.Kind.ToString(), itemHandle.ToString(System.Globalization.CultureInfo.InvariantCulture), failure, cancellationToken)
|
await constraintEnforcer.RecordDenialAsync(identity, command.Kind.ToString(), itemHandle.ToString(System.Globalization.CultureInfo.InvariantCulture), failure, correlationId, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
denied[index] = new SubscribeResult
|
denied[index] = new SubscribeResult
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -120,20 +120,22 @@ public sealed class ConstraintEnforcer(
|
|||||||
/// <param name="commandKind">The command type (e.g., read, write).</param>
|
/// <param name="commandKind">The command type (e.g., read, write).</param>
|
||||||
/// <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">
|
||||||
|
/// The per-request client correlation id, if any. Persisted as the audit record's
|
||||||
|
/// <c>CorrelationId</c> when it parses as a GUID; a non-GUID value is dropped (left null).
|
||||||
|
/// </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(
|
||||||
ApiKeyIdentity? identity,
|
ApiKeyIdentity? identity,
|
||||||
string commandKind,
|
string commandKind,
|
||||||
string target,
|
string target,
|
||||||
ConstraintFailure failure,
|
ConstraintFailure failure,
|
||||||
|
string? correlationId,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Emit a canonical Denied AuditEvent directly through the best-effort IAuditWriter
|
// Emit a canonical Denied AuditEvent directly through the best-effort IAuditWriter
|
||||||
// (Task 2.3 #6): structured Target ("<commandKind>:<target>") and a richer DetailsJson
|
// (Task 2.3 #6): structured Target ("<commandKind>:<target>") and a richer DetailsJson
|
||||||
// envelope carrying constraint/message/commandKind/target.
|
// envelope carrying constraint/message/commandKind/target.
|
||||||
// TODO(Task 2.3): CorrelationId is left null here. Threading the per-request
|
|
||||||
// ClientCorrelationId down to RecordDenialAsync would require an invasive IConstraintEnforcer
|
|
||||||
// signature change across the gRPC call path; that is deferred to a follow-up.
|
|
||||||
AuditEvent auditEvent = new()
|
AuditEvent auditEvent = new()
|
||||||
{
|
{
|
||||||
EventId = Guid.NewGuid(),
|
EventId = Guid.NewGuid(),
|
||||||
@@ -144,7 +146,7 @@ public sealed class ConstraintEnforcer(
|
|||||||
Category = CanonicalForwardingApiKeyAuditStore.ApiKeyCategory,
|
Category = CanonicalForwardingApiKeyAuditStore.ApiKeyCategory,
|
||||||
Target = $"{commandKind}:{target}",
|
Target = $"{commandKind}:{target}",
|
||||||
SourceNode = null,
|
SourceNode = null,
|
||||||
CorrelationId = null,
|
CorrelationId = Guid.TryParse(correlationId, out var cid) ? cid : (Guid?)null,
|
||||||
DetailsJson = JsonSerializer.Serialize(new Dictionary<string, string>
|
DetailsJson = JsonSerializer.Serialize(new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
["constraint"] = failure.ConstraintName,
|
["constraint"] = failure.ConstraintName,
|
||||||
|
|||||||
@@ -45,11 +45,16 @@ public interface IConstraintEnforcer
|
|||||||
/// <param name="commandKind">The kind of command denied.</param>
|
/// <param name="commandKind">The kind of command denied.</param>
|
||||||
/// <param name="target">The target of the denied command.</param>
|
/// <param name="target">The target of the denied command.</param>
|
||||||
/// <param name="failure">The constraint failure details.</param>
|
/// <param name="failure">The constraint failure details.</param>
|
||||||
|
/// <param name="correlationId">
|
||||||
|
/// The per-request client correlation id, if any. Stored on the audit record's
|
||||||
|
/// <c>CorrelationId</c> when it parses as a GUID; otherwise left null.
|
||||||
|
/// </param>
|
||||||
/// <param name="cancellationToken">Token to observe for cancellation.</param>
|
/// <param name="cancellationToken">Token to observe for cancellation.</param>
|
||||||
Task RecordDenialAsync(
|
Task RecordDenialAsync(
|
||||||
ApiKeyIdentity? identity,
|
ApiKeyIdentity? identity,
|
||||||
string commandKind,
|
string commandKind,
|
||||||
string target,
|
string target,
|
||||||
ConstraintFailure failure,
|
ConstraintFailure failure,
|
||||||
|
string? correlationId,
|
||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ public sealed class ConstraintEnforcerTests
|
|||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
Assert.NotNull(failure);
|
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);
|
AuditEvent auditEvent = Assert.Single(auditWriter.Events);
|
||||||
Assert.Equal("operator01", auditEvent.Actor);
|
Assert.Equal("operator01", auditEvent.Actor);
|
||||||
@@ -83,6 +83,43 @@ public sealed class ConstraintEnforcerTests
|
|||||||
Assert.Null(auditEvent.CorrelationId);
|
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>
|
/// <summary>A denial with no identity records the canonical "anonymous" actor.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RecordDenialAsync_WithoutIdentity_UsesAnonymousActor()
|
public async Task RecordDenialAsync_WithoutIdentity_UsesAnonymousActor()
|
||||||
@@ -94,6 +131,7 @@ 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."),
|
||||||
|
correlationId: null,
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
AuditEvent auditEvent = Assert.Single(auditWriter.Events);
|
AuditEvent auditEvent = Assert.Single(auditWriter.Events);
|
||||||
|
|||||||
@@ -38,5 +38,6 @@ public sealed class AllowAllConstraintEnforcer : IConstraintEnforcer
|
|||||||
string commandKind,
|
string commandKind,
|
||||||
string target,
|
string target,
|
||||||
ConstraintFailure failure,
|
ConstraintFailure failure,
|
||||||
|
string? correlationId,
|
||||||
CancellationToken cancellationToken) => Task.CompletedTask;
|
CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ public sealed class PredicateConstraintEnforcer : IConstraintEnforcer
|
|||||||
/// <summary>Deny predicate keyed on (serverHandle, itemHandle) (returns true to deny).</summary>
|
/// <summary>Deny predicate keyed on (serverHandle, itemHandle) (returns true to deny).</summary>
|
||||||
public Func<int, int, bool> DenyWriteHandle { get; init; } = (_, _) => false;
|
public Func<int, int, bool> DenyWriteHandle { get; init; } = (_, _) => false;
|
||||||
|
|
||||||
/// <summary>Recorded denial messages — (commandKind, target) tuples.</summary>
|
/// <summary>Recorded denial messages — (commandKind, target, correlationId) tuples.</summary>
|
||||||
public List<(string CommandKind, string Target)> RecordedDenials { get; } = [];
|
public List<(string CommandKind, string Target, string? CorrelationId)> RecordedDenials { get; } = [];
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<ConstraintFailure?> CheckReadTagAsync(
|
public Task<ConstraintFailure?> CheckReadTagAsync(
|
||||||
@@ -81,9 +81,10 @@ public sealed class PredicateConstraintEnforcer : IConstraintEnforcer
|
|||||||
string commandKind,
|
string commandKind,
|
||||||
string target,
|
string target,
|
||||||
ConstraintFailure failure,
|
ConstraintFailure failure,
|
||||||
|
string? correlationId,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
RecordedDenials.Add((commandKind, target));
|
RecordedDenials.Add((commandKind, target, correlationId));
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user