humanresources 26.500.0 Ломающее

Humanresources 26.500.0: виртуальные пользователи пропали из выборок и счётчиков оргструктуры

3 мин чтения

Ломающее обновление

Удалены или изменены публичные API — прикладной код может перестать работать.

В humanresources 26.500.0 (коробочный Битрикс24) фильтры, репозитории и счётчики оргструктуры по умолчанию перестали видеть ботов, пользователей приложений и остальных виртуальных пользователей. В том же релизе поменяли местами перепутанные ONLY_ACTIVE и ONLY_GLOBAL_ACTIVE, RolePermissionService начал бросать \DomainException, а установщик переехал на install/migrations. Проверьте свой код, если он собирает участников отделов, считает сотрудников или сохраняет роли доступа, а авторам приложений 1С для HCM Link стоит посмотреть на новый поток расчётных листков и отпусков по ПИН-коду.

Что сломается

Верните виртуальных пользователей туда, где они нужны

Реальным модуль теперь считает пользователя с IS_REAL_USER = 'Y' в b_user, то есть того, чей EXTERNAL_AUTH_ID не входит в UserTable::getExternalUserTypes(). Пользователи с типами bot, email, replica, imconnector и другими внешними из выборок по умолчанию выпадают. Условие собрано подзапросом в новом Internals\Repository\Query\RealUserFilter.

Где это проявится:

  • Builder\Structure\Filter\NodeMemberFilter получил последний параметр bool $withVirtualUsers = false. При entityType = USER фильтр добавляет условие «ENTITY_ID среди реальных пользователей». Код, который собирает участников через NodeMemberDataBuilder с этим фильтром, после обновления перестанет видеть ботов и пользователей приложений.
  • У Repository\NodeMemberRepository (Container::getNodeMemberRepository()) метод findAllByNodeId() раньше отдавал участников любого типа, теперь только ENTITY_TYPE = USER и только реальных. Тот же фильтр встал в findAllByNodeIdAndRoleIdList(), findAllByRoleIdAndNodeCollection(), findAllByRoleIdAndStructureId(), findAllByRoleIdAndNodeId(), countAllByStructureAndGroupByNode() и в findAllByNodeIdAndEntityType() для USER. Сигнатуры прежние, флага у этих методов нет.
  • Public\Service\Department\UserService::getTotalEmployeeCount() и Util\NodeMemberCounterHelper::countByNodeId() получили bool $withVirtualUsers = false и по умолчанию возвращают меньшие числа, чем раньше. countByNodeId() без флага к тому же считает только ENTITY_TYPE = USER. В ключи кеша обоих методов добавлен суффикс _with_virtual_Y или _with_virtual_N.
  • Public\Service\NodeMemberService::findAllByRoleIdAndNodeId() получил withVirtualUsers = false и виртуальных скрывает. У findAllByEntityIds() значение по умолчанию true, по докблоку этот метод обслуживает контуры прав и целостности. Во внутреннем Internals\Repository\Structure\NodeMemberRepository false по умолчанию стоит у countUniqueUsersByNodeIdWithSubNodes(), getMultipleNodeMembers() и findAllByRoleIdAndNodeId(), true у findAllByEntityIds().
  • Repository\UserRepository::findByNodeAndSearchQuery() и глобальный поиск Internals\Service\Structure\UserService::searchByName() без $nodeId (приватный searchGlobal()) ищут только среди IS_REAL_USER = 'Y'.
  • Service\NodeMemberService::saveUsersToDepartment() пропускает идентификаторы через RealUserFilter::filterRealUserIds(), так что этим методом виртуального пользователя в отдел не добавить.

Сам модуль там, где виртуальные пользователи нужны, передаёт withVirtualUsers: true явно: в StructureAuthProvider, UserEventHandler, NewToOldEventHandler, StructureBackwardConverter и в DepartmentProvider для текущего пользователя. В StructureBackwardConverter рядом оставлен комментарий, что на членстве таких пользователей держатся права приложений. Если ваш код строит права по членству в отделах, делайте так же. Счётчикам этот флаг возвращает прежние числа, countByNodeId() с ним снова считает участников любого типа.

        <?php declare(strict_types=1);

