@zenstackhq/runtime 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/constants.d.ts +0 -28
- package/constants.js +1 -29
- package/constants.js.map +1 -1
- package/enhancements/delegate.js +8 -0
- package/enhancements/delegate.js.map +1 -1
- package/enhancements/policy/constraint-solver.d.ts +2 -2
- package/enhancements/policy/constraint-solver.js.map +1 -1
- package/enhancements/policy/handler.d.ts +8 -0
- package/enhancements/policy/handler.js +192 -67
- package/enhancements/policy/handler.js.map +1 -1
- package/enhancements/policy/policy-utils.d.ts +19 -13
- package/enhancements/policy/policy-utils.js +294 -169
- package/enhancements/policy/policy-utils.js.map +1 -1
- package/enhancements/proxy.d.ts +12 -0
- package/enhancements/proxy.js +3 -0
- package/enhancements/proxy.js.map +1 -1
- package/enhancements/types.d.ts +151 -19
- package/enhancements/types.js +0 -1
- package/enhancements/types.js.map +1 -1
- package/package.json +4 -3
- package/types.d.ts +3 -2
|
@@ -15,6 +15,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
15
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
16
|
exports.PolicyUtil = void 0;
|
|
17
17
|
const deepcopy_1 = __importDefault(require("deepcopy"));
|
|
18
|
+
const deepmerge_1 = __importDefault(require("deepmerge"));
|
|
18
19
|
const lower_case_first_1 = require("lower-case-first");
|
|
19
20
|
const upper_case_first_1 = require("upper-case-first");
|
|
20
21
|
const zod_validation_error_1 = require("zod-validation-error");
|
|
@@ -34,14 +35,14 @@ class PolicyUtil extends query_utils_1.QueryUtils {
|
|
|
34
35
|
this.shouldLogQuery = shouldLogQuery;
|
|
35
36
|
//#endregion
|
|
36
37
|
//#region Auth guard
|
|
37
|
-
this.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
this.FULL_OPEN_MODEL_POLICY = {
|
|
39
|
+
modelLevel: {
|
|
40
|
+
read: { guard: true },
|
|
41
|
+
create: { guard: true, inputChecker: true },
|
|
42
|
+
update: { guard: true },
|
|
43
|
+
delete: { guard: true },
|
|
44
|
+
postUpdate: { guard: true },
|
|
45
|
+
},
|
|
45
46
|
};
|
|
46
47
|
this.logger = new logger_1.Logger(db);
|
|
47
48
|
this.user = context === null || context === void 0 ? void 0 : context.user;
|
|
@@ -215,14 +216,21 @@ class PolicyUtil extends query_utils_1.QueryUtils {
|
|
|
215
216
|
}
|
|
216
217
|
return result;
|
|
217
218
|
}
|
|
218
|
-
|
|
219
|
+
getModelPolicyDef(model) {
|
|
219
220
|
if (this.options.kinds && !this.options.kinds.includes('policy')) {
|
|
220
221
|
// policy enhancement not enabled, return an fully open guard
|
|
221
|
-
return this.
|
|
222
|
+
return this.FULL_OPEN_MODEL_POLICY;
|
|
222
223
|
}
|
|
223
|
-
|
|
224
|
-
|
|
224
|
+
const def = this.policy.policy[(0, lower_case_first_1.lowerCaseFirst)(model)];
|
|
225
|
+
if (!def) {
|
|
226
|
+
throw this.unknownError(`unable to load policy guard for ${model}`);
|
|
225
227
|
}
|
|
228
|
+
return def;
|
|
229
|
+
}
|
|
230
|
+
getModelGuardForOperation(model, operation) {
|
|
231
|
+
var _a;
|
|
232
|
+
const def = this.getModelPolicyDef(model);
|
|
233
|
+
return (_a = def.modelLevel[operation].guard) !== null && _a !== void 0 ? _a : true;
|
|
226
234
|
}
|
|
227
235
|
/**
|
|
228
236
|
* Gets pregenerated authorization guard object for a given model and operation.
|
|
@@ -231,91 +239,104 @@ class PolicyUtil extends query_utils_1.QueryUtils {
|
|
|
231
239
|
* otherwise returns a guard object
|
|
232
240
|
*/
|
|
233
241
|
getAuthGuard(db, model, operation, preValue) {
|
|
234
|
-
const guard = this.
|
|
235
|
-
|
|
236
|
-
|
|
242
|
+
const guard = this.getModelGuardForOperation(model, operation);
|
|
243
|
+
// constant guard
|
|
244
|
+
if (typeof guard === 'boolean') {
|
|
245
|
+
return this.reduce(guard);
|
|
237
246
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
247
|
+
// invoke guard function
|
|
248
|
+
const r = guard({ user: this.user, preValue }, db);
|
|
249
|
+
return this.reduce(r);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Get field-level read auth guard
|
|
253
|
+
*/
|
|
254
|
+
getFieldReadAuthGuard(db, model, field) {
|
|
255
|
+
var _a, _b, _c;
|
|
256
|
+
const def = this.getModelPolicyDef(model);
|
|
257
|
+
const guard = (_c = (_b = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a.read) === null || _b === void 0 ? void 0 : _b[field]) === null || _c === void 0 ? void 0 : _c.guard;
|
|
258
|
+
if (guard === undefined) {
|
|
259
|
+
// field access is allowed by default
|
|
260
|
+
return this.makeTrue();
|
|
241
261
|
}
|
|
242
|
-
if (
|
|
243
|
-
|
|
262
|
+
if (typeof guard === 'boolean') {
|
|
263
|
+
return this.reduce(guard);
|
|
244
264
|
}
|
|
245
|
-
const r =
|
|
265
|
+
const r = guard({ user: this.user }, db);
|
|
246
266
|
return this.reduce(r);
|
|
247
267
|
}
|
|
248
268
|
/**
|
|
249
269
|
* Get field-level read auth guard that overrides the model-level
|
|
250
270
|
*/
|
|
251
271
|
getFieldOverrideReadAuthGuard(db, model, field) {
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
272
|
+
var _a, _b, _c;
|
|
273
|
+
const def = this.getModelPolicyDef(model);
|
|
274
|
+
const guard = (_c = (_b = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a.read) === null || _b === void 0 ? void 0 : _b[field]) === null || _c === void 0 ? void 0 : _c.overrideGuard;
|
|
275
|
+
if (guard === undefined) {
|
|
255
276
|
// field access is denied by default in override mode
|
|
256
277
|
return this.makeFalse();
|
|
257
278
|
}
|
|
258
|
-
if (typeof
|
|
259
|
-
return this.reduce(
|
|
279
|
+
if (typeof guard === 'boolean') {
|
|
280
|
+
return this.reduce(guard);
|
|
260
281
|
}
|
|
261
|
-
const r =
|
|
282
|
+
const r = guard({ user: this.user }, db);
|
|
262
283
|
return this.reduce(r);
|
|
263
284
|
}
|
|
264
285
|
/**
|
|
265
286
|
* Get field-level update auth guard
|
|
266
287
|
*/
|
|
267
288
|
getFieldUpdateAuthGuard(db, model, field) {
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
|
|
289
|
+
var _a, _b, _c;
|
|
290
|
+
const def = this.getModelPolicyDef(model);
|
|
291
|
+
const guard = (_c = (_b = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a.update) === null || _b === void 0 ? void 0 : _b[field]) === null || _c === void 0 ? void 0 : _c.guard;
|
|
292
|
+
if (guard === undefined) {
|
|
271
293
|
// field access is allowed by default
|
|
272
294
|
return this.makeTrue();
|
|
273
295
|
}
|
|
274
|
-
if (typeof
|
|
275
|
-
return this.reduce(
|
|
296
|
+
if (typeof guard === 'boolean') {
|
|
297
|
+
return this.reduce(guard);
|
|
276
298
|
}
|
|
277
|
-
const r =
|
|
299
|
+
const r = guard({ user: this.user }, db);
|
|
278
300
|
return this.reduce(r);
|
|
279
301
|
}
|
|
280
302
|
/**
|
|
281
303
|
* Get field-level update auth guard that overrides the model-level
|
|
282
304
|
*/
|
|
283
305
|
getFieldOverrideUpdateAuthGuard(db, model, field) {
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
306
|
+
var _a, _b, _c;
|
|
307
|
+
const def = this.getModelPolicyDef(model);
|
|
308
|
+
const guard = (_c = (_b = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a.update) === null || _b === void 0 ? void 0 : _b[field]) === null || _c === void 0 ? void 0 : _c.overrideGuard;
|
|
309
|
+
if (guard === undefined) {
|
|
287
310
|
// field access is denied by default in override mode
|
|
288
311
|
return this.makeFalse();
|
|
289
312
|
}
|
|
290
|
-
if (typeof
|
|
291
|
-
return this.reduce(
|
|
313
|
+
if (typeof guard === 'boolean') {
|
|
314
|
+
return this.reduce(guard);
|
|
292
315
|
}
|
|
293
|
-
const r =
|
|
316
|
+
const r = guard({ user: this.user }, db);
|
|
294
317
|
return this.reduce(r);
|
|
295
318
|
}
|
|
296
319
|
/**
|
|
297
320
|
* Checks if the given model has a policy guard for the given operation.
|
|
298
321
|
*/
|
|
299
322
|
hasAuthGuard(model, operation) {
|
|
300
|
-
const guard = this.
|
|
301
|
-
|
|
302
|
-
return false;
|
|
303
|
-
}
|
|
304
|
-
const provider = guard[operation];
|
|
305
|
-
return typeof provider !== 'boolean' || provider !== true;
|
|
323
|
+
const guard = this.getModelGuardForOperation(model, operation);
|
|
324
|
+
return typeof guard !== 'boolean' || guard !== true;
|
|
306
325
|
}
|
|
307
326
|
/**
|
|
308
327
|
* Checks if the given model has any field-level override policy guard for the given operation.
|
|
309
328
|
*/
|
|
310
329
|
hasOverrideAuthGuard(model, operation) {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
330
|
+
var _a;
|
|
331
|
+
if (operation !== 'read' && operation !== 'update') {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
const def = this.getModelPolicyDef(model);
|
|
335
|
+
if ((_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a[operation]) {
|
|
336
|
+
return Object.values(def.fieldLevel[operation]).some((f) => f.overrideGuard !== undefined || f.overrideEntityChecker !== undefined);
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
return false;
|
|
319
340
|
}
|
|
320
341
|
}
|
|
321
342
|
/**
|
|
@@ -324,18 +345,15 @@ class PolicyUtil extends query_utils_1.QueryUtils {
|
|
|
324
345
|
* @returns boolean if static analysis is enough to determine the result, undefined if not
|
|
325
346
|
*/
|
|
326
347
|
checkInputGuard(model, args, operation) {
|
|
327
|
-
const
|
|
328
|
-
|
|
348
|
+
const def = this.getModelPolicyDef(model);
|
|
349
|
+
const guard = def.modelLevel[operation].inputChecker;
|
|
350
|
+
if (guard === undefined) {
|
|
329
351
|
return undefined;
|
|
330
352
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
return provider;
|
|
353
|
+
if (typeof guard === 'boolean') {
|
|
354
|
+
return guard;
|
|
334
355
|
}
|
|
335
|
-
|
|
336
|
-
return undefined;
|
|
337
|
-
}
|
|
338
|
-
return provider(args, { user: this.user });
|
|
356
|
+
return guard(args, { user: this.user });
|
|
339
357
|
}
|
|
340
358
|
/**
|
|
341
359
|
* Injects model auth guard as where clause.
|
|
@@ -372,74 +390,93 @@ class PolicyUtil extends query_utils_1.QueryUtils {
|
|
|
372
390
|
args.where = this.makeFalse();
|
|
373
391
|
return false;
|
|
374
392
|
}
|
|
393
|
+
let mergedGuard = guard;
|
|
375
394
|
if (args.where) {
|
|
376
395
|
// inject into relation fields:
|
|
377
396
|
// to-many: some/none/every
|
|
378
397
|
// to-one: direct-conditions/is/isNot
|
|
379
|
-
this.
|
|
398
|
+
mergedGuard = this.injectReadGuardForRelationFields(db, model, args.where, guard);
|
|
380
399
|
}
|
|
381
|
-
args.where = this.and(args.where,
|
|
400
|
+
args.where = this.and(args.where, mergedGuard);
|
|
382
401
|
return true;
|
|
383
402
|
}
|
|
384
|
-
|
|
403
|
+
// Injects guard for relation fields nested in `payload`. The `modelGuard` parameter represents the model-level guard for `model`.
|
|
404
|
+
// The function returns a modified copy of `modelGuard` with field-level policies combined.
|
|
405
|
+
injectReadGuardForRelationFields(db, model, payload, modelGuard) {
|
|
406
|
+
if (!payload || typeof payload !== 'object' || Object.keys(payload).length === 0) {
|
|
407
|
+
return modelGuard;
|
|
408
|
+
}
|
|
409
|
+
const allFieldGuards = [];
|
|
410
|
+
const allFieldOverrideGuards = [];
|
|
385
411
|
for (const [field, subPayload] of Object.entries(payload)) {
|
|
386
412
|
if (!subPayload) {
|
|
387
413
|
continue;
|
|
388
414
|
}
|
|
415
|
+
allFieldGuards.push(this.getFieldReadAuthGuard(db, model, field));
|
|
416
|
+
allFieldOverrideGuards.push(this.getFieldOverrideReadAuthGuard(db, model, field));
|
|
389
417
|
const fieldInfo = (0, cross_1.resolveField)(this.modelMeta, model, field);
|
|
390
|
-
if (
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
this.injectGuardForToOneField(db, fieldInfo, subPayload, operation);
|
|
418
|
+
if (fieldInfo === null || fieldInfo === void 0 ? void 0 : fieldInfo.isDataModel) {
|
|
419
|
+
if (fieldInfo.isArray) {
|
|
420
|
+
this.injectReadGuardForToManyField(db, fieldInfo, subPayload);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
this.injectReadGuardForToOneField(db, fieldInfo, subPayload);
|
|
424
|
+
}
|
|
398
425
|
}
|
|
399
426
|
}
|
|
427
|
+
// all existing field-level guards must be true
|
|
428
|
+
const mergedGuard = this.and(...allFieldGuards);
|
|
429
|
+
// all existing field-level override guards must be true for override to take effect; override is disabled by default
|
|
430
|
+
const mergedOverrideGuard = allFieldOverrideGuards.length === 0 ? this.makeFalse() : this.and(...allFieldOverrideGuards);
|
|
431
|
+
// (original-guard && field-level-guard) || field-level-override-guard
|
|
432
|
+
const updatedGuard = this.or(this.and(modelGuard, mergedGuard), mergedOverrideGuard);
|
|
433
|
+
return updatedGuard;
|
|
400
434
|
}
|
|
401
|
-
|
|
402
|
-
const guard = this.getAuthGuard(db, fieldInfo.type,
|
|
435
|
+
injectReadGuardForToManyField(db, fieldInfo, payload) {
|
|
436
|
+
const guard = this.getAuthGuard(db, fieldInfo.type, 'read');
|
|
403
437
|
if (payload.some) {
|
|
404
|
-
this.
|
|
438
|
+
const mergedGuard = this.injectReadGuardForRelationFields(db, fieldInfo.type, payload.some, guard);
|
|
405
439
|
// turn "some" into: { some: { AND: [guard, payload.some] } }
|
|
406
|
-
payload.some = this.and(payload.some,
|
|
440
|
+
payload.some = this.and(payload.some, mergedGuard);
|
|
407
441
|
}
|
|
408
442
|
if (payload.none) {
|
|
409
|
-
this.
|
|
443
|
+
const mergedGuard = this.injectReadGuardForRelationFields(db, fieldInfo.type, payload.none, guard);
|
|
410
444
|
// turn none into: { none: { AND: [guard, payload.none] } }
|
|
411
|
-
payload.none = this.and(payload.none,
|
|
445
|
+
payload.none = this.and(payload.none, mergedGuard);
|
|
412
446
|
}
|
|
413
447
|
if (payload.every &&
|
|
414
448
|
typeof payload.every === 'object' &&
|
|
415
449
|
// ignore empty every clause
|
|
416
450
|
Object.keys(payload.every).length > 0) {
|
|
417
|
-
this.
|
|
451
|
+
const mergedGuard = this.injectReadGuardForRelationFields(db, fieldInfo.type, payload.every, guard);
|
|
418
452
|
// turn "every" into: { none: { AND: [guard, { NOT: payload.every }] } }
|
|
419
453
|
if (!payload.none) {
|
|
420
454
|
payload.none = {};
|
|
421
455
|
}
|
|
422
|
-
payload.none = this.and(payload.none,
|
|
456
|
+
payload.none = this.and(payload.none, mergedGuard, this.not(payload.every));
|
|
423
457
|
delete payload.every;
|
|
424
458
|
}
|
|
425
459
|
}
|
|
426
|
-
|
|
427
|
-
const guard = this.getAuthGuard(db, fieldInfo.type,
|
|
460
|
+
injectReadGuardForToOneField(db, fieldInfo, payload) {
|
|
461
|
+
const guard = this.getAuthGuard(db, fieldInfo.type, 'read');
|
|
428
462
|
// is|isNot and flat fields conditions are mutually exclusive
|
|
429
|
-
|
|
463
|
+
// is and isNot can be null value
|
|
464
|
+
if (payload.is !== undefined || payload.isNot !== undefined) {
|
|
430
465
|
if (payload.is) {
|
|
431
|
-
this.
|
|
466
|
+
const mergedGuard = this.injectReadGuardForRelationFields(db, fieldInfo.type, payload.is, guard);
|
|
467
|
+
// merge guard with existing "is": { is: { AND: [originalIs, guard] } }
|
|
468
|
+
payload.is = this.and(payload.is, mergedGuard);
|
|
432
469
|
}
|
|
433
470
|
if (payload.isNot) {
|
|
434
|
-
this.
|
|
471
|
+
const mergedGuard = this.injectReadGuardForRelationFields(db, fieldInfo.type, payload.isNot, guard);
|
|
472
|
+
// merge guard with existing "isNot": { isNot: { AND: [originalIsNot, guard] } }
|
|
473
|
+
payload.isNot = this.and(payload.isNot, mergedGuard);
|
|
435
474
|
}
|
|
436
|
-
// merge guard with existing "is": { is: [originalIs, guard] }
|
|
437
|
-
payload.is = this.and(payload.is, guard);
|
|
438
475
|
}
|
|
439
476
|
else {
|
|
440
|
-
this.
|
|
477
|
+
const mergedGuard = this.injectReadGuardForRelationFields(db, fieldInfo.type, payload, guard);
|
|
441
478
|
// turn direct conditions into: { is: { AND: [ originalConditions, guard ] } }
|
|
442
|
-
const combined = this.and((0, deepcopy_1.default)(payload),
|
|
479
|
+
const combined = this.and((0, deepcopy_1.default)(payload), mergedGuard);
|
|
443
480
|
Object.keys(payload).forEach((key) => delete payload[key]);
|
|
444
481
|
payload.is = combined;
|
|
445
482
|
}
|
|
@@ -457,7 +494,7 @@ class PolicyUtil extends query_utils_1.QueryUtils {
|
|
|
457
494
|
// inject into relation fields:
|
|
458
495
|
// to-many: some/none/every
|
|
459
496
|
// to-one: direct-conditions/is/isNot
|
|
460
|
-
this.
|
|
497
|
+
this.injectReadGuardForRelationFields(db, model, args.where, {});
|
|
461
498
|
}
|
|
462
499
|
if (injected.where && Object.keys(injected.where).length > 0 && !this.isTrue(injected.where)) {
|
|
463
500
|
if (!args.where) {
|
|
@@ -487,31 +524,23 @@ class PolicyUtil extends query_utils_1.QueryUtils {
|
|
|
487
524
|
* Gets checker constraints for the given model and operation.
|
|
488
525
|
*/
|
|
489
526
|
getCheckerConstraint(model, operation) {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
return provider;
|
|
527
|
+
if (this.options.kinds && !this.options.kinds.includes('policy')) {
|
|
528
|
+
// policy enhancement not enabled, return a constant true checker result
|
|
529
|
+
return true;
|
|
494
530
|
}
|
|
495
|
-
|
|
496
|
-
|
|
531
|
+
const def = this.getModelPolicyDef(model);
|
|
532
|
+
const checker = def.modelLevel[operation].permissionChecker;
|
|
533
|
+
if (checker === undefined) {
|
|
534
|
+
throw new Error(`Generated permission checkers not found. Please make sure the "generatePermissionChecker" option is set to true in the "@core/enhancer" plugin.`);
|
|
497
535
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
getModelChecker(model) {
|
|
502
|
-
var _a;
|
|
503
|
-
if (this.options.kinds && !this.options.kinds.includes('policy')) {
|
|
504
|
-
// policy enhancement not enabled, return a constant true checker
|
|
505
|
-
return { create: true, read: true, update: true, delete: true };
|
|
536
|
+
if (typeof checker === 'boolean') {
|
|
537
|
+
return checker;
|
|
506
538
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
if (!result) {
|
|
510
|
-
// checker generation not enabled, return constant false checker
|
|
511
|
-
throw new Error(`Generated permission checkers not found. Please make sure the "generatePermissionChecker" option is set to true in the "@core/enhancer" plugin.`);
|
|
512
|
-
}
|
|
513
|
-
return result;
|
|
539
|
+
if (typeof checker !== 'function') {
|
|
540
|
+
throw this.unknownError(`invalid ${operation} checker function for ${model}`);
|
|
514
541
|
}
|
|
542
|
+
// call checker function
|
|
543
|
+
return checker({ user: this.user });
|
|
515
544
|
}
|
|
516
545
|
//#endregion
|
|
517
546
|
/**
|
|
@@ -609,6 +638,7 @@ class PolicyUtil extends query_utils_1.QueryUtils {
|
|
|
609
638
|
if (this.isFalse(guard) && !this.hasOverrideAuthGuard(model, operation)) {
|
|
610
639
|
throw this.deniedByPolicy(model, operation, `entity ${(0, utils_1.formatObject)(uniqueFilter, false)} failed policy check`, constants_1.CrudFailureReason.ACCESS_POLICY_VIOLATION);
|
|
611
640
|
}
|
|
641
|
+
let entityChecker;
|
|
612
642
|
if (operation === 'update' && args) {
|
|
613
643
|
// merge field-level policy guards
|
|
614
644
|
const fieldUpdateGuard = this.getFieldUpdateGuards(db, model, args);
|
|
@@ -616,28 +646,37 @@ class PolicyUtil extends query_utils_1.QueryUtils {
|
|
|
616
646
|
// rejected
|
|
617
647
|
throw this.deniedByPolicy(model, 'update', `entity ${(0, utils_1.formatObject)(uniqueFilter, false)} failed update policy check for field "${fieldUpdateGuard.rejectedByField}"`, constants_1.CrudFailureReason.ACCESS_POLICY_VIOLATION);
|
|
618
648
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
guard = this.and(guard, fieldUpdateGuard.guard);
|
|
623
|
-
}
|
|
624
|
-
if (fieldUpdateGuard.overrideGuard) {
|
|
625
|
-
// merge field-level override guard
|
|
626
|
-
guard = this.or(guard, fieldUpdateGuard.overrideGuard);
|
|
627
|
-
}
|
|
649
|
+
if (fieldUpdateGuard.guard) {
|
|
650
|
+
// merge field-level guard with AND
|
|
651
|
+
guard = this.and(guard, fieldUpdateGuard.guard);
|
|
628
652
|
}
|
|
653
|
+
if (fieldUpdateGuard.overrideGuard) {
|
|
654
|
+
// merge field-level override guard with OR
|
|
655
|
+
guard = this.or(guard, fieldUpdateGuard.overrideGuard);
|
|
656
|
+
}
|
|
657
|
+
// field-level entity checker
|
|
658
|
+
entityChecker = fieldUpdateGuard.entityChecker;
|
|
629
659
|
}
|
|
630
660
|
// Zod schema is to be checked for "create" and "postUpdate"
|
|
631
661
|
const schema = ['create', 'postUpdate'].includes(operation) ? this.getZodSchema(model) : undefined;
|
|
632
|
-
|
|
662
|
+
// combine field-level entity checker with model-level
|
|
663
|
+
const modelEntityChecker = this.getEntityChecker(model, operation);
|
|
664
|
+
entityChecker = this.combineEntityChecker(entityChecker, modelEntityChecker, 'and');
|
|
665
|
+
if (this.isTrue(guard) && !schema && !entityChecker) {
|
|
633
666
|
// unconditionally allowed
|
|
634
667
|
return;
|
|
635
668
|
}
|
|
636
|
-
|
|
669
|
+
let select = schema
|
|
637
670
|
? // need to validate against schema, need to fetch all fields
|
|
638
671
|
undefined
|
|
639
672
|
: // only fetch id fields
|
|
640
673
|
this.makeIdSelection(model);
|
|
674
|
+
if (entityChecker === null || entityChecker === void 0 ? void 0 : entityChecker.selector) {
|
|
675
|
+
if (!select) {
|
|
676
|
+
select = this.makeAllScalarFieldSelect(model);
|
|
677
|
+
}
|
|
678
|
+
select = Object.assign(Object.assign({}, select), entityChecker.selector);
|
|
679
|
+
}
|
|
641
680
|
let where = this.clone(uniqueFilter);
|
|
642
681
|
// query args may have be of combined-id form, need to flatten it to call findFirst
|
|
643
682
|
this.flattenGeneratedUniqueField(model, where);
|
|
@@ -651,6 +690,14 @@ class PolicyUtil extends query_utils_1.QueryUtils {
|
|
|
651
690
|
if (!result) {
|
|
652
691
|
throw this.deniedByPolicy(model, operation, `entity ${(0, utils_1.formatObject)(uniqueFilter, false)} failed policy check`, constants_1.CrudFailureReason.ACCESS_POLICY_VIOLATION);
|
|
653
692
|
}
|
|
693
|
+
if (entityChecker) {
|
|
694
|
+
if (this.logger.enabled('info')) {
|
|
695
|
+
this.logger.info(`[policy] running entity checker on ${model} for ${operation}`);
|
|
696
|
+
}
|
|
697
|
+
if (!entityChecker.func(result, { user: this.user, preValue })) {
|
|
698
|
+
throw this.deniedByPolicy(model, operation, `entity ${(0, utils_1.formatObject)(uniqueFilter, false)} failed policy check`, constants_1.CrudFailureReason.ACCESS_POLICY_VIOLATION);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
654
701
|
if (schema) {
|
|
655
702
|
// TODO: push down schema check to the database
|
|
656
703
|
const parseResult = schema.safeParse(result);
|
|
@@ -664,6 +711,21 @@ class PolicyUtil extends query_utils_1.QueryUtils {
|
|
|
664
711
|
}
|
|
665
712
|
});
|
|
666
713
|
}
|
|
714
|
+
getEntityChecker(model, operation, field) {
|
|
715
|
+
var _a, _b, _c;
|
|
716
|
+
const def = this.getModelPolicyDef(model);
|
|
717
|
+
if (field) {
|
|
718
|
+
return (_c = (_b = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a[operation]) === null || _b === void 0 ? void 0 : _b[field]) === null || _c === void 0 ? void 0 : _c.entityChecker;
|
|
719
|
+
}
|
|
720
|
+
else {
|
|
721
|
+
return def.modelLevel[operation].entityChecker;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
getUpdateOverrideEntityCheckerForField(model, field) {
|
|
725
|
+
var _a, _b, _c;
|
|
726
|
+
const def = this.getModelPolicyDef(model);
|
|
727
|
+
return (_c = (_b = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a.update) === null || _b === void 0 ? void 0 : _b[field]) === null || _c === void 0 ? void 0 : _c.overrideEntityChecker;
|
|
728
|
+
}
|
|
667
729
|
getFieldReadGuards(db, model, args) {
|
|
668
730
|
const allFields = Object.values((0, cross_1.getFields)(this.modelMeta, model));
|
|
669
731
|
// all scalar fields by default
|
|
@@ -687,16 +749,17 @@ class PolicyUtil extends query_utils_1.QueryUtils {
|
|
|
687
749
|
var _a;
|
|
688
750
|
const allFieldGuards = [];
|
|
689
751
|
const allOverrideFieldGuards = [];
|
|
690
|
-
|
|
691
|
-
|
|
752
|
+
let entityChecker;
|
|
753
|
+
for (const [field, value] of Object.entries((_a = args.data) !== null && _a !== void 0 ? _a : args)) {
|
|
754
|
+
if (typeof value === 'undefined') {
|
|
692
755
|
continue;
|
|
693
756
|
}
|
|
694
|
-
const
|
|
695
|
-
if (
|
|
757
|
+
const fieldInfo = (0, cross_1.resolveField)(this.modelMeta, model, field);
|
|
758
|
+
if (fieldInfo === null || fieldInfo === void 0 ? void 0 : fieldInfo.isDataModel) {
|
|
696
759
|
// relation field update should be treated as foreign key update,
|
|
697
760
|
// fetch and merge all foreign key guards
|
|
698
|
-
if (
|
|
699
|
-
const foreignKeys = Object.values(
|
|
761
|
+
if (fieldInfo.isRelationOwner && fieldInfo.foreignKeyMapping) {
|
|
762
|
+
const foreignKeys = Object.values(fieldInfo.foreignKeyMapping);
|
|
700
763
|
for (const fk of foreignKeys) {
|
|
701
764
|
const fieldGuard = this.getFieldUpdateAuthGuard(db, model, fk);
|
|
702
765
|
if (this.isFalse(fieldGuard)) {
|
|
@@ -711,16 +774,22 @@ class PolicyUtil extends query_utils_1.QueryUtils {
|
|
|
711
774
|
}
|
|
712
775
|
}
|
|
713
776
|
else {
|
|
714
|
-
const fieldGuard = this.getFieldUpdateAuthGuard(db, model,
|
|
777
|
+
const fieldGuard = this.getFieldUpdateAuthGuard(db, model, field);
|
|
715
778
|
if (this.isFalse(fieldGuard)) {
|
|
716
|
-
return { guard: fieldGuard, rejectedByField:
|
|
779
|
+
return { guard: fieldGuard, rejectedByField: field };
|
|
717
780
|
}
|
|
718
781
|
// add field guard
|
|
719
782
|
allFieldGuards.push(fieldGuard);
|
|
720
783
|
// add field override guard
|
|
721
|
-
const overrideFieldGuard = this.getFieldOverrideUpdateAuthGuard(db, model,
|
|
784
|
+
const overrideFieldGuard = this.getFieldOverrideUpdateAuthGuard(db, model, field);
|
|
722
785
|
allOverrideFieldGuards.push(overrideFieldGuard);
|
|
723
786
|
}
|
|
787
|
+
// merge regular and override entity checkers with OR
|
|
788
|
+
let checker = this.getEntityChecker(model, 'update', field);
|
|
789
|
+
const overrideChecker = this.getUpdateOverrideEntityCheckerForField(model, field);
|
|
790
|
+
checker = this.combineEntityChecker(checker, overrideChecker, 'or');
|
|
791
|
+
// accumulate entity checker across fields
|
|
792
|
+
entityChecker = this.combineEntityChecker(entityChecker, checker, 'and');
|
|
724
793
|
}
|
|
725
794
|
const allFieldsCombined = this.and(...allFieldGuards);
|
|
726
795
|
const allOverrideFieldsCombined = allOverrideFieldGuards.length !== 0 ? this.and(...allOverrideFieldGuards) : undefined;
|
|
@@ -728,6 +797,23 @@ class PolicyUtil extends query_utils_1.QueryUtils {
|
|
|
728
797
|
guard: allFieldsCombined,
|
|
729
798
|
overrideGuard: allOverrideFieldsCombined,
|
|
730
799
|
rejectedByField: undefined,
|
|
800
|
+
entityChecker,
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
combineEntityChecker(left, right, combiner) {
|
|
804
|
+
var _a, _b;
|
|
805
|
+
if (!left) {
|
|
806
|
+
return right;
|
|
807
|
+
}
|
|
808
|
+
if (!right) {
|
|
809
|
+
return left;
|
|
810
|
+
}
|
|
811
|
+
const func = combiner === 'and'
|
|
812
|
+
? (entity, context) => left.func(entity, context) && right.func(entity, context)
|
|
813
|
+
: (entity, context) => left.func(entity, context) || right.func(entity, context);
|
|
814
|
+
return {
|
|
815
|
+
func,
|
|
816
|
+
selector: (0, deepmerge_1.default)((_a = left.selector) !== null && _a !== void 0 ? _a : {}, (_b = right.selector) !== null && _b !== void 0 ? _b : {}),
|
|
731
817
|
};
|
|
732
818
|
}
|
|
733
819
|
/**
|
|
@@ -788,8 +874,8 @@ class PolicyUtil extends query_utils_1.QueryUtils {
|
|
|
788
874
|
});
|
|
789
875
|
}
|
|
790
876
|
/**
|
|
791
|
-
* Injects field selection needed for checking field-level read policy
|
|
792
|
-
*
|
|
877
|
+
* Injects field selection needed for checking field-level read policy check and evaluating
|
|
878
|
+
* entity checker into query args.
|
|
793
879
|
*/
|
|
794
880
|
injectReadCheckSelect(model, args) {
|
|
795
881
|
// we need to recurse into relation fields before injecting the current level, because
|
|
@@ -805,11 +891,15 @@ class PolicyUtil extends query_utils_1.QueryUtils {
|
|
|
805
891
|
}
|
|
806
892
|
if (this.hasFieldLevelPolicy(model)) {
|
|
807
893
|
// recursively inject selection for fields needed for field-level read checks
|
|
808
|
-
const readFieldSelect = this.
|
|
894
|
+
const readFieldSelect = this.getFieldReadCheckSelector(model);
|
|
809
895
|
if (readFieldSelect) {
|
|
810
896
|
this.doInjectReadCheckSelect(model, args, { select: readFieldSelect });
|
|
811
897
|
}
|
|
812
898
|
}
|
|
899
|
+
const entityChecker = this.getEntityChecker(model, 'read');
|
|
900
|
+
if (entityChecker === null || entityChecker === void 0 ? void 0 : entityChecker.selector) {
|
|
901
|
+
this.doInjectReadCheckSelect(model, args, { select: entityChecker.selector });
|
|
902
|
+
}
|
|
813
903
|
}
|
|
814
904
|
doInjectReadCheckSelect(model, args, input) {
|
|
815
905
|
// omit should be ignored to avoid interfering with field selection
|
|
@@ -899,30 +989,39 @@ class PolicyUtil extends query_utils_1.QueryUtils {
|
|
|
899
989
|
* Gets field selection for fetching pre-update entity values for the given model.
|
|
900
990
|
*/
|
|
901
991
|
getPreValueSelect(model) {
|
|
902
|
-
const
|
|
903
|
-
|
|
904
|
-
throw this.unknownError(`unable to load policy guard for ${model}`);
|
|
905
|
-
}
|
|
906
|
-
return guard[constants_1.PRE_UPDATE_VALUE_SELECTOR];
|
|
992
|
+
const def = this.getModelPolicyDef(model);
|
|
993
|
+
return def.modelLevel.postUpdate.preUpdateSelector;
|
|
907
994
|
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
995
|
+
// get a merged selector object for all field-level read policies
|
|
996
|
+
getFieldReadCheckSelector(model) {
|
|
997
|
+
var _a, _b, _c;
|
|
998
|
+
const def = this.getModelPolicyDef(model);
|
|
999
|
+
let result = {};
|
|
1000
|
+
const fieldLevel = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a.read;
|
|
1001
|
+
if (fieldLevel) {
|
|
1002
|
+
for (const def of Object.values(fieldLevel)) {
|
|
1003
|
+
if ((_b = def.entityChecker) === null || _b === void 0 ? void 0 : _b.selector) {
|
|
1004
|
+
result = (0, deepmerge_1.default)(result, def.entityChecker.selector);
|
|
1005
|
+
}
|
|
1006
|
+
if ((_c = def.overrideEntityChecker) === null || _c === void 0 ? void 0 : _c.selector) {
|
|
1007
|
+
result = (0, deepmerge_1.default)(result, def.overrideEntityChecker.selector);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
912
1010
|
}
|
|
913
|
-
return
|
|
1011
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
914
1012
|
}
|
|
915
1013
|
checkReadField(model, field, entity) {
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
const
|
|
921
|
-
|
|
1014
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1015
|
+
const def = this.getModelPolicyDef(model);
|
|
1016
|
+
// combine regular and override field-level entity checkers with OR
|
|
1017
|
+
const checker = (_c = (_b = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a.read) === null || _b === void 0 ? void 0 : _b[field]) === null || _c === void 0 ? void 0 : _c.entityChecker;
|
|
1018
|
+
const overrideChecker = (_f = (_e = (_d = def.fieldLevel) === null || _d === void 0 ? void 0 : _d.read) === null || _e === void 0 ? void 0 : _e[field]) === null || _f === void 0 ? void 0 : _f.overrideEntityChecker;
|
|
1019
|
+
const combinedChecker = this.combineEntityChecker(checker, overrideChecker, 'or');
|
|
1020
|
+
if (combinedChecker === undefined) {
|
|
922
1021
|
return true;
|
|
923
1022
|
}
|
|
924
1023
|
else {
|
|
925
|
-
return func(entity, { user: this.user });
|
|
1024
|
+
return combinedChecker.func(entity, { user: this.user });
|
|
926
1025
|
}
|
|
927
1026
|
}
|
|
928
1027
|
hasFieldValidation(model) {
|
|
@@ -930,11 +1029,9 @@ class PolicyUtil extends query_utils_1.QueryUtils {
|
|
|
930
1029
|
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;
|
|
931
1030
|
}
|
|
932
1031
|
hasFieldLevelPolicy(model) {
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
}
|
|
937
|
-
return !!guard[constants_1.HAS_FIELD_LEVEL_POLICY_FLAG];
|
|
1032
|
+
var _a, _b;
|
|
1033
|
+
const def = this.getModelPolicyDef(model);
|
|
1034
|
+
return Object.keys((_b = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a.read) !== null && _b !== void 0 ? _b : {}).length > 0;
|
|
938
1035
|
}
|
|
939
1036
|
/**
|
|
940
1037
|
* Gets Zod schema for the given model and access kind.
|
|
@@ -956,16 +1053,44 @@ class PolicyUtil extends query_utils_1.QueryUtils {
|
|
|
956
1053
|
// preserve the original data as it may be needed for checking field-level readability,
|
|
957
1054
|
// while the "data" will be manipulated during traversal (deleting unreadable fields)
|
|
958
1055
|
const origData = this.clone(data);
|
|
959
|
-
this.doPostProcessForRead(data, model, origData, queryArgs, this.hasFieldLevelPolicy(model));
|
|
1056
|
+
return this.doPostProcessForRead(data, model, origData, queryArgs, this.hasFieldLevelPolicy(model));
|
|
960
1057
|
}
|
|
961
1058
|
doPostProcessForRead(data, model, fullData, queryArgs, hasFieldLevelPolicy, path = '') {
|
|
962
1059
|
var _a, _b, _c;
|
|
963
1060
|
if (data === null || data === undefined) {
|
|
964
|
-
return;
|
|
1061
|
+
return data;
|
|
1062
|
+
}
|
|
1063
|
+
let filteredData = data;
|
|
1064
|
+
let filteredFullData = fullData;
|
|
1065
|
+
const entityChecker = this.getEntityChecker(model, 'read');
|
|
1066
|
+
if (entityChecker) {
|
|
1067
|
+
if (Array.isArray(data)) {
|
|
1068
|
+
filteredData = [];
|
|
1069
|
+
filteredFullData = [];
|
|
1070
|
+
for (const [entityData, entityFullData] of (0, cross_1.zip)(data, fullData)) {
|
|
1071
|
+
if (!entityChecker.func(entityData, { user: this.user })) {
|
|
1072
|
+
if (this.shouldLogQuery) {
|
|
1073
|
+
this.logger.info(`[policy] dropping ${model} entity${path ? ' at ' + path : ''} due to entity checker`);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
else {
|
|
1077
|
+
filteredData.push(entityData);
|
|
1078
|
+
filteredFullData.push(entityFullData);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
else {
|
|
1083
|
+
if (!entityChecker.func(data, { user: this.user })) {
|
|
1084
|
+
if (this.shouldLogQuery) {
|
|
1085
|
+
this.logger.info(`[policy] dropping ${model} entity${path ? ' at ' + path : ''} due to entity checker`);
|
|
1086
|
+
}
|
|
1087
|
+
return null;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
965
1090
|
}
|
|
966
|
-
for (const [entityData, entityFullData] of (0, cross_1.zip)(
|
|
1091
|
+
for (const [entityData, entityFullData] of (0, cross_1.zip)(filteredData, filteredFullData)) {
|
|
967
1092
|
if (typeof entityData !== 'object' || !entityData) {
|
|
968
|
-
|
|
1093
|
+
continue;
|
|
969
1094
|
}
|
|
970
1095
|
for (const [field, fieldData] of Object.entries(entityData)) {
|
|
971
1096
|
if (fieldData === undefined) {
|
|
@@ -1015,10 +1140,17 @@ class PolicyUtil extends query_utils_1.QueryUtils {
|
|
|
1015
1140
|
if (fieldInfo.isDataModel) {
|
|
1016
1141
|
// recurse into nested fields
|
|
1017
1142
|
const nextArgs = (_c = ((_b = queryArgs === null || queryArgs === void 0 ? void 0 : queryArgs.select) !== null && _b !== void 0 ? _b : queryArgs === null || queryArgs === void 0 ? void 0 : queryArgs.include)) === null || _c === void 0 ? void 0 : _c[field];
|
|
1018
|
-
this.doPostProcessForRead(fieldData, fieldInfo.type, entityFullData[field], nextArgs, this.hasFieldLevelPolicy(fieldInfo.type), path ? path + '.' + field : field);
|
|
1143
|
+
const nestedResult = this.doPostProcessForRead(fieldData, fieldInfo.type, entityFullData[field], nextArgs, this.hasFieldLevelPolicy(fieldInfo.type), path ? path + '.' + field : field);
|
|
1144
|
+
if (nestedResult === undefined) {
|
|
1145
|
+
delete entityData[field];
|
|
1146
|
+
}
|
|
1147
|
+
else {
|
|
1148
|
+
entityData[field] = nestedResult;
|
|
1149
|
+
}
|
|
1019
1150
|
}
|
|
1020
1151
|
}
|
|
1021
1152
|
}
|
|
1153
|
+
return filteredData;
|
|
1022
1154
|
}
|
|
1023
1155
|
/**
|
|
1024
1156
|
* Clones an object and makes sure it's not empty.
|
|
@@ -1080,13 +1212,6 @@ class PolicyUtil extends query_utils_1.QueryUtils {
|
|
|
1080
1212
|
where.AND = [extra];
|
|
1081
1213
|
}
|
|
1082
1214
|
}
|
|
1083
|
-
requireGuard(model) {
|
|
1084
|
-
const guard = this.getModelAuthGuard(model);
|
|
1085
|
-
if (!guard) {
|
|
1086
|
-
throw this.unknownError(`unable to load policy guard for ${model}`);
|
|
1087
|
-
}
|
|
1088
|
-
return guard;
|
|
1089
|
-
}
|
|
1090
1215
|
/**
|
|
1091
1216
|
* Given an entity data, returns an object only containing id fields.
|
|
1092
1217
|
*/
|