feat: /v1/role list, get, create, update and delete. Reassign members on delete. Write-time validation (ValidateGrants, duplicates, typed keys against real content types). Name uniqueness (role_name_taken), role_locked, role_has_members, role_conflict on version mismatch. No-escalation: nobody grants a permission they don't hold, and roles.edit_owner_only guards the Owner role. Activity events for role changes. Typed-key exposure in /auth/me. Cleanup of typed rows when a content type is deleted. | Backend only. Will carry the deferred ListGrants decision.
This commit is contained in:
1 parent
a043f7f41a
commit
094df63a7f
51 files changed
+4822
-115
No files matched your search
+52
-1
@@ -50,7 +50,7 @@ key never reads the audit trail. Events are append-only; there is no `POST`, `PU
|
||||
| `GET /v1/activity/export` | Owner, admin | The same filters as the list without `page` or `size`, as a `text/csv; charset=utf-8` attachment (`when,actor,action,target,outcome,source,ip`). Cells starting with `=`, `+`, `-` or `@` get a leading quote. |
|
||||
|
||||
Query: `q`, `actor` (`user:ID`, `key:ID`, `system`), `category` (`articles`, `media`, `users`,
|
||||
`api_keys`, `auth`, `system`, `sites`, `authors`), `outcome` (`all`, `success`, `failed`), `security` (`true`), `range`
|
||||
`api_keys`, `auth`, `system`, `sites`, `authors`, `roles`), `outcome` (`all`, `success`, `failed`), `security` (`true`), `range`
|
||||
(`today`, `7d`, `30d`, `all`, `custom`; default `7d`), `from` and `to` (dates, with `custom`), `page` (from
|
||||
1) and `size` (25, 50 or 100). Any invalid value answers 400 with the body-mismatch message. Dates
|
||||
are UTC. The list answers `{ items, total, page, size }`, newest first (`created_at`, then `id`).
|
||||
@@ -353,6 +353,57 @@ disabled variant on `POST`), 413 "The source image is too large to generate this
|
||||
source image can't be read, so this variant can't be generated.", 422 "Variants aren't generated for
|
||||
this format." and 500 "Internal server error".
|
||||
|
||||
## Roles
|
||||
|
||||
Routes under `/v1/role`, behind the verified chain. Reads accept a key; every write is session only (a key answers
|
||||
403 "This action needs a real session."). The permission is checked before the id, the body and any lookup.
|
||||
|
||||
| Route | Permission | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `GET /v1/role` | `roles.view` | Every role by id, built-ins first. |
|
||||
| `GET /v1/role/{id}` | `roles.view` | One role. |
|
||||
| `POST /v1/role` | `roles.manage` | Create a role (201). `built_in` and `locked` in the body are ignored. |
|
||||
| `PUT /v1/role/{id}` | `roles.manage` | Replace name, description and grants. `version` is required and must match. |
|
||||
| `DELETE /v1/role/{id}` | `roles.manage` | Delete a role with no members (204). |
|
||||
| `POST /v1/role/{id}/reassign` | `roles.manage` | Move every member to `{"target_role_id": N}`; answers `{"moved": n}`. |
|
||||
| `POST /v1/role/{id}/reset` | `roles.manage` and `roles.edit_owner_only` | Restore a built-in role to its seeded grants and description; body `{"version": N}`. |
|
||||
|
||||
A role is `{ id, name, description, built_in, locked, version, grants, members, updated_at, updated_by }`.
|
||||
`grants` is the normalised effective list: catalogue order, unknown keys and owner-only keys on an unlocked role
|
||||
dropped, duplicates resolved to the most restrictive scope, and a per-type entry (`content.update:events`, scope
|
||||
`none` included) right after its wildcard. A response is always valid as the `grants` of a `PUT`. `members` counts
|
||||
users by their effective role (the legacy `owner` and `admin` strings count for the built-in role of that name).
|
||||
`updated_at` (RFC 3339, UTC) and `updated_by` (`{ id, name }`, the display name else the username) are `null` for a role
|
||||
never edited since seeding, and `updated_by` is also `null` once the saving user is deleted.
|
||||
|
||||
Refusals are plain text, one snake_case code per body:
|
||||
|
||||
| Status | Body | When |
|
||||
| --- | --- | --- |
|
||||
| 400 | `unknown_permission`, `invalid_scope`, `owner_only_permission`, `duplicate_permission`, `unknown_content_type` | A grant is not valid (`unknown_content_type` for a `content.*:{type}` key that is neither `article` nor a content type). |
|
||||
| 400 | `invalid_role_name`, `built_in_name`, `not_built_in`, `invalid_reassign` | Blank name after trim; renaming a built-in role; resetting a custom role; a bad reassign target (self, the Owner role, unknown). |
|
||||
| 403 | `role_locked` | Editing, deleting or moving from the Owner role. |
|
||||
| 403 | `missing_permission` | The usual gate, plus `X-Missing-Permission: roles.edit_owner_only` when a built-in role is edited or reset without that permission. |
|
||||
| 403 | `own_role` | Editing, deleting or moving members out of the caller's own role. |
|
||||
| 403 | `escalation` | The caller would grant, or touch a role holding, more than they hold. `X-Escalation-Key` names the first offending key, such as `content.publish` or `content.publish:events`. |
|
||||
| 404 | `role_not_found` | Unknown role id. |
|
||||
| 409 | `role_name_taken`, `role_conflict`, `role_has_members`, `role_built_in` | Name already used (case-insensitive); stale `version`; the role still has members; deleting or moving members out of a built-in role. |
|
||||
|
||||
Names are trimmed and unique case-insensitively. The order of checks on a write is: permission, id, body, role lookup,
|
||||
locked, built-in rules, own role, existing role covered by the caller, version, grant validation, new grants covered
|
||||
by the caller, name taken, write. Only the Owner holds `roles.edit_owner_only`, so only the Owner edits the Admin and
|
||||
Writer roles; owner-only keys are refused on every role a client can write. A save with nothing changed answers 200
|
||||
at the same version and records nothing. Members see an edit on their next request, and so do the keys they created.
|
||||
|
||||
Activity: `role.create`, `role.edit`, `role.delete` and `role.reassign` in the `roles` category, target
|
||||
`{ type: "role", id, name }`. `role.edit` carries `name` and `description` changes and the params `added`, `removed`,
|
||||
`changed` (`{ key, from, to }`) and `members`; `role.reassign` carries `from`, `to` and `moved` and is recorded only when
|
||||
`moved` is above 0. A refusal records nothing.
|
||||
|
||||
`GET /v1/auth/me` lists per-type entries in `access.permissions` (scope `none` included), right after their wildcard.
|
||||
Deleting a content type removes its per-type rows from every role (the affected roles' `version` goes up by one), and
|
||||
creating one with the key of a deleted type clears any stale row for that key.
|
||||
|
||||
## Content permissions
|
||||
|
||||
Articles and the entries of custom content types are checked against the `content.*` permissions of the
|
||||
|
||||
Reference in new issue
Block a user