calendar · Календарь событий 26.100.0 Безопасность

Calendar 26.100.0: почтовые приглашения и calendar.section.update берут хоста, тип и владельца из базы

4 мин чтения

Обновление безопасности

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

В Calendar 26.100.0 REST-метод calendar.section.update берёт тип и владельца секции из базы, а обработчик входящих iCal-приглашений ищет событие только среди встреч получателя и при обновлении встречи читает её хоста из базы. В участники встречи теперь можно звать команды из модуля humanresources, а JS-метод EntryManager.setMeetingStatus() научился отклонять промис. PHP-сигнатуры совместимы, схема БД не менялась.

Проверьте интеграции с calendar.section.update

Раньше CCalendarRestService::SectionUpdate собирал модель для проверки права ACTION_SECTION_EDIT из параметров запроса: SectionModel::createFromId($id)->setType($type)->setOwnerId($ownerId). Те же type и ownerId уходили в поля для сохранения, так что через REST можно было переписать секции тип и владельца.

Теперь метод читает CAL_TYPE и OWNER_ID через Internals\SectionTable и строит модель по ним:

        $sectionModel = SectionModel::createNew()
	->setId($id)
	->setType($realType)
	->setOwnerId($realOwnerId);

    

Что изменится для интеграций:

  • права проверяются по владельцу секции из базы;
  • ownerId при type не равном user по-прежнему обязателен (проверку оставили), но на результат не влияет;
  • сменить секции тип или владельца через calendar.section.update больше нельзя, в сохранение уходят значения из базы;
  • если секции с таким id нет, модель собирается с пустым типом и нулевым владельцем. Обычный пользователь на ней получит CAL_REST_ACCESS_DENIED, и только администратор дойдёт до CAL_REST_SECT_ID_EXCEPTION.

Почтовые приглашения сверяют организатора

Сильнее всего переписан обработчик входящих iCal-приглашений IncomingInvitationRequestHandler, в нём +233/−39 строк. Раньше он искал локальное событие по UID из письма среди всех событий портала, при обновлении писал в родительское событие организатора (ID = PARENT_ID, OWNER_ID = MEETING_HOST) и заново определял хоста встречи по адресу организатора из письма.

Что проверяется теперь:

  • если в хите авторизован другой пользователь, не получатель письма, обработка останавливается;
  • email организатора должен совпасть с одним из адресов письма, иначе письмо не обрабатывается;
  • событие ищется только среди встреч получателя, через Helper::getEventByUId($uid, $this->userId, true);
  • при обновлении организатор из письма должен совпасть по email с хостом уже существующей встречи;
  • при обновлении MEETING_HOST берётся из локального события. У нового приглашения хост, как и раньше, определяется по адресу организатора через Helper::getUserIdByEmail();
  • обновляется копия получателя (ID = $localEvent['ID'], OWNER_ID = $this->userId) через CCalendar::SaveEvent с checkPermission и новым ключом параметров checkCurrentEventPermission;
  • список участников при обновлении больше не затирается до двух кодов, в него попадают коды родительского события плюс получатель и хост.

Во всех отказных ветках handle() возвращает false.

Поменялась и работа с SEQUENCE. Номер ревизии из письма больше не пишется в поле VERSION, он хранится в MEETING['ICAL_SEQUENCE']. Письмо с SEQUENCE меньше сохранённого считается устаревшим. Встречу оно не обновляет, но handle() всё равно возвращает true. Нечисловой SEQUENCE превращается в 0.

IncomingInvitationCancelHandler, который обрабатывает отмену, тоже ищет событие только среди встреч получателя.

Слова «уязвимость» в диффе нет ни здесь, ни в правке calendar.section.update. Проверка прав по данным из базы и сверка организатора с отправителем и хостом похожи на защиту от подделанных запросов и приглашений, но это вывод из кода.

Если вы зовёте Helper::getEventByUId() у себя, вызов с одним аргументом работает по-старому. Новые параметры необязательные:

        public static function getEventByUId(
	?string $uid,
	?int $userId = null,
	bool $includeChildUid = false,
): ?array

    

