Files
clickstream-data-platform/docs/adr/0007-clickhouse-access.md
T
ddadmin ae5bd31dbd docs(dds): уточнён публичный контракт события
- Зачем:
  - Закрепить реалистичную границу доступа BI и убрать двусмысленность для неизвестных кодов.
- Что:
  - Пояснены доступ к ODS, пояс счётчика и порядок DDL.
  - Проверка равенства строк отделена от размера конкретной загрузки.
- Проверка:
  - `git diff --check`.
  - `make config-test` не стартовал: Snap требует работающий `snapd.apparmor`.
2026-08-25 17:53:22 -04:00

210 lines
18 KiB
Markdown
Raw 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.
# ADR 0007. Доступ к ClickHouse: пользователи файлом, доверие нод секретом
Дата: 9 августа 2026 года. Статус: принято; роль `monitoring_reader`
и пользователь `grafana` добавлены 21 августа 2026 года (#114), права
`bi_reader` расширены до ODS, DDS и DM 25 августа 2026 года (#150).
## Решение
У кластера семь пользователей и шесть ролей.
- `default` — с паролем, только служебный: проверки здоровья нод и работа
изнутри контейнеров. Приложения им не ходят.
- `etl` с ролью `etl_writer` — чтение и запись во всех слоях. Им ходят рабочие
даги Airflow.
- `lifecycle` с ролью `lifecycle_owner` — создание и удаление только
прикладных баз и объектов. Им даги жизненного цикла применяют DDL и
пересоздают мир ([ADR 0013](0013-world-lifecycle.md)).
- `bi` с ролью `bi_reader` — чтение слоёв ODS, DDS и DM, а также справочников
`dic`. Им ходит Superset; его запросы обращаются к публичным `_v`, хотя права
шире.
- `analyst` с ролью `analyst_reader` — чтение всех слоёв и справочников. Им
человек подключается снаружи, из своего клиента.
- `grafana` с ролью `monitoring_reader` — чтение слоёв, таблиц базы `dic` и
системных таблиц, `CLUSTER` и `REMOTE`. Им ходит Grafana, и больше никто.
- `dict` с ролью `dict_reader` — чтение одной базы `dic`, пароля нет. Им
словарь читает свою подложку, и больше он никем не используется
([ADR 0012](0012-dictionary-home.md)).
Роль здесь шире прав на слои: образ ClickHouse требует отдельного разрешения
на запросы `ON CLUSTER`, на чтение системных таблиц и на каждый движок
внешнего источника. Без права на системные таблицы клиент вовсе не открывает
соединение.
Пользователи, роли и права объявлены файлами настройки сервера, а не
запросами SQL. Файл монтируется на обе ноды и лежит в git. Пароли живут
в `.env` и попадают в настройку подстановкой по имени переменной; значения
собраны по конвенции «слово-число-слово», потому что пароль `analyst`
человек набирает руками.
Ноды доверяют друг другу по общему секрету, объявленному в описании
кластера. Учётные данные по репликам не расписываются.
Сетевых ограничений на `default` нет: порты стенда и так открыты только на
`127.0.0.1`, а внутри сети Compose дверь заперта паролем.
Раздел README «Состав и доступ» описывает, кто каким пользователем ходит и где
живут пароли; здесь остаются решение и доводы. Отдельного справочника по
доступу в `docs/architecture/` нет: точные права видны в `access.xml`, конвенция
чтения через `_v` описана в доке хранилища.
## Почему
Стенд учит не администрированию доступа, а чтению отказа в правах: человек
должен узнать в бою картину, которую видел здесь, и понимать, чего просить.
Отсюда критерий отбора — модель должна читаться из текста ошибки, а не из
описания. Поэтому пользователей мало, имена у них говорящие. В бою BI обычно
читает витрины и модельный слой. ODS ему могут открыть для расследования или
сверки. Стенд оставляет этот доступ постоянным, но сохраняет две крупные
границы: `bi` отделён от STG и от записи. Один пользователь может сверить всю
цепочку DM → DDS → ODS, не получая доступа к сырью и изменению данных.
Справочники видны всем читателям и никому на запись: зона входит в хранилище
сбоку, и её содержимым владеет файл репозитория, а не слой.
Ограничить `bi` одними `_v` поверх `Distributed` можно только отдельным
маршрутом, учётной записью или копией данных. Эта механика не даёт нового урока.
Публичное представление остаётся договором чтения; границу прав оно не задаёт
([исследование](../research/2026-08-25-clickhouse-distributed-view-security.md)).
**Беспарольный `dict`.** Пароль пришлось бы вписать в текст DDL словаря, то
есть положить секрет в git. Ходить этой учёткой некуда: право у неё одно —
`SELECT` на `dic`, и хост источника локальный.
**Своя роль дашбордам.** Панели мониторинга живут не на витринах, а на
`system.query_log`, частях MergeTree и очереди распределённых DDL — и читают
их через `clusterAllReplicas`, потому что системные таблицы у каждой ноды свои
([ADR 0002](0002-monitoring-scope.md)). Для этого нужны `SELECT` на `system.*`
и `REMOTE`; контракт роли также включает `CLUSTER` для запросов `ON CLUSTER`.
Такого набора не было ни у одного читателя: у обеих читающих ролей из
системных таблиц только `system.settings`. Догрузить
недостающее в `analyst_reader` отвергнуто: аналитик получил бы системные
таблицы и `REMOTE` заодно, а в `system.query_log` запросы Grafana стали бы
неотличимы от человеческих — ровно там, где дашборд «Запросы» и должен их
различать. Пускать Grafana под `etl` отвергнуто сразу: читатель не получает
права писать. Осталась своя роль по здешней общей схеме — у каждого
потребителя она своя.
**Секрет, а не учётные данные по репликам.** У каждой реплики в описании
кластера своя учётка — вписанная явно или подразумеваемая, и тогда это
беспарольный `default`. Подключение к соседней ноде идёт от неё, а не от
того, кто задал вопрос. Значит, стоит `default` получить пароль — и без
правки описания кластера межнодовые запросы встают. Починить это можно двумя
способами, и они различаются не удобством. С учётными данными по репликам вся
межшардовая работа идёт от одной общей учётки: на своей ноде права
применяются, на соседней — нет. Ролевая модель тогда наполовину декорация, а
это хуже её отсутствия: она утверждает разграничение, которого не существует.
Общий секрет передаёт на соседа личность инициатора, стоит одной строки в
описании кластера и не размножает пароль по репликам. Шифрование тут ни при
чём; в соседнем `clickhouse-learning-cluster` выбран как раз путь с учётными
данными по репликам, и там пароль вписан в каждую реплику каждого из трёх
кластеров.
Довод подтверждён замером 13 августа 2026 года: на соседнем шарде сохранился
пользователь-инициатор, поэтому там применились его права. Результат замера —
в разделе «Что проверено».
**Файлы, а не SQL.** Объявленное файлом не оставляет состояния в томах:
источник истины — файл в git. Стенд считает `access.xml` и переданные через
`.env` значения настройкой при создании контейнера; отдельного способа
применять их на живом стенде нет, потому что уроки не меняют базовую модель
доступа. Порядок применения изменений — в разделе README «Состав и доступ».
Оба варианта с SQL — локальное хранилище доступа с `ON CLUSTER` и
реплицированное в keeper — делают пользователей состоянием тома: `.env` на
них больше не влияет, и появляется расхождение того же сорта, которое README
уже описывает для Postgres и Grafana. Цена выбора известна: объявленных файлом
пользователей нельзя менять запросами. Она принята как упрощение — сломать
модель случайно труднее.
**Что ещё отвергнуто.** Оставить всё на беспарольном `default`
— цена нулевая, но и рассказать по итогам нечего. Раздать всем пароль без
ролей — дёшево и показывает ровно тот антипаттерн, за который ругают в бою:
все под одним админом. Вынести доступ в лекцию, как репликационную
эксплуатацию, — довод «отдельный операционный домен» здесь не работает:
у репликации это живые процессы, учения и runbook, а тут несколько десятков
строк настройки и ноль эксплуатации. Запереть `default` ещё и по сети —
второй замок на ту же дверь.
**Отложено явно**: TLS и защищённые межсерверные соединения, настоящее
хранилище секретов, внешние поставщики учётных записей, квоты и профили под
пользователя, политики строк, аудит запросов.
## Следствия
Новые таблицы ODS, DDS и DM не требуют отдельного гранта для `bi_reader`.
Явный договор `_v` защищает потребителя от устройства хранения и случайных
новых колонок.
DDL для матвью приёма выполняется от имени `lifecycle`. Это видно в журнале
запросов, а при создании ClickHouse проверяет у этой учётки чтение источника и
вставку в цель. Так отдельная учётка показывает границу между редким
управлением структурой и повседневной записью `etl`, не выдавая рабочим дагам
право удалять базы.
Лаба «сломать права» вариантом с файлами не закрывается: создать через SQL
новую роль и нового пользователя ничто не мешает, и новую роль разрешено
выдавать даже пользователю, объявленному файлом. Недоступно обратное —
править объявленных файлом. Всё, что лаба создаст, живёт в томе и убирается
`make clean`.
## Что проверено
Сверено 9 августа 2026 года по документации ClickHouse через MCP Context7 и
на живом стенде 26.3.17.56.
- В `system.clusters` у каждой реплики своя колонка `user`; на стенде там
`default`. Отсюда весь довод про общий секрет.
- Секрет кластера аутентифицирует серверы хешем от соли, нонса, секрета и
запроса и позволяет назначить пользователя, от которого запрос выполняется
на соседней ноде.
- Роли и права объявляются в файле настройки пользователей наравне с самими
пользователями; SQL-хранилище доступа для этого не требуется.
- Хранилище пользователей из файла — только для чтения: попытка изменить
такого пользователя запросом даёт `ACCESS_STORAGE_READONLY`.
- Роль, созданную через SQL, разрешено выдавать пользователю из файла —
отдельное улучшение ClickHouse 2025 года, в 26.3 оно есть.
- Подстановка значений из переменных окружения — общий механизм файлов
настройки ClickHouse.
- Весь DDL репозитория идёт через `IF NOT EXISTS` (файлы `sql/ddl/`).
Права роли `monitoring_reader` сверены 21 августа 2026 года по документации
ClickHouse через MCP Context7 (#114).
- Системные таблицы выдаются обычным `SELECT`: в документации это
`GRANT SELECT ON system.query_log`, значит `SELECT ON system.*` и есть «все
системные таблицы». Отдельного права на них нет.
- `GRANT CLUSTER ON *.*` — документированная форма права на запросы
`ON CLUSTER`; текущий запрос `clusterAllReplicas` его не использует.
- `clusterAllReplicas` документация держит в одном ряду с `remote`,
`remoteSecure` и `cluster`: это табличная функция, которая берёт имя
кластера из `remote_servers` и идёт на каждую его реплику. Отсюда `REMOTE` —
без права на удалённое обращение запрос дальше своей ноды не пойдёт, а
панели ADR 0002 читают `system.*` именно так.
Набор проверен на живом стенде 21 августа 2026 года (#114): пользователь
`grafana` из Grafana выполнил
`SELECT count() AS replicas FROM clusterAllReplicas('clickstream_cluster', system.one)`
и получил 2 — прав хватает, чтобы прочесть системную таблицу с обеих нод.
Проверено на живом стенде 13 августа 2026 года, ClickHouse 26.3.17.56.
- Секрет кластера принимается внутри `remote_servers` в виде
`<secret from_env="CLICKHOUSE_CLUSTER_SECRET"/>`. Запрос от `etl` через обе
ноды выполнился на каждой под пользователем `etl`.
- `from_env` работает у элемента `password`: `etl`, `lifecycle`, `bi` и
`analyst` вошли с паролями из окружения и получили свои роли.
- При запросе от `bi` через Distributed на второй ноде `system.query_log`
показал `user = bi` и `initial_user = bi`. Значит, на соседнем шарде
применяются права инициатора, а не общей учётной записи.
- Права на отсутствующие базы `dds` и `dm` приняты. До создания баз они видны
в `SHOW GRANTS FOR etl_writer`; временная таблица в `dm` подтвердила право
`bi` на чтение после появления базы.
Расширение `bi_reader` проверено на живом стенде 25 августа 2026 года (#150):
- `SHOW GRANTS FOR bi_reader` содержит `SELECT` на ODS, DDS и DM;
- `bi` прочитал одинаковое число строк из `ods.event_v` и `dds.event_v`;
- чтение `stg.hits_raw_rep` завершилось ошибкой `ACCESS_DENIED`;
- представление над `Distributed` с `SQL SECURITY DEFINER` на удалённой ноде
сохранило исходного пользователя `bi`. Причина и отклонённые варианты
разобраны в [исследовании](../research/2026-08-25-clickhouse-distributed-view-security.md).