@xemahq/biome-database-nest 0.8.0 → 0.9.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/README.md +21 -4
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/bootstrap.d.ts +1 -0
- package/dist/lib/bootstrap.d.ts.map +1 -1
- package/dist/lib/bootstrap.js +20 -7
- package/dist/lib/bootstrap.js.map +1 -1
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +12 -2
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/control-plane-client.d.ts +1 -0
- package/dist/lib/control-plane-client.d.ts.map +1 -1
- package/dist/lib/control-plane-client.js +10 -0
- package/dist/lib/control-plane-client.js.map +1 -1
- package/dist/lib/credential-generation.d.ts +4 -0
- package/dist/lib/credential-generation.d.ts.map +1 -0
- package/dist/lib/credential-generation.js +24 -0
- package/dist/lib/credential-generation.js.map +1 -0
- package/dist/lib/external-runtime-verifier.d.ts.map +1 -1
- package/dist/lib/external-runtime-verifier.js +586 -137
- package/dist/lib/external-runtime-verifier.js.map +1 -1
- package/dist/lib/migration-mode.d.ts +61 -1
- package/dist/lib/migration-mode.d.ts.map +1 -1
- package/dist/lib/migration-mode.js +29 -1
- package/dist/lib/migration-mode.js.map +1 -1
- package/dist/lib/nest/biome-database.module.d.ts +1 -1
- package/dist/lib/nest/biome-database.module.d.ts.map +1 -1
- package/dist/lib/nest/biome-database.module.js +3 -9
- package/dist/lib/nest/biome-database.module.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,11 +3,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.verifyExternallyManagedRuntime = verifyExternallyManagedRuntime;
|
|
4
4
|
const node_crypto_1 = require("node:crypto");
|
|
5
5
|
const pg_1 = require("pg");
|
|
6
|
+
const credential_generation_1 = require("./credential-generation");
|
|
6
7
|
const migration_mode_1 = require("./migration-mode");
|
|
7
8
|
const LOG_PREFIX = '[biome-database]';
|
|
8
9
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
9
10
|
const SHA_256_PATTERN = /^[0-9a-f]{64}$/;
|
|
10
11
|
const ALL_TABLE_DML = Object.values(migration_mode_1.RuntimeTablePrivilege);
|
|
12
|
+
const ALL_TABLE_PRIVILEGES = [
|
|
13
|
+
...ALL_TABLE_DML,
|
|
14
|
+
'TRUNCATE',
|
|
15
|
+
'REFERENCES',
|
|
16
|
+
'TRIGGER',
|
|
17
|
+
];
|
|
11
18
|
const ALL_SEQUENCE_PRIVILEGES = Object.values(migration_mode_1.RuntimeSequencePrivilege);
|
|
12
19
|
const TABLE_DML_SET = new Set(ALL_TABLE_DML);
|
|
13
20
|
const SEQUENCE_PRIVILEGE_SET = new Set(ALL_SEQUENCE_PRIVILEGES);
|
|
@@ -30,6 +37,29 @@ function assertExpectation(expectation) {
|
|
|
30
37
|
expectation.tablePrivileges.length === 0) {
|
|
31
38
|
throw new Error(`${LOG_PREFIX} externally-managed runtime access must require at least one table privilege.`);
|
|
32
39
|
}
|
|
40
|
+
if (!Array.isArray(expectation.writableRelations)) {
|
|
41
|
+
throw new Error(`${LOG_PREFIX} externally-managed runtime access must explicitly declare writableRelations.`);
|
|
42
|
+
}
|
|
43
|
+
const writableRelationKeys = new Set();
|
|
44
|
+
for (const relation of expectation.writableRelations) {
|
|
45
|
+
assertNonEmpty(relation.name, 'writable relation name');
|
|
46
|
+
const relationPrivileges = relation.privileges;
|
|
47
|
+
if (!Object.values(migration_mode_1.RuntimeWritableRelationKind).includes(relation.kind)) {
|
|
48
|
+
throw new Error(`${LOG_PREFIX} writable relation kind is invalid.`);
|
|
49
|
+
}
|
|
50
|
+
if (!Array.isArray(relationPrivileges) ||
|
|
51
|
+
!relationPrivileges.includes(migration_mode_1.RuntimeTablePrivilege.Select) ||
|
|
52
|
+
!relationPrivileges.some((privilege) => privilege !== migration_mode_1.RuntimeTablePrivilege.Select) ||
|
|
53
|
+
relationPrivileges.some((privilege) => !TABLE_DML_SET.has(privilege)) ||
|
|
54
|
+
new Set(relationPrivileges).size !== relationPrivileges.length) {
|
|
55
|
+
throw new Error(`${LOG_PREFIX} writable relation ${relation.name} must declare unique SELECT plus at least one DML privilege.`);
|
|
56
|
+
}
|
|
57
|
+
const key = `${relation.kind}\u0000${relation.name}`;
|
|
58
|
+
if (writableRelationKeys.has(key)) {
|
|
59
|
+
throw new Error(`${LOG_PREFIX} writableRelations contains duplicate ${relation.name}.`);
|
|
60
|
+
}
|
|
61
|
+
writableRelationKeys.add(key);
|
|
62
|
+
}
|
|
33
63
|
if (expectation.tablePrivileges.some((privilege) => !TABLE_DML_SET.has(privilege)) ||
|
|
34
64
|
new Set(expectation.tablePrivileges).size !==
|
|
35
65
|
expectation.tablePrivileges.length) {
|
|
@@ -42,122 +72,404 @@ function assertExpectation(expectation) {
|
|
|
42
72
|
throw new Error(`${LOG_PREFIX} runtime sequence privileges must be unique RuntimeSequencePrivilege values.`);
|
|
43
73
|
}
|
|
44
74
|
const memberships = expectation.authorizedRoleMemberships;
|
|
45
|
-
if (!Array.isArray(memberships)) {
|
|
46
|
-
throw new Error(`${LOG_PREFIX} externally-managed runtime access must
|
|
75
|
+
if (!Array.isArray(memberships) || memberships.length !== 0) {
|
|
76
|
+
throw new Error(`${LOG_PREFIX} externally-managed runtime access forbids all role memberships; authorizedRoleMemberships must be an empty tuple.`);
|
|
47
77
|
}
|
|
48
|
-
|
|
49
|
-
|
|
78
|
+
if (!Array.isArray(expectation.targetFunctions)) {
|
|
79
|
+
throw new Error(`${LOG_PREFIX} externally-managed runtime access must explicitly declare targetFunctions.`);
|
|
50
80
|
}
|
|
51
|
-
|
|
52
|
-
|
|
81
|
+
const functionKeys = new Set();
|
|
82
|
+
for (const fn of expectation.targetFunctions) {
|
|
83
|
+
assertNonEmpty(fn.name, 'target function name');
|
|
84
|
+
if (typeof fn.identityArguments !== 'string') {
|
|
85
|
+
throw new Error(`${LOG_PREFIX} target function identityArguments must be a string.`);
|
|
86
|
+
}
|
|
87
|
+
if (!Array.isArray(fn.privileges) ||
|
|
88
|
+
fn.privileges.length !== 1 ||
|
|
89
|
+
fn.privileges[0] !== migration_mode_1.RuntimeExtensionFunctionPrivilege.Execute) {
|
|
90
|
+
throw new Error(`${LOG_PREFIX} target function ${fn.name} must declare exactly EXECUTE.`);
|
|
91
|
+
}
|
|
92
|
+
const key = `${fn.name}\u0000${fn.identityArguments}`;
|
|
93
|
+
if (functionKeys.has(key)) {
|
|
94
|
+
throw new Error(`${LOG_PREFIX} targetFunctions contains duplicate ${fn.name}.`);
|
|
95
|
+
}
|
|
96
|
+
functionKeys.add(key);
|
|
53
97
|
}
|
|
98
|
+
assertExtensionDependencies(expectation.extensionDependencies);
|
|
54
99
|
}
|
|
55
|
-
function
|
|
56
|
-
const
|
|
57
|
-
.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
AND table_type = 'BASE TABLE'
|
|
67
|
-
AND table_name <> '_prisma_migrations'
|
|
68
|
-
AND NOT (
|
|
69
|
-
${privileges
|
|
70
|
-
.map((privilege) => `has_table_privilege(current_user, ` +
|
|
71
|
-
`format('%I.%I', table_schema, table_name), '${privilege}')`)
|
|
72
|
-
.join(' AND ')}
|
|
73
|
-
)
|
|
74
|
-
ORDER BY table_name
|
|
75
|
-
`;
|
|
100
|
+
function extensionObjectKey(extensionName, schema, object) {
|
|
101
|
+
const identityArguments = object.kind === migration_mode_1.RuntimeExtensionObjectKind.Function
|
|
102
|
+
? object.identityArguments
|
|
103
|
+
: '';
|
|
104
|
+
return [
|
|
105
|
+
extensionName,
|
|
106
|
+
object.kind,
|
|
107
|
+
schema,
|
|
108
|
+
object.name,
|
|
109
|
+
identityArguments,
|
|
110
|
+
].join('\u0000');
|
|
76
111
|
}
|
|
77
|
-
function
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
112
|
+
function assertExtensionDependencies(dependencies) {
|
|
113
|
+
if (!Array.isArray(dependencies)) {
|
|
114
|
+
throw new Error(`${LOG_PREFIX} externally-managed runtime access must explicitly declare extensionDependencies.`);
|
|
115
|
+
}
|
|
116
|
+
const names = new Set();
|
|
117
|
+
const objectKeys = new Set();
|
|
118
|
+
for (const dependency of dependencies) {
|
|
119
|
+
assertNonEmpty(dependency.name, 'extension dependency name');
|
|
120
|
+
assertNonEmpty(dependency.schema, 'extension dependency schema');
|
|
121
|
+
assertNonEmpty(dependency.version, 'extension dependency version');
|
|
122
|
+
if (names.has(dependency.name)) {
|
|
123
|
+
throw new Error(`${LOG_PREFIX} extensionDependencies contains duplicate extension ${dependency.name}.`);
|
|
124
|
+
}
|
|
125
|
+
names.add(dependency.name);
|
|
126
|
+
if (!Array.isArray(dependency.objects) || dependency.objects.length === 0) {
|
|
127
|
+
throw new Error(`${LOG_PREFIX} extension dependency ${dependency.name} must declare at least one object.`);
|
|
128
|
+
}
|
|
129
|
+
for (const object of dependency.objects) {
|
|
130
|
+
assertNonEmpty(object.name, `${dependency.name} extension object name`);
|
|
131
|
+
const expectedPrivilege = (() => {
|
|
132
|
+
switch (object.kind) {
|
|
133
|
+
case migration_mode_1.RuntimeExtensionObjectKind.Type:
|
|
134
|
+
return migration_mode_1.RuntimeExtensionTypePrivilege.Usage;
|
|
135
|
+
case migration_mode_1.RuntimeExtensionObjectKind.Function:
|
|
136
|
+
if (typeof object.identityArguments !== 'string') {
|
|
137
|
+
throw new Error(`${LOG_PREFIX} ${dependency.name}.${object.name} function identityArguments must be a string.`);
|
|
138
|
+
}
|
|
139
|
+
return migration_mode_1.RuntimeExtensionFunctionPrivilege.Execute;
|
|
140
|
+
case migration_mode_1.RuntimeExtensionObjectKind.Table:
|
|
141
|
+
return migration_mode_1.RuntimeExtensionTablePrivilege.Select;
|
|
142
|
+
case migration_mode_1.RuntimeExtensionObjectKind.Sequence:
|
|
143
|
+
return migration_mode_1.RuntimeExtensionSequencePrivilege.Select;
|
|
144
|
+
default:
|
|
145
|
+
throw new Error(`${LOG_PREFIX} extension dependency ${dependency.name} contains an unsupported object kind.`);
|
|
146
|
+
}
|
|
147
|
+
})();
|
|
148
|
+
if (!Array.isArray(object.privileges) ||
|
|
149
|
+
object.privileges.length !== 1 ||
|
|
150
|
+
object.privileges[0] !== expectedPrivilege) {
|
|
151
|
+
throw new Error(`${LOG_PREFIX} extension object ${dependency.schema}.${object.name} must declare exactly ${expectedPrivilege}.`);
|
|
152
|
+
}
|
|
153
|
+
const key = extensionObjectKey(dependency.name, dependency.schema, object);
|
|
154
|
+
if (objectKeys.has(key)) {
|
|
155
|
+
throw new Error(`${LOG_PREFIX} extensionDependencies contains duplicate object ${dependency.schema}.${object.name}.`);
|
|
156
|
+
}
|
|
157
|
+
objectKeys.add(key);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
99
160
|
}
|
|
100
|
-
function
|
|
101
|
-
const projections =
|
|
102
|
-
`format('%I.%I', table_schema, table_name), '${privilege}') ` +
|
|
161
|
+
function buildTablePrivilegeQuery(crossSchema) {
|
|
162
|
+
const projections = ALL_TABLE_PRIVILEGES.map((privilege) => `CASE WHEN has_table_privilege(current_user, c.oid, '${privilege}') ` +
|
|
103
163
|
`THEN '${privilege}' END`).join(', ');
|
|
164
|
+
const publicProjections = ALL_TABLE_PRIVILEGES.map((privilege) => `CASE WHEN EXISTS (` +
|
|
165
|
+
`SELECT 1 FROM aclexplode(COALESCE(c.relacl, acldefault('r', c.relowner))) acl ` +
|
|
166
|
+
`WHERE acl.grantee = 0 AND acl.privilege_type = '${privilege}'` +
|
|
167
|
+
`) THEN '${privilege}' END`).join(', ');
|
|
104
168
|
return `
|
|
105
|
-
SELECT
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
169
|
+
SELECT n.nspname AS schema_name,
|
|
170
|
+
c.relname AS object_name,
|
|
171
|
+
c.relkind AS relation_kind,
|
|
172
|
+
array_remove(ARRAY[${projections}], NULL) AS privileges,
|
|
173
|
+
e.extname AS extension_name,
|
|
174
|
+
array_remove(ARRAY[${publicProjections}], NULL) AS public_privileges,
|
|
175
|
+
pg_has_role(current_user, c.relowner, 'USAGE') AS owns_object
|
|
176
|
+
FROM pg_class c
|
|
177
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
178
|
+
LEFT JOIN pg_depend d
|
|
179
|
+
ON d.classid = 'pg_class'::regclass
|
|
180
|
+
AND d.objid = c.oid
|
|
181
|
+
AND d.objsubid = 0
|
|
182
|
+
AND d.refclassid = 'pg_extension'::regclass
|
|
183
|
+
AND d.deptype = 'e'
|
|
184
|
+
LEFT JOIN pg_extension e ON e.oid = d.refobjid
|
|
185
|
+
WHERE n.nspname ${crossSchema ? '<>' : '='} $1
|
|
186
|
+
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
187
|
+
AND n.nspname NOT LIKE 'pg_toast%'
|
|
188
|
+
AND n.nspname NOT LIKE 'pg_temp_%'
|
|
189
|
+
AND c.relkind IN ('r', 'p', 'v', 'm', 'f')
|
|
190
|
+
AND c.relname <> '_prisma_migrations'
|
|
191
|
+
${crossSchema
|
|
192
|
+
? `AND (${ALL_TABLE_PRIVILEGES.map((privilege) => `has_table_privilege(current_user, c.oid, '${privilege}')`).join(' OR ')})`
|
|
193
|
+
: ''}
|
|
194
|
+
ORDER BY n.nspname, c.relname
|
|
118
195
|
`;
|
|
119
196
|
}
|
|
120
|
-
function
|
|
121
|
-
const projections = ALL_SEQUENCE_PRIVILEGES.map((privilege) => `CASE WHEN has_sequence_privilege(current_user, ` +
|
|
122
|
-
`format('%I.%I', sequence_schema, sequence_name), '${privilege}') ` +
|
|
197
|
+
function buildSequencePrivilegeQuery(crossSchema) {
|
|
198
|
+
const projections = ALL_SEQUENCE_PRIVILEGES.map((privilege) => `CASE WHEN has_sequence_privilege(current_user, c.oid, '${privilege}') ` +
|
|
123
199
|
`THEN '${privilege}' END`).join(', ');
|
|
200
|
+
const publicProjections = ALL_SEQUENCE_PRIVILEGES.map((privilege) => `CASE WHEN EXISTS (` +
|
|
201
|
+
`SELECT 1 FROM aclexplode(COALESCE(c.relacl, acldefault('s', c.relowner))) acl ` +
|
|
202
|
+
`WHERE acl.grantee = 0 AND acl.privilege_type = '${privilege}'` +
|
|
203
|
+
`) THEN '${privilege}' END`).join(', ');
|
|
124
204
|
return `
|
|
125
|
-
SELECT
|
|
126
|
-
|
|
127
|
-
array_remove(ARRAY[${projections}], NULL) AS privileges
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
205
|
+
SELECT n.nspname AS schema_name,
|
|
206
|
+
c.relname AS object_name,
|
|
207
|
+
array_remove(ARRAY[${projections}], NULL) AS privileges,
|
|
208
|
+
e.extname AS extension_name,
|
|
209
|
+
array_remove(ARRAY[${publicProjections}], NULL) AS public_privileges,
|
|
210
|
+
pg_has_role(current_user, c.relowner, 'USAGE') AS owns_object
|
|
211
|
+
FROM pg_class c
|
|
212
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
213
|
+
LEFT JOIN pg_depend d
|
|
214
|
+
ON d.classid = 'pg_class'::regclass
|
|
215
|
+
AND d.objid = c.oid
|
|
216
|
+
AND d.objsubid = 0
|
|
217
|
+
AND d.refclassid = 'pg_extension'::regclass
|
|
218
|
+
AND d.deptype = 'e'
|
|
219
|
+
LEFT JOIN pg_extension e ON e.oid = d.refobjid
|
|
220
|
+
WHERE n.nspname ${crossSchema ? '<>' : '='} $1
|
|
221
|
+
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
222
|
+
AND n.nspname NOT LIKE 'pg_toast%'
|
|
223
|
+
AND n.nspname NOT LIKE 'pg_temp_%'
|
|
224
|
+
AND c.relkind = 'S'
|
|
225
|
+
${crossSchema
|
|
226
|
+
? `AND (${ALL_SEQUENCE_PRIVILEGES.map((privilege) => `has_sequence_privilege(current_user, c.oid, '${privilege}')`).join(' OR ')})`
|
|
227
|
+
: ''}
|
|
228
|
+
ORDER BY n.nspname, c.relname
|
|
138
229
|
`;
|
|
139
230
|
}
|
|
140
|
-
function formatMissingPrivileges(kind, rows) {
|
|
141
|
-
return rows
|
|
142
|
-
.map((row) => `${kind} ${row.object_name} missing ${row.missing_privileges.join('/')}`)
|
|
143
|
-
.join(', ');
|
|
144
|
-
}
|
|
145
231
|
function formatCrossSchemaPrivileges(kind, rows) {
|
|
146
232
|
return rows
|
|
147
233
|
.map((row) => `${kind} ${row.schema_name}.${row.object_name} ` +
|
|
148
234
|
`[${row.privileges.join('/')}]`)
|
|
149
235
|
.join(', ');
|
|
150
236
|
}
|
|
151
|
-
function
|
|
152
|
-
|
|
153
|
-
|
|
237
|
+
function samePrivileges(observed, expected) {
|
|
238
|
+
const sortedObserved = [...observed].sort();
|
|
239
|
+
const sortedExpected = [...expected].sort();
|
|
240
|
+
return (sortedObserved.length === sortedExpected.length &&
|
|
241
|
+
sortedObserved.every((privilege, index) => privilege === sortedExpected[index]));
|
|
242
|
+
}
|
|
243
|
+
function expectedRelationPrivileges(row, tablePrivileges, writableRelations) {
|
|
244
|
+
if (row.relation_kind === 'r' || row.relation_kind === 'p') {
|
|
245
|
+
return tablePrivileges;
|
|
246
|
+
}
|
|
247
|
+
if (row.relation_kind === 'm') {
|
|
248
|
+
return [migration_mode_1.RuntimeTablePrivilege.Select];
|
|
249
|
+
}
|
|
250
|
+
const writableKind = row.relation_kind === 'v'
|
|
251
|
+
? migration_mode_1.RuntimeWritableRelationKind.View
|
|
252
|
+
: migration_mode_1.RuntimeWritableRelationKind.ForeignTable;
|
|
253
|
+
return (writableRelations.find((relation) => relation.kind === writableKind && relation.name === row.object_name)?.privileges ?? [migration_mode_1.RuntimeTablePrivilege.Select]);
|
|
254
|
+
}
|
|
255
|
+
function assertExactTargetPrivileges(kind, rows, expected, writableRelations) {
|
|
256
|
+
if (kind === 'table') {
|
|
257
|
+
const observedWritableRelations = new Set(rows.flatMap((row) => {
|
|
258
|
+
if (!('relation_kind' in row))
|
|
259
|
+
return [];
|
|
260
|
+
const writableKind = row.relation_kind === 'v'
|
|
261
|
+
? migration_mode_1.RuntimeWritableRelationKind.View
|
|
262
|
+
: row.relation_kind === 'f'
|
|
263
|
+
? migration_mode_1.RuntimeWritableRelationKind.ForeignTable
|
|
264
|
+
: null;
|
|
265
|
+
return writableKind === null
|
|
266
|
+
? []
|
|
267
|
+
: [`${writableKind}\u0000${row.object_name}`];
|
|
268
|
+
}));
|
|
269
|
+
const missingDeclarations = writableRelations.filter((relation) => !observedWritableRelations.has(`${relation.kind}\u0000${relation.name}`));
|
|
270
|
+
if (missingDeclarations.length > 0) {
|
|
271
|
+
throw new Error(`${LOG_PREFIX} writableRelations names missing target view/foreign table: ` +
|
|
272
|
+
missingDeclarations.map((relation) => relation.name).join(', '));
|
|
273
|
+
}
|
|
154
274
|
}
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
275
|
+
const mismatches = rows.filter((row) => {
|
|
276
|
+
const applicableExpected = kind === 'table' && 'relation_kind' in row
|
|
277
|
+
? expectedRelationPrivileges(row, expected, writableRelations)
|
|
278
|
+
: expected;
|
|
279
|
+
return (!samePrivileges(row.privileges, applicableExpected) ||
|
|
280
|
+
row.public_privileges.length > 0 ||
|
|
281
|
+
row.owns_object);
|
|
282
|
+
});
|
|
283
|
+
if (mismatches.length > 0) {
|
|
284
|
+
throw new Error(`${LOG_PREFIX} runtime ${kind} privilege contract failed: ${mismatches
|
|
285
|
+
.map((row) => {
|
|
286
|
+
const applicableExpected = kind === 'table' && 'relation_kind' in row
|
|
287
|
+
? expectedRelationPrivileges(row, expected, writableRelations)
|
|
288
|
+
: expected;
|
|
289
|
+
return (`${row.schema_name}.${row.object_name} expected [${applicableExpected.join('/')}], observed [${row.privileges.join('/')}], PUBLIC [` +
|
|
290
|
+
`${row.public_privileges.join('/')}], owns=${row.owns_object}`);
|
|
291
|
+
})
|
|
292
|
+
.join(', ')}.`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function assertTargetTypes(rows) {
|
|
296
|
+
const violations = rows.filter((row) => !row.can_use || row.public_has_privilege || row.owns_object);
|
|
297
|
+
if (violations.length > 0) {
|
|
298
|
+
throw new Error(`${LOG_PREFIX} target type privilege contract failed: ${violations
|
|
299
|
+
.map((row) => `${row.object_name} kind=${row.type_kind} USAGE=${row.can_use} ` +
|
|
300
|
+
`PUBLIC=${row.public_has_privilege} owns=${row.owns_object}`)
|
|
301
|
+
.join(', ')}.`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function functionIdentityKey(name, identityArguments) {
|
|
305
|
+
return `${name}\u0000${identityArguments}`;
|
|
306
|
+
}
|
|
307
|
+
function hasSafeTargetFunctionConfiguration(configuration, targetSchema) {
|
|
308
|
+
if (configuration === null || configuration.length !== 1) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
const prefix = 'search_path=';
|
|
312
|
+
const setting = configuration[0];
|
|
313
|
+
if (!setting.startsWith(prefix)) {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
const path = setting
|
|
317
|
+
.slice(prefix.length)
|
|
318
|
+
.split(',')
|
|
319
|
+
.map((schema) => schema.trim());
|
|
320
|
+
return (path.length === 2 && path[0] === 'pg_catalog' && path[1] === targetSchema);
|
|
321
|
+
}
|
|
322
|
+
function assertTargetFunctions(rows, expectedFunctions, targetSchema) {
|
|
323
|
+
const ownership = rows.filter((row) => row.owns_object);
|
|
324
|
+
const publicExecute = rows.filter((row) => row.public_has_privilege);
|
|
325
|
+
const executable = rows.filter((row) => row.can_execute);
|
|
326
|
+
const expected = new Set(expectedFunctions.map((fn) => functionIdentityKey(fn.name, fn.identityArguments)));
|
|
327
|
+
const observed = new Set(executable.map((row) => functionIdentityKey(row.object_name, row.identity_arguments)));
|
|
328
|
+
const missing = [...expected].filter((key) => !observed.has(key));
|
|
329
|
+
const extra = [...observed].filter((key) => !expected.has(key));
|
|
330
|
+
const unsafe = executable.filter((row) => row.security_definer ||
|
|
331
|
+
!hasSafeTargetFunctionConfiguration(row.configuration, targetSchema));
|
|
332
|
+
if (ownership.length > 0 ||
|
|
333
|
+
publicExecute.length > 0 ||
|
|
334
|
+
missing.length > 0 ||
|
|
335
|
+
extra.length > 0 ||
|
|
336
|
+
unsafe.length > 0) {
|
|
337
|
+
throw new Error(`${LOG_PREFIX} target function privilege contract failed: ` +
|
|
338
|
+
`owned=${JSON.stringify(ownership.map((row) => functionIdentityKey(row.object_name, row.identity_arguments)))}, PUBLIC=${JSON.stringify(publicExecute.map((row) => functionIdentityKey(row.object_name, row.identity_arguments)))}, missing=${JSON.stringify(missing)}, extra=${JSON.stringify(extra)}, ` +
|
|
339
|
+
`unsafe=${JSON.stringify(unsafe.map((row) => ({
|
|
340
|
+
function: functionIdentityKey(row.object_name, row.identity_arguments),
|
|
341
|
+
securityDefiner: row.security_definer,
|
|
342
|
+
configuration: row.configuration ?? [],
|
|
343
|
+
})))}.`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function findDependency(dependencies, extensionName, schema) {
|
|
347
|
+
if (extensionName === null) {
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
return dependencies.find((dependency) => dependency.name === extensionName && dependency.schema === schema);
|
|
351
|
+
}
|
|
352
|
+
function expectedExtensionObjectKeys(dependencies) {
|
|
353
|
+
return new Set(dependencies.flatMap((dependency) => dependency.objects.map((object) => extensionObjectKey(dependency.name, dependency.schema, object))));
|
|
354
|
+
}
|
|
355
|
+
function assertExtensionMetadata(expected, observed) {
|
|
356
|
+
const actualByName = new Map(observed.map((row) => [row.extension_name, row]));
|
|
357
|
+
const mismatches = expected.filter((dependency) => {
|
|
358
|
+
const actual = actualByName.get(dependency.name);
|
|
359
|
+
return (actual === undefined ||
|
|
360
|
+
actual.schema_name !== dependency.schema ||
|
|
361
|
+
actual.extension_version !== dependency.version ||
|
|
362
|
+
actual.owns_extension);
|
|
363
|
+
});
|
|
364
|
+
if (observed.length !== expected.length || mismatches.length > 0) {
|
|
365
|
+
throw new Error(`${LOG_PREFIX} extension dependency mismatch: expected ${expected
|
|
366
|
+
.map((dependency) => `${dependency.name}@${dependency.version} in ${dependency.schema}`)
|
|
367
|
+
.join(', ')}, observed ${observed
|
|
368
|
+
.map((row) => `${row.extension_name}@${row.extension_version} in ${row.schema_name} owns=${row.owns_extension}`)
|
|
369
|
+
.join(', ')}.`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function assertExactExtensionObjects(dependencies, types, functions, tables, sequences) {
|
|
373
|
+
const expected = expectedExtensionObjectKeys(dependencies);
|
|
374
|
+
const observed = new Set();
|
|
375
|
+
const publicObjects = [];
|
|
376
|
+
const invalidMembership = [];
|
|
377
|
+
for (const row of types) {
|
|
378
|
+
const dependency = findDependency(dependencies, row.extension_name, row.schema_name);
|
|
379
|
+
const key = extensionObjectKey(row.extension_name ?? '<none>', row.schema_name, {
|
|
380
|
+
kind: migration_mode_1.RuntimeExtensionObjectKind.Type,
|
|
381
|
+
name: row.object_name,
|
|
382
|
+
privileges: [migration_mode_1.RuntimeExtensionTypePrivilege.Usage],
|
|
383
|
+
});
|
|
384
|
+
observed.add(key);
|
|
385
|
+
if (dependency === undefined) {
|
|
386
|
+
invalidMembership.push(`${row.schema_name}.${row.object_name}`);
|
|
387
|
+
}
|
|
388
|
+
if (row.owns_object) {
|
|
389
|
+
invalidMembership.push(`${row.schema_name}.${row.object_name} owned by runtime`);
|
|
390
|
+
}
|
|
391
|
+
if (row.public_has_privilege) {
|
|
392
|
+
publicObjects.push(`${row.schema_name}.${row.object_name} TYPE USAGE`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
for (const row of functions) {
|
|
396
|
+
const dependency = findDependency(dependencies, row.extension_name, row.schema_name);
|
|
397
|
+
const key = extensionObjectKey(row.extension_name ?? '<none>', row.schema_name, {
|
|
398
|
+
kind: migration_mode_1.RuntimeExtensionObjectKind.Function,
|
|
399
|
+
name: row.object_name,
|
|
400
|
+
identityArguments: row.identity_arguments,
|
|
401
|
+
privileges: [migration_mode_1.RuntimeExtensionFunctionPrivilege.Execute],
|
|
402
|
+
});
|
|
403
|
+
observed.add(key);
|
|
404
|
+
if (dependency === undefined) {
|
|
405
|
+
invalidMembership.push(`${row.schema_name}.${row.object_name}(${row.identity_arguments})`);
|
|
406
|
+
}
|
|
407
|
+
if (row.owns_object ||
|
|
408
|
+
row.security_definer ||
|
|
409
|
+
(row.configuration !== null && row.configuration.length > 0)) {
|
|
410
|
+
invalidMembership.push(`${row.schema_name}.${row.object_name}(${row.identity_arguments}) ` +
|
|
411
|
+
`owner=${row.owns_object} securityDefiner=${row.security_definer} ` +
|
|
412
|
+
`configuration=${JSON.stringify(row.configuration ?? [])}`);
|
|
413
|
+
}
|
|
414
|
+
if (row.public_has_privilege) {
|
|
415
|
+
publicObjects.push(`${row.schema_name}.${row.object_name}(${row.identity_arguments}) EXECUTE`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
for (const row of tables) {
|
|
419
|
+
const dependency = findDependency(dependencies, row.extension_name, row.schema_name);
|
|
420
|
+
if (dependency === undefined ||
|
|
421
|
+
!samePrivileges(row.privileges, [
|
|
422
|
+
migration_mode_1.RuntimeExtensionTablePrivilege.Select,
|
|
423
|
+
]) ||
|
|
424
|
+
row.owns_object) {
|
|
425
|
+
invalidMembership.push(`${row.schema_name}.${row.object_name}`);
|
|
426
|
+
}
|
|
427
|
+
observed.add(extensionObjectKey(row.extension_name ?? '<none>', row.schema_name, {
|
|
428
|
+
kind: migration_mode_1.RuntimeExtensionObjectKind.Table,
|
|
429
|
+
name: row.object_name,
|
|
430
|
+
privileges: [migration_mode_1.RuntimeExtensionTablePrivilege.Select],
|
|
431
|
+
}));
|
|
432
|
+
if (row.public_privileges.length > 0) {
|
|
433
|
+
publicObjects.push(`${row.schema_name}.${row.object_name} [${row.public_privileges.join('/')}]`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
for (const row of sequences) {
|
|
437
|
+
const dependency = findDependency(dependencies, row.extension_name, row.schema_name);
|
|
438
|
+
if (dependency === undefined ||
|
|
439
|
+
!samePrivileges(row.privileges, [
|
|
440
|
+
migration_mode_1.RuntimeExtensionSequencePrivilege.Select,
|
|
441
|
+
]) ||
|
|
442
|
+
row.owns_object) {
|
|
443
|
+
invalidMembership.push(`${row.schema_name}.${row.object_name}`);
|
|
444
|
+
}
|
|
445
|
+
observed.add(extensionObjectKey(row.extension_name ?? '<none>', row.schema_name, {
|
|
446
|
+
kind: migration_mode_1.RuntimeExtensionObjectKind.Sequence,
|
|
447
|
+
name: row.object_name,
|
|
448
|
+
privileges: [migration_mode_1.RuntimeExtensionSequencePrivilege.Select],
|
|
449
|
+
}));
|
|
450
|
+
if (row.public_privileges.length > 0) {
|
|
451
|
+
publicObjects.push(`${row.schema_name}.${row.object_name} [${row.public_privileges.join('/')}]`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const missing = [...expected].filter((key) => !observed.has(key));
|
|
455
|
+
const extra = [...observed].filter((key) => !expected.has(key));
|
|
456
|
+
if (missing.length > 0 ||
|
|
457
|
+
extra.length > 0 ||
|
|
458
|
+
invalidMembership.length > 0 ||
|
|
459
|
+
publicObjects.length > 0) {
|
|
460
|
+
throw new Error(`${LOG_PREFIX} extension object privilege contract failed: ` +
|
|
461
|
+
`missing=${JSON.stringify(missing)}, extra=${JSON.stringify(extra)}, ` +
|
|
462
|
+
`invalidExtensionMembership=${JSON.stringify(invalidMembership)}, ` +
|
|
463
|
+
`PUBLIC=${JSON.stringify(publicObjects)}.`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function assertNoRoleMemberships(observedRows) {
|
|
467
|
+
if (observedRows.length > 0) {
|
|
468
|
+
throw new Error(`${LOG_PREFIX} runtime role has forbidden direct or transitive membership(s): ` +
|
|
469
|
+
observedRows
|
|
470
|
+
.map((row) => row.role_name)
|
|
471
|
+
.sort()
|
|
472
|
+
.join(', '));
|
|
161
473
|
}
|
|
162
474
|
}
|
|
163
475
|
function parseLedgerTimestamp(value, migrationName, field) {
|
|
@@ -224,6 +536,7 @@ function assertTarget(target) {
|
|
|
224
536
|
assertNonEmpty(target.schema, 'expected target schema');
|
|
225
537
|
assertNonEmpty(target.applicationName, 'expected application_name');
|
|
226
538
|
assertNonEmpty(target.expectedRole, 'expected runtime role');
|
|
539
|
+
(0, credential_generation_1.assertDatabaseCredentialGeneration)(target.credentialGeneration, `${LOG_PREFIX} expected credential generation`);
|
|
227
540
|
}
|
|
228
541
|
async function verifyExternallyManagedRuntime(runtimeUrl, target, access, compatibility) {
|
|
229
542
|
assertTarget(target);
|
|
@@ -276,7 +589,7 @@ async function verifyExternallyManagedRuntime(runtimeUrl, target, access, compat
|
|
|
276
589
|
AND pg_has_role(current_user, oid, 'MEMBER')
|
|
277
590
|
ORDER BY rolname
|
|
278
591
|
`)).rows;
|
|
279
|
-
|
|
592
|
+
assertNoRoleMemberships(roleMemberships);
|
|
280
593
|
const databasePrivileges = requireSingleRow((await client.query(`
|
|
281
594
|
SELECT has_database_privilege(current_user, current_database(), 'CONNECT') AS can_connect,
|
|
282
595
|
has_database_privilege(current_user, current_database(), 'CREATE') AS can_create,
|
|
@@ -295,7 +608,13 @@ async function verifyExternallyManagedRuntime(runtimeUrl, target, access, compat
|
|
|
295
608
|
SELECT nspname AS schema_name,
|
|
296
609
|
has_schema_privilege(current_user, oid, 'USAGE') AS can_use,
|
|
297
610
|
has_schema_privilege(current_user, oid, 'CREATE') AS can_create,
|
|
298
|
-
pg_has_role(current_user, nspowner, 'USAGE') AS owns_schema
|
|
611
|
+
pg_has_role(current_user, nspowner, 'USAGE') AS owns_schema,
|
|
612
|
+
EXISTS (
|
|
613
|
+
SELECT 1
|
|
614
|
+
FROM aclexplode(COALESCE(nspacl, acldefault('n', nspowner))) acl
|
|
615
|
+
WHERE acl.grantee = 0
|
|
616
|
+
AND acl.privilege_type = 'USAGE'
|
|
617
|
+
) AS public_can_use
|
|
299
618
|
FROM pg_namespace
|
|
300
619
|
WHERE nspname NOT IN ('pg_catalog', 'information_schema')
|
|
301
620
|
AND nspname NOT LIKE 'pg_toast%'
|
|
@@ -306,6 +625,9 @@ async function verifyExternallyManagedRuntime(runtimeUrl, target, access, compat
|
|
|
306
625
|
if (!targetSchemaPrivileges.can_use) {
|
|
307
626
|
throw new Error(`${LOG_PREFIX} runtime role ${identity.role_name} lacks USAGE on schema ${target.schema}.`);
|
|
308
627
|
}
|
|
628
|
+
if (targetSchemaPrivileges.public_can_use) {
|
|
629
|
+
throw new Error(`${LOG_PREFIX} target schema ${target.schema} grants forbidden USAGE to PUBLIC.`);
|
|
630
|
+
}
|
|
309
631
|
const persistentDdlSchemas = persistentSchemaPrivileges.filter((row) => row.can_create || row.owns_schema);
|
|
310
632
|
if (persistentDdlSchemas.length > 0) {
|
|
311
633
|
throw new Error(`${LOG_PREFIX} runtime role ${identity.role_name} has forbidden persistent ` +
|
|
@@ -313,62 +635,189 @@ async function verifyExternallyManagedRuntime(runtimeUrl, target, access, compat
|
|
|
313
635
|
.map((row) => row.schema_name)
|
|
314
636
|
.join(', ')}.`);
|
|
315
637
|
}
|
|
316
|
-
const
|
|
638
|
+
const extensionSchemas = new Set(access.extensionDependencies.map((dependency) => dependency.schema));
|
|
639
|
+
if (extensionSchemas.has(target.schema)) {
|
|
640
|
+
throw new Error(`${LOG_PREFIX} extension dependency schema must differ from target schema ${target.schema}.`);
|
|
641
|
+
}
|
|
642
|
+
const crossSchemaUsage = persistentSchemaPrivileges.filter((row) => row.schema_name !== target.schema &&
|
|
643
|
+
row.can_use &&
|
|
644
|
+
!extensionSchemas.has(row.schema_name));
|
|
317
645
|
if (crossSchemaUsage.length > 0) {
|
|
318
646
|
throw new Error(`${LOG_PREFIX} runtime role ${identity.role_name} has forbidden cross-schema ` +
|
|
319
647
|
`USAGE on: ${crossSchemaUsage
|
|
320
648
|
.map((row) => row.schema_name)
|
|
321
649
|
.join(', ')}.`);
|
|
322
650
|
}
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
651
|
+
const missingOrPublicExtensionSchemas = [...extensionSchemas].filter((schema) => {
|
|
652
|
+
const row = persistentSchemaPrivileges.find((candidate) => candidate.schema_name === schema);
|
|
653
|
+
return row === undefined || !row.can_use || row.public_can_use;
|
|
654
|
+
});
|
|
655
|
+
if (missingOrPublicExtensionSchemas.length > 0) {
|
|
656
|
+
throw new Error(`${LOG_PREFIX} extension schemas must grant direct runtime USAGE and revoke PUBLIC USAGE: ` +
|
|
657
|
+
missingOrPublicExtensionSchemas.join(', '));
|
|
327
658
|
}
|
|
328
|
-
const
|
|
329
|
-
|
|
659
|
+
const crossSchemaTablePrivileges = (await client.query(buildTablePrivilegeQuery(true), [
|
|
660
|
+
target.schema,
|
|
661
|
+
])).rows;
|
|
662
|
+
const crossSchemaSequencePrivileges = (await client.query(buildSequencePrivilegeQuery(true), [target.schema])).rows;
|
|
663
|
+
const undeclaredCrossSchemaTables = crossSchemaTablePrivileges.filter((row) => !access.extensionDependencies.some((dependency) => dependency.name === row.extension_name &&
|
|
664
|
+
dependency.schema === row.schema_name));
|
|
665
|
+
if (undeclaredCrossSchemaTables.length > 0) {
|
|
330
666
|
throw new Error(`${LOG_PREFIX} runtime role ${identity.role_name} has forbidden cross-schema ` +
|
|
331
|
-
`
|
|
667
|
+
`table privileges: ${formatCrossSchemaPrivileges('table', undeclaredCrossSchemaTables)}.`);
|
|
332
668
|
}
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
669
|
+
const undeclaredCrossSchemaSequences = crossSchemaSequencePrivileges.filter((row) => !access.extensionDependencies.some((dependency) => dependency.name === row.extension_name &&
|
|
670
|
+
dependency.schema === row.schema_name));
|
|
671
|
+
if (undeclaredCrossSchemaSequences.length > 0) {
|
|
672
|
+
throw new Error(`${LOG_PREFIX} runtime role ${identity.role_name} has forbidden cross-schema ` +
|
|
673
|
+
`sequence privileges: ${formatCrossSchemaPrivileges('sequence', undeclaredCrossSchemaSequences)}.`);
|
|
336
674
|
}
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
675
|
+
const targetTablePrivileges = (await client.query(buildTablePrivilegeQuery(false), [
|
|
676
|
+
target.schema,
|
|
677
|
+
])).rows;
|
|
678
|
+
assertExactTargetPrivileges('table', targetTablePrivileges, access.tablePrivileges, access.writableRelations);
|
|
679
|
+
const targetSequencePrivileges = (await client.query(buildSequencePrivilegeQuery(false), [target.schema])).rows;
|
|
680
|
+
assertExactTargetPrivileges('sequence', targetSequencePrivileges, access.sequencePrivileges, access.writableRelations);
|
|
681
|
+
const targetTypes = (await client.query(`
|
|
682
|
+
SELECT t.typname AS object_name,
|
|
683
|
+
t.typtype AS type_kind,
|
|
684
|
+
has_type_privilege(current_user, t.oid, 'USAGE') AS can_use,
|
|
685
|
+
EXISTS (
|
|
686
|
+
SELECT 1
|
|
687
|
+
FROM aclexplode(COALESCE(t.typacl, acldefault('T', t.typowner))) acl
|
|
688
|
+
WHERE acl.grantee = 0
|
|
689
|
+
AND acl.privilege_type = 'USAGE'
|
|
690
|
+
) AS public_has_privilege,
|
|
691
|
+
pg_has_role(current_user, t.typowner, 'USAGE') AS owns_object
|
|
692
|
+
FROM pg_type t
|
|
693
|
+
JOIN pg_namespace n ON n.oid = t.typnamespace
|
|
694
|
+
WHERE n.nspname = $1
|
|
695
|
+
AND t.typelem = 0
|
|
696
|
+
AND t.typtype IN ('b', 'd', 'e', 'r', 'm')
|
|
697
|
+
ORDER BY t.typname
|
|
698
|
+
`, [target.schema])).rows;
|
|
699
|
+
assertTargetTypes(targetTypes);
|
|
700
|
+
const targetFunctions = (await client.query(`
|
|
701
|
+
SELECT p.proname AS object_name,
|
|
702
|
+
pg_get_function_identity_arguments(p.oid) AS identity_arguments,
|
|
703
|
+
has_function_privilege(current_user, p.oid, 'EXECUTE') AS can_execute,
|
|
704
|
+
EXISTS (
|
|
705
|
+
SELECT 1
|
|
706
|
+
FROM aclexplode(COALESCE(p.proacl, acldefault('f', p.proowner))) acl
|
|
707
|
+
WHERE acl.grantee = 0
|
|
708
|
+
AND acl.privilege_type = 'EXECUTE'
|
|
709
|
+
) AS public_has_privilege,
|
|
710
|
+
pg_has_role(current_user, p.proowner, 'USAGE') AS owns_object,
|
|
711
|
+
p.prosecdef AS security_definer,
|
|
712
|
+
p.proconfig AS configuration
|
|
713
|
+
FROM pg_proc p
|
|
714
|
+
JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
715
|
+
WHERE n.nspname = $1
|
|
716
|
+
ORDER BY p.proname, identity_arguments
|
|
717
|
+
`, [target.schema])).rows;
|
|
718
|
+
assertTargetFunctions(targetFunctions, access.targetFunctions, target.schema);
|
|
719
|
+
if (access.extensionDependencies.length > 0) {
|
|
720
|
+
const extensionNames = access.extensionDependencies.map((dependency) => dependency.name);
|
|
721
|
+
const extensionSchemaNames = [...extensionSchemas];
|
|
722
|
+
const extensionRows = (await client.query(`
|
|
723
|
+
SELECT e.extname AS extension_name,
|
|
724
|
+
n.nspname AS schema_name,
|
|
725
|
+
e.extversion AS extension_version,
|
|
726
|
+
pg_has_role(current_user, e.extowner, 'USAGE') AS owns_extension
|
|
727
|
+
FROM pg_extension e
|
|
728
|
+
JOIN pg_namespace n ON n.oid = e.extnamespace
|
|
729
|
+
WHERE e.extname = ANY($1::text[])
|
|
730
|
+
ORDER BY e.extname
|
|
731
|
+
`, [extensionNames])).rows;
|
|
732
|
+
assertExtensionMetadata(access.extensionDependencies, extensionRows);
|
|
733
|
+
const extensionTypes = (await client.query(`
|
|
734
|
+
SELECT n.nspname AS schema_name,
|
|
735
|
+
t.typname AS object_name,
|
|
736
|
+
e.extname AS extension_name,
|
|
737
|
+
EXISTS (
|
|
738
|
+
SELECT 1
|
|
739
|
+
FROM aclexplode(COALESCE(t.typacl, acldefault('T', t.typowner))) acl
|
|
740
|
+
WHERE acl.grantee = 0
|
|
741
|
+
AND acl.privilege_type = 'USAGE'
|
|
742
|
+
) AS public_has_privilege,
|
|
743
|
+
pg_has_role(current_user, t.typowner, 'USAGE') AS owns_object
|
|
744
|
+
FROM pg_type t
|
|
745
|
+
JOIN pg_namespace n ON n.oid = t.typnamespace
|
|
746
|
+
LEFT JOIN pg_depend d
|
|
747
|
+
ON d.classid = 'pg_type'::regclass
|
|
748
|
+
AND d.objid = t.oid
|
|
749
|
+
AND d.objsubid = 0
|
|
750
|
+
AND d.refclassid = 'pg_extension'::regclass
|
|
751
|
+
AND d.deptype = 'e'
|
|
752
|
+
LEFT JOIN pg_extension e ON e.oid = d.refobjid
|
|
753
|
+
WHERE n.nspname = ANY($1::text[])
|
|
754
|
+
-- Array type privileges are inherited from their element type
|
|
755
|
+
-- and PostgreSQL rejects direct GRANT/REVOKE on array types.
|
|
756
|
+
AND t.typelem = 0
|
|
757
|
+
AND has_type_privilege(current_user, t.oid, 'USAGE')
|
|
758
|
+
ORDER BY n.nspname, t.typname
|
|
759
|
+
`, [extensionSchemaNames])).rows;
|
|
760
|
+
const extensionFunctions = (await client.query(`
|
|
761
|
+
SELECT n.nspname AS schema_name,
|
|
762
|
+
p.proname AS object_name,
|
|
763
|
+
pg_get_function_identity_arguments(p.oid) AS identity_arguments,
|
|
764
|
+
e.extname AS extension_name,
|
|
765
|
+
EXISTS (
|
|
766
|
+
SELECT 1
|
|
767
|
+
FROM aclexplode(COALESCE(p.proacl, acldefault('f', p.proowner))) acl
|
|
768
|
+
WHERE acl.grantee = 0
|
|
769
|
+
AND acl.privilege_type = 'EXECUTE'
|
|
770
|
+
) AS public_has_privilege,
|
|
771
|
+
pg_has_role(current_user, p.proowner, 'USAGE') AS owns_object,
|
|
772
|
+
p.prosecdef AS security_definer,
|
|
773
|
+
p.proconfig AS configuration
|
|
774
|
+
FROM pg_proc p
|
|
775
|
+
JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
776
|
+
LEFT JOIN pg_depend d
|
|
777
|
+
ON d.classid = 'pg_proc'::regclass
|
|
778
|
+
AND d.objid = p.oid
|
|
779
|
+
AND d.objsubid = 0
|
|
780
|
+
AND d.refclassid = 'pg_extension'::regclass
|
|
781
|
+
AND d.deptype = 'e'
|
|
782
|
+
LEFT JOIN pg_extension e ON e.oid = d.refobjid
|
|
783
|
+
WHERE n.nspname = ANY($1::text[])
|
|
784
|
+
AND has_function_privilege(current_user, p.oid, 'EXECUTE')
|
|
785
|
+
ORDER BY n.nspname, p.proname, identity_arguments
|
|
786
|
+
`, [extensionSchemaNames])).rows;
|
|
787
|
+
assertExactExtensionObjects(access.extensionDependencies, extensionTypes, extensionFunctions, crossSchemaTablePrivileges, crossSchemaSequencePrivileges);
|
|
343
788
|
}
|
|
344
789
|
const ledgerPrivileges = requireSingleRow((await client.query(`
|
|
345
|
-
SELECT has_table_privilege(
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
) AS
|
|
350
|
-
has_table_privilege(
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
'DELETE'
|
|
364
|
-
) AS can_delete
|
|
790
|
+
SELECT has_table_privilege(current_user, c.oid, 'SELECT') AS can_select,
|
|
791
|
+
has_table_privilege(current_user, c.oid, 'INSERT') AS can_insert,
|
|
792
|
+
has_table_privilege(current_user, c.oid, 'UPDATE') AS can_update,
|
|
793
|
+
has_table_privilege(current_user, c.oid, 'DELETE') AS can_delete,
|
|
794
|
+
has_table_privilege(current_user, c.oid, 'TRUNCATE') AS can_truncate,
|
|
795
|
+
has_table_privilege(current_user, c.oid, 'REFERENCES') AS can_references,
|
|
796
|
+
has_table_privilege(current_user, c.oid, 'TRIGGER') AS can_trigger,
|
|
797
|
+
pg_has_role(current_user, c.relowner, 'USAGE') AS owns_object,
|
|
798
|
+
ARRAY(
|
|
799
|
+
SELECT acl.privilege_type
|
|
800
|
+
FROM aclexplode(COALESCE(c.relacl, acldefault('r', c.relowner))) acl
|
|
801
|
+
WHERE acl.grantee = 0
|
|
802
|
+
ORDER BY acl.privilege_type
|
|
803
|
+
) AS public_privileges
|
|
804
|
+
FROM pg_class c
|
|
805
|
+
WHERE c.oid = to_regclass(
|
|
806
|
+
format('%I.%I', $1::text, '_prisma_migrations')
|
|
807
|
+
)
|
|
365
808
|
`, [target.schema])).rows, 'migration ledger privilege');
|
|
366
|
-
if (!ledgerPrivileges.can_select
|
|
367
|
-
|
|
809
|
+
if (!ledgerPrivileges.can_select ||
|
|
810
|
+
ledgerPrivileges.public_privileges.length > 0 ||
|
|
811
|
+
ledgerPrivileges.owns_object) {
|
|
812
|
+
throw new Error(`${LOG_PREFIX} runtime role ${identity.role_name} does not have exact private SELECT-only access to ` +
|
|
813
|
+
`${target.schema}._prisma_migrations (PUBLIC=[${ledgerPrivileges.public_privileges.join('/')}], owns=${ledgerPrivileges.owns_object}).`);
|
|
368
814
|
}
|
|
369
815
|
if (ledgerPrivileges.can_insert ||
|
|
370
816
|
ledgerPrivileges.can_update ||
|
|
371
|
-
ledgerPrivileges.can_delete
|
|
817
|
+
ledgerPrivileges.can_delete ||
|
|
818
|
+
ledgerPrivileges.can_truncate ||
|
|
819
|
+
ledgerPrivileges.can_references ||
|
|
820
|
+
ledgerPrivileges.can_trigger) {
|
|
372
821
|
throw new Error(`${LOG_PREFIX} runtime role ${identity.role_name} has forbidden write privilege on ` +
|
|
373
822
|
`${target.schema}._prisma_migrations.`);
|
|
374
823
|
}
|