namespace Vendor\Hr\Structure;

use Bitrix\HumanResources\Builder\Structure\Filter\NodeFilter;
use Bitrix\HumanResources\Builder\Structure\Filter\NodeMemberFilter;
use Bitrix\HumanResources\Builder\Structure\NodeMemberDataBuilder;
use Bitrix\HumanResources\Util\NodeMemberCounterHelper;
use Bitrix\Main\Loader;

final class DepartmentAudience
{
    private readonly NodeMemberCounterHelper $counter;

    public function __construct()
    {
        Loader::requireModule('humanresources');
        $this->counter = new NodeMemberCounterHelper();
    }

    /**
     * Все участники отдела вместе с ботами: по этому списку приложение раздаёт права.
     *
     * @return list<int>
     */
    public function userIdsForPermissions(int $departmentId): array
    {
        return NodeMemberDataBuilder::createWithFilter(
            new NodeMemberFilter(
                nodeFilter: NodeFilter::createWithNodeId($departmentId),
                withVirtualUsers: true,
            ),
        )
            ->getAll()
            ->getUniqueEntityIds();
    }

    public function headcount(int $departmentId, bool $withVirtualUsers = false): int
    {
        return $this->counter->countByNodeId(
            $departmentId,
            withAllChildNodes: true,
            withVirtualUsers: $withVirtualUsers,
        ) ?? 0;
    }
}

    

У методов NodeMemberRepository флага нет. Если вы брали участников через findAllByNodeId() и вам нужны виртуальные пользователи, соберите выборку через NodeMemberDataBuilder, как в userIdsForPermissions(). Старый findAllByNodeId() отдавал участников любого типа, для того же через builder передайте в фильтр ещё entityType: null.

Проверьте, какую активность отделов вы запрашиваете

До 26.500 значения NodeActiveFilter в Builder\Structure\Filter\NodeFilter работали наоборот: ONLY_ACTIVE фильтровал по GLOBAL_ACTIVE, а ONLY_GLOBAL_ACTIVE по ACTIVE. Теперь каждое значение фильтрует по своему полю. ONLY_GLOBAL_ACTIVE стоит по умолчанию в большинстве методов NodeRepository, поэтому из таких выборок начнут выпадать отделы с неактивным родителем. Код, который явно передаёт ONLY_ACTIVE, наоборот, начнёт получать активные отделы под неактивным родителем. Если значение когда-то подбирали по фактическому поведению, поменяйте его.

Ловите \DomainException при сохранении ролей

Service\Access\RolePermissionService::saveRolePermissions() проверяет права новым Access\Permission\RolePermissionValidator и бросает \DomainException, если:

  • право не относится к категории роли;
  • значение не int;
  • значение не входит в список областей NONE/SELF/SELF+SUB/ALL (привязать право к конкретному отделу по id нельзя), а у переключателей не равно 0 или 1;
  • одно право указано дважды;
  • в командном праве NONE передан вместе с разрешающим значением.

То же исключение прилетит ещё в четырёх случаях: роль из чужой категории, переименование предустановленной роли, новое имя совпадает с именем предустановленной роли, удаление предустановленной роли через deleteRole() или deleteRoles(). Заранее это можно проверить новым публичным validateRolesCanBeDeleted().

Поменялась и сама запись. Она идёт в транзакции, и права роли теперь удаляются перед вставкой всегда. Раньше их удаляли, только если новая коллекция прав непустая, и сохранение с пустым набором прав старые права не трогало. Теперь оно их стирает, в том числе у элемента без ключа accessRights, например когда роль только переименовывают. Остальные исключения больше не заворачиваются в SqlQueryException и после rollback пробрасываются как есть, поэтому catch (SqlQueryException $e) вокруг вызова поймает меньше, чем раньше. В конструктор добавлены необязательные ?RolePermissionValidator $rolePermissionValidator и ?Connection $connection.

