MACRO

Встраивание внешних приложений (MacroFrame)

MacroFrame позволяет встроить ваше внешнее приложение (внешний веб-сервис) прямо в интерфейс MacroCRM — в виде блока на странице или пункта навигации. Внешнее приложение открывается в iFrame и автоматически получает контекст той страницы, на которой оно показано: например, id сделки, id объекта, имя и телефон клиента. Это превращает внешнее приложение в полноценную часть рабочего места сотрудника, не требуя от него переключаться между вкладками и вручную переносить данные.

Статья рассчитана на две аудитории:

  • Администратора компании, который настраивает размещение внешнего приложения в интерфейсе MacroCRM;
  • Разработчика внешнего приложения, который встраивает свой сервис и принимает контекстные параметры (в том числе расшифровывает зашифрованные параметры).

#Что это и что даёт внешним приложениям

MacroFrame — это механизм размещения внешних приложений внутри рабочих экранов MacroCRM. Компания регистрирует размещение: указывает адрес внешнего приложения, выбирает место в интерфейсе и при необходимости задаёт ключ шифрования. Платформа сама встраивает внешнее приложение в выбранное место и передаёт ему параметры текущего контекста.

Что это даёт внешнему приложению:

  • Контекст без интеграции по API. Внешнее приложение получает идентификаторы и данные прямо из той страницы, где открыто (id сделки, id объекта, id контакта, имя и телефон клиента). Не нужно спрашивать у пользователя, с какой сделкой он работает, — это уже известно.
  • Присутствие в рабочем потоке. Сотрудник видит внешнее приложение на странице сделки, заявки или карточки контакта, в навигации раздела или на главной — там, где он уже работает.
  • Управляемая безопасность. Параметры можно передавать в зашифрованном виде, доступ к камере/микрофону/акселерометру выдаётся точечно, а сам фрейм ограничен sandbox-атрибутом: ему не выдан allow-top-navigation, поэтому он не может перенаправить родительскую вкладку и увести пользователя с платформы.

#Как это работает

  1. Администратор компании создаёт размещение: указывает URL внешнего приложения, выбирает место и параметры безопасности.
  2. Когда пользователь открывает страницу (или пункт навигации), MacroCRM подставляет в это место iFrame с адресом внешнего приложения.
  3. К адресу внешнего приложения MacroCRM добавляет GET-параметры контекста — набор параметров зависит от места размещения (см. таблицу ниже).
  4. Если у размещения задан «Ключ безопасности», все параметры сворачиваются в один зашифрованный параметр data. Если ключ не задан — параметры уходят обычной строкой запроса.
  5. Внешнее приложение загружается в iFrame, читает параметры и показывает нужный контент.

