diff --git a/server/channels/api4/post.go b/server/channels/api4/post.go index 385ac6d9b2..11eb08aed3 100644 --- a/server/channels/api4/post.go +++ b/server/channels/api4/post.go @@ -1160,15 +1160,20 @@ func moveThread(c *Context, w http.ResponseWriter, r *http.Request) { return } - // If there are no configured PermittedWranglerRoles, skip the check - userHasRole := len(c.App.Config().WranglerSettings.PermittedWranglerRoles) == 0 - for _, role := range c.App.Config().WranglerSettings.PermittedWranglerRoles { - if user.IsInRole(role) { - userHasRole = true - break - } + posts, _, err := c.App.GetPostsByIds([]string{c.Params.PostId}) + if err != nil { + c.Err = err + return } + channelMember, err := c.App.GetChannelMember(c.AppContext, posts[0].ChannelId, user.Id) + if err != nil { + c.Err = err + return + } + + userHasRole := hasPermittedWranglerRole(c, user, channelMember) + // Sysadmins are always permitted if !userHasRole && !user.IsSystemAdmin() { c.Err = model.NewAppError("moveThread", "api.post.move_thread.no_permission", nil, "", http.StatusForbidden) @@ -1267,3 +1272,19 @@ func getPostInfo(c *Context, w http.ResponseWriter, r *http.Request) { w.Write(js) } + +func hasPermittedWranglerRole(c *Context, user *model.User, channelMember *model.ChannelMember) bool { + // If there are no configured PermittedWranglerRoles, skip the check + if len(c.App.Config().WranglerSettings.PermittedWranglerRoles) == 0 { + return true + } + + userRoles := user.Roles + " " + channelMember.Roles + for _, role := range c.App.Config().WranglerSettings.PermittedWranglerRoles { + if model.IsInRole(userRoles, role) { + return true + } + } + + return false +} diff --git a/server/channels/api4/post_test.go b/server/channels/api4/post_test.go index eaf0f40216..db0faf663b 100644 --- a/server/channels/api4/post_test.go +++ b/server/channels/api4/post_test.go @@ -761,16 +761,20 @@ func TestMoveThread(t *testing.T) { basicUser2 := th.BasicUser2 basicUser3 := th.CreateUser() - // Create a new public channel to move the post to - publicChannel, resp, err := client.CreateChannel(ctx, &model.Channel{ - TeamId: th.BasicTeam.Id, - Name: "test-public-channel", - DisplayName: "Test Public Channel", - Type: model.ChannelTypeOpen, - }) - require.NoError(t, err) - require.NotNil(t, resp) - require.NotNil(t, publicChannel) + // Helper function to create a new public channel to move the post to + createPublicChannel := func(teamId, name, displayName string) *model.Channel { + channel, resp, err := client.CreateChannel(ctx, &model.Channel{ + TeamId: teamId, + Name: name, + DisplayName: displayName, + Type: model.ChannelTypeOpen, + }) + require.NoError(t, err) + require.NotNil(t, resp) + require.NotNil(t, channel) + + return channel + } // Create a new private channel to move the post to privateChannel, resp, err := client.CreateChannel(ctx, &model.Channel{ @@ -795,6 +799,9 @@ func TestMoveThread(t *testing.T) { require.NotNil(t, resp) require.NotNil(t, gmChannel) t.Run("Move to public channel", func(t *testing.T) { + // Create a public channel + publicChannel := createPublicChannel(th.BasicTeam.Id, "test-public-channel", "Test Public Channel") + // Create a new post to move post := &model.Post{ ChannelId: th.BasicChannel.Id, @@ -971,6 +978,53 @@ func TestMoveThread(t *testing.T) { require.Equal(t, newPost.Message, posts.Posts[posts.Order[2]].Message) require.Equal(t, rootPost.Message, posts.Posts[posts.Order[3]].Message) }) + + t.Run("Move thread when permitted role is channel admin", func(t *testing.T) { + // Create public channel + publicChannel := createPublicChannel(th.BasicTeam.Id, "test-public-channel-admin", "Test Public Channel Admin") + + // Set permitted role as channel admin + enabled := true + th.App.UpdateConfig(func(cfg *model.Config) { + cfg.WranglerSettings = model.WranglerSettings{MoveThreadToAnotherTeamEnable: &enabled, + PermittedWranglerRoles: []string{model.PermissionsChannelAdmin}} + }) + defer th.App.UpdateConfig(func(cfg *model.Config) { + cfg.WranglerSettings = model.WranglerSettings{} + }) + + // Login as channel admin and add to channel + th.LoginTeamAdmin() + th.AddUserToChannel(th.TeamAdminUser, publicChannel) + defer th.LoginBasic() + + // Create a new post to move + post := &model.Post{ + ChannelId: th.BasicChannel.Id, + Message: "test post", + } + newPost, resp, err := client.CreatePost(ctx, post) + require.NoError(t, err) + require.NotNil(t, resp) + require.NotNil(t, newPost) + + // Move the post to the public channel + moveThreadParams := &model.MoveThreadParams{ + ChannelId: publicChannel.Id, + } + resp, err = client.MoveThread(ctx, newPost.Id, moveThreadParams) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + + // Check that the post was moved to the public channel + posts, resp, err := client.GetPostsForChannel(ctx, publicChannel.Id, 0, 100, "", true, false) + require.NoError(t, err) + require.NotNil(t, resp) + require.NotNil(t, posts) + // There should be 2 posts, the system join message for the user who moved it joining the channel, and the post we moved + require.Equal(t, 2, len(posts.Posts)) + require.Equal(t, newPost.Message, posts.Posts[posts.Order[0]].Message) + }) } func TestCreatePostPublic(t *testing.T) { diff --git a/webapp/channels/src/selectors/posts.ts b/webapp/channels/src/selectors/posts.ts index 7635ebb1bc..755e70df11 100644 --- a/webapp/channels/src/selectors/posts.ts +++ b/webapp/channels/src/selectors/posts.ts @@ -11,7 +11,7 @@ import {getCurrentUser} from 'mattermost-redux/selectors/entities/common'; import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {getPost} from 'mattermost-redux/selectors/entities/posts'; import {moveThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; +import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users'; import {arePreviewsCollapsed} from 'selectors/preferences'; import {getGlobalItem} from 'selectors/storage'; @@ -64,10 +64,11 @@ export function makeCanWrangler() { 'makeCanWrangler', getConfig, getCurrentUser, + getCurrentUserRoles, moveThreadsEnabled, (_state: GlobalState, channelType: Channel['type']) => channelType, (_state: GlobalState, _channelType: Channel['type'], replyCount: number) => replyCount, - (config: Partial, user: UserProfile, enabled: boolean, channelType: Channel['type'], replyCount: number) => { + (config: Partial, user: UserProfile, userRoles: string, enabled: boolean, channelType: Channel['type'], replyCount: number) => { if (!enabled) { return false; } @@ -90,8 +91,8 @@ export function makeCanWrangler() { allowedEmailDomains = WranglerAllowedEmailDomain?.split(',') || []; } - if (permittedUsers.length > 0 && !user.roles.includes('system_admin')) { - const roles = user.roles.split(' '); + if (permittedUsers.length > 0 && !userRoles.includes('system_admin')) { + const roles = userRoles.split(' '); const hasRole = roles.some((role) => permittedUsers.includes(role)); if (!hasRole) { return false;