## What & why S-15a, the first of the S-15 (#16) split. A new **beheer** portal (medewerker realm, like behandel) shows the ZTC catalogus — the published zaaktypen — **read-only**. A beheerder logs in and sees the seeded BIG-REGISTRATIE zaaktype. Closes #130 ### The vertical portal → BFF `GET /beheer/catalogi/zaaktypen` (medewerker realm + `beheerder` role) → ACL `GET /catalogi/zaaktypen` → ZGW Catalogi API. - **ACL**: new read-only `GET /catalogi/zaaktypen` listing published zaaktypen (reuses the ADR-0021 Catalogi client; public-safe `identificatie`/`omschrijving`). - **BFF**: new typed `IAclClient` + `Downstream:Acl:BaseUrl`, and `GET /beheer/catalogi/zaaktypen` behind a new `beheerder` policy (reuses the medewerker bearer scheme + realm-role lifting). OpenAPI spec + generated Angular client regenerated. - **Keycloak**: `beheerder` realm role + `bram-beheerder` test user in the medewerker realm. - **Frontend**: new `apps/beheer` Angular app (copied from behandel) with a read-only catalogus page; `SECURE_API_ROUTES=['/beheer/']`. - **Infra**: `beheer` compose service (port 8143), added to `WAIT_SVCS` + CI log-dump; a Playwright e2e (beheerder login → catalogus shows BIG-REGISTRATIE). ### New boundary → ADR-0025 The BFF now reaches the **ACL directly** for the catalogus read — a new service-to-service edge (§14). The catalogus is neither a domain nor a projection concern, and §8.1 means only the ACL may read ZGW; routing through the domain would pollute it with a non-domain passthrough. §8.1/§8.3 stay intact. Recorded in **ADR-0025**. ## Definition of Done - [x] Failing test committed before each implementation (red→green per layer: ACL, BFF, frontend). - [x] Conventional Commits referencing #130. - [ ] CI green — pending Gitea Actions run. - [x] `docker compose up` brings up `beheer` (health-gated in `WAIT_SVCS`). - [x] Docs — ADR-0025 + demo-script S-15a note. - [x] Demo note in `docs/demo-script.md`. ## Verified locally lint (`dotnet format`) ✓ · .NET unit (Acl 57 / Big 152 / EventSubscriber 19 / Bff 40) ✓ · frontend lint+test (8 projects) ✓ · frontend build (4 apps) ✓. Mutation ratchet: added a gateway unit test for the new `ListZaaktypenAsync` mapping so the ACL score holds. verify-stack (compose smoke + e2e) runs in CI. ## Notes for reviewers - The BFF drops the ZGW URL from `BeheerZaaktype` (public-safe: identificatie + omschrijving only). - The catalogus e2e asserts on the stable seeded `BIG-REGISTRATIE` (not a per-test reference), safe on the shared verify stack. - Follow-ups: **S-15b** (#131) default-fill CRUD, **S-15c** (#132) medewerker-realm MFA. 🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #133
66 lines
2.7 KiB
TypeScript
66 lines
2.7 KiB
TypeScript
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
|
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
|
import { TestBed } from '@angular/core/testing';
|
|
import { BffApiV1Service } from 'api-client';
|
|
import { authInterceptor } from 'auth';
|
|
import { AbstractSecurityStorage, ConfigurationService } from 'angular-auth-oidc-client';
|
|
import { SECURE_API_ROUTES } from './app.config';
|
|
|
|
// Guards the medewerker token wiring end-to-end. The api-client calls the BFF with RELATIVE URLs, and
|
|
// the angular-auth-oidc-client interceptor attaches the token only when `req.url` starts with a
|
|
// configured secureRoute. A regression to an absolute origin makes the relative URL never match, so
|
|
// the beheer calls go out unauthenticated and the BFF answers 401. This drives the REAL interceptor
|
|
// and the REAL api-client against the REAL production route value (SECURE_API_ROUTES); only the config
|
|
// source and token storage are faked, so the assertion turns on the actual route-matching.
|
|
describe('beheer medewerker token wiring', () => {
|
|
let http: HttpTestingController;
|
|
let bff: BffApiV1Service;
|
|
const token = 'medewerker-access-token';
|
|
|
|
beforeEach(() => {
|
|
TestBed.configureTestingModule({
|
|
providers: [
|
|
provideHttpClient(withInterceptors([authInterceptor()])),
|
|
provideHttpClientTesting(),
|
|
{
|
|
provide: ConfigurationService,
|
|
useValue: {
|
|
hasAtLeastOneConfig: () => true,
|
|
getAllConfigurations: () => [{ configId: 'medewerker', secureRoutes: SECURE_API_ROUTES }],
|
|
},
|
|
},
|
|
{
|
|
// A signed-in session: the storage the interceptor's token lookup reads from.
|
|
provide: AbstractSecurityStorage,
|
|
useValue: {
|
|
read: () => JSON.stringify({ authzData: token, authnResult: { id_token: 'id-token' } }),
|
|
write: () => undefined,
|
|
remove: () => undefined,
|
|
clear: () => undefined,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
http = TestBed.inject(HttpTestingController);
|
|
bff = TestBed.inject(BffApiV1Service);
|
|
});
|
|
|
|
afterEach(() => http.verify());
|
|
|
|
it('attaches the bearer token to the relative catalogus call', () => {
|
|
bff.getBeheerCatalogiZaaktypen().subscribe();
|
|
|
|
const req = http.expectOne('/beheer/catalogi/zaaktypen');
|
|
expect(req.request.headers.get('Authorization')).toBe(`Bearer ${token}`);
|
|
req.flush([]);
|
|
});
|
|
|
|
it('leaves the anonymous openbaar register call unauthenticated', () => {
|
|
bff.getOpenbaarRegister().subscribe();
|
|
|
|
const req = http.expectOne((r) => r.url === '/openbaar/register');
|
|
expect(req.request.headers.has('Authorization')).toBe(false);
|
|
req.flush([]);
|
|
});
|
|
});
|