Files
scadaproj/ZB.MOM.WW.Auth/src/ZB.MOM.WW.Auth.ApiKeys.SqlServer/SqlServerApiKeyAuditStore.cs
T

81 lines
3.6 KiB
C#

using System.Data;
using Microsoft.Data.SqlClient;
using ZB.MOM.WW.Auth.Abstractions.ApiKeys;
namespace ZB.MOM.WW.Auth.ApiKeys.SqlServer;
/// <summary>
/// SQL Server-backed, append-only audit store for API-key events. A one-for-one port of
/// <c>SqliteApiKeyAuditStore</c>; on a clustered host the rows from every node land in one table,
/// so the audit trail no longer has to be stitched together per node.
/// </summary>
/// <param name="connectionFactory">Factory for connections to the shared API-key database.</param>
public sealed class SqlServerApiKeyAuditStore(AuthSqlServerConnectionFactory connectionFactory) : IApiKeyAuditStore
{
/// <inheritdoc />
public async Task AppendAsync(ApiKeyAuditEntry entry, CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(entry);
string schema = connectionFactory.Schema;
await using SqlConnection connection =
await connectionFactory.OpenConnectionAsync(ct).ConfigureAwait(false);
await using SqlCommand command = connectionFactory.CreateCommand(connection, $"""
INSERT INTO [{schema}].[{SqlServerAuthSchema.ApiKeyAuditTable}] (
key_id, event_type, remote_address, created_utc, details)
VALUES (@key_id, @event_type, @remote_address, @created_utc, @details);
""");
command.Parameters.Add("@key_id", SqlDbType.NVarChar, 64).Value = (object?)entry.KeyId ?? DBNull.Value;
command.Parameters.Add("@event_type", SqlDbType.NVarChar, 64).Value = entry.EventType;
command.Parameters.Add("@remote_address", SqlDbType.NVarChar, 64).Value = (object?)entry.RemoteAddress ?? DBNull.Value;
command.Parameters.Add("@created_utc", SqlDbType.DateTimeOffset).Value = entry.CreatedUtc;
command.Parameters.Add("@details", SqlDbType.NVarChar, -1).Value = (object?)entry.Details ?? DBNull.Value;
await command.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
}
/// <inheritdoc />
/// <remarks>
/// Ordered by the IDENTITY <c>audit_id</c>, not by <c>created_utc</c>: two events stamped in the
/// same instant still come back in insertion order. A non-positive limit short-circuits without
/// opening a connection — <c>SELECT TOP (-1)</c> is not even legal T-SQL.
/// </remarks>
public async Task<IReadOnlyList<ApiKeyAuditEntry>> ListRecentAsync(int limit, CancellationToken ct)
{
if (limit <= 0)
{
return [];
}
string schema = connectionFactory.Schema;
await using SqlConnection connection =
await connectionFactory.OpenConnectionAsync(ct).ConfigureAwait(false);
await using SqlCommand command = connectionFactory.CreateCommand(connection, $"""
SELECT TOP (@limit) key_id, event_type, remote_address, created_utc, details
FROM [{schema}].[{SqlServerAuthSchema.ApiKeyAuditTable}]
ORDER BY audit_id DESC;
""");
command.Parameters.Add("@limit", SqlDbType.Int).Value = limit;
List<ApiKeyAuditEntry> entries = [];
await using SqlDataReader reader = await command.ExecuteReaderAsync(ct).ConfigureAwait(false);
while (await reader.ReadAsync(ct).ConfigureAwait(false))
{
entries.Add(new ApiKeyAuditEntry(
KeyId: reader.IsDBNull(0) ? null : reader.GetString(0),
EventType: reader.GetString(1),
RemoteAddress: reader.IsDBNull(2) ? null : reader.GetString(2),
CreatedUtc: reader.GetDateTimeOffset(3),
Details: reader.IsDBNull(4) ? null : reader.GetString(4)));
}
return entries;
}
}