All checks were successful
## What & why First half of **S-12c** (behandel-portal backend), per **ADR-0013** (decisions recorded in #84): - **BFF multi-realm auth.** A second JWT bearer scheme (`medewerker`) alongside the default `digid` scheme. On validation it lifts Keycloak's `realm_access.roles` onto the principal, and a `behandelaar` policy (medewerker scheme + `behandelaar` role) gates `/behandel/*`. Self-service keeps the digid scheme. - **Werkbak = Flowable tasks.** The domain `Werkbak` query reads the open `Beoordelen` tasks (§8.2, S-12b's `IUserTaskClient`) and enriches each with its aggregate's bsn + status; `GET /behandel/werkbak` (domain) is proxied by the BFF `GET /behandel/werkbak` behind the behandelaar policy. The read projection stays the anonymous openbaar model (no premature `IN_BEHANDELING`/personal-data plumbing — deferred in ADR-0008). Behavior: `/behandel/werkbak` is **401** without a token, **403** for a medewerker lacking the role, **200 + werkbak** for a behandelaar. **S-12c-2** (next): `POST /behandel/registrations/{id}/decide` → domain decision + complete the Flowable task. ## Definition of Done - [x] Linked issue: #13 (umbrella, `refs`); closes the adr-proposal #84 - [x] Tests first; red → green per layer - [x] Unit + acceptance green (`make unit`): domain 78, bff 23, acceptance 9 (+ acl/event-subscriber unaffected) - [x] api-client `test` green; openapi.json regenerated (drift guard passes) - [x] Mutation ≥ break(90): **domain 100%, bff 100%** - [x] ADR-0013 added; `Keycloak__MedewerkerAuthority` wired into compose - [ ] CI green (pending) Part of #13. closes #84 Reviewed-on: #85
84 lines
3.4 KiB
C#
84 lines
3.4 KiB
C#
using System.Text;
|
|
using Bff.Api;
|
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
using Microsoft.AspNetCore.Hosting;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
using Microsoft.AspNetCore.TestHost;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.IdentityModel.JsonWebTokens;
|
|
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
|
using Microsoft.IdentityModel.Tokens;
|
|
|
|
namespace Acceptance.Support;
|
|
|
|
/// <summary>Hosts the real BFF in-process for the acceptance scenario, with fake downstreams and a
|
|
/// local test signing key so tokens can be minted without a live Keycloak (ADR-0010).</summary>
|
|
public sealed class BffAcceptanceHost : WebApplicationFactory<Program>
|
|
{
|
|
private static readonly SymmetricSecurityKey TestKey =
|
|
new(Encoding.UTF8.GetBytes("acceptance-bff-signing-key-at-least-256-bits-long!!"));
|
|
|
|
public CapturingDomainClient Domain { get; } = new();
|
|
public StubProjectionClient Projection { get; } = new();
|
|
|
|
public static string MintToken(string bsn) => new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor
|
|
{
|
|
Claims = new Dictionary<string, object> { ["bsn"] = bsn },
|
|
Expires = DateTime.UtcNow.AddMinutes(30),
|
|
SigningCredentials = new SigningCredentials(TestKey, SecurityAlgorithms.HmacSha256),
|
|
});
|
|
|
|
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
|
{
|
|
builder.UseSetting("Keycloak:Authority", "https://keycloak.invalid/realms/digid");
|
|
builder.UseSetting("Downstream:Domain:BaseUrl", "http://domain.invalid/");
|
|
builder.UseSetting("Downstream:Projection:BaseUrl", "http://projection.invalid/");
|
|
|
|
builder.ConfigureTestServices(services =>
|
|
{
|
|
services.AddSingleton<IDomainClient>(Domain);
|
|
services.AddSingleton<IProjectionClient>(Projection);
|
|
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
|
|
{
|
|
options.Authority = null;
|
|
options.MetadataAddress = null!;
|
|
options.RequireHttpsMetadata = false;
|
|
options.Configuration = new OpenIdConnectConfiguration();
|
|
options.TokenValidationParameters = new TokenValidationParameters
|
|
{
|
|
ValidateIssuer = false,
|
|
ValidateAudience = false,
|
|
ValidateLifetime = true,
|
|
ValidateIssuerSigningKey = true,
|
|
IssuerSigningKey = TestKey,
|
|
ClockSkew = TimeSpan.Zero,
|
|
};
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
/// <summary>Records the bsn the BFF forwarded.</summary>
|
|
public sealed class CapturingDomainClient : IDomainClient
|
|
{
|
|
public string? SubmittedBsn { get; private set; }
|
|
|
|
public Task<SubmitAccepted> SubmitRegistrationAsync(string bsn, CancellationToken ct = default)
|
|
{
|
|
SubmittedBsn = bsn;
|
|
return Task.FromResult(new SubmitAccepted("reg-acc-1", "Ingediend"));
|
|
}
|
|
|
|
public Task<IReadOnlyList<WerkbakItem>> GetWerkbakAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<WerkbakItem>>([]);
|
|
}
|
|
|
|
/// <summary>Serves configurable projection rows.</summary>
|
|
public sealed class StubProjectionClient : IProjectionClient
|
|
{
|
|
public List<ProjectionEntry> Entries { get; } = [];
|
|
|
|
public Task<IReadOnlyList<ProjectionEntry>> GetRegisterAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<ProjectionEntry>>(Entries);
|
|
}
|