feat: audit log of admin writes, sign-ins and key creation, with an audit tab [CI]
This commit is contained in:
1 parent
359313a57b
commit
1379b8c7cb
21 files changed
+579
-7
No files matched your search
@@ -0,0 +1,68 @@
|
||||
using System.Net;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace MoShiro.Backend.Tests;
|
||||
|
||||
/// <summary>审计日志:写操作、登录成败都会记一行;谁做的(用户名或 key:令牌名)、结果、细节。</summary>
|
||||
[Collection("probe-e2e")]
|
||||
public sealed class AuditTests
|
||||
{
|
||||
private readonly BackendFixture _backend;
|
||||
|
||||
public AuditTests(BackendFixture backend)
|
||||
{
|
||||
_backend = backend;
|
||||
}
|
||||
|
||||
/// <summary>审计在响应发完之后才写,轮询一下。</summary>
|
||||
private async Task<JsonNode?> FindAsync(string query, Func<JsonNode, bool> match)
|
||||
{
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
JsonNode page = JsonNode.Parse(await _backend.Admin.GetStringAsync("/api/admin/audit?" + query))!;
|
||||
if (page["items"]!.AsArray().FirstOrDefault(x => x is not null && match(x)) is { } found)
|
||||
{
|
||||
return found;
|
||||
}
|
||||
|
||||
await Task.Delay(100);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteActions_AreRecorded_WithWhoAndTheDetail()
|
||||
{
|
||||
string note = "audit-" + Guid.NewGuid().ToString("N")[..8];
|
||||
using HttpResponseMessage created = await _backend.Admin.PostAsync("/api/admin/join-tokens", BackendFixture.Json(new JsonObject { ["role"] = "probe", ["note"] = note }));
|
||||
Assert.Equal(HttpStatusCode.Created, created.StatusCode);
|
||||
|
||||
JsonNode? row = await FindAsync("limit=100", x => x["detail"]?.GetValue<string>().Contains(note, StringComparison.Ordinal) == true);
|
||||
Assert.NotNull(row);
|
||||
Assert.StartsWith("key:", row["actor"]!.GetValue<string>(), StringComparison.Ordinal);
|
||||
Assert.Equal("key", row["method"]!.GetValue<string>());
|
||||
Assert.True(row["success"]!.GetValue<bool>());
|
||||
Assert.False(string.IsNullOrEmpty(row["action"]!.GetValue<string>()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FailedSignIn_IsRecorded_WithTheAttemptedName_AndWithoutThePassword()
|
||||
{
|
||||
string name = "nobody-" + Guid.NewGuid().ToString("N")[..8];
|
||||
using HttpResponseMessage denied = await _backend.Anonymous.PostAsync("/api/admin/auth/login", BackendFixture.Json(new JsonObject { ["username"] = name, ["password"] = "secret-pw-123" }));
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, denied.StatusCode);
|
||||
|
||||
JsonNode? row = await FindAsync("action=auth.login&limit=100", x => x["actor"]!.GetValue<string>() == name);
|
||||
Assert.NotNull(row);
|
||||
Assert.False(row["success"]!.GetValue<bool>());
|
||||
Assert.DoesNotContain("secret-pw-123", row.ToJsonString(), StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Audit_NeedsItsOwnCapability()
|
||||
{
|
||||
using HttpResponseMessage response = await _backend.Anonymous.GetAsync("/api/admin/audit");
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user