From c2e6e0fa2cdf80df4dbf942c6f26d9e45db8fcf2 Mon Sep 17 00:00:00 2001 From: Reid Wakida Date: Wed, 9 Sep 2015 12:04:52 -1000 Subject: [PATCH 1/7] Create RocketChat authorization package that handles role and permission based authorization Leverages alanning:roles package to associate a user to a role. Uses alanning:roles optional "group" parameter to limit the role's scope to either the global level or room level. The global level is applicable to users that can perform administrative functions. The room level is applicable to users that can perform room specific administrative functions (like a moderator). A role can have zero or more permissions. Permissions and their association to roles are defined by this package Authorization checks are based on whether or not the user has a role or permission. The roles, permissions, and their association are statically defined at this time. Eventually, there should be an API to dynamically create a role and associate it to static permission(s). Old 'isAdmin' and '.admin is true' checks have been replaced with corresponding hasPermission authorization checks. Additionally, code that automatically assigned admin privileges are updated to assign 'admin' role instead. channel/direct message/private group code checks authorization to edit properties (e.g. title) and edit/delete messages (regardless of the system level allow edit/delete settings). - user with 'admin' role are authorized to do anything - room creator is assigned 'moderator' role that can edit the room and edit/delete messages - members can only edit/delete their own messages IF system wide settings permit them to. v19 migration will - add 'admin' role to users with admin:true property - add 'moderator' role scoped to room for room creators - add 'user' role to all users. There are known issues unrelated to the changes made - If a user with edit/delete message room permissions logs out then a user without edit/delete message room permissions logs in, then they will see edit/delete icons. The server will deny execution - edit/delete icons are not reactive Thus if the system level allow edit/delete message setting is toggled, the icons will not reflect it. The server will deny execution. --- .meteor/packages | 1 + .meteor/versions | 2 + client/lib/chatMessages.coffee | 10 ++- client/methods/deleteMessage.coffee | 7 +- client/methods/updateMessage.coffee | 8 +- client/stylesheets/base.less | 4 +- client/views/admin/admin.coffee | 2 - client/views/admin/admin.html | 2 +- client/views/admin/adminFlex.html | 41 +++++---- client/views/admin/adminStatistics.coffee | 2 - client/views/admin/adminStatistics.html | 2 +- client/views/admin/rooms/adminRoomInfo.coffee | 3 + client/views/admin/rooms/adminRoomInfo.html | 30 ++++--- client/views/admin/rooms/adminRooms.coffee | 2 - client/views/admin/rooms/adminRooms.html | 2 +- .../views/admin/users/adminUserChannels.html | 10 ++- client/views/admin/users/adminUserEdit.html | 38 ++++---- client/views/admin/users/adminUserInfo.coffee | 5 +- client/views/admin/users/adminUserInfo.html | 22 +++-- client/views/admin/users/adminUsers.coffee | 2 - client/views/admin/users/adminUsers.html | 2 +- client/views/app/message.coffee | 11 ++- client/views/app/room.coffee | 8 +- client/views/app/sideNav/channels.coffee | 7 +- client/views/app/sideNav/channels.html | 4 +- .../views/app/sideNav/listChannelsFlex.coffee | 3 +- .../views/app/sideNav/listChannelsFlex.html | 2 + client/views/app/sideNav/privateGroups.coffee | 7 +- client/views/app/sideNav/privateGroups.html | 4 +- client/views/app/sideNav/sideNav.coffee | 3 +- client/views/app/sideNav/userStatus.coffee | 4 +- client/views/app/sideNav/userStatus.html | 2 +- client/views/app/userInfo.coffee | 2 - client/views/app/userInfo.html | 2 +- packages/rocketchat-authorization/README.md | 41 +++++++++ .../client/hasPermission.coffee | 40 +++++++++ .../client/hasRole.coffee | 6 ++ .../client/startup.coffee | 2 + .../lib/permissions.coffee | 1 + .../lib/rocketchat.coffee | 1 + packages/rocketchat-authorization/package.js | 38 ++++++++ .../server/functions/addUsersToRoles.coffee | 26 ++++++ .../functions/getPermissionsForRole.coffee | 9 ++ .../server/functions/getRoles.coffee | 2 + .../server/functions/getRolesForUser.coffee | 7 ++ .../server/functions/getUsersInRole.coffee | 6 ++ .../server/functions/hasPermission.coffee | 12 +++ .../server/functions/hasRole.coffee | 4 + .../functions/removeUsersFromRoles.coffee | 26 ++++++ .../server/publication.coffee | 3 + .../server/startup.coffee | 87 +++++++++++++++++++ .../server/methods/setAdminStatus.coffee | 7 +- .../server/methods/updateUser.coffee | 3 +- .../settings/server/addOAuthService.coffee | 2 +- .../settings/server/methods.coffee | 2 +- .../settings/server/publication.coffee | 3 +- .../server/methods/getStatistics.coffee | 2 +- server/lib/accounts.coffee | 8 +- server/methods/createChannel.coffee | 5 ++ server/methods/createPrivateGroup.coffee | 6 ++ server/methods/deleteMessage.coffee | 13 ++- server/methods/deleteUser.coffee | 2 +- server/methods/eraseRoom.coffee | 7 +- server/methods/migrate.coffee | 3 +- server/methods/removeUserFromRoom.coffee | 6 ++ server/methods/saveRoomName.coffee | 6 +- server/methods/setUserActiveStatus.coffee | 3 +- server/methods/updateMessage.coffee | 17 ++-- server/publications/adminRooms.coffee | 3 +- server/publications/fullUserData.coffee | 4 +- server/publications/userChannels.coffee | 3 +- server/publications/userData.coffee | 1 - server/startup/initialData.coffee | 10 +-- server/startup/migrations/v19.coffee | 28 ++++++ 74 files changed, 568 insertions(+), 143 deletions(-) create mode 100644 packages/rocketchat-authorization/README.md create mode 100644 packages/rocketchat-authorization/client/hasPermission.coffee create mode 100644 packages/rocketchat-authorization/client/hasRole.coffee create mode 100644 packages/rocketchat-authorization/client/startup.coffee create mode 100644 packages/rocketchat-authorization/lib/permissions.coffee create mode 100644 packages/rocketchat-authorization/lib/rocketchat.coffee create mode 100644 packages/rocketchat-authorization/package.js create mode 100644 packages/rocketchat-authorization/server/functions/addUsersToRoles.coffee create mode 100644 packages/rocketchat-authorization/server/functions/getPermissionsForRole.coffee create mode 100644 packages/rocketchat-authorization/server/functions/getRoles.coffee create mode 100644 packages/rocketchat-authorization/server/functions/getRolesForUser.coffee create mode 100644 packages/rocketchat-authorization/server/functions/getUsersInRole.coffee create mode 100644 packages/rocketchat-authorization/server/functions/hasPermission.coffee create mode 100644 packages/rocketchat-authorization/server/functions/hasRole.coffee create mode 100644 packages/rocketchat-authorization/server/functions/removeUsersFromRoles.coffee create mode 100644 packages/rocketchat-authorization/server/publication.coffee create mode 100644 packages/rocketchat-authorization/server/startup.coffee create mode 100644 server/startup/migrations/v19.coffee diff --git a/.meteor/packages b/.meteor/packages index 113c4d33f646..ac1dbedc0d01 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -85,3 +85,4 @@ todda00:friendly-slugs underscorestring:underscore.string yasaricli:slugify yasinuslu:blaze-meta +rocketchat:authorization diff --git a/.meteor/versions b/.meteor/versions index b09ffefd8ab5..be5c92d06392 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -6,6 +6,7 @@ accounts-meteor-developer@1.0.4 accounts-oauth@1.1.5 accounts-password@1.1.1 accounts-twitter@1.0.4 +alanning:roles@1.2.13 aldeed:simple-schema@1.3.3 arunoda:streams@0.1.17 autoupdate@1.2.1 @@ -100,6 +101,7 @@ reactive-dict@1.1.0 reactive-var@1.0.5 reload@1.1.3 retry@1.0.3 +rocketchat:authorization@0.0.1 rocketchat:autolinker@0.0.1 rocketchat:colors@0.0.1 rocketchat:custom-oauth@1.0.0 diff --git a/client/lib/chatMessages.coffee b/client/lib/chatMessages.coffee index 2ecd8cc4634c..15f9289252f6 100644 --- a/client/lib/chatMessages.coffee +++ b/client/lib/chatMessages.coffee @@ -40,11 +40,15 @@ class @ChatMessages return -1 edit: (element, index) -> - return unless RocketChat.settings.get 'Message_AllowEditing' + id = element.getAttribute("id") + message = ChatMessage.findOne { _id: id } + hasPermission = RocketChat.authz.hasAtLeastOnePermission('edit-message', message.rid) + editAllowed = RocketChat.settings.get 'Message_AllowEditing' + editOwn = message?.u?._id is Meteor.userId() + + return unless hasPermission or (editAllowed and editOwn) return if element.classList.contains("system") this.clearEditing() - id = element.getAttribute("id") - message = ChatMessage.findOne { _id: id, 'u._id': Meteor.userId() } this.input.value = message.msg this.editing.element = element this.editing.index = index or this.getEditingIndex(element) diff --git a/client/methods/deleteMessage.coffee b/client/methods/deleteMessage.coffee index f6a02bf90b2d..24e418d6d28b 100644 --- a/client/methods/deleteMessage.coffee +++ b/client/methods/deleteMessage.coffee @@ -3,9 +3,14 @@ Meteor.methods if not Meteor.userId() throw new Meteor.Error 203, t('general.User_logged_out') - if not RocketChat.settings.get 'Message_AllowDeleting' + hasPermission = RocketChat.authz.hasAtLeastOnePermission('delete-message', message.rid) + deleteAllowed = RocketChat.settings.get 'Message_AllowDeleting' + deleteOwn = message?.u?._id is Meteor.userId() + + unless hasPermission or (deleteAllowed and deleteOwn) throw new Meteor.Error 'message-deleting-not-allowed', t('Message_deleting_not_allowed') + Tracker.nonreactive -> ChatMessage.remove _id: message._id diff --git a/client/methods/updateMessage.coffee b/client/methods/updateMessage.coffee index 1366c4e31cd2..773bff4a1676 100644 --- a/client/methods/updateMessage.coffee +++ b/client/methods/updateMessage.coffee @@ -3,7 +3,13 @@ Meteor.methods if not Meteor.userId() throw new Meteor.Error 203, t('User_logged_out') - if not RocketChat.settings.get 'Message_AllowEditing' + originalMessage = ChatMessage.findOne message._id + + hasPermission = RocketChat.authz.hasAtLeastOnePermission('edit-message', message.rid) + editAllowed = RocketChat.settings.get 'Message_AllowEditing' + editOwn = originalMessage?.u?._id is Meteor.userId() + + unless hasPermission or (editAllowed and editOwn) throw new Meteor.Error 'message-editing-not-allowed', t('Message_editing_not_allowed') Tracker.nonreactive -> diff --git a/client/stylesheets/base.less b/client/stylesheets/base.less index 407cc4eb4da7..6bca06133d45 100644 --- a/client/stylesheets/base.less +++ b/client/stylesheets/base.less @@ -2375,14 +2375,14 @@ a.github-fork { display: none; cursor: pointer; } - &.own:hover:not(.system) .edit-message { + &:hover:not(.system) .edit-message { display: inline-block; } .delete-message { display: none; cursor: pointer; } - &.own:hover:not(.system) .delete-message { + &:hover:not(.system) .delete-message { display: inline-block; } .user { diff --git a/client/views/admin/admin.coffee b/client/views/admin/admin.coffee index 07d4c7eaa98a..9ab84108dea2 100644 --- a/client/views/admin/admin.coffee +++ b/client/views/admin/admin.coffee @@ -1,6 +1,4 @@ Template.admin.helpers - isAdmin: -> - return Meteor.user().admin is true group: -> group = FlowRouter.getParam('group') group ?= Settings.findOne({ type: 'group' })?._id diff --git a/client/views/admin/admin.html b/client/views/admin/admin.html index 9bb7f03c4beb..b559af0e10cb 100644 --- a/client/views/admin/admin.html +++ b/client/views/admin/admin.html @@ -7,7 +7,7 @@

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

