diff --git a/client/coral-admin/src/containers/LayoutContainer.js b/client/coral-admin/src/containers/LayoutContainer.js index 8a3e0b0d2..b72acb59c 100644 --- a/client/coral-admin/src/containers/LayoutContainer.js +++ b/client/coral-admin/src/containers/LayoutContainer.js @@ -38,6 +38,8 @@ class LayoutContainer extends Component { } if (roleUtils.canAccessAdmin(user) && loggedIn) { return ; + } else if (loggedIn) { + return

you do not have permission to see this page.

; } return ; } diff --git a/client/coral-framework/utils/roles.js b/client/coral-framework/utils/roles.js index b457ff1b3..63d34fb41 100644 --- a/client/coral-framework/utils/roles.js +++ b/client/coral-framework/utils/roles.js @@ -1,10 +1,30 @@ -import includes from 'lodash/includes'; +import intersection from 'lodash/intersection'; -export default { - canAccessConfig: (user) => includes(user.roles, 'ADMIN'), - canChangeRoles: user => includes(user.roles, 'ADMIN'), - hasStaffTag: user => includes(user.roles, 'ADMIN', 'MODERATOR', 'STAFF'), - canViewUserEmails: user => includes(user.roles, 'ADMIN'), - canModerate: user => includes(user.roles, 'ADMIN', 'MODERATOR'), - canAccessAdmin: user => includes(user.roles, 'ADMIN', 'MODERATOR') +const basicRoles = { + hasStaffTag: ['ADMIN', 'MODERATOR', 'STAFF'] +}; + +const queryRoles = { + canAccessConfig: ['ADMIN', 'MODERATOR'], + canAccessAdmin: ['ADMIN', 'MODERATOR'], + canViewUserEmails: ['ADMIN'] +}; + +const mutationRoles = { + canChangeRoles: ['ADMIN'], + canModerateComments: ['ADMIN', 'MODERATOR'] +}; + +const roles = {...basicRoles, ...queryRoles, ...mutationRoles}; + +export const can = (user, perms) => { + for (let perm in perms) { + const role = roles[perm]; + if (typeof role === 'undefined') { + continue; + } + let grant = intersection(role, user.roles).length > 0; + return grant; + } + return false; }; diff --git a/graph/resolvers/action.js b/graph/resolvers/action.js index 393024fe1..cf7be3beb 100644 --- a/graph/resolvers/action.js +++ b/graph/resolvers/action.js @@ -11,7 +11,7 @@ const Action = { // This will load the user for the specific action. We'll limit this to the // admin users only or the current logged in user. user({user_id}, _, {loaders: {Users}, user}) { - if (user && (user.hasRole('ADMIN') || user_id === user.id)) { + if (user && (user.canViewOtherUsers() || user_id === user.id)) { return Users.getByID.load(user_id); } } diff --git a/models/user.js b/models/user.js index 179e6f7a5..ab88fe6f1 100644 --- a/models/user.js +++ b/models/user.js @@ -2,6 +2,7 @@ const mongoose = require('../services/mongoose'); const bcrypt = require('bcrypt'); const uuid = require('uuid'); const intersection = require('lodash/intersection'); +const can = require('../perms'); // USER_ROLES is the array of roles that is permissible as a user role. const USER_ROLES = [ @@ -269,13 +270,7 @@ const USER_GRAPH_OPERATIONS = [ * operation. */ UserSchema.method('can', function(...actions) { - if (actions.some((action) => USER_GRAPH_OPERATIONS.indexOf(action) === -1)) { - throw new Error(`invalid actions: ${actions}`); - } - - if (this.status === 'BANNED') { - return false; - } + return can(this, null, actions); if (actions.some((action) => action === 'mutation:setUserStatus' || action === 'mutation:suspendUser' || action === 'mutation:setCommentStatus') && !this.canSetUserStatus()) { return false; diff --git a/perms/index.js b/perms/index.js new file mode 100644 index 000000000..c9b29ea85 --- /dev/null +++ b/perms/index.js @@ -0,0 +1,48 @@ +const root = require('./rootReducer'); +const queries = require('./queryReducer'); +const mutations = require('./mutationReducer'); + +const reducers = [ + root.reducer, + queries.reducer, + mutations.reducer +]; + +const allPermissions = [...root.constants, ...queries.constants, ...mutations.constants]; + +const findGrant = (user, perms, context, initialState) => { + return perms.every(perm => { + + for (let reducer in reducers) { + const grant = reducer(user, perm, context, initialState); + + if (grant !== null && typeof grant !== 'undefined') { + return grant; + } + } + + return false; + }); +}; + +/** + * returns true, false, or null depending on whether the user has those permissions + * throws an error if you pass a permission that's not known to the system + * @param {User} user the user making the request for db operations + * @param {[type]} context [description] + * @param {String/Array} perms a string an array of strings which are the names of the permissions + * @return {Boolean} + */ +module.exports = (user, context, ...perms) => { + + // make sure all the passed permissions are not typos + const missingPerms = perms.filter(perm => { + return typeof allPermissions[perm] === 'undefined'; + }); + + if (missingPerms.length) { + throw new Error(`${missingPerms.join(' ')} are not valid permissions.`); + } + + return findGrant(user, perms, context, null); +}; diff --git a/perms/mutationReducer.js b/perms/mutationReducer.js new file mode 100644 index 000000000..75eb22400 --- /dev/null +++ b/perms/mutationReducer.js @@ -0,0 +1,48 @@ +const intersection = require('lodash/intersection'); +const check = (user, roles) => { + return !!intersection(roles, user.roles).length; +}; + +const CREATE_COMMENT = 'CREATE_COMMENT'; +const CREATE_ACTION = 'CREATE_ACTION'; +const DELETE_ACTION = 'DELETE_ACTION'; +const EDIT_NAME = 'EDIT_NAME'; +const SET_USER_STATUS = 'SET_USER_STATUS'; +const SUSPEND_USER = 'SUSPEND_USER'; +const SET_COMMENT_STATUS = 'SET_COMMENT_STATUS'; +const ADD_COMMENT_TAG = 'ADD_COMMENT_TAG'; +const REMOVE_COMMENT_TAG = 'REMOVE_COMMENT_TAG'; +const UPDATE_CONFIG = 'UPDATE_CONFIG'; + +module.exports = { + constants: [ + CREATE_COMMENT, CREATE_ACTION, DELETE_ACTION, EDIT_NAME, SET_USER_STATUS, + SUSPEND_USER, SET_COMMENT_STATUS, ADD_COMMENT_TAG, REMOVE_COMMENT_TAG, UPDATE_CONFIG + ], + reducer: (user, perm, context, initialState) => { + switch (perm) { + case 'muation:createComment': + return true; + case 'mutation:createAction': + return true; + case 'mutation:deleteAction': + return true; + case 'mutation:editName': + return true; + case 'mutation:setUserStatus': + return check(user, ['ADMIN', 'MODERATOR']); + case 'mutation:suspendUser': + return check(user, ['ADMIN', 'MODERATOR']); + case 'mutation:setCommentStatus': + return check(user, ['ADMIN', 'MODERATOR']); + case 'mutation:addCommentTag': + return check(user, ['ADMIN', 'MODERATOR']); + case 'mutation:removeCommentTag': + return check(user, ['ADMIN', 'MODERATOR']); + case 'mutation:updateConfig': + return check(user, ['ADMIN', 'MODERATOR']); + default: + return initialState; + } + } +}; diff --git a/perms/queryReducer.js b/perms/queryReducer.js new file mode 100644 index 000000000..b80a61ea7 --- /dev/null +++ b/perms/queryReducer.js @@ -0,0 +1,36 @@ +const intersection = require('lodash/intersection'); +const check = (user, roles) => { + return !!intersection(roles, user.roles).length; +}; + +const SEARCH_ASSETS = 'SEARCH_ASSETS'; +const SEARCH_OTHER_USERS = 'SEARCH_OTHER_USERS'; +const SEARCH_ACTIONS = 'SEARCH_ACTIONS'; +const SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS = 'SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS'; +const SEARCH_OTHERS_COMMENTS = 'SEARCH_OTHERS_COMMENTS'; +const SEARCH_COMMENT_METRICS = 'SEARCH_COMMENT_METRICS'; + +module.exports = { + constants: [ + SEARCH_ASSETS, SEARCH_OTHER_USERS, SEARCH_ACTIONS, SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS, + SEARCH_OTHERS_COMMENTS, SEARCH_COMMENT_METRICS + ], + reducer: (perm, user, context, initialState) => { + switch (perm) { + case SEARCH_ASSETS: + return check(user, ['ADMIN', 'MODERATOR']); + case SEARCH_OTHER_USERS: + return check(user, ['ADMIN', 'MODERATOR']); + case SEARCH_ACTIONS: + return check(user, ['ADMIN', 'MODERATOR']); + case SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS: + return check(user, ['ADMIN', 'MODERATOR']); + case SEARCH_OTHERS_COMMENTS: + return check(user, ['ADMIN', 'MODERATOR']); + case SEARCH_COMMENT_METRICS: + return check(user, ['ADMIN', 'MODERATOR']); + default: + return initialState; + } + } +}; diff --git a/perms/rootReducer.js b/perms/rootReducer.js new file mode 100644 index 000000000..b46b6e82a --- /dev/null +++ b/perms/rootReducer.js @@ -0,0 +1,12 @@ +module.exports = { + constants: [], + reducer: (perm, user, context, initialState) => { + + // this runs before everything + if (user.status === 'BANNED') { + return false; + } + + return initialState; + } +};