Компонент humanresources.config.permissions ловит \DomainException и отдаёт ошибку с кодом ROLE_DOMAIN_ERROR. В своём коде можно сделать так же. Числовые строки из формы приводить к int не нужно: saveRolePermissions() сам пропускает значения через is_numeric() и (int) до валидатора.

        <?php declare(strict_types=1);

namespace Vendor\Hr\Access;

use Bitrix\HumanResources\Service\Container;
use Bitrix\Main\Error;
use Bitrix\Main\Loader;
use Bitrix\Main\Result;

final class RoleSettingsSaver
{
    /**
     * @param list<array{id: int|string, title: string, accessRights?: list<array{id: mixed, value: mixed}>}> $settings
     */
    public function save(array $settings): Result
    {
        Loader::requireModule('humanresources');
        $result = new Result();

        try {
            Container::getAccessRolePermissionService()->saveRolePermissions($settings);
        } catch (\DomainException $e) {
            $result->addError(new Error($e->getMessage(), 'ROLE_DOMAIN_ERROR'));
        }

        return $result;
    }
}

    

Нечисловое значение раньше молча сохранялось как 0 ((int)$permission['value'] в старом коде), теперь на него валидатор отвечает \DomainException.

Не вызывайте installEvents() у humanresources

Из install/db/mysql и install/db/pgsql удалены install.sql, install_ft.sql и uninstall.sql. Из класса модуля в install/index.php пропали публичный installEvents() и приватный installAgents(). Вызов installEvents() не упадёт: он уйдёт в пустой CModule::InstallEvents() и ничего не зарегистрирует. Таблицы, обработчики событий и агенты теперь описаны в install/migrations/. Если ваш скрипт восстановления вызывал installEvents() у humanresources, сверяйте обработчики по install/migrations/events.php. При установке его применяет installMigrations() из installDB(). Что лежит в папке у humanresources, смотрите в разделе «БД».

Новое

Управляйте ролями через REST v3

Методы humanresources.access.* написаны на REST v3. Контроллеры наследуют Bitrix\Rest\V3\Controller\RestController и подхватываются из rest.defaultNamespace в .settings.php модуля. Что умеет этот базовый класс, мы разбирали в rest 26.600.0.

  • Rest\Controller\Access\Role отвечает за list, get, add, update и delete. Он работает с ролями категорий DEPARTMENT и TEAM, list принимает ещё BOTH. Права передаются парами {permissionId, area}, у предустановленных ролей стоит isDefault.
  • Rest\Controller\Access\Permission::listAction() отдаёт справочник прав по секциям с допустимыми значениями.
  • Rest\Controller\Access\User::permissionsAction() отдаёт итоговые права сотрудника. Свои права можно смотреть с правом STRUCTURE_VIEW, чужие только с правом управлять доступом категории.

Вход разбирает Rest\RequestParams. assertAllowedFields() отвечает ошибкой валидации на лишний ключ, а has(), requireList(), requireNonEmptyString() и requirePositiveInt() проверяют остальное. Лишних полей в запрос не кладите.

Запрашивайте расчётный листок из 1С по ПИН-коду

По докблоку новый контроллер Controller\HcmLink\SalaryVacation обслуживает публичный поток расчётных листков и отпусков для мобильного приложения. Экшены companyList, requestPin, requestDocument и getResult (humanresources.HcmLink.SalaryVacation.*) принимают только POST и закрыты флагом Config\Feature::isHcmLinkSalaryVacationApiAvailable(). Флаг читает опцию hcmlink_salary_vacation_api_available модуля humanresources со значением N по умолчанию, так что после обновления поток выключен.

ПИН ходит по четырём шагам:

  1. requestPin создаёт задание PIN_REQUEST и отправляет приложению 1С событие OnHumanResourcesHcmLinkPinRequested.
  2. 1С возвращает ПИН методом humanresources.hcmlink.field.value.set в поле с кодом PIN.
  3. Битрикс24 сразу отправляет ПИН тому же сотруднику pull-командой salaryVacationPinReady ({taskId, pin}) и отдаёт его в getResult в поле pin.
  4. requestDocument кладёт ПИН в OUTPUT_DATA задания SALARY_VACATION_REQUEST, а проверяет ПИН уже 1С.

