feat(audit): MxGateway local producers (dashboard + constraint-denial) emit canonical AuditEvent with Target/CorrelationId (Task 2.3 #6)
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
using ZB.MOM.WW.Auth.Abstractions.ApiKeys;
|
||||
using System.Text.Json;
|
||||
using ZB.MOM.WW.Audit;
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
|
||||
using ZB.MOM.WW.MxGateway.Server.Galaxy;
|
||||
using ZB.MOM.WW.MxGateway.Server.Security.Audit;
|
||||
using ZB.MOM.WW.MxGateway.Server.Security.Authentication;
|
||||
using ZB.MOM.WW.MxGateway.Server.Sessions;
|
||||
|
||||
@@ -12,7 +14,7 @@ namespace ZB.MOM.WW.MxGateway.Server.Security.Authorization;
|
||||
|
||||
public sealed class ConstraintEnforcer(
|
||||
IGalaxyHierarchyCache cache,
|
||||
IApiKeyAuditStore auditStore) : IConstraintEnforcer
|
||||
IAuditWriter auditWriter) : IConstraintEnforcer
|
||||
{
|
||||
/// <summary>Checks read constraints on a tag address.</summary>
|
||||
/// <param name="identity">The API key identity to check constraints for.</param>
|
||||
@@ -126,15 +128,33 @@ public sealed class ConstraintEnforcer(
|
||||
ConstraintFailure failure,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await auditStore.AppendAsync(
|
||||
new ApiKeyAuditEntry(
|
||||
KeyId: identity?.KeyId,
|
||||
EventType: "constraint-denied",
|
||||
RemoteAddress: null,
|
||||
CreatedUtc: DateTimeOffset.UtcNow,
|
||||
Details: $"{commandKind}: {target}: {failure.ConstraintName}: {failure.Message}"),
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
// Emit a canonical Denied AuditEvent directly through the best-effort IAuditWriter
|
||||
// (Task 2.3 #6): structured Target ("<commandKind>:<target>") and a richer DetailsJson
|
||||
// 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()
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
OccurredAtUtc = DateTimeOffset.UtcNow,
|
||||
Actor = identity?.KeyId ?? "anonymous",
|
||||
Action = "constraint-denied",
|
||||
Outcome = AuditOutcome.Denied,
|
||||
Category = CanonicalForwardingApiKeyAuditStore.ApiKeyCategory,
|
||||
Target = $"{commandKind}:{target}",
|
||||
SourceNode = null,
|
||||
CorrelationId = null,
|
||||
DetailsJson = JsonSerializer.Serialize(new Dictionary<string, string>
|
||||
{
|
||||
["constraint"] = failure.ConstraintName,
|
||||
["message"] = failure.Message,
|
||||
["commandKind"] = commandKind,
|
||||
["target"] = target,
|
||||
}),
|
||||
};
|
||||
|
||||
await auditWriter.WriteAsync(auditEvent, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private ConstraintFailure? CheckReadTarget(
|
||||
|
||||
Reference in New Issue
Block a user