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.
Note:
To qualify a $owner
, the target model needs to have a belongsTo relation to the User model (or a model that extends User)
and property matching the foreign key of the target model instance.
The check for $owner
is performed only for a remote method that has ‘:id’ on the path, for example, GET /api/users/:id
.
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) {
// Q: Is the current request accessing a Project?
if (context.modelName !== 'project') {
// A: No. This role is only for projects: callback with FALSE
return process.nextTick(() => cb(null, false));
}
//Q: Is the user logged in? (there will be an accessToken with an ID if so)
var userId = context.accessToken.userId;
if (!userId) {
//A: No, user is NOT logged in: callback with FALSE
return process.nextTick(() => cb(null, false));
}
// Q: Is the current logged-in user associated with this Project?
// Step 1: lookup the requested project
context.model.findById(context.modelId, function(err, project) {
// A: The datastore produced an error! Pass error to callback
if(err) return cb(err);
// A: There's no project by this ID! Pass error to callback
if(!project) return cb(new Error("Project not found"));
// Step 2: check if User is part of the Team associated with this Project
// (using count() because we only want to know if such a record exists)
var Team = app.models.Team;
Team.count({
ownerId: project.ownerId,
memberId: userId
}, function(err, count) {
// A: The datastore produced an error! Pass error to callback
if (err) return cb(err);
if(count > 0){
// A: YES. At least one Team associated with this User AND Project
// callback with TRUE, user is role:`teamMember`
return cb(null, true);
}
else{
// A: NO, User is not in this Project's Team
// callback with FALSE, user is NOT role:`teamMember`
return cb(null, false);
}
});
});
});
};
Note:
Why does the code above wrap some callback invocations in process.nextTick(()=> cb(...))
, but not others.
In asynchronous functions like this one that take a callback and then pass results to it at a later time, you must call the callback at a later time, not right away (synchronously).
Call the callback from a function passed to process.nextTick
in places where it would otherwise be called synchronously.
Calls from the findById
or count
callbacks are already guaranteed to happen at a later time as they access the database, an asynchronous operation, so we don’t need to wrap those calls in process.nextTick
.
For more information, see Designing APIs for Asynchrony.
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"
}
REVIEW COMMENT from Rand
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