@intentius/chant-lexicon-sql 0.117.0 → 0.118.1
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/clickhouse/access.d.ts +132 -0
- package/dist/clickhouse/access.d.ts.map +1 -0
- package/dist/clickhouse/apply/apply.d.ts.map +1 -1
- package/dist/clickhouse/apply/statements.d.ts +2 -0
- package/dist/clickhouse/apply/statements.d.ts.map +1 -1
- package/dist/clickhouse/entities.d.ts +171 -2
- package/dist/clickhouse/entities.d.ts.map +1 -1
- package/dist/clickhouse/http.d.ts +3 -0
- package/dist/clickhouse/http.d.ts.map +1 -1
- package/dist/clickhouse/import/generator.d.ts +28 -0
- package/dist/clickhouse/import/generator.d.ts.map +1 -1
- package/dist/clickhouse/import/ir.d.ts.map +1 -1
- package/dist/clickhouse/import/parser.d.ts +3 -3
- package/dist/clickhouse/import/parser.d.ts.map +1 -1
- package/dist/clickhouse/live/bind.d.ts.map +1 -1
- package/dist/clickhouse/live/catalog.d.ts +41 -6
- package/dist/clickhouse/live/catalog.d.ts.map +1 -1
- package/dist/clickhouse/live/describe-resources.d.ts +3 -2
- package/dist/clickhouse/live/describe-resources.d.ts.map +1 -1
- package/dist/clickhouse/parser.d.ts +54 -3
- package/dist/clickhouse/parser.d.ts.map +1 -1
- package/dist/clickhouse/plan/commands.d.ts +3 -1
- package/dist/clickhouse/plan/commands.d.ts.map +1 -1
- package/dist/clickhouse/plan/deep.d.ts.map +1 -1
- package/dist/clickhouse/plan/diff.d.ts +12 -0
- package/dist/clickhouse/plan/diff.d.ts.map +1 -1
- package/dist/clickhouse/plan/normalize.d.ts +61 -1
- package/dist/clickhouse/plan/normalize.d.ts.map +1 -1
- package/dist/clickhouse/plan/report.d.ts.map +1 -1
- package/dist/clickhouse/plan/rules.d.ts +7 -0
- package/dist/clickhouse/plan/rules.d.ts.map +1 -1
- package/dist/clickhouse/plan/schema.d.ts +8 -4
- package/dist/clickhouse/plan/schema.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/backfill.d.ts +3 -1
- package/dist/clickhouse/rebuild/backfill.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/observe.d.ts +7 -3
- package/dist/clickhouse/rebuild/observe.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/op.d.ts +8 -0
- package/dist/clickhouse/rebuild/op.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/steps.d.ts +48 -0
- package/dist/clickhouse/rebuild/steps.d.ts.map +1 -1
- package/dist/clickhouse/testing/fake-server.d.ts.map +1 -1
- package/dist/clickhouse/testing/writable-server.d.ts.map +1 -1
- package/dist/clickhouse/topology.d.ts.map +1 -1
- package/dist/clickhouse.d.ts +1 -1
- package/dist/clickhouse.d.ts.map +1 -1
- package/dist/codegen/classifier-page.d.ts.map +1 -1
- package/dist/codegen/registry.d.ts.map +1 -1
- package/dist/config.d.ts +50 -7
- package/dist/config.d.ts.map +1 -1
- package/dist/core/references.d.ts +4 -2
- package/dist/core/references.d.ts.map +1 -1
- package/dist/core/statements.d.ts +11 -0
- package/dist/core/statements.d.ts.map +1 -1
- package/dist/files/build-root.d.ts +34 -0
- package/dist/files/build-root.d.ts.map +1 -0
- package/dist/files/clickhouse.d.ts +19 -0
- package/dist/files/clickhouse.d.ts.map +1 -0
- package/dist/files/common.d.ts +25 -0
- package/dist/files/common.d.ts.map +1 -0
- package/dist/files/postgres.d.ts +36 -0
- package/dist/files/postgres.d.ts.map +1 -0
- package/dist/import-parser.d.ts +25 -0
- package/dist/import-parser.d.ts.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +6 -6
- package/dist/lint/rules/sqlch001.d.ts +1 -1
- package/dist/lint/rules/sqlch001.d.ts.map +1 -1
- package/dist/lint/rules/sqlch002.d.ts.map +1 -1
- package/dist/lint/rules/templates.d.ts +1 -1
- package/dist/lint/rules/templates.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/meta.json +30 -0
- package/dist/okf/index.md +7 -1
- package/dist/okf/rules/SQLCH001.md +2 -2
- package/dist/okf/types/Dictionary.md +9 -0
- package/dist/okf/types/Function.md +9 -0
- package/dist/okf/types/Grant.md +9 -0
- package/dist/okf/types/Role.md +9 -0
- package/dist/okf/types/RowPolicy.md +9 -0
- package/dist/okf/types/User.md +9 -0
- package/dist/op/activities/clickhouse-rebuild.d.ts.map +1 -1
- package/dist/op/activity-contracts.d.ts +9 -0
- package/dist/op/activity-contracts.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/postgres/apply/statements.d.ts +68 -0
- package/dist/postgres/apply/statements.d.ts.map +1 -1
- package/dist/postgres/import/generator.d.ts +33 -0
- package/dist/postgres/import/generator.d.ts.map +1 -1
- package/dist/postgres/live/bind.d.ts.map +1 -1
- package/dist/postgres/live/client.d.ts +3 -0
- package/dist/postgres/live/client.d.ts.map +1 -1
- package/dist/rules/sqlch001.ts +26 -4
- package/dist/rules/sqlch002.ts +3 -2
- package/dist/rules/templates.ts +2 -2
- package/dist/serializer.d.ts +2 -1
- package/dist/serializer.d.ts.map +1 -1
- package/dist/sql-files.d.ts +79 -0
- package/dist/sql-files.d.ts.map +1 -0
- package/dist/token-source.d.ts +88 -0
- package/dist/token-source.d.ts.map +1 -0
- package/dist/validate.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/clickhouse/access.test.ts +203 -0
- package/src/clickhouse/access.ts +595 -0
- package/src/clickhouse/apply/apply.ts +29 -20
- package/src/clickhouse/apply/statements.ts +105 -8
- package/src/clickhouse/dictionary.test.ts +177 -0
- package/src/clickhouse/entities.ts +356 -6
- package/src/clickhouse/function.test.ts +91 -0
- package/src/clickhouse/http.ts +11 -2
- package/src/clickhouse/import/generator.ts +81 -46
- package/src/clickhouse/import/import.test.ts +9 -3
- package/src/clickhouse/import/ir.ts +6 -2
- package/src/clickhouse/import/parser.ts +10 -18
- package/src/clickhouse/live/access.e2e.test.ts +218 -0
- package/src/clickhouse/live/bind.ts +17 -6
- package/src/clickhouse/live/catalog.ts +106 -10
- package/src/clickhouse/live/describe-resources.ts +23 -10
- package/src/clickhouse/live/dictionaries.e2e.test.ts +186 -0
- package/src/clickhouse/live/functions.e2e.test.ts +155 -0
- package/src/clickhouse/live/live.test.ts +2 -2
- package/src/clickhouse/parser.ts +136 -4
- package/src/clickhouse/plan/commands.ts +18 -7
- package/src/clickhouse/plan/deep.ts +71 -11
- package/src/clickhouse/plan/diff.ts +65 -2
- package/src/clickhouse/plan/normalize.ts +200 -6
- package/src/clickhouse/plan/report.ts +14 -3
- package/src/clickhouse/plan/rules.ts +44 -1
- package/src/clickhouse/plan/schema.ts +40 -17
- package/src/clickhouse/plan/server-format.ts +1 -1
- package/src/clickhouse/rebuild/backfill.ts +5 -25
- package/src/clickhouse/rebuild/observe.ts +7 -3
- package/src/clickhouse/rebuild/op.ts +9 -0
- package/src/clickhouse/rebuild/rebuild.e2e.test.ts +62 -0
- package/src/clickhouse/rebuild/rebuild.test.ts +54 -0
- package/src/clickhouse/rebuild/steps.ts +136 -1
- package/src/clickhouse/testing/fake-server.ts +2 -0
- package/src/clickhouse/testing/writable-server.ts +2 -0
- package/src/clickhouse/topology.ts +17 -2
- package/src/clickhouse.ts +19 -0
- package/src/codegen/classifier-page.ts +2 -0
- package/src/codegen/registry.ts +6 -0
- package/src/config.ts +12 -3
- package/src/core/references.ts +5 -3
- package/src/core/statements.ts +11 -1
- package/src/files/build-root.ts +74 -0
- package/src/files/clickhouse.ts +52 -0
- package/src/files/common.ts +54 -0
- package/src/files/postgres.ts +371 -0
- package/src/generated/lexicon-sql.json +30 -0
- package/src/import-parser.ts +61 -0
- package/src/index.ts +15 -2
- package/src/lint/rules/sqlch001.ts +26 -4
- package/src/lint/rules/sqlch002.ts +3 -2
- package/src/lint/rules/templates.ts +2 -2
- package/src/lsp/template.ts +3 -3
- package/src/mcp/index.ts +6 -6
- package/src/migration-statements.test.ts +25 -0
- package/src/migration-statements.ts +1 -0
- package/src/op/activities/clickhouse-rebuild.ts +1 -0
- package/src/op/activity-contracts.ts +1 -0
- package/src/plugin.ts +29 -8
- package/src/postgres/apply/constraint-sequences.e2e.test.ts +179 -0
- package/src/postgres/apply/safe-sequences.e2e.test.ts +176 -0
- package/src/postgres/apply/statements.test.ts +138 -6
- package/src/postgres/apply/statements.ts +156 -10
- package/src/postgres/import/generator.ts +118 -45
- package/src/postgres/live/bind.ts +17 -6
- package/src/postgres/live/client.ts +10 -2
- package/src/postgres/plan/plan.test.ts +2 -2
- package/src/postgres/plan/rules.ts +5 -5
- package/src/serializer.ts +14 -9
- package/src/skills/chant-sql-postgres-plan.md +3 -3
- package/src/skills/chant-sql-rebuild.md +3 -2
- package/src/skills/chant-sql.md +7 -1
- package/src/sql-files.test.ts +220 -0
- package/src/sql-files.ts +218 -0
- package/src/token-source.e2e.test.ts +142 -0
- package/src/token-source.test.ts +119 -0
- package/src/token-source.ts +189 -0
- package/src/validate.ts +6 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access control (#3682): the `user`, `role`, `policy` and `grant` tags,
|
|
3
|
+
* each compared with what the server prints for it, grants compared per
|
|
4
|
+
* grantee, the statements for each change, and `ON CLUSTER` per topology.
|
|
5
|
+
* `live/access.e2e.test.ts` runs them against a server.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "vitest";
|
|
9
|
+
import { grant, policy, role, table, user, CLICKHOUSE_ENTITY_TYPES } from "./entities";
|
|
10
|
+
import { canonicalObject, grantsObject, objectKey } from "./plan/normalize";
|
|
11
|
+
import { diffSchemas } from "./plan/diff";
|
|
12
|
+
import { planStatements, type DeclaredObject } from "./apply/statements";
|
|
13
|
+
import { declaredObjects } from "./apply/apply";
|
|
14
|
+
import { foldAtoms, grantsByGrantee } from "./access";
|
|
15
|
+
import { renderStatement } from "./topology";
|
|
16
|
+
|
|
17
|
+
const t = (s: string) => [s] as unknown as TemplateStringsArray;
|
|
18
|
+
|
|
19
|
+
describe("the access tags", () => {
|
|
20
|
+
test("a user, with a method that holds no secret", () => {
|
|
21
|
+
const u = user(t("CREATE USER app IDENTIFIED WITH ssl_certificate CN 'app' HOST IP '10.0.0.0/8', LOCAL DEFAULT DATABASE shop SETTINGS max_threads = 4 READONLY"));
|
|
22
|
+
expect(u.entityType).toBe("ClickHouse::User");
|
|
23
|
+
expect(u.props).toMatchObject({ name: "app", identified: "WITH ssl_certificate CN 'app'", host: "IP '10.0.0.0/8', LOCAL", defaultDatabase: "shop", settings: "max_threads = 4 READONLY" });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("a password is refused, written or hashed; ssh_key's public key is taken", () => {
|
|
27
|
+
expect(() => user(t("CREATE USER app IDENTIFIED BY 'secret'"))).toThrow(/environment's/);
|
|
28
|
+
expect(() => user(t("CREATE USER app IDENTIFIED WITH sha256_hash BY 'abc'"))).toThrow(/environment's/);
|
|
29
|
+
expect(user(t("CREATE USER app IDENTIFIED WITH ssh_key BY KEY 'AAAA' TYPE 'ssh-ed25519'")).props.identified).toBe("WITH ssh_key BY KEY 'AAAA' TYPE 'ssh-ed25519'");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("a role, a row policy and grants, with references", () => {
|
|
33
|
+
const events = table`CREATE TABLE shop.events (id UInt64, tenant String) ENGINE = MergeTree ORDER BY id`;
|
|
34
|
+
const reader = role`CREATE ROLE reader SETTINGS max_memory_usage = 1000000000`;
|
|
35
|
+
const p = policy`CREATE ROW POLICY tenant_a ON ${events} AS RESTRICTIVE FOR SELECT USING ${events.columns.tenant} = 'a' TO ${reader}`;
|
|
36
|
+
expect(p.props).toMatchObject({ name: "tenant_a", database: "shop", table: "events", as: "RESTRICTIVE", using: "tenant = 'a'", to: "reader" });
|
|
37
|
+
const g = grant`GRANT SELECT(id, tenant), INSERT ON ${events} TO ${reader} WITH GRANT OPTION`;
|
|
38
|
+
expect(g.props).toMatchObject({ privileges: ["SELECT(id, tenant)", "INSERT"], on: "shop.events", to: ["reader"], withGrantOption: true });
|
|
39
|
+
expect(grant`GRANT ${reader} TO app WITH ADMIN OPTION`.props).toMatchObject({ roles: ["reader"], to: ["app"], withAdminOption: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("a REVOKE, WITH REPLACE OPTION and the wrong tag are refused", () => {
|
|
43
|
+
expect(() => grant(t("REVOKE SELECT ON shop.* FROM reader"))).toThrow(/REVOKE/);
|
|
44
|
+
expect(() => grant(t("GRANT SELECT ON shop.* TO reader WITH REPLACE OPTION"))).toThrow(/REPLACE OPTION/);
|
|
45
|
+
expect(() => role(t("CREATE USER app"))).toThrow(/use the user tag/);
|
|
46
|
+
expect(() => policy(t("CREATE ROW POLICY p ON shop.t FOR SELECT TO r"))).toThrow(/USING/);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("compared with what the server prints", () => {
|
|
51
|
+
const same = (declared: string, shown: string) => diffSchemas([{ key: "k", canonical: canonicalObject(shown, "shop") }], [{ key: "k", canonical: canonicalObject(declared, "shop") }]).changes;
|
|
52
|
+
|
|
53
|
+
test("a user: hosts sorted, READONLY as CONST, defaults left out, a password's method read without the password", () => {
|
|
54
|
+
expect(
|
|
55
|
+
same(
|
|
56
|
+
"CREATE USER app IDENTIFIED WITH ssl_certificate CN 'app' HOST IP '10.0.0.0/8', LOCAL DEFAULT ROLE ALL DEFAULT DATABASE shop SETTINGS readonly = 1, max_threads = 4 READONLY",
|
|
57
|
+
"CREATE USER app IDENTIFIED WITH ssl_certificate CN 'app' HOST LOCAL, IP '10.0.0.0/8' DEFAULT DATABASE shop SETTINGS max_threads = 4 CONST, readonly = 1",
|
|
58
|
+
),
|
|
59
|
+
).toEqual([]);
|
|
60
|
+
// Declared without IDENTIFIED: the server's password is not compared.
|
|
61
|
+
expect(same("CREATE USER app DEFAULT ROLE reader", "CREATE USER app IDENTIFIED WITH sha256_password DEFAULT ROLE reader")).toEqual([]);
|
|
62
|
+
expect(same("CREATE USER app HOST LOCAL", "CREATE USER app IDENTIFIED WITH sha256_password").map((c) => [c.field, c.rule])).toEqual([["host", "SQLCH271"]]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("a role's settings and a row policy's defaults", () => {
|
|
66
|
+
expect(same("CREATE ROLE r SETTINGS max_threads = 2 READONLY", "CREATE ROLE r SETTINGS max_threads = 2 CONST")).toEqual([]);
|
|
67
|
+
expect(same("CREATE ROLE r", "CREATE ROLE r SETTINGS max_threads = 2").map((c) => [c.field, c.rule])).toEqual([["settings", "SQLCH270"]]);
|
|
68
|
+
expect(same("CREATE ROW POLICY p ON t AS PERMISSIVE USING a > 1 TO b, a", "CREATE ROW POLICY p ON shop.t FOR SELECT USING a > 1 TO a, b")).toEqual([]);
|
|
69
|
+
expect(same("CREATE ROW POLICY p ON t USING a > 2", "CREATE ROW POLICY p ON shop.t FOR SELECT USING a > 1").map((c) => [c.field, c.rule])).toEqual([["using", "SQLCH272"]]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("grants: what SHOW GRANTS prints, folded the server's way", () => {
|
|
73
|
+
const declared = grantsObject(
|
|
74
|
+
["GRANT SELECT(a, b), INSERT ON t TO r", "GRANT SELECT ON shop.* TO r WITH GRANT OPTION", "GRANT dictget ON shop.* TO r", "GRANT viewer TO r"].join(";\n"),
|
|
75
|
+
"r",
|
|
76
|
+
"shop",
|
|
77
|
+
);
|
|
78
|
+
const shown = grantsObject(
|
|
79
|
+
[
|
|
80
|
+
"GRANT dictGet ON shop.* TO r",
|
|
81
|
+
"GRANT SELECT ON shop.* TO r WITH GRANT OPTION",
|
|
82
|
+
"GRANT INSERT ON shop.t TO r",
|
|
83
|
+
"GRANT viewer TO r",
|
|
84
|
+
].join("\n"),
|
|
85
|
+
"r",
|
|
86
|
+
"shop",
|
|
87
|
+
);
|
|
88
|
+
expect(Object.keys(declared.access!)).toEqual(["DICTGET ON shop.*", "INSERT ON shop.t", "ROLE viewer", "SELECT ON shop.* WITH GRANT OPTION"]);
|
|
89
|
+
expect(diffSchemas([{ key: "g", canonical: shown }], [{ key: "g", canonical: declared }]).changes).toEqual([]);
|
|
90
|
+
// A grant made by hand is revoked, a declared one missing is granted.
|
|
91
|
+
const live = grantsObject("GRANT INSERT ON shop.t TO r\nGRANT ALTER ON shop.t TO r\nREVOKE SELECT(b) ON shop.t FROM r", "r", "shop");
|
|
92
|
+
const changes = diffSchemas([{ key: "g", canonical: live }], [{ key: "g", canonical: grantsObject("GRANT INSERT, SELECT ON shop.t TO r", "r", "shop") }]).changes;
|
|
93
|
+
expect(changes.map((c) => [c.field, c.rule])).toEqual([
|
|
94
|
+
["grants.ALTER ON shop.t", "SQLCH274"],
|
|
95
|
+
["grants.REVOKE SELECT(b) ON shop.t", "SQLCH274"],
|
|
96
|
+
["grants.SELECT ON shop.t", "SQLCH273"],
|
|
97
|
+
]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("an atom another covers is folded away; a wider grant option is not covered", () => {
|
|
101
|
+
const folded = foldAtoms([
|
|
102
|
+
{ privilege: "SELECT", target: "shop.t", column: "a" },
|
|
103
|
+
{ privilege: "SELECT", target: "shop.t" },
|
|
104
|
+
{ privilege: "SELECT", target: "shop.*", option: true },
|
|
105
|
+
{ privilege: "INSERT", target: "shop.t", option: true },
|
|
106
|
+
{ privilege: "INSERT", target: "shop.*" },
|
|
107
|
+
]);
|
|
108
|
+
expect(folded.map((a) => `${a.privilege} ${a.target}${a.option ? " +" : ""}`)).toEqual(["SELECT shop.* +", "INSERT shop.t +", "INSERT shop.*"]);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("the statements", () => {
|
|
113
|
+
const build = (objects: Array<{ export: string; type: string; ddl: string; dependsOn?: string[] }>) =>
|
|
114
|
+
JSON.stringify({ dialect: "clickhouse", objects: objects.map((o) => ({ dependsOn: [], ...o })) });
|
|
115
|
+
|
|
116
|
+
test("grant declarations are compared per grantee, after every other object", () => {
|
|
117
|
+
const declared = declaredObjects(
|
|
118
|
+
build([
|
|
119
|
+
{ export: "toReader", type: "ClickHouse::Grant", ddl: "GRANT SELECT ON shop.* TO reader, auditor" },
|
|
120
|
+
{ export: "reader", type: "ClickHouse::Role", ddl: "CREATE ROLE reader" },
|
|
121
|
+
{ export: "toAuditor", type: "ClickHouse::Grant", ddl: "GRANT ALTER ON shop.t TO auditor", dependsOn: ["reader"] },
|
|
122
|
+
]),
|
|
123
|
+
"shop",
|
|
124
|
+
);
|
|
125
|
+
expect(declared.map((o) => [o.exportName, o.key])).toEqual([
|
|
126
|
+
["reader", "role reader"],
|
|
127
|
+
["grants reader", "grants reader"],
|
|
128
|
+
["grants auditor", "grants auditor"],
|
|
129
|
+
]);
|
|
130
|
+
expect(declared[2]!.dependsOn).toEqual(["reader"]);
|
|
131
|
+
expect(grantsByGrantee([{ export: "x", type: "ClickHouse::Grant", ddl: "GRANT r1, r2 TO a" }])).toEqual([{ grantee: "a", exports: ["x"], ddl: "GRANT r1, r2 TO a", dependsOn: [] }]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("a grantee's first grants, then its revokes before its grants", () => {
|
|
135
|
+
const [obj] = declaredObjects(build([{ export: "g", type: "ClickHouse::Grant", ddl: "GRANT SELECT(a), INSERT ON shop.t TO r WITH GRANT OPTION" }]), "shop") as [DeclaredObject];
|
|
136
|
+
const created = planStatements({ declared: [obj], changes: diffSchemas([], [{ key: obj.key, canonical: obj.canonical }]).changes, current: new Map() });
|
|
137
|
+
const c = created.objects[0]!;
|
|
138
|
+
expect(c.verdict === "create" ? c.steps.map((s) => s.sql) : []).toEqual([
|
|
139
|
+
"GRANT INSERT ON `shop`.`t` TO `r` WITH GRANT OPTION",
|
|
140
|
+
"GRANT SELECT(`a`) ON `shop`.`t` TO `r` WITH GRANT OPTION",
|
|
141
|
+
]);
|
|
142
|
+
const live = grantsObject("GRANT INSERT ON shop.t TO r WITH GRANT OPTION\nGRANT ALTER ON shop.* TO r", "r", "shop");
|
|
143
|
+
const changes = diffSchemas([{ key: obj.key, canonical: live }], [{ key: obj.key, canonical: obj.canonical }]).changes;
|
|
144
|
+
const altered = planStatements({ declared: [obj], changes, current: new Map([[obj.key, live]]) }).objects[0]!;
|
|
145
|
+
expect(altered.verdict === "alter" ? altered.steps.map((s) => [s.rule, s.sql]) : []).toEqual([
|
|
146
|
+
["SQLCH274", "REVOKE ALTER ON `shop`.* FROM `r`"],
|
|
147
|
+
["SQLCH273", "GRANT SELECT(`a`) ON `shop`.`t` TO `r` WITH GRANT OPTION"],
|
|
148
|
+
]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("a user with a password is the environment's to create; once there, each changed clause is one ALTER USER", () => {
|
|
152
|
+
const decl = (ddl: string): DeclaredObject => {
|
|
153
|
+
const canonical = canonicalObject(ddl);
|
|
154
|
+
return { exportName: "app", type: CLICKHOUSE_ENTITY_TYPES.user, key: objectKey(canonical), ddl, canonical, dependsOn: [] };
|
|
155
|
+
};
|
|
156
|
+
const app = decl("CREATE USER app HOST LOCAL DEFAULT ROLE reader SETTINGS max_threads = 4");
|
|
157
|
+
const created = planStatements({ declared: [app], changes: diffSchemas([], [{ key: app.key, canonical: app.canonical }]).changes, current: new Map() }).objects[0]!;
|
|
158
|
+
expect(created.verdict).toBe("withheld");
|
|
159
|
+
expect(created.verdict === "withheld" ? created.detail : "").toMatch(/password is the environment's/);
|
|
160
|
+
const cert = decl("CREATE USER app IDENTIFIED WITH ssl_certificate CN 'app'");
|
|
161
|
+
const made = planStatements({ declared: [cert], changes: diffSchemas([], [{ key: cert.key, canonical: cert.canonical }]).changes, current: new Map() }).objects[0]!;
|
|
162
|
+
expect(made.verdict === "create" ? made.steps.map((s) => s.sql) : []).toEqual(["CREATE USER app IDENTIFIED WITH ssl_certificate CN 'app'"]);
|
|
163
|
+
|
|
164
|
+
const live = canonicalObject("CREATE USER app IDENTIFIED WITH sha256_password HOST ANY DEFAULT ROLE NONE SETTINGS max_threads = 8");
|
|
165
|
+
const changes = diffSchemas([{ key: app.key, canonical: live }], [{ key: app.key, canonical: app.canonical }]).changes;
|
|
166
|
+
const altered = planStatements({ declared: [app], changes, current: new Map([[app.key, live]]) }).objects[0]!;
|
|
167
|
+
expect(altered.verdict === "alter" ? altered.steps.map((s) => s.sql) : []).toEqual([
|
|
168
|
+
"ALTER USER `app` HOST LOCAL",
|
|
169
|
+
"ALTER USER `app` DEFAULT ROLE reader",
|
|
170
|
+
"ALTER USER `app` SETTINGS max_threads = 4",
|
|
171
|
+
]);
|
|
172
|
+
// A clause left out goes back to the server's default.
|
|
173
|
+
const back = diffSchemas([{ key: app.key, canonical: app.canonical }], [{ key: app.key, canonical: canonicalObject("CREATE USER app") }]).changes;
|
|
174
|
+
const reset = planStatements({ declared: [decl("CREATE USER app")], changes: back, current: new Map([[app.key, app.canonical]]) }).objects[0]!;
|
|
175
|
+
expect(reset.verdict === "alter" ? reset.steps.map((s) => s.sql) : []).toEqual(["ALTER USER `app` HOST ANY", "ALTER USER `app` DEFAULT ROLE ALL", "ALTER USER `app` SETTINGS NONE"]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("a row policy is replaced whole; nothing of access control is ever dropped", () => {
|
|
179
|
+
const ddl = "CREATE ROW POLICY p ON shop.t USING a > 2 TO r";
|
|
180
|
+
const canonical = canonicalObject(ddl);
|
|
181
|
+
const obj: DeclaredObject = { exportName: "p", type: CLICKHOUSE_ENTITY_TYPES.rowPolicy, key: objectKey(canonical), ddl, canonical, dependsOn: [] };
|
|
182
|
+
expect(obj.key).toBe("row policy p ON shop.t");
|
|
183
|
+
const live = canonicalObject("CREATE ROW POLICY p ON shop.t FOR SELECT USING a > 1 TO r");
|
|
184
|
+
const changes = diffSchemas([{ key: obj.key, canonical: live }], [{ key: obj.key, canonical }]).changes;
|
|
185
|
+
const entry = planStatements({ declared: [obj], changes, current: new Map([[obj.key, live]]) }).objects[0]!;
|
|
186
|
+
expect(entry.verdict === "alter" ? entry.steps.map((s) => s.sql) : []).toEqual(["CREATE ROW POLICY OR REPLACE p ON shop.t USING a > 2 TO r"]);
|
|
187
|
+
const gone = diffSchemas([{ key: obj.key, canonical: live }, { key: "role r", canonical: canonicalObject("CREATE ROLE r") }], []).changes;
|
|
188
|
+
expect(planStatements({ declared: [], changes: gone, current: new Map([[obj.key, live], ["role r", canonicalObject("CREATE ROLE r")]]), allowDestructive: true }).drops).toEqual([]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("ON CLUSTER on a cluster, and in the replicated topology its cluster, since none of these is in a database", () => {
|
|
192
|
+
const cluster = { kind: "cluster" as const, cluster: "main" };
|
|
193
|
+
expect(renderStatement("CREATE USER app IDENTIFIED WITH ssl_certificate CN 'app'", cluster)).toBe("CREATE USER app ON CLUSTER `main` IDENTIFIED WITH ssl_certificate CN 'app'");
|
|
194
|
+
expect(renderStatement("CREATE ROLE reader", cluster)).toBe("CREATE ROLE reader ON CLUSTER `main`");
|
|
195
|
+
expect(renderStatement("CREATE ROW POLICY p ON shop.t USING 1", cluster)).toBe("CREATE ROW POLICY p ON CLUSTER `main` ON shop.t USING 1");
|
|
196
|
+
expect(renderStatement("CREATE ROW POLICY OR REPLACE p ON shop.t USING 1", { kind: "replicated", cluster: "main" })).toBe("CREATE ROW POLICY OR REPLACE p ON CLUSTER `main` ON shop.t USING 1");
|
|
197
|
+
expect(renderStatement("GRANT SELECT ON `shop`.* TO `r`", cluster)).toBe("GRANT ON CLUSTER `main` SELECT ON `shop`.* TO `r`");
|
|
198
|
+
expect(renderStatement("REVOKE ALTER ON `shop`.* FROM `r`", cluster)).toBe("REVOKE ON CLUSTER `main` ALTER ON `shop`.* FROM `r`");
|
|
199
|
+
expect(renderStatement("ALTER USER `app` HOST LOCAL", cluster)).toBe("ALTER USER `app` ON CLUSTER `main` HOST LOCAL");
|
|
200
|
+
expect(renderStatement("ALTER ROLE `r` SETTINGS NONE", { kind: "replicated" })).toBe("ALTER ROLE `r` SETTINGS NONE");
|
|
201
|
+
expect(renderStatement("GRANT SELECT ON `shop`.* TO `r`", { kind: "single" })).toBe("GRANT SELECT ON `shop`.* TO `r`");
|
|
202
|
+
});
|
|
203
|
+
});
|