Две компании работают в одном ИИ-сервисе, и при выгрузке отчёта в нём оказывается строка из базы соседа. Ниже — как пройти по местам, где компания-арендатор должна быть явно названа и проверена.
Изоляция данных в мультитенантном ИИ-сервисе требует проверки компании в каждом слое: базе, очереди, кеше, файлах, поиске и экспорте. Личность пользователя и его право работать в конкретной компании проверяют отдельно. Контекст компании передают вместе с задачей и сверяют на сервере. Общие справочники или совместные проекты требуют явно заданных правил доступа.
Представьте: две компании, «Альфа» и «Бета», работают в одном ИИ-сервисе, у каждой свой аккаунт, свои сотрудники и свои документы. В понедельник инженер «Альфы» выгружает отчёт и обнаруживает в нём строку из базы «Беты». Никто ничего не взламывал — просто фоновая задача взяла список компаний из кеша и обработала всех подряд. Сцена условная, но именно так выглядит класс ошибок, который ищут при проверке изоляции.
Изоляция данных в мультитенантном сервисе — это не один рубильник «включить приватность», а набор мест, где компания-арендатор (тенант) должна быть явно названа и проверена. Общая инфраструктура сама по себе нормальна: одна база, одни очереди, одни модели. Проблема начинается там, где код или конфигурация перестают спрашивать «а чья это строка?» и начинают доверять тому, что пришло снаружи или осталось в памяти с прошлого запроса. Ниже — рабочая методика, как пройти по этим местам и не перепутать аутентификацию с авторизацией тенанта.

