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.
This commit is contained in:
ddadmin committed 2026-08-25 17:13:35 -04:00
1 parent 4ea9591a16
commit 9ea361d199
11 files changed
+288 -76

No files matched your search

@@ -0,0 +1,132 @@
# Права 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`
поддерживаемым ограничением или зарегистрированным дефектом, не найдено;
классификация как дефекта остаётся выводом из расхождения документации и кода.