From fe279587b59df8a63856b1ec3b357e16a16ad7e5 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Wed, 10 May 2017 09:25:39 -0600 Subject: [PATCH 01/19] add staff to dropdown --- client/coral-admin/src/containers/Community/Table.js | 1 + models/user.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/client/coral-admin/src/containers/Community/Table.js b/client/coral-admin/src/containers/Community/Table.js index aacc9c82f..2924e7fd3 100644 --- a/client/coral-admin/src/containers/Community/Table.js +++ b/client/coral-admin/src/containers/Community/Table.js @@ -65,6 +65,7 @@ class Table extends Component { label={lang.t('community.role')} onChange={role => this.onRoleChange(row.id, role)}> + diff --git a/models/user.js b/models/user.js index 458ac68f0..ec215b3fa 100644 --- a/models/user.js +++ b/models/user.js @@ -5,7 +5,8 @@ const uuid = require('uuid'); // USER_ROLES is the array of roles that is permissible as a user role. const USER_ROLES = [ 'ADMIN', - 'MODERATOR' + 'MODERATOR', + 'STAFF' ]; // USER_STATUS is the list of statuses that are permitted for the user status. From 14b483d0894a07ca16b37da71158194f74e93be9 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Thu, 11 May 2017 14:23:02 -0600 Subject: [PATCH 02/19] create roles helper --- client/coral-admin/src/actions/auth.js | 8 +++----- client/coral-admin/src/containers/LayoutContainer.js | 7 ++++--- client/coral-admin/src/reducers/auth.js | 2 -- client/coral-embed-stream/src/components/Embed.js | 5 +++-- client/coral-embed-stream/src/components/Stream.js | 5 +++-- client/coral-framework/actions/auth.js | 7 +++---- client/coral-framework/reducers/auth.js | 6 +----- client/coral-framework/utils/roles.js | 10 ++++++++++ services/users.js | 11 ++++------- 9 files changed, 31 insertions(+), 30 deletions(-) create mode 100644 client/coral-framework/utils/roles.js diff --git a/client/coral-admin/src/actions/auth.js b/client/coral-admin/src/actions/auth.js index 89b56d4dc..68da3577e 100644 --- a/client/coral-admin/src/actions/auth.js +++ b/client/coral-admin/src/actions/auth.js @@ -14,8 +14,7 @@ export const handleLogin = (email, password, recaptchaResponse) => dispatch => { return dispatch(checkLoginFailure('not logged in')); } - const isAdmin = !!user.roles.filter(i => i === 'ADMIN').length; - dispatch(checkLoginSuccess(user, isAdmin)); + dispatch(checkLoginSuccess(user)); }) .catch(error => { @@ -41,7 +40,7 @@ export const requestPasswordReset = email => dispatch => { // Check Login const checkLoginRequest = () => ({type: actions.CHECK_LOGIN_REQUEST}); -const checkLoginSuccess = (user, isAdmin) => ({type: actions.CHECK_LOGIN_SUCCESS, user, isAdmin}); +const checkLoginSuccess = (user) => ({type: actions.CHECK_LOGIN_SUCCESS, user}); const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error}); export const checkLogin = () => dispatch => { @@ -52,8 +51,7 @@ export const checkLogin = () => dispatch => { return dispatch(checkLoginFailure('not logged in')); } - const isAdmin = !!user.roles.filter(i => i === 'ADMIN').length; - dispatch(checkLoginSuccess(user, isAdmin)); + dispatch(checkLoginSuccess(user)); }) .catch(error => { console.error(error); diff --git a/client/coral-admin/src/containers/LayoutContainer.js b/client/coral-admin/src/containers/LayoutContainer.js index 42e2baade..fea730c98 100644 --- a/client/coral-admin/src/containers/LayoutContainer.js +++ b/client/coral-admin/src/containers/LayoutContainer.js @@ -6,6 +6,7 @@ import {toggleModal as toggleShortcutModal} from '../actions/moderation'; import {fetchConfig} from '../actions/config'; import {FullLoading} from '../components/FullLoading'; import AdminLogin from '../components/AdminLogin'; +import roleUtils from 'coral-framework/utils/roles'; class LayoutContainer extends Component { componentWillMount () { @@ -16,7 +17,7 @@ class LayoutContainer extends Component { } render () { const { - isAdmin, + user, loggedIn, loadingUser, loginError, @@ -26,7 +27,7 @@ class LayoutContainer extends Component { const {handleLogout, toggleShortcutModal, TALK_RECAPTCHA_PUBLIC} = this.props; if (loadingUser) { return ; } - if (!isAdmin) { + if (roleUtils.canAccessAdmin(user)) { return ; } - if (isAdmin && loggedIn) { + if (roleUtils.canAccessAdmin(user) && loggedIn) { return ; } return ; diff --git a/client/coral-admin/src/reducers/auth.js b/client/coral-admin/src/reducers/auth.js index a7054ddfa..2bd42d112 100644 --- a/client/coral-admin/src/reducers/auth.js +++ b/client/coral-admin/src/reducers/auth.js @@ -4,7 +4,6 @@ import * as actions from '../constants/auth'; const initialState = Map({ loggedIn: false, user: null, - isAdmin: false, loginError: null, loginMaxExceeded: false, passwordRequestSuccess: null @@ -24,7 +23,6 @@ export default function auth (state = initialState, action) { return state .set('loggedIn', true) .set('loadingUser', false) - .set('isAdmin', action.isAdmin) .set('user', action.user); case actions.LOGOUT_SUCCESS: return initialState; diff --git a/client/coral-embed-stream/src/components/Embed.js b/client/coral-embed-stream/src/components/Embed.js index 0d26cf740..908fd4a90 100644 --- a/client/coral-embed-stream/src/components/Embed.js +++ b/client/coral-embed-stream/src/components/Embed.js @@ -1,6 +1,7 @@ import React from 'react'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from 'coral-framework/translations'; +import rolesHelper from 'coral-framework/utils/roles'; const lang = new I18n(translations); import {TabBar, Tab, TabContent, Button} from 'coral-ui'; @@ -38,7 +39,7 @@ export default class Embed extends React.Component { render () { const {activeTab, logout, viewAllComments, commentId} = this.props; const {asset: {totalCommentCount}} = this.props.root; - const {loggedIn, isAdmin, user} = this.props.auth; + const {loggedIn, user} = this.props.auth; const userBox = ; @@ -48,7 +49,7 @@ export default class Embed extends React.Component { {lang.t('myProfile')} - Configure Stream + Configure Stream { commentId && diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index 46ac75dca..7efadc921 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -11,6 +11,7 @@ import QuestionBox from 'coral-plugin-questionbox/QuestionBox'; import IgnoredCommentTombstone from './IgnoredCommentTombstone'; import SuspendedAccount from 'coral-framework/components/SuspendedAccount'; import RestrictedContent from 'coral-framework/components/RestrictedContent'; +import rolesHelper from 'coral-framework/utils/roles'; import ChangeUsernameContainer from 'coral-sign-in/containers/ChangeUsernameContainer'; @@ -37,7 +38,7 @@ class Stream extends React.Component { removeCommentTag, pluginProps, ignoreUser, - auth: {loggedIn, isAdmin, user}, + auth: {loggedIn, user}, commentCountCache, editName } = this.props; @@ -111,7 +112,7 @@ class Stream extends React.Component { {loggedIn && user && } - {loggedIn && } + {loggedIn && } {/* the highlightedComment is isolated after the user followed a permalink */} {highlightedComment diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index d689aeb63..527369b60 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -108,7 +108,7 @@ export const cleanState = () => ({type: actions.CLEAN_STATE}); const signInRequest = () => ({type: actions.FETCH_SIGNIN_REQUEST}); // TODO: revisit login redux flow. -// const signInSuccess = (user, isAdmin) => ({type: actions.FETCH_SIGNIN_SUCCESS, user, isAdmin}); +// const signInSuccess = (user) => ({type: actions.FETCH_SIGNIN_SUCCESS, user}); // const signInFailure = error => ({type: actions.FETCH_SIGNIN_FAILURE, error}); @@ -236,7 +236,7 @@ export const invalidForm = error => ({type: actions.INVALID_FORM, error}); // Check Login const checkLoginRequest = () => ({type: actions.CHECK_LOGIN_REQUEST}); -const checkLoginSuccess = (user, isAdmin) => ({type: actions.CHECK_LOGIN_SUCCESS, user, isAdmin}); +const checkLoginSuccess = (user) => ({type: actions.CHECK_LOGIN_SUCCESS, user}); const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error}); export const checkLogin = () => dispatch => { @@ -247,8 +247,7 @@ export const checkLogin = () => dispatch => { throw new Error('Not logged in'); } - const isAdmin = !!result.user.roles.filter(i => i === 'ADMIN').length; - dispatch(checkLoginSuccess(result.user, isAdmin)); + dispatch(checkLoginSuccess(result.user)); }) .catch(error => { console.error(error); diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index 83ea49ce5..5ac862431 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -4,7 +4,6 @@ import * as actions from '../constants/auth'; const initialState = Map({ isLoading: false, loggedIn: false, - isAdmin: false, user: null, showSignInDialog: false, showCreateUsernameDialog: false, @@ -79,12 +78,10 @@ export default function auth (state = initialState, action) { return state .set('checkedInitialLogin', true) .set('loggedIn', true) - .set('isAdmin', action.isAdmin) .set('user', purge(action.user)); case actions.FETCH_SIGNIN_SUCCESS: return state .set('loggedIn', true) - .set('isAdmin', action.isAdmin) .set('user', purge(action.user)); case actions.FETCH_SIGNIN_FAILURE: return state @@ -120,8 +117,7 @@ export default function auth (state = initialState, action) { return state .set('user', null) .set('isLoading', false) - .set('loggedIn', false) - .set('isAdmin', false); + .set('loggedIn', false); case actions.INVALID_FORM: return state .set('error', action.error); diff --git a/client/coral-framework/utils/roles.js b/client/coral-framework/utils/roles.js new file mode 100644 index 000000000..b457ff1b3 --- /dev/null +++ b/client/coral-framework/utils/roles.js @@ -0,0 +1,10 @@ +import includes from 'lodash/includes'; + +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') +}; diff --git a/services/users.js b/services/users.js index 14301ccf4..246d3fded 100644 --- a/services/users.js +++ b/services/users.js @@ -395,13 +395,10 @@ module.exports = class UsersService { return Promise.reject(new Error(`role ${role} is not supported`)); } - return UserModel.update({ - id: id - }, { - $addToSet: { - roles: role - } - }); + // 5.11.2017 - Restricting this to a hierarchical system like WordPress + // where you can only set one role at a time. + // I'm not changing the data structure here, because I don't want a migration + return UserModel.update({id}, {$set: {roles: [role]}}); } /** From e32ab99edf17a85a41c7e1bc7e4f2cfabb3b83d6 Mon Sep 17 00:00:00 2001 From: riley Date: Fri, 12 May 2017 09:57:07 -0600 Subject: [PATCH 03/19] remove hasRoles --- .../src/containers/LayoutContainer.js | 2 +- graph/loaders/comments.js | 6 +- graph/mutators/comment.js | 2 +- graph/resolvers/comment.js | 7 +- graph/resolvers/root_query.js | 12 +-- graph/resolvers/user.js | 6 +- models/user.js | 78 ++++++++++++++++--- test/server/graph/mutations/addCommentTag.js | 1 + 8 files changed, 87 insertions(+), 27 deletions(-) diff --git a/client/coral-admin/src/containers/LayoutContainer.js b/client/coral-admin/src/containers/LayoutContainer.js index fea730c98..8a3e0b0d2 100644 --- a/client/coral-admin/src/containers/LayoutContainer.js +++ b/client/coral-admin/src/containers/LayoutContainer.js @@ -27,7 +27,7 @@ class LayoutContainer extends Component { const {handleLogout, toggleShortcutModal, TALK_RECAPTCHA_PUBLIC} = this.props; if (loadingUser) { return ; } - if (roleUtils.canAccessAdmin(user)) { + if (!loggedIn) { return { */ const genComments = ({user}, ids) => { let comments; - if (user && user.hasRoles('ADMIN')) { + if (user && user.canViewOthersComments()) { comments = CommentModel.find({ id: { $in: ids diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index e2f2d3bac..9557c1dcf 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -22,7 +22,7 @@ const createComment = ({user, loaders: {Comments}, pubsub}, {body, asset_id, par tags = tags.map(tag => ({name: tag})); // If admin or moderator, adding STAFF tag - if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) { + if (user.isStaff()) { tags.push({name: 'STAFF'}); } diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js index 19ea11efe..fb01a1585 100644 --- a/graph/resolvers/comment.js +++ b/graph/resolvers/comment.js @@ -23,14 +23,13 @@ const Comment = { }, replyCount({id}, {excludeIgnored}, {user, loaders: {Comments}}) { if (user && excludeIgnored) { - return Comments.countByParentIDPersonalized({id, excludeIgnored}); + return Comments.countByParentIDPersonalized({id, excludeIgnored}); } - return Comments.countByParentID.load(id); + return Comments.countByParentID.load(id); }, actions({id}, _, {user, loaders: {Actions}}) { - // Only return the actions if the user is not an admin. - if (user && user.hasRoles('ADMIN')) { + if (user && user.canViewActions()) { return Actions.getByID.load(id); } diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index 4dcb7ea11..9deaba5f6 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -1,6 +1,6 @@ const RootQuery = { assets(_, args, {loaders: {Assets}, user}) { - if (user == null || !user.hasRoles('ADMIN')) { + if (user == null || !user.canQueryAssets()) { return null; } @@ -22,7 +22,7 @@ const RootQuery = { comments(_, {query: {action_type, statuses, asset_id, parent_id, limit, cursor, sort, excludeIgnored}}, {user, loaders: {Comments, Actions}}) { let query = {statuses, asset_id, parent_id, limit, cursor, sort, excludeIgnored}; - if (user != null && user.hasRoles('ADMIN') && action_type) { + if (user != null && user.canViewOthersComments() && action_type) { return Actions.getByTypes({action_type, item_type: 'COMMENTS'}) .then((ids) => { @@ -37,7 +37,7 @@ const RootQuery = { return Comments.get.load(id); }, commentCount(_, {query: {action_type, statuses, asset_id, parent_id}}, {user, loaders: {Actions, Comments}}) { - if (user == null || !user.hasRoles('ADMIN')) { + if (user == null || !user.canViewOthersComments()) { return null; } @@ -54,7 +54,7 @@ const RootQuery = { }, assetMetrics(_, {from, to, sort, limit = 10}, {user, loaders: {Metrics: {Assets}}}) { - if (user == null || !user.hasRoles('ADMIN')) { + if (user == null || !user.canQueryAssets()) { return null; } @@ -66,7 +66,7 @@ const RootQuery = { }, commentMetrics(_, {from, to, sort, limit = 10}, {user, loaders: {Metrics: {Comments}}}) { - if (user == null || !user.hasRoles('ADMIN')) { + if (user == null || !user.canViewCommentMetrics()) { return null; } @@ -100,7 +100,7 @@ const RootQuery = { // so hide it in the event that we aren't an admin. users(_, {query: {action_type, limit, cursor, sort}}, {user, loaders: {Users, Actions}}) { - if (user == null || !user.hasRoles('ADMIN')) { + if (user == null || !user.canViewOtherUsers()) { return null; } diff --git a/graph/resolvers/user.js b/graph/resolvers/user.js index d8ed7ee15..03e1b121a 100644 --- a/graph/resolvers/user.js +++ b/graph/resolvers/user.js @@ -5,7 +5,7 @@ const User = { actions({id}, _, {user, loaders: {Actions}}) { // Only return the actions if the user is not an admin. - if (user && user.hasRoles('ADMIN')) { + if (user && user.canViewActions()) { return Actions.getByID.load(id); } @@ -14,7 +14,7 @@ const User = { // If the user is not an admin, only return comment list for the owner of // the comments. - if (user && (user.hasRoles('ADMIN') || user.id === id)) { + if (user && (user.canViewOthersComments() || user.id === id)) { return Comments.getByQuery({author_id: id, sort: 'REVERSE_CHRONOLOGICAL'}); } @@ -23,7 +23,7 @@ const User = { roles({id, roles}, _, {user}) { // If the user is not an admin, only return the current user's roles. - if (user && (user.hasRoles('ADMIN') || user.id === id)) { + if (user && (user.canChangeRoles() || user.id === id)) { return roles; } diff --git a/models/user.js b/models/user.js index ec215b3fa..179e6f7a5 100644 --- a/models/user.js +++ b/models/user.js @@ -1,6 +1,7 @@ const mongoose = require('../services/mongoose'); const bcrypt = require('bcrypt'); const uuid = require('uuid'); +const intersection = require('lodash/intersection'); // USER_ROLES is the array of roles that is permissible as a user role. const USER_ROLES = [ @@ -158,14 +159,74 @@ UserSchema.index({ }); /** - * Returns true if the user has all the roles specified. + * returns true if the user can look up assets through the api */ -UserSchema.method('hasRoles', function(...roles) { - return roles.every((role) => { +UserSchema.method('canQueryAssets', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); - // TODO: remove toUpperCase() once we've migrated usage. - return this.roles.indexOf(role.toUpperCase()) >= 0; - }); +/** + * returns true if the user can view actions + */ +UserSchema.method('canViewActions', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); + +/** + * returns true if the user can view non-null or non-ACCEPTED comments + */ +UserSchema.method('canViewNonNullOrAcceptedComments', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); + +/** + * returns true when a user can view comments that are not their own + */ +UserSchema.method('canViewOthersComments', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); + +/** + * returns true when a user can view comment metrics + */ +UserSchema.method('canViewCommentMetrics', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); + +/** + * returns true if a commenter is staff + */ +UserSchema.method('isStaff', function () { + return !!intersection(['ADMIN', 'MODERATOR', 'STAFF'], this.roles).length; +}); + +/** + * returns true when a user can see other user info + */ +UserSchema.method('canViewOtherUsers', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); + +/** + * when a user can modify tags + */ +UserSchema.method('canModifyTags', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); + +/** + * when a user can change roles + */ +UserSchema.method('canChangeUserRoles', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); + +UserSchema.method('canSetCommentStatus', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); + +UserSchema.method('canSetUserStatus', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; }); /** @@ -216,13 +277,12 @@ UserSchema.method('can', function(...actions) { return false; } - if (actions.some((action) => action === 'mutation:setUserStatus' || action === 'mutation:suspendUser' || action === 'mutation:setCommentStatus') && !this.hasRoles('ADMIN')) { + if (actions.some((action) => action === 'mutation:setUserStatus' || action === 'mutation:suspendUser' || action === 'mutation:setCommentStatus') && !this.canSetUserStatus()) { return false; } // {add,remove}CommentTag - requires admin and/or moderator role - const userCanModifyTags = user => ['ADMIN', 'MODERATOR'].some(r => user.hasRoles(r)); - if (actions.some(a => ['mutation:removeCommentTag', 'mutation:addCommentTag'].includes(a)) && ! userCanModifyTags(this)) { + if (actions.some(a => ['mutation:removeCommentTag', 'mutation:addCommentTag'].includes(a)) && ! this.canModifyTags()) { return false; } diff --git a/test/server/graph/mutations/addCommentTag.js b/test/server/graph/mutations/addCommentTag.js index 018e96631..1386d8339 100644 --- a/test/server/graph/mutations/addCommentTag.js +++ b/test/server/graph/mutations/addCommentTag.js @@ -44,6 +44,7 @@ describe('graph.mutations.addCommentTag', () => { Object.entries({ 'anonymous': undefined, 'regular commenter': new UserModel({}), + 'staff': new UserModel({roles: ['STAFF']}), 'banned moderator': new UserModel({roles: ['MODERATOR'], status: 'BANNED'}) }).forEach(([ userDescription, user ]) => { it(userDescription, async function () { From 4d3788e4c96a8cc54642eaae778053338af3fce8 Mon Sep 17 00:00:00 2001 From: riley Date: Mon, 15 May 2017 16:25:32 -0600 Subject: [PATCH 04/19] saving my place so i dont have too many threads --- .../src/containers/LayoutContainer.js | 2 + client/coral-framework/utils/roles.js | 36 ++++++++++---- graph/resolvers/action.js | 2 +- models/user.js | 9 +--- perms/index.js | 48 +++++++++++++++++++ perms/mutationReducer.js | 48 +++++++++++++++++++ perms/queryReducer.js | 36 ++++++++++++++ perms/rootReducer.js | 12 +++++ 8 files changed, 177 insertions(+), 16 deletions(-) create mode 100644 perms/index.js create mode 100644 perms/mutationReducer.js create mode 100644 perms/queryReducer.js create mode 100644 perms/rootReducer.js 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; + } +}; From 62e8e4c5c57bf43e80d9fd478b91d02b3822c4ab Mon Sep 17 00:00:00 2001 From: riley Date: Tue, 16 May 2017 10:15:43 -0600 Subject: [PATCH 05/19] change how constants are exposed --- perms/index.js | 3 ++- perms/mutationReducer.js | 27 +++++++++++---------------- perms/queryReducer.js | 31 +++++++++++++------------------ perms/rootReducer.js | 3 +-- 4 files changed, 27 insertions(+), 37 deletions(-) diff --git a/perms/index.js b/perms/index.js index c9b29ea85..b2baf00ec 100644 --- a/perms/index.js +++ b/perms/index.js @@ -8,7 +8,8 @@ const reducers = [ mutations.reducer ]; -const allPermissions = [...root.constants, ...queries.constants, ...mutations.constants]; +// this will make 'reducer' a key in this array. hm. +const allPermissions = [...Object.keys(root), ...Object.keys(queries), ...Object.keys(mutations)]; const findGrant = (user, perms, context, initialState) => { return perms.every(perm => { diff --git a/perms/mutationReducer.js b/perms/mutationReducer.js index 75eb22400..7e3c1b3cd 100644 --- a/perms/mutationReducer.js +++ b/perms/mutationReducer.js @@ -3,23 +3,18 @@ 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) => { + CREATE_COMMENT: 'CREATE_COMMENT', + CREATE_ACTION: 'CREATE_ACTION', + DELETE_ACTION: 'DELETE_ACTION', + EDIT_NAME: 'EDIT_NAME', + SET_USER_STATUS: 'SET_USER_STATUS', + SUSPEND_USER: 'SUSPEND_USER', + SET_COMMENT_STATUS: 'SET_COMMENT_STATUS', + ADD_COMMENT_TAG: 'ADD_COMMENT_TAG', + REMOVE_COMMENT_TAG: 'REMOVE_COMMENT_TAG', + UPDATE_CONFIG: 'UPDATE_CONFIG', + reducer: function (user, perm, context, initialState) { switch (perm) { case 'muation:createComment': return true; diff --git a/perms/queryReducer.js b/perms/queryReducer.js index b80a61ea7..9e97c8ec4 100644 --- a/perms/queryReducer.js +++ b/perms/queryReducer.js @@ -3,31 +3,26 @@ 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) => { + SEARCH_ASSETS: 'SEARCH_ASSETS', + SEARCH_OTHER_USERS: 'SEARCH_OTHER_USERS', + SEARCH_ACTIONS: 'SEARCH_ACTIONS', + SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS: 'SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS', + SEARCH_OTHERS_COMMENTS: 'SEARCH_OTHERS_COMMENTS', + SEARCH_COMMENT_METRICS: 'SEARCH_COMMENT_METRICS', + reducer: function (perm, user, context, initialState) { switch (perm) { - case SEARCH_ASSETS: + case this.SEARCH_ASSETS: return check(user, ['ADMIN', 'MODERATOR']); - case SEARCH_OTHER_USERS: + case this.SEARCH_OTHER_USERS: return check(user, ['ADMIN', 'MODERATOR']); - case SEARCH_ACTIONS: + case this.SEARCH_ACTIONS: return check(user, ['ADMIN', 'MODERATOR']); - case SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS: + case this.SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS: return check(user, ['ADMIN', 'MODERATOR']); - case SEARCH_OTHERS_COMMENTS: + case this.SEARCH_OTHERS_COMMENTS: return check(user, ['ADMIN', 'MODERATOR']); - case SEARCH_COMMENT_METRICS: + case this.SEARCH_COMMENT_METRICS: return check(user, ['ADMIN', 'MODERATOR']); default: return initialState; diff --git a/perms/rootReducer.js b/perms/rootReducer.js index b46b6e82a..e8552c765 100644 --- a/perms/rootReducer.js +++ b/perms/rootReducer.js @@ -1,6 +1,5 @@ module.exports = { - constants: [], - reducer: (perm, user, context, initialState) => { + reducer: function (perm, user, context, initialState) { // this runs before everything if (user.status === 'BANNED') { From c7e9fe3f5fb52979feb3a3ba3b1d1ea605f742ff Mon Sep 17 00:00:00 2001 From: riley Date: Tue, 16 May 2017 10:50:09 -0600 Subject: [PATCH 06/19] update backend with the can() method --- graph/loaders/comments.js | 6 +-- graph/mutators/action.js | 2 +- graph/mutators/comment.js | 8 +-- graph/mutators/user.js | 4 +- graph/resolvers/action.js | 2 +- graph/resolvers/comment.js | 2 +- graph/resolvers/root_query.js | 12 ++--- graph/resolvers/user.js | 6 +-- models/user.js | 91 ----------------------------------- perms/mutationReducer.js | 23 +++++---- 10 files changed, 34 insertions(+), 122 deletions(-) diff --git a/graph/loaders/comments.js b/graph/loaders/comments.js index b09dbc497..2287527e6 100644 --- a/graph/loaders/comments.js +++ b/graph/loaders/comments.js @@ -226,7 +226,7 @@ const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, a // Only administrators can search for comments with statuses that are not // `null`, or `'ACCEPTED'`. - if (user != null && user.canViewNonNullOrAcceptedComments() && statuses) { + if (user != null && user.can('SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS') && statuses) { comments = comments.where({ status: { $in: statuses @@ -249,7 +249,7 @@ const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, a } // Only let an admin request any user or the current user request themself. - if (user && (user.canViewOthersComments() || user.id === author_id) && author_id != null) { + if (user && (user.can('SEARCH_OTHERS_COMMENTS') || user.id === author_id) && author_id != null) { comments = comments.where({author_id}); } @@ -403,7 +403,7 @@ const genRecentComments = (_, ids) => { */ const genComments = ({user}, ids) => { let comments; - if (user && user.canViewOthersComments()) { + if (user && user.can('SEARCH_OTHERS_COMMENTS')) { comments = CommentModel.find({ id: { $in: ids diff --git a/graph/mutators/action.js b/graph/mutators/action.js index a572a641c..dc3fe1a70 100644 --- a/graph/mutators/action.js +++ b/graph/mutators/action.js @@ -45,7 +45,7 @@ const deleteAction = ({user}, {id}) => { }; module.exports = (context) => { - if (context.user && context.user.can('mutation:createAction', 'mutation:deleteAction')) { + if (context.user && context.user.can('CREATE_ACTION', 'DELETE_ACTION')) { return { Action: { create: (action) => createAction(context, action), diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 9557c1dcf..51ebc5a4f 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -236,19 +236,19 @@ module.exports = (context) => { } }; - if (context.user && context.user.can('mutation:createComment')) { + if (context.user && context.user.can('CREATE_COMMENT')) { mutators.Comment.create = (comment) => createPublicComment(context, comment); } - if (context.user && context.user.can('mutation:setCommentStatus')) { + if (context.user && context.user.can('SET_COMMENT_STATUS')) { mutators.Comment.setCommentStatus = (action) => setCommentStatus(context, action); } - if (context.user && context.user.can('mutation:addCommentTag')) { + if (context.user && context.user.can('ADD_COMMENT_TAG')) { mutators.Comment.addCommentTag = (action) => addCommentTag(context, action); } - if (context.user && context.user.can('mutation:removeCommentTag')) { + if (context.user && context.user.can('REMOVE_COMMENT_TAG')) { mutators.Comment.removeCommentTag = (action) => removeCommentTag(context, action); } diff --git a/graph/mutators/user.js b/graph/mutators/user.js index d68351701..c5819fded 100644 --- a/graph/mutators/user.js +++ b/graph/mutators/user.js @@ -31,11 +31,11 @@ module.exports = (context) => { } }; - if (context.user && context.user.can('mutation:setUserStatus')) { + if (context.user && context.user.can('SET_USER_STATUS')) { mutators.User.setUserStatus = (action) => setUserStatus(context, action); } - if (context.user && context.user.can('mutation:suspendUser')) { + if (context.user && context.user.can('SUSPEND_USER')) { mutators.User.suspendUser = (action) => suspendUser(context, action); } diff --git a/graph/resolvers/action.js b/graph/resolvers/action.js index cf7be3beb..2814dc85f 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.canViewOtherUsers() || user_id === user.id)) { + if (user && (user.can('SEARCH_OTHER_USERS') || user_id === user.id)) { return Users.getByID.load(user_id); } } diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js index fb01a1585..2a678469b 100644 --- a/graph/resolvers/comment.js +++ b/graph/resolvers/comment.js @@ -29,7 +29,7 @@ const Comment = { }, actions({id}, _, {user, loaders: {Actions}}) { - if (user && user.canViewActions()) { + if (user && user.can('SEARCH_ACTIONS')) { return Actions.getByID.load(id); } diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index 9deaba5f6..3f76cb9d2 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -1,6 +1,6 @@ const RootQuery = { assets(_, args, {loaders: {Assets}, user}) { - if (user == null || !user.canQueryAssets()) { + if (user == null || !user.can('SEARCH_ASSETS')) { return null; } @@ -22,7 +22,7 @@ const RootQuery = { comments(_, {query: {action_type, statuses, asset_id, parent_id, limit, cursor, sort, excludeIgnored}}, {user, loaders: {Comments, Actions}}) { let query = {statuses, asset_id, parent_id, limit, cursor, sort, excludeIgnored}; - if (user != null && user.canViewOthersComments() && action_type) { + if (user != null && user.can('SEARCH_OTHERS_COMMENTS') && action_type) { return Actions.getByTypes({action_type, item_type: 'COMMENTS'}) .then((ids) => { @@ -37,7 +37,7 @@ const RootQuery = { return Comments.get.load(id); }, commentCount(_, {query: {action_type, statuses, asset_id, parent_id}}, {user, loaders: {Actions, Comments}}) { - if (user == null || !user.canViewOthersComments()) { + if (user == null || !user.can('SEARCH_OTHERS_COMMENTS')) { return null; } @@ -54,7 +54,7 @@ const RootQuery = { }, assetMetrics(_, {from, to, sort, limit = 10}, {user, loaders: {Metrics: {Assets}}}) { - if (user == null || !user.canQueryAssets()) { + if (user == null || !user.can('SEARCH_ASSETS')) { return null; } @@ -66,7 +66,7 @@ const RootQuery = { }, commentMetrics(_, {from, to, sort, limit = 10}, {user, loaders: {Metrics: {Comments}}}) { - if (user == null || !user.canViewCommentMetrics()) { + if (user == null || !user.can('SEARCH_COMMENT_METRICS')) { return null; } @@ -100,7 +100,7 @@ const RootQuery = { // so hide it in the event that we aren't an admin. users(_, {query: {action_type, limit, cursor, sort}}, {user, loaders: {Users, Actions}}) { - if (user == null || !user.canViewOtherUsers()) { + if (user == null || !user.can('SEARCH_OTHER_USERS')) { return null; } diff --git a/graph/resolvers/user.js b/graph/resolvers/user.js index 03e1b121a..faec4bd75 100644 --- a/graph/resolvers/user.js +++ b/graph/resolvers/user.js @@ -5,7 +5,7 @@ const User = { actions({id}, _, {user, loaders: {Actions}}) { // Only return the actions if the user is not an admin. - if (user && user.canViewActions()) { + if (user && user.can('SEARCH_ACTIONS')) { return Actions.getByID.load(id); } @@ -14,7 +14,7 @@ const User = { // If the user is not an admin, only return comment list for the owner of // the comments. - if (user && (user.canViewOthersComments() || user.id === id)) { + if (user && (user.can('SEARCH_OTHERS_COMMENTS') || user.id === id)) { return Comments.getByQuery({author_id: id, sort: 'REVERSE_CHRONOLOGICAL'}); } @@ -23,7 +23,7 @@ const User = { roles({id, roles}, _, {user}) { // If the user is not an admin, only return the current user's roles. - if (user && (user.canChangeRoles() || user.id === id)) { + if (user && (user.can('UPDATE_USER_ROLES') || user.id === id)) { return roles; } diff --git a/models/user.js b/models/user.js index ab88fe6f1..932d11775 100644 --- a/models/user.js +++ b/models/user.js @@ -159,41 +159,6 @@ UserSchema.index({ background: false }); -/** - * returns true if the user can look up assets through the api - */ -UserSchema.method('canQueryAssets', function () { - return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; -}); - -/** - * returns true if the user can view actions - */ -UserSchema.method('canViewActions', function () { - return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; -}); - -/** - * returns true if the user can view non-null or non-ACCEPTED comments - */ -UserSchema.method('canViewNonNullOrAcceptedComments', function () { - return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; -}); - -/** - * returns true when a user can view comments that are not their own - */ -UserSchema.method('canViewOthersComments', function () { - return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; -}); - -/** - * returns true when a user can view comment metrics - */ -UserSchema.method('canViewCommentMetrics', function () { - return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; -}); - /** * returns true if a commenter is staff */ @@ -201,35 +166,6 @@ UserSchema.method('isStaff', function () { return !!intersection(['ADMIN', 'MODERATOR', 'STAFF'], this.roles).length; }); -/** - * returns true when a user can see other user info - */ -UserSchema.method('canViewOtherUsers', function () { - return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; -}); - -/** - * when a user can modify tags - */ -UserSchema.method('canModifyTags', function () { - return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; -}); - -/** - * when a user can change roles - */ -UserSchema.method('canChangeUserRoles', function () { - return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; -}); - -UserSchema.method('canSetCommentStatus', function () { - return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; -}); - -UserSchema.method('canSetUserStatus', function () { - return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; -}); - /** * This verifies that a password is valid. */ @@ -249,39 +185,12 @@ UserSchema.method('verifyPassword', function(password) { }); }); -/** - * All the graph operations that are available for a user. - * @type {Array} - */ -const USER_GRAPH_OPERATIONS = [ - 'mutation:createComment', - 'mutation:createAction', - 'mutation:deleteAction', - 'mutation:editName', - 'mutation:setUserStatus', - 'mutation:suspendUser', - 'mutation:setCommentStatus', - 'mutation:addCommentTag', - 'mutation:removeCommentTag' -]; - /** * Can returns true if the user is allowed to perform a specific graph * operation. */ UserSchema.method('can', function(...actions) { return can(this, null, actions); - - if (actions.some((action) => action === 'mutation:setUserStatus' || action === 'mutation:suspendUser' || action === 'mutation:setCommentStatus') && !this.canSetUserStatus()) { - return false; - } - - // {add,remove}CommentTag - requires admin and/or moderator role - if (actions.some(a => ['mutation:removeCommentTag', 'mutation:addCommentTag'].includes(a)) && ! this.canModifyTags()) { - return false; - } - - return true; }); // Create the User model. diff --git a/perms/mutationReducer.js b/perms/mutationReducer.js index 7e3c1b3cd..9aa34b017 100644 --- a/perms/mutationReducer.js +++ b/perms/mutationReducer.js @@ -13,28 +13,31 @@ module.exports = { SET_COMMENT_STATUS: 'SET_COMMENT_STATUS', ADD_COMMENT_TAG: 'ADD_COMMENT_TAG', REMOVE_COMMENT_TAG: 'REMOVE_COMMENT_TAG', + UPDATE_USER_ROLES: 'UPDATE_USER_ROLES', UPDATE_CONFIG: 'UPDATE_CONFIG', reducer: function (user, perm, context, initialState) { switch (perm) { - case 'muation:createComment': + case this.CREATE_COMMENT: return true; - case 'mutation:createAction': + case this.CREATE_ACTION: return true; - case 'mutation:deleteAction': + case this.DELETE_ACTION: return true; - case 'mutation:editName': + case this.EDIT_NAME: return true; - case 'mutation:setUserStatus': + case this.UPDATE_USER_ROLES: + return check(user, ['ADMIN']); + case this.SET_USER_STATUS: return check(user, ['ADMIN', 'MODERATOR']); - case 'mutation:suspendUser': + case this.SUSPEND_USER: return check(user, ['ADMIN', 'MODERATOR']); - case 'mutation:setCommentStatus': + case this.SET_COMMENT_STATUS: return check(user, ['ADMIN', 'MODERATOR']); - case 'mutation:addCommentTag': + case this.ADD_COMMENT_TAG: return check(user, ['ADMIN', 'MODERATOR']); - case 'mutation:removeCommentTag': + case this.REMOVE_COMMENT_TAG: return check(user, ['ADMIN', 'MODERATOR']); - case 'mutation:updateConfig': + case this.UPDATE_CONFIG: return check(user, ['ADMIN', 'MODERATOR']); default: return initialState; From 31647fb2f0f4cc6ef98f5a3bf80de092eded1b38 Mon Sep 17 00:00:00 2001 From: riley Date: Tue, 16 May 2017 15:41:27 -0600 Subject: [PATCH 07/19] have function params in the correct order --- .../src/containers/LayoutContainer.js | 4 +-- client/coral-framework/utils/roles.js | 25 +++++++++---------- models/user.js | 2 +- perms/index.js | 20 +++++++++------ perms/mutationReducer.js | 4 +-- perms/queryReducer.js | 4 +-- perms/rootReducer.js | 4 +-- 7 files changed, 32 insertions(+), 31 deletions(-) diff --git a/client/coral-admin/src/containers/LayoutContainer.js b/client/coral-admin/src/containers/LayoutContainer.js index b72acb59c..d4440c454 100644 --- a/client/coral-admin/src/containers/LayoutContainer.js +++ b/client/coral-admin/src/containers/LayoutContainer.js @@ -6,7 +6,7 @@ import {toggleModal as toggleShortcutModal} from '../actions/moderation'; import {fetchConfig} from '../actions/config'; import {FullLoading} from '../components/FullLoading'; import AdminLogin from '../components/AdminLogin'; -import roleUtils from 'coral-framework/utils/roles'; +import {can} from 'coral-framework/utils/roles'; class LayoutContainer extends Component { componentWillMount () { @@ -36,7 +36,7 @@ class LayoutContainer extends Component { recaptchaPublic={TALK_RECAPTCHA_PUBLIC} errorMessage={loginError} />; } - if (roleUtils.canAccessAdmin(user) && loggedIn) { + if (can(user, 'ACCESS_ADMIN') && loggedIn) { return ; } else if (loggedIn) { return

you do not have permission to see this page.

; diff --git a/client/coral-framework/utils/roles.js b/client/coral-framework/utils/roles.js index 63d34fb41..41fd8abfa 100644 --- a/client/coral-framework/utils/roles.js +++ b/client/coral-framework/utils/roles.js @@ -1,30 +1,29 @@ import intersection from 'lodash/intersection'; const basicRoles = { - hasStaffTag: ['ADMIN', 'MODERATOR', 'STAFF'] + HAS_STAFF_TAG: ['ADMIN', 'MODERATOR', 'STAFF'] }; const queryRoles = { - canAccessConfig: ['ADMIN', 'MODERATOR'], - canAccessAdmin: ['ADMIN', 'MODERATOR'], - canViewUserEmails: ['ADMIN'] + UPDATE_CONFIG: ['ADMIN', 'MODERATOR'], + ACCESS_ADMIN: ['ADMIN', 'MODERATOR'], + VIEW_USER_EMAILS: ['ADMIN'] }; const mutationRoles = { - canChangeRoles: ['ADMIN'], - canModerateComments: ['ADMIN', 'MODERATOR'] + CHANGE_ROLES: ['ADMIN'], + MODERATE_COMMENTS: ['ADMIN', 'MODERATOR'] }; const roles = {...basicRoles, ...queryRoles, ...mutationRoles}; -export const can = (user, perms) => { - for (let perm in perms) { +export const can = (user, ...perms) => { + return perms.every(perm => { const role = roles[perm]; if (typeof role === 'undefined') { - continue; + throw new Error(`${perm} is not a valid role`); } - let grant = intersection(role, user.roles).length > 0; - return grant; - } - return false; + + return intersection(role, user.roles).length > 0; + }); }; diff --git a/models/user.js b/models/user.js index 932d11775..1a7b0fa7b 100644 --- a/models/user.js +++ b/models/user.js @@ -190,7 +190,7 @@ UserSchema.method('verifyPassword', function(password) { * operation. */ UserSchema.method('can', function(...actions) { - return can(this, null, actions); + return can(this, null, ...actions); }); // Create the User model. diff --git a/perms/index.js b/perms/index.js index b2baf00ec..9d29c1e57 100644 --- a/perms/index.js +++ b/perms/index.js @@ -3,19 +3,21 @@ const queries = require('./queryReducer'); const mutations = require('./mutationReducer'); const reducers = [ - root.reducer, - queries.reducer, - mutations.reducer + root, + queries, + mutations ]; // this will make 'reducer' a key in this array. hm. const allPermissions = [...Object.keys(root), ...Object.keys(queries), ...Object.keys(mutations)]; -const findGrant = (user, perms, context, initialState) => { +const findGrant = (user, perms, context) => { + return perms.every(perm => { - for (let reducer in reducers) { - const grant = reducer(user, perm, context, initialState); + for (let key in reducers) { + const reducer = reducers[key]; + const grant = reducer.checkRoles(user, perm, context); if (grant !== null && typeof grant !== 'undefined') { return grant; @@ -38,12 +40,14 @@ module.exports = (user, context, ...perms) => { // make sure all the passed permissions are not typos const missingPerms = perms.filter(perm => { - return typeof allPermissions[perm] === 'undefined'; + return allPermissions.indexOf(perm) === -1; }); if (missingPerms.length) { + + // not sure if this is working. throw new Error(`${missingPerms.join(' ')} are not valid permissions.`); } - return findGrant(user, perms, context, null); + return findGrant(user, perms, context); }; diff --git a/perms/mutationReducer.js b/perms/mutationReducer.js index 9aa34b017..5f763805d 100644 --- a/perms/mutationReducer.js +++ b/perms/mutationReducer.js @@ -15,7 +15,7 @@ module.exports = { REMOVE_COMMENT_TAG: 'REMOVE_COMMENT_TAG', UPDATE_USER_ROLES: 'UPDATE_USER_ROLES', UPDATE_CONFIG: 'UPDATE_CONFIG', - reducer: function (user, perm, context, initialState) { + checkRoles: function (user, perm, context) { switch (perm) { case this.CREATE_COMMENT: return true; @@ -40,7 +40,7 @@ module.exports = { case this.UPDATE_CONFIG: return check(user, ['ADMIN', 'MODERATOR']); default: - return initialState; + break; } } }; diff --git a/perms/queryReducer.js b/perms/queryReducer.js index 9e97c8ec4..bea059639 100644 --- a/perms/queryReducer.js +++ b/perms/queryReducer.js @@ -10,7 +10,7 @@ module.exports = { SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS: 'SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS', SEARCH_OTHERS_COMMENTS: 'SEARCH_OTHERS_COMMENTS', SEARCH_COMMENT_METRICS: 'SEARCH_COMMENT_METRICS', - reducer: function (perm, user, context, initialState) { + checkRoles: function (user, perm, context) { switch (perm) { case this.SEARCH_ASSETS: return check(user, ['ADMIN', 'MODERATOR']); @@ -25,7 +25,7 @@ module.exports = { case this.SEARCH_COMMENT_METRICS: return check(user, ['ADMIN', 'MODERATOR']); default: - return initialState; + break; } } }; diff --git a/perms/rootReducer.js b/perms/rootReducer.js index e8552c765..344c33ada 100644 --- a/perms/rootReducer.js +++ b/perms/rootReducer.js @@ -1,11 +1,9 @@ module.exports = { - reducer: function (perm, user, context, initialState) { + checkRoles: function (user, perm, context) { // this runs before everything if (user.status === 'BANNED') { return false; } - - return initialState; } }; From d081c0c5b500039ccf0e0aee101eecfa08a2c3fc Mon Sep 17 00:00:00 2001 From: riley Date: Tue, 16 May 2017 16:34:04 -0600 Subject: [PATCH 08/19] remove context --- graph/resolvers/user.js | 2 +- models/user.js | 2 +- perms/mutationReducer.js | 2 +- perms/queryReducer.js | 2 +- perms/rootReducer.js | 8 +++++++- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/graph/resolvers/user.js b/graph/resolvers/user.js index 8a6ca69e3..6aba327ad 100644 --- a/graph/resolvers/user.js +++ b/graph/resolvers/user.js @@ -14,7 +14,7 @@ const User = { // If the user is not an admin, only return comment list for the owner of // the comments. - if (user && (user.can('SEARCH_OTHERS_COMMENTS') || user.id === id)) { + if (user && (user.can('SEARCH_OTHERS_COMMENTZ') || user.id === id)) { return Comments.getByQuery({author_id: id, sort: 'REVERSE_CHRONOLOGICAL'}); } diff --git a/models/user.js b/models/user.js index 1a7b0fa7b..3d12d49e9 100644 --- a/models/user.js +++ b/models/user.js @@ -190,7 +190,7 @@ UserSchema.method('verifyPassword', function(password) { * operation. */ UserSchema.method('can', function(...actions) { - return can(this, null, ...actions); + return can(this, ...actions); }); // Create the User model. diff --git a/perms/mutationReducer.js b/perms/mutationReducer.js index 5f763805d..0078c327b 100644 --- a/perms/mutationReducer.js +++ b/perms/mutationReducer.js @@ -15,7 +15,7 @@ module.exports = { REMOVE_COMMENT_TAG: 'REMOVE_COMMENT_TAG', UPDATE_USER_ROLES: 'UPDATE_USER_ROLES', UPDATE_CONFIG: 'UPDATE_CONFIG', - checkRoles: function (user, perm, context) { + checkRoles: function (user, perm) { switch (perm) { case this.CREATE_COMMENT: return true; diff --git a/perms/queryReducer.js b/perms/queryReducer.js index bea059639..92f1566d5 100644 --- a/perms/queryReducer.js +++ b/perms/queryReducer.js @@ -10,7 +10,7 @@ module.exports = { SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS: 'SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS', SEARCH_OTHERS_COMMENTS: 'SEARCH_OTHERS_COMMENTS', SEARCH_COMMENT_METRICS: 'SEARCH_COMMENT_METRICS', - checkRoles: function (user, perm, context) { + checkRoles: function (user, perm) { switch (perm) { case this.SEARCH_ASSETS: return check(user, ['ADMIN', 'MODERATOR']); diff --git a/perms/rootReducer.js b/perms/rootReducer.js index 344c33ada..7abf92753 100644 --- a/perms/rootReducer.js +++ b/perms/rootReducer.js @@ -1,9 +1,15 @@ module.exports = { - checkRoles: function (user, perm, context) { + DUMMY_ROLE: 'DUMMY_ROLE', + checkRoles: function (user, perm) { // this runs before everything if (user.status === 'BANNED') { return false; } + + switch (perm) { + default: + break; + } } }; From b49ace99eb0443e11db235ed83e383dd3bde6cf9 Mon Sep 17 00:00:00 2001 From: riley Date: Tue, 16 May 2017 16:38:30 -0600 Subject: [PATCH 09/19] add permission for EDIT_COMMENT --- graph/mutators/comment.js | 6 +++--- perms/mutationReducer.js | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index e35080314..538bba931 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -99,7 +99,7 @@ const resolveNewCommentStatus = async (context, {asset_id, body}, wordlist = {}, if (wordlist.banned) { return 'REJECTED'; } - + if (settings.premodLinksEnable && linkify.test(body)) { return 'PREMOD'; } @@ -229,7 +229,7 @@ const edit = async (context, {id, asset_id, edit: {body}}) => { const [wordlist, settings] = await filterNewComment(context, {asset_id, body}); // Determine the new status of the comment. - const status = await resolveNewCommentStatus(context, {asset_id, body}, wordlist, settings); + const status = await resolveNewCommentStatus(context, {asset_id, body}, wordlist, settings); // Execute the edit. await CommentsService.edit(id, context.user.id, {body, status}); @@ -264,7 +264,7 @@ module.exports = (context) => { mutators.Comment.removeCommentTag = (action) => removeCommentTag(context, action); } - if (context.user && context.user.can('mutation:editComment')) { + if (context.user && context.user.can('EDIT_COMMENT')) { mutators.Comment.edit = (action) => edit(context, action); } diff --git a/perms/mutationReducer.js b/perms/mutationReducer.js index 0078c327b..b589bde90 100644 --- a/perms/mutationReducer.js +++ b/perms/mutationReducer.js @@ -8,6 +8,7 @@ module.exports = { CREATE_ACTION: 'CREATE_ACTION', DELETE_ACTION: 'DELETE_ACTION', EDIT_NAME: 'EDIT_NAME', + EDIT_COMMENT: 'EDIT_COMMENT', SET_USER_STATUS: 'SET_USER_STATUS', SUSPEND_USER: 'SUSPEND_USER', SET_COMMENT_STATUS: 'SET_COMMENT_STATUS', @@ -25,6 +26,8 @@ module.exports = { return true; case this.EDIT_NAME: return true; + case this.EDIT_COMMENT: + return true; case this.UPDATE_USER_ROLES: return check(user, ['ADMIN']); case this.SET_USER_STATUS: From c66e3141e0b30adb6ff13df04c82a1c073596728 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Wed, 17 May 2017 11:38:43 -0600 Subject: [PATCH 10/19] actually check permissions --- client/coral-embed-stream/src/components/Embed.js | 4 ++-- client/coral-embed-stream/src/components/Stream.js | 4 ++-- client/coral-framework/utils/roles.js | 5 +++++ graph/resolvers/user.js | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/client/coral-embed-stream/src/components/Embed.js b/client/coral-embed-stream/src/components/Embed.js index 908fd4a90..aa97eb091 100644 --- a/client/coral-embed-stream/src/components/Embed.js +++ b/client/coral-embed-stream/src/components/Embed.js @@ -1,7 +1,7 @@ import React from 'react'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from 'coral-framework/translations'; -import rolesHelper from 'coral-framework/utils/roles'; +import {can} from 'coral-framework/utils/roles'; const lang = new I18n(translations); import {TabBar, Tab, TabContent, Button} from 'coral-ui'; @@ -49,7 +49,7 @@ export default class Embed extends React.Component { {lang.t('myProfile')} - Configure Stream + Configure Stream { commentId && diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index f0ec8f353..225c7fafa 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -11,7 +11,7 @@ import QuestionBox from 'coral-plugin-questionbox/QuestionBox'; import IgnoredCommentTombstone from './IgnoredCommentTombstone'; import SuspendedAccount from 'coral-framework/components/SuspendedAccount'; import RestrictedContent from 'coral-framework/components/RestrictedContent'; -import rolesHelper from 'coral-framework/utils/roles'; +import {can} from 'coral-framework/utils/roles'; import ChangeUsernameContainer from 'coral-sign-in/containers/ChangeUsernameContainer'; @@ -112,7 +112,7 @@ class Stream extends React.Component { {loggedIn && user && } - {loggedIn && } + {loggedIn && } {/* the highlightedComment is isolated after the user followed a permalink */} {highlightedComment diff --git a/client/coral-framework/utils/roles.js b/client/coral-framework/utils/roles.js index 4c9af3ca1..f5a285e17 100644 --- a/client/coral-framework/utils/roles.js +++ b/client/coral-framework/utils/roles.js @@ -18,6 +18,11 @@ const mutationRoles = { const roles = {...basicRoles, ...queryRoles, ...mutationRoles}; export const can = (user, ...perms) => { + + if (!user) { + return false; + } + return perms.every((perm) => { const role = roles[perm]; if (typeof role === 'undefined') { diff --git a/graph/resolvers/user.js b/graph/resolvers/user.js index 6aba327ad..8a6ca69e3 100644 --- a/graph/resolvers/user.js +++ b/graph/resolvers/user.js @@ -14,7 +14,7 @@ const User = { // If the user is not an admin, only return comment list for the owner of // the comments. - if (user && (user.can('SEARCH_OTHERS_COMMENTZ') || user.id === id)) { + if (user && (user.can('SEARCH_OTHERS_COMMENTS') || user.id === id)) { return Comments.getByQuery({author_id: id, sort: 'REVERSE_CHRONOLOGICAL'}); } From 88377b31804fd4bd44e08e8629051798304027bc Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Wed, 17 May 2017 15:37:13 -0600 Subject: [PATCH 11/19] do not show Config tab for non-admins --- .../coral-admin/src/components/ui/Header.js | 27 +++++++++++++------ .../coral-admin/src/components/ui/Layout.js | 7 ++++- .../src/containers/Configure/Configure.js | 7 +++++ client/coral-framework/utils/roles.js | 2 +- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/client/coral-admin/src/components/ui/Header.js b/client/coral-admin/src/components/ui/Header.js index 6418efb10..32df0c011 100644 --- a/client/coral-admin/src/components/ui/Header.js +++ b/client/coral-admin/src/components/ui/Header.js @@ -5,8 +5,14 @@ import styles from './Header.css'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../../translations.json'; import {Logo} from './Logo'; +import {can} from 'coral-framework/utils/roles'; -const CoralHeader = ({handleLogout, showShortcuts = () => {}, restricted = false}) => ( +const CoralHeader = ({ + handleLogout, + showShortcuts = () => {}, + restricted = false, + auth +}) => (
{ @@ -41,13 +47,17 @@ const CoralHeader = ({handleLogout, showShortcuts = () => {}, restricted = false activeClassName={styles.active}> {lang.t('configure.community')} - - {lang.t('configure.configure')} - + { + auth.user && can(auth.user, 'UPDATE_CONFIG') && ( + + {lang.t('configure.configure')} + + ) + }
    @@ -73,6 +83,7 @@ const CoralHeader = ({handleLogout, showShortcuts = () => {}, restricted = false ); CoralHeader.propTypes = { + auth: PropTypes.object.isRequired, showShortcuts: PropTypes.func, handleLogout: PropTypes.func.isRequired, restricted: PropTypes.bool // hide elemnts from a user that's logged out diff --git a/client/coral-admin/src/components/ui/Layout.js b/client/coral-admin/src/components/ui/Layout.js index 6bf9661b7..cb783ce5e 100644 --- a/client/coral-admin/src/components/ui/Layout.js +++ b/client/coral-admin/src/components/ui/Layout.js @@ -4,7 +4,12 @@ import Header from './Header'; import Drawer from './Drawer'; import styles from './Layout.css'; -const Layout = ({children, handleLogout = () => {}, toggleShortcutModal, restricted = false, ...props}) => ( +const Layout = ({ + children, + handleLogout = () => {}, + toggleShortcutModal, + restricted = false, + ...props}) => (
    You must be an administrator to access config settings. Please find the nearest Admin and ask them to level you up!

    ; + } const showSave = Object.keys(this.state.errors).reduce( (bool, error) => this.state.errors[error] ? false : bool, this.state.changed); @@ -172,6 +178,7 @@ class Configure extends Component { } const mapStateToProps = (state) => ({ + auth: state.auth.toJS(), settings: state.settings.toJS() }); export default connect(mapStateToProps)(Configure); diff --git a/client/coral-framework/utils/roles.js b/client/coral-framework/utils/roles.js index f5a285e17..e93744506 100644 --- a/client/coral-framework/utils/roles.js +++ b/client/coral-framework/utils/roles.js @@ -5,7 +5,7 @@ const basicRoles = { }; const queryRoles = { - UPDATE_CONFIG: ['ADMIN', 'MODERATOR'], + UPDATE_CONFIG: ['ADMIN'], ACCESS_ADMIN: ['ADMIN', 'MODERATOR'], VIEW_USER_EMAILS: ['ADMIN'] }; From 4a97db3357803a64df843b230fb926ff3f1a7b0c Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Wed, 17 May 2017 15:41:49 -0600 Subject: [PATCH 12/19] emma's copy --- client/coral-admin/src/containers/LayoutContainer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/coral-admin/src/containers/LayoutContainer.js b/client/coral-admin/src/containers/LayoutContainer.js index 6f14c6684..ce2761ce8 100644 --- a/client/coral-admin/src/containers/LayoutContainer.js +++ b/client/coral-admin/src/containers/LayoutContainer.js @@ -55,7 +55,7 @@ class LayoutContainer extends Component { /> ); } else if (loggedIn) { - return

    you do not have permission to see this page.

    ; + return

    This page is for team use only. Please contact an administrator if you want to join this team.

    ; } return ; } From 54abaeb587d410749ca98bcb28787aece2cc3f0b Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Wed, 17 May 2017 15:47:41 -0600 Subject: [PATCH 13/19] wrap the not-authorized warning in a template --- client/coral-admin/src/containers/LayoutContainer.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/coral-admin/src/containers/LayoutContainer.js b/client/coral-admin/src/containers/LayoutContainer.js index ce2761ce8..adeb8a346 100644 --- a/client/coral-admin/src/containers/LayoutContainer.js +++ b/client/coral-admin/src/containers/LayoutContainer.js @@ -55,7 +55,11 @@ class LayoutContainer extends Component { /> ); } else if (loggedIn) { - return

    This page is for team use only. Please contact an administrator if you want to join this team.

    ; + return ( + +

    This page is for team use only. Please contact an administrator if you want to join this team.

    ; +
    + ); } return ; } From 3fc1029a9315e2daacdf42296427e1175d69a8bd Mon Sep 17 00:00:00 2001 From: riley Date: Wed, 17 May 2017 16:00:01 -0600 Subject: [PATCH 14/19] do not require auth in the Header --- client/coral-admin/src/components/ui/Header.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/coral-admin/src/components/ui/Header.js b/client/coral-admin/src/components/ui/Header.js index 32df0c011..094aeee7d 100644 --- a/client/coral-admin/src/components/ui/Header.js +++ b/client/coral-admin/src/components/ui/Header.js @@ -48,7 +48,7 @@ const CoralHeader = ({ {lang.t('configure.community')} { - auth.user && can(auth.user, 'UPDATE_CONFIG') && ( + auth && auth.user && can(auth.user, 'UPDATE_CONFIG') && ( Date: Wed, 17 May 2017 16:10:02 -0600 Subject: [PATCH 15/19] staff should not see moderate tag. translations --- client/coral-admin/src/components/ui/Header.js | 18 +++++++++++------- client/coral-admin/src/translations.json | 2 ++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/client/coral-admin/src/components/ui/Header.js b/client/coral-admin/src/components/ui/Header.js index 094aeee7d..38797a7c3 100644 --- a/client/coral-admin/src/components/ui/Header.js +++ b/client/coral-admin/src/components/ui/Header.js @@ -26,13 +26,17 @@ const CoralHeader = ({ activeClassName={styles.active}> {lang.t('configure.dashboard')} - - {lang.t('configure.moderate')} - + { + auth && auth.user && can(auth.user, 'MODERATE_COMMENTS') && ( + + {lang.t('configure.moderate')} + + ) + } Date: Thu, 18 May 2017 12:06:07 -0600 Subject: [PATCH 16/19] show config tab to admins. hide menu items from users that can't access admin --- .../coral-admin/src/components/ui/Header.js | 104 +++++++++--------- .../src/containers/Configure/Configure.js | 2 +- .../src/containers/LayoutContainer.js | 2 +- 3 files changed, 53 insertions(+), 55 deletions(-) diff --git a/client/coral-admin/src/components/ui/Header.js b/client/coral-admin/src/components/ui/Header.js index 38797a7c3..181bfe688 100644 --- a/client/coral-admin/src/components/ui/Header.js +++ b/client/coral-admin/src/components/ui/Header.js @@ -10,59 +10,61 @@ import {can} from 'coral-framework/utils/roles'; const CoralHeader = ({ handleLogout, showShortcuts = () => {}, - restricted = false, auth }) => (
    - { - !restricted ?
    - - - {lang.t('configure.dashboard')} - - { - auth && auth.user && can(auth.user, 'MODERATE_COMMENTS') && ( - - {lang.t('configure.moderate')} - - ) - } - - {lang.t('configure.stories')} - - - {lang.t('configure.community')} - - { - auth && auth.user && can(auth.user, 'UPDATE_CONFIG') && ( - - {lang.t('configure.configure')} - - ) - } - + { + auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? + + + {lang.t('configure.dashboard')} + + { + auth && auth.user && can(auth.user, 'MODERATE_COMMENTS') && ( + + {lang.t('configure.moderate')} + + ) + } + + {lang.t('configure.stories')} + + + {lang.t('configure.community')} + + { + auth && auth.user && can(auth.user, 'UPDATE_CONFIG') && ( + + {lang.t('configure.configure')} + + ) + } + + : + null + }
    • @@ -80,17 +82,13 @@ const CoralHeader = ({
    - : - null - }
    ); CoralHeader.propTypes = { auth: PropTypes.object, showShortcuts: PropTypes.func, - handleLogout: PropTypes.func.isRequired, - restricted: PropTypes.bool // hide elemnts from a user that's logged out + handleLogout: PropTypes.func.isRequired }; const lang = new I18n(translations); diff --git a/client/coral-admin/src/containers/Configure/Configure.js b/client/coral-admin/src/containers/Configure/Configure.js index 44c38cd50..79dd76cc5 100644 --- a/client/coral-admin/src/containers/Configure/Configure.js +++ b/client/coral-admin/src/containers/Configure/Configure.js @@ -119,7 +119,7 @@ class Configure extends Component { render () { const {activeSection} = this.state; const section = this.getSection(activeSection); - const {auth: user} = this.props; + const {auth: {user}} = this.props; if (!can(user, 'UPDATE_CONFIG')) { return

    You must be an administrator to access config settings. Please find the nearest Admin and ask them to level you up!

    ; diff --git a/client/coral-admin/src/containers/LayoutContainer.js b/client/coral-admin/src/containers/LayoutContainer.js index adeb8a346..14376b390 100644 --- a/client/coral-admin/src/containers/LayoutContainer.js +++ b/client/coral-admin/src/containers/LayoutContainer.js @@ -57,7 +57,7 @@ class LayoutContainer extends Component { } else if (loggedIn) { return ( -

    This page is for team use only. Please contact an administrator if you want to join this team.

    ; +

    This page is for team use only. Please contact an administrator if you want to join this team.

    ); } From 056ba9a04c3a9d891777482339d0b7e6e6124760 Mon Sep 17 00:00:00 2001 From: riley Date: Thu, 18 May 2017 12:11:11 -0600 Subject: [PATCH 17/19] do the same thing for the Drawer --- .../coral-admin/src/components/ui/Drawer.js | 46 +++++++++++-------- .../coral-admin/src/components/ui/Header.js | 4 +- .../coral-admin/src/components/ui/Layout.js | 1 - 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/client/coral-admin/src/components/ui/Drawer.js b/client/coral-admin/src/components/ui/Drawer.js index 5ac55045c..6a6737883 100644 --- a/client/coral-admin/src/components/ui/Drawer.js +++ b/client/coral-admin/src/components/ui/Drawer.js @@ -4,10 +4,11 @@ import {IndexLink, Link} from 'react-router'; import styles from './Drawer.css'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../../translations.json'; +import {can} from 'coral-framework/utils/roles'; -const CoralDrawer = ({handleLogout, restricted = false}) => ( +const CoralDrawer = ({handleLogout, auth}) => ( - { !restricted ? + { auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
    ( activeClassName={styles.active}> {lang.t('configure.dashboard')} - - {lang.t('configure.moderate')} - - + {lang.t('configure.moderate')} + + ) + } + {lang.t('configure.stories')} + to="/admin/community" + activeClassName={styles.active}> {lang.t('configure.community')} - - {lang.t('configure.configure')} - + { + can(auth.user, 'UPDATE_CONFIG') && + ( + + {lang.t('configure.configure')} + + ) + } Sign Out {`v${process.env.VERSION}`} diff --git a/client/coral-admin/src/components/ui/Header.js b/client/coral-admin/src/components/ui/Header.js index 181bfe688..454aea62b 100644 --- a/client/coral-admin/src/components/ui/Header.js +++ b/client/coral-admin/src/components/ui/Header.js @@ -26,7 +26,7 @@ const CoralHeader = ({ {lang.t('configure.dashboard')} { - auth && auth.user && can(auth.user, 'MODERATE_COMMENTS') && ( + can(auth.user, 'MODERATE_COMMENTS') && ( { - auth && auth.user && can(auth.user, 'UPDATE_CONFIG') && ( + can(auth.user, 'UPDATE_CONFIG') && (
    From 6deee7cb8029abd1b63e16a831dce43a0796cc3f Mon Sep 17 00:00:00 2001 From: riley Date: Thu, 18 May 2017 14:28:43 -0600 Subject: [PATCH 18/19] add a perms utils file --- models/user.js | 2 +- perms/mutationReducer.js | 5 +---- perms/queryReducer.js | 5 +---- perms/rootReducer.js | 8 +------- perms/utils.js | 8 ++++++++ services/users.js | 3 --- 6 files changed, 12 insertions(+), 19 deletions(-) create mode 100644 perms/utils.js diff --git a/models/user.js b/models/user.js index 3d12d49e9..30069abd0 100644 --- a/models/user.js +++ b/models/user.js @@ -163,7 +163,7 @@ UserSchema.index({ * returns true if a commenter is staff */ UserSchema.method('isStaff', function () { - return !!intersection(['ADMIN', 'MODERATOR', 'STAFF'], this.roles).length; + return intersection(['ADMIN', 'MODERATOR', 'STAFF'], this.roles).length !== 0; }); /** diff --git a/perms/mutationReducer.js b/perms/mutationReducer.js index b589bde90..82bc2de42 100644 --- a/perms/mutationReducer.js +++ b/perms/mutationReducer.js @@ -1,7 +1,4 @@ -const intersection = require('lodash/intersection'); -const check = (user, roles) => { - return !!intersection(roles, user.roles).length; -}; +const {check} = require('./utils'); module.exports = { CREATE_COMMENT: 'CREATE_COMMENT', diff --git a/perms/queryReducer.js b/perms/queryReducer.js index 92f1566d5..65a52358a 100644 --- a/perms/queryReducer.js +++ b/perms/queryReducer.js @@ -1,7 +1,4 @@ -const intersection = require('lodash/intersection'); -const check = (user, roles) => { - return !!intersection(roles, user.roles).length; -}; +const {check} = require('./utils'); module.exports = { SEARCH_ASSETS: 'SEARCH_ASSETS', diff --git a/perms/rootReducer.js b/perms/rootReducer.js index 7abf92753..c48fb9c4d 100644 --- a/perms/rootReducer.js +++ b/perms/rootReducer.js @@ -1,15 +1,9 @@ module.exports = { - DUMMY_ROLE: 'DUMMY_ROLE', - checkRoles: function (user, perm) { + checkRoles: function (user /* , perm*/) { // this runs before everything if (user.status === 'BANNED') { return false; } - - switch (perm) { - default: - break; - } } }; diff --git a/perms/utils.js b/perms/utils.js new file mode 100644 index 000000000..e72a49c53 --- /dev/null +++ b/perms/utils.js @@ -0,0 +1,8 @@ +const intersection = require('lodash/intersection'); +const check = (user, roles) => { + return intersection(roles, user.roles).length > 0; +}; + +module.exports = { + check +}; diff --git a/services/users.js b/services/users.js index 09a4f3a55..83a33e6f6 100644 --- a/services/users.js +++ b/services/users.js @@ -389,9 +389,6 @@ module.exports = class UsersService { return Promise.reject(new Error(`role ${role} is not supported`)); } - // 5.11.2017 - Restricting this to a hierarchical system like WordPress - // where you can only set one role at a time. - // I'm not changing the data structure here, because I don't want a migration return UserModel.update({id}, {$set: {roles: [role]}}); } From 17ceadd8c9c0aa3528f2228774b9c0ce612eefc0 Mon Sep 17 00:00:00 2001 From: riley Date: Fri, 19 May 2017 09:18:24 -0600 Subject: [PATCH 19/19] move client-side perms to services --- client/coral-admin/src/components/ui/Drawer.js | 2 +- client/coral-admin/src/components/ui/Header.js | 2 +- client/coral-admin/src/containers/Configure/Configure.js | 2 +- client/coral-admin/src/containers/LayoutContainer.js | 2 +- client/coral-embed-stream/src/components/Embed.js | 2 +- client/coral-embed-stream/src/components/Stream.js | 2 +- client/coral-framework/{utils/roles.js => services/perms.js} | 0 7 files changed, 6 insertions(+), 6 deletions(-) rename client/coral-framework/{utils/roles.js => services/perms.js} (100%) diff --git a/client/coral-admin/src/components/ui/Drawer.js b/client/coral-admin/src/components/ui/Drawer.js index 6a6737883..2fcb5aafe 100644 --- a/client/coral-admin/src/components/ui/Drawer.js +++ b/client/coral-admin/src/components/ui/Drawer.js @@ -4,7 +4,7 @@ import {IndexLink, Link} from 'react-router'; import styles from './Drawer.css'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../../translations.json'; -import {can} from 'coral-framework/utils/roles'; +import {can} from 'coral-framework/services/perms'; const CoralDrawer = ({handleLogout, auth}) => ( diff --git a/client/coral-admin/src/components/ui/Header.js b/client/coral-admin/src/components/ui/Header.js index 454aea62b..f7ba83491 100644 --- a/client/coral-admin/src/components/ui/Header.js +++ b/client/coral-admin/src/components/ui/Header.js @@ -5,7 +5,7 @@ import styles from './Header.css'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../../translations.json'; import {Logo} from './Logo'; -import {can} from 'coral-framework/utils/roles'; +import {can} from 'coral-framework/services/perms'; const CoralHeader = ({ handleLogout, diff --git a/client/coral-admin/src/containers/Configure/Configure.js b/client/coral-admin/src/containers/Configure/Configure.js index 79dd76cc5..d22a19997 100644 --- a/client/coral-admin/src/containers/Configure/Configure.js +++ b/client/coral-admin/src/containers/Configure/Configure.js @@ -15,7 +15,7 @@ import translations from 'coral-admin/src/translations.json'; import StreamSettings from './StreamSettings'; import ModerationSettings from './ModerationSettings'; import TechSettings from './TechSettings'; -import {can} from 'coral-framework/utils/roles'; +import {can} from 'coral-framework/services/perms'; class Configure extends Component { constructor (props) { diff --git a/client/coral-admin/src/containers/LayoutContainer.js b/client/coral-admin/src/containers/LayoutContainer.js index 14376b390..340a18e3b 100644 --- a/client/coral-admin/src/containers/LayoutContainer.js +++ b/client/coral-admin/src/containers/LayoutContainer.js @@ -7,7 +7,7 @@ import {logout} from 'coral-framework/actions/auth'; import {FullLoading} from '../components/FullLoading'; import {toggleModal as toggleShortcutModal} from '../actions/moderation'; import {checkLogin, handleLogin, requestPasswordReset} from '../actions/auth'; -import {can} from 'coral-framework/utils/roles'; +import {can} from 'coral-framework/services/perms'; class LayoutContainer extends Component { componentWillMount() { diff --git a/client/coral-embed-stream/src/components/Embed.js b/client/coral-embed-stream/src/components/Embed.js index aa97eb091..1688ae31e 100644 --- a/client/coral-embed-stream/src/components/Embed.js +++ b/client/coral-embed-stream/src/components/Embed.js @@ -1,7 +1,7 @@ import React from 'react'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from 'coral-framework/translations'; -import {can} from 'coral-framework/utils/roles'; +import {can} from 'coral-framework/services/perms'; const lang = new I18n(translations); import {TabBar, Tab, TabContent, Button} from 'coral-ui'; diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index 225c7fafa..cb5af4eb5 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -11,7 +11,7 @@ import QuestionBox from 'coral-plugin-questionbox/QuestionBox'; import IgnoredCommentTombstone from './IgnoredCommentTombstone'; import SuspendedAccount from 'coral-framework/components/SuspendedAccount'; import RestrictedContent from 'coral-framework/components/RestrictedContent'; -import {can} from 'coral-framework/utils/roles'; +import {can} from 'coral-framework/services/perms'; import ChangeUsernameContainer from 'coral-sign-in/containers/ChangeUsernameContainer'; diff --git a/client/coral-framework/utils/roles.js b/client/coral-framework/services/perms.js similarity index 100% rename from client/coral-framework/utils/roles.js rename to client/coral-framework/services/perms.js