Перейти к содержимому

Учётная запись изнутри - что создаётся при заведении пользователя

Разбираем учётную запись по составу: что платформа записывает помимо строки в таблице пользователей. Дальше смотрим, какие события стреляют при заведении и почему прямой запрос в базу даёт неполноценную запись.

Механика

Учётная запись собирается из нескольких хранилищ сразу, а не из одной строки таблицы b_user. Рядом с ней лежат связи с группами, значения пользовательских полей и очередь писем.

Логин уникален всегда, и это единственное поле записи с жёсткой уникальностью. Уникальность почты включается отдельной настройкой главного модуля, а по умолчанию совпадение адресов допускается.

Отдельная настройка приравнивает логин к почте на всех формах платформы. При включённой настройке заданный логин молча заменяется адресом, и импорт получает совсем не те записи, которые от него ждали.

Пароль хранится необратимо: рядом с хэшем записывается своя соль конкретной установки. Ни один метод платформы не возвращает пароль обратно, и переносить хэши из чужой системы бессмысленно.

Пароль проверяется политикой тех групп, в которые запись попадает при создании. Политика собирается из всех групп сразу и берёт самые строгие требования к длине и составу.

Членство в группе - это отдельная строка на пару «пользователь-группа» со своими датами действия. Запись списка групп заменяет прежний список целиком, поэтому неаккуратная выдача снимает с человека все остальные группы.

Пользовательские поля лежат не в самой таблице пользователей, а в хранилище сущности USER. В массив полей они передаются наравне с обычными, по коду с обязательным префиксом UF_.

До вставки строки платформа вызывает событие OnBeforeUserAdd, и обработчик умеет отменить создание. Отказ приходит чужим текстом ошибки, которого нет ни в одном сообщении платформы.

Событие OnAfterUserRegister стреляет только на регистрации и до создания из кода не доходит. Поэтому логику, общую для всех путей заведения, вешают на событие добавления пользователя.

Письмо о новой записи не уходит в момент создания, а ложится в очередь b_event. Разбор очереди идёт в прологе следующих хитов, и массовое заведение выливается в пачку одинаковых приглашений.

Шаги

  1. Проверить настройки главного модуля про логин и уникальность почты до первого запуска импорта.
  2. Найти существующую запись по выбранному ключу связи, чтобы не завести того же человека повторно.
  3. Собрать массив полей целиком: логин, почту, пароль с подтверждением, группы и поля с префиксом.
  4. Записать данные методом добавления и прочитать причину отказа из свойства объекта.
  5. Проверить состав созданной записи: связи с группами, значения своих полей и очередь писем.

Код

Смотрим ядро учётной записи:

$rs = \Bitrix\Main\UserTable::getList(['filter' => ['=ID' => $userId],
'select' => ['ID', 'LOGIN', 'EMAIL', 'ACTIVE', 'EXTERNAL_AUTH_ID', 'DATE_REGISTER']]);
print_r($rs->fetch());
// EXTERNAL_AUTH_ID не пуст у записей из внешнего каталога: их правят не здесь
// хэш пароля в выборку не берём намеренно: читать его незачем и нечем

Ядро записи отвечает только за вход и за признак источника. Всё остальное - группы, свои поля и письма - лежит рядом и создаётся отдельными шагами той же операции.

Смотрим ту же запись старым методом:

$row = \CUser::GetByID($userId)->Fetch(); // весь набор полей ядра сразу
print_r(array_keys($row)); // видно, что относится к самой записи
unset($row['PASSWORD']); // хэш в логи и выгрузки не отдаём
printf("%s / %s / активна: %s\n", $row['LOGIN'], $row['EMAIL'], $row['ACTIVE']);

Список ключей отвечает на вопрос, что вообще относится к самой строке записи. Групп и своих полей в этом списке нет: они приезжают отдельными выборками из своих хранилищ.

Заводим запись через API:

$user = new \CUser();
$id = $user->Add([
'LOGIN' => 'ivanov', 'EMAIL' => 'ivanov@example.com',
'PASSWORD' => $pass, 'CONFIRM_PASSWORD' => $pass, // подтверждение обязательно
'GROUP_ID' => [5, 6], // связи создаются здесь же
'UF_TABLE_NUM' => '1043', // своё поле сущности USER
]);
if (!$id) { echo $user->LAST_ERROR; } // причина отказа в свойстве
// GROUP_ID здесь задаёт весь список членства: прежние группы не сохраняются

Один вызов заводит строку записи, связи с группами и значения своих полей. Возвращается идентификатор либо ложь, а текст причины лежит отдельным свойством объекта и в возвращаемое значение не попадает.

Проверяем связи с группами:

global $DB;
print_r(\CUser::GetUserGroup($id)); // номера групп записи одним массивом
$rs = $DB->Query("SELECT * FROM b_user_group WHERE USER_ID = " . (int)$id);
while ($row = $rs->Fetch()) { print_r($row); } // строка связи со всеми полями
// группа 1 - администраторы, группа 2 - все пользователи: они есть на любом сайте

Прямую вставку пары номеров в таблицу связей платформа допускает, но переносимый между СУБД вид ей придаёт помощник getInsertIgnore, а не сырой запрос. Выдача группы методом записи заменяет весь список, поэтому новую группу добавляют к текущим, а не пишут в одиночку.

Читаем свои поля записи:

