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

Модуль XX — многотенантность и управление организациями

Модуль XX — это не сервис, навешенный на движок, а свойство самого движка. Нет отдельного модуля тенантности, который надо подключить; вместо этого модель данных ядра несёт границу тенанта на каждой сущности, а хранилище обеспечивает её соблюдение ниже каждого запроса. Эта страница — справочник о том, что гарантирует эта граница сегодня, и о тех частях управления организациями, которые пока находятся на стадии проектирования.

Многотенантность живёт на уровне движка (уровень 0), рядом с собственным API платформы (модуль XIX), потому что встраивание изоляции в уже работающую модель данных задним числом — это тот вид изменений, который нельзя безопасно сделать позже. Каждая сущность ядра несёт tenant_id, и вызывающая сторона никогда не передаёт его как свободный параметр: она фиксирует тенант один раз и получает область видимости, репозитории которой уже привязаны к нему. В API нет словаря для пересечения границ тенантов — само это отсутствие и есть первый барьер изоляции, ещё до любого механизма базы данных. Привилегированная межтенантная область (создание организации, перечисление организаций, удаление тенанта) достижима только из собственного запуска движка, но никогда из модуля.

Модель тенанта принадлежит контракту модели данных, а не схеме отдельного модуля. Корневая сущность — это Org, которая и есть тенант: когда движок засевает организацию, её идентификатор становится идентификатором тенанта, и собственная цепочка аудита организации устанавливается в тот же момент. Любая другая сущность ядра — агенты, сессии, ресурсы, идентичности, политики, записи о стоимости, находки, развёртывания, карта доступа и журнал аудита — создаётся внутри области тенанта и штампуется этим тенантом при записи; вызывающая сторона не может это переопределить.

Изоляция обеспечивается на уровне запросов, в зависимости от развёртывания:

  • На PostgreSQL каждая таблица, несущая tenant_id, работает под FORCE ROW LEVEL SECURITY с политикой tenant_isolation, привязанной на каждую транзакцию. Транзакция, которая не смогла привязать тенант, вызывает ошибку, а не молча возвращает ноль строк (fail-closed, отказ в закрытом состоянии). Роль приложения не суперпользователь и никогда не имеет BYPASSRLS, а FORCE связывает политику даже для владельца таблицы. Владение — это выбор развёртывания: установка с одной ролью по умолчанию оставляет роль приложения владельцем базы данных — RLS её всё равно связывает, но владелец может изменять собственные таблицы, поэтому такая позиция обнаруживает вмешательство (tamper-evident), а не защищает от владельца (owner-proof). Жёсткая граница привилегий — роль приложения, которая вдобавок не является владельцем, — даётся топологией с разделением owner/app, где отдельная роль-владелец выполняет provisioning, а роль приложения получает только тот DML, который ей нужен.
  • На SQLite (однонодовое развёртывание) нет защиты на уровне строк (row-level security); эквивалентность обеспечивается двумя фактами — единственный путь к базе данных — это сгенерированный из дескриптора SQL, который всегда добавляет предикат тенанта, и триггеры-растяжки (tripwire) прерывают любую запись, тенант которой не совпадает с зафиксированной областью.

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

У модуля XX нет поверхности шины событий и нет актуации. Он не потребляет edge.observed, не эмитит находки и не вызывает никаких провайдеров — это субстрат, сквозь который пишут другие модули. Его единственный наблюдаемый эффект — структурный: каждая сущность, которую сохраняет любой модуль, уже привязана к области тенанта, и каждая мутация над аудируемой сущностью добавляется в hash-chained журнал аудита этого тенанта в рамках той же транзакции.