@intentius/chant-lexicon-sql 0.116.0 → 0.117.0
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/codegen/registry.d.ts.map +1 -1
- package/dist/config.d.ts +20 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +5 -5
- package/dist/lint/rules/sqlpg001.d.ts.map +1 -1
- package/dist/lsp/postgres-template.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/meta.json +20 -0
- package/dist/okf/index.md +4 -0
- package/dist/okf/types/PostgresDefaultPrivileges.md +9 -0
- package/dist/okf/types/PostgresGrant.md +9 -0
- package/dist/okf/types/PostgresPolicy.md +9 -0
- package/dist/okf/types/PostgresRole.md +9 -0
- package/dist/op/activities/postgres-migration.d.ts.map +1 -1
- package/dist/op/activity-contracts.d.ts +10 -0
- package/dist/op/activity-contracts.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/postgres/access/acl.d.ts +119 -0
- package/dist/postgres/access/acl.d.ts.map +1 -0
- package/dist/postgres/access/import.d.ts +13 -0
- package/dist/postgres/access/import.d.ts.map +1 -0
- package/dist/postgres/access/live.d.ts +24 -0
- package/dist/postgres/access/live.d.ts.map +1 -0
- package/dist/postgres/apply/apply.d.ts.map +1 -1
- package/dist/postgres/apply/statements.d.ts +3 -0
- package/dist/postgres/apply/statements.d.ts.map +1 -1
- package/dist/postgres/entities.d.ts +106 -4
- package/dist/postgres/entities.d.ts.map +1 -1
- package/dist/postgres/entity-types.d.ts +4 -0
- package/dist/postgres/entity-types.d.ts.map +1 -1
- package/dist/postgres/import/generator.d.ts +4 -1
- package/dist/postgres/import/generator.d.ts.map +1 -1
- package/dist/postgres/import/live-export.d.ts +6 -0
- package/dist/postgres/import/live-export.d.ts.map +1 -1
- package/dist/postgres/live/bind.d.ts +2 -0
- package/dist/postgres/live/bind.d.ts.map +1 -1
- package/dist/postgres/live/catalog.d.ts +11 -2
- package/dist/postgres/live/catalog.d.ts.map +1 -1
- package/dist/postgres/live/describe-resources.d.ts +9 -0
- package/dist/postgres/live/describe-resources.d.ts.map +1 -1
- package/dist/postgres/migrate/op.d.ts +29 -0
- package/dist/postgres/migrate/op.d.ts.map +1 -1
- package/dist/postgres/parser.d.ts +76 -3
- package/dist/postgres/parser.d.ts.map +1 -1
- package/dist/postgres/plan/commands.d.ts +12 -0
- package/dist/postgres/plan/commands.d.ts.map +1 -1
- package/dist/postgres/plan/deep.d.ts.map +1 -1
- package/dist/postgres/plan/diff.d.ts +10 -1
- package/dist/postgres/plan/diff.d.ts.map +1 -1
- package/dist/postgres/plan/normalize.d.ts +8 -1
- package/dist/postgres/plan/normalize.d.ts.map +1 -1
- package/dist/postgres/plan/rules.d.ts +8 -0
- package/dist/postgres/plan/rules.d.ts.map +1 -1
- package/dist/postgres/plan/schema.d.ts +2 -1
- package/dist/postgres/plan/schema.d.ts.map +1 -1
- package/dist/postgres/plan/server-normalize.d.ts +4 -1
- package/dist/postgres/plan/server-normalize.d.ts.map +1 -1
- package/dist/postgres.d.ts +1 -1
- package/dist/postgres.d.ts.map +1 -1
- package/dist/rules/postgres-templates.ts +2 -2
- package/dist/rules/sqlpg001.ts +7 -4
- package/dist/validate.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/codegen/registry.ts +4 -0
- package/src/config.ts +10 -0
- package/src/generated/lexicon-sql.json +20 -0
- package/src/index.ts +1 -1
- package/src/lint/rules/postgres-templates.ts +2 -2
- package/src/lint/rules/sqlpg001.ts +7 -4
- package/src/lsp/postgres-template.ts +2 -0
- package/src/mcp/postgres.ts +1 -1
- package/src/migration-statements.ts +7 -0
- package/src/op/activities/postgres-migration.ts +27 -2
- package/src/op/activity-contracts.ts +1 -0
- package/src/plugin.ts +3 -0
- package/src/postgres/access/access.e2e.test.ts +191 -0
- package/src/postgres/access/access.test.ts +147 -0
- package/src/postgres/access/acl.ts +400 -0
- package/src/postgres/access/import.ts +64 -0
- package/src/postgres/access/live.ts +123 -0
- package/src/postgres/apply/apply.ts +117 -4
- package/src/postgres/apply/statements.ts +55 -9
- package/src/postgres/entities.ts +287 -11
- package/src/postgres/entity-types.ts +4 -0
- package/src/postgres/import/generator.ts +14 -4
- package/src/postgres/import/live-export.ts +12 -2
- package/src/postgres/live/bind.ts +3 -0
- package/src/postgres/live/catalog.ts +79 -3
- package/src/postgres/live/describe-resources.ts +27 -3
- package/src/postgres/migrate/migrate.test.ts +24 -1
- package/src/postgres/migrate/op.ts +80 -35
- package/src/postgres/migrate/outer.e2e.test.ts +163 -0
- package/src/postgres/parser.ts +247 -8
- package/src/postgres/plan/commands.ts +60 -7
- package/src/postgres/plan/deep.ts +24 -3
- package/src/postgres/plan/diff.ts +71 -6
- package/src/postgres/plan/normalize.ts +115 -2
- package/src/postgres/plan/rules.ts +44 -0
- package/src/postgres/plan/schema.ts +4 -2
- package/src/postgres/plan/server-normalize.ts +29 -1
- package/src/postgres.ts +11 -0
- package/src/skill-defs.ts +1 -1
- package/src/skills/chant-sql-postgres-migration.md +3 -1
- package/src/skills/chant-sql-postgres.md +9 -2
- package/src/validate.ts +4 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access as chant compares it (#3681): the privileges each role holds on each
|
|
3
|
+
* object, and the default privileges objects are created with.
|
|
4
|
+
*
|
|
5
|
+
* A `grant` declaration is a statement, not an object: `GRANT SELECT ON a, b
|
|
6
|
+
* TO r1, r2` is four privileges, and the catalog keeps no statement, only
|
|
7
|
+
* each object's access control list. So declarations and the server are both
|
|
8
|
+
* brought to the same form, one entry per object (or column) and grantee
|
|
9
|
+
* holding a set of privileges, and those entries are compared.
|
|
10
|
+
*
|
|
11
|
+
* What a declared object's entries should be:
|
|
12
|
+
*
|
|
13
|
+
* - what Postgres gives a new object of its kind, the owner's own privileges
|
|
14
|
+
* left out (a function or procedure: EXECUTE to PUBLIC; anything else:
|
|
15
|
+
* nothing);
|
|
16
|
+
* - with the declared default privileges for objects of that kind created by
|
|
17
|
+
* the role that applies, globally and in the object's schema, since chant
|
|
18
|
+
* creates it as that role;
|
|
19
|
+
* - with every declared GRANT added and every declared REVOKE taken away, in
|
|
20
|
+
* the build's order.
|
|
21
|
+
*
|
|
22
|
+
* The owner's own privileges are never compared: the owner has them by
|
|
23
|
+
* owning the object. A grant on an object the build does not declare (the
|
|
24
|
+
* `public` schema) is compared too, from what Postgres gives a new object.
|
|
25
|
+
*
|
|
26
|
+
* Default privileges are compared for the role that applies (and each role a
|
|
27
|
+
* declaration names with `FOR ROLE`): globally, where none declared means
|
|
28
|
+
* Postgres's own defaults, and per schema, where none declared means none.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { quoteIdent } from "../keywords";
|
|
32
|
+
import { PG_CLASSIFIER_RULES } from "../plan/rules";
|
|
33
|
+
import { classifiedChange } from "../../core/classifier";
|
|
34
|
+
import type { PgChange } from "../plan/diff";
|
|
35
|
+
import type { CanonicalPgObject } from "../plan/normalize";
|
|
36
|
+
|
|
37
|
+
export type AclKind = "schema" | "table" | "sequence" | "function" | "procedure" | "column" | "default";
|
|
38
|
+
|
|
39
|
+
/** The kinds of object default privileges are for. */
|
|
40
|
+
export type DefaultObjects = "tables" | "sequences" | "functions" | "types" | "schemas";
|
|
41
|
+
|
|
42
|
+
/** What an entry is on. */
|
|
43
|
+
export interface AclTarget {
|
|
44
|
+
kind: AclKind;
|
|
45
|
+
/** The object's canonical name: `app`, `app.orders`, `app.f(integer)`. Empty for default privileges. */
|
|
46
|
+
name: string;
|
|
47
|
+
column?: string;
|
|
48
|
+
/** Default privileges: whose new objects (`""` for the role that applies), in which schema (undefined: every schema), of which kind. */
|
|
49
|
+
role?: string;
|
|
50
|
+
schema?: string;
|
|
51
|
+
objects?: DefaultObjects;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** One grantee's privileges on one target: each privilege, and whether it was granted WITH GRANT OPTION. */
|
|
55
|
+
export interface AclEntry {
|
|
56
|
+
target: AclTarget;
|
|
57
|
+
grantee: string;
|
|
58
|
+
privileges: Map<string, boolean>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type AccessState = Map<string, AclEntry>;
|
|
62
|
+
|
|
63
|
+
/** An access change, with the statements that make it and the grant declarations behind it. */
|
|
64
|
+
export interface AccessChange {
|
|
65
|
+
change: PgChange;
|
|
66
|
+
target: AclTarget;
|
|
67
|
+
grantee: string;
|
|
68
|
+
sql: string[];
|
|
69
|
+
/** The export names of the declarations that grant or revoke on this target to this grantee. */
|
|
70
|
+
exports: string[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const SELF = "";
|
|
74
|
+
|
|
75
|
+
/** A target's key: relations (tables, views, sequences) share one namespace, as in the catalog. */
|
|
76
|
+
export function targetKey(t: AclTarget): string {
|
|
77
|
+
switch (t.kind) {
|
|
78
|
+
case "schema":
|
|
79
|
+
return `schema ${t.name}`;
|
|
80
|
+
case "table":
|
|
81
|
+
case "sequence":
|
|
82
|
+
return `relation ${t.name}`;
|
|
83
|
+
case "column":
|
|
84
|
+
return `relation ${t.name} (${quoteIdent(t.column ?? "")})`;
|
|
85
|
+
case "function":
|
|
86
|
+
case "procedure":
|
|
87
|
+
return `routine ${t.name}`;
|
|
88
|
+
case "default":
|
|
89
|
+
return `default privileges${t.role ? ` for ${quoteIdent(t.role)}` : ""}${t.schema !== undefined ? ` in ${quoteIdent(t.schema)}` : ""} on ${t.objects}`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const entryKey = (t: AclTarget, grantee: string): string => `${targetKey(t)} TO ${grantee === "public" ? "PUBLIC" : quoteIdent(grantee)}`;
|
|
94
|
+
|
|
95
|
+
/** Every privilege of a kind of object, as ALL PRIVILEGES grants them. MAINTAIN is a table's from 17. */
|
|
96
|
+
export function allPrivileges(kind: AclKind | DefaultObjects, major: number): string[] {
|
|
97
|
+
switch (kind) {
|
|
98
|
+
case "table":
|
|
99
|
+
case "tables":
|
|
100
|
+
return ["select", "insert", "update", "delete", "truncate", "references", "trigger", ...(major >= 17 ? ["maintain"] : [])];
|
|
101
|
+
case "sequence":
|
|
102
|
+
case "sequences":
|
|
103
|
+
return ["usage", "select", "update"];
|
|
104
|
+
case "schema":
|
|
105
|
+
case "schemas":
|
|
106
|
+
return ["usage", "create"];
|
|
107
|
+
case "function":
|
|
108
|
+
case "procedure":
|
|
109
|
+
case "functions":
|
|
110
|
+
return ["execute"];
|
|
111
|
+
case "types":
|
|
112
|
+
return ["usage"];
|
|
113
|
+
case "column":
|
|
114
|
+
return ["select", "insert", "update", "references"];
|
|
115
|
+
case "default":
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** What Postgres gives a new object of a kind, the owner's own privileges left out: EXECUTE on a routine and USAGE on a type to PUBLIC. */
|
|
121
|
+
export function builtinEntries(kind: AclKind | DefaultObjects): Array<{ grantee: string; privilege: string }> {
|
|
122
|
+
if (kind === "function" || kind === "procedure" || kind === "functions") return [{ grantee: "public", privilege: "execute" }];
|
|
123
|
+
if (kind === "types") return [{ grantee: "public", privilege: "usage" }];
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The default-privileges kind an object kind is created as. */
|
|
128
|
+
const defaultObjectsOf = (kind: AclKind): DefaultObjects | undefined =>
|
|
129
|
+
kind === "table" ? "tables" : kind === "sequence" ? "sequences" : kind === "function" || kind === "procedure" ? "functions" : kind === "schema" ? "schemas" : undefined;
|
|
130
|
+
|
|
131
|
+
function add(state: AccessState, target: AclTarget, grantee: string, privilege: string, grantable: boolean): void {
|
|
132
|
+
const key = entryKey(target, grantee);
|
|
133
|
+
let e = state.get(key);
|
|
134
|
+
if (!e) state.set(key, (e = { target, grantee, privileges: new Map() }));
|
|
135
|
+
e.privileges.set(privilege, grantable || e.privileges.get(privilege) === true);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function remove(state: AccessState, target: AclTarget, grantee: string, privilege: string, grantOptionOnly: boolean): void {
|
|
139
|
+
const e = state.get(entryKey(target, grantee));
|
|
140
|
+
if (!e || !e.privileges.has(privilege)) return;
|
|
141
|
+
if (grantOptionOnly) e.privileges.set(privilege, false);
|
|
142
|
+
else e.privileges.delete(privilege);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The state with every empty entry left out. */
|
|
146
|
+
const compact = (state: AccessState): AccessState => new Map([...state].filter(([, e]) => e.privileges.size > 0));
|
|
147
|
+
|
|
148
|
+
interface GrantFields {
|
|
149
|
+
action: "grant" | "revoke";
|
|
150
|
+
on: string;
|
|
151
|
+
objects?: string[];
|
|
152
|
+
privileges: Array<{ privilege: string; columns?: string[] }>;
|
|
153
|
+
grantees: string[];
|
|
154
|
+
forRoles?: string[];
|
|
155
|
+
inSchemas?: string[];
|
|
156
|
+
withGrantOption?: boolean;
|
|
157
|
+
grantOptionFor?: boolean;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface DeclaredAccess {
|
|
161
|
+
state: AccessState;
|
|
162
|
+
/** Every target the declarations decide: the declared objects and what the grants name. */
|
|
163
|
+
targets: AclTarget[];
|
|
164
|
+
/** The export names of the declarations behind each entry. */
|
|
165
|
+
exportsOf: Map<string, string[]>;
|
|
166
|
+
/** The roles `FOR ROLE` names, whose default privileges are compared too. */
|
|
167
|
+
forRoles: string[];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The access a set of declarations adds up to. `objects` are canonical, in
|
|
172
|
+
* the build's order, keyed by export name. `self` is the role that applies,
|
|
173
|
+
* when known: a declaration naming it with `FOR ROLE` is its own default
|
|
174
|
+
* privileges, and a grant to it is the owner's, which is never compared.
|
|
175
|
+
*/
|
|
176
|
+
export function declaredAccess(objects: ReadonlyArray<{ key: string; canonical: CanonicalPgObject }>, opts: { major: number; self?: string }): DeclaredAccess {
|
|
177
|
+
const state: AccessState = new Map();
|
|
178
|
+
const exportsOf = new Map<string, string[]>();
|
|
179
|
+
const targets = new Map<string, AclTarget>();
|
|
180
|
+
const relationKind = new Map<string, "table" | "sequence">();
|
|
181
|
+
const routineKind = new Map<string, "function" | "procedure">();
|
|
182
|
+
const forRoles = new Set<string>();
|
|
183
|
+
const roleOf = (r: string | undefined) => (r === undefined || r === opts.self ? SELF : r);
|
|
184
|
+
const note = (key: string, exportName: string) => exportsOf.set(key, [...new Set([...(exportsOf.get(key) ?? []), exportName])]);
|
|
185
|
+
|
|
186
|
+
// The objects themselves.
|
|
187
|
+
const declaredTargets: AclTarget[] = [];
|
|
188
|
+
for (const { canonical: o } of objects) {
|
|
189
|
+
const name = `${o.schema ? `${quoteIdent(o.schema)}.` : ""}${quoteIdent(o.name)}`;
|
|
190
|
+
if (o.kind === "schema") declaredTargets.push({ kind: "schema", name: quoteIdent(o.name) });
|
|
191
|
+
else if (o.kind === "table" || o.kind === "view" || o.kind === "materializedView") {
|
|
192
|
+
relationKind.set(name, "table");
|
|
193
|
+
declaredTargets.push({ kind: "table", name });
|
|
194
|
+
} else if (o.kind === "sequence") {
|
|
195
|
+
relationKind.set(name, "sequence");
|
|
196
|
+
declaredTargets.push({ kind: "sequence", name });
|
|
197
|
+
} else if (o.kind === "function" || o.kind === "procedure") {
|
|
198
|
+
routineKind.set(`${name}${o.signature ?? ""}`, o.kind);
|
|
199
|
+
declaredTargets.push({ kind: o.kind, name: `${name}${o.signature ?? ""}` });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Default privileges, per role, schema and kind of object.
|
|
204
|
+
const defaults: AccessState = new Map();
|
|
205
|
+
const declaredDefaults = new Set<string>();
|
|
206
|
+
for (const { key: exportName, canonical: o } of objects) {
|
|
207
|
+
if (o.kind !== "defaultPrivileges") continue;
|
|
208
|
+
const g = o.fields.grant as GrantFields;
|
|
209
|
+
const on = g.on as DefaultObjects;
|
|
210
|
+
for (const r of g.forRoles ?? [undefined]) {
|
|
211
|
+
const role = roleOf(r);
|
|
212
|
+
if (role !== SELF) forRoles.add(role);
|
|
213
|
+
for (const schema of g.inSchemas ?? [undefined]) {
|
|
214
|
+
const target: AclTarget = { kind: "default", name: "", role, ...(schema !== undefined ? { schema } : {}), objects: on };
|
|
215
|
+
const tk = targetKey(target);
|
|
216
|
+
if (!declaredDefaults.has(tk)) {
|
|
217
|
+
declaredDefaults.add(tk);
|
|
218
|
+
targets.set(tk, target);
|
|
219
|
+
// A global entry starts from Postgres's own defaults; a schema's from nothing.
|
|
220
|
+
if (schema === undefined) for (const b of builtinEntries(on)) add(defaults, target, b.grantee, b.privilege, false);
|
|
221
|
+
}
|
|
222
|
+
for (const grantee of g.grantees) {
|
|
223
|
+
for (const p of g.privileges) {
|
|
224
|
+
for (const privilege of p.privilege === "all" ? allPrivileges(on, opts.major) : [p.privilege]) {
|
|
225
|
+
if (g.action === "grant") add(defaults, target, grantee, privilege, g.withGrantOption === true);
|
|
226
|
+
else remove(defaults, target, grantee, privilege, g.grantOptionFor === true);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
note(entryKey(target, grantee), exportName);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
for (const [k, e] of defaults) state.set(k, e);
|
|
235
|
+
|
|
236
|
+
/** What an object chant creates is given: Postgres's defaults, or the declared global ones, and the declared ones for its schema. */
|
|
237
|
+
const created = (t: AclTarget, schema: string | undefined) => {
|
|
238
|
+
const objectsKind = defaultObjectsOf(t.kind);
|
|
239
|
+
const global = objectsKind ? [...defaults.values()].filter((e) => e.target.role === SELF && e.target.schema === undefined && e.target.objects === objectsKind) : [];
|
|
240
|
+
const anyGlobal = objectsKind !== undefined && declaredDefaults.has(targetKey({ kind: "default", name: "", role: SELF, objects: objectsKind }));
|
|
241
|
+
if (!anyGlobal) for (const b of builtinEntries(t.kind)) add(state, t, b.grantee, b.privilege, false);
|
|
242
|
+
for (const e of global) for (const [p, g] of e.privileges) add(state, t, e.grantee, p, g);
|
|
243
|
+
if (objectsKind && schema !== undefined && objectsKind !== "schemas") {
|
|
244
|
+
for (const e of defaults.values()) {
|
|
245
|
+
if (e.target.role === SELF && e.target.schema === schema && e.target.objects === objectsKind) for (const [p, g] of e.privileges) add(state, t, e.grantee, p, g);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
for (const t of declaredTargets) {
|
|
250
|
+
targets.set(targetKey(t), t);
|
|
251
|
+
const schema = t.kind === "schema" ? undefined : unquotedSchema(t.name);
|
|
252
|
+
created(t, schema);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Grants and revokes, in the build's order.
|
|
256
|
+
for (const { key: exportName, canonical: o } of objects) {
|
|
257
|
+
if (o.kind !== "grant") continue;
|
|
258
|
+
const g = o.fields.grant as GrantFields;
|
|
259
|
+
for (const name of g.objects ?? []) {
|
|
260
|
+
const kind: AclKind =
|
|
261
|
+
g.on === "schema"
|
|
262
|
+
? "schema"
|
|
263
|
+
: g.on === "sequence"
|
|
264
|
+
? "sequence"
|
|
265
|
+
: g.on === "table"
|
|
266
|
+
? (relationKind.get(name) ?? "table")
|
|
267
|
+
: g.on === "procedure"
|
|
268
|
+
? "procedure"
|
|
269
|
+
: g.on === "function"
|
|
270
|
+
? "function"
|
|
271
|
+
: (routineKind.get(name) ?? "function");
|
|
272
|
+
const target: AclTarget = { kind, name };
|
|
273
|
+
const tk = targetKey(target);
|
|
274
|
+
if (!targets.has(tk)) {
|
|
275
|
+
// An object the build does not declare starts from what Postgres gives a new one.
|
|
276
|
+
targets.set(tk, target);
|
|
277
|
+
for (const b of builtinEntries(kind)) add(state, target, b.grantee, b.privilege, false);
|
|
278
|
+
}
|
|
279
|
+
for (const grantee of g.grantees) {
|
|
280
|
+
for (const p of g.privileges) {
|
|
281
|
+
const on: AclTarget[] = p.columns ? p.columns.map((column) => ({ kind: "column" as const, name, column })) : [target];
|
|
282
|
+
for (const t of on) {
|
|
283
|
+
if (t.kind === "column") targets.set(targetKey(t), t);
|
|
284
|
+
for (const privilege of p.privilege === "all" ? allPrivileges(t.kind, opts.major) : [p.privilege]) {
|
|
285
|
+
if (g.action === "grant") add(state, t, grantee, privilege, g.withGrantOption === true);
|
|
286
|
+
else remove(state, t, grantee, privilege, g.grantOptionFor === true);
|
|
287
|
+
}
|
|
288
|
+
note(entryKey(t, grantee), exportName);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// A grant to the role that applies is the owner's, which owning gives.
|
|
296
|
+
const out = compact(new Map([...state].filter(([, e]) => opts.self === undefined || e.grantee !== opts.self)));
|
|
297
|
+
return { state: out, targets: [...targets.values()], exportsOf, forRoles: [...forRoles].sort() };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** The schema of a canonical qualified name, unquoted. */
|
|
301
|
+
function unquotedSchema(name: string): string | undefined {
|
|
302
|
+
const m = /^("(?:[^"]|"")*"|[^."]+)\./.exec(name);
|
|
303
|
+
if (!m) return undefined;
|
|
304
|
+
return m[1]!.startsWith('"') ? m[1]!.slice(1, -1).replace(/""/g, '"') : m[1]!;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const show = (privileges: ReadonlyMap<string, boolean> | undefined): string | undefined =>
|
|
308
|
+
privileges && privileges.size > 0 ? [...privileges].sort(([a], [b]) => (a < b ? -1 : 1)).map(([p, g]) => `${p}${g ? "*" : ""}`).join(", ") : undefined;
|
|
309
|
+
|
|
310
|
+
const ON: Record<Exclude<AclKind, "default" | "column">, string> = { schema: "SCHEMA", table: "TABLE", sequence: "SEQUENCE", function: "FUNCTION", procedure: "PROCEDURE" };
|
|
311
|
+
|
|
312
|
+
/** The `ON ...` and grantee of an entry's statements. */
|
|
313
|
+
function onClause(t: AclTarget): string {
|
|
314
|
+
if (t.kind === "column" || t.kind === "default") return "";
|
|
315
|
+
return `ON ${ON[t.kind]} ${t.name}`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const granteeSql = (g: string) => (g === "public" ? "PUBLIC" : quoteIdent(g));
|
|
319
|
+
|
|
320
|
+
/** One GRANT or REVOKE for a set of privileges on one entry's target. */
|
|
321
|
+
function statement(t: AclTarget, grantee: string, action: "grant" | "revoke" | "revoke-option", privileges: string[], withGrantOption = false): string {
|
|
322
|
+
const list = privileges.map((p) => p.toUpperCase()).join(", ");
|
|
323
|
+
const verb = action === "grant" ? "GRANT" : action === "revoke" ? "REVOKE" : "REVOKE GRANT OPTION FOR";
|
|
324
|
+
const to = action === "grant" ? "TO" : "FROM";
|
|
325
|
+
const tail = action === "grant" && withGrantOption ? " WITH GRANT OPTION" : "";
|
|
326
|
+
if (t.kind === "column") return `${verb} ${privileges.map((p) => `${p.toUpperCase()} (${quoteIdent(t.column ?? "")})`).join(", ")} ON TABLE ${t.name} ${to} ${granteeSql(grantee)}${tail}`;
|
|
327
|
+
if (t.kind === "default") {
|
|
328
|
+
const head = `ALTER DEFAULT PRIVILEGES${t.role ? ` FOR ROLE ${quoteIdent(t.role)}` : ""}${t.schema !== undefined ? ` IN SCHEMA ${quoteIdent(t.schema)}` : ""}`;
|
|
329
|
+
return `${head} ${verb} ${list} ON ${String(t.objects).toUpperCase()} ${to} ${granteeSql(grantee)}${tail}`;
|
|
330
|
+
}
|
|
331
|
+
return `${verb} ${list} ${onClause(t)} ${to} ${granteeSql(grantee)}${tail}`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* The changes from `before` to `after`, one per entry and direction, each
|
|
336
|
+
* with its statements: revokes first (a privilege, then a grant option), then
|
|
337
|
+
* grants (a grant option on its own with WITH GRANT OPTION).
|
|
338
|
+
*/
|
|
339
|
+
export function diffAccess(before: AccessState, after: AccessState, exportsOf: ReadonlyMap<string, string[]> = new Map()): AccessChange[] {
|
|
340
|
+
const out: AccessChange[] = [];
|
|
341
|
+
const keys = [...new Set([...before.keys(), ...after.keys()])].sort();
|
|
342
|
+
for (const key of keys) {
|
|
343
|
+
const b = before.get(key);
|
|
344
|
+
const a = after.get(key);
|
|
345
|
+
const target = (a ?? b)!.target;
|
|
346
|
+
const grantee = (a ?? b)!.grantee;
|
|
347
|
+
const bp = b?.privileges ?? new Map<string, boolean>();
|
|
348
|
+
const ap = a?.privileges ?? new Map<string, boolean>();
|
|
349
|
+
const revoke = [...bp.keys()].filter((p) => !ap.has(p)).sort();
|
|
350
|
+
const revokeOption = [...bp].filter(([p, g]) => g && ap.get(p) === false).map(([p]) => p).sort();
|
|
351
|
+
const grant = [...ap].filter(([p, g]) => !bp.has(p) || (g && bp.get(p) === false)).map(([p]) => p).sort();
|
|
352
|
+
if (revoke.length + revokeOption.length + grant.length === 0) continue;
|
|
353
|
+
const exports = exportsOf.get(key) ?? [];
|
|
354
|
+
const rule = target.kind === "default" ? "SQLPG298" : revoke.length + revokeOption.length > 0 && grant.length === 0 ? "SQLPG297" : "SQLPG296";
|
|
355
|
+
const sql: string[] = [];
|
|
356
|
+
if (revoke.length > 0) sql.push(statement(target, grantee, "revoke", revoke));
|
|
357
|
+
if (revokeOption.length > 0) sql.push(statement(target, grantee, "revoke-option", revokeOption));
|
|
358
|
+
const plain = grant.filter((p) => ap.get(p) === false);
|
|
359
|
+
const withOption = grant.filter((p) => ap.get(p) === true);
|
|
360
|
+
if (plain.length > 0) sql.push(statement(target, grantee, "grant", plain));
|
|
361
|
+
if (withOption.length > 0) sql.push(statement(target, grantee, "grant", withOption, true));
|
|
362
|
+
const change = classifiedChange(PG_CLASSIFIER_RULES, `acl ${key}`, "privileges", rule, show(bp), show(ap), revoke.length > 0 && grant.length > 0 ? { note: `revokes ${revoke.join(", ")}` } : {});
|
|
363
|
+
out.push({ change, target, grantee, sql, exports });
|
|
364
|
+
}
|
|
365
|
+
return out;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** What a server holds: its entries, which objects exist, and for which kinds the role that applies has global default privileges of its own. */
|
|
369
|
+
export interface LiveAccess {
|
|
370
|
+
state: AccessState;
|
|
371
|
+
/** The target keys of the objects that exist. */
|
|
372
|
+
present: Set<string>;
|
|
373
|
+
/** The kinds of object the role that applies has a global `pg_default_acl` row for. */
|
|
374
|
+
globalDefaults: Set<DefaultObjects>;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* The entries the server would give each declared object that does not exist
|
|
379
|
+
* yet: Postgres's defaults, or the role's global default privileges, and its
|
|
380
|
+
* default privileges in the object's schema.
|
|
381
|
+
*/
|
|
382
|
+
export function predictedAccess(missing: readonly AclTarget[], live: LiveAccess): AccessState {
|
|
383
|
+
const out: AccessState = new Map();
|
|
384
|
+
const defaults = [...live.state.values()].filter((e) => e.target.kind === "default" && e.target.role === SELF);
|
|
385
|
+
for (const t of missing) {
|
|
386
|
+
const objectsKind = defaultObjectsOf(t.kind);
|
|
387
|
+
if (!objectsKind) continue;
|
|
388
|
+
if (!live.globalDefaults.has(objectsKind)) for (const b of builtinEntries(t.kind)) add(out, t, b.grantee, b.privilege, false);
|
|
389
|
+
const schema = t.kind === "schema" ? undefined : unquotedSchema(t.name);
|
|
390
|
+
for (const e of defaults) {
|
|
391
|
+
if (e.target.objects !== objectsKind) continue;
|
|
392
|
+
if (e.target.schema === undefined || (e.target.schema === schema && objectsKind !== "schemas")) for (const [p, g] of e.privileges) add(out, t, e.grantee, p, g);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return compact(out);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Adds an entry, for the live reader. */
|
|
399
|
+
export const addEntry = add;
|
|
400
|
+
export { SELF as APPLYING_ROLE };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import's access (#3681): the privileges on the imported objects, and the
|
|
3
|
+
* default privileges of the role that reads, as `grant` declarations. Each
|
|
4
|
+
* is the GRANT, REVOKE or ALTER DEFAULT PRIVILEGES that takes what Postgres
|
|
5
|
+
* gives a new object (EXECUTE to PUBLIC on a routine, nothing on the rest) to
|
|
6
|
+
* what the server holds, one per object and grantee, so the import plans as
|
|
7
|
+
* no change.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { PostgresClient } from "../live/client";
|
|
11
|
+
import type { LivePgObject } from "../live/catalog";
|
|
12
|
+
import type { ImportedPgObject } from "../import/ir";
|
|
13
|
+
import { POSTGRES_ENTITY_TYPES } from "../entity-types";
|
|
14
|
+
import { quoteIdent } from "../keywords";
|
|
15
|
+
import { builtinEntries, diffAccess, entryKey, targetKey, type AccessState, type AclTarget } from "./acl";
|
|
16
|
+
import { readLiveAccess } from "./live";
|
|
17
|
+
|
|
18
|
+
/** The access targets of imported objects, named as the catalog read names them. */
|
|
19
|
+
function targetsOf(objects: readonly LivePgObject[]): AclTarget[] {
|
|
20
|
+
const out: AclTarget[] = [];
|
|
21
|
+
for (const o of objects) {
|
|
22
|
+
const name = `${o.schema ? `${quoteIdent(o.schema)}.` : ""}${quoteIdent(o.name)}`;
|
|
23
|
+
if (o.type === POSTGRES_ENTITY_TYPES.schema) out.push({ kind: "schema", name: quoteIdent(o.name) });
|
|
24
|
+
else if (o.type === POSTGRES_ENTITY_TYPES.table || o.type === POSTGRES_ENTITY_TYPES.view || o.type === POSTGRES_ENTITY_TYPES.materializedView) out.push({ kind: "table", name });
|
|
25
|
+
else if (o.type === POSTGRES_ENTITY_TYPES.sequence) out.push({ kind: "sequence", name });
|
|
26
|
+
else if (o.type === POSTGRES_ENTITY_TYPES.function || o.type === POSTGRES_ENTITY_TYPES.procedure) {
|
|
27
|
+
out.push({ kind: o.type === POSTGRES_ENTITY_TYPES.function ? "function" : "procedure", name: `${name}${o.signature ?? ""}` });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const word = (s: string) => s.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
34
|
+
|
|
35
|
+
export async function importedAccess(client: PostgresClient, objects: readonly LivePgObject[], schemas: readonly string[] | undefined): Promise<ImportedPgObject[]> {
|
|
36
|
+
const targets = targetsOf(objects);
|
|
37
|
+
const live = await readLiveAccess(client, targets, schemas ? { schemas } : {});
|
|
38
|
+
// What a new object is given: the starting point each declaration takes to the server's.
|
|
39
|
+
const baseline: AccessState = new Map();
|
|
40
|
+
for (const t of targets) {
|
|
41
|
+
for (const b of builtinEntries(t.kind)) baseline.set(entryKey(t, b.grantee), { target: t, grantee: b.grantee, privileges: new Map([[b.privilege, false]]) });
|
|
42
|
+
}
|
|
43
|
+
for (const e of live.state.values()) {
|
|
44
|
+
if (e.target.kind !== "default" || e.target.schema !== undefined) continue;
|
|
45
|
+
for (const b of builtinEntries(e.target.objects!)) {
|
|
46
|
+
const k = entryKey(e.target, b.grantee);
|
|
47
|
+
if (!baseline.has(k)) baseline.set(k, { target: e.target, grantee: b.grantee, privileges: new Map([[b.privilege, false]]) });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Only global default rows that exist on the server are compared from Postgres's own defaults.
|
|
51
|
+
const globalRows = new Set([...live.state.values()].filter((e) => e.target.kind === "default").map((e) => targetKey(e.target)));
|
|
52
|
+
const from = new Map([...baseline].filter(([, e]) => e.target.kind !== "default" || globalRows.has(targetKey(e.target))));
|
|
53
|
+
return diffAccess(from, live.state).flatMap((c) =>
|
|
54
|
+
c.sql.map((sql, i) => {
|
|
55
|
+
const defaults = c.target.kind === "default";
|
|
56
|
+
const on = defaults ? `default_${c.target.schema ?? "all"}_${c.target.objects}` : c.target.name.replace(/\(.*$/, "").split(".").pop()!;
|
|
57
|
+
return {
|
|
58
|
+
type: defaults ? POSTGRES_ENTITY_TYPES.defaultPrivileges : POSTGRES_ENTITY_TYPES.grant,
|
|
59
|
+
name: word(`${/^REVOKE|^ALTER DEFAULT PRIVILEGES.* REVOKE /.test(sql) ? "revoke" : "grant"}_${on}${c.target.column ? `_${c.target.column}` : ""}_${c.grantee}${i > 0 ? `_${i}` : ""}`),
|
|
60
|
+
ddl: sql,
|
|
61
|
+
};
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading access from a live server (#3681): each target's access control
|
|
3
|
+
* list, exploded into one entry per grantee (`aclexplode`, with
|
|
4
|
+
* `acldefault` where the list is NULL, which means Postgres's own defaults),
|
|
5
|
+
* the owner's own privileges left out; every column's list of a relation
|
|
6
|
+
* read; and the default privileges (`pg_default_acl`) of the role that runs
|
|
7
|
+
* the read and of the roles the declarations name with `FOR ROLE`, globally
|
|
8
|
+
* and in the schemas in scope.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { PostgresClient } from "../live/client";
|
|
12
|
+
import { addEntry, APPLYING_ROLE, builtinEntries, targetKey, type AccessState, type AclTarget, type DefaultObjects, type LiveAccess } from "./acl";
|
|
13
|
+
|
|
14
|
+
type Row = Record<string, unknown>;
|
|
15
|
+
|
|
16
|
+
const OBJTYPE: Readonly<Record<string, DefaultObjects>> = { r: "tables", S: "sequences", f: "functions", T: "types", n: "schemas" };
|
|
17
|
+
|
|
18
|
+
const GRANTEE = "CASE WHEN a.grantee = 0 THEN 'public' ELSE pg_catalog.pg_get_userbyid(a.grantee) END";
|
|
19
|
+
|
|
20
|
+
export interface LiveAccessScope {
|
|
21
|
+
/** The schemas whose default privileges are read; undefined is every schema. */
|
|
22
|
+
schemas?: readonly string[];
|
|
23
|
+
/** Roles besides the one that runs the read whose default privileges are read. */
|
|
24
|
+
forRoles?: readonly string[];
|
|
25
|
+
/** Global default-privilege targets the declarations decide: one with no row on the server holds Postgres's own defaults. */
|
|
26
|
+
declaredDefaults?: readonly AclTarget[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The access the server holds on `targets`, and the default privileges in scope. */
|
|
30
|
+
export async function readLiveAccess(client: PostgresClient, targets: readonly AclTarget[], scope: LiveAccessScope = {}): Promise<LiveAccess & { self: string }> {
|
|
31
|
+
const state: AccessState = new Map();
|
|
32
|
+
const present = new Set<string>();
|
|
33
|
+
const [me] = await client.query<Row>("SELECT current_user AS self");
|
|
34
|
+
const self = String(me?.self ?? "");
|
|
35
|
+
const entry = (t: AclTarget, r: Row) => addEntry(state, t, String(r.grantee), String(r.privilege).toLowerCase(), r.grantable === true);
|
|
36
|
+
|
|
37
|
+
const names = (kinds: readonly string[]) => [...new Set(targets.filter((t) => kinds.includes(t.kind)).map((t) => t.name))];
|
|
38
|
+
|
|
39
|
+
const schemas = names(["schema"]);
|
|
40
|
+
if (schemas.length > 0) {
|
|
41
|
+
const rows = await client.query<Row>(
|
|
42
|
+
`SELECT x AS target, ${GRANTEE} AS grantee, a.privilege_type AS privilege, a.is_grantable AS grantable
|
|
43
|
+
FROM pg_catalog.unnest($1::text[]) x JOIN pg_catalog.pg_namespace n ON n.oid = pg_catalog.to_regnamespace(x)
|
|
44
|
+
LEFT JOIN LATERAL pg_catalog.aclexplode(COALESCE(n.nspacl, pg_catalog.acldefault('n'::"char", n.nspowner))) a ON a.grantee <> n.nspowner`,
|
|
45
|
+
[schemas],
|
|
46
|
+
);
|
|
47
|
+
for (const r of rows) {
|
|
48
|
+
const t: AclTarget = { kind: "schema", name: String(r.target) };
|
|
49
|
+
present.add(targetKey(t));
|
|
50
|
+
if (r.privilege !== null) entry(t, r);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const relations = names(["table", "sequence", "column"]);
|
|
55
|
+
if (relations.length > 0) {
|
|
56
|
+
const rows = await client.query<Row>(
|
|
57
|
+
`SELECT x AS target, c.relkind AS kind, ${GRANTEE} AS grantee, a.privilege_type AS privilege, a.is_grantable AS grantable
|
|
58
|
+
FROM pg_catalog.unnest($1::text[]) x JOIN pg_catalog.pg_class c ON c.oid = pg_catalog.to_regclass(x)
|
|
59
|
+
LEFT JOIN LATERAL pg_catalog.aclexplode(COALESCE(c.relacl, pg_catalog.acldefault((CASE WHEN c.relkind = 'S' THEN 's' ELSE 'r' END)::"char", c.relowner))) a ON a.grantee <> c.relowner`,
|
|
60
|
+
[relations],
|
|
61
|
+
);
|
|
62
|
+
for (const r of rows) {
|
|
63
|
+
const t: AclTarget = { kind: r.kind === "S" ? "sequence" : "table", name: String(r.target) };
|
|
64
|
+
present.add(targetKey(t));
|
|
65
|
+
if (r.privilege !== null) entry(t, r);
|
|
66
|
+
}
|
|
67
|
+
const columns = await client.query<Row>(
|
|
68
|
+
`SELECT x AS target, att.attname AS column, ${GRANTEE} AS grantee, a.privilege_type AS privilege, a.is_grantable AS grantable
|
|
69
|
+
FROM pg_catalog.unnest($1::text[]) x JOIN pg_catalog.pg_class c ON c.oid = pg_catalog.to_regclass(x)
|
|
70
|
+
JOIN pg_catalog.pg_attribute att ON att.attrelid = c.oid AND att.attnum > 0 AND NOT att.attisdropped AND att.attacl IS NOT NULL
|
|
71
|
+
CROSS JOIN LATERAL pg_catalog.aclexplode(att.attacl) a
|
|
72
|
+
WHERE a.grantee <> c.relowner`,
|
|
73
|
+
[relations],
|
|
74
|
+
);
|
|
75
|
+
for (const r of columns) entry({ kind: "column", name: String(r.target), column: String(r.column) }, r);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const routines = names(["function", "procedure"]);
|
|
79
|
+
if (routines.length > 0) {
|
|
80
|
+
const rows = await client.query<Row>(
|
|
81
|
+
`SELECT x AS target, p.prokind AS kind, ${GRANTEE} AS grantee, a.privilege_type AS privilege, a.is_grantable AS grantable
|
|
82
|
+
FROM pg_catalog.unnest($1::text[]) x JOIN pg_catalog.pg_proc p ON p.oid = pg_catalog.to_regprocedure(x)
|
|
83
|
+
LEFT JOIN LATERAL pg_catalog.aclexplode(COALESCE(p.proacl, pg_catalog.acldefault('f'::"char", p.proowner))) a ON a.grantee <> p.proowner`,
|
|
84
|
+
[routines],
|
|
85
|
+
);
|
|
86
|
+
for (const r of rows) {
|
|
87
|
+
const t: AclTarget = { kind: r.kind === "p" ? "procedure" : "function", name: String(r.target) };
|
|
88
|
+
present.add(targetKey(t));
|
|
89
|
+
if (r.privilege !== null) entry(t, r);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Default privileges: the reading role's (written as the role that applies) and the FOR ROLE ones.
|
|
94
|
+
const roles = [self, ...(scope.forRoles ?? [])];
|
|
95
|
+
const defaults = await client.query<Row>(
|
|
96
|
+
`SELECT pg_catalog.pg_get_userbyid(d.defaclrole) AS role, n.nspname AS schema, d.defaclobjtype AS objtype,
|
|
97
|
+
${GRANTEE} AS grantee, a.privilege_type AS privilege, a.is_grantable AS grantable
|
|
98
|
+
FROM pg_catalog.pg_default_acl d LEFT JOIN pg_catalog.pg_namespace n ON n.oid = d.defaclnamespace
|
|
99
|
+
LEFT JOIN LATERAL pg_catalog.aclexplode(d.defaclacl) a ON a.grantee <> d.defaclrole
|
|
100
|
+
WHERE pg_catalog.pg_get_userbyid(d.defaclrole) = ANY($1::text[])
|
|
101
|
+
AND (d.defaclnamespace = 0${scope.schemas ? " OR n.nspname = ANY($2::text[])" : ""})`,
|
|
102
|
+
scope.schemas ? [roles, [...scope.schemas]] : [roles],
|
|
103
|
+
);
|
|
104
|
+
const globalRows = new Set<string>();
|
|
105
|
+
const globalDefaults = new Set<DefaultObjects>();
|
|
106
|
+
for (const r of defaults) {
|
|
107
|
+
const objects = OBJTYPE[String(r.objtype)];
|
|
108
|
+
if (!objects) continue;
|
|
109
|
+
const role = String(r.role) === self ? APPLYING_ROLE : String(r.role);
|
|
110
|
+
const t: AclTarget = { kind: "default", name: "", role, ...(r.schema !== null && r.schema !== undefined ? { schema: String(r.schema) } : {}), objects };
|
|
111
|
+
if (t.schema === undefined) {
|
|
112
|
+
globalRows.add(targetKey(t));
|
|
113
|
+
if (role === APPLYING_ROLE) globalDefaults.add(objects);
|
|
114
|
+
}
|
|
115
|
+
if (r.privilege !== null) entry(t, r);
|
|
116
|
+
}
|
|
117
|
+
// A global default the declarations decide with no row on the server is Postgres's own.
|
|
118
|
+
for (const t of scope.declaredDefaults ?? []) {
|
|
119
|
+
if (t.kind !== "default" || t.schema !== undefined || globalRows.has(targetKey(t))) continue;
|
|
120
|
+
for (const b of builtinEntries(t.objects!)) addEntry(state, t, b.grantee, b.privilege, false);
|
|
121
|
+
}
|
|
122
|
+
return { state, present, globalDefaults, self };
|
|
123
|
+
}
|