Files
clickstream-data-platform/docs/adr/0007-clickhouse-access.md
T
ddadminandClaude Fable 5.1 7108cd4af3 docs(readme): корневой README пересобран по четырем блокам
Зачем: README был складом сведений, осевших там, где писались. По
раскладке резолюции #211 он становится лицом репозитория: промо-блок,
«куда я попал», типовые действия, ссылки. Слово «менти» уходит из README
и путеводителя, хвосты #212 и #214 закрываются.

Что: промо-цитата со ссылкой на план обучения и ментора; шапка с
нейтральной адресацией и одной фразой о составе и пути данных; «Быстрый
старт» сжат до двух команд по решению владельца — проверки остаются
роботам, в README одна ссылка на карту проверок; разделы «Адреса и
пароли», «Сдвинуть мир вперед», «Вернуть мир к началу», «Выключить или
сбросить целиком», «Подключиться из DBeaver»; «Документация» называет все
семь справочников вложенным списком. Умерли «Статус», «Что здесь будет»,
«Чем отличается от предшественника» и три заголовка-якоря. В world.md
снята устаревшая фраза о «даге этапа 5», stand.md и ADR 0007 ведут на
новый раздел «Адреса и пароли», в путеводителе снято слово «менти».

Проверка: скрипт ссылок пуст, grep «менти» по README и путеводителю пуст,
make config-test зеленый. Саморевью Sol 1/1; слепое ревью Opus — 0
блокеров, 2 minor, 3 nit, все закрыты; вычитка прозы ai-text-lint и
оптикой литературного русского — 15 находок, 9 внесены, 6 отклонены как
слова владельца и термины.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 15:37:04 -04:00

18 KiB
Raw Blame History

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).
  • 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).

Роль здесь шире прав на слои: образ 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 можно только отдельным маршрутом, учётной записью или копией данных. Эта механика не даёт нового урока. Публичное представление остаётся договором чтения; границу прав оно не задаёт (исследование).

Беспарольный dict. Пароль пришлось бы вписать в текст DDL словаря, то есть положить секрет в git. Ходить этой учёткой некуда: право у неё одно — SELECT на dic, и хост источника локальный.

Своя роль дашбордам. Панели мониторинга живут не на витринах, а на system.query_log, частях MergeTree и очереди распределённых DDL — и читают их через clusterAllReplicas, потому что системные таблицы у каждой ноды свои (ADR 0002). Для этого нужны 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. Причина и отклонённые варианты разобраны в исследовании.