From 24b6762dfb118511528b42fb496278a6f921d040 Mon Sep 17 00:00:00 2001 From: Maria A Nunez Date: Fri, 29 May 2026 10:24:08 -0400 Subject: [PATCH] MM-68845: Tighten authorization on /share-channel autocomplete (#36662) (#36808) Automatic Merge --- .../app/slashcommands/command_share.go | 4 + .../app/slashcommands/command_share_test.go | 302 ++++++++++++++++++ 2 files changed, 306 insertions(+) diff --git a/server/channels/app/slashcommands/command_share.go b/server/channels/app/slashcommands/command_share.go index 518a082b10..50cdb0ab6c 100644 --- a/server/channels/app/slashcommands/command_share.go +++ b/server/channels/app/slashcommands/command_share.go @@ -63,6 +63,10 @@ func (sp *ShareProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Com } func (sp *ShareProvider) GetAutoCompleteListItems(c request.CTX, a *app.App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) ([]model.AutocompleteListItem, error) { + if !a.HasPermissionTo(commandArgs.UserId, model.PermissionManageSharedChannels) { + return []model.AutocompleteListItem{}, nil + } + switch { case strings.Contains(parsed, " share "): diff --git a/server/channels/app/slashcommands/command_share_test.go b/server/channels/app/slashcommands/command_share_test.go index 1a92f9a6f4..2a8a967a15 100644 --- a/server/channels/app/slashcommands/command_share_test.go +++ b/server/channels/app/slashcommands/command_share_test.go @@ -126,3 +126,305 @@ func TestShareProviderDoCommand(t *testing.T) { require.Contains(t, response.Text, args.T("api.command_share.invite_remote_to_channel.error")) }) } + +func TestShareProviderGetAutoCompleteListItemsPermission(t *testing.T) { + connectionIDArg := func() *model.AutocompleteArg { + return &model.AutocompleteArg{Name: "connectionID"} + } + + seedRemote := func(t *testing.T, th *TestHelper) *model.RemoteCluster { + t.Helper() + rc, err := th.App.AddRemoteCluster(&model.RemoteCluster{ + RemoteId: model.NewId(), + Name: "remote-" + model.NewId(), + DisplayName: "Remote Display Name Sentinel", + SiteURL: "https://remote-sentinel.example.com", + Token: model.NewId(), + Topics: "topic", + CreateAt: model.GetMillis(), + LastPingAt: model.GetMillis(), + CreatorId: model.NewId(), + }) + require.Nil(t, err) + return rc + } + + assertNoRemoteData := func(t *testing.T, items []model.AutocompleteListItem, rc *model.RemoteCluster) { + t.Helper() + for i, item := range items { + assert.NotContains(t, item.Item, rc.RemoteId, "item[%d].Item contained RemoteId", i) + assert.NotContains(t, item.HelpText, rc.RemoteId, "item[%d].HelpText contained RemoteId", i) + assert.NotContains(t, item.HelpText, rc.DisplayName, "item[%d].HelpText contained DisplayName", i) + assert.NotContains(t, item.HelpText, rc.SiteURL, "item[%d].HelpText contained SiteURL", i) + } + } + + t.Run("invite without manage_shared_channels permission returns no remote cluster data", func(t *testing.T) { + th := setupForSharedChannels(t).initBasic(t) + + require.False(t, th.App.HasPermissionTo(th.BasicUser.Id, model.PermissionManageSharedChannels), + "precondition: BasicUser must not have manage_shared_channels for this subtest") + + rc := seedRemote(t, th) + + commandProvider := ShareProvider{} + channel := th.CreateChannel(t, th.BasicTeam, WithShared(false)) + + args := &model.CommandArgs{ + T: func(s string, args ...any) string { return s }, + ChannelId: channel.Id, + UserId: th.BasicUser.Id, + TeamId: th.BasicTeam.Id, + Command: "/share-channel invite --connectionID", + } + + items, err := commandProvider.GetAutoCompleteListItems(th.Context, th.App, args, connectionIDArg(), "/share-channel invite ", "") + + if err == nil { + assert.Empty(t, items, "expected empty autocomplete list when caller lacks manage_shared_channels") + } + assertNoRemoteData(t, items, rc) + }) + + t.Run("uninvite without manage_shared_channels permission returns no remote cluster data", func(t *testing.T) { + th := setupForSharedChannels(t).initBasic(t) + + require.False(t, th.App.HasPermissionTo(th.BasicUser.Id, model.PermissionManageSharedChannels), + "precondition: BasicUser must not have manage_shared_channels for this subtest") + + rc := seedRemote(t, th) + + commandProvider := ShareProvider{} + channel := th.CreateChannel(t, th.BasicTeam, WithShared(false)) + + args := &model.CommandArgs{ + T: func(s string, args ...any) string { return s }, + ChannelId: channel.Id, + UserId: th.BasicUser.Id, + TeamId: th.BasicTeam.Id, + Command: "/share-channel uninvite --connectionID", + } + + items, err := commandProvider.GetAutoCompleteListItems(th.Context, th.App, args, connectionIDArg(), "/share-channel uninvite ", "") + + if err == nil { + assert.Empty(t, items, "expected empty autocomplete list when caller lacks manage_shared_channels") + } + assertNoRemoteData(t, items, rc) + }) + + t.Run("invite with manage_shared_channels permission returns remote cluster data", func(t *testing.T) { + th := setupForSharedChannels(t).initBasic(t) + th.addPermissionToRole(t, model.PermissionManageSharedChannels.Id, th.BasicUser.Roles) + + rc := seedRemote(t, th) + + commandProvider := ShareProvider{} + channel := th.CreateChannel(t, th.BasicTeam, WithShared(false)) + + args := &model.CommandArgs{ + T: func(s string, args ...any) string { return s }, + ChannelId: channel.Id, + UserId: th.BasicUser.Id, + TeamId: th.BasicTeam.Id, + Command: "/share-channel invite --connectionID", + } + + items, err := commandProvider.GetAutoCompleteListItems(th.Context, th.App, args, connectionIDArg(), "/share-channel invite ", "") + require.NoError(t, err) + require.NotEmpty(t, items, "expected at least one autocomplete item when caller has manage_shared_channels") + + found := false + for _, item := range items { + if item.Item == rc.RemoteId { + found = true + break + } + } + require.True(t, found, "expected seeded RemoteId %q to appear in autocomplete items when caller has manage_shared_channels", rc.RemoteId) + }) + + t.Run("uninvite with manage_shared_channels permission returns remote cluster data", func(t *testing.T) { + th := setupForSharedChannels(t).initBasic(t) + th.addPermissionToRole(t, model.PermissionManageSharedChannels.Id, th.BasicUser.Roles) + + rc := seedRemote(t, th) + + commandProvider := ShareProvider{} + channel := th.CreateChannel(t, th.BasicTeam, WithShared(false)) + + args := &model.CommandArgs{ + T: func(s string, args ...any) string { return s }, + ChannelId: channel.Id, + UserId: th.BasicUser.Id, + TeamId: th.BasicTeam.Id, + Command: "/share-channel uninvite --connectionID", + } + + items, err := commandProvider.GetAutoCompleteListItems(th.Context, th.App, args, connectionIDArg(), "/share-channel uninvite ", "") + require.NoError(t, err) + require.NotEmpty(t, items, "expected at least one autocomplete item when caller has manage_shared_channels") + + found := false + for _, item := range items { + if item.Item == rc.RemoteId { + found = true + break + } + } + require.True(t, found, "expected seeded RemoteId %q to appear in autocomplete items when caller has manage_shared_channels", rc.RemoteId) + }) +} + +func TestShareProviderGetAutoCompleteListItemsAdjacentRoles(t *testing.T) { + connectionIDArg := func() *model.AutocompleteArg { + return &model.AutocompleteArg{Name: "connectionID"} + } + + seedRemote := func(t *testing.T, th *TestHelper) *model.RemoteCluster { + t.Helper() + rc, err := th.App.AddRemoteCluster(&model.RemoteCluster{ + RemoteId: model.NewId(), + Name: "remote-" + model.NewId(), + DisplayName: "Adjacent Sentinel Display", + SiteURL: "https://adjacent-sentinel.example.com", + Token: model.NewId(), + Topics: "topic", + CreateAt: model.GetMillis(), + LastPingAt: model.GetMillis(), + CreatorId: model.NewId(), + }) + require.Nil(t, err) + return rc + } + + assertNoRemoteData := func(t *testing.T, items []model.AutocompleteListItem, rc *model.RemoteCluster) { + t.Helper() + for i, item := range items { + assert.NotContains(t, item.Item, rc.RemoteId, "item[%d].Item contained RemoteId", i) + assert.NotContains(t, item.HelpText, rc.RemoteId, "item[%d].HelpText contained RemoteId", i) + assert.NotContains(t, item.HelpText, rc.DisplayName, "item[%d].HelpText contained DisplayName", i) + assert.NotContains(t, item.HelpText, rc.SiteURL, "item[%d].HelpText contained SiteURL", i) + } + } + + t.Run("guest user receives no remote cluster data on invite autocomplete", func(t *testing.T) { + th := setupForSharedChannels(t).initBasic(t) + + guest := th.createGuest(t) + require.False(t, th.App.HasPermissionTo(guest.Id, model.PermissionManageSharedChannels), + "precondition: a freshly-created guest must not have manage_shared_channels") + + rc := seedRemote(t, th) + + commandProvider := ShareProvider{} + channel := th.CreateChannel(t, th.BasicTeam, WithShared(false)) + + args := &model.CommandArgs{ + T: func(s string, args ...any) string { return s }, + ChannelId: channel.Id, + UserId: guest.Id, + TeamId: th.BasicTeam.Id, + Command: "/share-channel invite --connectionID", + } + + items, err := commandProvider.GetAutoCompleteListItems(th.Context, th.App, args, connectionIDArg(), "/share-channel invite ", "") + if err == nil { + assert.Empty(t, items, "expected empty autocomplete list for guest on invite") + } + assertNoRemoteData(t, items, rc) + }) + + t.Run("guest user receives no remote cluster data on uninvite autocomplete", func(t *testing.T) { + th := setupForSharedChannels(t).initBasic(t) + + guest := th.createGuest(t) + require.False(t, th.App.HasPermissionTo(guest.Id, model.PermissionManageSharedChannels), + "precondition: a freshly-created guest must not have manage_shared_channels") + + rc := seedRemote(t, th) + + commandProvider := ShareProvider{} + channel := th.CreateChannel(t, th.BasicTeam, WithShared(false)) + + args := &model.CommandArgs{ + T: func(s string, args ...any) string { return s }, + ChannelId: channel.Id, + UserId: guest.Id, + TeamId: th.BasicTeam.Id, + Command: "/share-channel uninvite --connectionID", + } + + items, err := commandProvider.GetAutoCompleteListItems(th.Context, th.App, args, connectionIDArg(), "/share-channel uninvite ", "") + if err == nil { + assert.Empty(t, items, "expected empty autocomplete list for guest on uninvite") + } + assertNoRemoteData(t, items, rc) + }) + + t.Run("system admin receives remote cluster data on invite", func(t *testing.T) { + th := setupForSharedChannels(t).initBasic(t) + + require.True(t, th.App.HasPermissionTo(th.SystemAdminUser.Id, model.PermissionManageSharedChannels), + "precondition: SystemAdminUser must have manage_shared_channels via inherited permissions") + + rc := seedRemote(t, th) + + commandProvider := ShareProvider{} + channel := th.CreateChannel(t, th.BasicTeam, WithShared(false)) + + args := &model.CommandArgs{ + T: func(s string, args ...any) string { return s }, + ChannelId: channel.Id, + UserId: th.SystemAdminUser.Id, + TeamId: th.BasicTeam.Id, + Command: "/share-channel invite --connectionID", + } + + items, err := commandProvider.GetAutoCompleteListItems(th.Context, th.App, args, connectionIDArg(), "/share-channel invite ", "") + require.NoError(t, err) + require.NotEmpty(t, items, "expected at least one autocomplete item for system admin") + + found := false + for _, item := range items { + if item.Item == rc.RemoteId { + found = true + break + } + } + require.True(t, found, "expected seeded RemoteId %q to appear for system admin", rc.RemoteId) + }) + + t.Run("system admin receives remote cluster data on uninvite", func(t *testing.T) { + th := setupForSharedChannels(t).initBasic(t) + + require.True(t, th.App.HasPermissionTo(th.SystemAdminUser.Id, model.PermissionManageSharedChannels), + "precondition: SystemAdminUser must have manage_shared_channels via inherited permissions") + + rc := seedRemote(t, th) + + commandProvider := ShareProvider{} + channel := th.CreateChannel(t, th.BasicTeam, WithShared(false)) + + args := &model.CommandArgs{ + T: func(s string, args ...any) string { return s }, + ChannelId: channel.Id, + UserId: th.SystemAdminUser.Id, + TeamId: th.BasicTeam.Id, + Command: "/share-channel uninvite --connectionID", + } + + items, err := commandProvider.GetAutoCompleteListItems(th.Context, th.App, args, connectionIDArg(), "/share-channel uninvite ", "") + require.NoError(t, err) + require.NotEmpty(t, items, "expected at least one autocomplete item for system admin") + + found := false + for _, item := range items { + if item.Item == rc.RemoteId { + found = true + break + } + } + require.True(t, found, "expected seeded RemoteId %q to appear for system admin", rc.RemoteId) + }) +}