Express.js/Mongoose user roles and permissions

2020-05-19 08:19发布

问题:

I am creating a fairly simple site with Node, Express and Mongoose. The site needs to have have user roles and permissions. My thoughts are that i'll validate permissions based on user interaction with the data base.

In mongoose is there a way to determine the type of CRUD operation currently being carried out possibly by a user?

回答1:

I've found a solution. It would be great to hear peoples opinions on this.

I have a permissions config object which defines each role and their permissions.

Permissions config object

roles.admin = {
    id: "admin",
    name: "Admin",
    description: "",
    resource : [
        {
            id : 'blog', 
            permissions: ['create', 'read', 'update', 'delete']
        },
        {
            id : 'user',
            permissions: ['create', 'read', 'update', 'delete']
        },
        {
            id : 'journal',
            permissions: ['create', 'read', 'update', 'delete']
        },

    ]
};

roles.editor = {
    id: "editor",
    name: "Editor",
    description: "",
    resource : [
        {
            id : 'blog', 
            permissions: ['create', 'read', 'update', 'delete']
        },
        {
            id : 'user',
            permissions: ['read']
        },
        {
            id : 'journal',
            permissions: ['create', 'read', 'update']
        },

    ]
};

Middleware function

var roles = require('./config');


var permissions = (function () {

  var getRoles = function (role) {

    var rolesArr = [];

    if (typeof role === 'object' && Array.isArray(role)) {

        // Returns selected roles   
        for (var i = 0, len = role.length; i < len; i++) {
            rolesArr.push(roles[role[i]]);
        };
        return rolesArr;

    } else if (typeof role === 'string' || !role) {

        // Returns all roles
        if (!role) {
            for (var role in roles) {
                rolesArr.push(roles[role]);
            };
        }   

        // Returns single role
        rolesArr.push(roles[role]);
        return rolesArr;

    }

},
check = function (action, resource, loginRequired) {

    return function(req, res, next) {

        var isAuth = req.isAuthenticated();

        // If user is required to be logged in & isn't
        if (loginRequired  && !isAuth) {
            return next(new Error("You must be logged in to view this area"));
        }

        if (isAuth || !loginRequired) {

            var authRole = isAuth ? req.user.role : 'user', 
                role =  get(authRole),
                hasPermission = false;

            (function () {
                for (var i = 0, len = role[0].resource.length; i < len; i++){
                    if (role[0].resource[i].id === resource && role[0].resource[i].permissions.indexOf(action) !== -1) {
                        hasPermission = true;
                        return;
                    }
                };
            })();

            if (hasPermission) {
                next();
            } else {
                return next(new Error("You are trying to " + action + " a " + resource + " and do not have the correct permissions."));
            }

        }
    }
}

return {
    get : function (role) {

        var roles = getRoles(role);

        return roles;
    },
    check : function (action, resource, loginRequired) {
        return check(action, resource, loginRequired);
    }
}

})();

module.exports = permissions;

Then i created a middleware function, when the check method gets called it gets the users role from the req object (req.user.role). It then looks at the params passed to the middleware and cross references them with those in the permissions config object.

Route with middlware

