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:
@@ -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)`,
|
||||
|
||||
@@ -79,6 +79,19 @@ export const WithContent: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
/** Field-level PII (PRD-0002 §5c): the case BIG-nummer arrives MASKED, as the server
|
||||
ships it. The behandelaar holds the reveal capability, so the "Toon BIG-nummer"
|
||||
action shows — it runs a step-up confirm and an audited server call before unmasking. */
|
||||
export const MaskedBigNummer: Story = {
|
||||
args: {
|
||||
brief: brief({ tag: 'draft' }),
|
||||
diagnostics: [],
|
||||
canSubmit: false,
|
||||
caseContext: { ...caseContext, bigNummer: '********601' },
|
||||
canRevealBigNummer: true,
|
||||
},
|
||||
};
|
||||
|
||||
/** Rejected: the drafter reopens; the rejection comments show above the editor. */
|
||||
export const Rejected: Story = {
|
||||
render: (args) => {
|
||||
|
||||
@@ -95,9 +95,11 @@ import { BehandelSchermComponent } from '@brief/ui/behandel-scherm/behandel-sche
|
||||
[caseContext]="caseContext"
|
||||
[canSubmit]="store.canSubmit()"
|
||||
[busy]="store.busy()"
|
||||
[canRevealBigNummer]="store.canRevealBigNummer()"
|
||||
(edit)="store.edit($event)"
|
||||
(submit)="store.submit()"
|
||||
(preview)="store.previewLetter()"
|
||||
(revealBigNummer)="store.revealBigNummer()"
|
||||
/>
|
||||
} @else {
|
||||
<!-- Approver / read-only: review + approve/reject/send. -->
|
||||
|
||||
@@ -144,6 +144,7 @@ export const SubmittedApprover: Story = {
|
||||
canApprove: true,
|
||||
canReject: true,
|
||||
canSend: false,
|
||||
canRevealBigNummer: false,
|
||||
}),
|
||||
};
|
||||
export const ApprovedSender: Story = {
|
||||
@@ -153,6 +154,7 @@ export const ApprovedSender: Story = {
|
||||
canApprove: false,
|
||||
canReject: false,
|
||||
canSend: true,
|
||||
canRevealBigNummer: false,
|
||||
}),
|
||||
};
|
||||
export const Sent: Story = {
|
||||
@@ -162,5 +164,6 @@ export const Sent: Story = {
|
||||
canApprove: false,
|
||||
canReject: false,
|
||||
canSend: false,
|
||||
canRevealBigNummer: false,
|
||||
}),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user