Files
clickstream-data-platform/docs/research/2026-08-25-clickhouse-distributed-view-security.md
ddadmin 9ea361d199 feat(dds): добавлен публичный контракт события
Зачем:
- Показать слой DDS как договор данных, которому не нужна физическая копия.
- Сохранить простой учебный стенд без отдельного маршрута ради узких прав.

Что:
- Добавлено кластерное представление dds.event_v с явными 44 колонками.
- Роли bi_reader разрешено чтение ODS, DDS и DM при договоре чтения через _v.
- Согласованы мастер-спека, ADR, справочники архитектуры и живые тикеты этапа.
- Зафиксировано исследование поведения DEFINER поверх Distributed.

Проверка:
- make config-test
- make smoke
- make check-clickhouse
- Приёмочные запросы: равенство ODS/DDS, состав колонок, коды устройств и права bi.
2026-08-25 17:13:35 -04:00

133 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Права DEFINER у представления над Distributed
Дата исследования: 2026-08-25. Тикет #150. ClickHouse 26.3.17.56.
## Вопрос
Почему `dds.event_v` с `SQL SECURITY DEFINER`, созданное пользователем
`lifecycle`, всё равно читает удалённый `ods.event_rep` как `bi`? Можно ли
оставить `bi` только с правом на публичное представление DDS, не заводя отдельный
маршрут или пользователя и не копируя данные?
## Короткий вывод
Простого поддерживаемого способа в ClickHouse 26.3 нет. `DEFINER` меняет
пользователя, чьи права проверяет локальный запрос представления, но сохраняет
в запросе имя его первоначального инициатора — `initial_user`. Движок
`Distributed` передаёт это имя на шарды, а межсерверный секрет делает его
пользователем удалённой сессии. Поэтому на координаторе источники читает
`lifecycle`, а на соседнем шарде физическую таблицу читает `bi`.
Это не заявленная семантика `SQL SECURITY DEFINER`: документация обещает, что
право на источник требуется определившему представление пользователю, а
вызывающему достаточно `SELECT` на представление. Поведение следует из
сочетания двух реализаций и выглядит как недокументированное ограничение или
дефект распределённого исполнения.
## Где меняется пользователь
`dds.event_v` разворачивается в запрос к `ods.event_v`, затем к
`ods.event_dist FINAL`. Для внутреннего запроса представления ClickHouse
создаёт новый контекст в
[`getSQLSecurityOverriddenContext`](https://github.com/ClickHouse/ClickHouse/blob/v26.3.17.56-lts/src/Storages/StorageInMemoryMetadata.cpp#L135-L175):
1. копирует исходный `ClientInfo` целиком, включая `initial_user = bi`;
2. затем отдельно вызывает `setUser` для определившего представление
пользователя `lifecycle`.
В контексте остаются два имени с разными задачами: `lifecycle` отвечает за
локальную проверку прав, а `bi` остаётся первоначальным пользователем запроса.
Второй `SQL SECURITY DEFINER` на `ods.event_v` повторяет те же два шага и
поэтому ничего не меняет.
## Что происходит на границе шарда
`Distributed` копирует `ClientInfo` из контекста и отправляет его на удалённую
ноду без замены `initial_user`
([`RemoteQueryExecutor.cpp`](https://github.com/ClickHouse/ClickHouse/blob/v26.3.17.56-lts/src/QueryPipeline/RemoteQueryExecutor.cpp#L415-L444)).
При межсерверном секрете имя входит в подписываемые данные запроса
([`Connection.cpp`](https://github.com/ClickHouse/ClickHouse/blob/v26.3.17.56-lts/src/Client/Connection.cpp#L968-L994)).
Принимающая нода проверяет подпись и открывает сессию с
`AlwaysAllowCredentials{client_info.initial_user}`
([`TCPHandler.cpp`](https://github.com/ClickHouse/ClickHouse/blob/v26.3.17.56-lts/src/Server/TCPHandler.cpp#L2303-L2356)).
Секрет здесь выполняет штатную функцию. Это не случайный побочный эффект.
Документация `Distributed` прямо говорит: если задан `<secret>`, `initial_user`
становится текущим пользователем удалённого запроса
([описание кластера](https://github.com/ClickHouse/ClickHouse/blob/v26.3.17.56-lts/docs/en/engines/table-engines/special/distributed.md#L105-L118)).
Так права, роли и ограничения исходного пользователя одинаково действуют на
всех шардах: координатор подтверждает личность пользователя, но не превращает
его запрос в запрос своей служебной учётки. Протокол не передаёт отдельно
эффективного пользователя `DEFINER`, поэтому на стенде эта защита и
останавливает `bi` у `ods.event_rep`.
## Рассмотренные варианты
### Настройка или другая форма представления
В 26.3 нет настройки, которая подменяет `initial_user` на определившего
представление пользователя только для чтения `Distributed`.
`push_external_roles_in_interserver_queries` пересылает роли именно
`initial_user`, не роли `DEFINER`
([исходник отправки](https://github.com/ClickHouse/ClickHouse/blob/v26.3.17.56-lts/src/QueryPipeline/RemoteQueryExecutor.cpp#L425-L444)).
`distributed_ddl_use_initial_user_and_roles` относится только к заданиям
`ON CLUSTER`. На `SELECT` настройка не действует
([объявление настройки](https://github.com/ClickHouse/ClickHouse/blob/v26.3.17.56-lts/src/Core/ServerSettings.cpp#L1002-L1006)).
### Пользователь в существующем описании кластера
С межсерверным секретом ClickHouse выбирает специальную межсерверную
аутентификацию и не использует пароль из описания реплики
([`Connection::sendHello`](https://github.com/ClickHouse/ClickHouse/blob/v26.3.17.56-lts/src/Client/Connection.cpp#L452-L476)).
Чтобы удалённый запрос всегда шёл как `lifecycle`, пришлось бы убрать секрет и
передавать его пароль. Тогда между нодами перестанет передаваться исходный
пользователь. Одна модель доступа сменится другой, менее прозрачной.
### `remote()` с явными реквизитами
Для `remote()` ClickHouse умеет заменить `initial_user` пользователем из
адреса
([`executeQuery.cpp`](https://github.com/ClickHouse/ClickHouse/blob/v26.3.17.56-lts/src/Interpreters/ClusterProxy/executeQuery.cpp#L96-L120)).
Но представлению понадобятся адреса и реквизиты удалённого пользователя — в
тексте DDL либо в отдельной настройке. Это и есть отдельный маршрут доступа;
он сложнее нынешнего кластера и требует хранить ещё один секрет.
### Промежуточное локальное представление на шарде
Можно направить `Distributed` на локальное представление с `DEFINER`, но
удалённому пользователю всё равно нужен `SELECT` на это представление. Значит
появятся дополнительный объект и дополнительный грант, а `bi` увидит
поверхность одного шарда. Физическая таблица останется закрытой, но простота и
контракт «только публичная поверхность DDS» исчезнут.
### Ослабление проверки
- `SQL SECURITY NONE` снимает проверку источника, но устарело и позволяет
создателю такого представления исполнять произвольный запрос; ClickHouse
требует для этого отдельное право
([документация](https://github.com/ClickHouse/ClickHouse/blob/v26.3.17.56-lts/docs/en/sql-reference/statements/create/view.md#L115-L136)).
- Право `bi` на физическую ODS-таблицу решает ошибку без новой механики, но
ослабляет исходную границу.
- Физическая или материализованная копия в DDS сохраняет границу, но добавляет
хранение и жизненный цикл данных ради переименования колонок.
## Решение для стенда
25 августа 2026 года выбрана простота: роль `bi_reader` читает ODS, DDS и DM
целиком. Потребительские запросы продолжают обращаться к `_v`. Этот договор
держат документация и ревью; границы прав задают гранты.
Отдельный межсерверный маршрут, служебная учётка и физическая поверхность DDS
отклонены: каждый вариант добавляет механику только ради защиты, которой стенд
не учит. Готовой настройки, которая сохранила бы и узкую границу, и простое
устройство, в закреплённой версии нет.
## Граница уверенности
Механизм подтверждён исходным кодом точной версии 26.3.17.56. Простую настройку
искали в настройках сервера и в путях формирования удалённого запроса этой же
версии. Уверенность в обоих выводах высокая. Официального
текста, который называет сочетание `DEFINER + Distributed + secret`
поддерживаемым ограничением или зарегистрированным дефектом, не найдено;
классификация как дефекта остаётся выводом из расхождения документации и кода.