Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <role>;
GRANT SELECT ON system.data_skipping_indices TO <role>;
GRANT SELECT ON system.parts TO <role>;
```

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**
Expand Down
5 changes: 3 additions & 2 deletions src/net/ch-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions tests/unit/ch-client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Loading