diff --git a/.meteor/packages b/.meteor/packages index 113c4d33f64661fa4ebf308f55045ad3d78527dd..ac1dbedc0d013b0e22e969b8b06669dcd6fb0865 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 b09ffefd8ab57310f6488ac362c2295d0a8de9c6..be5c92d06392264932fde52ebb51d1f219ddc2d2 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 2ecd8cc4634c19cc36020176c53370ffa6256977..15f9289252f6931044889f92a48ad86661295e92 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 f6a02bf90b2d2cd2084fa2cbe58f2840ef28ac5e..24e418d6d28b09c253f3f83967f8e3b90dc451d8 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 1366c4e31cd2ea0d41ea5eec7cdebaa80100a3e2..773bff4a1676498545abb7b402d8acacfa64c2a0 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 cb84db26fd1efc49567e9c0d86075dd2f13248e3..a12b94f2d8806bbeecc60ca1f253e2bd2821e3c1 100644 --- a/client/stylesheets/base.less +++ b/client/stylesheets/base.less @@ -2374,14 +2374,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 07d4c7eaa98aa2a633c38540f97607cc5888ec24..9ab84108dea21037e2cc6b90db6853ff8a38e9c6 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 9bb7f03c4bebf5abaa9992ead276e3506cec8fef..b559af0e10cb1f03f1386205baa5b43a2a3939a6 100644 --- a/client/views/admin/admin.html +++ b/client/views/admin/admin.html @@ -7,7 +7,7 @@ </h2> </head> <div class="content"> - {{#unless isAdmin}} + {{#unless hasPermission 'view-privileged-setting'}} <p>You are not authorized to view this page.</p> {{else}} {{#with group}} diff --git a/client/views/admin/adminFlex.html b/client/views/admin/adminFlex.html index 164c694c210ff0664fbbc82b62fef8a7dc8eabaa..cc7ebf8731708dcf75d1fb6fe316469810cd3f40 100644 --- a/client/views/admin/adminFlex.html +++ b/client/views/admin/adminFlex.html @@ -7,24 +7,35 @@ <div class="content"> <div class="wrapper"> <ul> - <li> - <a href="{{pathFor 'admin-statistics'}}" class="admin-link">{{_ "Statistics"}}</a> - </li> - <li> - <a href="{{pathFor 'admin-rooms'}}" class="admin-link">{{_ "Rooms"}}</a> - </li> - <li> - <a href="{{pathFor 'admin-users'}}" class="admin-link">{{_ "Users"}}</a> - </li> - + {{#if hasPermission 'view-statistics'}} + <li> + <a href="{{pathFor 'admin-statistics'}}" class="admin-link">{{_ "Statistics"}}</a> + </li> + {{/if}} + + {{#if hasPermission 'view-room-administration'}} + <li> + <a href="{{pathFor 'admin-rooms'}}" class="admin-link">{{_ "Rooms"}}</a> + </li> + {{/if}} + + {{#if hasPermission 'view-user-administration'}} + <li> + <a href="{{pathFor 'admin-users'}}" class="admin-link">{{_ "Users"}}</a> + </li> + {{/if}} + <h3 class="add-room"> {{_ "Settings"}} </h3> - {{#each groups}} - <li> - <a href="{{pathFor 'admin' group=_id}}" class="admin-link">{{_ i18nLabel}}</a> - </li> - {{/each}} + + {{#if hasPermission 'view-privileged-setting'}} + {{#each groups}} + <li> + <a href="{{pathFor 'admin' group=_id}}" class="admin-link">{{_ i18nLabel}}</a> + </li> + {{/each}} + {{/if}} </ul> </div> </div> diff --git a/client/views/admin/adminStatistics.coffee b/client/views/admin/adminStatistics.coffee index 4622e627e507a723fafc6ed5b3726bf8a0159f4a..8c27a0374efde26b96442c78e7e1845b886cdf52 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 fd02286c166ed1b5e888091668c79d3df8b076b7..f039362a8ea9340bf240c4ca74a783acc837e5cc 100644 --- a/client/views/admin/adminStatistics.html +++ b/client/views/admin/adminStatistics.html @@ -7,7 +7,7 @@ </h2> </head> <div class="content"> - {{#unless isAdmin}} + {{#unless hasPermission 'view-statistics'}} <p>You are not authorized to view this page.</p> {{else}} {{#if isReady}} diff --git a/client/views/admin/rooms/adminRoomInfo.coffee b/client/views/admin/rooms/adminRoomInfo.coffee index ccc7ce451b9856da00cbe0a29051b88a122c607b..731ea8daf1cb41907a8edaf3f89a3f088cadcbbc 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 fc35afc68209f21684550a9304f1e20ff49a0f66..090f36d5e8984416513aa1488e3a5e1b08230da1 100644 --- a/client/views/admin/rooms/adminRoomInfo.html +++ b/client/views/admin/rooms/adminRoomInfo.html @@ -1,14 +1,20 @@ <template name="adminRoomInfo"> - <div> - <h3><a href="{{route}}"><i class="icon-{{type}}"></i> {{name}}</a></h3> - </div> - <div> - <h3>{{_ "Users"}}:</h3> - {{#each usernames}} - {{.}}<br /> - {{/each}} - </div> - <nav> - <button class='button delete red'><span><i class='icon-trash'></i> {{_ "Delete"}}</span></button> - </nav> + {{#unless hasPermission 'view-room-administration'}} + <p>You are not authorized to view this page.</p> + {{else}} + <div> + <h3><a href="{{route}}"><i class="icon-{{type}}"></i> {{name}}</a></h3> + </div> + <div> + <h3>{{_ "Users"}}:</h3> + {{#each usernames}} + {{.}}<br /> + {{/each}} + </div> + {{#if canDeleteRoom}} + <nav> + <button class='button delete red'><span><i class='icon-trash'></i> {{_ "Delete"}}</span></button> + </nav> + {{/if}} + {{/unless}} </template> \ No newline at end of file diff --git a/client/views/admin/rooms/adminRooms.coffee b/client/views/admin/rooms/adminRooms.coffee index 82b9995d6da8bc23784c7721f784690f2f9bd322..b871209f47571f11fb3af860f009ebd5836a2a5a 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 e9e358203619e65b6207173d5d2ca0297bb45ad2..ce0454d58f062a1f79639afd1a31a6cb2f59e3f9 100644 --- a/client/views/admin/rooms/adminRooms.html +++ b/client/views/admin/rooms/adminRooms.html @@ -7,7 +7,7 @@ </h2> </head> <div class="content"> - {{#unless isAdmin}} + {{#unless hasPermission 'view-room-administration'}} <p>You are not authorized to view this page.</p> {{else}} <form class="search-form" role="form"> diff --git a/client/views/admin/users/adminUserChannels.html b/client/views/admin/users/adminUserChannels.html index a2f020d077f569b0de5516bf28066da0a4fd1a38..af72dcbbad0e95647200bd38ea81c5f7472fd765 100644 --- a/client/views/admin/users/adminUserChannels.html +++ b/client/views/admin/users/adminUserChannels.html @@ -1,5 +1,9 @@ <template name="adminUserChannels"> - <div class="user-info-channel"> - <h3><a href="{{route}}"><i class="icon-{{type}}"></i> {{name}}</a></h3> - </div> + {{#unless hasPermission 'view-full-other-user-info'}} + <p>You are not authorized to view this page.</p> + {{else}} + <div class="user-info-channel"> + <h3><a href="{{route}}"><i class="icon-{{type}}"></i> {{name}}</a></h3> + </div> + {{/unless}} </template> \ No newline at end of file diff --git a/client/views/admin/users/adminUserEdit.html b/client/views/admin/users/adminUserEdit.html index d00a9875e0c0f814f542ae7b676a1fe480ab53d1..686f999ffdba88b0c4db402a401fba8d0247fbf8 100644 --- a/client/views/admin/users/adminUserEdit.html +++ b/client/views/admin/users/adminUserEdit.html @@ -1,19 +1,23 @@ <template name="adminUserEdit"> - <div class="about clearfix"> - <form class="edit-form"> - <h3>{{name}}</h3> - <div class="input-line"> - <label for="name">{{_ "Name"}}</label> - <input type="text" id="name" autocomplete="off" value="{{name}}"> - </div> - <div class="input-line"> - <label for="username">{{_ "Username"}}</label> - <input type="text" id="username" autocomplete="off" value="{{username}}"> - </div> - </form> - </div> - <nav> - <button class='button button-block cancel secondary'><span>{{_ "Cancel"}}</span></button> - <button class='button button-block blue save'><span>{{_ "Save"}}</span></button> - </nav> + {{#unless hasPermission 'edit-other-user-info'}} + <p>You are not authorized to view this page.</p> + {{else}} + <div class="about clearfix"> + <form class="edit-form"> + <h3>{{name}}</h3> + <div class="input-line"> + <label for="name">{{_ "Name"}}</label> + <input type="text" id="name" autocomplete="off" value="{{name}}"> + </div> + <div class="input-line"> + <label for="username">{{_ "Username"}}</label> + <input type="text" id="username" autocomplete="off" value="{{username}}"> + </div> + </form> + </div> + <nav> + <button class='button button-block cancel secondary'><span>{{_ "Cancel"}}</span></button> + <button class='button button-block blue save'><span>{{_ "Save"}}</span></button> + </nav> + {{/unless}} </template> \ No newline at end of file diff --git a/client/views/admin/users/adminUserInfo.coffee b/client/views/admin/users/adminUserInfo.coffee index 17fef2107ec6d80e51b8c2b433610b34ec854579..90c5bdda538390e7622a80e3655e7b58cddb386b 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 645f560e9d4dcdd7edf580ffd3aac8ab461d1779..bbf17a35256a33f93940752f647eda05d47f5d04 100644 --- a/client/views/admin/users/adminUserInfo.html +++ b/client/views/admin/users/adminUserInfo.html @@ -1,19 +1,25 @@ <template name="adminUserInfo"> - {{#if isAdmin}} - {{> userInfo user=.}} - <nav> - <button class='button lightblue edit-user button-block'><span><i class='icon-edit'></i> {{_ "Edit"}}</span></button> - {{#if admin}} + {{> userInfo user=.}} + <nav> + {{#if hasPermission 'edit-other-user-info'}} + <button class='button lightblue edit-user button-block'><span><i class='icon-edit'></i> {{_ "Edit"}}</span></button> + {{/if}} + {{#if hasPermission 'assign-admin-role'}} + {{#if hasAdminRole}} <button class='button lightblue remove-admin button-block'><span><i class='icon-shield'></i> {{_ "Remove_Admin"}}</span></button> {{else}} <button class='button lightblue make-admin button-block'><span><i class='icon-shield'></i> {{_ "Make_Admin"}}</span></button> {{/if}} + {{/if}} + {{#if hasPermission 'edit-other-user-active-status'}} {{#if active}} <button class='button deactivate button-block'><span><i class='icon-block'></i> {{_ "Deactivate"}}</span></button> {{else}} <button class='button activate button-block'><span><i class='icon-ok-circled'></i> {{_ "Activate"}}</span></button> {{/if}} - <button class='button delete red button-block'><span><i class='icon-trash'></i> {{_ "Delete"}}</span></button> - </nav> - {{/if}} + {{/if}} + {{#if hasPermission 'delete-user'}} + <button class='button delete red button-block'><span><i class='icon-trash'></i> {{_ "Delete"}}</span></button> + {{/if}} + </nav> </template> \ No newline at end of file diff --git a/client/views/admin/users/adminUsers.coffee b/client/views/admin/users/adminUsers.coffee index e1fc43f0f32c12f751688ec6ba219ea4d153803d..89834d56db5b3e4f60f3fd0ed7549ec181d395e6 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 28921562e118f7fa711c8e8ad2b352c517de3728..e85242b85f58436b2020ee16b18d7eeba9834a89 100644 --- a/client/views/admin/users/adminUsers.html +++ b/client/views/admin/users/adminUsers.html @@ -7,7 +7,7 @@ </h2> </head> <div class="content"> - {{#unless isAdmin}} + {{#unless hasPermission 'view-user-administration'}} <p>You are not authorized to view this page.</p> {{else}} <form class="search-form" role="form"> diff --git a/client/views/app/message.coffee b/client/views/app/message.coffee index 1b58609d16afb999f471fbf3903d36c9bae67bef..bdd7fb52b13b6082532825bb72f00407e38420c3 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 29712c677b1a3a82a42cebba795b54e9624379a6..d71bf3f5407e1c13c4d25c2f6c910470b2621a37 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 7dffc0db7b284f974057bc422a437b88929b5be9..5d715a1453bcfb9a07d81a13888b778b5370eff5 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 e4b3cec776b270c56f32de03e87cc1bbfdcfe5a4..82defaa8d0df2658848f2d3038014af9b5346a58 100644 --- a/client/views/app/sideNav/channels.html +++ b/client/views/app/sideNav/channels.html @@ -1,7 +1,9 @@ <template name="channels"> <h3 class="add-room {{isActive}}"> {{_ "Channels"}} - <i class="a-plus"></i> + {{#if hasPermission 'create-c'}} + <i class="a-plus"></i> + {{/if}} </h3> <ul> {{#each rooms}} diff --git a/client/views/app/sideNav/listChannelsFlex.coffee b/client/views/app/sideNav/listChannelsFlex.coffee index 1f3332b6b071804fdb858273c341d49d8b384849..1e9aa4f4c6bdfa07869497ae3ff53ae3aeec10ed 100644 --- a/client/views/app/sideNav/listChannelsFlex.coffee +++ b/client/views/app/sideNav/listChannelsFlex.coffee @@ -10,7 +10,8 @@ Template.listChannelsFlex.events SideNav.closeFlex() 'click footer .create': -> - SideNav.setFlex "createChannelFlex" + if RocketChat.authz.hasAtLeastOnePermission( 'create-c') + SideNav.setFlex "createChannelFlex" 'mouseenter header': -> SideNav.overArrow() diff --git a/client/views/app/sideNav/listChannelsFlex.html b/client/views/app/sideNav/listChannelsFlex.html index 573c33e1722cb9c2406c33ca084829cf53e44ec8..86ee731e9b077d8ae9221726d4f963063e3b6e7f 100644 --- a/client/views/app/sideNav/listChannelsFlex.html +++ b/client/views/app/sideNav/listChannelsFlex.html @@ -21,7 +21,9 @@ </div> <footer> <div> + {{#if hasPermission 'create-c'}} <button class="button clean create">{{_ "Create_new"}}</button> + {{/if}} </div> </footer> </template> diff --git a/client/views/app/sideNav/privateGroups.coffee b/client/views/app/sideNav/privateGroups.coffee index 7e729c5af148c0731036a37afd06b08573fe398b..1b577816fe42cf312e14c9d049e0b315e52e3210 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 22db34a7c6f383a63fb8070bbac4928569c15b29..2d5685cc45576cdb9d78a3928e9237c419362caa 100644 --- a/client/views/app/sideNav/privateGroups.html +++ b/client/views/app/sideNav/privateGroups.html @@ -1,7 +1,9 @@ <template name="privateGroups"> <h3 class="add-room {{isActive}}"> {{_ "Private_Groups"}} - <i class="a-plus"></i> + {{#if hasPermission 'create-p'}} + <i class="a-plus"></i> + {{/if}} </h3> <ul> {{#each rooms}} diff --git a/client/views/app/sideNav/sideNav.coffee b/client/views/app/sideNav/sideNav.coffee index 78633f2189e02250aa07545cb59eb923aedc8521..6038e3608c7248da6261c9bb04255cf5e67c464d 100644 --- a/client/views/app/sideNav/sideNav.coffee +++ b/client/views/app/sideNav/sideNav.coffee @@ -1,6 +1,5 @@ Template.sideNav.helpers - isAdmin: -> - return Meteor.user()?.admin is true + flexTemplate: -> return SideNav.getFlex().template flexData: -> diff --git a/client/views/app/sideNav/userStatus.coffee b/client/views/app/sideNav/userStatus.coffee index cf77f86ce4765a6ac8a3f85e929cb8ad4fb0138c..b722e4d2e2740af39b3d2d463d64c75f2938b9dc 100644 --- a/client/views/app/sideNav/userStatus.coffee +++ b/client/views/app/sideNav/userStatus.coffee @@ -17,8 +17,8 @@ Template.userStatus.helpers username: username } - isAdmin: -> - return Meteor.user()?.admin is true + showAdminOption: -> + return RocketChat.authz.hasAtLeastOnePermission( ['view-statistics', 'view-room-administration', 'view-user-administration', 'view-privileged-setting']) Template.userStatus.events 'click .options .status': (event) -> diff --git a/client/views/app/sideNav/userStatus.html b/client/views/app/sideNav/userStatus.html index 97e782c1e4458fd9e1f3c5ad4fb5ffc81992d8f5..a2c749354c3878ee80210c65fc92c6f84ca57859 100644 --- a/client/views/app/sideNav/userStatus.html +++ b/client/views/app/sideNav/userStatus.html @@ -18,7 +18,7 @@ <a href="" data-status="busy" class="status busy"><span>{{_ "Busy" context="male"}}</span></a> <a href="" data-status="offline" class="status offline"><span>{{_ "Invisible"}}</span></a> <a href="" id="account" class='account-link'><i class="icon-sliders"></i><span>{{_ "My_Account"}}</span></a> - {{#if isAdmin}} + {{#if showAdminOption }} <a href="" id="admin" class='account-link'><i class="icon-wrench"></i><span>{{_ "Administration"}}</span></a> {{/if}} <a href="" id="logout"><i class="icon-logout"></i><span>{{_ "Logout"}}</span></a> diff --git a/client/views/app/userInfo.coffee b/client/views/app/userInfo.coffee index 2e6a19b7ca132e7c3c2989fa835cb98303532097..f94d4bb837d1f707d33b62c498ef31676c4ee182 100644 --- a/client/views/app/userInfo.coffee +++ b/client/views/app/userInfo.coffee @@ -1,6 +1,4 @@ Template.userInfo.helpers - isAdmin: -> - return Meteor.user()?.admin is true utc: -> if @utcOffset? if @utcOffset > 0 diff --git a/client/views/app/userInfo.html b/client/views/app/userInfo.html index c8a5d18a7c8822b3fa43041131b9ea86df092616..6eadfa93a376d4cf40894289df57572c74a90c72 100644 --- a/client/views/app/userInfo.html +++ b/client/views/app/userInfo.html @@ -37,7 +37,7 @@ <h3 title="{{username}}"><i class="status-{{status}}"></i> {{username}}</h3> <p>{{name}}</p> {{#if utc}}<p><i class="icon-clock"></i>{{userTime}} (UTC {{utc}})</p>{{/if}} - {{#if isAdmin}} + {{#if hasPermission 'view-full-other-user-info'}} {{#each emails}} <p><i class="icon-mail"></i> {{address}}{{#if verified}} <i class="icon-ok"></i>{{/if}}</p> {{/each}} {{#each phone}} <p><i class="icon-phone"></i> {{phoneNumber}}</p> {{/each}} {{#if lastLogin}} <p><i class="icon-calendar"></i> {{_ "Created_at"}}: {{createdAt}}</p> {{/if}} diff --git a/packages/rocketchat-authorization/README.md b/packages/rocketchat-authorization/README.md new file mode 100644 index 0000000000000000000000000000000000000000..239b085c856dc0d1280af7d604b5f5a40c2d2eb7 --- /dev/null +++ b/packages/rocketchat-authorization/README.md @@ -0,0 +1,41 @@ +Supports role or permission based authorization, and defines the association between them. + +A user is associated with role(s), and a role is associated with permission(s). This package depends on alanning:roles for the role/user association, while the role/permission association is handled internally. Thus, the underlying alanning:roles has no concept of a permission or the association between a role and permission. + +Authorization checks can be done based on a role or permission. However, permission based checks are preferred because they loosely associate an action with a role. For example: + +``` +# permission based check +if hasPermission(userId, 'edit-message') ... + # action is loosely associated to role via permission. Thus action can be revoked + # at runtime by removing the permission for user's role instead of modifying the action code. + +# role based check +if hasRole(userId, ['admin','site-moderator','moderator']) + # action is statically associated with the role + # action code has to be modified to add/remove role authorization + +``` + +Usage: +``` +# assign user to moderator role. Permissions scoped globally +# user can moderate (e.g. edit channel name, delete private group message) for all rooms +RocketChat.authz.addUsersToRoles(userId, 'moderator') + +# assign user to moderator role. Permissions scoped to the specified room +# user can moderate (e.g. edit channel name, delete private group message) for only one room specified by the roomId +RocketChat.authz.addUsersToRoles(userId, 'moderator', roomId ) + +# check if user can modify message for any room +RocketChat.authz.hasPermission(userId, 'edit-message') + +# check if user can modify message for the specified room. Also returns true if user +# has 'edit-message' at global scope. +RocketChat.authz.hasPermission(userId, 'edit-message', roomId) +``` + +Notes: +1. Roles are statically defined. UI needs to be implemented to dynamically assign permission(s) to a Role +2. 'admin', 'moderator', 'user' role identifiers should NOT be changed (unless you update the associated code) because they are referenced when creating users and creating rooms. +3. edit, delete message permissions are at either the global or room scope. i.e. role with edit-message with GLOBAL scope can edit ANY message regardless of the room type. However, role with edit-message with room scope can only edit messages for the room. The global scope is associated with the admin role while the "room-scoped" permission is assigned to the room "moderator" (room creator). If we want a middle ground that allows for edit-message for only channel/group/direct, then we need to create individual edit-c-message, edit-p-message, edit-d-message permissions. diff --git a/packages/rocketchat-authorization/client/hasPermission.coffee b/packages/rocketchat-authorization/client/hasPermission.coffee new file mode 100644 index 0000000000000000000000000000000000000000..5155bf54c486b533ad4daa88aaee23662077d545 --- /dev/null +++ b/packages/rocketchat-authorization/client/hasPermission.coffee @@ -0,0 +1,40 @@ +atLeastOne = (toFind, toSearch) -> + console.log 'toFind: ', toFind if window.rocketDebug + console.log 'toSearch: ', toSearch if window.rocketDebug + return not _.isEmpty(_.intersection(toFind, toSearch)) + +all = (toFind, toSearch) -> + toFind = _.uniq(toFind) + toSearch = _.uniq(toSearch) + return _.isEmpty( _.difference( toFind, toSearch)) + +Template.registerHelper 'hasPermission', (permission, scope) -> + unless _.isString( scope ) + scope = Roles.GLOBAL_GROUP + return hasPermission( permission, scope, atLeastOne) + +RocketChat.authz.hasAllPermission = (permissions, scope=Roles.GLOBAL_GROUP) -> + return hasPermission( permissions, scope, all ) + +RocketChat.authz.hasAtLeastOnePermission = (permissions, scope=Roles.GLOBAL_GROUP) -> + return hasPermission(permissions, scope, atLeastOne) + +hasPermission = (permissions, scope=Roles.GLOBAL_GROUP, strategy) -> + userId = Meteor.userId() + + unless userId + return false + + unless RocketChat.authz.subscription.ready() + return false + + unless _.isArray(permissions) + permissions = [permissions] + + roleNames = Roles.getRolesForUser(userId, scope) + + userPermissions = [] + for roleName in roleNames + userPermissions = userPermissions.concat(_.pluck(ChatPermissions.find({roles : roleName }).fetch(), '_id')) + + return strategy( permissions, userPermissions) diff --git a/packages/rocketchat-authorization/client/hasRole.coffee b/packages/rocketchat-authorization/client/hasRole.coffee new file mode 100644 index 0000000000000000000000000000000000000000..f0a95261041ff651fd5d19e8827cb1e26f09443c --- /dev/null +++ b/packages/rocketchat-authorization/client/hasRole.coffee @@ -0,0 +1,6 @@ +RocketChat.authz.hasRole = (userId, roleName, scope=Roles.GLOBAL_GROUP) -> + unless Meteor.userId() + return false + + # per alanning:roles, returns true if user is in ANY roles + return Roles.userIsInRole(userId, [roleName], scope) \ No newline at end of file diff --git a/packages/rocketchat-authorization/client/startup.coffee b/packages/rocketchat-authorization/client/startup.coffee new file mode 100644 index 0000000000000000000000000000000000000000..ded8f7799ffbee2cdf91b9c999ed3776a6484270 --- /dev/null +++ b/packages/rocketchat-authorization/client/startup.coffee @@ -0,0 +1,2 @@ +Meteor.startup -> + RocketChat.authz.subscription = Meteor.subscribe 'permissions' \ No newline at end of file diff --git a/packages/rocketchat-authorization/lib/permissions.coffee b/packages/rocketchat-authorization/lib/permissions.coffee new file mode 100644 index 0000000000000000000000000000000000000000..b719f7f9ed648c4d5df0d16de2fd541787017d97 --- /dev/null +++ b/packages/rocketchat-authorization/lib/permissions.coffee @@ -0,0 +1 @@ +@ChatPermissions = new Meteor.Collection 'rocketchat_permissions' \ No newline at end of file diff --git a/packages/rocketchat-authorization/lib/rocketchat.coffee b/packages/rocketchat-authorization/lib/rocketchat.coffee new file mode 100644 index 0000000000000000000000000000000000000000..a8c5e45883456f3e5154c9b4db8958ce845d9937 --- /dev/null +++ b/packages/rocketchat-authorization/lib/rocketchat.coffee @@ -0,0 +1 @@ +RocketChat.authz = {} diff --git a/packages/rocketchat-authorization/package.js b/packages/rocketchat-authorization/package.js new file mode 100644 index 0000000000000000000000000000000000000000..bb32b2ac371ef0fd28f8421991318a32a1b21f17 --- /dev/null +++ b/packages/rocketchat-authorization/package.js @@ -0,0 +1,37 @@ +Package.describe({ + name: 'rocketchat:authorization', + version: '0.0.1', + summary: 'Role based authorization of actions', + git: '', + documentation: 'README.md' +}); + +Package.onUse(function(api) { + api.versionsFrom('1.0'); + api.use([ + 'coffeescript', + 'rocketchat:lib@0.0.1', + 'alanning:roles@1.2.12' + ]); + + api.use('templating', 'client'); + + api.addFiles('lib/permissions.coffee', ['server', 'client']); + api.addFiles('lib/rocketchat.coffee', ['server','client']); + api.addFiles('client/startup.coffee', ['client']); + api.addFiles('client/hasPermission.coffee', ['client']); + api.addFiles('client/hasRole.coffee', ['client']); + + + api.addFiles('server/functions/addUsersToRoles.coffee', ['server']); + api.addFiles('server/functions/getPermissionsForRole.coffee', ['server']); + api.addFiles('server/functions/getRoles.coffee', ['server']); + api.addFiles('server/functions/getRolesForUser.coffee', ['server']); + api.addFiles('server/functions/getUsersInRole.coffee', ['server']); + api.addFiles('server/functions/hasPermission.coffee', ['server']); + api.addFiles('server/functions/hasRole.coffee', ['server']); + api.addFiles('server/functions/removeUsersFromRoles.coffee', ['server']); + + api.addFiles('server/publication.coffee', ['server']); + api.addFiles('server/startup.coffee', ['server']); +}); diff --git a/packages/rocketchat-authorization/server/functions/addUsersToRoles.coffee b/packages/rocketchat-authorization/server/functions/addUsersToRoles.coffee new file mode 100644 index 0000000000000000000000000000000000000000..3b04cfa6fd5f82848a62d2fbe2dff8a5bf352d4a --- /dev/null +++ b/packages/rocketchat-authorization/server/functions/addUsersToRoles.coffee @@ -0,0 +1,26 @@ +RocketChat.authz.addUsersToRoles = (userIds, roleNames, scope ) -> + console.log '[methods] addUserToRoles -> '.green, 'arguments:', arguments + if not userIds or not roleNames + return false + + unless _.isArray(userIds) + userIds = [userIds] + + users = Meteor.users.find({_id: {$in : userIds}}).fetch() + unless userIds.length is users.length + throw new Meteor.Error 'invalid-user' + + unless _.isArray(roleNames) + roleNames = [roleNames] + + existingRoleNames = _.pluck(RocketChat.authz.getRoles().fetch(), 'name') + invalidRoleNames = _.difference( roleNames, existingRoleNames) + unless _.isEmpty(invalidRoleNames) + throw new Meteor.Error 'invalid-role' + + unless _.isString(scope) + scope = Roles.GLOBAL_GROUP + + Roles.addUsersToRoles( userIds, roleNames, scope) + + return true \ No newline at end of file diff --git a/packages/rocketchat-authorization/server/functions/getPermissionsForRole.coffee b/packages/rocketchat-authorization/server/functions/getPermissionsForRole.coffee new file mode 100644 index 0000000000000000000000000000000000000000..f19203fe8c875a661059084b74c660b084c22504 --- /dev/null +++ b/packages/rocketchat-authorization/server/functions/getPermissionsForRole.coffee @@ -0,0 +1,9 @@ +RocketChat.authz.getPermissionsForRole = (roleName) -> + unless roleName + throw new Meteor.Error 'invalid-role' + + roleNames = _.pluck(RocketChat.authz.getRoles().fetch(), 'name') + unless roleName in roleNames + throw new Meteor.Error 'invalid-role' + + return _.pluck(ChatPermissions.find({roles : roleName }).fetch(), '_id') \ No newline at end of file diff --git a/packages/rocketchat-authorization/server/functions/getRoles.coffee b/packages/rocketchat-authorization/server/functions/getRoles.coffee new file mode 100644 index 0000000000000000000000000000000000000000..37c3d2537fdb438dcc9afc654e5eb1d941ef69af --- /dev/null +++ b/packages/rocketchat-authorization/server/functions/getRoles.coffee @@ -0,0 +1,2 @@ +RocketChat.authz.getRoles = -> + return Roles.getAllRoles() \ No newline at end of file diff --git a/packages/rocketchat-authorization/server/functions/getRolesForUser.coffee b/packages/rocketchat-authorization/server/functions/getRolesForUser.coffee new file mode 100644 index 0000000000000000000000000000000000000000..40f8564a28eaf39aa1bc64178b7e0be4fa02b1cd --- /dev/null +++ b/packages/rocketchat-authorization/server/functions/getRolesForUser.coffee @@ -0,0 +1,7 @@ +RocketChat.authz.getRolesForUser = (userId, scope) -> + console.log '[methods] getRolesForUser -> '.green, 'arguments:', arguments + # returns roles for the given scope as well as the global scope + unless scope + scope = Roles.GLOBAL_GROUP + + return Roles.getRolesForUser(userId, scope) \ No newline at end of file diff --git a/packages/rocketchat-authorization/server/functions/getUsersInRole.coffee b/packages/rocketchat-authorization/server/functions/getUsersInRole.coffee new file mode 100644 index 0000000000000000000000000000000000000000..d5193830a0ad50ada1513624d1781479670c261d --- /dev/null +++ b/packages/rocketchat-authorization/server/functions/getUsersInRole.coffee @@ -0,0 +1,6 @@ +RocketChat.authz.getUsersInRole = (roleName, scope) -> + # alanning:roles doc says this is an expensive operation + unless _.isString(scope) + scope = Roles.GLOBAL_GROUP + + return Roles.getUsersInRole(roleName, scope) \ No newline at end of file diff --git a/packages/rocketchat-authorization/server/functions/hasPermission.coffee b/packages/rocketchat-authorization/server/functions/hasPermission.coffee new file mode 100644 index 0000000000000000000000000000000000000000..f0d2d3ca2e92327ddd41a49cf760d4a3806f9a56 --- /dev/null +++ b/packages/rocketchat-authorization/server/functions/hasPermission.coffee @@ -0,0 +1,12 @@ +RocketChat.authz.hasPermission = (userId, permissionId, scope) -> + console.log '[methods] hasPermission -> '.green, 'arguments:', arguments + + # get user's roles + roles = RocketChat.authz.getRolesForUser(userId, scope) + + # get permissions for user's roles + permissions = [] + for role in roles + permissions = permissions.concat( RocketChat.authz.getPermissionsForRole( role )) + # may contain duplicate, but doesn't matter + return permissionId in permissions \ No newline at end of file diff --git a/packages/rocketchat-authorization/server/functions/hasRole.coffee b/packages/rocketchat-authorization/server/functions/hasRole.coffee new file mode 100644 index 0000000000000000000000000000000000000000..83d32ea862a3000591167a5cefe5f1d1bcaf198c --- /dev/null +++ b/packages/rocketchat-authorization/server/functions/hasRole.coffee @@ -0,0 +1,4 @@ +RocketChat.authz.hasRole = (userId, roleName, scope) -> + console.log '[methods] hasRoles -> '.green, 'arguments:', arguments + # per alanning:roles, returns true if user is in ANY roles + return Roles.userIsInRole(userId, [roleName], scope) \ No newline at end of file diff --git a/packages/rocketchat-authorization/server/functions/removeUsersFromRoles.coffee b/packages/rocketchat-authorization/server/functions/removeUsersFromRoles.coffee new file mode 100644 index 0000000000000000000000000000000000000000..a3f7c794d898a711815f3488eafae4f30b135248 --- /dev/null +++ b/packages/rocketchat-authorization/server/functions/removeUsersFromRoles.coffee @@ -0,0 +1,26 @@ +RocketChat.authz.removeUsersFromRoles = (userIds, roleNames, scope ) -> + console.log '[methods] removeUsersFromRoles -> '.green, 'arguments:', arguments + if not userIds or not roleNames + return false + + unless _.isArray(userIds) + userIds = [userIds] + + users = Meteor.users.find({_id: {$in : userIds}}).fetch() + unless userIds.length is users.length + throw new Meteor.Error 'invalid-user' + + unless _.isArray(roleNames) + roleNames = [roleNames] + + existingRoleNames = _.pluck(RocketChat.authz.getRoles().fetch(), 'name') + invalidRoleNames = _.difference( roleNames, existingRoleNames) + unless _.isEmpty(invalidRoleNames) + throw new Meteor.Error 'invalid-role' + + unless _.isString(scope) + scope = Roles.GLOBAL_GROUP + + Roles.removeUsersFromRoles( userIds, roleNames, scope) + + return true \ No newline at end of file diff --git a/packages/rocketchat-authorization/server/publication.coffee b/packages/rocketchat-authorization/server/publication.coffee new file mode 100644 index 0000000000000000000000000000000000000000..5cf4adef1df72ad69605fce232cd4328a3ef0034 --- /dev/null +++ b/packages/rocketchat-authorization/server/publication.coffee @@ -0,0 +1,3 @@ +Meteor.publish 'permissions', -> + console.log '[publish] permissions'.green + return ChatPermissions.find {} \ No newline at end of file diff --git a/packages/rocketchat-authorization/server/startup.coffee b/packages/rocketchat-authorization/server/startup.coffee new file mode 100644 index 0000000000000000000000000000000000000000..9edeec2d97582b78fb11f5d6187d55606f05ba16 --- /dev/null +++ b/packages/rocketchat-authorization/server/startup.coffee @@ -0,0 +1,87 @@ +Meteor.startup -> + + # Note: + # 1.if we need to create a role that can only edit channel message, but not edit group message + # then we can define edit-<type>-message instead of edit-message + # 2. admin, moderator, and user roles should not be deleted as they are referened in the code. + permissions = [ + + { _id: 'view-statistics', + roles : ['admin', 'temp-role']} + + { _id: 'view-privileged-setting', + roles : ['admin']} + + { _id: 'edit-privileged-setting', + roles : ['admin']} + + { _id: 'view-room-administration', + roles : ['admin']} + + { _id: 'view-user-administration', + roles : ['admin']} + + { _id: 'view-full-other-user-info', + roles : ['admin']} + + { _id: 'edit-other-user-info', + roles : ['admin']} + + { _id: 'assign-admin-role', + roles : ['admin']} + + { _id: 'edit-other-user-active-status', + roles : ['admin', 'site-moderator']} + + { _id: 'delete-user', + roles : ['admin']} + + { _id: 'view-other-user-channels', + roles : ['admin']} + + { _id: 'add-oath-service', + roles : ['admin']} + + { _id: 'run-migration', + roles : ['admin']} + + { _id: 'create-c', + roles : ['admin', 'site-moderator', 'user']} + + { _id: 'delete-c', + roles : ['admin', 'site-moderator']} + + { _id: 'edit-room', + roles : ['admin', 'site-moderator', 'moderator']} + + { _id: 'edit-message', + roles : ['admin', 'site-moderator', 'moderator']} + + { _id: 'delete-message', + roles : ['admin', 'site-moderator', 'moderator']} + + { _id: 'ban-user', + roles : ['admin', 'site-moderator', 'moderator']} + + { _id: 'create-p', + roles : ['admin', 'site-moderator', 'user']} + + { _id: 'delete-p', + roles : ['admin', 'site-moderator']} + + { _id: 'delete-d', + roles : ['admin', 'site-moderator']} + + ] + + #alanning:roles + roles = _.pluck(Roles.getAllRoles().fetch(), 'name'); + + for permission in permissions + ChatPermissions.upsert( permission._id, {$setOnInsert : permission }) + for role in permission.roles + unless role in roles + Roles.createRole role + roles.push(role) + + diff --git a/packages/rocketchat-lib/server/methods/setAdminStatus.coffee b/packages/rocketchat-lib/server/methods/setAdminStatus.coffee index 6d2cdbb82ae68a8e403e24583f0543bfc01136d1..b81d6d9f04f62db1ee3817794d0afd9854d3f5c2 100644 --- a/packages/rocketchat-lib/server/methods/setAdminStatus.coffee +++ b/packages/rocketchat-lib/server/methods/setAdminStatus.coffee @@ -3,9 +3,12 @@ Meteor.methods if not Meteor.userId() throw new Meteor.Error 'invalid-user', "[methods] setAdminStatus -> Invalid user" - unless Meteor.user()?.admin is true + unless RocketChat.authz.hasPermission( Meteor.userId(), 'assign-admin-role') is true throw new Meteor.Error 'not-authorized', '[methods] setAdminStatus -> Not authorized' - Meteor.users.update userId, { $set: { admin: admin } } + if admin + RocketChat.authz.addUsersToRoles( userId, 'admin') + else + RocketChat.authz.removeUsersFromRoles( userId, 'admin') return true diff --git a/packages/rocketchat-lib/server/methods/updateUser.coffee b/packages/rocketchat-lib/server/methods/updateUser.coffee index 981560219a125aa698407a0e34ef72a911230e81..9a20d41012221417c70dc3176a562077088f4b80 100644 --- a/packages/rocketchat-lib/server/methods/updateUser.coffee +++ b/packages/rocketchat-lib/server/methods/updateUser.coffee @@ -7,7 +7,8 @@ Meteor.methods user = Meteor.user() - if user._id isnt userData._id and user.admin isnt true + canEditUserPermission = RocketChat.authz.hasPermission( user._id, 'edit-other-user-info') + if user._id isnt userData._id and canEditUserPermission isnt true throw new Meteor.Error 'not-authorized', '[methods] updateUser -> Not authorized' unless userData._id diff --git a/packages/rocketchat-lib/settings/server/addOAuthService.coffee b/packages/rocketchat-lib/settings/server/addOAuthService.coffee index a90709bf36215f28e393b31a1668335d16e28633..8a42ff2df70c3fe6d317606803ac801b7a880bab 100644 --- a/packages/rocketchat-lib/settings/server/addOAuthService.coffee +++ b/packages/rocketchat-lib/settings/server/addOAuthService.coffee @@ -5,7 +5,7 @@ Meteor.methods console.log '[methods] addOAuthService -> '.green, 'userId:', Meteor.userId(), 'arguments:', arguments - unless Meteor.user()?.admin is true + unless RocketChat.authz.hasALeastOnePermission( Meteor.userId(), 'add-oath-service') is true throw new Meteor.Error 'not-authorized', '[methods] addOAuthService -> Not authorized' name = s.capitalize(name) diff --git a/packages/rocketchat-lib/settings/server/methods.coffee b/packages/rocketchat-lib/settings/server/methods.coffee index 0808f9582f1dba57e7759c57329ea193b467fd62..cf19272c96b47a32490bfd52db54df3831110f13 100644 --- a/packages/rocketchat-lib/settings/server/methods.coffee +++ b/packages/rocketchat-lib/settings/server/methods.coffee @@ -50,7 +50,7 @@ Meteor.methods if Meteor.userId()? user = Meteor.users.findOne Meteor.userId() - unless user?.admin is true + unless RocketChat.authz.hasPermission(Meteor.userId(), 'edit-privileged-setting') is true throw new Meteor.Error 503, 'Not authorized' # console.log "saveSetting -> ".green, _id, value diff --git a/packages/rocketchat-lib/settings/server/publication.coffee b/packages/rocketchat-lib/settings/server/publication.coffee index 83a8ed6d967a77957c9abfe41358a89390f74eee..92dab9b981ac925e3bbe73bd95b775acfc99cca5 100644 --- a/packages/rocketchat-lib/settings/server/publication.coffee +++ b/packages/rocketchat-lib/settings/server/publication.coffee @@ -8,8 +8,7 @@ Meteor.publish 'admin-settings', -> unless @userId return @ready() - user = Meteor.users.findOne @userId - if user.admin + if RocketChat.authz.hasPermission( @userId, 'view-privileged-setting') return Settings.find() else return @ready() diff --git a/packages/rocketchat-statistics/server/methods/getStatistics.coffee b/packages/rocketchat-statistics/server/methods/getStatistics.coffee index dde2b348a103da856a059c549de8ff860e86b08e..8bf3e8cb1d915b244f6eab3f68d2be6f6361b66a 100644 --- a/packages/rocketchat-statistics/server/methods/getStatistics.coffee +++ b/packages/rocketchat-statistics/server/methods/getStatistics.coffee @@ -5,7 +5,7 @@ Meteor.methods console.log '[methods] getStatistics -> '.green, 'userId:', Meteor.userId(), 'arguments:', arguments - unless Meteor.user()?.admin is true + unless RocketChat.authz.hasPermission(Meteor.userId(), 'view-statistics') is true throw new Meteor.Error 'not-authorized', '[methods] getStatistics -> Not authorized' return RocketChat.statistics.get() \ No newline at end of file diff --git a/server/lib/accounts.coffee b/server/lib/accounts.coffee index 809a70ac490605f53b3bcd67ca6bbff57d0c9f71..bf6b440764b75ab31640f0ff71ce5441feab152f 100644 --- a/server/lib/accounts.coffee +++ b/server/lib/accounts.coffee @@ -24,9 +24,8 @@ Accounts.onCreateUser (options, user) -> user.status = 'offline' user.active = not RocketChat.settings.get 'Accounts_ManuallyApproveNewUsers' - # when inserting first user, set admin: true - unless RocketChat.models.Users.findOne() - user.admin = true + # when inserting first user give them admin privileges otherwise make a regular user + roleName = if RocketChat.models.Users.findOne() then 'user' else 'admin' if not user?.name? or user.name is '' if options.profile?.name? @@ -47,6 +46,9 @@ Accounts.onCreateUser (options, user) -> ] Meteor.defer -> + # need to defer role assignment because underlying alanning:roles requires user + # to exist in users collection + RocketChat.authz.addUsersToRoles( user._id, roleName) RocketChat.callbacks.run 'afterCreateUser', options, user return user diff --git a/server/methods/createChannel.coffee b/server/methods/createChannel.coffee index 283fb8ec64c16a8a6fc84171935417a840f99f3a..2867d975b6ce6e997e311bd682ba87ff53951dc9 100644 --- a/server/methods/createChannel.coffee +++ b/server/methods/createChannel.coffee @@ -6,6 +6,9 @@ Meteor.methods if not /^[0-9a-z-_]+$/.test name throw new Meteor.Error 'name-invalid' + if RocketChat.authz.hasPermission(Meteor.userId(), 'create-c') isnt true + throw new Meteor.Error 'not-authorized', '[methods] createChannel -> Not authorized' + console.log '[methods] createChannel -> '.green, 'userId:', Meteor.userId(), 'arguments:', arguments now = new Date() @@ -33,6 +36,8 @@ Meteor.methods # create new room rid = ChatRoom.insert room + # set creator as channel moderator. permission limited to channel by scoping to rid + RocketChat.authz.addUsersToRoles(Meteor.userId(), 'moderator', rid) for username in members member = RocketChat.models.Users.findOneByUsername username diff --git a/server/methods/createPrivateGroup.coffee b/server/methods/createPrivateGroup.coffee index 5b741e42afb37659c0d4206f3617650383e67f99..e055382e5a2e46d4e17b7d41c1f44b99087bc60f 100644 --- a/server/methods/createPrivateGroup.coffee +++ b/server/methods/createPrivateGroup.coffee @@ -3,6 +3,9 @@ Meteor.methods if not Meteor.userId() throw new Meteor.Error 'invalid-user', "[methods] createPrivateGroup -> Invalid user" + unless RocketChat.authz.hasPermission(Meteor.userId(), 'create-p') + throw new Meteor.Error 'not-authorized', '[methods] createPrivateGroup -> Not authorized' + console.log '[methods] createPrivateGroup -> '.green, 'userId:', Meteor.userId(), 'arguments:', arguments if not /^[0-9a-z-_]+$/.test name @@ -31,6 +34,9 @@ Meteor.methods name: name msgs: 0 + # set creator as group moderator. permission limited to group by scoping to rid + RocketChat.authz.addUsersToRoles(Meteor.userId(), 'moderator', rid) + for username in members member = RocketChat.models.Users.findOneByUsername(username, { fields: { username: 1 }}) if not member? diff --git a/server/methods/deleteMessage.coffee b/server/methods/deleteMessage.coffee index e18e4d7a27ae6390646360ee38a6f407b3a99b1b..8a9288b437c74f29544f4f07a6558d5ca486a02d 100644 --- a/server/methods/deleteMessage.coffee +++ b/server/methods/deleteMessage.coffee @@ -3,13 +3,17 @@ Meteor.methods if not Meteor.userId() throw new Meteor.Error('invalid-user', "[methods] deleteMessage -> Invalid user") - if not RocketChat.settings.get 'Message_AllowDeleting' - throw new Meteor.Error 'message-deleting-not-allowed', "[methods] updateMessage -> Message deleting not allowed" + originalMessage = ChatMessage.findOne message._id, {fields: {u: 1, rid: 1}} + if not originalMessage? + throw new Meteor.Error 'message-deleting-not-allowed', "[methods] deleteMessage -> Message with id [#{message._id} dos not exists]" - user = RocketChat.models.Users.findOneById Meteor.userId() + hasPermission = RocketChat.authz.hasPermission(Meteor.userId(), 'delete-message', originalMessage.rid) + deleteAllowed = RocketChat.settings.get 'Message_AllowDeleting' - unless user?.admin is true or message.u._id is Meteor.userId() - throw new Meteor.Error 'not-authorized', '[methods] deleteMessage -> Not authorized' + deleteOwn = originalMessage?.u?._id is Meteor.userId() + + unless hasPermission or (deleteAllowed and deleteOwn) + throw new Meteor.Error 'message-deleting-not-allowed', "[methods] deleteMessage -> Message deleting not allowed" console.log '[methods] deleteMessage -> '.green, 'userId:', Meteor.userId(), 'arguments:', arguments @@ -17,12 +21,11 @@ Meteor.methods showDeletedStatus = RocketChat.settings.get 'Message_ShowDeletedStatus' deleteQuery = - _id: message._id - deleteQuery['u._id'] = Meteor.userId() if user?.admin isnt true + _id: originalMessage._id if keepHistory if showDeletedStatus - history = ChatMessage.findOne message._id + history = ChatMessage.findOne originalMessage._id history._hidden = true history.parent = history._id history.ets = new Date() @@ -44,4 +47,4 @@ Meteor.methods t: 'rm' ets: new Date() else - RocketChat.Notifications.notifyRoom message.rid, 'deleteMessage', { _id: message._id } + RocketChat.Notifications.notifyRoom originalMessage.rid, 'deleteMessage', { _id: originalMessage._id } diff --git a/server/methods/deleteUser.coffee b/server/methods/deleteUser.coffee index 0a0dc948f3cec7b7b1f67fa66e674b3aaeae796f..e678b04a752e7f3753ac0530aa11edc74b855f51 100644 --- a/server/methods/deleteUser.coffee +++ b/server/methods/deleteUser.coffee @@ -4,7 +4,8 @@ Meteor.methods throw new Meteor.Error('invalid-user', "[methods] deleteUser -> Invalid user") user = RocketChat.models.Users.findOneById Meteor.userId() - unless user?.admin is true + + unless RocketChat.authz.hasPermission(Meteor.userId(), 'delete-user') is true throw new Meteor.Error 'not-authorized', '[methods] deleteUser -> Not authorized' user = RocketChat.models.Users.findOneById userId diff --git a/server/methods/eraseRoom.coffee b/server/methods/eraseRoom.coffee index 90881984d009bfdd5469cdfbad88bbb800f44178..d1de6d3e7f05e06cdaf17845ede7722cb2948899 100644 --- a/server/methods/eraseRoom.coffee +++ b/server/methods/eraseRoom.coffee @@ -2,10 +2,9 @@ Meteor.methods eraseRoom: (rid) -> fromId = Meteor.userId() - user = RocketChat.models.Users.findOneById Meteor.userId() - if user.admin is true - + roomType = ChatRoom.findOne(rid)?.t + if RocketChat.authz.hasPermission( fromId, "delete-#{roomType}", rid ) # console.log '[methods] eraseRoom -> '.green, 'fromId:', fromId, 'rid:', rid # ChatRoom.update({ _id: rid}, {'$pull': { userWatching: Meteor.userId(), userIn: Meteor.userId() }}) @@ -14,3 +13,5 @@ Meteor.methods ChatSubscription.remove({rid: rid}) ChatRoom.remove(rid) # @TODO remove das mensagens lidas do usuário + else + throw new Meteor.Error 'unauthorized' diff --git a/server/methods/migrate.coffee b/server/methods/migrate.coffee index 056eac9f4af0bd47ababf5a64cece83d135d64f8..915b4582f37ac9de86dd7a76f8c332ef4ef56917 100644 --- a/server/methods/migrate.coffee +++ b/server/methods/migrate.coffee @@ -2,7 +2,8 @@ Meteor.methods migrateTo: (version) -> user = Meteor.user() - if not user? or user.admin isnt true + if not user? or RocketChat.authz.hasPermission(user._id, 'run-migration') isnt true + console.log '[methods] createChannel -> Not authorized' return this.unblock() diff --git a/server/methods/removeUserFromRoom.coffee b/server/methods/removeUserFromRoom.coffee index eee2536e70b5dedae63f2f43cd57037655773c3b..8500ff030649f5365176b7147c41225baf3021ff 100644 --- a/server/methods/removeUserFromRoom.coffee +++ b/server/methods/removeUserFromRoom.coffee @@ -18,6 +18,12 @@ Meteor.methods ChatSubscription.remove { 'u._id': data.username, rid: data.rid } + switch room.t + when 'c' + RocketChat.authz.removeUsersFromRole(removedUser._id; 'channel-moderator', data.rid) + when 'p' + RocketChat.authz.removeUsersFromRole(removedUser._id; 'group-moderator', data.rid) + ChatMessage.insert rid: data.rid ts: (new Date) diff --git a/server/methods/saveRoomName.coffee b/server/methods/saveRoomName.coffee index be073725c466cfb9275c4d1449b6f44cc054616e..23de97d60d5fc9cbaee0faaacdd27278345a95d4 100644 --- a/server/methods/saveRoomName.coffee +++ b/server/methods/saveRoomName.coffee @@ -5,7 +5,11 @@ Meteor.methods room = ChatRoom.findOne rid - if room.u._id isnt Meteor.userId() or room.t not in ['c', 'p'] + if room.t not in ['c', 'p'] + throw new Meteor.Error 403, 'Not allowed' + + unless RocketChat.authz.hasPermission(Meteor.userId(), 'edit-room', rid) + #if room.u._id isnt Meteor.userId() and not hasPermission throw new Meteor.Error 403, 'Not allowed' if not /^[0-9a-z-_]+$/.test name diff --git a/server/methods/setUserActiveStatus.coffee b/server/methods/setUserActiveStatus.coffee index 59e4dd268c3d62fdd09d7e878e01bc83c48f4ceb..f7084ae499fc67eee8238d4cbbd3a255259e5726 100644 --- a/server/methods/setUserActiveStatus.coffee +++ b/server/methods/setUserActiveStatus.coffee @@ -3,8 +3,7 @@ Meteor.methods if not Meteor.userId() throw new Meteor.Error 'invalid-user', '[methods] setUserActiveStatus -> Invalid user' - user = RocketChat.models.Users.findOneById Meteor.userId() - unless user?.admin is true + unless RocketChat.authz.hasPermission( Meteor.userId(), 'edit-other-user-active-status') is true throw new Meteor.Error 'not-authorized', '[methods] setUserActiveStatus -> Not authorized' RocketChat.models.Users.setUserActive userId, active diff --git a/server/methods/updateMessage.coffee b/server/methods/updateMessage.coffee index 73d884f614d7c81f2f6d786ff4e9306f3508b2fd..65e3332e3ae4136cfbd38448d05a93cb2cb7044e 100644 --- a/server/methods/updateMessage.coffee +++ b/server/methods/updateMessage.coffee @@ -3,15 +3,14 @@ Meteor.methods if not Meteor.userId() throw new Meteor.Error('invalid-user', "[methods] updateMessage -> Invalid user") - if not RocketChat.settings.get 'Message_AllowEditing' - throw new Meteor.Error 'message-editing-not-allowed', "[methods] updateMessage -> Message editing not allowed" - - user = RocketChat.models.Users.findOneById Meteor.userId() - originalMessage = ChatMessage.findOne message._id - unless user?.admin is true or originalMessage?.u?._id is Meteor.userId() - throw new Meteor.Error 'not-authorized', '[methods] updateMessage -> Not authorized' + hasPermission = RocketChat.authz.hasPermission(Meteor.userId(), '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', "[methods] updateMessage -> Message editing not allowed" console.log '[methods] updateMessage -> '.green, 'userId:', Meteor.userId(), 'arguments:', arguments @@ -35,9 +34,9 @@ Meteor.methods ChatMessage.update _id: tempid - 'u._id': Meteor.userId() , $set: message # Meteor.defer -> # RocketChat.callbacks.run 'afterSaveMessage', ChatMessage.findOne(message.id) + diff --git a/server/publications/adminRooms.coffee b/server/publications/adminRooms.coffee index 6a0e0e32a1031d60fee7978bb49bd48e1dd8a723..6d02cb7fcfbd7fd10ef195476dc5ab37775a0900 100644 --- a/server/publications/adminRooms.coffee +++ b/server/publications/adminRooms.coffee @@ -2,8 +2,7 @@ Meteor.publish 'adminRooms', (filter, types, limit) -> unless this.userId return this.ready() - user = RocketChat.models.Users.findOneById this.userId - if user.admin isnt true + if RocketChat.authz.hasPermission(@userId, 'view-room-administration') isnt true return this.ready() unless _.isArray types diff --git a/server/publications/fullUserData.coffee b/server/publications/fullUserData.coffee index 5683f5537584df2fe58e3eee41012428f3ee8622..36949ab9142da784cc7b43321e09c4302227d1f1 100644 --- a/server/publications/fullUserData.coffee +++ b/server/publications/fullUserData.coffee @@ -10,16 +10,16 @@ Meteor.publish 'fullUserData', (filter, limit) -> status: 1 utcOffset: 1 - if user.admin is true + if RocketChat.authz.hasPermission( @userId, 'view-full-other-user-info') is true fields = _.extend fields, emails: 1 phone: 1 statusConnection: 1 - admin: 1 createdAt: 1 lastLogin: 1 active: 1 services: 1 + roles : 1 else limit = 1 diff --git a/server/publications/userChannels.coffee b/server/publications/userChannels.coffee index d6ba81b346007d0571bb5cd062ae26eae523208a..51fcf4ab09bfdc42f8a7578a9258d882e85128d4 100644 --- a/server/publications/userChannels.coffee +++ b/server/publications/userChannels.coffee @@ -2,8 +2,7 @@ Meteor.publish 'userChannels', (userId) -> unless this.userId return this.ready() - user = RocketChat.models.Users.findOneById this.userId - if user.admin isnt true + if RocketChat.authz.hasPermission( @userId, 'view-other-user-channels') isnt true return this.ready() query = { "u._id": userId } diff --git a/server/publications/userData.coffee b/server/publications/userData.coffee index ba4a8c9a36df71b5a7e358ebfbf30cc8a8e91853..3d7811c9f40bf6c0a7f2aefd578607a262629809 100644 --- a/server/publications/userData.coffee +++ b/server/publications/userData.coffee @@ -12,7 +12,6 @@ Meteor.publish 'userData', -> statusDefault: 1 statusConnection: 1 avatarOrigin: 1 - admin: 1 utcOffset: 1 language: 1 settings: 1 diff --git a/server/startup/initialData.coffee b/server/startup/initialData.coffee index dd04fd9504c75e3d5006392e49ab78111e3d86c1..0bd2da0513adc26d68ab04b89970c79dfd569770 100644 --- a/server/startup/initialData.coffee +++ b/server/startup/initialData.coffee @@ -19,7 +19,7 @@ Meteor.startup -> if process.env.ADMIN_EMAIL? and process.env.ADMIN_PASS? re = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i if re.test process.env.ADMIN_EMAIL - if not RocketChat.models.Users.findOneAdmin(true)? + if _.isEmpty(RocketChat.authz.getUsersInRole( 'admin' ).fetch()) if not RocketChat.models.Users.findOneByEmailAddress process.env.ADMIN_EMAIL console.log 'Inserting admin user'.red console.log "email: #{process.env.ADMIN_EMAIL} | password: #{process.env.ADMIN_PASS}".red @@ -30,9 +30,10 @@ Meteor.startup -> verified: true ], name: 'Admin' - admin: true Accounts.setPassword id, process.env.ADMIN_PASS + RocketChat.authz.addUsersToRoles( id, 'admin') + else console.log 'E-mail exists; ignoring environment variables ADMIN_EMAIL and ADMIN_PASS'.red else @@ -41,10 +42,9 @@ Meteor.startup -> console.log 'E-mail provided is invalid; ignoring environment variables ADMIN_EMAIL and ADMIN_PASS'.red # Set oldest user as admin, if none exists yet - admin = RocketChat.models.Users.findOneAdmin true, { fields: { _id: 1 } } - unless admin + if _.isEmpty( RocketChat.authz.getUsersInRole( 'admin' ).fetch()) # get oldest user oldestUser = RocketChat.models.Users.findOne({}, { fields: { username: 1 }, sort: {createdAt: 1}}) if oldestUser - Meteor.users.update {_id: oldestUser._id}, {$set: {admin: true}} + RocketChat.authz.addUsersToRoles( oldestUser._id, 'admin') console.log "No admins are found. Set #{oldestUser.username} as admin for being the oldest user" diff --git a/server/startup/migrations/v19.coffee b/server/startup/migrations/v19.coffee new file mode 100644 index 0000000000000000000000000000000000000000..02c47df293c6cc7b701a0773ffaa782a85f372b2 --- /dev/null +++ b/server/startup/migrations/v19.coffee @@ -0,0 +1,28 @@ +Meteor.startup -> + Migrations.add + version: 19 + up: -> + ### + # Migrate existing admin users to Role based admin functionality + # 'admin' role applies to global scope + ### + admins = Meteor.users.find({ admin: true }, { fields: { _id: 1, username:1 } }).fetch() + RocketChat.authz.addUsersToRoles( _.pluck(admins, '_id'), ['admin']) + Meteor.users.update({}, { $unset :{admin:''}}, {multi:true}) + usernames = _.pluck( admins, 'username').join(', ') + console.log "Migrate #{usernames} from admin field to 'admin' role".green + + # Add 'user' role to all users + users = Meteor.users.find().fetch() + RocketChat.authz.addUsersToRoles( _.pluck(users, '_id'), ['user']) + usernames = _.pluck( users, 'username').join(', ') + console.log "Add #{usernames} to 'user' role".green + + # Add 'moderator' role to channel/group creators + rooms = ChatRoom.find({t: {$in : ['c','p']}}).fetch() + _.each( rooms, (room) -> + creator = room?.u?._id + if creator + RocketChat.authz.addUsersToRoles( creator, ['moderator'], room._id) + console.log "Add #{room.u.username} to 'moderator' role".green + )