@@ -13,12 +13,14 @@ module.exports = class AccessUtils {
13
13
this . options = _defaults ( { } , options , {
14
14
userModel : 'User' ,
15
15
roleModel : 'Role' ,
16
- groupModel : 'Group' ,
17
16
groupAccessModel : 'GroupAccess' ,
17
+ groupModel : 'Group' ,
18
+ foreignKey : 'groupId' ,
18
19
groupRoles : [
19
20
'$group:admin' ,
20
21
'$group:member'
21
- ]
22
+ ] ,
23
+ applyToStatic : false
22
24
} ) ;
23
25
// Default the foreignKey to the group model name + Id.
24
26
this . options . foreignKey = this . options . foreignKey || `${ this . options . groupModel . toLowerCase ( ) } Id` ;
@@ -47,8 +49,10 @@ module.exports = class AccessUtils {
47
49
/**
48
50
* Add operation hooks to limit access.
49
51
*/
50
- setupModels ( ) {
51
- this . getGroupContentModels ( ) . forEach ( modelName => {
52
+ setupFilters ( ) {
53
+ const models = [ this . options . groupModel ] . concat ( this . getGroupContentModels ( ) ) ;
54
+
55
+ models . forEach ( modelName => {
52
56
const Model = this . app . models [ modelName ] ;
53
57
54
58
if ( typeof Model . observe === 'function' ) {
@@ -81,15 +85,15 @@ module.exports = class AccessUtils {
81
85
debug ( '%s observe access: query=%s, options=%o, hookState=%o' ,
82
86
Model . modelName , JSON . stringify ( ctx . query , null , 4 ) , ctx . options , ctx . hookState ) ;
83
87
84
- return this . buildFilter ( currentUser . getId ( ) )
88
+ return this . buildFilter ( currentUser . getId ( ) , ctx . Model )
85
89
. then ( filter => {
86
- debug ( 'filter : %o' , filter ) ;
90
+ debug ( 'original query : %o' , JSON . stringify ( ctx . query , null , 4 ) ) ;
87
91
const where = ctx . query . where ? {
88
92
and : [ ctx . query . where , filter ]
89
93
} : filter ;
90
94
91
95
ctx . query . where = where ;
92
- debug ( 'where query modified to : %s' , JSON . stringify ( ctx . query , null , 4 ) ) ;
96
+ debug ( 'modified query : %s' , JSON . stringify ( ctx . query , null , 4 ) ) ;
93
97
} ) ;
94
98
}
95
99
return next ( ) ;
@@ -101,16 +105,19 @@ module.exports = class AccessUtils {
101
105
/**
102
106
* Build a where filter to restrict search results to a users group
103
107
*
104
- * @param {String } userId UserId to build filter for,
108
+ * @param {String } userId UserId to build filter for.
109
+ * @param {Object } Model Model to build filter for,
105
110
* @returns {Object } A where filter.
106
111
*/
107
- buildFilter ( userId ) {
112
+ buildFilter ( userId , Model ) {
108
113
const filter = { } ;
114
+ const key = this . isGroupModel ( Model ) ? Model . getIdName ( ) : this . options . foreignKey ;
115
+ // TODO: Support key determination based on the belongsTo relationship.
109
116
110
117
return this . getUserGroups ( userId )
111
118
. then ( userGroups => {
112
119
userGroups = Array . from ( userGroups , group => group [ this . options . foreignKey ] ) ;
113
- filter [ this . options . foreignKey ] = { inq : userGroups } ;
120
+ filter [ key ] = { inq : userGroups } ;
114
121
return filter ;
115
122
} ) ;
116
123
}
@@ -132,6 +139,23 @@ module.exports = class AccessUtils {
132
139
return false ;
133
140
}
134
141
142
+ /**
143
+ * Check if a model class is the configured group access model.
144
+ *
145
+ * @param {String|Object } modelClass Model class to check.
146
+ * @returns {Boolean } Returns true if the principalId is on the expected format.
147
+ */
148
+ isGroupAccessModel ( modelClass ) {
149
+ if ( modelClass ) {
150
+ const groupAccessModel = this . app . models [ this . options . groupAccessModel ] ;
151
+
152
+ return modelClass === groupAccessModel ||
153
+ modelClass . prototype instanceof groupAccessModel ||
154
+ modelClass === this . options . groupAccessModel ;
155
+ }
156
+ return false ;
157
+ }
158
+
135
159
/**
136
160
* Get a list of group content models (models that have a belongs to relationship to the group model)
137
161
*
@@ -143,8 +167,8 @@ module.exports = class AccessUtils {
143
167
Object . keys ( this . app . models ) . forEach ( modelName => {
144
168
const modelClass = this . app . models [ modelName ] ;
145
169
146
- // TODO: Should we allow the access group model to be treated as a group content model too?
147
- if ( modelName === this . options . groupAccessModel ) {
170
+ // Mark the group itself as a group or the group access model.
171
+ if ( this . isGroupModel ( modelClass ) || this . isGroupAccessModel ( modelClass ) ) {
148
172
return ;
149
173
}
150
174
@@ -207,14 +231,6 @@ module.exports = class AccessUtils {
207
231
const ctx = this . app . loopback . getCurrentContext ( ) ;
208
232
const currentUser = ctx && ctx . get ( 'currentUser' ) || null ;
209
233
210
- if ( ctx ) {
211
- debug ( 'getCurrentUser() - currentUser: %o' , currentUser ) ;
212
- }
213
- else {
214
- // this means its a server-side logic call w/o any HTTP req/resp aspect to it.
215
- debug ( 'getCurrentUser() - no loopback context' ) ;
216
- }
217
-
218
234
return currentUser ;
219
235
}
220
236
@@ -227,14 +243,6 @@ module.exports = class AccessUtils {
227
243
const ctx = this . app . loopback . getCurrentContext ( ) ;
228
244
const currentUserGroups = ctx && ctx . get ( 'currentUserGroups' ) || [ ] ;
229
245
230
- if ( ctx ) {
231
- debug ( 'currentUserGroups(): %o' , currentUserGroups ) ;
232
- }
233
- else {
234
- // this means its a server-side logic call w/o any HTTP req/resp aspect to it.
235
- debug ( 'currentUserGroups(): no loopback context' ) ;
236
- }
237
-
238
246
return currentUserGroups ;
239
247
}
240
248
@@ -268,21 +276,51 @@ module.exports = class AccessUtils {
268
276
const Role = this . app . models [ this . options . roleModel ] ;
269
277
270
278
Role . registerResolver ( accessGroup , ( role , context , cb ) => {
271
- const currentUser = this . getCurrentUser ( ) ;
279
+ cb = cb || createPromiseCallback ( ) ;
280
+ const modelClass = context . model ;
281
+ const modelId = context . modelId ;
282
+ const userId = context . getUserId ( ) ;
272
283
const roleName = this . extractRoleName ( role ) ;
273
284
const GroupAccess = this . app . models [ this . options . groupAccessModel ] ;
274
285
const scope = { } ;
275
286
276
- // Do not allow anonymous users.
277
- if ( ! currentUser ) {
278
- debug ( 'access denied for anonymous user' ) ;
279
- return process . nextTick ( ( ) => cb ( null , false ) ) ;
287
+ debug ( `Role resolver for ${ role } : evaluate ${ modelClass . modelName } with id: ${ modelId } for user: ${ userId } ` ) ;
288
+
289
+ // No userId is present
290
+ if ( ! userId ) {
291
+ process . nextTick ( ( ) => {
292
+ debug ( 'Deny access for anonymous user' ) ;
293
+ cb ( null , false ) ;
294
+ } ) ;
295
+ return cb . promise ;
280
296
}
281
297
282
- debug ( `Role resolver for ${ role } : evaluate ${ context . model . definition . name } with id: ${ context . modelId } ` +
283
- ` for currentUser.getId(): ${ currentUser . getId ( ) } ` ) ;
298
+ this . app . loopback . getCurrentContext ( ) . set ( 'groupAccessApplied' , true ) ;
299
+
300
+ /**
301
+ * Basic application that does not cover static methods. Similar to $owner. (RECOMMENDED)
302
+ */
303
+ if ( ! this . options . applyToStatic ) {
304
+ if ( ! context || ! modelClass || ! modelId ) {
305
+ process . nextTick ( ( ) => {
306
+ debug ( 'Deny access (context: %s, context.model: %s, context.modelId: %s)' ,
307
+ Boolean ( context ) , Boolean ( modelClass ) , Boolean ( modelId ) ) ;
308
+ cb ( null , false ) ;
309
+ } ) ;
310
+ return cb . promise ;
311
+ }
312
+
313
+ this . isGroupMemberWithRole ( modelClass , modelId , userId , roleName )
314
+ . then ( res => cb ( null , res ) )
315
+ . catch ( cb ) ;
284
316
285
- return Promise . join ( this . getCurrentGroupId ( context ) , this . getTargetGroupId ( context ) ,
317
+ return cb . promise ;
318
+ }
319
+
320
+ /**
321
+ * More complex application that also covers static methods. (EXPERIMENTAL)
322
+ */
323
+ Promise . join ( this . getCurrentGroupId ( context ) , this . getTargetGroupId ( context ) ,
286
324
( currentGroupId , targetGroupId ) => {
287
325
if ( ! currentGroupId ) {
288
326
// TODO: Use promise cancellation to abort the chain early.
@@ -293,10 +331,7 @@ module.exports = class AccessUtils {
293
331
scope . currentGroupId = currentGroupId ;
294
332
scope . targetGroupId = targetGroupId ;
295
333
const actions = [ ] ;
296
- const conditions = {
297
- userId : currentUser . getId ( ) ,
298
- role : roleName
299
- } ;
334
+ const conditions = { userId, role : roleName } ;
300
335
301
336
conditions [ this . options . foreignKey ] = currentGroupId ;
302
337
actions . push ( GroupAccess . count ( conditions ) ) ;
@@ -320,15 +355,13 @@ module.exports = class AccessUtils {
320
355
// Determine grant based on the current/target group context.
321
356
res = currentGroupCount > 0 ;
322
357
323
- debug ( `user ${ currentUser . getId ( ) } ${ res ? 'is a' : 'is not a' } ` +
324
- `${ roleName } of group ${ scope . currentGroupId } ` ) ;
358
+ debug ( `user ${ userId } ${ res ? 'is a' : 'is not a' } ${ roleName } of group ${ scope . currentGroupId } ` ) ;
325
359
326
360
// If it's an attempt to save into a new group, also ensure the user has access to the target group.
327
361
if ( scope . targetGroupId && scope . targetGroupId !== scope . currentGroupId ) {
328
362
const tMember = targetGroupCount > 0 ;
329
363
330
- debug ( `user ${ currentUser . getId ( ) } ${ tMember ? 'is a' : 'is not a' } ` +
331
- `${ roleName } of group ${ scope . targetGroupId } ` ) ;
364
+ debug ( `user ${ userId } ${ tMember ? 'is a' : 'is not a' } ${ roleName } of group ${ scope . targetGroupId } ` ) ;
332
365
res = res && tMember ;
333
366
}
334
367
}
@@ -341,7 +374,88 @@ module.exports = class AccessUtils {
341
374
return cb ( null , res ) ;
342
375
} )
343
376
. catch ( cb ) ;
377
+ return cb . promise ;
378
+ } ) ;
379
+ }
380
+
381
+ /**
382
+ * Check if a given user ID has a given role in the model instances group.
383
+ * @param {Function } modelClass The model class
384
+ * @param {* } modelId The model ID
385
+ * @param {* } userId The user ID
386
+ * @param {* } roleId The role ID
387
+ * @param {Function } callback Callback function
388
+ */
389
+ isGroupMemberWithRole ( modelClass , modelId , userId , roleId , cb ) {
390
+ cb = cb || createPromiseCallback ( ) ;
391
+ debug ( 'isGroupMemberWithRole: modelClass: %o, modelId: %o, userId: %o, roleId: %o' ,
392
+ modelClass && modelClass . modelName , modelId , userId , roleId ) ;
393
+
394
+ // No userId is present
395
+ if ( ! userId ) {
396
+ process . nextTick ( ( ) => {
397
+ cb ( null , false ) ;
398
+ } ) ;
399
+ return cb . promise ;
400
+ }
401
+
402
+ // Is the modelClass GroupModel or a subclass of GroupModel?
403
+ if ( this . isGroupModel ( modelClass ) ) {
404
+ debug ( 'Access to Group Model %s attempted' , modelId ) ;
405
+ this . hasRoleInGroup ( userId , roleId , modelId )
406
+ . then ( res => cb ( null , res ) ) ;
407
+ return cb . promise ;
408
+ }
409
+
410
+ modelClass . findById ( modelId , ( err , inst ) => {
411
+ if ( err || ! inst ) {
412
+ debug ( 'Model not found for id %j' , modelId ) ;
413
+ return cb ( err , false ) ;
414
+ }
415
+ debug ( 'Model found: %j' , inst ) ;
416
+ const groupId = inst [ this . options . foreignKey ] ;
417
+
418
+ // Ensure groupId exists and is not a function/relation
419
+ if ( groupId && typeof groupId !== 'function' ) {
420
+ return this . hasRoleInGroup ( userId , roleId , groupId )
421
+ . then ( res => cb ( null , res ) ) ;
422
+ }
423
+ // Try to follow belongsTo
424
+ for ( const relName in modelClass . relations ) {
425
+ const rel = modelClass . relations [ relName ] ;
426
+
427
+ if ( rel . type === 'belongsTo' && this . isGroupModel ( rel . modelTo ) ) {
428
+ debug ( 'Checking relation %s to %s: %j' , relName , rel . modelTo . modelName , rel ) ;
429
+ return inst [ relName ] ( function processRelatedGroup ( error , group ) {
430
+ if ( ! error && group ) {
431
+ debug ( 'Group found: %j' , group . getId ( ) ) ;
432
+ return cb ( null , this . hasRoleInGroup ( userId , roleId , group . getId ( ) ) ) ;
433
+ }
434
+ return cb ( error , false ) ;
435
+ } ) ;
436
+ }
437
+ }
438
+ debug ( 'No matching belongsTo relation found for model %j and group: %j' , modelId , groupId ) ;
439
+ return cb ( null , false ) ;
344
440
} ) ;
441
+ return cb . promise ;
442
+ }
443
+
444
+ hasRoleInGroup ( userId , role , group , cb ) {
445
+ debug ( 'hasRoleInGroup: role: %o, group: %o, userId: %o' , role , group , userId ) ;
446
+ cb = cb || createPromiseCallback ( ) ;
447
+ const GroupAccess = this . app . models [ this . options . groupAccessModel ] ;
448
+ const conditions = { userId, role } ;
449
+
450
+ conditions [ this . options . foreignKey ] = group ;
451
+ GroupAccess . count ( conditions )
452
+ . then ( count => {
453
+ const res = count > 0 ;
454
+
455
+ debug ( `User ${ userId } ${ res ? 'HAS' : 'DOESNT HAVE' } ${ role } role in group ${ group } ` ) ;
456
+ cb ( null , res ) ;
457
+ } ) ;
458
+ return cb . promise ;
345
459
}
346
460
347
461
/**
@@ -362,7 +476,7 @@ module.exports = class AccessUtils {
362
476
return cb . promise ;
363
477
}
364
478
365
- // If we are accessing an existing model, get the store id from the existing model instance.
479
+ // If we are accessing an existing model, get the group id from the existing model instance.
366
480
// TODO: Cache this result so that it can be reused across each ACL lookup attempt.
367
481
if ( context . modelId ) {
368
482
debug ( `fetching group id for existing model with id: ${ context . modelId } ` ) ;
@@ -372,7 +486,7 @@ module.exports = class AccessUtils {
372
486
. then ( item => {
373
487
// TODO: Attempt to follow relationships in addition to the foreign key.
374
488
if ( item ) {
375
- debug ( `determined group id ${ item [ this . options . foreignKey ] } from existing model %o` , item ) ;
489
+ debug ( `determined group id ${ item [ this . options . foreignKey ] } from existing model ${ context . modelId } ` ) ;
376
490
groupId = item [ this . options . foreignKey ] ;
377
491
}
378
492
cb ( null , groupId ) ;
0 commit comments