diff options
author | Joram Wilander <jwawilander@gmail.com> | 2016-07-06 08:23:24 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-07-06 08:23:24 -0400 |
commit | 5f7cb8cfbf879aa0b0d43a7b7068688368fda9fc (patch) | |
tree | 555f578f1346d0b18448176ce1dc2a4771f16c6a | |
parent | 19d452c74efb96f718079d5a268ca51a8983c4bd (diff) | |
download | chat-5f7cb8cfbf879aa0b0d43a7b7068688368fda9fc.tar.gz chat-5f7cb8cfbf879aa0b0d43a7b7068688368fda9fc.tar.bz2 chat-5f7cb8cfbf879aa0b0d43a7b7068688368fda9fc.zip |
PLT-3346/PLT-3342/PLT-3360 EE: Add the ability to restrict channel management permissions (#3453)
* EE: Add the ability to restrict channel management permissions
* Always allow last user in a channel to delete that channel
-rw-r--r-- | api/channel.go | 73 | ||||
-rw-r--r-- | api/channel_test.go | 412 | ||||
-rw-r--r-- | api/team.go | 4 | ||||
-rw-r--r-- | api/team_test.go | 4 | ||||
-rw-r--r-- | config/config.json | 4 | ||||
-rw-r--r-- | i18n/en.json | 16 | ||||
-rw-r--r-- | model/config.go | 42 | ||||
-rw-r--r-- | utils/config.go | 2 | ||||
-rw-r--r-- | webapp/components/admin_console/policy_settings.jsx | 56 | ||||
-rw-r--r-- | webapp/components/channel_header.jsx | 157 | ||||
-rw-r--r-- | webapp/components/more_channels.jsx | 56 | ||||
-rw-r--r-- | webapp/components/navbar_dropdown.jsx | 4 | ||||
-rw-r--r-- | webapp/components/new_channel_modal.jsx | 65 | ||||
-rw-r--r-- | webapp/components/sidebar.jsx | 77 | ||||
-rw-r--r-- | webapp/components/sidebar_right_menu.jsx | 4 | ||||
-rw-r--r-- | webapp/components/tutorial/tutorial_intro_screens.jsx | 2 | ||||
-rw-r--r-- | webapp/i18n/en.json | 12 | ||||
-rw-r--r-- | webapp/stores/channel_store.jsx | 43 | ||||
-rw-r--r-- | webapp/utils/channel_intro_messages.jsx | 4 | ||||
-rw-r--r-- | webapp/utils/constants.jsx | 6 |
20 files changed, 867 insertions, 176 deletions
diff --git a/api/channel.go b/api/channel.go index b4a5b78dc..038a4286a 100644 --- a/api/channel.go +++ b/api/channel.go @@ -59,6 +59,11 @@ func createChannel(c *Context, w http.ResponseWriter, r *http.Request) { channel.TeamId = c.TeamId } + if err := CanManageChannel(c, channel); err != nil { + c.Err = err + return + } + if !c.HasPermissionsToTeam(channel.TeamId, "createChannel") { return } @@ -178,6 +183,32 @@ func CreateDefaultChannels(c *Context, teamId string) ([]*model.Channel, *model. return channels, nil } +func CanManageChannel(c *Context, channel *model.Channel) *model.AppError { + if utils.IsLicensed { + if channel.Type == model.CHANNEL_OPEN { + if *utils.Cfg.TeamSettings.RestrictPublicChannelManagement == model.PERMISSIONS_SYSTEM_ADMIN && !c.IsSystemAdmin() { + return model.NewLocAppError("CanManageChannel", "api.channel.can_manage_channel.public_restricted_system_admin.app_error", nil, "") + } + + if *utils.Cfg.TeamSettings.RestrictPublicChannelManagement == model.PERMISSIONS_TEAM_ADMIN && !c.IsTeamAdmin() { + return model.NewLocAppError("CanManageChannel", "api.channel.can_manage_channel.public_restricted_team_admin.app_error", nil, "") + } + } + + if channel.Type == model.CHANNEL_PRIVATE { + if *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement == model.PERMISSIONS_SYSTEM_ADMIN && !c.IsSystemAdmin() { + return model.NewLocAppError("CanManageChannel", "api.channel.can_manage_channel.private_restricted_system_admin.app_error", nil, "") + } + + if *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement == model.PERMISSIONS_TEAM_ADMIN && !c.IsTeamAdmin() { + return model.NewLocAppError("CanManageChannel", "api.channel.can_manage_channel.private_restricted_team_admin.app_error", nil, "") + } + } + } + + return nil +} + func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) { channel := model.ChannelFromJson(r.Body) @@ -198,15 +229,14 @@ func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) { return } else { oldChannel := cresult.Data.(*model.Channel) - channelMember := cmcresult.Data.(model.ChannelMember) + // Don't need to do anything with channel member, just wanted to confirm it exists - if !c.HasPermissionsToTeam(oldChannel.TeamId, "updateChannel") { + if err := CanManageChannel(c, oldChannel); err != nil { + c.Err = err return } - if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !c.IsTeamAdmin() { - c.Err = model.NewLocAppError("updateChannel", "api.channel.update_channel.permission.app_error", nil, "") - c.Err.StatusCode = http.StatusForbidden + if !c.HasPermissionsToTeam(oldChannel.TeamId, "updateChannel") { return } @@ -275,7 +305,12 @@ func updateChannelHeader(c *Context, w http.ResponseWriter, r *http.Request) { return } else { channel := cresult.Data.(*model.Channel) - // Don't need to do anything channel member, just wanted to confirm it exists + // Don't need to do anything with channel member, just wanted to confirm it exists + + if err := CanManageChannel(c, channel); err != nil { + c.Err = err + return + } if channel.TeamId != "" && !c.HasPermissionsToTeam(channel.TeamId, "updateChannelHeader") { return @@ -348,7 +383,12 @@ func updateChannelPurpose(c *Context, w http.ResponseWriter, r *http.Request) { return } else { channel := cresult.Data.(*model.Channel) - // Don't need to do anything channel member, just wanted to confirm it exists + // Don't need to do anything with channel member, just wanted to confirm it exists + + if err := CanManageChannel(c, channel); err != nil { + c.Err = err + return + } if !c.HasPermissionsToTeam(channel.TeamId, "updateChannelPurpose") { return @@ -646,6 +686,7 @@ func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) { sc := Srv.Store.Channel().Get(id) scm := Srv.Store.Channel().GetMember(id, c.Session.UserId) + cmc := Srv.Store.Channel().GetMemberCount(id) uc := Srv.Store.User().Get(c.Session.UserId) ihc := Srv.Store.Webhook().GetIncomingByChannel(id) ohc := Srv.Store.Webhook().GetOutgoingByChannel(id) @@ -659,6 +700,9 @@ func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) { } else if scmresult := <-scm; scmresult.Err != nil { c.Err = scmresult.Err return + } else if cmcresult := <-cmc; cmcresult.Err != nil { + c.Err = cmcresult.Err + return } else if ihcresult := <-ihc; ihcresult.Err != nil { c.Err = ihcresult.Err return @@ -667,18 +711,21 @@ func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) { return } else { channel := cresult.Data.(*model.Channel) + memberCount := cmcresult.Data.(int64) user := uresult.Data.(*model.User) - channelMember := scmresult.Data.(model.ChannelMember) incomingHooks := ihcresult.Data.([]*model.IncomingWebhook) outgoingHooks := ohcresult.Data.([]*model.OutgoingWebhook) + // Don't need to do anything with channel member, just wanted to confirm it exists - if !c.HasPermissionsToTeam(channel.TeamId, "deleteChannel") { - return + // Allow delete if user is the only member left in channel + if memberCount > 1 { + if err := CanManageChannel(c, channel); err != nil { + c.Err = err + return + } } - if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !c.IsTeamAdmin() { - c.Err = model.NewLocAppError("deleteChannel", "api.channel.delete_channel.permissions.app_error", nil, "") - c.Err.StatusCode = http.StatusForbidden + if !c.HasPermissionsToTeam(channel.TeamId, "deleteChannel") { return } diff --git a/api/channel_test.go b/api/channel_test.go index 7480dea23..ac47d4eed 100644 --- a/api/channel_test.go +++ b/api/channel_test.go @@ -14,8 +14,9 @@ import ( ) func TestCreateChannel(t *testing.T) { - th := Setup().InitBasic() + th := Setup().InitBasic().InitSystemAdmin() Client := th.BasicClient + SystemAdminClient := th.SystemAdminClient team := th.BasicTeam Client.Must(Client.Logout()) team2 := th.CreateTeam(th.BasicClient) @@ -74,6 +75,12 @@ func TestCreateChannel(t *testing.T) { t.Fatal(err) } + channel = model.Channel{DisplayName: "Channel With No TeamId", Name: "aaaa" + model.NewId() + "abbb", Type: model.CHANNEL_OPEN, TeamId: ""} + + if _, err := Client.CreateChannel(&channel); err != nil { + t.Fatal(err) + } + channel = model.Channel{DisplayName: "Test API Name", Name: model.NewId() + "__" + model.NewId(), Type: model.CHANNEL_OPEN, TeamId: team.Id} if _, err := Client.CreateChannel(&channel); err == nil { @@ -85,6 +92,72 @@ func TestCreateChannel(t *testing.T) { if _, err := Client.CreateChannel(&channel); err == nil { t.Fatal("Should have errored out on direct channel type") } + + isLicensed := utils.IsLicensed + restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement + restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement + defer func() { + *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel + *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel + utils.IsLicensed = isLicensed + }() + *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL + *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL + utils.IsLicensed = true + + channel2 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel3 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id} + if _, err := Client.CreateChannel(channel2); err != nil { + t.Fatal(err) + } + if _, err := Client.CreateChannel(channel3); err != nil { + t.Fatal(err) + } + + *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_TEAM_ADMIN + *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_TEAM_ADMIN + + channel2.Name = "a" + model.NewId() + "a" + channel3.Name = "a" + model.NewId() + "a" + if _, err := Client.CreateChannel(channel2); err == nil { + t.Fatal("should have errored not team admin") + } + if _, err := Client.CreateChannel(channel3); err == nil { + t.Fatal("should have errored not team admin") + } + + UpdateUserToTeamAdmin(th.BasicUser, team) + Client.Logout() + th.LoginBasic() + Client.SetTeamId(team.Id) + + if _, err := Client.CreateChannel(channel2); err != nil { + t.Fatal(err) + } + if _, err := Client.CreateChannel(channel3); err != nil { + t.Fatal(err) + } + + *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_SYSTEM_ADMIN + *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_SYSTEM_ADMIN + + channel2.Name = "a" + model.NewId() + "a" + channel3.Name = "a" + model.NewId() + "a" + if _, err := Client.CreateChannel(channel2); err == nil { + t.Fatal("should have errored not system admin") + } + if _, err := Client.CreateChannel(channel3); err == nil { + t.Fatal("should have errored not system admin") + } + + LinkUserToTeam(th.SystemAdminUser, team) + + if _, err := SystemAdminClient.CreateChannel(channel2); err != nil { + t.Fatal(err) + } + if _, err := SystemAdminClient.CreateChannel(channel3); err != nil { + t.Fatal(err) + } } func TestCreateDirectChannel(t *testing.T) { @@ -129,7 +202,7 @@ func TestCreateDirectChannel(t *testing.T) { } func TestUpdateChannel(t *testing.T) { - th := Setup().InitSystemAdmin() + th := Setup().InitBasic().InitSystemAdmin() Client := th.SystemAdminClient team := th.SystemAdminTeam sysAdminUser := th.SystemAdminUser @@ -193,6 +266,7 @@ func TestUpdateChannel(t *testing.T) { } Client.Login(sysAdminUser.Email, sysAdminUser.Password) + LinkUserToTeam(sysAdminUser, team) Client.Must(Client.JoinChannel(channel1.Id)) if _, err := Client.UpdateChannel(upChannel1); err != nil { @@ -204,11 +278,82 @@ func TestUpdateChannel(t *testing.T) { if _, err := Client.UpdateChannel(upChannel1); err == nil { t.Fatal("should have failed - channel deleted") } + + isLicensed := utils.IsLicensed + restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement + restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement + defer func() { + *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel + *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel + utils.IsLicensed = isLicensed + }() + *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL + *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL + utils.IsLicensed = true + + channel2 := th.CreateChannel(Client, team) + channel3 := th.CreatePrivateChannel(Client, team) + + LinkUserToTeam(th.BasicUser, team) + + Client.Must(Client.AddChannelMember(channel2.Id, th.BasicUser.Id)) + Client.Must(Client.AddChannelMember(channel3.Id, th.BasicUser.Id)) + + Client.Login(th.BasicUser.Email, th.BasicUser.Password) + + if _, err := Client.UpdateChannel(channel2); err != nil { + t.Fatal(err) + } + if _, err := Client.UpdateChannel(channel3); err != nil { + t.Fatal(err) + } + + *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_TEAM_ADMIN + *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_TEAM_ADMIN + + if _, err := Client.UpdateChannel(channel2); err == nil { + t.Fatal("should have errored not team admin") + } + if _, err := Client.UpdateChannel(channel3); err == nil { + t.Fatal("should have errored not team admin") + } + + UpdateUserToTeamAdmin(th.BasicUser, team) + Client.Logout() + Client.Login(th.BasicUser.Email, th.BasicUser.Password) + Client.SetTeamId(team.Id) + + if _, err := Client.UpdateChannel(channel2); err != nil { + t.Fatal(err) + } + if _, err := Client.UpdateChannel(channel3); err != nil { + t.Fatal(err) + } + + *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_SYSTEM_ADMIN + *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_SYSTEM_ADMIN + + if _, err := Client.UpdateChannel(channel2); err == nil { + t.Fatal("should have errored not system admin") + } + if _, err := Client.UpdateChannel(channel3); err == nil { + t.Fatal("should have errored not system admin") + } + + th.LoginSystemAdmin() + + if _, err := Client.UpdateChannel(channel2); err != nil { + t.Fatal(err) + } + if _, err := Client.UpdateChannel(channel3); err != nil { + t.Fatal(err) + } } func TestUpdateChannelHeader(t *testing.T) { - th := Setup().InitBasic() + th := Setup().InitBasic().InitSystemAdmin() Client := th.BasicClient + SystemAdminClient := th.SystemAdminClient team := th.BasicTeam channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} @@ -266,11 +411,89 @@ func TestUpdateChannelHeader(t *testing.T) { if _, err := Client.UpdateChannelHeader(data); err == nil { t.Fatal("should have errored non-channel member trying to update header") } + + isLicensed := utils.IsLicensed + restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement + restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement + defer func() { + *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel + *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel + utils.IsLicensed = isLicensed + }() + *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL + *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL + utils.IsLicensed = true + + th.LoginBasic() + channel2 := th.CreateChannel(Client, team) + channel3 := th.CreatePrivateChannel(Client, team) + + data2 := make(map[string]string) + data2["channel_id"] = channel2.Id + data2["channel_header"] = "new header" + + data3 := make(map[string]string) + data3["channel_id"] = channel3.Id + data3["channel_header"] = "new header" + + Client.Login(th.BasicUser.Email, th.BasicUser.Password) + + if _, err := Client.UpdateChannelHeader(data2); err != nil { + t.Fatal(err) + } + if _, err := Client.UpdateChannelHeader(data3); err != nil { + t.Fatal(err) + } + + *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_TEAM_ADMIN + *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_TEAM_ADMIN + + if _, err := Client.UpdateChannelHeader(data2); err == nil { + t.Fatal("should have errored not team admin") + } + if _, err := Client.UpdateChannelHeader(data3); err == nil { + t.Fatal("should have errored not team admin") + } + + UpdateUserToTeamAdmin(th.BasicUser, team) + Client.Logout() + th.LoginBasic() + Client.SetTeamId(team.Id) + + if _, err := Client.UpdateChannelHeader(data2); err != nil { + t.Fatal(err) + } + if _, err := Client.UpdateChannelHeader(data3); err != nil { + t.Fatal(err) + } + + *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_SYSTEM_ADMIN + *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_SYSTEM_ADMIN + + if _, err := Client.UpdateChannelHeader(data2); err == nil { + t.Fatal("should have errored not system admin") + } + if _, err := Client.UpdateChannelHeader(data3); err == nil { + t.Fatal("should have errored not system admin") + } + + LinkUserToTeam(th.SystemAdminUser, team) + Client.Must(Client.AddChannelMember(channel2.Id, th.SystemAdminUser.Id)) + Client.Must(Client.AddChannelMember(channel3.Id, th.SystemAdminUser.Id)) + th.LoginSystemAdmin() + + if _, err := SystemAdminClient.UpdateChannelHeader(data2); err != nil { + t.Fatal(err) + } + if _, err := SystemAdminClient.UpdateChannelHeader(data3); err != nil { + t.Fatal(err) + } } func TestUpdateChannelPurpose(t *testing.T) { - th := Setup().InitBasic() + th := Setup().InitBasic().InitSystemAdmin() Client := th.BasicClient + SystemAdminClient := th.SystemAdminClient team := th.BasicTeam channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} @@ -314,6 +537,83 @@ func TestUpdateChannelPurpose(t *testing.T) { if _, err := Client.UpdateChannelPurpose(data); err == nil { t.Fatal("should have errored non-channel member trying to update purpose") } + + isLicensed := utils.IsLicensed + restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement + restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement + defer func() { + *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel + *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel + utils.IsLicensed = isLicensed + }() + *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL + *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL + utils.IsLicensed = true + + th.LoginBasic() + channel2 := th.CreateChannel(Client, team) + channel3 := th.CreatePrivateChannel(Client, team) + + data2 := make(map[string]string) + data2["channel_id"] = channel2.Id + data2["channel_purpose"] = "new purpose" + + data3 := make(map[string]string) + data3["channel_id"] = channel3.Id + data3["channel_purpose"] = "new purpose" + + Client.Login(th.BasicUser.Email, th.BasicUser.Password) + + if _, err := Client.UpdateChannelPurpose(data2); err != nil { + t.Fatal(err) + } + if _, err := Client.UpdateChannelPurpose(data3); err != nil { + t.Fatal(err) + } + + *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_TEAM_ADMIN + *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_TEAM_ADMIN + + if _, err := Client.UpdateChannelPurpose(data2); err == nil { + t.Fatal("should have errored not team admin") + } + if _, err := Client.UpdateChannelPurpose(data3); err == nil { + t.Fatal("should have errored not team admin") + } + + UpdateUserToTeamAdmin(th.BasicUser, team) + Client.Logout() + th.LoginBasic() + Client.SetTeamId(team.Id) + + if _, err := Client.UpdateChannelPurpose(data2); err != nil { + t.Fatal(err) + } + if _, err := Client.UpdateChannelPurpose(data3); err != nil { + t.Fatal(err) + } + + *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_SYSTEM_ADMIN + *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_SYSTEM_ADMIN + + if _, err := Client.UpdateChannelPurpose(data2); err == nil { + t.Fatal("should have errored not system admin") + } + if _, err := Client.UpdateChannelPurpose(data3); err == nil { + t.Fatal("should have errored not system admin") + } + + LinkUserToTeam(th.SystemAdminUser, team) + Client.Must(Client.AddChannelMember(channel2.Id, th.SystemAdminUser.Id)) + Client.Must(Client.AddChannelMember(channel3.Id, th.SystemAdminUser.Id)) + th.LoginSystemAdmin() + + if _, err := SystemAdminClient.UpdateChannelPurpose(data2); err != nil { + t.Fatal(err) + } + if _, err := SystemAdminClient.UpdateChannelPurpose(data3); err != nil { + t.Fatal(err) + } } func TestGetChannel(t *testing.T) { @@ -572,7 +872,7 @@ func TestLeaveChannel(t *testing.T) { } func TestDeleteChannel(t *testing.T) { - th := Setup().InitSystemAdmin() + th := Setup().InitBasic().InitSystemAdmin() Client := th.SystemAdminClient team := th.SystemAdminTeam userSystemAdmin := th.SystemAdminUser @@ -619,8 +919,8 @@ func TestDeleteChannel(t *testing.T) { Client.Must(Client.JoinChannel(channel2.Id)) - if _, err := Client.DeleteChannel(channel2.Id); err == nil { - t.Fatal("should have failed to delete channel you're not an admin of") + if _, err := Client.DeleteChannel(channel2.Id); err != nil { + t.Fatal(err) } rget := Client.Must(Client.GetChannels("")) @@ -640,6 +940,8 @@ func TestDeleteChannel(t *testing.T) { Client.Login(userStd.Email, userStd.Password) Client.SetTeamId(team.Id) + channel2 = th.CreateChannel(Client, team) + if _, err := Client.DeleteChannel(channel2.Id); err != nil { t.Fatal(err) } @@ -657,6 +959,102 @@ func TestDeleteChannel(t *testing.T) { if _, err := Client.DeleteChannel(channel3.Id); err == nil { t.Fatal("should have failed - channel already deleted") } + + isLicensed := utils.IsLicensed + restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement + restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement + defer func() { + *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel + *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel + utils.IsLicensed = isLicensed + }() + *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL + *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL + utils.IsLicensed = true + + th.LoginSystemAdmin() + LinkUserToTeam(th.BasicUser, team) + + channel2 = th.CreateChannel(Client, team) + channel3 = th.CreatePrivateChannel(Client, team) + channel4 := th.CreateChannel(Client, team) + Client.Must(Client.AddChannelMember(channel2.Id, th.BasicUser.Id)) + Client.Must(Client.AddChannelMember(channel3.Id, th.BasicUser.Id)) + Client.Must(Client.AddChannelMember(channel4.Id, th.BasicUser.Id)) + Client.Must(Client.LeaveChannel(channel4.Id)) + + Client.Login(th.BasicUser.Email, th.BasicUser.Password) + + if _, err := Client.DeleteChannel(channel2.Id); err != nil { + t.Fatal(err) + } + if _, err := Client.DeleteChannel(channel3.Id); err != nil { + t.Fatal(err) + } + + *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_TEAM_ADMIN + *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_TEAM_ADMIN + + th.LoginSystemAdmin() + + channel2 = th.CreateChannel(Client, team) + channel3 = th.CreatePrivateChannel(Client, team) + Client.Must(Client.AddChannelMember(channel2.Id, th.BasicUser.Id)) + Client.Must(Client.AddChannelMember(channel3.Id, th.BasicUser.Id)) + + Client.Login(th.BasicUser.Email, th.BasicUser.Password) + + if _, err := Client.DeleteChannel(channel2.Id); err == nil { + t.Fatal("should have errored not team admin") + } + if _, err := Client.DeleteChannel(channel3.Id); err == nil { + t.Fatal("should have errored not team admin") + } + + UpdateUserToTeamAdmin(th.BasicUser, team) + Client.Logout() + Client.Login(th.BasicUser.Email, th.BasicUser.Password) + Client.SetTeamId(team.Id) + + if _, err := Client.DeleteChannel(channel2.Id); err != nil { + t.Fatal(err) + } + if _, err := Client.DeleteChannel(channel3.Id); err != nil { + t.Fatal(err) + } + + *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_SYSTEM_ADMIN + *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_SYSTEM_ADMIN + + th.LoginSystemAdmin() + + channel2 = th.CreateChannel(Client, team) + channel3 = th.CreatePrivateChannel(Client, team) + Client.Must(Client.AddChannelMember(channel2.Id, th.BasicUser.Id)) + Client.Must(Client.AddChannelMember(channel3.Id, th.BasicUser.Id)) + + Client.Login(th.BasicUser.Email, th.BasicUser.Password) + + if _, err := Client.DeleteChannel(channel2.Id); err == nil { + t.Fatal("should have errored not system admin") + } + if _, err := Client.DeleteChannel(channel3.Id); err == nil { + t.Fatal("should have errored not system admin") + } + + // Only one left in channel, should be able to delete + if _, err := Client.DeleteChannel(channel4.Id); err != nil { + t.Fatal(err) + } + + th.LoginSystemAdmin() + + if _, err := Client.DeleteChannel(channel2.Id); err != nil { + t.Fatal(err) + } + if _, err := Client.DeleteChannel(channel3.Id); err != nil { + t.Fatal(err) + } } func TestGetChannelExtraInfo(t *testing.T) { diff --git a/api/team.go b/api/team.go index 9b23a63af..50e32e625 100644 --- a/api/team.go +++ b/api/team.go @@ -400,12 +400,12 @@ func inviteMembers(c *Context, w http.ResponseWriter, r *http.Request) { } if utils.IsLicensed { - if *utils.Cfg.TeamSettings.RestrictTeamInvite == model.TEAM_INVITE_SYSTEM_ADMIN && !c.IsSystemAdmin() { + if *utils.Cfg.TeamSettings.RestrictTeamInvite == model.PERMISSIONS_SYSTEM_ADMIN && !c.IsSystemAdmin() { c.Err = model.NewLocAppError("inviteMembers", "api.team.invite_members.restricted_system_admin.app_error", nil, "") return } - if *utils.Cfg.TeamSettings.RestrictTeamInvite == model.TEAM_INVITE_TEAM_ADMIN && !c.IsTeamAdmin() { + if *utils.Cfg.TeamSettings.RestrictTeamInvite == model.PERMISSIONS_TEAM_ADMIN && !c.IsTeamAdmin() { c.Err = model.NewLocAppError("inviteMembers", "api.team.invite_members.restricted_team_admin.app_error", nil, "") return } diff --git a/api/team_test.go b/api/team_test.go index 91c73bed5..a62ffcdb5 100644 --- a/api/team_test.go +++ b/api/team_test.go @@ -399,7 +399,7 @@ func TestInviteMembers(t *testing.T) { defer func() { *utils.Cfg.TeamSettings.RestrictTeamInvite = restrictTeamInvite }() - *utils.Cfg.TeamSettings.RestrictTeamInvite = model.TEAM_INVITE_TEAM_ADMIN + *utils.Cfg.TeamSettings.RestrictTeamInvite = model.PERMISSIONS_TEAM_ADMIN th.LoginBasic2() LinkUserToTeam(th.BasicUser2, team) @@ -427,7 +427,7 @@ func TestInviteMembers(t *testing.T) { t.Fatal(err) } - *utils.Cfg.TeamSettings.RestrictTeamInvite = model.TEAM_INVITE_SYSTEM_ADMIN + *utils.Cfg.TeamSettings.RestrictTeamInvite = model.PERMISSIONS_SYSTEM_ADMIN if _, err := Client.InviteMembers(invites); err == nil { t.Fatal("should have errored not system admin and licensed") diff --git a/config/config.json b/config/config.json index ec021045f..c70ace9ff 100644 --- a/config/config.json +++ b/config/config.json @@ -38,7 +38,9 @@ "EnableCustomBrand": false, "CustomBrandText": "", "RestrictDirectMessage": "any", - "RestrictTeamInvite": "system_admin" + "RestrictTeamInvite": "all", + "RestrictPublicChannelManagement": "all", + "RestrictPrivateChannelManagement": "all" }, "SqlSettings": { "DriverName": "mysql", diff --git a/i18n/en.json b/i18n/en.json index 69b18adf3..8436d48d9 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -312,6 +312,22 @@ "translation": "The channel has been archived or deleted" }, { + "id": "api.channel.can_manage_channel.public_restricted_system_admin.app_error", + "translation": "Public Channel management and creation is restricted to System Administrators." + }, + { + "id": "api.channel.can_manage_channel.public_restricted_team_admin.app_error", + "translation": "Public Channel management and creation is restricted to Team and System Administrators." + }, + { + "id": "api.channel.can_manage_channel.private_restricted_system_admin.app_error", + "translation": "Private Group management and creation is restricted to System Administrators." + }, + { + "id": "api.channel.can_manage_channel.private_restricted_team_admin.app_error", + "translation": "Private Group management and creation is restricted to Team and System Administrators." + }, + { "id": "api.channel.update_channel.permission.app_error", "translation": "You do not have the appropriate permissions" }, diff --git a/model/config.go b/model/config.go index 32994a279..61c39bc5b 100644 --- a/model/config.go +++ b/model/config.go @@ -32,9 +32,9 @@ const ( DIRECT_MESSAGE_ANY = "any" DIRECT_MESSAGE_TEAM = "team" - TEAM_INVITE_ALL = "all" - TEAM_INVITE_TEAM_ADMIN = "team_admin" - TEAM_INVITE_SYSTEM_ADMIN = "system_admin" + PERMISSIONS_ALL = "all" + PERMISSIONS_TEAM_ADMIN = "team_admin" + PERMISSIONS_SYSTEM_ADMIN = "system_admin" FAKE_SETTING = "********************************" @@ -169,17 +169,19 @@ type SupportSettings struct { } type TeamSettings struct { - SiteName string - MaxUsersPerTeam int - EnableTeamCreation bool - EnableUserCreation bool - EnableOpenServer *bool - RestrictCreationToDomains string - RestrictTeamNames *bool - EnableCustomBrand *bool - CustomBrandText *string - RestrictDirectMessage *string - RestrictTeamInvite *string + SiteName string + MaxUsersPerTeam int + EnableTeamCreation bool + EnableUserCreation bool + EnableOpenServer *bool + RestrictCreationToDomains string + RestrictTeamNames *bool + EnableCustomBrand *bool + CustomBrandText *string + RestrictDirectMessage *string + RestrictTeamInvite *string + RestrictPublicChannelManagement *string + RestrictPrivateChannelManagement *string } type LdapSettings struct { @@ -381,7 +383,17 @@ func (o *Config) SetDefaults() { if o.TeamSettings.RestrictTeamInvite == nil { o.TeamSettings.RestrictTeamInvite = new(string) - *o.TeamSettings.RestrictTeamInvite = TEAM_INVITE_ALL + *o.TeamSettings.RestrictTeamInvite = PERMISSIONS_ALL + } + + if o.TeamSettings.RestrictPublicChannelManagement == nil { + o.TeamSettings.RestrictPublicChannelManagement = new(string) + *o.TeamSettings.RestrictPublicChannelManagement = PERMISSIONS_ALL + } + + if o.TeamSettings.RestrictPrivateChannelManagement == nil { + o.TeamSettings.RestrictPrivateChannelManagement = new(string) + *o.TeamSettings.RestrictPrivateChannelManagement = PERMISSIONS_ALL } if o.EmailSettings.EnableSignInWithEmail == nil { diff --git a/utils/config.go b/utils/config.go index abb24c085..1fbed5d7b 100644 --- a/utils/config.go +++ b/utils/config.go @@ -215,6 +215,8 @@ func getClientConfig(c *model.Config) map[string]string { props["RestrictTeamNames"] = strconv.FormatBool(*c.TeamSettings.RestrictTeamNames) props["RestrictDirectMessage"] = *c.TeamSettings.RestrictDirectMessage props["RestrictTeamInvite"] = *c.TeamSettings.RestrictTeamInvite + props["RestrictPublicChannelManagement"] = *c.TeamSettings.RestrictPublicChannelManagement + props["RestrictPrivateChannelManagement"] = *c.TeamSettings.RestrictPrivateChannelManagement props["EnableOAuthServiceProvider"] = strconv.FormatBool(c.ServiceSettings.EnableOAuthServiceProvider) props["SegmentDeveloperKey"] = c.ServiceSettings.SegmentDeveloperKey diff --git a/webapp/components/admin_console/policy_settings.jsx b/webapp/components/admin_console/policy_settings.jsx index 7fe8e9460..c7031af7b 100644 --- a/webapp/components/admin_console/policy_settings.jsx +++ b/webapp/components/admin_console/policy_settings.jsx @@ -21,12 +21,16 @@ export default class PolicySettings extends AdminSettings { this.renderSettings = this.renderSettings.bind(this); this.state = Object.assign(this.state, { - restrictTeamInvite: props.config.TeamSettings.RestrictTeamInvite + restrictTeamInvite: props.config.TeamSettings.RestrictTeamInvite, + restrictPublicChannelManagement: props.config.TeamSettings.RestrictPublicChannelManagement, + restrictPrivateChannelManagement: props.config.TeamSettings.RestrictPrivateChannelManagement }); } getConfigFromState(config) { config.TeamSettings.RestrictTeamInvite = this.state.restrictTeamInvite; + config.TeamSettings.RestrictPublicChannelManagement = this.state.restrictPublicChannelManagement; + config.TeamSettings.RestrictPrivateChannelManagement = this.state.restrictPrivateChannelManagement; return config; } @@ -48,9 +52,9 @@ export default class PolicySettings extends AdminSettings { <DropdownSetting id='restrictTeamInvite' values={[ - {value: Constants.TEAM_INVITE_ALL, text: Utils.localizeMessage('admin.general.policy.teamInviteAll', 'All team members')}, - {value: Constants.TEAM_INVITE_TEAM_ADMIN, text: Utils.localizeMessage('admin.general.policy.teamInviteAdmin', 'Team and System Admins')}, - {value: Constants.TEAM_INVITE_SYSTEM_ADMIN, text: Utils.localizeMessage('admin.general.policy.teamInviteSystemAdmin', 'System Admins')} + {value: Constants.PERMISSIONS_ALL, text: Utils.localizeMessage('admin.general.policy.permissionsAll', 'All team members')}, + {value: Constants.PERMISSIONS_TEAM_ADMIN, text: Utils.localizeMessage('admin.general.policy.permissionsAdmin', 'Team and System Admins')}, + {value: Constants.PERMISSIONS_SYSTEM_ADMIN, text: Utils.localizeMessage('admin.general.policy.permissionsSystemAdmin', 'System Admins')} ]} label={ <FormattedMessage @@ -67,6 +71,50 @@ export default class PolicySettings extends AdminSettings { /> } /> + <DropdownSetting + id='restrictPublicChannelManagement' + values={[ + {value: Constants.PERMISSIONS_ALL, text: Utils.localizeMessage('admin.general.policy.permissionsAll', 'All team members')}, + {value: Constants.PERMISSIONS_TEAM_ADMIN, text: Utils.localizeMessage('admin.general.policy.permissionsAdmin', 'Team and System Admins')}, + {value: Constants.PERMISSIONS_SYSTEM_ADMIN, text: Utils.localizeMessage('admin.general.policy.permissionsSystemAdmin', 'System Admins')} + ]} + label={ + <FormattedMessage + id='admin.general.policy.restrictPublicChannelManagementTitle' + defaultMessage='Enable public channel management permissions for:' + /> + } + value={this.state.restrictPublicChannelManagement} + onChange={this.handleChange} + helpText={ + <FormattedHTMLMessage + id='admin.general.policy.restrictPublicChannelManagementDescription' + defaultMessage='Selecting "All team members" allows any team members to create, delete, rename, and set the header or purpose for public channels.<br/><br/>Selecting "Team and System Admins" restricts channel management permissions for public channels to Team and System Admins, including creating, deleting, renaming, and setting the channel header or purpose.<br/><br/>Selecting "System Admins" restricts channel management permissions for public channels to System Admins, including creating, deleting, renaming, and setting the channel header or purpose.' + /> + } + /> + <DropdownSetting + id='restrictPrivateChannelManagement' + values={[ + {value: Constants.PERMISSIONS_ALL, text: Utils.localizeMessage('admin.general.policy.permissionsAll', 'All team members')}, + {value: Constants.PERMISSIONS_TEAM_ADMIN, text: Utils.localizeMessage('admin.general.policy.permissionsAdmin', 'Team and System Admins')}, + {value: Constants.PERMISSIONS_SYSTEM_ADMIN, text: Utils.localizeMessage('admin.general.policy.permissionsSystemAdmin', 'System Admins')} + ]} + label={ + <FormattedMessage + id='admin.general.policy.restrictPrivateChannelManagementTitle' + defaultMessage='Enable private group management permissions for:' + /> + } + value={this.state.restrictPrivateChannelManagement} + onChange={this.handleChange} + helpText={ + <FormattedHTMLMessage + id='admin.general.policy.restrictPrivateChannelManagementDescription' + defaultMessage='Selecting "All team members" allows any team members to create, delete, rename, and set the header or purpose for private groups.<br/><br/>Selecting "Team and System Admins" restricts group management permissions for private groups to Team and System Admins, including creating, deleting, renaming, and setting the group header or purpose.<br/><br/>Selecting "System Admins" restricts group management permissions for private groups to System Admins, including creating, deleting, renaming, and setting the group header or purpose.' + /> + } + /> </SettingsGroup> ); } diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx index 3449a0fd6..2b9b1e1cc 100644 --- a/webapp/components/channel_header.jsx +++ b/webapp/components/channel_header.jsx @@ -56,6 +56,7 @@ export default class ChannelHeader extends React.Component { state.showRenameChannelModal = false; this.state = state; } + getStateFromStores() { const extraInfo = ChannelStore.getExtraInfo(this.props.channelId); @@ -67,6 +68,7 @@ export default class ChannelHeader extends React.Component { currentUser: UserStore.getCurrentUser() }; } + validState() { if (!this.state.channel || !this.state.memberChannel || @@ -77,6 +79,7 @@ export default class ChannelHeader extends React.Component { } return true; } + componentDidMount() { ChannelStore.addChangeListener(this.onListenerChange); ChannelStore.addExtraInfoChangeListener(this.onListenerChange); @@ -87,6 +90,7 @@ export default class ChannelHeader extends React.Component { $('.sidebar--left .dropdown-menu').perfectScrollbar(); document.addEventListener('keydown', this.openRecentMentions); } + componentWillUnmount() { ChannelStore.removeChangeListener(this.onListenerChange); ChannelStore.removeExtraInfoChangeListener(this.onListenerChange); @@ -96,6 +100,7 @@ export default class ChannelHeader extends React.Component { UserStore.removeStatusesChangeListener(this.onListenerChange); document.removeEventListener('keydown', this.openRecentMentions); } + onListenerChange() { const newState = this.getStateFromStores(); if (!Utils.areObjectsEqual(newState, this.state)) { @@ -103,6 +108,7 @@ export default class ChannelHeader extends React.Component { } $('.channel-header__info .description').popover({placement: 'bottom', trigger: 'hover', html: true, delay: {show: 500, hide: 500}}); } + handleLeave() { Client.leaveChannel(this.state.channel.id, () => { @@ -119,6 +125,7 @@ export default class ChannelHeader extends React.Component { } ); } + searchMentions(e) { e.preventDefault(); @@ -146,12 +153,14 @@ export default class ChannelHeader extends React.Component { is_mention_search: true }); } + openRecentMentions(e) { if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.keyCode === Constants.KeyCodes.M) { e.preventDefault(); this.searchMentions(e); } } + showRenameChannelModal(e) { e.preventDefault(); @@ -159,6 +168,7 @@ export default class ChannelHeader extends React.Component { showRenameChannelModal: true }); } + hideRenameChannelModal() { this.setState({ showRenameChannelModal: false @@ -179,6 +189,30 @@ export default class ChannelHeader extends React.Component { return null; } + showManagementOptions(channel, isAdmin, isSystemAdmin) { + if (global.window.mm_license.IsLicensed !== 'true') { + return true; + } + + if (channel.type === Constants.OPEN_CHANNEL) { + if (global.window.mm_config.RestrictPublicChannelManagement === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { + return false; + } + if (global.window.mm_config.RestrictPublicChannelManagement === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { + return false; + } + } else if (channel.type === Constants.PRIVATE_CHANNEL) { + if (global.window.mm_config.RestrictPrivateChannelManagement === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { + return false; + } + if (global.window.mm_config.RestrictPrivateChannelManagement === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { + return false; + } + } + + return true; + } + render() { if (!this.validState()) { return null; @@ -210,7 +244,8 @@ export default class ChannelHeader extends React.Component { ); let channelTitle = channel.display_name; const currentId = this.state.currentUser.id; - const isAdmin = Utils.isAdmin(this.state.memberChannel.roles) || TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser(); + const isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser(); + const isSystemAdmin = UserStore.isSystemAdminForCurrentUser(); const isDirect = (this.state.channel.type === 'D'); if (isDirect) { @@ -331,67 +366,90 @@ export default class ChannelHeader extends React.Component { dropdownContents.push( <li - key='set_channel_header' + key='notification_preferences' role='presentation' > <ToggleModalButton role='menuitem' - dialogType={EditChannelHeaderModal} - dialogProps={{channel}} + dialogType={ChannelNotificationsModal} + dialogProps={{ + channel, + channelMember: this.state.memberChannel, + currentUser: this.state.currentUser + }} > <FormattedMessage - id='channel_header.setHeader' - defaultMessage='Set {term} Header...' - values={{ - term: (channelTerm) - }} + id='channel_header.notificationPreferences' + defaultMessage='Notification Preferences' /> </ToggleModalButton> </li> ); - dropdownContents.push( + + const deleteOption = ( <li - key='set_channel_purpose' + key='delete_channel' role='presentation' > - <a + <ToggleModalButton role='menuitem' - href='#' - onClick={() => this.setState({showEditChannelPurposeModal: true})} + dialogType={DeleteChannelModal} + dialogProps={{channel}} > <FormattedMessage - id='channel_header.setPurpose' - defaultMessage='Set {term} Purpose...' + id='channel_header.delete' + defaultMessage='Delete {term}...' values={{ term: (channelTerm) }} /> - </a> - </li> - ); - dropdownContents.push( - <li - key='notification_preferences' - role='presentation' - > - <ToggleModalButton - role='menuitem' - dialogType={ChannelNotificationsModal} - dialogProps={{ - channel, - channelMember: this.state.memberChannel, - currentUser: this.state.currentUser - }} - > - <FormattedMessage - id='channel_header.notificationPreferences' - defaultMessage='Notification Preferences' - /> </ToggleModalButton> </li> ); - if (isAdmin) { + if (this.showManagementOptions(channel, isAdmin, isSystemAdmin)) { + dropdownContents.push( + <li + key='set_channel_header' + role='presentation' + > + <ToggleModalButton + role='menuitem' + dialogType={EditChannelHeaderModal} + dialogProps={{channel}} + > + <FormattedMessage + id='channel_header.setHeader' + defaultMessage='Set {term} Header...' + values={{ + term: (channelTerm) + }} + /> + </ToggleModalButton> + </li> + ); + + dropdownContents.push( + <li + key='set_channel_purpose' + role='presentation' + > + <a + role='menuitem' + href='#' + onClick={() => this.setState({showEditChannelPurposeModal: true})} + > + <FormattedMessage + id='channel_header.setPurpose' + defaultMessage='Set {term} Purpose...' + values={{ + term: (channelTerm) + }} + /> + </a> + </li> + ); + dropdownContents.push( <li key='rename_channel' @@ -414,27 +472,10 @@ export default class ChannelHeader extends React.Component { ); if (!ChannelStore.isDefault(channel)) { - dropdownContents.push( - <li - key='delete_channel' - role='presentation' - > - <ToggleModalButton - role='menuitem' - dialogType={DeleteChannelModal} - dialogProps={{channel}} - > - <FormattedMessage - id='channel_header.delete' - defaultMessage='Delete {term}...' - values={{ - term: (channelTerm) - }} - /> - </ToggleModalButton> - </li> - ); + dropdownContents.push(deleteOption); } + } else if (this.state.userCount === 1) { + dropdownContents.push(deleteOption); } const canLeave = channel.type === Constants.PRIVATE_CHANNEL ? this.state.userCount > 1 : true; diff --git a/webapp/components/more_channels.jsx b/webapp/components/more_channels.jsx index 54a06d0ae..b7ffff712 100644 --- a/webapp/components/more_channels.jsx +++ b/webapp/components/more_channels.jsx @@ -6,8 +6,11 @@ import LoadingScreen from './loading_screen.jsx'; import NewChannelFlow from './new_channel_flow.jsx'; import ChannelStore from 'stores/channel_store.jsx'; +import UserStore from 'stores/user_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; import * as Utils from 'utils/utils.jsx'; +import Constants from 'utils/constants.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; @@ -132,6 +135,41 @@ export default class MoreChannels extends React.Component { serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>; } + let createNewChannelButton = ( + <button + type='button' + className='btn btn-primary channel-create-btn' + onClick={this.handleNewChannel} + > + <FormattedMessage + id='more_channels.create' + defaultMessage='Create New Channel' + /> + </button> + ); + + let createChannelHelpText = ( + <p className='secondary-message'> + <FormattedMessage + id='more_channels.createClick' + defaultMessage="Click 'Create New Channel' to make a new one" + /> + </p> + ); + + const isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser(); + const isSystemAdmin = UserStore.isSystemAdminForCurrentUser(); + + if (global.window.mm_license.IsLicensed === 'true') { + if (global.window.mm_config.RestrictPublicChannelManagement === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { + createNewChannelButton = null; + createChannelHelpText = null; + } else if (global.window.mm_config.RestrictPublicChannelManagement === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { + createNewChannelButton = null; + createChannelHelpText = null; + } + } + var moreChannels; if (this.state.channels != null) { @@ -153,12 +191,7 @@ export default class MoreChannels extends React.Component { defaultMessage='No more channels to join' /> </p> - <p className='secondary-message'> - <FormattedMessage - id='more_channels.createClick' - defaultMessage="Click 'Create New Channel' to make a new one" - /> - </p> + {createChannelHelpText} </div> ); } @@ -195,16 +228,7 @@ export default class MoreChannels extends React.Component { defaultMessage='More Channels' /> </h4> - <button - type='button' - className='btn btn-primary channel-create-btn' - onClick={this.handleNewChannel} - > - <FormattedMessage - id='more_channels.create' - defaultMessage='Create New Channel' - /> - </button> + {createNewChannelButton} <NewChannelFlow show={this.state.showNewChannelModal} channelType={this.state.channelType} diff --git a/webapp/components/navbar_dropdown.jsx b/webapp/components/navbar_dropdown.jsx index 4f137979e..bc7aaeb5f 100644 --- a/webapp/components/navbar_dropdown.jsx +++ b/webapp/components/navbar_dropdown.jsx @@ -122,10 +122,10 @@ export default class NavbarDropdown extends React.Component { } if (global.window.mm_license.IsLicensed === 'true') { - if (global.window.mm_config.RestrictTeamInvite === Constants.TEAM_INVITE_SYSTEM_ADMIN && !isSystemAdmin) { + if (global.window.mm_config.RestrictTeamInvite === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { teamLink = null; inviteLink = null; - } else if (global.window.mm_config.RestrictTeamInvite === Constants.TEAM_INVITE_TEAM_ADMIN && !isAdmin) { + } else if (global.window.mm_config.RestrictTeamInvite === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { teamLink = null; inviteLink = null; } diff --git a/webapp/components/new_channel_modal.jsx b/webapp/components/new_channel_modal.jsx index 9fd76395c..23eee625d 100644 --- a/webapp/components/new_channel_modal.jsx +++ b/webapp/components/new_channel_modal.jsx @@ -3,8 +3,12 @@ import $ from 'jquery'; import ReactDOM from 'react-dom'; + import * as Utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; + +import UserStore from 'stores/user_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl'; @@ -113,6 +117,47 @@ class NewChannelModal extends React.Component { serverError = <div className='form-group has-error'><p className='input__help error'>{this.props.serverError}</p></div>; } + let createPublicChannelLink = ( + <a + href='#' + onClick={this.props.onTypeSwitched} + > + <FormattedMessage + id='channel_modal.publicChannel1' + defaultMessage='Create a public channel' + /> + </a> + ); + + let createPrivateChannelLink = ( + <a + href='#' + onClick={this.props.onTypeSwitched} + > + <FormattedMessage + id='channel_modal.privateGroup2' + defaultMessage='Create a private group' + /> + </a> + ); + + const isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser(); + const isSystemAdmin = UserStore.isSystemAdminForCurrentUser(); + + if (global.window.mm_license.IsLicensed === 'true') { + if (global.window.mm_config.RestrictPublicChannelManagement === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { + createPublicChannelLink = null; + } else if (global.window.mm_config.RestrictPublicChannelManagement === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { + createPublicChannelLink = null; + } + + if (global.window.mm_config.RestrictPrivateChannelManagement === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { + createPrivateChannelLink = null; + } else if (global.window.mm_config.RestrictPrivateChannelManagement === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { + createPrivateChannelLink = null; + } + } + var channelTerm = ''; var channelSwitchText = ''; switch (this.props.channelType) { @@ -129,15 +174,7 @@ class NewChannelModal extends React.Component { id='channel_modal.privateGroup1' defaultMessage='Create a new private group with restricted membership. ' /> - <a - href='#' - onClick={this.props.onTypeSwitched} - > - <FormattedMessage - id='channel_modal.publicChannel1' - defaultMessage='Create a public channel' - /> - </a> + {createPublicChannelLink} </div> ); break; @@ -154,15 +191,7 @@ class NewChannelModal extends React.Component { id='channel_modal.publicChannel2' defaultMessage='Create a new public channel anyone can join. ' /> - <a - href='#' - onClick={this.props.onTypeSwitched} - > - <FormattedMessage - id='channel_modal.privateGroup2' - defaultMessage='Create a private group' - /> - </a> + {createPrivateChannelLink} </div> ); break; diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx index 4f678274d..fdcae1dff 100644 --- a/webapp/components/sidebar.jsx +++ b/webapp/components/sidebar.jsx @@ -682,6 +682,55 @@ export default class Sidebar extends React.Component { /> ); + const isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser(); + const isSystemAdmin = UserStore.isSystemAdminForCurrentUser(); + + let createPublicChannelIcon = ( + <OverlayTrigger + delayShow={500} + placement='top' + overlay={createChannelTootlip} + > + <a + className='add-channel-btn' + href='#' + onClick={this.showNewChannelModal.bind(this, Constants.OPEN_CHANNEL)} + > + {'+'} + </a> + </OverlayTrigger> + ); + + let createPrivateChannelIcon = ( + <OverlayTrigger + delayShow={500} + placement='top' + overlay={createGroupTootlip} + > + <a + className='add-channel-btn' + href='#' + onClick={this.showNewChannelModal.bind(this, Constants.PRIVATE_CHANNEL)} + > + {'+'} + </a> + </OverlayTrigger> + ); + + if (global.window.mm_license.IsLicensed === 'true') { + if (global.window.mm_config.RestrictPublicChannelManagement === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { + createPublicChannelIcon = null; + } else if (global.window.mm_config.RestrictPublicChannelManagement === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { + createPublicChannelIcon = null; + } + + if (global.window.mm_config.RestrictPrivateChannelManagement === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { + createPrivateChannelIcon = null; + } else if (global.window.mm_config.RestrictPrivateChannelManagement === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { + createPrivateChannelIcon = null; + } + } + return ( <div className='sidebar--left' @@ -728,19 +777,7 @@ export default class Sidebar extends React.Component { id='sidebar.channels' defaultMessage='Channels' /> - <OverlayTrigger - delayShow={500} - placement='top' - overlay={createChannelTootlip} - > - <a - className='add-channel-btn' - href='#' - onClick={this.showNewChannelModal.bind(this, 'O')} - > - {'+'} - </a> - </OverlayTrigger> + {createPublicChannelIcon} </h4> </li> {publicChannelItems} @@ -765,19 +802,7 @@ export default class Sidebar extends React.Component { id='sidebar.pg' defaultMessage='Private Groups' /> - <OverlayTrigger - delayShow={500} - placement='top' - overlay={createGroupTootlip} - > - <a - className='add-channel-btn' - href='#' - onClick={this.showNewChannelModal.bind(this, 'P')} - > - {'+'} - </a> - </OverlayTrigger> + {createPrivateChannelIcon} </h4> </li> {privateChannelItems} diff --git a/webapp/components/sidebar_right_menu.jsx b/webapp/components/sidebar_right_menu.jsx index 2cf758f00..25136e8bc 100644 --- a/webapp/components/sidebar_right_menu.jsx +++ b/webapp/components/sidebar_right_menu.jsx @@ -186,10 +186,10 @@ export default class SidebarRightMenu extends React.Component { } if (global.window.mm_license.IsLicensed === 'true') { - if (global.window.mm_config.RestrictTeamInvite === Constants.TEAM_INVITE_SYSTEM_ADMIN && !isSystemAdmin) { + if (global.window.mm_config.RestrictTeamInvite === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { teamLink = null; inviteLink = null; - } else if (global.window.mm_config.RestrictTeamInvite === Constants.TEAM_INVITE_TEAM_ADMIN && !isAdmin) { + } else if (global.window.mm_config.RestrictTeamInvite === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { teamLink = null; inviteLink = null; } diff --git a/webapp/components/tutorial/tutorial_intro_screens.jsx b/webapp/components/tutorial/tutorial_intro_screens.jsx index b0d831d96..639fa07b2 100644 --- a/webapp/components/tutorial/tutorial_intro_screens.jsx +++ b/webapp/components/tutorial/tutorial_intro_screens.jsx @@ -108,7 +108,7 @@ export default class TutorialIntroScreens extends React.Component { let inviteModalLink; let inviteText; - if (global.window.mm_license.IsLicensed !== 'true' || global.window.mm_config.RestrictTeamInvite === Constants.TEAM_INVITE_ALL) { + if (global.window.mm_license.IsLicensed !== 'true' || global.window.mm_config.RestrictTeamInvite === Constants.PERMISSIONS_ALL) { if (team.type === Constants.INVITE_TEAM) { inviteModalLink = ( <a diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 26b1b47fd..a723b68c7 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -233,11 +233,15 @@ "admin.general.localization.serverLocaleTitle": "Default Server Language:", "admin.general.log": "Logging", "admin.general.policy": "Policy", - "admin.general.policy.teamInviteAdmin": "Team and System Admins", - "admin.general.policy.teamInviteAll": "All team members", - "admin.general.policy.teamInviteDescription": "Selecting \"All team members\" allows any team member to invite others using an email invitation or team invite link.<br/><br/>Selecting \"Team and System Admins\" hides the email invitation and team invite link in the Main Menu from users who are not Team or System Admins. Note: If \"Get Team Invite Link\" is used to share a link, it will need to be regenerated after the desired users joined the team.<br/><br/>Selecting \"System Admins\" hides the email invitation and team invite link in the Main Menu from users who are not System Admins. Note: If \"Get Team Invite Link\" is used to share a link, it will need to be regenerated after the desired users joined the team.", - "admin.general.policy.teamInviteSystemAdmin": "System Admins", + "admin.general.policy.permissionsAll": "All team members", + "admin.general.policy.permissionsAdmin": "Team and System Admins", + "admin.general.policy.permissionsSystemAdmin": "System Admins", "admin.general.policy.teamInviteTitle": "Enable sending team invites from:", + "admin.general.policy.teamInviteDescription": "Selecting \"All team members\" allows any team member to invite others using an email invitation or team invite link.<br/><br/>Selecting \"Team and System Admins\" hides the email invitation and team invite link in the Main Menu from users who are not Team or System Admins. Note: If \"Get Team Invite Link\" is used to share a link, it will need to be regenerated after the desired users joined the team.<br/><br/>Selecting \"System Admins\" hides the email invitation and team invite link in the Main Menu from users who are not System Admins. Note: If \"Get Team Invite Link\" is used to share a link, it will need to be regenerated after the desired users joined the team.", + "admin.general.policy.restrictPublicChannelManagementTitle": "Enable public channel management permissions for:", + "admin.general.policy.restrictPublicChannelManagementDescription": "Selecting \"All team members\" allows any team members to create, delete, rename, and set the header or purpose for public channels.<br/><br/>Selecting \"Team and System Admins\" restricts channel management permissions for public channels to Team and System Admins, including creating, deleting, renaming, and setting the channel header or purpose.<br/><br/>Selecting \"System Admins\" restricts channel management permissions for public channels to System Admins, including creating, deleting, renaming, and setting the channel header or purpose.", + "admin.general.policy.restrictPrivateChannelManagementTitle": "Enable private group management permissions for:", + "admin.general.policy.restrictPrivateChannelManagementDescription": "Selecting \"All team members\" allows any team members to create, delete, rename, and set the header or purpose for private groups.<br/><br/>Selecting \"Team and System Admins\" restricts group management permissions for private groups to Team and System Admins, including creating, deleting, renaming, and setting the group header or purpose.<br/><br/>Selecting \"System Admins\" restricts group management permissions for private groups to System Admins, including creating, deleting, renaming, and setting the group header or purpose.", "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/channel_store.jsx b/webapp/stores/channel_store.jsx index b65ec330c..dc2577811 100644 --- a/webapp/stores/channel_store.jsx +++ b/webapp/stores/channel_store.jsx @@ -53,54 +53,70 @@ class ChannelStoreClass extends EventEmitter { this.extraInfos = {}; this.unreadCounts = {}; } + get POST_MODE_CHANNEL() { return 1; } + get POST_MODE_FOCUS() { return 2; } + emitChange() { this.emit(CHANGE_EVENT); } + addChangeListener(callback) { this.on(CHANGE_EVENT, callback); } + removeChangeListener(callback) { this.removeListener(CHANGE_EVENT, callback); } + emitMoreChange() { this.emit(MORE_CHANGE_EVENT); } + addMoreChangeListener(callback) { this.on(MORE_CHANGE_EVENT, callback); } + removeMoreChangeListener(callback) { this.removeListener(MORE_CHANGE_EVENT, callback); } + emitExtraInfoChange() { this.emit(EXTRA_INFO_EVENT); } + addExtraInfoChangeListener(callback) { this.on(EXTRA_INFO_EVENT, callback); } + removeExtraInfoChangeListener(callback) { this.removeListener(EXTRA_INFO_EVENT, callback); } emitLeave(id) { this.emit(LEAVE_EVENT, id); } + addLeaveListener(callback) { this.on(LEAVE_EVENT, callback); } + removeLeaveListener(callback) { this.removeListener(LEAVE_EVENT, callback); } + findFirstBy(field, value) { return this.doFindFirst(field, value, this.getChannels()); } + findFirstMoreBy(field, value) { return this.doFindFirst(field, value, this.getMoreChannels()); } + doFindFirst(field, value, channels) { for (var i = 0; i < channels.length; i++) { if (channels[i][field] === value) { @@ -110,33 +126,43 @@ class ChannelStoreClass extends EventEmitter { return null; } + get(id) { return this.findFirstBy('id', id); } + getMember(id) { return this.getAllMembers()[id]; } + getByName(name) { return this.findFirstBy('name', name); } + getByDisplayName(displayName) { return this.findFirstBy('display_name', displayName); } + getMoreByName(name) { return this.findFirstMoreBy('name', name); } + getAll() { return this.getChannels(); } + getAllMembers() { return this.getChannelMembers(); } + getMoreAll() { return this.getMoreChannels(); } + setCurrentId(id) { this.currentId = id; } + resetCounts(id) { const cm = this.channelMembers; for (var cmid in cm) { @@ -151,9 +177,11 @@ class ChannelStoreClass extends EventEmitter { } } } + getCurrentId() { return this.currentId; } + getCurrent() { var currentId = this.getCurrentId(); @@ -163,6 +191,7 @@ class ChannelStoreClass extends EventEmitter { return null; } + getCurrentMember() { var currentId = this.getCurrentId(); @@ -172,15 +201,18 @@ class ChannelStoreClass extends EventEmitter { return null; } + setChannelMember(member) { var members = this.getChannelMembers(); members[member.channel_id] = member; this.storeChannelMembers(members); this.emitChange(); } + getCurrentExtraInfo() { return this.getExtraInfo(this.getCurrentId()); } + getExtraInfo(channelId) { var extra = null; @@ -197,6 +229,7 @@ class ChannelStoreClass extends EventEmitter { return extra; } + pStoreChannel(channel) { var channels = this.getChannels(); var found; @@ -220,35 +253,45 @@ class ChannelStoreClass extends EventEmitter { channels.sort(Utils.sortByDisplayName); this.storeChannels(channels); } + storeChannels(channels) { this.channels = channels; } + getChannels() { return this.channels; } + pStoreChannelMember(channelMember) { var members = this.getChannelMembers(); members[channelMember.channel_id] = channelMember; this.storeChannelMembers(members); } + storeChannelMembers(channelMembers) { this.channelMembers = channelMembers; } + getChannelMembers() { return this.channelMembers; } + storeMoreChannels(channels) { this.moreChannels = channels; } + getMoreChannels() { return this.moreChannels; } + storeExtraInfos(extraInfos) { this.extraInfos = extraInfos; } + getExtraInfos() { return this.extraInfos; } + isDefault(channel) { return channel.name === Constants.DEFAULT_CHANNEL; } diff --git a/webapp/utils/channel_intro_messages.jsx b/webapp/utils/channel_intro_messages.jsx index 043894b7b..50d12ed42 100644 --- a/webapp/utils/channel_intro_messages.jsx +++ b/webapp/utils/channel_intro_messages.jsx @@ -114,9 +114,9 @@ export function createDefaultIntroMessage(channel) { const isSystemAdmin = UserStore.isSystemAdminForCurrentUser(); if (global.window.mm_license.IsLicensed === 'true') { - if (global.window.mm_config.RestrictTeamInvite === Constants.TEAM_INVITE_SYSTEM_ADMIN && !isSystemAdmin) { + if (global.window.mm_config.RestrictTeamInvite === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { inviteModalLink = null; - } else if (global.window.mm_config.RestrictTeamInvite === Constants.TEAM_INVITE_TEAM_ADMIN && !isAdmin) { + } else if (global.window.mm_config.RestrictTeamInvite === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { inviteModalLink = null; } } diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx index 1b0fa6374..0191edcf0 100644 --- a/webapp/utils/constants.jsx +++ b/webapp/utils/constants.jsx @@ -762,7 +762,7 @@ export default { POST_COLLAPSE_TIMEOUT: 1000 * 60 * 5, // five minutes LICENSE_EXPIRY_NOTIFICATION: 1000 * 60 * 60 * 24 * 15, // 15 days LICENSE_GRACE_PERIOD: 1000 * 60 * 60 * 24 * 15, // 15 days - TEAM_INVITE_ALL: 'all', - TEAM_INVITE_TEAM_ADMIN: 'team_admin', - TEAM_INVITE_SYSTEM_ADMIN: 'system_admin' + PERMISSIONS_ALL: 'all', + PERMISSIONS_TEAM_ADMIN: 'team_admin', + PERMISSIONS_SYSTEM_ADMIN: 'system_admin' }; |