using System.Text.Json;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.Auth.Abstractions.ApiKeys;
namespace ZB.MOM.WW.MxGateway.Server.Security.Audit;
///
/// Adapter that overrides the shared library's so that
/// library-emitted API-key audit events (CLI / admin verbs from
/// ApiKeyAdminCommands) are canonicalized onto and routed
/// through the gateway's into the canonical
/// audit_event store.
///
///
/// Overriding the registered is the ONLY way to
/// canonicalize the library-internal ApiKeyAdminCommands events, since that type
/// cannot be edited. reads back from the canonical store
/// and maps each to an so the
/// existing dashboard "recent audit" view (and the CLI/store tests) keep working through
/// this same seam, unchanged.
///
/// The library's own api_key_audit table is left in place but UNUSED — nothing
/// writes to it once this adapter overrides the library's SqliteApiKeyAuditStore
/// registration.
///
///
public sealed class CanonicalForwardingApiKeyAuditStore(
IAuditWriter auditWriter,
SqliteCanonicalAuditStore store) : IApiKeyAuditStore
{
/// The canonical assigned to API-key events.
public const string ApiKeyCategory = "ApiKey";
/// Actor used for the library's keyless init-db event.
private const string SystemActor = "system";
/// Actor used for any other keyless (CLI-originated) library event.
private const string CliActor = "cli";
/// The library event type that denotes a constraint denial.
private const string ConstraintDeniedEventType = "constraint-denied";
/// The library's keyless schema-init event type.
private const string InitDbEventType = "init-db";
///
public async Task AppendAsync(ApiKeyAuditEntry entry, CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(entry);
AuditEvent auditEvent = new()
{
EventId = Guid.NewGuid(),
OccurredAtUtc = entry.CreatedUtc,
// Keyless library events: init-db is system-originated; any other keyless event
// is a CLI/admin verb run without an authenticated principal.
Actor = entry.KeyId
?? (entry.EventType == InitDbEventType ? SystemActor : CliActor),
Action = entry.EventType,
Outcome = entry.EventType == ConstraintDeniedEventType
? AuditOutcome.Denied
: AuditOutcome.Success,
Category = ApiKeyCategory,
Target = entry.KeyId,
SourceNode = entry.RemoteAddress,
CorrelationId = null,
DetailsJson = WrapDetails(entry.Details),
};
// Best-effort: IAuditWriter swallows/logs failures, so this never throws.
await auditWriter.WriteAsync(auditEvent, ct).ConfigureAwait(false);
}
///
public async Task> ListRecentAsync(int limit, CancellationToken ct)
{
IReadOnlyList events = await store.ListRecentAsync(limit, ct).ConfigureAwait(false);
ApiKeyAuditEntry[] entries = new ApiKeyAuditEntry[events.Count];
for (int index = 0; index < events.Count; index++)
{
AuditEvent auditEvent = events[index];
entries[index] = new ApiKeyAuditEntry(
KeyId: auditEvent.Actor switch
{
// Keyless library events were mapped to the system/cli sentinel actors on the
// way in; map them back to a null KeyId so the dashboard view is faithful.
SystemActor or CliActor => null,
string actor => actor,
},
EventType: auditEvent.Action,
RemoteAddress: auditEvent.SourceNode,
CreatedUtc: auditEvent.OccurredAtUtc,
Details: UnwrapDetails(auditEvent.DetailsJson));
}
return entries;
}
///
/// Wraps a free-form library detail string into the canonical
/// {"detail": "<escaped>"} JSON envelope, or null when there is no detail.
///
private static string? WrapDetails(string? details)
{
if (details is null)
{
return null;
}
return JsonSerializer.Serialize(new Dictionary { ["detail"] = details });
}
///
/// Unwraps the canonical detail envelope back to the original free-form string. Falls
/// back to the raw JSON when it is not a recognised {"detail": ...} envelope, so
/// directly-emitted canonical events (whose DetailsJson is richer) still surface text.
///
private static string? UnwrapDetails(string? detailsJson)
{
if (string.IsNullOrEmpty(detailsJson))
{
return null;
}
try
{
using JsonDocument document = JsonDocument.Parse(detailsJson);
if (document.RootElement.ValueKind == JsonValueKind.Object
&& document.RootElement.TryGetProperty("detail", out JsonElement detail)
&& detail.ValueKind == JsonValueKind.String)
{
return detail.GetString();
}
}
catch (JsonException)
{
// Not JSON we recognise; surface the raw payload below.
}
return detailsJson;
}
}