Skip to content

Commit b4af709

Browse files
author
Tom Kirkpatrick
committed
Initial start at rework
1 parent 856cd89 commit b4af709

File tree

2 files changed

+156
-64
lines changed

2 files changed

+156
-64
lines changed

lib/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@ module.exports = function loopbackComponentAccess(app, options) {
2727
// Set up role resolvers.
2828
accessUtils.setupRoleResolvers();
2929
// Set up model opertion hooks
30-
accessUtils.setupModels();
30+
// accessUtils.setupModels();
3131
// TODO: Create Group Access model automatically if one hasn't been specified
3232
};

lib/utils.js

Lines changed: 155 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -268,80 +268,172 @@ module.exports = class AccessUtils {
268268
const Role = this.app.models[this.options.roleModel];
269269

270270
Role.registerResolver(accessGroup, (role, context, cb) => {
271-
const currentUser = this.getCurrentUser();
271+
if (!context || !context.model || !context.modelId) {
272+
process.nextTick(function() {
273+
if (cb) cb(null, false);
274+
});
275+
return;
276+
}
277+
278+
const modelClass = context.model;
279+
const modelId = context.modelId;
280+
const userId = context.getUserId();
272281
const roleName = this.extractRoleName(role);
273282
const GroupAccess = this.app.models[this.options.groupAccessModel];
274283
const scope = { };
275284

276-
// Do not allow anonymous users.
277-
if (!currentUser) {
278-
debug('access denied for anonymous user');
279-
return process.nextTick(() => cb(null, false));
280-
}
281-
282-
debug(`Role resolver for ${role}: evaluate ${context.model.definition.name} with id: ${context.modelId}` +
283-
` for currentUser.getId(): ${currentUser.getId()}`);
285+
debug(`Role resolver for ${role}: evaluate ${modelClass.modelName} with id: ${modelId} for user: ${userId}`);
284286

285-
return Promise.join(this.getCurrentGroupId(context), this.getTargetGroupId(context),
286-
(currentGroupId, targetGroupId) => {
287-
if (!currentGroupId) {
288-
// TODO: Use promise cancellation to abort the chain early.
289-
// Causes the access check to be bypassed (see below).
290-
return [ false ];
291-
}
292-
293-
scope.currentGroupId = currentGroupId;
294-
scope.targetGroupId = targetGroupId;
295-
const actions = [ ];
296-
const conditions = {
297-
userId: currentUser.getId(),
298-
role: roleName
299-
};
300-
301-
conditions[this.options.foreignKey] = currentGroupId;
302-
actions.push(GroupAccess.count(conditions));
303-
304-
// If this is an attempt to save the item into a new group, check the user has access to the target group.
305-
if (targetGroupId && targetGroupId !== currentGroupId) {
306-
conditions[this.options.foreignKey] = targetGroupId;
307-
actions.push(GroupAccess.count(conditions));
308-
}
309-
310-
return actions;
287+
this.isGroupMemberWithRole(modelClass, modelId, userId, roleName)
288+
.then(res => {
289+
debug('Resolved to', res);
290+
cb(null, res);
311291
})
312-
.spread((currentGroupCount, targetGroupCount) => {
313-
let res = false;
314-
315-
if (currentGroupCount === false) {
316-
// No group context was determined, so allow passthrough access.
317-
res = true;
318-
}
319-
else {
320-
// Determine grant based on the current/target group context.
321-
res = currentGroupCount > 0;
292+
.catch(cb);
322293

323-
debug(`user ${currentUser.getId()} ${res ? 'is a' : 'is not a'}` +
324-
`${roleName} of group ${scope.currentGroupId}`);
294+
// return Promise.join(this.getCurrentGroupId(context), this.getTargetGroupId(context),
295+
// (currentGroupId, targetGroupId) => {
296+
// if (!currentGroupId) {
297+
// // TODO: Use promise cancellation to abort the chain early.
298+
// // Causes the access check to be bypassed (see below).
299+
// return [ false ];
300+
// }
301+
//
302+
// scope.currentGroupId = currentGroupId;
303+
// scope.targetGroupId = targetGroupId;
304+
// const actions = [ ];
305+
//
306+
// actions.push(this.hasRoleInGroup(userId, roleName, currentGroupId, context));
307+
//
308+
// // If this is an attempt to save the item into a new group, check the user has access to the target group.
309+
// if (targetGroupId && targetGroupId !== currentGroupId) {
310+
// actions.push(this.hasRoleInGroup(userId, roleName, targetGroupId, context));
311+
// }
312+
//
313+
// return actions;
314+
// })
315+
// .spread((currentGroupCount, targetGroupCount) => {
316+
// let res = false;
317+
//
318+
// if (currentGroupCount === false) {
319+
// // No group context was determined, so allow passthrough access.
320+
// res = true;
321+
// }
322+
// else {
323+
// // Determine grant based on the current/target group context.
324+
// res = currentGroupCount > 0;
325+
//
326+
// debug(`user ${userId} ${res ? 'is a' : 'is not a'}` +
327+
// ` ${roleName} of group ${scope.currentGroupId}`);
328+
//
329+
// // If it's an attempt to save into a new group, also ensure the user has access to the target group.
330+
// if (scope.targetGroupId && scope.targetGroupId !== scope.currentGroupId) {
331+
// const tMember = targetGroupCount > 0;
332+
//
333+
// debug(`user ${userId} ${tMember ? 'is a' : 'is not a'}` +
334+
// ` ${roleName} of group ${scope.targetGroupId}`);
335+
// res = res && tMember;
336+
// }
337+
// }
338+
//
339+
// // Note the fact that we are allowing access due to passing an ACL.
340+
// if (res) {
341+
// this.app.loopback.getCurrentContext().set('groupAccessApplied', true);
342+
// }
343+
//
344+
// debug(`${accessGroup} role resolver returns ${res} for user ${userId}`);
345+
// return cb(null, res);
346+
// })
347+
// .catch(cb);
348+
});
349+
}
325350

326-
// If it's an attempt to save into a new group, also ensure the user has access to the target group.
327-
if (scope.targetGroupId && scope.targetGroupId !== scope.currentGroupId) {
328-
const tMember = targetGroupCount > 0;
351+
/**
352+
* Check if a given user ID has a given role in the model instances group.
353+
* @param {Function} modelClass The model class
354+
* @param {*} modelId The model ID
355+
* @param {*} userId The user ID
356+
* @param {*} roleId The role ID
357+
* @param {Function} callback Callback function
358+
*/
359+
isGroupMemberWithRole(modelClass, modelId, userId, roleId, cb) {
360+
cb = cb || createPromiseCallback();
361+
assert(modelClass, 'Model class is required');
362+
debug('isGroupMemberWithRole: modelClass: %o, modelId: %o, userId: %o, roleId: %o',
363+
modelClass && modelClass.modelName, modelId, userId, roleId);
364+
365+
// No userId is present
366+
if (!userId) {
367+
process.nextTick(() => {
368+
cb(null, false);
369+
});
370+
return cb.promise;
371+
}
329372

330-
debug(`user ${currentUser.getId()} ${tMember ? 'is a' : 'is not a'}` +
331-
`${roleName} of group ${scope.targetGroupId}`);
332-
res = res && tMember;
333-
}
334-
}
373+
// Is the modelClass GroupModel or a subclass of GroupModel?
374+
if (this.isGroupModel(modelClass)) {
375+
this.hasRoleInGroup(userId, roleId, modelId, context)
376+
.then(res => cb(null, res));
377+
return cb.promise;
378+
}
335379

336-
// Note the fact that we are allowing access due to passing an ACL.
337-
if (res) {
338-
this.app.loopback.getCurrentContext().set('groupAccessApplied', true);
380+
modelClass.findById(modelId, (err, inst) => {
381+
if (err || !inst) {
382+
debug('Model not found for id %j', modelId);
383+
if (cb) cb(err, false);
384+
return;
385+
}
386+
debug('Model found: %j', inst);
387+
var groupId = inst[this.options.foreignKey];
388+
// Ensure groupId exists and is not a function/relation
389+
if (groupId && 'function' !== typeof groupId) {
390+
if (cb) {
391+
return this.hasRoleInGroup(userId, roleId, groupId, context)
392+
.then(res => cb(null, res));
393+
}
394+
} else {
395+
// Try to follow belongsTo
396+
for (var r in modelClass.relations) {
397+
var rel = modelClass.relations[r];
398+
if (rel.type === 'belongsTo' && isGroupModel(rel.modelTo)) {
399+
debug('Checking relation %s to %s: %j', r, rel.modelTo.modelName, rel);
400+
inst[r](processRelatedGroup);
401+
return;
339402
}
403+
}
404+
debug('No matching belongsTo relation found for model %j and group: %j', modelId, groupId);
405+
if (cb) cb(null, false);
406+
}
340407

341-
return cb(null, res);
342-
})
343-
.catch(cb);
408+
function processRelatedGroup(err, group) {
409+
if (!err && group) {
410+
debug('Group found: %j', group.getId());
411+
if (cb) cb(null, this.hasRoleInGroup(userId, roleId, group.getId(), context, cb));
412+
} else {
413+
if (cb) cb(err, false);
414+
}
415+
}
344416
});
417+
return cb.promise;
418+
};
419+
420+
hasRoleInGroup(userId, role, group, context, cb) {
421+
debug('hasRoleInGroup: role: %o, group: %o, userId: %o', role, group, userId);
422+
cb = cb || createPromiseCallback();
423+
const GroupAccess = this.app.models[this.options.groupAccessModel];
424+
const conditions = {
425+
userId,
426+
role,
427+
}
428+
conditions[this.options.foreignKey] = group;
429+
GroupAccess.count(conditions)
430+
.then(count => {
431+
const res = count > 0;
432+
433+
debug(`user ${userId} has role ${role} in group ${group}: ${res}`);
434+
cb(null, res);
435+
})
436+
return cb.promise;
345437
}
346438

347439
/**
@@ -362,7 +454,7 @@ module.exports = class AccessUtils {
362454
return cb.promise;
363455
}
364456

365-
// If we are accessing an existing model, get the store id from the existing model instance.
457+
// If we are accessing an existing model, get the group id from the existing model instance.
366458
// TODO: Cache this result so that it can be reused across each ACL lookup attempt.
367459
if (context.modelId) {
368460
debug(`fetching group id for existing model with id: ${context.modelId}`);
@@ -372,7 +464,7 @@ module.exports = class AccessUtils {
372464
.then(item => {
373465
// TODO: Attempt to follow relationships in addition to the foreign key.
374466
if (item) {
375-
debug(`determined group id ${item[this.options.foreignKey]} from existing model %o`, item);
467+
debug(`determined group id ${item[this.options.foreignKey]} from existing model ${context.modelId}`);
376468
groupId = item[this.options.foreignKey];
377469
}
378470
cb(null, groupId);

0 commit comments

Comments
 (0)