С $userId метод смотрит только встречи этого пользователя с неудалённым родителем. Событие он вернёт, если хост внутренний или если хост почтовый и встреча помечена EXTERNAL_TYPE = 'mail':

        <?php declare(strict_types=1);

use Bitrix\Calendar\ICal\MailInvitation\Helper;
use Bitrix\Main\Loader;

Loader::requireModule('calendar');

// ищем только среди встреч получателя
$event = Helper::getEventByUId($uidFromIcs, userId: $recipientId, includeChildUid: true);

if ($event === null)
{
	// у получателя такой встречи нет
}

    

Ловите отказ от EntryManager.setMeetingStatus

Раньше промис из BX.Calendar.EntryManager.setMeetingStatus() мог только разрешиться. Теперь он отклоняется в двух случаях. Первый: пользователь закрыл диалог подтверждения отказа, ничего не выбрав. У ConfirmStatusDialog для этого появилось событие onCancel, промис падает с Error('cancelled'). Второй: запрос вернул ошибку.

Вызовы в исходниках модуля получили обработчик ошибки. Если вы дёргаете метод из своего JS, без обработчика в консоли появится Uncaught (in promise):

        BX.Calendar.EntryManager.setMeetingStatus(entry, 'N')
	.then(() => myWidget.refresh())
	.catch((error) => {
		if (error?.message !== 'cancelled')
		{
			console.error(error);
		}
	});

    

Зовите на встречу команду целиком

В селекторе участников можно выбрать команду из humanresources (сущность structure-node). В событии она хранится кодом доступа SNT<id>, а CCalendar::GetDestinationUsers() разворачивает его в прямых участников команды. Подкоманды не обходятся. Рекурсивный код SNTR<id> календарь намеренно не распознаёт.

По умолчанию команд в интерфейсе не видно. FeatureService::isTeamsAsAttendeeEnabled() вернёт true, только если установлен humanresources, в нём доступны кросс-функциональные команды и включена опция календаря. Переключателя в админке в этом релизе нет, включать придётся кодом:

        <?php declare(strict_types=1);

use Bitrix\Calendar\Integration\HumanResources\FeatureService;
use Bitrix\Main\Config\Option;
use Bitrix\Main\Loader;

Loader::requireModule('calendar');

Option::set('calendar', 'team_attendees_enabled', 'Y');

var_dump(FeatureService::isTeamsAsAttendeeEnabled());

    

Опция отвечает только за видимость команд в селекторе. В комментарии к FeatureService сказано лишь, что серверная часть активна всегда. По коду в неё входят разворачивание SNT в GetDestinationUsers() и пересчёт состава через watcher.

Команды, которые пользователь не вправе просматривать (ACTION_TEAM_VIEW в humanresources), отбрасываются при сохранении участников и в планировщике. Когда в команде меняется состав, обработчик Watcher\Membership\Handler\Team ставит в очередь пересчёт событий с кодом этой команды. Отделы он пропускает, у них свой watcher.

Для своего кода в Bitrix\Calendar\Integration\HumanResources лежат три небольших класса: TeamAccessCode, TeamAccessService и TeamMemberService. Это интеграционный слой календаря, так что на обещанный контракт он не тянет, но пользоваться можно:

        <?php declare(strict_types=1);

namespace Vendor\Meetings\Application;

use Bitrix\Calendar\Integration\HumanResources\TeamAccessCode;
use Bitrix\Calendar\Integration\HumanResources\TeamAccessService;
use Bitrix\Calendar\Integration\HumanResources\TeamMemberService;

final class TeamAttendeeResolver
{
	public function __construct(
		private readonly TeamAccessService $access = new TeamAccessService(),
		private readonly TeamMemberService $members = new TeamMemberService(),
	) {}

	/** @return int[] */
	public function resolve(string $code, int $viewerId): array
	{
		// 'SNT15' -> 15, а 'SNTR15' и всё прочее -> null
		$nodeId = TeamAccessCode::extractNodeId($code);

		if ($nodeId === null || !$this->access->canViewTeam($nodeId, $viewerId))
		{
			return [];
		}

		return $this->members->getDirectMemberUserIds([$nodeId]);
	}
}

    

