Created
July 28, 2015 02:51
-
-
Save spencermefford/bc73812f216e0e254ad1 to your computer and use it in GitHub Desktop.
An alternative to extending Loopback's built in models. In our application, we wanted to create a custom role called "ecm-administrator" that would have the ability to create and manage users.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | |
} | |
} | |
); | |
}; |
@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.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I added above relations as you mentioned and I am adding user using
MyUser
model which extends fromUser
model. When I add a role mapping,principalId
is set toNaN
. Here is the code: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 withprincipalId
being set toNaN
.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.
Why it didn't work?
I was using base model
User
but should be using the extended modelMyUser
.This solved the
NaN
issue and I was able to useinclude: 'roles'
on theMyUser
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:
and could query successfully.