summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid Meza <dmeza@users.noreply.github.com>2017-09-01 08:53:55 -0500
committerJoram Wilander <jwawilander@gmail.com>2017-09-01 09:53:55 -0400
commit3c5280119357e3742811fd724601d0bef01bcb29 (patch)
tree70a63e6748a5fd15b001f2750992deb03b4b68cf
parentbaa992a5594c271504c4551177a3d69eab848913 (diff)
downloadchat-3c5280119357e3742811fd724601d0bef01bcb29.tar.gz
chat-3c5280119357e3742811fd724601d0bef01bcb29.tar.bz2
chat-3c5280119357e3742811fd724601d0bef01bcb29.zip
Config to make town square read only (#7140)
* Be able to make Town Square read-only (Disable typing messages for non admins). * Do not emit UserTypingEvent when TownSquareIsReadOnly and is Town Square. * Add unit tests for TownSquareIsReadOnly config value and logic. * Add TownSquareIsReadOnly to System console>Policy. Added Telemetry. * Add control for TownSquareIsReadOnly=true only for License Enterprise Edition E10 & E20. * Update en.json * Update en.json * Update policy_settings.jsx * Change config value from TownSquareIsReadOnly to ExperimentalTownSquareIsReadOnly. * Refactored to simplify. Avoid code repeat and multiple db calls.
-rw-r--r--api/context.go2
-rw-r--r--api/post_test.go39
-rw-r--r--api/webhook_test.go25
-rw-r--r--app/diagnostics.go1
-rw-r--r--app/post.go51
-rw-r--r--app/webhook.go5
-rw-r--r--config/default.json3
-rw-r--r--i18n/en.json4
-rw-r--r--model/config.go6
-rw-r--r--utils/config.go1
-rw-r--r--webapp/actions/global_actions.jsx7
11 files changed, 130 insertions, 14 deletions
diff --git a/api/context.go b/api/context.go
index fe3448ebd..9f09540e6 100644
--- a/api/context.go
+++ b/api/context.go
@@ -282,7 +282,7 @@ func (c *Context) LogError(err *model.AppError) {
// filter out endless reconnects
if c.Path == "/api/v3/users/websocket" && err.StatusCode == 401 || err.Id == "web.check_browser_compatibility.app_error" {
c.LogDebug(err)
- } else {
+ } else if err.Id != "api.post.create_post.town_square_read_only" {
l4g.Error(utils.TDefault("api.context.log.error"), c.Path, err.Where, err.StatusCode,
c.RequestId, c.Session.UserId, c.IpAddress, err.SystemMessage(utils.TDefault), err.DetailedError)
}
diff --git a/api/post_test.go b/api/post_test.go
index c31439c82..18bf22f5c 100644
--- a/api/post_test.go
+++ b/api/post_test.go
@@ -22,6 +22,12 @@ import (
)
func TestCreatePost(t *testing.T) {
+ adm := Setup().InitSystemAdmin()
+ AdminClient := adm.SystemAdminClient
+ adminTeam := adm.SystemAdminTeam
+ adminUser := adm.CreateUser(adm.SystemAdminClient)
+ LinkUserToTeam(adminUser, adminTeam)
+
th := Setup().InitBasic()
Client := th.BasicClient
team := th.BasicTeam
@@ -142,6 +148,39 @@ func TestCreatePost(t *testing.T) {
t.Fatal("should've attached all 3 files to post")
}
}
+
+ isLicensed := utils.IsLicensed()
+ license := utils.License()
+ disableTownSquareReadOnly := utils.Cfg.TeamSettings.ExperimentalTownSquareIsReadOnly
+ defer func() {
+ utils.Cfg.TeamSettings.ExperimentalTownSquareIsReadOnly = disableTownSquareReadOnly
+ utils.SetIsLicensed(isLicensed)
+ utils.SetLicense(license)
+ utils.SetDefaultRolesBasedOnConfig()
+ }()
+ *utils.Cfg.TeamSettings.ExperimentalTownSquareIsReadOnly = true
+ utils.SetDefaultRolesBasedOnConfig()
+ utils.SetIsLicensed(true)
+ utils.SetLicense(&model.License{Features: &model.Features{}})
+ utils.License().Features.SetDefaults()
+
+ defaultChannel := store.Must(app.Srv.Store.Channel().GetByName(team.Id, model.DEFAULT_CHANNEL, true)).(*model.Channel)
+ defaultPost := &model.Post{
+ ChannelId: defaultChannel.Id,
+ Message: "Default Channel Post",
+ }
+ if _, err = Client.CreatePost(defaultPost); err == nil {
+ t.Fatal("should have failed -- ExperimentalTownSquareIsReadOnly is true and it's a read only channel")
+ }
+
+ adminDefaultChannel := store.Must(app.Srv.Store.Channel().GetByName(adminTeam.Id, model.DEFAULT_CHANNEL, true)).(*model.Channel)
+ adminDefaultPost := &model.Post{
+ ChannelId: adminDefaultChannel.Id,
+ Message: "Admin Default Channel Post",
+ }
+ if _, err = AdminClient.CreatePost(adminDefaultPost); err != nil {
+ t.Fatal("should not have failed -- ExperimentalTownSquareIsReadOnly is true and admin can post to channel")
+ }
}
func TestCreatePostWithCreateAt(t *testing.T) {
diff --git a/api/webhook_test.go b/api/webhook_test.go
index 93d596bb1..c84aee992 100644
--- a/api/webhook_test.go
+++ b/api/webhook_test.go
@@ -956,7 +956,7 @@ func TestRegenOutgoingHookToken(t *testing.T) {
}
func TestIncomingWebhooks(t *testing.T) {
- th := Setup().InitSystemAdmin()
+ th := Setup().InitBasic().InitSystemAdmin()
Client := th.SystemAdminClient
team := th.SystemAdminTeam
channel1 := th.CreateChannel(Client, team)
@@ -1004,6 +1004,29 @@ func TestIncomingWebhooks(t *testing.T) {
t.Fatal(err)
}
+ if _, err := th.BasicClient.DoPost(url, fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", model.DEFAULT_CHANNEL), "application/json"); err != nil {
+ t.Fatal("should not have failed -- ExperimentalTownSquareIsReadOnly is false and it's not a read only channel")
+ }
+
+ isLicensed := utils.IsLicensed()
+ license := utils.License()
+ disableTownSquareReadOnly := utils.Cfg.TeamSettings.ExperimentalTownSquareIsReadOnly
+ defer func() {
+ utils.Cfg.TeamSettings.ExperimentalTownSquareIsReadOnly = disableTownSquareReadOnly
+ utils.SetIsLicensed(isLicensed)
+ utils.SetLicense(license)
+ utils.SetDefaultRolesBasedOnConfig()
+ }()
+ *utils.Cfg.TeamSettings.ExperimentalTownSquareIsReadOnly = true
+ utils.SetDefaultRolesBasedOnConfig()
+ utils.SetIsLicensed(true)
+ utils.SetLicense(&model.License{Features: &model.Features{}})
+ utils.License().Features.SetDefaults()
+
+ if _, err := th.BasicClient.DoPost(url, fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", model.DEFAULT_CHANNEL), "application/json"); err == nil {
+ t.Fatal("should have failed -- ExperimentalTownSquareIsReadOnly is true and it's a read only channel")
+ }
+
attachmentPayload := `{
"text": "this is a test",
"attachments": [
diff --git a/app/diagnostics.go b/app/diagnostics.go
index 84d11054b..f05d90bec 100644
--- a/app/diagnostics.go
+++ b/app/diagnostics.go
@@ -243,6 +243,7 @@ func trackConfig() {
"isdefault_custom_description_text": isDefault(*utils.Cfg.TeamSettings.CustomDescriptionText, model.TEAM_SETTINGS_DEFAULT_CUSTOM_DESCRIPTION_TEXT),
"isdefault_user_status_away_timeout": isDefault(*utils.Cfg.TeamSettings.UserStatusAwayTimeout, model.TEAM_SETTINGS_DEFAULT_USER_STATUS_AWAY_TIMEOUT),
"restrict_private_channel_manage_members": *utils.Cfg.TeamSettings.RestrictPrivateChannelManageMembers,
+ "experimental_town_square_is_read_only": *utils.Cfg.TeamSettings.ExperimentalTownSquareIsReadOnly,
})
SendDiagnostic(TRACK_CONFIG_CLIENT_REQ, map[string]interface{}{
diff --git a/app/post.go b/app/post.go
index c852a90d2..3845e1006 100644
--- a/app/post.go
+++ b/app/post.go
@@ -44,6 +44,29 @@ func CreatePostAsUser(post *model.Post) (*model.Post, *model.AppError) {
err.StatusCode = http.StatusBadRequest
}
+ if err.Id == "api.post.create_post.town_square_read_only" {
+ uchan := Srv.Store.User().Get(post.UserId)
+ var user *model.User
+ if result := <-uchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ user = result.Data.(*model.User)
+ }
+
+ T := utils.GetUserTranslations(user.Locale)
+ SendEphemeralPost(
+ post.UserId,
+ &model.Post{
+ ChannelId: channel.Id,
+ ParentId: post.ParentId,
+ RootId: post.RootId,
+ UserId: post.UserId,
+ Message: T("api.post.create_post.town_square_read_only"),
+ CreateAt: model.GetMillis() + 1,
+ },
+ )
+ }
+
return nil, err
} else {
// Update the LastViewAt only if the post does not have from_webhook prop set (eg. Zapier app)
@@ -82,6 +105,21 @@ func CreatePost(post *model.Post, channel *model.Channel, triggerWebhooks bool)
pchan = Srv.Store.Post().Get(post.RootId)
}
+ uchan := Srv.Store.User().Get(post.UserId)
+ var user *model.User
+ if result := <-uchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ user = result.Data.(*model.User)
+ }
+
+ if utils.IsLicensed() && *utils.Cfg.TeamSettings.ExperimentalTownSquareIsReadOnly &&
+ !post.IsSystemMessage() &&
+ channel.Name == model.DEFAULT_CHANNEL &&
+ !CheckIfRolesGrantPermission(user.GetRoles(), model.PERMISSION_MANAGE_SYSTEM.Id) {
+ return nil, model.NewLocAppError("createPost", "api.post.create_post.town_square_read_only", nil, "")
+ }
+
// Verify the parent/child relationships are correct
var parentPostList *model.PostList
if pchan != nil {
@@ -139,21 +177,19 @@ func CreatePost(post *model.Post, channel *model.Channel, triggerWebhooks bool)
}
}
- if err := handlePostEvents(rpost, channel, triggerWebhooks, parentPostList); err != nil {
+ if err := handlePostEvents(rpost, user, channel, triggerWebhooks, parentPostList); err != nil {
return nil, err
}
return rpost, nil
}
-func handlePostEvents(post *model.Post, channel *model.Channel, triggerWebhooks bool, parentPostList *model.PostList) *model.AppError {
+func handlePostEvents(post *model.Post, user *model.User, channel *model.Channel, triggerWebhooks bool, parentPostList *model.PostList) *model.AppError {
var tchan store.StoreChannel
if len(channel.TeamId) > 0 {
tchan = Srv.Store.Team().Get(channel.TeamId)
}
- uchan := Srv.Store.User().Get(post.UserId)
-
var team *model.Team
if tchan != nil {
if result := <-tchan; result.Err != nil {
@@ -169,13 +205,6 @@ func handlePostEvents(post *model.Post, channel *model.Channel, triggerWebhooks
InvalidateCacheForChannel(channel)
InvalidateCacheForChannelPosts(channel.Id)
- var user *model.User
- if result := <-uchan; result.Err != nil {
- return result.Err
- } else {
- user = result.Data.(*model.User)
- }
-
if _, err := SendNotifications(post, team, channel, user, parentPostList); err != nil {
return err
}
diff --git a/app/webhook.go b/app/webhook.go
index ce154ff70..cf4f156a2 100644
--- a/app/webhook.go
+++ b/app/webhook.go
@@ -520,6 +520,11 @@ func HandleIncomingWebhook(hookId string, req *model.IncomingWebhookRequest) *mo
}
}
+ if utils.IsLicensed() && *utils.Cfg.TeamSettings.ExperimentalTownSquareIsReadOnly &&
+ channel.Name == model.DEFAULT_CHANNEL {
+ return model.NewLocAppError("HandleIncomingWebhook", "api.post.create_post.town_square_read_only", nil, "")
+ }
+
if channel.Type != model.CHANNEL_OPEN && !HasPermissionToChannel(hook.UserId, channel.Id, model.PERMISSION_READ_CHANNEL) {
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.permissions.app_error", nil, "", http.StatusForbidden)
}
diff --git a/config/default.json b/config/default.json
index 1c772c4ff..1d08fd7cf 100644
--- a/config/default.json
+++ b/config/default.json
@@ -74,7 +74,8 @@
"UserStatusAwayTimeout": 300,
"MaxChannelsPerTeam": 2000,
"MaxNotificationsPerChannel": 1000,
- "TeammateNameDisplay": "username"
+ "TeammateNameDisplay": "username",
+ "ExperimentalTownSquareIsReadOnly": false
},
"ClientRequirements": {
"AndroidLatestVersion": "",
diff --git a/i18n/en.json b/i18n/en.json
index 794424aff..febcb9d9c 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -1630,6 +1630,10 @@
"translation": "Invalid RootId parameter"
},
{
+ "id": "api.post.create_post.town_square_read_only",
+ "translation": "This channel is read-only. Only members with permission can post here."
+ },
+ {
"id": "api.post.create_webhook_post.creating.app_error",
"translation": "Error creating post"
},
diff --git a/model/config.go b/model/config.go
index 65608c9a5..58b3da4d1 100644
--- a/model/config.go
+++ b/model/config.go
@@ -354,6 +354,7 @@ type TeamSettings struct {
MaxChannelsPerTeam *int64
MaxNotificationsPerChannel *int64
TeammateNameDisplay *string
+ ExperimentalTownSquareIsReadOnly *bool
}
type ClientRequirements struct {
@@ -824,6 +825,11 @@ func (o *Config) SetDefaults() {
*o.TeamSettings.MaxNotificationsPerChannel = 1000
}
+ if o.TeamSettings.ExperimentalTownSquareIsReadOnly == nil {
+ o.TeamSettings.ExperimentalTownSquareIsReadOnly = new(bool)
+ *o.TeamSettings.ExperimentalTownSquareIsReadOnly = false
+ }
+
if o.EmailSettings.EnableSignInWithEmail == nil {
o.EmailSettings.EnableSignInWithEmail = new(bool)
diff --git a/utils/config.go b/utils/config.go
index 7183ef92b..642abfdf0 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -518,6 +518,7 @@ func getClientConfig(c *model.Config) map[string]string {
if IsLicensed() {
License := License()
+ props["ExperimentalTownSquareIsReadOnly"] = strconv.FormatBool(*c.TeamSettings.ExperimentalTownSquareIsReadOnly)
if *License.Features.CustomBrand {
props["EnableCustomBrand"] = strconv.FormatBool(*c.TeamSettings.EnableCustomBrand)
diff --git a/webapp/actions/global_actions.jsx b/webapp/actions/global_actions.jsx
index a67d1b751..025e56f7d 100644
--- a/webapp/actions/global_actions.jsx
+++ b/webapp/actions/global_actions.jsx
@@ -443,6 +443,13 @@ export function emitLocalUserTypingEvent(channelId, parentId) {
const t = Date.now();
const membersInChannel = ChannelStore.getStats(channelId).member_count;
+ if (global.mm_license.IsLicensed === 'true' && global.mm_config.ExperimentalTownSquareIsReadOnly === 'true') {
+ const channel = ChannelStore.getChannelById(channelId);
+ if (channel && ChannelStore.isDefault(channel)) {
+ return;
+ }
+ }
+
if (((t - lastTimeTypingSent) > global.window.mm_config.TimeBetweenUserTypingUpdatesMilliseconds) && membersInChannel < global.window.mm_config.MaxNotificationsPerChannel && global.window.mm_config.EnableUserTypingMessages === 'true') {
WebSocketClient.userTyping(channelId, parentId);
lastTimeTypingSent = t;