From 4b8b1e5ca02eeae904d42395e6c2c66762fb3b59 Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Thu, 22 Jan 2026 15:48:51 +0530 Subject: [PATCH] Cherry picker search-api-filter-guest-permission to release-10.11 (#35018) Automatic Merge --- server/channels/app/file.go | 66 ++++++++++++++- server/channels/app/file_test.go | 139 +++++++++++++++++++++++++++++++ server/channels/app/post.go | 60 +++++++++++++ server/channels/app/post_test.go | 133 +++++++++++++++++++++++++++++ 4 files changed, 397 insertions(+), 1 deletion(-) diff --git a/server/channels/app/file.go b/server/channels/app/file.go index 92ce4e239f..aa1a79a014 100644 --- a/server/channels/app/file.go +++ b/server/channels/app/file.go @@ -12,6 +12,7 @@ import ( "fmt" "image" "io" + "maps" "math" "net/http" "net/url" @@ -19,6 +20,7 @@ import ( "path" "path/filepath" "regexp" + "slices" "strconv" "strings" "sync" @@ -1501,7 +1503,69 @@ func (a *App) SearchFilesInTeamForUser(c request.CTX, terms string, userId strin } } - return fileInfoSearchResults, a.filterInaccessibleFiles(fileInfoSearchResults, filterFileOptions{assumeSortedCreatedAt: true}) + if appErr := a.filterInaccessibleFiles(fileInfoSearchResults, filterFileOptions{assumeSortedCreatedAt: true}); appErr != nil { + return nil, appErr + } + + if appErr := a.FilterFilesByChannelPermissions(c, fileInfoSearchResults, userId); appErr != nil { + return nil, appErr + } + + return fileInfoSearchResults, nil +} + +func (a *App) FilterFilesByChannelPermissions(rctx request.CTX, fileList *model.FileInfoList, userID string) *model.AppError { + if fileList == nil || fileList.FileInfos == nil || len(fileList.FileInfos) == 0 { + return nil + } + + channels := make(map[string]*model.Channel) + for _, fileInfo := range fileList.FileInfos { + if fileInfo.ChannelId != "" { + channels[fileInfo.ChannelId] = nil + } + } + + if len(channels) > 0 { + channelIDs := slices.Collect(maps.Keys(channels)) + channelList, err := a.GetChannels(rctx, channelIDs) + if err != nil && err.StatusCode != http.StatusNotFound { + return err + } + for _, channel := range channelList { + channels[channel.Id] = channel + } + } + + channelReadPermission := make(map[string]bool) + filteredFiles := make(map[string]*model.FileInfo) + filteredOrder := []string{} + + for _, fileID := range fileList.Order { + fileInfo, ok := fileList.FileInfos[fileID] + if !ok { + continue + } + + if _, ok := channelReadPermission[fileInfo.ChannelId]; !ok { + channel := channels[fileInfo.ChannelId] + allowed := false + if channel != nil { + allowed = a.HasPermissionToReadChannel(rctx, userID, channel) + } + channelReadPermission[fileInfo.ChannelId] = allowed + } + + if channelReadPermission[fileInfo.ChannelId] { + filteredFiles[fileID] = fileInfo + filteredOrder = append(filteredOrder, fileID) + } + } + + fileList.FileInfos = filteredFiles + fileList.Order = filteredOrder + + return nil } func (a *App) ExtractContentFromFileInfo(rctx request.CTX, fileInfo *model.FileInfo) error { diff --git a/server/channels/app/file_test.go b/server/channels/app/file_test.go index e3cdfa659a..cc30ae1ffa 100644 --- a/server/channels/app/file_test.go +++ b/server/channels/app/file_test.go @@ -5,6 +5,7 @@ package app import ( "archive/zip" + "context" "errors" "fmt" "image" @@ -816,3 +817,141 @@ func TestPermanentDeleteFilesByPost(t *testing.T) { assert.Nil(t, err) }) } + +func TestFilterFilesByChannelPermissions(t *testing.T) { + mainHelper.Parallel(t) + th := Setup(t).InitBasic() + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.GuestAccountsSettings.Enable = true + }) + + guestUser := th.CreateGuest() + _, _, appErr := th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, guestUser.Id, "") + require.Nil(t, appErr) + + privateChannel := th.CreatePrivateChannel(th.Context, th.BasicTeam) + + _, appErr = th.App.AddUserToChannel(th.Context, guestUser, privateChannel, false) + require.Nil(t, appErr) + _, appErr = th.App.AddUserToChannel(th.Context, guestUser, th.BasicChannel, false) + require.Nil(t, appErr) + + post1 := th.CreatePost(th.BasicChannel) + post2 := th.CreatePost(privateChannel) + post3 := th.CreatePost(th.BasicChannel) + + fileInfo1 := th.CreateFileInfo(th.BasicUser.Id, post1.Id, th.BasicChannel.Id) + fileInfo2 := th.CreateFileInfo(th.BasicUser.Id, post2.Id, privateChannel.Id) + fileInfo3 := th.CreateFileInfo(th.BasicUser.Id, post3.Id, th.BasicChannel.Id) + + t.Run("should filter files when user has read_channel_content permission", func(t *testing.T) { + fileList := model.NewFileInfoList() + fileList.FileInfos[fileInfo1.Id] = fileInfo1 + fileList.FileInfos[fileInfo2.Id] = fileInfo2 + fileList.FileInfos[fileInfo3.Id] = fileInfo3 + fileList.Order = []string{fileInfo1.Id, fileInfo2.Id, fileInfo3.Id} + + // BasicUser should have access to all files + appErr := th.App.FilterFilesByChannelPermissions(th.Context, fileList, th.BasicUser.Id) + require.Nil(t, appErr) + require.Len(t, fileList.FileInfos, 3) + require.Len(t, fileList.Order, 3) + }) + + t.Run("should filter files when guest has read_channel_content permission", func(t *testing.T) { + fileList := model.NewFileInfoList() + fileList.FileInfos[fileInfo1.Id] = fileInfo1 + fileList.FileInfos[fileInfo2.Id] = fileInfo2 + fileList.FileInfos[fileInfo3.Id] = fileInfo3 + fileList.Order = []string{fileInfo1.Id, fileInfo2.Id, fileInfo3.Id} + + appErr := th.App.FilterFilesByChannelPermissions(th.Context, fileList, guestUser.Id) + require.Nil(t, appErr) + require.Len(t, fileList.FileInfos, 3) + require.Len(t, fileList.Order, 3) + }) + + t.Run("should filter files when guest does not have read_channel_content permission", func(t *testing.T) { + channelGuestRole, appErr := th.App.GetRoleByName(context.Background(), model.ChannelGuestRoleId) + require.Nil(t, appErr) + + originalPermissions := make([]string, len(channelGuestRole.Permissions)) + copy(originalPermissions, channelGuestRole.Permissions) + + newPermissions := []string{} + for _, perm := range channelGuestRole.Permissions { + if perm != model.PermissionReadChannelContent.Id && perm != model.PermissionReadChannel.Id { + newPermissions = append(newPermissions, perm) + } + } + + _, appErr = th.App.PatchRole(channelGuestRole, &model.RolePatch{ + Permissions: &newPermissions, + }) + require.Nil(t, appErr) + + defer func() { + _, err := th.App.PatchRole(channelGuestRole, &model.RolePatch{ + Permissions: &originalPermissions, + }) + require.Nil(t, err) + }() + + fileList := model.NewFileInfoList() + fileList.FileInfos[fileInfo1.Id] = fileInfo1 + fileList.FileInfos[fileInfo2.Id] = fileInfo2 + fileList.FileInfos[fileInfo3.Id] = fileInfo3 + fileList.Order = []string{fileInfo1.Id, fileInfo2.Id, fileInfo3.Id} + + appErr = th.App.FilterFilesByChannelPermissions(th.Context, fileList, guestUser.Id) + require.Nil(t, appErr) + require.Len(t, fileList.FileInfos, 0) + require.Len(t, fileList.Order, 0) + }) + + t.Run("should handle empty file list", func(t *testing.T) { + fileList := model.NewFileInfoList() + appErr := th.App.FilterFilesByChannelPermissions(th.Context, fileList, th.BasicUser.Id) + require.Nil(t, appErr) + require.Len(t, fileList.FileInfos, 0) + require.Len(t, fileList.Order, 0) + }) + + t.Run("should handle nil file list", func(t *testing.T) { + appErr := th.App.FilterFilesByChannelPermissions(th.Context, nil, th.BasicUser.Id) + require.Nil(t, appErr) + }) + + t.Run("should handle files with empty channel IDs", func(t *testing.T) { + fileList := model.NewFileInfoList() + fileWithoutChannel := &model.FileInfo{ + Id: model.NewId(), + ChannelId: "", + Name: "test.txt", + } + fileList.FileInfos[fileWithoutChannel.Id] = fileWithoutChannel + fileList.Order = []string{fileWithoutChannel.Id} + + appErr := th.App.FilterFilesByChannelPermissions(th.Context, fileList, th.BasicUser.Id) + require.Nil(t, appErr) + require.Len(t, fileList.FileInfos, 0) + require.Len(t, fileList.Order, 0) + }) + + t.Run("should handle files from non-existent channels", func(t *testing.T) { + fileList := model.NewFileInfoList() + fileWithInvalidChannel := &model.FileInfo{ + Id: model.NewId(), + ChannelId: model.NewId(), + Name: "test.txt", + } + fileList.FileInfos[fileWithInvalidChannel.Id] = fileWithInvalidChannel + fileList.Order = []string{fileWithInvalidChannel.Id} + + appErr := th.App.FilterFilesByChannelPermissions(th.Context, fileList, th.BasicUser.Id) + require.Nil(t, appErr) + require.Len(t, fileList.FileInfos, 0) + require.Len(t, fileList.Order, 0) + }) +} diff --git a/server/channels/app/post.go b/server/channels/app/post.go index d01b91a77b..90c8c481b4 100644 --- a/server/channels/app/post.go +++ b/server/channels/app/post.go @@ -8,8 +8,10 @@ import ( "encoding/json" "errors" "fmt" + "maps" "net/http" "regexp" + "slices" "strconv" "strings" "sync" @@ -1764,9 +1766,67 @@ func (a *App) SearchPostsForUser(c request.CTX, terms string, userID string, tea return nil, appErr } + if appErr := a.FilterPostsByChannelPermissions(c, postSearchResults.PostList, userID); appErr != nil { + return nil, appErr + } + return postSearchResults, nil } +func (a *App) FilterPostsByChannelPermissions(rctx request.CTX, postList *model.PostList, userID string) *model.AppError { + if postList == nil || postList.Posts == nil || len(postList.Posts) == 0 { + return nil + } + + channels := make(map[string]*model.Channel) + for _, post := range postList.Posts { + if post.ChannelId != "" { + channels[post.ChannelId] = nil + } + } + + if len(channels) > 0 { + channelIDs := slices.Collect(maps.Keys(channels)) + channelList, err := a.GetChannels(rctx, channelIDs) + if err != nil && err.StatusCode != http.StatusNotFound { + return err + } + for _, channel := range channelList { + channels[channel.Id] = channel + } + } + + channelReadPermission := make(map[string]bool) + filteredPosts := make(map[string]*model.Post) + filteredOrder := []string{} + + for _, postID := range postList.Order { + post, ok := postList.Posts[postID] + if !ok { + continue + } + + if _, ok := channelReadPermission[post.ChannelId]; !ok { + channel := channels[post.ChannelId] + allowed := false + if channel != nil { + allowed = a.HasPermissionToReadChannel(rctx, userID, channel) + } + channelReadPermission[post.ChannelId] = allowed + } + + if channelReadPermission[post.ChannelId] { + filteredPosts[postID] = post + filteredOrder = append(filteredOrder, postID) + } + } + + postList.Posts = filteredPosts + postList.Order = filteredOrder + + return nil +} + func (a *App) GetFileInfosForPostWithMigration(rctx request.CTX, postID string, includeDeleted bool) ([]*model.FileInfo, *model.AppError) { pchan := make(chan store.StoreResult[*model.Post], 1) go func() { diff --git a/server/channels/app/post_test.go b/server/channels/app/post_test.go index cabdc8c29a..4bac0e87ac 100644 --- a/server/channels/app/post_test.go +++ b/server/channels/app/post_test.go @@ -4216,3 +4216,136 @@ func TestPopulateEditHistoryFileMetadata(t *testing.T) { require.Greater(t, post2.Metadata.Files[0].DeleteAt, int64(0)) }) } + +func TestFilterPostsByChannelPermissions(t *testing.T) { + mainHelper.Parallel(t) + th := Setup(t).InitBasic() + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.GuestAccountsSettings.Enable = true + }) + + guestUser := th.CreateGuest() + _, _, appErr := th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, guestUser.Id, "") + require.Nil(t, appErr) + + privateChannel := th.CreatePrivateChannel(th.Context, th.BasicTeam) + + _, appErr = th.App.AddUserToChannel(th.Context, guestUser, privateChannel, false) + require.Nil(t, appErr) + _, appErr = th.App.AddUserToChannel(th.Context, guestUser, th.BasicChannel, false) + require.Nil(t, appErr) + + post1 := th.CreatePost(th.BasicChannel) + post2 := th.CreatePost(privateChannel) + post3 := th.CreatePost(th.BasicChannel) + + t.Run("should filter posts when user has read_channel_content permission", func(t *testing.T) { + postList := model.NewPostList() + postList.Posts[post1.Id] = post1 + postList.Posts[post2.Id] = post2 + postList.Posts[post3.Id] = post3 + postList.Order = []string{post1.Id, post2.Id, post3.Id} + + appErr := th.App.FilterPostsByChannelPermissions(th.Context, postList, th.BasicUser.Id) + require.Nil(t, appErr) + require.Len(t, postList.Posts, 3) + require.Len(t, postList.Order, 3) + }) + + t.Run("should filter posts when guest has read_channel_content permission", func(t *testing.T) { + postList := model.NewPostList() + postList.Posts[post1.Id] = post1 + postList.Posts[post2.Id] = post2 + postList.Posts[post3.Id] = post3 + postList.Order = []string{post1.Id, post2.Id, post3.Id} + + appErr := th.App.FilterPostsByChannelPermissions(th.Context, postList, guestUser.Id) + require.Nil(t, appErr) + require.Len(t, postList.Posts, 3) + require.Len(t, postList.Order, 3) + }) + + t.Run("should filter posts when guest does not have read_channel_content permission", func(t *testing.T) { + channelGuestRole, appErr := th.App.GetRoleByName(context.Background(), model.ChannelGuestRoleId) + require.Nil(t, appErr) + + originalPermissions := make([]string, len(channelGuestRole.Permissions)) + copy(originalPermissions, channelGuestRole.Permissions) + + newPermissions := []string{} + for _, perm := range channelGuestRole.Permissions { + if perm != model.PermissionReadChannelContent.Id && perm != model.PermissionReadChannel.Id { + newPermissions = append(newPermissions, perm) + } + } + + _, appErr = th.App.PatchRole(channelGuestRole, &model.RolePatch{ + Permissions: &newPermissions, + }) + require.Nil(t, appErr) + + defer func() { + _, err := th.App.PatchRole(channelGuestRole, &model.RolePatch{ + Permissions: &originalPermissions, + }) + require.Nil(t, err) + }() + + postList := model.NewPostList() + postList.Posts[post1.Id] = post1 + postList.Posts[post2.Id] = post2 + postList.Posts[post3.Id] = post3 + postList.Order = []string{post1.Id, post2.Id, post3.Id} + + appErr = th.App.FilterPostsByChannelPermissions(th.Context, postList, guestUser.Id) + require.Nil(t, appErr) + require.Len(t, postList.Posts, 0) + require.Len(t, postList.Order, 0) + }) + + t.Run("should handle empty post list", func(t *testing.T) { + postList := model.NewPostList() + appErr := th.App.FilterPostsByChannelPermissions(th.Context, postList, th.BasicUser.Id) + require.Nil(t, appErr) + require.Len(t, postList.Posts, 0) + require.Len(t, postList.Order, 0) + }) + + t.Run("should handle nil post list", func(t *testing.T) { + appErr := th.App.FilterPostsByChannelPermissions(th.Context, nil, th.BasicUser.Id) + require.Nil(t, appErr) + }) + + t.Run("should handle posts with empty channel IDs", func(t *testing.T) { + postList := model.NewPostList() + postWithoutChannel := &model.Post{ + Id: model.NewId(), + ChannelId: "", + Message: "test", + } + postList.Posts[postWithoutChannel.Id] = postWithoutChannel + postList.Order = []string{postWithoutChannel.Id} + + appErr := th.App.FilterPostsByChannelPermissions(th.Context, postList, th.BasicUser.Id) + require.Nil(t, appErr) + require.Len(t, postList.Posts, 0) + require.Len(t, postList.Order, 0) + }) + + t.Run("should handle posts from non-existent channels", func(t *testing.T) { + postList := model.NewPostList() + postWithInvalidChannel := &model.Post{ + Id: model.NewId(), + ChannelId: model.NewId(), + Message: "test", + } + postList.Posts[postWithInvalidChannel.Id] = postWithInvalidChannel + postList.Order = []string{postWithInvalidChannel.Id} + + appErr := th.App.FilterPostsByChannelPermissions(th.Context, postList, th.BasicUser.Id) + require.Nil(t, appErr) + require.Len(t, postList.Posts, 0) + require.Len(t, postList.Order, 0) + }) +}