diff --git a/services/bff/Bff.Api/DownstreamClients.cs b/services/bff/Bff.Api/DownstreamClients.cs
index f9761d5..8696b66 100644
--- a/services/bff/Bff.Api/DownstreamClients.cs
+++ b/services/bff/Bff.Api/DownstreamClients.cs
@@ -59,12 +59,26 @@ public interface IProjectionClient
/// internal reference, not shown in the portal.
public sealed record BeheerZaaktype(string Identificatie, string Omschrijving);
-/// Port to the ACL for read-only catalogus queries (beheer portal, S-15a). The BFF reaches the
-/// ACL directly for this read: the catalogus isn't a domain concern, and the ACL is the only code
-/// allowed to read the ZGW Catalogi API (§8.1, ADR-0025).
+/// The ACL default-fill settings the beheer portal reads + edits (S-15b): the three ZGW-mandatory
+/// fields the ACL stamps on every zaak (ADR-0003).
+public sealed record BeheerDefaultFill(
+ string Bronorganisatie,
+ string VerantwoordelijkeOrganisatie,
+ string Vertrouwelijkheidaanduiding);
+
+/// Port to the ACL for beheer queries (beheer portal). The BFF reaches the ACL directly: these
+/// aren't a domain concern, and the ACL is the only code allowed to read/own the ZGW-facing config
+/// (§8.1, ADR-0025).
public interface IAclClient
{
+ /// The published catalogus zaaktypen, read-only (S-15a).
Task> GetZaaktypenAsync(CancellationToken ct = default);
+
+ /// The current default-fill settings (S-15b).
+ Task GetDefaultFillAsync(CancellationToken ct = default);
+
+ /// Replace the default-fill settings (S-15b).
+ Task UpdateDefaultFillAsync(BeheerDefaultFill settings, CancellationToken ct = default);
}
/// Calls the Domain Service's POST /registrations.
@@ -141,4 +155,14 @@ public sealed class AclClient(HttpClient http) : IAclClient
{
public async Task> GetZaaktypenAsync(CancellationToken ct = default)
=> await http.GetFromJsonAsync>("catalogi/zaaktypen", ct) ?? [];
+
+ public async Task GetDefaultFillAsync(CancellationToken ct = default)
+ => await http.GetFromJsonAsync("default-fill", ct)
+ ?? throw new InvalidOperationException("The ACL returned an empty default-fill response.");
+
+ public async Task UpdateDefaultFillAsync(BeheerDefaultFill settings, CancellationToken ct = default)
+ {
+ using var response = await http.PutAsJsonAsync("default-fill", settings, ct);
+ response.EnsureSuccessStatusCode();
+ }
}
diff --git a/services/bff/Bff.Tests/BeheerDefaultFillEndpointTests.cs b/services/bff/Bff.Tests/BeheerDefaultFillEndpointTests.cs
new file mode 100644
index 0000000..472c664
--- /dev/null
+++ b/services/bff/Bff.Tests/BeheerDefaultFillEndpointTests.cs
@@ -0,0 +1,84 @@
+using System.Net;
+using System.Net.Http.Headers;
+using System.Net.Http.Json;
+using Bff.Api;
+
+namespace Bff.Tests;
+
+///
+/// The beheer default-fill config endpoints (S-15b): read (GET) and edit (PUT) the ACL's default-fill,
+/// reached only with a medewerker-realm token carrying the beheerder role. Missing token → 401;
+/// a medewerker without the role → 403; a beheerder reads and updates via the ACL client.
+///
+public class BeheerDefaultFillEndpointTests
+{
+ private static HttpRequestMessage Get(string? bearer)
+ {
+ var r = new HttpRequestMessage(HttpMethod.Get, "/beheer/default-fill");
+ if (bearer is not null) r.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearer);
+ return r;
+ }
+
+ private static HttpRequestMessage Put(string? bearer, object body)
+ {
+ var r = new HttpRequestMessage(HttpMethod.Put, "/beheer/default-fill") { Content = JsonContent.Create(body) };
+ if (bearer is not null) r.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearer);
+ return r;
+ }
+
+ [Fact]
+ public async Task Rejects_read_without_a_token()
+ {
+ using var factory = new BffFactory();
+ var response = await factory.CreateClient().SendAsync(Get(bearer: null));
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task Rejects_a_medewerker_without_the_beheerder_role()
+ {
+ using var factory = new BffFactory();
+ var response = await factory.CreateClient().SendAsync(Get(TestTokens.Medewerker("behandelaar")));
+ Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task Serves_the_current_default_fill_to_a_beheerder()
+ {
+ using var factory = new BffFactory();
+ factory.Acl.DefaultFill = new BeheerDefaultFill("517439943", "517439943", "openbaar");
+
+ var response = await factory.CreateClient().SendAsync(Get(TestTokens.Medewerker("beheerder")));
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var body = await response.Content.ReadFromJsonAsync();
+ Assert.Equal("517439943", body!.Bronorganisatie);
+ Assert.Equal("openbaar", body.Vertrouwelijkheidaanduiding);
+ }
+
+ [Fact]
+ public async Task Updates_the_default_fill_via_the_acl_for_a_beheerder()
+ {
+ using var factory = new BffFactory();
+
+ var response = await factory.CreateClient().SendAsync(
+ Put(TestTokens.Medewerker("beheerder"),
+ new { bronorganisatie = "999999999", verantwoordelijkeOrganisatie = "888888888", vertrouwelijkheidaanduiding = "vertrouwelijk" }));
+
+ Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
+ Assert.Equal("999999999", factory.Acl.Updated!.Bronorganisatie);
+ Assert.Equal("vertrouwelijk", factory.Acl.Updated.Vertrouwelijkheidaanduiding);
+ }
+
+ [Fact]
+ public async Task Rejects_an_update_from_a_non_beheerder()
+ {
+ using var factory = new BffFactory();
+
+ var response = await factory.CreateClient().SendAsync(
+ Put(TestTokens.Medewerker("behandelaar"), new { bronorganisatie = "1", verantwoordelijkeOrganisatie = "2", vertrouwelijkheidaanduiding = "openbaar" }));
+
+ Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
+ Assert.Null(factory.Acl.Updated);
+ }
+}
diff --git a/services/bff/Bff.Tests/BffFactory.cs b/services/bff/Bff.Tests/BffFactory.cs
index 7f1141e..278a61c 100644
--- a/services/bff/Bff.Tests/BffFactory.cs
+++ b/services/bff/Bff.Tests/BffFactory.cs
@@ -142,11 +142,24 @@ internal sealed class FakeProjectionClient : IProjectionClient
=> Task.FromResult>(Entries);
}
-/// Serves a configurable set of catalogus zaaktypen (beheer viewer, S-15a).
+/// Serves catalogus zaaktypen (S-15a) and holds the default-fill settings (S-15b).
internal sealed class FakeAclClient : IAclClient
{
public List Zaaktypen { get; } = [];
public Task> GetZaaktypenAsync(CancellationToken ct = default)
=> Task.FromResult>(Zaaktypen);
+
+ public BeheerDefaultFill DefaultFill { get; set; } = new("517439943", "517439943", "openbaar");
+ public BeheerDefaultFill? Updated { get; private set; }
+
+ public Task GetDefaultFillAsync(CancellationToken ct = default)
+ => Task.FromResult(DefaultFill);
+
+ public Task UpdateDefaultFillAsync(BeheerDefaultFill settings, CancellationToken ct = default)
+ {
+ Updated = settings;
+ DefaultFill = settings;
+ return Task.CompletedTask;
+ }
}