Skip to content

Let the ClickHouse connector work with a token that cannot read the system database - #1344

Open
adamparrott-ls wants to merge 3 commits into
getnao:mainfrom
adamparrott-ls:clickhouse-least-privilege-token
Open

Let the ClickHouse connector work with a token that cannot read the system database#1344
adamparrott-ls wants to merge 3 commits into
getnao:mainfrom
adamparrott-ls:clickhouse-least-privilege-token

Conversation

@adamparrott-ls

@adamparrott-ls adamparrott-ls commented Aug 4, 2026

Copy link
Copy Markdown

Closes #1319.

Tinybird serves the system database to ADMIN tokens only, so a token scoped to a few tables
can't be used with nao at all. system.settings was just the first wall — system.databases
and system.tables are blocked too, so fixing the connect-time read alone only moves the
failure one step.

The practical effect is that the only credential that can connect is a workspace admin token, meaning that if a guardrail was not fully realised, then a create, update, or delete statement would be actioned on a production database.

This adds one opt-in field, tolerate_unreadable_system_tables (default false), which does
three things when the system database is unreadable:

  1. Connect survives an unreadable system.settings. clickhouse-connect reads it inside
    Client.__init__ to validate query settings, so there's no client to configure beforehand —
    the query method is wrapped for the length of the connection, matching that one query and
    nothing else. A query you write against system.settings still fails as it should.
  2. Discovery falls back to the tables named outright in include, via a thin wrapper around
    the Ibis backend. No change to the shared sync path, so no other backend is affected.
    Wildcards can't be expanded without a listing, so restricted-token users must name each
    table; any pattern that gets skipped is logged rather than silently dropped.
  3. Column metadata falls back to DESCRIBE TABLE, which returns the same name, type,
    comment and default fields as system.columns and needs no system access.

Nothing changes with the flag off, and nothing is lost with it on: the fallbacks are only
reached when the underlying call actually fails, so a server that serves the system database
still gets full listings and system.columns metadata.

What a Tinybird token actually needs

This is the second half of #1319 — the minimum-privilege set, established by testing rather than
by reading docs. With the field enabled, nao needs only:

  • SELECT on each datasource named in include
  • DESCRIBE TABLE on those datasources (column names, types, comments, defaults)

No system-database access at all. SELECT version(), timezone() is permitted to any token.
system.projections doesn't exist on Tinybird, and every other system read nao attempts already
degrades to None, costing only the table comment and the engine/index metadata.

Why not just scope the token instead

Tinybird's error suggests adding the resource to a token with DATASOURCES:READ. That doesn't
work for system.settings — it can't be granted to a non-ADMIN token, so an ADMIN token
belonging to a named individual is currently the only credential that can connect. That's the
blast radius the issue is about.

Testing

Against a real Tinybird workspace with a read-only token, and a local ClickHouse for regression:

  • nao sync from the CLI: 15/15 tables across 2 datasets, no errors
  • All four templates generated — columns, preview, profiling, ai_summary. Profiling
    produced real per-column stats (min/max/distinct/nulls/stddev, top values for
    LowCardinality) over a 242k-row table
  • /execute_sql on the FastAPI sidecar (the chat path): real aggregates returned, dialect
    detected as clickhouse
  • ClickHouse sync integration suite: identical before and after — 23 passed, 3 skipped — on
    clickhouse-connect 1.6.0 and 0.14.1
  • Full cli unit suite: same 25 pre-existing env-related failures before and after, +23 new
    tests passing
  • New unit tests pass on Python 3.13 and 3.12, and without the clickhouse extra installed
  • ruff check, ruff check --select I, ruff format --check clean apart from pre-existing
    findings

Notes for review

  • The connect-time wrapper is a workaround for a third-party constraint. clickhouse-connect
    already tolerates the adjacent client_protocol_version probe failing
    (_backend/orchestration.py); the same treatment for the settings probe would remove the need
    for that piece. Happy to send that upstream separately — parts 2 and 3 belong here regardless.
  • ClickHouse isn't in DATABASE_CONFIGS in scripts/generate-config-docs.py (nor MySQL, Fabric
    or StarRocks), so no ClickHouse option appears in the generated config reference and this new
    one won't either. Left alone here since it's pre-existing and unrelated — happy to raise it
    separately if useful.
  • One behaviour change worth knowing: with a live listing, an include entry naming a table that
    doesn't exist is silently filtered out. When discovery falls back to include, the name is
    trusted, so it surfaces as a logged per-table error and a columns.md containing that error
    instead. Louder rather than silent, which seems right when include is the source of truth,
    but it is a difference.
  • Two new BLE001 blind-excepts, consistent with the 13 already in this file. ty wants
    setattr where ruff's B010 wants direct assignment; I went with ruff, since that's what CI
    runs.

This PR was written using Claude Opus 5 (claude-opus-5).

Tinybird serves the system database to ADMIN tokens only, so a token scoped
to a few tables cannot be used with nao at all. Behind a new opt-in
tolerate_unreadable_system_tables field:

- connection survives an unreadable system.settings, which clickhouse-connect
  reads while constructing the client
- schema and table discovery fall back to the tables named outright in
  include, via a wrapper around the Ibis backend
- column metadata falls back to DESCRIBE TABLE, which needs no system access

Nothing changes with the field off, and the fallbacks are only reached when
the underlying call fails, so a server that serves the system database keeps
full listings and system.columns metadata.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread cli/nao_core/config/databases/clickhouse.py
Comment thread cli/nao_core/config/databases/clickhouse.py
Comment thread cli/nao_core/config/databases/clickhouse.py Outdated
Comment thread cli/nao_core/config/databases/clickhouse.py Outdated
- warn about unexpandable include patterns from list_databases too, so an
  include made only of wildcards reports the patterns it is dropping instead
  of syncing zero schemas silently
- escape backticks when quoting identifiers for DESCRIBE TABLE
- gate the DESCRIBE fallback on tolerate_unreadable_system_tables, so an
  empty system.columns result stays empty on the default path
- hold the server settings probe lock when connecting without the opt-in, so
  a concurrent opt-in connection's patch cannot leak into it

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@adamparrott-ls

Copy link
Copy Markdown
Author

Cubic review validated, changes pushed.

@adamparrott-ls

Copy link
Copy Markdown
Author

@Bl3f we'd still be keen to get this merged in to reduce the blast radius on access to our warehouse. Please let me know if there's anything I can do to help get this moved forwards.

@Bl3f

Bl3f commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

@adamparrott-ls hello Adam, sorry for being a bit slow. I'll try to merge this asap, the thing i need to understand is what's the impact for people using already Clickhouse? I'm not sure while reading the PR that add a significant amount of code for it.

@adamparrott-ls

adamparrott-ls commented Aug 28, 2026 via email

Copy link
Copy Markdown
Author

ty rejects assigning the wrapper over HttpClient.query as an implicit
shadowing of the declared method, which failed the CLI Lint job.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ClickHouse/Tinybird connections require an ADMIN token because system.settings is read on connect

2 participants