Closes #132.
Staff logins (behandel + beheer portals) now need a second factor; the citizen realms are unchanged.
**How:** every seeded medewerker carries a TOTP credential, which activates Keycloak's stock *conditional OTP* step in both the browser flow and the direct grant — no custom browser-flow JSON in the export. `CONFIGURE_TOTP` is a default required action so a medewerker added later must enrol first. ADR-0031 records the choice and, explicitly, that the shared fixture secret is a demo posture only.
**Tests (red first, 30c5279):**
- `check_realms.py` asserts the medewerker password-only grant is **refused**, then that password + TOTP succeeds and still carries the `behandelaar` role. It failed with `[MFA NOT ENFORCED]` against the old export.
- The three medewerker e2e logins move to `loginMedewerker()` (`tests/e2e/medewerker-login.ts`), which submits Keycloak's OTP prompt. Both TOTP implementations (Python `hmac`, Node `crypto`) are ~6 lines of RFC 6238 — no new dependency.
Verified locally against Keycloak 26.1: password-only → `invalid_grant`, password + code → 200, and the browser flow's `#otp` prompt accepts a computed code and issues an auth code.
## Definition of Done
- [x] Failing test/verify committed first; implementation makes it pass.
- [x] Conventional Commits referencing the issue (`refs #132`).
- [ ] CI green (verify-stack compose smoke + relevant checks).
- [x] `docker compose up` reaches green health within 3 minutes (Keycloak change is import-time only).
- [x] Docs touched (runbook, synthetic-data, demo-script) + ADR-0031 + demo note.
- [x] Closed by the merging PR (`closes #132`).
🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #158
26 lines
1.2 KiB
TypeScript
26 lines
1.2 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
|
import { loginMedewerker } from './medewerker-login';
|
|
|
|
// S-15b: a beheerder edits the ACL default-fill in the beheer portal and gets a saved confirmation.
|
|
// Runs against the shared verify stack; it edits + saves (the ACL store is in-memory, ADR-0026) and
|
|
// asserts the confirmation, without depending on another test's state.
|
|
test('a beheerder edits and saves the default-fill', async ({ page }) => {
|
|
await page.goto('http://beheer/');
|
|
|
|
// Keycloak medewerker-realm login (same realm as behandel) — password + enforced TOTP.
|
|
await loginMedewerker(page, 'bram-beheerder');
|
|
|
|
await expect(page.getByRole('heading', { name: /Catalogus/i })).toBeVisible();
|
|
|
|
// Navigate to the default-fill editor and change a value.
|
|
await page.getByRole('link', { name: /Default-fill/i }).click();
|
|
await expect(page.getByRole('heading', { name: /Default-fill/i })).toBeVisible();
|
|
|
|
const bron = page.getByLabel('Bronorganisatie');
|
|
await expect(bron).toBeVisible();
|
|
await bron.fill('517439943');
|
|
await page.getByRole('button', { name: /Opslaan/i }).click();
|
|
|
|
await expect(page.getByText(/standaardwaarden zijn opgeslagen/i)).toBeVisible();
|
|
});
|