2018-01-03 04:05:53 +00:00
|
|
|
// Copyright IBM Corp. 2014,2018. All Rights Reserved.
|
2016-05-03 22:50:21 +00:00
|
|
|
// Node module: loopback
|
|
|
|
// This file is licensed under the MIT License.
|
|
|
|
// License text available at https://opensource.org/licenses/MIT
|
|
|
|
|
2016-11-15 21:46:23 +00:00
|
|
|
'use strict';
|
2014-10-09 15:32:03 +00:00
|
|
|
var loopback = require('../../lib/loopback');
|
2013-12-12 03:15:19 +00:00
|
|
|
var debug = require('debug')('loopback:security:role');
|
2013-12-11 17:06:21 +00:00
|
|
|
var assert = require('assert');
|
|
|
|
var async = require('async');
|
2017-01-27 09:51:56 +00:00
|
|
|
var utils = require('../../lib/utils');
|
2016-11-21 20:51:43 +00:00
|
|
|
var ctx = require('../../lib/access-context');
|
|
|
|
var AccessContext = ctx.AccessContext;
|
|
|
|
var Principal = ctx.Principal;
|
2014-10-13 09:31:27 +00:00
|
|
|
var RoleMapping = loopback.RoleMapping;
|
2015-03-13 15:50:30 +00:00
|
|
|
|
2014-10-13 09:31:27 +00:00
|
|
|
assert(RoleMapping, 'RoleMapping model must be defined before Role model');
|
2013-11-04 21:19:02 +00:00
|
|
|
|
2013-11-12 18:10:32 +00:00
|
|
|
/**
|
2015-03-12 18:15:36 +00:00
|
|
|
* The Role model
|
2014-10-13 09:31:27 +00:00
|
|
|
* @class Role
|
2015-03-12 18:15:36 +00:00
|
|
|
* @header Role object
|
2013-11-12 18:10:32 +00:00
|
|
|
*/
|
2014-10-13 09:31:27 +00:00
|
|
|
module.exports = function(Role) {
|
2015-08-13 15:58:41 +00:00
|
|
|
Role.resolveRelatedModels = function() {
|
|
|
|
if (!this.userModel) {
|
|
|
|
var reg = this.registry;
|
2017-01-16 14:23:33 +00:00
|
|
|
this.roleMappingModel = reg.getModelByType('RoleMapping');
|
|
|
|
this.userModel = reg.getModelByType('User');
|
|
|
|
this.applicationModel = reg.getModelByType('Application');
|
2015-08-13 15:58:41 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2014-10-13 09:31:27 +00:00
|
|
|
// Set up the connection to users/applications/roles once the model
|
2015-08-13 15:58:41 +00:00
|
|
|
Role.once('dataSourceAttached', function(roleModel) {
|
|
|
|
['users', 'applications', 'roles'].forEach(function(rel) {
|
2015-03-13 20:53:26 +00:00
|
|
|
/**
|
2015-03-13 15:50:30 +00:00
|
|
|
* Fetch all users assigned to this role
|
|
|
|
* @function Role.prototype#users
|
|
|
|
* @param {object} [query] query object passed to model find call
|
2017-01-27 09:51:56 +00:00
|
|
|
* @callback {Function} [callback] The callback function
|
|
|
|
* @param {String|Error} err The error string or object
|
|
|
|
* @param {Array} list The list of users.
|
|
|
|
* @promise
|
2015-03-13 20:53:26 +00:00
|
|
|
*/
|
|
|
|
/**
|
2015-03-13 15:50:30 +00:00
|
|
|
* Fetch all applications assigned to this role
|
|
|
|
* @function Role.prototype#applications
|
|
|
|
* @param {object} [query] query object passed to model find call
|
2017-01-27 09:51:56 +00:00
|
|
|
* @callback {Function} [callback] The callback function
|
|
|
|
* @param {String|Error} err The error string or object
|
|
|
|
* @param {Array} list The list of applications.
|
|
|
|
* @promise
|
2015-03-13 15:50:30 +00:00
|
|
|
*/
|
2015-03-13 20:53:26 +00:00
|
|
|
/**
|
2015-03-13 15:50:30 +00:00
|
|
|
* Fetch all roles assigned to this role
|
|
|
|
* @function Role.prototype#roles
|
|
|
|
* @param {object} [query] query object passed to model find call
|
2017-01-27 09:51:56 +00:00
|
|
|
* @callback {Function} [callback] The callback function
|
|
|
|
* @param {String|Error} err The error string or object
|
|
|
|
* @param {Array} list The list of roles.
|
|
|
|
* @promise
|
2015-03-13 20:53:26 +00:00
|
|
|
*/
|
2015-08-13 15:58:41 +00:00
|
|
|
Role.prototype[rel] = function(query, callback) {
|
2017-01-27 09:51:56 +00:00
|
|
|
if (!callback) {
|
|
|
|
if (typeof query === 'function') {
|
|
|
|
callback = query;
|
|
|
|
query = {};
|
|
|
|
} else {
|
|
|
|
callback = utils.createPromiseCallback();
|
|
|
|
}
|
|
|
|
}
|
2016-11-21 20:51:43 +00:00
|
|
|
query = query || {};
|
|
|
|
query.where = query.where || {};
|
2017-01-27 09:51:56 +00:00
|
|
|
|
2015-08-13 15:58:41 +00:00
|
|
|
roleModel.resolveRelatedModels();
|
|
|
|
var relsToModels = {
|
|
|
|
users: roleModel.userModel,
|
|
|
|
applications: roleModel.applicationModel,
|
2016-04-01 09:14:26 +00:00
|
|
|
roles: roleModel,
|
2015-08-13 15:58:41 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
var ACL = loopback.ACL;
|
|
|
|
var relsToTypes = {
|
|
|
|
users: ACL.USER,
|
|
|
|
applications: ACL.APP,
|
2016-04-01 09:14:26 +00:00
|
|
|
roles: ACL.ROLE,
|
2015-08-13 15:58:41 +00:00
|
|
|
};
|
|
|
|
|
2016-11-21 20:51:43 +00:00
|
|
|
var principalModel = relsToModels[rel];
|
|
|
|
var principalType = relsToTypes[rel];
|
|
|
|
|
|
|
|
// redefine user model and user type if user principalType is custom (available and not "USER")
|
|
|
|
var isCustomUserPrincipalType = rel === 'users' &&
|
|
|
|
query.where.principalType &&
|
|
|
|
query.where.principalType !== RoleMapping.USER;
|
|
|
|
|
|
|
|
if (isCustomUserPrincipalType) {
|
|
|
|
var registry = this.constructor.registry;
|
|
|
|
principalModel = registry.findModel(query.where.principalType);
|
|
|
|
principalType = query.where.principalType;
|
|
|
|
}
|
|
|
|
// make sure we don't keep principalType in userModel query
|
|
|
|
delete query.where.principalType;
|
|
|
|
|
|
|
|
if (principalModel) {
|
|
|
|
listByPrincipalType(this, principalModel, principalType, query, callback);
|
|
|
|
} else {
|
|
|
|
process.nextTick(function() {
|
|
|
|
callback(null, []);
|
|
|
|
});
|
|
|
|
}
|
2017-01-27 09:51:56 +00:00
|
|
|
return callback.promise;
|
2015-03-13 22:30:53 +00:00
|
|
|
};
|
2015-03-13 15:50:30 +00:00
|
|
|
});
|
2013-11-12 06:16:51 +00:00
|
|
|
|
2015-03-12 18:55:39 +00:00
|
|
|
/**
|
2015-03-13 15:50:30 +00:00
|
|
|
* Fetch all models assigned to this role
|
2015-03-25 14:10:34 +00:00
|
|
|
* @private
|
2016-09-13 19:43:34 +00:00
|
|
|
* @param {object} Context role context
|
2015-03-13 15:50:30 +00:00
|
|
|
* @param {*} model model type to fetch
|
|
|
|
* @param {String} [principalType] principalType used in the rolemapping for model
|
|
|
|
* @param {object} [query] query object passed to model find call
|
2015-03-26 14:10:13 +00:00
|
|
|
* @param {Function} [callback] callback function called with `(err, models)` arguments.
|
2015-03-12 18:55:39 +00:00
|
|
|
*/
|
2016-09-13 19:43:34 +00:00
|
|
|
function listByPrincipalType(context, model, principalType, query, callback) {
|
2016-11-21 20:51:43 +00:00
|
|
|
if (callback === undefined && typeof query === 'function') {
|
2015-03-13 15:50:30 +00:00
|
|
|
callback = query;
|
|
|
|
query = {};
|
|
|
|
}
|
2016-11-21 20:51:43 +00:00
|
|
|
query = query || {};
|
2015-03-13 15:50:30 +00:00
|
|
|
|
2015-08-13 15:58:41 +00:00
|
|
|
roleModel.roleMappingModel.find({
|
2016-11-15 21:46:23 +00:00
|
|
|
where: {roleId: context.id, principalType: principalType},
|
2015-03-12 18:55:39 +00:00
|
|
|
}, function(err, mappings) {
|
2015-03-13 15:50:30 +00:00
|
|
|
var ids;
|
2014-10-13 09:31:27 +00:00
|
|
|
if (err) {
|
2015-03-12 18:55:39 +00:00
|
|
|
return callback(err);
|
2014-10-13 09:31:27 +00:00
|
|
|
}
|
2015-03-13 15:50:30 +00:00
|
|
|
ids = mappings.map(function(m) {
|
2014-10-13 09:31:27 +00:00
|
|
|
return m.principalId;
|
|
|
|
});
|
2015-03-13 15:50:30 +00:00
|
|
|
query.where = query.where || {};
|
2016-11-15 21:46:23 +00:00
|
|
|
query.where.id = {inq: ids};
|
2015-03-26 14:10:13 +00:00
|
|
|
model.find(query, function(err, models) {
|
|
|
|
callback(err, models);
|
|
|
|
});
|
2014-10-13 09:31:27 +00:00
|
|
|
});
|
2015-03-13 15:50:30 +00:00
|
|
|
}
|
2014-10-13 09:31:27 +00:00
|
|
|
});
|
2013-07-09 22:06:42 +00:00
|
|
|
|
2015-03-27 15:59:11 +00:00
|
|
|
// Special roles
|
2014-10-13 09:31:27 +00:00
|
|
|
Role.OWNER = '$owner'; // owner of the object
|
2014-11-04 12:52:49 +00:00
|
|
|
Role.RELATED = '$related'; // any User with a relationship to the object
|
|
|
|
Role.AUTHENTICATED = '$authenticated'; // authenticated user
|
|
|
|
Role.UNAUTHENTICATED = '$unauthenticated'; // authenticated user
|
|
|
|
Role.EVERYONE = '$everyone'; // everyone
|
2014-10-13 09:31:27 +00:00
|
|
|
|
|
|
|
/**
|
2014-10-20 21:45:40 +00:00
|
|
|
* Add custom handler for roles.
|
|
|
|
* @param {String} role Name of role.
|
2016-04-26 04:35:54 +00:00
|
|
|
* @param {Function} resolver Function that determines
|
|
|
|
* if a principal is in the specified role.
|
|
|
|
* Should provide a callback or return a promise.
|
2014-10-13 09:31:27 +00:00
|
|
|
*/
|
|
|
|
Role.registerResolver = function(role, resolver) {
|
|
|
|
if (!Role.resolvers) {
|
|
|
|
Role.resolvers = {};
|
|
|
|
}
|
|
|
|
Role.resolvers[role] = resolver;
|
|
|
|
};
|
2013-11-13 18:29:33 +00:00
|
|
|
|
2014-10-13 09:31:27 +00:00
|
|
|
Role.registerResolver(Role.OWNER, function(role, context, callback) {
|
|
|
|
if (!context || !context.model || !context.modelId) {
|
|
|
|
process.nextTick(function() {
|
2014-11-04 12:52:49 +00:00
|
|
|
if (callback) callback(null, false);
|
2014-10-13 09:31:27 +00:00
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
var modelClass = context.model;
|
|
|
|
var modelId = context.modelId;
|
2017-02-03 20:53:27 +00:00
|
|
|
var user = context.getUser();
|
|
|
|
var userId = user && user.id;
|
|
|
|
var principalType = user && user.principalType;
|
2017-02-18 13:23:37 +00:00
|
|
|
var opts = {accessToken: context.accessToken};
|
|
|
|
Role.isOwner(modelClass, modelId, userId, principalType, opts, callback);
|
2014-10-13 09:31:27 +00:00
|
|
|
});
|
2014-03-19 22:09:20 +00:00
|
|
|
|
2014-10-13 09:31:27 +00:00
|
|
|
function isUserClass(modelClass) {
|
2016-05-02 12:30:11 +00:00
|
|
|
if (!modelClass) return false;
|
|
|
|
var User = modelClass.modelBuilder.models.User;
|
|
|
|
if (!User) return false;
|
|
|
|
return modelClass == User || modelClass.prototype instanceof User;
|
2013-12-11 05:49:18 +00:00
|
|
|
}
|
|
|
|
|
2014-10-13 09:31:27 +00:00
|
|
|
/*!
|
2015-03-12 18:15:36 +00:00
|
|
|
* Check if two user IDs matches
|
2014-10-13 09:31:27 +00:00
|
|
|
* @param {*} id1
|
|
|
|
* @param {*} id2
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
function matches(id1, id2) {
|
2015-02-20 14:31:15 +00:00
|
|
|
if (id1 === undefined || id1 === null || id1 === '' ||
|
|
|
|
id2 === undefined || id2 === null || id2 === '') {
|
2014-10-13 09:31:27 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
// The id can be a MongoDB ObjectID
|
|
|
|
return id1 === id2 || id1.toString() === id2.toString();
|
2013-12-11 05:49:18 +00:00
|
|
|
}
|
|
|
|
|
2014-10-13 09:31:27 +00:00
|
|
|
/**
|
2014-10-15 07:07:30 +00:00
|
|
|
* Check if a given user ID is the owner the model instance.
|
2014-10-13 09:31:27 +00:00
|
|
|
* @param {Function} modelClass The model class
|
2014-10-15 07:07:30 +00:00
|
|
|
* @param {*} modelId The model ID
|
|
|
|
* @param {*} userId The user ID
|
2017-02-03 20:53:27 +00:00
|
|
|
* @param {String} principalType The user principalType (optional)
|
2017-02-18 13:23:37 +00:00
|
|
|
* @options {Object} options
|
|
|
|
* @property {accessToken} The access token used to authorize the current user.
|
2017-01-27 09:51:56 +00:00
|
|
|
* @callback {Function} [callback] The callback function
|
|
|
|
* @param {String|Error} err The error string or object
|
|
|
|
* @param {Boolean} isOwner True if the user is an owner.
|
|
|
|
* @promise
|
2014-10-13 09:31:27 +00:00
|
|
|
*/
|
2017-02-18 13:23:37 +00:00
|
|
|
Role.isOwner = function isOwner(modelClass, modelId, userId, principalType, options, callback) {
|
2017-01-26 12:07:20 +00:00
|
|
|
var _this = this;
|
|
|
|
|
2017-02-18 13:23:37 +00:00
|
|
|
if (!callback && typeof options === 'function') {
|
|
|
|
callback = options;
|
|
|
|
options = {};
|
|
|
|
} else if (!callback && typeof principalType === 'function') {
|
2017-02-03 20:53:27 +00:00
|
|
|
callback = principalType;
|
|
|
|
principalType = undefined;
|
2017-02-18 13:23:37 +00:00
|
|
|
options = {};
|
2017-02-03 20:53:27 +00:00
|
|
|
}
|
|
|
|
principalType = principalType || Principal.USER;
|
|
|
|
|
2014-10-13 09:31:27 +00:00
|
|
|
assert(modelClass, 'Model class is required');
|
2017-01-27 09:51:56 +00:00
|
|
|
if (!callback) callback = utils.createPromiseCallback();
|
|
|
|
|
2017-02-03 20:53:27 +00:00
|
|
|
debug('isOwner(): %s %s userId: %s principalType: %s',
|
|
|
|
modelClass && modelClass.modelName, modelId, userId, principalType);
|
2017-01-27 09:51:56 +00:00
|
|
|
|
2017-01-26 12:07:20 +00:00
|
|
|
// Resolve isOwner false if userId is missing
|
2014-10-13 09:31:27 +00:00
|
|
|
if (!userId) {
|
|
|
|
process.nextTick(function() {
|
|
|
|
callback(null, false);
|
|
|
|
});
|
2017-01-27 09:51:56 +00:00
|
|
|
return callback.promise;
|
2013-11-19 19:58:30 +00:00
|
|
|
}
|
2014-10-13 09:31:27 +00:00
|
|
|
|
2017-01-26 12:07:20 +00:00
|
|
|
// At this stage, principalType is valid in one of 2 following condition:
|
|
|
|
// 1. the app has a single user model and principalType is 'USER'
|
|
|
|
// 2. the app has multiple user models and principalType is not 'USER'
|
|
|
|
// multiple user models
|
|
|
|
var isMultipleUsers = _isMultipleUsers();
|
|
|
|
var isPrincipalTypeValid =
|
|
|
|
(!isMultipleUsers && principalType === Principal.USER) ||
|
|
|
|
(isMultipleUsers && principalType !== Principal.USER);
|
|
|
|
|
|
|
|
// Resolve isOwner false if principalType is invalid
|
|
|
|
if (!isPrincipalTypeValid) {
|
|
|
|
process.nextTick(function() {
|
|
|
|
callback(null, false);
|
|
|
|
});
|
|
|
|
return callback.promise;
|
|
|
|
}
|
|
|
|
|
2014-10-13 09:31:27 +00:00
|
|
|
// Is the modelClass User or a subclass of User?
|
|
|
|
if (isUserClass(modelClass)) {
|
2017-02-03 20:53:27 +00:00
|
|
|
var userModelName = modelClass.modelName;
|
|
|
|
// matching ids is enough if principalType is USER or matches given user model name
|
|
|
|
if (principalType === Principal.USER || principalType === userModelName) {
|
|
|
|
process.nextTick(function() {
|
|
|
|
callback(null, matches(modelId, userId));
|
|
|
|
});
|
|
|
|
}
|
2017-01-27 09:51:56 +00:00
|
|
|
return callback.promise;
|
2014-10-13 09:31:27 +00:00
|
|
|
}
|
|
|
|
|
2017-02-18 13:23:37 +00:00
|
|
|
modelClass.findById(modelId, options, function(err, inst) {
|
2014-10-13 09:31:27 +00:00
|
|
|
if (err || !inst) {
|
|
|
|
debug('Model not found for id %j', modelId);
|
2017-02-03 20:53:27 +00:00
|
|
|
return callback(err, false);
|
2014-10-13 09:31:27 +00:00
|
|
|
}
|
|
|
|
debug('Model found: %j', inst);
|
2017-02-03 20:53:27 +00:00
|
|
|
|
2017-01-26 12:07:20 +00:00
|
|
|
var ownerRelations = modelClass.settings.ownerRelations;
|
|
|
|
if (!ownerRelations) {
|
|
|
|
return legacyOwnershipCheck(inst);
|
|
|
|
} else {
|
|
|
|
return checkOwnership(inst);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return callback.promise;
|
|
|
|
|
|
|
|
// NOTE Historically, for principalType USER, we were resolving isOwner()
|
|
|
|
// as true if the model has "userId" or "owner" property matching
|
|
|
|
// id of the current user (principalId), even though there was no
|
|
|
|
// belongsTo relation set up.
|
|
|
|
// Additionaly, the original implementation did not support the
|
|
|
|
// possibility for a model to have multiple related users: when
|
|
|
|
// testing belongsTo relations, the first related user failing the
|
|
|
|
// ownership check induced the whole isOwner() to resolve as false.
|
|
|
|
// This behaviour will be pruned at next LoopBack major release.
|
|
|
|
function legacyOwnershipCheck(inst) {
|
2014-10-13 09:31:27 +00:00
|
|
|
var ownerId = inst.userId || inst.owner;
|
2017-02-03 20:53:27 +00:00
|
|
|
if (principalType === Principal.USER && ownerId && 'function' !== typeof ownerId) {
|
|
|
|
return callback(null, matches(ownerId, userId));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Try to follow belongsTo
|
|
|
|
for (var r in modelClass.relations) {
|
|
|
|
var rel = modelClass.relations[r];
|
|
|
|
// relation should be belongsTo and target a User based class
|
|
|
|
var belongsToUser = rel.type === 'belongsTo' && isUserClass(rel.modelTo);
|
|
|
|
if (!belongsToUser) {
|
|
|
|
continue;
|
|
|
|
}
|
2017-01-26 12:07:20 +00:00
|
|
|
|
2017-02-03 20:53:27 +00:00
|
|
|
// checking related user
|
2017-01-26 12:07:20 +00:00
|
|
|
var relatedUser = rel.modelTo;
|
|
|
|
var userModelName = relatedUser.modelName;
|
|
|
|
var isMultipleUsers = _isMultipleUsers(relatedUser);
|
|
|
|
// a relation can be considered for isOwner resolution if:
|
|
|
|
// 1. the app has a single user model and principalType is 'USER'
|
|
|
|
// 2. the app has multiple user models and principalType is the related user model name
|
|
|
|
if ((!isMultipleUsers && principalType === Principal.USER) ||
|
|
|
|
(isMultipleUsers && principalType === userModelName)) {
|
2017-02-03 20:53:27 +00:00
|
|
|
debug('Checking relation %s to %s: %j', r, userModelName, rel);
|
|
|
|
inst[r](processRelatedUser);
|
|
|
|
return;
|
2013-11-19 19:58:30 +00:00
|
|
|
}
|
2014-11-04 12:52:49 +00:00
|
|
|
}
|
2017-02-03 20:53:27 +00:00
|
|
|
debug('No matching belongsTo relation found for model %j - user %j principalType %j',
|
|
|
|
modelId, userId, principalType);
|
|
|
|
callback(null, false);
|
2014-11-04 12:52:49 +00:00
|
|
|
|
|
|
|
function processRelatedUser(err, user) {
|
2017-01-26 12:07:20 +00:00
|
|
|
if (err || !user) return callback(err, false);
|
|
|
|
|
|
|
|
debug('User found: %j', user.id);
|
|
|
|
callback(null, matches(user.id, userId));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function checkOwnership(inst) {
|
|
|
|
var ownerRelations = inst.constructor.settings.ownerRelations;
|
|
|
|
// collecting related users
|
|
|
|
var relWithUsers = [];
|
|
|
|
for (var r in modelClass.relations) {
|
|
|
|
var rel = modelClass.relations[r];
|
|
|
|
// relation should be belongsTo and target a User based class
|
|
|
|
if (rel.type !== 'belongsTo' || !isUserClass(rel.modelTo)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// checking related user
|
|
|
|
var relatedUser = rel.modelTo;
|
|
|
|
var userModelName = relatedUser.modelName;
|
|
|
|
var isMultipleUsers = _isMultipleUsers(relatedUser);
|
|
|
|
// a relation can be considered for isOwner resolution if:
|
|
|
|
// 1. the app has a single user model and principalType is 'USER'
|
|
|
|
// 2. the app has multiple user models and principalType is the related user model name
|
|
|
|
// In addition, if an array of relations if provided with the ownerRelations option,
|
|
|
|
// then the given relation name is further checked against this array
|
|
|
|
if ((!isMultipleUsers && principalType === Principal.USER) ||
|
|
|
|
(isMultipleUsers && principalType === userModelName)) {
|
|
|
|
debug('Checking relation %s to %s: %j', r, userModelName, rel);
|
|
|
|
if (ownerRelations === true) {
|
|
|
|
relWithUsers.push(r);
|
|
|
|
} else if (Array.isArray(ownerRelations) && ownerRelations.indexOf(r) !== -1) {
|
|
|
|
relWithUsers.push(r);
|
|
|
|
}
|
2014-11-04 12:52:49 +00:00
|
|
|
}
|
2013-11-19 19:58:30 +00:00
|
|
|
}
|
2017-01-26 12:07:20 +00:00
|
|
|
if (relWithUsers.length === 0) {
|
|
|
|
debug('No matching belongsTo relation found for model %j and user: %j principalType: %j',
|
|
|
|
modelId, userId, principalType);
|
|
|
|
return callback(null, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
// check related users: someSeries is used to avoid spamming the db
|
|
|
|
async.someSeries(relWithUsers, processRelation, callback);
|
|
|
|
|
|
|
|
function processRelation(r, cb) {
|
|
|
|
inst[r](function processRelatedUser(err, user) {
|
|
|
|
if (err || !user) return cb(err, false);
|
|
|
|
|
|
|
|
debug('User found: %j (through %j)', user.id, r);
|
|
|
|
cb(null, matches(user.id, userId));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// A helper function to check if the app user config is multiple users or
|
|
|
|
// single user. It can be used with or without a reference user model.
|
|
|
|
// In case no user model is provided, we use the registry to get any of the
|
|
|
|
// user model by type. The relation with AccessToken is used to check
|
|
|
|
// if polymorphism is used, and thus if multiple users.
|
|
|
|
function _isMultipleUsers(userModel) {
|
|
|
|
var oneOfUserModels = userModel || _this.registry.getModelByType('User');
|
|
|
|
var accessTokensRel = oneOfUserModels.relations.accessTokens;
|
|
|
|
return !!(accessTokensRel && accessTokensRel.polymorphic);
|
|
|
|
}
|
2014-10-13 09:31:27 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
Role.registerResolver(Role.AUTHENTICATED, function(role, context, callback) {
|
|
|
|
if (!context) {
|
|
|
|
process.nextTick(function() {
|
2014-11-04 12:52:49 +00:00
|
|
|
if (callback) callback(null, false);
|
2014-10-13 09:31:27 +00:00
|
|
|
});
|
|
|
|
return;
|
2013-11-19 19:58:30 +00:00
|
|
|
}
|
2014-10-13 09:31:27 +00:00
|
|
|
Role.isAuthenticated(context, callback);
|
2013-11-19 19:58:30 +00:00
|
|
|
});
|
|
|
|
|
2014-10-13 09:31:27 +00:00
|
|
|
/**
|
2015-03-12 18:15:36 +00:00
|
|
|
* Check if the user ID is authenticated
|
|
|
|
* @param {Object} context The security context.
|
|
|
|
*
|
|
|
|
* @callback {Function} callback Callback function.
|
|
|
|
* @param {Error} err Error object.
|
|
|
|
* @param {Boolean} isAuthenticated True if the user is authenticated.
|
2017-01-27 09:51:56 +00:00
|
|
|
* @promise
|
2014-10-13 09:31:27 +00:00
|
|
|
*/
|
|
|
|
Role.isAuthenticated = function isAuthenticated(context, callback) {
|
2017-01-27 09:51:56 +00:00
|
|
|
if (!callback) callback = utils.createPromiseCallback();
|
2013-11-19 19:58:30 +00:00
|
|
|
process.nextTick(function() {
|
2014-11-04 12:52:49 +00:00
|
|
|
if (callback) callback(null, context.isAuthenticated());
|
2013-11-19 19:58:30 +00:00
|
|
|
});
|
2017-01-27 09:51:56 +00:00
|
|
|
return callback.promise;
|
2014-10-13 09:31:27 +00:00
|
|
|
};
|
2013-11-20 21:38:14 +00:00
|
|
|
|
2014-10-13 09:31:27 +00:00
|
|
|
Role.registerResolver(Role.UNAUTHENTICATED, function(role, context, callback) {
|
|
|
|
process.nextTick(function() {
|
2014-11-04 12:52:49 +00:00
|
|
|
if (callback) callback(null, !context || !context.isAuthenticated());
|
2014-10-13 09:31:27 +00:00
|
|
|
});
|
2013-11-19 19:58:30 +00:00
|
|
|
});
|
|
|
|
|
2014-10-13 09:31:27 +00:00
|
|
|
Role.registerResolver(Role.EVERYONE, function(role, context, callback) {
|
|
|
|
process.nextTick(function() {
|
2014-11-04 12:52:49 +00:00
|
|
|
if (callback) callback(null, true); // Always true
|
2013-12-12 00:03:48 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2014-10-13 09:31:27 +00:00
|
|
|
/**
|
2015-03-12 18:15:36 +00:00
|
|
|
* Check if a given principal is in the specified role.
|
|
|
|
*
|
|
|
|
* @param {String} role The role name.
|
|
|
|
* @param {Object} context The context object.
|
2014-10-13 09:31:27 +00:00
|
|
|
*
|
2015-03-12 18:15:36 +00:00
|
|
|
* @callback {Function} callback Callback function.
|
|
|
|
* @param {Error} err Error object.
|
|
|
|
* @param {Boolean} isInRole True if the principal is in the specified role.
|
2017-01-27 09:51:56 +00:00
|
|
|
* @promise
|
2014-10-13 09:31:27 +00:00
|
|
|
*/
|
|
|
|
Role.isInRole = function(role, context, callback) {
|
2016-11-21 20:51:43 +00:00
|
|
|
context.registry = this.registry;
|
2014-10-13 09:31:27 +00:00
|
|
|
if (!(context instanceof AccessContext)) {
|
|
|
|
context = new AccessContext(context);
|
|
|
|
}
|
2013-11-20 21:31:30 +00:00
|
|
|
|
2017-01-27 09:51:56 +00:00
|
|
|
if (!callback) {
|
|
|
|
callback = utils.createPromiseCallback();
|
|
|
|
// historically, isInRole is returning the Role instance instead of true
|
|
|
|
// we are preserving that behaviour for callback-based invocation,
|
|
|
|
// but fixing it when invoked in Promise mode
|
|
|
|
callback.promise = callback.promise.then(function(isInRole) {
|
|
|
|
return !!isInRole;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2015-08-13 15:58:41 +00:00
|
|
|
this.resolveRelatedModels();
|
2015-04-01 21:50:36 +00:00
|
|
|
|
2014-10-13 09:31:27 +00:00
|
|
|
debug('isInRole(): %s', role);
|
|
|
|
context.debug();
|
|
|
|
|
|
|
|
var resolver = Role.resolvers[role];
|
|
|
|
if (resolver) {
|
|
|
|
debug('Custom resolver found for role %s', role);
|
2016-04-26 04:35:54 +00:00
|
|
|
|
|
|
|
var promise = resolver(role, context, callback);
|
|
|
|
if (promise && typeof promise.then === 'function') {
|
|
|
|
promise.then(
|
|
|
|
function(result) { callback(null, result); },
|
|
|
|
callback
|
|
|
|
);
|
|
|
|
}
|
2017-01-27 09:51:56 +00:00
|
|
|
return callback.promise;
|
2013-11-12 06:16:51 +00:00
|
|
|
}
|
2014-10-13 09:31:27 +00:00
|
|
|
|
|
|
|
if (context.principals.length === 0) {
|
|
|
|
debug('isInRole() returns: false');
|
|
|
|
process.nextTick(function() {
|
2014-11-04 12:52:49 +00:00
|
|
|
if (callback) callback(null, false);
|
2014-10-13 09:31:27 +00:00
|
|
|
});
|
2017-01-27 09:51:56 +00:00
|
|
|
return callback.promise;
|
2013-11-12 18:47:59 +00:00
|
|
|
}
|
2013-12-12 00:03:48 +00:00
|
|
|
|
2014-10-13 09:31:27 +00:00
|
|
|
var inRole = context.principals.some(function(p) {
|
2013-12-12 00:03:48 +00:00
|
|
|
var principalType = p.type || undefined;
|
|
|
|
var principalId = p.id || undefined;
|
2014-05-03 03:43:03 +00:00
|
|
|
|
2014-10-13 09:31:27 +00:00
|
|
|
// Check if it's the same role
|
|
|
|
return principalType === RoleMapping.ROLE && principalId === role;
|
2013-12-12 00:03:48 +00:00
|
|
|
});
|
|
|
|
|
2014-10-13 09:31:27 +00:00
|
|
|
if (inRole) {
|
|
|
|
debug('isInRole() returns: %j', inRole);
|
|
|
|
process.nextTick(function() {
|
2014-11-04 12:52:49 +00:00
|
|
|
if (callback) callback(null, true);
|
2014-10-13 09:31:27 +00:00
|
|
|
});
|
2017-01-27 09:51:56 +00:00
|
|
|
return callback.promise;
|
2014-10-13 09:31:27 +00:00
|
|
|
}
|
2013-11-12 06:16:51 +00:00
|
|
|
|
2015-08-13 15:58:41 +00:00
|
|
|
var roleMappingModel = this.roleMappingModel;
|
2016-11-15 21:46:23 +00:00
|
|
|
this.findOne({where: {name: role}}, function(err, result) {
|
2014-10-13 09:31:27 +00:00
|
|
|
if (err) {
|
2014-11-04 12:52:49 +00:00
|
|
|
if (callback) callback(err);
|
2014-10-13 09:31:27 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!result) {
|
2014-11-04 12:52:49 +00:00
|
|
|
if (callback) callback(null, false);
|
2014-10-13 09:31:27 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
debug('Role found: %j', result);
|
2013-12-11 17:06:21 +00:00
|
|
|
|
2014-10-13 09:31:27 +00:00
|
|
|
// Iterate through the list of principals
|
|
|
|
async.some(context.principals, function(p, done) {
|
|
|
|
var principalType = p.type || undefined;
|
|
|
|
var principalId = p.id || undefined;
|
|
|
|
var roleId = result.id.toString();
|
2016-04-01 09:14:26 +00:00
|
|
|
var principalIdIsString = typeof principalId === 'string';
|
2013-12-12 00:03:48 +00:00
|
|
|
|
2016-04-01 09:14:26 +00:00
|
|
|
if (principalId !== null && principalId !== undefined && !principalIdIsString) {
|
2014-10-13 09:31:27 +00:00
|
|
|
principalId = principalId.toString();
|
2014-03-19 22:09:20 +00:00
|
|
|
}
|
2014-10-13 09:31:27 +00:00
|
|
|
|
|
|
|
if (principalType && principalId) {
|
2016-11-15 21:46:23 +00:00
|
|
|
roleMappingModel.findOne({where: {roleId: roleId,
|
2016-12-06 14:40:42 +00:00
|
|
|
principalType: principalType, principalId: principalId}},
|
2017-12-12 08:33:15 +00:00
|
|
|
function(err, result) {
|
|
|
|
debug('Role mapping found: %j', result);
|
|
|
|
done(!err && result); // The only arg is the result
|
|
|
|
});
|
2013-12-11 17:06:21 +00:00
|
|
|
} else {
|
2014-10-13 09:31:27 +00:00
|
|
|
process.nextTick(function() {
|
|
|
|
done(false);
|
|
|
|
});
|
2013-12-11 17:06:21 +00:00
|
|
|
}
|
2014-10-13 09:31:27 +00:00
|
|
|
}, function(inRole) {
|
|
|
|
debug('isInRole() returns: %j', inRole);
|
2014-11-04 12:52:49 +00:00
|
|
|
if (callback) callback(null, inRole);
|
2013-12-11 17:06:21 +00:00
|
|
|
});
|
2013-11-12 06:16:51 +00:00
|
|
|
});
|
2017-01-27 09:51:56 +00:00
|
|
|
return callback.promise;
|
2014-10-13 09:31:27 +00:00
|
|
|
};
|
2013-12-11 17:06:21 +00:00
|
|
|
|
2014-10-13 09:31:27 +00:00
|
|
|
/**
|
2015-03-12 18:15:36 +00:00
|
|
|
* List roles for a given principal.
|
|
|
|
* @param {Object} context The security context.
|
2014-10-13 09:31:27 +00:00
|
|
|
*
|
2015-03-12 18:15:36 +00:00
|
|
|
* @callback {Function} callback Callback function.
|
|
|
|
* @param {Error} err Error object.
|
|
|
|
* @param {String[]} roles An array of role IDs
|
2017-01-27 09:51:56 +00:00
|
|
|
* @promise
|
2014-10-13 09:31:27 +00:00
|
|
|
*/
|
2016-11-23 07:51:23 +00:00
|
|
|
Role.getRoles = function(context, options, callback) {
|
2017-01-27 09:51:56 +00:00
|
|
|
if (!callback) {
|
|
|
|
if (typeof options === 'function') {
|
|
|
|
callback = options;
|
|
|
|
options = {};
|
|
|
|
} else {
|
|
|
|
callback = utils.createPromiseCallback();
|
|
|
|
}
|
2016-11-23 07:51:23 +00:00
|
|
|
}
|
2017-01-27 09:51:56 +00:00
|
|
|
if (!options) options = {};
|
|
|
|
|
2016-11-21 20:51:43 +00:00
|
|
|
context.registry = this.registry;
|
2014-10-13 09:31:27 +00:00
|
|
|
if (!(context instanceof AccessContext)) {
|
|
|
|
context = new AccessContext(context);
|
2013-12-12 00:03:48 +00:00
|
|
|
}
|
2014-10-13 09:31:27 +00:00
|
|
|
var roles = [];
|
2015-08-13 15:58:41 +00:00
|
|
|
this.resolveRelatedModels();
|
2013-12-12 00:03:48 +00:00
|
|
|
|
2014-10-13 09:31:27 +00:00
|
|
|
var addRole = function(role) {
|
|
|
|
if (role && roles.indexOf(role) === -1) {
|
|
|
|
roles.push(role);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
var self = this;
|
|
|
|
// Check against the smart roles
|
|
|
|
var inRoleTasks = [];
|
|
|
|
Object.keys(Role.resolvers).forEach(function(role) {
|
|
|
|
inRoleTasks.push(function(done) {
|
|
|
|
self.isInRole(role, context, function(err, inRole) {
|
|
|
|
if (debug.enabled) {
|
|
|
|
debug('In role %j: %j', role, inRole);
|
|
|
|
}
|
|
|
|
if (!err && inRole) {
|
|
|
|
addRole(role);
|
|
|
|
done();
|
|
|
|
} else {
|
|
|
|
done(err, null);
|
2013-12-11 17:06:21 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
2014-10-13 09:31:27 +00:00
|
|
|
});
|
2013-11-12 06:16:51 +00:00
|
|
|
|
2015-08-13 15:58:41 +00:00
|
|
|
var roleMappingModel = this.roleMappingModel;
|
2014-10-13 09:31:27 +00:00
|
|
|
context.principals.forEach(function(p) {
|
|
|
|
// Check against the role mappings
|
|
|
|
var principalType = p.type || undefined;
|
2014-10-22 16:35:25 +00:00
|
|
|
var principalId = p.id == null ? undefined : p.id;
|
2014-11-04 12:52:49 +00:00
|
|
|
|
|
|
|
if (typeof principalId !== 'string' && principalId != null) {
|
2014-10-22 15:05:29 +00:00
|
|
|
principalId = principalId.toString();
|
|
|
|
}
|
2013-11-12 06:16:51 +00:00
|
|
|
|
2014-10-13 09:31:27 +00:00
|
|
|
// Add the role itself
|
|
|
|
if (principalType === RoleMapping.ROLE && principalId) {
|
|
|
|
addRole(principalId);
|
|
|
|
}
|
2013-12-12 00:03:48 +00:00
|
|
|
|
2014-10-13 09:31:27 +00:00
|
|
|
if (principalType && principalId) {
|
|
|
|
// Please find() treat undefined matches all values
|
|
|
|
inRoleTasks.push(function(done) {
|
2016-11-23 07:51:23 +00:00
|
|
|
var filter = {where: {principalType: principalType, principalId: principalId}};
|
|
|
|
if (options.returnOnlyRoleNames === true) {
|
|
|
|
filter.include = ['role'];
|
|
|
|
}
|
|
|
|
roleMappingModel.find(filter, function(err, mappings) {
|
2014-10-13 09:31:27 +00:00
|
|
|
debug('Role mappings found: %s %j', err, mappings);
|
|
|
|
if (err) {
|
2014-11-04 12:52:49 +00:00
|
|
|
if (done) done(err);
|
2014-10-13 09:31:27 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
mappings.forEach(function(m) {
|
2016-11-23 07:51:23 +00:00
|
|
|
var role;
|
|
|
|
if (options.returnOnlyRoleNames === true) {
|
|
|
|
role = m.toJSON().role.name;
|
|
|
|
} else {
|
|
|
|
role = m.roleId;
|
|
|
|
}
|
|
|
|
addRole(role);
|
2014-10-13 09:31:27 +00:00
|
|
|
});
|
2014-11-04 12:52:49 +00:00
|
|
|
if (done) done();
|
2014-10-13 09:31:27 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
2013-12-12 00:03:48 +00:00
|
|
|
|
2014-10-13 09:31:27 +00:00
|
|
|
async.parallel(inRoleTasks, function(err, results) {
|
|
|
|
debug('getRoles() returns: %j %j', err, roles);
|
2014-11-04 12:52:49 +00:00
|
|
|
if (callback) callback(err, roles);
|
2014-10-13 09:31:27 +00:00
|
|
|
});
|
2017-01-27 09:51:56 +00:00
|
|
|
return callback.promise;
|
2014-10-13 09:31:27 +00:00
|
|
|
};
|
2016-04-29 08:50:11 +00:00
|
|
|
|
2016-11-15 21:46:23 +00:00
|
|
|
Role.validatesUniquenessOf('name', {message: 'already exists'});
|
2014-10-13 09:31:27 +00:00
|
|
|
};
|