@intentius/chant-lexicon-sql 0.124.54 → 0.124.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capability-plugin.d.ts +160 -0
- package/dist/capability-plugin.d.ts.map +1 -0
- package/dist/capability-plugin.js +370 -0
- package/dist/change-set.d.ts +35 -0
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +43 -0
- package/dist/ci-setup.d.ts +34 -0
- package/dist/ci-setup.d.ts.map +1 -0
- package/dist/ci-setup.js +106 -0
- package/dist/clickhouse/access.d.ts +20 -0
- package/dist/clickhouse/access.d.ts.map +1 -1
- package/dist/clickhouse/access.js +42 -1
- package/dist/clickhouse/live/describe-resources.d.ts +3 -1
- package/dist/clickhouse/live/describe-resources.d.ts.map +1 -1
- package/dist/clickhouse/live/describe-resources.js +31 -3
- package/dist/clickhouse/plan/deep.d.ts +42 -11
- package/dist/clickhouse/plan/deep.d.ts.map +1 -1
- package/dist/clickhouse/plan/deep.js +104 -33
- package/dist/exit-codes.d.ts +69 -0
- package/dist/exit-codes.d.ts.map +1 -0
- package/dist/exit-codes.js +105 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/integrity.json +2 -2
- package/dist/lock-grain.d.ts +27 -0
- package/dist/lock-grain.d.ts.map +1 -0
- package/dist/lock-grain.js +13 -0
- package/dist/manifest.json +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +8 -0
- package/dist/postgres/testing/migration.d.ts +2 -0
- package/dist/postgres/testing/migration.d.ts.map +1 -1
- package/dist/token-source.d.ts +28 -0
- package/dist/token-source.d.ts.map +1 -1
- package/dist/token-source.js +70 -1
- package/dist/yodel-run.d.ts +58 -0
- package/dist/yodel-run.d.ts.map +1 -0
- package/dist/yodel-run.js +145 -0
- package/package.json +2 -2
- package/src/capability-plugin.test.ts +317 -0
- package/src/capability-plugin.ts +515 -0
- package/src/change-set.ts +74 -0
- package/src/ci-setup.test.ts +67 -0
- package/src/ci-setup.ts +116 -0
- package/src/clickhouse/access.ts +49 -2
- package/src/clickhouse/live/access.e2e.test.ts +8 -7
- package/src/clickhouse/live/describe-resources.ts +27 -3
- package/src/clickhouse/plan/deep-grants-drift.test.ts +151 -0
- package/src/clickhouse/plan/deep-grants.test.ts +10 -5
- package/src/clickhouse/plan/deep.ts +103 -32
- package/src/exit-codes.test.ts +46 -0
- package/src/exit-codes.ts +140 -0
- package/src/index.ts +28 -0
- package/src/lock-grain.test.ts +57 -0
- package/src/lock-grain.ts +37 -0
- package/src/plugin.ts +9 -0
- package/src/testdata/fake-yodel.mjs +58 -0
- package/src/token-source-traded.test.ts +75 -0
- package/src/token-source.ts +74 -1
- package/src/yodel-run.ts +175 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { CiService } from "@intentius/chant/components/ci-services";
|
|
2
|
+
export type SqlCiDialect = "clickhouse" | "postgres";
|
|
3
|
+
/** The profile the replay check runs on, as sql-yodeler names it. */
|
|
4
|
+
export declare const REPLAY_PROFILE = "replay";
|
|
5
|
+
/** The oldest Node the lexicon and chant run on. */
|
|
6
|
+
export declare const SQL_CI_NODE_MAJOR = 22;
|
|
7
|
+
/** Fails the job, naming the version, when the image's Node is older than {@link SQL_CI_NODE_MAJOR}. */
|
|
8
|
+
export declare const NODE_CHECK_LINE: string;
|
|
9
|
+
/**
|
|
10
|
+
* The project's packages, unless the lexicon is already installed (an image
|
|
11
|
+
* that carries them pays nothing), and their bins on PATH: exported for the
|
|
12
|
+
* rest of a GitLab script, appended to `$GITHUB_PATH` for a GitHub or
|
|
13
|
+
* Forgejo job's later steps.
|
|
14
|
+
*/
|
|
15
|
+
export declare const PACKAGES_INSTALL_LINE = "{ [ -d node_modules/@intentius/chant-lexicon-sql ] || npm ci; } && export PATH=\"$PWD/node_modules/.bin:$PATH\" && if [ -n \"${GITHUB_PATH:-}\" ]; then echo \"$PWD/node_modules/.bin\" >> \"$GITHUB_PATH\"; fi";
|
|
16
|
+
/** The replay server of each dialect: the emulator's pinned image, under a prefixed name. */
|
|
17
|
+
export declare function replayService(dialect: SqlCiDialect): CiService;
|
|
18
|
+
/**
|
|
19
|
+
* The dialects the config names (`dialect: "postgres"`, or a list), in
|
|
20
|
+
* {@link SqlCiDialect} order; ClickHouse, the lexicon's default, when it
|
|
21
|
+
* names none.
|
|
22
|
+
*/
|
|
23
|
+
export declare function sqlDialectsOf(text: string): SqlCiDialect[];
|
|
24
|
+
/** Whether the config declares the `replay` profile, the one the replay check runs on. */
|
|
25
|
+
export declare function declaresReplay(text: string): boolean;
|
|
26
|
+
/** The setup lines for the project at `dir`: none when it has no config naming the sql lexicon's settings. */
|
|
27
|
+
export declare function sqlCiSetup(ctx: {
|
|
28
|
+
dir: string;
|
|
29
|
+
}): string[];
|
|
30
|
+
/** The replay service per dialect for the project at `dir`, when its config declares the `replay` profile. */
|
|
31
|
+
export declare function sqlCiServices(ctx: {
|
|
32
|
+
dir: string;
|
|
33
|
+
}): CiService[];
|
|
34
|
+
//# sourceMappingURL=ci-setup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ci-setup.d.ts","sourceRoot":"","sources":["../src/ci-setup.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIzE,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG,UAAU,CAAC;AAErD,qEAAqE;AACrE,eAAO,MAAM,cAAc,WAAW,CAAC;AAEvC,oDAAoD;AACpD,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAEpC,wGAAwG;AACxG,eAAO,MAAM,eAAe,QAA4P,CAAC;AAEzR;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,oNACyK,CAAC;AAE5M,6FAA6F;AAC7F,wBAAgB,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,SAAS,CAqB9D;AAgBD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,EAAE,CAM1D;AAED,0FAA0F;AAC1F,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED,8GAA8G;AAC9G,wBAAgB,UAAU,CAAC,GAAG,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,EAAE,CAEzD;AAED,8GAA8G;AAC9G,wBAAgB,aAAa,CAAC,GAAG,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EAAE,CAI/D"}
|
package/dist/ci-setup.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a SQL project's CI jobs need from the sql lexicon (#4572, over
|
|
3
|
+
* #4550's `LexiconPlugin.ciSetup`): the setup lines for chant's default
|
|
4
|
+
* image, and the replay server per dialect as a job service
|
|
5
|
+
* (`LexiconPlugin.ciServices`). sql-yodeler's own versions were
|
|
6
|
+
* `src/ci/render.ts` (`installLines`, `REPLAY_SERVICE`, `waitFor`), which
|
|
7
|
+
* now come from here.
|
|
8
|
+
*
|
|
9
|
+
* The lines: a Node check, since the lexicon and chant run on Node 22, then
|
|
10
|
+
* the project's packages with `npm ci` unless the lexicon is already
|
|
11
|
+
* installed, with `node_modules/.bin` put on PATH for the job's later
|
|
12
|
+
* steps. That one install carries the dialect's client too: Postgres is the
|
|
13
|
+
* lexicon's own `pg` dependency and ClickHouse is spoken over HTTP, so no
|
|
14
|
+
* separate client goes into the image. A token source needs no cloud
|
|
15
|
+
* command line in CI either: in a job that traded its OIDC token through
|
|
16
|
+
* chant's `ci.identity`, `./token-source.ts` mints from what the trade left.
|
|
17
|
+
*
|
|
18
|
+
* The replay server is the image the lexicon's emulator runs, at the same
|
|
19
|
+
* pin (`./spec/pin.ts`, `./spec/postgres-pin.ts`), so a replay in CI and
|
|
20
|
+
* `chant emulator up` on a laptop run one server. It is a throwaway: the
|
|
21
|
+
* replay resets it. The dialects are read from the text of the project's
|
|
22
|
+
* `chant.config.ts`, never by running it, as lexicon detection reads source.
|
|
23
|
+
*/
|
|
24
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import { clickhouseImage } from "./spec/pin.js";
|
|
27
|
+
import { POSTGRES_LATEST_MAJOR, postgresImage } from "./spec/postgres-pin.js";
|
|
28
|
+
/** The profile the replay check runs on, as sql-yodeler names it. */
|
|
29
|
+
export const REPLAY_PROFILE = "replay";
|
|
30
|
+
/** The oldest Node the lexicon and chant run on. */
|
|
31
|
+
export const SQL_CI_NODE_MAJOR = 22;
|
|
32
|
+
/** Fails the job, naming the version, when the image's Node is older than {@link SQL_CI_NODE_MAJOR}. */
|
|
33
|
+
export const NODE_CHECK_LINE = `node -e 'const m = Number(process.versions.node.split(".")[0]); if (m < ${String(SQL_CI_NODE_MAJOR)}) { console.error("the sql lexicon needs Node ${String(SQL_CI_NODE_MAJOR)} or newer; this image has " + process.versions.node); process.exit(1); }'`;
|
|
34
|
+
/**
|
|
35
|
+
* The project's packages, unless the lexicon is already installed (an image
|
|
36
|
+
* that carries them pays nothing), and their bins on PATH: exported for the
|
|
37
|
+
* rest of a GitLab script, appended to `$GITHUB_PATH` for a GitHub or
|
|
38
|
+
* Forgejo job's later steps.
|
|
39
|
+
*/
|
|
40
|
+
export const PACKAGES_INSTALL_LINE = '{ [ -d node_modules/@intentius/chant-lexicon-sql ] || npm ci; } && export PATH="$PWD/node_modules/.bin:$PATH" && if [ -n "${GITHUB_PATH:-}" ]; then echo "$PWD/node_modules/.bin" >> "$GITHUB_PATH"; fi';
|
|
41
|
+
/** The replay server of each dialect: the emulator's pinned image, under a prefixed name. */
|
|
42
|
+
export function replayService(dialect) {
|
|
43
|
+
if (dialect === "clickhouse") {
|
|
44
|
+
return {
|
|
45
|
+
name: "replay-clickhouse",
|
|
46
|
+
image: clickhouseImage(),
|
|
47
|
+
port: 8123,
|
|
48
|
+
// The default user, with no password, from outside the container.
|
|
49
|
+
env: { CLICKHOUSE_SKIP_USER_SETUP: "1" },
|
|
50
|
+
health: "clickhouse-client --query 'SELECT 1'",
|
|
51
|
+
jobEnv: { REPLAY_CLICKHOUSE_URL: "http://replay-clickhouse:8123" },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
name: "replay-postgres",
|
|
56
|
+
image: postgresImage(POSTGRES_LATEST_MAJOR),
|
|
57
|
+
port: 5432,
|
|
58
|
+
env: { POSTGRES_PASSWORD: "replay" },
|
|
59
|
+
// Over TCP: during its first start the image runs a server on the socket only, then restarts.
|
|
60
|
+
health: "pg_isready -h 127.0.0.1 -U postgres",
|
|
61
|
+
jobEnv: { REPLAY_POSTGRES_URL: "postgres://replay-postgres:5432/postgres", REPLAY_POSTGRES_USER: "postgres", REPLAY_POSTGRES_PASSWORD: "replay" },
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** The text of the project's config, or undefined without one. */
|
|
65
|
+
function configText(dir) {
|
|
66
|
+
for (const name of ["chant.config.ts", "chant.config.mts", "chant.config.js"]) {
|
|
67
|
+
const file = join(dir, name);
|
|
68
|
+
if (!existsSync(file))
|
|
69
|
+
continue;
|
|
70
|
+
try {
|
|
71
|
+
return readFileSync(file, "utf-8");
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The dialects the config names (`dialect: "postgres"`, or a list), in
|
|
81
|
+
* {@link SqlCiDialect} order; ClickHouse, the lexicon's default, when it
|
|
82
|
+
* names none.
|
|
83
|
+
*/
|
|
84
|
+
export function sqlDialectsOf(text) {
|
|
85
|
+
const m = /\bdialect\s*:\s*(\[[^\]]*\]|["'][a-z]+["'])/.exec(text);
|
|
86
|
+
if (!m)
|
|
87
|
+
return ["clickhouse"];
|
|
88
|
+
const named = new Set([...m[1].matchAll(/["']([a-z]+)["']/g)].map((x) => x[1]));
|
|
89
|
+
const out = ["clickhouse", "postgres"].filter((d) => named.has(d));
|
|
90
|
+
return out.length > 0 ? [...out] : ["clickhouse"];
|
|
91
|
+
}
|
|
92
|
+
/** Whether the config declares the `replay` profile, the one the replay check runs on. */
|
|
93
|
+
export function declaresReplay(text) {
|
|
94
|
+
return new RegExp(`(^|[\\s{,])["']?${REPLAY_PROFILE}["']?\\s*:`).test(text);
|
|
95
|
+
}
|
|
96
|
+
/** The setup lines for the project at `dir`: none when it has no config naming the sql lexicon's settings. */
|
|
97
|
+
export function sqlCiSetup(ctx) {
|
|
98
|
+
return configText(ctx.dir) === undefined ? [] : [NODE_CHECK_LINE, PACKAGES_INSTALL_LINE];
|
|
99
|
+
}
|
|
100
|
+
/** The replay service per dialect for the project at `dir`, when its config declares the `replay` profile. */
|
|
101
|
+
export function sqlCiServices(ctx) {
|
|
102
|
+
const text = configText(ctx.dir);
|
|
103
|
+
if (text === undefined || !declaresReplay(text))
|
|
104
|
+
return [];
|
|
105
|
+
return sqlDialectsOf(text).map(replayService);
|
|
106
|
+
}
|
|
@@ -116,6 +116,26 @@ export declare function atomKey(a: GrantAtom): string;
|
|
|
116
116
|
export declare function foldAtoms(atoms: readonly GrantAtom[]): GrantAtom[];
|
|
117
117
|
/** A grantee's grants as a plan compares them: each atom's key, mapped to the atom. */
|
|
118
118
|
export declare function grantsCanonical(ddl: string, grantee: string, defaultDatabase?: string): Record<string, string>;
|
|
119
|
+
/**
|
|
120
|
+
* The atoms one grant declaration gives, one per grantee (#4582): `key` is
|
|
121
|
+
* the atom as the declaration spells its target (a bare table stays bare),
|
|
122
|
+
* which is how the deep diff addresses it on both sides; `atom` has the
|
|
123
|
+
* target qualified with `defaultDatabase`, which is what the server is asked
|
|
124
|
+
* about.
|
|
125
|
+
*/
|
|
126
|
+
export declare function declaredGrantAtoms(ddl: string, defaultDatabase?: string): Array<{
|
|
127
|
+
grantee: string;
|
|
128
|
+
key: string;
|
|
129
|
+
atom: GrantAtom;
|
|
130
|
+
}>;
|
|
131
|
+
/**
|
|
132
|
+
* Whether a grantee holding `have` (its `SHOW GRANTS` atoms) holds `a`: the
|
|
133
|
+
* same atom, or one the server folds it under (the table's privilege for a
|
|
134
|
+
* column's, the database's for a table's, `*.*` for any), with a grant or
|
|
135
|
+
* admin option at least as wide, and no partial revoke the server holds
|
|
136
|
+
* taking part of it back.
|
|
137
|
+
*/
|
|
138
|
+
export declare function holdsAtom(have: readonly GrantAtom[], a: GrantAtom): boolean;
|
|
119
139
|
/** Every grantee a grant statement names. */
|
|
120
140
|
export declare function granteesOf(ddl: string): string[];
|
|
121
141
|
/** The statement that gives `grantee` one atom. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../../src/clickhouse/access.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAA0B,KAAK,KAAK,EAAE,MAAM,UAAU,CAAC;AAC9D,OAAO,EAAiB,KAAK,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAG1D,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,GAAG,WAAW,GAAG,OAAO,GAAG,iBAAiB,GAAG,OAAO,CAAC;AAEpG,oFAAoF;AACpF,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,eAAe,CAAC;IAC3B,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;IACrB,4FAA4F;IAC5F,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,IAAI,CAAC;IACb;;;;;OAKG;IACH,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;IACvC,oDAAoD;IACpD,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,wDAAwD;IACxD,MAAM,CAAC,EAAE,IAAI,CAAC;IACd,0BAA0B;IAC1B,QAAQ,EAAE,IAAI,EAAE,CAAC;IACjB,kDAAkD;IAClD,UAAU,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IAC/B,iFAAiF;IACjF,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AA6LD,oGAAoG;AACpG,eAAO,MAAM,UAAU,aAAqF,CAAC;AAoB7G,oFAAoF;AACpF,wBAAgB,WAAW,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,UAAU,CAEvD;AAED,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAStD;AA6ED,4HAA4H;AAC5H,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASnE,CAAC;AASF;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA6ClF;AAyBD,gIAAgI;AAChI,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,WAAW,GAAG,OAAO,GAAG,iBAAiB,CAAC;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4BAA4B;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,eAAe,SAAY,GAAG,eAAe,CAmDzF;AAID,+DAA+D;AAC/D,MAAM,WAAW,SAAS;IACxB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8CAA8C;IAC9C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,sFAAsF;IACtF,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,iHAAiH;AACjH,wBAAgB,OAAO,CAAC,CAAC,EAAE,SAAS,GAAG,MAAM,CAG5C;
|
|
1
|
+
{"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../../src/clickhouse/access.ts"],"names":[],"mappings":"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"}
|
|
@@ -520,6 +520,7 @@ export function atomKey(a) {
|
|
|
520
520
|
return `ROLE ${a.role}${a.option ? " WITH ADMIN OPTION" : ""}`;
|
|
521
521
|
return `${a.revoke ? "REVOKE " : ""}${a.privilege}${a.column !== undefined ? `(${a.column})` : ""} ON ${a.target}${a.option ? " WITH GRANT OPTION" : ""}`;
|
|
522
522
|
}
|
|
523
|
+
/** Each statement's grantees and atoms. A bare table is qualified with `defaultDatabase`, or left bare when it is undefined. */
|
|
523
524
|
function grantAtoms(ddl, defaultDatabase) {
|
|
524
525
|
const out = [];
|
|
525
526
|
for (const statement of splitLines(ddl)) {
|
|
@@ -534,7 +535,7 @@ function grantAtoms(ddl, defaultDatabase) {
|
|
|
534
535
|
const parts = sigOf(tokens, node.target)
|
|
535
536
|
.filter((t) => !(t.kind === "punct" && t.text === "."))
|
|
536
537
|
.map((t) => unquote(t.text));
|
|
537
|
-
target = parts.length >= 2 ? `${parts[0]}.${parts[1]}` : parts[0] === "*" ?
|
|
538
|
+
target = parts.length >= 2 ? `${parts[0]}.${parts[1]}` : parts[0] === "*" || defaultDatabase === undefined ? parts[0] : `${defaultDatabase}.${parts[0]}`;
|
|
538
539
|
}
|
|
539
540
|
for (const item of node.items) {
|
|
540
541
|
const sig = sigOf(tokens, item);
|
|
@@ -603,6 +604,46 @@ export function grantsCanonical(ddl, grantee, defaultDatabase = "default") {
|
|
|
603
604
|
.map((a) => [atomKey(a), JSON.stringify(a)])
|
|
604
605
|
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
|
|
605
606
|
}
|
|
607
|
+
/**
|
|
608
|
+
* The atoms one grant declaration gives, one per grantee (#4582): `key` is
|
|
609
|
+
* the atom as the declaration spells its target (a bare table stays bare),
|
|
610
|
+
* which is how the deep diff addresses it on both sides; `atom` has the
|
|
611
|
+
* target qualified with `defaultDatabase`, which is what the server is asked
|
|
612
|
+
* about.
|
|
613
|
+
*/
|
|
614
|
+
export function declaredGrantAtoms(ddl, defaultDatabase = "default") {
|
|
615
|
+
const spelled = grantAtoms(ddl, undefined);
|
|
616
|
+
const resolved = grantAtoms(ddl, defaultDatabase);
|
|
617
|
+
const out = [];
|
|
618
|
+
spelled.forEach((g, i) => {
|
|
619
|
+
for (const grantee of g.grantees) {
|
|
620
|
+
g.atoms.forEach((a, j) => out.push({ grantee, key: atomKey(a), atom: resolved[i].atoms[j] }));
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
return out;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Whether a grantee holding `have` (its `SHOW GRANTS` atoms) holds `a`: the
|
|
627
|
+
* same atom, or one the server folds it under (the table's privilege for a
|
|
628
|
+
* column's, the database's for a table's, `*.*` for any), with a grant or
|
|
629
|
+
* admin option at least as wide, and no partial revoke the server holds
|
|
630
|
+
* taking part of it back.
|
|
631
|
+
*/
|
|
632
|
+
export function holdsAtom(have, a) {
|
|
633
|
+
if (a.role !== undefined)
|
|
634
|
+
return have.some((b) => b.role === a.role && (b.option ?? false) >= (a.option ?? false));
|
|
635
|
+
const granted = have.some((b) => !b.revoke &&
|
|
636
|
+
b.role === undefined &&
|
|
637
|
+
b.privilege === a.privilege &&
|
|
638
|
+
(b.option ?? false) >= (a.option ?? false) &&
|
|
639
|
+
((b.target === a.target && (b.column === undefined || b.column === a.column)) || (b.column === undefined && covers(b.target, a.target))));
|
|
640
|
+
if (!granted)
|
|
641
|
+
return false;
|
|
642
|
+
return !have.some((r) => r.revoke === true &&
|
|
643
|
+
r.privilege === a.privilege &&
|
|
644
|
+
(r.target === a.target || (a.column === undefined && covers(a.target, r.target))) &&
|
|
645
|
+
(a.column === undefined || r.column === undefined || r.column === a.column));
|
|
646
|
+
}
|
|
606
647
|
/** Every grantee a grant statement names. */
|
|
607
648
|
export function granteesOf(ddl) {
|
|
608
649
|
return [...new Set(grantAtoms(ddl, "default").flatMap((g) => g.grantees))];
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `describeResources()` for the ClickHouse dialect: which declared databases,
|
|
3
|
-
* tables and views exist on the environment's server.
|
|
3
|
+
* tables and views exist on the environment's server. A grant declaration is
|
|
4
|
+
* present when the server has every grantee it names, and missing when it
|
|
5
|
+
* lacks one (#4582); which privileges the grantee holds is the deep diff's.
|
|
4
6
|
*
|
|
5
7
|
* One read of `system.databases`, `system.tables` and `system.functions` per
|
|
6
8
|
* run, then a lookup per declared entity, so N entities are three queries,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"describe-resources.d.ts","sourceRoot":"","sources":["../../../src/clickhouse/live/describe-resources.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"describe-resources.d.ts","sourceRoot":"","sources":["../../../src/clickhouse/live/describe-resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,uBAAuB,EAG7B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAA6C,KAAK,WAAW,EAAyB,MAAM,QAAQ,CAAC;AAa5G,6DAA6D;AAC7D,wBAAgB,eAAe,CAAC,MAAM,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,GAAG;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAapH;AA6DD,wBAAsB,iBAAiB,CACrC,OAAO,EAAE;IACP,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IAC9E,mEAAmE;IACnE,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,GAAG,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,GACnC,OAAO,CAAC,uBAAuB,CAAC,CAgBlC"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `describeResources()` for the ClickHouse dialect: which declared databases,
|
|
3
|
-
* tables and views exist on the environment's server.
|
|
3
|
+
* tables and views exist on the environment's server. A grant declaration is
|
|
4
|
+
* present when the server has every grantee it names, and missing when it
|
|
5
|
+
* lacks one (#4582); which privileges the grantee holds is the deep diff's.
|
|
4
6
|
*
|
|
5
7
|
* One read of `system.databases`, `system.tables` and `system.functions` per
|
|
6
8
|
* run, then a lookup per declared entity, so N entities are three queries,
|
|
@@ -19,6 +21,7 @@ import { observeEntities, } from "@intentius/chant/observation";
|
|
|
19
21
|
import { bindClickHouse, classifyClickHouseFailure } from "./bind.js";
|
|
20
22
|
import { ACCESS_ENTITY_TYPES, ACCESS_UNMANAGED_DETAIL, accessOf, readLiveAccess, readLiveSchema } from "./catalog.js";
|
|
21
23
|
import { CLICKHOUSE_ENTITY_TYPES } from "../entities.js";
|
|
24
|
+
import { granteesOf } from "../access.js";
|
|
22
25
|
import { objectKey } from "../plan/normalize.js";
|
|
23
26
|
import { isChantManaged, readMarker, stripMarker } from "../ownership.js";
|
|
24
27
|
/** The database and name a declared entity is created as. */
|
|
@@ -54,7 +57,21 @@ function adapter(options) {
|
|
|
54
57
|
return { unobserved: { reason: "filtered", detail: ACCESS_UNMANAGED_DETAIL } };
|
|
55
58
|
}
|
|
56
59
|
if (entity.type === CLICKHOUSE_ENTITY_TYPES.grant) {
|
|
57
|
-
|
|
60
|
+
let grantees;
|
|
61
|
+
try {
|
|
62
|
+
grantees = granteesOf(String(entity.props.ddl ?? ""));
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
return { unobserved: { reason: "read-failed", detail: `the grant does not parse: ${err.message}` } };
|
|
66
|
+
}
|
|
67
|
+
const gone = grantees.find((g) => !byKey.has(objectKey({ type: entity.type, name: g })));
|
|
68
|
+
const queried = `${target.endpoint.url} grants ${gone ?? grantees.join(", ")}`;
|
|
69
|
+
if (gone !== undefined || grantees.length === 0)
|
|
70
|
+
return { absent: true, queried };
|
|
71
|
+
// A grant carries no comment, so no marker: like a user or a role, it is never chant's by marker.
|
|
72
|
+
if (options.owned)
|
|
73
|
+
return { unobserved: { reason: "filtered", detail: "a grant carries no chant ownership marker and owned was requested" }, queried };
|
|
74
|
+
return { present: { type: entity.type, physicalId: `grants ${grantees.join(", ")}`, status: "", ownership: "foreign" }, queried };
|
|
58
75
|
}
|
|
59
76
|
const { database, name } = declaredAddress(entity, target.defaultDatabase);
|
|
60
77
|
const queried = `${target.endpoint.url} ${database ? `${database}.` : ""}${name}`;
|
|
@@ -87,5 +104,16 @@ export async function describeResources(options) {
|
|
|
87
104
|
const entity = options.entities.get(name);
|
|
88
105
|
return { name, type: entity?.entityType ?? "", props: entity?.props ?? {} };
|
|
89
106
|
});
|
|
90
|
-
|
|
107
|
+
const grantees = [...options.entities.values()]
|
|
108
|
+
.filter((e) => e.entityType === CLICKHOUSE_ENTITY_TYPES.grant)
|
|
109
|
+
.flatMap((e) => {
|
|
110
|
+
try {
|
|
111
|
+
return granteesOf(String(e.props.ddl ?? ""));
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
const access = (db) => [...accessOf(options.entities, db), ...[...new Set(grantees)].map((name) => ({ kind: "grants", name }))];
|
|
118
|
+
return observeEntities(declared, adapter({ ...options, access }));
|
|
91
119
|
}
|
|
@@ -18,15 +18,35 @@
|
|
|
18
18
|
* server's, that copy is what is reported, with the server in `observedOn`,
|
|
19
19
|
* which the drift report prints as `seen on`.
|
|
20
20
|
*
|
|
21
|
-
* Where the profile manages access,
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* Where the profile manages access, each grant declaration is read as
|
|
22
|
+
* `grants.<grantee>.<atom>` (#4582): a privilege or role it declares that the
|
|
23
|
+
* grantee does not hold (revoked by hand, SQLCH273) is `absent` drift on that
|
|
24
|
+
* declaration. A grant declaration whose grantee the server does not have is
|
|
25
|
+
* not read here; the thin diff reports it missing. A privilege a grantee holds
|
|
26
|
+
* that no declaration gives (granted by hand, SQLCH274) belongs to no one
|
|
27
|
+
* declaration, so it is reported as `pending` (#3733) with every statement an
|
|
28
|
+
* apply would send that grantee, as `chant sql plan` plans them. Either way a
|
|
29
|
+
* privilege granted or revoked by hand after an approval moves the plan digest.
|
|
25
30
|
*/
|
|
26
31
|
import { type DeepObservationResult, type DeepPendingChange, type DeepNormalizationHooks } from "@intentius/chant/deep-observation";
|
|
27
32
|
import { type BindOptions, type ClickHouseTarget } from "../live/bind.js";
|
|
28
33
|
import { type CanonicalObject } from "./normalize.js";
|
|
34
|
+
import { type GrantAtom } from "../access.js";
|
|
29
35
|
type Props = Record<string, unknown>;
|
|
36
|
+
/**
|
|
37
|
+
* A grant declaration's tree as the diff compares it (#4582):
|
|
38
|
+
* `{ grants: { <grantee>: { <atom>: "granted" } } }`, each atom as the
|
|
39
|
+
* declaration spells it. The reader writes the live side in the same shape,
|
|
40
|
+
* with the atoms the grantee holds. Undefined when the statement does not
|
|
41
|
+
* parse as a grant.
|
|
42
|
+
*/
|
|
43
|
+
export declare function declaredGrantTree(ddl: string): Props | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* A grant declaration's live tree (#4582): each atom it declares that the
|
|
46
|
+
* grantee holds, keyed as {@link declaredGrantTree} keys it. Undefined when
|
|
47
|
+
* the server does not have one of its grantees.
|
|
48
|
+
*/
|
|
49
|
+
export declare function liveGrantTree(ddl: string, held: ReadonlyMap<string, readonly GrantAtom[]>, defaultDatabase: string): Props | undefined;
|
|
30
50
|
export declare const sqlDeepNormalizationHooks: DeepNormalizationHooks;
|
|
31
51
|
/** The live props, with each field the declaration means the same as written the declaration's way. */
|
|
32
52
|
export declare function inDeclaredVocabulary(declared: Props, live: Props, d: CanonicalObject, l: CanonicalObject): Props;
|
|
@@ -39,16 +59,27 @@ export declare function observeResourcesDeep(options: {
|
|
|
39
59
|
}>;
|
|
40
60
|
} & Omit<BindOptions, "environment">): Promise<DeepObservationResult>;
|
|
41
61
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
62
|
+
* What the server holds for each declared grantee, as `SHOW GRANTS FOR`
|
|
63
|
+
* prints it, folded into atoms. A grantee the server does not have is left
|
|
64
|
+
* out.
|
|
65
|
+
*/
|
|
66
|
+
export declare function heldGrants(target: ClickHouseTarget, entities: ReadonlyMap<string, {
|
|
67
|
+
entityType: string;
|
|
68
|
+
props: Record<string, unknown>;
|
|
69
|
+
}>): Promise<Map<string, GrantAtom[]>>;
|
|
70
|
+
/**
|
|
71
|
+
* The grantees holding a privilege or role no declaration gives them
|
|
72
|
+
* (#3733), as `chant sql plan` compares them: the declared grants against
|
|
73
|
+
* what `SHOW GRANTS FOR` prints, atom by atom. One change per such grantee,
|
|
74
|
+
* every statement an apply would send it, its REVOKEs before its GRANTs as
|
|
75
|
+
* the applier sends them, with the grant declarations behind it. A grantee
|
|
76
|
+
* that only lacks declared grants has nothing pending: each declaration
|
|
77
|
+
* reports those as `absent` drift of its own (#4582). A grantee the server
|
|
78
|
+
* does not have is the thin diff's missing.
|
|
48
79
|
*/
|
|
49
80
|
export declare function pendingGrants(target: ClickHouseTarget, entities: ReadonlyMap<string, {
|
|
50
81
|
entityType: string;
|
|
51
82
|
props: Record<string, unknown>;
|
|
52
|
-
}>): Promise<DeepPendingChange[]>;
|
|
83
|
+
}>, held?: ReadonlyMap<string, readonly GrantAtom[]>): Promise<DeepPendingChange[]>;
|
|
53
84
|
export {};
|
|
54
85
|
//# sourceMappingURL=deep.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deep.d.ts","sourceRoot":"","sources":["../../../src/clickhouse/plan/deep.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"deep.d.ts","sourceRoot":"","sources":["../../../src/clickhouse/plan/deep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAEL,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EAEtB,KAAK,sBAAsB,EAC5B,MAAM,mCAAmC,CAAC;AAE3C,OAAO,EAA6C,KAAK,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAMlH,OAAO,EAAkE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AACnH,OAAO,EAA4F,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAGrI,KAAK,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAQrC;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS,CAQhE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC,EAAE,eAAe,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS,CAQtI;AAED,eAAO,MAAM,yBAAyB,EAAE,sBAOvC,CAAC;AAgCF,uGAAuG;AACvG,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,eAAe,EAAE,CAAC,EAAE,eAAe,GAAG,KAAK,CAmDhH;AA4FD,wBAAsB,oBAAoB,CACxC,OAAO,EAAE;IACP,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;CAC/E,GAAG,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,GACnC,OAAO,CAAC,qBAAqB,CAAC,CAuGhC;AAKD;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,CASnL;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,gBAAgB,EACxB,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,EACrF,IAAI,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC,GAC/C,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAqB9B"}
|
|
@@ -18,10 +18,15 @@
|
|
|
18
18
|
* server's, that copy is what is reported, with the server in `observedOn`,
|
|
19
19
|
* which the drift report prints as `seen on`.
|
|
20
20
|
*
|
|
21
|
-
* Where the profile manages access,
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* Where the profile manages access, each grant declaration is read as
|
|
22
|
+
* `grants.<grantee>.<atom>` (#4582): a privilege or role it declares that the
|
|
23
|
+
* grantee does not hold (revoked by hand, SQLCH273) is `absent` drift on that
|
|
24
|
+
* declaration. A grant declaration whose grantee the server does not have is
|
|
25
|
+
* not read here; the thin diff reports it missing. A privilege a grantee holds
|
|
26
|
+
* that no declaration gives (granted by hand, SQLCH274) belongs to no one
|
|
27
|
+
* declaration, so it is reported as `pending` (#3733) with every statement an
|
|
28
|
+
* apply would send that grantee, as `chant sql plan` plans them. Either way a
|
|
29
|
+
* privilege granted or revoked by hand after an approval moves the plan digest.
|
|
25
30
|
*/
|
|
26
31
|
import { deepObservation, } from "@intentius/chant/deep-observation";
|
|
27
32
|
import { bindClickHouse, classifyClickHouseFailure } from "../live/bind.js";
|
|
@@ -31,12 +36,54 @@ import { sqlString } from "../apply/statements.js";
|
|
|
31
36
|
import { stripMarkerFromStatement } from "../ownership.js";
|
|
32
37
|
import { database, dictionary, func, policy, quota, role, settingsProfile, table, user, view, CLICKHOUSE_ENTITY_TYPES } from "../entities.js";
|
|
33
38
|
import { canonicalObject, grantsObject, objectKey } from "./normalize.js";
|
|
34
|
-
import { grantStatement, grantsByGrantee, revokeStatement } from "../access.js";
|
|
39
|
+
import { atomKey, declaredGrantAtoms, grantStatement, grantsByGrantee, holdsAtom, revokeStatement } from "../access.js";
|
|
35
40
|
import { renderFor } from "../topology.js";
|
|
36
41
|
// `concurrently` is how a Postgres index is created, not a property of it.
|
|
37
42
|
const PRUNED = new Set(["ddl", "source", "lineage", "reads", "to", "orReplace", "ifNotExists", "concurrently"]);
|
|
43
|
+
/** What a held grant atom reads as, on both sides of the diff. */
|
|
44
|
+
const GRANTED = "granted";
|
|
45
|
+
/**
|
|
46
|
+
* A grant declaration's tree as the diff compares it (#4582):
|
|
47
|
+
* `{ grants: { <grantee>: { <atom>: "granted" } } }`, each atom as the
|
|
48
|
+
* declaration spells it. The reader writes the live side in the same shape,
|
|
49
|
+
* with the atoms the grantee holds. Undefined when the statement does not
|
|
50
|
+
* parse as a grant.
|
|
51
|
+
*/
|
|
52
|
+
export function declaredGrantTree(ddl) {
|
|
53
|
+
try {
|
|
54
|
+
const grants = {};
|
|
55
|
+
for (const { grantee, key } of declaredGrantAtoms(ddl))
|
|
56
|
+
(grants[grantee] ?? (grants[grantee] = {}))[key] = GRANTED;
|
|
57
|
+
return { grants };
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A grant declaration's live tree (#4582): each atom it declares that the
|
|
65
|
+
* grantee holds, keyed as {@link declaredGrantTree} keys it. Undefined when
|
|
66
|
+
* the server does not have one of its grantees.
|
|
67
|
+
*/
|
|
68
|
+
export function liveGrantTree(ddl, held, defaultDatabase) {
|
|
69
|
+
const grants = {};
|
|
70
|
+
for (const { grantee, key, atom } of declaredGrantAtoms(ddl, defaultDatabase)) {
|
|
71
|
+
const have = held.get(grantee);
|
|
72
|
+
if (!have)
|
|
73
|
+
return undefined;
|
|
74
|
+
if (holdsAtom(have, atom))
|
|
75
|
+
(grants[grantee] ?? (grants[grantee] = {}))[key] = GRANTED;
|
|
76
|
+
}
|
|
77
|
+
return Object.keys(grants).length > 0 ? { grants } : {};
|
|
78
|
+
}
|
|
38
79
|
export const sqlDeepNormalizationHooks = {
|
|
39
80
|
prune: (node) => PRUNED.has(node.pattern.split(/[.[]/)[0]),
|
|
81
|
+
// A grant declaration is compared atom by atom (#4582); the reader writes the live side in this shape.
|
|
82
|
+
prepare: (tree, { entityType, side }) => {
|
|
83
|
+
if (entityType !== CLICKHOUSE_ENTITY_TYPES.grant || side !== "declared" || typeof tree.ddl !== "string")
|
|
84
|
+
return tree;
|
|
85
|
+
return declaredGrantTree(tree.ddl) ?? tree;
|
|
86
|
+
},
|
|
40
87
|
};
|
|
41
88
|
function liveProps(o) {
|
|
42
89
|
const tag = o.type === CLICKHOUSE_ENTITY_TYPES.database
|
|
@@ -235,9 +282,11 @@ export async function observeResourcesDeep(options) {
|
|
|
235
282
|
return deepObservation({}, unobserved);
|
|
236
283
|
}
|
|
237
284
|
const pending = [];
|
|
285
|
+
let held;
|
|
238
286
|
if (target.access === true) {
|
|
239
287
|
try {
|
|
240
|
-
|
|
288
|
+
held = await heldGrants(target, options.entities);
|
|
289
|
+
pending.push(...(await pendingGrants(target, options.entities, held)));
|
|
241
290
|
}
|
|
242
291
|
catch (err) {
|
|
243
292
|
const why = classifyClickHouseFailure(err);
|
|
@@ -259,13 +308,18 @@ export async function observeResourcesDeep(options) {
|
|
|
259
308
|
continue;
|
|
260
309
|
}
|
|
261
310
|
if (entity.entityType === CLICKHOUSE_ENTITY_TYPES.grant) {
|
|
262
|
-
if (unobserved[name])
|
|
311
|
+
if (unobserved[name] || !held)
|
|
263
312
|
continue;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
313
|
+
const ddl = String(entity.props.ddl ?? "");
|
|
314
|
+
try {
|
|
315
|
+
const properties = liveGrantTree(ddl, held, target.defaultDatabase);
|
|
316
|
+
// A grantee the server does not have: the thin diff reports the declaration missing.
|
|
317
|
+
if (properties)
|
|
318
|
+
resources[name] = { type: entity.entityType, physicalId: `grants ${[...new Set(declaredGrantAtoms(ddl).map((a) => a.grantee))].join(", ")}`, properties };
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
unobserved[name] = { type: entity.entityType, reason: "read-failed", detail: `the grant does not parse: ${err.message}` };
|
|
322
|
+
}
|
|
269
323
|
continue;
|
|
270
324
|
}
|
|
271
325
|
const props = entity.props;
|
|
@@ -310,36 +364,53 @@ export async function observeResourcesDeep(options) {
|
|
|
310
364
|
}
|
|
311
365
|
return deepObservation(resources, unobserved, pending);
|
|
312
366
|
}
|
|
367
|
+
const grantDeclarations = (entities) => grantsByGrantee([...entities].map(([name, e]) => ({ export: name, type: e.entityType, ddl: String(e.props.ddl ?? "") })));
|
|
313
368
|
/**
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
* REVOKEs before its GRANTs as the applier sends them, with the grant
|
|
318
|
-
* declarations behind it. A grantee the server does not hold yet is given
|
|
319
|
-
* every declared grant.
|
|
369
|
+
* What the server holds for each declared grantee, as `SHOW GRANTS FOR`
|
|
370
|
+
* prints it, folded into atoms. A grantee the server does not have is left
|
|
371
|
+
* out.
|
|
320
372
|
*/
|
|
321
|
-
export async function
|
|
322
|
-
const declared =
|
|
373
|
+
export async function heldGrants(target, entities) {
|
|
374
|
+
const declared = grantDeclarations(entities);
|
|
323
375
|
if (declared.length === 0)
|
|
324
|
-
return
|
|
376
|
+
return new Map();
|
|
325
377
|
const live = await readLiveAccess(target, declared.map((g) => ({ kind: "grants", name: g.grantee })));
|
|
326
|
-
|
|
378
|
+
return new Map(live
|
|
379
|
+
.filter((o) => o.type === CLICKHOUSE_ENTITY_TYPES.grant)
|
|
380
|
+
.map((o) => [o.name, Object.values(grantsObject(o.statement, o.name, target.defaultDatabase).access ?? {}).map((json) => JSON.parse(json))]));
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* The grantees holding a privilege or role no declaration gives them
|
|
384
|
+
* (#3733), as `chant sql plan` compares them: the declared grants against
|
|
385
|
+
* what `SHOW GRANTS FOR` prints, atom by atom. One change per such grantee,
|
|
386
|
+
* every statement an apply would send it, its REVOKEs before its GRANTs as
|
|
387
|
+
* the applier sends them, with the grant declarations behind it. A grantee
|
|
388
|
+
* that only lacks declared grants has nothing pending: each declaration
|
|
389
|
+
* reports those as `absent` drift of its own (#4582). A grantee the server
|
|
390
|
+
* does not have is the thin diff's missing.
|
|
391
|
+
*/
|
|
392
|
+
export async function pendingGrants(target, entities, held) {
|
|
393
|
+
const declared = grantDeclarations(entities);
|
|
394
|
+
if (declared.length === 0)
|
|
395
|
+
return [];
|
|
396
|
+
const live = held ?? (await heldGrants(target, entities));
|
|
327
397
|
const atom = (json) => JSON.parse(json);
|
|
328
398
|
const out = [];
|
|
329
399
|
for (const g of declared) {
|
|
400
|
+
const atoms = live.get(g.grantee);
|
|
401
|
+
if (!atoms)
|
|
402
|
+
continue;
|
|
403
|
+
const have = Object.fromEntries(atoms.map((a) => [atomKey(a), JSON.stringify(a)]));
|
|
330
404
|
const want = grantsObject(g.ddl, g.grantee, target.defaultDatabase).access ?? {};
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
.map((k) => revokeStatement(atom(have[k]), g.grantee)),
|
|
336
|
-
...Object.keys(want)
|
|
337
|
-
.filter((k) => !(k in have))
|
|
338
|
-
.map((k) => grantStatement(atom(want[k]), g.grantee)),
|
|
339
|
-
];
|
|
340
|
-
if (statements.length === 0)
|
|
405
|
+
const revokes = Object.keys(have)
|
|
406
|
+
.filter((k) => !(k in want))
|
|
407
|
+
.map((k) => revokeStatement(atom(have[k]), g.grantee));
|
|
408
|
+
if (revokes.length === 0)
|
|
341
409
|
continue;
|
|
342
|
-
|
|
410
|
+
const grants = Object.keys(want)
|
|
411
|
+
.filter((k) => !(k in have))
|
|
412
|
+
.map((k) => grantStatement(atom(want[k]), g.grantee));
|
|
413
|
+
out.push({ subject: `grants ${g.grantee}`, change: [...revokes, ...grants].join("; "), entities: [...g.exports].sort() });
|
|
343
414
|
}
|
|
344
415
|
return out;
|
|
345
416
|
}
|