app.get('/journal', `**permissions.check('read', 'journal')**`, function (req, res) {
     // do stuff
};


回答2:

This is my implementation. The code is reusable for client and server. I use it for my express/angular website

  1. Reduce code duplicate, better consistence between client/server
  2. Bonus benefit: on client's adapter, we can simply return true to grant max access to test the robustness of server (since hackers and easily overcome client side restrict )

in app/both/both.js

var accessList = {
    //note: same name as controller's function name
    assignEditor: 'assignEditor'

    ,adminPage: 'adminPage'
    ,editorPage: 'editorPage'
    ,profilePage: 'profilePage'

    ,createArticle: 'createArticle'
    ,updateArticle: 'updateArticle'
    ,deleteArticle: 'deleteArticle'
    ,undeleteArticle: 'undeleteArticle'
    ,banArticle: 'banArticle'
    ,unbanArticle: 'unbanArticle'

    ,createComment: 'createComment'
    ,updateComment: 'updateComment'
    ,deleteComment: 'deleteComment'
    ,undeleteComment: 'undeleteComment'
    ,banComment: 'banComment'
    ,unbanComment: 'unbanComment'

    ,updateProfile: 'updateProfile'

}
exports.accessList = accessList

var resourceList = {
    //Note: same name as req.resource name
    profile: 'profile'
    ,article: 'article'
    ,comment: 'comment'
}
exports.resourceList = resourceList

var roleList = {
    admin: 'admin'
    ,editor: 'editor'
    ,entityCreator: 'entityCreator'
    ,profileOwner: 'profileOwner' //creator or profile owner
    ,normal: 'normal' //normal user, signed in
    ,visitor: 'visitor' //not signed in, not used, open pages are uncontrolled
}

var permissionList = {}

permissionList[accessList.assignEditor]     = [roleList.admin]

permissionList[accessList.adminPage]        = [roleList.admin]
permissionList[accessList.editorPage]       = [roleList.admin, roleList.editor]
permissionList[accessList.profilePage]      = [roleList.admin, roleList.editor, roleList.normal]

permissionList[accessList.createArticle]    = [roleList.admin, roleList.editor, roleList.normal]
permissionList[accessList.updateArticle]    = [roleList.admin, roleList.editor, roleList.entityCreator]
permissionList[accessList.deleteArticle]    = [roleList.admin, roleList.editor, roleList.entityCreator]
permissionList[accessList.undeleteArticle]  = [roleList.admin, roleList.editor, roleList.entityCreator]
permissionList[accessList.banArticle]       = [roleList.admin, roleList.editor]
permissionList[accessList.unbanArticle]     = [roleList.admin, roleList.editor]

permissionList[accessList.createComment]    = [roleList.admin, roleList.editor, roleList.normal]
permissionList[accessList.updateComment]    = [roleList.admin, roleList.editor, roleList.entityCreator]
permissionList[accessList.deleteComment]    = [roleList.admin, roleList.editor, roleList.entityCreator]
permissionList[accessList.undeleteComment]  = [roleList.admin, roleList.editor, roleList.entityCreator]
permissionList[accessList.banComment]       = [roleList.admin, roleList.editor]
permissionList[accessList.unbanComment]     = [roleList.admin, roleList.editor]

permissionList[accessList.updateProfile]    = [roleList.admin, roleList.profileOwner]



var getRoles = function(access, resource, isAuthenticated, entity, user) {
    var roles = [roleList.visitor]
    if (isAuthenticated) {
        roles = [roleList.normal]
        if (user.username === 'admin')
            roles = [roleList.admin]
        else if (user.type === 'editor')
            roles = [roleList.editor]


        if (resource) {
            if (resource === resourceList.profile) {
                //Note: on server _id is a object, client _id is string, which does not have equals method
                if (entity && entity._id.toString() === user._id.toString())
                    roles.push(roleList.profileOwner)
            }
            else if (resource === resourceList.article) {
                if (entity && entity.statusMeta.createdBy._id.toString() === user._id.toString())
                    roles.push(roleList.entityCreator)
            }
            else if (resource === resourceList.comment) {
                if (entity && entity.statusMeta.createdBy._id.toString() === user._id.toString())
                    roles.push(roleList.entityCreator)
            }
        }
    }
    return roles
}


exports.havePermission = function(access, resource, isAuthenticated, entity, user) {
    var roles = getRoles(access, resource, isAuthenticated, entity, user)


    //Note: we can implement black list here as well, like IP Ban

    if (!permissionList[access])
        return true

    for (var i = 0; i < roles.length; i++) {
        var role = roles[i]
        if (permissionList[access].indexOf(role) !== -1)
            return true
    }
    return false

}

Then on app/server/helper.js (act as adapter)

var both = require(dir.both + '/both.js')
exports.accessList = both.accessList
exports.resourceList = both.resourceList
exports.havePermission = function(access, resource, req) {
    return both.havePermission(access, resource, req.isAuthenticated(), req[resource], req.user)
}


//todo: use this function in other places
exports.getPermissionError = function(message) {
    var err = new Error(message || 'you do not have the permission')
    err.status = 403
    return err
}

exports.getAuthenticationError = function(message) {
    var err = new Error(message || 'please sign in')
    err.status = 401
    return err
}

exports.requiresPermission = function(access, resource) {
    return function(req, res, next) {
        if (exports.havePermission(access, resource, req))
            return next()
        else {
            if (!req.isAuthenticated())
                return next(exports.getAuthenticationError())
            else
                return next(exports.getPermissionError())
        }
    }
}

on app/client/helper.js, also act as adapter.

exports.accessList = both.accessList
exports.resourceList = both.resourceList
exports.havePermission = function(access, resource, userService, entity) {
    //Note: In debugging, we can grant client helper all access, and test robustness of server
    return both.havePermission(access, resource, userService.isAuthenticated(), entity, userService.user)
}


回答3:

I personnally took inspiration from ghost. In my config there is the perms, and permissions.jsexport a canThisfunction that take the current logged user. Here is the whole project

Part of my config file

"user_groups": {
    "admin": {
      "full_name": "Administrators",
      "description": "Adminsitators.",
      "allowedActions": "all"
    },
    "modo": {
      "full_name": "Moderators",
      "description": "Moderators.",
      "allowedActions": ["mod:*", "comment:*", "user:delete browse add banish edit"]
    },
    "user": {
      "full_name": "User",
      "description": "User.",
      "allowedActions": ["mod:browse add star", "comment:browse add", "user:browse"]
    },
    "guest": {
      "full_name": "Guest",
      "description": "Guest.",
      "allowedActions": ["mod:browse", "comment:browse", "user:browse add"]
    }
  },

mongoose = require("mongoose")
###
This utility function determine whether an user can do this or this
using the permissions. e. g. "mod" "delete"

@param userId the id of the user
@param object the current object name ("mod", "user"...)
@param action to be executed on the object (delete, edit, browse...)
@param owner the optional owner id of the object to be "actionned"
###

# **Important this is a promise but to make a lighter code I removed it**
exports.canThis = (userId, object, action, ownerId, callback) ->
  User = mongoose.model("User")
  if typeof ownerId is "function"
    callback = ownerId
    ownerId = undefined
  if userId is ""
    return process(undefined, object, action, ownerId, callback)
  User.findById(userId, (err, user) ->
    if err then return callback err
    process(user, object, action, ownerId, callback)
  )


process = (user, object, action, ownerId, callback) ->
  if user then role = user.role or "user"
  group = config.user_groups[role or "guest"]
  if not group then return callback(new Error "No suitable group")

  # Parses the perms
  actions = group.allowedActions
  for objAction in actions when objAction.indexOf object is 0
    # We get all the allowed actions for the object and group
    act = objAction.split(":")[1]
    obj = objAction.split(":")[0]
    if act.split(" ").indexOf(action) isnt -1 and obj is object
      return callback true

  callback false

config = require "../config"

Usage example:

exports.edit = (userid, name) ->
  # Q promise
  deferred = Q.defer()
  # default value
  can = false
  # We check wheteher it can or not
  canThis(userid, "user", "edit").then((can)->
    if not userid
      return deferred.reject(error.throwError "", "UNAUTHORIZED")
    User = mongoose.model "User"
    User.findOne({username: name}).select("username location website public_email company bio").exec()
  ).then((user) ->
    # Can the current user do that?
    if not user._id.equals(userid) and can is false
      return deferred.reject(error.throwError "", "UNAUTHORIZED")
    # Done!
    deferred.resolve user
  ).fail((err) ->
    deferred.reject err
  )
  deferred.promise

Perhaps what I've done isn't good, but it works well as far as I can see.



回答4:

Yes, you can access that through the request argument.

app.use(function(req,res,next){
     console.log(req.method);
});

http://nodejs.org/api/http.html#http_message_method

Edit:

Misread your question. It would probably just be better to assign user permissions and allow access to the database based upon the permissions. I don't understand what you mean by validate by means of interaction with the database. If you are already allowing them to interact with the database and they don't have the proper permissions to do so, isn't that a security issue?



回答5:

Check the Node module permission for that matter. It's pretty simple concept, I hope they'll allow all CRUD methods too.