@@ -7,6 +7,8 @@ import { ZodError } from 'zod';
77import { fromZodError } from 'zod-validation-error' ;
88import {
99 CrudFailureReason ,
10+ FIELD_LEVEL_OVERRIDE_READ_GUARD_PREFIX ,
11+ FIELD_LEVEL_OVERRIDE_UPDATE_GUARD_PREFIX ,
1012 FIELD_LEVEL_READ_CHECKER_PREFIX ,
1113 FIELD_LEVEL_READ_CHECKER_SELECTOR ,
1214 FIELD_LEVEL_UPDATE_GUARD_PREFIX ,
@@ -236,12 +238,7 @@ export class PolicyUtil {
236238 * @returns true if operation is unconditionally allowed, false if unconditionally denied,
237239 * otherwise returns a guard object
238240 */
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 ) {
245242 const guard = this . policy . guard [ lowerCaseFirst ( model ) ] ;
246243 if ( ! guard ) {
247244 throw this . unknownError ( `unable to load policy guard for ${ model } ` ) ;
@@ -260,23 +257,61 @@ export class PolicyUtil {
260257 }
261258
262259 /**
263- * Get field-level auth guard
260+ * Get field-level read auth guard that overrides the model-level
264261 */
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 ( ) ;
269269 }
270270
271- const provider = guard [ `${ FIELD_LEVEL_UPDATE_GUARD_PREFIX } ${ field } ` ] ;
272271 if ( typeof provider === 'boolean' ) {
273272 return this . reduce ( provider ) ;
274273 }
275274
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 ) {
277287 // field access is allowed by default
278288 return this . makeTrue ( ) ;
279289 }
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+
280315 const r = provider ( { user : this . user } , db ) ;
281316 return this . reduce ( r ) ;
282317 }
@@ -322,10 +357,6 @@ export class PolicyUtil {
322357 */
323358 injectAuthGuard ( db : Record < string , DbOperations > , args : any , model : string , operation : PolicyOperationKind ) {
324359 let guard = this . getAuthGuard ( db , model , operation ) ;
325- if ( this . isFalse ( guard ) ) {
326- args . where = this . makeFalse ( ) ;
327- return false ;
328- }
329360
330361 if ( operation === 'update' && args ) {
331362 // merge field-level policy guards
@@ -334,12 +365,32 @@ export class PolicyUtil {
334365 // rejected
335366 args . where = this . makeFalse ( ) ;
336367 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 ) ;
340386 }
341387 }
342388
389+ if ( this . isFalse ( guard ) ) {
390+ args . where = this . makeFalse ( ) ;
391+ return false ;
392+ }
393+
343394 if ( args . where ) {
344395 // inject into relation fields:
345396 // to-many: some/none/every
@@ -441,7 +492,8 @@ export class PolicyUtil {
441492 * Injects auth guard for read operations.
442493 */
443494 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 } ;
445497 if ( ! this . injectAuthGuard ( db , injected , model , 'read' ) ) {
446498 return false ;
447499 }
@@ -701,9 +753,16 @@ export class PolicyUtil {
701753 } "`,
702754 CrudFailureReason . ACCESS_POLICY_VIOLATION
703755 ) ;
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+ }
707766 }
708767 }
709768
@@ -761,8 +820,33 @@ export class PolicyUtil {
761820 }
762821 }
763822
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+
764846 private getFieldUpdateGuards ( db : Record < string , DbOperations > , model : string , args : any ) {
765847 const allFieldGuards = [ ] ;
848+ const allOverrideFieldGuards = [ ] ;
849+
766850 for ( const [ k , v ] of Object . entries < any > ( args . data ?? args ) ) {
767851 if ( typeof v === 'undefined' ) {
768852 continue ;
@@ -778,20 +862,41 @@ export class PolicyUtil {
778862 for ( const fk of foreignKeys ) {
779863 const fieldGuard = this . getFieldUpdateAuthGuard ( db , model , fk ) ;
780864 if ( this . isFalse ( fieldGuard ) ) {
781- return { guard : allFieldGuards , rejectedByField : fk } ;
865+ return { guard : fieldGuard , rejectedByField : fk } ;
782866 }
867+
868+ // add field guard
783869 allFieldGuards . push ( fieldGuard ) ;
870+
871+ // add field override guard
872+ const overrideFieldGuard = this . getFieldOverrideUpdateAuthGuard ( db , model , fk ) ;
873+ allOverrideFieldGuards . push ( overrideFieldGuard ) ;
784874 }
785875 }
786876 } else {
787877 const fieldGuard = this . getFieldUpdateAuthGuard ( db , model , k ) ;
788878 if ( this . isFalse ( fieldGuard ) ) {
789- return { guard : allFieldGuards , rejectedByField : k } ;
879+ return { guard : fieldGuard , rejectedByField : k } ;
790880 }
881+
882+ // add field guard
791883 allFieldGuards . push ( fieldGuard ) ;
884+
885+ // add field override guard
886+ const overrideFieldGuard = this . getFieldOverrideUpdateAuthGuard ( db , model , k ) ;
887+ allOverrideFieldGuards . push ( overrideFieldGuard ) ;
792888 }
793889 }
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+ } ;
795900 }
796901
797902 /**
@@ -841,7 +946,13 @@ export class PolicyUtil {
841946 ) : Promise < { result : unknown ; error ?: Error } > {
842947 uniqueFilter = this . clone ( uniqueFilter ) ;
843948 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+ } ;
845956
846957 const error = this . deniedByPolicy (
847958 model ,
@@ -866,7 +977,7 @@ export class PolicyUtil {
866977 return { error, result : undefined } ;
867978 }
868979
869- this . postProcessForRead ( result , model , selectInclude ) ;
980+ this . postProcessForRead ( result , model , selectIncludeClean ) ;
870981 return { result, error : undefined } ;
871982 }
872983
@@ -1165,6 +1276,19 @@ export class PolicyUtil {
11651276 return value ? deepcopy ( value ) : { } ;
11661277 }
11671278
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+
11681292 /**
11691293 * Gets "id" fields for a given model.
11701294 */
@@ -1218,5 +1342,13 @@ export class PolicyUtil {
12181342 }
12191343 }
12201344
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+
12211353 //#endregion
12221354}
0 commit comments