From c2e6e0fa2cdf80df4dbf942c6f26d9e45db8fcf2 Mon Sep 17 00:00:00 2001 From: Reid Wakida Date: Wed, 9 Sep 2015 12:04:52 -1000 Subject: [PATCH] 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 113c4d33f64..ac1dbedc0d0 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 b09ffefd8ab..be5c92d0639 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 2ecd8cc4634..15f9289252f 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 f6a02bf90b2..24e418d6d28 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 1366c4e31cd..773bff4a167 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 407cc4eb4da..6bca06133d4 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 07d4c7eaa98..9ab84108dea 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 9bb7f03c4be..b559af0e10c 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 164c694c210..cc7ebf87317 100644 --- a/client/views/admin/adminFlex.html +++ b/client/views/admin/adminFlex.html @@ -7,24 +7,35 @@
diff --git a/client/views/admin/adminStatistics.coffee b/client/views/admin/adminStatistics.coffee index 4622e627e50..8c27a0374ef 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 fd02286c166..f039362a8ea 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 ccc7ce451b9..731ea8daf1c 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 fc35afc6820..090f36d5e89 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 82b9995d6da..b871209f475 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 e9e35820361..ce0454d58f0 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 a2f020d077f..af72dcbbad0 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 d00a9875e0c..686f999ffdb 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 17fef2107ec..90c5bdda538 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 645f560e9d4..bbf17a35256 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 4b4a990af20..d0a05d6ccca 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 28921562e11..e85242b85f5 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 1b58609d16a..bdd7fb52b13 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 29712c677b1..d71bf3f5407 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 7dffc0db7b2..5d715a1453b 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 e4b3cec776b..82defaa8d0d 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 7e729c5af14..1b577816fe4 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 22db34a7c6f..2d5685cc455 100644 --- a/client/views/app/sideNav/privateGroups.html +++ b/client/views/app/sideNav/privateGroups.html @@ -1,7 +1,9 @@