feat(acl): ACL skeleton — OpenZaak default-fill (refs #5) (#45)
Some checks failed
CI / lint (push) Has been cancelled
CI / build (push) Has been cancelled
CI / unit (push) Has been cancelled
CI / compose-smoke (push) Has been cancelled

This commit was merged in pull request #45.
This commit is contained in:
2026-06-04 07:50:45 +00:00
parent 71b76a0ef9
commit 4b2af5c635
24 changed files with 447 additions and 1 deletions

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Acl.Application\Acl.Application.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,48 @@
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using Acl.Application;
namespace Acl.Infrastructure;
/// <summary>The only code that talks to OpenZaak's Zaken API (ADR-0001).</summary>
public sealed class OpenZaakGateway(HttpClient http, OpenZaakOptions options) : IZaakGateway
{
public async Task<Uri> OpenZaakAsync(ZaakRequest request, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(request);
using var message = new HttpRequestMessage(
HttpMethod.Post, new Uri(options.BaseUrl, "/zaken/api/v1/zaken"))
{
Content = JsonContent.Create(new ZaakDto(
request.Bronorganisatie,
request.Zaaktype.ToString(),
request.VerantwoordelijkeOrganisatie,
request.Startdatum.ToString("yyyy-MM-dd"),
request.Vertrouwelijkheidaanduiding)),
};
message.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", ZgwToken.Mint(options.ClientId, options.Secret));
// ZRC is a geo API; it requires the CRS headers.
message.Headers.Add("Accept-Crs", "EPSG:4326");
message.Content.Headers.Add("Content-Crs", "EPSG:4326");
using var response = await http.SendAsync(message, ct);
response.EnsureSuccessStatusCode();
var created = await response.Content.ReadFromJsonAsync<ZaakCreatedDto>(ct)
?? throw new InvalidOperationException("OpenZaak returned an empty zaak response");
return new Uri(created.Url);
}
private sealed record ZaakDto(
[property: JsonPropertyName("bronorganisatie")] string Bronorganisatie,
[property: JsonPropertyName("zaaktype")] string Zaaktype,
[property: JsonPropertyName("verantwoordelijkeOrganisatie")] string VerantwoordelijkeOrganisatie,
[property: JsonPropertyName("startdatum")] string Startdatum,
[property: JsonPropertyName("vertrouwelijkheidaanduiding")] string Vertrouwelijkheidaanduiding);
private sealed record ZaakCreatedDto(
[property: JsonPropertyName("url")] string Url);
}

View File

@@ -0,0 +1,9 @@
namespace Acl.Infrastructure;
/// <summary>Connection + credential config for OpenZaak's ZGW APIs.</summary>
public sealed class OpenZaakOptions
{
public required Uri BaseUrl { get; init; }
public required string ClientId { get; init; }
public required string Secret { get; init; }
}

View File

@@ -0,0 +1,8 @@
using Acl.Application;
namespace Acl.Infrastructure;
public sealed class SystemClock : IClock
{
public DateOnly Today => DateOnly.FromDateTime(DateTime.UtcNow);
}

View File

@@ -0,0 +1,29 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace Acl.Infrastructure;
/// <summary>Mints a ZGW (vng-api-common) JWT: HS256 over the standard claims.</summary>
internal static class ZgwToken
{
public static string Mint(string clientId, string secret)
{
var header = B64Url(JsonSerializer.SerializeToUtf8Bytes(new { alg = "HS256", typ = "JWT" }));
var payload = B64Url(JsonSerializer.SerializeToUtf8Bytes(new
{
iss = clientId,
iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
client_id = clientId,
user_id = "acl",
user_representation = "acl",
}));
var signingInput = $"{header}.{payload}";
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var signature = B64Url(hmac.ComputeHash(Encoding.UTF8.GetBytes(signingInput)));
return $"{signingInput}.{signature}";
}
private static string B64Url(byte[] bytes) =>
Convert.ToBase64String(bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_');
}