Зачем: - Показать слой 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.
133 lines
11 KiB
Markdown
133 lines
11 KiB
Markdown
# Права 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`
|
||
поддерживаемым ограничением или зарегистрированным дефектом, не найдено;
|
||
классификация как дефекта остаётся выводом из расхождения документации и кода.
|