fix(cabana): safe account action URLs, per-section isolation and the prompted record action contract (B-WR-03, B-WR-05, B-WR-06)
- an account action URL is kept only when it is http(s) with a host or a same-origin path; anything else is dropped and logged without the URL - a failing Status marks its section error: true and a failing Applies drops only that action, instead of a 500 for the whole list - status text goes through localizeDetail (B-IN-03); the non-string payload refusal is the phrase key action_prompt_invalid (B-IN-02) - the record action annotations and pact's prompt doc say what happens (B-IN-05 text); admin.json and schema.d.ts regenerated
This commit is contained in:
@@ -50,6 +50,10 @@
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"error": {
|
||||
"description": "Error is true when the section's status could not be loaded (the\nplugin's Status failed); the other sections are answered as usual.\nAbsent otherwise.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -57,7 +61,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"description": "Status is the section's line for the signed-in admin; \"\" when the\nsection has none.",
|
||||
"description": "Status is the section's line for the signed-in admin; \"\" when the\nsection has none or its status could not be loaded.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -247,7 +251,7 @@
|
||||
"cabana.AdminActionRequest": {
|
||||
"properties": {
|
||||
"payload": {
|
||||
"description": "Payload is the widget's own JSON value (the summer-action event's\ndetail.payload): any JSON, at most 64 KiB, handed to the action as-is.\nToolbar and record routes refuse it."
|
||||
"description": "Payload is the widget's own JSON value (the summer-action event's\ndetail.payload): any JSON, at most 64 KiB, handed to the action as-is.\nToolbar routes refuse it; record routes accept it only for a prompted\naction, as a JSON string (the prompt's answer)."
|
||||
},
|
||||
"record_id": {
|
||||
"type": "integer"
|
||||
@@ -5934,7 +5938,7 @@
|
||||
},
|
||||
"/{vendor}/{plugin}/{controller}/toolbar/{action}": {
|
||||
"post": {
|
||||
"description": "Runs a controller-registered action that the list's toolbar.buttons declares. The body must be {}: record_id, values and payload are all refused, and the answer's fill is always empty.",
|
||||
"description": "Runs a controller-registered action that the list's toolbar.buttons declares. The body must be {}: the route refuses record_id, values and payload, and the answer's fill is always empty.",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Vendor",
|
||||
@@ -6480,7 +6484,7 @@
|
||||
},
|
||||
"/{vendor}/{plugin}/{controller}/{id}/actions/{action}": {
|
||||
"post": {
|
||||
"description": "Runs a record action the controller registers and the form's recordActions declares. The record is loaded and row-locked through the controller's form scope in one transaction (404 when missing or out of scope); an action that does not apply to the record's current state answers 409. The body must be {}: record_id, values and payload are all refused, and the answer's fill is always empty.",
|
||||
"description": "Runs a record action the controller registers and the form's recordActions declares. The record is loaded and row-locked through the controller's form scope in one transaction (404 when missing or out of scope); an action that does not apply to the record's current state answers 409. The body is {} for an action without a prompt; an action whose record response carries prompt takes {\"payload\": \"\u003ctext\u003e\"}, trimmed, non-empty and at most prompt.max_length runes, with a 422 on payload otherwise. record_id and values are always refused, and the answer's fill is always empty.",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Vendor",
|
||||
@@ -6536,7 +6540,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Empty object",
|
||||
"description": "Empty object, or {payload: text} for a prompted action",
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
|
||||
17
admin/src/api/schema.d.ts
vendored
17
admin/src/api/schema.d.ts
vendored
@@ -2636,7 +2636,7 @@ export interface paths {
|
||||
put?: never;
|
||||
/**
|
||||
* Run a toolbar action
|
||||
* @description Runs a controller-registered action that the list's toolbar.buttons declares. The body must be {}: record_id, values and payload are all refused, and the answer's fill is always empty.
|
||||
* @description Runs a controller-registered action that the list's toolbar.buttons declares. The body must be {}: the route refuses record_id, values and payload, and the answer's fill is always empty.
|
||||
*/
|
||||
post: {
|
||||
parameters: {
|
||||
@@ -3030,7 +3030,7 @@ export interface paths {
|
||||
put?: never;
|
||||
/**
|
||||
* Run a declared record action
|
||||
* @description Runs a record action the controller registers and the form's recordActions declares. The record is loaded and row-locked through the controller's form scope in one transaction (404 when missing or out of scope); an action that does not apply to the record's current state answers 409. The body must be {}: record_id, values and payload are all refused, and the answer's fill is always empty.
|
||||
* @description Runs a record action the controller registers and the form's recordActions declares. The record is loaded and row-locked through the controller's form scope in one transaction (404 when missing or out of scope); an action that does not apply to the record's current state answers 409. The body is {} for an action without a prompt; an action whose record response carries prompt takes {"payload": "<text>"}, trimmed, non-empty and at most prompt.max_length runes, with a 422 on payload otherwise. record_id and values are always refused, and the answer's fill is always empty.
|
||||
*/
|
||||
post: {
|
||||
parameters: {
|
||||
@@ -3050,7 +3050,7 @@ export interface paths {
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
/** @description Empty object */
|
||||
/** @description Empty object, or {payload: text} for a prompted action */
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["cabana.AdminActionRequest"];
|
||||
@@ -5521,11 +5521,17 @@ export interface components {
|
||||
"cabana.AccountContributionView": {
|
||||
actions: components["schemas"]["cabana.AccountContributionActionView"][];
|
||||
description: string;
|
||||
/**
|
||||
* @description Error is true when the section's status could not be loaded (the
|
||||
* plugin's Status failed); the other sections are answered as usual.
|
||||
* Absent otherwise.
|
||||
*/
|
||||
error?: boolean;
|
||||
label: string;
|
||||
name: string;
|
||||
/**
|
||||
* @description Status is the section's line for the signed-in admin; "" when the
|
||||
* section has none.
|
||||
* section has none or its status could not be loaded.
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
@@ -5577,7 +5583,8 @@ export interface components {
|
||||
/**
|
||||
* @description Payload is the widget's own JSON value (the summer-action event's
|
||||
* detail.payload): any JSON, at most 64 KiB, handed to the action as-is.
|
||||
* Toolbar and record routes refuse it.
|
||||
* Toolbar routes refuse it; record routes accept it only for a prompted
|
||||
* action, as a JSON string (the prompt's answer).
|
||||
*/
|
||||
payload?: unknown;
|
||||
record_id?: number;
|
||||
|
||||
3
admin/tests/fixtures/lang.json
vendored
3
admin/tests/fixtures/lang.json
vendored
@@ -228,6 +228,9 @@
|
||||
"backend::lang.extension.widget_failed": {
|
||||
"other": "Nie udało się wczytać tej kontrolki. Odśwież stronę, a jeśli błąd wróci, sprawdź zasoby wtyczki."
|
||||
},
|
||||
"backend::lang.form.action_prompt_invalid": {
|
||||
"other": "Odpowiedź musi być tekstem."
|
||||
},
|
||||
"backend::lang.form.add": {
|
||||
"other": "Dodaj"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user