feat(12.1-01): cabana.ForbiddenError answers a refused write with 403
- hooks and bulk, record, toolbar and widget actions may return it - 403 forbidden with the localized message and field details; the write's transaction is rolled back; other errors stay the opaque 500 - form shows a refused save as a persistent banner and keeps the values; a refused delete is a toast - smoke tests, OpenAPI notes, dist, README, docs
This commit is contained in:
@@ -221,6 +221,20 @@ A hook or scope that has to read the database during a write should use the tran
|
||||
|
||||
Scope reads and writes with `pact.ListExtendQuery` and `pact.FormExtendQuery` rather than checking in a hook: the scope then applies to every route, including relation and action routes.
|
||||
|
||||
## Refusing a write
|
||||
|
||||
A hook or an action stops a write by returning an error. Which error decides what the administrator sees:
|
||||
|
||||
| Error | Answer |
|
||||
|-------|--------|
|
||||
| `cabana.ValidationError` | 422 `validation_failed`, with its `Details` as messages per field. |
|
||||
| `cabana.ForbiddenError` | 403 `forbidden`, with its `Message` and its `Details`. |
|
||||
| any other error | The opaque 500. The error is logged on the server and its text never reaches the client. |
|
||||
|
||||
Return a `cabana.ForbiddenError` when the signed-in administrator may open the screen but may not make this particular change, for example editing a record that needs a higher permission. `Message` is a translation key or text; cabana translates it, and every `Details` message, in the request locale. `Details` maps a field name to a list of messages, as a `cabana.ValidationError` does, and may be left out. With an empty `Message` the admin shows its own text.
|
||||
|
||||
Every one of these errors rolls the write's transaction back, so a refused write changes nothing: a bulk action that refuses on its third record leaves the first two untouched. The admin SPA shows a refused save as a banner above the form and keeps what the administrator typed; a refused delete or action is a toast. A `cabana.ForbiddenError` works the same from the form hooks, the relation hooks, and bulk, record, toolbar and widget actions.
|
||||
|
||||
## Toolbar actions
|
||||
|
||||
`toolbar.buttons` in `config_list.yaml` lists the built-in `create` and `delete` and any action the controller registers through `pact.HasAdminActions`. The declarations are enforced by the server, not only shown by the SPA. `POST /{controller}` needs a `config_form.yaml` and `create` in `toolbar.buttons`; `PUT` and `DELETE /{controller}/{id}` need a form (the form screen carries the delete button, as in WinterCMS); `POST /{controller}/bulk-delete` needs `delete` in `toolbar.buttons`, which in turn needs `showCheckboxes: true`. A write the controller does not declare answers 403 `forbidden`. See [Partials and widgets](partials-and-widgets.md) for actions and the rest of the extension points.
|
||||
|
||||
@@ -68,7 +68,9 @@ A controller's `pact.AdminPermissioned.RequiredPermissions` are checked before a
|
||||
|
||||
An administrator's grants are the role's `permissions` merged with the administrator's own `backend_users.permissions`, the way WinterCMS merges them: the administrator's value for a code replaces the role's, and only `1` grants. A `-1` (or `0`) on the administrator therefore removes a permission the role grants, so rows copied from a WinterCMS database keep their denies. As in WinterCMS the merge compares codes exactly, so denying `acme.blog.access_posts` does not take it back from a role that grants `acme.blog.*`.
|
||||
|
||||
Actions registered through `pact.HasAdminActions` may name extra permissions, checked on top of the controller's.
|
||||
Actions registered through `pact.HasAdminActions`, bulk actions (`pact.HasAdminBulkActions`) and record actions (`pact.HasAdminRecordActions`) may each name extra permissions, checked on top of the controller's. An administrator who lacks them does not get the action in the list schema or in a record's `meta.actions`, and posting it answers 403 and is written to the authentication log. A bulk or record action that ran is logged with the controller, the action, the administrator and the affected count or record id, without record contents.
|
||||
|
||||
A permission denial and a refusal are different answers with the same status. A denial comes from the framework: the administrator lacks a permission code, and the 403 carries the framework's fixed text. A refusal comes from controller code that returns a `cabana.ForbiddenError`: the administrator may run the route, but this change is not allowed for this record, and the 403 carries the plugin's translated message and, optionally, messages per field. See [Refusing a write](admin-controllers.md#refusing-a-write).
|
||||
|
||||
## Managing administrators
|
||||
|
||||
|
||||
Reference in New Issue
Block a user