diff --git a/.circleci/config.yml b/.circleci/config.yml index e61ee7343b..1f9739e5d8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -66,14 +66,37 @@ jobs: sleep 5 done if [[ -f "$FILE_DIST" ]]; then + echo "Precompiled version of web app found" mkdir dist && tar -xf $FILE_DIST -C dist --strip-components=1 else - npm ci --silent && make build + echo "Building web app from source" + make dist fi - persist_to_workspace: root: /mnt/ramdisk paths: - mattermost-webapp + setup-focalboard-product: + working_directory: /mnt/ramdisk/ + docker: + - image: cimg/go:1.18-node + resource_class: large + steps: + - attach_workspace: + at: /mnt/ramdisk + - run: | + cd focalboard + echo "Building Boards product for web app" + # make prebuild build-product # TODO figure out how to get this to run without bypassing the Makefile + make prebuild + cd mattermost-plugin/webapp + npm run build:product + cd ../.. + - persist_to_workspace: + root: /mnt/ramdisk + paths: + - focalboard/mattermost-plugin/webapp/dist + check-app-layers: docker: - image: cimg/go:1.18 @@ -99,7 +122,7 @@ jobs: command: | cd mattermost-server make modules-tidy - if [[ -n $(git status --porcelain) ]]; then echo "Please tidy up the Go modules using make modules-tidy"; exit 1; fi + if [[ -n $(git status --porcelain) ]]; then echo "Please tidy up the Go modules using make modules-tidy"; exit 1; fi check-store-layers: docker: - image: cimg/go:1.18 @@ -460,6 +483,14 @@ workflows: ignore: - master - /^release-.*/ + - setup-focalboard-product: + filters: + branches: + ignore: + - master + - /^release-.*/ + requires: + - setup-multi-product-repositories - check-go-mod-tidy: requires: - setup-multi-product-repositories @@ -503,6 +534,7 @@ workflows: - check-migrations - build-api-spec - setup-mattermost-webapp + - setup-focalboard-product - build-focalboard: requires: - check-go-mod-tidy @@ -532,7 +564,8 @@ workflows: - check-mattermost-vet - check-migrations - build-api-spec - - setup-mattermost-webapp + - setup-mattermost-webapp + - setup-focalboard-product - test: name: test-postgres-normal dbdriver: postgres @@ -549,7 +582,8 @@ workflows: - check-mattermost-vet - check-migrations - build-api-spec - - setup-mattermost-webapp + - setup-mattermost-webapp + - setup-focalboard-product - test: name: test-postgres-binary dbdriver: postgres @@ -566,7 +600,8 @@ workflows: - check-mattermost-vet - check-migrations - build-api-spec - - setup-mattermost-webapp + - setup-mattermost-webapp + - setup-focalboard-product - test-schema: requires: - check-go-mod-tidy @@ -580,6 +615,7 @@ workflows: - check-migrations - build-api-spec - setup-mattermost-webapp + - setup-focalboard-product - upload-artifacts-to-s3: context: mattermost-ci-pr-builds-s3 requires: @@ -605,6 +641,14 @@ workflows: - master - /^release-.*/ - cloud + - setup-focalboard-product: + filters: + branches: + ignore: + - master + - /^release-.*/ + requires: + - setup-multi-product-repositories - check-go-mod-tidy: requires: - setup-multi-product-repositories @@ -652,6 +696,7 @@ workflows: - check-migrations - build-api-spec - setup-mattermost-webapp + - setup-focalboard-product - build-focalboard: requires: - check-go-mod-tidy diff --git a/api4/apitestlib.go b/api4/apitestlib.go index 9b5e53abfc..4606541277 100644 --- a/api4/apitestlib.go +++ b/api4/apitestlib.go @@ -71,6 +71,7 @@ type TestHelper struct { IncludeCacheLayer bool + LogBuffer *mlog.Buffer TestLogger *mlog.Logger } @@ -119,11 +120,16 @@ func setupTestHelper(dbStore store.Store, searchEngine *searchengine.Broker, ent options = append(options, app.StoreOverride(dbStore)) } + buffer := &mlog.Buffer{} + testLogger, _ := mlog.NewLogger() logCfg, _ := config.MloggerConfigFromLoggerConfig(&memoryConfig.LogSettings, nil, config.GetLogFileLocation) if errCfg := testLogger.ConfigureTargets(logCfg, nil); errCfg != nil { panic("failed to configure test logger: " + errCfg.Error()) } + if errW := mlog.AddWriterTarget(testLogger, buffer, true, mlog.StdAll...); errW != nil { + panic("failed to add writer target to test logger: " + errW.Error()) + } // lock logger config so server init cannot override it during testing. testLogger.LockConfiguration() options = append(options, app.SetLogger(testLogger)) @@ -140,6 +146,7 @@ func setupTestHelper(dbStore store.Store, searchEngine *searchengine.Broker, ent IncludeCacheLayer: includeCache, Context: request.EmptyContext(testLogger), TestLogger: testLogger, + LogBuffer: buffer, } th.Context.SetLogger(testLogger) diff --git a/api4/cloud_test.go b/api4/cloud_test.go index 2092342c89..15ff7b1831 100644 --- a/api4/cloud_test.go +++ b/api4/cloud_test.go @@ -515,8 +515,9 @@ func TestGetCloudProducts(t *testing.T) { SKU: "sku", PriceID: "price_id", Family: "family", - RecurringInterval: "recurring_interval", + RecurringInterval: "monthly", BillingScheme: "billing_scheme", + CrossSellsTo: "", }, { ID: "prod_test2", @@ -526,8 +527,9 @@ func TestGetCloudProducts(t *testing.T) { SKU: "sku2", PriceID: "price_id2", Family: "family2", - RecurringInterval: "recurring_interval2", + RecurringInterval: "monthly", BillingScheme: "billing_scheme2", + CrossSellsTo: "prod_test3", }, { ID: "prod_test3", @@ -537,29 +539,36 @@ func TestGetCloudProducts(t *testing.T) { SKU: "sku3", PriceID: "price_id3", Family: "family3", - RecurringInterval: "recurring_interval3", + RecurringInterval: "yearly", BillingScheme: "billing_scheme3", + CrossSellsTo: "prod_test2", }, } sanitizedProducts := []*model.Product{ { - ID: "prod_test1", - Name: "name", - PricePerSeat: 10, - SKU: "sku", + ID: "prod_test1", + Name: "name", + PricePerSeat: 10, + SKU: "sku", + RecurringInterval: "monthly", + CrossSellsTo: "", }, { - ID: "prod_test2", - Name: "name2", - PricePerSeat: 100, - SKU: "sku2", + ID: "prod_test2", + Name: "name2", + PricePerSeat: 100, + SKU: "sku2", + RecurringInterval: "monthly", + CrossSellsTo: "prod_test3", }, { - ID: "prod_test3", - Name: "name3", - PricePerSeat: 1000, - SKU: "sku3", + ID: "prod_test3", + Name: "name3", + PricePerSeat: 1000, + SKU: "sku3", + RecurringInterval: "yearly", + CrossSellsTo: "prod_test2", }, } t.Run("get products for admins", func(t *testing.T) { @@ -615,8 +624,9 @@ func TestGetCloudProducts(t *testing.T) { require.Equal(t, returnedProducts[0].Description, "") require.Equal(t, returnedProducts[0].PriceID, "") require.Equal(t, returnedProducts[0].Family, model.SubscriptionFamily("")) - require.Equal(t, returnedProducts[0].RecurringInterval, model.RecurringInterval("")) + require.Equal(t, returnedProducts[0].RecurringInterval, model.RecurringInterval("monthly")) require.Equal(t, returnedProducts[0].BillingScheme, model.BillingScheme("")) + require.Equal(t, returnedProducts[0].CrossSellsTo, "") require.Equal(t, returnedProducts[1].ID, "prod_test2") require.Equal(t, returnedProducts[1].Name, "name2") @@ -625,8 +635,9 @@ func TestGetCloudProducts(t *testing.T) { require.Equal(t, returnedProducts[1].Description, "") require.Equal(t, returnedProducts[1].PriceID, "") require.Equal(t, returnedProducts[1].Family, model.SubscriptionFamily("")) - require.Equal(t, returnedProducts[1].RecurringInterval, model.RecurringInterval("")) + require.Equal(t, returnedProducts[1].RecurringInterval, model.RecurringInterval("monthly")) require.Equal(t, returnedProducts[1].BillingScheme, model.BillingScheme("")) + require.Equal(t, returnedProducts[1].CrossSellsTo, "prod_test3") require.Equal(t, returnedProducts[2].ID, "prod_test3") require.Equal(t, returnedProducts[2].Name, "name3") @@ -635,7 +646,8 @@ func TestGetCloudProducts(t *testing.T) { require.Equal(t, returnedProducts[2].Description, "") require.Equal(t, returnedProducts[2].PriceID, "") require.Equal(t, returnedProducts[2].Family, model.SubscriptionFamily("")) - require.Equal(t, returnedProducts[2].RecurringInterval, model.RecurringInterval("")) + require.Equal(t, returnedProducts[2].RecurringInterval, model.RecurringInterval("yearly")) require.Equal(t, returnedProducts[2].BillingScheme, model.BillingScheme("")) + require.Equal(t, returnedProducts[2].CrossSellsTo, "prod_test2") }) } diff --git a/api4/group.go b/api4/group.go index abcde27459..da17d31ce3 100644 --- a/api4/group.go +++ b/api4/group.go @@ -105,9 +105,15 @@ func getGroup(c *Context, w http.ResponseWriter, r *http.Request) { return } + restrictions, appErr := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId) + if appErr != nil { + c.Err = appErr + return + } + group, appErr := c.App.GetGroup(c.Params.GroupId, &model.GetGroupOpts{ IncludeMemberCount: c.Params.IncludeMemberCount, - }) + }, restrictions) if appErr != nil { c.Err = appErr return @@ -206,7 +212,7 @@ func patchGroup(c *Context, w http.ResponseWriter, r *http.Request) { return } - group, appErr := c.App.GetGroup(c.Params.GroupId, nil) + group, appErr := c.App.GetGroup(c.Params.GroupId, nil, nil) if appErr != nil { c.Err = appErr return @@ -321,7 +327,7 @@ func linkGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) { return } - group, appErr := c.App.GetGroup(c.Params.GroupId, nil) + group, appErr := c.App.GetGroup(c.Params.GroupId, nil, nil) if appErr != nil { c.Err = appErr return @@ -657,7 +663,7 @@ func getGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) { return } - group, appErr := c.App.GetGroup(c.Params.GroupId, nil) + group, appErr := c.App.GetGroup(c.Params.GroupId, nil, nil) if appErr != nil { c.Err = appErr return @@ -675,7 +681,13 @@ func getGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) { return } - members, count, appErr := c.App.GetGroupMemberUsersPage(c.Params.GroupId, c.Params.Page, c.Params.PerPage) + restrictions, appErr := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId) + if appErr != nil { + c.Err = appErr + return + } + + members, count, appErr := c.App.GetGroupMemberUsersPage(c.Params.GroupId, c.Params.Page, c.Params.PerPage, restrictions) if appErr != nil { c.Err = appErr return @@ -718,7 +730,7 @@ func getGroupStats(c *Context, w http.ResponseWriter, r *http.Request) { } groupID := c.Params.GroupId - count, appErr := c.App.GetGroupMemberCount(groupID) + count, appErr := c.App.GetGroupMemberCount(groupID, nil) if appErr != nil { c.Err = appErr return @@ -1011,12 +1023,33 @@ func getGroups(c *Context, w http.ResponseWriter, r *http.Request) { opts.Since = since } - groups, appErr := c.App.GetGroups(c.Params.Page, c.Params.PerPage, opts) + restrictions, appErr := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId) if appErr != nil { c.Err = appErr return } + var ( + groups = []*model.Group{} + canSee bool = true + ) + + if opts.FilterHasMember != "" { + canSee, appErr = c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, opts.FilterHasMember) + if appErr != nil { + c.Err = appErr + return + } + } + + if canSee { + groups, appErr = c.App.GetGroups(c.Params.Page, c.Params.PerPage, opts, restrictions) + if appErr != nil { + c.Err = appErr + return + } + } + var ( b []byte err error @@ -1055,7 +1088,7 @@ func deleteGroup(c *Context, w http.ResponseWriter, r *http.Request) { return } - group, err := c.App.GetGroup(c.Params.GroupId, nil) + group, err := c.App.GetGroup(c.Params.GroupId, nil, nil) if err != nil { c.Err = err return @@ -1103,7 +1136,7 @@ func addGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) { return } - group, appErr := c.App.GetGroup(c.Params.GroupId, nil) + group, appErr := c.App.GetGroup(c.Params.GroupId, nil, nil) if appErr != nil { c.Err = appErr return @@ -1162,7 +1195,7 @@ func deleteGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) { return } - group, appErr := c.App.GetGroup(c.Params.GroupId, nil) + group, appErr := c.App.GetGroup(c.Params.GroupId, nil, nil) if appErr != nil { c.Err = appErr return diff --git a/api4/group_test.go b/api4/group_test.go index b6f155ad1c..bbe4fa33c4 100644 --- a/api4/group_test.go +++ b/api4/group_test.go @@ -1518,7 +1518,7 @@ func TestAddMembersToGroup(t *testing.T) { assert.Len(t, groupMembers, 2) - count, countErr := th.App.GetGroupMemberCount(group.Id) + count, countErr := th.App.GetGroupMemberCount(group.Id, nil) assert.Nil(t, countErr) assert.Equal(t, count, int64(2)) diff --git a/api4/team_local.go b/api4/team_local.go index 8f35695e56..ff9b4da4b3 100644 --- a/api4/team_local.go +++ b/api4/team_local.go @@ -160,10 +160,10 @@ func localInviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request) if len(goodEmails) > 0 { var invitesWithErrors2 []*model.EmailInviteWithError if len(channels) > 0 { - invitesWithErrors2, err = c.App.Srv().EmailService.SendInviteEmailsToTeamAndChannels(team, channels, "Administrator", "mmctl "+model.NewId(), nil, goodEmails, *c.App.Config().ServiceSettings.SiteURL, nil, memberInvite.Message, true) + invitesWithErrors2, err = c.App.Srv().EmailService.SendInviteEmailsToTeamAndChannels(team, channels, "Administrator", "mmctl "+model.NewId(), nil, goodEmails, *c.App.Config().ServiceSettings.SiteURL, nil, memberInvite.Message, true, true, false) invitesWithErrors = append(invitesWithErrors, invitesWithErrors2...) } else { - err = c.App.Srv().EmailService.SendInviteEmails(team, "Administrator", "mmctl "+model.NewId(), goodEmails, *c.App.Config().ServiceSettings.SiteURL, nil, false) + err = c.App.Srv().EmailService.SendInviteEmails(team, "Administrator", "mmctl "+model.NewId(), goodEmails, *c.App.Config().ServiceSettings.SiteURL, nil, false, true, false) } if err != nil { @@ -200,7 +200,7 @@ func localInviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request) c.Err = model.NewAppError("localInviteUsersToTeam", "api.team.invite_members.invalid_email.app_error", map[string]any{"Addresses": s}, "", http.StatusBadRequest) return } - err := c.App.Srv().EmailService.SendInviteEmails(team, "Administrator", "mmctl "+model.NewId(), emailList, *c.App.Config().ServiceSettings.SiteURL, nil, false) + err := c.App.Srv().EmailService.SendInviteEmails(team, "Administrator", "mmctl "+model.NewId(), emailList, *c.App.Config().ServiceSettings.SiteURL, nil, false, true, false) if err != nil { switch { case errors.Is(err, email.NoRateLimiterError): diff --git a/api4/user.go b/api4/user.go index 423edc755c..69a7c6214a 100644 --- a/api4/user.go +++ b/api4/user.go @@ -829,7 +829,7 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { return } - profiles, _, appErr = c.App.GetGroupMemberUsersPage(inGroupId, c.Params.Page, c.Params.PerPage) + profiles, _, appErr = c.App.GetGroupMemberUsersPage(inGroupId, c.Params.Page, c.Params.PerPage, userGetOptions.ViewRestrictions) if appErr != nil { c.Err = appErr return @@ -842,7 +842,7 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { return } - profiles, appErr = c.App.GetUsersNotInGroupPage(notInGroupId, c.Params.Page, c.Params.PerPage) + profiles, appErr = c.App.GetUsersNotInGroupPage(notInGroupId, c.Params.Page, c.Params.PerPage, userGetOptions.ViewRestrictions) if appErr != nil { c.Err = appErr return @@ -876,7 +876,7 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { } func requireGroupAccess(c *web.Context, groupID string) *model.AppError { - group, err := c.App.GetGroup(groupID, nil) + group, err := c.App.GetGroup(groupID, nil, nil) if err != nil { return err } diff --git a/api4/user_test.go b/api4/user_test.go index 7f937fa7fc..80bdea263e 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -2717,13 +2717,26 @@ func TestGetUsersInGroup(t *testing.T) { }) assert.Nil(t, appErr) + cid := model.NewId() + customGroup, appErr := th.App.CreateGroup(&model.Group{ + DisplayName: "dn-foo_" + cid, + Name: model.NewString("name" + cid), + Source: model.GroupSourceCustom, + Description: "description_" + cid, + RemoteId: model.NewString(model.NewId()), + }) + assert.Nil(t, appErr) + + user1, err := th.App.CreateUser(th.Context, &model.User{Email: th.GenerateTestEmail(), Nickname: "test user1", Password: "test-password-1", Username: "test-user-1", Roles: model.SystemUserRoleId}) + assert.Nil(t, err) + t.Run("Requires ldap license", func(t *testing.T) { _, response, err := th.SystemAdminClient.GetUsersInGroup(group.Id, 0, 60, "") require.Error(t, err) CheckForbiddenStatus(t, response) }) - th.App.Srv().SetLicense(model.NewTestLicense("ldap")) + th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional)) t.Run("Requires manage system permission to access users in group", func(t *testing.T) { th.Client.Login(th.BasicUser.Email, th.BasicUser.Password) @@ -2732,8 +2745,6 @@ func TestGetUsersInGroup(t *testing.T) { CheckForbiddenStatus(t, response) }) - user1, err := th.App.CreateUser(th.Context, &model.User{Email: th.GenerateTestEmail(), Nickname: "test user1", Password: "test-password-1", Username: "test-user-1", Roles: model.SystemUserRoleId}) - assert.Nil(t, err) _, err = th.App.UpsertGroupMember(group.Id, user1.Id) assert.Nil(t, err) @@ -2748,6 +2759,26 @@ func TestGetUsersInGroup(t *testing.T) { require.NoError(t, err) assert.Empty(t, users) }) + + _, err = th.App.UpsertGroupMember(customGroup.Id, user1.Id) + assert.Nil(t, err) + + t.Run("Returns users in custom group when called by regular user", func(t *testing.T) { + th.Client.Login(th.BasicUser.Email, th.BasicUser.Password) + users, _, err := th.Client.GetUsersInGroup(customGroup.Id, 0, 60, "") + require.NoError(t, err) + assert.Equal(t, users[0].Id, user1.Id) + }) + + t.Run("Returns no users in custom group when called by guest user", func(t *testing.T) { + th.Client.Login(th.BasicUser.Email, th.BasicUser.Password) + th.App.DemoteUserToGuest(th.Context, th.BasicUser) + + users, _, err := th.Client.GetUsersInGroup(customGroup.Id, 0, 60, "") + require.NoError(t, err) + assert.Equal(t, len(users), 0) + }) + } func TestUpdateUserMfa(t *testing.T) { diff --git a/api4/websocket.go b/api4/websocket.go index 540188be8a..5f1cb2cdd3 100644 --- a/api4/websocket.go +++ b/api4/websocket.go @@ -32,7 +32,7 @@ func connectWebSocket(c *Context, w http.ResponseWriter, r *http.Request) { ws, err := upgrader.Upgrade(w, r, nil) if err != nil { - c.Err = model.NewAppError("connect", "api.web_socket.connect.upgrade.app_error", nil, err.Error(), http.StatusInternalServerError) + c.Err = model.NewAppError("connect", "api.web_socket.connect.upgrade.app_error", nil, err.Error(), http.StatusBadRequest) return } diff --git a/api4/websocket_test.go b/api4/websocket_test.go index 28729b936a..7d35c12578 100644 --- a/api4/websocket_test.go +++ b/api4/websocket_test.go @@ -15,6 +15,8 @@ import ( "github.com/stretchr/testify/require" "github.com/mattermost/mattermost-server/v6/model" + "github.com/mattermost/mattermost-server/v6/shared/mlog" + "github.com/mattermost/mattermost-server/v6/testlib" ) func TestWebSocketTrailingSlash(t *testing.T) { @@ -417,3 +419,15 @@ func TestWebSocketStatuses(t *testing.T) { WebSocketClient.Close() } + +func TestWebSocketUpgrade(t *testing.T) { + th := Setup(t) + defer th.TearDown() + + url := fmt.Sprintf("http://localhost:%v", th.App.Srv().ListenAddr.Port) + model.APIURLSuffix + "/websocket" + resp, err := http.Get(url) + require.NoError(t, err) + require.Equal(t, resp.StatusCode, http.StatusBadRequest) + require.NoError(t, th.TestLogger.Flush()) + testlib.AssertLog(t, th.LogBuffer, mlog.LvlDebug.Name, "Failed to upgrade websocket connection.") +} diff --git a/app/app_iface.go b/app/app_iface.go index aed132ba5f..a113169c5c 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -639,16 +639,16 @@ type AppIface interface { GetFlaggedPostsForChannel(userID, channelID string, offset int, limit int) (*model.PostList, *model.AppError) GetFlaggedPostsForTeam(userID, teamID string, offset int, limit int) (*model.PostList, *model.AppError) GetGlobalRetentionPolicy() (*model.GlobalRetentionPolicy, *model.AppError) - GetGroup(id string, opts *model.GetGroupOpts) (*model.Group, *model.AppError) + GetGroup(id string, opts *model.GetGroupOpts, viewRestrictions *model.ViewUsersRestrictions) (*model.Group, *model.AppError) GetGroupByName(name string, opts model.GroupSearchOpts) (*model.Group, *model.AppError) GetGroupByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, *model.AppError) GetGroupChannel(c request.CTX, userIDs []string) (*model.Channel, *model.AppError) - GetGroupMemberCount(groupID string) (int64, *model.AppError) + GetGroupMemberCount(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, *model.AppError) GetGroupMemberUsers(groupID string) ([]*model.User, *model.AppError) - GetGroupMemberUsersPage(groupID string, page int, perPage int) ([]*model.User, int, *model.AppError) + GetGroupMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, int, *model.AppError) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) GetGroupSyncables(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, *model.AppError) - GetGroups(page, perPage int, opts model.GroupSearchOpts) ([]*model.Group, *model.AppError) + GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, *model.AppError) GetGroupsAssociatedToChannelsByTeam(teamID string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, *model.AppError) GetGroupsByChannel(channelID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, int, *model.AppError) GetGroupsByIDs(groupIDs []string) ([]*model.Group, *model.AppError) @@ -831,7 +831,7 @@ type AppIface interface { GetUsersNotInChannel(teamID string, channelID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) GetUsersNotInChannelMap(teamID string, channelID string, groupConstrained bool, offset int, limit int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) (map[string]*model.User, *model.AppError) GetUsersNotInChannelPage(teamID string, channelID string, groupConstrained bool, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) - GetUsersNotInGroupPage(groupID string, page int, perPage int) ([]*model.User, *model.AppError) + GetUsersNotInGroupPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) GetUsersNotInTeam(teamID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) GetUsersNotInTeamEtag(teamID string, restrictionsHash string) string GetUsersNotInTeamPage(teamID string, groupConstrained bool, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) @@ -1143,6 +1143,7 @@ type AppIface interface { UpsertGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) UserAlreadyNotifiedOnRequiredFeature(user string, feature model.MattermostPaidFeature) bool UserCanSeeOtherUser(userID string, otherUserId string) (bool, *model.AppError) + UserIsFirstAdmin(user *model.User) bool VerifyEmailFromToken(c request.CTX, userSuppliedTokenString string) *model.AppError VerifyUserEmail(userID, email string) *model.AppError ViewChannel(c request.CTX, view *model.ChannelView, userID string, currentSessionId string, collapsedThreadsSupported bool) (map[string]int64, *model.AppError) diff --git a/app/email/email.go b/app/email/email.go index d9f3882c61..ca1d7752e5 100644 --- a/app/email/email.go +++ b/app/email/email.go @@ -410,7 +410,17 @@ func (es *Service) SendMfaChangeEmail(email string, activated bool, locale, site return nil } -func (es *Service) SendInviteEmails(team *model.Team, senderName string, senderUserId string, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, errorWhenNotSent bool) error { +func (es *Service) SendInviteEmails( + team *model.Team, + senderName string, + senderUserId string, + invites []string, + siteURL string, + reminderData *model.TeamInviteReminderData, + errorWhenNotSent bool, + isSystemAdmin bool, + isFirstAdmin bool, +) error { if es.perHourEmailRateLimiter == nil { return NoRateLimiterError } @@ -466,7 +476,8 @@ func (es *Service) SendInviteEmails(team *model.Team, senderName string, senderU mlog.Error("Failed to send invite email successfully ", mlog.Err(err)) continue } - data.Props["ButtonURL"] = fmt.Sprintf("%s/signup_user_complete/?d=%s&t=%s", siteURL, url.QueryEscape(tokenData), url.QueryEscape(token.Token)) + + data.Props["ButtonURL"] = fmt.Sprintf("%s/signup_user_complete/?d=%s&t=%s&sbr=%s", siteURL, url.QueryEscape(tokenData), url.QueryEscape(token.Token), es.GetTrackFlowStartedByRole(isFirstAdmin, isSystemAdmin)) body, err := es.templatesContainer.RenderToString("invite_body", data) if err != nil { @@ -484,7 +495,19 @@ func (es *Service) SendInviteEmails(team *model.Team, senderName string, senderU return nil } -func (es *Service) SendGuestInviteEmails(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, message string, errorWhenNotSent bool) error { +func (es *Service) SendGuestInviteEmails( + team *model.Team, + channels []*model.Channel, + senderName string, + senderUserId string, + senderProfileImage []byte, + invites []string, + siteURL string, + message string, + errorWhenNotSent bool, + isSystemAdmin bool, + isFirstAdmin bool, +) error { if es.perHourEmailRateLimiter == nil { return NoRateLimiterError } @@ -545,7 +568,8 @@ func (es *Service) SendGuestInviteEmails(team *model.Team, channels []*model.Cha mlog.Error("Failed to send invite email successfully ", mlog.Err(err)) continue } - data.Props["ButtonURL"] = fmt.Sprintf("%s/signup_user_complete/?d=%s&t=%s", siteURL, url.QueryEscape(tokenData), url.QueryEscape(token.Token)) + + data.Props["ButtonURL"] = fmt.Sprintf("%s/signup_user_complete/?d=%s&t=%s&sbr=%s", siteURL, url.QueryEscape(tokenData), url.QueryEscape(token.Token), es.GetTrackFlowStartedByRole(isFirstAdmin, isSystemAdmin)) if !*es.config().EmailSettings.SendEmailNotifications { mlog.Info("sending invitation ", mlog.String("to", invite), mlog.String("link", data.Props["ButtonURL"].(string))) @@ -597,6 +621,8 @@ func (es *Service) SendInviteEmailsToTeamAndChannels( reminderData *model.TeamInviteReminderData, message string, errorWhenNotSent bool, + isSystemAdmin bool, + isFirstAdmin bool, ) ([]*model.EmailInviteWithError, error) { if es.perHourEmailRateLimiter == nil { return nil, NoRateLimiterError @@ -694,7 +720,8 @@ func (es *Service) SendInviteEmailsToTeamAndChannels( mlog.Error("Failed to send invite email successfully ", mlog.Err(err)) continue } - data.Props["ButtonURL"] = fmt.Sprintf("%s/signup_user_complete/?d=%s&t=%s", siteURL, url.QueryEscape(tokenData), url.QueryEscape(token.Token)) + + data.Props["ButtonURL"] = fmt.Sprintf("%s/signup_user_complete/?d=%s&t=%s&sbr=%s", siteURL, url.QueryEscape(tokenData), url.QueryEscape(token.Token), es.GetTrackFlowStartedByRole(isFirstAdmin, isSystemAdmin)) senderPhoto := "" embeddedFiles := make(map[string]io.Reader) diff --git a/app/email/email_test.go b/app/email/email_test.go index 5742a67c73..0fe3b237e0 100644 --- a/app/email/email_test.go +++ b/app/email/email_test.go @@ -76,7 +76,7 @@ func TestSendInviteEmails(t *testing.T) { t.Run("SendInviteEmails", func(t *testing.T) { mail.DeleteMailBox(emailTo) - err := th.service.SendInviteEmails(th.BasicTeam, "test-user", th.BasicUser.Id, []string{emailTo}, "http://testserver", nil, false) + err := th.service.SendInviteEmails(th.BasicTeam, "test-user", th.BasicUser.Id, []string{emailTo}, "http://testserver", nil, false, false, false) require.NoError(t, err) verifyMailbox(t) @@ -93,10 +93,10 @@ func TestSendInviteEmails(t *testing.T) { *cfg.EmailSettings.SMTPPort = originalPort }) - err := th.service.SendInviteEmails(th.BasicTeam, "test-user", th.BasicUser.Id, []string{emailTo}, "http://testserver", nil, true) + err := th.service.SendInviteEmails(th.BasicTeam, "test-user", th.BasicUser.Id, []string{emailTo}, "http://testserver", nil, true, false, false) require.Error(t, err) - err = th.service.SendInviteEmails(th.BasicTeam, "test-user", th.BasicUser.Id, []string{emailTo}, "http://testserver", nil, false) + err = th.service.SendInviteEmails(th.BasicTeam, "test-user", th.BasicUser.Id, []string{emailTo}, "http://testserver", nil, false, false, false) require.NoError(t, err) }) @@ -113,6 +113,8 @@ func TestSendInviteEmails(t *testing.T) { "http://testserver", "hello world", false, + false, + false, ) require.NoError(t, err) @@ -140,6 +142,8 @@ func TestSendInviteEmails(t *testing.T) { "http://testserver", "hello world", false, + false, + false, ) require.NoError(t, err) @@ -153,6 +157,8 @@ func TestSendInviteEmails(t *testing.T) { "http://testserver", "hello world", true, + false, + false, ) require.Error(t, err) @@ -172,6 +178,8 @@ func TestSendInviteEmails(t *testing.T) { "http://testserver", message, false, + false, + false, ) require.NoError(t, err) @@ -180,6 +188,66 @@ func TestSendInviteEmails(t *testing.T) { require.Contains(t, email.Body.HTML, "sanitized message") require.Contains(t, email.Body.Text, "sanitized message") }) + + t.Run("SendInviteEmails should contain button URL with 'started by role' param for system user", func(t *testing.T) { + mail.DeleteMailBox(emailTo) + + err := th.service.SendInviteEmails( + th.BasicTeam, + "test-user", + th.BasicUser.Id, + []string{emailTo}, + "http://testserver", + nil, + false, + false, + false, + ) + require.NoError(t, err) + + email := retrieveEmail(t) + require.Contains(t, email.Body.HTML, "&sbr=su") + }) + + t.Run("SendInviteEmails should contain button URL with 'started by role' param for system admin", func(t *testing.T) { + mail.DeleteMailBox(emailTo) + + err := th.service.SendInviteEmails( + th.BasicTeam, + "test-user", + th.BasicUser.Id, + []string{emailTo}, + "http://testserver", + nil, + false, + true, + false, + ) + require.NoError(t, err) + + email := retrieveEmail(t) + require.Contains(t, email.Body.HTML, "&sbr=sa") + }) + + t.Run("SendInviteEmails should contain button URL with 'started by role' param for first system admin", func(t *testing.T) { + mail.DeleteMailBox(emailTo) + + err := th.service.SendInviteEmails( + th.BasicTeam, + "test-user", + th.BasicUser.Id, + []string{emailTo}, + "http://testserver", + nil, + false, + true, + true, + ) + require.NoError(t, err) + + email := retrieveEmail(t) + require.Contains(t, email.Body.HTML, "&sbr=fa") + }) } func TestSendCloudUpgradedEmail(t *testing.T) { diff --git a/app/email/mocks/ServiceInterface.go b/app/email/mocks/ServiceInterface.go index dbcabb1146..08cddbe4ad 100644 --- a/app/email/mocks/ServiceInterface.go +++ b/app/email/mocks/ServiceInterface.go @@ -293,13 +293,13 @@ func (_m *ServiceInterface) SendEmailChangeVerifyEmail(newUserEmail string, loca return r0 } -// SendGuestInviteEmails provides a mock function with given fields: team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, message, errorWhenNotSent -func (_m *ServiceInterface) SendGuestInviteEmails(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, message string, errorWhenNotSent bool) error { - ret := _m.Called(team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, message, errorWhenNotSent) +// SendGuestInviteEmails provides a mock function with given fields: team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, message, errorWhenNotSent, isSystemAdmin, isFirstAdmin +func (_m *ServiceInterface) SendGuestInviteEmails(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, message string, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) error { + ret := _m.Called(team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, message, errorWhenNotSent, isSystemAdmin, isFirstAdmin) var r0 error - if rf, ok := ret.Get(0).(func(*model.Team, []*model.Channel, string, string, []byte, []string, string, string, bool) error); ok { - r0 = rf(team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, message, errorWhenNotSent) + if rf, ok := ret.Get(0).(func(*model.Team, []*model.Channel, string, string, []byte, []string, string, string, bool, bool, bool) error); ok { + r0 = rf(team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, message, errorWhenNotSent, isSystemAdmin, isFirstAdmin) } else { r0 = ret.Error(0) } @@ -307,13 +307,13 @@ func (_m *ServiceInterface) SendGuestInviteEmails(team *model.Team, channels []* return r0 } -// SendInviteEmails provides a mock function with given fields: team, senderName, senderUserId, invites, siteURL, reminderData, errorWhenNotSent -func (_m *ServiceInterface) SendInviteEmails(team *model.Team, senderName string, senderUserId string, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, errorWhenNotSent bool) error { - ret := _m.Called(team, senderName, senderUserId, invites, siteURL, reminderData, errorWhenNotSent) +// SendInviteEmails provides a mock function with given fields: team, senderName, senderUserId, invites, siteURL, reminderData, errorWhenNotSent, isSystemAdmin, isFirstAdmin +func (_m *ServiceInterface) SendInviteEmails(team *model.Team, senderName string, senderUserId string, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) error { + ret := _m.Called(team, senderName, senderUserId, invites, siteURL, reminderData, errorWhenNotSent, isSystemAdmin, isFirstAdmin) var r0 error - if rf, ok := ret.Get(0).(func(*model.Team, string, string, []string, string, *model.TeamInviteReminderData, bool) error); ok { - r0 = rf(team, senderName, senderUserId, invites, siteURL, reminderData, errorWhenNotSent) + if rf, ok := ret.Get(0).(func(*model.Team, string, string, []string, string, *model.TeamInviteReminderData, bool, bool, bool) error); ok { + r0 = rf(team, senderName, senderUserId, invites, siteURL, reminderData, errorWhenNotSent, isSystemAdmin, isFirstAdmin) } else { r0 = ret.Error(0) } @@ -321,13 +321,13 @@ func (_m *ServiceInterface) SendInviteEmails(team *model.Team, senderName string return r0 } -// SendInviteEmailsToTeamAndChannels provides a mock function with given fields: team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, reminderData, message, errorWhenNotSent -func (_m *ServiceInterface) SendInviteEmailsToTeamAndChannels(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, message string, errorWhenNotSent bool) ([]*model.EmailInviteWithError, error) { - ret := _m.Called(team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, reminderData, message, errorWhenNotSent) +// SendInviteEmailsToTeamAndChannels provides a mock function with given fields: team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, reminderData, message, errorWhenNotSent, isSystemAdmin, isFirstAdmin +func (_m *ServiceInterface) SendInviteEmailsToTeamAndChannels(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, message string, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) ([]*model.EmailInviteWithError, error) { + ret := _m.Called(team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, reminderData, message, errorWhenNotSent, isSystemAdmin, isFirstAdmin) var r0 []*model.EmailInviteWithError - if rf, ok := ret.Get(0).(func(*model.Team, []*model.Channel, string, string, []byte, []string, string, *model.TeamInviteReminderData, string, bool) []*model.EmailInviteWithError); ok { - r0 = rf(team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, reminderData, message, errorWhenNotSent) + if rf, ok := ret.Get(0).(func(*model.Team, []*model.Channel, string, string, []byte, []string, string, *model.TeamInviteReminderData, string, bool, bool, bool) []*model.EmailInviteWithError); ok { + r0 = rf(team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, reminderData, message, errorWhenNotSent, isSystemAdmin, isFirstAdmin) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*model.EmailInviteWithError) @@ -335,8 +335,8 @@ func (_m *ServiceInterface) SendInviteEmailsToTeamAndChannels(team *model.Team, } var r1 error - if rf, ok := ret.Get(1).(func(*model.Team, []*model.Channel, string, string, []byte, []string, string, *model.TeamInviteReminderData, string, bool) error); ok { - r1 = rf(team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, reminderData, message, errorWhenNotSent) + if rf, ok := ret.Get(1).(func(*model.Team, []*model.Channel, string, string, []byte, []string, string, *model.TeamInviteReminderData, string, bool, bool, bool) error); ok { + r1 = rf(team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, reminderData, message, errorWhenNotSent, isSystemAdmin, isFirstAdmin) } else { r1 = ret.Error(1) } diff --git a/app/email/service.go b/app/email/service.go index 135991692e..0f9158e343 100644 --- a/app/email/service.go +++ b/app/email/service.go @@ -135,9 +135,9 @@ type ServiceInterface interface { SendUserAccessTokenAddedEmail(email, locale, siteURL string) error SendPasswordResetEmail(email string, token *model.Token, locale, siteURL string) (bool, error) SendMfaChangeEmail(email string, activated bool, locale, siteURL string) error - SendInviteEmails(team *model.Team, senderName string, senderUserId string, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, errorWhenNotSent bool) error - SendGuestInviteEmails(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, message string, errorWhenNotSent bool) error - SendInviteEmailsToTeamAndChannels(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, message string, errorWhenNotSent bool) ([]*model.EmailInviteWithError, error) + SendInviteEmails(team *model.Team, senderName string, senderUserId string, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) error + SendGuestInviteEmails(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, message string, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) error + SendInviteEmailsToTeamAndChannels(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, message string, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) ([]*model.EmailInviteWithError, error) SendDeactivateAccountEmail(email string, locale, siteURL string) error SendNotificationMail(to, subject, htmlBody string) error SendMailWithEmbeddedFiles(to, subject, htmlBody string, embeddedFiles map[string]io.Reader, messageID string, inReplyTo string, references string) error diff --git a/app/email/utils.go b/app/email/utils.go index 623e0c6969..b203a4f62e 100644 --- a/app/email/utils.go +++ b/app/email/utils.go @@ -34,3 +34,15 @@ func (es *Service) mailServiceConfig(replyToAddress string) *mail.SMTPConfig { } return &cfg } + +func (es *Service) GetTrackFlowStartedByRole(isFirstAdmin bool, isSystemAdmin bool) string { + trackFlowStartedByRole := "su" + + if isFirstAdmin { + trackFlowStartedByRole = "fa" + } else if isSystemAdmin { + trackFlowStartedByRole = "sa" + } + + return trackFlowStartedByRole +} diff --git a/app/group.go b/app/group.go index beead51982..209c9e28c4 100644 --- a/app/group.go +++ b/app/group.go @@ -13,7 +13,7 @@ import ( "github.com/mattermost/mattermost-server/v6/store" ) -func (a *App) GetGroup(id string, opts *model.GetGroupOpts) (*model.Group, *model.AppError) { +func (a *App) GetGroup(id string, opts *model.GetGroupOpts, viewRestrictions *model.ViewUsersRestrictions) (*model.Group, *model.AppError) { group, err := a.Srv().Store().Group().Get(id) if err != nil { var nfErr *store.ErrNotFound @@ -26,7 +26,7 @@ func (a *App) GetGroup(id string, opts *model.GetGroupOpts) (*model.Group, *mode } if opts != nil && opts.IncludeMemberCount { - memberCount, err := a.Srv().Store().Group().GetMemberCount(id) + memberCount, err := a.Srv().Store().Group().GetMemberCountWithRestrictions(id, viewRestrictions) if err != nil { return nil, model.NewAppError("GetGroup", "app.member_count", nil, "", http.StatusInternalServerError).Wrap(err) } @@ -147,7 +147,6 @@ func (a *App) CreateGroupWithUserIds(group *model.GroupWithUserIds) (*model.Grou messageWs := model.NewWebSocketEvent(model.WebsocketEventReceivedGroup, "", "", "", nil, "") count, err := a.Srv().Store().Group().GetMemberCount(newGroup.Id) - if err != nil { return nil, model.NewAppError("CreateGroupWithUserIds", "app.group.id.app_error", nil, "", http.StatusBadRequest).Wrap(err) } @@ -218,8 +217,8 @@ func (a *App) DeleteGroup(groupID string) (*model.Group, *model.AppError) { return deletedGroup, nil } -func (a *App) GetGroupMemberCount(groupID string) (int64, *model.AppError) { - count, err := a.Srv().Store().Group().GetMemberCount(groupID) +func (a *App) GetGroupMemberCount(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, *model.AppError) { + count, err := a.Srv().Store().Group().GetMemberCountWithRestrictions(groupID, viewRestrictions) if err != nil { return 0, model.NewAppError("GetGroupMemberCount", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err) } @@ -236,20 +235,21 @@ func (a *App) GetGroupMemberUsers(groupID string) ([]*model.User, *model.AppErro return users, nil } -func (a *App) GetGroupMemberUsersPage(groupID string, page int, perPage int) ([]*model.User, int, *model.AppError) { - members, err := a.Srv().Store().Group().GetMemberUsersPage(groupID, page, perPage) +func (a *App) GetGroupMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, int, *model.AppError) { + members, err := a.Srv().Store().Group().GetMemberUsersPage(groupID, page, perPage, viewRestrictions) if err != nil { return nil, 0, model.NewAppError("GetGroupMemberUsersPage", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err) } - count, appErr := a.GetGroupMemberCount(groupID) + count, appErr := a.GetGroupMemberCount(groupID, viewRestrictions) if appErr != nil { return nil, 0, appErr } return a.sanitizeProfiles(members, false), int(count), nil } -func (a *App) GetUsersNotInGroupPage(groupID string, page int, perPage int) ([]*model.User, *model.AppError) { - members, err := a.Srv().Store().Group().GetNonMemberUsersPage(groupID, page, perPage) + +func (a *App) GetUsersNotInGroupPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) { + members, err := a.Srv().Store().Group().GetNonMemberUsersPage(groupID, page, perPage, viewRestrictions) if err != nil { return nil, model.NewAppError("GetUsersNotInGroupPage", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err) } @@ -580,8 +580,8 @@ func (a *App) GetGroupsAssociatedToChannelsByTeam(teamID string, opts model.Grou return groupsAssociatedByChannelId, nil } -func (a *App) GetGroups(page, perPage int, opts model.GroupSearchOpts) ([]*model.Group, *model.AppError) { - groups, err := a.Srv().Store().Group().GetGroups(page, perPage, opts) +func (a *App) GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, *model.AppError) { + groups, err := a.Srv().Store().Group().GetGroups(page, perPage, opts, viewRestrictions) if err != nil { return nil, model.NewAppError("GetGroups", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err) } diff --git a/app/group_test.go b/app/group_test.go index 9a1084cfec..3225d881b7 100644 --- a/app/group_test.go +++ b/app/group_test.go @@ -17,19 +17,19 @@ func TestGetGroup(t *testing.T) { defer th.TearDown() group := th.CreateGroup() - group, err := th.App.GetGroup(group.Id, nil) + group, err := th.App.GetGroup(group.Id, nil, nil) require.Nil(t, err) require.NotNil(t, group) - nilGroup, err := th.App.GetGroup(model.NewId(), nil) + nilGroup, err := th.App.GetGroup(model.NewId(), nil, nil) require.NotNil(t, err) require.Nil(t, nilGroup) - group, err = th.App.GetGroup(group.Id, &model.GetGroupOpts{IncludeMemberCount: false}) + group, err = th.App.GetGroup(group.Id, &model.GetGroupOpts{IncludeMemberCount: false}, nil) require.Nil(t, err) require.Nil(t, group.MemberCount) - group, err = th.App.GetGroup(group.Id, &model.GetGroupOpts{IncludeMemberCount: true}) + group, err = th.App.GetGroup(group.Id, &model.GetGroupOpts{IncludeMemberCount: true}, nil) require.Nil(t, err) require.NotNil(t, group.MemberCount) } @@ -369,7 +369,7 @@ func TestGetGroups(t *testing.T) { defer th.TearDown() group := th.CreateGroup() - groups, err := th.App.GetGroups(0, 60, model.GroupSearchOpts{}) + groups, err := th.App.GetGroups(0, 60, model.GroupSearchOpts{}, nil) require.Nil(t, err) require.ElementsMatch(t, []*model.Group{group}, groups) } diff --git a/app/notification.go b/app/notification.go index f143789f4b..c5d36e14b8 100644 --- a/app/notification.go +++ b/app/notification.go @@ -1082,7 +1082,7 @@ func (a *App) getGroupsAllowedForReferenceInChannel(channel *model.Channel, team return groupsMap, nil } - groups, err := a.Srv().Store().Group().GetGroups(0, 0, opts) + groups, err := a.Srv().Store().Group().GetGroups(0, 0, opts, nil) if err != nil { return nil, errors.Wrap(err, "unable to get groups") } diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 97f929c77d..3c5b181160 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -6151,7 +6151,7 @@ func (a *OpenTracingAppLayer) GetGlobalRetentionPolicy() (*model.GlobalRetention return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) GetGroup(id string, opts *model.GetGroupOpts) (*model.Group, *model.AppError) { +func (a *OpenTracingAppLayer) GetGroup(id string, opts *model.GetGroupOpts, viewRestrictions *model.ViewUsersRestrictions) (*model.Group, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroup") @@ -6163,7 +6163,7 @@ func (a *OpenTracingAppLayer) GetGroup(id string, opts *model.GetGroupOpts) (*mo }() defer span.Finish() - resultVar0, resultVar1 := a.app.GetGroup(id, opts) + resultVar0, resultVar1 := a.app.GetGroup(id, opts, viewRestrictions) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) @@ -6239,7 +6239,7 @@ func (a *OpenTracingAppLayer) GetGroupChannel(c request.CTX, userIDs []string) ( return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) GetGroupMemberCount(groupID string) (int64, *model.AppError) { +func (a *OpenTracingAppLayer) GetGroupMemberCount(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupMemberCount") @@ -6251,7 +6251,7 @@ func (a *OpenTracingAppLayer) GetGroupMemberCount(groupID string) (int64, *model }() defer span.Finish() - resultVar0, resultVar1 := a.app.GetGroupMemberCount(groupID) + resultVar0, resultVar1 := a.app.GetGroupMemberCount(groupID, viewRestrictions) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) @@ -6283,7 +6283,7 @@ func (a *OpenTracingAppLayer) GetGroupMemberUsers(groupID string) ([]*model.User return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) GetGroupMemberUsersPage(groupID string, page int, perPage int) ([]*model.User, int, *model.AppError) { +func (a *OpenTracingAppLayer) GetGroupMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, int, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupMemberUsersPage") @@ -6295,7 +6295,7 @@ func (a *OpenTracingAppLayer) GetGroupMemberUsersPage(groupID string, page int, }() defer span.Finish() - resultVar0, resultVar1, resultVar2 := a.app.GetGroupMemberUsersPage(groupID, page, perPage) + resultVar0, resultVar1, resultVar2 := a.app.GetGroupMemberUsersPage(groupID, page, perPage, viewRestrictions) if resultVar2 != nil { span.LogFields(spanlog.Error(resultVar2)) @@ -6349,7 +6349,7 @@ func (a *OpenTracingAppLayer) GetGroupSyncables(groupID string, syncableType mod return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) GetGroups(page int, perPage int, opts model.GroupSearchOpts) ([]*model.Group, *model.AppError) { +func (a *OpenTracingAppLayer) GetGroups(page int, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroups") @@ -6361,7 +6361,7 @@ func (a *OpenTracingAppLayer) GetGroups(page int, perPage int, opts model.GroupS }() defer span.Finish() - resultVar0, resultVar1 := a.app.GetGroups(page, perPage, opts) + resultVar0, resultVar1 := a.app.GetGroups(page, perPage, opts, viewRestrictions) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) @@ -10801,7 +10801,7 @@ func (a *OpenTracingAppLayer) GetUsersNotInChannelPage(teamID string, channelID return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) GetUsersNotInGroupPage(groupID string, page int, perPage int) ([]*model.User, *model.AppError) { +func (a *OpenTracingAppLayer) GetUsersNotInGroupPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersNotInGroupPage") @@ -10813,7 +10813,7 @@ func (a *OpenTracingAppLayer) GetUsersNotInGroupPage(groupID string, page int, p }() defer span.Finish() - resultVar0, resultVar1 := a.app.GetUsersNotInGroupPage(groupID, page, perPage) + resultVar0, resultVar1 := a.app.GetUsersNotInGroupPage(groupID, page, perPage, viewRestrictions) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) @@ -18052,6 +18052,23 @@ func (a *OpenTracingAppLayer) UserCanSeeOtherUser(userID string, otherUserId str return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) UserIsFirstAdmin(user *model.User) bool { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UserIsFirstAdmin") + + a.ctx = newCtx + a.app.Srv().Store().SetContext(newCtx) + defer func() { + a.app.Srv().Store().SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0 := a.app.UserIsFirstAdmin(user) + + return resultVar0 +} + func (a *OpenTracingAppLayer) UserIsInAdminRoleGroup(userID string, syncableID string, syncableType model.GroupSyncableType) (bool, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UserIsInAdminRoleGroup") diff --git a/app/platform/service.go b/app/platform/service.go index 6620b7f487..018dc9569a 100644 --- a/app/platform/service.go +++ b/app/platform/service.go @@ -173,7 +173,7 @@ func New(sc ServiceConfig, options ...Option) (*PlatformService, error) { ps.initEnterprise() // Step 5: Init Metrics - if metricsInterfaceFn != nil { + if metricsInterfaceFn != nil && ps.metricsIFace == nil { // if the metrics interface is set by options, do not override it ps.metricsIFace = metricsInterfaceFn(ps, *ps.configStore.Get().SqlSettings.DriverName, *ps.configStore.Get().SqlSettings.DataSource) } diff --git a/app/platform/service_test.go b/app/platform/service_test.go index b2b7fa1e89..49470021f7 100644 --- a/app/platform/service_test.go +++ b/app/platform/service_test.go @@ -124,6 +124,7 @@ func TestMetrics(t *testing.T) { }) t.Run("ensure the metrics server is started with advanced metrics", func(t *testing.T) { + t.Skip("MM-47635") th := Setup(t, StartMetrics()) defer th.TearDown() diff --git a/app/plugin_api.go b/app/plugin_api.go index ad33a4d4d9..a9a3c67907 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -608,7 +608,7 @@ func (api *PluginAPI) DeleteChannelMember(channelID, userID string) *model.AppEr } func (api *PluginAPI) GetGroup(groupId string) (*model.Group, *model.AppError) { - return api.app.GetGroup(groupId, nil) + return api.app.GetGroup(groupId, nil, nil) } func (api *PluginAPI) GetGroupByName(name string) (*model.Group, *model.AppError) { @@ -616,7 +616,7 @@ func (api *PluginAPI) GetGroupByName(name string) (*model.Group, *model.AppError } func (api *PluginAPI) GetGroupMemberUsers(groupID string, page, perPage int) ([]*model.User, *model.AppError) { - users, _, err := api.app.GetGroupMemberUsersPage(groupID, page, perPage) + users, _, err := api.app.GetGroupMemberUsersPage(groupID, page, perPage, nil) return users, err } diff --git a/app/team.go b/app/team.go index fc69fbb652..30020e25ef 100644 --- a/app/team.go +++ b/app/team.go @@ -1461,13 +1461,15 @@ func (a *App) InviteNewUsersToTeamGracefully(memberInvite *model.MemberInvite, t if err != nil { a.Log().Warn("Unable to get the sender user profile image.", mlog.String("user_id", user.Id), mlog.String("team_id", team.Id), mlog.Err(err)) } + + userIsFirstAdmin := a.UserIsFirstAdmin(user) var eErr error var invitesWithErrors2 []*model.EmailInviteWithError if len(channels) > 0 { - invitesWithErrors2, eErr = a.Srv().EmailService.SendInviteEmailsToTeamAndChannels(team, channels, user.GetDisplayName(nameFormat), user.Id, senderProfileImage, goodEmails, a.GetSiteURL(), reminderData, memberInvite.Message, true) + invitesWithErrors2, eErr = a.Srv().EmailService.SendInviteEmailsToTeamAndChannels(team, channels, user.GetDisplayName(nameFormat), user.Id, senderProfileImage, goodEmails, a.GetSiteURL(), reminderData, memberInvite.Message, true, user.IsSystemAdmin(), userIsFirstAdmin) inviteListWithErrors = append(inviteListWithErrors, invitesWithErrors2...) } else { - eErr = a.Srv().EmailService.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, goodEmails, a.GetSiteURL(), reminderData, true) + eErr = a.Srv().EmailService.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, goodEmails, a.GetSiteURL(), reminderData, true, user.IsSystemAdmin(), userIsFirstAdmin) } if eErr != nil { switch { @@ -1587,7 +1589,8 @@ func (a *App) InviteGuestsToChannelsGracefully(teamID string, guestsInvite *mode if err != nil { a.Log().Warn("Unable to get the sender user profile image.", mlog.String("user_id", user.Id), mlog.String("team_id", team.Id), mlog.Err(err)) } - eErr := a.Srv().EmailService.SendGuestInviteEmails(team, channels, user.GetDisplayName(nameFormat), user.Id, senderProfileImage, goodEmails, a.GetSiteURL(), guestsInvite.Message, true) + + eErr := a.Srv().EmailService.SendGuestInviteEmails(team, channels, user.GetDisplayName(nameFormat), user.Id, senderProfileImage, goodEmails, a.GetSiteURL(), guestsInvite.Message, true, user.IsSystemAdmin(), a.UserIsFirstAdmin(user)) if eErr != nil { switch { case errors.Is(eErr, email.SendMailError): @@ -1644,7 +1647,7 @@ func (a *App) InviteNewUsersToTeam(emailList []string, teamID, senderId string) } nameFormat := *a.Config().TeamSettings.TeammateNameDisplay - eErr := a.Srv().EmailService.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, emailList, a.GetSiteURL(), nil, false) + eErr := a.Srv().EmailService.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, emailList, a.GetSiteURL(), nil, false, user.IsSystemAdmin(), a.UserIsFirstAdmin(user)) if eErr != nil { switch { case errors.Is(eErr, email.NoRateLimiterError): @@ -1686,7 +1689,8 @@ func (a *App) InviteGuestsToChannels(teamID string, guestsInvite *model.GuestsIn if err != nil { a.Log().Warn("Unable to get the sender user profile image.", mlog.String("user_id", user.Id), mlog.String("team_id", team.Id), mlog.Err(err)) } - eErr := a.Srv().EmailService.SendGuestInviteEmails(team, channels, user.GetDisplayName(nameFormat), user.Id, senderProfileImage, guestsInvite.Emails, a.GetSiteURL(), guestsInvite.Message, false) + + eErr := a.Srv().EmailService.SendGuestInviteEmails(team, channels, user.GetDisplayName(nameFormat), user.Id, senderProfileImage, guestsInvite.Emails, a.GetSiteURL(), guestsInvite.Message, false, user.IsSystemAdmin(), a.UserIsFirstAdmin(user)) if eErr != nil { switch { case errors.Is(eErr, email.NoRateLimiterError): diff --git a/app/team_test.go b/app/team_test.go index 14eb0273a7..604cef33d6 100644 --- a/app/team_test.go +++ b/app/team_test.go @@ -1457,6 +1457,8 @@ func TestInviteNewUsersToTeamGracefully(t *testing.T) { "", mock.Anything, true, + false, + false, ).Once().Return(nil) th.App.Srv().EmailService = &emailServiceMock @@ -1479,6 +1481,8 @@ func TestInviteNewUsersToTeamGracefully(t *testing.T) { "", mock.Anything, true, + false, + false, ).Once().Return(email.SendMailError) th.App.Srv().EmailService = &emailServiceMock @@ -1505,6 +1509,8 @@ func TestInviteNewUsersToTeamGracefully(t *testing.T) { mock.Anything, mock.AnythingOfType("string"), true, + false, + false, ).Once().Return([]*model.EmailInviteWithError{}, nil) th.App.Srv().EmailService = &emailServiceMock @@ -1527,6 +1533,8 @@ func TestInviteNewUsersToTeamGracefully(t *testing.T) { "", mock.Anything, true, + false, + false, ).Once().Return(nil) th.App.Srv().EmailService = &emailServiceMock @@ -1557,6 +1565,8 @@ func TestInviteGuestsToChannelsGracefully(t *testing.T) { "", "", true, + false, + false, ).Once().Return(nil) th.App.Srv().EmailService = &emailServiceMock @@ -1581,6 +1591,8 @@ func TestInviteGuestsToChannelsGracefully(t *testing.T) { "", "", true, + false, + false, ).Once().Return(email.SendMailError) th.App.Srv().EmailService = &emailServiceMock diff --git a/app/user.go b/app/user.go index c0a9773e61..9b0840eea6 100644 --- a/app/user.go +++ b/app/user.go @@ -2671,3 +2671,25 @@ func getProfileImagePath(userID string) string { func getProfileImageDirectory(userID string) string { return filepath.Join("users", userID) } + +func (a *App) UserIsFirstAdmin(user *model.User) bool { + if !user.IsSystemAdmin() { + return false + } + + systemAdminUsers, errServer := a.Srv().Store().User().GetSystemAdminProfiles() + if errServer != nil { + mlog.Warn("Failed to get system admins to check for first admin from Mattermost.") + return false + } + + for _, systemAdminUser := range systemAdminUsers { + systemAdminUser := systemAdminUser + + if systemAdminUser.CreateAt < user.CreateAt { + return false + } + } + + return true +} diff --git a/build/release.mk b/build/release.mk index 4ac581fee7..e48c3d70b1 100644 --- a/build/release.mk +++ b/build/release.mk @@ -190,6 +190,17 @@ else done endif + @# Products + + @if [ -d $(BUILD_BOARDS_DIR) ] ; then \ + echo "Copying web app files for Boards product"; \ + mkdir -p $(DIST_PATH_GENERIC)/client/products/boards; \ + cp -R $(BUILD_BOARDS_DIR)/mattermost-plugin/webapp/dist/* $(DIST_PATH_GENERIC)/client/products/boards/; \ + else \ + echo "Unable to find files for Boards product. Please ensure that the Focalboard repository is checked out alongside the server and run 'make build-product' in it."; \ + exit 1; \ + fi + package-osx-amd64: package-prep DIST_PATH_GENERIC=$(DIST_PATH_OSX_AMD64) CURRENT_PACKAGE_ARCH=darwin_amd64 PLUGIN_ARCH=osx-amd64 MMCTL_PLATFORM="Darwin-x86_64" MM_BIN_NAME=mattermost $(MAKE) package-general @# Package diff --git a/db/migrations/migrations.list b/db/migrations/migrations.list index dcdb14f484..1bab52d768 100644 --- a/db/migrations/migrations.list +++ b/db/migrations/migrations.list @@ -186,6 +186,10 @@ db/migrations/mysql/000092_add_createat_to_teammembers.down.sql db/migrations/mysql/000092_add_createat_to_teammembers.up.sql db/migrations/mysql/000093_notify_admin.down.sql db/migrations/mysql/000093_notify_admin.up.sql +db/migrations/mysql/000094_threads_teamid.down.sql +db/migrations/mysql/000094_threads_teamid.up.sql +db/migrations/mysql/000095_remove_posts_parentid.down.sql +db/migrations/mysql/000095_remove_posts_parentid.up.sql db/migrations/postgres/000001_create_teams.down.sql db/migrations/postgres/000001_create_teams.up.sql db/migrations/postgres/000002_create_team_members.down.sql @@ -372,3 +376,7 @@ db/migrations/postgres/000092_add_createat_to_teamembers.down.sql db/migrations/postgres/000092_add_createat_to_teamembers.up.sql db/migrations/postgres/000093_notify_admin.down.sql db/migrations/postgres/000093_notify_admin.up.sql +db/migrations/postgres/000094_threads_teamid.down.sql +db/migrations/postgres/000094_threads_teamid.up.sql +db/migrations/postgres/000095_remove_posts_parentid.down.sql +db/migrations/postgres/000095_remove_posts_parentid.up.sql diff --git a/db/migrations/mysql/000094_threads_teamid.down.sql b/db/migrations/mysql/000094_threads_teamid.down.sql new file mode 100644 index 0000000000..2ac406dc39 --- /dev/null +++ b/db/migrations/mysql/000094_threads_teamid.down.sql @@ -0,0 +1,14 @@ +SET @preparedStatement = (SELECT IF( + EXISTS( + SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS + WHERE table_name = 'Threads' + AND table_schema = DATABASE() + AND column_name = 'TeamId' + ), + 'ALTER TABLE Threads DROP COLUMN TeamId;', + 'SELECT 1;' +)); + +PREPARE removeColumnIfExists FROM @preparedStatement; +EXECUTE removeColumnIfExists; +DEALLOCATE PREPARE removeColumnIfExists; diff --git a/db/migrations/mysql/000094_threads_teamid.up.sql b/db/migrations/mysql/000094_threads_teamid.up.sql new file mode 100644 index 0000000000..392a2d58ae --- /dev/null +++ b/db/migrations/mysql/000094_threads_teamid.up.sql @@ -0,0 +1,19 @@ +SET @preparedStatement = (SELECT IF( + NOT EXISTS( + SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = 'Threads' + AND table_schema = DATABASE() + AND column_name = 'TeamId' + ), + 'ALTER TABLE Threads ADD COLUMN TeamId varchar(26) DEFAULT NULL;', + 'SELECT 1;' +)); + +PREPARE addColumnIfNotExists FROM @preparedStatement; +EXECUTE addColumnIfNotExists; +DEALLOCATE PREPARE addColumnIfNotExists; + +UPDATE Threads, Channels +SET Threads.TeamId = Channels.TeamId +WHERE Channels.Id = Threads.ChannelId +AND Threads.TeamId IS NULL; diff --git a/db/migrations/mysql/000095_remove_posts_parentid.down.sql b/db/migrations/mysql/000095_remove_posts_parentid.down.sql new file mode 100644 index 0000000000..abdf428361 --- /dev/null +++ b/db/migrations/mysql/000095_remove_posts_parentid.down.sql @@ -0,0 +1 @@ +-- Intentionally left blank as forward migration is not reversible. diff --git a/db/migrations/mysql/000095_remove_posts_parentid.up.sql b/db/migrations/mysql/000095_remove_posts_parentid.up.sql new file mode 100644 index 0000000000..269febfac0 --- /dev/null +++ b/db/migrations/mysql/000095_remove_posts_parentid.up.sql @@ -0,0 +1,17 @@ +-- While upgrading from 5.x to 6.x with manual queries, there is a chance that this +-- migration is skipped. In that case, we need to make sure that the column is dropped. + +SET @preparedStatement = (SELECT IF( + ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = 'Posts' + AND table_schema = DATABASE() + AND column_name = 'ParentId' + ) > 0, + 'ALTER TABLE Posts DROP COLUMN ParentId;', + 'SELECT 1' +)); + +PREPARE alterIfExists FROM @preparedStatement; +EXECUTE alterIfExists; +DEALLOCATE PREPARE alterIfExists; diff --git a/db/migrations/postgres/000094_threads_teamid.down.sql b/db/migrations/postgres/000094_threads_teamid.down.sql new file mode 100644 index 0000000000..9f4c061f39 --- /dev/null +++ b/db/migrations/postgres/000094_threads_teamid.down.sql @@ -0,0 +1 @@ +ALTER TABLE threads DROP COLUMN IF EXISTS teamid; diff --git a/db/migrations/postgres/000094_threads_teamid.up.sql b/db/migrations/postgres/000094_threads_teamid.up.sql new file mode 100644 index 0000000000..5c66138f1e --- /dev/null +++ b/db/migrations/postgres/000094_threads_teamid.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE threads ADD COLUMN IF NOT EXISTS teamid VARCHAR(26); +UPDATE threads SET teamid = channels.teamid FROM channels WHERE threads.teamid IS NULL AND channels.id = threads.channelid; diff --git a/db/migrations/postgres/000095_remove_posts_parentid.down.sql b/db/migrations/postgres/000095_remove_posts_parentid.down.sql new file mode 100644 index 0000000000..abdf428361 --- /dev/null +++ b/db/migrations/postgres/000095_remove_posts_parentid.down.sql @@ -0,0 +1 @@ +-- Intentionally left blank as forward migration is not reversible. diff --git a/db/migrations/postgres/000095_remove_posts_parentid.up.sql b/db/migrations/postgres/000095_remove_posts_parentid.up.sql new file mode 100644 index 0000000000..173d58abef --- /dev/null +++ b/db/migrations/postgres/000095_remove_posts_parentid.up.sql @@ -0,0 +1,4 @@ +-- While upgrading from 5.x to 6.x with manual queries, there is a chance that this +-- migration is skipped. In that case, we need to make sure that the column is dropped. + +ALTER TABLE posts DROP COLUMN IF EXISTS parentid; diff --git a/model/cloud.go b/model/cloud.go index fd777672b4..2b072e1f00 100644 --- a/model/cloud.go +++ b/model/cloud.go @@ -52,13 +52,16 @@ type Product struct { Family SubscriptionFamily `json:"product_family"` RecurringInterval RecurringInterval `json:"recurring_interval"` BillingScheme BillingScheme `json:"billing_scheme"` + CrossSellsTo string `json:"cross_sells_to"` } type UserFacingProduct struct { - ID string `json:"id"` - Name string `json:"name"` - SKU string `json:"sku"` - PricePerSeat float64 `json:"price_per_seat"` + ID string `json:"id"` + Name string `json:"name"` + SKU string `json:"sku"` + PricePerSeat float64 `json:"price_per_seat"` + RecurringInterval RecurringInterval `json:"recurring_interval"` + CrossSellsTo string `json:"cross_sells_to"` } // AddOn represents an addon to a product. diff --git a/model/config.go b/model/config.go index bccc3904a1..be7a88b61d 100644 --- a/model/config.go +++ b/model/config.go @@ -2751,9 +2751,13 @@ type ProductSettings struct { EnablePublicSharedBoards *bool } -func (s *ProductSettings) SetDefaults() { +func (s *ProductSettings) SetDefaults(plugins map[string]map[string]any) { if s.EnablePublicSharedBoards == nil { - s.EnablePublicSharedBoards = NewBool(false) + if p, ok := plugins[PluginIdFocalboard]; ok { + s.EnablePublicSharedBoards = NewBool(p["enablepublicsharedboards"].(bool)) + } else { + s.EnablePublicSharedBoards = NewBool(false) + } } } @@ -3246,7 +3250,7 @@ func (o *Config) SetDefaults() { o.ThemeSettings.SetDefaults() o.ClusterSettings.SetDefaults() o.PluginSettings.SetDefaults(o.LogSettings) - o.ProductSettings.SetDefaults() + o.ProductSettings.SetDefaults(o.PluginSettings.Plugins) o.AnalyticsSettings.SetDefaults() o.ComplianceSettings.SetDefaults() o.LocalizationSettings.SetDefaults() diff --git a/model/thread.go b/model/thread.go index f9c4842c71..5d7bdb70f3 100644 --- a/model/thread.go +++ b/model/thread.go @@ -26,6 +26,9 @@ type Thread struct { // DeleteAt is a denormalized copy of the root posts's DeleteAt. In the database, it's // named ThreadDeleteAt to avoid introducing a query conflict with older server versions. DeleteAt int64 `json:"delete_at"` + + // TeamId is a denormalized copy of the Channel's teamId. + TeamId string `json:"team_id"` } type ThreadResponse struct { diff --git a/model/user.go b/model/user.go index d3ec889ce8..2140663006 100644 --- a/model/user.go +++ b/model/user.go @@ -318,89 +318,90 @@ func (u *User) DeepCopy() *User { // correctly. func (u *User) IsValid() *AppError { if !IsValidId(u.Id) { - return InvalidUserError("id", "") + return InvalidUserError("id", "", u.Id) } if u.CreateAt == 0 { - return InvalidUserError("create_at", u.Id) + return InvalidUserError("create_at", u.Id, u.CreateAt) } if u.UpdateAt == 0 { - return InvalidUserError("update_at", u.Id) + return InvalidUserError("update_at", u.Id, u.UpdateAt) } if u.IsRemote() { if !IsValidUsernameAllowRemote(u.Username) { - return InvalidUserError("username", u.Id) + return InvalidUserError("username", u.Id, u.Username) } } else { if !IsValidUsername(u.Username) { - return InvalidUserError("username", u.Id) + return InvalidUserError("username", u.Id, u.Username) } } if len(u.Email) > UserEmailMaxLength || u.Email == "" || !IsValidEmail(u.Email) { - return InvalidUserError("email", u.Id) + return InvalidUserError("email", u.Id, u.Email) } if utf8.RuneCountInString(u.Nickname) > UserNicknameMaxRunes { - return InvalidUserError("nickname", u.Id) + return InvalidUserError("nickname", u.Id, u.Nickname) } if utf8.RuneCountInString(u.Position) > UserPositionMaxRunes { - return InvalidUserError("position", u.Id) + return InvalidUserError("position", u.Id, u.Position) } if utf8.RuneCountInString(u.FirstName) > UserFirstNameMaxRunes { - return InvalidUserError("first_name", u.Id) + return InvalidUserError("first_name", u.Id, u.FirstName) } if utf8.RuneCountInString(u.LastName) > UserLastNameMaxRunes { - return InvalidUserError("last_name", u.Id) + return InvalidUserError("last_name", u.Id, u.LastName) } if u.AuthData != nil && len(*u.AuthData) > UserAuthDataMaxLength { - return InvalidUserError("auth_data", u.Id) + return InvalidUserError("auth_data", u.Id, u.AuthData) } if u.AuthData != nil && *u.AuthData != "" && u.AuthService == "" { - return InvalidUserError("auth_data_type", u.Id) + return InvalidUserError("auth_data_type", u.Id, *u.AuthData+" "+u.AuthService) } if u.Password != "" && u.AuthData != nil && *u.AuthData != "" { - return InvalidUserError("auth_data_pwd", u.Id) + return InvalidUserError("auth_data_pwd", u.Id, *u.AuthData) } if len(u.Password) > UserPasswordMaxLength { - return InvalidUserError("password_limit", u.Id) + return InvalidUserError("password_limit", u.Id, "") } if !IsValidLocale(u.Locale) { - return InvalidUserError("locale", u.Id) + return InvalidUserError("locale", u.Id, u.Locale) } if len(u.Timezone) > 0 { if tzJSON, err := json.Marshal(u.Timezone); err != nil { return NewAppError("User.IsValid", "model.user.is_valid.marshal.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } else if utf8.RuneCount(tzJSON) > UserTimezoneMaxRunes { - return InvalidUserError("timezone_limit", u.Id) + return InvalidUserError("timezone_limit", u.Id, u.Timezone) } } if len(u.Roles) > UserRolesMaxLength { return NewAppError("User.IsValid", "model.user.is_valid.roles_limit.app_error", - map[string]any{"Limit": UserRolesMaxLength}, "user_id="+u.Id, http.StatusBadRequest) + map[string]any{"Limit": UserRolesMaxLength}, "user_id="+u.Id+" roles_limit="+u.Roles, http.StatusBadRequest) } return nil } -func InvalidUserError(fieldName string, userId string) *AppError { +func InvalidUserError(fieldName, userId string, fieldValue any) *AppError { id := fmt.Sprintf("model.user.is_valid.%s.app_error", fieldName) details := "" if userId != "" { details = "user_id=" + userId } + details += fmt.Sprintf(" %s=%v", fieldName, fieldValue) return NewAppError("User.IsValid", id, nil, details, http.StatusBadRequest) } diff --git a/model/user_test.go b/model/user_test.go index d20ebacfa2..a99d36da47 100644 --- a/model/user_test.go +++ b/model/user_test.go @@ -77,37 +77,37 @@ func TestUserUpdateMentionKeysFromUsername(t *testing.T) { func TestUserIsValid(t *testing.T) { user := User{} appErr := user.IsValid() - require.True(t, HasExpectedUserIsValidError(appErr, "id", ""), "expected user is valid error: %s", appErr.Error()) + require.True(t, HasExpectedUserIsValidError(appErr, "id", "", user.Id), "expected user is valid error: %s", appErr.Error()) user.Id = NewId() appErr = user.IsValid() - require.True(t, HasExpectedUserIsValidError(appErr, "create_at", user.Id), "expected user is valid error: %s", appErr.Error()) + require.True(t, HasExpectedUserIsValidError(appErr, "create_at", user.Id, user.CreateAt), "expected user is valid error: %s", appErr.Error()) user.CreateAt = GetMillis() appErr = user.IsValid() - require.True(t, HasExpectedUserIsValidError(appErr, "update_at", user.Id), "expected user is valid error: %s", appErr.Error()) + require.True(t, HasExpectedUserIsValidError(appErr, "update_at", user.Id, user.UpdateAt), "expected user is valid error: %s", appErr.Error()) user.UpdateAt = GetMillis() appErr = user.IsValid() - require.True(t, HasExpectedUserIsValidError(appErr, "username", user.Id), "expected user is valid error: %s", appErr.Error()) + require.True(t, HasExpectedUserIsValidError(appErr, "username", user.Id, user.Username), "expected user is valid error: %s", appErr.Error()) user.Username = NewId() + "^hello#" appErr = user.IsValid() - require.True(t, HasExpectedUserIsValidError(appErr, "username", user.Id), "expected user is valid error: %s", appErr.Error()) + require.True(t, HasExpectedUserIsValidError(appErr, "username", user.Id, user.Username), "expected user is valid error: %s", appErr.Error()) user.Username = NewId() appErr = user.IsValid() - require.True(t, HasExpectedUserIsValidError(appErr, "email", user.Id), "expected user is valid error: %s", appErr.Error()) + require.True(t, HasExpectedUserIsValidError(appErr, "email", user.Id, user.Email), "expected user is valid error: %s", appErr.Error()) user.Email = strings.Repeat("01234567890", 20) appErr = user.IsValid() - require.True(t, HasExpectedUserIsValidError(appErr, "email", user.Id), "expected user is valid error: %s", appErr.Error()) + require.True(t, HasExpectedUserIsValidError(appErr, "email", user.Id, user.Email), "expected user is valid error: %s", appErr.Error()) user.Email = "user@example.com" user.Nickname = strings.Repeat("a", 65) appErr = user.IsValid() - require.True(t, HasExpectedUserIsValidError(appErr, "nickname", user.Id), "expected user is valid error: %s", appErr.Error()) + require.True(t, HasExpectedUserIsValidError(appErr, "nickname", user.Id, user.Nickname), "expected user is valid error: %s", appErr.Error()) user.Nickname = strings.Repeat("a", 64) require.Nil(t, user.IsValid()) @@ -118,12 +118,12 @@ func TestUserIsValid(t *testing.T) { user.FirstName = strings.Repeat("a", 65) appErr = user.IsValid() - require.True(t, HasExpectedUserIsValidError(appErr, "first_name", user.Id), "expected user is valid error: %s", appErr.Error()) + require.True(t, HasExpectedUserIsValidError(appErr, "first_name", user.Id, user.FirstName), "expected user is valid error: %s", appErr.Error()) user.FirstName = strings.Repeat("a", 64) user.LastName = strings.Repeat("a", 65) appErr = user.IsValid() - require.True(t, HasExpectedUserIsValidError(appErr, "last_name", user.Id), "expected user is valid error: %s", appErr.Error()) + require.True(t, HasExpectedUserIsValidError(appErr, "last_name", user.Id, user.LastName), "expected user is valid error: %s", appErr.Error()) user.LastName = strings.Repeat("a", 64) user.Position = strings.Repeat("a", 128) @@ -131,7 +131,7 @@ func TestUserIsValid(t *testing.T) { user.Position = strings.Repeat("a", 129) appErr = user.IsValid() - require.True(t, HasExpectedUserIsValidError(appErr, "position", user.Id), "expected user is valid error: %s", appErr.Error()) + require.True(t, HasExpectedUserIsValidError(appErr, "position", user.Id, user.Position), "expected user is valid error: %s", appErr.Error()) user.Position = "" user.Roles = strings.Repeat("a", UserRolesMaxLength) @@ -140,10 +140,10 @@ func TestUserIsValid(t *testing.T) { user.Roles = strings.Repeat("a", UserRolesMaxLength+1) appErr = user.IsValid() - require.True(t, HasExpectedUserIsValidError(appErr, "roles_limit", user.Id), "expected user is valid error: %s", appErr.Error()) + require.True(t, HasExpectedUserIsValidError(appErr, "roles_limit", user.Id, user.Roles), "expected user is valid error: %s", appErr.Error()) } -func HasExpectedUserIsValidError(err *AppError, fieldName string, userId string) bool { +func HasExpectedUserIsValidError(err *AppError, fieldName, userId string, fieldValue any) bool { if err == nil { return false } @@ -151,7 +151,7 @@ func HasExpectedUserIsValidError(err *AppError, fieldName string, userId string) return err.Where == "User.IsValid" && err.Id == fmt.Sprintf("model.user.is_valid.%s.app_error", fieldName) && err.StatusCode == http.StatusBadRequest && - (userId == "" || err.DetailedError == "user_id="+userId) + (userId == "" || err.DetailedError == fmt.Sprintf("user_id=%s %s=%v", userId, fieldName, fieldValue)) } func TestUserGetFullName(t *testing.T) { diff --git a/store/opentracinglayer/opentracinglayer.go b/store/opentracinglayer/opentracinglayer.go index 44f7d09e86..f66f3352b4 100644 --- a/store/opentracinglayer/opentracinglayer.go +++ b/store/opentracinglayer/opentracinglayer.go @@ -4152,7 +4152,7 @@ func (s *OpenTracingLayerGroupStore) GetGroupSyncable(groupID string, syncableID return result, err } -func (s *OpenTracingLayerGroupStore) GetGroups(page int, perPage int, opts model.GroupSearchOpts) ([]*model.Group, error) { +func (s *OpenTracingLayerGroupStore) GetGroups(page int, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetGroups") s.Root.Store.SetContext(newCtx) @@ -4161,7 +4161,7 @@ func (s *OpenTracingLayerGroupStore) GetGroups(page int, perPage int, opts model }() defer span.Finish() - result, err := s.GroupStore.GetGroups(page, perPage, opts) + result, err := s.GroupStore.GetGroups(page, perPage, opts, viewRestrictions) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) @@ -4260,6 +4260,24 @@ func (s *OpenTracingLayerGroupStore) GetMemberCount(groupID string) (int64, erro return result, err } +func (s *OpenTracingLayerGroupStore) GetMemberCountWithRestrictions(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetMemberCountWithRestrictions") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.GroupStore.GetMemberCountWithRestrictions(groupID, viewRestrictions) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerGroupStore) GetMemberUsers(groupID string) ([]*model.User, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetMemberUsers") @@ -4314,7 +4332,7 @@ func (s *OpenTracingLayerGroupStore) GetMemberUsersNotInChannel(groupID string, return result, err } -func (s *OpenTracingLayerGroupStore) GetMemberUsersPage(groupID string, page int, perPage int) ([]*model.User, error) { +func (s *OpenTracingLayerGroupStore) GetMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetMemberUsersPage") s.Root.Store.SetContext(newCtx) @@ -4323,7 +4341,7 @@ func (s *OpenTracingLayerGroupStore) GetMemberUsersPage(groupID string, page int }() defer span.Finish() - result, err := s.GroupStore.GetMemberUsersPage(groupID, page, perPage) + result, err := s.GroupStore.GetMemberUsersPage(groupID, page, perPage, viewRestrictions) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) @@ -4332,7 +4350,7 @@ func (s *OpenTracingLayerGroupStore) GetMemberUsersPage(groupID string, page int return result, err } -func (s *OpenTracingLayerGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int) ([]*model.User, error) { +func (s *OpenTracingLayerGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetNonMemberUsersPage") s.Root.Store.SetContext(newCtx) @@ -4341,7 +4359,7 @@ func (s *OpenTracingLayerGroupStore) GetNonMemberUsersPage(groupID string, page }() defer span.Finish() - result, err := s.GroupStore.GetNonMemberUsersPage(groupID, page, perPage) + result, err := s.GroupStore.GetNonMemberUsersPage(groupID, page, perPage, viewRestrictions) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) diff --git a/store/retrylayer/retrylayer.go b/store/retrylayer/retrylayer.go index 292924476a..c28caa0d31 100644 --- a/store/retrylayer/retrylayer.go +++ b/store/retrylayer/retrylayer.go @@ -4677,11 +4677,11 @@ func (s *RetryLayerGroupStore) GetGroupSyncable(groupID string, syncableID strin } -func (s *RetryLayerGroupStore) GetGroups(page int, perPage int, opts model.GroupSearchOpts) ([]*model.Group, error) { +func (s *RetryLayerGroupStore) GetGroups(page int, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, error) { tries := 0 for { - result, err := s.GroupStore.GetGroups(page, perPage, opts) + result, err := s.GroupStore.GetGroups(page, perPage, opts, viewRestrictions) if err == nil { return result, nil } @@ -4803,6 +4803,27 @@ func (s *RetryLayerGroupStore) GetMemberCount(groupID string) (int64, error) { } +func (s *RetryLayerGroupStore) GetMemberCountWithRestrictions(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, error) { + + tries := 0 + for { + result, err := s.GroupStore.GetMemberCountWithRestrictions(groupID, viewRestrictions) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerGroupStore) GetMemberUsers(groupID string) ([]*model.User, error) { tries := 0 @@ -4866,11 +4887,11 @@ func (s *RetryLayerGroupStore) GetMemberUsersNotInChannel(groupID string, channe } -func (s *RetryLayerGroupStore) GetMemberUsersPage(groupID string, page int, perPage int) ([]*model.User, error) { +func (s *RetryLayerGroupStore) GetMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) { tries := 0 for { - result, err := s.GroupStore.GetMemberUsersPage(groupID, page, perPage) + result, err := s.GroupStore.GetMemberUsersPage(groupID, page, perPage, viewRestrictions) if err == nil { return result, nil } @@ -4887,11 +4908,11 @@ func (s *RetryLayerGroupStore) GetMemberUsersPage(groupID string, page int, perP } -func (s *RetryLayerGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int) ([]*model.User, error) { +func (s *RetryLayerGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) { tries := 0 for { - result, err := s.GroupStore.GetNonMemberUsersPage(groupID, page, perPage) + result, err := s.GroupStore.GetNonMemberUsersPage(groupID, page, perPage, viewRestrictions) if err == nil { return result, nil } diff --git a/store/sqlstore/group_store.go b/store/sqlstore/group_store.go index 8a560395fa..c3341dc9d6 100644 --- a/store/sqlstore/group_store.go +++ b/store/sqlstore/group_store.go @@ -401,58 +401,59 @@ func (s *SqlGroupStore) GetMemberUsers(groupID string) ([]*model.User, error) { return groupMembers, nil } -func (s *SqlGroupStore) GetMemberUsersPage(groupID string, page int, perPage int) ([]*model.User, error) { +func (s *SqlGroupStore) GetMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) { groupMembers := []*model.User{} - query := ` - SELECT - Users.* - FROM - GroupMembers - JOIN Users ON Users.Id = GroupMembers.UserId - WHERE - GroupMembers.DeleteAt = 0 - AND Users.DeleteAt = 0 - AND GroupId = ? - ORDER BY - GroupMembers.CreateAt DESC - LIMIT - ? - OFFSET - ?` + query := s.getQueryBuilder(). + Select("u.*"). + From("GroupMembers"). + Join("Users u ON u.Id = GroupMembers.UserId"). + Where(sq.Eq{"GroupMembers.DeleteAt": 0}). + Where(sq.Eq{"u.DeleteAt": 0}). + Where(sq.Eq{"GroupId": groupID}). + Limit(uint64(perPage)). + Offset(uint64(page * perPage)). + OrderBy("u.CreateAt DESC") - if err := s.GetReplicaX().Select(&groupMembers, query, groupID, perPage, page*perPage); err != nil { + query = applyViewRestrictionsFilter(query, viewRestrictions, true) + + queryString, args, err := query.ToSql() + if err != nil { + return nil, errors.Wrap(err, "") + } + + if err := s.GetReplicaX().Select(&groupMembers, queryString, args...); err != nil { return nil, errors.Wrapf(err, "failed to find member Users for Group with id=%s", groupID) } return groupMembers, nil } -func (s *SqlGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int) ([]*model.User, error) { +func (s *SqlGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) { groupMembers := []*model.User{} if err := s.GetReplicaX().Get(&model.Group{}, "SELECT * FROM UserGroups WHERE Id = ?", groupID); err != nil { return nil, errors.Wrap(err, "GetNonMemberUsersPage") } - query := ` - SELECT - Users.* - FROM - Users - LEFT JOIN - GroupMembers ON (GroupMembers.UserId = Users.Id AND GroupMembers.GroupId = ?) - WHERE - Users.DeleteAt = 0 - AND ( GroupMembers.UserId IS NULL OR GroupMembers.DeleteAt != 0) - ORDER BY - GroupMembers.CreateAt DESC - LIMIT - ? - OFFSET - ?` + query := s.getQueryBuilder(). + Select("u.*"). + From("Users u"). + LeftJoin("GroupMembers ON (GroupMembers.UserId = u.Id AND GroupMembers.GroupId = ?)", groupID). + Where(sq.Eq{"u.DeleteAt": 0}). + Where("(GroupMembers.UserID IS NULL OR GroupMembers.DeleteAt != 0)"). + Limit(uint64(perPage)). + Offset(uint64(page * perPage)). + OrderBy("u.CreateAt DESC") - if err := s.GetReplicaX().Select(&groupMembers, query, groupID, perPage, page*perPage); err != nil { + query = applyViewRestrictionsFilter(query, viewRestrictions, true) + + queryString, args, err := query.ToSql() + if err != nil { + return nil, errors.Wrap(err, "") + } + + if err := s.GetReplicaX().Select(&groupMembers, queryString, args...); err != nil { return nil, errors.Wrapf(err, "failed to find member Users for Group with id=%s", groupID) } @@ -460,19 +461,27 @@ func (s *SqlGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage } func (s *SqlGroupStore) GetMemberCount(groupID string) (int64, error) { - query := ` - SELECT - count(*) - FROM - GroupMembers - JOIN Users ON Users.Id = GroupMembers.UserId - WHERE - GroupMembers.GroupId = ? - AND Users.DeleteAt = 0 - AND GroupMembers.DeleteAt = 0` + return s.GetMemberCountWithRestrictions(groupID, nil) +} + +func (s *SqlGroupStore) GetMemberCountWithRestrictions(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, error) { + query := s.getQueryBuilder(). + Select("COUNT(DISTINCT u.Id)"). + From("GroupMembers"). + Join("Users u ON u.Id = GroupMembers.UserId"). + Where(sq.Eq{"GroupMembers.GroupId": groupID}). + Where(sq.Eq{"u.DeleteAt": 0}). + Where(sq.Eq{"GroupMembers.DeleteAt": 0}) + + query = applyViewRestrictionsFilter(query, viewRestrictions, false) + + queryString, args, err := query.ToSql() + if err != nil { + return int64(0), errors.Wrap(err, "") + } var count int64 - err := s.GetReplicaX().Get(&count, query, groupID) + err = s.GetReplicaX().Get(&count, queryString, args...) if err != nil { return int64(0), errors.Wrapf(err, "failed to count member Users for Group with id=%s", groupID) } @@ -1385,15 +1394,30 @@ func (s *SqlGroupStore) GetGroupsAssociatedToChannelsByTeam(teamId string, opts return groups, nil } -func (s *SqlGroupStore) GetGroups(page, perPage int, opts model.GroupSearchOpts) ([]*model.Group, error) { +func (s *SqlGroupStore) GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, error) { groupsVar := groups{} groupsQuery := s.getQueryBuilder().Select("g.*") if opts.IncludeMemberCount { + countQuery := s.getQueryBuilder(). + Select("GroupMembers.GroupId, COUNT(DISTINCT u.Id) AS MemberCount"). + From("GroupMembers"). + LeftJoin("Users u ON u.Id = GroupMembers.UserId"). + Where(sq.Eq{"GroupMembers.DeleteAt": 0}). + Where(sq.Eq{"u.DeleteAt": 0}). + GroupBy("GroupId") + + countQuery = applyViewRestrictionsFilter(countQuery, viewRestrictions, false) + + countString, params, err := countQuery.PlaceholderFormat(sq.Question).ToSql() + if err != nil { + return nil, errors.Wrap(err, "get_groups_tosql") + } + groupsQuery = s.getQueryBuilder(). Select("g.*, coalesce(Members.MemberCount, 0) AS MemberCount"). - LeftJoin("(SELECT GroupMembers.GroupId, COUNT(*) AS MemberCount FROM GroupMembers LEFT JOIN Users ON Users.Id = GroupMembers.UserId WHERE GroupMembers.DeleteAt = 0 AND Users.DeleteAt = 0 GROUP BY GroupId) AS Members ON Members.GroupId = g.Id") + LeftJoin("("+countString+") AS Members ON Members.GroupId = g.Id", params...) } if opts.FilterHasMember != "" { diff --git a/store/sqlstore/integrity.go b/store/sqlstore/integrity.go index 51a262a3e4..3ee486aa9c 100644 --- a/store/sqlstore/integrity.go +++ b/store/sqlstore/integrity.go @@ -468,6 +468,7 @@ func checkPostsIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult results <- checkPostsFileInfoIntegrity(ss) results <- checkPostsPostsRootIdIntegrity(ss) results <- checkPostsReactionsIntegrity(ss) + results <- checkThreadsTeamsIntegrity(ss) } func checkSchemesIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) { @@ -511,6 +512,16 @@ func checkUsersIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult results <- checkUsersUserAccessTokensIntegrity(ss) } +func checkThreadsTeamsIntegrity(ss *SqlStore) model.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Teams", + parentIdAttr: "TeamId", + childName: "Threads", + childIdAttr: "PostId", + canParentIdBeEmpty: false, + }) +} + func CheckRelationalIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) { mlog.Info("Starting relational integrity checks...") checkChannelsIntegrity(ss, results) diff --git a/store/sqlstore/integrity_test.go b/store/sqlstore/integrity_test.go index 84096ab998..2b775c6bd3 100644 --- a/store/sqlstore/integrity_test.go +++ b/store/sqlstore/integrity_test.go @@ -650,9 +650,10 @@ func TestCheckPostsPostsRootIdIntegrity(t *testing.T) { }) t.Run("should generate a report with one record", func(t *testing.T) { - root := createPost(ss, model.NewId(), model.NewId(), "", "") + channel := createChannel(ss, model.NewId(), model.NewId()) + root := createPost(ss, channel.Id, model.NewId(), "", "") rootId := root.Id - post := createPost(ss, model.NewId(), model.NewId(), root.Id, root.Id) + post := createPost(ss, channel.Id, model.NewId(), root.Id, root.Id) dbmap.Exec(`DELETE FROM Posts WHERE Id=?`, root.Id) result := checkPostsPostsRootIdIntegrity(store) require.NoError(t, result.Err) @@ -663,6 +664,8 @@ func TestCheckPostsPostsRootIdIntegrity(t *testing.T) { ChildId: &post.Id, }, data.Records[0]) dbmap.Exec(`DELETE FROM Posts WHERE Id=?`, post.Id) + dbmap.Exec(`DELETE FROM Channels WHERE Id=?`, channel.Id) + dbmap.Exec(`DELETE FROM Threads WHERE PostId=?`, rootId) }) }) } @@ -1602,3 +1605,39 @@ func TestCheckUsersUserAccessTokensIntegrity(t *testing.T) { }) }) } + +func TestCheckThreadsTeamsIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + store := ss.(*SqlStore) + dbmap := store.GetMasterX() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkThreadsTeamsIntegrity(store) + require.NoError(t, result.Err) + data := result.Data.(model.RelationalIntegrityCheckData) + require.Empty(t, data.Records) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + team := createTeam(ss) + channel := createChannel(ss, team.Id, model.NewId()) + root := createPost(ss, channel.Id, model.NewId(), "", "") + post := createPost(ss, channel.Id, model.NewId(), root.Id, root.Id) + + dbmap.Exec(`DELETE FROM Teams WHERE Id=?`, team.Id) + result := checkThreadsTeamsIntegrity(store) + require.NoError(t, result.Err) + data := result.Data.(model.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + + require.Equal(t, model.OrphanedRecord{ + ParentId: &team.Id, + ChildId: &root.Id, + }, data.Records[0]) + dbmap.Exec(`DELETE FROM Posts WHERE Id=?`, post.Id) + dbmap.Exec(`DELETE FROM Posts WHERE Id=?`, root.Id) + dbmap.Exec(`DELETE FROM Channels WHERE Id=?`, channel.Id) + dbmap.Exec(`DELETE FROM Threads WHERE PostId=?`, root.Id) + }) + }) +} diff --git a/store/sqlstore/post_store.go b/store/sqlstore/post_store.go index 33dee42c36..14cec75d2b 100644 --- a/store/sqlstore/post_store.go +++ b/store/sqlstore/post_store.go @@ -2948,7 +2948,8 @@ func (s *SqlPostStore) updateThreadsFromPosts(transaction *sqlxTxWrapper, posts } threadsByRoots := []*model.Thread{} - if err := transaction.Select(&threadsByRoots, threadsByRootsSql, threadsByRootsArgs...); err != nil { + err = transaction.Select(&threadsByRoots, threadsByRootsSql, threadsByRootsArgs...) + if err != nil { return err } @@ -2957,6 +2958,8 @@ func (s *SqlPostStore) updateThreadsFromPosts(transaction *sqlxTxWrapper, posts threadByRoot[thread.PostId] = thread } + teamIdByChannelId := map[string]string{} + for rootId, posts := range postsByRoot { if thread, found := threadByRoot[rootId]; !found { data := []struct { @@ -2986,16 +2989,30 @@ func (s *SqlPostStore) updateThreadsFromPosts(transaction *sqlxTxWrapper, posts if err != nil { return err } + + channelId := posts[0].ChannelId + teamId, ok := teamIdByChannelId[channelId] + if !ok { + // get teamId for channel + err = transaction.Get(&teamId, "SELECT COALESCE(Channels.TeamId, '') FROM Channels WHERE Channels.Id=?", channelId) + if err != nil { + return err + } + + // store teamId for channel for efficiency + teamIdByChannelId[channelId] = teamId + } // no metadata entry, create one if _, err := transaction.NamedExec(`INSERT INTO Threads - (PostId, ChannelId, ReplyCount, LastReplyAt, Participants) + (PostId, ChannelId, ReplyCount, LastReplyAt, Participants, TeamId) VALUES - (:PostId, :ChannelId, :ReplyCount, :LastReplyAt, :Participants)`, &model.Thread{ + (:PostId, :ChannelId, :ReplyCount, :LastReplyAt, :Participants, :TeamId)`, &model.Thread{ PostId: rootId, - ChannelId: posts[0].ChannelId, + ChannelId: channelId, ReplyCount: count, LastReplyAt: lastReplyAt, Participants: participants, + TeamId: teamId, }); err != nil { return err } diff --git a/store/sqlstore/thread_store.go b/store/sqlstore/thread_store.go index a20b4f9362..3306a2856d 100644 --- a/store/sqlstore/thread_store.go +++ b/store/sqlstore/thread_store.go @@ -51,6 +51,7 @@ func (s *SqlThreadStore) initializeQueries() { "Threads.LastReplyAt", "Threads.Participants", "COALESCE(Threads.ThreadDeleteAt, 0) AS DeleteAt", + "COALESCE(Threads.TeamId, '') AS TeamId", ). From("Threads") @@ -62,6 +63,7 @@ func (s *SqlThreadStore) initializeQueries() { "Threads.LastReplyAt", "Threads.Participants", "COALESCE(Threads.ThreadDeleteAt, 0) AS ThreadDeleteAt", + "COALESCE(Threads.TeamId, '') AS TeamId", ). From("Threads") } @@ -95,10 +97,9 @@ func (s *SqlThreadStore) getTotalThreadsQuery(userId, teamId string, opts model. if teamId != "" { query = query. - LeftJoin("Channels ON Threads.ChannelId = Channels.Id"). Where(sq.Or{ - sq.Eq{"Channels.TeamId": teamId}, - sq.Eq{"Channels.TeamId": ""}, + sq.Eq{"Threads.TeamId": teamId}, + sq.Eq{"Threads.TeamId": ""}, }) } @@ -158,10 +159,9 @@ func (s *SqlThreadStore) GetTotalUnreadMentions(userId, teamId string, opts mode if teamId != "" { query = query. - LeftJoin("Channels ON Threads.ChannelId = Channels.Id"). Where(sq.Or{ - sq.Eq{"Channels.TeamId": teamId}, - sq.Eq{"Channels.TeamId": ""}, + sq.Eq{"Threads.TeamId": teamId}, + sq.Eq{"Threads.TeamId": ""}, }) } @@ -192,6 +192,7 @@ func (s *SqlThreadStore) GetThreadsForUser(userId, teamId string, opts model.Get UnreadMentions int64 Participants model.StringArray ThreadDeleteAt int64 + TeamId string model.Post } @@ -223,10 +224,9 @@ func (s *SqlThreadStore) GetThreadsForUser(userId, teamId string, opts model.Get // a team at all. if teamId != "" { query = query. - Join("Channels ON Threads.ChannelId = Channels.Id"). Where(sq.Or{ - sq.Eq{"Channels.TeamId": teamId}, - sq.Eq{"Channels.TeamId": ""}, + sq.Eq{"Threads.TeamId": teamId}, + sq.Eq{"Threads.TeamId": ""}, }) } @@ -322,7 +322,7 @@ func (s *SqlThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string) fetchConditions := sq.And{ sq.Eq{"ThreadMemberships.UserId": userID}, sq.Eq{"ThreadMemberships.Following": true}, - sq.Eq{"Channels.TeamId": teamIDs}, + sq.Eq{"Threads.TeamId": teamIDs}, sq.Eq{"COALESCE(Threads.ThreadDeleteAt, 0)": 0}, } @@ -348,10 +348,9 @@ func (s *SqlThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string) Select("COUNT(Threads.PostId) AS Count, TeamId"). From("Threads"). LeftJoin("ThreadMemberships ON Threads.PostId = ThreadMemberships.PostId"). - LeftJoin("Channels ON Threads.ChannelId = Channels.Id"). Where(fetchConditions). Where("Threads.LastReplyAt > ThreadMemberships.LastViewed"). - GroupBy("Channels.TeamId") + GroupBy("Threads.TeamId") err := s.GetReplicaX().SelectBuilder(&unreadThreads, repliesQuery) if err != nil { @@ -366,9 +365,8 @@ func (s *SqlThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string) Select("COALESCE(SUM(ThreadMemberships.UnreadMentions),0) AS Count, TeamId"). From("ThreadMemberships"). LeftJoin("Threads ON Threads.PostId = ThreadMemberships.PostId"). - LeftJoin("Channels ON Threads.ChannelId = Channels.Id"). Where(fetchConditions). - GroupBy("Channels.TeamId") + GroupBy("Threads.TeamId") err := s.GetReplicaX().SelectBuilder(&unreadMentions, mentionsQuery) if err != nil { @@ -449,6 +447,7 @@ func (s *SqlThreadStore) GetThreadForUser(teamId string, threadMembership *model UnreadMentions int64 Participants model.StringArray ThreadDeleteAt int64 + TeamId string model.Post } @@ -462,7 +461,7 @@ func (s *SqlThreadStore) GetThreadForUser(teamId string, threadMembership *model }) fetchConditions := sq.And{ - sq.Or{sq.Eq{"Channels.TeamId": teamId}, sq.Eq{"Channels.TeamId": ""}}, + sq.Or{sq.Eq{"Threads.TeamId": teamId}, sq.Eq{"Threads.TeamId": ""}}, sq.Eq{"Threads.PostId": threadMembership.PostId}, } @@ -476,7 +475,6 @@ func (s *SqlThreadStore) GetThreadForUser(teamId string, threadMembership *model query = query. Column(sq.Alias(unreadRepliesQuery, "UnreadReplies")). LeftJoin("Posts ON Posts.Id = Threads.PostId"). - LeftJoin("Channels ON Posts.ChannelId = Channels.Id"). Where(fetchConditions) err := s.GetReplicaX().GetBuilder(&thread, query) @@ -671,9 +669,8 @@ func (s *SqlThreadStore) GetMembershipsForUser(userId, teamId string) ([]*model. query := s.getQueryBuilder(). Select("ThreadMemberships.*"). Join("Threads ON Threads.PostId = ThreadMemberships.PostId"). - Join("Channels ON Threads.ChannelId = Channels.Id"). From("ThreadMemberships"). - Where(sq.Or{sq.Eq{"Channels.TeamId": teamId}, sq.Eq{"Channels.TeamId": ""}}). + Where(sq.Or{sq.Eq{"Threads.TeamId": teamId}, sq.Eq{"Threads.TeamId": ""}}). Where(sq.Eq{"ThreadMemberships.UserId": userId}) err := s.GetReplicaX().SelectBuilder(&memberships, query) diff --git a/store/store.go b/store/store.go index f7c33a938e..5b4ca3dc6d 100644 --- a/store/store.go +++ b/store/store.go @@ -833,10 +833,11 @@ type GroupStore interface { Delete(groupID string) (*model.Group, error) GetMemberUsers(groupID string) ([]*model.User, error) - GetMemberUsersPage(groupID string, page int, perPage int) ([]*model.User, error) + GetMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) + GetMemberCountWithRestrictions(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, error) GetMemberCount(groupID string) (int64, error) - GetNonMemberUsersPage(groupID string, page int, perPage int) ([]*model.User, error) + GetNonMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) GetMemberUsersInTeam(groupID string, teamID string) ([]*model.User, error) GetMemberUsersNotInChannel(groupID string, channelID string) ([]*model.User, error) @@ -880,7 +881,7 @@ type GroupStore interface { GetGroupsAssociatedToChannelsByTeam(teamID string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, error) CountGroupsByTeam(teamID string, opts model.GroupSearchOpts) (int64, error) - GetGroups(page, perPage int, opts model.GroupSearchOpts) ([]*model.Group, error) + GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, error) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, error) CountTeamMembersMinusGroupMembers(teamID string, groupIDs []string) (int64, error) diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index 3f4dd4cedd..2138f011ed 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -6894,11 +6894,19 @@ func testChannelStoreGetPinnedPosts(t *testing.T, ss store.Store) { require.Empty(t, pl.Posts, "wasn't supposed to return posts") t.Run("with correct ReplyCount", func(t *testing.T) { - channelId := model.NewId() + teamId := model.NewId() + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + userId := model.NewId() post1, err := ss.Post().Save(&model.Post{ - ChannelId: channelId, + ChannelId: channel.Id, UserId: userId, Message: "message", IsPinned: true, @@ -6907,7 +6915,7 @@ func testChannelStoreGetPinnedPosts(t *testing.T, ss store.Store) { time.Sleep(time.Millisecond) post2, err := ss.Post().Save(&model.Post{ - ChannelId: channelId, + ChannelId: channel.Id, UserId: userId, Message: "message", IsPinned: true, @@ -6916,7 +6924,7 @@ func testChannelStoreGetPinnedPosts(t *testing.T, ss store.Store) { time.Sleep(time.Millisecond) post3, err := ss.Post().Save(&model.Post{ - ChannelId: channelId, + ChannelId: channel.Id, UserId: userId, RootId: post1.Id, Message: "message", @@ -6925,7 +6933,7 @@ func testChannelStoreGetPinnedPosts(t *testing.T, ss store.Store) { require.NoError(t, err) time.Sleep(time.Millisecond) - posts, err := ss.Channel().GetPinnedPosts(channelId) + posts, err := ss.Channel().GetPinnedPosts(channel.Id) require.NoError(t, err) require.Len(t, posts.Posts, 3) require.Equal(t, posts.Posts[post1.Id].ReplyCount, int64(1)) diff --git a/store/storetest/group_store.go b/store/storetest/group_store.go index e7a199da24..8696e15bf1 100644 --- a/store/storetest/group_store.go +++ b/store/storetest/group_store.go @@ -836,25 +836,25 @@ func testGroupGetMemberUsersPage(t *testing.T, ss store.Store) { require.NoError(t, err) // Check returns members - groupMembers, err := ss.Group().GetMemberUsersPage(group.Id, 0, 100) + groupMembers, err := ss.Group().GetMemberUsersPage(group.Id, 0, 100, nil) require.NoError(t, err) require.Equal(t, 3, len(groupMembers)) // Check page 1 - groupMembers, err = ss.Group().GetMemberUsersPage(group.Id, 0, 2) + groupMembers, err = ss.Group().GetMemberUsersPage(group.Id, 0, 2, nil) require.NoError(t, err) require.Equal(t, 2, len(groupMembers)) require.Equal(t, user3.Id, groupMembers[0].Id) require.Equal(t, user2.Id, groupMembers[1].Id) // Check page 2 - groupMembers, err = ss.Group().GetMemberUsersPage(group.Id, 1, 2) + groupMembers, err = ss.Group().GetMemberUsersPage(group.Id, 1, 2, nil) require.NoError(t, err) require.Equal(t, 1, len(groupMembers)) require.Equal(t, user1.Id, groupMembers[0].Id) // Check madeup id - groupMembers, err = ss.Group().GetMemberUsersPage(model.NewId(), 0, 100) + groupMembers, err = ss.Group().GetMemberUsersPage(model.NewId(), 0, 100, nil) require.NoError(t, err) require.Equal(t, 0, len(groupMembers)) @@ -863,7 +863,7 @@ func testGroupGetMemberUsersPage(t *testing.T, ss store.Store) { require.NoError(t, err) // Should not return deleted members - groupMembers, err = ss.Group().GetMemberUsersPage(group.Id, 0, 100) + groupMembers, err = ss.Group().GetMemberUsersPage(group.Id, 0, 100, nil) require.NoError(t, err) require.Equal(t, 2, len(groupMembers)) } @@ -3476,6 +3476,13 @@ func testGetGroups(t *testing.T, ss store.Store) { user2, err := ss.User().Save(u2) require.NoError(t, err) + u3 := &model.User{ + Email: MakeEmail(), + Username: model.NewId(), + } + user3, err := ss.User().Save(u3) + require.NoError(t, err) + _, err = ss.Group().UpsertMember(group1.Id, user1.Id) require.NoError(t, err) @@ -3485,9 +3492,20 @@ func testGetGroups(t *testing.T, ss store.Store) { _, err = ss.Group().UpsertMember(group2.Id, user2.Id) require.NoError(t, err) + _, err = ss.Group().UpsertMember(group2.Id, user3.Id) + require.NoError(t, err) + _, err = ss.Group().UpsertMember(deletedGroup.Id, user1.Id) require.NoError(t, err) + m1 := model.ChannelMember{ + ChannelId: channel1.Id, + UserId: user1.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + } + _, err = ss.Channel().SaveMember(&m1) + require.NoError(t, err) + user2.DeleteAt = 1 u2Update, _ := ss.User().Update(user2, true) @@ -3519,39 +3537,44 @@ func testGetGroups(t *testing.T, ss store.Store) { require.NoError(t, nErr) testCases := []struct { - Name string - Page int - PerPage int - Opts model.GroupSearchOpts - Resultf func([]*model.Group) bool + Name string + Page int + PerPage int + Opts model.GroupSearchOpts + Resultf func([]*model.Group) bool + Restrictions *model.ViewUsersRestrictions }{ { - Name: "Get all the Groups", - Opts: model.GroupSearchOpts{}, - Page: 0, - PerPage: 3, - Resultf: func(groups []*model.Group) bool { return len(groups) == 3 }, + Name: "Get all the Groups", + Opts: model.GroupSearchOpts{}, + Page: 0, + PerPage: 3, + Resultf: func(groups []*model.Group) bool { return len(groups) == 3 }, + Restrictions: nil, }, { - Name: "Get first Group with page 0 with 1 element", - Opts: model.GroupSearchOpts{}, - Page: 0, - PerPage: 1, - Resultf: func(groups []*model.Group) bool { return len(groups) == 1 }, + Name: "Get first Group with page 0 with 1 element", + Opts: model.GroupSearchOpts{}, + Page: 0, + PerPage: 1, + Resultf: func(groups []*model.Group) bool { return len(groups) == 1 }, + Restrictions: nil, }, { - Name: "Get single result from page 1", - Opts: model.GroupSearchOpts{}, - Page: 1, - PerPage: 1, - Resultf: func(groups []*model.Group) bool { return len(groups) == 1 }, + Name: "Get single result from page 1", + Opts: model.GroupSearchOpts{}, + Page: 1, + PerPage: 1, + Resultf: func(groups []*model.Group) bool { return len(groups) == 1 }, + Restrictions: nil, }, { - Name: "Get multiple results from page 1", - Opts: model.GroupSearchOpts{}, - Page: 1, - PerPage: 2, - Resultf: func(groups []*model.Group) bool { return len(groups) == 2 }, + Name: "Get multiple results from page 1", + Opts: model.GroupSearchOpts{}, + Page: 1, + PerPage: 2, + Resultf: func(groups []*model.Group) bool { return len(groups) == 2 }, + Restrictions: nil, }, { Name: "Get group matching name", @@ -3566,6 +3589,7 @@ func testGetGroups(t *testing.T, ss store.Store) { } return true }, + Restrictions: nil, }, { Name: "Get group matching display name", @@ -3580,6 +3604,7 @@ func testGetGroups(t *testing.T, ss store.Store) { } return true }, + Restrictions: nil, }, { Name: "Get group matching multiple display names", @@ -3594,6 +3619,7 @@ func testGetGroups(t *testing.T, ss store.Store) { } return true }, + Restrictions: nil, }, { Name: "Include member counts", @@ -3605,7 +3631,7 @@ func testGetGroups(t *testing.T, ss store.Store) { if g.MemberCount == nil { return false } - if g.Id == group1.Id && *g.MemberCount != 1 { + if (g.Id == group1.Id || g.Id == group2.Id) && *g.MemberCount != 1 { return false } if g.DeleteAt != 0 { @@ -3614,6 +3640,31 @@ func testGetGroups(t *testing.T, ss store.Store) { } return true }, + Restrictions: nil, + }, + { + Name: "Include member counts with restrictions", + Opts: model.GroupSearchOpts{IncludeMemberCount: true}, + Page: 0, + PerPage: 100, + Resultf: func(groups []*model.Group) bool { + for _, g := range groups { + if g.MemberCount == nil { + return false + } + if g.Id == group1.Id && *g.MemberCount != 1 { + return false + } + if g.Id == group2.Id && *g.MemberCount != 0 { + return false + } + if g.DeleteAt != 0 { + return false + } + } + return true + }, + Restrictions: &model.ViewUsersRestrictions{Channels: []string{channel1.Id}}, }, { Name: "Not associated to team", @@ -3634,6 +3685,7 @@ func testGetGroups(t *testing.T, ss store.Store) { } return true }, + Restrictions: nil, }, { Name: "Not associated to other team", @@ -3654,6 +3706,7 @@ func testGetGroups(t *testing.T, ss store.Store) { } return true }, + Restrictions: nil, }, { Name: "Include allow reference", @@ -3674,6 +3727,7 @@ func testGetGroups(t *testing.T, ss store.Store) { } return true }, + Restrictions: nil, }, { Name: "Use Since return all", @@ -3691,6 +3745,7 @@ func testGetGroups(t *testing.T, ss store.Store) { } return true }, + Restrictions: nil, }, { Name: "Use Since return none", @@ -3700,6 +3755,7 @@ func testGetGroups(t *testing.T, ss store.Store) { Resultf: func(groups []*model.Group) bool { return len(groups) == 0 }, + Restrictions: nil, }, { Name: "Filter groups from group-constrained teams", @@ -3709,6 +3765,7 @@ func testGetGroups(t *testing.T, ss store.Store) { Resultf: func(groups []*model.Group) bool { return len(groups) == 2 && groups[0].Id == group1.Id && groups[1].Id == group2.Id }, + Restrictions: nil, }, { Name: "Filter groups from group-constrained page 0", @@ -3718,6 +3775,7 @@ func testGetGroups(t *testing.T, ss store.Store) { Resultf: func(groups []*model.Group) bool { return groups[0].Id == group1.Id }, + Restrictions: nil, }, { Name: "Filter groups from group-constrained page 1", @@ -3727,6 +3785,7 @@ func testGetGroups(t *testing.T, ss store.Store) { Resultf: func(groups []*model.Group) bool { return groups[0].Id == group2.Id }, + Restrictions: nil, }, { Name: "Non-group constrained team with no associated groups still returns groups for the child channel", @@ -3736,6 +3795,7 @@ func testGetGroups(t *testing.T, ss store.Store) { Resultf: func(groups []*model.Group) bool { return len(groups) > 0 }, + Restrictions: nil, }, { Name: "Filter by group member", @@ -3745,6 +3805,7 @@ func testGetGroups(t *testing.T, ss store.Store) { Resultf: func(groups []*model.Group) bool { return len(groups) == 1 && groups[0].Id == group1.Id }, + Restrictions: nil, }, { Name: "Filter by non-existent group member", @@ -3754,6 +3815,7 @@ func testGetGroups(t *testing.T, ss store.Store) { Resultf: func(groups []*model.Group) bool { return len(groups) == 0 }, + Restrictions: nil, }, { Name: "Filter by non-member member", @@ -3763,12 +3825,13 @@ func testGetGroups(t *testing.T, ss store.Store) { Resultf: func(groups []*model.Group) bool { return len(groups) == 2 }, + Restrictions: nil, }, } for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { - groups, err := ss.Group().GetGroups(tc.Page, tc.PerPage, tc.Opts) + groups, err := ss.Group().GetGroups(tc.Page, tc.PerPage, tc.Opts, tc.Restrictions) require.NoError(t, err) require.True(t, tc.Resultf(groups)) }) @@ -5048,7 +5111,7 @@ func groupTestGetNonMemberUsersPage(t *testing.T, ss store.Store) { _, nErr = ss.User().Save(u2) require.NoError(t, nErr) - users, err := ss.Group().GetNonMemberUsersPage(group.Id, 0, 1000) + users, err := ss.Group().GetNonMemberUsersPage(group.Id, 0, 1000, nil) require.NoError(t, err) originalLen := len(users) @@ -5056,11 +5119,11 @@ func groupTestGetNonMemberUsersPage(t *testing.T, ss store.Store) { _, err = ss.Group().UpsertMember(group.Id, user1.Id) require.NoError(t, err) - users, err = ss.Group().GetNonMemberUsersPage(group.Id, 0, 1000) + users, err = ss.Group().GetNonMemberUsersPage(group.Id, 0, 1000, nil) require.NoError(t, err) require.Len(t, users, originalLen-1) - users, err = ss.Group().GetNonMemberUsersPage(model.NewId(), 0, 1000) + users, err = ss.Group().GetNonMemberUsersPage(model.NewId(), 0, 1000, nil) require.Error(t, err) require.Nil(t, users) } diff --git a/store/storetest/mocks/GroupStore.go b/store/storetest/mocks/GroupStore.go index f3f2f839a9..80d6fdd492 100644 --- a/store/storetest/mocks/GroupStore.go +++ b/store/storetest/mocks/GroupStore.go @@ -577,13 +577,13 @@ func (_m *GroupStore) GetGroupSyncable(groupID string, syncableID string, syncab return r0, r1 } -// GetGroups provides a mock function with given fields: page, perPage, opts -func (_m *GroupStore) GetGroups(page int, perPage int, opts model.GroupSearchOpts) ([]*model.Group, error) { - ret := _m.Called(page, perPage, opts) +// GetGroups provides a mock function with given fields: page, perPage, opts, viewRestrictions +func (_m *GroupStore) GetGroups(page int, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, error) { + ret := _m.Called(page, perPage, opts, viewRestrictions) var r0 []*model.Group - if rf, ok := ret.Get(0).(func(int, int, model.GroupSearchOpts) []*model.Group); ok { - r0 = rf(page, perPage, opts) + if rf, ok := ret.Get(0).(func(int, int, model.GroupSearchOpts, *model.ViewUsersRestrictions) []*model.Group); ok { + r0 = rf(page, perPage, opts, viewRestrictions) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*model.Group) @@ -591,8 +591,8 @@ func (_m *GroupStore) GetGroups(page int, perPage int, opts model.GroupSearchOpt } var r1 error - if rf, ok := ret.Get(1).(func(int, int, model.GroupSearchOpts) error); ok { - r1 = rf(page, perPage, opts) + if rf, ok := ret.Get(1).(func(int, int, model.GroupSearchOpts, *model.ViewUsersRestrictions) error); ok { + r1 = rf(page, perPage, opts, viewRestrictions) } else { r1 = ret.Error(1) } @@ -713,6 +713,27 @@ func (_m *GroupStore) GetMemberCount(groupID string) (int64, error) { return r0, r1 } +// GetMemberCountWithRestrictions provides a mock function with given fields: groupID, viewRestrictions +func (_m *GroupStore) GetMemberCountWithRestrictions(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, error) { + ret := _m.Called(groupID, viewRestrictions) + + var r0 int64 + if rf, ok := ret.Get(0).(func(string, *model.ViewUsersRestrictions) int64); ok { + r0 = rf(groupID, viewRestrictions) + } else { + r0 = ret.Get(0).(int64) + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, *model.ViewUsersRestrictions) error); ok { + r1 = rf(groupID, viewRestrictions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetMemberUsers provides a mock function with given fields: groupID func (_m *GroupStore) GetMemberUsers(groupID string) ([]*model.User, error) { ret := _m.Called(groupID) @@ -782,13 +803,13 @@ func (_m *GroupStore) GetMemberUsersNotInChannel(groupID string, channelID strin return r0, r1 } -// GetMemberUsersPage provides a mock function with given fields: groupID, page, perPage -func (_m *GroupStore) GetMemberUsersPage(groupID string, page int, perPage int) ([]*model.User, error) { - ret := _m.Called(groupID, page, perPage) +// GetMemberUsersPage provides a mock function with given fields: groupID, page, perPage, viewRestrictions +func (_m *GroupStore) GetMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) { + ret := _m.Called(groupID, page, perPage, viewRestrictions) var r0 []*model.User - if rf, ok := ret.Get(0).(func(string, int, int) []*model.User); ok { - r0 = rf(groupID, page, perPage) + if rf, ok := ret.Get(0).(func(string, int, int, *model.ViewUsersRestrictions) []*model.User); ok { + r0 = rf(groupID, page, perPage, viewRestrictions) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*model.User) @@ -796,8 +817,8 @@ func (_m *GroupStore) GetMemberUsersPage(groupID string, page int, perPage int) } var r1 error - if rf, ok := ret.Get(1).(func(string, int, int) error); ok { - r1 = rf(groupID, page, perPage) + if rf, ok := ret.Get(1).(func(string, int, int, *model.ViewUsersRestrictions) error); ok { + r1 = rf(groupID, page, perPage, viewRestrictions) } else { r1 = ret.Error(1) } @@ -805,13 +826,13 @@ func (_m *GroupStore) GetMemberUsersPage(groupID string, page int, perPage int) return r0, r1 } -// GetNonMemberUsersPage provides a mock function with given fields: groupID, page, perPage -func (_m *GroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int) ([]*model.User, error) { - ret := _m.Called(groupID, page, perPage) +// GetNonMemberUsersPage provides a mock function with given fields: groupID, page, perPage, viewRestrictions +func (_m *GroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) { + ret := _m.Called(groupID, page, perPage, viewRestrictions) var r0 []*model.User - if rf, ok := ret.Get(0).(func(string, int, int) []*model.User); ok { - r0 = rf(groupID, page, perPage) + if rf, ok := ret.Get(0).(func(string, int, int, *model.ViewUsersRestrictions) []*model.User); ok { + r0 = rf(groupID, page, perPage, viewRestrictions) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*model.User) @@ -819,8 +840,8 @@ func (_m *GroupStore) GetNonMemberUsersPage(groupID string, page int, perPage in } var r1 error - if rf, ok := ret.Get(1).(func(string, int, int) error); ok { - r1 = rf(groupID, page, perPage) + if rf, ok := ret.Get(1).(func(string, int, int, *model.ViewUsersRestrictions) error); ok { + r1 = rf(groupID, page, perPage, viewRestrictions) } else { r1 = ret.Error(1) } diff --git a/store/storetest/post_store.go b/store/storetest/post_store.go index dd1ba40ae4..677d049218 100644 --- a/store/storetest/post_store.go +++ b/store/storetest/post_store.go @@ -78,20 +78,45 @@ func testPostStoreSave(t *testing.T, ss store.Store) { }) t.Run("Save replies", func(t *testing.T) { + teamId := model.NewId() + channel1, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + o1 := model.Post{} - o1.ChannelId = model.NewId() + o1.ChannelId = channel1.Id o1.UserId = model.NewId() o1.RootId = model.NewId() o1.Message = NewTestId() + channel2, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName2", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + o2 := model.Post{} - o2.ChannelId = model.NewId() + o2.ChannelId = channel2.Id o2.UserId = model.NewId() o2.RootId = o1.RootId o2.Message = NewTestId() + channel3, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName3", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + o3 := model.Post{} - o3.ChannelId = model.NewId() + o3.ChannelId = channel3.Id o3.UserId = model.NewId() o3.RootId = model.NewId() o3.Message = NewTestId() @@ -123,12 +148,21 @@ func testPostStoreSave(t *testing.T, ss store.Store) { }) t.Run("Update reply should update the UpdateAt of the root post", func(t *testing.T) { + teamId := model.NewId() + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + rootPost := model.Post{} - rootPost.ChannelId = model.NewId() + rootPost.ChannelId = channel.Id rootPost.UserId = model.NewId() rootPost.Message = NewTestId() - _, err := ss.Post().Save(&rootPost) + _, err = ss.Post().Save(&rootPost) require.NoError(t, err) time.Sleep(2 * time.Millisecond) @@ -241,26 +275,58 @@ func testPostStoreSaveMultiple(t *testing.T, ss store.Store) { }) t.Run("Save replies", func(t *testing.T) { + teamId := model.NewId() + channel1, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + + channel2, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName2", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + + channel3, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName3", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + + channel4, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName4", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) o1 := model.Post{} - o1.ChannelId = model.NewId() + o1.ChannelId = channel1.Id o1.UserId = model.NewId() o1.RootId = model.NewId() o1.Message = NewTestId() o2 := model.Post{} - o2.ChannelId = model.NewId() + o2.ChannelId = channel2.Id o2.UserId = model.NewId() o2.RootId = o1.RootId o2.Message = NewTestId() o3 := model.Post{} - o3.ChannelId = model.NewId() + o3.ChannelId = channel3.Id o3.UserId = model.NewId() o3.RootId = model.NewId() o3.Message = NewTestId() o4 := model.Post{} - o4.ChannelId = model.NewId() + o4.ChannelId = channel4.Id o4.UserId = model.NewId() o4.Message = NewTestId() @@ -291,8 +357,17 @@ func testPostStoreSaveMultiple(t *testing.T, ss store.Store) { }) t.Run("Update reply should update the UpdateAt of the root post", func(t *testing.T) { + teamId := model.NewId() + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + rootPost := model.Post{} - rootPost.ChannelId = model.NewId() + rootPost.ChannelId = channel.Id rootPost.UserId = model.NewId() rootPost.Message = NewTestId() @@ -302,7 +377,7 @@ func testPostStoreSaveMultiple(t *testing.T, ss store.Store) { replyPost.Message = NewTestId() replyPost.RootId = rootPost.Id - _, _, err := ss.Post().SaveMultiple([]*model.Post{&rootPost, &replyPost}) + _, _, err = ss.Post().SaveMultiple([]*model.Post{&rootPost, &replyPost}) require.NoError(t, err) rrootPost, err := ss.Post().GetSingle(rootPost.Id, false) @@ -367,34 +442,74 @@ func testPostStoreSaveMultiple(t *testing.T, ss store.Store) { }) t.Run("Thread participants", func(t *testing.T) { + teamId := model.NewId() + channel1, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + o1 := model.Post{} - o1.ChannelId = model.NewId() + o1.ChannelId = channel1.Id o1.UserId = model.NewId() o1.Message = "jessica hyde" + model.NewId() + "b" root, err := ss.Post().Save(&o1) require.NoError(t, err) + channel2, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName2", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + + channel3, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName3", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + + channel4, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName4", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + + channel5, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName5", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) o2 := model.Post{} - o2.ChannelId = model.NewId() + o2.ChannelId = channel2.Id o2.UserId = model.NewId() o2.RootId = root.Id o2.Message = "zz" + model.NewId() + "b" o3 := model.Post{} - o3.ChannelId = model.NewId() + o3.ChannelId = channel3.Id o3.UserId = model.NewId() o3.RootId = root.Id o3.Message = "zz" + model.NewId() + "b" o4 := model.Post{} - o4.ChannelId = model.NewId() + o4.ChannelId = channel4.Id o4.UserId = o2.UserId o4.RootId = root.Id o4.Message = "zz" + model.NewId() + "b" o5 := model.Post{} - o5.ChannelId = model.NewId() + o5.ChannelId = channel5.Id o5.UserId = o1.UserId o5.RootId = root.Id o5.Message = "zz" + model.NewId() + "b" @@ -438,7 +553,7 @@ func testPostStoreSaveMultiple(t *testing.T, ss store.Store) { } func testPostStoreSaveChannelMsgCounts(t *testing.T, ss store.Store) { - c1 := &model.Channel{Name: model.NewId(), DisplayName: "posttestchannel", Type: model.ChannelTypeOpen} + c1 := &model.Channel{Name: model.NewId(), DisplayName: "posttestchannel", Type: model.ChannelTypeOpen, TeamId: model.NewId()} _, err := ss.Channel().Save(c1, 1000000) require.NoError(t, err) @@ -484,15 +599,23 @@ func testPostStoreSaveChannelMsgCounts(t *testing.T, ss store.Store) { } func testPostStoreGet(t *testing.T, ss store.Store) { + teamId := model.NewId() + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) o1 := &model.Post{} - o1.ChannelId = model.NewId() + o1.ChannelId = channel.Id o1.UserId = model.NewId() o1.Message = NewTestId() etag1 := ss.Post().GetEtag(o1.ChannelId, false, false) require.Equal(t, 0, strings.Index(etag1, model.CurrentVersion+"."), "Invalid Etag") - o1, err := ss.Post().Save(o1) + o1, err = ss.Post().Save(o1) require.NoError(t, err) etag2 := ss.Post().GetEtag(o1.ChannelId, false, false) @@ -511,8 +634,17 @@ func testPostStoreGet(t *testing.T, ss store.Store) { func testPostStoreGetForThread(t *testing.T, ss store.Store) { t.Run("Post thread is followed", func(t *testing.T) { - o1 := &model.Post{ChannelId: model.NewId(), UserId: model.NewId(), Message: NewTestId()} - o1, err := ss.Post().Save(o1) + teamId := model.NewId() + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + + o1 := &model.Post{ChannelId: channel.Id, UserId: model.NewId(), Message: NewTestId()} + o1, err = ss.Post().Save(o1) require.NoError(t, err) _, err = ss.Post().Save(&model.Post{ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestId(), RootId: o1.Id}) require.NoError(t, err) @@ -532,8 +664,17 @@ func testPostStoreGetForThread(t *testing.T, ss store.Store) { }) t.Run("Post thread is explicitly not followed", func(t *testing.T) { - o1 := &model.Post{ChannelId: model.NewId(), UserId: model.NewId(), Message: NewTestId()} - o1, err := ss.Post().Save(o1) + teamId := model.NewId() + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + + o1 := &model.Post{ChannelId: channel.Id, UserId: model.NewId(), Message: NewTestId()} + o1, err = ss.Post().Save(o1) require.NoError(t, err) _, err = ss.Post().Save(&model.Post{ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestId(), RootId: o1.Id}) require.NoError(t, err) @@ -553,8 +694,17 @@ func testPostStoreGetForThread(t *testing.T, ss store.Store) { }) t.Run("Post threadmembership does not exist", func(t *testing.T) { - o1 := &model.Post{ChannelId: model.NewId(), UserId: model.NewId(), Message: NewTestId()} - o1, err := ss.Post().Save(o1) + teamId := model.NewId() + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + + o1 := &model.Post{ChannelId: channel.Id, UserId: model.NewId(), Message: NewTestId()} + o1, err = ss.Post().Save(o1) require.NoError(t, err) _, err = ss.Post().Save(&model.Post{ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestId(), RootId: o1.Id}) require.NoError(t, err) @@ -570,7 +720,16 @@ func testPostStoreGetForThread(t *testing.T, ss store.Store) { t.Run("Pagination", func(t *testing.T) { t.Skip("MM-46134") - o1, err := ss.Post().Save(&model.Post{ChannelId: model.NewId(), UserId: model.NewId(), Message: NewTestId()}) + teamId := model.NewId() + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + + o1, err := ss.Post().Save(&model.Post{ChannelId: channel.Id, UserId: model.NewId(), Message: NewTestId()}) require.NoError(t, err) _, err = ss.Post().Save(&model.Post{ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestId(), RootId: o1.Id}) require.NoError(t, err) @@ -698,8 +857,17 @@ func testPostStoreGetForThread(t *testing.T, ss store.Store) { } func testPostStoreGetSingle(t *testing.T, ss store.Store) { + teamId := model.NewId() + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + o1 := &model.Post{} - o1.ChannelId = model.NewId() + o1.ChannelId = channel.Id o1.UserId = model.NewId() o1.Message = NewTestId() @@ -708,7 +876,7 @@ func testPostStoreGetSingle(t *testing.T, ss store.Store) { o2.UserId = o1.UserId o2.Message = NewTestId() - o1, err := ss.Post().Save(o1) + o1, err = ss.Post().Save(o1) require.NoError(t, err) o2, err = ss.Post().Save(o2) @@ -757,11 +925,20 @@ func testPostStoreGetSingle(t *testing.T, ss store.Store) { } func testPostStoreUpdate(t *testing.T, ss store.Store) { + teamId := model.NewId() + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + o1 := &model.Post{} - o1.ChannelId = model.NewId() + o1.ChannelId = channel.Id o1.UserId = model.NewId() o1.Message = NewTestId() - o1, err := ss.Post().Save(o1) + o1, err = ss.Post().Save(o1) require.NoError(t, err) o2 := &model.Post{} @@ -828,8 +1005,15 @@ func testPostStoreUpdate(t *testing.T, ss store.Store) { require.Equal(t, ro3a.Hashtags, o3a.Hashtags, "Failed to update/get") } + channel2, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) o4, err := ss.Post().Save(&model.Post{ - ChannelId: model.NewId(), + ChannelId: channel2.Id, UserId: model.NewId(), Message: model.NewId(), Filenames: []string{"test"}, @@ -856,9 +1040,18 @@ func testPostStoreUpdate(t *testing.T, ss store.Store) { func testPostStoreDelete(t *testing.T, ss store.Store) { t.Run("single post, no replies", func(t *testing.T) { + teamId := model.NewId() + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + // Create a post rootPost, err := ss.Post().Save(&model.Post{ - ChannelId: model.NewId(), + ChannelId: channel.Id, UserId: model.NewId(), Message: model.NewRandomString(10), }) @@ -896,9 +1089,18 @@ func testPostStoreDelete(t *testing.T, ss store.Store) { }) t.Run("thread with one reply", func(t *testing.T) { + teamId := model.NewId() + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + // Create a root post rootPost, err := ss.Post().Save(&model.Post{ - ChannelId: model.NewId(), + ChannelId: channel.Id, UserId: model.NewId(), Message: NewTestId(), }) @@ -929,9 +1131,18 @@ func testPostStoreDelete(t *testing.T, ss store.Store) { }) t.Run("thread with multiple replies", func(t *testing.T) { + teamId := model.NewId() + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + // Create a root post rootPost1, err := ss.Post().Save(&model.Post{ - ChannelId: model.NewId(), + ChannelId: channel.Id, UserId: model.NewId(), Message: NewTestId(), }) @@ -955,9 +1166,17 @@ func testPostStoreDelete(t *testing.T, ss store.Store) { }) require.NoError(t, err) + channel2, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + // Create another root post in a separate channel rootPost2, err := ss.Post().Save(&model.Post{ - ChannelId: model.NewId(), + ChannelId: channel2.Id, UserId: model.NewId(), Message: NewTestId(), }) @@ -983,9 +1202,18 @@ func testPostStoreDelete(t *testing.T, ss store.Store) { }) t.Run("thread with multiple replies, update thread last reply at", func(t *testing.T) { + teamId := model.NewId() + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + // Create a root post rootPost1, err := ss.Post().Save(&model.Post{ - ChannelId: model.NewId(), + ChannelId: channel.Id, UserId: model.NewId(), Message: NewTestId(), }) @@ -1052,11 +1280,20 @@ func testPostStoreDelete(t *testing.T, ss store.Store) { } func testPostStorePermDelete1Level(t *testing.T, ss store.Store) { + teamId := model.NewId() + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + o1 := &model.Post{} - o1.ChannelId = model.NewId() + o1.ChannelId = channel.Id o1.UserId = model.NewId() o1.Message = NewTestId() - o1, err := ss.Post().Save(o1) + o1, err = ss.Post().Save(o1) require.NoError(t, err) o2 := &model.Post{} @@ -1067,15 +1304,30 @@ func testPostStorePermDelete1Level(t *testing.T, ss store.Store) { o2, err = ss.Post().Save(o2) require.NoError(t, err) + channel2, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName2", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) o3 := &model.Post{} - o3.ChannelId = model.NewId() + o3.ChannelId = channel2.Id o3.UserId = model.NewId() o3.Message = NewTestId() o3, err = ss.Post().Save(o3) require.NoError(t, err) + channel3, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName3", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + o4 := &model.Post{} - o4.ChannelId = model.NewId() + o4.ChannelId = channel3.Id o4.RootId = o1.Id o4.UserId = o2.UserId o4.Message = NewTestId() @@ -1144,11 +1396,20 @@ func testPostStorePermDelete1Level(t *testing.T, ss store.Store) { } func testPostStorePermDelete1Level2(t *testing.T, ss store.Store) { + teamId := model.NewId() + channel1, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + o1 := &model.Post{} - o1.ChannelId = model.NewId() + o1.ChannelId = channel1.Id o1.UserId = model.NewId() o1.Message = NewTestId() - o1, err := ss.Post().Save(o1) + o1, err = ss.Post().Save(o1) require.NoError(t, err) o2 := &model.Post{} @@ -1159,8 +1420,16 @@ func testPostStorePermDelete1Level2(t *testing.T, ss store.Store) { o2, err = ss.Post().Save(o2) require.NoError(t, err) + channel2, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName2", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + o3 := &model.Post{} - o3.ChannelId = model.NewId() + o3.ChannelId = channel2.Id o3.UserId = model.NewId() o3.Message = NewTestId() o3, err = ss.Post().Save(o3) @@ -1180,11 +1449,20 @@ func testPostStorePermDelete1Level2(t *testing.T, ss store.Store) { } func testPostStoreGetWithChildren(t *testing.T, ss store.Store) { + teamId := model.NewId() + channel1, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + o1 := &model.Post{} - o1.ChannelId = model.NewId() + o1.ChannelId = channel1.Id o1.UserId = model.NewId() o1.Message = NewTestId() - o1, err := ss.Post().Save(o1) + o1, err = ss.Post().Save(o1) require.NoError(t, err) o2 := &model.Post{} @@ -1226,11 +1504,20 @@ func testPostStoreGetWithChildren(t *testing.T, ss store.Store) { } func testPostStoreGetPostsWithDetails(t *testing.T, ss store.Store) { + teamId := model.NewId() + channel1, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + o1 := &model.Post{} - o1.ChannelId = model.NewId() + o1.ChannelId = channel1.Id o1.UserId = model.NewId() o1.Message = NewTestId() - o1, err := ss.Post().Save(o1) + o1, err = ss.Post().Save(o1) require.NoError(t, err) time.Sleep(2 * time.Millisecond) @@ -1321,7 +1608,16 @@ func testPostStoreGetPostsWithDetails(t *testing.T, ss store.Store) { func testPostStoreGetPostsBeforeAfter(t *testing.T, ss store.Store) { t.Run("without threads", func(t *testing.T) { - channelId := model.NewId() + teamId := model.NewId() + channel1, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + + channelId := channel1.Id userId := model.NewId() var posts []*model.Post @@ -1416,7 +1712,16 @@ func testPostStoreGetPostsBeforeAfter(t *testing.T, ss store.Store) { }) }) t.Run("with threads", func(t *testing.T) { - channelId := model.NewId() + teamId := model.NewId() + channel1, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + + channelId := channel1.Id userId := model.NewId() // This creates a series of posts that looks like: @@ -1514,7 +1819,16 @@ func testPostStoreGetPostsBeforeAfter(t *testing.T, ss store.Store) { }) }) t.Run("with threads (skipFetchThreads)", func(t *testing.T) { - channelId := model.NewId() + teamId := model.NewId() + channel1, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + + channelId := channel1.Id userId := model.NewId() // This creates a series of posts that looks like: @@ -1620,7 +1934,16 @@ func testPostStoreGetPostsBeforeAfter(t *testing.T, ss store.Store) { }) }) t.Run("with threads (collapsedThreads)", func(t *testing.T) { - channelId := model.NewId() + teamId := model.NewId() + channel1, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + + channelId := channel1.Id userId := model.NewId() // This creates a series of posts that looks like: @@ -1715,7 +2038,16 @@ func testPostStoreGetPostsBeforeAfter(t *testing.T, ss store.Store) { func testPostStoreGetPostsSince(t *testing.T, ss store.Store) { t.Run("should return posts created after the given time", func(t *testing.T) { - channelId := model.NewId() + teamId := model.NewId() + channel1, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + + channelId := channel1.Id userId := model.NewId() post1, err := ss.Post().Save(&model.Post{ @@ -1788,7 +2120,16 @@ func testPostStoreGetPostsSince(t *testing.T, ss store.Store) { }) t.Run("should return empty list when nothing has changed", func(t *testing.T) { - channelId := model.NewId() + teamId := model.NewId() + channel1, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + + channelId := channel1.Id userId := model.NewId() post1, err := ss.Post().Save(&model.Post{ @@ -1809,7 +2150,16 @@ func testPostStoreGetPostsSince(t *testing.T, ss store.Store) { t.Run("should not cache a timestamp of 0 when nothing has changed", func(t *testing.T) { ss.Post().ClearCaches() - channelId := model.NewId() + teamId := model.NewId() + channel1, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + + channelId := channel1.Id userId := model.NewId() post1, err := ss.Post().Save(&model.Post{ @@ -1837,7 +2187,16 @@ func testPostStoreGetPostsSince(t *testing.T, ss store.Store) { } func testPostStoreGetPosts(t *testing.T, ss store.Store) { - channelId := model.NewId() + teamId := model.NewId() + channel1, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + + channelId := channel1.Id userId := model.NewId() post1, err := ss.Post().Save(&model.Post{ @@ -2016,13 +2375,22 @@ func testPostStoreGetPosts(t *testing.T, ss store.Store) { } func testPostStoreGetPostBeforeAfter(t *testing.T, ss store.Store) { - channelId := model.NewId() + teamId := model.NewId() + channel1, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + + channelId := channel1.Id o0 := &model.Post{} o0.ChannelId = channelId o0.UserId = model.NewId() o0.Message = NewTestId() - _, err := ss.Post().Save(o0) + _, err = ss.Post().Save(o0) require.NoError(t, err) time.Sleep(2 * time.Millisecond) @@ -2054,8 +2422,16 @@ func testPostStoreGetPostBeforeAfter(t *testing.T, ss store.Store) { require.NoError(t, err) time.Sleep(2 * time.Millisecond) + channel2, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName2", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + otherChannelPost := &model.Post{} - otherChannelPost.ChannelId = model.NewId() + otherChannelPost.ChannelId = channel2.Id otherChannelPost.UserId = model.NewId() otherChannelPost.Message = NewTestId() _, err = ss.Post().Save(otherChannelPost) @@ -2366,8 +2742,17 @@ func testPostStoreGetFlaggedPostsForTeam(t *testing.T, ss store.Store, s SqlStor _, err = ss.Channel().SaveMember(m0) require.NoError(t, err) + teamId := model.NewId() + channel2, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName2", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + o4 := &model.Post{} - o4.ChannelId = model.NewId() + o4.ChannelId = channel2.Id o4.UserId = model.NewId() o4.Message = NewTestId() o4, err = ss.Post().Save(o4) @@ -2401,8 +2786,16 @@ func testPostStoreGetFlaggedPostsForTeam(t *testing.T, ss store.Store, s SqlStor time.Sleep(2 * time.Millisecond) // Post on channel where user is not a member + channel3, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName3", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + o6 := &model.Post{} - o6.ChannelId = model.NewId() + o6.ChannelId = channel3.Id o6.UserId = m2.UserId o6.Message = NewTestId() o6, err = ss.Post().Save(o6) @@ -2562,8 +2955,17 @@ func testPostStoreGetFlaggedPosts(t *testing.T, ss store.Store) { time.Sleep(2 * time.Millisecond) // Post on channel where user is not a member + teamId := model.NewId() + channel2, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName2", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + o4 := &model.Post{} - o4.ChannelId = model.NewId() + o4.ChannelId = channel2.Id o4.UserId = model.NewId() o4.Message = NewTestId() o4, err = ss.Post().Save(o4) @@ -2692,8 +3094,17 @@ func testPostStoreGetFlaggedPostsForChannel(t *testing.T, ss store.Store) { time.Sleep(2 * time.Millisecond) // deleted post + teamId := model.NewId() + channel3, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName3", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + o3 := &model.Post{} - o3.ChannelId = model.NewId() + o3.ChannelId = channel3.Id o3.UserId = o1.ChannelId o3.Message = NewTestId() o3.DeleteAt = 1 @@ -2710,8 +3121,16 @@ func testPostStoreGetFlaggedPostsForChannel(t *testing.T, ss store.Store) { time.Sleep(2 * time.Millisecond) // Post on channel where user is not a member + channel4, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName4", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + o5 := &model.Post{} - o5.ChannelId = model.NewId() + o5.ChannelId = channel4.Id o5.UserId = model.NewId() o5.Message = NewTestId() o5, err = ss.Post().Save(o5) @@ -2792,13 +3211,22 @@ func testPostStoreGetFlaggedPostsForChannel(t *testing.T, ss store.Store) { } func testPostStoreGetLastPostRowCreateAt(t *testing.T, ss store.Store) { + teamId := model.NewId() + channel1, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + createTime1 := model.GetMillis() + 1 o0 := &model.Post{} - o0.ChannelId = model.NewId() + o0.ChannelId = channel1.Id o0.UserId = model.NewId() o0.Message = NewTestId() o0.CreateAt = createTime1 - o0, err := ss.Post().Save(o0) + o0, err = ss.Post().Save(o0) require.NoError(t, err) createTime2 := model.GetMillis() + 2 @@ -2817,14 +3245,23 @@ func testPostStoreGetLastPostRowCreateAt(t *testing.T, ss store.Store) { } func testPostStoreGetPostsCreatedAt(t *testing.T, ss store.Store) { + teamId := model.NewId() + channel1, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + createTime := model.GetMillis() + 1 o0 := &model.Post{} - o0.ChannelId = model.NewId() + o0.ChannelId = channel1.Id o0.UserId = model.NewId() o0.Message = NewTestId() o0.CreateAt = createTime - o0, err := ss.Post().Save(o0) + o0, err = ss.Post().Save(o0) require.NoError(t, err) o1 := &model.Post{} @@ -2844,8 +3281,16 @@ func testPostStoreGetPostsCreatedAt(t *testing.T, ss store.Store) { _, err = ss.Post().Save(o2) require.NoError(t, err) + channel2, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName2", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + o3 := &model.Post{} - o3.ChannelId = model.NewId() + o3.ChannelId = channel2.Id o3.UserId = model.NewId() o3.Message = NewTestId() o3.CreateAt = createTime @@ -2857,11 +3302,20 @@ func testPostStoreGetPostsCreatedAt(t *testing.T, ss store.Store) { } func testPostStoreOverwriteMultiple(t *testing.T, ss store.Store) { + teamId := model.NewId() + channel1, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + o1 := &model.Post{} - o1.ChannelId = model.NewId() + o1.ChannelId = channel1.Id o1.UserId = model.NewId() o1.Message = NewTestId() - o1, err := ss.Post().Save(o1) + o1, err = ss.Post().Save(o1) require.NoError(t, err) o2 := &model.Post{} @@ -2879,16 +3333,31 @@ func testPostStoreOverwriteMultiple(t *testing.T, ss store.Store) { o3, err = ss.Post().Save(o3) require.NoError(t, err) + channel2, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName2", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + o4, err := ss.Post().Save(&model.Post{ - ChannelId: model.NewId(), + ChannelId: channel2.Id, UserId: model.NewId(), Message: model.NewId(), Filenames: []string{"test"}, }) require.NoError(t, err) + channel3, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName3", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) o5, err := ss.Post().Save(&model.Post{ - ChannelId: model.NewId(), + ChannelId: channel3.Id, UserId: model.NewId(), Message: model.NewId(), Filenames: []string{"test2", "test3"}, @@ -2983,11 +3452,20 @@ func testPostStoreOverwriteMultiple(t *testing.T, ss store.Store) { } func testPostStoreOverwrite(t *testing.T, ss store.Store) { + teamId := model.NewId() + channel1, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + o1 := &model.Post{} - o1.ChannelId = model.NewId() + o1.ChannelId = channel1.Id o1.UserId = model.NewId() o1.Message = NewTestId() - o1, err := ss.Post().Save(o1) + o1, err = ss.Post().Save(o1) require.NoError(t, err) o2 := &model.Post{} @@ -3005,8 +3483,15 @@ func testPostStoreOverwrite(t *testing.T, ss store.Store) { o3, err = ss.Post().Save(o3) require.NoError(t, err) + channel2, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName2", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) o4, err := ss.Post().Save(&model.Post{ - ChannelId: model.NewId(), + ChannelId: channel2.Id, UserId: model.NewId(), Message: model.NewId(), Filenames: []string{"test"}, @@ -3084,11 +3569,20 @@ func testPostStoreOverwrite(t *testing.T, ss store.Store) { } func testPostStoreGetPostsByIds(t *testing.T, ss store.Store) { + teamId := model.NewId() + channel1, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + o1 := &model.Post{} - o1.ChannelId = model.NewId() + o1.ChannelId = channel1.Id o1.UserId = model.NewId() o1.Message = NewTestId() - o1, err := ss.Post().Save(o1) + o1, err = ss.Post().Save(o1) require.NoError(t, err) o2 := &model.Post{} @@ -3402,12 +3896,21 @@ func testPostStorePermanentDeleteBatch(t *testing.T, ss store.Store) { } func testPostStoreGetOldest(t *testing.T, ss store.Store) { + teamId := model.NewId() + channel1, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + o0 := &model.Post{} - o0.ChannelId = model.NewId() + o0.ChannelId = channel1.Id o0.UserId = model.NewId() o0.Message = NewTestId() o0.CreateAt = 3 - o0, err := ss.Post().Save(o0) + o0, err = ss.Post().Save(o0) require.NoError(t, err) o1 := &model.Post{} @@ -3751,10 +4254,19 @@ func testPostStoreGetDirectPostParentsForExportAfterBatched(t *testing.T, ss sto func testHasAutoResponsePostByUserSince(t *testing.T, ss store.Store) { t.Run("should return posts created after the given time", func(t *testing.T) { - channelId := model.NewId() + teamId := model.NewId() + channel1, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + + channelId := channel1.Id userId := model.NewId() - _, err := ss.Post().Save(&model.Post{ + _, err = ss.Post().Save(&model.Post{ ChannelId: channelId, UserId: userId, Message: "message", diff --git a/store/storetest/thread_store.go b/store/storetest/thread_store.go index e438ed8516..885a48c1a4 100644 --- a/store/storetest/thread_store.go +++ b/store/storetest/thread_store.go @@ -106,8 +106,17 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) { require.Equal(t, int64(2), thread.ReplyCount) require.ElementsMatch(t, model.StringArray{newPosts[0].UserId, newPosts[1].UserId}, thread.Participants) + teamId := model.NewId() + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName1", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + o5 := model.Post{} - o5.ChannelId = model.NewId() + o5.ChannelId = channel.Id o5.UserId = model.NewId() o5.RootId = newPosts[0].Id o5.Message = NewTestId() @@ -141,9 +150,18 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) { }) t.Run("Update reply should update the UpdateAt of the thread", func(t *testing.T) { + teamId := model.NewId() + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + rootPost := model.Post{} rootPost.RootId = model.NewId() - rootPost.ChannelId = model.NewId() + rootPost.ChannelId = channel.Id rootPost.UserId = model.NewId() rootPost.Message = NewTestId() @@ -188,8 +206,17 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) { }) t.Run("Deleting reply should update the thread", func(t *testing.T) { + teamId := model.NewId() + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + o1 := model.Post{} - o1.ChannelId = model.NewId() + o1.ChannelId = channel.Id o1.UserId = model.NewId() o1.Message = NewTestId() rootPost, err := ss.Post().Save(&o1) @@ -240,8 +267,17 @@ func testThreadStorePopulation(t *testing.T, ss store.Store) { }) t.Run("Deleting root post should delete the thread", func(t *testing.T) { + teamId := model.NewId() + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + DisplayName: "DisplayName", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + rootPost := model.Post{} - rootPost.ChannelId = model.NewId() + rootPost.ChannelId = channel.Id rootPost.UserId = model.NewId() rootPost.Message = NewTestId() diff --git a/store/timerlayer/timerlayer.go b/store/timerlayer/timerlayer.go index 0558fa66c1..cec6251fc7 100644 --- a/store/timerlayer/timerlayer.go +++ b/store/timerlayer/timerlayer.go @@ -3781,10 +3781,10 @@ func (s *TimerLayerGroupStore) GetGroupSyncable(groupID string, syncableID strin return result, err } -func (s *TimerLayerGroupStore) GetGroups(page int, perPage int, opts model.GroupSearchOpts) ([]*model.Group, error) { +func (s *TimerLayerGroupStore) GetGroups(page int, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, error) { start := time.Now() - result, err := s.GroupStore.GetGroups(page, perPage, opts) + result, err := s.GroupStore.GetGroups(page, perPage, opts, viewRestrictions) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { @@ -3877,6 +3877,22 @@ func (s *TimerLayerGroupStore) GetMemberCount(groupID string) (int64, error) { return result, err } +func (s *TimerLayerGroupStore) GetMemberCountWithRestrictions(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, error) { + start := time.Now() + + result, err := s.GroupStore.GetMemberCountWithRestrictions(groupID, viewRestrictions) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetMemberCountWithRestrictions", success, elapsed) + } + return result, err +} + func (s *TimerLayerGroupStore) GetMemberUsers(groupID string) ([]*model.User, error) { start := time.Now() @@ -3925,10 +3941,10 @@ func (s *TimerLayerGroupStore) GetMemberUsersNotInChannel(groupID string, channe return result, err } -func (s *TimerLayerGroupStore) GetMemberUsersPage(groupID string, page int, perPage int) ([]*model.User, error) { +func (s *TimerLayerGroupStore) GetMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) { start := time.Now() - result, err := s.GroupStore.GetMemberUsersPage(groupID, page, perPage) + result, err := s.GroupStore.GetMemberUsersPage(groupID, page, perPage, viewRestrictions) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { @@ -3941,10 +3957,10 @@ func (s *TimerLayerGroupStore) GetMemberUsersPage(groupID string, page int, perP return result, err } -func (s *TimerLayerGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int) ([]*model.User, error) { +func (s *TimerLayerGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) { start := time.Now() - result, err := s.GroupStore.GetNonMemberUsersPage(groupID, page, perPage) + result, err := s.GroupStore.GetNonMemberUsersPage(groupID, page, perPage, viewRestrictions) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { diff --git a/templates/cloud_14_day_arrears.html b/templates/cloud_14_day_arrears.html index 395a78824d..d5115d151e 100644 --- a/templates/cloud_14_day_arrears.html +++ b/templates/cloud_14_day_arrears.html @@ -103,7 +103,7 @@ @import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,600,700); .emailBody { - background: #F3F3F3 !important; + background-color: #F3F3F3 } .emailBody a { @@ -350,7 +350,7 @@ -
+
diff --git a/templates/cloud_30_day_arrears.html b/templates/cloud_30_day_arrears.html index aae98b9df4..19290dd87f 100644 --- a/templates/cloud_30_day_arrears.html +++ b/templates/cloud_30_day_arrears.html @@ -103,7 +103,7 @@ @import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,600,700); .emailBody { - background: #F3F3F3 !important; + background-color: #F3F3F3 } .emailBody a { @@ -350,7 +350,7 @@ -
+
@@ -416,15 +416,13 @@ diff --git a/templates/cloud_45_day_arrears.html b/templates/cloud_45_day_arrears.html index 0225d5b2e6..9c4af6731c 100644 --- a/templates/cloud_45_day_arrears.html +++ b/templates/cloud_45_day_arrears.html @@ -103,7 +103,7 @@ @import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,600,700); .emailBody { - background: #F3F3F3 !important; + background-color: #F3F3F3 } .emailBody a { @@ -350,7 +350,7 @@ -
+
-
- +
    {{ range .Props.BulletListItems}}
  • {{.}}
  • {{end}}
-
-
+
diff --git a/templates/cloud_7_day_arrears.html b/templates/cloud_7_day_arrears.html index b59e370f29..712b29fa37 100644 --- a/templates/cloud_7_day_arrears.html +++ b/templates/cloud_7_day_arrears.html @@ -103,7 +103,7 @@ @import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,600,700); .emailBody { - background: #F3F3F3 !important; + background-color: #F3F3F3 } .emailBody a { @@ -350,7 +350,7 @@ -
+
diff --git a/templates/cloud_90_day_arrears.html b/templates/cloud_90_day_arrears.html index da1137dbb9..f63b2b2a07 100644 --- a/templates/cloud_90_day_arrears.html +++ b/templates/cloud_90_day_arrears.html @@ -103,7 +103,7 @@ @import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,600,700); .emailBody { - background: #F3F3F3 !important; + background-color: #F3F3F3 } .emailBody a { @@ -350,7 +350,7 @@ -
+
diff --git a/templates/cloud_upgrade_confirmation.html b/templates/cloud_upgrade_confirmation.html index 9b30132f49..27f4fca15c 100644 --- a/templates/cloud_upgrade_confirmation.html +++ b/templates/cloud_upgrade_confirmation.html @@ -93,7 +93,7 @@ @import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,600,700); .emailBody { - background: #F3F3F3 !important; + background-color: #F3F3F3 } .emailBody a { @@ -340,7 +340,7 @@ -
+
diff --git a/templates/cloud_welcome_email.html b/templates/cloud_welcome_email.html index 1ef02cd670..d2e8235cc5 100644 --- a/templates/cloud_welcome_email.html +++ b/templates/cloud_welcome_email.html @@ -14,15 +14,15 @@
+ style="width: 724px; padding: 32px; background: #FFFFFF; color: #3D3C40;">
- logo_email_blue + logo_email
diff --git a/templates/inactivity_body.html b/templates/inactivity_body.html index 1c5507cd14..36bb7795cb 100644 --- a/templates/inactivity_body.html +++ b/templates/inactivity_body.html @@ -93,7 +93,7 @@ @import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,600,700); .emailBody { - background: #F3F3F3 !important; + background-color: #F3F3F3 } .emailBody a { @@ -340,7 +340,7 @@ -
+
diff --git a/templates/invite_body.html b/templates/invite_body.html index 017afb677e..8a3b232b65 100644 --- a/templates/invite_body.html +++ b/templates/invite_body.html @@ -113,7 +113,7 @@ @import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,600,700); .emailBody { - background: #F3F3F3 !important; + background-color: #F3F3F3 } .emailBody a { @@ -360,7 +360,7 @@ -
+
diff --git a/templates/license_up_for_renewal.html b/templates/license_up_for_renewal.html index 13780c89c5..d05cc8d8be 100644 --- a/templates/license_up_for_renewal.html +++ b/templates/license_up_for_renewal.html @@ -93,7 +93,7 @@ @import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,600,700); .emailBody { - background: #F3F3F3 !important; + background-color: #F3F3F3 } .emailBody a { @@ -340,7 +340,7 @@ -
+
diff --git a/templates/messages_notification.html b/templates/messages_notification.html index 7b47478cfa..ba2aaee252 100644 --- a/templates/messages_notification.html +++ b/templates/messages_notification.html @@ -113,7 +113,7 @@ @import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,600,700); .emailBody { - background: #F3F3F3 !important; + background-color: #F3F3F3 } .emailBody a { @@ -513,7 +513,7 @@ -
+
diff --git a/templates/partials/style.css b/templates/partials/style.css index f91787b5d9..3c72f05ea0 100644 --- a/templates/partials/style.css +++ b/templates/partials/style.css @@ -1,7 +1,7 @@ @import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,600,700); .emailBody { - background: #F3F3F3 !important; + background-color: #F3F3F3 } .emailBody a{ diff --git a/templates/password_change_body.html b/templates/password_change_body.html index 550b40f665..663b608e59 100644 --- a/templates/password_change_body.html +++ b/templates/password_change_body.html @@ -10,7 +10,7 @@
diff --git a/templates/payment_failed_body.html b/templates/payment_failed_body.html index 0d8daf6b7c..81060f9e37 100644 --- a/templates/payment_failed_body.html +++ b/templates/payment_failed_body.html @@ -103,7 +103,7 @@ @import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,600,700); .emailBody { - background: #F3F3F3 !important; + background-color: #F3F3F3 } .emailBody a { @@ -350,7 +350,7 @@ -
+
- +
diff --git a/templates/reset_body.html b/templates/reset_body.html index e5f62d4dec..ab2b11be32 100644 --- a/templates/reset_body.html +++ b/templates/reset_body.html @@ -93,7 +93,7 @@ @import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,600,700); .emailBody { - background: #F3F3F3 !important; + background-color: #F3F3F3 } .emailBody a { @@ -339,8 +339,8 @@ - -
+ +
diff --git a/templates/reset_body.mjml b/templates/reset_body.mjml index 06bd55c508..3d2e3c8e66 100644 --- a/templates/reset_body.mjml +++ b/templates/reset_body.mjml @@ -2,7 +2,7 @@ - + diff --git a/templates/verify_body.html b/templates/verify_body.html index 5c8cc5550f..a94df5f287 100644 --- a/templates/verify_body.html +++ b/templates/verify_body.html @@ -93,7 +93,7 @@ @import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,600,700); .emailBody { - background: #F3F3F3 !important; + background-color: #F3F3F3 } .emailBody a { @@ -340,7 +340,7 @@ -
+
diff --git a/templates/welcome_body.html b/templates/welcome_body.html index 28ea7d65f5..5e85097a31 100644 --- a/templates/welcome_body.html +++ b/templates/welcome_body.html @@ -93,7 +93,7 @@ @import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,600,700); .emailBody { - background: #F3F3F3 !important; + background-color: #F3F3F3 } .emailBody a { @@ -339,8 +339,8 @@ - -
+ +
diff --git a/templates/welcome_body.mjml b/templates/welcome_body.mjml index 960689d6ef..f27ad1dc3c 100644 --- a/templates/welcome_body.mjml +++ b/templates/welcome_body.mjml @@ -2,7 +2,7 @@ - + diff --git a/web/handlers.go b/web/handlers.go index 9792460aff..65dda02cf0 100644 --- a/web/handlers.go +++ b/web/handlers.go @@ -8,6 +8,7 @@ import ( "context" "fmt" "net/http" + "os" "reflect" "runtime" "strconv" @@ -133,8 +134,13 @@ func generateDevCSP(c Context) string { // Add flags for Webpack dev servers used by other products during development if model.BuildNumber == "dev" { - // Focalboard runs on http://localhost:9006 - devCSP = append(devCSP, "http://localhost:9006") + boardsURL := os.Getenv("MM_BOARDS_DEV_SERVER_URL") + if boardsURL == "" { + // Focalboard runs on http://localhost:9006 by default + boardsURL = "http://localhost:9006" + } + + devCSP = append(devCSP, boardsURL) } if len(devCSP) == 0 {