From 0a8f0cb0a6d0b92f8f531429958fb0ee5db4edfe Mon Sep 17 00:00:00 2001 From: Chris Brame Date: Sun, 17 Feb 2019 18:07:12 -0500 Subject: [PATCH] chore(permissions): updates --- src/client/actions/settings.js | 6 +- src/client/actions/types.js | 2 + src/client/api/index.js | 10 ++ .../Settings/SplitSettingsPanel/index.jsx | 2 +- .../containers/Modals/CreateRoleModal.jsx | 84 ++++++++++ .../containers/Modals/DeleteRoleModal.jsx | 108 +++++++++++++ src/client/containers/Modals/index.jsx | 8 +- .../containers/Settings/Permissions/index.jsx | 23 ++- .../Settings/Permissions/permissionBody.jsx | 66 +++++++- src/client/sagas/settings/index.js | 33 ++++ src/controllers/api.js | 75 +++++++++ src/controllers/api/v1/groups.js | 2 +- src/controllers/api/v1/tickets.js | 45 ++++-- src/controllers/tickets.js | 16 +- src/helpers/hbs/helpers.js | 44 ++++- src/models/role.js | 17 ++ src/models/roleorder.js | 58 ++++--- src/permissions/index.js | 27 +++- .../js/angularjs/controllers/accounts.js | 6 +- .../js/angularjs/controllers/singleTicket.js | 4 +- src/public/js/modules/helpers.js | 26 ++- src/public/js/modules/ui.js | 45 +++--- src/routes/index.js | 2 + src/socketio/ticketSocket.js | 12 +- src/views/subviews/singleticket.hbs | 152 ++++++++---------- 25 files changed, 697 insertions(+), 176 deletions(-) create mode 100644 src/client/containers/Modals/CreateRoleModal.jsx create mode 100644 src/client/containers/Modals/DeleteRoleModal.jsx diff --git a/src/client/actions/settings.js b/src/client/actions/settings.js index 4d4294ba7..6edc62063 100644 --- a/src/client/actions/settings.js +++ b/src/client/actions/settings.js @@ -24,7 +24,9 @@ import { CHANGE_DELETED_TICKETS_PAGE, BACKUP_NOW, RESTORE_DELETED_TICKET, - UPDATE_PERMISSIONS + UPDATE_PERMISSIONS, + CREATE_ROLE, + DELETE_ROLE } from './types' export const fetchSettings = createAction(FETCH_SETTINGS.ACTION) @@ -49,4 +51,6 @@ export const restoreDeletedTicket = createAction(RESTORE_DELETED_TICKET.ACTION) export const changeDeletedTicketsPage = createAction(CHANGE_DELETED_TICKETS_PAGE.ACTION, pageIndex => ({ pageIndex })) +export const createRole = createAction(CREATE_ROLE.ACTION) export const updatePermissions = createAction(UPDATE_PERMISSIONS.ACTION) +export const deleteRole = createAction(DELETE_ROLE.ACTION) diff --git a/src/client/actions/types.js b/src/client/actions/types.js index 7e6099cca..e3dd9fe1a 100644 --- a/src/client/actions/types.js +++ b/src/client/actions/types.js @@ -50,3 +50,5 @@ export const FETCH_DELETED_TICKETS = defineAction('FETCH_DELETED_TICKETS', [PEND export const RESTORE_DELETED_TICKET = defineAction('RESTORE_DELETED_TICKET', [SUCCESS, ERROR]) export const CHANGE_DELETED_TICKETS_PAGE = defineAction('CHANGE_DELETED_TICKETS_PAGE') export const UPDATE_PERMISSIONS = defineAction('UPDATE_PERMISSIONS', [PENDING, SUCCESS, ERROR]) +export const CREATE_ROLE = defineAction('CREATE_ROLE', [SUCCESS, ERROR]) +export const DELETE_ROLE = defineAction('DELETE_ROLE', [SUCCESS, ERROR]) diff --git a/src/client/api/index.js b/src/client/api/index.js index b6d157aac..043c29c0d 100644 --- a/src/client/api/index.js +++ b/src/client/api/index.js @@ -158,6 +158,16 @@ api.settings.updatePermissions = payload => { return res.data }) } +api.settings.createRole = ({ name }) => { + return axios.post('/api/v1/roles', { name }).then(res => { + return res.data + }) +} +api.settings.deleteRole = ({ _id, newRoleId }) => { + return axios.delete(`/api/v1/roles/${_id}`, { data: { newRoleId } }).then(res => { + return res.data + }) +} api.common = {} api.common.fetchRoles = () => { diff --git a/src/client/components/Settings/SplitSettingsPanel/index.jsx b/src/client/components/Settings/SplitSettingsPanel/index.jsx index 906db5691..69e854140 100644 --- a/src/client/components/Settings/SplitSettingsPanel/index.jsx +++ b/src/client/components/Settings/SplitSettingsPanel/index.jsx @@ -71,7 +71,7 @@ class SplitSettingsPanel extends React.Component {
{menuItems.map(item => { diff --git a/src/client/containers/Modals/CreateRoleModal.jsx b/src/client/containers/Modals/CreateRoleModal.jsx new file mode 100644 index 000000000..d1045799b --- /dev/null +++ b/src/client/containers/Modals/CreateRoleModal.jsx @@ -0,0 +1,84 @@ +/* + * . .o8 oooo + * .o8 "888 `888 + * .o888oo oooo d8b oooo oooo .oooo888 .ooooo. .oooo.o 888 oooo + * 888 `888""8P `888 `888 d88' `888 d88' `88b d88( "8 888 .8P' + * 888 888 888 888 888 888 888ooo888 `"Y88b. 888888. + * 888 . 888 888 888 888 888 888 .o o. )88b 888 `88b. + * "888" d888b `V88V"V8P' `Y8bod88P" `Y8bod8P' 8""888P' o888o o888o + * ======================================================================== + * Author: Chris Brame + * Updated: 2/16/19 4:29 PM + * Copyright (c) 2014-2019. All rights reserved. + */ + +import React from 'react' +import PropTypes from 'prop-types' +import { observer } from 'mobx-react' +import { observable } from 'mobx' +import { connect } from 'react-redux' + +import { createRole } from 'actions/settings' + +import Button from 'components/Button' +import BaseModal from './BaseModal' + +@observer +class CreateRoleModal extends React.Component { + @observable name = '' + + onNameChange (e) { + this.name = e.target.value + } + + onCreateRoleClicked (e) { + e.preventDefault() + + this.props.createRole({ name: this.name }) + } + + render () { + return ( + +
+
+

Create Role

+

Once created, the role will become editable in the permission editor

+ + + this.onNameChange(e)} + /> +
+
+
+
+
+ ) + } +} + +CreateRoleModal.propTypes = { + createRole: PropTypes.func.isRequired +} + +export default connect( + null, + { createRole } +)(CreateRoleModal) diff --git a/src/client/containers/Modals/DeleteRoleModal.jsx b/src/client/containers/Modals/DeleteRoleModal.jsx new file mode 100644 index 000000000..9f5644d98 --- /dev/null +++ b/src/client/containers/Modals/DeleteRoleModal.jsx @@ -0,0 +1,108 @@ +/* + * . .o8 oooo + * .o8 "888 `888 + * .o888oo oooo d8b oooo oooo .oooo888 .ooooo. .oooo.o 888 oooo + * 888 `888""8P `888 `888 d88' `888 d88' `88b d88( "8 888 .8P' + * 888 888 888 888 888 888 888ooo888 `"Y88b. 888888. + * 888 . 888 888 888 888 888 888 .o o. )88b 888 `88b. + * "888" d888b `V88V"V8P' `Y8bod88P" `Y8bod8P' 8""888P' o888o o888o + * ======================================================================== + * Author: Chris Brame + * Updated: 2/16/19 5:49 PM + * Copyright (c) 2014-2019. All rights reserved. + */ + +import React from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import { observer } from 'mobx-react' +import { observable } from 'mobx' + +import { deleteRole } from 'actions/settings' + +import BaseModal from './BaseModal' +import Button from 'components/Button' +import SingleSelect from 'components/SingleSelect' + +@observer +class DeleteRoleModal extends React.Component { + @observable selectedRole = '' + + onSelectChanged (e) { + this.selectedRole = e.target.value + } + + onFormSubmit (e) { + e.preventDefault() + + this.props.deleteRole({ _id: this.props.role.get('_id'), newRoleId: this.selectedRole }) + } + + render () { + const { role } = this.props + const mappedRoles = this.props.shared.roles + .filter(obj => { + return obj.get('_id') !== role.get('_id') + }) + .map(r => { + return { text: r.get('name'), value: r.get('_id') } + }) + .toArray() + return ( + +
this.onFormSubmit(e)}> +
+

Remove Role

+ Please select the role you wish to assign ALL users to + {/*
*/} +
+
+
+ + this.onSelectChanged(e)} + value={this.selectedRole} + /> +
+
+
+ + WARNING: This will change all accounts with role {role.get('name')} to the selected role + above. + {role.get('isAdmin') && ( + + The role you are about to remove is an admin role. Please ensure there is another Admin role or you + could be locked out! + + )} +
+
+ This is permanent! +
+
+
+
+
+
+ ) + } +} + +DeleteRoleModal.propTypes = { + role: PropTypes.object, + deleteRole: PropTypes.func.isRequired, + shared: PropTypes.object.isRequired +} + +const mapStateToProps = state => ({ + shared: state.shared +}) + +export default connect( + mapStateToProps, + { deleteRole } +)(DeleteRoleModal) diff --git a/src/client/containers/Modals/index.jsx b/src/client/containers/Modals/index.jsx index 5c17cd6f0..e11a2138b 100644 --- a/src/client/containers/Modals/index.jsx +++ b/src/client/containers/Modals/index.jsx @@ -23,7 +23,9 @@ import AddPriorityToTypeModal from './AddPriorityToTypeModal' import CreatePriorityModal from './CreatePriorityModal' import DeletePriorityModal from './DeletePriorityModal' import CreateTagModal from './CreateTagModal' -import CreateTicketModal from 'containers/Modals/CreateTicketModal' +import CreateTicketModal from './CreateTicketModal' +import CreateRoleModal from './CreateRoleModal' +import DeleteRoleModal from './DeleteRoleModal' const MODAL_COMPONENTS = { CREATE_TICKET: CreateTicketModal, @@ -32,7 +34,9 @@ const MODAL_COMPONENTS = { ADD_PRIORITY_TO_TYPE: AddPriorityToTypeModal, CREATE_PRIORITY: CreatePriorityModal, DELETE_PRIORITY: DeletePriorityModal, - CREATE_TAG: CreateTagModal + CREATE_TAG: CreateTagModal, + CREATE_ROLE: CreateRoleModal, + DELETE_ROLE: DeleteRoleModal } const ModalRoot = ({ modalType, modalProps }) => { diff --git a/src/client/containers/Settings/Permissions/index.jsx b/src/client/containers/Settings/Permissions/index.jsx index e74675670..42430541b 100644 --- a/src/client/containers/Settings/Permissions/index.jsx +++ b/src/client/containers/Settings/Permissions/index.jsx @@ -16,7 +16,7 @@ import React from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' -import { fetchRoles, updateRoleOrder } from 'actions/common' +import { showModal, fetchRoles, updateRoleOrder } from 'actions/common' import Button from 'components/Button' import SplitSettingsPanel from 'components/Settings/SplitSettingsPanel' @@ -51,6 +51,12 @@ class PermissionsSettingsContainer extends React.Component { return [] } + onCreateRoleClicked (e) { + e.preventDefault() + + this.props.showModal('CREATE_ROLE') + } + render () { return (
@@ -58,7 +64,15 @@ class PermissionsSettingsContainer extends React.Component { title={'Permissions'} tooltip={'Permission order is top down. ex: Admins at top; Users at bottom.'} subtitle={'Create/Modify Role Permissions'} - rightComponent={
+
+
+ +
@@ -211,10 +266,11 @@ class PermissionBody extends React.Component { PermissionBody.propTypes = { role: PropTypes.object.isRequired, - updatePermissions: PropTypes.func.isRequired + updatePermissions: PropTypes.func.isRequired, + showModal: PropTypes.func.isRequired } export default connect( null, - { updatePermissions } + { updatePermissions, showModal } )(PermissionBody) diff --git a/src/client/sagas/settings/index.js b/src/client/sagas/settings/index.js index e9feadde0..cf94ca833 100644 --- a/src/client/sagas/settings/index.js +++ b/src/client/sagas/settings/index.js @@ -18,10 +18,14 @@ import axios from 'axios' import api from '../../api' import { BACKUP_NOW, + CREATE_ROLE, + DELETE_ROLE, FETCH_BACKUPS, FETCH_DELETED_TICKETS, FETCH_MONGODB_TOOLS, + FETCH_ROLES, FETCH_SETTINGS, + HIDE_MODAL, RESTORE_DELETED_TICKET, UPDATE_COLORSCHEME, UPDATE_MULTIPLE_SETTINGS, @@ -176,6 +180,33 @@ function * updatePermissions ({ payload }) { } } +function * createRole ({ payload }) { + try { + const response = yield call(api.settings.createRole, payload) + yield put({ type: CREATE_ROLE.SUCCESS, response }) + yield put({ type: FETCH_ROLES.ACTION }) + yield put({ type: HIDE_MODAL }) + } catch (error) { + const errorText = error.response.data.error + helpers.UI.showSnackbar(`Error: ${errorText}`, true) + yield put({ type: CREATE_ROLE.ERROR, error }) + } +} + +function * deleteRole ({ payload }) { + try { + const response = yield call(api.settings.deleteRole, payload) + yield put({ type: DELETE_ROLE.SUCCESS, response }) + yield put({ type: FETCH_ROLES.ACTION }) + yield put({ type: HIDE_MODAL }) + helpers.UI.showSnackbar('Role successfully deleted') + } catch (error) { + const errorText = error.response.data.error + helpers.UI.showSnackbar(`Error: ${errorText}`, true) + yield put({ type: DELETE_ROLE.ERROR, error }) + } +} + export default function * settingsWatcher () { yield takeLatest(FETCH_SETTINGS.ACTION, fetchFlow) yield takeLatest(UPDATE_SETTING.ACTION, updateSetting) @@ -188,4 +219,6 @@ export default function * settingsWatcher () { yield takeLatest(RESTORE_DELETED_TICKET.ACTION, restoreDeletedTicket) yield takeLatest(UPDATE_ROLE_ORDER.ACTION, updateRoleOrder) yield takeLatest(UPDATE_PERMISSIONS.ACTION, updatePermissions) + yield takeLatest(CREATE_ROLE.ACTION, createRole) + yield takeLatest(DELETE_ROLE.ACTION, deleteRole) } diff --git a/src/controllers/api.js b/src/controllers/api.js index 4188f2801..ac8201fd7 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -308,6 +308,45 @@ apiController.roles.get = function (req, res) { ) } +apiController.roles.create = function (req, res) { + var name = req.body.name + if (!name) return res.status(400).json({ success: false, error: 'Invalid Post Data' }) + + var roleSchema = require('../models/role') + var roleOrder = require('../models/roleorder') + + async.waterfall( + [ + function (next) { + roleSchema.create({ name: name }, next) + }, + function (role, next) { + if (!role) return next('Invalid Role') + + roleOrder.getOrder(function (err, ro) { + if (err) return next(err) + + ro.order.push(role._id) + + ro.save(function (err, savedRo) { + if (err) return next(err) + + return next(null, role, savedRo) + }) + }) + } + ], + function (err, role, roleOrder) { + if (err) return res.status(400).json({ success: false, error: err }) + + global.roleOrder = roleOrder + global.roles.push(role) + + return res.json({ success: true, role: role, roleOrder: roleOrder }) + } + ) +} + apiController.roles.update = function (req, res) { var _id = req.params.id var data = req.body @@ -332,4 +371,40 @@ apiController.roles.update = function (req, res) { }) } +apiController.roles.delete = function (req, res) { + var _id = req.params.id + var newRoleId = req.body.newRoleId + if (!_id || !newRoleId) return res.status(400).json({ success: false, error: 'Invalid Post Data' }) + + var roleSchema = require('../models/role') + var roleOrderSchema = require('../models/roleorder') + + async.series( + [ + function (done) { + userSchema.updateMany({ role: _id }, { $set: { role: newRoleId } }, done) + }, + function (done) { + roleSchema.deleteOne({ _id: _id }, done) + }, + function (done) { + roleOrderSchema.getOrder(function (err, ro) { + if (err) return done(err) + + ro.removeFromOrder(_id, done) + }) + } + ], + function (err) { + if (err) return res.status(500).json({ success: false, error: err }) + + permissions.register(function (err) { + if (err) return res.status(500).json({ success: false, error: err }) + + return res.json({ success: true }) + }) + } + ) +} + module.exports = apiController diff --git a/src/controllers/api/v1/groups.js b/src/controllers/api/v1/groups.js index 1a71914e9..37e9ae4e8 100644 --- a/src/controllers/api/v1/groups.js +++ b/src/controllers/api/v1/groups.js @@ -43,7 +43,7 @@ var apiGroups = {} apiGroups.get = function (req, res) { var user = req.user var permissions = require('../../../permissions') - var hasPublic = permissions.canThis(user.role, 'ticket:public') + var hasPublic = permissions.canThis(user.role, 'tickets:public') GroupSchema.getAllGroupsOfUser(user._id, function (err, groups) { if (err) return res.status(400).json({ success: false, error: err.message }) diff --git a/src/controllers/api/v1/tickets.js b/src/controllers/api/v1/tickets.js index 3f2e196c5..e7306e994 100644 --- a/src/controllers/api/v1/tickets.js +++ b/src/controllers/api/v1/tickets.js @@ -162,7 +162,7 @@ apiTickets.get = function (req, res) { }) }, function (grps, callback) { - if (permissions.canThis(user.role, 'ticket:public')) { + if (permissions.canThis(user.role, 'tickets:public')) { groupModel.getAllPublicGroups(function (err, publicGroups) { if (err) return callback(err) @@ -176,12 +176,39 @@ apiTickets.get = function (req, res) { }, function (grps, callback) { ticketModel.getTicketsWithObject(grps, object, function (err, results) { - if (!permissions.canThis(user.role, 'notes:view')) { + if (!permissions.canThis(user.role, 'comments:view')) { + _.each(results, function (ticket) { + ticket.comments = [] + }) + } + + if (!permissions.canThis(user.role, 'tickets:notes')) { _.each(results, function (ticket) { ticket.notes = [] }) } + // sanitize + _.each(results, function (ticket) { + ticket.subscribers = _.map(ticket.subscribers, function (s) { + return s._id + }) + + ticket.history = _.map(ticket.history, function (h) { + var obj = { + date: h.date, + _id: h._id, + action: h.action, + description: h.description, + owner: _.clone(h.owner) + } + obj.owner.role = h.owner.role._id + return obj + }) + + ticket.owner.role = ticket.owner.role._id + }) + return callback(err, results) }) } @@ -229,7 +256,7 @@ apiTickets.search = function (req, res) { }) }, function (grps, callback) { - if (permissions.canThis(req.user.role, 'ticket:public')) { + if (permissions.canThis(req.user.role, 'tickets:public')) { groupModel.getAllPublicGroups(function (err, publicGroups) { if (err) return callback(err) @@ -243,7 +270,7 @@ apiTickets.search = function (req, res) { }, function (grps, callback) { ticketModel.getTicketsWithSearchString(grps, searchString, function (err, results) { - if (!permissions.canThis(req.user.role.role, 'notes:view')) { + if (!permissions.canThis(req.user.role.role, 'tickets:notes')) { _.each(results, function (ticket) { ticket.notes = [] }) @@ -549,7 +576,7 @@ apiTickets.single = function (req, res) { } ticket = _.clone(ticket._doc) - if (!permissions.canThis(req.user.role, 'notes:view')) { + if (!permissions.canThis(req.user.role, 'tickets:notes')) { delete ticket.notes } @@ -586,7 +613,7 @@ apiTickets.update = function (req, res) { var user = req.user if (!_.isUndefined(user) && !_.isNull(user)) { var permissions = require('../../../permissions') - if (!permissions.canThis(user.role, 'ticket:update')) { + if (!permissions.canThis(user.role, 'tickets:update')) { return res.status(401).json({ success: false, error: 'Invalid Permissions' }) } var oId = req.params.id @@ -670,7 +697,7 @@ apiTickets.update = function (req, res) { return res.status(400).json({ success: false, error: err.message }) } - if (!permissions.canThis(user.role, 'notes:view')) { + if (!permissions.canThis(user.role, 'tickets:notes')) { t.notes = [] } @@ -795,7 +822,7 @@ apiTickets.postComment = function (req, res) { t.save(function (err, tt) { if (err) return res.status(400).json({ success: false, error: err.message }) - if (!permissions.canThis(req.user.role, 'notes:view')) { + if (!permissions.canThis(req.user.role, 'tickets:notes')) { tt.notes = [] } @@ -1322,7 +1349,7 @@ function parseTicketStats (role, tickets, callback) { if (_.isEmpty(tickets)) return callback({ tickets: tickets, tags: {} }) var t = [] var tags = {} - if (!permissions.canThis(role, 'notes:view')) { + if (!permissions.canThis(role, 'tickets:notes')) { _.each(tickets, function (ticket) { ticket.notes = [] }) diff --git a/src/controllers/tickets.js b/src/controllers/tickets.js index efd262113..455202872 100644 --- a/src/controllers/tickets.js +++ b/src/controllers/tickets.js @@ -309,7 +309,7 @@ ticketsController.processor = function (req, res) { groupSchema.getAllGroupsOfUserNoPopulate(req.user._id, function (err, grps) { if (err) return callback(err) userGroups = grps - if (permissions.canThis(req.user.role, 'ticket:public')) { + if (permissions.canThis(req.user.role, 'tickets:public')) { groupSchema.getAllPublicGroups(function (err, groups) { if (err) return callback(err) userGroups = groups.concat(grps) @@ -325,7 +325,7 @@ ticketsController.processor = function (req, res) { ticketSchema.getTicketsWithObject(grps, object, function (err, results) { if (err) return callback(err) - if (!permissions.canThis(req.user.role, 'notes:view')) { + if (!permissions.canThis(req.user.role, 'tickets:notes')) { _.each(results, function (ticket) { ticket.notes = [] }) @@ -406,7 +406,7 @@ ticketsController.print = function (req, res) { if (err) return handleError(res, err) if (_.isNull(ticket) || _.isUndefined(ticket)) return res.redirect('/tickets') - var hasPublic = permissions.canThis(user.role, 'ticket:public') + var hasPublic = permissions.canThis(user.role, 'tickets:public') if (!_.some(ticket.group.members, user._id)) { if (ticket.group.public && hasPublic) { @@ -417,7 +417,7 @@ ticketsController.print = function (req, res) { } } - if (!permissions.canThis(user.role, 'notes:view')) { + if (!permissions.canThis(user.role, 'tickets:notes')) { ticket.notes = [] } @@ -472,7 +472,7 @@ ticketsController.single = function (req, res) { if (err) return handleError(res, err) if (_.isNull(ticket) || _.isUndefined(ticket)) return res.redirect('/tickets') - var hasPublic = permissions.canThis(user.role, 'ticket:public') + var hasPublic = permissions.canThis(user.role, 'tickets:public') if (!_.some(ticket.group.members, user._id)) { if (ticket.group.public && hasPublic) { // Blank to bypass @@ -482,9 +482,9 @@ ticketsController.single = function (req, res) { } } - if (!permissions.canThis(user.role, 'notes:view')) { - ticket.notes = [] - } + if (!permissions.canThis(user.role, 'comments:view')) ticket.comments = [] + + if (!permissions.canThis(user.role, 'tickets:notes')) ticket.notes = [] content.data.ticket = ticket content.data.ticket.priorityname = ticket.priority.name diff --git a/src/helpers/hbs/helpers.js b/src/helpers/hbs/helpers.js index 1dd76972c..6522bcce5 100644 --- a/src/helpers/hbs/helpers.js +++ b/src/helpers/hbs/helpers.js @@ -647,6 +647,28 @@ var helpers = { return options.inverse(this) }, + hasPermOverRole: function (ownerRole, userRole, perm, options) { + if (_.isUndefined(ownerRole) || _.isUndefined(userRole) || _.isUndefined(perm)) return options.inverse(this) + if ( + typeof ownerRole !== 'object' || + typeof userRole !== 'object' || + _.isUndefined(ownerRole._id) || + _.isUndefined(userRole._id) + ) { + throw new Error('Invalid Type sent to hasPermOverRole. Should be role object') + } + + var p = require('../../permissions') + if (!p.canThis(userRole, perm)) return options.inverse(this) + if (ownerRole._id.toString() === userRole._id.toString()) return options.fn(this) + + if (userRole.isAdmin) return options.fn(this) + var hasHierarchyEnabled = p.hasHierarchyEnabled(userRole._id) + if (hasHierarchyEnabled && p.hasPermOverRole(ownerRole._id, userRole._id)) return options.fn(this) + + return options.inverse(this) + }, + checkPerm: function (user, perm, options) { var P = require('../../permissions') if (_.isUndefined(user)) return options.inverse(this) @@ -658,6 +680,16 @@ var helpers = { return options.inverse(this) }, + checkPermOrAdmin: function (user, perm, options) { + if (_.isUndefined(user)) return options.inverse(this) + if (user.role.isAdmin) return options.fn(this) + + var p = require('../../permissions') + if (p.canThis(user.role, perm)) return options.fn(this) + + options.inverse(this) + }, + checkRole: function (role, perm, options) { var P = require('../../permissions') if (P.canThis(role, perm)) { @@ -782,6 +814,15 @@ var helpers = { randomNum: function () { return Math.floor(Math.random() * (99999 - 10000 + 1)) + 10000 + }, + + shouldShowCommentSection: function (user, options) { + var p = require('../../permissions') + var hasComments = p.canThis(user.role, 'comments:create') + var hasNotes = p.canThis(user.role, 'tickets:notes') + + if (hasComments || hasNotes) return options.fn(this) + return options.inverse(this) } } @@ -798,8 +839,9 @@ helpers.ifLtEq = helpers.if_lteq helpers.unlessLtEq = helpers.unless_lteq helpers.foreach = helpers.forEach helpers.canUser = helpers.checkPerm +helpers.canUserOrAdmin = helpers.checkPermOrAdmin helpers.canUserRole = helpers.checkRole -helpers.canEditSelf = helpers.checkEditSelf +helpers.canEditSelf = helpers.checkEditSelf // This will go away helpers.hasPluginPerm = helpers.checkPlugin helpers.inArray = helpers.hasGroup diff --git a/src/models/role.js b/src/models/role.js index 4343de0f6..d40f708fb 100644 --- a/src/models/role.js +++ b/src/models/role.js @@ -34,12 +34,16 @@ var roleSchema = mongoose.Schema( roleSchema.virtual('isAdmin').get(function () { if (_.isUndefined(global.roles)) return false var role = _.find(global.roles, { normalized: this.normalized }) + if (!role) return false + return _.indexOf(role.grants, 'admin:*') !== -1 }) roleSchema.virtual('isAgent').get(function () { if (_.isUndefined(global.roles)) return false var role = _.find(global.roles, { normalized: this.normalized }) + if (!role) return false + return _.indexOf(role.grants, 'agent:*') !== -1 }) @@ -79,6 +83,19 @@ roleSchema.statics.getRoleByName = function (name, callback) { return q.exec(callback) } +roleSchema.statics.getAgentRoles = function (callback) { + var q = this.model(COLLECTION).find({}) + q.exec(function (err, roles) { + if (err) return callback(err) + + var rolesWithAgent = _.filter(roles, function (role) { + return _.indexOf(role.grants, 'agent:*') !== -1 + }) + + return callback(null, rolesWithAgent) + }) +} + // Alias roleSchema.statics.get = roleSchema.statics.getRole diff --git a/src/models/roleorder.js b/src/models/roleorder.js index 8767601ed..2ed272fab 100644 --- a/src/models/roleorder.js +++ b/src/models/roleorder.js @@ -12,29 +12,41 @@ **/ -var _ = require('lodash'); -var mongoose = require('mongoose'); +var _ = require('lodash') +var mongoose = require('mongoose') -var COLLECTION = 'role_order'; +var COLLECTION = 'role_order' var roleOrder = mongoose.Schema({ - order: [ mongoose.Schema.Types.ObjectId ] -}); - -roleOrder.statics.getOrder = function(callback) { - return this.model(COLLECTION).findOne({}).exec(callback); -}; - -roleOrder.methods.updateOrder = function(order, callback) { - this.order = order; - this.save(callback); -}; - -roleOrder.methods.getHierarchy = function(checkRoleId) { - var idx = _.findIndex(this.order, function(i) { return i.toString() === checkRoleId.toString(); }); - if (idx === -1) return []; - if (idx === 0) return this.order; - return _.drop(this.order, idx); -}; - -module.exports = mongoose.model(COLLECTION, roleOrder, COLLECTION); \ No newline at end of file + order: [mongoose.Schema.Types.ObjectId] +}) + +roleOrder.statics.getOrder = function (callback) { + return this.model(COLLECTION) + .findOne({}) + .exec(callback) +} + +roleOrder.methods.updateOrder = function (order, callback) { + this.order = order + this.save(callback) +} + +roleOrder.methods.getHierarchy = function (checkRoleId) { + var idx = _.findIndex(this.order, function (i) { + return i.toString() === checkRoleId.toString() + }) + if (idx === -1) return [] + if (idx === 0) return this.order + return _.drop(this.order, idx) +} + +roleOrder.methods.removeFromOrder = function (_id, callback) { + this.order = _.filter(this.order, function (o) { + return o.toString() !== _id.toString() + }) + + this.save(callback) +} + +module.exports = mongoose.model(COLLECTION, roleOrder, COLLECTION) diff --git a/src/permissions/index.js b/src/permissions/index.js index fdd632497..7d0c8834f 100644 --- a/src/permissions/index.js +++ b/src/permissions/index.js @@ -38,11 +38,13 @@ var register = function (callback) { * Checks to see if a role as the given action * @param role [role to check against] * @param a [action to check] + * @param adminOverride [Override permission check if idAdmin] * @returns {boolean} */ -var canThis = function (role, a) { +var canThis = function (role, a, adminOverride) { if (_.isUndefined(role)) return false + if (adminOverride === true && role.isAdmin) return true var roles = global.roles if (_.isUndefined(roles)) return false @@ -129,20 +131,21 @@ function hasHierarchyEnabled (roleId) { } function parseRoleHierarchy (roleId) { - var roleOrder = global.roleOrder + var roleOrder = global.roleOrder.order + var idx = _.findIndex(roleOrder, function (i) { return i.toString() === roleId.toString() }) if (idx === -1) return [] - return _.rest(roleOrder, idx) + return _.slice(roleOrder, idx) } function hasPermOverRole (ownRole, extRole) { - var roles = parseRoleHierarchy(ownRole) + var roles = parseRoleHierarchy(extRole) var i = _.find(roles, function (o) { - return o.toString() === extRole.toString() + return o.toString() === ownRole.toString() }) return !_.isUndefined(i) @@ -156,6 +159,18 @@ function isAdmin (roleId, callback) { }) } +function isAdminSync (roleId) { + var roles = global.roles + if (!roles) return false + var role = _.find(roles, function (r) { + return r._id.toString() === roleId.toString() + }) + + if (!role) return false + + return role.isAdmin +} + function buildGrants (obj) { return _.map(obj, function (v, k) { return k + ':' + _.join(v, ' ') @@ -165,10 +180,12 @@ function buildGrants (obj) { module.exports = { register: register, canThis: canThis, + hasHierarchyEnabled: hasHierarchyEnabled, parseRoleHierarchy: parseRoleHierarchy, hasPermOverRole: hasPermOverRole, getRoles: getRoles, isAdmin: isAdmin, + isAdminSync: isAdminSync, buildGrants: buildGrants } diff --git a/src/public/js/angularjs/controllers/accounts.js b/src/public/js/angularjs/controllers/accounts.js index 716dda129..6cc364834 100644 --- a/src/public/js/angularjs/controllers/accounts.js +++ b/src/public/js/angularjs/controllers/accounts.js @@ -219,18 +219,18 @@ define([ var canEdit = false var hasEdit = helpers.canUser('accounts:update') - if (isEditingSelf && helpers.canUserEditSelf(loggedInAccount._id, 'account')) { + if (isEditingSelf && hasEdit) { hasEdit = true canEdit = true } if ( helpers.hasHierarchyEnabled(loggedInAccount.role._id) && - helpers.hasPermOverRole(loggedInAccount.role._id, user.role._id) + helpers.hasPermOverRole(user.role._id, loggedInAccount.role._id) ) canEdit = true - if (!hasEdit && !canEdit) { + if (!canEdit) { // Disable editing user with higher roles. form .find('#aPass') diff --git a/src/public/js/angularjs/controllers/singleTicket.js b/src/public/js/angularjs/controllers/singleTicket.js index 2031bbc6e..207974a8b 100644 --- a/src/public/js/angularjs/controllers/singleTicket.js +++ b/src/public/js/angularjs/controllers/singleTicket.js @@ -582,8 +582,8 @@ define([ UIkit.modal('#addTagModal').hide() }) .error(function (e) { - $log.log('[trudesk:singleTicket:submitAddTags] - ' + e) - helpers.UI.showSnackbar('Error: ' + e.message, true) + $log.log('[trudesk:singleTicket:submitAddTags] - ', e) + helpers.UI.showSnackbar('Error: ' + e.error, true) UIkit.modal('#addTagModal').hide() }) diff --git a/src/public/js/modules/helpers.js b/src/public/js/modules/helpers.js index 7367d30d1..4ab47ae3d 100644 --- a/src/public/js/modules/helpers.js +++ b/src/public/js/modules/helpers.js @@ -1564,10 +1564,12 @@ define([ }) } - helpers.canUser = function (a) { + helpers.canUser = function (a, adminOverride) { var role = window.trudeskSessionService.getUser().role var roles = window.trudeskSessionService.getRoles() + if (adminOverride === true && role.isAdmin) return true + if (_.isUndefined(role)) return false if (_.isUndefined(roles)) return false if (__.hasIn(role, '_id')) role = role._id @@ -1654,17 +1656,33 @@ define([ return _.rest(roleOrder, idx) } - helpers.hasPermOverRole = function (ownRole, extRole) { - var roles = helpers.parseRoleHierarchy(ownRole) + helpers.hasPermOverRole = function (ownerRole, extRole, action, adminOverride) { + if (action && !helpers.canUser(action)) return false + if (!extRole) extRole = window.trudeskSessionService.getUser().role._id + + if (adminOverride === true) { + if (extRole && extRole.role && extRole.role.isAdmin) { + return true + } else { + var r = window.trudeskSessionService.getRoles() + var role = _.find(r, function (_role) { + return _role._id.toString() === extRole.toString() + }) + if (!_.isUndefined(role) && role.isAdmin) return true + } + } + + var roles = helpers.parseRoleHierarchy(extRole) var i = _.find(roles, function (o) { - return o.toString() === extRole.toString() + return o.toString() === ownerRole.toString() }) return !_.isUndefined(i) } helpers.flushRoles = function () { + window.trudeskSessionService.flushRoles() window.react.redux.store.dispatch({ type: 'FETCH_ROLES' }) } diff --git a/src/public/js/modules/ui.js b/src/public/js/modules/ui.js index c628908e8..e44722508 100644 --- a/src/public/js/modules/ui.js +++ b/src/public/js/modules/ui.js @@ -31,7 +31,7 @@ define('modules/ui', [ // loggedInAccount = window.trudeskSessionService.getUser(); socketUi.socket = socket = sock - this.flushRoles(); + this.flushRoles() this.onReconnect() this.onDisconnect() // this.updateUsers() @@ -106,16 +106,16 @@ define('modules/ui', [ socketUi.fetchServerLogs = function () { socket.emit('logs:fetch') } - socketUi.flushRoles = function() { - socket.removeAllListeners('$trudesk:flushRoles'); - socket.on('$trudesk:flushRoles', function() { - helpers.flushRoles(); - }); - }; + socketUi.flushRoles = function () { + socket.removeAllListeners('$trudesk:flushRoles') + socket.on('$trudesk:flushRoles', function () { + helpers.flushRoles() + }) + } - socketUi.sendUpdateTicketStatus = function(id, status) { - socket.emit('updateTicketStatus', {ticketId: id, status: status}); - }; + socketUi.sendUpdateTicketStatus = function (id, status) { + socket.emit('updateTicketStatus', { ticketId: id, status: status }) + } socketUi.onReconnect = function () { socket.removeAllListeners('reconnect') @@ -655,7 +655,12 @@ define('modules/ui', [ socket.removeAllListeners('updateComments') socket.on('updateComments', function (data) { var ticket = data - var canViewNotes = helpers.canUser('notes:view') + var canViewNotes = helpers.canUser('tickets:notes') + var canViewComments = helpers.canUser('comments:view') + + if (!canViewComments) ticket.comments = [] + if (!canViewNotes) ticket.notes = [] + _.each(ticket.comments, function (i) { i.isComment = true }) @@ -700,9 +705,7 @@ define('modules/ui', [ .html(ticket.notes.length) var allCommentsHtml = '' - var commentsHtml = '' - var notesHtml = '' // Build All Comments / Notes Section @@ -763,14 +766,14 @@ define('modules/ui', [ '' + '
' + '
' - if (helpers.canUser('comment:delete') || helpers.canUserEditSelf(item.owner._id, 'comment')) { + if (helpers.hasPermOverRole(item.owner.role._id, null, 'comments:delete', true)) { allCommentsHtml += '
' } - if (helpers.canUser('comment:edit') || helpers.canUserEditSelf(item.owner._id, 'comment')) { + if (helpers.hasPermOverRole(item.owner.role._id, null, 'comments:update', true)) { allCommentsHtml += '
' - if (helpers.canUser('note:delete') || helpers.canUserEditSelf(item.owner._id, 'note')) { + if (helpers.hasPermOverRole(item.owner.role._id, null, 'tickets:notes', true)) { allCommentsHtml += '
' } - if (helpers.canUser('note:edit') || helpers.canUserEditSelf(item.owner._id, 'note')) { + if (helpers.hasPermOverRole(item.owner.role._id, null, 'tickets:notes', true)) { allCommentsHtml += '
' - if (helpers.canUser('comment:delete') || helpers.canUserEditSelf(comment.owner._id, 'comment')) { + if (helpers.hasPermOverRole(comment.owner.role._id, null, 'comments:delete', true)) { commentsHtml += '
' } - if (helpers.canUser('comment:edit') || helpers.canUserEditSelf(comment.owner._id, 'comment')) { + if (helpers.hasPermOverRole(comment.owner.role._id, null, 'comments:update', true)) { commentsHtml += '
' - if (helpers.canUser('note:delete') || helpers.canUserEditSelf(note.owner._id, 'note')) { + if (helpers.hasPermOverRole(note.owner.role._id, null, 'tickets:notes', true)) { notesHtml += '
' } - if (helpers.canUser('note:edit') || helpers.canUserEditSelf(note.owner._id, 'note')) { + if (helpers.hasPermOverRole(note.owner.role._id, null, 'tickets:notes', true)) { notesHtml += '
{{#if data.ticket.assignee.image}} {{data.ticket.assignee.fullname}} Profile Image @@ -84,13 +84,13 @@ {{else}}
Default Profile Image @@ -121,7 +121,7 @@
Type - {{#canUser data.user "tickets:update"}} + {{#hasPermOverRole data.ticket.owner.role data.user.role "tickets:update"}} {{else}}
{{data.ticket.priorityname}}
- {{/canUser}} + {{/hasPermOverRole}}
Group - {{#canUser data.user "tickets:update"}} + {{#hasPermOverRole data.ticket.owner.role data.user.role "tickets:update"}} -
- - -
-
-
- + {{#canUser data.common.loggedInAccount 'comments:create'}} +
+
+ +
+ +
-
- -
+
+
+ +
+
+ +
+ {{/canUser}} - {{#canUser data.common.loggedInAccount 'note:create'}} -