Files
MoShiro/backend/src/MoShiro.Backend.Application/Audit/AuditService.cs
T

61 lines
3.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.Extensions.Logging;
namespace MoShiro.Backend.Application;
/// <summary>
/// 审计日志里的一行:谁(<see cref="Actor"/>,用户名或 <c>key:令牌名</c>)、怎么登录的(<see cref="Method"/>)、做了什么(<see cref="Action"/>,
/// 管理接口的 operationId,或 <c>auth.login</c> 这类登录事件)、对谁(<see cref="Target"/>)、结果如何。
/// </summary>
public sealed record AuditRecord(long Id, long AtMs, string? UserId, string Actor, string Method, string Action, string? Target, string? Detail, string? Remote, bool Success);
public sealed record AuditQuery(long? BeforeId, int Limit, string? Actor, string? Action);
/// <summary>审计日志只追加:没有修改和单条删除,只按保留期整体清理。</summary>
public sealed partial class AuditService
{
/// <summary>保留一年。审计记录的是谁动过信任相关的东西,比监测数据留得久。</summary>
public const int RetentionDays = 365;
private readonly IAuditStore _store;
private readonly TimeProvider _time;
private readonly ILogger<AuditService> _logger;
public AuditService(IAuditStore store, TimeProvider time, ILogger<AuditService> logger)
{
_store = store;
_time = time;
_logger = logger;
}
/// <summary>记一条。写不进去只记日志,不能因为审计失败而让已经完成的操作报错。</summary>
public async ValueTask RecordAsync(Principal principal, string action, string? target, string? detail, string? remote, bool success, CancellationToken ct = default) =>
await RecordAsync(principal.UserId, principal.Label, MethodOf(principal), action, target, detail, remote, success, ct);
public async ValueTask RecordAsync(string? userId, string actor, string method, string action, string? target, string? detail, string? remote, bool success, CancellationToken ct = default)
{
try
{
await _store.AddAsync(new AuditRecord(0, _time.GetUtcNow().ToUnixTimeMilliseconds(), userId, actor, method, action, Clip(target, 200), Clip(detail, 500), remote, success), ct);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
LogFailed(ex, action);
}
}
public ValueTask<IReadOnlyList<AuditRecord>> ListAsync(AuditQuery query, CancellationToken ct) => _store.ListAsync(query, ct);
public ValueTask<int> PruneAsync(long nowMs, CancellationToken ct) => _store.DeleteOlderThanAsync(nowMs - (RetentionDays * 86_400_000L), ct);
/// <summary>登录方式:密码、passkey,或 API key。</summary>
public static string MethodOf(Principal principal) =>
principal.Kind == PrincipalKind.Anonymous ? "anonymous"
: principal.Kind == PrincipalKind.Token && principal.UserId is null ? "key"
: principal.AuthMethod.Length > 0 ? principal.AuthMethod : AuthMethods.Password;
private static string? Clip(string? text, int max) => text is { } t && t.Length > max ? t[..max] : text;
[LoggerMessage(Level = LogLevel.Warning, Message = "could not write the audit record for {Action}")]
private partial void LogFailed(Exception exception, string action);
}