Persist the security-relevant events (authz denials + BIG-nummer reveal/step-up) into a data-minimised EF table (AuthzAuditEntry: At/Action/Resource/Decision/Role/CorrelationId — never a name/BSN/value), extending the DocumentStore AuditEntry pattern (migration AuthzAudit). AuditAuthz now persists via AuthzAuditStore.Record alongside its log line. GET /admin/audit (admin-gated by the existing CasesAdmin) returns the trail newest-first. +3 backend tests incl. a schema-carries-no-PII reflection test. Typed client regenerated (audit() + AuthzAuditDto); no FE consumer yet (a future audit view must add the ROLE_AWARE prefix). Finishes WP-42's audit half. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
56 lines
2.0 KiB
C#
56 lines
2.0 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using System.Text.RegularExpressions;
|
|
using BigRegister.Api.Contracts;
|
|
using BigRegister.Api.Data;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
|
|
namespace BigRegister.Tests;
|
|
|
|
/// WP-41: the persisted authz/PII-reveal audit trail is queryable, data-minimised (no PII).
|
|
public class AuthzAuditTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
|
{
|
|
private readonly HttpClient _client = factory.CreateClient();
|
|
|
|
private HttpRequestMessage Admin(HttpMethod method, string path)
|
|
{
|
|
var req = new HttpRequestMessage(method, path);
|
|
req.Headers.Add("X-Role", "admin");
|
|
return req;
|
|
}
|
|
|
|
private async Task<List<AuthzAuditDto>> AuditLog()
|
|
{
|
|
var res = await _client.SendAsync(Admin(HttpMethod.Get, "/api/v1/admin/audit"));
|
|
res.EnsureSuccessStatusCode();
|
|
return (await res.Content.ReadFromJsonAsync<List<AuthzAuditDto>>())!;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_denied_admin_action_is_recorded()
|
|
{
|
|
// No X-Role → drafter → 403 on an admin endpoint → a deny entry.
|
|
Assert.Equal(HttpStatusCode.Forbidden, (await _client.GetAsync("/api/v1/admin/cases")).StatusCode);
|
|
Assert.Contains(await AuditLog(), e => e.Action == "cases:manage" && e.Decision == "deny");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_reveal_attempt_is_recorded()
|
|
{
|
|
// Drafter (capable role) without X-Step-Up → reveal denied → recorded.
|
|
var res = await _client.PostAsync("/api/v1/brief/reveal-bignummer", null);
|
|
Assert.Equal(HttpStatusCode.Forbidden, res.StatusCode);
|
|
Assert.Contains(await AuditLog(), e => e.Action == "brief:reveal-bignummer");
|
|
}
|
|
|
|
[Fact]
|
|
public void The_audit_schema_carries_no_pii()
|
|
{
|
|
var names = typeof(AuthzAuditEntry).GetProperties().Select(p => p.Name).ToArray();
|
|
Assert.Equal(
|
|
new[] { "At", "Action", "Resource", "Decision", "Role", "CorrelationId", "Id" }.OrderBy(x => x),
|
|
names.OrderBy(x => x));
|
|
Assert.DoesNotContain(names, n => Regex.IsMatch(n, "naam|name|bsn|value|waarde", RegexOptions.IgnoreCase));
|
|
}
|
|
}
|