feat(security): ABAC P2/P3-lite — BIG-nummer redaction, authz audit, guard; clear dev audit

- fix(deps): pin @babel/core ^7.29.7 via overrides → npm audit 0 (dev+prod),
  no --force / no Angular downgrade; README corrected
- feat(brief): field-level PII reveal (PRD-0002 §5c) — CaseContext BIG-nummer
  ships masked; step-up-stubbed (X-Step-Up), audited POST /brief/reveal-bignummer
  unmasks it; drafter-only capability, deny-by-default. Realized on the BIG-nummer
  (no BSN on the wire)
- feat(authz): no-PII AuditAuthz log for reveal attempts + org-admin denials (§8)
- feat(routes): wire capabilityGuard('orgtemplate:edit') onto brief/huisstijl (§6)
- test: backend +5 (Authz + reveal endpoint), FE +3 (adapter boundary, store swap)
- docs: PRD-0002 §5c/§9, WP-18 follow-up, README

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-20 19:59:20 +02:00
co-authored by Claude Opus 4.8
parent 0edfbba2a9
commit 5cae44f163
24 changed files with 353 additions and 211 deletions
@@ -91,7 +91,12 @@ import { BesluitPanelComponent } from '@brief/ui/besluit-panel/besluit-panel.com
<div class="case-meta">
<span>{{ caseContext().aanvraagReferentie }}</span>
<span>{{ caseContext().zorgverlenerNaam }}</span>
<span>{{ bigLabel() }} {{ caseContext().bigNummer }}</span>
<span>
{{ bigLabel() }} {{ caseContext().bigNummer }}
@if (canRevealBigNummer() && isMasked()) {
<app-button variant="subtle" (click)="onReveal()">{{ revealLabel() }}</app-button>
}
</span>
<span>{{ caseContext().beroep }}</span>
</div>
</div>
@@ -159,11 +164,24 @@ export class BehandelSchermComponent {
caseContext = input.required<CaseContext>();
canSubmit = input(false);
busy = input(false);
/** Server decision (PRD-0002 §5c): may this actor unmask the case BIG-nummer? */
canRevealBigNummer = input(false);
edit = output<BriefMsg>();
submit = output<void>();
preview = output<void>();
locate = output<Diagnostic>();
revealBigNummer = output<void>();
/** The BIG-nummer arrives masked (contains `*`); once revealed the swapped value has
no `*`, so the reveal action hides itself — no separate "revealed" flag needed. */
protected isMasked = computed(() => this.caseContext().bigNummer.includes('*'));
/** Step-up (PRD-0002 §5d) stubbed as a native confirm — the extra verification gesture
before an audited PII reveal. ponytail: real systems prompt MFA / recent re-auth. */
protected onReveal() {
if (confirm(this.stepUpPrompt())) this.revealBigNummer.emit();
}
private previewDialog = viewChild<ElementRef<HTMLDialogElement>>('previewDialog');
@@ -214,6 +232,10 @@ export class BehandelSchermComponent {
protected stepTitle = input($localize`:@@brief.step.opstellen:Brief opstellen`);
protected caseHeading = input($localize`:@@brief.case.heading:Aanvraag herregistratie`);
protected bigLabel = input($localize`:@@brief.case.big:BIG-nummer`);
protected revealLabel = input($localize`:@@brief.case.reveal:Toon BIG-nummer`);
protected stepUpPrompt = input(
$localize`:@@brief.case.revealConfirm:Extra verificatie vereist. Het tonen van het BIG-nummer wordt vastgelegd. Doorgaan?`,
);
protected previewLabel = input($localize`:@@brief.preview.open:Voorbeeld`);
protected openDocumentLabel = input(
$localize`:@@brief.preview.openDocument:Openen als document (PDF)`,