Say what edit_clients and edit_users reach, and document the new refusals

Setting a password and removing a second factor are how an
administrator lets a locked-out person back in, so a token holding
edit_clients or edit_users can sign in as the accounts it may edit. That
stays what those abilities mean; it is now said where it is chosen. The
token form warns when either is ticked, and the API guide says it beside
the abilities, with the three refusals on your own account under "Staff
accounts". The OpenAPI document carries the new 403s, and CHANGELOG.md
an Unreleased entry.

GHSA-j5cp-r8pr-m5cr
This commit is contained in:
ignacionelson
2026-10-03 23:09:03 -03:00
parent 2da341b821
commit d3230a4b64
5 changed files with 100 additions and 5 deletions
+16
View File
@@ -10,6 +10,22 @@ Anything under **⚠️ Important — do these yourself** is something you have
we did. It sits at the top of a release for that reason. Older entries call the same section
**Upgrade notes**.
## Unreleased
**Fixed**
- **An API token can no longer change its own owner's password, email address or second factor,
and neither can the staff screen without your current password.** Reported by
[@simjiun](https://github.com/simjiun).
*Who this affected:* installations where someone holds an API token with "Manage users" and
"Edit users". Such a token could give its own owner a new password, remove their second factor,
and then sign in as them with everything they can do, including abilities the token was never
given. The same staff screen also let a signed-in administrator change their own email address
or password without the current password the profile asks for. Your own credentials are now
changed only from your profile. Setting *someone else's* password now also revokes their API
tokens. Nothing to do on upgrade.
## 2.6.0 — 25 September 2026
Mostly fixes: files and folders are easier to tidy, the sign-in pages carry your brand better, and
@@ -160,6 +160,11 @@ class UsersController extends Controller
*
* Refused with a 422 if the change would leave the installation with
* no active administrator, or if you would be deactivating yourself.
*
* Your own email address and password cannot be changed here: that is
* a `403`. Change them from your profile in the web interface, which
* asks for your current password. Setting somebody else's password
* signs them out of the API: every token they hold is revoked.
*/
public function update(Request $request, User $user): StaffUserResource
{
@@ -278,6 +283,9 @@ class UsersController extends Controller
* The account holder is emailed that this happened, and the action is
* recorded in the activity log against the caller. Answers 204 whether
* or not a second factor was actually in force.
*
* Not for your own account, which is a `403`: remove your own second
* factor from your profile in the web interface.
*/
public function destroyTwoFactor(Request $request, User $user, TwoFactorAdministration $twoFactor): JsonResponse
{
+15 -3
View File
@@ -90,12 +90,12 @@ list for your account.
| `upload_public` | set `public` when editing |
| `upload` / `edit_files` / `edit_others_files` | read and write a file's comments |
| `manage_clients` | list clients |
| `create_clients` / `edit_clients` / `delete_clients` | create, read and edit, delete clients; `edit_clients` also removes a client's two-factor authentication |
| `create_clients` / `edit_clients` / `delete_clients` | create, read and edit, delete clients; `edit_clients` also sets a client's password and removes their two-factor authentication |
| `manage_groups` | list groups |
| `create_groups` / `edit_groups` / `delete_groups` | create, read and edit (including membership), delete groups |
| `moderate_comments` | list what is awaiting approval, and approve it |
| `manage_users` | list staff accounts and the roles you may assign |
| `create_users` / `edit_users` / `delete_users` | create, read and edit, delete staff accounts; `edit_users` also removes an account's two-factor authentication |
| `create_users` / `edit_users` / `delete_users` | create, read and edit, delete staff accounts; `edit_users` also sets an account's password and removes its two-factor authentication |
There is no ability for *writing* a comment. Who may comment is an installation setting rather than
a per-role permission, so the file abilities are the gate — the same question the web asks, which is
@@ -104,6 +104,13 @@ endpoint also lets an author remove their own within the editing window and that
it additionally requires the token's owner to hold `moderate_comments`, checked live against the
account rather than carried by the token.
**`edit_clients` and `edit_users` are control of the accounts they reach.** Setting a password and
removing a second factor are what an administrator does for somebody who is locked out, so a token
holding either ability can sign in as any client, or any staff account below its owner, that it may
edit, and do what that account can do. Give these abilities to a token only when you would trust its
holder with those accounts themselves. Your *own* credentials are never reachable this way (see
"Staff accounts").
Where an endpoint accepts several — `edit_files` *or* `edit_others_files` — holding either is enough,
and which one applies to a given file depends on whether you uploaded it. For a folder, it depends
on whether you created it.
@@ -417,7 +424,7 @@ Two abilities are needed for each call: `manage_users` to reach the area at all,
action (`create_users`, `edit_users`, `delete_users`). That mirrors the web UI, where the whole
section sits behind `manage_users` and each button behind its own key.
### Two rules that will refuse you
### Three rules that will refuse you
**You cannot hand out authority you do not hold.** `role_id` must name a role you could grant
yourself: a caller who is not an administrator may not create one, nor assign any role carrying a
@@ -428,6 +435,11 @@ guess an id.
**The installation always keeps an active administrator.** Demoting, deactivating or deleting the
last one is a `422`. So is deactivating or deleting yourself, from either surface.
**Your own credentials stay behind your profile.** Changing your own email address or password, or
removing your own second factor, is a `403`. Do it from your profile in the web interface, which
asks for your current password; a token cannot be asked for one. Setting *somebody else's* password
revokes every token they hold.
### Changing a role
The assigned role is a field on the account, so `PATCH /users/{user}` with `role_id` is the whole
+46 -2
View File
@@ -4034,7 +4034,7 @@
},
"patch": {
"operationId": "users.update",
"description": "PATCH semantics: an absent key means \"leave alone\", not \"clear\".\nSending `assigned_clients` replaces the whole list; omitting it\nleaves it, except that moving to a role which is not client-scoped\nclears it either way.\n\nRefused with a 422 if the change would leave the installation with\nno active administrator, or if you would be deactivating yourself.\n\nRequires a token with the ability: `edit_users`.",
"description": "PATCH semantics: an absent key means \"leave alone\", not \"clear\".\nSending `assigned_clients` replaces the whole list; omitting it\nleaves it, except that moving to a role which is not client-scoped\nclears it either way.\n\nRefused with a 422 if the change would leave the installation with\nno active administrator, or if you would be deactivating yourself.\n\nYour own email address and password cannot be changed here: that is\na `403`. Change them from your profile in the web interface, which\nasks for your current password. Setting somebody else's password\nsigns them out of the API: every token they hold is revoked.\n\nRequires a token with the ability: `edit_users`.",
"summary": "Update a staff account, including the role assigned to it",
"tags": [
"Users"
@@ -4108,6 +4108,28 @@
}
}
},
"403": {
"description": "An error",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Error overview.",
"examples": [
"Change your own email address and password from your profile."
]
}
},
"required": [
"message"
]
}
}
}
},
"404": {
"$ref": "#/components/responses/ModelNotFoundException"
},
@@ -4161,7 +4183,7 @@
"/users/{user}/two-factor": {
"delete": {
"operationId": "users.two-factor.destroy",
"description": "The remedy for a locked-out account: somebody whose authenticator\napp and recovery codes are both gone cannot sign in, and nobody else\ncan open the account for them either. Afterwards the account signs\nin with its password alone, and \u2014 if this installation enforces\ntwo-factor authentication for staff \u2014 is asked to enrol again on its\nnext request.\n\nThe account holder is emailed that this happened, and the action is\nrecorded in the activity log against the caller. Answers 204 whether\nor not a second factor was actually in force.\n\nRequires a token with the ability: `edit_users`.",
"description": "The remedy for a locked-out account: somebody whose authenticator\napp and recovery codes are both gone cannot sign in, and nobody else\ncan open the account for them either. Afterwards the account signs\nin with its password alone, and \u2014 if this installation enforces\ntwo-factor authentication for staff \u2014 is asked to enrol again on its\nnext request.\n\nThe account holder is emailed that this happened, and the action is\nrecorded in the activity log against the caller. Answers 204 whether\nor not a second factor was actually in force.\n\nNot for your own account, which is a `403`: remove your own second\nfactor from your profile in the web interface.\n\nRequires a token with the ability: `edit_users`.",
"summary": "Remove a staff account's two-factor authentication",
"tags": [
"Users"
@@ -4189,6 +4211,28 @@
}
}
},
"403": {
"description": "An error",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Error overview.",
"examples": [
"Remove your own two-factor authentication from your profile."
]
}
},
"required": [
"message"
]
}
}
}
},
"404": {
"$ref": "#/components/responses/ModelNotFoundException"
},
@@ -1,4 +1,5 @@
import InputError from '@/components/input-error';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
@@ -129,6 +130,20 @@ export function ApiTokenForm({ values, setValue, errors, availableAbilities, max
})}
</div>
{/* Both reach other people's sign-in: setting a password and
removing a second factor are how an administrator lets a
locked-out person back in, so a token holding either can
become the accounts it may edit. */}
{(values.abilities.includes('edit_clients') || values.abilities.includes('edit_users')) && (
<Alert>
<AlertDescription>
{t(
'This token can set passwords and remove two-factor authentication on the accounts it may edit, so whoever holds it can sign in as those accounts. Choose these abilities only for a holder you would trust with the accounts themselves.',
)}
</AlertDescription>
</Alert>
)}
<InputError message={errors.abilities ?? errors['abilities.0']} />
</div>