global $USER_FIELD_MANAGER;
$fields = $USER_FIELD_MANAGER->GetUserFields('USER', $id); // описания и значения
$value = $USER_FIELD_MANAGER->GetUserFieldValue('USER', 'UF_TABLE_NUM', $id);
printf("полей у записи: %d, табельный номер: %s\n", count($fields), $value);
// сущность пользователя в механизме своих полей называется строкой USER
// пустой список означает, что поля не заведены, а не то, что значений нет

Поле обязано существовать у сущности USER до заведения записи. Незнакомый код в массиве полей платформа просто игнорирует, и значение теряется молча, без единой строки в ошибке.

Смотрим, кто вмешивается в создание:

AddEventHandler('main', 'OnBeforeUserAdd', 'checkTableNum'); // обычно в init.php
function checkTableNum(&$arFields) {
if (empty($arFields['UF_TABLE_NUM'])) {
global $APPLICATION;
$APPLICATION->throwException('Не указан табельный номер');
return false; // ложь отменяет создание записи
}
}

Обработчик получает массив полей по ссылке и правит его до вставки строки. Брошенное исключение и становится тем самым чужим текстом, который импорт потом читает из свойства ошибки.

Считаем письма в очереди:

$conn = \Bitrix\Main\Application::getConnection();
print_r($conn->query('SELECT COUNT(*) AS C FROM b_event')->fetch());
// очередь разбирается в прологе следующих хитов, а не в момент заведения записи
// перед импортом почтовый шаблон отключают в списке почтовых событий сайта

Заведение сотни записей ставит в очередь сотню писем. Перед массовым импортом почтовые шаблоны отключают или подменяют адресом разработчика, иначе люди получают приглашения с тестового стенда.

Дособираем связи прямым запросом:

$helper = \Bitrix\Main\Application::getConnection()->getSqlHelper();
$sql = $helper->getInsertIgnore('b_user_group', '(USER_ID, GROUP_ID)', 'VALUES (12, 5)');
$conn->query($sql);
// getInsertIgnore переносим между СУБД, в отличие от прямого INSERT IGNORE
// сама запись при этом уже создана через API: строку b_user так не заводят

Прямая вставка допустима лишь как разовая досборка связей на готовых записях. Строку пользователя так не создают: платформа о ней не узнает и не заполнит ни одного производного значения.

Ограничения

Пароль не читается обратно ни одним методом платформы. Повторное письмо с паролем поэтому невозможно, и вместо него отправляют одноразовую ссылку на смену пароля.

Хэши паролей из чужой системы не переносятся вместе с записями. Соль своя у каждой установки, а алгоритм проверки менялся по версиям платформы, и чужой хэш не сойдётся никогда.

Прямая вставка строки в таблицу пользователей даёт запись без групп и без своих полей. События при этом не срабатывают, производные данные не заполняются, а вход под такой записью обычно не проходит.

Событие регистрации проходит не на каждом пути заведения записи. Создание из кода и автоматическая регистрация при оформлении заказа до него не доходят, а событие добавления срабатывает в любом случае.

Записи с непустой отметкой внешнего источника живут по правилам этого источника. Их состав и порядок обновления разбираются отдельно, вместе с настройкой каталога организации.

Типичные проблемы

В логин попала почта вместо заданного значения.

В главном модуле включена настройка «логин совпадает с почтой». Она подменяет логин на всех путях заведения, и импорт молча получает совсем другие записи.

Пользователи залиты запросом в базу, но прав у них нет.

Прямая вставка строки не создаёт связей с группами и не заполняет свои поля. Группы живут отдельной таблицей связи, и заполнять её приходится руками.

Создание падает с текстом, которого нет в платформе.

Создание отклоняет чужой обработчик события добавления, зарегистрированный в общем файле проекта. Текст ошибки написан автором обработчика, поэтому и не похож на системные сообщения.

После импорта сотрудникам ушла пачка писем со стенда.

Каждое заведение записи ставит письмо в очередь рассылки платформы. Очередь разбирается в прологе следующих хитов, и остановить её после запуска импорта уже нельзя.

Перенесли хэши паролей, а вход не проходит ни у кого.

Пароль хранится вместе с солью конкретной установки платформы. Перенос одного лишь хэша ломает проверку, поэтому пароли задают заново или переводят вход на каталог организации.

Обработчик срабатывает дважды на каждой регистрации.

Один и тот же код подписан сразу на событие добавления и на событие регистрации. При регистрации через форму отрабатывают оба события подряд, и действие дублируется.

Частые вопросы

Можно ли создать пользователя прямым запросом в базу?

Технически строка появится, но записью её платформа не считает. Не будет ни связей с группами, ни своих полей, ни событий, а вход под такой записью не пройдёт.

Как создать пользователя с логином не почтой, если логин подставляется сам?

Отключить в настройках главного модуля связку логина с почтой, завести запись с нужным логином и при необходимости вернуть настройку обратно.

Как вытащить читабельный пароль из Битрикса?

Никак: пароль хранится необратимо, вместе с солью установки. Вместо повторной отправки пароля шлют письмо со ссылкой на его смену.

Как импортировать пользователей сразу с группами?

Передавать список групп тем же массивом полей при создании записи. Файл импорта группы не переносит, а прямая запись в базу связей не создаёт.

Почему событие регистрации не сработало при создании из кода?

Оно относится к регистрации через форму, а не к любому заведению записи. Общую для всех путей логику вешают на событие добавления пользователя.

Смежное

Первоисточник