This is my implementation. The code can be reused for the client and server . I use it for express / angular website
- Reduce duplicate code, improve consistency between client / server
- Bonus benefit: on the client adapter, we can simply return true to provide maximum access, to check the reliability of the server (since hackers can easily overcome the restriction on the client side)
in the app /both/both.js
var accessList = { //note: same name as controller 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 like an 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 an adapter.
exports.accessList = both.accessList exports.resourceList = both.resourceList exports.havePermission = function(access, resource, userService, entity) {