@@ -7,6 +7,8 @@ import { ZodError } from 'zod';
7
7
import { fromZodError } from 'zod-validation-error' ;
8
8
import {
9
9
CrudFailureReason ,
10
+ FIELD_LEVEL_OVERRIDE_READ_GUARD_PREFIX ,
11
+ FIELD_LEVEL_OVERRIDE_UPDATE_GUARD_PREFIX ,
10
12
FIELD_LEVEL_READ_CHECKER_PREFIX ,
11
13
FIELD_LEVEL_READ_CHECKER_SELECTOR ,
12
14
FIELD_LEVEL_UPDATE_GUARD_PREFIX ,
@@ -236,12 +238,7 @@ export class PolicyUtil {
236
238
* @returns true if operation is unconditionally allowed, false if unconditionally denied,
237
239
* otherwise returns a guard object
238
240
*/
239
- getAuthGuard (
240
- db : Record < string , DbOperations > ,
241
- model : string ,
242
- operation : PolicyOperationKind ,
243
- preValue ?: any
244
- ) : object {
241
+ getAuthGuard ( db : Record < string , DbOperations > , model : string , operation : PolicyOperationKind , preValue ?: any ) {
245
242
const guard = this . policy . guard [ lowerCaseFirst ( model ) ] ;
246
243
if ( ! guard ) {
247
244
throw this . unknownError ( `unable to load policy guard for ${ model } ` ) ;
@@ -260,23 +257,61 @@ export class PolicyUtil {
260
257
}
261
258
262
259
/**
263
- * Get field-level auth guard
260
+ * Get field-level read auth guard that overrides the model-level
264
261
*/
265
- getFieldUpdateAuthGuard ( db : Record < string , DbOperations > , model : string , field : string ) : object {
266
- const guard = this . policy . guard [ lowerCaseFirst ( model ) ] ;
267
- if ( ! guard ) {
268
- throw this . unknownError ( `unable to load policy guard for ${ model } ` ) ;
262
+ getFieldOverrideReadAuthGuard ( db : Record < string , DbOperations > , model : string , field : string ) {
263
+ const guard = this . requireGuard ( model ) ;
264
+
265
+ const provider = guard [ `${ FIELD_LEVEL_OVERRIDE_READ_GUARD_PREFIX } ${ field } ` ] ;
266
+ if ( provider === undefined ) {
267
+ // field access is denied by default in override mode
268
+ return this . makeFalse ( ) ;
269
269
}
270
270
271
- const provider = guard [ `${ FIELD_LEVEL_UPDATE_GUARD_PREFIX } ${ field } ` ] ;
272
271
if ( typeof provider === 'boolean' ) {
273
272
return this . reduce ( provider ) ;
274
273
}
275
274
276
- if ( ! provider ) {
275
+ const r = provider ( { user : this . user } , db ) ;
276
+ return this . reduce ( r ) ;
277
+ }
278
+
279
+ /**
280
+ * Get field-level update auth guard
281
+ */
282
+ getFieldUpdateAuthGuard ( db : Record < string , DbOperations > , model : string , field : string ) {
283
+ const guard = this . requireGuard ( model ) ;
284
+
285
+ const provider = guard [ `${ FIELD_LEVEL_UPDATE_GUARD_PREFIX } ${ field } ` ] ;
286
+ if ( provider === undefined ) {
277
287
// field access is allowed by default
278
288
return this . makeTrue ( ) ;
279
289
}
290
+
291
+ if ( typeof provider === 'boolean' ) {
292
+ return this . reduce ( provider ) ;
293
+ }
294
+
295
+ const r = provider ( { user : this . user } , db ) ;
296
+ return this . reduce ( r ) ;
297
+ }
298
+
299
+ /**
300
+ * Get field-level update auth guard that overrides the model-level
301
+ */
302
+ getFieldOverrideUpdateAuthGuard ( db : Record < string , DbOperations > , model : string , field : string ) {
303
+ const guard = this . requireGuard ( model ) ;
304
+
305
+ const provider = guard [ `${ FIELD_LEVEL_OVERRIDE_UPDATE_GUARD_PREFIX } ${ field } ` ] ;
306
+ if ( provider === undefined ) {
307
+ // field access is denied by default in override mode
308
+ return this . makeFalse ( ) ;
309
+ }
310
+
311
+ if ( typeof provider === 'boolean' ) {
312
+ return this . reduce ( provider ) ;
313
+ }
314
+
280
315
const r = provider ( { user : this . user } , db ) ;
281
316
return this . reduce ( r ) ;
282
317
}
@@ -322,10 +357,6 @@ export class PolicyUtil {
322
357
*/
323
358
injectAuthGuard ( db : Record < string , DbOperations > , args : any , model : string , operation : PolicyOperationKind ) {
324
359
let guard = this . getAuthGuard ( db , model , operation ) ;
325
- if ( this . isFalse ( guard ) ) {
326
- args . where = this . makeFalse ( ) ;
327
- return false ;
328
- }
329
360
330
361
if ( operation === 'update' && args ) {
331
362
// merge field-level policy guards
@@ -334,12 +365,32 @@ export class PolicyUtil {
334
365
// rejected
335
366
args . where = this . makeFalse ( ) ;
336
367
return false ;
337
- } else if ( fieldUpdateGuard . guard ) {
338
- // merge
339
- guard = this . and ( guard , fieldUpdateGuard . guard ) ;
368
+ } else {
369
+ if ( fieldUpdateGuard . guard ) {
370
+ // merge field-level guard
371
+ guard = this . and ( guard , fieldUpdateGuard . guard ) ;
372
+ }
373
+
374
+ if ( fieldUpdateGuard . overrideGuard ) {
375
+ // merge field-level override guard on the top level
376
+ guard = this . or ( guard , fieldUpdateGuard . overrideGuard ) ;
377
+ }
378
+ }
379
+ }
380
+
381
+ if ( operation === 'read' ) {
382
+ // merge field-level read override guards
383
+ const fieldReadOverrideGuard = this . getFieldReadGuards ( db , model , args ) ;
384
+ if ( fieldReadOverrideGuard ) {
385
+ guard = this . or ( guard , fieldReadOverrideGuard ) ;
340
386
}
341
387
}
342
388
389
+ if ( this . isFalse ( guard ) ) {
390
+ args . where = this . makeFalse ( ) ;
391
+ return false ;
392
+ }
393
+
343
394
if ( args . where ) {
344
395
// inject into relation fields:
345
396
// to-many: some/none/every
@@ -441,7 +492,8 @@ export class PolicyUtil {
441
492
* Injects auth guard for read operations.
442
493
*/
443
494
injectForRead ( db : Record < string , DbOperations > , model : string , args : any ) {
444
- const injected : any = { } ;
495
+ // make select and include visible to the injection
496
+ const injected : any = { select : args . select , include : args . include } ;
445
497
if ( ! this . injectAuthGuard ( db , injected , model , 'read' ) ) {
446
498
return false ;
447
499
}
@@ -701,9 +753,16 @@ export class PolicyUtil {
701
753
} "`,
702
754
CrudFailureReason . ACCESS_POLICY_VIOLATION
703
755
) ;
704
- } else if ( fieldUpdateGuard . guard ) {
705
- // merge
706
- guard = this . and ( guard , fieldUpdateGuard . guard ) ;
756
+ } else {
757
+ if ( fieldUpdateGuard . guard ) {
758
+ // merge field-level guard
759
+ guard = this . and ( guard , fieldUpdateGuard . guard ) ;
760
+ }
761
+
762
+ if ( fieldUpdateGuard . overrideGuard ) {
763
+ // merge field-level override guard
764
+ guard = this . or ( guard , fieldUpdateGuard . overrideGuard ) ;
765
+ }
707
766
}
708
767
}
709
768
@@ -761,8 +820,33 @@ export class PolicyUtil {
761
820
}
762
821
}
763
822
823
+ private getFieldReadGuards ( db : Record < string , DbOperations > , model : string , args : { select ?: any ; include ?: any } ) {
824
+ const allFields = Object . values ( getFields ( this . modelMeta , model ) ) ;
825
+
826
+ // all scalar fields by default
827
+ let fields = allFields . filter ( ( f ) => ! f . isDataModel ) ;
828
+
829
+ if ( args . select ) {
830
+ // explicitly selected fields
831
+ fields = allFields . filter ( ( f ) => args . select ?. [ f . name ] === true ) ;
832
+ } else if ( args . include ) {
833
+ // included relations
834
+ fields . push ( ...allFields . filter ( ( f ) => ! fields . includes ( f ) && args . include [ f . name ] ) ) ;
835
+ }
836
+
837
+ if ( fields . length === 0 ) {
838
+ // this can happen if only selecting pseudo fields like "_count"
839
+ return undefined ;
840
+ }
841
+
842
+ const allFieldGuards = fields . map ( ( field ) => this . getFieldOverrideReadAuthGuard ( db , model , field . name ) ) ;
843
+ return this . and ( ...allFieldGuards ) ;
844
+ }
845
+
764
846
private getFieldUpdateGuards ( db : Record < string , DbOperations > , model : string , args : any ) {
765
847
const allFieldGuards = [ ] ;
848
+ const allOverrideFieldGuards = [ ] ;
849
+
766
850
for ( const [ k , v ] of Object . entries < any > ( args . data ?? args ) ) {
767
851
if ( typeof v === 'undefined' ) {
768
852
continue ;
@@ -778,20 +862,41 @@ export class PolicyUtil {
778
862
for ( const fk of foreignKeys ) {
779
863
const fieldGuard = this . getFieldUpdateAuthGuard ( db , model , fk ) ;
780
864
if ( this . isFalse ( fieldGuard ) ) {
781
- return { guard : allFieldGuards , rejectedByField : fk } ;
865
+ return { guard : fieldGuard , rejectedByField : fk } ;
782
866
}
867
+
868
+ // add field guard
783
869
allFieldGuards . push ( fieldGuard ) ;
870
+
871
+ // add field override guard
872
+ const overrideFieldGuard = this . getFieldOverrideUpdateAuthGuard ( db , model , fk ) ;
873
+ allOverrideFieldGuards . push ( overrideFieldGuard ) ;
784
874
}
785
875
}
786
876
} else {
787
877
const fieldGuard = this . getFieldUpdateAuthGuard ( db , model , k ) ;
788
878
if ( this . isFalse ( fieldGuard ) ) {
789
- return { guard : allFieldGuards , rejectedByField : k } ;
879
+ return { guard : fieldGuard , rejectedByField : k } ;
790
880
}
881
+
882
+ // add field guard
791
883
allFieldGuards . push ( fieldGuard ) ;
884
+
885
+ // add field override guard
886
+ const overrideFieldGuard = this . getFieldOverrideUpdateAuthGuard ( db , model , k ) ;
887
+ allOverrideFieldGuards . push ( overrideFieldGuard ) ;
792
888
}
793
889
}
794
- return { guard : this . and ( ...allFieldGuards ) , rejectedByField : undefined } ;
890
+
891
+ const allFieldsCombined = this . and ( ...allFieldGuards ) ;
892
+ const allOverrideFieldsCombined =
893
+ allOverrideFieldGuards . length !== 0 ? this . and ( ...allOverrideFieldGuards ) : undefined ;
894
+
895
+ return {
896
+ guard : allFieldsCombined ,
897
+ overrideGuard : allOverrideFieldsCombined ,
898
+ rejectedByField : undefined ,
899
+ } ;
795
900
}
796
901
797
902
/**
@@ -841,7 +946,13 @@ export class PolicyUtil {
841
946
) : Promise < { result : unknown ; error ?: Error } > {
842
947
uniqueFilter = this . clone ( uniqueFilter ) ;
843
948
this . flattenGeneratedUniqueField ( model , uniqueFilter ) ;
844
- const readArgs = { select : selectInclude . select , include : selectInclude . include , where : uniqueFilter } ;
949
+
950
+ // make sure only select and include are picked
951
+ const selectIncludeClean = this . pick ( selectInclude , 'select' , 'include' ) ;
952
+ const readArgs = {
953
+ ...this . clone ( selectIncludeClean ) ,
954
+ where : uniqueFilter ,
955
+ } ;
845
956
846
957
const error = this . deniedByPolicy (
847
958
model ,
@@ -866,7 +977,7 @@ export class PolicyUtil {
866
977
return { error, result : undefined } ;
867
978
}
868
979
869
- this . postProcessForRead ( result , model , selectInclude ) ;
980
+ this . postProcessForRead ( result , model , selectIncludeClean ) ;
870
981
return { result, error : undefined } ;
871
982
}
872
983
@@ -1165,6 +1276,19 @@ export class PolicyUtil {
1165
1276
return value ? deepcopy ( value ) : { } ;
1166
1277
}
1167
1278
1279
+ /**
1280
+ * Picks properties from an object.
1281
+ */
1282
+ pick < T > ( value : T , ...props : ( keyof T ) [ ] ) : Pick < T , ( typeof props ) [ number ] > {
1283
+ const v : any = value ;
1284
+ return props . reduce ( function ( result , prop ) {
1285
+ if ( prop in v ) {
1286
+ result [ prop ] = v [ prop ] ;
1287
+ }
1288
+ return result ;
1289
+ } , { } as any ) ;
1290
+ }
1291
+
1168
1292
/**
1169
1293
* Gets "id" fields for a given model.
1170
1294
*/
@@ -1218,5 +1342,13 @@ export class PolicyUtil {
1218
1342
}
1219
1343
}
1220
1344
1345
+ private requireGuard ( model : string ) {
1346
+ const guard = this . policy . guard [ lowerCaseFirst ( model ) ] ;
1347
+ if ( ! guard ) {
1348
+ throw this . unknownError ( `unable to load policy guard for ${ model } ` ) ;
1349
+ }
1350
+ return guard ;
1351
+ }
1352
+
1221
1353
//#endregion
1222
1354
}
0 commit comments