feat(behandelportal): WP-62 medewerker caller identity + authz seam
Splits backend CallerIdentity into the two ADR-0002 §3 actor kinds (ZorgverlenerCaller/MedewerkerCaller), a stub X-Medewerker/X-Rollen header path mirroring WP-53's citizen stub, and Authz.CanBeoordelen as the first medewerker capability — backend-only, no consumer until WP-64. Also fixes the backlog README's stale WP-61 status (done, but table said todo). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -81,4 +81,30 @@ public class AuthzTests
|
||||
Assert.True(Authz.Decisions(Drafter, "sent", DrafterId).CanRevealBigNummer);
|
||||
Assert.False(Authz.Decisions(Approver, "draft", DrafterId).CanRevealBigNummer);
|
||||
}
|
||||
|
||||
// --- CanBeoordelen (WP-62) --------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void CanBeoordelen_true_for_a_medewerker_with_the_behandelaar_rol()
|
||||
{
|
||||
var medewerker = new MedewerkerCaller("m.jansen", [MedewerkerRol.Behandelaar], "M. Jansen", PrincipalRole.Drafter);
|
||||
Assert.True(Authz.CanBeoordelen(medewerker));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanBeoordelen_false_for_a_medewerker_without_it()
|
||||
{
|
||||
var medewerker = new MedewerkerCaller("m.jansen", [], "M. Jansen", PrincipalRole.Drafter);
|
||||
Assert.False(Authz.CanBeoordelen(medewerker));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(PrincipalRole.Drafter)]
|
||||
[InlineData(PrincipalRole.Approver)]
|
||||
[InlineData(PrincipalRole.Admin)]
|
||||
public void CanBeoordelen_false_for_a_zorgverlener_regardless_of_role(PrincipalRole role)
|
||||
{
|
||||
var zorgverlener = new ZorgverlenerCaller("111222333", "Dr. Test", role);
|
||||
Assert.False(Authz.CanBeoordelen(zorgverlener));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public class OpenZaakDocumentSourceTests
|
||||
InformatieobjecttypeUrls = new() { ["identiteit"] = InformatieobjecttypeUrl },
|
||||
};
|
||||
|
||||
private static readonly CallerIdentity Caller = new("111222333", "Dr. Test", PrincipalRole.Drafter);
|
||||
private static readonly ZorgverlenerCaller Caller = new("111222333", "Dr. Test", PrincipalRole.Drafter);
|
||||
|
||||
[Fact]
|
||||
public void Upload_registers_an_eio_in_drc_and_persists_its_url_locally()
|
||||
|
||||
@@ -71,7 +71,7 @@ public class OpenZaakZaakSourceTests
|
||||
|
||||
var options = new ZgwOptions { ZrcBaseUrl = ZrcBase, ZtcBaseUrl = ZtBase, ClientId = "c", Secret = "s" };
|
||||
var source = new OpenZaakZaakSource(new HttpClient(handler), new ZgwTokenProvider(options), options);
|
||||
var caller = new CallerIdentity("111222333", "Dr. Test", PrincipalRole.Drafter);
|
||||
var caller = new ZorgverlenerCaller("111222333", "Dr. Test", PrincipalRole.Drafter);
|
||||
|
||||
source.ListMyCases(caller, DateTimeOffset.UtcNow);
|
||||
|
||||
@@ -123,7 +123,7 @@ public class OpenZaakZaakSourceTests
|
||||
Referentie = "BIG-2026-000123",
|
||||
};
|
||||
|
||||
var caller = new CallerIdentity(aanvraag.Owner, "Dr. Test", PrincipalRole.Drafter);
|
||||
var caller = new ZorgverlenerCaller(aanvraag.Owner, "Dr. Test", PrincipalRole.Drafter);
|
||||
var (referentie, status, zaakUrl) = source.CreateZaak(aanvraag, new DateTimeOffset(2026, 7, 28, 12, 0, 0, TimeSpan.Zero), caller);
|
||||
|
||||
Assert.Equal("BIG-2026-000123", referentie);
|
||||
@@ -156,7 +156,7 @@ public class OpenZaakZaakSourceTests
|
||||
var handler = new ZgwStubHandler(url => throw new InvalidOperationException($"no HTTP call expected, got {url}"));
|
||||
var source = new OpenZaakZaakSource(new HttpClient(handler), new ZgwTokenProvider(options), options);
|
||||
var aanvraag = new Aanvraag { Id = "a1", Type = "unknown-type", Owner = "111222333", Referentie = "BIG-2026-000123" };
|
||||
var caller = new CallerIdentity(aanvraag.Owner, "Dr. Test", PrincipalRole.Drafter);
|
||||
var caller = new ZorgverlenerCaller(aanvraag.Owner, "Dr. Test", PrincipalRole.Drafter);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => source.CreateZaak(aanvraag, DateTimeOffset.UtcNow, caller));
|
||||
}
|
||||
@@ -177,7 +177,7 @@ public class OpenZaakZaakSourceTests
|
||||
ZaaktypeUrls = new() { ["registratie"] = zaaktypeUrl },
|
||||
};
|
||||
var aanvraag = new Aanvraag { Id = "a1", Type = "registratie", Owner = "111222333", Referentie = "BIG-2026-000123" };
|
||||
var caller = new CallerIdentity(aanvraag.Owner, "Dr. Test", PrincipalRole.Drafter);
|
||||
var caller = new ZorgverlenerCaller(aanvraag.Owner, "Dr. Test", PrincipalRole.Drafter);
|
||||
return (options, aanvraag, caller);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,23 +4,30 @@ using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace BigRegister.Tests;
|
||||
|
||||
/// WP-53: the dev stub identity provider — role from X-Role (unchanged behaviour), subject BSN
|
||||
/// from the new X-Subject header, defaulting to the single seeded citizen so every existing
|
||||
/// request (none of which send X-Subject) resolves exactly as before this WP.
|
||||
/// WP-53 (extended WP-62): the dev stub identity provider — role from X-Role (unchanged
|
||||
/// behaviour, applies to either actor kind), subject BSN from X-Subject defaulting to the
|
||||
/// single seeded citizen so every existing request (none of which send X-Subject) resolves
|
||||
/// exactly as before this WP. X-Medewerker (+ X-Rollen) selects the medewerker actor kind.
|
||||
public class StubIdentityProviderTests
|
||||
{
|
||||
private static CallerIdentity Resolve(string? role, string? subject)
|
||||
private static CallerIdentity Resolve(
|
||||
string? role = null, string? subject = null, string? medewerker = null, string? rollen = null)
|
||||
{
|
||||
var ctx = new DefaultHttpContext();
|
||||
if (role is not null) ctx.Request.Headers["X-Role"] = role;
|
||||
if (subject is not null) ctx.Request.Headers["X-Subject"] = subject;
|
||||
if (medewerker is not null) ctx.Request.Headers["X-Medewerker"] = medewerker;
|
||||
if (rollen is not null) ctx.Request.Headers["X-Rollen"] = rollen;
|
||||
return new StubIdentityProvider().Resolve(ctx);
|
||||
}
|
||||
|
||||
private static ZorgverlenerCaller ResolveZorgverlener(string? role = null, string? subject = null) =>
|
||||
Assert.IsType<ZorgverlenerCaller>(Resolve(role, subject));
|
||||
|
||||
[Fact]
|
||||
public void No_headers_resolves_to_the_seeded_citizen_as_a_drafter()
|
||||
{
|
||||
var caller = Resolve(role: null, subject: null);
|
||||
var caller = ResolveZorgverlener(role: null, subject: null);
|
||||
Assert.Equal(DocumentStore.DemoOwner, caller.Bsn);
|
||||
Assert.Equal(PrincipalRole.Drafter, caller.Role);
|
||||
}
|
||||
@@ -37,7 +44,53 @@ public class StubIdentityProviderTests
|
||||
[Fact]
|
||||
public void X_subject_overrides_the_default_bsn()
|
||||
{
|
||||
var caller = Resolve(role: null, subject: "999888777");
|
||||
var caller = ResolveZorgverlener(role: null, subject: "999888777");
|
||||
Assert.Equal("999888777", caller.Bsn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void No_headers_resolves_a_zorgverlener_kind()
|
||||
{
|
||||
Assert.IsType<ZorgverlenerCaller>(Resolve());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void X_medewerker_resolves_a_medewerker_with_the_default_behandelaar_rol()
|
||||
{
|
||||
var caller = Assert.IsType<MedewerkerCaller>(Resolve(medewerker: "m.jansen"));
|
||||
Assert.Equal("m.jansen", caller.MedewerkerId);
|
||||
Assert.Equal("m.jansen", caller.SubjectId);
|
||||
Assert.Contains(MedewerkerRol.Behandelaar, caller.Rollen);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void X_medewerker_takes_precedence_over_x_subject()
|
||||
{
|
||||
var caller = Resolve(subject: "999888777", medewerker: "m.jansen");
|
||||
Assert.IsType<MedewerkerCaller>(caller);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Empty_x_medewerker_falls_through_to_the_zorgverlener_default()
|
||||
{
|
||||
var caller = Assert.IsType<ZorgverlenerCaller>(Resolve(medewerker: ""));
|
||||
Assert.Equal(DocumentStore.DemoOwner, caller.Bsn);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("behandelaar", new[] { MedewerkerRol.Behandelaar })]
|
||||
[InlineData("Behandelaar, behandelaar", new[] { MedewerkerRol.Behandelaar })]
|
||||
[InlineData("geen", new MedewerkerRol[0])]
|
||||
public void X_rollen_parses_known_tokens_and_drops_unknown_ones(string rollen, MedewerkerRol[] expected)
|
||||
{
|
||||
var caller = Assert.IsType<MedewerkerCaller>(Resolve(medewerker: "m.jansen", rollen: rollen));
|
||||
Assert.Equal(expected, caller.Rollen);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void X_role_still_applies_to_a_medewerker()
|
||||
{
|
||||
var caller = Resolve(role: "admin", medewerker: "m.jansen");
|
||||
Assert.Equal(PrincipalRole.Admin, caller.Role);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ public class ZgwTokenProviderTests
|
||||
[Fact]
|
||||
public void Mint_with_a_caller_carries_that_citizen_not_the_static_config_identity()
|
||||
{
|
||||
var caller = new CallerIdentity("111222333", "Dr. Citizen", PrincipalRole.Drafter);
|
||||
var caller = new ZorgverlenerCaller("111222333", "Dr. Citizen", PrincipalRole.Drafter);
|
||||
var token = new ZgwTokenProvider(Options).Mint(caller);
|
||||
|
||||
var payload = JsonSerializer.Deserialize<JsonElement>(Decode(token.Split('.')[1]));
|
||||
@@ -56,6 +56,19 @@ public class ZgwTokenProviderTests
|
||||
Assert.Equal("big-register", payload.GetProperty("client_id").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Mint_with_a_medewerker_caller_uses_the_medewerkerId_as_user_id()
|
||||
{
|
||||
// WP-62: SubjectId is what ZgwTokenProvider.Mint reads — a medewerker's is its
|
||||
// medewerkerId, not a BSN, and this is the only place that's directly observable.
|
||||
var caller = new MedewerkerCaller("m.jansen", [MedewerkerRol.Behandelaar], "M. Jansen", PrincipalRole.Drafter);
|
||||
var token = new ZgwTokenProvider(Options).Mint(caller);
|
||||
|
||||
var payload = JsonSerializer.Deserialize<JsonElement>(Decode(token.Split('.')[1]));
|
||||
Assert.Equal("m.jansen", payload.GetProperty("user_id").GetString());
|
||||
Assert.Equal("M. Jansen", payload.GetProperty("user_representation").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Signature_verifies_with_the_shared_secret()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user