LoopBack enables you to define both static and dynamic roles. Static roles are stored in a data source and are mapped to users. In contrast, dynamic roles aren’t assigned to users and are determined during access.
Static roles
Here is an example defining a new static role and assigning a user to that role.
User.create([ {username: 'John', email: 'john@doe.com', password: 'opensesame'}, {username: 'Jane', email: 'jane@doe.com', password: 'opensesame'}, {username: 'Bob', email: 'bob@projects.com', password: 'opensesame'} ], function(err, users) { if (err) return cb(err); //create the admin role Role.create({ name: 'admin' }, function(err, role) { if (err) cb(err); //make bob an admin role.principals.create({ principalType: RoleMapping.USER, principalId: users[2].id }, function(err, principal) { cb(err); }); }); });
Now you can use the role defined above in the access controls. For example, add the following to common/models/project.json to enable users in the "admin" role to call all REST APIs.
{ "accessType": "EXECUTE", "principalType": "ROLE", "principalId": "admin", "permission": "ALLOW", "property": "find" }
Dynamic roles
Sometimes static roles aren’t flexible enough. LoopBack also enables you to define dynamic roles that are defined at run-time.
LoopBack provides the following built-in dynamic roles.
Role object property | String value | Description |
---|---|---|
Role.OWNER | $owner | Owner of the object |
Role.AUTHENTICATED | $authenticated | authenticated user |
Role.UNAUTHENTICATED | $unauthenticated | Unauthenticated user |
Role.EVERYONE | $everyone | Everyone |
The first example used the “$owner” dynamic role to allow access to the owner of the requested project model.
Use Role.registerResolver()
to set up a custom role handler in a boot script. This function takes two parameters:
- String name of the role in question.
- Function that determines if a principal is in the specified role. The function signature must be
function(role, context, callback)
.
For example, here is the role resolver from loopback-example-access-control:
module.exports = function(app) { var Role = app.models.Role; Role.registerResolver('teamMember', function(role, context, cb) { function reject(err) { if(err) { return cb(err); } cb(null, false); } if (context.modelName !== 'project') { // the target model is not project return reject(); } var userId = context.accessToken.userId; if (!userId) { return reject(); // do not allow anonymous users } // check if userId is in team table for the given project id context.model.findById(context.modelId, function(err, project) { if(err || !project) { reject(err); } var Team = app.models.Team; Team.count({ ownerId: project.ownerId, memberId: userId }, function(err, count) { if (err) { return reject(err); } cb(null, count > 0); // true = is a team member }); }); }); };
Using the dynamic role defined above, we can restrict access of project information to users that are team members of the project.
{ "accessType": "READ", "principalType": "ROLE", "principalId": "teamMember", "permission": "ALLOW", "property": "findById" }
I assume that you could define any number of role resolvers in a single boot script. Is that true?
Need some more explanation:
- What is
context
and where does it come from?- is an object provided by loopback to give the user context into the request (ie. when the request comes in, they can access context.req or context.res, like you normally would with middleware)
- What is purpose of process.nextTick() ?
- this is part of node and requires a whole explanation into the heart of node, (ie the event loop). basically this delays the `cb` call until the next `tick` of the event loop, so the machinery can process all events currently in the queue before processing your callback.
- what is return
reject()
and where doesreject()
come from?- reject is a function we define inline ie function reject() ...
- we basically say, if request is not a a request to api/projects (by checking the modelname), do not execute the rest of the script and reject the request (do nothing).
- do this by calling "reject", which will return false during the next cycle of the event loop (returning false in the second param means the person is NOT a team member, ie is not in that role)
- The logic at the end that determines whether teamMember is in team based on Team.count() seems a bit convoluted. Explain, esp. how cb works in this specific case.
- This example is provided verbatim by raymond
- but the idea is you have a team table and you do a query to count the "rows" because the requester can be on multiple teams, so any number you get greater than 0 is ok