Несколько технических следствий, важных для разработчика:

  • Параметры всегда подставляются на стороне сервера MacroCRM, фронтенд их не модифицирует. Это значит, что вы получаете данные ровно в том виде, в каком их сформировала платформа.
  • Строка запроса вставляется перед фрагментом (#...). Если ваше внешнее приложение — SPA с hash-роутингом вида https://app/#/route, параметры не попадут во фрагмент, а останутся в обычной части запроса и дойдут до сервера приложения. Адрес соберётся как https://app/?estate_id=123#/route.
  • Для пункта навигации внешнее приложение открывается на отдельной полноэкранной странице, ссылка на которую подписана и привязана к конкретному размещению. Пользователь не может подделать параметры, отредактировав адрес вручную.

#Места размещения

Место — это точка интерфейса, куда можно встроить внешнее приложение. Есть два типа мест.

  • Блок на странице. Внешнее приложение встраивается прямо в разметку страницы как панель. Высота задаётся в настройках размещения (поле «Высота, px»), ширина всегда 100%.
  • Пункт навигации. В навигацию раздела добавляется пункт с заданным заголовком. Внешнее приложение открывается на отдельной полноэкранной странице по адресу /account/mf/<id>/. Такой пункт открывается в новой вкладке браузера: полноэкранная страница не имеет собственной поднавигации, и новая вкладка не уводит пользователя из раздела, где он находился.

#Таблица мест и передаваемых параметров

МестоТипGET-параметры
Страница сделки: после плана платежейБлок на страницеestate_id — id объекта, deal_id — id сделки, client_name — имя клиента, client_phone — телефон клиента
Страница сделки: под лентой событийБлок на страницеestate_id — id объекта
Страница заявки: под лентой событийБлок на страницеestate_id — id заявки, contacts_id — id контакта клиента
Карточка контакта: под лентой событийБлок на страницеcontacts_id — id контакта
Карточка контакта: низ страницыБлок на страницеcontacts_id — id контакта
Навигация: страница объекта/сделкиПункт навигацииestate_id — id объекта
Навигация: главная страницаПункт навигациипараметров нет
Навигация: отчётыПункт навигациипараметров нет

Точный набор параметров для выбранного места всегда показан в форме размещения в блоке «GET-параметры» — сверяйтесь с ним при разработке внешнего приложения.

#Ключ безопасности и шифрование параметров

По умолчанию параметры контекста передаются внешнему приложению открытой строкой запроса:

https://app.example.com/widget?estate_id=123&deal_id=456&client_name=...&client_phone=...

Если данные не должны быть видны в открытом виде в адресе iFrame, задайте у размещения «Ключ безопасности». Тогда все параметры шифруются и передаются в одном GET-параметре data:

https://app.example.com/widget?data=<urlencoded base64 шифротекста>

#Зачем нужен ключ безопасности

Ключ безопасности — это общий секрет между вашим внешним приложением и MacroCRM. Платформа шифрует им параметры, ваше внешнее приложение тем же ключом их расшифровывает. Без знания ключа постороннему недоступно содержимое параметра data.

#Формат шифрования

Шифрование выполняется алгоритмом AES-128-CBC. Формат параметра data:

data = urlencode( base64( iv . AES-128-CBC( json(params), key16, iv ) ) )

где:

  • iv — 16 случайных байт (вектор инициализации), генерируется заново на каждый запрос и ставится в начало результата;
  • key16 — первые 16 байт от сырого (binary) SHA-256 от вашего ключа безопасности: substr(hash('sha256', key, true), 0, 16). Эта нормализация позволяет использовать ключ любой длины;
  • json(params) — параметры, сериализованные в JSON с флагом JSON_UNESCAPED_UNICODE (кириллица сохраняется как есть, без \uXXXX).

То есть в base64 кодируется конкатенация iv (16 байт) и шифротекста, а весь результат дополнительно проходит urlencode, чтобы безопасно встать в строку запроса.

#Алгоритм расшифровки на стороне приложения

Пошагово:

  1. Прочитать GET-параметр data (он уже URL-декодирован вашим веб-сервером при разборе query string).
  2. Декодировать строку из base64 — получите бинарный буфер.
  3. Отделить первые 16 байт — это iv. Остаток — шифротекст.
  4. Вывести 16-байтный ключ: взять SHA-256 от вашего ключа безопасности в сыром (binary) виде и оставить первые 16 байт.
  5. Расшифровать остаток алгоритмом AES-128-CBC с этими key16 и iv.
  6. Разобрать получившийся JSON — это исходный набор параметров.

Пример на PHP:

$key = 'ваш-ключ-безопасности';
$raw = base64_decode($_GET['data']);          // urldecode уже сделал веб-сервер
$iv = substr($raw, 0, 16);
$ciphertext = substr($raw, 16);
$key16 = substr(hash('sha256', $key, true), 0, 16);

$json = openssl_decrypt($ciphertext, 'AES-128-CBC', $key16, OPENSSL_RAW_DATA, $iv);
$params = json_decode($json, true);            // ['estate_id' => 123, ...]

Пример на Node.js:

const crypto = require('crypto');

const key = 'ваш-ключ-безопасности';
const raw = Buffer.from(req.query.data, 'base64');
const iv = raw.subarray(0, 16);
const ciphertext = raw.subarray(16);
const key16 = crypto.createHash('sha256').update(key).digest().subarray(0, 16);

const decipher = crypto.createDecipheriv('aes-128-cbc', key16, iv);
const json = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
const params = JSON.parse(json); // { estate_id: 123, ... }

#Как настраивать размещения

Размещения настраиваются в разделе «Компания» → «MacroFrame» (адрес /account/company/macroFrame/). По сути это конструктор: вы заполняете форму, а платформа дальше сама встраивает внешнее приложение в выбранное место.

#Доступ

Раздел доступен пользователю с правом редактирования компании. На текущем этапе он открыт только в режиме разработчика. При обращении без этого режима система отвечает HTTP 403 с сообщением «Раздел доступен только разработчикам». Все операции создания, изменения и удаления размещений выполняются только методом POST; попытка вызвать их не POST-методом отклоняется с HTTP 405 («Метод не поддерживается»). Размещения строго изолированы по компании — чужие записи недоступны.

#Поля формы размещения

Чтобы добавить размещение, нажмите Добавить размещение и заполните форму. Сначала выбирается место — оно определяет, какие остальные поля появятся и будут обязательными.

ПолеПрименимостьОписание
«Место размещения»ВсегдаВыбирается из списка мест. Задаётся при создании и далее неизменно — при редактировании поле заблокировано, так как смена места поменяла бы и набор параметров, и способ вывода.
«GET-параметры»Всегда (справка)Справочный блок: какие параметры контекста получит внешнее приложение в выбранном месте.
«Название размещения (внутреннее)»ВсегдаОбязательное, до 255 символов, уникальное в пределах компании. Пользователям не видно — нужно, чтобы различать размещения в списке.
«URL приложения»ВсегдаОбязательный адрес внешнего приложения, только https, до 2048 символов. Нельзя указывать IP-адрес и нельзя указывать домен самой системы; поддомены системы разрешены.
«Высота, px»Только блок на страницеВысота встраиваемого блока: от 100 до 5000, шаг 10. Ширина всегда 100%.
«Заголовок пункта навигации»Только пункт навигацииОбязательный текст пункта, который увидит пользователь в навигации раздела.
«Ключ безопасности»ВсегдаНеобязательный, до 64 символов. Кнопка Сгенерировать ключ создаёт криптослучайный ключ. Если задан — параметры шифруются. В списке размещений у такого размещения перед URL показывается иконка-замок.
«Доступы фрейма»ВсегдаТумблеры «Микрофон», «Камера», «Акселерометр», включающие соответствующую allow-политику iFrame. По умолчанию выключены.
«Размещение включено»ВсегдаТумблер активности. Выключение мгновенно убирает фрейм со страниц и из навигации.

Ограничения поля «URL приложения» заданы намеренно:

  • только https — иначе вставка по http ломает защищённую страницу кабинета (mixed content);
  • запрет IP-адреса — защита от обращения к внутренним хостам (SSRF);
  • запрет домена системы — защита от фишинга: интерфейс системы внутри фрейма самой системы маскировал бы подмену. Поддомены системы при этом разрешены, так как это легитимные внешние сервисы.

#Как пользоваться

#Сценарий администратора

  1. Откройте «Компания» → «MacroFrame».
  2. Нажмите Добавить размещение.
  3. Выберите «Место размещения». После выбора появятся остальные поля и справка по передаваемым параметрам.
  4. Задайте «Название размещения (внутреннее)» — оно нужно только вам, чтобы различать размещения в списке.
  5. Укажите «URL приложения» (только https).
  6. Для блока на странице задайте «Высота, px». Для пункта навигации задайте «Заголовок пункта навигации».
  7. Если параметры нужно передавать в зашифрованном виде, задайте «Ключ безопасности» (можно нажать Сгенерировать ключ) и передайте этот же ключ разработчику внешнего приложения.
  8. При необходимости включите нужные «Доступы фрейма» (микрофон, камера, акселерометр).
  9. Убедитесь, что «Размещение включено», и сохраните.
  10. Откройте соответствующую страницу (или пункт навигации) — внешнее приложение появится на месте.

#Сценарий разработчика приложения

  1. Получите от администратора компании: выбранное место, точный набор GET-параметров (по справке «GET-параметры» в форме) и, если используется шифрование, «Ключ безопасности».
  2. Реализуйте приём параметров:
    • без ключа — читайте обычные GET-параметры (estate_id, deal_id, client_name, client_phone, contacts_id в зависимости от места);
    • с ключом — читайте параметр data и расшифровывайте его по алгоритму выше.
  3. Учтите, что внешнее приложение работает в iFrame с фиксированным sandbox-атрибутом (см. ниже). Если вам нужны камера, микрофон или акселерометр, попросите администратора включить соответствующие «Доступы фрейма».
  4. Если внешнее приложение — SPA с hash-роутингом, помните: параметры приходят в обычной части запроса перед #, а не во фрагменте.
  5. Не полагайтесь на кэширование адреса iFrame при включённом шифровании — он меняется на каждый запрос.

#Безопасность и ограничения

  • Только https. Внешнее приложение встраивается в защищённый кабинет; http-вставка ломала бы страницу и была бы небезопасна.
  • Запрет IP-адреса и домена системы в URL. IP — типовой вектор SSRF (обращение к внутренним хостам), домен системы — вектор фишинга. Поддомены системы разрешены как легитимные внешние сервисы.
  • Sandbox-атрибут iFrame. Фрейм запускается с фиксированным набором sandbox-разрешений, одинаковым для блока на странице и для полноэкранной страницы. Внешнему приложению разрешено: отправлять формы (allow-forms), исполнять собственные скрипты в своём origin (allow-scripts + allow-same-origin), открывать всплывающие окна (allow-popups) и выводить их из песочницы (allow-popups-to-escape-sandbox), показывать модальные диалоги (allow-modals), запрашивать доступ к хранилищу по действию пользователя (allow-storage-access-by-user-activation). Комбинация allow-scripts + allow-same-origin фактически снимает изоляцию для собственного origin приложения — то есть полноценной «песочницы» для самого приложения нет (это согласуется с тем, что URL задаёт только разработчик). Ключевое реальное ограничение в том, что фрейму не выдан allow-top-navigation: он не может перенаправить родительскую вкладку и увести пользователя с платформы. Набор sandbox-разрешений фиксирован платформой и не настраивается.
  • Точечный доступ к устройствам. Камера, микрофон и акселерометр доступны внешнему приложению только при включённом соответствующем тумблере в размещении.
  • Шифрование параметров. При заданном ключе безопасности параметры не видны в открытом виде в адресе iFrame. Это формат для несекретных данных, целостность ссылок полноэкранных страниц обеспечивается отдельно (см. ниже).
  • Защита полноэкранных ссылок. Ссылки на полноэкранную страницу пункта навигации (/account/mf/<id>/...) собираются только платформой. Сегмент с параметрами подписан HMAC-SHA256 и привязан к id размещения: пользователь не может открыть фрейм с произвольными параметрами, отредактировав адрес, и не может «переклеить» подписанный сегмент на другое размещение. Если у размещения параметров нет, ссылка короткая (/account/mf/<id>/) и подписи не требует.
  • Подпись ссылок завязана на платформенный секрет. Сегмент /account/mf/<id>/... подписывается общим секретом инсталляции (SECURITY_SALT). При его ротации все ранее сформированные ссылки полноэкранных страниц перестают открываться (отдают «Размещение не найдено») и пересобираются платформой автоматически при следующем заходе в раздел — это ожидаемый эффект, отдельный от смены ключа безопасности конкретного размещения (последняя ломает только расшифровку параметра data).
  • Единый ответ «не найдено». Полноэкранная страница загружает размещение строго по паре «id + компания текущей сессии». Чужое, удалённое или выключенное размещение, а также битая подпись отвечают одинаково — «Размещение не найдено». Перебор id ничего не раскрывает.
  • Изоляция по компании. Размещение всегда привязано к компании из текущей сессии; прочитать, изменить или удалить чужое размещение нельзя.

#Важные нюансы

  • Выключение размещения действует мгновенно. Как только «Размещение включено» переведено в выключенное состояние, фрейм сразу исчезает со страниц и из навигации — выборка берёт только активные размещения.
  • Смена ключа безопасности ломает расшифровку. Если вы меняете «Ключ безопасности» у размещения, внешнее приложение перестанет расшифровывать параметры старым ключом. Меняйте ключ синхронно с приложением.
  • Зашифрованный адрес не кэшируется. Из-за случайного вектора инициализации адрес iFrame с параметром data различается на каждый запрос.
  • Ротация платформенного секрета гасит полноэкранные ссылки. Смена SECURITY_SALT инвалидирует подпись всех ранее выданных ссылок /account/mf/; они перестают открываться и пересобираются платформой при следующем заходе в раздел.
  • Место и тип нельзя сменить после создания. Если нужно перенести внешнее приложение в другое место, создайте новое размещение, а старое удалите или выключите.
  • Название размещения уникально в пределах компании. Двух размещений с одинаковым внутренним названием в одной компании быть не может.
  • Параметры подставляет платформа. Внешнее приложение не управляет составом GET-параметров — он жёстко определён местом размещения.