feat(19.6.1-02): per-record locked permission values
- cabana.PermissionRecordLock and the optional PermissionRecordLockProvider - RecordMeta.PermissionLocks (meta.permissionLocks) on show and save responses, notes localized - save rule: a record-locked code keeps its stored value, a differing present value is 403 - Roles lock code-declared grants (locked_by_code); Administrators lock every override on your own record (locked_self) - SPA: FORM_PERMISSION_LOCKS, locked codes never sent, record-locked rows in the permission editor - regenerated admin OpenAPI and schema.d.ts; docs and README
This commit is contained in:
@@ -371,6 +371,33 @@ func (MembersController) AdminSetPermissionValues(_ context.Context, field strin
|
||||
- A save that does not send the field leaves the stored permissions alone, and the field's `context` applies as for any field.
|
||||
- The keys `options`, `default`, `nameFrom`, `emptyOption`, `relation` and `preset` are refused on the type. A field without `mode`, or on a controller that does not implement the provider, stops the start-up. The type is not available on settings forms or relation forms.
|
||||
|
||||
### Permissions the record fixes
|
||||
|
||||
Some values cannot change on one particular record: grants a plugin declares in code for a role apply whatever the role stores, and an administrator may not change their own overrides. The controller says so by also implementing `cabana.PermissionRecordLockProvider`. Its `AdminPermissionRecordLocks` returns, for a stored record, a list of `cabana.PermissionRecordLock`: the `Code`, the `Value` the editor shows (1, -1, or 0 for inherit) and a `Note` (a translation key or text). It is asked with the request's administrator, for a stored record only, never on a create. A roles controller locks the grants its plugins declare:
|
||||
|
||||
```go src=modules/cabana/example_permission_record_locks_test.go#AccessRolesController.AdminPermissionRecordLocks
|
||||
// AdminPermissionRecordLocks shows the grants plugins declare for the role
|
||||
// as granted and locked, with a note: unticking them on the form would
|
||||
// change nothing.
|
||||
func (c AccessRolesController) AdminPermissionRecordLocks(_ context.Context, field string, record any) ([]cabana.PermissionRecordLock, error) {
|
||||
role := record.(*AccessRole)
|
||||
var locks []cabana.PermissionRecordLock
|
||||
for _, code := range c.CodeGrants[role.Code] {
|
||||
locks = append(locks, cabana.PermissionRecordLock{
|
||||
Code: code,
|
||||
Value: 1,
|
||||
Note: "acme.access::lang.roles.granted_by_plugin",
|
||||
})
|
||||
}
|
||||
return locks, nil
|
||||
}
|
||||
```
|
||||
|
||||
- The record response (show, create and update) carries the locks as `meta.permissionLocks`, keyed by field name: `{"permissions": [{"code": "posts.edit", "value": 1, "note": "Granted to this role by a plugin. It cannot be removed here."}]}`, with the note translated in the request locale. The key is absent when no lock applies, and the form schema never carries it.
|
||||
- The admin shows a record-locked row with the lock's value, a disabled control, the lock icon and the note instead of the option's comment. A record lock wins over `Locked`. On a read-only screen (the preview) only the value is shown.
|
||||
- The admin never sends a record-locked code. On an update the server keeps the stored value of such a code when it is absent from the body; a present value must equal the stored one, otherwise the save is answered 403 `forbidden` on the field. The stored value is what `AdminPermissionValues` returns, so a role whose code grant is not in its stored permissions still saves, and the code is left out of the `Locked` comparison.
|
||||
- cabana's own Roles screen locks a role's code-declared grants with `backend::lang.permissioneditor.locked_by_code`, and the Administrators screen locks every override on the editor's own record with `backend::lang.permissioneditor.locked_self`.
|
||||
|
||||
## Markdown and multilingual fields
|
||||
|
||||
`type: markdown` edits markdown source on a host text column. The admin SPA shows a source editor with a Preview toggle. When Preview opens, and again shortly after the source changes while it is open, the SPA posts the field's source (the active locale's text for `mlmarkdown`) to the preview route below and renders only the server's answer; when the output is refused it shows the server's message as text. The server renders through `cabana.RenderMarkdown`, which uses the pinned goldmark engine without unsafe HTML. Output that still contains a script or iframe tag, an event handler, or a javascript, vbscript or data URL is refused, so translated raw HTML cannot become executable preview content. `POST <prefix>/api/v1/markdown/preview` renders a `{markdown}` source through `cabana.RenderMarkdown` for any signed-in administrator and answers `{html}`, or a 422 `validation_failed` on `markdown` when the output is refused.
|
||||
|
||||
Reference in New Issue
Block a user