@cerebruminc/yates 3.7.1 → 3.7.2-beta.dangerous.30d5b2f
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/CHANGELOG.md +7 -0
- package/dist/index.d.ts +43 -23
- package/dist/index.js +739 -629
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -113,9 +113,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
113
113
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
114
114
|
};
|
|
115
115
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
116
|
-
exports.setup = exports.
|
|
116
|
+
exports.setup = exports.Yates = exports.sanitizeSlug = void 0;
|
|
117
117
|
var crypto = __importStar(require("crypto"));
|
|
118
118
|
var debug_1 = __importDefault(require("debug"));
|
|
119
|
+
var cloneDeep_1 = __importDefault(require("lodash/cloneDeep"));
|
|
119
120
|
var difference_1 = __importDefault(require("lodash/difference"));
|
|
120
121
|
var flatMap_1 = __importDefault(require("lodash/flatMap"));
|
|
121
122
|
var map_1 = __importDefault(require("lodash/map"));
|
|
@@ -130,23 +131,6 @@ var debug = (0, debug_1.default)("yates");
|
|
|
130
131
|
var takeLock = function (prisma) {
|
|
131
132
|
return prisma.$executeRawUnsafe("SELECT pg_advisory_xact_lock(2142616474639426746);");
|
|
132
133
|
};
|
|
133
|
-
/*
|
|
134
|
-
* This function creates a table used to track the abilities that have been
|
|
135
|
-
* defined in the system. We can use this to see if an ability needs to be updated.
|
|
136
|
-
* We can't look up the pg policy table for this, as pg performs formatting on
|
|
137
|
-
* the expression, making it very hard to check if the two expressions are equivalent.
|
|
138
|
-
*
|
|
139
|
-
* We also need to create a schema for this table, as we don't want to pollute the public schema.
|
|
140
|
-
* If we use the public schema, we could potentially conflict with a user's table and we will
|
|
141
|
-
* also cause issues for Prisma's migrate tooling, as it will detect a DB drift.
|
|
142
|
-
*/
|
|
143
|
-
var setupAbilityTable = function (prisma) {
|
|
144
|
-
return prisma.$transaction([
|
|
145
|
-
takeLock(prisma),
|
|
146
|
-
prisma.$executeRawUnsafe("\n\t\tCREATE SCHEMA IF NOT EXISTS _yates;\n\t\t"),
|
|
147
|
-
prisma.$executeRawUnsafe("\n\t\tCREATE TABLE IF NOT EXISTS _yates._yates_abilities (\n\t\t\tid SERIAL PRIMARY KEY,\n\t\t\tability_model TEXT NOT NULL,\n\t\t\tability_name TEXT NOT NULL,\n\t\t\tability_policy_name TEXT NOT NULL UNIQUE,\n\t\t\tability_description TEXT NOT NULL,\n\t\t\tability_operation TEXT NOT NULL,\n\t\t\tability_expression TEXT NOT NULL,\n\t\t\tcreated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n\t\t\tupdated_at TIMESTAMP\n\t\t);\n\t"),
|
|
148
|
-
]);
|
|
149
|
-
};
|
|
150
134
|
var upsertAbility = function (prisma, ability) {
|
|
151
135
|
var ability_model = ability.ability_model, ability_name = ability.ability_name, ability_policy_name = ability.ability_policy_name, ability_description = ability.ability_description, ability_operation = ability.ability_operation, ability_expression = ability.ability_expression;
|
|
152
136
|
return prisma.$queryRaw(templateObject_1 || (templateObject_1 = __makeTemplateObject(["\n\t\tINSERT INTO _yates._yates_abilities (ability_model, ability_name, ability_policy_name, ability_description, ability_operation, ability_expression)\n\t\tVALUES (", ", ", ", ", ", ", ", ", ", ", ")\n\t\tON CONFLICT (ability_policy_name) DO UPDATE\n\t\tSET ability_model = EXCLUDED.ability_model, ability_name = EXCLUDED.ability_name, ability_description = EXCLUDED.ability_description, ability_operation = EXCLUDED.ability_operation, ability_expression = EXCLUDED.ability_expression, updated_at = now();\n\t"], ["\n\t\tINSERT INTO _yates._yates_abilities (ability_model, ability_name, ability_policy_name, ability_description, ability_operation, ability_expression)\n\t\tVALUES (", ", ", ", ", ", ", ", ", ", ", ")\n\t\tON CONFLICT (ability_policy_name) DO UPDATE\n\t\tSET ability_model = EXCLUDED.ability_model, ability_name = EXCLUDED.ability_name, ability_description = EXCLUDED.ability_description, ability_operation = EXCLUDED.ability_operation, ability_expression = EXCLUDED.ability_expression, updated_at = now();\n\t"])), ability_model, ability_name, ability_policy_name, ability_description, ability_operation, ability_expression);
|
|
@@ -173,660 +157,786 @@ var sanitizeSlug = function (slug) {
|
|
|
173
157
|
.replace(/[^a-z0-9_]/gi, "");
|
|
174
158
|
};
|
|
175
159
|
exports.sanitizeSlug = sanitizeSlug;
|
|
176
|
-
var
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
if (query.query.arguments) {
|
|
194
|
-
parts.push(buildKeysString(query.query.arguments));
|
|
195
|
-
}
|
|
196
|
-
parts.push(buildKeysString(query.query.selection));
|
|
197
|
-
return parts.join("");
|
|
198
|
-
}
|
|
199
|
-
exports.getBatchId = getBatchId;
|
|
200
|
-
function buildKeysString(obj) {
|
|
201
|
-
var keysArray = Object.keys(obj)
|
|
202
|
-
.sort()
|
|
203
|
-
.map(function (key) {
|
|
204
|
-
// @ts-ignore
|
|
205
|
-
var value = obj[key];
|
|
206
|
-
if (typeof value === "object" && value !== null) {
|
|
207
|
-
return "(".concat(key, " ").concat(buildKeysString(value), ")");
|
|
208
|
-
}
|
|
209
|
-
return key;
|
|
210
|
-
});
|
|
211
|
-
return "(".concat(keysArray.join(" "), ")");
|
|
212
|
-
}
|
|
213
|
-
// This uses client extensions to set the role and context for the current user so that RLS can be applied
|
|
214
|
-
var createClient = function (prisma, getContext, options) {
|
|
215
|
-
if (options === void 0) { options = {}; }
|
|
216
|
-
// Set default options
|
|
217
|
-
var _a = options.txMaxWait, txMaxWait = _a === void 0 ? 30000 : _a, _b = options.txTimeout, txTimeout = _b === void 0 ? 30000 : _b;
|
|
218
|
-
// By default, Prisma will batch requests by the transaction ID if it is present.
|
|
219
|
-
// This behaviour prevents automatic batching from working when using Yates, since all queries are executed inside an interactive transaction.
|
|
220
|
-
// To get around this we by monkey patching the batching function to use the Yates ID as the batch ID.
|
|
221
|
-
// To get the batching to work we also need to ensure that all the requests we might want to batch together are generated inside the same tick.
|
|
222
|
-
// This means that all the requests per-tick that have the same role and context values will be batched together,
|
|
223
|
-
// allowing the in-built prisma batch optimizations to work for us.
|
|
224
|
-
// This is why we use process.nextTick and the tickActive flag to ensure we only tick once at a time.
|
|
225
|
-
// See:
|
|
226
|
-
// - https://github.com/prisma/prisma/blob/5.21.1/packages/client/src/runtime/RequestHandler.ts#L122
|
|
227
|
-
// - https://www.prisma.io/docs/orm/prisma-client/queries/query-optimization-performance
|
|
228
|
-
prisma._requestHandler.dataloader.options.batchBy = function (request) {
|
|
229
|
-
var _a;
|
|
230
|
-
var batchIdPQ = getBatchId(request.protocolQuery);
|
|
231
|
-
if ((_a = request.transaction) === null || _a === void 0 ? void 0 : _a.id) {
|
|
232
|
-
return "transaction-".concat(request.transaction.id).concat(batchIdPQ ? "-".concat(batchIdPQ) : "");
|
|
233
|
-
}
|
|
234
|
-
return getBatchId(request.protocolQuery);
|
|
235
|
-
};
|
|
236
|
-
var tickActive = false;
|
|
237
|
-
var batches = {};
|
|
238
|
-
// This function is called once per tick, and processes all the batches that have been created during that tick.
|
|
239
|
-
// Each batch represents a unique role + context combination, and contains all the requests that need to be executed with that role + context.
|
|
240
|
-
var dispatchBatches = function () {
|
|
241
|
-
var e_1, _a;
|
|
242
|
-
var _loop_1 = function (key, batch) {
|
|
243
|
-
delete batches[key];
|
|
244
|
-
// Because batch transactions inside a prisma client query extension can run out of order if used with async middleware,
|
|
245
|
-
// we need to run the logic inside an interactive transaction, however this brings a different set of problems in that the
|
|
246
|
-
// main query will no longer automatically run inside the transaction. We resolve this issue by manually executing the prisma request.
|
|
247
|
-
// See https://github.com/prisma/prisma/issues/18276
|
|
248
|
-
prisma
|
|
249
|
-
.$transaction(function (tx) { return __awaiter(void 0, void 0, void 0, function () {
|
|
250
|
-
var _a, _b, _c, key_1, value, e_2_1, txId, results;
|
|
251
|
-
var e_2, _d;
|
|
252
|
-
return __generator(this, function (_e) {
|
|
253
|
-
switch (_e.label) {
|
|
254
|
-
case 0:
|
|
255
|
-
// Switch to the user role, We can't use a prepared statement here, due to limitations in PG not allowing prepared statements to be used in SET LOCAL ROLE
|
|
256
|
-
return [4 /*yield*/, tx.$queryRawUnsafe("SET LOCAL ROLE ".concat(batch.pgRole))];
|
|
257
|
-
case 1:
|
|
258
|
-
// Switch to the user role, We can't use a prepared statement here, due to limitations in PG not allowing prepared statements to be used in SET LOCAL ROLE
|
|
259
|
-
_e.sent();
|
|
260
|
-
_e.label = 2;
|
|
261
|
-
case 2:
|
|
262
|
-
_e.trys.push([2, 7, 8, 9]);
|
|
263
|
-
_a = __values((0, toPairs_1.default)(batch.context)), _b = _a.next();
|
|
264
|
-
_e.label = 3;
|
|
265
|
-
case 3:
|
|
266
|
-
if (!!_b.done) return [3 /*break*/, 6];
|
|
267
|
-
_c = __read(_b.value, 2), key_1 = _c[0], value = _c[1];
|
|
268
|
-
return [4 /*yield*/, tx.$queryRaw(templateObject_2 || (templateObject_2 = __makeTemplateObject(["SELECT set_config(", ", ", ", true);"], ["SELECT set_config(", ", ", ", true);"])), key_1, value.toString())];
|
|
269
|
-
case 4:
|
|
270
|
-
_e.sent();
|
|
271
|
-
_e.label = 5;
|
|
272
|
-
case 5:
|
|
273
|
-
_b = _a.next();
|
|
274
|
-
return [3 /*break*/, 3];
|
|
275
|
-
case 6: return [3 /*break*/, 9];
|
|
276
|
-
case 7:
|
|
277
|
-
e_2_1 = _e.sent();
|
|
278
|
-
e_2 = { error: e_2_1 };
|
|
279
|
-
return [3 /*break*/, 9];
|
|
280
|
-
case 8:
|
|
281
|
-
try {
|
|
282
|
-
if (_b && !_b.done && (_d = _a.return)) _d.call(_a);
|
|
283
|
-
}
|
|
284
|
-
finally { if (e_2) throw e_2.error; }
|
|
285
|
-
return [7 /*endfinally*/];
|
|
286
|
-
case 9:
|
|
287
|
-
txId = tx[Symbol.for("prisma.client.transaction.id")];
|
|
288
|
-
return [4 /*yield*/, Promise.all(batch.requests.map(function (request) {
|
|
289
|
-
return prisma._executeRequest(__assign(__assign({}, request.params), { transaction: {
|
|
290
|
-
kind: "itx",
|
|
291
|
-
id: txId,
|
|
292
|
-
} }));
|
|
293
|
-
}))];
|
|
294
|
-
case 10:
|
|
295
|
-
results = _e.sent();
|
|
296
|
-
return [2 /*return*/, results];
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
}); }, {
|
|
300
|
-
maxWait: txMaxWait,
|
|
301
|
-
timeout: txTimeout,
|
|
302
|
-
})
|
|
303
|
-
.then(function (results) {
|
|
304
|
-
results.forEach(function (result, index) {
|
|
305
|
-
batch.requests[index].resolve(result);
|
|
306
|
-
});
|
|
307
|
-
})
|
|
308
|
-
.catch(function (e) {
|
|
309
|
-
var e_3, _a;
|
|
310
|
-
try {
|
|
311
|
-
for (var _b = (e_3 = void 0, __values(batch.requests)), _c = _b.next(); !_c.done; _c = _b.next()) {
|
|
312
|
-
var request = _c.value;
|
|
313
|
-
request.reject(e);
|
|
314
|
-
}
|
|
160
|
+
var Yates = /** @class */ (function () {
|
|
161
|
+
function Yates(prisma) {
|
|
162
|
+
var _this = this;
|
|
163
|
+
this.prisma = prisma;
|
|
164
|
+
this.databaseScope = null;
|
|
165
|
+
this.init = function () { return __awaiter(_this, void 0, void 0, function () {
|
|
166
|
+
return __generator(this, function (_a) {
|
|
167
|
+
switch (_a.label) {
|
|
168
|
+
case 0: return [4 /*yield*/, this.ensureDatabaseScope()];
|
|
169
|
+
case 1:
|
|
170
|
+
_a.sent();
|
|
171
|
+
debug("Setting up ability table");
|
|
172
|
+
return [4 /*yield*/, this.setupAbilityTable()];
|
|
173
|
+
case 2:
|
|
174
|
+
_a.sent();
|
|
175
|
+
return [2 /*return*/];
|
|
315
176
|
}
|
|
316
|
-
catch (e_3_1) { e_3 = { error: e_3_1 }; }
|
|
317
|
-
finally {
|
|
318
|
-
try {
|
|
319
|
-
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
|
|
320
|
-
}
|
|
321
|
-
finally { if (e_3) throw e_3.error; }
|
|
322
|
-
}
|
|
323
|
-
delete batches[key];
|
|
324
177
|
});
|
|
178
|
+
}); };
|
|
179
|
+
this.createDatabaseScope = function (databaseName) {
|
|
180
|
+
var sanitizedName = (0, exports.sanitizeSlug)(databaseName);
|
|
181
|
+
if (sanitizedName.length > 0) {
|
|
182
|
+
return sanitizedName;
|
|
183
|
+
}
|
|
184
|
+
var hash = crypto.createHash("sha256");
|
|
185
|
+
hash.update(databaseName);
|
|
186
|
+
return hash.digest("hex").slice(0, 8);
|
|
325
187
|
};
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
_loop_1(key, batch);
|
|
188
|
+
this.getDatabaseScope = function () {
|
|
189
|
+
if (!_this.databaseScope) {
|
|
190
|
+
throw new Error("Yates database scope has not been initialised. Ensure setup() has been called before using the client.");
|
|
330
191
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
192
|
+
return _this.databaseScope;
|
|
193
|
+
};
|
|
194
|
+
this.ensureDatabaseScope = function () { return __awaiter(_this, void 0, void 0, function () {
|
|
195
|
+
var result, currentDatabase;
|
|
196
|
+
var _a;
|
|
197
|
+
return __generator(this, function (_b) {
|
|
198
|
+
switch (_b.label) {
|
|
199
|
+
case 0:
|
|
200
|
+
if (this.databaseScope) {
|
|
201
|
+
return [2 /*return*/, this.databaseScope];
|
|
202
|
+
}
|
|
203
|
+
return [4 /*yield*/, this.prisma.$queryRawUnsafe("select current_database() as current_database;")];
|
|
204
|
+
case 1:
|
|
205
|
+
result = _b.sent();
|
|
206
|
+
currentDatabase = (_a = result[0]) === null || _a === void 0 ? void 0 : _a.current_database;
|
|
207
|
+
debug("Current database for Yates:", currentDatabase);
|
|
208
|
+
if (!currentDatabase) {
|
|
209
|
+
throw new Error("Failed to determine the current database for scoping Yates roles.");
|
|
210
|
+
}
|
|
211
|
+
this.databaseScope = this.createDatabaseScope(currentDatabase);
|
|
212
|
+
return [2 /*return*/, this.databaseScope];
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}); };
|
|
216
|
+
/*
|
|
217
|
+
* This function creates a table used to track the abilities that have been
|
|
218
|
+
* defined in the system. We can use this to see if an ability needs to be updated.
|
|
219
|
+
* We can't look up the pg policy table for this, as pg performs formatting on
|
|
220
|
+
* the expression, making it very hard to check if the two expressions are equivalent.
|
|
221
|
+
*
|
|
222
|
+
* We also need to create a schema for this table, as we don't want to pollute the public schema.
|
|
223
|
+
* If we use the public schema, we could potentially conflict with a user's table and we will
|
|
224
|
+
* also cause issues for Prisma's migrate tooling, as it will detect a DB drift.
|
|
225
|
+
*/
|
|
226
|
+
this.setupAbilityTable = function () {
|
|
227
|
+
return _this.prisma.$transaction([
|
|
228
|
+
takeLock(_this.prisma),
|
|
229
|
+
_this.prisma.$executeRawUnsafe("\n\t\t\t\tCREATE SCHEMA IF NOT EXISTS _yates;\n\t\t\t"),
|
|
230
|
+
_this.prisma.$executeRawUnsafe("\n\t\t\t\tCREATE TABLE IF NOT EXISTS _yates._yates_abilities (\n\t\t\t\t\tid SERIAL PRIMARY KEY,\n\t\t\t\t\tability_model TEXT NOT NULL,\n\t\t\t\t\tability_name TEXT NOT NULL,\n\t\t\t\t\tability_policy_name TEXT NOT NULL UNIQUE,\n\t\t\t\t\tability_description TEXT NOT NULL,\n\t\t\t\t\tability_operation TEXT NOT NULL,\n\t\t\t\t\tability_expression TEXT NOT NULL,\n\t\t\t\t\tcreated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n\t\t\t\t\tupdated_at TIMESTAMP\n\t\t\t\t);\n\t\t\t"),
|
|
231
|
+
]);
|
|
232
|
+
};
|
|
233
|
+
this.createAbilityName = function (model, ability) {
|
|
234
|
+
var scope = _this.getDatabaseScope();
|
|
235
|
+
return (0, exports.sanitizeSlug)(hashWithPrefix("yates_ability_", "".concat(scope, "_").concat(model, "_").concat(ability)));
|
|
236
|
+
};
|
|
237
|
+
this.createRoleName = function (name) {
|
|
238
|
+
var scope = _this.getDatabaseScope();
|
|
239
|
+
return (0, exports.sanitizeSlug)(hashWithPrefix("yates_role_", "".concat(scope, "_").concat(name)));
|
|
240
|
+
};
|
|
241
|
+
this.getDefaultAbilities = function (models) {
|
|
242
|
+
var e_1, _a;
|
|
243
|
+
var abilities = {};
|
|
334
244
|
try {
|
|
335
|
-
|
|
245
|
+
for (var models_1 = __values(models), models_1_1 = models_1.next(); !models_1_1.done; models_1_1 = models_1.next()) {
|
|
246
|
+
var model = models_1_1.value;
|
|
247
|
+
abilities[model] = {
|
|
248
|
+
create: {
|
|
249
|
+
description: "Create ".concat(model),
|
|
250
|
+
expression: "true",
|
|
251
|
+
operation: "INSERT",
|
|
252
|
+
model: model,
|
|
253
|
+
slug: "create",
|
|
254
|
+
},
|
|
255
|
+
read: {
|
|
256
|
+
description: "Read ".concat(model),
|
|
257
|
+
expression: "true",
|
|
258
|
+
operation: "SELECT",
|
|
259
|
+
model: model,
|
|
260
|
+
slug: "read",
|
|
261
|
+
},
|
|
262
|
+
update: {
|
|
263
|
+
description: "Update ".concat(model),
|
|
264
|
+
expression: "true",
|
|
265
|
+
operation: "UPDATE",
|
|
266
|
+
model: model,
|
|
267
|
+
slug: "update",
|
|
268
|
+
},
|
|
269
|
+
delete: {
|
|
270
|
+
description: "Delete ".concat(model),
|
|
271
|
+
expression: "true",
|
|
272
|
+
operation: "DELETE",
|
|
273
|
+
model: model,
|
|
274
|
+
slug: "delete",
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
336
278
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
name: "Yates client",
|
|
342
|
-
query: {
|
|
343
|
-
$allModels: {
|
|
344
|
-
$allOperations: function (params) {
|
|
345
|
-
return __awaiter(this, void 0, void 0, function () {
|
|
346
|
-
var model, args, query, operation, ctx, role, context, pgRole, _a, _b, k, _c, _d, v, txId, hash, __internalParams;
|
|
347
|
-
var e_4, _e, e_5, _f;
|
|
348
|
-
return __generator(this, function (_g) {
|
|
349
|
-
model = params.model, args = params.args, query = params.query, operation = params.operation;
|
|
350
|
-
if (!model) {
|
|
351
|
-
// If the model is not defined, we can't apply RLS
|
|
352
|
-
// This can occur when you are making a call with Prisma's $queryRaw method
|
|
353
|
-
return [2 /*return*/, query(args)];
|
|
354
|
-
}
|
|
355
|
-
ctx = getContext();
|
|
356
|
-
// If ctx is null, the middleware is explicitly skipped
|
|
357
|
-
if (ctx === null) {
|
|
358
|
-
return [2 /*return*/, query(args)];
|
|
359
|
-
}
|
|
360
|
-
role = ctx.role, context = ctx.context;
|
|
361
|
-
pgRole = (0, exports.createRoleName)(role);
|
|
362
|
-
if (context) {
|
|
363
|
-
try {
|
|
364
|
-
for (_a = __values(Object.keys(context)), _b = _a.next(); !_b.done; _b = _a.next()) {
|
|
365
|
-
k = _b.value;
|
|
366
|
-
if (!k.match(/^[a-z_\.]+$/)) {
|
|
367
|
-
throw new Error("Context variable \"".concat(k, "\" contains invalid characters. Context variables must only contain lowercase letters, numbers, periods and underscores."));
|
|
368
|
-
}
|
|
369
|
-
if (typeof context[k] !== "number" &&
|
|
370
|
-
typeof context[k] !== "string" &&
|
|
371
|
-
!Array.isArray(context[k])) {
|
|
372
|
-
throw new Error("Context variable \"".concat(k, "\" must be a string, number or array. Got ").concat(typeof context[k]));
|
|
373
|
-
}
|
|
374
|
-
if (Array.isArray(context[k])) {
|
|
375
|
-
try {
|
|
376
|
-
for (_c = (e_5 = void 0, __values(context[k])), _d = _c.next(); !_d.done; _d = _c.next()) {
|
|
377
|
-
v = _d.value;
|
|
378
|
-
if (typeof v !== "string") {
|
|
379
|
-
throw new Error("Context variable \"".concat(k, "\" must be an array of strings. Got ").concat(typeof v));
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
catch (e_5_1) { e_5 = { error: e_5_1 }; }
|
|
384
|
-
finally {
|
|
385
|
-
try {
|
|
386
|
-
if (_d && !_d.done && (_f = _c.return)) _f.call(_c);
|
|
387
|
-
}
|
|
388
|
-
finally { if (e_5) throw e_5.error; }
|
|
389
|
-
}
|
|
390
|
-
// Cast to a JSON string so that it can be used in RLS expressions
|
|
391
|
-
context[k] = JSON.stringify(context[k]);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
catch (e_4_1) { e_4 = { error: e_4_1 }; }
|
|
396
|
-
finally {
|
|
397
|
-
try {
|
|
398
|
-
if (_b && !_b.done && (_e = _a.return)) _e.call(_a);
|
|
399
|
-
}
|
|
400
|
-
finally { if (e_4) throw e_4.error; }
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
txId = hashWithPrefix("yates_tx_", JSON.stringify(ctx));
|
|
404
|
-
hash = txId;
|
|
405
|
-
if (!batches[hash]) {
|
|
406
|
-
// Create a new batch for this role + context combination
|
|
407
|
-
batches[hash] = {
|
|
408
|
-
pgRole: pgRole,
|
|
409
|
-
context: context,
|
|
410
|
-
requests: [],
|
|
411
|
-
};
|
|
412
|
-
// make sure, that we only tick once at a time
|
|
413
|
-
if (!tickActive) {
|
|
414
|
-
tickActive = true;
|
|
415
|
-
process.nextTick(function () {
|
|
416
|
-
dispatchBatches();
|
|
417
|
-
tickActive = false;
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
__internalParams = params.__internalParams;
|
|
422
|
-
// Add the request to the batch, and return a promise that will be resolved or rejected in dispatchBatches
|
|
423
|
-
return [2 /*return*/, new Promise(function (resolve, reject) {
|
|
424
|
-
batches[hash].requests.push({
|
|
425
|
-
params: __internalParams,
|
|
426
|
-
query: query,
|
|
427
|
-
args: args,
|
|
428
|
-
resolve: resolve,
|
|
429
|
-
reject: reject,
|
|
430
|
-
});
|
|
431
|
-
}).catch(function (e) {
|
|
432
|
-
var _a;
|
|
433
|
-
// Normalize RLS errors to make them a bit more readable.
|
|
434
|
-
if ((_a = e.message) === null || _a === void 0 ? void 0 : _a.includes("new row violates row-level security policy for table")) {
|
|
435
|
-
throw new Error("You do not have permission to perform this action: ".concat(model, ".").concat(operation, "(...)"));
|
|
436
|
-
}
|
|
437
|
-
throw e;
|
|
438
|
-
})];
|
|
439
|
-
});
|
|
440
|
-
});
|
|
441
|
-
},
|
|
442
|
-
},
|
|
443
|
-
},
|
|
444
|
-
});
|
|
445
|
-
return client;
|
|
446
|
-
};
|
|
447
|
-
exports.createClient = createClient;
|
|
448
|
-
var setRLS = function (prisma, table, roleName, slug, ability) { return __awaiter(void 0, void 0, void 0, function () {
|
|
449
|
-
var operation, rawExpression, description;
|
|
450
|
-
return __generator(this, function (_a) {
|
|
451
|
-
switch (_a.label) {
|
|
452
|
-
case 0:
|
|
453
|
-
operation = ability.operation, rawExpression = ability.expression, description = ability.description;
|
|
454
|
-
if (!rawExpression) {
|
|
455
|
-
throw new Error("Expression must be defined for RLS abilities");
|
|
279
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
280
|
+
finally {
|
|
281
|
+
try {
|
|
282
|
+
if (models_1_1 && !models_1_1.done && (_a = models_1.return)) _a.call(models_1);
|
|
456
283
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
284
|
+
finally { if (e_1) throw e_1.error; }
|
|
285
|
+
}
|
|
286
|
+
return abilities;
|
|
287
|
+
};
|
|
288
|
+
// This uses client extensions to set the role and context for the current user so that RLS can be applied
|
|
289
|
+
this.createClient = function (getContext, options) {
|
|
290
|
+
if (options === void 0) { options = {}; }
|
|
291
|
+
var prisma = _this.prisma;
|
|
292
|
+
// Set default options
|
|
293
|
+
var _a = options.txMaxWait, txMaxWait = _a === void 0 ? 30000 : _a, _b = options.txTimeout, txTimeout = _b === void 0 ? 30000 : _b;
|
|
294
|
+
// By default, Prisma will batch requests by the transaction ID if it is present.
|
|
295
|
+
// This behaviour prevents automatic batching from working when using Yates, since all queries are executed inside an interactive transaction.
|
|
296
|
+
// To get around this we by monkey patching the batching function to use the Yates ID as the batch ID.
|
|
297
|
+
// To get the batching to work we also need to ensure that all the requests we might want to batch together are generated inside the same tick.
|
|
298
|
+
// This means that all the requests per-tick that have the same role and context values will be batched together,
|
|
299
|
+
// allowing the in-built prisma batch optimizations to work for us.
|
|
300
|
+
// This is why we use process.nextTick and the tickActive flag to ensure we only tick once at a time.
|
|
301
|
+
// See:
|
|
302
|
+
// - https://github.com/prisma/prisma/blob/5.21.1/packages/client/src/runtime/RequestHandler.ts#L122
|
|
303
|
+
// - https://www.prisma.io/docs/orm/prisma-client/queries/query-optimization-performance
|
|
304
|
+
prisma._requestHandler.dataloader.options.batchBy = function (request) {
|
|
305
|
+
var _a;
|
|
306
|
+
var batchIdPQ = _this.getBatchId(request.protocolQuery);
|
|
307
|
+
if ((_a = request.transaction) === null || _a === void 0 ? void 0 : _a.id) {
|
|
308
|
+
return "transaction-".concat(request.transaction.id).concat(batchIdPQ ? "-".concat(batchIdPQ) : "");
|
|
309
|
+
}
|
|
310
|
+
return _this.getBatchId(request.protocolQuery);
|
|
311
|
+
};
|
|
312
|
+
var tickActive = false;
|
|
313
|
+
var batches = {};
|
|
314
|
+
// This function is called once per tick, and processes all the batches that have been created during that tick.
|
|
315
|
+
// Each batch represents a unique role + context combination, and contains all the requests that need to be executed with that role + context.
|
|
316
|
+
var dispatchBatches = function () {
|
|
317
|
+
var e_2, _a;
|
|
318
|
+
var _loop_1 = function (key, batch) {
|
|
319
|
+
delete batches[key];
|
|
320
|
+
// Because batch transactions inside a prisma client query extension can run out of order if used with async middleware,
|
|
321
|
+
// we need to run the logic inside an interactive transaction, however this brings a different set of problems in that the
|
|
322
|
+
// main query will no longer automatically run inside the transaction. We resolve this issue by manually executing the prisma request.
|
|
323
|
+
// See https://github.com/prisma/prisma/issues/18276
|
|
324
|
+
prisma
|
|
325
|
+
.$transaction(function (tx) { return __awaiter(_this, void 0, void 0, function () {
|
|
326
|
+
var _a, _b, _c, key_1, value, e_3_1, txId, results;
|
|
327
|
+
var e_3, _d;
|
|
328
|
+
return __generator(this, function (_e) {
|
|
329
|
+
switch (_e.label) {
|
|
330
|
+
case 0:
|
|
331
|
+
// Switch to the user role, We can't use a prepared statement here, due to limitations in PG not allowing prepared statements to be used in SET LOCAL ROLE
|
|
332
|
+
return [4 /*yield*/, tx.$queryRawUnsafe("SET LOCAL ROLE ".concat(batch.pgRole))];
|
|
464
333
|
case 1:
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
334
|
+
// Switch to the user role, We can't use a prepared statement here, due to limitations in PG not allowing prepared statements to be used in SET LOCAL ROLE
|
|
335
|
+
_e.sent();
|
|
336
|
+
_e.label = 2;
|
|
468
337
|
case 2:
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
if (!!existingAbility) return [3 /*break*/, 8];
|
|
473
|
-
debug("Creating RLS policy for", roleName, "on", table, "for", operation);
|
|
474
|
-
return [4 /*yield*/, (0, expressions_1.expressionToSQL)(rawExpression, table)];
|
|
338
|
+
_e.trys.push([2, 7, 8, 9]);
|
|
339
|
+
_a = __values((0, toPairs_1.default)(batch.context)), _b = _a.next();
|
|
340
|
+
_e.label = 3;
|
|
475
341
|
case 3:
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
return [4 /*yield*/, tx.$
|
|
342
|
+
if (!!_b.done) return [3 /*break*/, 6];
|
|
343
|
+
_c = __read(_b.value, 2), key_1 = _c[0], value = _c[1];
|
|
344
|
+
return [4 /*yield*/, tx.$queryRaw(templateObject_2 || (templateObject_2 = __makeTemplateObject(["SELECT set_config(", ", ", ", true);"], ["SELECT set_config(", ", ", ", true);"])), key_1, value.toString())];
|
|
479
345
|
case 4:
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
case 5:
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
346
|
+
_e.sent();
|
|
347
|
+
_e.label = 5;
|
|
348
|
+
case 5:
|
|
349
|
+
_b = _a.next();
|
|
350
|
+
return [3 /*break*/, 3];
|
|
351
|
+
case 6: return [3 /*break*/, 9];
|
|
486
352
|
case 7:
|
|
487
|
-
|
|
488
|
-
|
|
353
|
+
e_3_1 = _e.sent();
|
|
354
|
+
e_3 = { error: e_3_1 };
|
|
355
|
+
return [3 /*break*/, 9];
|
|
489
356
|
case 8:
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
357
|
+
try {
|
|
358
|
+
if (_b && !_b.done && (_d = _a.return)) _d.call(_a);
|
|
359
|
+
}
|
|
360
|
+
finally { if (e_3) throw e_3.error; }
|
|
361
|
+
return [7 /*endfinally*/];
|
|
493
362
|
case 9:
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
363
|
+
txId = tx[Symbol.for("prisma.client.transaction.id")];
|
|
364
|
+
return [4 /*yield*/, Promise.all(batch.requests.map(function (request) {
|
|
365
|
+
return prisma._executeRequest(__assign(__assign({}, request.params), { transaction: {
|
|
366
|
+
kind: "itx",
|
|
367
|
+
id: txId,
|
|
368
|
+
} }));
|
|
369
|
+
}))];
|
|
497
370
|
case 10:
|
|
498
|
-
|
|
499
|
-
return [
|
|
500
|
-
case 11: return [4 /*yield*/, tx.$queryRawUnsafe("\n\t\t\t\tALTER POLICY ".concat(policyName, " ON \"public\".\"").concat(table, "\" TO ").concat(roleName, " USING (").concat(expression, ");\n\t\t\t"))];
|
|
501
|
-
case 12:
|
|
502
|
-
_a.sent();
|
|
503
|
-
_a.label = 13;
|
|
504
|
-
case 13:
|
|
505
|
-
shouldUpdateAbilityTable = true;
|
|
506
|
-
_a.label = 14;
|
|
507
|
-
case 14:
|
|
508
|
-
if (!shouldUpdateAbilityTable) return [3 /*break*/, 16];
|
|
509
|
-
return [4 /*yield*/, upsertAbility(tx, {
|
|
510
|
-
ability_model: table,
|
|
511
|
-
ability_name: slug,
|
|
512
|
-
ability_policy_name: policyName,
|
|
513
|
-
ability_description: description !== null && description !== void 0 ? description : "",
|
|
514
|
-
ability_operation: operation,
|
|
515
|
-
// We store the string representation of the expression so that
|
|
516
|
-
// we can compare it later without having to recompute the SQL
|
|
517
|
-
ability_expression: rawExpression.toString(),
|
|
518
|
-
})];
|
|
519
|
-
case 15:
|
|
520
|
-
_a.sent();
|
|
521
|
-
_a.label = 16;
|
|
522
|
-
case 16: return [2 /*return*/];
|
|
371
|
+
results = _e.sent();
|
|
372
|
+
return [2 /*return*/, results];
|
|
523
373
|
}
|
|
524
374
|
});
|
|
525
|
-
}); }
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
})
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
if (customAbilities) {
|
|
547
|
-
diff = (0, difference_1.default)(Object.keys(customAbilities), models);
|
|
548
|
-
if (diff.length) {
|
|
549
|
-
throw new Error("Invalid models in custom abilities: ".concat(diff.join(", ")));
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
try {
|
|
553
|
-
for (models_1 = __values(models), models_1_1 = models_1.next(); !models_1_1.done; models_1_1 = models_1.next()) {
|
|
554
|
-
model = models_1_1.value;
|
|
555
|
-
abilities[model] = {
|
|
556
|
-
create: {
|
|
557
|
-
description: "Create ".concat(model),
|
|
558
|
-
expression: "true",
|
|
559
|
-
operation: "INSERT",
|
|
560
|
-
model: model,
|
|
561
|
-
slug: "create",
|
|
562
|
-
},
|
|
563
|
-
read: {
|
|
564
|
-
description: "Read ".concat(model),
|
|
565
|
-
expression: "true",
|
|
566
|
-
operation: "SELECT",
|
|
567
|
-
model: model,
|
|
568
|
-
slug: "read",
|
|
569
|
-
},
|
|
570
|
-
update: {
|
|
571
|
-
description: "Update ".concat(model),
|
|
572
|
-
expression: "true",
|
|
573
|
-
operation: "UPDATE",
|
|
574
|
-
model: model,
|
|
575
|
-
slug: "update",
|
|
576
|
-
},
|
|
577
|
-
delete: {
|
|
578
|
-
description: "Delete ".concat(model),
|
|
579
|
-
expression: "true",
|
|
580
|
-
operation: "DELETE",
|
|
581
|
-
model: model,
|
|
582
|
-
slug: "delete",
|
|
583
|
-
},
|
|
584
|
-
};
|
|
585
|
-
if (customAbilities === null || customAbilities === void 0 ? void 0 : customAbilities[model]) {
|
|
586
|
-
for (ability in customAbilities[model]) {
|
|
587
|
-
operation =
|
|
588
|
-
// biome-ignore lint/style/noNonNullAssertion: TODO fix this
|
|
589
|
-
(_q = customAbilities[model][ability]) === null || _q === void 0 ? void 0 : _q.operation;
|
|
590
|
-
if (!operation)
|
|
591
|
-
continue;
|
|
592
|
-
// biome-ignore lint/style/noNonNullAssertion: TODO fix this
|
|
593
|
-
abilities[model][ability] = __assign(__assign({}, customAbilities[model][ability]), { operation: operation, model: model, slug: ability });
|
|
375
|
+
}); }, {
|
|
376
|
+
maxWait: txMaxWait,
|
|
377
|
+
timeout: txTimeout,
|
|
378
|
+
})
|
|
379
|
+
.then(function (results) {
|
|
380
|
+
results.forEach(function (result, index) {
|
|
381
|
+
batch.requests[index].resolve(result);
|
|
382
|
+
});
|
|
383
|
+
})
|
|
384
|
+
.catch(function (e) {
|
|
385
|
+
var e_4, _a;
|
|
386
|
+
try {
|
|
387
|
+
for (var _b = (e_4 = void 0, __values(batch.requests)), _c = _b.next(); !_c.done; _c = _b.next()) {
|
|
388
|
+
var request = _c.value;
|
|
389
|
+
request.reject(e);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
catch (e_4_1) { e_4 = { error: e_4_1 }; }
|
|
393
|
+
finally {
|
|
394
|
+
try {
|
|
395
|
+
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
|
|
594
396
|
}
|
|
397
|
+
finally { if (e_4) throw e_4.error; }
|
|
595
398
|
}
|
|
399
|
+
delete batches[key];
|
|
400
|
+
});
|
|
401
|
+
};
|
|
402
|
+
try {
|
|
403
|
+
for (var _b = __values(Object.entries(batches)), _c = _b.next(); !_c.done; _c = _b.next()) {
|
|
404
|
+
var _d = __read(_c.value, 2), key = _d[0], batch = _d[1];
|
|
405
|
+
_loop_1(key, batch);
|
|
596
406
|
}
|
|
597
407
|
}
|
|
598
|
-
catch (
|
|
408
|
+
catch (e_2_1) { e_2 = { error: e_2_1 }; }
|
|
599
409
|
finally {
|
|
600
410
|
try {
|
|
601
|
-
if (
|
|
411
|
+
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
|
|
602
412
|
}
|
|
603
|
-
finally { if (
|
|
413
|
+
finally { if (e_2) throw e_2.error; }
|
|
604
414
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
415
|
+
};
|
|
416
|
+
var createRoleName = _this.createRoleName.bind(_this);
|
|
417
|
+
var client = prisma.$extends({
|
|
418
|
+
name: "Yates client",
|
|
419
|
+
query: {
|
|
420
|
+
$allModels: {
|
|
421
|
+
$allOperations: function (params) {
|
|
422
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
423
|
+
var model, args, query, operation, ctx, role, context, pgRole, _a, _b, k, _c, _d, v, txId, hash, __internalParams;
|
|
424
|
+
var e_5, _e, e_6, _f;
|
|
425
|
+
return __generator(this, function (_g) {
|
|
426
|
+
model = params.model, args = params.args, query = params.query, operation = params.operation;
|
|
427
|
+
if (!model) {
|
|
428
|
+
// If the model is not defined, we can't apply RLS
|
|
429
|
+
// This can occur when you are making a call with Prisma's $queryRaw method
|
|
430
|
+
return [2 /*return*/, query(args)];
|
|
431
|
+
}
|
|
432
|
+
ctx = getContext();
|
|
433
|
+
// If ctx is null, the middleware is explicitly skipped
|
|
434
|
+
if (ctx === null) {
|
|
435
|
+
return [2 /*return*/, query(args)];
|
|
436
|
+
}
|
|
437
|
+
role = ctx.role, context = ctx.context;
|
|
438
|
+
pgRole = createRoleName(role);
|
|
439
|
+
if (context) {
|
|
440
|
+
try {
|
|
441
|
+
for (_a = __values(Object.keys(context)), _b = _a.next(); !_b.done; _b = _a.next()) {
|
|
442
|
+
k = _b.value;
|
|
443
|
+
if (!k.match(/^[a-z_\.]+$/)) {
|
|
444
|
+
throw new Error("Context variable \"".concat(k, "\" contains invalid characters. Context variables must only contain lowercase letters, numbers, periods and underscores."));
|
|
445
|
+
}
|
|
446
|
+
if (typeof context[k] !== "number" &&
|
|
447
|
+
typeof context[k] !== "string" &&
|
|
448
|
+
!Array.isArray(context[k])) {
|
|
449
|
+
throw new Error("Context variable \"".concat(k, "\" must be a string, number or array. Got ").concat(typeof context[k]));
|
|
450
|
+
}
|
|
451
|
+
if (Array.isArray(context[k])) {
|
|
452
|
+
try {
|
|
453
|
+
for (_c = (e_6 = void 0, __values(context[k])), _d = _c.next(); !_d.done; _d = _c.next()) {
|
|
454
|
+
v = _d.value;
|
|
455
|
+
if (typeof v !== "string") {
|
|
456
|
+
throw new Error("Context variable \"".concat(k, "\" must be an array of strings. Got ").concat(typeof v));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
catch (e_6_1) { e_6 = { error: e_6_1 }; }
|
|
461
|
+
finally {
|
|
462
|
+
try {
|
|
463
|
+
if (_d && !_d.done && (_f = _c.return)) _f.call(_c);
|
|
464
|
+
}
|
|
465
|
+
finally { if (e_6) throw e_6.error; }
|
|
466
|
+
}
|
|
467
|
+
// Cast to a JSON string so that it can be used in RLS expressions
|
|
468
|
+
context[k] = JSON.stringify(context[k]);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
catch (e_5_1) { e_5 = { error: e_5_1 }; }
|
|
473
|
+
finally {
|
|
474
|
+
try {
|
|
475
|
+
if (_b && !_b.done && (_e = _a.return)) _e.call(_a);
|
|
476
|
+
}
|
|
477
|
+
finally { if (e_5) throw e_5.error; }
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
txId = hashWithPrefix("yates_tx_", JSON.stringify(ctx));
|
|
481
|
+
hash = txId;
|
|
482
|
+
if (!batches[hash]) {
|
|
483
|
+
// Create a new batch for this role + context combination
|
|
484
|
+
batches[hash] = {
|
|
485
|
+
pgRole: pgRole,
|
|
486
|
+
context: context,
|
|
487
|
+
requests: [],
|
|
488
|
+
};
|
|
489
|
+
// make sure, that we only tick once at a time
|
|
490
|
+
if (!tickActive) {
|
|
491
|
+
tickActive = true;
|
|
492
|
+
process.nextTick(function () {
|
|
493
|
+
dispatchBatches();
|
|
494
|
+
tickActive = false;
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
__internalParams = params.__internalParams;
|
|
499
|
+
// Add the request to the batch, and return a promise that will be resolved or rejected in dispatchBatches
|
|
500
|
+
return [2 /*return*/, new Promise(function (resolve, reject) {
|
|
501
|
+
batches[hash].requests.push({
|
|
502
|
+
params: __internalParams,
|
|
503
|
+
query: query,
|
|
504
|
+
args: args,
|
|
505
|
+
resolve: resolve,
|
|
506
|
+
reject: reject,
|
|
507
|
+
});
|
|
508
|
+
}).catch(function (e) {
|
|
509
|
+
var _a;
|
|
510
|
+
// Normalize RLS errors to make them a bit more readable.
|
|
511
|
+
if ((_a = e.message) === null || _a === void 0 ? void 0 : _a.includes("new row violates row-level security policy for table")) {
|
|
512
|
+
throw new Error("You do not have permission to perform this action: ".concat(model, ".").concat(operation, "(...)"));
|
|
513
|
+
}
|
|
514
|
+
throw e;
|
|
515
|
+
})];
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
return client;
|
|
523
|
+
};
|
|
524
|
+
this.setRLS = function (prisma, table, roleName, slug, ability) { return __awaiter(_this, void 0, void 0, function () {
|
|
525
|
+
var operation, rawExpression, description;
|
|
526
|
+
var _this = this;
|
|
527
|
+
return __generator(this, function (_a) {
|
|
528
|
+
switch (_a.label) {
|
|
529
|
+
case 0:
|
|
530
|
+
operation = ability.operation, rawExpression = ability.expression, description = ability.description;
|
|
531
|
+
if (!rawExpression) {
|
|
532
|
+
throw new Error("Expression must be defined for RLS abilities");
|
|
689
533
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
534
|
+
// Take a lock and run the RLS setup in a transaction to prevent conflicts
|
|
535
|
+
// in a multi-server environment
|
|
536
|
+
return [4 /*yield*/, prisma.$transaction(function (tx) { return __awaiter(_this, void 0, void 0, function () {
|
|
537
|
+
var policyName, existingAbilities, existingAbility, shouldUpdateAbilityTable, expression, expression;
|
|
538
|
+
return __generator(this, function (_a) {
|
|
539
|
+
switch (_a.label) {
|
|
540
|
+
case 0: return [4 /*yield*/, takeLock(tx)];
|
|
541
|
+
case 1:
|
|
542
|
+
_a.sent();
|
|
543
|
+
policyName = roleName;
|
|
544
|
+
return [4 /*yield*/, tx.$queryRaw(templateObject_3 || (templateObject_3 = __makeTemplateObject(["\n\t\t\t\tselect * from _yates._yates_abilities where ability_model = ", " and ability_policy_name = ", "\n\t\t\t"], ["\n\t\t\t\tselect * from _yates._yates_abilities where ability_model = ", " and ability_policy_name = ", "\n\t\t\t"])), table, policyName)];
|
|
545
|
+
case 2:
|
|
546
|
+
existingAbilities = _a.sent();
|
|
547
|
+
existingAbility = existingAbilities[0];
|
|
548
|
+
shouldUpdateAbilityTable = false;
|
|
549
|
+
if (!!existingAbility) return [3 /*break*/, 8];
|
|
550
|
+
debug("Creating RLS policy for", roleName, "on", table, "for", operation);
|
|
551
|
+
return [4 /*yield*/, (0, expressions_1.expressionToSQL)(rawExpression, table)];
|
|
552
|
+
case 3:
|
|
553
|
+
expression = _a.sent();
|
|
554
|
+
if (!(operation === "INSERT")) return [3 /*break*/, 5];
|
|
555
|
+
return [4 /*yield*/, tx.$queryRawUnsafe("\n\t\t\t\t\tCREATE POLICY ".concat(policyName, " ON \"public\".\"").concat(table, "\" FOR ").concat(operation, " TO ").concat(roleName, " WITH CHECK (").concat(expression, ");\n\t\t\t\t"))];
|
|
556
|
+
case 4:
|
|
557
|
+
_a.sent();
|
|
558
|
+
return [3 /*break*/, 7];
|
|
559
|
+
case 5: return [4 /*yield*/, tx.$queryRawUnsafe("\n\t\t\t\t\tCREATE POLICY ".concat(policyName, " ON \"public\".\"").concat(table, "\" FOR ").concat(operation, " TO ").concat(roleName, " USING (").concat(expression, ");\n\t\t\t\t"))];
|
|
560
|
+
case 6:
|
|
561
|
+
_a.sent();
|
|
562
|
+
_a.label = 7;
|
|
563
|
+
case 7:
|
|
564
|
+
shouldUpdateAbilityTable = true;
|
|
565
|
+
return [3 /*break*/, 14];
|
|
566
|
+
case 8:
|
|
567
|
+
if (!(existingAbility.ability_expression !== rawExpression.toString())) return [3 /*break*/, 14];
|
|
568
|
+
debug("Updating RLS policy for", roleName, "on", table, "for", operation);
|
|
569
|
+
return [4 /*yield*/, (0, expressions_1.expressionToSQL)(rawExpression, table)];
|
|
570
|
+
case 9:
|
|
571
|
+
expression = _a.sent();
|
|
572
|
+
if (!(operation === "INSERT")) return [3 /*break*/, 11];
|
|
573
|
+
return [4 /*yield*/, tx.$queryRawUnsafe("\n\t\t\t\t\tALTER POLICY ".concat(policyName, " ON \"public\".\"").concat(table, "\" TO ").concat(roleName, " WITH CHECK (").concat(expression, ");\n\t\t\t\t"))];
|
|
574
|
+
case 10:
|
|
575
|
+
_a.sent();
|
|
576
|
+
return [3 /*break*/, 13];
|
|
577
|
+
case 11: return [4 /*yield*/, tx.$queryRawUnsafe("\n\t\t\t\t\tALTER POLICY ".concat(policyName, " ON \"public\".\"").concat(table, "\" TO ").concat(roleName, " USING (").concat(expression, ");\n\t\t\t\t"))];
|
|
578
|
+
case 12:
|
|
579
|
+
_a.sent();
|
|
580
|
+
_a.label = 13;
|
|
581
|
+
case 13:
|
|
582
|
+
shouldUpdateAbilityTable = true;
|
|
583
|
+
_a.label = 14;
|
|
584
|
+
case 14:
|
|
585
|
+
if (!shouldUpdateAbilityTable) return [3 /*break*/, 16];
|
|
586
|
+
return [4 /*yield*/, upsertAbility(tx, {
|
|
587
|
+
ability_model: table,
|
|
588
|
+
ability_name: slug,
|
|
589
|
+
ability_policy_name: policyName,
|
|
590
|
+
ability_description: description !== null && description !== void 0 ? description : "",
|
|
591
|
+
ability_operation: operation,
|
|
592
|
+
// We store the string representation of the expression so that
|
|
593
|
+
// we can compare it later without having to recompute the SQL
|
|
594
|
+
ability_expression: rawExpression.toString(),
|
|
595
|
+
})];
|
|
596
|
+
case 15:
|
|
597
|
+
_a.sent();
|
|
598
|
+
_a.label = 16;
|
|
599
|
+
case 16: return [2 /*return*/];
|
|
600
|
+
}
|
|
727
601
|
});
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
602
|
+
}); })];
|
|
603
|
+
case 1:
|
|
604
|
+
// Take a lock and run the RLS setup in a transaction to prevent conflicts
|
|
605
|
+
// in a multi-server environment
|
|
606
|
+
_a.sent();
|
|
607
|
+
return [2 /*return*/];
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
}); };
|
|
611
|
+
this.createRoles = function (_a) { return __awaiter(_this, [_a], void 0, function (_b) {
|
|
612
|
+
var runtimeDataModel, models, diff, defaultAbilities, abilities, models_2, models_2_1, model, ability, operation, roles, pgRoles, existingAbilities, pgPolicies, migratedAbilities, _c, _d, _e, _i, model, table, _loop_2, this_1, _f, _g, _h, _j, slug, _loop_3, this_2, _k, _l, _m, _o, key;
|
|
613
|
+
var e_7, _p;
|
|
614
|
+
var _this = this;
|
|
615
|
+
var _q;
|
|
616
|
+
var customAbilities = _b.customAbilities, getRoles = _b.getRoles;
|
|
617
|
+
return __generator(this, function (_r) {
|
|
618
|
+
switch (_r.label) {
|
|
619
|
+
case 0: return [4 /*yield*/, this.ensureDatabaseScope()];
|
|
620
|
+
case 1:
|
|
621
|
+
_r.sent();
|
|
622
|
+
runtimeDataModel = this.inspectRunTimeDataModel();
|
|
623
|
+
models = Object.keys(runtimeDataModel.models).map(function (m) { return runtimeDataModel.models[m].dbName || m; });
|
|
624
|
+
if (customAbilities) {
|
|
625
|
+
diff = (0, difference_1.default)(Object.keys(customAbilities), models);
|
|
626
|
+
if (diff.length) {
|
|
627
|
+
throw new Error("Invalid models in custom abilities: ".concat(diff.join(", ")));
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
defaultAbilities = this.getDefaultAbilities(models);
|
|
631
|
+
abilities = (0, cloneDeep_1.default)(defaultAbilities);
|
|
632
|
+
try {
|
|
633
|
+
for (models_2 = __values(models), models_2_1 = models_2.next(); !models_2_1.done; models_2_1 = models_2.next()) {
|
|
634
|
+
model = models_2_1.value;
|
|
635
|
+
if (customAbilities === null || customAbilities === void 0 ? void 0 : customAbilities[model]) {
|
|
636
|
+
for (ability in customAbilities[model]) {
|
|
637
|
+
operation =
|
|
732
638
|
// biome-ignore lint/style/noNonNullAssertion: TODO fix this
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
prisma.$executeRawUnsafe("\n\t\t\t\tGRANT ALL ON SCHEMA public TO ".concat(role, ";\n\t\t\t")),
|
|
742
|
-
prisma.$queryRawUnsafe("GRANT ".concat(rlsRoles.join(", "), " TO ").concat(role)),
|
|
743
|
-
])];
|
|
744
|
-
case 2:
|
|
745
|
-
// Note: We need to GRANT all on schema public so that we can resolve relation queries with prisma, as they will sometimes use a join table.
|
|
746
|
-
// This is not ideal, but because we are using RLS, it's not a security risk. Any table with RLS also needs a corresponding policy for the role to have access.
|
|
747
|
-
_t.sent();
|
|
748
|
-
return [4 /*yield*/, prisma.$queryRawUnsafe("\n\t\t\tWITH RECURSIVE cte AS (\n\t\t\t\tSELECT oid FROM pg_roles where rolname = '".concat(role, "'\n\t\t\t\tUNION ALL\n\t\t\t\tSELECT m.roleid\n\t\t\t\tFROM cte\n\t\t\t\tJOIN pg_auth_members m ON m.member = cte.oid\n\t\t\t\t)\n\t\t\tSELECT oid, oid::regrole::text AS rolename FROM cte where oid::regrole::text != '").concat(role, "'; \n\t "))];
|
|
749
|
-
case 3:
|
|
750
|
-
userRoles = _t.sent();
|
|
751
|
-
oldRoles = userRoles
|
|
752
|
-
.filter(function (_a) {
|
|
753
|
-
var rolename = _a.rolename;
|
|
754
|
-
return !rlsRoles.includes(rolename);
|
|
755
|
-
})
|
|
756
|
-
.map(function (_a) {
|
|
757
|
-
var rolename = _a.rolename;
|
|
758
|
-
return rolename;
|
|
759
|
-
});
|
|
760
|
-
if (!oldRoles.length) return [3 /*break*/, 8];
|
|
761
|
-
// Now revoke old roles from the user role
|
|
762
|
-
debug("Revoking old roles", oldRoles.join(", "));
|
|
763
|
-
return [4 /*yield*/, prisma.$executeRawUnsafe("REVOKE ".concat(oldRoles.join(", "), " FROM ").concat(role))];
|
|
764
|
-
case 4:
|
|
765
|
-
_t.sent();
|
|
766
|
-
return [4 /*yield*/, prisma.$queryRawUnsafe("SELECT * FROM pg_catalog.pg_policies WHERE policyname IN (".concat(oldRoles
|
|
767
|
-
.map(function (or) { return "'".concat(or, "'"); })
|
|
768
|
-
.join(", "), ")"))];
|
|
769
|
-
case 5:
|
|
770
|
-
policies = _t.sent();
|
|
771
|
-
return [4 /*yield*/, prisma.$transaction(__spreadArray([
|
|
772
|
-
takeLock(prisma)
|
|
773
|
-
], __read(policies.map(function (oldPolicy) {
|
|
774
|
-
return prisma.$executeRawUnsafe("DROP POLICY ".concat(oldPolicy.policyname, " ON \"").concat(oldPolicy.tablename, "\""));
|
|
775
|
-
})), false))];
|
|
776
|
-
case 6:
|
|
777
|
-
_t.sent();
|
|
778
|
-
debug("Revoked old rows from ability table", oldRoles.join(", "));
|
|
779
|
-
return [4 /*yield*/, prisma.$executeRawUnsafe("DELETE FROM _yates._yates_abilities WHERE ability_policy_name IN (".concat(oldRoles
|
|
780
|
-
.map(function (or) { return "'".concat(or, "'"); })
|
|
781
|
-
.join(", "), ")"))];
|
|
782
|
-
case 7:
|
|
783
|
-
_t.sent();
|
|
784
|
-
_t.label = 8;
|
|
785
|
-
case 8: return [2 /*return*/];
|
|
639
|
+
(_q = customAbilities[model][ability]) === null || _q === void 0 ? void 0 : _q.operation;
|
|
640
|
+
if (!operation)
|
|
641
|
+
continue;
|
|
642
|
+
// biome-ignore lint/style/noNonNullAssertion: TODO fix this
|
|
643
|
+
abilities[model][ability] = __assign(__assign({}, customAbilities[model][ability]), { operation: operation, model: model, slug: ability });
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
786
647
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
648
|
+
catch (e_7_1) { e_7 = { error: e_7_1 }; }
|
|
649
|
+
finally {
|
|
650
|
+
try {
|
|
651
|
+
if (models_2_1 && !models_2_1.done && (_p = models_2.return)) _p.call(models_2);
|
|
652
|
+
}
|
|
653
|
+
finally { if (e_7) throw e_7.error; }
|
|
654
|
+
}
|
|
655
|
+
roles = getRoles(abilities);
|
|
656
|
+
return [4 /*yield*/, this.prisma.$queryRawUnsafe("\n\t\t\tselect * from pg_catalog.pg_roles where rolname like 'yates%'\n\t\t")];
|
|
657
|
+
case 2:
|
|
658
|
+
pgRoles = _r.sent();
|
|
659
|
+
return [4 /*yield*/, this.prisma.$queryRawUnsafe("\n\t\t\tselect * from _yates._yates_abilities;\n\t\t")];
|
|
660
|
+
case 3:
|
|
661
|
+
existingAbilities = _r.sent();
|
|
662
|
+
if (!(existingAbilities.length === 0)) return [3 /*break*/, 6];
|
|
663
|
+
debug('No existing abilities found, importing from "pg_policies" table');
|
|
664
|
+
return [4 /*yield*/, this.prisma.$queryRawUnsafe("\n\t\t\t\tselect * from pg_catalog.pg_policies where policyname like 'yates%'\n\t\t\t")];
|
|
665
|
+
case 4:
|
|
666
|
+
pgPolicies = _r.sent();
|
|
667
|
+
if (!pgPolicies.length) return [3 /*break*/, 6];
|
|
668
|
+
migratedAbilities = pgPolicies.map(function (policy) {
|
|
669
|
+
var _a, _b;
|
|
670
|
+
return ({
|
|
671
|
+
ability_model: policy.tablename,
|
|
672
|
+
ability_name: policy.policyname,
|
|
673
|
+
ability_policy_name: policy.policyname,
|
|
674
|
+
ability_description: "",
|
|
675
|
+
ability_operation: policy.cmd,
|
|
676
|
+
ability_expression: (_b = (_a = policy.qual) !== null && _a !== void 0 ? _a : policy.with_check) !== null && _b !== void 0 ? _b : "",
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
return [4 /*yield*/, this.prisma.$transaction(__spreadArray([
|
|
680
|
+
takeLock(this.prisma)
|
|
681
|
+
], __read(migratedAbilities.map(function (ma) { return upsertAbility(_this.prisma, ma); })), false))];
|
|
682
|
+
case 5:
|
|
683
|
+
_r.sent();
|
|
684
|
+
existingAbilities.push.apply(existingAbilities, __spreadArray([], __read(migratedAbilities), false));
|
|
685
|
+
_r.label = 6;
|
|
686
|
+
case 6:
|
|
687
|
+
_c = abilities;
|
|
688
|
+
_d = [];
|
|
689
|
+
for (_e in _c)
|
|
690
|
+
_d.push(_e);
|
|
691
|
+
_i = 0;
|
|
692
|
+
_r.label = 7;
|
|
693
|
+
case 7:
|
|
694
|
+
if (!(_i < _d.length)) return [3 /*break*/, 13];
|
|
695
|
+
_e = _d[_i];
|
|
696
|
+
if (!(_e in _c)) return [3 /*break*/, 12];
|
|
697
|
+
model = _e;
|
|
698
|
+
table = model;
|
|
699
|
+
return [4 /*yield*/, this.prisma.$transaction([
|
|
700
|
+
takeLock(this.prisma),
|
|
701
|
+
this.prisma.$queryRawUnsafe("ALTER table \"".concat(table, "\" enable row level security;")),
|
|
702
|
+
])];
|
|
703
|
+
case 8:
|
|
704
|
+
_r.sent();
|
|
705
|
+
_loop_2 = function (slug) {
|
|
706
|
+
var ability, roleName;
|
|
707
|
+
return __generator(this, function (_s) {
|
|
708
|
+
switch (_s.label) {
|
|
709
|
+
case 0:
|
|
710
|
+
ability =
|
|
711
|
+
// biome-ignore lint/style/noNonNullAssertion: TODO fix this
|
|
712
|
+
abilities[model][slug];
|
|
713
|
+
if (!VALID_OPERATIONS.includes(ability.operation)) {
|
|
714
|
+
throw new Error("Invalid operation: ".concat(ability.operation));
|
|
715
|
+
}
|
|
716
|
+
roleName = this_1.createAbilityName(model, slug);
|
|
717
|
+
if (!pgRoles.find(function (role) { return role.rolname === roleName; })) return [3 /*break*/, 1];
|
|
718
|
+
debug("Role already exists", roleName, model, slug);
|
|
719
|
+
return [3 /*break*/, 3];
|
|
720
|
+
case 1: return [4 /*yield*/, this_1.prisma.$transaction([
|
|
721
|
+
takeLock(this_1.prisma),
|
|
722
|
+
this_1.prisma.$queryRawUnsafe("\n\t\t\t\t\t\tdo\n\t\t\t\t\t\t$$\n\t\t\t\t\t\tbegin\n\t\t\t\t\t\tif not exists (select * from pg_catalog.pg_roles where rolname = '".concat(roleName, "') then \n\t\t\t\t\t\t\tcreate role ").concat(roleName, ";\n\t\t\t\t\t\tend if;\n\t\t\t\t\t\tend\n\t\t\t\t\t\t$$\n\t\t\t\t\t\t;\n\t\t\t\t\t")),
|
|
723
|
+
this_1.prisma.$queryRawUnsafe("\n\t\t\t\t\t\tGRANT ".concat(ability.operation, " ON \"").concat(table, "\" TO ").concat(roleName, ";\n\t\t\t\t\t")),
|
|
724
|
+
])];
|
|
725
|
+
case 2:
|
|
726
|
+
_s.sent();
|
|
727
|
+
_s.label = 3;
|
|
728
|
+
case 3:
|
|
729
|
+
if (!ability.expression) return [3 /*break*/, 5];
|
|
730
|
+
return [4 /*yield*/, this_1.setRLS(this_1.prisma, table, roleName, slug, ability)];
|
|
731
|
+
case 4:
|
|
732
|
+
_s.sent();
|
|
733
|
+
_s.label = 5;
|
|
734
|
+
case 5: return [2 /*return*/];
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
};
|
|
738
|
+
this_1 = this;
|
|
739
|
+
_f = abilities[model];
|
|
740
|
+
_g = [];
|
|
741
|
+
for (_h in _f)
|
|
742
|
+
_g.push(_h);
|
|
743
|
+
_j = 0;
|
|
744
|
+
_r.label = 9;
|
|
745
|
+
case 9:
|
|
746
|
+
if (!(_j < _g.length)) return [3 /*break*/, 12];
|
|
747
|
+
_h = _g[_j];
|
|
748
|
+
if (!(_h in _f)) return [3 /*break*/, 11];
|
|
749
|
+
slug = _h;
|
|
750
|
+
return [5 /*yield**/, _loop_2(slug)];
|
|
751
|
+
case 10:
|
|
752
|
+
_r.sent();
|
|
753
|
+
_r.label = 11;
|
|
754
|
+
case 11:
|
|
755
|
+
_j++;
|
|
756
|
+
return [3 /*break*/, 9];
|
|
757
|
+
case 12:
|
|
758
|
+
_i++;
|
|
759
|
+
return [3 /*break*/, 7];
|
|
760
|
+
case 13:
|
|
761
|
+
_loop_3 = function (key) {
|
|
762
|
+
var role, wildCardAbilities, roleAbilities, rlsRoles, userRoles, oldRoles, policies;
|
|
763
|
+
return __generator(this, function (_t) {
|
|
764
|
+
switch (_t.label) {
|
|
765
|
+
case 0:
|
|
766
|
+
role = this_2.createRoleName(key);
|
|
767
|
+
return [4 /*yield*/, this_2.prisma.$executeRawUnsafe("\n\t\t\t\tdo\n\t\t\t\t$$\n\t\t\t\tbegin\n\t\t\t\tif not exists (select * from pg_catalog.pg_roles where rolname = '".concat(role, "') then \n\t\t\t\t\tcreate role ").concat(role, ";\n\t\t\t\tend if;\n\t\t\t\tend\n\t\t\t\t$$\n\t\t\t\t;\n\t\t\t"))];
|
|
768
|
+
case 1:
|
|
769
|
+
_t.sent();
|
|
770
|
+
wildCardAbilities = (0, flatMap_1.default)(defaultAbilities, function (model, modelName) {
|
|
771
|
+
return (0, map_1.default)(model, function (_params, slug) {
|
|
772
|
+
return _this.createAbilityName(modelName, slug);
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
roleAbilities = roles[key];
|
|
776
|
+
rlsRoles = roleAbilities === "*"
|
|
777
|
+
? wildCardAbilities
|
|
778
|
+
: roleAbilities.map(function (ability) {
|
|
779
|
+
// biome-ignore lint/style/noNonNullAssertion: TODO fix this
|
|
780
|
+
return _this.createAbilityName(ability.model, ability.slug);
|
|
781
|
+
});
|
|
782
|
+
debug("Setting up role", key, role, "with abilities", rlsRoles.join(", "));
|
|
783
|
+
// Note: We need to GRANT all on schema public so that we can resolve relation queries with prisma, as they will sometimes use a join table.
|
|
784
|
+
// This is not ideal, but because we are using RLS, it's not a security risk. Any table with RLS also needs a corresponding policy for the role to have access.
|
|
785
|
+
return [4 /*yield*/, this_2.prisma.$transaction([
|
|
786
|
+
takeLock(this_2.prisma),
|
|
787
|
+
this_2.prisma.$executeRawUnsafe("GRANT ALL ON ALL TABLES IN SCHEMA public TO ".concat(role, ";")),
|
|
788
|
+
this_2.prisma.$executeRawUnsafe("\n\t\t\t\t\tGRANT ALL ON ALL SEQUENCES IN SCHEMA public TO ".concat(role, ";\n\t\t\t\t")),
|
|
789
|
+
this_2.prisma.$executeRawUnsafe("\n\t\t\t\t\tGRANT ALL ON SCHEMA public TO ".concat(role, ";\n\t\t\t\t")),
|
|
790
|
+
this_2.prisma.$queryRawUnsafe("GRANT ".concat(rlsRoles.join(", "), " TO ").concat(role)),
|
|
791
|
+
])];
|
|
792
|
+
case 2:
|
|
793
|
+
// Note: We need to GRANT all on schema public so that we can resolve relation queries with prisma, as they will sometimes use a join table.
|
|
794
|
+
// This is not ideal, but because we are using RLS, it's not a security risk. Any table with RLS also needs a corresponding policy for the role to have access.
|
|
795
|
+
_t.sent();
|
|
796
|
+
return [4 /*yield*/, this_2.prisma.$queryRawUnsafe("\n\t\t\t\tWITH RECURSIVE cte AS (\n\t\t\t\t\tSELECT oid FROM pg_roles where rolname = '".concat(role, "'\n\t\t\t\t\tUNION ALL\n\t\t\t\t\tSELECT m.roleid\n\t\t\t\t\tFROM cte\n\t\t\t\t\tJOIN pg_auth_members m ON m.member = cte.oid\n\t\t\t\t\t)\n\t\t\t\tSELECT oid, oid::regrole::text AS rolename FROM cte where oid::regrole::text != '").concat(role, "'; \n\t\t"))];
|
|
797
|
+
case 3:
|
|
798
|
+
userRoles = _t.sent();
|
|
799
|
+
oldRoles = userRoles
|
|
800
|
+
.filter(function (_a) {
|
|
801
|
+
var rolename = _a.rolename;
|
|
802
|
+
return !rlsRoles.includes(rolename);
|
|
803
|
+
})
|
|
804
|
+
.map(function (_a) {
|
|
805
|
+
var rolename = _a.rolename;
|
|
806
|
+
return rolename;
|
|
807
|
+
});
|
|
808
|
+
if (!oldRoles.length) return [3 /*break*/, 8];
|
|
809
|
+
// Now revoke old roles from the user role
|
|
810
|
+
debug("Revoking old roles", key, role, oldRoles.join(", "));
|
|
811
|
+
return [4 /*yield*/, this_2.prisma.$executeRawUnsafe("REVOKE ".concat(oldRoles.join(", "), " FROM ").concat(role))];
|
|
812
|
+
case 4:
|
|
813
|
+
_t.sent();
|
|
814
|
+
return [4 /*yield*/, this_2.prisma.$queryRawUnsafe("SELECT * FROM pg_catalog.pg_policies WHERE policyname IN (".concat(oldRoles
|
|
815
|
+
.map(function (or) { return "'".concat(or, "'"); })
|
|
816
|
+
.join(", "), ")"))];
|
|
817
|
+
case 5:
|
|
818
|
+
policies = _t.sent();
|
|
819
|
+
return [4 /*yield*/, this_2.prisma.$transaction(__spreadArray([
|
|
820
|
+
takeLock(this_2.prisma)
|
|
821
|
+
], __read(policies.map(function (oldPolicy) {
|
|
822
|
+
return _this.prisma.$executeRawUnsafe("DROP POLICY ".concat(oldPolicy.policyname, " ON \"").concat(oldPolicy.tablename, "\""));
|
|
823
|
+
})), false))];
|
|
824
|
+
case 6:
|
|
825
|
+
_t.sent();
|
|
826
|
+
debug("Revoked old rows from ability table", oldRoles.join(", "));
|
|
827
|
+
return [4 /*yield*/, this_2.prisma.$executeRawUnsafe("DELETE FROM _yates._yates_abilities WHERE ability_policy_name IN (".concat(oldRoles
|
|
828
|
+
.map(function (or) { return "'".concat(or, "'"); })
|
|
829
|
+
.join(", "), ")"))];
|
|
830
|
+
case 7:
|
|
831
|
+
_t.sent();
|
|
832
|
+
_t.label = 8;
|
|
833
|
+
case 8: return [2 /*return*/];
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
};
|
|
837
|
+
this_2 = this;
|
|
838
|
+
_k = roles;
|
|
839
|
+
_l = [];
|
|
840
|
+
for (_m in _k)
|
|
841
|
+
_l.push(_m);
|
|
842
|
+
_o = 0;
|
|
843
|
+
_r.label = 14;
|
|
844
|
+
case 14:
|
|
845
|
+
if (!(_o < _l.length)) return [3 /*break*/, 17];
|
|
846
|
+
_m = _l[_o];
|
|
847
|
+
if (!(_m in _k)) return [3 /*break*/, 16];
|
|
848
|
+
key = _m;
|
|
849
|
+
return [5 /*yield**/, _loop_3(key)];
|
|
850
|
+
case 15:
|
|
851
|
+
_r.sent();
|
|
852
|
+
_r.label = 16;
|
|
853
|
+
case 16:
|
|
854
|
+
_o++;
|
|
855
|
+
return [3 /*break*/, 14];
|
|
856
|
+
case 17: return [2 /*return*/];
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
}); };
|
|
860
|
+
this.inspectDBRoles = function (role) { return __awaiter(_this, void 0, void 0, function () {
|
|
861
|
+
var hashedRoleName, roles;
|
|
862
|
+
return __generator(this, function (_a) {
|
|
863
|
+
switch (_a.label) {
|
|
864
|
+
case 0: return [4 /*yield*/, this.ensureDatabaseScope()];
|
|
865
|
+
case 1:
|
|
866
|
+
_a.sent();
|
|
867
|
+
hashedRoleName = this.createRoleName(role);
|
|
868
|
+
return [4 /*yield*/, this.prisma.$queryRawUnsafe("\n WITH RECURSIVE role_tree AS(\n --Start from your role\n SELECT \n r.oid,\n r.rolname\n FROM pg_roles r\n WHERE r.rolname = '".concat(hashedRoleName, "'\n\n UNION\n\n --Walk \"upwards\": all parent roles granted to it\n SELECT \n parent.oid,\n parent.rolname\n FROM pg_auth_members m\n JOIN role_tree rt\n ON m.member = rt.oid\n JOIN pg_roles parent\n ON parent.oid = m.roleid\n )\n SELECT\n p.tablename,\n p.policyname,\n p.cmd,\n p.roles:: text[] AS policy_roles,\n rt.rolname AS matched_role\n FROM pg_policies p\n JOIN role_tree rt\n ON(\n p.roles IS NULL\n OR array_length(p.roles, 1) = 0\n OR rt.rolname = ANY(p.roles:: text[])\n )\n WHERE p.schemaname = 'public'\n ORDER BY p.policyname, matched_role;\n "))];
|
|
869
|
+
case 2:
|
|
870
|
+
roles = _a.sent();
|
|
871
|
+
return [2 /*return*/, roles];
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
}); };
|
|
875
|
+
this.inspectRunTimeDataModel = function () {
|
|
876
|
+
// See https://github.com/prisma/prisma/discussions/14777
|
|
877
|
+
// We are reaching into the prisma internals to get the data model.
|
|
878
|
+
// This is a bit sketchy, but we can get the internal type definition from the runtime library
|
|
879
|
+
// and there is even a test case in prisma that checks that this value is exported
|
|
880
|
+
// See https://github.com/prisma/prisma/blob/5.1.0/packages/client/tests/functional/extensions/pdp.ts#L51
|
|
881
|
+
// This is a private API, so not much we can do about the cast
|
|
882
|
+
var runtimeDataModel = _this.prisma
|
|
883
|
+
._runtimeDataModel;
|
|
884
|
+
return runtimeDataModel;
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
// @ts-ignore
|
|
888
|
+
Yates.prototype.getBatchId = function (query) {
|
|
889
|
+
if (query.action !== "findUnique" && query.action !== "findUniqueOrThrow") {
|
|
890
|
+
return undefined;
|
|
808
891
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
892
|
+
var parts = [];
|
|
893
|
+
if (query.modelName) {
|
|
894
|
+
parts.push(query.modelName);
|
|
895
|
+
}
|
|
896
|
+
if (query.query.arguments) {
|
|
897
|
+
parts.push(this.buildKeysString(query.query.arguments));
|
|
898
|
+
}
|
|
899
|
+
parts.push(this.buildKeysString(query.query.selection));
|
|
900
|
+
return parts.join("");
|
|
901
|
+
};
|
|
902
|
+
Yates.prototype.buildKeysString = function (obj) {
|
|
903
|
+
var _this = this;
|
|
904
|
+
var keysArray = Object.keys(obj)
|
|
905
|
+
.sort()
|
|
906
|
+
.map(function (key) {
|
|
907
|
+
// @ts-ignore
|
|
908
|
+
var value = obj[key];
|
|
909
|
+
if (typeof value === "object" && value !== null) {
|
|
910
|
+
return "(".concat(key, " ").concat(_this.buildKeysString(value), ")");
|
|
911
|
+
}
|
|
912
|
+
return key;
|
|
913
|
+
});
|
|
914
|
+
return "(".concat(keysArray.join(" "), ")");
|
|
915
|
+
};
|
|
916
|
+
return Yates;
|
|
917
|
+
}());
|
|
918
|
+
exports.Yates = Yates;
|
|
812
919
|
/**
|
|
813
920
|
* Creates an extended client that sets contextual parameters and user role on every query
|
|
814
921
|
**/
|
|
815
922
|
var setup = function (params) { return __awaiter(void 0, void 0, void 0, function () {
|
|
816
|
-
var start, prisma, customAbilities, getRoles, getContext, client;
|
|
923
|
+
var start, prisma, customAbilities, getRoles, getContext, yates, client;
|
|
817
924
|
return __generator(this, function (_a) {
|
|
818
925
|
switch (_a.label) {
|
|
819
926
|
case 0:
|
|
820
927
|
start = performance.now();
|
|
821
928
|
prisma = params.prisma, customAbilities = params.customAbilities, getRoles = params.getRoles, getContext = params.getContext;
|
|
822
|
-
|
|
823
|
-
|
|
929
|
+
yates = new Yates(prisma);
|
|
930
|
+
return [4 /*yield*/, yates.init()];
|
|
931
|
+
case 1:
|
|
932
|
+
_a.sent();
|
|
933
|
+
return [4 /*yield*/, yates.createRoles({
|
|
824
934
|
customAbilities: customAbilities,
|
|
825
935
|
getRoles: getRoles,
|
|
826
936
|
})];
|
|
827
|
-
case
|
|
937
|
+
case 2:
|
|
828
938
|
_a.sent();
|
|
829
|
-
client =
|
|
939
|
+
client = yates.createClient(getContext, params.options);
|
|
830
940
|
debug("Setup completed in", performance.now() - start, "ms");
|
|
831
941
|
return [2 /*return*/, client];
|
|
832
942
|
}
|