Другого канала доставки ПИНа, SMS или почты, в коде нет. Включать флаг есть смысл, когда приложение 1С отвечает на OnHumanResourcesHcmLinkPinRequested и OnHumanResourcesHcmLinkSalaryVacationRequested, и делается это одной строкой: Option::set('humanresources', 'hcmlink_salary_vacation_api_available', 'Y'). Кроме флага, базовый HcmLinkController в processBeforeAction() требует Feature::isHcmLinkAvailable(), а тот пропускает только лицензию с регионом ru.

Классы, константы и события потока:

  • Service\HcmLink\SalaryVacationApiService (DI humanresources.service.hcmlink.salaryVacationApi) с методами getCompanies, getOwnedCompanies, getOwnEmployee, isOwnEmployee, requestPin, request, getResult, getStatus и getPin;
  • Service\HcmLink\PinService (DI humanresources.service.hcmlink.pin), его deliver() и deliverDocument() шлют pull-команды salaryVacationPinReady и salaryVacationDocumentReady пользователю Person.userId;
  • Type\HcmLink\PayrollType (SALARY = 1, VACATION = 2), JobType::PIN_REQUEST = 6 и JobType::SALARY_VACATION_REQUEST = 7;
  • REST-события OnHumanResourcesHcmLinkPinRequested и OnHumanResourcesHcmLinkSalaryVacationRequested;
  • Contract\Service\HcmLink\JobService::ERROR_COMPANY_NOT_FOUND со значением COMPANY_NOT_FOUND (текст ошибки «Company not found»).

Для приложений со стороны 1С humanresources.hcmlink.field.value.set стал строже. Задание должно принадлежать компании, каждая запись должна совпадать с сотрудником и полем из задания, а статус DONE принимается, только когда пришли все значения (isCompleteJobResponse). Компанию метод ищет по GUID из 1С, по id CRM-компании или по заданию.

Для агента прав Марты нужен модуль aiassistant

В секции aiassistant.marta файла .settings.php модуль регистрирует агента Integration\AiAssistant\Agents\AccessAgent с кодом access и набор ToolSets\AccessToolSet из шести инструментов: hr_role_list, hr_role_permissions, hr_user_permissions, hr_create_role, hr_update_role и hr_delete_role. У инструментов общий предок AccessBaseTool и общая схема прав InputProperty::accessRights().

Каждый инструмент наследует Bitrix\AiAssistant\Definition\Tool\Contract\ToolContract и реализует getName(), getDescription(), getInputSchema() с JSON Schema, execute(int $userId, ...$args): string, canList() и canRun(). Enum и обязательные поля проверяет валидатор входа в ToolManager::callTool. По комментарию в AccessBaseTool, в MCP-контексте CurrentUser::getId() возвращает 0, поэтому пользователь приходит только аргументом execute().

Модуля aiassistant (namespace Bitrix\AiAssistant) нет ни в коробочном Битрикс24, ни в «Управлении сайтом», так что запустить агента в коробке не на чем. Секцию aiassistant.marta при этом объявляют уже 11 модулей: biconnector, bizprocdesigner, booking, crm, humanresources, im, intranet, landing, mail, socialnetwork и tasks.

Читайте роли и права новыми методами

Для ролей появились RoleRepository::getRoleById(), getRolesByIds() и updateName(), а также PermissionRepository::getPermissionListByRoleIds(), который выбирает чанками по 300. В RolePermissionService добавили getRoleById(), getRoleAccessRightsMap(), getCanonicalRoleAccessRights() и getCanonicalRoleAccessRightsMap(). В канонической форме командные права сворачиваются обратно в пары значений.

Внутренний Internals\Repository\Structure\NodeRepository::findAllByIds() получил параметр ?int $viewerUserId = null и проверяет доступ от имени этого пользователя.

