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

Only admin can create or update organization settings.

Error message

Only admin can create or update organization settings.

What it means

OrganizationSettingsTable::createOrUpdateSetting() throws UnauthorizedException when the UserAccessControl object does not represent an administrator. Only admins may create or update organization settings (e.g. SMTP, MFA policy). The check happens before any database work, so the request is rejected with 403/401 before any write.

Solutions

  1. Log in with, or impersonate, an administrator account (role id/name 'admin') when calling the settings API.
  2. For CLI tasks, ensure the UserAccessControl is built with admin privileges (e.g. root/admin user context).
  3. Fix the role assignment if the user should be an admin (update users.role_id to the admin role).
  4. If RBAC plugin customizes role permissions, ensure it still allows admins to write OrgSettings.

Example fix

// before
$control = new UserAccessControl($user);
$this->OrganizationSettings->createOrUpdateSetting('smtp', $value, $control);
// after
if ($control->isAdmin()) {
    $this->OrganizationSettings->createOrUpdateSetting('smtp', $value, $control);
}
Defensive patterns

Strategy: validation

Validate before calling

if (!$control->isAdmin()) { throw new UnauthorizedException('Admin role required to modify organization settings.'); }

Type guard

function isAdminContext(App\Utility\UserAccessControl $c): bool { return $c->isAdmin(); }

Try / catch

try { $this->OrganizationSettings->createOrUpdateSetting($property, $value, $control); } catch (\Cake\Http\Exception\UnauthorizedException $e) { // return 403 to the client }

Prevention

When it happens

Trigger: POST/PUT to organization settings endpoints with a logged-in user whose role is not admin — e.g. a 'user' role account calling PUT /org-settings.json, or a service calling createOrUpdateSetting() with a UserAccessControl built from a non-admin user or a missing/invalid role.

Common situations: Running CLI/automation tasks without impersonating an admin; provisioning scripts using a normal user's credentials; forgetting to pass role context so $control->isAdmin() is false; testing with a non-admin account.

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/9c866d2bdb89168f. Report an issue: GitHub.

Appendix: source

Thrown at src/Model/Table/OrganizationSettingsTable.php:169

            return null;
        }
    }

    /**
     * Create (or update) an organization setting
     *
     * @param string $property The property name
     * @param array|string $value The property value
     * @param \App\Utility\UserAccessControl $control user access control object
     * @return \App\Model\Entity\OrganizationSetting
     * @throws \Cake\Http\Exception\UnauthorizedException When user role is not admin.
     * @throws \App\Error\Exception\CustomValidationException When there are validation errors.
     * @throws \Cake\Http\Exception\InternalErrorException|\Exception When unable to save the entity.
     */
    public function createOrUpdateSetting(string $property, string|array $value, UserAccessControl $control): OrganizationSetting // phpcs:ignore
    {
        if (!$control->isAdmin()) {
            throw new UnauthorizedException(__('Only admin can create or update organization settings.'));
        }

        $settingId = $this->_getSettingPropertyId($property);
        $settingFinder = ['property_id' => $settingId];
        $settingValues = ['value' => $value, 'property' => $property];
        $settingItem = $this->find()
            ->where($settingFinder)
            ->first();
        if ($settingItem) {
            $settingValues['modified_by'] = $control->getId();
            /** @var \App\Model\Entity\OrganizationSetting $settingItem */
            $settingItem = $this->patchEntity($settingItem, $settingValues);
        } else {
            $settingValues['created_by'] = $settingValues['modified_by'] = $control->getId();
            $settingItem = $this->newEntity(array_merge($settingFinder, $settingValues));
        }
        if ($settingItem->getErrors()) {
            throw new CustomValidationException(__('This is not a valid setting.'), $settingItem->getErrors(), $this);

View on GitHub (pinned to 31c1bbc10f)