Program.cs built the BIG-nummer reveal's audit resource ref as
"brief/" + ctx.Zorgverlener().Bsn. AuditAuthz persists that to the
AuthzAudit.Resource column in SQLite and /admin/audit renders it, so a BSN
reached durable storage and a UI on the one trail four documents describe as
data-minimised and PII-free — on the endpoint whose own comment promises the
audit carries no PII.
The ref is now "brief". Nothing is lost: BriefStore keys one brief per owner,
so the id named what the row's acting principal already implies.
The existing guard, The_audit_schema_carries_no_pii, asserts on column names,
so a BSN inside a column called Resource could never fail it. Added
No_audit_row_carries_a_subjects_bsn, which drives a denied reveal as a
non-default subject and scans every string field of every row for that BSN
and for DemoOwner — asserting on the two BSNs actually in play rather than a
\d{9} shape, since a hex correlation id can hold nine digits by chance.
Verified it goes red when only the Program.cs line is reverted.
AuditEntry.Actor on document audit rows holds a raw BSN too; that is a
different store and stays with RB-04.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
74 lines
3.0 KiB
C#
74 lines
3.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");
|
|
}
|
|
|
|
/// RB-02/BIO-008: the schema test below asserts on **column names**, so a BSN inside a
|
|
/// column called `Resource` was invisible to it — and one was there, concatenated as
|
|
/// `"brief/" + Bsn`. This asserts on the stored **values** instead. Four documents
|
|
/// promise this trail holds no PII; this is the test that makes the promise checkable.
|
|
[Fact]
|
|
public async Task No_audit_row_carries_a_subjects_bsn()
|
|
{
|
|
const string subject = "999999990";
|
|
var reveal = new HttpRequestMessage(HttpMethod.Post, "/api/v1/brief/reveal-bignummer");
|
|
reveal.Headers.Add("X-Subject", subject);
|
|
Assert.Equal(HttpStatusCode.Forbidden, (await _client.SendAsync(reveal)).StatusCode);
|
|
|
|
var bsns = new[] { subject, DocumentStore.DemoOwner };
|
|
foreach (var e in await AuditLog())
|
|
foreach (var field in new[] { e.Action, e.Resource, e.Decision, e.Role, e.At, e.CorrelationId })
|
|
Assert.DoesNotContain(bsns, bsn => field.Contains(bsn, StringComparison.Ordinal));
|
|
}
|
|
|
|
[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));
|
|
}
|
|
}
|