БД

Новых таблиц и полей нет. Переписан install/index.php:

  • installDB() вместо $DB->runSQLBatch(install.sql) вызывает installMigrations();
  • uninstallDB() вызывает uninstallMigrations() с $dropTables = false, так что при удалении модуля таблицы остаются. Раньше uninstallDB() просто возвращал true, и uninstall.sql тоже не выполнялся.

install/migrations/tables.php повторяет удалённый install.sql один в один: 22 таблицы (b_hr_structure*, b_hr_access_*, b_hr_log, b_hr_hcmlink_*) с теми же колонками и индексами, включая два FULLTEXT, IXF_B_HR_STRUCTURE_NODE_NAME и IXF_B_HR_HCMLINK_PERSON_INDEX_SEARCH_CONTENT. Мы сверили его поколоночно. agents.php регистрирует те же пять агентов, events.php те же обработчики iblock, main, humanresources и rest. В migration_config.json defaultTableName равен b_hr_structure_node_backward_access_code, там же маппинг install/components, install/js и install/activities.

ПИН и документы HCM Link пишутся в существующие таблицы. Задания лежат в b_hr_hcmlink_job с TYPE 6 или 7, в OUTPUT_DATA у них ключи company, companyGuid, employees, persons, date и type, у запроса документа ещё fields, period и pin. Значения лежат в b_hr_hcmlink_field_value под id задания (documentIdByEmployeeId). Флаг hcmlink_salary_vacation_api_available хранится строкой в b_option.

Мелочи и находки

  • ПИН в OUTPUT_DATA задания SALARY_VACATION_REQUEST лежит в базе открытым текстом. getResult вырезает outputData.pin из ответа, но getPin(), который по докблоку нужен для диагностики при разработке, вызывается из рабочего getStatus().
  • Повторный requestPin в течение 60 секунд возвращает то же задание, если оно в статусе STARTED или IN_PROGRESS. Кроме того, для ПИНа и для документа отдельно на 60 секунд выдаётся слот на связку «пользователь, компания, сотрудник, тип» через PersistentStorageInterface под Connection::lock(). Без свободного слота приходит ошибка TOO_MANY_REQUESTS. SalaryVacationApiService::acquireRequestSlot() годится как рецепт TTL-троттлинга без Redis и своих таблиц для любой кнопки вроде «отправить код» или «запросить выгрузку».
  • JobKillerService задания PIN_REQUEST и SALARY_VACATION_REQUEST повторно не пересылает и отменяет по TTL. Повторное событие, как сказано в комментарии, заставило бы 1С выдать лишний ПИН или прислать устаревший.
  • getStatus() на чужой или несуществующий taskId отвечает EXPIRED и не раскрывает, существует ли задание.
  • Для расчётного листка запрашивается одно поле с кириллическим кодом РасчётныйЛисток (сущность DOCUMENT, месяц и год обязательны). Для отпуска контракта полей пока нет, и, по комментарию в коде, первое значение может оказаться посторонними данными сотрудника.
  • Для заданий типов 6 и 7 JobEventHandler не шлёт pull external_employee_list_updated.
  • Агенты CompanyStructure и Department и SearchEmployeeTool проверяли $user->getPermission(STRUCTURE_VIEW). Вызов мог вернуть null, а условие null !== VARIABLE_NONE пропускало пользователя. Теперь значение берётся через PermissionHelper с NONE по умолчанию.
  • StructureAccessController кешировал правила в static $ruleHandler внутри метода, хотя правило создаётся с $this контроллера. В одном хите правило первого пользователя получали контроллеры остальных. Кеш переехал в свойство экземпляра.
  • Пункт вендора «Усилена безопасность отображения названий подразделений» — это ResponsiveHint. ui.hint рендерит подсказку через innerHTML, и теперь текст перед show() проходит через Text.encode().
  • OnAfterUserAdd с сортировкой 9 регистрируется в events.php старым EventManager::registerEventHandler() в зависимости от DatabaseUpdateMode::ModuleInstall или ModuleUninstall. Комментарий объясняет это тем, что register() не принимает sort, но с main 26.650.0 у Migration\Event::register() есть параметр ?int $sort. В своём модуле порядок задавайте им: ->register('main', 'OnAfterUserAdd', Handler::class, 'onAfterUserAdd', 9).
  • Исправили роль руководителя при переводе сотрудника. Internals\Service\Structure\NodeMemberService::moveMember() при переносе без роли в другой узел ставит роль по умолчанию целевого узла, а NewToOldEventHandler::clearSourceDepartmentHead() снимает UF_HEAD у прежнего отдела, если сотрудник там больше не руководитель.
  • HcmLink\Mapper::endAction() обращался к $company->id у null, когда компании нет. Теперь там проверка checkCompanyExists() и код ошибки ERROR_COMPANY_NOT_FOUND.
  • В мастере оргструктуры уже добавленного сотрудника можно назначить руководителем или заместителем. Во фронте releaseMemberRole() снимает прежнюю роль, а селекторы руководителя и заместителя больше не прячут занятых участников.
  • Действие БП humanresourcesgetaireportusersactivity скрывается через setExcluded и возвращает ошибку, когда Bizproc\Public\Service\AiAgent\NodeAvailabilityServiceInterface::isAvailable() отвечает false. Так сделано региональное ограничение ИИ.
  • Фильтр ботов для своих запросов можно собрать по образцу RealUserFilter. Это подзапрос UserTable::query()->where('IS_REAL_USER', 'Y') внутри whereIn и суффикс в ключе кеша. Сам класс лежит в Internals, поэтому приём надёжнее повторить у себя, чем вызывать класс напрямую.

