@zenstackhq/runtime 1.0.0-alpha.99 → 1.0.0-beta.10
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/browser/index.d.mts +13 -0
- package/browser/index.d.ts +13 -0
- package/browser/index.js +69 -0
- package/browser/index.js.map +1 -0
- package/browser/index.mjs +31 -0
- package/browser/index.mjs.map +1 -0
- package/constants.d.ts +31 -0
- package/constants.js +34 -1
- package/constants.js.map +1 -1
- package/enhancements/index.d.ts +5 -0
- package/enhancements/index.js +5 -0
- package/enhancements/index.js.map +1 -1
- package/enhancements/model-meta.d.ts +4 -0
- package/enhancements/model-meta.js +26 -5
- package/enhancements/model-meta.js.map +1 -1
- package/enhancements/nested-write-vistor.d.ts +17 -16
- package/enhancements/nested-write-vistor.js +86 -59
- package/enhancements/nested-write-vistor.js.map +1 -1
- package/enhancements/omit.d.ts +10 -1
- package/enhancements/omit.js +4 -3
- package/enhancements/omit.js.map +1 -1
- package/enhancements/password.d.ts +10 -1
- package/enhancements/password.js +3 -2
- package/enhancements/password.js.map +1 -1
- package/enhancements/policy/handler.d.ts +6 -3
- package/enhancements/policy/handler.js +99 -39
- package/enhancements/policy/handler.js.map +1 -1
- package/enhancements/policy/index.d.ts +23 -2
- package/enhancements/policy/index.js +39 -6
- package/enhancements/policy/index.js.map +1 -1
- package/enhancements/policy/logger.d.ts +9 -1
- package/enhancements/policy/logger.js +14 -3
- package/enhancements/policy/logger.js.map +1 -1
- package/enhancements/policy/policy-utils.d.ts +13 -9
- package/enhancements/policy/policy-utils.js +250 -138
- package/enhancements/policy/policy-utils.js.map +1 -1
- package/enhancements/preset.d.ts +9 -7
- package/enhancements/preset.js +3 -6
- package/enhancements/preset.js.map +1 -1
- package/enhancements/proxy.js +62 -1
- package/enhancements/proxy.js.map +1 -1
- package/enhancements/types.d.ts +10 -1
- package/enhancements/utils.d.ts +12 -4
- package/enhancements/utils.js +97 -11
- package/enhancements/utils.js.map +1 -1
- package/enhancements/where-visitor.d.ts +33 -0
- package/enhancements/where-visitor.js +87 -0
- package/enhancements/where-visitor.js.map +1 -0
- package/index.d.ts +2 -2
- package/index.js +2 -2
- package/index.js.map +1 -1
- package/package.json +33 -12
- package/version.js +1 -0
- package/version.js.map +1 -1
- package/zod/index.d.ts +2 -0
- package/zod/index.js +4 -0
- package/zod/input.d.ts +1 -0
- package/zod/input.js +8 -0
- package/zod/models.d.ts +1 -0
- package/zod/models.js +8 -0
- package/serialization-utils.d.ts +0 -1
- package/serialization-utils.js +0 -22
- package/serialization-utils.js.map +0 -1
- package/zod.d.ts +0 -10
- package/zod.js +0 -17
- package/zod.js.map +0 -1
|
@@ -14,27 +14,30 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
14
14
|
};
|
|
15
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
16
|
exports.PolicyUtil = void 0;
|
|
17
|
-
const
|
|
18
|
-
const sdk_1 = require("@zenstackhq/sdk");
|
|
19
|
-
const change_case_1 = require("change-case");
|
|
20
|
-
const cuid_1 = __importDefault(require("cuid"));
|
|
17
|
+
const cuid2_1 = require("@paralleldrive/cuid2");
|
|
21
18
|
const deepcopy_1 = __importDefault(require("deepcopy"));
|
|
19
|
+
const lower_case_first_1 = require("lower-case-first");
|
|
20
|
+
const pluralize_1 = __importDefault(require("pluralize"));
|
|
21
|
+
const upper_case_first_1 = require("upper-case-first");
|
|
22
22
|
const zod_validation_error_1 = require("zod-validation-error");
|
|
23
|
+
const constants_1 = require("../../constants");
|
|
24
|
+
const error_1 = require("../../error");
|
|
23
25
|
const version_1 = require("../../version");
|
|
24
26
|
const model_meta_1 = require("../model-meta");
|
|
25
27
|
const nested_write_vistor_1 = require("../nested-write-vistor");
|
|
26
28
|
const utils_1 = require("../utils");
|
|
27
29
|
const logger_1 = require("./logger");
|
|
28
|
-
const pluralize_1 = __importDefault(require("pluralize"));
|
|
29
30
|
/**
|
|
30
31
|
* Access policy enforcement utilities
|
|
31
32
|
*/
|
|
32
33
|
class PolicyUtil {
|
|
33
|
-
constructor(db, modelMeta, policy, user) {
|
|
34
|
+
constructor(db, modelMeta, policy, zodSchemas, user, logPrismaQuery) {
|
|
34
35
|
this.db = db;
|
|
35
36
|
this.modelMeta = modelMeta;
|
|
36
37
|
this.policy = policy;
|
|
38
|
+
this.zodSchemas = zodSchemas;
|
|
37
39
|
this.user = user;
|
|
40
|
+
this.logPrismaQuery = logPrismaQuery;
|
|
38
41
|
this.logger = new logger_1.Logger(db);
|
|
39
42
|
}
|
|
40
43
|
/**
|
|
@@ -43,7 +46,7 @@ class PolicyUtil {
|
|
|
43
46
|
and(...conditions) {
|
|
44
47
|
if (conditions.includes(false)) {
|
|
45
48
|
// always false
|
|
46
|
-
return { [
|
|
49
|
+
return { [constants_1.GUARD_FIELD_NAME]: false };
|
|
47
50
|
}
|
|
48
51
|
const filtered = conditions.filter((c) => typeof c === 'object' && !!c && Object.keys(c).length > 0);
|
|
49
52
|
if (filtered.length === 0) {
|
|
@@ -62,7 +65,7 @@ class PolicyUtil {
|
|
|
62
65
|
or(...conditions) {
|
|
63
66
|
if (conditions.includes(true)) {
|
|
64
67
|
// always true
|
|
65
|
-
return { [
|
|
68
|
+
return { [constants_1.GUARD_FIELD_NAME]: true };
|
|
66
69
|
}
|
|
67
70
|
const filtered = conditions.filter((c) => typeof c === 'object' && !!c);
|
|
68
71
|
if (filtered.length === 0) {
|
|
@@ -93,24 +96,26 @@ class PolicyUtil {
|
|
|
93
96
|
* otherwise returns a guard object
|
|
94
97
|
*/
|
|
95
98
|
getAuthGuard(model, operation, preValue) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
99
|
+
const guard = this.policy.guard[(0, lower_case_first_1.lowerCaseFirst)(model)];
|
|
100
|
+
if (!guard) {
|
|
101
|
+
throw this.unknownError(`unable to load policy guard for ${model}`);
|
|
102
|
+
}
|
|
103
|
+
const provider = guard[operation];
|
|
104
|
+
if (typeof provider === 'boolean') {
|
|
105
|
+
return provider;
|
|
106
|
+
}
|
|
107
|
+
if (!provider) {
|
|
108
|
+
throw this.unknownError(`zenstack: unable to load authorization guard for ${model}`);
|
|
109
|
+
}
|
|
110
|
+
return provider({ user: this.user, preValue });
|
|
111
|
+
}
|
|
112
|
+
hasValidation(model) {
|
|
113
|
+
var _a, _b;
|
|
114
|
+
return ((_b = (_a = this.policy.validation) === null || _a === void 0 ? void 0 : _a[(0, lower_case_first_1.lowerCaseFirst)(model)]) === null || _b === void 0 ? void 0 : _b.hasValidation) === true;
|
|
110
115
|
}
|
|
111
116
|
getPreValueSelect(model) {
|
|
112
117
|
return __awaiter(this, void 0, void 0, function* () {
|
|
113
|
-
const guard = this.policy.guard[(0,
|
|
118
|
+
const guard = this.policy.guard[(0, lower_case_first_1.lowerCaseFirst)(model)];
|
|
114
119
|
if (!guard) {
|
|
115
120
|
throw this.unknownError(`unable to load policy guard for ${model}`);
|
|
116
121
|
}
|
|
@@ -118,9 +123,8 @@ class PolicyUtil {
|
|
|
118
123
|
});
|
|
119
124
|
}
|
|
120
125
|
getModelSchema(model) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
});
|
|
126
|
+
var _a, _b;
|
|
127
|
+
return this.hasValidation(model) && ((_b = (_a = this.zodSchemas) === null || _a === void 0 ? void 0 : _a.models) === null || _b === void 0 ? void 0 : _b[`${(0, upper_case_first_1.upperCaseFirst)(model)}Schema`]);
|
|
124
128
|
}
|
|
125
129
|
/**
|
|
126
130
|
* Injects model auth guard as where clause.
|
|
@@ -133,7 +137,7 @@ class PolicyUtil {
|
|
|
133
137
|
// to-one: direct-conditions/is/isNot
|
|
134
138
|
yield this.injectGuardForFields(model, args.where, operation);
|
|
135
139
|
}
|
|
136
|
-
const guard =
|
|
140
|
+
const guard = this.getAuthGuard(model, operation);
|
|
137
141
|
args.where = this.and(args.where, guard);
|
|
138
142
|
});
|
|
139
143
|
}
|
|
@@ -158,7 +162,7 @@ class PolicyUtil {
|
|
|
158
162
|
}
|
|
159
163
|
injectGuardForToManyField(fieldInfo, payload, operation) {
|
|
160
164
|
return __awaiter(this, void 0, void 0, function* () {
|
|
161
|
-
const guard =
|
|
165
|
+
const guard = this.getAuthGuard(fieldInfo.type, operation);
|
|
162
166
|
if (payload.some) {
|
|
163
167
|
yield this.injectGuardForFields(fieldInfo.type, payload.some, operation);
|
|
164
168
|
// turn "some" into: { some: { AND: [guard, payload.some] } }
|
|
@@ -185,7 +189,7 @@ class PolicyUtil {
|
|
|
185
189
|
}
|
|
186
190
|
injectGuardForToOneField(fieldInfo, payload, operation) {
|
|
187
191
|
return __awaiter(this, void 0, void 0, function* () {
|
|
188
|
-
const guard =
|
|
192
|
+
const guard = this.getAuthGuard(fieldInfo.type, operation);
|
|
189
193
|
if (payload.is || payload.isNot) {
|
|
190
194
|
if (payload.is) {
|
|
191
195
|
yield this.injectGuardForFields(fieldInfo.type, payload.is, operation);
|
|
@@ -228,9 +232,11 @@ class PolicyUtil {
|
|
|
228
232
|
yield this.injectAuthGuard(args, model, 'read');
|
|
229
233
|
// recursively inject read guard conditions into the query args
|
|
230
234
|
yield this.injectNestedReadConditions(model, args);
|
|
231
|
-
this.
|
|
235
|
+
if (this.shouldLogQuery) {
|
|
236
|
+
this.logger.info(`[withPolicy] \`findMany\`:\n${(0, utils_1.formatObject)(args)}`);
|
|
237
|
+
}
|
|
232
238
|
const result = yield this.db[model].findMany(args);
|
|
233
|
-
yield
|
|
239
|
+
yield this.postProcessForRead(result, model, args, 'read');
|
|
234
240
|
return result;
|
|
235
241
|
});
|
|
236
242
|
}
|
|
@@ -239,9 +245,9 @@ class PolicyUtil {
|
|
|
239
245
|
var _a;
|
|
240
246
|
return __awaiter(this, void 0, void 0, function* () {
|
|
241
247
|
// e.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' }
|
|
242
|
-
const uniqueConstraints = (_a = this.modelMeta.uniqueConstraints) === null || _a === void 0 ? void 0 : _a[(0,
|
|
248
|
+
const uniqueConstraints = (_a = this.modelMeta.uniqueConstraints) === null || _a === void 0 ? void 0 : _a[(0, lower_case_first_1.lowerCaseFirst)(model)];
|
|
243
249
|
let flattened = false;
|
|
244
|
-
if (uniqueConstraints) {
|
|
250
|
+
if (uniqueConstraints && Object.keys(uniqueConstraints).length > 0) {
|
|
245
251
|
for (const [field, value] of Object.entries(args)) {
|
|
246
252
|
if (uniqueConstraints[field] && typeof value === 'object') {
|
|
247
253
|
for (const [f, v] of Object.entries(value)) {
|
|
@@ -253,7 +259,8 @@ class PolicyUtil {
|
|
|
253
259
|
}
|
|
254
260
|
}
|
|
255
261
|
if (flattened) {
|
|
256
|
-
|
|
262
|
+
// DEBUG
|
|
263
|
+
// this.logger.info(`Filter flattened: ${JSON.stringify(args)}`);
|
|
257
264
|
}
|
|
258
265
|
});
|
|
259
266
|
}
|
|
@@ -264,6 +271,35 @@ class PolicyUtil {
|
|
|
264
271
|
if (!injectTarget) {
|
|
265
272
|
return;
|
|
266
273
|
}
|
|
274
|
+
if (injectTarget._count !== undefined) {
|
|
275
|
+
// _count needs to respect read policies of related models
|
|
276
|
+
if (injectTarget._count === true) {
|
|
277
|
+
// include count for all relations, expand to all fields
|
|
278
|
+
// so that we can inject guard conditions for each of them
|
|
279
|
+
injectTarget._count = { select: {} };
|
|
280
|
+
const modelFields = (0, model_meta_1.getFields)(this.modelMeta, model);
|
|
281
|
+
if (modelFields) {
|
|
282
|
+
for (const [k, v] of Object.entries(modelFields)) {
|
|
283
|
+
if (v.isDataModel && v.isArray) {
|
|
284
|
+
// create an entry for to-many relation
|
|
285
|
+
injectTarget._count.select[k] = {};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// inject conditions for each relation
|
|
291
|
+
for (const field of Object.keys(injectTarget._count.select)) {
|
|
292
|
+
if (typeof injectTarget._count.select[field] !== 'object') {
|
|
293
|
+
injectTarget._count.select[field] = {};
|
|
294
|
+
}
|
|
295
|
+
const fieldInfo = (0, model_meta_1.resolveField)(this.modelMeta, model, field);
|
|
296
|
+
if (!fieldInfo) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
// inject into the "where" clause inside select
|
|
300
|
+
yield this.injectAuthGuard(injectTarget._count.select[field], fieldInfo.type, 'read');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
267
303
|
const idFields = this.getIdFields(model);
|
|
268
304
|
for (const field of (0, utils_1.getModelFields)(injectTarget)) {
|
|
269
305
|
const fieldInfo = (0, model_meta_1.resolveField)(this.modelMeta, model, field);
|
|
@@ -301,41 +337,68 @@ class PolicyUtil {
|
|
|
301
337
|
* (which can't be trimmed at query time) and removes fields that should be
|
|
302
338
|
* omitted.
|
|
303
339
|
*/
|
|
304
|
-
postProcessForRead(
|
|
340
|
+
postProcessForRead(data, model, args, operation) {
|
|
305
341
|
var _a;
|
|
306
342
|
return __awaiter(this, void 0, void 0, function* () {
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
// strip auxiliary fields
|
|
312
|
-
for (const auxField of sdk_1.AUXILIARY_FIELDS) {
|
|
313
|
-
if (auxField in entityData) {
|
|
314
|
-
delete entityData[auxField];
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
const injectTarget = (_a = args.select) !== null && _a !== void 0 ? _a : args.include;
|
|
318
|
-
if (!injectTarget) {
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
// to-one relation data cannot be trimmed by injected guards, we have to
|
|
322
|
-
// post-check them
|
|
323
|
-
for (const field of (0, utils_1.getModelFields)(injectTarget)) {
|
|
324
|
-
if (!(entityData === null || entityData === void 0 ? void 0 : entityData[field])) {
|
|
343
|
+
for (const entityData of (0, utils_1.enumerate)(data)) {
|
|
344
|
+
if (typeof entityData !== 'object' || !entityData) {
|
|
325
345
|
continue;
|
|
326
346
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
347
|
+
// strip auxiliary fields
|
|
348
|
+
for (const auxField of constants_1.AUXILIARY_FIELDS) {
|
|
349
|
+
if (auxField in entityData) {
|
|
350
|
+
delete entityData[auxField];
|
|
351
|
+
}
|
|
330
352
|
}
|
|
331
|
-
const
|
|
332
|
-
if (
|
|
353
|
+
const injectTarget = (_a = args.select) !== null && _a !== void 0 ? _a : args.include;
|
|
354
|
+
if (!injectTarget) {
|
|
333
355
|
continue;
|
|
334
356
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
357
|
+
// recurse into nested entities
|
|
358
|
+
for (const field of Object.keys(injectTarget)) {
|
|
359
|
+
const fieldData = entityData[field];
|
|
360
|
+
if (typeof fieldData !== 'object' || !fieldData) {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
const fieldInfo = (0, model_meta_1.resolveField)(this.modelMeta, model, field);
|
|
364
|
+
if (fieldInfo) {
|
|
365
|
+
if (fieldInfo.isDataModel && !fieldInfo.isArray) {
|
|
366
|
+
// to-one relation data cannot be trimmed by injected guards, we have to
|
|
367
|
+
// post-check them
|
|
368
|
+
const ids = this.getEntityIds(fieldInfo.type, fieldData);
|
|
369
|
+
if (Object.keys(ids).length !== 0) {
|
|
370
|
+
// if (this.logger.enabled('info')) {
|
|
371
|
+
// this.logger.info(
|
|
372
|
+
// `Validating read of to-one relation: ${fieldInfo.type}#${formatObject(ids)}`
|
|
373
|
+
// );
|
|
374
|
+
// }
|
|
375
|
+
try {
|
|
376
|
+
yield this.checkPolicyForFilter(fieldInfo.type, ids, operation, this.db);
|
|
377
|
+
}
|
|
378
|
+
catch (err) {
|
|
379
|
+
if ((0, error_1.isPrismaClientKnownRequestError)(err) &&
|
|
380
|
+
err.code === constants_1.PrismaErrorCode.CONSTRAINED_FAILED) {
|
|
381
|
+
// denied by policy
|
|
382
|
+
if (fieldInfo.isOptional) {
|
|
383
|
+
// if the relation is optional, just nullify it
|
|
384
|
+
entityData[field] = null;
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
// otherwise reject
|
|
388
|
+
throw err;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
// unknown error
|
|
393
|
+
throw err;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
// recurse
|
|
399
|
+
yield this.postProcessForRead(fieldData, fieldInfo.type, injectTarget[field], operation);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
339
402
|
}
|
|
340
403
|
});
|
|
341
404
|
}
|
|
@@ -371,17 +434,17 @@ class PolicyUtil {
|
|
|
371
434
|
}
|
|
372
435
|
// use a transaction to conduct write, so in case any create or nested create
|
|
373
436
|
// fails access policies, we can roll back the entire operation
|
|
374
|
-
const transactionId = (0,
|
|
437
|
+
const transactionId = (0, cuid2_1.createId)();
|
|
375
438
|
// args processor for create
|
|
376
439
|
const processCreate = (model, args) => __awaiter(this, void 0, void 0, function* () {
|
|
377
|
-
const guard =
|
|
378
|
-
const schema =
|
|
440
|
+
const guard = this.getAuthGuard(model, 'create');
|
|
441
|
+
const schema = this.getModelSchema(model);
|
|
379
442
|
if (guard === false) {
|
|
380
443
|
throw this.deniedByPolicy(model, 'create');
|
|
381
444
|
}
|
|
382
445
|
else if (guard !== true || schema) {
|
|
383
446
|
// mark the create with a transaction tag so we can check them later
|
|
384
|
-
args[
|
|
447
|
+
args[constants_1.TRANSACTION_FIELD_NAME] = `${transactionId}:create`;
|
|
385
448
|
createdModels.add(model);
|
|
386
449
|
}
|
|
387
450
|
});
|
|
@@ -390,10 +453,16 @@ class PolicyUtil {
|
|
|
390
453
|
let result, currQuery;
|
|
391
454
|
let currField;
|
|
392
455
|
for (let i = context.nestingPath.length - 1; i >= 0; i--) {
|
|
393
|
-
const { field, where, unique } = context.nestingPath[i];
|
|
456
|
+
const { field, model, where, unique } = context.nestingPath[i];
|
|
457
|
+
// never modify the original where because it's shared in the structure
|
|
458
|
+
const visitWhere = Object.assign({}, where);
|
|
459
|
+
if (model && where) {
|
|
460
|
+
// make sure composite unique condition is flattened
|
|
461
|
+
yield this.flattenGeneratedUniqueField(model, visitWhere);
|
|
462
|
+
}
|
|
394
463
|
if (!result) {
|
|
395
464
|
// first segment (bottom), just use its where clause
|
|
396
|
-
result = currQuery = Object.assign({},
|
|
465
|
+
result = currQuery = Object.assign({}, visitWhere);
|
|
397
466
|
currField = field;
|
|
398
467
|
}
|
|
399
468
|
else {
|
|
@@ -403,7 +472,14 @@ class PolicyUtil {
|
|
|
403
472
|
if (!currField.backLink) {
|
|
404
473
|
throw this.unknownError(`field ${currField.type}.${currField.name} doesn't have a backLink`);
|
|
405
474
|
}
|
|
406
|
-
|
|
475
|
+
const backLinkField = this.getModelField(currField.type, currField.backLink);
|
|
476
|
+
if (backLinkField === null || backLinkField === void 0 ? void 0 : backLinkField.isArray) {
|
|
477
|
+
// many-side of relationship, wrap with "some" query
|
|
478
|
+
currQuery[currField.backLink] = { some: Object.assign({}, visitWhere) };
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
currQuery[currField.backLink] = Object.assign({}, visitWhere);
|
|
482
|
+
}
|
|
407
483
|
currQuery = currQuery[currField.backLink];
|
|
408
484
|
currField = field;
|
|
409
485
|
}
|
|
@@ -416,7 +492,7 @@ class PolicyUtil {
|
|
|
416
492
|
});
|
|
417
493
|
// args processor for update/upsert
|
|
418
494
|
const processUpdate = (model, where, context) => __awaiter(this, void 0, void 0, function* () {
|
|
419
|
-
const preGuard =
|
|
495
|
+
const preGuard = this.getAuthGuard(model, 'update');
|
|
420
496
|
if (preGuard === false) {
|
|
421
497
|
throw this.deniedByPolicy(model, 'update');
|
|
422
498
|
}
|
|
@@ -467,7 +543,7 @@ class PolicyUtil {
|
|
|
467
543
|
});
|
|
468
544
|
// args processor for updateMany
|
|
469
545
|
const processUpdateMany = (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
470
|
-
const guard =
|
|
546
|
+
const guard = this.getAuthGuard(model, 'update');
|
|
471
547
|
if (guard === false) {
|
|
472
548
|
throw this.deniedByPolicy(model, 'update');
|
|
473
549
|
}
|
|
@@ -480,8 +556,8 @@ class PolicyUtil {
|
|
|
480
556
|
// for models with post-update rules, we need to read and store
|
|
481
557
|
// entity values before the update for post-update check
|
|
482
558
|
const preparePostUpdateCheck = (model, context) => __awaiter(this, void 0, void 0, function* () {
|
|
483
|
-
const postGuard =
|
|
484
|
-
const schema =
|
|
559
|
+
const postGuard = this.getAuthGuard(model, 'postUpdate');
|
|
560
|
+
const schema = this.getModelSchema(model);
|
|
485
561
|
// post-update check is needed if there's post-update rule or validation schema
|
|
486
562
|
if (postGuard !== true || schema) {
|
|
487
563
|
// fetch preValue selection (analyzed from the post-update rules)
|
|
@@ -498,7 +574,9 @@ class PolicyUtil {
|
|
|
498
574
|
select[idField.name] = true;
|
|
499
575
|
}
|
|
500
576
|
const query = { where: filter, select };
|
|
501
|
-
this.
|
|
577
|
+
if (this.shouldLogQuery) {
|
|
578
|
+
this.logger.info(`[withPolicy] \`findMany\` for fetching pre-update entities:\n${(0, utils_1.formatObject)(args)}`);
|
|
579
|
+
}
|
|
502
580
|
const entities = yield this.db[model].findMany(query);
|
|
503
581
|
entities.forEach((entity) => {
|
|
504
582
|
addUpdatedEntity(model, this.getEntityIds(model, entity), entity);
|
|
@@ -507,7 +585,7 @@ class PolicyUtil {
|
|
|
507
585
|
});
|
|
508
586
|
// args processor for delete
|
|
509
587
|
const processDelete = (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
510
|
-
const guard =
|
|
588
|
+
const guard = this.getAuthGuard(model, 'delete');
|
|
511
589
|
if (guard === false) {
|
|
512
590
|
throw this.deniedByPolicy(model, 'delete');
|
|
513
591
|
}
|
|
@@ -524,6 +602,8 @@ class PolicyUtil {
|
|
|
524
602
|
});
|
|
525
603
|
// process relation updates: connect, connectOrCreate, and disconnect
|
|
526
604
|
const processRelationUpdate = (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
605
|
+
// CHECK ME: equire the entity being connected readable?
|
|
606
|
+
// await this.checkPolicyForFilter(model, args, 'read', this.db);
|
|
527
607
|
var _a;
|
|
528
608
|
if ((_a = context.field) === null || _a === void 0 ? void 0 : _a.backLink) {
|
|
529
609
|
// fetch the backlink field of the model being connected
|
|
@@ -538,66 +618,56 @@ class PolicyUtil {
|
|
|
538
618
|
// use a visitor to process args before conducting the write action
|
|
539
619
|
const visitor = new nested_write_vistor_1.NestedWriteVisitor(this.modelMeta, {
|
|
540
620
|
create: (model, args) => __awaiter(this, void 0, void 0, function* () {
|
|
541
|
-
|
|
542
|
-
yield processCreate(model, oneArgs);
|
|
543
|
-
}
|
|
621
|
+
yield processCreate(model, args);
|
|
544
622
|
}),
|
|
545
623
|
connectOrCreate: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
yield processRelationUpdate(model, oneArgs.where, context);
|
|
552
|
-
}
|
|
624
|
+
if (args.create) {
|
|
625
|
+
yield processCreate(model, args.create);
|
|
626
|
+
}
|
|
627
|
+
if (args.where) {
|
|
628
|
+
yield processRelationUpdate(model, args.where, context);
|
|
553
629
|
}
|
|
554
630
|
}),
|
|
555
631
|
connect: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
556
|
-
|
|
557
|
-
yield processRelationUpdate(model, oneArgs, context);
|
|
558
|
-
}
|
|
632
|
+
yield processRelationUpdate(model, args, context);
|
|
559
633
|
}),
|
|
560
634
|
disconnect: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
561
|
-
|
|
562
|
-
yield processRelationUpdate(model, oneArgs, context);
|
|
563
|
-
}
|
|
635
|
+
yield processRelationUpdate(model, args, context);
|
|
564
636
|
}),
|
|
565
637
|
update: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
566
|
-
|
|
567
|
-
yield processUpdate(model, oneArgs.where, context);
|
|
568
|
-
}
|
|
638
|
+
yield processUpdate(model, args.where, context);
|
|
569
639
|
}),
|
|
570
640
|
updateMany: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
571
|
-
|
|
572
|
-
yield processUpdateMany(model, oneArgs, context);
|
|
573
|
-
}
|
|
641
|
+
yield processUpdateMany(model, args, context);
|
|
574
642
|
}),
|
|
575
643
|
upsert: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
yield processUpdate(model, oneArgs.where, context);
|
|
582
|
-
}
|
|
644
|
+
if (args.create) {
|
|
645
|
+
yield processCreate(model, args.create);
|
|
646
|
+
}
|
|
647
|
+
if (args.update) {
|
|
648
|
+
yield processUpdate(model, args.where, context);
|
|
583
649
|
}
|
|
584
650
|
}),
|
|
585
651
|
delete: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
586
|
-
|
|
587
|
-
yield processDelete(model, oneArgs, context);
|
|
588
|
-
}
|
|
652
|
+
yield processDelete(model, args, context);
|
|
589
653
|
}),
|
|
590
|
-
|
|
591
|
-
|
|
654
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
655
|
+
deleteMany: (model, args, _context) => __awaiter(this, void 0, void 0, function* () {
|
|
656
|
+
const guard = this.getAuthGuard(model, 'delete');
|
|
592
657
|
if (guard === false) {
|
|
593
658
|
throw this.deniedByPolicy(model, 'delete');
|
|
594
659
|
}
|
|
595
660
|
else if (guard !== true) {
|
|
596
|
-
if (
|
|
597
|
-
|
|
661
|
+
if (args.where) {
|
|
662
|
+
args.where = this.and(args.where, guard);
|
|
598
663
|
}
|
|
599
664
|
else {
|
|
600
|
-
|
|
665
|
+
const copy = (0, deepcopy_1.default)(args);
|
|
666
|
+
for (const key of Object.keys(args)) {
|
|
667
|
+
delete args[key];
|
|
668
|
+
}
|
|
669
|
+
const combined = this.and(copy, guard);
|
|
670
|
+
Object.assign(args, combined);
|
|
601
671
|
}
|
|
602
672
|
}
|
|
603
673
|
}),
|
|
@@ -613,7 +683,7 @@ class PolicyUtil {
|
|
|
613
683
|
const result = yield writeAction(tx[model], args);
|
|
614
684
|
if (createdModels.size > 0) {
|
|
615
685
|
// do post-check on created entities
|
|
616
|
-
yield Promise.all([...createdModels].map((model) => this.checkPolicyForFilter(model, { [
|
|
686
|
+
yield Promise.all([...createdModels].map((model) => this.checkPolicyForFilter(model, { [constants_1.TRANSACTION_FIELD_NAME]: `${transactionId}:create` }, 'create', tx)));
|
|
617
687
|
}
|
|
618
688
|
if (updatedModels.size > 0) {
|
|
619
689
|
// do post-check on updated entities
|
|
@@ -626,6 +696,11 @@ class PolicyUtil {
|
|
|
626
696
|
}
|
|
627
697
|
});
|
|
628
698
|
}
|
|
699
|
+
getModelField(model, field) {
|
|
700
|
+
var _a;
|
|
701
|
+
model = (0, lower_case_first_1.lowerCaseFirst)(model);
|
|
702
|
+
return (_a = this.modelMeta.fields[model]) === null || _a === void 0 ? void 0 : _a[field];
|
|
703
|
+
}
|
|
629
704
|
transaction(db, action) {
|
|
630
705
|
if (db.__zenstack_tx) {
|
|
631
706
|
// already in transaction, don't nest
|
|
@@ -636,16 +711,16 @@ class PolicyUtil {
|
|
|
636
711
|
}
|
|
637
712
|
}
|
|
638
713
|
deniedByPolicy(model, operation, extra, reason) {
|
|
639
|
-
return
|
|
714
|
+
return (0, utils_1.prismaClientKnownRequestError)(this.db, `denied by policy: ${model} entities failed '${operation}' check${extra ? ', ' + extra : ''}`, { clientVersion: (0, version_1.getVersion)(), code: constants_1.PrismaErrorCode.CONSTRAINED_FAILED, meta: { reason } });
|
|
640
715
|
}
|
|
641
716
|
notFound(model) {
|
|
642
|
-
return
|
|
717
|
+
return (0, utils_1.prismaClientKnownRequestError)(this.db, `entity not found for model ${model}`, {
|
|
643
718
|
clientVersion: (0, version_1.getVersion)(),
|
|
644
719
|
code: 'P2025',
|
|
645
720
|
});
|
|
646
721
|
}
|
|
647
722
|
unknownError(message) {
|
|
648
|
-
return
|
|
723
|
+
return (0, utils_1.prismaClientUnknownRequestError)(this.db, message, {
|
|
649
724
|
clientVersion: (0, version_1.getVersion)(),
|
|
650
725
|
});
|
|
651
726
|
}
|
|
@@ -655,37 +730,73 @@ class PolicyUtil {
|
|
|
655
730
|
*/
|
|
656
731
|
checkPolicyForFilter(model, filter, operation, db) {
|
|
657
732
|
return __awaiter(this, void 0, void 0, function* () {
|
|
658
|
-
|
|
733
|
+
const guard = this.getAuthGuard(model, operation);
|
|
734
|
+
const schema = (operation === 'create' || operation === 'update') && this.getModelSchema(model);
|
|
735
|
+
if (guard === true && !schema) {
|
|
736
|
+
// unconditionally allowed
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
// if (this.logger.enabled('info')) {
|
|
740
|
+
// this.logger.info(`Checking policy for ${model}#${JSON.stringify(filter)} for ${operation}`);
|
|
741
|
+
// }
|
|
659
742
|
const queryFilter = (0, deepcopy_1.default)(filter);
|
|
660
743
|
// query args will be used with findMany, so we need to
|
|
661
744
|
// translate unique constraint filters into a flat filter
|
|
662
745
|
// e.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' }
|
|
663
746
|
yield this.flattenGeneratedUniqueField(model, queryFilter);
|
|
664
|
-
const
|
|
665
|
-
|
|
747
|
+
const countArgs = { where: queryFilter };
|
|
748
|
+
// if (this.shouldLogQuery) {
|
|
749
|
+
// this.logger.info(
|
|
750
|
+
// `[withPolicy] \`count\` for policy check without guard:\n${formatObject(countArgs)}`
|
|
751
|
+
// );
|
|
752
|
+
// }
|
|
753
|
+
const count = (yield db[model].count(countArgs));
|
|
754
|
+
if (count === 0) {
|
|
755
|
+
// there's nothing to filter out
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if (guard === false) {
|
|
759
|
+
// unconditionally denied
|
|
760
|
+
throw this.deniedByPolicy(model, operation, `${count} ${(0, pluralize_1.default)('entity', count)} failed policy check`);
|
|
761
|
+
}
|
|
666
762
|
// build a query condition with policy injected
|
|
667
763
|
const guardedQuery = { where: this.and(queryFilter, guard) };
|
|
668
|
-
const schema = (operation === 'create' || operation === 'update') && (yield this.getModelSchema(model));
|
|
669
764
|
if (schema) {
|
|
670
765
|
// we've got schemas, so have to fetch entities and validate them
|
|
766
|
+
// if (this.shouldLogQuery) {
|
|
767
|
+
// this.logger.info(
|
|
768
|
+
// `[withPolicy] \`findMany\` for policy check with guard:\n${formatObject(countArgs)}`
|
|
769
|
+
// );
|
|
770
|
+
// }
|
|
671
771
|
const entities = yield db[model].findMany(guardedQuery);
|
|
672
772
|
if (entities.length < count) {
|
|
673
|
-
this.logger.info
|
|
773
|
+
if (this.logger.enabled('info')) {
|
|
774
|
+
this.logger.info(`entity ${model} failed policy check for operation ${operation}`);
|
|
775
|
+
}
|
|
674
776
|
throw this.deniedByPolicy(model, operation, `${count - entities.length} ${(0, pluralize_1.default)('entity', count - entities.length)} failed policy check`);
|
|
675
777
|
}
|
|
676
778
|
// TODO: push down schema check to the database
|
|
677
779
|
const schemaCheckErrors = entities.map((entity) => schema.safeParse(entity)).filter((r) => !r.success);
|
|
678
780
|
if (schemaCheckErrors.length > 0) {
|
|
679
781
|
const error = schemaCheckErrors.map((r) => !r.success && (0, zod_validation_error_1.fromZodError)(r.error).message).join(', ');
|
|
680
|
-
this.logger.info
|
|
681
|
-
|
|
782
|
+
if (this.logger.enabled('info')) {
|
|
783
|
+
this.logger.info(`entity ${model} failed schema check for operation ${operation}: ${error}`);
|
|
784
|
+
}
|
|
785
|
+
throw this.deniedByPolicy(model, operation, `entities failed schema check: [${error}]`, constants_1.CrudFailureReason.DATA_VALIDATION_VIOLATION);
|
|
682
786
|
}
|
|
683
787
|
}
|
|
684
788
|
else {
|
|
685
789
|
// count entities with policy injected and see if any of them are filtered out
|
|
790
|
+
// if (this.shouldLogQuery) {
|
|
791
|
+
// this.logger.info(
|
|
792
|
+
// `[withPolicy] \`count\` for policy check with guard:\n${formatObject(guardedQuery)}`
|
|
793
|
+
// );
|
|
794
|
+
// }
|
|
686
795
|
const guardedCount = (yield db[model].count(guardedQuery));
|
|
687
796
|
if (guardedCount < count) {
|
|
688
|
-
this.logger.info
|
|
797
|
+
if (this.logger.enabled('info')) {
|
|
798
|
+
this.logger.info(`entity ${model} failed policy check for operation ${operation}`);
|
|
799
|
+
}
|
|
689
800
|
throw this.deniedByPolicy(model, operation, `${count - guardedCount} ${(0, pluralize_1.default)('entity', count - guardedCount)} failed policy check`);
|
|
690
801
|
}
|
|
691
802
|
}
|
|
@@ -693,24 +804,30 @@ class PolicyUtil {
|
|
|
693
804
|
}
|
|
694
805
|
checkPostUpdate(model, ids, db, preValue) {
|
|
695
806
|
return __awaiter(this, void 0, void 0, function* () {
|
|
696
|
-
this.logger.info
|
|
697
|
-
|
|
807
|
+
// if (this.logger.enabled('info')) {
|
|
808
|
+
// this.logger.info(`Checking post-update policy for ${model}#${ids}, preValue: ${formatObject(preValue)}`);
|
|
809
|
+
// }
|
|
810
|
+
const guard = this.getAuthGuard(model, 'postUpdate', preValue);
|
|
698
811
|
// build a query condition with policy injected
|
|
699
812
|
const guardedQuery = { where: this.and(ids, guard) };
|
|
700
813
|
// query with policy injected
|
|
701
814
|
const entity = yield db[model].findFirst(guardedQuery);
|
|
702
815
|
// see if we get fewer items with policy, if so, reject with an throw
|
|
703
816
|
if (!entity) {
|
|
704
|
-
this.logger.info
|
|
817
|
+
if (this.logger.enabled('info')) {
|
|
818
|
+
this.logger.info(`entity ${model} failed policy check for operation postUpdate`);
|
|
819
|
+
}
|
|
705
820
|
throw this.deniedByPolicy(model, 'postUpdate');
|
|
706
821
|
}
|
|
707
822
|
// TODO: push down schema check to the database
|
|
708
|
-
const schema =
|
|
823
|
+
const schema = this.getModelSchema(model);
|
|
709
824
|
if (schema) {
|
|
710
825
|
const schemaCheckResult = schema.safeParse(entity);
|
|
711
826
|
if (!schemaCheckResult.success) {
|
|
712
827
|
const error = (0, zod_validation_error_1.fromZodError)(schemaCheckResult.error).message;
|
|
713
|
-
this.logger.info
|
|
828
|
+
if (this.logger.enabled('info')) {
|
|
829
|
+
this.logger.info(`entity ${model} failed schema check for operation postUpdate: ${error}`);
|
|
830
|
+
}
|
|
714
831
|
throw this.deniedByPolicy(model, 'postUpdate', `entity failed schema check: ${error}`);
|
|
715
832
|
}
|
|
716
833
|
}
|
|
@@ -729,15 +846,7 @@ class PolicyUtil {
|
|
|
729
846
|
* Gets "id" field for a given model.
|
|
730
847
|
*/
|
|
731
848
|
getIdFields(model) {
|
|
732
|
-
|
|
733
|
-
if (!fields) {
|
|
734
|
-
throw this.unknownError(`Unable to load fields for ${model}`);
|
|
735
|
-
}
|
|
736
|
-
const result = Object.values(fields).filter((f) => f.isId);
|
|
737
|
-
if (result.length === 0) {
|
|
738
|
-
throw this.unknownError(`model ${model} does not have an id field`);
|
|
739
|
-
}
|
|
740
|
-
return result;
|
|
849
|
+
return (0, utils_1.getIdFields)(this.modelMeta, model, true);
|
|
741
850
|
}
|
|
742
851
|
/**
|
|
743
852
|
* Gets id field value from an entity.
|
|
@@ -750,6 +859,9 @@ class PolicyUtil {
|
|
|
750
859
|
}
|
|
751
860
|
return result;
|
|
752
861
|
}
|
|
862
|
+
get shouldLogQuery() {
|
|
863
|
+
return this.logPrismaQuery && this.logger.enabled('info');
|
|
864
|
+
}
|
|
753
865
|
}
|
|
754
866
|
exports.PolicyUtil = PolicyUtil;
|
|
755
867
|
//# sourceMappingURL=policy-utils.js.map
|