passbolt/passbolt_api · error · BadRequestException

The permissions data must be an array.

Error message

The permissions data must be an array.

What it means

BadRequestException thrown by PermissionsUpdatePermissionsService::updatePermissions when an element of the $data array passed for bulk permission updates is not itself an array. Each row must be an associative array describing a permission change (id, type, delete flags).

Solutions

  1. Ensure every element of $data is an associative array, e.g. [{"id": "uuid", "type": 7}, ...].
  2. Validate/normalize the payload client-side before sending (map rows to objects).
  3. Catch BadRequestException and return a message clarifying the expected changes shape.
  4. Filter out null/scalar entries before calling updatePermissions if the source list is untrusted.

Example fix

// before
$service->updatePermissions($uac, $resourceId, $changesIds, [$permId]); // scalar row

// after
$rows = array_map(fn ($id) => ['id' => $id, 'type' => PermissionsTable::OWNER], $permIds);
$rows = array_filter($rows, 'is_array');
$service->updatePermissions($uac, $resourceId, $changesIds, $rows);
Defensive patterns

Strategy: validation

Validate before calling

// PHP
foreach ($data as $row) {
    if (!is_array($row)) {
        throw new BadRequestException(__('Each permission change must be an object with id/type.'));
    }
}

Type guard

function isPermissionChangeRows(mixed $data): bool {
    return is_array($data) && array_reduce($data, fn ($ok, $r) => $ok && is_array($r), true);
}

Try / catch

try {
    $dto = $service->updatePermissions($uac, $resourceId, $changesIds, $data);
} catch (\Cake\Http\Exception\BadRequestException $e) {
    return $this->respondWithError(400, __('Malformed permission changes payload: {0}', $e->getMessage()));
}

Prevention

When it happens

Trigger: Calling updatePermissions($user, $resourceId, $changesIds, $data) where some entries of $data are scalars or null — e.g. JSON payload like {"changes": ["abc"]} or a flat list of ids instead of row objects.

Common situations: API clients sending a plain array of permission ids when the endpoint expects rows with id/type; malformed JSON where a row was serialized as a string; frontend sending null entries after filtering.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

Thrown at src/Service/Permissions/PermissionsUpdatePermissionsService.php:78

     * @param \App\Utility\UserAccessControl $uac The operator.
     * @param string $aco The type of entity
     * @param string $acoForeignkey The target entity id
     * @param array|null $data The permissions to update
     * @return \App\Model\Dto\EntitiesChangesDto
     * @throws \Cake\Http\Exception\BadRequestException If the permissions passed
     * @throws \Exception If something unexpected occurred
     */
    public function updatePermissions(
        UserAccessControl $uac,
        string $aco,
        string $acoForeignkey,
        ?array $data = []
    ): EntitiesChangesDto {
        $entitiesChanges = new EntitiesChangesDto();

        foreach ($data as $rowIndex => $row) {
            if (!is_array($row)) {
                throw new BadRequestException(__('The permissions data must be an array.'));
            }
            if (!is_int($rowIndex)) {
                throw new BadRequestException(__('The permissions data array keys must be integers.'));
            }
            $permissionId = Hash::get($row, 'id', null);

            // A new permission is provided when no id is found in the raw data.
            if (is_null($permissionId)) {
                $permission = $this->addPermission($uac, $rowIndex, $aco, $acoForeignkey, $row);
                $entitiesChanges->pushAddedEntity($permission);
            } else {
                // If a property delete is found and set to true, then delete the permission.
                // Otherwise update it.
                $permission = $this->getPermission($rowIndex, $acoForeignkey, $permissionId);
                $delete = Hash::get($row, 'delete');
                if ($delete) {
                    $permission = $this->deletePermission($permission);
                    $entitiesChanges->pushDeletedEntity($permission);

View on GitHub (pinned to 31c1bbc10f)