You are not authorized to view this page.

{{else}} {{#with group}} diff --git a/client/views/admin/adminFlex.html b/client/views/admin/adminFlex.html index 164c694c210f..cc7ebf873170 100644 --- a/client/views/admin/adminFlex.html +++ b/client/views/admin/adminFlex.html @@ -7,24 +7,35 @@

{{_ "Administration"}}

diff --git a/client/views/admin/adminStatistics.coffee b/client/views/admin/adminStatistics.coffee index 4622e627e507..8c27a0374efd 100644 --- a/client/views/admin/adminStatistics.coffee +++ b/client/views/admin/adminStatistics.coffee @@ -1,6 +1,4 @@ Template.adminStatistics.helpers - isAdmin: -> - return Meteor.user().admin is true isReady: -> return Template.instance().ready.get() statistics: -> diff --git a/client/views/admin/adminStatistics.html b/client/views/admin/adminStatistics.html index fd02286c166e..f039362a8ea9 100644 --- a/client/views/admin/adminStatistics.html +++ b/client/views/admin/adminStatistics.html @@ -7,7 +7,7 @@

- {{#unless isAdmin}} + {{#unless hasPermission 'view-statistics'}}

You are not authorized to view this page.

{{else}} {{#if isReady}} diff --git a/client/views/admin/rooms/adminRoomInfo.coffee b/client/views/admin/rooms/adminRoomInfo.coffee index ccc7ce451b98..731ea8daf1cb 100644 --- a/client/views/admin/rooms/adminRoomInfo.coffee +++ b/client/views/admin/rooms/adminRoomInfo.coffee @@ -1,4 +1,7 @@ Template.adminRoomInfo.helpers + canDeleteRoom: -> + return RocketChat.authz.hasAtLeastOnePermission("delete-#{@t}") + type: -> return if @t is 'd' then 'at' else if @t is 'p' then 'lock' else 'hash' name: -> diff --git a/client/views/admin/rooms/adminRoomInfo.html b/client/views/admin/rooms/adminRoomInfo.html index fc35afc68209..090f36d5e898 100644 --- a/client/views/admin/rooms/adminRoomInfo.html +++ b/client/views/admin/rooms/adminRoomInfo.html @@ -1,14 +1,20 @@ \ No newline at end of file diff --git a/client/views/admin/rooms/adminRooms.coffee b/client/views/admin/rooms/adminRooms.coffee index 82b9995d6da8..b871209f4757 100644 --- a/client/views/admin/rooms/adminRooms.coffee +++ b/client/views/admin/rooms/adminRooms.coffee @@ -1,6 +1,4 @@ Template.adminRooms.helpers - isAdmin: -> - return Meteor.user().admin is true isReady: -> return Template.instance().ready?.get() rooms: -> diff --git a/client/views/admin/rooms/adminRooms.html b/client/views/admin/rooms/adminRooms.html index e9e358203619..ce0454d58f06 100644 --- a/client/views/admin/rooms/adminRooms.html +++ b/client/views/admin/rooms/adminRooms.html @@ -7,7 +7,7 @@

- {{#unless isAdmin}} + {{#unless hasPermission 'view-room-administration'}}

You are not authorized to view this page.

{{else}}
diff --git a/client/views/admin/users/adminUserChannels.html b/client/views/admin/users/adminUserChannels.html index a2f020d077f5..af72dcbbad0e 100644 --- a/client/views/admin/users/adminUserChannels.html +++ b/client/views/admin/users/adminUserChannels.html @@ -1,5 +1,9 @@ \ No newline at end of file diff --git a/client/views/admin/users/adminUserEdit.html b/client/views/admin/users/adminUserEdit.html index d00a9875e0c0..686f999ffdba 100644 --- a/client/views/admin/users/adminUserEdit.html +++ b/client/views/admin/users/adminUserEdit.html @@ -1,19 +1,23 @@ \ No newline at end of file diff --git a/client/views/admin/users/adminUserInfo.coffee b/client/views/admin/users/adminUserInfo.coffee index 17fef2107ec6..90c5bdda5383 100644 --- a/client/views/admin/users/adminUserInfo.coffee +++ b/client/views/admin/users/adminUserInfo.coffee @@ -1,6 +1,4 @@ Template.adminUserInfo.helpers - isAdmin: -> - return Meteor.user()?.admin is true name: -> return if @name then @name else TAPi18next.t 'project:Unnamed' email: -> @@ -20,6 +18,9 @@ Template.adminUserInfo.helpers @utcOffset = "+#{@utcOffset}" return "UTC #{@utcOffset}" + hasAdminRole: -> + console.log 'hasAdmin: ', RocketChat.authz.hasRole(@_id, 'admin') + return RocketChat.authz.hasRole(@_id, 'admin') Template.adminUserInfo.events 'click .deactivate': (e) -> diff --git a/client/views/admin/users/adminUserInfo.html b/client/views/admin/users/adminUserInfo.html index 645f560e9d4d..bbf17a35256a 100644 --- a/client/views/admin/users/adminUserInfo.html +++ b/client/views/admin/users/adminUserInfo.html @@ -1,19 +1,25 @@ \ No newline at end of file diff --git a/client/views/admin/users/adminUsers.coffee b/client/views/admin/users/adminUsers.coffee index 4b4a990af200..d0a05d6ccca4 100644 --- a/client/views/admin/users/adminUsers.coffee +++ b/client/views/admin/users/adminUsers.coffee @@ -1,6 +1,4 @@ Template.adminUsers.helpers - isAdmin: -> - return Meteor.user().admin is true isReady: -> return Template.instance().ready?.get() users: -> diff --git a/client/views/admin/users/adminUsers.html b/client/views/admin/users/adminUsers.html index 28921562e118..e85242b85f58 100644 --- a/client/views/admin/users/adminUsers.html +++ b/client/views/admin/users/adminUsers.html @@ -7,7 +7,7 @@

- {{#unless isAdmin}} + {{#unless hasPermission 'view-user-administration'}}

You are not authorized to view this page.

{{else}} diff --git a/client/views/app/message.coffee b/client/views/app/message.coffee index 1b58609d16af..bdd7fb52b13b 100644 --- a/client/views/app/message.coffee +++ b/client/views/app/message.coffee @@ -40,9 +40,16 @@ Template.message.helpers pinned: -> return this.pinned canEdit: -> - return RocketChat.settings.get 'Message_AllowEditing' + if RocketChat.authz.hasAtLeastOnePermission('edit-message', this.rid ) + return true + + return RocketChat.settings.get('Message_AllowEditing') and this.u?._id is Meteor.userId() + canDelete: -> - return RocketChat.settings.get 'Message_AllowDeleting' + if RocketChat.authz.hasAtLeastOnePermission('delete-message', this.rid ) + return true + + return RocketChat.settings.get('Message_AllowDeleting') and this.u?._id is Meteor.userId() canPin: -> return RocketChat.settings.get 'Message_AllowPinning' showEditedStatus: -> diff --git a/client/views/app/room.coffee b/client/views/app/room.coffee index 29712c677b1a..d71bf3f5407e 100644 --- a/client/views/app/room.coffee +++ b/client/views/app/room.coffee @@ -104,7 +104,10 @@ Template.room.helpers canEditName: -> roomData = Session.get('roomData' + this._id) return '' unless roomData - return roomData.u?._id is Meteor.userId() and roomData.t in ['c', 'p'] + if roomData.t in ['c', 'p'] + return RocketChat.authz.hasAtLeastOnePermission('edit-room', this._id) + else + return '' canDirectMessage: -> return Meteor.user()?.username isnt this.username @@ -183,9 +186,6 @@ Template.room.helpers maxMessageLength: -> return RocketChat.settings.get('Message_MaxAllowedSize') - isAdmin: -> - return Meteor.user()?.admin is true - utc: -> if @utcOffset? return "UTC #{@utcOffset}" diff --git a/client/views/app/sideNav/channels.coffee b/client/views/app/sideNav/channels.coffee index 7dffc0db7b28..5d715a1453bc 100644 --- a/client/views/app/sideNav/channels.coffee +++ b/client/views/app/sideNav/channels.coffee @@ -10,8 +10,11 @@ Template.channels.helpers Template.channels.events 'click .add-room': (e, instance) -> - SideNav.setFlex "createChannelFlex" - SideNav.openFlex() + if RocketChat.authz.hasAtLeastOnePermission('create-c') + SideNav.setFlex "createChannelFlex" + SideNav.openFlex() + else + e.preventDefault() 'click .more-channels': -> SideNav.setFlex "listChannelsFlex" diff --git a/client/views/app/sideNav/channels.html b/client/views/app/sideNav/channels.html index e4b3cec776b2..82defaa8d0df 100644 --- a/client/views/app/sideNav/channels.html +++ b/client/views/app/sideNav/channels.html @@ -1,7 +1,9 @@ diff --git a/client/views/app/sideNav/privateGroups.coffee b/client/views/app/sideNav/privateGroups.coffee index 7e729c5af148..1b577816fe42 100644 --- a/client/views/app/sideNav/privateGroups.coffee +++ b/client/views/app/sideNav/privateGroups.coffee @@ -16,8 +16,11 @@ Template.privateGroups.helpers Template.privateGroups.events 'click .add-room': (e, instance) -> - SideNav.setFlex "privateGroupsFlex" - SideNav.openFlex() + if RocketChat.authz.hasAtLeastOnePermission('create-p') + SideNav.setFlex "privateGroupsFlex" + SideNav.openFlex() + else + e.preventDefault() 'click .more-groups': -> SideNav.setFlex "listPrivateGroupsFlex" diff --git a/client/views/app/sideNav/privateGroups.html b/client/views/app/sideNav/privateGroups.html index 22db34a7c6f3..2d5685cc4557 100644 --- a/client/views/app/sideNav/privateGroups.html +++ b/client/views/app/sideNav/privateGroups.html @@ -1,7 +1,9 @@