feat: audit log of admin writes, sign-ins and key creation, with an audit tab [CI]

This commit is contained in:
justme committed 2026-10-07 22:17:52 +08:00
1 parent 359313a57b
commit 1379b8c7cb
21 files changed
+579 -7

No files matched your search

@@ -191,6 +191,21 @@ internal static class Schema
connection.Exec("ALTER TABLE join_tokens ADD COLUMN created_by TEXT");
connection.Exec("ALTER TABLE agents ADD COLUMN added_by TEXT");
connection.Exec("ALTER TABLE probes ADD COLUMN added_by TEXT");
connection.Exec("""
CREATE TABLE audit_log (
id INTEGER PRIMARY KEY,
at INTEGER NOT NULL,
user_id TEXT,
actor TEXT NOT NULL,
method TEXT NOT NULL,
action TEXT NOT NULL,
target TEXT,
detail TEXT,
remote TEXT,
success INTEGER NOT NULL
)
""");
connection.Exec("CREATE INDEX ix_audit_time ON audit_log (at)");
connection.Exec("PRAGMA user_version = 13");
transaction.Commit();
}
@@ -0,0 +1,33 @@
using MoShiro.Backend.Application;
namespace MoShiro.Backend.Storage.Sqlite;
public sealed class SqliteAuditStore : IAuditStore
{
private readonly SqliteDatabase _db;
public SqliteAuditStore(SqliteDatabase db)
{
_db = db;
}
public ValueTask AddAsync(AuditRecord record, CancellationToken ct) =>
_db.ExecuteAsync(
c => c.Exec(
"INSERT INTO audit_log (at, user_id, actor, method, action, target, detail, remote, success) VALUES (@at, @uid, @actor, @method, @action, @target, @detail, @remote, @ok)",
("@at", record.AtMs), ("@uid", record.UserId), ("@actor", record.Actor), ("@method", record.Method), ("@action", record.Action),
("@target", record.Target), ("@detail", record.Detail), ("@remote", record.Remote), ("@ok", record.Success ? 1 : 0)),
ct);
public ValueTask<IReadOnlyList<AuditRecord>> ListAsync(AuditQuery query, CancellationToken ct) =>
_db.ReadAsync<IReadOnlyList<AuditRecord>>(
c => c.Query(
"SELECT id, at, user_id, actor, method, action, target, detail, remote, success FROM audit_log " +
"WHERE (@before IS NULL OR id < @before) AND (@actor IS NULL OR actor = @actor) AND (@action IS NULL OR action = @action) ORDER BY id DESC LIMIT @limit",
r => new AuditRecord(r.GetInt64(0), r.GetInt64(1), r.NullableString(2), r.GetString(3), r.GetString(4), r.GetString(5), r.NullableString(6), r.NullableString(7), r.NullableString(8), r.GetInt64(9) != 0),
("@before", query.BeforeId), ("@actor", query.Actor), ("@action", query.Action), ("@limit", query.Limit)),
ct);
public ValueTask<int> DeleteOlderThanAsync(long cutoffMs, CancellationToken ct) =>
_db.WriteAsync(c => c.Exec("DELETE FROM audit_log WHERE at < @cutoff", ("@cutoff", cutoffMs)), ct);
}
@@ -22,6 +22,7 @@ public static class StorageServiceCollectionExtensions
services.AddSingleton<IHostStore, SqliteHostStore>();
services.AddSingleton<INodeMetaStore, SqliteNodeMetaStore>();
services.AddSingleton<IAuthStore, SqliteAuthStore>();
services.AddSingleton<IAuditStore, SqliteAuditStore>();
services.AddSingleton<IRawStore, SqliteRawStore>();
services.AddSingleton<IMetricStore, SqliteMetricStore>();
services.AddSingleton<IStorageHealth, SqliteStorageHealth>();