@@ -275,6 +275,26 @@ export class PolicyUtil extends QueryUtils {
275
275
return this . reduce ( r ) ;
276
276
}
277
277
278
+ /**
279
+ * Get field-level read auth guard
280
+ */
281
+ getFieldReadAuthGuard ( db : CrudContract , model : string , field : string ) {
282
+ const def = this . getModelPolicyDef ( model ) ;
283
+ const guard = def . fieldLevel ?. read ?. [ field ] ?. guard ;
284
+
285
+ if ( guard === undefined ) {
286
+ // field access is allowed by default
287
+ return this . makeTrue ( ) ;
288
+ }
289
+
290
+ if ( typeof guard === 'boolean' ) {
291
+ return this . reduce ( guard ) ;
292
+ }
293
+
294
+ const r = guard ( { user : this . user } , db ) ;
295
+ return this . reduce ( r ) ;
296
+ }
297
+
278
298
/**
279
299
* Get field-level read auth guard that overrides the model-level
280
300
*/
@@ -419,98 +439,118 @@ export class PolicyUtil extends QueryUtils {
419
439
return false ;
420
440
}
421
441
442
+ let mergedGuard = guard ;
422
443
if ( args . where ) {
423
444
// inject into relation fields:
424
445
// to-many: some/none/every
425
446
// to-one: direct-conditions/is/isNot
426
- this . injectGuardForRelationFields ( db , model , args . where , operation ) ;
447
+ mergedGuard = this . injectReadGuardForRelationFields ( db , model , args . where , guard ) ;
427
448
}
428
449
429
- args . where = this . and ( args . where , guard ) ;
450
+ args . where = this . and ( args . where , mergedGuard ) ;
430
451
return true ;
431
452
}
432
453
433
- private injectGuardForRelationFields (
434
- db : CrudContract ,
435
- model : string ,
436
- payload : any ,
437
- operation : PolicyOperationKind
438
- ) {
454
+ // Injects guard for relation fields nested in `payload`. The `modelGuard` parameter represents the model-level guard for `model`.
455
+ // The function returns a modified copy of `modelGuard` with field-level policies combined.
456
+ private injectReadGuardForRelationFields ( db : CrudContract , model : string , payload : any , modelGuard : any ) {
457
+ if ( ! payload || typeof payload !== 'object' || Object . keys ( payload ) . length === 0 ) {
458
+ return modelGuard ;
459
+ }
460
+
461
+ const allFieldGuards : object [ ] = [ ] ;
462
+ const allFieldOverrideGuards : object [ ] = [ ] ;
463
+
439
464
for ( const [ field , subPayload ] of Object . entries < any > ( payload ) ) {
440
465
if ( ! subPayload ) {
441
466
continue ;
442
467
}
443
468
444
- const fieldInfo = resolveField ( this . modelMeta , model , field ) ;
445
- if ( ! fieldInfo || ! fieldInfo . isDataModel ) {
446
- continue ;
447
- }
469
+ allFieldGuards . push ( this . getFieldReadAuthGuard ( db , model , field ) ) ;
470
+ allFieldOverrideGuards . push ( this . getFieldOverrideReadAuthGuard ( db , model , field ) ) ;
448
471
449
- if ( fieldInfo . isArray ) {
450
- this . injectGuardForToManyField ( db , fieldInfo , subPayload , operation ) ;
451
- } else {
452
- this . injectGuardForToOneField ( db , fieldInfo , subPayload , operation ) ;
472
+ const fieldInfo = resolveField ( this . modelMeta , model , field ) ;
473
+ if ( fieldInfo ?. isDataModel ) {
474
+ if ( fieldInfo . isArray ) {
475
+ this . injectReadGuardForToManyField ( db , fieldInfo , subPayload ) ;
476
+ } else {
477
+ this . injectReadGuardForToOneField ( db , fieldInfo , subPayload ) ;
478
+ }
453
479
}
454
480
}
481
+
482
+ // all existing field-level guards must be true
483
+ const mergedGuard : object = this . and ( ...allFieldGuards ) ;
484
+
485
+ // all existing field-level override guards must be true for override to take effect; override is disabled by default
486
+ const mergedOverrideGuard : object =
487
+ allFieldOverrideGuards . length === 0 ? this . makeFalse ( ) : this . and ( ...allFieldOverrideGuards ) ;
488
+
489
+ // (original-guard && field-level-guard) || field-level-override-guard
490
+ const updatedGuard = this . or ( this . and ( modelGuard , mergedGuard ) , mergedOverrideGuard ) ;
491
+ return updatedGuard ;
455
492
}
456
493
457
- private injectGuardForToManyField (
494
+ private injectReadGuardForToManyField (
458
495
db : CrudContract ,
459
496
fieldInfo : FieldInfo ,
460
- payload : { some ?: any ; every ?: any ; none ?: any } ,
461
- operation : PolicyOperationKind
497
+ payload : { some ?: any ; every ?: any ; none ?: any }
462
498
) {
463
- const guard = this . getAuthGuard ( db , fieldInfo . type , operation ) ;
499
+ const guard = this . getAuthGuard ( db , fieldInfo . type , 'read' ) ;
464
500
if ( payload . some ) {
465
- this . injectGuardForRelationFields ( db , fieldInfo . type , payload . some , operation ) ;
501
+ const mergedGuard = this . injectReadGuardForRelationFields ( db , fieldInfo . type , payload . some , guard ) ;
466
502
// turn "some" into: { some: { AND: [guard, payload.some] } }
467
- payload . some = this . and ( payload . some , guard ) ;
503
+ payload . some = this . and ( payload . some , mergedGuard ) ;
468
504
}
469
505
if ( payload . none ) {
470
- this . injectGuardForRelationFields ( db , fieldInfo . type , payload . none , operation ) ;
506
+ const mergedGuard = this . injectReadGuardForRelationFields ( db , fieldInfo . type , payload . none , guard ) ;
471
507
// turn none into: { none: { AND: [guard, payload.none] } }
472
- payload . none = this . and ( payload . none , guard ) ;
508
+ payload . none = this . and ( payload . none , mergedGuard ) ;
473
509
}
474
510
if (
475
511
payload . every &&
476
512
typeof payload . every === 'object' &&
477
513
// ignore empty every clause
478
514
Object . keys ( payload . every ) . length > 0
479
515
) {
480
- this . injectGuardForRelationFields ( db , fieldInfo . type , payload . every , operation ) ;
516
+ const mergedGuard = this . injectReadGuardForRelationFields ( db , fieldInfo . type , payload . every , guard ) ;
481
517
482
518
// turn "every" into: { none: { AND: [guard, { NOT: payload.every }] } }
483
519
if ( ! payload . none ) {
484
520
payload . none = { } ;
485
521
}
486
- payload . none = this . and ( payload . none , guard , this . not ( payload . every ) ) ;
522
+ payload . none = this . and ( payload . none , mergedGuard , this . not ( payload . every ) ) ;
487
523
delete payload . every ;
488
524
}
489
525
}
490
526
491
- private injectGuardForToOneField (
527
+ private injectReadGuardForToOneField (
492
528
db : CrudContract ,
493
529
fieldInfo : FieldInfo ,
494
- payload : { is ?: any ; isNot ?: any } & Record < string , any > ,
495
- operation : PolicyOperationKind
530
+ payload : { is ?: any ; isNot ?: any } & Record < string , any >
496
531
) {
497
- const guard = this . getAuthGuard ( db , fieldInfo . type , operation ) ;
532
+ const guard = this . getAuthGuard ( db , fieldInfo . type , 'read' ) ;
498
533
499
534
// is|isNot and flat fields conditions are mutually exclusive
500
535
501
- if ( payload . is || payload . isNot ) {
536
+ // is and isNot can be null value
537
+
538
+ if ( payload . is !== undefined || payload . isNot !== undefined ) {
502
539
if ( payload . is ) {
503
- this . injectGuardForRelationFields ( db , fieldInfo . type , payload . is , operation ) ;
540
+ const mergedGuard = this . injectReadGuardForRelationFields ( db , fieldInfo . type , payload . is , guard ) ;
541
+ // merge guard with existing "is": { is: { AND: [originalIs, guard] } }
542
+ payload . is = this . and ( payload . is , mergedGuard ) ;
504
543
}
544
+
505
545
if ( payload . isNot ) {
506
- this . injectGuardForRelationFields ( db , fieldInfo . type , payload . isNot , operation ) ;
546
+ const mergedGuard = this . injectReadGuardForRelationFields ( db , fieldInfo . type , payload . isNot , guard ) ;
547
+ // merge guard with existing "isNot": { isNot: { AND: [originalIsNot, guard] } }
548
+ payload . isNot = this . and ( payload . isNot , mergedGuard ) ;
507
549
}
508
- // merge guard with existing "is": { is: [originalIs, guard] }
509
- payload . is = this . and ( payload . is , guard ) ;
510
550
} else {
511
- this . injectGuardForRelationFields ( db , fieldInfo . type , payload , operation ) ;
551
+ const mergedGuard = this . injectReadGuardForRelationFields ( db , fieldInfo . type , payload , guard ) ;
512
552
// turn direct conditions into: { is: { AND: [ originalConditions, guard ] } }
513
- const combined = this . and ( deepcopy ( payload ) , guard ) ;
553
+ const combined = this . and ( deepcopy ( payload ) , mergedGuard ) ;
514
554
Object . keys ( payload ) . forEach ( ( key ) => delete payload [ key ] ) ;
515
555
payload . is = combined ;
516
556
}
@@ -530,7 +570,7 @@ export class PolicyUtil extends QueryUtils {
530
570
// inject into relation fields:
531
571
// to-many: some/none/every
532
572
// to-one: direct-conditions/is/isNot
533
- this . injectGuardForRelationFields ( db , model , args . where , 'read' ) ;
573
+ this . injectReadGuardForRelationFields ( db , model , args . where , { } ) ;
534
574
}
535
575
536
576
if ( injected . where && Object . keys ( injected . where ) . length > 0 && ! this . isTrue ( injected . where ) ) {
0 commit comments