61 lines
3.2 KiB
C#
61 lines
3.2 KiB
C#
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);
|
||
}
|