-
-
Save spencermefford/bc73812f216e0e254ad1 to your computer and use it in GitHub Desktop.
module.exports = function (app) { | |
var _ = require('lodash'); | |
var User = app.models.User; | |
var Role = app.models.Role; | |
var RoleMapping = app.models.RoleMapping; | |
var ACL = app.models.ACL; | |
/* | |
* Configure ACL's | |
*/ | |
ACL.create({ | |
model: 'User', | |
property: '*', | |
accessType: '*', | |
principalType: 'ROLE', | |
principalId: 'ecm-administrator', | |
permission: 'ALLOW' | |
}, function (err, acl) { // Create the acl | |
if (err) console.error(err); | |
}); | |
ACL.create({ | |
model: 'Role', | |
property: '*', | |
accessType: '*', | |
principalType: 'ROLE', | |
principalId: 'ecm-administrator', | |
permission: 'ALLOW' | |
}, function (err, acl) { // Create the acl | |
if (err) console.error(err); | |
}); | |
ACL.create({ | |
model: 'RoleMapping', | |
property: '*', | |
accessType: '*', | |
principalType: 'ROLE', | |
principalId: 'ecm-administrator', | |
permission: 'ALLOW' | |
}, function (err, acl) { // Create the acl | |
if (err) console.error(err); | |
}); | |
/* | |
* Add hooks | |
*/ | |
RoleMapping.observe('before save', function filterProperties(ctx, next) { | |
/* | |
* Since there is no built in method to add users to roles in Loopback via REST API, we have leveraged | |
* the hasManyThrough relationship to handle this. Unfortunately, the RoleMapping model has an extra | |
* field called principalType that a typical join table would not have. We have to manually set this. | |
*/ | |
if (_.isEmpty(ctx.instance.principalType)) { // If no principalType has been set... | |
ctx.instance.principalType = RoleMapping.USER; // Set it to USER since it's likely that the User REST API is creating this | |
} | |
if (!_.isEmpty(ctx.instance.userId)) { | |
ctx.instance.principalId = ctx.instance.userId; | |
ctx.instance.unsetAttribute('userId'); | |
} | |
next(); | |
}); | |
/* | |
* Configure relationships | |
*/ | |
RoleMapping.belongsTo(User); | |
RoleMapping.belongsTo(Role); | |
User.hasMany(Role, {through: RoleMapping, foreignKey: 'principalId'}); | |
User.hasMany(RoleMapping, {foreignKey: 'principalId'}); | |
Role.hasMany(User, {through: RoleMapping, foreignKey: 'roleId'}); | |
/* | |
* Add additional attributes to models. | |
*/ | |
Role.defineProperty('label', { type: 'string' }); // Add a role label that is user readable | |
User.defineProperty('firstName', { type: 'string' }); // Give the user a first name field | |
User.defineProperty('lastName', { type: 'string' }); // Give the user a last name field | |
/** | |
* Add a user to the given role. | |
* @param {string} userId | |
* @param {string} roleId | |
* @param {Function} cb | |
*/ | |
User.addRole = function(userId, roleId, cb) { | |
var error; | |
User.findOne({ where: { id: userId } }, function(err, user) { // Find the user... | |
if (err) cb(err); // Error | |
if (!_.isEmpty(user)) { | |
Role.findOne({ where: { id: roleId } }, function(err, role) { // Find the role... | |
if (err) cb(err); // Error | |
if (!_.isEmpty(role)) { | |
RoleMapping.findOne({ where: { principalId: userId, roleId: roleId } }, function(err, roleMapping) { // Find the role mapping... | |
if (err) cb(err); // Error | |
if (_.isEmpty(roleMapping)) { // Only create if one doesn't exist to avoid duplicates | |
role.principals.create({ | |
principalType: RoleMapping.USER, | |
principalId: user.id | |
}, function(err, principal) { | |
if (err) cb(err); // Error | |
cb(null, role); // Success, return role object | |
}); | |
} else { | |
cb(null, role); // Success, return role object | |
} | |
}); | |
} else { | |
error = new Error('Role.' + roleId + ' was not found.'); | |
error.http_code = 404; | |
cb(error); // Error | |
} | |
}); | |
} else { | |
error = new Error('User.' + userId + ' was not found.'); | |
error.http_code = 404; | |
cb(error); // Error | |
} | |
}); | |
}; | |
User.remoteMethod( | |
'addRole', | |
{ | |
accepts: [ | |
{arg: 'userId', type: 'string'}, | |
{arg: 'roleId', type: 'string'} | |
], | |
http: { | |
path: '/add-role', | |
verb: 'post' | |
}, | |
returns: {type: 'object', root: true} | |
} | |
); | |
/** | |
* Remove a user from the given role. | |
* @param {string} userId | |
* @param {string} roleId | |
* @param {Function} cb | |
*/ | |
User.removeRole = function(userId, roleId, cb) { | |
var error; | |
User.findOne({ where: { id: userId } }, function(err, user) { // Find the user... | |
if (err) cb(err); // Error | |
if (!_.isEmpty(user)) { | |
Role.findOne({ where: { id: roleId } }, function(err, role) { // Find the role... | |
if (err) cb(err); // Error | |
if (!_.isEmpty(role)) { | |
RoleMapping.findOne({ where: { principalId: userId, roleId: roleId } }, function(err, roleMapping) { // Find the role mapping... | |
if (err) cb(err); // Error | |
if (!_.isEmpty(roleMapping)) { | |
roleMapping.destroy(function(err) { | |
if (err) cb(err); // Error | |
cb(null, role); // Success, return role object | |
}); | |
} else { | |
cb(null, role); // Success, return role object | |
} | |
}); | |
} else { | |
error = new Error('Role.' + roleId + ' was not found.'); | |
error.http_code = 404; | |
cb(error); // Error | |
} | |
}); | |
} else { | |
error = new Error('User.' + userId + ' was not found.'); | |
error.http_code = 404; | |
cb(error); // Error | |
} | |
}); | |
}; | |
User.remoteMethod( | |
'removeRole', | |
{ | |
accepts: [ | |
{arg: 'userId', type: 'string'}, | |
{arg: 'roleId', type: 'string'} | |
], | |
http: { | |
path: '/remove-role', | |
verb: 'post' | |
} | |
} | |
); | |
}; |
I added above relations as you mentioned and I am adding user using MyUser
model which extends from User
model. When I add a role mapping, principalId
is set to NaN
. Here is the code:
MyUser.create( {name: 'Ritesh Jagga', email: '[email protected]', password: 'test'}),
function (error, user) {
Role.findOne({where: {name: 'super'}}, function (error, role) {
if (error) throw error;
role.principals.create({
principalType: RoleMapping.USER,
principalId: user.id
}, function (error, principal) {
if (error) throw error;
console.log('Added ' + user.name + ' as super user.');
});
});
Is it due to ObjectId
difference as mentioned in this issue strongloop/loopback#1441?
I am not using the hook that you added to RoleMapping
which I believe has nothing to do with principalId
being set to NaN
.
Can you please help to resolve this?
Update
This was for MongoDB connector. I eventually got it working using the below code in the script file where you are defining other properties.
RoleMapping.belongsTo(MyUser);
RoleMapping.belongsTo(Role);
MyUser.hasMany(Role, {through: RoleMapping, foreignKey: 'principalId'});
MyUser.hasMany(RoleMapping, {foreignKey: 'principalId'});
Role.hasMany(MyUser, {as: 'users', through: RoleMapping, foreignKey: 'roleId'});
Why it didn't work?
I was using base model User
but should be using the extended model MyUser
.
This solved the NaN
issue and I was able to use include: 'roles'
on the MyUser
model to successfully get roles of a user.
Few more issues
I couldn't get list of users belonging to a particular role. For that I modified first 2 belongsTo relation configuration like this:
RoleMapping.belongsTo(MyUser, {foreignKey: 'principalId'});
RoleMapping.belongsTo(Role, {foreignKey: 'roleId'});
and could query successfully.
@spencermefford Thanks for this, nice work 👍
I've made a fork which has a few changes here: https://gist.github.com/leftclickben/aa3cf418312c0ffcc547
Most of my changes are purely code style, but there are two things worth pointing out:
- When you do
if (err) cb(err);
you really want to doif (err) return cb(err);
(addreturn
) otherwise the method will continue and strange things may happen. For example, in yourUser.addRole
method, if theUser.find
comes back with an error, you will call the callback with that error, and then call it again with your own error further down. Adding thereturn
will prevent this, and can also help prevent nesting blocks too deeply. - This is more opinion on API design than an actual problem: I changed the
addRole
/removeRole
methods from static methods (User.addRole
) to instance methods (User.prototype.addRole
), and specifiedisStatic: false
in the API config. This way you don't need the wholeUser.find
part because the method is called on theUser
instance which is retrieved for you by the framework / router. That is,this
inside the method refers to the instance retrieved by the id in the URL.
My version also differs in that I'm passing the role name
in rather than the id
, because my clients only know about names and have no canonical source for the correct id
values for the roles.
I really appreciate your effort in this matter and willingness to share it.