From 75d149158b01270b4b72c293291b23e1527cb690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20J=C3=A4gle?= Date: Mon, 4 Dec 2017 14:07:25 +0100 Subject: [PATCH] Setting based permissions - downport (#158) * Allow maintenance of per-setting permissions (cherry picked from commit eed869a) * Implicitly assign and revoke setting group permissions (cherry picked from commit 28b769b) * Improve Display of setting permissions (cherry picked from commit 8523456) * Add path to permission title (cherry picked from commit c87a30d) * Permission to access setting permissions (cherry picked from commit 48b1076) * Adapt wording (cherry picked from commit daccad8) * UI-adaptation: Allow users with permission 'manage-selected-permissions' to see and change the affected settings. However, this is not reactive: Once the permissions for a particular setting are changed, the user needs to log off and on again before it becomes effective in the UI. This is most probably a consequence of the CachedCollection. This collection needed to be changed on permission-change. In the backend however, the permissions become effective immediately. (cherry picked from commit 00e4bb5) * Don't adapt sorting on the client side (cherry picked from commit 9b71b62) * Fix: Apply changed setting permissions reactively (cherry picked from commit 293ad73) * Move setting-based permissions to own collection (cherry picked from commit 8f59f1c) * Unify collections for setting and other permissions again into one (cherry picked from commit 8d923c2) * Get rid of frontend exceptions on changing selected settings (cherry picked from commit a7fdc87) * - Sort permissions by group - Do not try to create permissions for hidden settings in higher-level-callbacks - Remove `setting-permissions` collection - fully integrated into `permissions` (cherry picked from commit f007231) * Harmonize wording in German (cherry picked from commit 5cf5df2) --- package.json | 4 +- .../client/stylesheets/permissions.css | 4 + .../client/views/permissions.html | 108 +++++--- .../client/views/permissions.js | 115 +++++++-- .../lib/rocketchat.js | 4 + .../server/methods/addPermissionToRole.js | 18 +- .../methods/removeRoleFromPermission.js | 26 +- .../server/publications/permissions.js | 17 +- .../server/startup.js | 230 ++++++++++++------ packages/rocketchat-i18n/i18n/de.i18n.json | 2 + packages/rocketchat-i18n/i18n/en.i18n.json | 2 + .../server/methods/saveSetting.js | 8 +- .../server/publications/settings.js | 14 +- .../client/SettingsCachedCollection.js | 31 +++ .../rocketchat-ui-admin/client/admin.html | 4 +- packages/rocketchat-ui-admin/client/admin.js | 81 ++++-- .../rocketchat-ui-admin/client/adminFlex.html | 2 +- .../rocketchat-ui-admin/client/adminFlex.js | 15 +- .../client/accountBox.js | 2 +- 19 files changed, 510 insertions(+), 177 deletions(-) create mode 100644 packages/rocketchat-ui-admin/client/SettingsCachedCollection.js diff --git a/package.json b/package.json index a24bd267011a..ebb7df79e0e1 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,9 @@ "chat" ], "scripts": { - "start": "meteor npm i && meteor", + "start": "meteor npm i && meteor run", + "debug": "meteor run --inspect", + "debug-brk": "meteor run --inspect-brk", "lint": "eslint .", "lint-fix": "eslint . --fix", "stylelint": "stylelint packages/**/*.css", diff --git a/packages/rocketchat-authorization/client/stylesheets/permissions.css b/packages/rocketchat-authorization/client/stylesheets/permissions.css index 5b3fa94211c2..942994ad1169 100644 --- a/packages/rocketchat-authorization/client/stylesheets/permissions.css +++ b/packages/rocketchat-authorization/client/stylesheets/permissions.css @@ -9,6 +9,10 @@ font-weight: bold !important; } + & .section:not(.section-collapsed) { + inline-size: fit-content; + } + & .permission-grid { & th { position: relative; diff --git a/packages/rocketchat-authorization/client/views/permissions.html b/packages/rocketchat-authorization/client/views/permissions.html index e0e47d6b426d..5b63d667e007 100644 --- a/packages/rocketchat-authorization/client/views/permissions.html +++ b/packages/rocketchat-authorization/client/views/permissions.html @@ -1,36 +1,76 @@ + diff --git a/packages/rocketchat-authorization/client/views/permissions.js b/packages/rocketchat-authorization/client/views/permissions.js index 6205927af5a1..07ed97a84d0f 100644 --- a/packages/rocketchat-authorization/client/views/permissions.js +++ b/packages/rocketchat-authorization/client/views/permissions.js @@ -1,45 +1,105 @@ /* globals ChatPermissions */ +import {permissionLevel} from '../../lib/rocketchat'; + +const whereNotSetting = { + $where: function() { + return this.level !== permissionLevel.SETTING; + }.toString() +}; Template.permissions.helpers({ - role() { + roles() { return Template.instance().roles.get(); }, - permission() { - return ChatPermissions.find({}, { - sort: { - _id: 1 + permissions() { + return ChatPermissions.find(whereNotSetting, //the $where seems to have no effect - filtered as workaround after fetch() + { + sort: { + _id: 1 + } + }).fetch() + .filter((setting) => !setting.level); + }, + + settingPermissions() { + return ChatPermissions.find({ + level: permissionLevel.SETTING + }, + { + sort: { //sorting seems not to be copied from the publication, we need to request it explicitly in find() + group: 1, + section: 1 } - }); + }).fetch() + .filter((setting) => setting.group); //group permissions are assigned implicitly, we can hide them. $exists: {group:false} not supported by Minimongo }, - granted(roles) { + hasPermission() { + return RocketChat.authz.hasAllPermission('access-permissions'); + }, + + hasSettingPermission() { + return RocketChat.authz.hasAllPermission('access-setting-permissions'); + }, + + settingPermissionExpanded() { + return Template.instance().settingPermissionsExpanded.get(); + } +}); + +Template.permissions.events({ + 'click .js-toggle-setting-permissions'(event, instance) { + instance.settingPermissionsExpanded.set(!instance.settingPermissionsExpanded.get()); + } +}); + +Template.permissions.onCreated(function() { + this.settingPermissionsExpanded = new ReactiveVar(false); + this.roles = new ReactiveVar([]); + + Tracker.autorun(() => { + this.roles.set(RocketChat.models.Roles.find().fetch()); + }); +}); + +Template.permissionsTable.helpers({ + granted(roles, role) { if (roles) { - if (roles.indexOf(this._id) !== -1) { + if (roles.indexOf(role._id) !== -1) { return 'checked'; } } }, - permissionName() { - return `${ this._id }`; - }, - - permissionDescription() { - return `${ this._id }_description`; + permissionName(permission) { + if (permission.level === permissionLevel.SETTING) { + let path = ''; + if (permission.group) { + path = `${ t(permission.group) } > `; + } + if (permission.section) { + path = `${ path }${ t(permission.section) } > `; + } + path = `${ path }${ t(permission.settingId) }`; + return path; + } else { + return t(permission._id); + } }, - hasPermission() { - return RocketChat.authz.hasAllPermission('access-permissions'); + permissionDescription(permission) { + return t(`${ permission._id }_description`); } }); -Template.permissions.events({ +Template.permissionsTable.events({ 'click .role-permission'(e, instance) { const permission = e.currentTarget.getAttribute('data-permission'); const role = e.currentTarget.getAttribute('data-role'); - if (instance.permissionByRole[permission].indexOf(role) === -1) { + if (!instance.permissionByRole[permission] // the permissino has this role not assigned at all (undefined) + || instance.permissionByRole[permission].indexOf(role) === -1) { return Meteor.call('authorization:addPermissionToRole', permission, role); } else { return Meteor.call('authorization:removeRoleFromPermission', permission, role); @@ -47,8 +107,7 @@ Template.permissions.events({ } }); -Template.permissions.onCreated(function() { - this.roles = new ReactiveVar([]); +Template.permissionsTable.onCreated(function() { this.permissionByRole = {}; this.actions = { added: {}, @@ -56,11 +115,7 @@ Template.permissions.onCreated(function() { }; Tracker.autorun(() => { - this.roles.set(RocketChat.models.Roles.find().fetch()); - }); - - Tracker.autorun(() => { - ChatPermissions.find().observeChanges({ + const observer = { added: (id, fields) => { this.permissionByRole[id] = fields.roles; }, @@ -70,6 +125,14 @@ Template.permissions.onCreated(function() { removed: (id) => { delete this.permissionByRole[id]; } - }); + }; + if (this.data.collection === 'Chat') { + ChatPermissions.find(whereNotSetting).observeChanges(observer); + } + + if (this.data.collection === 'Setting') { + ChatPermissions.find({level: permissionLevel.SETTING}).observeChanges(observer); + } }); }); + diff --git a/packages/rocketchat-authorization/lib/rocketchat.js b/packages/rocketchat-authorization/lib/rocketchat.js index 6445a1b8b5f0..22dc30e0348a 100644 --- a/packages/rocketchat-authorization/lib/rocketchat.js +++ b/packages/rocketchat-authorization/lib/rocketchat.js @@ -1 +1,5 @@ RocketChat.authz = {}; + +export const permissionLevel = { + SETTING: 'setting' +}; diff --git a/packages/rocketchat-authorization/server/methods/addPermissionToRole.js b/packages/rocketchat-authorization/server/methods/addPermissionToRole.js index bae94f4d3dc8..e2d2e85c2a9b 100644 --- a/packages/rocketchat-authorization/server/methods/addPermissionToRole.js +++ b/packages/rocketchat-authorization/server/methods/addPermissionToRole.js @@ -1,12 +1,28 @@ +import {permissionLevel} from '../../lib/rocketchat'; + Meteor.methods({ 'authorization:addPermissionToRole'(permission, role) { - if (!Meteor.userId() || !RocketChat.authz.hasPermission(Meteor.userId(), 'access-permissions')) { + if (!Meteor.userId() || !RocketChat.authz.hasPermission(Meteor.userId(), 'access-permissions') + || (permission.level === permissionLevel.SETTING && !RocketChat.authz.hasPermission(Meteor.userId(), 'access-setting-permissions')) + ) { throw new Meteor.Error('error-action-not-allowed', 'Adding permission is not allowed', { method: 'authorization:addPermissionToRole', action: 'Adding_permission' }); } + // for setting-based-permissions, authorize the group access as well + const addParentPermissions = function(permissionId, role) { + const permission = RocketChat.models.Permissions.findOneById(permissionId); + if (permission.groupPermissionId) { + const groupPermission = RocketChat.models.Permissions.findOneById(permission.groupPermissionId); + if (groupPermission.roles.indexOf(role) === -1) { + RocketChat.models.Permissions.addRole(permission.groupPermissionId, role); + } + } + }; + + addParentPermissions(permission, role); return RocketChat.models.Permissions.addRole(permission, role); } }); diff --git a/packages/rocketchat-authorization/server/methods/removeRoleFromPermission.js b/packages/rocketchat-authorization/server/methods/removeRoleFromPermission.js index 0602d1e3437b..d9c828bba5fb 100644 --- a/packages/rocketchat-authorization/server/methods/removeRoleFromPermission.js +++ b/packages/rocketchat-authorization/server/methods/removeRoleFromPermission.js @@ -1,12 +1,34 @@ +import {permissionLevel} from '../../lib/rocketchat'; + Meteor.methods({ 'authorization:removeRoleFromPermission'(permission, role) { - if (!Meteor.userId() || !RocketChat.authz.hasPermission(Meteor.userId(), 'access-permissions')) { + if (!Meteor.userId() || !RocketChat.authz.hasPermission(Meteor.userId(), 'access-permissions') + || (permission.level === permissionLevel.SETTING && !RocketChat.authz.hasPermission(Meteor.userId(), 'access-setting-permissions')) + ) { throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed', { method: 'authorization:removeRoleFromPermission', action: 'Accessing_permissions' }); } - return RocketChat.models.Permissions.removeRole(permission, role); + // for setting based permissions, revoke the group permission once all setting permissions + // related to this group have been removed + const removeStaleParentPermissions = function(permissionId, role) { + const permission = RocketChat.models.Permissions.findOneById(permissionId); + if (permission.groupPermissionId) { + const groupPermission = RocketChat.models.Permissions.findOneById(permission.groupPermissionId); + if (groupPermission.roles.indexOf(role) !== -1) { + // the role has the group permission assigned, so check whether it's still needed + if (RocketChat.models.Permissions.find({ + groupPermissionId: permission.groupPermissionId, + roles: role + }).count() === 0) { + RocketChat.models.Permissions.removeRole(permission.groupPermissionId, role); + } + } + } + }; + RocketChat.models.Permissions.removeRole(permission, role); + removeStaleParentPermissions(permission, role); } }); diff --git a/packages/rocketchat-authorization/server/publications/permissions.js b/packages/rocketchat-authorization/server/publications/permissions.js index 029109db025c..eb65e7bb5ee4 100644 --- a/packages/rocketchat-authorization/server/publications/permissions.js +++ b/packages/rocketchat-authorization/server/publications/permissions.js @@ -1,3 +1,5 @@ +import {permissionLevel} from '../../lib/rocketchat'; + Meteor.methods({ 'permissions/get'(updatedAt) { this.unblock(); @@ -9,7 +11,12 @@ Meteor.methods({ update: records.filter((record) => { return record._updatedAt > updatedAt; }), - remove: RocketChat.models.Permissions.trashFindDeletedAfter(updatedAt, {}, {fields: {_id: 1, _deletedAt: 1}}).fetch() + remove: RocketChat.models.Permissions.trashFindDeletedAfter(updatedAt, {}, { + fields: { + _id: 1, + _deletedAt: 1 + } + }).fetch() }; } @@ -20,4 +27,12 @@ Meteor.methods({ RocketChat.models.Permissions.on('changed', (type, permission) => { RocketChat.Notifications.notifyLoggedInThisInstance('permissions-changed', type, permission); + + if (permission.level && permission.level === permissionLevel.SETTING) { + // if the permission changes, the effect on the visible settings depends on the role affected. + // The selected-settings-based consumers have to react accordingly and either add or remove the + // setting from the user's collection + const setting = RocketChat.models.Settings.findOneById(permission.settingId); + RocketChat.Notifications.notifyLoggedInThisInstance('private-settings-changed', 'auth', setting); + } }); diff --git a/packages/rocketchat-authorization/server/startup.js b/packages/rocketchat-authorization/server/startup.js index bd1ea3ff6e44..230c3f21018e 100644 --- a/packages/rocketchat-authorization/server/startup.js +++ b/packages/rocketchat-authorization/server/startup.js @@ -1,4 +1,7 @@ /* eslint no-multi-spaces: 0 */ +/* globals SystemLogger */ + +import {permissionLevel} from '../lib/rocketchat'; Meteor.startup(function() { // Note: @@ -6,85 +9,174 @@ Meteor.startup(function() { // then we can define edit--message instead of edit-message // 2. admin, moderator, and user roles should not be deleted as they are referened in the code. const permissions = [ - { _id: 'access-permissions', roles : ['admin'] }, - { _id: 'add-oauth-service', roles : ['admin'] }, - { _id: 'add-user-to-joined-room', roles : ['admin', 'owner', 'moderator'] }, - { _id: 'add-user-to-any-c-room', roles : ['admin'] }, - { _id: 'add-user-to-any-p-room', roles : [] }, - { _id: 'archive-room', roles : ['admin', 'owner'] }, - { _id: 'assign-admin-role', roles : ['admin'] }, - { _id: 'ban-user', roles : ['admin', 'owner', 'moderator'] }, - { _id: 'bulk-create-c', roles : ['admin'] }, - { _id: 'bulk-register-user', roles : ['admin'] }, - { _id: 'create-c', roles : ['admin', 'user', 'bot'] }, - { _id: 'create-d', roles : ['admin', 'user', 'bot'] }, - { _id: 'create-p', roles : ['admin', 'user', 'bot'] }, - { _id: 'create-user', roles : ['admin'] }, - { _id: 'clean-channel-history', roles : ['admin'] }, // special permission to bulk delete a channel's mesages - { _id: 'delete-c', roles : ['admin'] }, - { _id: 'delete-d', roles : ['admin'] }, - { _id: 'delete-message', roles : ['admin', 'owner', 'moderator'] }, - { _id: 'delete-p', roles : ['admin'] }, - { _id: 'delete-user', roles : ['admin'] }, - { _id: 'edit-message', roles : ['admin', 'owner', 'moderator'] }, - { _id: 'edit-other-user-active-status', roles : ['admin'] }, - { _id: 'edit-other-user-info', roles : ['admin'] }, - { _id: 'edit-other-user-password', roles : ['admin'] }, - { _id: 'edit-privileged-setting', roles : ['admin'] }, - { _id: 'edit-room', roles : ['admin', 'owner', 'moderator'] }, - { _id: 'force-delete-message', roles : ['admin', 'owner'] }, - { _id: 'join-without-join-code', roles : ['admin', 'bot'] }, - { _id: 'manage-assets', roles : ['admin'] }, - { _id: 'manage-emoji', roles : ['admin'] }, - { _id: 'manage-integrations', roles : ['admin'] }, - { _id: 'manage-own-integrations', roles : ['admin', 'bot'] }, - { _id: 'manage-oauth-apps', roles : ['admin'] }, - { _id: 'mention-all', roles : ['admin', 'owner', 'moderator', 'user'] }, - { _id: 'mute-user', roles : ['admin', 'owner', 'moderator'] }, - { _id: 'remove-user', roles : ['admin', 'owner', 'moderator'] }, - { _id: 'run-import', roles : ['admin'] }, - { _id: 'run-migration', roles : ['admin'] }, - { _id: 'set-moderator', roles : ['admin', 'owner'] }, - { _id: 'set-owner', roles : ['admin', 'owner'] }, - { _id: 'send-many-messages', roles : ['admin', 'bot'] }, - { _id: 'set-leader', roles : ['admin', 'owner'] }, - { _id: 'unarchive-room', roles : ['admin'] }, - { _id: 'view-c-room', roles : ['admin', 'user', 'bot', 'anonymous'] }, - { _id: 'user-generate-access-token', roles : ['admin'] }, - { _id: 'view-d-room', roles : ['admin', 'user', 'bot'] }, - { _id: 'view-full-other-user-info', roles : ['admin'] }, - { _id: 'view-history', roles : ['admin', 'user', 'anonymous'] }, - { _id: 'view-joined-room', roles : ['guest', 'bot', 'anonymous'] }, - { _id: 'view-join-code', roles : ['admin'] }, - { _id: 'view-logs', roles : ['admin'] }, - { _id: 'view-other-user-channels', roles : ['admin'] }, - { _id: 'view-p-room', roles : ['admin', 'user', 'anonymous'] }, - { _id: 'view-privileged-setting', roles : ['admin'] }, - { _id: 'view-room-administration', roles : ['admin'] }, - { _id: 'view-statistics', roles : ['admin'] }, - { _id: 'view-user-administration', roles : ['admin'] }, - { _id: 'preview-c-room', roles : ['admin', 'user', 'anonymous'] }, - { _id: 'view-outside-room', roles : ['admin', 'owner', 'moderator', 'user'] } + {_id: 'access-permissions', roles: ['admin']}, + {_id: 'access-setting-permissions', roles: ['admin']}, + {_id: 'add-oauth-service', roles: ['admin']}, + {_id: 'add-user-to-joined-room', roles: ['admin', 'owner', 'moderator']}, + {_id: 'add-user-to-any-c-room', roles: ['admin']}, + {_id: 'add-user-to-any-p-room', roles: []}, + {_id: 'archive-room', roles: ['admin', 'owner']}, + {_id: 'assign-admin-role', roles: ['admin']}, + {_id: 'ban-user', roles: ['admin', 'owner', 'moderator']}, + {_id: 'bulk-create-c', roles: ['admin']}, + {_id: 'bulk-register-user', roles: ['admin']}, + {_id: 'create-c', roles: ['admin', 'user', 'bot']}, + {_id: 'create-d', roles: ['admin', 'user', 'bot']}, + {_id: 'create-p', roles: ['admin', 'user', 'bot']}, + {_id: 'create-user', roles: ['admin']}, + {_id: 'clean-channel-history', roles: ['admin']}, // special permission to bulk delete a channel's mesages + {_id: 'delete-c', roles: ['admin']}, + {_id: 'delete-d', roles: ['admin']}, + {_id: 'delete-message', roles: ['admin', 'owner', 'moderator']}, + {_id: 'delete-p', roles: ['admin']}, + {_id: 'delete-user', roles: ['admin']}, + {_id: 'edit-message', roles: ['admin', 'owner', 'moderator']}, + {_id: 'edit-other-user-active-status', roles: ['admin']}, + {_id: 'edit-other-user-info', roles: ['admin']}, + {_id: 'edit-other-user-password', roles: ['admin']}, + {_id: 'edit-privileged-setting', roles: ['admin']}, + {_id: 'edit-room', roles: ['admin', 'owner', 'moderator']}, + {_id: 'force-delete-message', roles: ['admin', 'owner']}, + {_id: 'join-without-join-code', roles: ['admin', 'bot']}, + {_id: 'manage-assets', roles: ['admin']}, + {_id: 'manage-emoji', roles: ['admin']}, + {_id: 'manage-integrations', roles: ['admin']}, + {_id: 'manage-own-integrations', roles: ['admin', 'bot']}, + {_id: 'manage-oauth-apps', roles: ['admin']}, + {_id: 'mention-all', roles: ['admin', 'owner', 'moderator', 'user']}, + {_id: 'mute-user', roles: ['admin', 'owner', 'moderator']}, + {_id: 'remove-user', roles: ['admin', 'owner', 'moderator']}, + {_id: 'run-import', roles: ['admin']}, + {_id: 'run-migration', roles: ['admin']}, + {_id: 'set-moderator', roles: ['admin', 'owner']}, + {_id: 'set-owner', roles: ['admin', 'owner']}, + {_id: 'send-many-messages', roles: ['admin', 'bot']}, + {_id: 'set-leader', roles: ['admin', 'owner']}, + {_id: 'unarchive-room', roles: ['admin']}, + {_id: 'view-c-room', roles: ['admin', 'user', 'bot', 'anonymous']}, + {_id: 'user-generate-access-token', roles: ['admin']}, + {_id: 'view-d-room', roles: ['admin', 'user', 'bot']}, + {_id: 'view-full-other-user-info', roles: ['admin']}, + {_id: 'view-history', roles: ['admin', 'user', 'anonymous']}, + {_id: 'view-joined-room', roles: ['guest', 'bot', 'anonymous']}, + {_id: 'view-join-code', roles: ['admin']}, + {_id: 'view-logs', roles: ['admin']}, + {_id: 'view-other-user-channels', roles: ['admin']}, + {_id: 'view-p-room', roles: ['admin', 'user', 'anonymous']}, + {_id: 'view-privileged-setting', roles: ['admin']}, + {_id: 'manage-selected-settings', roles: ['admin']}, + {_id: 'view-room-administration', roles: ['admin']}, + {_id: 'view-statistics', roles: ['admin']}, + {_id: 'view-user-administration', roles: ['admin']}, + {_id: 'preview-c-room', roles: ['admin', 'user', 'anonymous']}, + {_id: 'view-outside-room', roles: ['admin', 'owner', 'moderator', 'user']} ]; for (const permission of permissions) { if (!RocketChat.models.Permissions.findOneById(permission._id)) { - RocketChat.models.Permissions.upsert(permission._id, {$set: permission }); + RocketChat.models.Permissions.upsert(permission._id, {$set: permission}); } } const defaultRoles = [ - { name: 'admin', scope: 'Users', description: 'Admin' }, - { name: 'moderator', scope: 'Subscriptions', description: 'Moderator' }, - { name: 'leader', scope: 'Subscriptions', description: 'Leader' }, - { name: 'owner', scope: 'Subscriptions', description: 'Owner' }, - { name: 'user', scope: 'Users', description: '' }, - { name: 'bot', scope: 'Users', description: '' }, - { name: 'guest', scope: 'Users', description: '' }, - { name: 'anonymous', scope: 'Users', description: '' } + {name: 'admin', scope: 'Users', description: 'Admin'}, + {name: 'moderator', scope: 'Subscriptions', description: 'Moderator'}, + {name: 'leader', scope: 'Subscriptions', description: 'Leader'}, + {name: 'owner', scope: 'Subscriptions', description: 'Owner'}, + {name: 'user', scope: 'Users', description: ''}, + {name: 'bot', scope: 'Users', description: ''}, + {name: 'guest', scope: 'Users', description: ''}, + {name: 'anonymous', scope: 'Users', description: ''} ]; for (const role of defaultRoles) { - RocketChat.models.Roles.upsert({ _id: role.name }, { $setOnInsert: { scope: role.scope, description: role.description || '', protected: true } }); + RocketChat.models.Roles.upsert({_id: role.name}, { + $setOnInsert: { + scope: role.scope, + description: role.description || '', + protected: true + } + }); } + + + // setting-based permissions + const getSettingPermissionId = function(settingId) { + return `change-setting-${ settingId }`; + }; + + const getPreviousPermissions = function(settingId) { + const previousSettingPermissions = {}; + + const selector = {level: permissionLevel.SETTING}; + if (settingId) { + selector.settingId = settingId; + } + + RocketChat.models.Permissions.find(selector).fetch().forEach( + function(permission) { + previousSettingPermissions[permission._id] = permission; + }); + return previousSettingPermissions; + }; + const createSettingPermission = function(setting, previousSettingPermissions) { + const permissionId = getSettingPermissionId(setting._id); + const permission = { + _id: permissionId, + level: permissionLevel.SETTING, + //copy those setting-properties which are needed to properly publish the setting-based permissions + settingId: setting._id, + group: setting.group, + section: setting.section, + sorter: setting.sorter + }; + // copy previously assigned roles if available + if (previousSettingPermissions[permissionId] && previousSettingPermissions[permissionId].roles) { + permission.roles = previousSettingPermissions[permissionId].roles; + } else { + permission.roles = []; + } + if (setting.group) { + permission.groupPermissionId = getSettingPermissionId(setting.group); + } + if (setting.section) { + permission.sectionPermissionId = getSettingPermissionId(setting.section); + } + RocketChat.models.Permissions.upsert(permission._id, {$set: permission}); + delete previousSettingPermissions[permissionId]; + }; + + const createPermissionsForExistingSettings = function() { + const previousSettingPermissions = getPreviousPermissions(); + + RocketChat.models.Settings.findNotHidden().fetch().forEach((setting) => { + createSettingPermission(setting, previousSettingPermissions); + }); + + // remove permissions for non-existent settings + for (const obsoletePermission in previousSettingPermissions) { + if (previousSettingPermissions.hasOwnProperty(obsoletePermission)) { + RocketChat.models.Permissions.remove({_id: obsoletePermission}); + SystemLogger.info('Removed permission', obsoletePermission); + } + } + }; + + // for each setting which already exists, create a permission to allow changing just this one setting + createPermissionsForExistingSettings(); + + // register a callback for settings for be create in higher-level-packages + const createPermissionForAddedSetting = function(settingId) { + const previousSettingPermissions = getPreviousPermissions(settingId); + const setting = RocketChat.models.Settings.findOneById(settingId); + if (setting) { + if (!setting.hidden) { + createSettingPermission(setting, previousSettingPermissions); + } + } else { + SystemLogger.error('Could not create permission for setting', settingId); + } + }; + + RocketChat.settings.onload('*', createPermissionForAddedSetting); }); diff --git a/packages/rocketchat-i18n/i18n/de.i18n.json b/packages/rocketchat-i18n/i18n/de.i18n.json index 0a0710dddf3a..6bad319a19b1 100755 --- a/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/packages/rocketchat-i18n/i18n/de.i18n.json @@ -16,6 +16,7 @@ "access-mailer_description": "Berechtigung, Massen-E-Mails an alle Benutzer zu versenden.", "access-permissions": "Zugriff auf die Berechtigungs-Übersicht", "access-permissions_description": "Anpassen der Berechtigungen für die unterschiedlichen Rollen.", + "access-setting-permissions": "Einstellungsbezogene Berechtigungen ändern", "Access_not_authorized": "Der Zugriff ist nicht gestattet.", "Access_Token_URL": "URL des Access-Token", "Accessing_permissions": "Zugriff auf Berechtigungen", @@ -1575,6 +1576,7 @@ "Set_as_leader": "Zum Diskussionsleiter ernennen", "Set_as_moderator": "Zum Moderator ernennen", "Set_as_owner": "Zum Besitzer machen", + "Setting_permissions": "Einstellungsbezogene Berechtigungen", "Settings": "Einstellungen", "Settings_updated": "Die Einstellungen wurden aktualisiert", "Share_Location_Title": "Standort teilen?", diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 0c44f166f29e..9298ba3fdae8 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -16,6 +16,7 @@ "access-mailer_description": "Permission to send mass email to all users.", "access-permissions": "Access Permissions Screen", "access-permissions_description": "Modify permissions for various roles.", + "access-setting-permissions": "Modify setting-based permissions", "Access_not_authorized": "Access not authorized", "Access_Token_URL": "Access Token URL", "Accessing_permissions": "Accessing permissions", @@ -1605,6 +1606,7 @@ "Set_as_leader": "Set as leader", "Set_as_moderator": "Set as moderator", "Set_as_owner": "Set as owner", + "Setting_permissions": "Setting-based permissions", "Settings": "Settings", "Settings_updated": "Settings updated", "Share_Location_Title": "Share Location?", diff --git a/packages/rocketchat-lib/server/methods/saveSetting.js b/packages/rocketchat-lib/server/methods/saveSetting.js index 963876864e31..a52250ecb95a 100644 --- a/packages/rocketchat-lib/server/methods/saveSetting.js +++ b/packages/rocketchat-lib/server/methods/saveSetting.js @@ -8,9 +8,13 @@ Meteor.methods({ }); } - if (!RocketChat.authz.hasPermission(Meteor.userId(), 'edit-privileged-setting')) { + if (!RocketChat.authz.hasPermission(Meteor.userId(), 'edit-privileged-setting') + && !( + RocketChat.authz.hasAllPermission(Meteor.userId(), ['manage-selected-settings', `change-setting-${ _id }`]) + )) { throw new Meteor.Error('error-action-not-allowed', 'Editing settings is not allowed', { - method: 'saveSetting' + method: 'saveSetting', + settingId: _id }); } diff --git a/packages/rocketchat-lib/server/publications/settings.js b/packages/rocketchat-lib/server/publications/settings.js index cbc39fb4b8f6..696a6c15df15 100644 --- a/packages/rocketchat-lib/server/publications/settings.js +++ b/packages/rocketchat-lib/server/publications/settings.js @@ -29,12 +29,16 @@ Meteor.methods({ return []; } this.unblock(); - if (!RocketChat.authz.hasPermission(Meteor.userId(), 'view-privileged-setting')) { - return []; - } const records = RocketChat.models.Settings.find().fetch().filter(function(record) { - return record.hidden !== true; + if (RocketChat.authz.hasPermission(Meteor.userId(), 'view-privileged-setting')) { + return record.hidden !== true; + } else if (RocketChat.authz.hasPermission(Meteor.userId(), 'manage-selected-settings')) { + return record.hidden !== true && RocketChat.authz.hasPermission(Meteor.userId(), `change-setting-${ record._id }`); + } else { + return false; + } }); + if (updatedAt instanceof Date) { return { update: records.filter(function(record) { @@ -67,5 +71,5 @@ RocketChat.Notifications.streamAll.allowRead('private-settings-changed', functio if (this.userId == null) { return false; } - return RocketChat.authz.hasPermission(this.userId, 'view-privileged-setting'); + return RocketChat.authz.hasAtLeastOnePermission(this.userId, ['view-privileged-setting', 'manage-selected-settings']); }); diff --git a/packages/rocketchat-ui-admin/client/SettingsCachedCollection.js b/packages/rocketchat-ui-admin/client/SettingsCachedCollection.js new file mode 100644 index 000000000000..bf48d9c8edac --- /dev/null +++ b/packages/rocketchat-ui-admin/client/SettingsCachedCollection.js @@ -0,0 +1,31 @@ +import _ from 'underscore'; + +export class PrivateSettingsCachedCollection extends RocketChat.CachedCollection { + constructor() { + super({ + name: 'private-settings', + eventType: 'onLogged' + }); + } + + setupListener(eventType, eventName) { + super.setupListener(eventType, eventName); + + // private settings also need to listen to a change of authorizationsfor the setting-based authorizations + RocketChat.Notifications[eventType || this.eventType](eventName || this.eventName, (t, record) => { + this.log('record received', t, record); + if (t === 'auth') { + if (! (RocketChat.authz.hasAllPermission([`change-setting-${ record._id }`, 'manage-selected-settings']) + || RocketChat.authz.hasAllPermission('view-privileged-setting'))) { + this.collection.remove(record._id); + RoomManager.close(record.t + record.name); + } else { + delete record.$loki; + this.collection.upsert({_id: record._id}, _.omit(record, '_id')); + } + + this.saveCache(); + } + }); + } +} diff --git a/packages/rocketchat-ui-admin/client/admin.html b/packages/rocketchat-ui-admin/client/admin.html index b47e6e83cf5f..e360e0724672 100644 --- a/packages/rocketchat-ui-admin/client/admin.html +++ b/packages/rocketchat-ui-admin/client/admin.html @@ -15,7 +15,7 @@

- {{#unless hasPermission 'view-privileged-setting'}} + {{#unless hasSettingPermission}}

{{_ "You_are_not_authorized_to_view_this_page"}}

{{else}} {{#if description}} @@ -23,7 +23,7 @@

{{description}}

{{/if}} -
+
{{#each sections}}
{{#if section}} diff --git a/packages/rocketchat-ui-admin/client/admin.js b/packages/rocketchat-ui-admin/client/admin.js index b5a675758998..086902570e59 100644 --- a/packages/rocketchat-ui-admin/client/admin.js +++ b/packages/rocketchat-ui-admin/client/admin.js @@ -1,5 +1,9 @@ /*globals jscolor, i18nDefaultQuery */ +import _ from 'underscore'; +import s from 'underscore.string'; import toastr from 'toastr'; +import {PrivateSettingsCachedCollection} from './SettingsCachedCollection'; + const TempSettings = new Mongo.Collection(null); RocketChat.TempSettings = TempSettings; @@ -28,7 +32,12 @@ const setFieldValue = function(settingId, value, type, editor) { const selectedRooms = Template.instance().selectedRooms.get(); selectedRooms[settingId] = value; Template.instance().selectedRooms.set(selectedRooms); - TempSettings.update({ _id: settingId }, { $set: { value, changed: JSON.stringify(RocketChat.settings.collectionPrivate.findOne(settingId).value) !== JSON.stringify(value) } }); + TempSettings.update({_id: settingId}, { + $set: { + value, + changed: JSON.stringify(RocketChat.settings.collectionPrivate.findOne(settingId).value) !== JSON.stringify(value) + } + }); break; default: input.val(value).change(); @@ -37,10 +46,7 @@ const setFieldValue = function(settingId, value, type, editor) { Template.admin.onCreated(function() { if (RocketChat.settings.cachedCollectionPrivate == null) { - RocketChat.settings.cachedCollectionPrivate = new RocketChat.CachedCollection({ - name: 'private-settings', - eventType: 'onLogged' - }); + RocketChat.settings.cachedCollectionPrivate = new PrivateSettingsCachedCollection(); RocketChat.settings.collectionPrivate = RocketChat.settings.cachedCollectionPrivate.collection; RocketChat.settings.cachedCollectionPrivate.init(); } @@ -78,12 +84,15 @@ Template.admin.onDestroyed(function() { }); Template.admin.helpers({ + hasSettingPermission() { + return RocketChat.authz.hasAtLeastOnePermission(['view-privileged-setting', 'manage-selected-settings']); + }, languages() { const languages = TAPi18n.getLanguages(); let result = Object.keys(languages).map(key => { const language = languages[key]; - return _.extend(language, { key }); + return _.extend(language, {key}); }); result = _.sortBy(result, 'key'); @@ -107,7 +116,13 @@ Template.admin.helpers({ if (!group) { return; } - const settings = RocketChat.settings.collectionPrivate.find({ group: groupId }, { sort: { section: 1, sorter: 1, i18nLabel: 1 }}).fetch(); + const settings = RocketChat.settings.collectionPrivate.find({group: groupId}, { + sort: { + section: 1, + sorter: 1, + i18nLabel: 1 + } + }).fetch(); const sections = {}; Object.keys(settings).forEach(key => { @@ -135,7 +150,7 @@ Template.admin.helpers({ sections[settingSection].push(setting); }); - group.sections = Object.keys(sections).map(key =>{ + group.sections = Object.keys(sections).map(key => { const value = sections[key]; return { section: key, @@ -167,7 +182,7 @@ Template.admin.helpers({ } let found = 0; - Object.keys(enableQuery).forEach(key =>{ + Object.keys(enableQuery).forEach(key => { const item = enableQuery[key]; if (TempSettings.findOne(item) != null) { found++; @@ -252,7 +267,7 @@ Template.admin.helpers({ return Meteor.absoluteUrl(url); }, selectedOption(_id, val) { - const option = RocketChat.settings.collectionPrivate.findOne({ _id }); + const option = RocketChat.settings.collectionPrivate.findOne({_id}); return option && option.value === val; }, random() { @@ -283,7 +298,12 @@ Template.admin.helpers({ } const onChange = function() { const value = codeMirror.getValue(); - TempSettings.update({ _id }, { $set: { value, changed: RocketChat.settings.collectionPrivate.findOne(_id).value !== value }}); + TempSettings.update({_id}, { + $set: { + value, + changed: RocketChat.settings.collectionPrivate.findOne(_id).value !== value + } + }); }; const onChangeDelayed = _.debounce(onChange, 500); codeMirror.on('change', onChangeDelayed); @@ -325,14 +345,14 @@ Template.admin.helpers({ return color.replace(/theme-color-/, '@'); }, showResetButton() { - const setting = TempSettings.findOne({ _id: this._id }, { fields: { value: 1, packageValue: 1 }}); + const setting = TempSettings.findOne({_id: this._id}, {fields: {value: 1, packageValue: 1}}); return this.type !== 'asset' && setting.value !== setting.packageValue && !this.blocked; } }); Template.admin.events({ 'change .input-monitor, keyup .input-monitor': _.throttle(function(e) { - let value = _.trim($(e.target).val()); + let value = s.trim($(e.target).val()); switch (this.type) { case 'int': value = parseInt(value); @@ -353,9 +373,9 @@ Template.admin.events({ }); }, 500), 'change select[name=color-editor]'(e) { - const value = _.trim($(e.target).val()); - TempSettings.update({ _id: this._id }, { $set: { editor: value }}); - RocketChat.settings.collectionPrivate.update({ _id: this._id }, { $set: { editor: value }}); + const value = s.trim($(e.target).val()); + TempSettings.update({_id: this._id}, {$set: {editor: value}}); + RocketChat.settings.collectionPrivate.update({_id: this._id}, {$set: {editor: value}}); }, 'click .submit .discard'() { const group = FlowRouter.getParam('group'); @@ -364,9 +384,16 @@ Template.admin.events({ changed: true }; const settings = TempSettings.find(query, { - fields: { _id: 1, value: 1, packageValue: 1 }}).fetch(); + fields: {_id: 1, value: 1, packageValue: 1} + }).fetch(); settings.forEach(function(setting) { - const oldSetting = RocketChat.settings.collectionPrivate.findOne({ _id: setting._id }, { fields: { value: 1, type: 1, editor: 1 }}); + const oldSetting = RocketChat.settings.collectionPrivate.findOne({_id: setting._id}, { + fields: { + value: 1, + type: 1, + editor: 1 + } + }); setFieldValue(setting._id, oldSetting.value, oldSetting.type, oldSetting.editor); }); }, @@ -385,14 +412,14 @@ Template.admin.events({ const group = FlowRouter.getParam('group'); const section = $(e.target).data('section'); if (section === '') { - settings = TempSettings.find({ group, section: { $exists: false }}, { fields: { _id: 1 }}).fetch(); + settings = TempSettings.find({group, section: {$exists: false}}, {fields: {_id: 1}}).fetch(); } else { - settings = TempSettings.find({ group, section }, { fields: { _id: 1 }}).fetch(); + settings = TempSettings.find({group, section}, {fields: {_id: 1}}).fetch(); } settings.forEach(function(setting) { const defaultValue = getDefaultSetting(setting._id); setFieldValue(setting._id, defaultValue.packageValue, defaultValue.type, defaultValue.editor); - TempSettings.update({_id: setting._id }, { + TempSettings.update({_id: setting._id}, { $set: { value: defaultValue.packageValue, changed: RocketChat.settings.collectionPrivate.findOne(setting._id).value !== defaultValue.packageValue @@ -402,14 +429,14 @@ Template.admin.events({ }, 'click .submit .save'() { const group = FlowRouter.getParam('group'); - const query = { group, changed: true }; - const settings = TempSettings.find(query, { fields: { _id: 1, value: 1, editor: 1 }}).fetch(); + const query = {group, changed: true}; + const settings = TempSettings.find(query, {fields: {_id: 1, value: 1, editor: 1}}).fetch(); if (!_.isEmpty(settings)) { RocketChat.settings.batchSet(settings, function(err) { if (err) { return handleError(err); } - TempSettings.update({ changed: true }, { $unset: { changed: 1 }}); + TempSettings.update({changed: true}, {$unset: {changed: 1}}); toastr.success(TAPi18n.__('Settings_updated')); }); } @@ -541,7 +568,7 @@ Template.admin.events({ selectedRooms[this.id] = (selectedRooms[this.id] || []).concat(doc); instance.selectedRooms.set(selectedRooms); const value = selectedRooms[this.id]; - TempSettings.update({ _id: this.id }, { $set: { value }}); + TempSettings.update({_id: this.id}, {$set: {value}}); event.currentTarget.value = ''; event.currentTarget.focus(); }, @@ -554,7 +581,7 @@ Template.admin.events({ }); instance.selectedRooms.set(selectedRooms); const value = selectedRooms[settingId]; - TempSettings.update({ _id: settingId }, { + TempSettings.update({_id: settingId}, { $set: { value } @@ -571,7 +598,7 @@ Template.admin.onRendered(function() { const hasColor = TempSettings.find({ group: FlowRouter.getParam('group'), type: 'color' - }, { fields: { _id: 1, editor: 1 }}).fetch().length; + }, {fields: {_id: 1, editor: 1}}).fetch().length; if (hasColor) { Meteor.setTimeout(function() { $('.colorpicker-input').each(function(index, el) { diff --git a/packages/rocketchat-ui-admin/client/adminFlex.html b/packages/rocketchat-ui-admin/client/adminFlex.html index 98c3207f9b20..aa66794fe688 100644 --- a/packages/rocketchat-ui-admin/client/adminFlex.html +++ b/packages/rocketchat-ui-admin/client/adminFlex.html @@ -31,7 +31,7 @@

{{_ "Administration"}}

{{/each}} - {{#if hasPermission 'view-privileged-setting'}} + {{#if hasSettingPermission}}

{{_ "Settings"}}