What is the best way to implement roles and permission in Express REST Api

Role rights in Node.js


Part 1:What is role and rights?

Role rights implementation is important part of any software.Role is a position of responsibility and every responsibility enjoys some rights given to them.There may be some common rights between few roles and some rights may strictly belong to specific role.

Rights are Urls that a role is authorised to access.It is thus necessary to create collection in db storing information of rights to a role. We have role collection schema as

const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const RoleSchema = new Schema({
  roleId:{
    type:String,
    unique:true,
    required:[true,"Role Id required"]
  },
  type:{
    type:String,
    unique:true,
    required:[true,"Role type is required"]
  },
  rights:[{
    name: String,
    path: String,
    url: String
  }]
});
module.exports = Role = mongoose.model('role',RoleSchema);

Now remember every role that is supose to exist is in Role collection and of above schema type.

In schema rights array of object we see the object has keys:

  • name(for name of url like "set-username")
  • path(for base path hit "/users/")
  • url(requested url or complete path "/users/set-username")

Thus if a user with role user has right to change username then he can hit url /users/set-username.However a wanderer will not be able to access this url.A higher role like admin & superadmin should logically have access to all lower role rights(urls).

Role in real application are:-

  1. Wanderer (Someone who is just visiting our site.He should be able to access all public routes.Simple Urls/Public urls accessible to all thus need not to make a seprate role for this as it is not any authenticated right.)
  2. Guest (Someone who has registered but not verified say email not verified).
  3. User (Someone who has his verified email)
  4. Admin (Made a Admin by SuperAdmin after verifying.he enjoy most of rights)
  5. Superadmin (Master of application.He enjoy some more sophisticated rights.More rights then admin)

Till Now we have understood what excatly is right and how it is mapped to a role.


Part 1.5:Registerd Urls/Config Urls

Here we have a file called registeredUrls.js which is like:

module.exports = {
    simple:{
        "/":[""],
        '/users/':["login","register"],
    },
    auth:{
        //admin
        '/admin/': ['load-users' , 'set-new-password','delete-user'],
        '/teacher/':["add-teacher","delete-teacher","edit-teacher"],
        '/student/':["add-student","delete-student","edit-student","test-result"],
        
        //user
        '/test/':["view-test","submit-test"],
        '/profile/': ['change-username', 'update-profile-data',  'set-new-password', 'upload-pic', 'update-social-links'],
        '/teacher/':['load-teacher'],
        '/student/':['load-student']

    }
}

Similarly confgUrls.js

const configUrls= {
    '/roles/': ['get-rights', 'create', 'update-rights', 'load', 'delete', 'assign']
}
module.exports = configUrls;

Part 2:Creating SuperAdmin

This is most essential part of application.Whenever server starts for first time or restart/reboot this step occurs.In config/init.js follow procedure:

  1. Load All simple urls(public) and Auth Urls(admin & users) & super-admin-specific urls into superAdminRights[].
  2. Then run a function to create a user with role superadmin if doesn't exist.
  3. Get a Role of type:"superadmin" if found:replace its rights with new rights(superAdminRights).else:create Role of type :"superadmin" and then fill its rights(superAdminRights).

At end of this function call we are always sure that we have a superadmin in application with all its sophisticated urls/rights initialised.


Part 3:Super-Admin-Specific-Urls

These are rights that are enjoyed by super admin only and must be maintained in seprate file in parallel to registerd url file.These include url rights which map routes used only by superadmin. Here we have routes to create role,load roles,get-rights for a roleId,update-rights for roleId/role type, assign-role to a user,delete a role.

For each user in code we need to change their role from guest to user(say after email verification).Or guest/user to admin by superadmin using assign-role url.Then updating admin rights using route update-rights.

The process ensure that every role has A collection Document and filled rights there.


Part 4:Authenticator Middleware

This heart of our RBACS logic.Here we use a middleware which follow process:

1. Fill all authentication required urls in a [AUTH_URLS] with auth-urls(registeredUrls.js) & super-admin-specific-urls(confgUrls.js) and simple url in different [SIMPLE_URLS].

2. Then check if (AUTH_URLS.indexOf(request.url) > -1){3rd step} else if (SIMPLE_URLS.indexOf(request.url)>-1){then it is public url so simple allow next()} else { response unknown url}

3. In this step we know that url being requested in AUTH_URLS thus required token check for authorization token header if found then extract token from it then{4th step}.if no authorization header found response "required token"/"unknown".

4. Token found, now verify that this token has a valid session.If yes {5th step} else token session not found login again. 5. validate jwt token verifying it if verified{6.step} else response "not valid jwt token".

6.till now correct url requested & token session exist & jwt valid token.Now using role-type information from session(stored in session is basic info of user and token) find in Role for this user session role-type thus we have its rights[].Now see if the request.url is included in rights[].if found {7th step} else {reponse "role not found/unknown user"}.

7. if found then {has access to this url next() } else { response "access denided"}


Create Tables

First you need to create your tables that will hold the associations between roles, permissions, and resources:

  1. Create a roles table ('Admin', 'User', 'Guest')
  2. Create a resources table ('Users', 'Projects', 'Programs')
  3. Create a permissions table ('Create', 'Read','Write','Delete','Deny')
  4. Create a junction table with all three tables as sources

You may not need that kind of granularity for the permissions table, but some people like it. For example, you don't really need 'Deny', since you just check for Read != true.

Now when you want the permissions of a role on a resource, you just look up role_id and resource_id and check for which permissions are set to true.

Create Middleware

Since you're using express, middleware will be easy to add. For example, let's say you have a router called users:

users.post('/', getAuth, handleUserPost)

Assuming you have a token of some sort on the request that identifies the user making the post, and attaching the user instance to the request object you can do this:

getAuth = function (req, res, next) {
  if(req.user) { 
    db.getPerms({role_id: req.user.role_id, resource_id: req.resource.id})
       .then(function(perms){
          var allow = false;
          //you can do this mapping of methods to permissions before the db call and just get the specific permission you want. 
          perms.forEach(function(perm){
              if (req.method == "POST" && perms.create) allow = true;
              else if (req.method == "GET" && perms.read) allow = true;
              else if (req.method == "PUT" && perms.write) allow = true;
              else if (req.method == "DELETE" && perm.delete) allow = true;

          })
          if (allow) next();
          else res.status(403).send({error: 'access denied'});
       })//handle your reject and catch here
   } else res.status(400).send({error: 'invalid token'})
}

That code was roughed in just for this example, so I wouldn't go copy and pasting it, but it should give you the right idea.