Аутентификация отвечает «кто вы», а не «в какой компании»
Самая частая путаница: команда считает, что раз пользователь вошёл по логину и паролю, то и данные он увидит только свои. Это неверно. Успешный вход подтверждает личность, но ничего не говорит о том, к каким компаниям эта личность имеет право и в каком контексте сейчас работает. OWASP в Authentication Cheat Sheet разводит эти слои: аутентификация устанавливает субъекта, а решение о доступе к ресурсу — отдельный шаг. Если сервис после входа просто кладёт user_id в сессию и дальше фильтрует данные по нему, то пользователь, состоящий в двух компаниях, неизбежно увидит смесь.
Поэтому тенант — это отдельный параметр контекста, который живёт рядом с пользователем, а не выводится из него автоматически. В токене или сессии полезно хранить не «я такой-то», а «я такой-то, действую от имени компании X, и вот роли внутри X». Тогда каждый запрос несёт явный ответ на вопрос «в чьём пространстве мы сейчас работаем», и его можно проверить до того, как код полезет в базу.
Если ваша задача — проверить изоляцию компаний, на странице соответствующего направления 404ai можно посмотреть опубликованные возможности и оставить описание текущего процесса. Конкретные подключения, исходные данные и критерии приёмки согласуем отдельно.
Контекст тенанта как обязательный аргумент, а не глобальная переменная
Инженерная привычка «положу текущую компанию в глобальную переменную и буду читать откуда угодно» экономит строки и создаёт утечки. В многопоточном или асинхронном сервисе глобальное состояние легко переживает запрос: один обработчик выставил компанию, второй не перезаписал, третий прочитал чужое. Надёжнее сделать тенант обязательным аргументом на границе слоя доступа к данным — так, чтобы функция чтения документов физически не могла выполниться без указания компании. Если аргумент забыли, код не соберётся или упадёт на тесте, а не тихо вернёт всё подряд.
Проверку удобно ставить в одном месте — в обёртке над хранилищем, через которую проходят все запросы. Тогда правило «нет тенанта — нет данных» действует единообразно, а не зависит от аккуратности каждого разработчика. Это не гарантия, что никто не напишет сырой запрос в обход обёртки, но такие обходы становятся видимыми на код-ревью: любой прямой вызов базы без контекста — красный флаг.
Очередь, в которой забыли компанию
Фоновые задания — тихий источник смешения, потому что у них нет живого пользователя, который «случайно» заметит чужие данные. Задача на пересчёт рекомендаций, индексацию документов или отправку сводки ставится в очередь; в сообщении передают идентификатор объекта, но забывают тенант. Воркер берёт объект по id, а id глобально уникален — и обрабатывает данные, не зная, чьи они. Если внутри обработчика есть запрос «дай все связанные записи», он может зацепить соседей.
Лечится это тем, что тенант кладут в само сообщение очереди и проверяют при приёме, а воркер работает только в рамках полученного контекста. Полезно добавить дешёвую страховку: перед обработкой сверить, что объект действительно принадлежит указанной компании, и если нет — отправить сообщение в карантин, а не «починить на лету». Карантин с алертом лучше, чем молчаливая обработка чужого.
Кеш, который помнит лишнее
Кеш ускоряет всё, включая утечки. Если ключ кеша — это user_id или document_id без компании, два разных тенанта с одинаковым числовым id получат одну и ту же закешированную запись. Ещё коварнее кеш на уровне процесса: словарь «последние настройки компании» без привязки к тенанту отдаст настройки того, кто обратился первым. Правило простое: тенант входит в ключ кеша всегда, даже когда кажется, что id и так уникален. «Кажется» — плохая защита.
Отдельно стоит проверить инвалидацию. Когда компания меняет права сотрудника или удаляет документ, кеш должен сбрасываться в пределах её пространства, а не глобально и не «когда-нибудь». Глобальный сброс — это дорого и всё равно не гарантирует, что старая копия не всплывёт из кеша другого узла. Привязка ключей к тенанту делает инвалидацию точечной и предсказуемой.
Файлы — классика. Документ лежит в объектном хранилище под ключом вроде reports/2024/q3.pdf. Если сервис отдаёт файл по прямой ссылке, не сверяя компанию, достаточно подставить чужой путь. Даже когда путь содержит случайный идентификатор, полагаться на «никто не угадает» — это безопасность через незнание, а не через проверку. OWASP и обзоры мультитенантных архитектур сходятся в том, что доступ к объекту проверяется на каждом обращении, а не один раз при выдаче ссылки.
Практичнее хранить файлы в пространстве, разграниченном по тенанту (отдельный префикс или контейнер на компанию), и выдавать не постоянный адрес, а короткоживущую ссылку, которую формирует сервис после проверки прав. Тогда даже утёкшая ссылка перестаёт работать через несколько минут, а прямая подстановка чужого пути упирается в проверку на стороне выдачи. Обзор подходов к мультитенантности от Microsoft в Multitenant architecture полезен как карта слоёв, где такая проверка должна стоять, — но конкретные механизмы хранения вы выбираете под свою инфраструктуру.
Поиск, который умеет отсекать чужие документы
Полнотекстовый и векторный поиск — место, где смешение особенно заметно: пользователь вводит запрос и получает фрагменты из документов другой компании, потому что индекс общий, а фильтр по тенанту не применён. Microsoft описывает подход security trimming в Security trimming in search: в индекс кладут идентификаторы, по которым потом отсекаются результаты в момент запроса. Идея переносима на любой поисковый движок: тенант — обязательное поле документа в индексе и обязательный фильтр в каждом запросе, а не украшение.
Тонкость в том, что фильтр должен применяться до выдачи результатов, а не после — иначе в логах и в промежуточных ответах уже мелькнут чужие фрагменты. И проверять надо не только основной поиск, но и подсказки, автодополнение, «похожие документы». Каждая такая функция — отдельная дверь, и у каждой должен быть свой замок с тенантом.
Администратор платформы: самый опасный «свой»
Внутренний администратор, который «видит всё для поддержки», — легитимная роль, но и самый вероятный канал утечки. Если у него один общий аккаунт без ограничений, любая ошибка или скомпрометированный доступ открывает данные всех компаний сразу. Разбор инцидентов регулярно показывает: проблема не в хакере, а в широких правах, выданных «на время».
Практика — сделать доступ поддержки явным и ограниченным: отдельная роль, доступ по конкретной заявке с указанием компании и срока, журналирование каждого просмотра чужих данных. Это не паранойя, а способ отличить «помогли клиенту» от «посмотрели из любопытства». И да, такой доступ тоже должен проходить через проверку тенанта — просто с явно выданным исключением, а не с молчаливым обходом.
Экспорт и отчёты: где смешение становится видимым
Экспорт — финальная точка, в которой ошибка изоляции превращается в файл на чужом столе. Отчёт, собранный «по всем записям за квартал», сводка для руководства, выгрузка в таблицу — всё это часто пишется отдельным кодом, который живёт в стороне от основного хранилища и не проходит через общую обёртку с проверкой тенанта. Именно поэтому экспорт стоит проверять отдельно: какие фильтры он применяет, откуда берёт список компаний, что попадёт в файл, если у пользователя несколько компаний.
Полезное правило: экспорт собирается тем же слоем доступа к данным, что и обычные экраны, а не собственным SQL-запросом. Если без отдельного запроса не обойтись, он проходит тот же тест на «нет тенанта — нет данных». И перед выдачей файла — ещё одна сверка: все строки принадлежат запрошенной компании.
Пользователь, который работает в двух компаниях
Человек может быть сотрудником двух компаний одновременно — консультант, совладелец, подрядчик. Это законный сценарий, и он ломает наивную модель «один пользователь — один тенант». Если сервис хранит компанию в профиле пользователя, при переключении контекста часть экранов продолжит показывать данные предыдущей компании, потому что где-то остался старый идентификатор.
Решение — явное переключение активного тенанта с перевыпуском контекста (токена или сессии) и сбросом кешей, привязанных к пользователю. Каждый запрос после переключения несёт новый тенант, а старые ссылки и открытые вкладки не должны давать доступ к прежним данным. Это чуть больше работы, чем «просто хранить компанию в профиле», зато двухкомпанейный пользователь перестаёт быть источником инцидентов.
Контекст компании в запросе: что должно быть в каждой строке
Чтобы проверка была не разовой акцией, а свойством системы, полезно зафиксировать форму рабочего объекта — минимальный набор полей, который обязан быть у любой записи, файла, сообщения очереди и документа в индексе. Если этих полей нет, объект считается неполным и не проходит в обработку.
| Объект | Обязательные поля | Где проверяется |
|---|---|---|
| Строка в базе | tenant_id, object_id, owner_user_id |
Обёртка доступа к данным |
| Сообщение очереди | tenant_id, job_type, object_id |
Приём и обработка воркером |
| Ключ кеша | tenant_id + исходный ключ |
Чтение и запись кеша |
| Файл в хранилище | Префикс/контейнер тенанта, метаданные | Выдача ссылки |
| Документ в индексе | tenant_id как фильтруемое поле |
Каждый поисковый запрос |
| Строка экспорта | tenant_id в каждой строке |
Сборка и выдача файла |
Такая таблица — не бюрократия, а чек-лист для код-ревью и для тестов. Когда форма объекта одинакова везде, забыть тенант становится труднее: он либо есть, либо объект явно неполон.
Контекст компании в запросе на экране сотрудника
Изоляция — это ещё и то, что видит человек на экране. Если сотрудник не понимает, в какой компании он сейчас работает, он сам создаст путаницу: отправит документ не туда, посмотрит отчёт не той компании и решит, что «система врёт». Поэтому активный тенант должен быть виден постоянно — в шапке, рядом с именем, в заголовке экспорта. Переключение — явное действие с подтверждением, а не случайный клик.
Полезно показывать в интерфейсе и границы: какие данные доступны в текущем контексте, что произойдёт при переключении, куда уйдёт созданный документ. Это снижает число обращений в поддержку «а почему я не вижу отчёт» и заодно уменьшает соблазн у администратора выдать широкий доступ «чтобы быстрее». Понятный интерфейс — часть защиты, а не только удобство.
Контекст компании в запросе: исключения и проверка условий
Реальные системы редко укладываются в «всё строго по тенанту». Есть сценарии, где данные законно пересекаются, и их надо назвать заранее, а не обнаруживать в проде.
- Общие справочники. Справочник городов или валют один на всех — это нормально, но он не должен смешиваться с клиентскими данными. Держите общие данные в отдельном пространстве с явной пометкой «публичное».
- Совместные проекты. Две компании работают над одним проектом и хотят видеть общие документы. Это не «утечка», а осознанный общий ресурс, у которого есть владелец и список участников. Такой ресурс — отдельная сущность, а не побочный эффект общего кеша.
- Перенос компании. При слиянии или продаже бизнеса данные переезжают из одного тенанта в другой. Это разовая операция с журналом, проверкой и обратимостью, а не «поправим tenant_id в базе скриптом».
- Демо и тестовые данные. Тестовый тенант не должен случайно оказаться доступным реальным клиентам, а реальные данные — попасть в демо.
Каждое исключение стоит описать в документе и закрыть тестом. Тогда оно перестаёт быть дырой и становится управляемым случаем.
Тесты, которые ловят смешение раньше клиента
Проверять изоляцию глазами недостаточно: нужен набор автоматических тестов, которые специально пытаются достать чужое. Начните с «перекрёстных» сценариев: пользователь компании A запрашивает объект компании B по прямому id и должен получить отказ, а не данные. То же — для файла, для поискового запроса, для экспорта, для сообщения очереди с подменённым тенантом.
| Тест | Что подставляем | Ожидаемый результат |
|---|---|---|
| Прямой доступ к чужому объекту | id компании B | Отказ, запись в журнал |
| Поиск с фильтром соседа | запрос от A, документы B | Пусто, без фрагментов B |
| Ссылка на чужой файл | путь компании B | Отказ выдачи |
| Очередь без тенанта | сообщение без tenant_id |
Карантин, алерт |
| Кеш с общим ключом | одинаковый id у A и B | Разные записи |
| Экспорт от A | данные A и B в выборке | Только строки A |
Отдельно стоит проверить поведение при переключении тенанта: после смены компании старые ссылки и открытые вкладки не должны отдавать прежние данные. И — журнал: каждое обращение к чужим данным, даже разрешённое для поддержки, должно оставлять след, по которому потом можно восстановить, кто и зачем смотрел.
Практикум: пройти путь одного документа
Возьмите один условный документ компании «Альфа» — скажем, коммерческое предложение — и проследите его путь по системе, отмечая на каждом шаге, где назван тенант. Документ создан сотрудником: есть ли tenant_id в записи? Попал в очередь на индексацию: передан ли тенант в сообщении? Залёг в кеш списка «последние документы»: входит ли компания в ключ? Проиндексирован: есть ли тенант как фильтруемое поле? Открыт по ссылке коллегой: проверена ли компания при выдаче? Выгружен в отчёт: прошли ли строки через общий слой доступа? Отправлен внешнему партнёру: не осталась ли постоянная ссылка, которая переживёт смену прав?
Если на каком-то шаге тенант не назван — это и есть место для исправления. Пройдите так по двум-трём типам объектов (документ, сообщение в чате, файл) и составьте карту: где проверка есть, где её нет, где она формальная. Такая карта честнее любого отчёта «мы всё изолировали», потому что показывает конкретные двери, а не общее впечатление.
Проверка на практике: контекст компании в запросе
Начните с одного объекта и одного сквозного теста: создайте в условной компании A документ, попробуйте достать его из компании B по прямому идентификатору, через поиск, по ссылке на файл и через экспорт. Если хотя бы один путь отдаёт данные — у вас есть конкретная задача, а не абстрактное «улучшить безопасность». Дальше расширяйте проверку на очередь, кеш и фоновые задания, пока каждый слой не начнёт требовать явный тенант.
Смежный вопрос разобран отдельно: Несколько брендов в одной CRM: от чьего имени отвечает агент.
Источники и границы разбора
Источники и статус проверки на 9 ноября 2026. Первичные документы поясняют указанные механизмы; доступность и границы каждого источника отмечены ниже. Методические рекомендации и условные примеры составляют самостоятельную часть разбора.
- Разработка под задачу
Опубликованное направление работы 404ai. Предлагаемая в статье методика не означает, что каждое описанное действие уже доступно без настройки; состав решения согласуется отдельно.
- Microsoft: Multitenant architecture
Первичная документация указанного инструмента или механизма. Практические рекомендации и условные эпизоды статьи — самостоятельная методика, а не описание готовых функций 404ai.
- Microsoft: Security trimming in search
Первичная документация указанного инструмента или механизма. Практические рекомендации и условные эпизоды статьи — самостоятельная методика, а не описание готовых функций 404ai.
- OWASP: Authentication Cheat Sheet
Первичная документация указанного инструмента или механизма. Практические рекомендации и условные эпизоды статьи — самостоятельная методика, а не описание готовых функций 404ai.