From 790f7c1bb71670467a85b04a6549b31c349220b0 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Thu, 13 Aug 2026 10:04:58 +0300 Subject: [PATCH 1/5] =?UTF-8?q?feat(clickhouse):=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=80=D0=BE=D0=BB=D0=B8?= =?UTF-8?q?=20=D0=B8=20=D0=BC=D0=B5=D0=B6=D0=BD=D0=BE=D0=B4=D0=BE=D0=B2?= =?UTF-8?q?=D1=8B=D0=B9=20=D1=81=D0=B5=D0=BA=D1=80=D0=B5=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - менти должен увидеть разделение доступа без состояния в томах. - Что: - пользователи и роли объявлены файлом с паролями из окружения. - межшардовые запросы передают пользователя через общий секрет. - четыре допущения реализации подтверждены в ADR живыми замерами. - Проверка: - make config-test, make smoke, make check-clickhouse, make check-services. --- .env.example | 5 +++ compose.yaml | 6 +++ docs/adr/0007-clickhouse-access.md | 22 ++++++---- infra/clickhouse/config.d/cluster.xml | 6 ++- infra/clickhouse/users.d/access.xml | 63 +++++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 10 deletions(-) create mode 100644 infra/clickhouse/users.d/access.xml diff --git a/.env.example b/.env.example index 0cb9c2d..55b193b 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,11 @@ CLICKHOUSE_01_HTTP_PORT=28123 CLICKHOUSE_01_TCP_PORT=29000 CLICKHOUSE_02_HTTP_PORT=28124 CLICKHOUSE_02_TCP_PORT=29001 +CLICKHOUSE_CLUSTER_SECRET=cluster-26-secret +CLICKHOUSE_DEFAULT_PASSWORD=default-26-local +CLICKHOUSE_ETL_PASSWORD=etl-26-writer +CLICKHOUSE_BI_PASSWORD=bi-26-reader +CLICKHOUSE_ANALYST_PASSWORD=analyst-26-reader KAFKA_EXTERNAL_PORT=29092 PROMETHEUS_PORT=29090 GRAFANA_PORT=23000 diff --git a/compose.yaml b/compose.yaml index 663cf94..4b7a748 100644 --- a/compose.yaml +++ b/compose.yaml @@ -11,6 +11,10 @@ x-clickhouse-common: &clickhouse-common environment: # Стенд локальный: нодам нужен общий пользователь без пароля для Distributed. CLICKHOUSE_SKIP_USER_SETUP: "1" + CLICKHOUSE_CLUSTER_SECRET: ${CLICKHOUSE_CLUSTER_SECRET:?Скопируйте .env.example в .env} + CLICKHOUSE_ETL_PASSWORD: ${CLICKHOUSE_ETL_PASSWORD:?Скопируйте .env.example в .env} + CLICKHOUSE_BI_PASSWORD: ${CLICKHOUSE_BI_PASSWORD:?Скопируйте .env.example в .env} + CLICKHOUSE_ANALYST_PASSWORD: ${CLICKHOUSE_ANALYST_PASSWORD:?Скопируйте .env.example в .env} depends_on: clickhouse-keeper: condition: service_healthy @@ -120,6 +124,7 @@ services: - ./infra/clickhouse/config.d/cluster.xml:/etc/clickhouse-server/config.d/cluster.xml:ro - ./infra/clickhouse/config.d/prometheus.xml:/etc/clickhouse-server/config.d/prometheus.xml:ro - ./infra/clickhouse/config.d/macros-01.xml:/etc/clickhouse-server/config.d/macros.xml:ro + - ./infra/clickhouse/users.d/access.xml:/etc/clickhouse-server/users.d/access.xml:ro - clickhouse_01_data:/var/lib/clickhouse # Будущее подключение Superset. @@ -133,6 +138,7 @@ services: - ./infra/clickhouse/config.d/cluster.xml:/etc/clickhouse-server/config.d/cluster.xml:ro - ./infra/clickhouse/config.d/prometheus.xml:/etc/clickhouse-server/config.d/prometheus.xml:ro - ./infra/clickhouse/config.d/macros-02.xml:/etc/clickhouse-server/config.d/macros.xml:ro + - ./infra/clickhouse/users.d/access.xml:/etc/clickhouse-server/users.d/access.xml:ro - clickhouse_02_data:/var/lib/clickhouse kafka: diff --git a/docs/adr/0007-clickhouse-access.md b/docs/adr/0007-clickhouse-access.md index d768b1c..151e116 100644 --- a/docs/adr/0007-clickhouse-access.md +++ b/docs/adr/0007-clickhouse-access.md @@ -132,12 +132,16 @@ Postgres и Grafana. Цена выбора известна: объявленн настройки ClickHouse. - Весь DDL репозитория идёт через `IF NOT EXISTS` (файлы `sql/ddl/`). -Осталось проверить при исполнении, и это работа тикета реализации: точная -форма объявления секрета в описании кластера (готового примера в -документации не нашлось); работает ли подстановка из окружения именно для -пароля пользователя, и если нет — пароли лягут в файл настройки прямым -текстом, а README назовёт это вслух; применяются ли на соседнем шарде права -`bi`, а не общей учётки (вывод из колонки `user`, живьём не измерен — -проверка требует создать пользователя); принимает ли ClickHouse права, -выданные на ещё не созданную базу — на момент решения `dds` и `dm` не -существуют. +Проверено на живом стенде 13 августа 2026 года, ClickHouse 26.3.17.56. + +- Секрет кластера принимается внутри `remote_servers` в виде + ``. Запрос от `etl` через обе + ноды выполнился на каждой под пользователем `etl`. +- `from_env` работает у элемента `password`: `etl`, `bi` и `analyst` вошли с + паролями из окружения и получили свои роли. +- При запросе от `bi` через Distributed на второй ноде `system.query_log` + показал `user = bi` и `initial_user = bi`. Значит, на соседнем шарде + применяются права инициатора, а не общей учётной записи. +- Права на отсутствующие базы `dds` и `dm` приняты. До создания баз они видны + в `SHOW GRANTS FOR etl_writer`; временная таблица в `dm` подтвердила право + `bi` на чтение после появления базы. diff --git a/infra/clickhouse/config.d/cluster.xml b/infra/clickhouse/config.d/cluster.xml index 3c32c5f..eddafeb 100644 --- a/infra/clickhouse/config.d/cluster.xml +++ b/infra/clickhouse/config.d/cluster.xml @@ -5,6 +5,11 @@ --> + + true @@ -33,4 +38,3 @@ /clickhouse/task_queue/ddl - diff --git a/infra/clickhouse/users.d/access.xml b/infra/clickhouse/users.d/access.xml new file mode 100644 index 0000000..b4fc45b --- /dev/null +++ b/infra/clickhouse/users.d/access.xml @@ -0,0 +1,63 @@ + + + + + + GRANT SELECT, INSERT, ALTER, CREATE TABLE, CREATE VIEW, DROP TABLE, DROP VIEW ON default.* + GRANT SELECT, INSERT, ALTER, CREATE TABLE, CREATE VIEW, DROP TABLE, DROP VIEW, CREATE DATABASE ON stg.* + GRANT SELECT, INSERT, ALTER, CREATE TABLE, CREATE VIEW, DROP TABLE, DROP VIEW, CREATE DATABASE ON ods.* + GRANT SELECT, INSERT, ALTER, CREATE TABLE, CREATE VIEW, DROP TABLE, DROP VIEW, CREATE DATABASE ON dds.* + GRANT SELECT, INSERT, ALTER, CREATE TABLE, CREATE VIEW, DROP TABLE, DROP VIEW, CREATE DATABASE ON dm.* + GRANT SELECT ON system.* + GRANT CLUSTER ON *.* + GRANT REMOTE ON *.* + + + + + GRANT SELECT ON dds.* + GRANT SELECT ON dm.* + GRANT SELECT ON system.settings + + + + + GRANT SELECT ON stg.* + GRANT SELECT ON ods.* + GRANT SELECT ON dds.* + GRANT SELECT ON dm.* + GRANT SELECT ON system.settings + + + + + + + + default + default + + GRANT etl_writer + + + + + default + default + + GRANT bi_reader + + + + + default + default + + GRANT analyst_reader + + + + From 62dc618252932933eb508cb8b6b678ec175c2831 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Thu, 13 Aug 2026 10:19:38 +0300 Subject: [PATCH 2/5] =?UTF-8?q?feat(clickhouse):=20=D0=BF=D1=80=D0=B8?= =?UTF-8?q?=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=B0?= =?UTF-8?q?=20=D1=80=D0=BE=D0=BB=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - DDL, Airflow и Superset не должны работать с правами default. - Что: - DDL и Airflow переведены на etl, включая явный доступ remote(). - Superset переведён на bi с паролем из окружения. - etl получил право KAFKA, которое ClickHouse 26.3 требует для движка. - Проверка: - make config-test, make lint, make up, make smoke, make check-services. --- compose.yaml | 5 +++ dags/test_clickhouse.py | 40 +++++++++++++++---- infra/airflow/init.sh | 3 +- infra/clickhouse/users.d/access.xml | 2 + .../superset/import/databases/clickhouse.yaml | 2 +- infra/superset/superset_config.py | 9 +++++ scripts/stand-services.sh | 6 ++- scripts/stand-smoke.sh | 7 +++- 8 files changed, 60 insertions(+), 14 deletions(-) diff --git a/compose.yaml b/compose.yaml index 4b7a748..f0ae5d4 100644 --- a/compose.yaml +++ b/compose.yaml @@ -48,6 +48,7 @@ x-airflow-common: &airflow-common AIRFLOW__SCHEDULER__ENABLE_HEALTH_CHECK: "true" AIRFLOW_ADMIN_USER: ${AIRFLOW_ADMIN_USER:?Скопируйте .env.example в .env} AIRFLOW_ADMIN_PASSWORD: ${AIRFLOW_ADMIN_PASSWORD:?Скопируйте .env.example в .env} + CLICKHOUSE_ETL_PASSWORD: ${CLICKHOUSE_ETL_PASSWORD:?Скопируйте .env.example в .env} volumes: - ./dags:/opt/airflow/dags:ro - ./infra/airflow/init.sh:/opt/airflow/init.sh:ro @@ -86,6 +87,7 @@ x-superset-common: &superset-common SUPERSET__SQLALCHEMY_DATABASE_URI: "postgresql+psycopg2://${SUPERSET_METADATA_USER:?Скопируйте .env.example в .env}:${SUPERSET_METADATA_PASSWORD:?Скопируйте .env.example в .env}@postgres-metadata:5432/superset" SUPERSET_ADMIN_USER: ${SUPERSET_ADMIN_USER:?Скопируйте .env.example в .env} SUPERSET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD:?Скопируйте .env.example в .env} + CLICKHOUSE_BI_PASSWORD: ${CLICKHOUSE_BI_PASSWORD:?Скопируйте .env.example в .env} volumes: - ./infra/superset/superset_config.py:/app/pythonpath/superset_config.py:ro - ./infra/superset/init.sh:/app/infra/init.sh:ro @@ -231,6 +233,7 @@ services: condition: service_healthy environment: LC_ALL: C + CLICKHOUSE_ETL_PASSWORD: ${CLICKHOUSE_ETL_PASSWORD:?Скопируйте .env.example в .env} entrypoint: ["/bin/bash", "-ec"] command: - | @@ -243,6 +246,8 @@ services: printf 'Применяем %s.\n' "$${ddl_file##*/}" clickhouse-client \ --host clickhouse-01 \ + --user etl \ + --password "$$CLICKHOUSE_ETL_PASSWORD" \ --multiquery \ --queries-file "$$ddl_file" done diff --git a/dags/test_clickhouse.py b/dags/test_clickhouse.py index da31590..8fb4f61 100644 --- a/dags/test_clickhouse.py +++ b/dags/test_clickhouse.py @@ -37,10 +37,24 @@ EXPECTED_TABLES = [ # подключение Airflow ходит по HTTP на 8123. NODES = ( ("ноде 1", "system.tables"), - ("ноде 2", "remote('clickhouse-02:9000', system.tables)"), + ( + "ноде 2", + """remote( + 'clickhouse-02:9000', 'system', 'tables', + {remote_user:String}, {remote_password:String} + )""", + ), ) +def _remote_parameters() -> dict[str, str]: + connection = Connection.get("clickhouse_default") + return { + "remote_user": connection.login, + "remote_password": connection.password, + } + + def _clickhouse_client(): # clickhouse_connect импортируется внутри функции, а не наверху файла: # обработчик DAG разбирает этот файл снова и снова, и импорт наверху @@ -52,8 +66,8 @@ def _clickhouse_client(): return clickhouse_connect.get_client( host=connection.host, port=connection.port, - username=connection.login or "default", - password=connection.password or "", + username=connection.login, + password=connection.password, database=connection.schema or "default", connect_timeout=5, send_receive_timeout=30, @@ -68,7 +82,8 @@ def _table_engines(client, source: str) -> list[tuple[str, str]]: WHERE database = 'default' AND name IN ('{LOCAL_TABLE}', '{DISTRIBUTED_TABLE}') ORDER BY name - """ + """, + parameters=_remote_parameters(), ) return result.result_rows @@ -173,8 +188,12 @@ def test_clickhouse(): node_2_rows = client.query( """ SELECT hostName() - FROM remote('clickhouse-02:9000', system.one) - """ + FROM remote( + 'clickhouse-02:9000', 'system', 'one', + {remote_user:String}, {remote_password:String} + ) + """, + parameters=_remote_parameters(), ).result_rows if len(node_2_rows) != 1: raise RuntimeError(f"не удалось определить имя ноды 2: {node_2_rows}") @@ -184,11 +203,16 @@ def test_clickhouse(): FROM remote( 'clickhouse-02:9000', 'default', - '{DISTRIBUTED_TABLE}' + '{DISTRIBUTED_TABLE}', + {{remote_user:String}}, + {{remote_password:String}} ) WHERE marker = {{marker:String}} """, - parameters={"marker": written["marker"]}, + parameters={ + "marker": written["marker"], + **_remote_parameters(), + }, ).result_rows if written["hostname"] == node_2_rows[0][0]: raise RuntimeError( diff --git a/infra/airflow/init.sh b/infra/airflow/init.sh index dbc5d1c..74ec46b 100755 --- a/infra/airflow/init.sh +++ b/infra/airflow/init.sh @@ -26,6 +26,7 @@ runuser -u airflow -- airflow connections add clickhouse_default \ --conn-type generic \ --conn-host clickhouse-01 \ --conn-port 8123 \ - --conn-login default \ + --conn-login etl \ + --conn-password "$CLICKHOUSE_ETL_PASSWORD" \ --conn-schema default \ --conn-description "ClickHouse, нода 1; типизированный провайдер появится на этапе ETL" diff --git a/infra/clickhouse/users.d/access.xml b/infra/clickhouse/users.d/access.xml index b4fc45b..dc45cff 100644 --- a/infra/clickhouse/users.d/access.xml +++ b/infra/clickhouse/users.d/access.xml @@ -14,6 +14,8 @@ GRANT SELECT ON system.* GRANT CLUSTER ON *.* GRANT REMOTE ON *.* + + GRANT KAFKA ON *.* diff --git a/infra/superset/import/databases/clickhouse.yaml b/infra/superset/import/databases/clickhouse.yaml index 4330e58..b52a4a3 100644 --- a/infra/superset/import/databases/clickhouse.yaml +++ b/infra/superset/import/databases/clickhouse.yaml @@ -1,5 +1,5 @@ database_name: ClickHouse -sqlalchemy_uri: clickhousedb://default@clickhouse-02:8123/default +sqlalchemy_uri: clickhousedb://bi@clickhouse-02:8123/default uuid: 4b8f2c6e-1d3a-4f5b-9c7d-2e8a1f0b3c5d cache_timeout: null expose_in_sqllab: true diff --git a/infra/superset/superset_config.py b/infra/superset/superset_config.py index 5ae7413..1ff180f 100644 --- a/infra/superset/superset_config.py +++ b/infra/superset/superset_config.py @@ -5,6 +5,15 @@ import os SQLALCHEMY_DATABASE_URI = os.environ["SUPERSET__SQLALCHEMY_DATABASE_URI"] SECRET_KEY = os.environ["SUPERSET_SECRET_KEY"] + +# Источник данных пока один, поэтому штатному хранилищу паролей Superset +# достаточно вернуть один секрет из окружения. +def clickhouse_password(_url): + return os.environ["CLICKHOUSE_BI_PASSWORD"] + + +SQLALCHEMY_CUSTOM_PASSWORD_STORE = clickhouse_password + SESSION_COOKIE_NAME = "superset_session" SESSION_COOKIE_SECURE = False SESSION_COOKIE_HTTPONLY = True diff --git a/scripts/stand-services.sh b/scripts/stand-services.sh index 9d701d4..205f627 100755 --- a/scripts/stand-services.sh +++ b/scripts/stand-services.sh @@ -237,6 +237,7 @@ check_airflow_probes() { } check_superset() { + local clickhouse_password local config local login local metadata_tables @@ -253,6 +254,7 @@ check_superset() { config="$(compose config --format json 2>/dev/null || true)" user="$(jq -r '.services.superset.environment.SUPERSET_ADMIN_USER // empty' <<<"$config")" password="$(jq -r '.services.superset.environment.SUPERSET_ADMIN_PASSWORD // empty' <<<"$config")" + clickhouse_password="$(jq -r '.services.superset.environment.CLICKHOUSE_BI_PASSWORD // empty' <<<"$config")" superset_user="$(jq -r '.services["postgres-metadata"].environment.SUPERSET_METADATA_USER // empty' <<<"$config")" superset_password="$(jq -r '.services["postgres-metadata"].environment.SUPERSET_METADATA_PASSWORD // empty' <<<"$config")" port="$(published_port superset 8088)" @@ -284,10 +286,10 @@ check_superset() { if [[ "$metadata_tables" == '1' ]] && \ [[ "$metadata_engine" == "postgresql|${superset_user}|postgres-metadata|5432|superset" ]] && \ [[ "$stored_uuid" == '4b8f2c6e-1d3a-4f5b-9c7d-2e8a1f0b3c5d' ]] && \ - [[ "$stored_uri" == 'clickhousedb://default@clickhouse-02:8123/default' ]]; then + [[ "$stored_uri" == "clickhousedb://bi:${clickhouse_password}@clickhouse-02:8123/default" ]]; then pass 'метаданные Superset живут в Postgres, подготовленное подключение указывает на clickhouse-02' else - fail "Superset не подтвердил Postgres и подготовленное подключение к ноде 2: таблицы=${metadata_tables:-нет}, движок=${metadata_engine:-нет}, UUID=${stored_uuid:-нет}, URI=${stored_uri:-нет}" + fail "Superset не подтвердил Postgres и подготовленное подключение bi к ноде 2: таблицы=${metadata_tables:-нет}, движок=${metadata_engine:-нет}, UUID=${stored_uuid:-нет}" return fi diff --git a/scripts/stand-smoke.sh b/scripts/stand-smoke.sh index e6c9595..a29239b 100755 --- a/scripts/stand-smoke.sh +++ b/scripts/stand-smoke.sh @@ -154,6 +154,7 @@ check_grafana_datasource() { check_airflow() { local airflow_port local airflow_token + local clickhouse_password local config local connection local dag @@ -166,6 +167,7 @@ check_airflow() { config="$(compose config --format json 2>/dev/null || true)" user="$(jq -r '.services["airflow-apiserver"].environment.AIRFLOW_ADMIN_USER // empty' <<<"$config")" password="$(jq -r '.services["airflow-apiserver"].environment.AIRFLOW_ADMIN_PASSWORD // empty' <<<"$config")" + clickhouse_password="$(jq -r '.services["airflow-scheduler"].environment.CLICKHOUSE_ETL_PASSWORD // empty' <<<"$config")" airflow_port="$(published_port airflow-apiserver 8080)" health="$(curl -sf --max-time 10 \ @@ -211,12 +213,13 @@ check_airflow() { .connection_id == "clickhouse_default" and .host == "clickhouse-01" and .port == 8123 and - .login == "default" and + .login == "etl" and .schema == "default" ' >/dev/null 2>&1 <<<"$connection" && \ timeout 20s "${COMPOSE_CMD[@]}" --project-directory "$ROOT_DIR" \ exec -T airflow-scheduler \ - curl -sf 'http://clickhouse-01:8123/?query=SELECT%201' \ + curl -sf -u "etl:${clickhouse_password}" \ + 'http://clickhouse-01:8123/?query=SELECT%201' \ 2>/dev/null | grep -qx '1'; then pass 'подготовленное подключение Airflow указывает на clickhouse-01, нода доступна из контейнера' else From 780b2d0ef98846d03f64f3d1890ae6963e994142 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Thu, 13 Aug 2026 10:23:32 +0300 Subject: [PATCH 3/5] =?UTF-8?q?feat(clickhouse):=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20=D0=BF=D0=B0=D1=80=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=20=D1=81=D0=BB=D1=83=D0=B6=D0=B5=D0=B1=D0=BD=D0=BE=D0=BC?= =?UTF-8?q?=D1=83=20default?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - default должен остаться только учёткой локальных служебных вызовов. - Что: - пароль default подставлен из окружения на обеих нодах. - healthcheck и служебные скрипты передают его внутри контейнера. - Проверка: - make config-test, make up, make smoke, make check-clickhouse, make check-services. --- compose.yaml | 3 ++- infra/clickhouse/users.d/access.xml | 3 +++ scripts/check-clickhouse.sh | 14 ++++++++++++-- scripts/wait-for-world.sh | 7 ++++++- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/compose.yaml b/compose.yaml index f0ae5d4..1f4ff61 100644 --- a/compose.yaml +++ b/compose.yaml @@ -12,6 +12,7 @@ x-clickhouse-common: &clickhouse-common # Стенд локальный: нодам нужен общий пользователь без пароля для Distributed. CLICKHOUSE_SKIP_USER_SETUP: "1" CLICKHOUSE_CLUSTER_SECRET: ${CLICKHOUSE_CLUSTER_SECRET:?Скопируйте .env.example в .env} + CLICKHOUSE_DEFAULT_PASSWORD: ${CLICKHOUSE_DEFAULT_PASSWORD:?Скопируйте .env.example в .env} CLICKHOUSE_ETL_PASSWORD: ${CLICKHOUSE_ETL_PASSWORD:?Скопируйте .env.example в .env} CLICKHOUSE_BI_PASSWORD: ${CLICKHOUSE_BI_PASSWORD:?Скопируйте .env.example в .env} CLICKHOUSE_ANALYST_PASSWORD: ${CLICKHOUSE_ANALYST_PASSWORD:?Скопируйте .env.example в .env} @@ -22,7 +23,7 @@ x-clickhouse-common: &clickhouse-common # Гигабайта не хватало: половину съедали страницы самого бинарника (ADR 0004). mem_limit: 4g healthcheck: - test: ["CMD-SHELL", "clickhouse-client --host 127.0.0.1 --query 'SELECT 1' >/dev/null 2>&1"] + test: ["CMD-SHELL", "clickhouse-client --host 127.0.0.1 --user default --password \"$$CLICKHOUSE_DEFAULT_PASSWORD\" --query 'SELECT 1' >/dev/null 2>&1"] interval: 5s timeout: 3s retries: 30 diff --git a/infra/clickhouse/users.d/access.xml b/infra/clickhouse/users.d/access.xml index dc45cff..caa8f6c 100644 --- a/infra/clickhouse/users.d/access.xml +++ b/infra/clickhouse/users.d/access.xml @@ -37,6 +37,9 @@ + + + default diff --git a/scripts/check-clickhouse.sh b/scripts/check-clickhouse.sh index 51423f3..e0c895a 100755 --- a/scripts/check-clickhouse.sh +++ b/scripts/check-clickhouse.sh @@ -18,7 +18,12 @@ compose() { query() { local service="$1" local sql="$2" - compose exec -T "$service" clickhouse-client --query "$sql" Date: Thu, 13 Aug 2026 10:26:35 +0300 Subject: [PATCH 4/5] =?UTF-8?q?docs(clickhouse):=20=D0=BE=D0=BF=D0=B8?= =?UTF-8?q?=D1=81=D0=B0=D0=BD=D0=B0=20=D1=83=D1=87=D0=B5=D0=B1=D0=BD=D0=B0?= =?UTF-8?q?=D1=8F=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D1=8C=20=D0=B4=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D1=83=D0=BF=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - читателю нужна честная граница между учебными ролями и боевой защитой. - Что: - README описывает пользователей, роли и место хранения паролей. - мастер-спека явно откладывает эксплуатационные меры защиты. - устаревшие комментарии Compose и пробника приведены к реализации. - Проверка: - make config-test, make lint. --- README.md | 28 +++++++++++++++-------- compose.yaml | 6 ++--- dags/test_clickhouse.py | 10 ++++---- docs/specs/2026-07-30-stand-v2-realism.md | 2 ++ 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index a9e8475..b32e471 100644 --- a/README.md +++ b/README.md @@ -242,16 +242,26 @@ uv run --project generator python -m clickstream_generator batch \ - Prometheus — `http://127.0.0.1:29090`; - Grafana — `http://127.0.0.1:23000`, пользователь `admin`, пароль `admin`. -У локального учебного кластера нет пароля: ноды используют общего пользователя -`default` для запросов `Distributed`. Порты поэтому привязаны к `127.0.0.1` и -не открыты во внешнюю сеть. +В ClickHouse четыре пользователя: -Пароли интерфейсов, пароли Postgres, ключи Airflow и Superset, отсутствие -пароля ClickHouse и отсутствие проверки доступа у Kafka и Prometheus — -намеренно простые и явно ненастоящие настройки локального учебного стенда. Это -не пример настройки защиты: не копируйте значения из `.env.example` в рабочую -среду. Все опубликованные порты привязаны только к `127.0.0.1`; Postgres наружу -не опубликован. +- `etl` применяет DDL и подключает Airflow; роль `etl_writer` читает и пишет + слои хранилища; +- `bi` подключает Superset; роль `bi_reader` читает будущие слои DDS и DM; +- `analyst` предназначен для подключения человека и читает все слои; +- `default` остаётся служебным: им ходят проверки здоровья и скрипты внутри + контейнеров, но не приложения. + +Пользователи и роли объявлены в `infra/clickhouse/users.d/access.xml`. Пароли +и общий секрет нод живут в `.env` и передаются в конфигурацию через окружение; +Superset получает пароль `bi` тем же путём через штатную функцию настройки. +Значения для локального стенда есть в `.env.example`. + +Пароли ClickHouse и интерфейсов, пароли Postgres, ключи Airflow и Superset, +отсутствие проверки доступа у Kafka и Prometheus — намеренно простые и явно +ненастоящие настройки локального учебного стенда. Это не пример настройки +защиты: не копируйте значения из `.env.example` в рабочую среду. Все +опубликованные порты привязаны только к `127.0.0.1`; Postgres наружу не +опубликован. В бою перед репликами ClickHouse обычно был бы балансировщик. Здесь в каждом шарде одна реплика, поэтому балансировать нечего. Балансировщик и топология diff --git a/compose.yaml b/compose.yaml index 1f4ff61..10e79f2 100644 --- a/compose.yaml +++ b/compose.yaml @@ -9,7 +9,7 @@ x-clickhouse-common: &clickhouse-common image: clickhouse/clickhouse-server:26.3.17.56 restart: unless-stopped environment: - # Стенд локальный: нодам нужен общий пользователь без пароля для Distributed. + # Пользователей объявляет access.xml; настройка учётки образом не нужна. CLICKHOUSE_SKIP_USER_SETUP: "1" CLICKHOUSE_CLUSTER_SECRET: ${CLICKHOUSE_CLUSTER_SECRET:?Скопируйте .env.example в .env} CLICKHOUSE_DEFAULT_PASSWORD: ${CLICKHOUSE_DEFAULT_PASSWORD:?Скопируйте .env.example в .env} @@ -116,7 +116,7 @@ services: retries: 30 start_period: 10s - # Инициатор DDL и будущее подключение Airflow. + # Инициатор DDL и точка подключения Airflow. clickhouse-01: <<: *clickhouse-common hostname: clickhouse-01 @@ -130,7 +130,7 @@ services: - ./infra/clickhouse/users.d/access.xml:/etc/clickhouse-server/users.d/access.xml:ro - clickhouse_01_data:/var/lib/clickhouse - # Будущее подключение Superset. + # Точка подключения Superset. clickhouse-02: <<: *clickhouse-common hostname: clickhouse-02 diff --git a/dags/test_clickhouse.py b/dags/test_clickhouse.py index 8fb4f61..ab05aa9 100644 --- a/dags/test_clickhouse.py +++ b/dags/test_clickhouse.py @@ -29,12 +29,10 @@ EXPECTED_TABLES = [ (LOCAL_TABLE, "ReplicatedMergeTree"), ] -# Ноду 2 пробник читает не своим подключением, а запросом remote() с ноды 1: у -# Airflow подготовлено одно подключение — к clickhouse-01, и второго ради -# пробника не заводят. При этом remote('clickhouse-02:9000', ...) делает -# инициатором распределённого запроса саму ноду 2 — проверяется именно это, а -# не доступность ноды 2 по сети. Порт 9000 — межсерверный, тогда как -# подключение Airflow ходит по HTTP на 8123. +# У Airflow одно подключение — к ноде 1; второго ради пробника не заводят. +# Ноду 2 он читает через remote(), который не использует секрет из описания +# кластера, поэтому учётные данные etl передаются явно. Порт 9000 — нативный, +# тогда как подключение Airflow ходит по HTTP на 8123. NODES = ( ("ноде 1", "system.tables"), ( diff --git a/docs/specs/2026-07-30-stand-v2-realism.md b/docs/specs/2026-07-30-stand-v2-realism.md index 4546e80..cf79609 100644 --- a/docs/specs/2026-07-30-stand-v2-realism.md +++ b/docs/specs/2026-07-30-stand-v2-realism.md @@ -576,6 +576,8 @@ v2 стартует пустым, поэтому объём ниже — это стенд работает, но учебного пути на нём ещё нет. - Инкрементальный ETL (#8) — свой issue. - Реплики (2×2), HAProxy, репликационная эксплуатация — в лекцию, не в стенд. +- TLS между нодами, настоящее хранилище и ротация секретов, внешние учётные + записи, квоты, политики строк и аудит запросов — за границей учебного стенда. - Полный словарь торговых событий Метрики (detail, remove, impressions), пять уровней категорий, блоки `purchasedProduct*`/`impressions*`. - Механика `Sign`/CollapsingMergeTree — кандидат на потом (дом — поток From d0f15bea020035364720d55f671b44910032bc94 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Thu, 13 Aug 2026 11:31:01 +0300 Subject: [PATCH 5/5] =?UTF-8?q?fix(clickhouse):=20=D1=83=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=B5=D0=BD=D1=8B=20=D0=B7=D0=B0=D0=BC=D0=B5=D1=87?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8F=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20?= =?UTF-8?q?=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8=20=D0=B4=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D1=83=D0=BF=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - документация и конфигурация доступа должны говорить только подтверждённое. - Что: - ADR приведён к результату межшардового замера. - объяснены служебный грант и учебный компромисс remote(). - удалены избыточные профили, README связан с ADR. - Проверка: - make config-test, make lint, make smoke, make check-clickhouse, make check-services. --- README.md | 5 +++-- dags/test_clickhouse.py | 3 ++- docs/adr/0007-clickhouse-access.md | 8 +++----- infra/clickhouse/users.d/access.xml | 4 +--- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index b32e471..fe91b6b 100644 --- a/README.md +++ b/README.md @@ -251,8 +251,9 @@ uv run --project generator python -m clickstream_generator batch \ - `default` остаётся служебным: им ходят проверки здоровья и скрипты внутри контейнеров, но не приложения. -Пользователи и роли объявлены в `infra/clickhouse/users.d/access.xml`. Пароли -и общий секрет нод живут в `.env` и передаются в конфигурацию через окружение; +Решение и его доводы — в [ADR 0007](docs/adr/0007-clickhouse-access.md). +Пользователи и роли объявлены в `infra/clickhouse/users.d/access.xml`. Пароли и +общий секрет нод живут в `.env` и передаются в конфигурацию через окружение; Superset получает пароль `bi` тем же путём через штатную функцию настройки. Значения для локального стенда есть в `.env.example`. diff --git a/dags/test_clickhouse.py b/dags/test_clickhouse.py index ab05aa9..91866ff 100644 --- a/dags/test_clickhouse.py +++ b/dags/test_clickhouse.py @@ -32,7 +32,8 @@ EXPECTED_TABLES = [ # У Airflow одно подключение — к ноде 1; второго ради пробника не заводят. # Ноду 2 он читает через remote(), который не использует секрет из описания # кластера, поэтому учётные данные etl передаются явно. Порт 9000 — нативный, -# тогда как подключение Airflow ходит по HTTP на 8123. +# тогда как подключение Airflow ходит по HTTP на 8123. Trace-журнал сервера +# видит пароль: это допустимо только для локального учебного стенда. NODES = ( ("ноде 1", "system.tables"), ( diff --git a/docs/adr/0007-clickhouse-access.md b/docs/adr/0007-clickhouse-access.md index 151e116..c350832 100644 --- a/docs/adr/0007-clickhouse-access.md +++ b/docs/adr/0007-clickhouse-access.md @@ -63,11 +63,9 @@ DDS под ними, потому что так чаще всего и быва данными по репликам, и там пароль вписан в каждую реплику каждого из трёх кластеров. -Довод опирается на измеренное лишь наполовину: колонку `user` видно на живом -стенде, а вот что права `bi` на соседнем шарде подменяются правами общей -учётки — вывод из неё, и он ждёт измерения (см. конец документа). Окажись -вывод неверным, выбор секрета устоит по остальным основаниям, но перестанет -быть единственно возможным. +Довод подтверждён замером 13 августа 2026 года: на соседнем шарде сохранился +пользователь-инициатор, поэтому там применились его права. Результат замера — +в разделе «Что проверено». **Файлы, а не SQL.** Объявленное файлом не оставляет состояния в томах: повторный `make up` на живом стенде ничего не сдвигает, правка пароля diff --git a/infra/clickhouse/users.d/access.xml b/infra/clickhouse/users.d/access.xml index caa8f6c..c66c711 100644 --- a/infra/clickhouse/users.d/access.xml +++ b/infra/clickhouse/users.d/access.xml @@ -18,6 +18,7 @@ GRANT KAFKA ON *.* + GRANT SELECT ON dds.* @@ -42,7 +43,6 @@ - default default GRANT etl_writer @@ -50,7 +50,6 @@ - default default GRANT bi_reader @@ -58,7 +57,6 @@ - default default GRANT analyst_reader