From 99cf08ac38bdee25d07f27a3d9bb5d74199d106c Mon Sep 17 00:00:00 2001 From: Amit Yadav Date: Wed, 18 Jan 2017 18:38:31 +0530 Subject: Message Editing and Deleting permissions (#4692) --- api/post.go | 23 ++- api/post_test.go | 155 ++++++++++++++++++++- config/config.json | 5 +- i18n/en.json | 8 ++ model/authorization.go | 14 ++ model/config.go | 26 ++++ utils/authorization.go | 22 +++ utils/config.go | 3 + utils/diagnostic.go | 3 + .../components/admin_console/policy_settings.jsx | 49 +++++++ .../components/admin_console/post_edit_setting.jsx | 99 +++++++++++++ webapp/components/admin_console/radio_setting.jsx | 63 +++++++++ webapp/components/edit_post_modal.jsx | 11 ++ .../components/post_view/components/post_info.jsx | 24 ++-- webapp/components/rhs_comment.jsx | 21 ++- webapp/components/rhs_root_post.jsx | 21 ++- webapp/i18n/en.json | 10 ++ webapp/stores/user_store.jsx | 2 +- webapp/utils/constants.jsx | 7 + webapp/utils/post_utils.jsx | 41 ++++++ 20 files changed, 575 insertions(+), 32 deletions(-) create mode 100644 webapp/components/admin_console/post_edit_setting.jsx create mode 100644 webapp/components/admin_console/radio_setting.jsx diff --git a/api/post.go b/api/post.go index bbdce78e8..0e3ad2aa5 100644 --- a/api/post.go +++ b/api/post.go @@ -91,6 +91,16 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) { } func updatePost(c *Context, w http.ResponseWriter, r *http.Request) { + + if utils.IsLicensed { + if *utils.Cfg.ServiceSettings.AllowEditPost == model.ALLOW_EDIT_POST_NEVER { + c.Err = model.NewLocAppError("updatePost", "api.post.update_post.permissions.app_error", nil, + c.T("api.post.update_post.permissions_denied.app_error")) + c.Err.StatusCode = http.StatusForbidden + return + } + } + post := model.PostFromJson(r.Body) if post == nil { @@ -135,6 +145,15 @@ func updatePost(c *Context, w http.ResponseWriter, r *http.Request) { c.Err.StatusCode = http.StatusForbidden return } + + if utils.IsLicensed { + if *utils.Cfg.ServiceSettings.AllowEditPost == model.ALLOW_EDIT_POST_TIME_LIMIT && model.GetMillis() > oldPost.CreateAt+int64(*utils.Cfg.ServiceSettings.PostEditTimeLimit*1000) { + c.Err = model.NewLocAppError("updatePost", "api.post.update_post.permissions.app_error", nil, + c.T("api.post.update_post.permissions_time_limit.app_error", map[string]interface{}{"timeLimit": *utils.Cfg.ServiceSettings.PostEditTimeLimit})) + c.Err.StatusCode = http.StatusForbidden + return + } + } } newPost := &model.Post{} @@ -402,7 +421,7 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_EDIT_POST) { + if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_DELETE_POST) { return } @@ -426,7 +445,7 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { return } - if post.UserId != c.Session.UserId && !HasPermissionToChannelContext(c, post.ChannelId, model.PERMISSION_EDIT_OTHERS_POSTS) { + if post.UserId != c.Session.UserId && !HasPermissionToChannelContext(c, post.ChannelId, model.PERMISSION_DELETE_OTHERS_POSTS) { c.Err = model.NewLocAppError("deletePost", "api.post.delete_post.permissions.app_error", nil, "") c.Err.StatusCode = http.StatusForbidden return diff --git a/api/post_test.go b/api/post_test.go index 151387953..4d3ee80b5 100644 --- a/api/post_test.go +++ b/api/post_test.go @@ -315,6 +315,15 @@ func TestUpdatePost(t *testing.T) { Client := th.BasicClient channel1 := th.BasicChannel + allowEditPost := *utils.Cfg.ServiceSettings.AllowEditPost + postEditTimeLimit := *utils.Cfg.ServiceSettings.PostEditTimeLimit + defer func() { + *utils.Cfg.ServiceSettings.AllowEditPost = allowEditPost + *utils.Cfg.ServiceSettings.PostEditTimeLimit = postEditTimeLimit + }() + + *utils.Cfg.ServiceSettings.AllowEditPost = model.ALLOW_EDIT_POST_ALWAYS + post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} rpost1, err := Client.CreatePost(post1) if err != nil { @@ -377,6 +386,56 @@ func TestUpdatePost(t *testing.T) { if _, err := Client.UpdatePost(up3); err == nil { t.Fatal("shouldn't have been able to update system message") } + + // Test licensed policy controls for edit post + isLicensed := utils.IsLicensed + license := utils.License + defer func() { + utils.IsLicensed = isLicensed + utils.License = license + }() + utils.IsLicensed = true + utils.License = &model.License{Features: &model.Features{}} + utils.License.Features.SetDefaults() + + *utils.Cfg.ServiceSettings.AllowEditPost = model.ALLOW_EDIT_POST_NEVER + + post4 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: rpost1.Data.(*model.Post).Id} + rpost4, err := Client.CreatePost(post4) + if err != nil { + t.Fatal(err) + } + + up4 := &model.Post{Id: rpost4.Data.(*model.Post).Id, ChannelId: channel1.Id, Message: "a" + model.NewId() + " update post 4"} + if _, err := Client.UpdatePost(up4); err == nil { + t.Fatal("shouldn't have been able to update a message when not allowed") + } + + *utils.Cfg.ServiceSettings.AllowEditPost = model.ALLOW_EDIT_POST_TIME_LIMIT + *utils.Cfg.ServiceSettings.PostEditTimeLimit = 1 //seconds + + post5 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: rpost1.Data.(*model.Post).Id} + rpost5, err := Client.CreatePost(post5) + if err != nil { + t.Fatal(err) + } + + msg5 := "a" + model.NewId() + " update post 5" + up5 := &model.Post{Id: rpost5.Data.(*model.Post).Id, ChannelId: channel1.Id, Message: msg5} + if rup5, err := Client.UpdatePost(up5); err != nil { + t.Fatal(err) + } else { + if rup5.Data.(*model.Post).Message != up5.Message { + t.Fatal("failed to updates") + } + } + + time.Sleep(1000 * time.Millisecond) + + up6 := &model.Post{Id: rpost5.Data.(*model.Post).Id, ChannelId: channel1.Id, Message: "a" + model.NewId() + " update post 5"} + if _, err := Client.UpdatePost(up6); err == nil { + t.Fatal("shouldn't have been able to update a message after time limit") + } } func TestGetPosts(t *testing.T) { @@ -805,10 +864,18 @@ func TestGetPostsCache(t *testing.T) { } func TestDeletePosts(t *testing.T) { - th := Setup().InitBasic() + th := Setup().InitBasic().InitSystemAdmin() Client := th.BasicClient channel1 := th.BasicChannel - UpdateUserToTeamAdmin(th.BasicUser2, th.BasicTeam) + team1 := th.BasicTeam + + restrictPostDelete := *utils.Cfg.ServiceSettings.RestrictPostDelete + defer func() { + *utils.Cfg.ServiceSettings.RestrictPostDelete = restrictPostDelete + utils.SetDefaultRolesBasedOnConfig() + }() + *utils.Cfg.ServiceSettings.RestrictPostDelete = model.PERMISSIONS_DELETE_POST_ALL + utils.SetDefaultRolesBasedOnConfig() time.Sleep(10 * time.Millisecond) post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} @@ -840,17 +907,93 @@ func TestDeletePosts(t *testing.T) { r2 := Client.Must(Client.GetPosts(channel1.Id, 0, 10, "")).Data.(*model.PostList) if len(r2.Posts) != 5 { - t.Fatal("should have returned 4 items") + t.Fatal("should have returned 5 items") } time.Sleep(10 * time.Millisecond) - post4 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} - post4 = Client.Must(Client.CreatePost(post4)).Data.(*model.Post) + post4a := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + post4a = Client.Must(Client.CreatePost(post4a)).Data.(*model.Post) + + time.Sleep(10 * time.Millisecond) + post4b := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + post4b = Client.Must(Client.CreatePost(post4b)).Data.(*model.Post) + + SystemAdminClient := th.SystemAdminClient + LinkUserToTeam(th.SystemAdminUser, th.BasicTeam) + SystemAdminClient.Must(SystemAdminClient.JoinChannel(channel1.Id)) th.LoginBasic2() Client.Must(Client.JoinChannel(channel1.Id)) - Client.Must(Client.DeletePost(channel1.Id, post4.Id)) + if _, err := Client.DeletePost(channel1.Id, post4a.Id); err == nil { + t.Fatal(err) + } + + // Test licensed policy controls for delete post + isLicensed := utils.IsLicensed + license := utils.License + defer func() { + utils.IsLicensed = isLicensed + utils.License = license + }() + utils.IsLicensed = true + utils.License = &model.License{Features: &model.Features{}} + utils.License.Features.SetDefaults() + + UpdateUserToTeamAdmin(th.BasicUser2, th.BasicTeam) + + Client.Logout() + th.LoginBasic2() + Client.SetTeamId(team1.Id) + + Client.Must(Client.DeletePost(channel1.Id, post4a.Id)) + + SystemAdminClient.Must(SystemAdminClient.DeletePost(channel1.Id, post4b.Id)) + + *utils.Cfg.ServiceSettings.RestrictPostDelete = model.PERMISSIONS_DELETE_POST_TEAM_ADMIN + utils.SetDefaultRolesBasedOnConfig() + + th.LoginBasic() + + time.Sleep(10 * time.Millisecond) + post5a := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + post5a = Client.Must(Client.CreatePost(post5a)).Data.(*model.Post) + + time.Sleep(10 * time.Millisecond) + post5b := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + post5b = Client.Must(Client.CreatePost(post5b)).Data.(*model.Post) + + if _, err := Client.DeletePost(channel1.Id, post5a.Id); err == nil { + t.Fatal(err) + } + + th.LoginBasic2() + + Client.Must(Client.DeletePost(channel1.Id, post5a.Id)) + + SystemAdminClient.Must(SystemAdminClient.DeletePost(channel1.Id, post5b.Id)) + + *utils.Cfg.ServiceSettings.RestrictPostDelete = model.PERMISSIONS_DELETE_POST_SYSTEM_ADMIN + utils.SetDefaultRolesBasedOnConfig() + + th.LoginBasic() + + time.Sleep(10 * time.Millisecond) + post6a := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + post6a = Client.Must(Client.CreatePost(post6a)).Data.(*model.Post) + + if _, err := Client.DeletePost(channel1.Id, post6a.Id); err == nil { + t.Fatal(err) + } + + th.LoginBasic2() + + if _, err := Client.DeletePost(channel1.Id, post6a.Id); err == nil { + t.Fatal(err) + } + + SystemAdminClient.Must(SystemAdminClient.DeletePost(channel1.Id, post6a.Id)) + } func TestEmailMention(t *testing.T) { diff --git a/config/config.json b/config/config.json index 971e77837..f538e9686 100644 --- a/config/config.json +++ b/config/config.json @@ -35,7 +35,10 @@ "WebsocketPort": 80, "WebserverMode": "gzip", "EnableCustomEmoji": false, - "RestrictCustomEmojiCreation": "all" + "RestrictCustomEmojiCreation": "all", + "RestrictPostDelete": "all", + "AllowEditPost": "always", + "PostEditTimeLimit": 300 }, "TeamSettings": { "SiteName": "Mattermost", diff --git a/i18n/en.json b/i18n/en.json index 84306f0ce..ac0832939 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1607,6 +1607,14 @@ "id": "api.post.update_post.permissions_details.app_error", "translation": "Already deleted id={{.PostId}}" }, + { + "id": "api.post.update_post.permissions_denied.app_error", + "translation": "Post edit has been disabled. Please ask your systems administrator for details." + }, + { + "id": "api.post.update_post.permissions_time_limit.app_error", + "translation": "Post edit is only allowed for {{.timeLimit}} seconds. Please ask your systems administrator for details." + }, { "id": "api.post.update_post.system_message.app_error", "translation": "Unable to update system message" diff --git a/model/authorization.go b/model/authorization.go index 58fed5854..78d7d3960 100644 --- a/model/authorization.go +++ b/model/authorization.go @@ -47,6 +47,8 @@ var PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH *Permission var PERMISSION_CREATE_POST *Permission var PERMISSION_EDIT_POST *Permission var PERMISSION_EDIT_OTHERS_POSTS *Permission +var PERMISSION_DELETE_POST *Permission +var PERMISSION_DELETE_OTHERS_POSTS *Permission var PERMISSION_REMOVE_USER_FROM_TEAM *Permission var PERMISSION_MANAGE_TEAM *Permission var PERMISSION_IMPORT_TEAM *Permission @@ -229,6 +231,16 @@ func InitalizePermissions() { "authentication.permissions.edit_others_posts.name", "authentication.permissions.edit_others_posts.description", } + PERMISSION_DELETE_POST = &Permission{ + "delete_post", + "authentication.permissions.delete_post.name", + "authentication.permissions.delete_post.description", + } + PERMISSION_DELETE_OTHERS_POSTS = &Permission{ + "delete_others_posts", + "authentication.permissions.delete_others_posts.name", + "authentication.permissions.delete_others_posts.description", + } PERMISSION_REMOVE_USER_FROM_TEAM = &Permission{ "remove_user_from_team", "authentication.permissions.remove_user_from_team.name", @@ -349,6 +361,8 @@ func InitalizeRoles() { PERMISSION_EDIT_OTHER_USERS.Id, PERMISSION_MANAGE_OAUTH.Id, PERMISSION_INVITE_USER.Id, + PERMISSION_DELETE_POST.Id, + PERMISSION_DELETE_OTHERS_POSTS.Id, }, ROLE_TEAM_USER.Permissions..., ), diff --git a/model/config.go b/model/config.go index 0134e1a34..13e795170 100644 --- a/model/config.go +++ b/model/config.go @@ -49,6 +49,14 @@ const ( RESTRICT_EMOJI_CREATION_ADMIN = "admin" RESTRICT_EMOJI_CREATION_SYSTEM_ADMIN = "system_admin" + PERMISSIONS_DELETE_POST_ALL = "all" + PERMISSIONS_DELETE_POST_TEAM_ADMIN = "team_admin" + PERMISSIONS_DELETE_POST_SYSTEM_ADMIN = "system_admin" + + ALLOW_EDIT_POST_ALWAYS = "always" + ALLOW_EDIT_POST_NEVER = "never" + ALLOW_EDIT_POST_TIME_LIMIT = "time_limit" + EMAIL_BATCHING_BUFFER_SIZE = 256 EMAIL_BATCHING_INTERVAL = 30 @@ -92,6 +100,9 @@ type ServiceSettings struct { WebserverMode *string EnableCustomEmoji *bool RestrictCustomEmojiCreation *string + RestrictPostDelete *string + AllowEditPost *string + PostEditTimeLimit *int } type ClusterSettings struct { @@ -827,6 +838,21 @@ func (o *Config) SetDefaults() { *o.ServiceSettings.RestrictCustomEmojiCreation = RESTRICT_EMOJI_CREATION_ALL } + if o.ServiceSettings.RestrictPostDelete == nil { + o.ServiceSettings.RestrictPostDelete = new(string) + *o.ServiceSettings.RestrictPostDelete = PERMISSIONS_DELETE_POST_ALL + } + + if o.ServiceSettings.AllowEditPost == nil { + o.ServiceSettings.AllowEditPost = new(string) + *o.ServiceSettings.AllowEditPost = ALLOW_EDIT_POST_TIME_LIMIT + } + + if o.ServiceSettings.PostEditTimeLimit == nil { + o.ServiceSettings.PostEditTimeLimit = new(int) + *o.ServiceSettings.PostEditTimeLimit = 300 + } + if o.ClusterSettings.InterNodeListenAddress == nil { o.ClusterSettings.InterNodeListenAddress = new(string) *o.ClusterSettings.InterNodeListenAddress = ":8075" diff --git a/utils/authorization.go b/utils/authorization.go index 533808467..ba4768140 100644 --- a/utils/authorization.go +++ b/utils/authorization.go @@ -148,4 +148,26 @@ func SetDefaultRolesBasedOnConfig() { model.PERMISSION_INVITE_USER.Id, ) } + + switch *Cfg.ServiceSettings.RestrictPostDelete { + case model.PERMISSIONS_DELETE_POST_ALL: + model.ROLE_CHANNEL_USER.Permissions = append( + model.ROLE_CHANNEL_USER.Permissions, + model.PERMISSION_DELETE_POST.Id, + ) + model.ROLE_TEAM_ADMIN.Permissions = append( + model.ROLE_TEAM_ADMIN.Permissions, + model.PERMISSION_DELETE_POST.Id, + model.PERMISSION_DELETE_OTHERS_POSTS.Id, + ) + break + case model.PERMISSIONS_DELETE_POST_TEAM_ADMIN: + model.ROLE_TEAM_ADMIN.Permissions = append( + model.ROLE_TEAM_ADMIN.Permissions, + model.PERMISSION_DELETE_POST.Id, + model.PERMISSION_DELETE_OTHERS_POSTS.Id, + ) + break + } + } diff --git a/utils/config.go b/utils/config.go index da070012e..3825d397d 100644 --- a/utils/config.go +++ b/utils/config.go @@ -267,6 +267,9 @@ func getClientConfig(c *model.Config) map[string]string { props["EnableTesting"] = strconv.FormatBool(c.ServiceSettings.EnableTesting) props["EnableDeveloper"] = strconv.FormatBool(*c.ServiceSettings.EnableDeveloper) props["EnableDiagnostics"] = strconv.FormatBool(*c.LogSettings.EnableDiagnostics) + props["RestrictPostDelete"] = *c.ServiceSettings.RestrictPostDelete + props["AllowEditPost"] = *c.ServiceSettings.AllowEditPost + props["PostEditTimeLimit"] = fmt.Sprintf("%v", *c.ServiceSettings.PostEditTimeLimit) props["SendEmailNotifications"] = strconv.FormatBool(c.EmailSettings.SendEmailNotifications) props["SendPushNotifications"] = strconv.FormatBool(*c.EmailSettings.SendPushNotifications) diff --git a/utils/diagnostic.go b/utils/diagnostic.go index 7509ccbb5..525dfd794 100644 --- a/utils/diagnostic.go +++ b/utils/diagnostic.go @@ -82,6 +82,9 @@ func trackConfig() { "restrict_custom_emoji_creation": *Cfg.ServiceSettings.RestrictCustomEmojiCreation, "enable_testing": Cfg.ServiceSettings.EnableTesting, "enable_developer": *Cfg.ServiceSettings.EnableDeveloper, + "restrict_post_delete": *Cfg.ServiceSettings.RestrictPostDelete, + "allow_edit_post": *Cfg.ServiceSettings.AllowEditPost, + "post_edit_time_limit": *Cfg.ServiceSettings.PostEditTimeLimit, }) SendDiagnostic(TRACK_CONFIG_TEAM, map[string]interface{}{ diff --git a/webapp/components/admin_console/policy_settings.jsx b/webapp/components/admin_console/policy_settings.jsx index 0e224af73..391726a93 100644 --- a/webapp/components/admin_console/policy_settings.jsx +++ b/webapp/components/admin_console/policy_settings.jsx @@ -6,6 +6,8 @@ import React from 'react'; import AdminSettings from './admin_settings.jsx'; import SettingsGroup from './settings_group.jsx'; import DropdownSetting from './dropdown_setting.jsx'; +import RadioSetting from './radio_setting.jsx'; +import PostEditSetting from './post_edit_setting.jsx'; import Constants from 'utils/constants.jsx'; import * as Utils from 'utils/utils.jsx'; @@ -22,6 +24,9 @@ export default class PolicySettings extends AdminSettings { } getConfigFromState(config) { + config.ServiceSettings.RestrictPostDelete = this.state.restrictPostDelete; + config.ServiceSettings.AllowEditPost = this.state.allowEditPost; + config.ServiceSettings.PostEditTimeLimit = this.parseIntNonZero(this.state.postEditTimeLimit, Constants.DEFAULT_POST_EDIT_TIME_LIMIT); config.TeamSettings.RestrictTeamInvite = this.state.restrictTeamInvite; config.TeamSettings.RestrictPublicChannelCreation = this.state.restrictPublicChannelCreation; config.TeamSettings.RestrictPrivateChannelCreation = this.state.restrictPrivateChannelCreation; @@ -35,6 +40,9 @@ export default class PolicySettings extends AdminSettings { getStateFromConfig(config) { return { + restrictPostDelete: config.ServiceSettings.RestrictPostDelete, + allowEditPost: config.ServiceSettings.AllowEditPost, + postEditTimeLimit: config.ServiceSettings.PostEditTimeLimit, restrictTeamInvite: config.TeamSettings.RestrictTeamInvite, restrictPublicChannelCreation: config.TeamSettings.RestrictPublicChannelCreation, restrictPrivateChannelCreation: config.TeamSettings.RestrictPrivateChannelCreation, @@ -241,6 +249,47 @@ export default class PolicySettings extends AdminSettings { /> } /> + + } + value={this.state.restrictPostDelete} + onChange={this.handleChange} + helpText={ + + } + /> + + } + value={this.state.allowEditPost} + timeLimitValue={this.state.postEditTimeLimit} + onChange={this.handleChange} + helpText={ + + } + /> ); } diff --git a/webapp/components/admin_console/post_edit_setting.jsx b/webapp/components/admin_console/post_edit_setting.jsx new file mode 100644 index 000000000..282a1b6c5 --- /dev/null +++ b/webapp/components/admin_console/post_edit_setting.jsx @@ -0,0 +1,99 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import Setting from './setting.jsx'; + +import Constants from 'utils/constants.jsx'; +import * as Utils from 'utils/utils.jsx'; + +export default class PostEditSetting extends React.Component { + constructor(props) { + super(props); + + this.handleChange = this.handleChange.bind(this); + this.handleTimeLimitChange = this.handleTimeLimitChange.bind(this); + } + + handleChange(e) { + this.props.onChange(this.props.id, e.target.value); + } + + handleTimeLimitChange(e) { + this.props.onChange(this.props.timeLimitId, e.target.value); + } + + render() { + return ( + +
+ +
+
+ +
+
+ +
+
+ ); + } +} + +PostEditSetting.defaultProps = { + isDisabled: false +}; + +PostEditSetting.propTypes = { + id: React.PropTypes.string.isRequired, + timeLimitId: React.PropTypes.string.isRequired, + label: React.PropTypes.node.isRequired, + value: React.PropTypes.string.isRequired, + timeLimitValue: React.PropTypes.number.isRequired, + onChange: React.PropTypes.func.isRequired, + disabled: React.PropTypes.bool, + helpText: React.PropTypes.node +}; diff --git a/webapp/components/admin_console/radio_setting.jsx b/webapp/components/admin_console/radio_setting.jsx new file mode 100644 index 000000000..dd45a5a26 --- /dev/null +++ b/webapp/components/admin_console/radio_setting.jsx @@ -0,0 +1,63 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import Setting from './setting.jsx'; + +export default class RadioSetting extends React.Component { + constructor(props) { + super(props); + + this.handleChange = this.handleChange.bind(this); + } + + handleChange(e) { + this.props.onChange(this.props.id, e.target.value); + } + + render() { + const options = []; + for (const {value, text} of this.props.values) { + options.push( +
+ +
+ ); + } + + return ( + + {options} + + ); + } +} + +RadioSetting.defaultProps = { + isDisabled: false +}; + +RadioSetting.propTypes = { + id: React.PropTypes.string.isRequired, + values: React.PropTypes.array.isRequired, + label: React.PropTypes.node.isRequired, + value: React.PropTypes.string.isRequired, + onChange: React.PropTypes.func.isRequired, + disabled: React.PropTypes.bool, + helpText: React.PropTypes.node +}; diff --git a/webapp/components/edit_post_modal.jsx b/webapp/components/edit_post_modal.jsx index 55bf5fe60..7c1747ef7 100644 --- a/webapp/components/edit_post_modal.jsx +++ b/webapp/components/edit_post_modal.jsx @@ -118,6 +118,17 @@ export default class EditPostModal extends React.Component { } handleEditPostEvent(options) { + var post = PostStore.getPost(options.channelId, options.postId); + if (global.window.mm_license.IsLicensed === 'true') { + if (global.window.mm_config.AllowEditPost === Constants.ALLOW_EDIT_POST_NEVER) { + return; + } + if (global.window.mm_config.AllowEditPost === Constants.ALLOW_EDIT_POST_TIME_LIMIT) { + if ((post.create_at + (global.window.mm_config.PostEditTimeLimit * 1000)) < Utils.getTimestamp()) { + return; + } + } + } this.setState({ editText: options.message || '', originalText: options.message || '', diff --git a/webapp/components/post_view/components/post_info.jsx b/webapp/components/post_view/components/post_info.jsx index aa204add1..c4de46e8f 100644 --- a/webapp/components/post_view/components/post_info.jsx +++ b/webapp/components/post_view/components/post_info.jsx @@ -8,12 +8,10 @@ import PostTime from './post_time.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; import * as PostActions from 'actions/post_actions.jsx'; -import TeamStore from 'stores/team_store.jsx'; -import UserStore from 'stores/user_store.jsx'; - import * as Utils from 'utils/utils.jsx'; import * as PostUtils from 'utils/post_utils.jsx'; import Constants from 'utils/constants.jsx'; +import DelayedAction from 'utils/delayed_action.jsx'; import {Tooltip, OverlayTrigger} from 'react-bootstrap'; import React from 'react'; @@ -28,6 +26,10 @@ export default class PostInfo extends React.Component { this.removePost = this.removePost.bind(this); this.flagPost = this.flagPost.bind(this); this.unflagPost = this.unflagPost.bind(this); + + this.canEdit = false; + this.canDelete = false; + this.editDisableAction = new DelayedAction(this.handleEditDisable); } handleDropdownClick(e) { @@ -38,6 +40,10 @@ export default class PostInfo extends React.Component { } } + handleEditDisable() { + this.canEdit = false; + } + componentDidMount() { $('#post_dropdown' + this.props.post.id).on('shown.bs.dropdown', () => this.props.handleDropdownOpened(true)); $('#post_dropdown' + this.props.post.id).on('hidden.bs.dropdown', () => this.props.handleDropdownOpened(false)); @@ -45,9 +51,9 @@ export default class PostInfo extends React.Component { createDropdown() { var post = this.props.post; - var isOwner = this.props.currentUser.id === post.user_id; - var isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser(); - const isSystemMessage = post.type && post.type.startsWith(Constants.SYSTEM_MESSAGE_PREFIX); + + this.canDelete = PostUtils.canDeletePost(post); + this.canEdit = PostUtils.canEditPost(post, this.editDisableAction); if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING) { return ''; @@ -139,7 +145,7 @@ export default class PostInfo extends React.Component { ); - if (isOwner || isAdmin) { + if (this.canDelete) { dropdownContents.push(
  • ); - if (isOwner || isAdmin) { + if (this.canDelete) { dropdownContents.push(
  • 0) { type = 'Comment'; @@ -189,7 +197,7 @@ export default class RhsRootPost extends React.Component {
  • ); - if (isOwner || isAdmin) { + if (this.canDelete) { dropdownContents.push(
  • Invite New Member to invite new users by email, or the Get Team Invite Link options from the Main Menu. If Get Team Invite Link is used to share a link, you can expire the invite code from Team Settings > Invite Code after the desired users join the team.", "admin.general.policy.teamInviteTitle": "Enable sending team invites from:", + "admin.general.policy.permissionsDeletePostAll": "Message authors can delete their own messages, and Administrators can delete any message", + "admin.general.policy.permissionsDeletePostAdmin": "Team Admins and System Admins", + "admin.general.policy.permissionsDeletePostSystemAdmin": "System Admins", + "admin.general.policy.restrictPostDeleteTitle": "Allow which users to delete messages:", + "admin.general.policy.restrictPostDeleteDescription": "Set policy on who has permission to delete messages.", + "admin.general.policy.allowEditPostTitle": "Allow users to edit their messages:", + "admin.general.policy.allowEditPostDescription": "Set policy on the length of time authors have to edit their messages after posting.", + "admin.general.policy.allowEditPostAlways": "Any time", + "admin.general.policy.allowEditPostNever": "Never", + "admin.general.policy.allowEditPostTimeLimit": "seconds after posting", "admin.general.privacy": "Privacy", "admin.general.usersAndTeams": "Users and Teams", "admin.gitab.clientSecretDescription": "Obtain this value via the instructions above for logging into GitLab.", diff --git a/webapp/stores/user_store.jsx b/webapp/stores/user_store.jsx index 2369c38df..fb1e36590 100644 --- a/webapp/stores/user_store.jsx +++ b/webapp/stores/user_store.jsx @@ -576,7 +576,7 @@ class UserStoreClass extends EventEmitter { var current = this.getCurrentUser(); if (current) { - return Utils.isAdmin(current.roles); + return Utils.isSystemAdmin(current.roles); } return false; diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx index c8ac74a82..6377f27f2 100644 --- a/webapp/utils/constants.jsx +++ b/webapp/utils/constants.jsx @@ -860,6 +860,13 @@ export const Constants = { PERMISSIONS_ALL: 'all', PERMISSIONS_TEAM_ADMIN: 'team_admin', PERMISSIONS_SYSTEM_ADMIN: 'system_admin', + PERMISSIONS_DELETE_POST_ALL: 'all', + PERMISSIONS_DELETE_POST_TEAM_ADMIN: 'team_admin', + PERMISSIONS_DELETE_POST_SYSTEM_ADMIN: 'system_admin', + ALLOW_EDIT_POST_ALWAYS: 'always', + ALLOW_EDIT_POST_NEVER: 'never', + ALLOW_EDIT_POST_TIME_LIMIT: 'time_limit', + DEFAULT_POST_EDIT_TIME_LIMIT: 300, MENTION_CHANNELS: 'mention.channels', MENTION_MORE_CHANNELS: 'mention.morechannels', MENTION_MEMBERS: 'mention.members', diff --git a/webapp/utils/post_utils.jsx b/webapp/utils/post_utils.jsx index 88021c2a5..20993b95c 100644 --- a/webapp/utils/post_utils.jsx +++ b/webapp/utils/post_utils.jsx @@ -3,11 +3,19 @@ import Client from 'client/web_client.jsx'; import Constants from 'utils/constants.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import TeamStore from 'stores/team_store.jsx'; +import UserStore from 'stores/user_store.jsx'; export function isSystemMessage(post) { return post.type && (post.type.lastIndexOf(Constants.SYSTEM_MESSAGE_PREFIX) === 0); } +export function isPostOwner(post) { + return UserStore.getCurrentId() === post.user_id; +} + export function isComment(post) { if ('root_id' in post) { return post.root_id !== '' && post.root_id != null; @@ -33,3 +41,36 @@ export function getProfilePicSrcForPost(post, timestamp) { return src; } + +export function canDeletePost(post) { + var isOwner = isPostOwner(post); + var isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser(); + var isSystemAdmin = UserStore.isSystemAdminForCurrentUser(); + + if (global.window.mm_license.IsLicensed === 'true') { + return (global.window.mm_config.RestrictPostDelete === Constants.PERMISSIONS_DELETE_POST_ALL && (isOwner || isAdmin)) || + (global.window.mm_config.RestrictPostDelete === Constants.PERMISSIONS_DELETE_POST_TEAM_ADMIN && isAdmin) || + (global.window.mm_config.RestrictPostDelete === Constants.PERMISSIONS_DELETE_POST_SYSTEM_ADMIN && isSystemAdmin); + } + return isOwner || isAdmin; +} + +export function canEditPost(post, editDisableAction) { + var isOwner = isPostOwner(post); + + var canEdit = isOwner && !isSystemMessage(post); + + if (canEdit && global.window.mm_license.IsLicensed === 'true') { + if (global.window.mm_config.AllowEditPost === Constants.ALLOW_EDIT_POST_NEVER) { + canEdit = false; + } else if (global.window.mm_config.AllowEditPost === Constants.ALLOW_EDIT_POST_TIME_LIMIT) { + var timeLeft = (post.create_at + (global.window.mm_config.PostEditTimeLimit * 1000)) - Utils.getTimestamp(); + if (timeLeft > 0) { + editDisableAction.fireAfter(timeLeft + 1000); + } else { + canEdit = false; + } + } + } + return canEdit; +} \ No newline at end of file -- cgit v1.2.3-1-g7c22