canViewTeam() возвращает false, если нет модуля, ID неположительный или узел недоступен. getDirectMemberUserIds() молча пропускает узлы, которые не являются командой.

БД

Схема не менялась. В install/index.php добавлена регистрация пяти обработчиков событий humanresources: OnMemberAdded, OnMemberUpdated, OnMemberDeleted, OnNodeUpdated, OnNodeDeleted. Обновление ставит их и на уже установленный модуль. На эталонной установке после обновления все пять обработчиков Watcher\Membership\Handler\Team есть в b_module_to_module.

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

  • CCalendarEvent::GetList() с пустым arSelect или ['*'] больше не читает колонку SEARCHABLE_CONTENT. Select теперь раскрывается через новый EventTable::getDefaultSelectFieldNames(). Из результата поле вырезалось и раньше, меняется только нагрузка на БД.
  • Встречи пользователя без секции теперь попадают в выборку по его секции встреч (getListOrm).
  • CCalendarEvent::SetMeetingStatusEx(): при ответе на повторяющуюся встречу связанные события выбираются до разделения серии. После разделения исключения перепривязывались к новому мастеру и терялись, в комментарии ссылка на Mantis #249047.
  • CCalendar::SaveEventEx() понимает новый ключ $params['checkCurrentEventPermission'], сигнатура SaveEventEx($params = []) та же. Ключ передаёт в проверку права на редактирование checkCurrentEvent => 'Y'.
  • В комментариях видны внутренние метки задач CFG-01 и CODE-01. В JS-комментариях объяснён выбор selectMode: 'departmentsOnly' в селекторе. Другие режимы дают SNTR<id> или постфикс :F, которые бэкенд не разбирает.
  • isOrganizerEmailMatchingSender() вызывается сразу после normalizeAddressesByOrganizer(), которая уже добилась того же совпадения. По коду вторая проверка лишняя.
  • В team.php нет declare(strict_types=1), в остальных четырёх новых файлах он есть.
  • VERSION_DATE у 26.100.0 (13 июля) раньше, чем у предыдущей 26.0.100 (5 августа).
  • В calendar_event_handlers.php и calendar_notify.php CHTTP::urlAddParams заменили на Bitrix\Main\Web\Uri, а <? на <?php.
  • В интерфейсе двойной клик по «Принять» или «Отказаться» в компактной форме больше не шлёт два запроса, переговорка сравнивается по типу и ID, планировщик открывается на дате события.
  • Deprecated в релизе нет.

Что делать

  • Проверьте права вебхуков и приложений на секции, которые они меняют через calendar.section.update.
  • Если пользователи принимают приглашения из внешней почты, отправьте тестовое приглашение, затем его обновление, и убедитесь, что встреча у получателя обновилась.

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

calendar 26.125.0 Рутинное Свежее

Calendar 26.125.0: компактная форма события вне календаря

Экшен `calendar.api.calendarajax.getStandaloneCompactFormData` собирает секции для формы заново. Обычному пользователю он гарантирует секцию для сохранения и при необходимости создаёт личный календарь. Коллаберу отдаёт только секции его коллабов с правом записи, экстранет-пользователю и коллаберу бе...

1 мин

calendar 25.195.0: активность БП считает минуты встреч и перестала терять последний день

Точечный релиз вокруг одной активности бизнес-процессов — «Получить информацию из календаря». Она научилась отдавать списки заголовков и суммарные минуты встреч, а заодно у неё поправили границу выборки. Публичного PHP API релиз не меняет.

3 мин

calendar 25.197.0: тип события call_sync и чат, который создаётся сам

У события календаря появился собственный тип, и первый его пользователь — синхронизированные созвоны: при создании такого события автоматически поднимается чат с гостевой ссылкой. Ломающих изменений нет, все правки сигнатур совместимы.

3 мин

calendar 26.0.0: потолок повторов RRULE, права на удалённые sharing-события и разбор ошибок REST

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

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

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

на связи

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

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

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

Войти