Page Contents

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.

/server/boot/script.js

User.create([
    {username: 'John', email: [email protected]', password: 'opensesame'},
    {username: 'Jane', email: [email protected]', password: 'opensesame'},
    {username: 'Bob', email: [email protected]', 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.

/common/models/model.json

{
  "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: 

  1. String name of the role in question.
  2. 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:

/server/boot/script.js

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);
        }
      });
    });
  });
};

Using the dynamic role defined above, we can restrict access of project information to users that are team members of the project.

/common/models/model.json

{
  "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 does reject() 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