passbolt/passbolt_api · error · Cake\Http\Exception\ForbiddenException

You are not authorized to edit the role.

Error message

You are not authorized to edit the role.

What it means

ForbiddenException (HTTP 403) from assertCanEdit: a non-admin user included `role` or `role_id` in the edit payload. Role management is admin-only, so any self-edit that attempts to alter the role is rejected even when editing one's own record.

Solutions

  1. Remove `role`/`role_id` from the payload; send only the fields you intend to change.
  2. If a role change is genuinely needed, perform it as an admin account.
  3. Patch client code to strip server-managed fields (role, role_id) before sending updates.

Example fix

// before
await api.editUser(id, { ...user, profile: updatedProfile }); // includes role_id
// after
const { role, role_id, ...payload } = user;
await api.editUser(id, { ...payload, profile: updatedProfile });
Defensive patterns

Strategy: type-guard

Validate before calling

function stripRoleFields(data) { const { role, role_id, ...rest } = data; return rest; }

Type guard

function hasRoleFields(data) { return 'role' in data || 'role_id' in data; }
if (hasRoleFields(payload) && session.role !== 'admin') payload = stripRoleFields(payload);

Try / catch

try { await api.editUser(id, data); } catch (e) { if (e.code === 403 && /edit the role/.test(e.message)) { data = stripRoleFields(data); return api.editUser(id, data); } throw e; }

Prevention

When it happens

Trigger: PUT /users/{id}.json with `role_id` (or `role`) present in the body while authenticated as a non-admin — including a user editing themselves and echoing back the full entity including role_id.

Common situations: Clients that serialize the entire fetched user object (including role_id) back on update instead of sending only changed fields; scripts attempting self-promotion to admin.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of passbolt/passbolt_api@31c1bbc10f (2026-09-17). Data as JSON: /api/errors/7033f2bb8a2b6ffa. Report an issue: GitHub.

Appendix: source

Thrown at src/Controller/Users/UsersEditController.php:154

        $this->success(__('The user has been updated successfully.'), $user);
    }

    /**
     * Validate if the user is authorized to edit the data
     *
     * @param array $data user data
     * @return void
     * @throws \Cake\Http\Exception\ForbiddenException if the user is not admin or not editing themselves
     * @throws \Cake\Http\Exception\ForbiddenException if the user is not admin and editing role
     */
    protected function assertCanEdit(array $data): void
    {
        // Admin can edit all users, other users can only edit themselves
        if ($this->User->role() !== Role::ADMIN && $data['id'] !== $this->User->id()) {
            throw new ForbiddenException(__('You are not authorized to access that location.'));
        }
        if ($this->User->role() !== Role::ADMIN && (isset($data['role']) || isset($data['role_id']))) {
            throw new ForbiddenException(__('You are not authorized to edit the role.'));
        }
    }

    /**
     * Validate the data coming from the request
     *
     * @param array $data user data
     * @return void
     * @throws \Cake\Http\Exception\BadRequestException if gpgkey is sent (v2 only)
     * @throws \Cake\Http\Exception\BadRequestException if groups data is sent (v2 only)
     * @throws \Cake\Http\Exception\BadRequestException if data is not provided or invalid
     */
    protected function assertRequestData(array $data): void
    {
        if (empty($data) || count($data) < 2) {
            throw new BadRequestException(__('Some user data should be provided.'));
        }
        if (isset($data['gpgkey'])) {

View on GitHub (pinned to 31c1bbc10f)