@zenstackhq/runtime 2.1.2 → 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.
@@ -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.FULLY_OPEN_AUTH_GUARD = {
38
- create: true,
39
- read: true,
40
- update: true,
41
- delete: true,
42
- postUpdate: true,
43
- create_input: true,
44
- update_input: true,
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
- getModelAuthGuard(model) {
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.FULLY_OPEN_AUTH_GUARD;
222
+ return this.FULL_OPEN_MODEL_POLICY;
222
223
  }
223
- else {
224
- return this.policy.guard[(0, lower_case_first_1.lowerCaseFirst)(model)];
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.getModelAuthGuard(model);
235
- if (!guard) {
236
- throw this.unknownError(`unable to load policy guard for ${model}`);
242
+ const guard = this.getModelGuardForOperation(model, operation);
243
+ // constant guard
244
+ if (typeof guard === 'boolean') {
245
+ return this.reduce(guard);
237
246
  }
238
- const provider = guard[operation];
239
- if (typeof provider === 'boolean') {
240
- return this.reduce(provider);
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 (!provider) {
243
- throw this.unknownError(`unable to load authorization guard for ${model}`);
262
+ if (typeof guard === 'boolean') {
263
+ return this.reduce(guard);
244
264
  }
245
- const r = provider({ user: this.user, preValue }, db);
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
- const guard = this.requireGuard(model);
253
- const provider = guard[`${constants_1.FIELD_LEVEL_OVERRIDE_READ_GUARD_PREFIX}${field}`];
254
- if (provider === undefined) {
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 provider === 'boolean') {
259
- return this.reduce(provider);
279
+ if (typeof guard === 'boolean') {
280
+ return this.reduce(guard);
260
281
  }
261
- const r = provider({ user: this.user }, db);
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
- const guard = this.requireGuard(model);
269
- const provider = guard[`${constants_1.FIELD_LEVEL_UPDATE_GUARD_PREFIX}${field}`];
270
- if (provider === undefined) {
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 provider === 'boolean') {
275
- return this.reduce(provider);
296
+ if (typeof guard === 'boolean') {
297
+ return this.reduce(guard);
276
298
  }
277
- const r = provider({ user: this.user }, db);
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
- const guard = this.requireGuard(model);
285
- const provider = guard[`${constants_1.FIELD_LEVEL_OVERRIDE_UPDATE_GUARD_PREFIX}${field}`];
286
- if (provider === undefined) {
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 provider === 'boolean') {
291
- return this.reduce(provider);
313
+ if (typeof guard === 'boolean') {
314
+ return this.reduce(guard);
292
315
  }
293
- const r = provider({ user: this.user }, db);
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.getModelAuthGuard(model);
301
- if (!guard) {
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
- const guard = this.requireGuard(model);
312
- switch (operation) {
313
- case 'read':
314
- return Object.keys(guard).some((k) => k.startsWith(constants_1.FIELD_LEVEL_OVERRIDE_READ_GUARD_PREFIX));
315
- case 'update':
316
- return Object.keys(guard).some((k) => k.startsWith(constants_1.FIELD_LEVEL_OVERRIDE_UPDATE_GUARD_PREFIX));
317
- default:
318
- return false;
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 guard = this.getModelAuthGuard(model);
328
- if (!guard) {
348
+ const def = this.getModelPolicyDef(model);
349
+ const guard = def.modelLevel[operation].inputChecker;
350
+ if (guard === undefined) {
329
351
  return undefined;
330
352
  }
331
- const provider = guard[`${operation}_input`];
332
- if (typeof provider === 'boolean') {
333
- return provider;
353
+ if (typeof guard === 'boolean') {
354
+ return guard;
334
355
  }
335
- if (!provider) {
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.injectGuardForRelationFields(db, model, args.where, operation);
398
+ mergedGuard = this.injectReadGuardForRelationFields(db, model, args.where, guard);
380
399
  }
381
- args.where = this.and(args.where, guard);
400
+ args.where = this.and(args.where, mergedGuard);
382
401
  return true;
383
402
  }
384
- injectGuardForRelationFields(db, model, payload, operation) {
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 (!fieldInfo || !fieldInfo.isDataModel) {
391
- continue;
392
- }
393
- if (fieldInfo.isArray) {
394
- this.injectGuardForToManyField(db, fieldInfo, subPayload, operation);
395
- }
396
- else {
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
- injectGuardForToManyField(db, fieldInfo, payload, operation) {
402
- const guard = this.getAuthGuard(db, fieldInfo.type, operation);
435
+ injectReadGuardForToManyField(db, fieldInfo, payload) {
436
+ const guard = this.getAuthGuard(db, fieldInfo.type, 'read');
403
437
  if (payload.some) {
404
- this.injectGuardForRelationFields(db, fieldInfo.type, payload.some, operation);
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, guard);
440
+ payload.some = this.and(payload.some, mergedGuard);
407
441
  }
408
442
  if (payload.none) {
409
- this.injectGuardForRelationFields(db, fieldInfo.type, payload.none, operation);
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, guard);
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.injectGuardForRelationFields(db, fieldInfo.type, payload.every, operation);
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, guard, this.not(payload.every));
456
+ payload.none = this.and(payload.none, mergedGuard, this.not(payload.every));
423
457
  delete payload.every;
424
458
  }
425
459
  }
426
- injectGuardForToOneField(db, fieldInfo, payload, operation) {
427
- const guard = this.getAuthGuard(db, fieldInfo.type, operation);
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
- if (payload.is || payload.isNot) {
463
+ // is and isNot can be null value
464
+ if (payload.is !== undefined || payload.isNot !== undefined) {
430
465
  if (payload.is) {
431
- this.injectGuardForRelationFields(db, fieldInfo.type, payload.is, operation);
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.injectGuardForRelationFields(db, fieldInfo.type, payload.isNot, operation);
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.injectGuardForRelationFields(db, fieldInfo.type, payload, operation);
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), guard);
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.injectGuardForRelationFields(db, model, args.where, 'read');
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
- const checker = this.getModelChecker(model);
491
- const provider = checker[operation];
492
- if (typeof provider === 'boolean') {
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
- if (typeof provider !== 'function') {
496
- throw this.unknownError(`invalid ${operation} checker function for ${model}`);
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
- // call checker function
499
- return provider({ user: this.user });
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
- else {
508
- const result = (_a = this.options.policy.checker) === null || _a === void 0 ? void 0 : _a[(0, lower_case_first_1.lowerCaseFirst)(model)];
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
- else {
620
- if (fieldUpdateGuard.guard) {
621
- // merge field-level guard
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
- if (this.isTrue(guard) && !schema) {
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
- const select = schema
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
- for (const [k, v] of Object.entries((_a = args.data) !== null && _a !== void 0 ? _a : args)) {
691
- if (typeof v === 'undefined') {
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 field = (0, cross_1.resolveField)(this.modelMeta, model, k);
695
- if (field === null || field === void 0 ? void 0 : field.isDataModel) {
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 (field.isRelationOwner && field.foreignKeyMapping) {
699
- const foreignKeys = Object.values(field.foreignKeyMapping);
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, k);
777
+ const fieldGuard = this.getFieldUpdateAuthGuard(db, model, field);
715
778
  if (this.isFalse(fieldGuard)) {
716
- return { guard: fieldGuard, rejectedByField: k };
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, k);
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 into query args.
792
- * @returns
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.getReadFieldSelect(model);
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 guard = this.getModelAuthGuard(model);
903
- if (!guard) {
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
- getReadFieldSelect(model) {
909
- const guard = this.getModelAuthGuard(model);
910
- if (!guard) {
911
- throw this.unknownError(`unable to load policy guard for ${model}`);
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 guard[constants_1.FIELD_LEVEL_READ_CHECKER_SELECTOR];
1011
+ return Object.keys(result).length > 0 ? result : undefined;
914
1012
  }
915
1013
  checkReadField(model, field, entity) {
916
- const guard = this.getModelAuthGuard(model);
917
- if (!guard) {
918
- throw this.unknownError(`unable to load policy guard for ${model}`);
919
- }
920
- const func = guard[`${constants_1.FIELD_LEVEL_READ_CHECKER_PREFIX}${field}`];
921
- if (!func) {
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
- const guard = this.getModelAuthGuard(model);
934
- if (!guard) {
935
- throw this.unknownError(`unable to load policy guard for ${model}`);
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)(data, fullData)) {
1091
+ for (const [entityData, entityFullData] of (0, cross_1.zip)(filteredData, filteredFullData)) {
967
1092
  if (typeof entityData !== 'object' || !entityData) {
968
- return;
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
  */