Учётная запись изнутри - что создаётся при заведении пользователя
Разбираем учётную запись по составу: что платформа записывает помимо строки в таблице пользователей. Дальше смотрим, какие события стреляют при заведении и почему прямой запрос в базу даёт неполноценную запись.
Механика
Учётная запись собирается из нескольких хранилищ сразу, а не из одной строки
таблицы b_user. Рядом с ней лежат связи с группами, значения пользовательских
полей и очередь писем.
Логин уникален всегда, и это единственное поле записи с жёсткой уникальностью. Уникальность почты включается отдельной настройкой главного модуля, а по умолчанию совпадение адресов допускается.
Отдельная настройка приравнивает логин к почте на всех формах платформы. При включённой настройке заданный логин молча заменяется адресом, и импорт получает совсем не те записи, которые от него ждали.
Пароль хранится необратимо: рядом с хэшем записывается своя соль конкретной установки. Ни один метод платформы не возвращает пароль обратно, и переносить хэши из чужой системы бессмысленно.
Пароль проверяется политикой тех групп, в которые запись попадает при создании. Политика собирается из всех групп сразу и берёт самые строгие требования к длине и составу.
Членство в группе - это отдельная строка на пару «пользователь-группа» со своими датами действия. Запись списка групп заменяет прежний список целиком, поэтому неаккуратная выдача снимает с человека все остальные группы.
Пользовательские поля лежат не в самой таблице пользователей, а в хранилище
сущности USER. В массив полей они передаются наравне с обычными, по коду с
обязательным префиксом UF_.
До вставки строки платформа вызывает событие OnBeforeUserAdd, и обработчик
умеет отменить создание. Отказ приходит чужим текстом ошибки, которого нет ни в
одном сообщении платформы.
Событие OnAfterUserRegister стреляет только на регистрации и до создания из
кода не доходит. Поэтому логику, общую для всех путей заведения, вешают на
событие добавления пользователя.
Письмо о новой записи не уходит в момент создания, а ложится в очередь b_event.
Разбор очереди идёт в прологе следующих хитов, и массовое заведение выливается в
пачку одинаковых приглашений.
Шаги
- Проверить настройки главного модуля про логин и уникальность почты до первого запуска импорта.
- Найти существующую запись по выбранному ключу связи, чтобы не завести того же человека повторно.
- Собрать массив полей целиком: логин, почту, пароль с подтверждением, группы и поля с префиксом.
- Записать данные методом добавления и прочитать причину отказа из свойства объекта.
- Проверить состав созданной записи: связи с группами, значения своих полей и очередь писем.
Код
Смотрим ядро учётной записи:
$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.phpfunction 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 так не заводятПрямая вставка допустима лишь как разовая досборка связей на готовых записях. Строку пользователя так не создают: платформа о ней не узнает и не заполнит ни одного производного значения.
Ограничения
Пароль не читается обратно ни одним методом платформы. Повторное письмо с паролем поэтому невозможно, и вместо него отправляют одноразовую ссылку на смену пароля.
Хэши паролей из чужой системы не переносятся вместе с записями. Соль своя у каждой установки, а алгоритм проверки менялся по версиям платформы, и чужой хэш не сойдётся никогда.
Прямая вставка строки в таблицу пользователей даёт запись без групп и без своих полей. События при этом не срабатывают, производные данные не заполняются, а вход под такой записью обычно не проходит.
Событие регистрации проходит не на каждом пути заведения записи. Создание из кода и автоматическая регистрация при оформлении заказа до него не доходят, а событие добавления срабатывает в любом случае.
Записи с непустой отметкой внешнего источника живут по правилам этого источника. Их состав и порядок обновления разбираются отдельно, вместе с настройкой каталога организации.
Типичные проблемы
В логин попала почта вместо заданного значения.
В главном модуле включена настройка «логин совпадает с почтой». Она подменяет логин на всех путях заведения, и импорт молча получает совсем другие записи.
Пользователи залиты запросом в базу, но прав у них нет.
Прямая вставка строки не создаёт связей с группами и не заполняет свои поля. Группы живут отдельной таблицей связи, и заполнять её приходится руками.
Создание падает с текстом, которого нет в платформе.
Создание отклоняет чужой обработчик события добавления, зарегистрированный в общем файле проекта. Текст ошибки написан автором обработчика, поэтому и не похож на системные сообщения.
После импорта сотрудникам ушла пачка писем со стенда.
Каждое заведение записи ставит письмо в очередь рассылки платформы. Очередь разбирается в прологе следующих хитов, и остановить её после запуска импорта уже нельзя.
Перенесли хэши паролей, а вход не проходит ни у кого.
Пароль хранится вместе с солью конкретной установки платформы. Перенос одного лишь хэша ломает проверку, поэтому пароли задают заново или переводят вход на каталог организации.
Обработчик срабатывает дважды на каждой регистрации.
Один и тот же код подписан сразу на событие добавления и на событие регистрации. При регистрации через форму отрабатывают оба события подряд, и действие дублируется.
Частые вопросы
Можно ли создать пользователя прямым запросом в базу?
Технически строка появится, но записью её платформа не считает. Не будет ни связей с группами, ни своих полей, ни событий, а вход под такой записью не пройдёт.
Как создать пользователя с логином не почтой, если логин подставляется сам?
Отключить в настройках главного модуля связку логина с почтой, завести запись с нужным логином и при необходимости вернуть настройку обратно.
Как вытащить читабельный пароль из Битрикса?
Никак: пароль хранится необратимо, вместе с солью установки. Вместо повторной отправки пароля шлют письмо со ссылкой на его смену.
Как импортировать пользователей сразу с группами?
Передавать список групп тем же массивом полей при создании записи. Файл импорта группы не переносит, а прямая запись в базу связей не создаёт.
Почему событие регистрации не сработало при создании из кода?
Оно относится к регистрации через форму, а не к любому заведению записи. Общую для всех путей логику вешают на событие добавления пользователя.
Смежное
- Импорт пользователей - оглавление подтемы
- Пользователи, группы и права доступа - устройство учётных записей целиком
- Импорт пользователей из файла: связь, пароли, группы - как прочитать файл и завести записи
- Пользователь не создался при импорте: разбор причин - когда заведение не проходит
- Группы пользователей: назначение, срок действия, автоматизация - работа со связями и сроками
- Поля разделов и пользователей: выборка, множественные значения, файлы - свои поля у сущности пользователя
- Регистрация покупателей: форма, подтверждение, свои поля - тот же состав на витрине
- Восстановление пароля: контрольная строка, письмо, ошибки - штатный путь смены пароля
- LDAP и Active Directory - записи из внешнего каталога
- Чистка учётных записей: неактивные, дубли, обезличивание - что делать с накопленными дублями