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.
Этот коммит содержится в:
коммит произвёл
Joram Wilander
родитель
baa992a559
Коммит
3c52801193
@@ -282,7 +282,7 @@ func (c *Context) LogError(err *model.AppError) {
|
|||||||
// filter out endless reconnects
|
// filter out endless reconnects
|
||||||
if c.Path == "/api/v3/users/websocket" && err.StatusCode == 401 || err.Id == "web.check_browser_compatibility.app_error" {
|
if c.Path == "/api/v3/users/websocket" && err.StatusCode == 401 || err.Id == "web.check_browser_compatibility.app_error" {
|
||||||
c.LogDebug(err)
|
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,
|
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)
|
c.RequestId, c.Session.UserId, c.IpAddress, err.SystemMessage(utils.TDefault), err.DetailedError)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestCreatePost(t *testing.T) {
|
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()
|
th := Setup().InitBasic()
|
||||||
Client := th.BasicClient
|
Client := th.BasicClient
|
||||||
team := th.BasicTeam
|
team := th.BasicTeam
|
||||||
@@ -142,6 +148,39 @@ func TestCreatePost(t *testing.T) {
|
|||||||
t.Fatal("should've attached all 3 files to post")
|
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) {
|
func TestCreatePostWithCreateAt(t *testing.T) {
|
||||||
|
|||||||
@@ -956,7 +956,7 @@ func TestRegenOutgoingHookToken(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestIncomingWebhooks(t *testing.T) {
|
func TestIncomingWebhooks(t *testing.T) {
|
||||||
th := Setup().InitSystemAdmin()
|
th := Setup().InitBasic().InitSystemAdmin()
|
||||||
Client := th.SystemAdminClient
|
Client := th.SystemAdminClient
|
||||||
team := th.SystemAdminTeam
|
team := th.SystemAdminTeam
|
||||||
channel1 := th.CreateChannel(Client, team)
|
channel1 := th.CreateChannel(Client, team)
|
||||||
@@ -1004,6 +1004,29 @@ func TestIncomingWebhooks(t *testing.T) {
|
|||||||
t.Fatal(err)
|
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 := `{
|
attachmentPayload := `{
|
||||||
"text": "this is a test",
|
"text": "this is a test",
|
||||||
"attachments": [
|
"attachments": [
|
||||||
|
|||||||
@@ -243,6 +243,7 @@ func trackConfig() {
|
|||||||
"isdefault_custom_description_text": isDefault(*utils.Cfg.TeamSettings.CustomDescriptionText, model.TEAM_SETTINGS_DEFAULT_CUSTOM_DESCRIPTION_TEXT),
|
"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),
|
"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,
|
"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{}{
|
SendDiagnostic(TRACK_CONFIG_CLIENT_REQ, map[string]interface{}{
|
||||||
|
|||||||
51
app/post.go
51
app/post.go
@@ -44,6 +44,29 @@ func CreatePostAsUser(post *model.Post) (*model.Post, *model.AppError) {
|
|||||||
err.StatusCode = http.StatusBadRequest
|
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
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
// Update the LastViewAt only if the post does not have from_webhook prop set (eg. Zapier app)
|
// 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)
|
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
|
// Verify the parent/child relationships are correct
|
||||||
var parentPostList *model.PostList
|
var parentPostList *model.PostList
|
||||||
if pchan != nil {
|
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 nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return rpost, nil
|
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
|
var tchan store.StoreChannel
|
||||||
if len(channel.TeamId) > 0 {
|
if len(channel.TeamId) > 0 {
|
||||||
tchan = Srv.Store.Team().Get(channel.TeamId)
|
tchan = Srv.Store.Team().Get(channel.TeamId)
|
||||||
}
|
}
|
||||||
|
|
||||||
uchan := Srv.Store.User().Get(post.UserId)
|
|
||||||
|
|
||||||
var team *model.Team
|
var team *model.Team
|
||||||
if tchan != nil {
|
if tchan != nil {
|
||||||
if result := <-tchan; result.Err != nil {
|
if result := <-tchan; result.Err != nil {
|
||||||
@@ -169,13 +205,6 @@ func handlePostEvents(post *model.Post, channel *model.Channel, triggerWebhooks
|
|||||||
InvalidateCacheForChannel(channel)
|
InvalidateCacheForChannel(channel)
|
||||||
InvalidateCacheForChannelPosts(channel.Id)
|
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 {
|
if _, err := SendNotifications(post, team, channel, user, parentPostList); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
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)
|
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.permissions.app_error", nil, "", http.StatusForbidden)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,8 @@
|
|||||||
"UserStatusAwayTimeout": 300,
|
"UserStatusAwayTimeout": 300,
|
||||||
"MaxChannelsPerTeam": 2000,
|
"MaxChannelsPerTeam": 2000,
|
||||||
"MaxNotificationsPerChannel": 1000,
|
"MaxNotificationsPerChannel": 1000,
|
||||||
"TeammateNameDisplay": "username"
|
"TeammateNameDisplay": "username",
|
||||||
|
"ExperimentalTownSquareIsReadOnly": false
|
||||||
},
|
},
|
||||||
"ClientRequirements": {
|
"ClientRequirements": {
|
||||||
"AndroidLatestVersion": "",
|
"AndroidLatestVersion": "",
|
||||||
|
|||||||
@@ -1629,6 +1629,10 @@
|
|||||||
"id": "api.post.create_post.root_id.app_error",
|
"id": "api.post.create_post.root_id.app_error",
|
||||||
"translation": "Invalid RootId parameter"
|
"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",
|
"id": "api.post.create_webhook_post.creating.app_error",
|
||||||
"translation": "Error creating post"
|
"translation": "Error creating post"
|
||||||
|
|||||||
@@ -354,6 +354,7 @@ type TeamSettings struct {
|
|||||||
MaxChannelsPerTeam *int64
|
MaxChannelsPerTeam *int64
|
||||||
MaxNotificationsPerChannel *int64
|
MaxNotificationsPerChannel *int64
|
||||||
TeammateNameDisplay *string
|
TeammateNameDisplay *string
|
||||||
|
ExperimentalTownSquareIsReadOnly *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientRequirements struct {
|
type ClientRequirements struct {
|
||||||
@@ -824,6 +825,11 @@ func (o *Config) SetDefaults() {
|
|||||||
*o.TeamSettings.MaxNotificationsPerChannel = 1000
|
*o.TeamSettings.MaxNotificationsPerChannel = 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if o.TeamSettings.ExperimentalTownSquareIsReadOnly == nil {
|
||||||
|
o.TeamSettings.ExperimentalTownSquareIsReadOnly = new(bool)
|
||||||
|
*o.TeamSettings.ExperimentalTownSquareIsReadOnly = false
|
||||||
|
}
|
||||||
|
|
||||||
if o.EmailSettings.EnableSignInWithEmail == nil {
|
if o.EmailSettings.EnableSignInWithEmail == nil {
|
||||||
o.EmailSettings.EnableSignInWithEmail = new(bool)
|
o.EmailSettings.EnableSignInWithEmail = new(bool)
|
||||||
|
|
||||||
|
|||||||
@@ -518,6 +518,7 @@ func getClientConfig(c *model.Config) map[string]string {
|
|||||||
if IsLicensed() {
|
if IsLicensed() {
|
||||||
|
|
||||||
License := License()
|
License := License()
|
||||||
|
props["ExperimentalTownSquareIsReadOnly"] = strconv.FormatBool(*c.TeamSettings.ExperimentalTownSquareIsReadOnly)
|
||||||
|
|
||||||
if *License.Features.CustomBrand {
|
if *License.Features.CustomBrand {
|
||||||
props["EnableCustomBrand"] = strconv.FormatBool(*c.TeamSettings.EnableCustomBrand)
|
props["EnableCustomBrand"] = strconv.FormatBool(*c.TeamSettings.EnableCustomBrand)
|
||||||
|
|||||||
@@ -443,6 +443,13 @@ export function emitLocalUserTypingEvent(channelId, parentId) {
|
|||||||
const t = Date.now();
|
const t = Date.now();
|
||||||
const membersInChannel = ChannelStore.getStats(channelId).member_count;
|
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') {
|
if (((t - lastTimeTypingSent) > global.window.mm_config.TimeBetweenUserTypingUpdatesMilliseconds) && membersInChannel < global.window.mm_config.MaxNotificationsPerChannel && global.window.mm_config.EnableUserTypingMessages === 'true') {
|
||||||
WebSocketClient.userTyping(channelId, parentId);
|
WebSocketClient.userTyping(channelId, parentId);
|
||||||
lastTimeTypingSent = t;
|
lastTimeTypingSent = t;
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user