feat(brief): WP-18 — ABAC capability spine (PRD-0002 phase P1)
Replace the FE-computed authorization anti-pattern in BriefStore.editable (derived from the unverified X-Role header) with server-computed decision flags, mirroring the existing HerregistratieDecisionsDto pattern: - Backend: Authz.cs is the single authorization helper — the SAME check (Authz.CanActOn) both gates BriefStore.Review's mutations and computes the BriefDecisionsDto flags shipped on every brief response, so emit and enforce can never drift. New GET /me returns coarse, role-derived capabilities (PRD-0002 SS6). - Every brief endpoint (including send, previously ungated on HttpContext) now returns a fresh BriefViewDto so decisions never go stale after a mutation. - FE: brief.store.ts reads canEdit/canApprove/canReject/canSend off the loaded decisions instead of computing them from currentRole(); the brief.machine carries decisions through every status transition. - New shared/domain/capability.ts + shared/application/access.store.ts + shared/infrastructure/me.adapter.ts: the general capability-spine infrastructure (AccessStore.can(), capabilityGuard) for future routes. Deviates from the original WP-18 draft by NOT renaming auth/domain's Session to a Principal union — ADR-0002 explicitly defers that refactor until a second actor exists, and the brief workflow's drafter/approver identity turned out to be a separate axis from the SSP login session entirely. See docs/backlog/WP-18-abac-capability-spine.md for the full as-built record. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -930,6 +930,42 @@ export class ApiClient {
|
||||
return Promise.resolve<SubmitApplicationResponse>(null as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return OK
|
||||
*/
|
||||
me(): Promise<MeDto> {
|
||||
let url_ = this.baseUrl + "/api/v1/me";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
let options_: RequestInit = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Accept": "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
return this.http.fetch(url_, options_).then((_response: Response) => {
|
||||
return this.processMe(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processMe(response: Response): Promise<MeDto> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200) {
|
||||
return response.text().then((_responseText) => {
|
||||
let result200: any = null;
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as MeDto;
|
||||
return result200;
|
||||
});
|
||||
} else if (status !== 200 && status !== 204) {
|
||||
return response.text().then((_responseText) => {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<MeDto>(null as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return OK
|
||||
*/
|
||||
@@ -969,7 +1005,7 @@ export class ApiClient {
|
||||
/**
|
||||
* @return OK
|
||||
*/
|
||||
briefPUT(body: SaveBriefRequest): Promise<BriefDto> {
|
||||
briefPUT(body: SaveBriefRequest): Promise<BriefViewDto> {
|
||||
let url_ = this.baseUrl + "/api/v1/brief";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
@@ -989,13 +1025,13 @@ export class ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
protected processBriefPUT(response: Response): Promise<BriefDto> {
|
||||
protected processBriefPUT(response: Response): Promise<BriefViewDto> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200) {
|
||||
return response.text().then((_responseText) => {
|
||||
let result200: any = null;
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as BriefDto;
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as BriefViewDto;
|
||||
return result200;
|
||||
});
|
||||
} else if (status === 403) {
|
||||
@@ -1015,13 +1051,13 @@ export class ApiClient {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<BriefDto>(null as any);
|
||||
return Promise.resolve<BriefViewDto>(null as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return OK
|
||||
*/
|
||||
briefSubmit(): Promise<BriefDto> {
|
||||
briefSubmit(): Promise<BriefViewDto> {
|
||||
let url_ = this.baseUrl + "/api/v1/brief/submit";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
@@ -1037,13 +1073,13 @@ export class ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
protected processBriefSubmit(response: Response): Promise<BriefDto> {
|
||||
protected processBriefSubmit(response: Response): Promise<BriefViewDto> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200) {
|
||||
return response.text().then((_responseText) => {
|
||||
let result200: any = null;
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as BriefDto;
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as BriefViewDto;
|
||||
return result200;
|
||||
});
|
||||
} else if (status === 403) {
|
||||
@@ -1063,13 +1099,13 @@ export class ApiClient {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<BriefDto>(null as any);
|
||||
return Promise.resolve<BriefViewDto>(null as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return OK
|
||||
*/
|
||||
approve(): Promise<BriefDto> {
|
||||
approve(): Promise<BriefViewDto> {
|
||||
let url_ = this.baseUrl + "/api/v1/brief/approve";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
@@ -1085,13 +1121,13 @@ export class ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
protected processApprove(response: Response): Promise<BriefDto> {
|
||||
protected processApprove(response: Response): Promise<BriefViewDto> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200) {
|
||||
return response.text().then((_responseText) => {
|
||||
let result200: any = null;
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as BriefDto;
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as BriefViewDto;
|
||||
return result200;
|
||||
});
|
||||
} else if (status === 403) {
|
||||
@@ -1111,13 +1147,13 @@ export class ApiClient {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<BriefDto>(null as any);
|
||||
return Promise.resolve<BriefViewDto>(null as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return OK
|
||||
*/
|
||||
reject(body: RejectBriefRequest): Promise<BriefDto> {
|
||||
reject(body: RejectBriefRequest): Promise<BriefViewDto> {
|
||||
let url_ = this.baseUrl + "/api/v1/brief/reject";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
@@ -1137,13 +1173,13 @@ export class ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
protected processReject(response: Response): Promise<BriefDto> {
|
||||
protected processReject(response: Response): Promise<BriefViewDto> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200) {
|
||||
return response.text().then((_responseText) => {
|
||||
let result200: any = null;
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as BriefDto;
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as BriefViewDto;
|
||||
return result200;
|
||||
});
|
||||
} else if (status === 403) {
|
||||
@@ -1163,13 +1199,13 @@ export class ApiClient {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<BriefDto>(null as any);
|
||||
return Promise.resolve<BriefViewDto>(null as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return OK
|
||||
*/
|
||||
send(): Promise<BriefDto> {
|
||||
send(): Promise<BriefViewDto> {
|
||||
let url_ = this.baseUrl + "/api/v1/brief/send";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
@@ -1185,13 +1221,13 @@ export class ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
protected processSend(response: Response): Promise<BriefDto> {
|
||||
protected processSend(response: Response): Promise<BriefViewDto> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200) {
|
||||
return response.text().then((_responseText) => {
|
||||
let result200: any = null;
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as BriefDto;
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as BriefViewDto;
|
||||
return result200;
|
||||
});
|
||||
} else if (status === 409) {
|
||||
@@ -1205,7 +1241,7 @@ export class ApiClient {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<BriefDto>(null as any);
|
||||
return Promise.resolve<BriefViewDto>(null as any);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1287,6 +1323,13 @@ export interface ApplicationSummaryDto {
|
||||
submittedAt?: string | undefined;
|
||||
}
|
||||
|
||||
export interface BriefDecisionsDto {
|
||||
canEdit?: boolean;
|
||||
canApprove?: boolean;
|
||||
canReject?: boolean;
|
||||
canSend?: boolean;
|
||||
}
|
||||
|
||||
export interface BriefDto {
|
||||
briefId?: string | undefined;
|
||||
beroep?: string | undefined;
|
||||
@@ -1312,6 +1355,7 @@ export interface BriefStatusDto {
|
||||
export interface BriefViewDto {
|
||||
brief?: BriefDto;
|
||||
availablePassages?: LibraryPassageDto[] | undefined;
|
||||
decisions?: BriefDecisionsDto;
|
||||
}
|
||||
|
||||
export interface BrpAddressDto {
|
||||
@@ -1423,6 +1467,10 @@ export interface ManualDiplomaPolicyDto {
|
||||
policyQuestions?: PolicyQuestionDto[] | undefined;
|
||||
}
|
||||
|
||||
export interface MeDto {
|
||||
capabilities?: string[] | undefined;
|
||||
}
|
||||
|
||||
export interface ParagraphDto {
|
||||
nodes?: RichTextNodeDto[] | undefined;
|
||||
list?: string | undefined;
|
||||
|
||||
Reference in New Issue
Block a user