Читайте дальше

voximplant 26.800.0 Ломающее Свежее

Voximplant 26.800.0: attachRecord и finish отказывают локальным адресам, searchCrmEntities отдаёт один лид

В voximplant 26.800.0 у четырёх REST-методов внешней телефонии поменялось поведение, хотя сигнатуры остались прежними. `telephony.externalCall.attachRecord` и `telephony.externalCall.finish` с `RECORD_URL` больше не скачивают записи с приватных адресов и не ходят через прокси. `searchCrmEntities` от...

4 мин
timeman 26.100.0 Ломающее Свежее

Timeman 26.100.0: время считается по IANA-зоне сотрудника, у CTimeManEntry и REST другие значения

С timeman 26.100.0 учёт рабочего времени берёт зону сотрудника из IANA-идентификатора в `b_user.TIME_ZONE` и считает смещение UTC на момент каждого события, с переходами на летнее и зимнее время. Форматы полей и ответов остались прежними, поменялись значения: `TIME_START`, `TIME_FINISH` и `DURATION`...

3 мин
report 26.200.100 Безопасность Свежее

Report 26.200.100: закрыта SQL-инъекция через формат имени пользователя

Релиз «Конструктора отчётов» вендор описал фразой «Усилена безопасность модуля». На деле изменилась одна строка в `CReport::getFormattedNameExpr()`, теперь метод экранирует литералы из формата имени пользователя перед вставкой в SQL. Если модуль `report` установлен, обновите его до 26.200.100. Файл...

2 мин
note 26.400.0 Ломающее Свежее

note 26.400.0: updatedAt не отражает правки в редакторе, уведомления выключены по умолчанию

В [note 26.300.0](/bitrix-updates/note/note-26-300-0) модуль «Базы знаний 2.0» получил события, историю версий и подписки. Версия 26.400.0 добавляет избранное, обратные ссылки между документами, отдельную материализацию markdown-проекции Yjs-документа, асинхронный пересчёт поиска и боковой чат Bitri...

2 мин
Мы используем файлы cookie для улучшения работы сайта. Продолжая использовать сайт, вы соглашаетесь с нашей политикой конфиденциальности.
AI Домовой

AI Домовой История

на связи

пишет…
Нет истории чатов
AI Домовой

Нужна авторизация

Войдите, чтобы задавать вопросы AI Домовому.

Войти