diff --git a/README.md b/README.md index 5f73c21..2528736 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,35 @@ plus light regex on `create_table_query` (`TO` target) and `engine_full` empty). Graph math is pure in `src/core/schema-graph.js` (100%-covered); the SVG is the same dagre-laid-out renderer the pipeline graph uses. +### Required grants + +Every introspection read is **best-effort**: a denied or missing `system.*` table +degrades the affected layer instead of failing the graph, so the lineage view works +even for low-privilege users. The graph draws with **no extra grants** — the implicit +`SELECT` that `SHOW TABLES` / `SHOW COLUMNS` give over `system.tables` / +`system.columns` is enough (and those rows are already filtered to the databases the +user can otherwise access). What you grant only buys *fidelity*: + +| To get… | the role needs | if denied (default) | +|---|---|---| +| the graph itself + node cards | `SHOW TABLES`, `SHOW COLUMNS` (→ implicit `SELECT ON system.tables` / `system.columns`) | required — without these there's nothing to draw | +| dictionary (`dict`) lineage edges | `SELECT ON system.dictionaries` | no dictionary edges; the rest of the graph still draws | +| skip-index badges on the rich cards | `SELECT ON system.data_skipping_indices` | cards show the engine/rows/bytes header without the skip line | +| per-partition rows in the node detail pane | `SELECT ON system.parts` | detail pane shows columns/keys/DDL but no partition breakdown | + +So for full, **no-degrade** schema mode, grant the three optional `SELECT`s above to +the role your users log in as, e.g.: + +```sql +GRANT SELECT ON system.dictionaries TO ; +GRANT SELECT ON system.data_skipping_indices TO ; +GRANT SELECT ON system.parts TO ; +``` + +These are metadata-only and stay row-filtered to the databases the role can already +read; DDL secrets remain masked unless the role separately holds +`displaySecretsInShowAndSelect`. + ## Saved queries & the Library Queries you save (★ **Save** next to Run, or `⌘S`) land in the sidebar **★ Library** diff --git a/src/net/ch-client.js b/src/net/ch-client.js index 2bdf12a..00a9c3d 100644 --- a/src/net/ch-client.js +++ b/src/net/ch-client.js @@ -149,8 +149,9 @@ export async function loadSchemaLineage(ctx, focus) { + 'partition_key, sorting_key, primary_key, sampling_key'; const tablesJson = await queryJson(ctx, `SELECT ${cols} FROM system.tables WHERE database = ${sqlString(db)} ORDER BY name`); const tables = tablesJson.data || []; - const dictsJson = await queryJson(ctx, `SELECT database, name, source FROM system.dictionaries WHERE database = ${sqlString(db)}`); - const dictionaries = dictsJson.data || []; + // Best-effort: a denied/missing system.dictionaries (low-priv users lack + // SELECT on it) must degrade to no dictionary edges, never abort the graph. + const dictionaries = await tryQueryData(ctx, `SELECT database, name, source FROM system.dictionaries WHERE database = ${sqlString(db)}`) || []; // Robust source extraction for views/MVs: let ClickHouse parse the SELECT. await Promise.all(tables.map(async (t) => { if (!t.as_select || (t.engine !== 'View' && t.engine !== 'MaterializedView')) return; diff --git a/tests/unit/ch-client.test.js b/tests/unit/ch-client.test.js index b12291c..d3ec742 100644 --- a/tests/unit/ch-client.test.js +++ b/tests/unit/ch-client.test.js @@ -359,6 +359,17 @@ describe('loadSchemaLineage', () => { const out = await loadSchemaLineage(ctx, { kind: 'db', db: 'lin' }); expect(out.tables[0].astTables).toBeUndefined(); }); + it('tolerates a denied system.dictionaries (degrades to no dictionary edges, graph still loads)', async () => { + const ctx = ctxWith((url, init) => { + const sql = init.body; + // Low-priv users (e.g. the demo role) lack SELECT on system.dictionaries. + if (/system\.dictionaries/.test(sql)) return jsonResp('DB::Exception: demo: Not enough privileges. ... grant SELECT ON system.dictionaries. (ACCESS_DENIED)', false, 500); + return jsonResp({ data: [{ database: 'lin', name: 'events', engine: 'MergeTree', as_select: '' }] }); + }); + const out = await loadSchemaLineage(ctx, { kind: 'db', db: 'lin' }); + expect(out.tables).toHaveLength(1); + expect(out.dictionaries).toEqual([]); + }); it('includes the card metadata columns in the scoped tables query', async () => { const seen = []; const ctx = ctxWith((url, init) => {