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