diff --git a/api4/team.go b/api4/team.go index b3c27b41bf..3e1824692c 100644 --- a/api4/team.go +++ b/api4/team.go @@ -295,6 +295,17 @@ func getTeamMember(c *Context, w http.ResponseWriter, r *http.Request) { return } + canSee, err := c.App.UserCanSeeOtherUser(c.App.Session.UserId, c.Params.UserId) + if err != nil { + c.Err = err + return + } + + if !canSee { + c.SetPermissionError(model.PERMISSION_VIEW_MEMBERS) + return + } + team, err := c.App.GetTeamMember(c.Params.TeamId, c.Params.UserId) if err != nil { c.Err = err @@ -315,7 +326,13 @@ func getTeamMembers(c *Context, w http.ResponseWriter, r *http.Request) { return } - members, err := c.App.GetTeamMembers(c.Params.TeamId, c.Params.Page*c.Params.PerPage, c.Params.PerPage) + restrictions, err := c.App.GetViewUsersRestrictions(c.App.Session.UserId) + if err != nil { + c.Err = err + return + } + + members, err := c.App.GetTeamMembers(c.Params.TeamId, c.Params.Page*c.Params.PerPage, c.Params.PerPage, restrictions) if err != nil { c.Err = err return @@ -335,6 +352,17 @@ func getTeamMembersForUser(c *Context, w http.ResponseWriter, r *http.Request) { return } + canSee, err := c.App.UserCanSeeOtherUser(c.App.Session.UserId, c.Params.UserId) + if err != nil { + c.Err = err + return + } + + if !canSee { + c.SetPermissionError(model.PERMISSION_VIEW_MEMBERS) + return + } + members, err := c.App.GetTeamMembersForUser(c.Params.UserId) if err != nil { c.Err = err @@ -362,7 +390,13 @@ func getTeamMembersByIds(c *Context, w http.ResponseWriter, r *http.Request) { return } - members, err := c.App.GetTeamMembersByIds(c.Params.TeamId, userIds) + restrictions, err := c.App.GetViewUsersRestrictions(c.App.Session.UserId) + if err != nil { + c.Err = err + return + } + + members, err := c.App.GetTeamMembersByIds(c.Params.TeamId, userIds, restrictions) if err != nil { c.Err = err return diff --git a/api4/user.go b/api4/user.go index dab4808590..8694e887b1 100644 --- a/api4/user.go +++ b/api4/user.go @@ -111,7 +111,16 @@ func getUser(c *Context, w http.ResponseWriter, r *http.Request) { return } - // No permission check required + canSee, err := c.App.UserCanSeeOtherUser(c.App.Session.UserId, c.Params.UserId) + if err != nil { + c.SetPermissionError(model.PERMISSION_VIEW_MEMBERS) + return + } + + if !canSee { + c.SetPermissionError(model.PERMISSION_VIEW_MEMBERS) + return + } user, err := c.App.GetUser(c.Params.UserId) if err != nil { @@ -154,14 +163,32 @@ func getUserByUsername(c *Context, w http.ResponseWriter, r *http.Request) { return } - // No permission check required - user, err := c.App.GetUserByUsername(c.Params.Username) if err != nil { + restrictions, err2 := c.App.GetViewUsersRestrictions(c.App.Session.UserId) + if err2 != nil { + c.Err = err2 + return + } + if restrictions != nil { + c.SetPermissionError(model.PERMISSION_VIEW_MEMBERS) + return + } c.Err = err return } + canSee, err := c.App.UserCanSeeOtherUser(c.App.Session.UserId, user.Id) + if err != nil { + c.Err = err + return + } + + if !canSee { + c.SetPermissionError(model.PERMISSION_VIEW_MEMBERS) + return + } + if c.IsSystemAdmin() || c.App.Session.UserId == user.Id { userTermsOfService, err := c.App.GetUserTermsOfService(user.Id) if err != nil && err.StatusCode != http.StatusNotFound { @@ -196,8 +223,6 @@ func getUserByEmail(c *Context, w http.ResponseWriter, r *http.Request) { return } - // No permission check required, but still prevent users who can't see another user's email address from using this - sanitizeOptions := c.App.GetSanitizeOptions(c.IsSystemAdmin()) if !sanitizeOptions["email"] { c.Err = model.NewAppError("getUserByEmail", "api.user.get_user_by_email.permissions.app_error", nil, "userId="+c.App.Session.UserId, http.StatusForbidden) @@ -206,10 +231,30 @@ func getUserByEmail(c *Context, w http.ResponseWriter, r *http.Request) { user, err := c.App.GetUserByEmail(c.Params.Email) if err != nil { + restrictions, err2 := c.App.GetViewUsersRestrictions(c.App.Session.UserId) + if err2 != nil { + c.Err = err2 + return + } + if restrictions != nil { + c.SetPermissionError(model.PERMISSION_VIEW_MEMBERS) + return + } c.Err = err return } + canSee, err := c.App.UserCanSeeOtherUser(c.App.Session.UserId, user.Id) + if err != nil { + c.Err = err + return + } + + if !canSee { + c.SetPermissionError(model.PERMISSION_VIEW_MEMBERS) + return + } + etag := user.Etag(*c.App.Config().PrivacySettings.ShowFullName, *c.App.Config().PrivacySettings.ShowEmailAddress) if c.HandleEtag(etag, "Get User", w, r) { @@ -227,18 +272,23 @@ func getDefaultProfileImage(c *Context, w http.ResponseWriter, r *http.Request) return } - users, err := c.App.GetUsersByIds([]string{c.Params.UserId}, c.IsSystemAdmin()) + canSee, err := c.App.UserCanSeeOtherUser(c.App.Session.UserId, c.Params.UserId) if err != nil { c.Err = err return } - if len(users) == 0 { - c.Err = model.NewAppError("getProfileImage", "api.user.get_profile_image.not_found.app_error", nil, "", http.StatusNotFound) + if !canSee { + c.SetPermissionError(model.PERMISSION_VIEW_MEMBERS) + return + } + + user, err := c.App.GetUser(c.Params.UserId) + if err != nil { + c.Err = err return } - user := users[0] img, err := c.App.GetDefaultProfileImage(user) if err != nil { c.Err = err @@ -256,18 +306,23 @@ func getProfileImage(c *Context, w http.ResponseWriter, r *http.Request) { return } - users, err := c.App.GetUsersByIds([]string{c.Params.UserId}, c.IsSystemAdmin()) + canSee, err := c.App.UserCanSeeOtherUser(c.App.Session.UserId, c.Params.UserId) if err != nil { c.Err = err return } - if len(users) == 0 { - c.Err = model.NewAppError("getProfileImage", "api.user.get_profile_image.not_found.app_error", nil, "", http.StatusNotFound) + if !canSee { + c.SetPermissionError(model.PERMISSION_VIEW_MEMBERS) + return + } + + user, err := c.App.GetUser(c.Params.UserId) + if err != nil { + c.Err = err return } - user := users[0] etag := strconv.FormatInt(user.LastPictureUpdate, 10) if c.HandleEtag(etag, "Get Profile Image", w, r) { return @@ -376,7 +431,13 @@ func getTotalUsersStats(c *Context, w http.ResponseWriter, r *http.Request) { return } - stats, err := c.App.GetTotalUsersStats() + restrictions, err := c.App.GetViewUsersRestrictions(c.App.Session.UserId) + if err != nil { + c.Err = err + return + } + + stats, err := c.App.GetTotalUsersStats(restrictions) if err != nil { c.Err = err return @@ -419,21 +480,27 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { withoutTeamBool, _ := strconv.ParseBool(withoutTeam) inactiveBool, _ := strconv.ParseBool(inactive) + restrictions, err := c.App.GetViewUsersRestrictions(c.App.Session.UserId) + if err != nil { + c.Err = err + return + } + userGetOptions := &model.UserGetOptions{ - InTeamId: inTeamId, - InChannelId: inChannelId, - NotInTeamId: notInTeamId, - NotInChannelId: notInChannelId, - WithoutTeam: withoutTeamBool, - Inactive: inactiveBool, - Role: role, - Sort: sort, - Page: c.Params.Page, - PerPage: c.Params.PerPage, + InTeamId: inTeamId, + InChannelId: inChannelId, + NotInTeamId: notInTeamId, + NotInChannelId: notInChannelId, + WithoutTeam: withoutTeamBool, + Inactive: inactiveBool, + Role: role, + Sort: sort, + Page: c.Params.Page, + PerPage: c.Params.PerPage, + ViewRestrictions: restrictions, } var profiles []*model.User - var err *model.AppError etag := "" if withoutTeamBool, _ := strconv.ParseBool(withoutTeam); withoutTeamBool { @@ -443,26 +510,26 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { return } - profiles, err = c.App.GetUsersWithoutTeamPage(c.Params.Page, c.Params.PerPage, c.IsSystemAdmin()) + profiles, err = c.App.GetUsersWithoutTeamPage(c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), restrictions) } else if len(notInChannelId) > 0 { if !c.App.SessionHasPermissionToChannel(c.App.Session, notInChannelId, model.PERMISSION_READ_CHANNEL) { c.SetPermissionError(model.PERMISSION_READ_CHANNEL) return } - profiles, err = c.App.GetUsersNotInChannelPage(inTeamId, notInChannelId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin()) + profiles, err = c.App.GetUsersNotInChannelPage(inTeamId, notInChannelId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), restrictions) } else if len(notInTeamId) > 0 { if !c.App.SessionHasPermissionToTeam(c.App.Session, notInTeamId, model.PERMISSION_VIEW_TEAM) { c.SetPermissionError(model.PERMISSION_VIEW_TEAM) return } - etag = c.App.GetUsersNotInTeamEtag(inTeamId) + etag = c.App.GetUsersNotInTeamEtag(inTeamId, restrictions.Hash()) if c.HandleEtag(etag, "Get Users Not in Team", w, r) { return } - profiles, err = c.App.GetUsersNotInTeamPage(notInTeamId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin()) + profiles, err = c.App.GetUsersNotInTeamPage(notInTeamId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), restrictions) } else if len(inTeamId) > 0 { if !c.App.SessionHasPermissionToTeam(c.App.Session, inTeamId, model.PERMISSION_VIEW_TEAM) { c.SetPermissionError(model.PERMISSION_VIEW_TEAM) @@ -470,11 +537,11 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { } if sort == "last_activity_at" { - profiles, err = c.App.GetRecentlyActiveUsersForTeamPage(inTeamId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin()) + profiles, err = c.App.GetRecentlyActiveUsersForTeamPage(inTeamId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), restrictions) } else if sort == "create_at" { - profiles, err = c.App.GetNewUsersForTeamPage(inTeamId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin()) + profiles, err = c.App.GetNewUsersForTeamPage(inTeamId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), restrictions) } else { - etag = c.App.GetUsersInTeamEtag(inTeamId) + etag = c.App.GetUsersInTeamEtag(inTeamId, restrictions.Hash()) if c.HandleEtag(etag, "Get Users in Team", w, r) { return } @@ -491,12 +558,16 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { profiles, err = c.App.GetUsersInChannelPage(inChannelId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin()) } } else { - // No permission check required - - etag = c.App.GetUsersEtag() + etag = c.App.GetUsersEtag(restrictions.Hash()) if c.HandleEtag(etag, "Get Users", w, r) { return } + + userGetOptions, err = c.App.RestrictUsersGetByPermissions(c.App.Session.UserId, userGetOptions) + if err != nil { + c.Err = err + return + } profiles, err = c.App.GetUsersPage(userGetOptions, c.IsSystemAdmin()) } @@ -520,9 +591,13 @@ func getUsersByIds(c *Context, w http.ResponseWriter, r *http.Request) { return } - // No permission check required + restrictions, err := c.App.GetViewUsersRestrictions(c.App.Session.UserId) + if err != nil { + c.Err = err + return + } - users, err := c.App.GetUsersByIds(userIds, c.IsSystemAdmin()) + users, err := c.App.GetUsersByIds(userIds, c.IsSystemAdmin(), restrictions) if err != nil { c.Err = err return @@ -539,9 +614,13 @@ func getUsersByNames(c *Context, w http.ResponseWriter, r *http.Request) { return } - // No permission check required + restrictions, err := c.App.GetViewUsersRestrictions(c.App.Session.UserId) + if err != nil { + c.Err = err + return + } - users, err := c.App.GetUsersByUsernames(usernames, c.IsSystemAdmin()) + users, err := c.App.GetUsersByUsernames(usernames, c.IsSystemAdmin(), restrictions) if err != nil { c.Err = err return @@ -607,6 +686,12 @@ func searchUsers(c *Context, w http.ResponseWriter, r *http.Request) { options.AllowFullNames = *c.App.Config().PrivacySettings.ShowFullName } + options, err := c.App.RestrictUsersSearchByPermissions(c.App.Session.UserId, options) + if err != nil { + c.Err = err + return + } + profiles, err := c.App.SearchUsers(props, options) if err != nil { c.Err = err @@ -670,6 +755,13 @@ func autocompleteUsers(c *Context, w http.ResponseWriter, r *http.Request) { autocomplete.Users = result.InChannel autocomplete.OutOfChannel = result.OutOfChannel } else if len(teamId) > 0 { + var err *model.AppError + options, err = c.App.RestrictUsersSearchByPermissions(c.App.Session.UserId, options) + if err != nil { + c.Err = err + return + } + result, err := c.App.AutocompleteUsersInTeam(teamId, name, options) if err != nil { c.Err = err @@ -678,7 +770,13 @@ func autocompleteUsers(c *Context, w http.ResponseWriter, r *http.Request) { autocomplete.Users = result.InTeam } else { - // No permission check required + var err *model.AppError + options, err = c.App.RestrictUsersSearchByPermissions(c.App.Session.UserId, options) + if err != nil { + c.Err = err + return + } + result, err := c.App.SearchUsersInTeam("", name, options) if err != nil { c.Err = err diff --git a/api4/user_test.go b/api4/user_test.go index 23b5fddf73..5672ca8505 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -525,7 +525,7 @@ func TestGetBotUser(t *testing.T) { defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) - th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + th.App.UpdateUserRoles(th.BasicUser.Id, model.SYSTEM_USER_ROLE_ID+" "+model.TEAM_USER_ROLE_ID, false) bot := &model.Bot{ Username: GenerateTestUsername(), @@ -538,6 +538,7 @@ func TestGetBotUser(t *testing.T) { defer th.App.PermanentDeleteBot(createdBot.UserId) botUser, resp := th.Client.GetUser(createdBot.UserId, "") + CheckNoError(t, resp) require.Equal(t, bot.Username, botUser.Username) require.True(t, botUser.IsBot) } diff --git a/api4/user_viewmembers_test.go b/api4/user_viewmembers_test.go new file mode 100644 index 0000000000..dbcf163950 --- /dev/null +++ b/api4/user_viewmembers_test.go @@ -0,0 +1,471 @@ +package api4 + +import ( + "testing" + + "github.com/mattermost/mattermost-server/model" + "github.com/stretchr/testify/require" +) + +func TestApiResctrictedViewMembers(t *testing.T) { + th := Setup() + defer th.TearDown() + + // Create first account for system admin + _, err := th.App.CreateUser(&model.User{Email: th.GenerateTestEmail(), Nickname: "test user0", Password: "test-password-0", Username: "test-user-0", Roles: model.SYSTEM_USER_ROLE_ID}) + require.Nil(t, err) + + user1, err := th.App.CreateUser(&model.User{Email: th.GenerateTestEmail(), Nickname: "test user1", Password: "test-password-1", Username: "test-user-1", Roles: model.SYSTEM_USER_ROLE_ID}) + require.Nil(t, err) + user2, err := th.App.CreateUser(&model.User{Email: th.GenerateTestEmail(), Nickname: "test user2", Password: "test-password-2", Username: "test-user-2", Roles: model.SYSTEM_USER_ROLE_ID}) + require.Nil(t, err) + user3, err := th.App.CreateUser(&model.User{Email: th.GenerateTestEmail(), Nickname: "test user3", Password: "test-password-3", Username: "test-user-3", Roles: model.SYSTEM_USER_ROLE_ID}) + require.Nil(t, err) + user4, err := th.App.CreateUser(&model.User{Email: th.GenerateTestEmail(), Nickname: "test user4", Password: "test-password-4", Username: "test-user-4", Roles: model.SYSTEM_USER_ROLE_ID}) + require.Nil(t, err) + user5, err := th.App.CreateUser(&model.User{Email: th.GenerateTestEmail(), Nickname: "test user5", Password: "test-password-5", Username: "test-user-5", Roles: model.SYSTEM_USER_ROLE_ID}) + require.Nil(t, err) + + team1, err := th.App.CreateTeam(&model.Team{DisplayName: "dn_" + model.NewId(), Name: GenerateTestTeamName(), Email: th.GenerateTestEmail(), Type: model.TEAM_OPEN}) + require.Nil(t, err) + team2, err := th.App.CreateTeam(&model.Team{DisplayName: "dn_" + model.NewId(), Name: GenerateTestTeamName(), Email: th.GenerateTestEmail(), Type: model.TEAM_OPEN}) + require.Nil(t, err) + + channel1, err := th.App.CreateChannel(&model.Channel{DisplayName: "dn_" + model.NewId(), Name: "name_" + model.NewId(), Type: model.CHANNEL_OPEN, TeamId: team1.Id, CreatorId: model.NewId()}, false) + require.Nil(t, err) + channel2, err := th.App.CreateChannel(&model.Channel{DisplayName: "dn_" + model.NewId(), Name: "name_" + model.NewId(), Type: model.CHANNEL_OPEN, TeamId: team1.Id, CreatorId: model.NewId()}, false) + require.Nil(t, err) + channel3, err := th.App.CreateChannel(&model.Channel{DisplayName: "dn_" + model.NewId(), Name: "name_" + model.NewId(), Type: model.CHANNEL_OPEN, TeamId: team2.Id, CreatorId: model.NewId()}, false) + require.Nil(t, err) + + th.LinkUserToTeam(user1, team1) + th.LinkUserToTeam(user2, team1) + th.LinkUserToTeam(user3, team2) + th.LinkUserToTeam(user4, team1) + th.LinkUserToTeam(user4, team2) + + th.AddUserToChannel(user1, channel1) + th.AddUserToChannel(user2, channel2) + th.AddUserToChannel(user3, channel3) + th.AddUserToChannel(user4, channel1) + th.AddUserToChannel(user4, channel3) + + th.App.SetStatusOnline(user1.Id, true) + th.App.SetStatusOnline(user2.Id, true) + th.App.SetStatusOnline(user3.Id, true) + th.App.SetStatusOnline(user4.Id, true) + th.App.SetStatusOnline(user5.Id, true) + + _, resp := th.Client.Login(user1.Username, "test-password-1") + CheckNoError(t, resp) + + t.Run("getUser", func(t *testing.T) { + testCases := []struct { + Name string + RestrictedTo string + UserId string + ExpectedError string + }{ + { + "Get visible user without restrictions", + "", + user5.Id, + "", + }, + { + "Get not existing user without restrictions", + "", + model.NewId(), + "store.sql_user.missing_account.const", + }, + { + "Get not existing user with restrictions to teams", + "teams", + model.NewId(), + "api.context.permissions.app_error", + }, + { + "Get visible user with restrictions to teams", + "teams", + user2.Id, + "", + }, + { + "Get not visible user with restrictions to teams", + "teams", + user5.Id, + "api.context.permissions.app_error", + }, + { + "Get not existing user with restrictions to channels", + "channels", + model.NewId(), + "api.context.permissions.app_error", + }, + { + "Get visible user with restrictions to channels", + "channels", + user4.Id, + "", + }, + { + "Get not visible user with restrictions to channels", + "channels", + user3.Id, + "api.context.permissions.app_error", + }, + } + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + if tc.RestrictedTo == "channels" { + th.RemovePermissionFromRole(model.PERMISSION_VIEW_MEMBERS.Id, model.SYSTEM_USER_ROLE_ID) + th.RemovePermissionFromRole(model.PERMISSION_VIEW_MEMBERS.Id, model.TEAM_USER_ROLE_ID) + } else if tc.RestrictedTo == "teams" { + th.RemovePermissionFromRole(model.PERMISSION_VIEW_MEMBERS.Id, model.SYSTEM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_VIEW_MEMBERS.Id, model.TEAM_USER_ROLE_ID) + } else { + th.RemovePermissionFromRole(model.PERMISSION_VIEW_MEMBERS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_VIEW_MEMBERS.Id, model.SYSTEM_USER_ROLE_ID) + } + + _, resp := th.Client.GetUser(tc.UserId, "") + require.Nil(t, err) + if tc.ExpectedError != "" { + CheckErrorMessage(t, resp, tc.ExpectedError) + } else { + CheckNoError(t, resp) + } + }) + } + }) + + t.Run("getUserByUsername", func(t *testing.T) { + testCases := []struct { + Name string + RestrictedTo string + Username string + ExpectedError string + }{ + { + "Get visible user without restrictions", + "", + user5.Username, + "", + }, + { + "Get not existing user without restrictions", + "", + model.NewId(), + "store.sql_user.get_by_username.app_error", + }, + { + "Get not existing user with restrictions to teams", + "teams", + model.NewId(), + "api.context.permissions.app_error", + }, + { + "Get visible user with restrictions to teams", + "teams", + user2.Username, + "", + }, + { + "Get not visible user with restrictions to teams", + "teams", + user5.Username, + "api.context.permissions.app_error", + }, + { + "Get not existing user with restrictions to channels", + "channels", + model.NewId(), + "api.context.permissions.app_error", + }, + { + "Get visible user with restrictions to channels", + "channels", + user4.Username, + "", + }, + { + "Get not visible user with restrictions to channels", + "channels", + user3.Username, + "api.context.permissions.app_error", + }, + } + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + if tc.RestrictedTo == "channels" { + th.RemovePermissionFromRole(model.PERMISSION_VIEW_MEMBERS.Id, model.SYSTEM_USER_ROLE_ID) + th.RemovePermissionFromRole(model.PERMISSION_VIEW_MEMBERS.Id, model.TEAM_USER_ROLE_ID) + } else if tc.RestrictedTo == "teams" { + th.RemovePermissionFromRole(model.PERMISSION_VIEW_MEMBERS.Id, model.SYSTEM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_VIEW_MEMBERS.Id, model.TEAM_USER_ROLE_ID) + } else { + th.RemovePermissionFromRole(model.PERMISSION_VIEW_MEMBERS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_VIEW_MEMBERS.Id, model.SYSTEM_USER_ROLE_ID) + } + + _, resp := th.Client.GetUserByUsername(tc.Username, "") + require.Nil(t, err) + if tc.ExpectedError != "" { + CheckErrorMessage(t, resp, tc.ExpectedError) + } else { + CheckNoError(t, resp) + } + }) + } + }) + + t.Run("getUserByEmail", func(t *testing.T) { + testCases := []struct { + Name string + RestrictedTo string + Email string + ExpectedError string + }{ + { + "Get visible user without restrictions", + "", + user5.Email, + "", + }, + { + "Get not existing user without restrictions", + "", + th.GenerateTestEmail(), + "store.sql_user.missing_account.const", + }, + { + "Get not existing user with restrictions to teams", + "teams", + th.GenerateTestEmail(), + "api.context.permissions.app_error", + }, + { + "Get visible user with restrictions to teams", + "teams", + user2.Email, + "", + }, + { + "Get not visible user with restrictions to teams", + "teams", + user5.Email, + "api.context.permissions.app_error", + }, + { + "Get not existing user with restrictions to channels", + "channels", + th.GenerateTestEmail(), + "api.context.permissions.app_error", + }, + { + "Get visible user with restrictions to channels", + "channels", + user4.Email, + "", + }, + { + "Get not visible user with restrictions to channels", + "channels", + user3.Email, + "api.context.permissions.app_error", + }, + } + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + if tc.RestrictedTo == "channels" { + th.RemovePermissionFromRole(model.PERMISSION_VIEW_MEMBERS.Id, model.SYSTEM_USER_ROLE_ID) + th.RemovePermissionFromRole(model.PERMISSION_VIEW_MEMBERS.Id, model.TEAM_USER_ROLE_ID) + } else if tc.RestrictedTo == "teams" { + th.RemovePermissionFromRole(model.PERMISSION_VIEW_MEMBERS.Id, model.SYSTEM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_VIEW_MEMBERS.Id, model.TEAM_USER_ROLE_ID) + } else { + th.RemovePermissionFromRole(model.PERMISSION_VIEW_MEMBERS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_VIEW_MEMBERS.Id, model.SYSTEM_USER_ROLE_ID) + } + + _, resp := th.Client.GetUserByEmail(tc.Email, "") + require.Nil(t, err) + if tc.ExpectedError != "" { + CheckErrorMessage(t, resp, tc.ExpectedError) + } else { + CheckNoError(t, resp) + } + }) + } + }) + + t.Run("getDefaultProfileImage", func(t *testing.T) { + testCases := []struct { + Name string + RestrictedTo string + UserId string + ExpectedError string + }{ + { + "Get visible user without restrictions", + "", + user5.Id, + "", + }, + { + "Get not existing user without restrictions", + "", + model.NewId(), + "store.sql_user.missing_account.const", + }, + { + "Get not existing user with restrictions to teams", + "teams", + model.NewId(), + "api.context.permissions.app_error", + }, + { + "Get visible user with restrictions to teams", + "teams", + user2.Id, + "", + }, + { + "Get not visible user with restrictions to teams", + "teams", + user5.Id, + "api.context.permissions.app_error", + }, + { + "Get not existing user with restrictions to channels", + "channels", + model.NewId(), + "api.context.permissions.app_error", + }, + { + "Get visible user with restrictions to channels", + "channels", + user4.Id, + "", + }, + { + "Get not visible user with restrictions to channels", + "channels", + user3.Id, + "api.context.permissions.app_error", + }, + } + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + if tc.RestrictedTo == "channels" { + th.RemovePermissionFromRole(model.PERMISSION_VIEW_MEMBERS.Id, model.SYSTEM_USER_ROLE_ID) + th.RemovePermissionFromRole(model.PERMISSION_VIEW_MEMBERS.Id, model.TEAM_USER_ROLE_ID) + } else if tc.RestrictedTo == "teams" { + th.RemovePermissionFromRole(model.PERMISSION_VIEW_MEMBERS.Id, model.SYSTEM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_VIEW_MEMBERS.Id, model.TEAM_USER_ROLE_ID) + } else { + th.RemovePermissionFromRole(model.PERMISSION_VIEW_MEMBERS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_VIEW_MEMBERS.Id, model.SYSTEM_USER_ROLE_ID) + } + + _, resp := th.Client.GetDefaultProfileImage(tc.UserId) + require.Nil(t, err) + if tc.ExpectedError != "" { + CheckErrorMessage(t, resp, tc.ExpectedError) + } else { + CheckNoError(t, resp) + } + }) + } + }) + + t.Run("getProfileImage", func(t *testing.T) { + testCases := []struct { + Name string + RestrictedTo string + UserId string + ExpectedError string + }{ + { + "Get visible user without restrictions", + "", + user5.Id, + "", + }, + { + "Get not existing user without restrictions", + "", + model.NewId(), + "store.sql_user.missing_account.const", + }, + { + "Get not existing user with restrictions to teams", + "teams", + model.NewId(), + "api.context.permissions.app_error", + }, + { + "Get visible user with restrictions to teams", + "teams", + user2.Id, + "", + }, + { + "Get not visible user with restrictions to teams", + "teams", + user5.Id, + "api.context.permissions.app_error", + }, + { + "Get not existing user with restrictions to channels", + "channels", + model.NewId(), + "api.context.permissions.app_error", + }, + { + "Get visible user with restrictions to channels", + "channels", + user4.Id, + "", + }, + { + "Get not visible user with restrictions to channels", + "channels", + user3.Id, + "api.context.permissions.app_error", + }, + } + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + if tc.RestrictedTo == "channels" { + th.RemovePermissionFromRole(model.PERMISSION_VIEW_MEMBERS.Id, model.SYSTEM_USER_ROLE_ID) + th.RemovePermissionFromRole(model.PERMISSION_VIEW_MEMBERS.Id, model.TEAM_USER_ROLE_ID) + } else if tc.RestrictedTo == "teams" { + th.RemovePermissionFromRole(model.PERMISSION_VIEW_MEMBERS.Id, model.SYSTEM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_VIEW_MEMBERS.Id, model.TEAM_USER_ROLE_ID) + } else { + th.RemovePermissionFromRole(model.PERMISSION_VIEW_MEMBERS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_VIEW_MEMBERS.Id, model.SYSTEM_USER_ROLE_ID) + } + + _, resp := th.Client.GetProfileImage(tc.UserId, "") + require.Nil(t, err) + if tc.ExpectedError != "" { + CheckErrorMessage(t, resp, tc.ExpectedError) + } else { + CheckNoError(t, resp) + } + }) + } + }) +} diff --git a/app/admin.go b/app/admin.go index a90972665c..a0d35e37af 100644 --- a/app/admin.go +++ b/app/admin.go @@ -150,6 +150,7 @@ func (a *App) InvalidateAllCachesSkipSend() { mlog.Info("Purging all caches") a.Srv.sessionCache.Purge() ClearStatusCache() + a.Srv.Store.Team().ClearCaches() a.Srv.Store.Channel().ClearCaches() a.Srv.Store.User().ClearCaches() a.Srv.Store.Post().ClearCaches() diff --git a/app/analytics.go b/app/analytics.go index 6d4014633d..719db77183 100644 --- a/app/analytics.go +++ b/app/analytics.go @@ -254,7 +254,7 @@ func (a *App) GetAnalytics(name string, teamId string) (model.AnalyticsRows, *mo } func (a *App) GetRecentlyActiveUsersForTeam(teamId string) (map[string]*model.User, *model.AppError) { - result := <-a.Srv.Store.User().GetRecentlyActiveUsersForTeam(teamId, 0, 100) + result := <-a.Srv.Store.User().GetRecentlyActiveUsersForTeam(teamId, 0, 100, nil) if result.Err != nil { return nil, result.Err } @@ -269,9 +269,9 @@ func (a *App) GetRecentlyActiveUsersForTeam(teamId string) (map[string]*model.Us return userMap, nil } -func (a *App) GetRecentlyActiveUsersForTeamPage(teamId string, page, perPage int, asAdmin bool) ([]*model.User, *model.AppError) { +func (a *App) GetRecentlyActiveUsersForTeamPage(teamId string, page, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) { var users []*model.User - result := <-a.Srv.Store.User().GetRecentlyActiveUsersForTeam(teamId, page*perPage, perPage) + result := <-a.Srv.Store.User().GetRecentlyActiveUsersForTeam(teamId, page*perPage, perPage, viewRestrictions) if result.Err != nil { return nil, result.Err } @@ -280,9 +280,9 @@ func (a *App) GetRecentlyActiveUsersForTeamPage(teamId string, page, perPage int return a.sanitizeProfiles(users, asAdmin), nil } -func (a *App) GetNewUsersForTeamPage(teamId string, page, perPage int, asAdmin bool) ([]*model.User, *model.AppError) { +func (a *App) GetNewUsersForTeamPage(teamId string, page, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) { var users []*model.User - result := <-a.Srv.Store.User().GetNewUsersForTeam(teamId, page*perPage, perPage) + result := <-a.Srv.Store.User().GetNewUsersForTeam(teamId, page*perPage, perPage, viewRestrictions) if result.Err != nil { return nil, result.Err } diff --git a/app/app_test.go b/app/app_test.go index cdf9909edc..228af06ab2 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -125,6 +125,7 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) { model.PERMISSION_JOIN_PUBLIC_TEAMS.Id, model.PERMISSION_CREATE_DIRECT_CHANNEL.Id, model.PERMISSION_CREATE_GROUP_CHANNEL.Id, + model.PERMISSION_VIEW_MEMBERS.Id, model.PERMISSION_CREATE_TEAM.Id, }, "system_post_all": []string{ @@ -176,6 +177,7 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) { model.PERMISSION_REMOVE_OTHERS_REACTIONS.Id, model.PERMISSION_LIST_PRIVATE_TEAMS.Id, model.PERMISSION_JOIN_PRIVATE_TEAMS.Id, + model.PERMISSION_VIEW_MEMBERS.Id, model.PERMISSION_LIST_TEAM_CHANNELS.Id, model.PERMISSION_JOIN_PUBLIC_CHANNELS.Id, model.PERMISSION_READ_PUBLIC_CHANNEL.Id, @@ -305,6 +307,7 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) { model.PERMISSION_JOIN_PUBLIC_TEAMS.Id, model.PERMISSION_CREATE_DIRECT_CHANNEL.Id, model.PERMISSION_CREATE_GROUP_CHANNEL.Id, + model.PERMISSION_VIEW_MEMBERS.Id, model.PERMISSION_CREATE_TEAM.Id, }, "system_post_all": []string{ @@ -356,6 +359,7 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) { model.PERMISSION_REMOVE_OTHERS_REACTIONS.Id, model.PERMISSION_LIST_PRIVATE_TEAMS.Id, model.PERMISSION_JOIN_PRIVATE_TEAMS.Id, + model.PERMISSION_VIEW_MEMBERS.Id, model.PERMISSION_LIST_TEAM_CHANNELS.Id, model.PERMISSION_JOIN_PUBLIC_CHANNELS.Id, model.PERMISSION_READ_PUBLIC_CHANNEL.Id, @@ -523,6 +527,7 @@ func TestDoEmojisPermissionsMigration(t *testing.T) { model.PERMISSION_CREATE_EMOJIS.Id, model.PERMISSION_DELETE_EMOJIS.Id, model.PERMISSION_DELETE_OTHERS_EMOJIS.Id, + model.PERMISSION_VIEW_MEMBERS.Id, } sort.Strings(expectedSystemAdmin) @@ -583,6 +588,7 @@ func TestDoEmojisPermissionsMigration(t *testing.T) { model.PERMISSION_CREATE_TEAM.Id, model.PERMISSION_CREATE_EMOJIS.Id, model.PERMISSION_DELETE_EMOJIS.Id, + model.PERMISSION_VIEW_MEMBERS.Id, } sort.Strings(expected3) sort.Strings(role3.Permissions) diff --git a/app/channel.go b/app/channel.go index 928514182d..8f48b796a7 100644 --- a/app/channel.go +++ b/app/channel.go @@ -420,7 +420,7 @@ func (a *App) createGroupChannel(userIds []string, creatorId string) (*model.Cha return nil, model.NewAppError("CreateGroupChannel", "api.channel.create_group.bad_size.app_error", nil, "", http.StatusBadRequest) } - result := <-a.Srv.Store.User().GetProfileByIds(userIds, true) + result := <-a.Srv.Store.User().GetProfileByIds(userIds, true, nil) if result.Err != nil { return nil, result.Err } @@ -469,7 +469,7 @@ func (a *App) GetGroupChannel(userIds []string) (*model.Channel, *model.AppError return nil, model.NewAppError("GetGroupChannel", "api.channel.create_group.bad_size.app_error", nil, "", http.StatusBadRequest) } - result := <-a.Srv.Store.User().GetProfileByIds(userIds, true) + result := <-a.Srv.Store.User().GetProfileByIds(userIds, true, nil) if result.Err != nil { return nil, result.Err } @@ -1907,7 +1907,7 @@ func (a *App) MoveChannel(team *model.Team, channel *model.Channel, user *model. } if len(channelMemberIds) > 0 { - teamMembers, err2 := a.GetTeamMembersByIds(team.Id, channelMemberIds) + teamMembers, err2 := a.GetTeamMembersByIds(team.Id, channelMemberIds, nil) if err2 != nil { return err2 } diff --git a/app/cluster_handlers.go b/app/cluster_handlers.go index 997de6dcde..5222099b1f 100644 --- a/app/cluster_handlers.go +++ b/app/cluster_handlers.go @@ -20,6 +20,7 @@ func (a *App) RegisterAllClusterMessageHandlers() { a.Cluster.RegisterClusterMessageHandler(model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_CHANNEL_BY_NAME, a.ClusterInvalidateCacheForChannelByNameHandler) a.Cluster.RegisterClusterMessageHandler(model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_CHANNEL, a.ClusterInvalidateCacheForChannelHandler) a.Cluster.RegisterClusterMessageHandler(model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_USER, a.ClusterInvalidateCacheForUserHandler) + a.Cluster.RegisterClusterMessageHandler(model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_USER_TEAMS, a.ClusterInvalidateCacheForUserTeamsHandler) a.Cluster.RegisterClusterMessageHandler(model.CLUSTER_EVENT_CLEAR_SESSION_CACHE_FOR_USER, a.ClusterClearSessionCacheForUserHandler) } @@ -65,6 +66,10 @@ func (a *App) ClusterInvalidateCacheForUserHandler(msg *model.ClusterMessage) { a.InvalidateCacheForUserSkipClusterSend(msg.Data) } +func (a *App) ClusterInvalidateCacheForUserTeamsHandler(msg *model.ClusterMessage) { + a.InvalidateCacheForUserTeamsSkipClusterSend(msg.Data) +} + func (a *App) ClusterClearSessionCacheForUserHandler(msg *model.ClusterMessage) { a.ClearSessionCacheForUserSkipClusterSend(msg.Data) } diff --git a/app/import_functions_test.go b/app/import_functions_test.go index cfb6fc5d48..7ea6f54a5d 100644 --- a/app/import_functions_test.go +++ b/app/import_functions_test.go @@ -802,7 +802,7 @@ func TestImportImportUser(t *testing.T) { Position: ptrStr(model.NewId()), } - teamMembers, err := th.App.GetTeamMembers(team.Id, 0, 1000) + teamMembers, err := th.App.GetTeamMembers(team.Id, 0, 1000, nil) if err != nil { t.Fatalf("Failed to get team member count") } @@ -884,7 +884,7 @@ func TestImportImportUser(t *testing.T) { assert.Nil(t, err) // Check no new member objects were created because dry run mode. - tmc, err := th.App.GetTeamMembers(team.Id, 0, 1000) + tmc, err := th.App.GetTeamMembers(team.Id, 0, 1000, nil) require.Nil(t, err, "Failed to get Team Member Count") require.Len(t, tmc, teamMemberCount, "Number of team members not as expected") @@ -935,7 +935,7 @@ func TestImportImportUser(t *testing.T) { assert.NotNil(t, err) // Check no new member objects were created because all tests should have failed so far. - tmc, err = th.App.GetTeamMembers(team.Id, 0, 1000) + tmc, err = th.App.GetTeamMembers(team.Id, 0, 1000, nil) require.Nil(t, err, "Failed to get Team Member Count") require.Len(t, tmc, teamMemberCount) @@ -958,7 +958,7 @@ func TestImportImportUser(t *testing.T) { assert.NotNil(t, err) // Check only new team member object created because dry run mode. - tmc, err = th.App.GetTeamMembers(team.Id, 0, 1000) + tmc, err = th.App.GetTeamMembers(team.Id, 0, 1000, nil) require.Nil(t, err, "Failed to get Team Member Count") require.Len(t, tmc, teamMemberCount+1) @@ -991,7 +991,7 @@ func TestImportImportUser(t *testing.T) { assert.Nil(t, err) // Check only new channel member object created because dry run mode. - tmc, err = th.App.GetTeamMembers(team.Id, 0, 1000) + tmc, err = th.App.GetTeamMembers(team.Id, 0, 1000, nil) require.Nil(t, err, "Failed to get Team Member Count") require.Len(t, tmc, teamMemberCount+1, "Number of team members not as expected") @@ -1046,7 +1046,7 @@ func TestImportImportUser(t *testing.T) { checkPreference(t, th.App, user.Id, model.PREFERENCE_CATEGORY_THEME, team.Id, *(*data.Teams)[0].Theme) // No more new member objects. - tmc, err = th.App.GetTeamMembers(team.Id, 0, 1000) + tmc, err = th.App.GetTeamMembers(team.Id, 0, 1000, nil) require.Nil(t, err, "Failed to get Team Member Count") require.Len(t, tmc, teamMemberCount+1, "Number of team members not as expected") diff --git a/app/notification.go b/app/notification.go index 7090e963c2..e8d9a98eae 100644 --- a/app/notification.go +++ b/app/notification.go @@ -125,7 +125,7 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod } if len(m.OtherPotentialMentions) > 0 && !post.IsSystemMessage() { - if result := <-a.Srv.Store.User().GetProfilesByUsernames(m.OtherPotentialMentions, team.Id); result.Err == nil { + if result := <-a.Srv.Store.User().GetProfilesByUsernames(m.OtherPotentialMentions, &model.ViewUsersRestrictions{Teams: []string{team.Id}}); result.Err == nil { channelMentions := model.UserSlice(result.Data.([]*model.User)).FilterByActive(true) var outOfChannelMentions model.UserSlice diff --git a/app/permissions_migrations.go b/app/permissions_migrations.go index 283836edc2..8de718d27a 100644 --- a/app/permissions_migrations.go +++ b/app/permissions_migrations.go @@ -22,6 +22,7 @@ const ( MIGRATION_KEY_ADD_BOT_PERMISSIONS = "add_bot_permissions" MIGRATION_KEY_APPLY_CHANNEL_MANAGE_DELETE_TO_CHANNEL_USER = "apply_channel_manage_delete_to_channel_user" MIGRATION_KEY_REMOVE_CHANNEL_MANAGE_DELETE_FROM_TEAM_USER = "remove_channel_manage_delete_from_team_user" + MIGRATION_KEY_VIEW_MEMBERS_NEW_PERMISSION = "view_members_new_permission" PERMISSION_MANAGE_SYSTEM = "manage_system" PERMISSION_MANAGE_EMOJIS = "manage_emojis" @@ -49,6 +50,7 @@ const ( PERMISSION_DELETE_PRIVATE_CHANNEL = "delete_private_channel" PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES = "manage_public_channel_properties" PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES = "manage_private_channel_properties" + PERMISSION_VIEW_MEMBERS = "view_members" ) func isRole(role string) func(string, map[string]map[string]bool) bool { @@ -258,6 +260,19 @@ func removeChannelManageDeleteFromTeamUser() permissionsMap { } } +func getViewMembersPermissionMigration() permissionsMap { + return permissionsMap{ + permissionTransformation{ + On: isRole(model.SYSTEM_USER_ROLE_ID), + Add: []string{PERMISSION_VIEW_MEMBERS}, + }, + permissionTransformation{ + On: isRole(model.SYSTEM_ADMIN_ROLE_ID), + Add: []string{PERMISSION_VIEW_MEMBERS}, + }, + } +} + // DoPermissionsMigrations execute all the permissions migrations need by the current version. func (a *App) DoPermissionsMigrations() *model.AppError { PermissionsMigrations := []struct { @@ -271,6 +286,7 @@ func (a *App) DoPermissionsMigrations() *model.AppError { {Key: MIGRATION_KEY_ADD_BOT_PERMISSIONS, Migration: getAddBotPermissionsMigration}, {Key: MIGRATION_KEY_APPLY_CHANNEL_MANAGE_DELETE_TO_CHANNEL_USER, Migration: applyChannelManageDeleteToChannelUser}, {Key: MIGRATION_KEY_REMOVE_CHANNEL_MANAGE_DELETE_FROM_TEAM_USER, Migration: removeChannelManageDeleteFromTeamUser}, + {Key: MIGRATION_KEY_VIEW_MEMBERS_NEW_PERMISSION, Migration: getViewMembersPermissionMigration}, } for _, migration := range PermissionsMigrations { diff --git a/app/plugin_api.go b/app/plugin_api.go index 7c0cbcc48d..b3464c5903 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -173,7 +173,7 @@ func (api *PluginAPI) DeleteTeamMember(teamId, userId, requestorId string) *mode } func (api *PluginAPI) GetTeamMembers(teamId string, page, perPage int) ([]*model.TeamMember, *model.AppError) { - return api.app.GetTeamMembers(teamId, page*perPage, perPage) + return api.app.GetTeamMembers(teamId, page*perPage, perPage, nil) } func (api *PluginAPI) GetTeamMember(teamId, userId string) (*model.TeamMember, *model.AppError) { @@ -222,7 +222,7 @@ func (api *PluginAPI) GetUserByUsername(name string) (*model.User, *model.AppErr } func (api *PluginAPI) GetUsersByUsernames(usernames []string) ([]*model.User, *model.AppError) { - return api.app.GetUsersByUsernames(usernames, true) + return api.app.GetUsersByUsernames(usernames, true, nil) } func (api *PluginAPI) GetUsersInTeam(teamId string, page int, perPage int) ([]*model.User, *model.AppError) { diff --git a/app/post.go b/app/post.go index 25a686996f..180ccbf7c8 100644 --- a/app/post.go +++ b/app/post.go @@ -780,7 +780,7 @@ func (a *App) DeletePostFiles(post *model.Post) { func (a *App) parseAndFetchChannelIdByNameFromInFilter(channelName, userId, teamId string, includeDeleted bool) (*model.Channel, error) { if strings.HasPrefix(channelName, "@") && strings.Contains(channelName, ",") { var userIds []string - users, err := a.GetUsersByUsernames(strings.Split(channelName[1:], ","), false) + users, err := a.GetUsersByUsernames(strings.Split(channelName[1:], ","), false, nil) if err != nil { return nil, err } diff --git a/app/slack.go b/app/slack.go index f05c274781..45b1391709 100644 --- a/app/slack.go +++ b/app/slack.go @@ -66,7 +66,7 @@ func replaceUserIds(userStore store.UserStore, text string) string { userIds = append(userIds, match[1]) } - if res := <-userStore.GetProfileByIds(userIds, true); res.Err == nil { + if res := <-userStore.GetProfileByIds(userIds, true, nil); res.Err == nil { for _, user := range res.Data.([]*model.User) { text = strings.Replace(text, "<@"+user.Id+">", "@"+user.Username, -1) } diff --git a/app/syncables_test.go b/app/syncables_test.go index c493b45d0e..67bb90af54 100644 --- a/app/syncables_test.go +++ b/app/syncables_test.go @@ -114,7 +114,7 @@ func TestCreateDefaultMemberships(t *testing.T) { t.Errorf("error retrieving channel member: %s", err.Error()) } - tMembers, err := th.App.GetTeamMembers(singersTeam.Id, 0, 999) + tMembers, err := th.App.GetTeamMembers(singersTeam.Id, 0, 999, nil) if err != nil { t.Errorf("error retrieving team members: %s", err.Error()) } @@ -143,7 +143,7 @@ func TestCreateDefaultMemberships(t *testing.T) { t.Errorf("wrong error: %s", err.Id) } - tMembers, err = th.App.GetTeamMembers(nerdsTeam.Id, 0, 999) + tMembers, err = th.App.GetTeamMembers(nerdsTeam.Id, 0, 999, nil) if err != nil { t.Errorf("error retrieving team members: %s", err.Error()) } @@ -185,7 +185,7 @@ func TestCreateDefaultMemberships(t *testing.T) { t.Errorf("wrong error: %s", err.Id) } - tMembers, err = th.App.GetTeamMembers(nerdsTeam.Id, 0, 999) + tMembers, err = th.App.GetTeamMembers(nerdsTeam.Id, 0, 999, nil) if err != nil { t.Errorf("error retrieving team members: %s", err.Error()) } @@ -366,7 +366,7 @@ func TestDeleteGroupMemberships(t *testing.T) { require.Nil(t, err) // verify the member count - tmembers, err := th.App.GetTeamMembers(th.BasicTeam.Id, 0, 100) + tmembers, err := th.App.GetTeamMembers(th.BasicTeam.Id, 0, 100, nil) require.Nil(t, err) require.Len(t, tmembers, 3) @@ -383,7 +383,7 @@ func TestDeleteGroupMemberships(t *testing.T) { require.Nil(t, appErr) // verify the new member counts - tmembers, err = th.App.GetTeamMembers(th.BasicTeam.Id, 0, 100) + tmembers, err = th.App.GetTeamMembers(th.BasicTeam.Id, 0, 100, nil) require.Nil(t, err) require.Len(t, tmembers, 1) require.Equal(t, th.SystemAdminUser.Id, tmembers[0].UserId) diff --git a/app/team.go b/app/team.go index c2bf112f7d..c69099e957 100644 --- a/app/team.go +++ b/app/team.go @@ -559,6 +559,7 @@ func (a *App) JoinUserToTeam(team *model.Team, user *model.User, userRequestorId a.ClearSessionCacheForUser(user.Id) a.InvalidateCacheForUser(user.Id) + a.InvalidateCacheForUserTeams(user.Id) message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_ADDED_TO_TEAM, "", "", user.Id, nil) message.Add("team_id", team.Id) @@ -693,16 +694,16 @@ func (a *App) GetTeamMembersForUserWithPagination(userId string, page, perPage i return result.Data.([]*model.TeamMember), nil } -func (a *App) GetTeamMembers(teamId string, offset int, limit int) ([]*model.TeamMember, *model.AppError) { - result := <-a.Srv.Store.Team().GetMembers(teamId, offset, limit) +func (a *App) GetTeamMembers(teamId string, offset int, limit int, restrictions *model.ViewUsersRestrictions) ([]*model.TeamMember, *model.AppError) { + result := <-a.Srv.Store.Team().GetMembers(teamId, offset, limit, restrictions) if result.Err != nil { return nil, result.Err } return result.Data.([]*model.TeamMember), nil } -func (a *App) GetTeamMembersByIds(teamId string, userIds []string) ([]*model.TeamMember, *model.AppError) { - result := <-a.Srv.Store.Team().GetMembersByIds(teamId, userIds) +func (a *App) GetTeamMembersByIds(teamId string, userIds []string, restrictions *model.ViewUsersRestrictions) ([]*model.TeamMember, *model.AppError) { + result := <-a.Srv.Store.Team().GetMembersByIds(teamId, userIds, restrictions) if result.Err != nil { return nil, result.Err } @@ -932,6 +933,7 @@ func (a *App) LeaveTeam(team *model.Team, user *model.User, requestorId string) a.ClearSessionCacheForUser(user.Id) a.InvalidateCacheForUser(user.Id) + a.InvalidateCacheForUserTeams(user.Id) return nil } diff --git a/app/team_test.go b/app/team_test.go index 14c388311a..f18ec81d59 100644 --- a/app/team_test.go +++ b/app/team_test.go @@ -756,10 +756,10 @@ func TestGetTeamMembers(t *testing.T) { sort.Sort(userIDs) // Fetch team members multipile times - members, err := th.App.GetTeamMembers(th.BasicTeam.Id, 0, 5) + members, err := th.App.GetTeamMembers(th.BasicTeam.Id, 0, 5, nil) require.Nil(t, err) // This should return 5 members - members2, err := th.App.GetTeamMembers(th.BasicTeam.Id, 5, 6) + members2, err := th.App.GetTeamMembers(th.BasicTeam.Id, 5, 6, nil) require.Nil(t, err) members = append(members, members2...) @@ -776,7 +776,7 @@ func TestGetTeamStats(t *testing.T) { teamStats, err := th.App.GetTeamStats(th.BasicTeam.Id) require.Nil(t, err) require.NotNil(t, teamStats) - members, err := th.App.GetTeamMembers(th.BasicTeam.Id, 0, 5) + members, err := th.App.GetTeamMembers(th.BasicTeam.Id, 0, 5, nil) require.Nil(t, err) assert.Equal(t, int64(len(members)), teamStats.TotalMemberCount) } diff --git a/app/user.go b/app/user.go index 9f5a977937..fafde29633 100644 --- a/app/user.go +++ b/app/user.go @@ -447,8 +447,8 @@ func (a *App) GetUsersPage(options *model.UserGetOptions, asAdmin bool) ([]*mode return a.sanitizeProfiles(users, asAdmin), nil } -func (a *App) GetUsersEtag() string { - return fmt.Sprintf("%v.%v.%v", (<-a.Srv.Store.User().GetEtagForAllProfiles()).Data.(string), a.Config().PrivacySettings.ShowFullName, a.Config().PrivacySettings.ShowEmailAddress) +func (a *App) GetUsersEtag(restrictionsHash string) string { + return fmt.Sprintf("%v.%v.%v.%v", (<-a.Srv.Store.User().GetEtagForAllProfiles()).Data.(string), a.Config().PrivacySettings.ShowFullName, a.Config().PrivacySettings.ShowEmailAddress, restrictionsHash) } func (a *App) GetUsersInTeam(options *model.UserGetOptions) ([]*model.User, *model.AppError) { @@ -459,8 +459,8 @@ func (a *App) GetUsersInTeam(options *model.UserGetOptions) ([]*model.User, *mod return result.Data.([]*model.User), nil } -func (a *App) GetUsersNotInTeam(teamId string, offset int, limit int) ([]*model.User, *model.AppError) { - result := <-a.Srv.Store.User().GetProfilesNotInTeam(teamId, offset, limit) +func (a *App) GetUsersNotInTeam(teamId string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) { + result := <-a.Srv.Store.User().GetProfilesNotInTeam(teamId, offset, limit, viewRestrictions) if result.Err != nil { return nil, result.Err } @@ -476,8 +476,8 @@ func (a *App) GetUsersInTeamPage(options *model.UserGetOptions, asAdmin bool) ([ return a.sanitizeProfiles(users, asAdmin), nil } -func (a *App) GetUsersNotInTeamPage(teamId string, page int, perPage int, asAdmin bool) ([]*model.User, *model.AppError) { - users, err := a.GetUsersNotInTeam(teamId, page*perPage, perPage) +func (a *App) GetUsersNotInTeamPage(teamId string, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) { + users, err := a.GetUsersNotInTeam(teamId, page*perPage, perPage, viewRestrictions) if err != nil { return nil, err } @@ -485,12 +485,12 @@ func (a *App) GetUsersNotInTeamPage(teamId string, page int, perPage int, asAdmi return a.sanitizeProfiles(users, asAdmin), nil } -func (a *App) GetUsersInTeamEtag(teamId string) string { - return fmt.Sprintf("%v.%v.%v", (<-a.Srv.Store.User().GetEtagForProfiles(teamId)).Data.(string), a.Config().PrivacySettings.ShowFullName, a.Config().PrivacySettings.ShowEmailAddress) +func (a *App) GetUsersInTeamEtag(teamId string, restrictionsHash string) string { + return fmt.Sprintf("%v.%v.%v.%v", (<-a.Srv.Store.User().GetEtagForProfiles(teamId)).Data.(string), a.Config().PrivacySettings.ShowFullName, a.Config().PrivacySettings.ShowEmailAddress, restrictionsHash) } -func (a *App) GetUsersNotInTeamEtag(teamId string) string { - return fmt.Sprintf("%v.%v.%v", (<-a.Srv.Store.User().GetEtagForProfilesNotInTeam(teamId)).Data.(string), a.Config().PrivacySettings.ShowFullName, a.Config().PrivacySettings.ShowEmailAddress) +func (a *App) GetUsersNotInTeamEtag(teamId string, restrictionsHash string) string { + return fmt.Sprintf("%v.%v.%v.%v", (<-a.Srv.Store.User().GetEtagForProfilesNotInTeam(teamId)).Data.(string), a.Config().PrivacySettings.ShowFullName, a.Config().PrivacySettings.ShowEmailAddress, restrictionsHash) } func (a *App) GetUsersInChannel(channelId string, offset int, limit int) ([]*model.User, *model.AppError) { @@ -541,16 +541,16 @@ func (a *App) GetUsersInChannelPageByStatus(channelId string, page int, perPage return a.sanitizeProfiles(users, asAdmin), nil } -func (a *App) GetUsersNotInChannel(teamId string, channelId string, offset int, limit int) ([]*model.User, *model.AppError) { - result := <-a.Srv.Store.User().GetProfilesNotInChannel(teamId, channelId, offset, limit) +func (a *App) GetUsersNotInChannel(teamId string, channelId string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) { + result := <-a.Srv.Store.User().GetProfilesNotInChannel(teamId, channelId, offset, limit, viewRestrictions) if result.Err != nil { return nil, result.Err } return result.Data.([]*model.User), nil } -func (a *App) GetUsersNotInChannelMap(teamId string, channelId string, offset int, limit int, asAdmin bool) (map[string]*model.User, *model.AppError) { - users, err := a.GetUsersNotInChannel(teamId, channelId, offset, limit) +func (a *App) GetUsersNotInChannelMap(teamId string, channelId string, offset int, limit int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) (map[string]*model.User, *model.AppError) { + users, err := a.GetUsersNotInChannel(teamId, channelId, offset, limit, viewRestrictions) if err != nil { return nil, err } @@ -565,8 +565,8 @@ func (a *App) GetUsersNotInChannelMap(teamId string, channelId string, offset in return userMap, nil } -func (a *App) GetUsersNotInChannelPage(teamId string, channelId string, page int, perPage int, asAdmin bool) ([]*model.User, *model.AppError) { - users, err := a.GetUsersNotInChannel(teamId, channelId, page*perPage, perPage) +func (a *App) GetUsersNotInChannelPage(teamId string, channelId string, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) { + users, err := a.GetUsersNotInChannel(teamId, channelId, page*perPage, perPage, viewRestrictions) if err != nil { return nil, err } @@ -574,8 +574,8 @@ func (a *App) GetUsersNotInChannelPage(teamId string, channelId string, page int return a.sanitizeProfiles(users, asAdmin), nil } -func (a *App) GetUsersWithoutTeamPage(page int, perPage int, asAdmin bool) ([]*model.User, *model.AppError) { - users, err := a.GetUsersWithoutTeam(page*perPage, perPage) +func (a *App) GetUsersWithoutTeamPage(page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) { + users, err := a.GetUsersWithoutTeam(page*perPage, perPage, viewRestrictions) if err != nil { return nil, err } @@ -583,8 +583,8 @@ func (a *App) GetUsersWithoutTeamPage(page int, perPage int, asAdmin bool) ([]*m return a.sanitizeProfiles(users, asAdmin), nil } -func (a *App) GetUsersWithoutTeam(offset int, limit int) ([]*model.User, *model.AppError) { - result := <-a.Srv.Store.User().GetProfilesWithoutTeam(offset, limit) +func (a *App) GetUsersWithoutTeam(offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) { + result := <-a.Srv.Store.User().GetProfilesWithoutTeam(offset, limit, viewRestrictions) if result.Err != nil { return nil, result.Err } @@ -609,16 +609,16 @@ func (a *App) GetChannelGroupUsers(channelID string) ([]*model.User, *model.AppE return result.Data.([]*model.User), nil } -func (a *App) GetUsersByIds(userIds []string, asAdmin bool) ([]*model.User, *model.AppError) { - result := <-a.Srv.Store.User().GetProfileByIds(userIds, true) +func (a *App) GetUsersByIds(userIds []string, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) { + result := <-a.Srv.Store.User().GetProfileByIds(userIds, viewRestrictions == nil, viewRestrictions) if result.Err != nil { return nil, result.Err } return a.sanitizeProfiles(result.Data.([]*model.User), asAdmin), nil } -func (a *App) GetUsersByUsernames(usernames []string, asAdmin bool) ([]*model.User, *model.AppError) { - result := <-a.Srv.Store.User().GetProfilesByUsernames(usernames, "") +func (a *App) GetUsersByUsernames(usernames []string, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) { + result := <-a.Srv.Store.User().GetProfilesByUsernames(usernames, viewRestrictions) if result.Err != nil { return nil, result.Err } @@ -1601,9 +1601,10 @@ func (a *App) GetVerifyEmailToken(token string) (*model.Token, *model.AppError) } // GetTotalUsersStats is used for the DM list total -func (a *App) GetTotalUsersStats() (*model.UsersStats, *model.AppError) { +func (a *App) GetTotalUsersStats(viewRestrictions *model.ViewUsersRestrictions) (*model.UsersStats, *model.AppError) { result := <-a.Srv.Store.User().Count(model.UserCountOptions{ IncludeBotAccounts: true, + ViewRestrictions: viewRestrictions, }) if result.Err != nil { return nil, result.Err @@ -1682,12 +1683,20 @@ func (a *App) SearchUsersInTeam(teamId string, term string, options *model.UserS esInterface := a.Elasticsearch license := a.License() if esInterface != nil && *a.Config().ElasticsearchSettings.EnableAutocomplete && license != nil && *license.Features.Elasticsearch { - usersIds, err := a.Elasticsearch.SearchUsersInTeam(teamId, term, options) + listOfAllowedChannels, err := a.GetViewUsersRestrictionsForTeam(a.Session.UserId, teamId) + if err != nil { + return nil, err + } + if len(listOfAllowedChannels) == 0 { + return []*model.User{}, nil + } + + usersIds, err := a.Elasticsearch.SearchUsersInTeam(teamId, listOfAllowedChannels, term, options) if err != nil { return nil, err } - result = <-a.Srv.Store.User().GetProfileByIds(usersIds, false) + result = <-a.Srv.Store.User().GetProfileByIds(usersIds, false, nil) } else { result = <-a.Srv.Store.User().Search(teamId, term, options) } @@ -1738,12 +1747,25 @@ func (a *App) AutocompleteUsersInChannel(teamId string, channelId string, term s esInterface := a.Elasticsearch license := a.License() if esInterface != nil && *a.Config().ElasticsearchSettings.EnableAutocomplete && license != nil && *license.Features.Elasticsearch { - uchanIds, nuchanIds, err := a.Elasticsearch.SearchUsersInChannel(teamId, channelId, term, options) + listOfAllowedChannels, err := a.getListOfAllowedChannelsForTeam(teamId, options.ViewRestrictions) if err != nil { return nil, err } - uchan = a.Srv.Store.User().GetProfileByIds(uchanIds, false) - nuchan = a.Srv.Store.User().GetProfileByIds(nuchanIds, false) + if len(listOfAllowedChannels) == 0 { + return &model.UserAutocompleteInChannel{}, nil + } + uchanIds := []string{} + nuchanIds := []string{} + if !strings.Contains(strings.Join(listOfAllowedChannels, "."), channelId) { + nuchanIds, err = a.Elasticsearch.SearchUsersInTeam(teamId, listOfAllowedChannels, term, options) + } else { + uchanIds, nuchanIds, err = a.Elasticsearch.SearchUsersInChannel(teamId, channelId, listOfAllowedChannels, term, options) + } + if err != nil { + return nil, err + } + uchan = a.Srv.Store.User().GetProfileByIds(uchanIds, false, nil) + nuchan = a.Srv.Store.User().GetProfileByIds(nuchanIds, false, nil) } else { uchan = a.Srv.Store.User().SearchInChannel(channelId, term, options) nuchan = a.Srv.Store.User().SearchNotInChannel(teamId, channelId, term, options) @@ -1785,12 +1807,20 @@ func (a *App) AutocompleteUsersInTeam(teamId string, term string, options *model esInterface := a.Elasticsearch license := a.License() if esInterface != nil && *a.Config().ElasticsearchSettings.EnableAutocomplete && license != nil && *license.Features.Elasticsearch { - usersIds, err := a.Elasticsearch.SearchUsersInTeam(teamId, term, options) + listOfAllowedChannels, err := a.getListOfAllowedChannelsForTeam(teamId, options.ViewRestrictions) + if err != nil { + return nil, err + } + if len(listOfAllowedChannels) == 0 { + return &model.UserAutocompleteInTeam{}, nil + } + + usersIds, err := a.Elasticsearch.SearchUsersInTeam(teamId, listOfAllowedChannels, term, options) if err != nil { return nil, err } - result = <-a.Srv.Store.User().GetProfileByIds(usersIds, false) + result = <-a.Srv.Store.User().GetProfileByIds(usersIds, false, nil) } else { result = <-a.Srv.Store.User().Search(teamId, term, options) } @@ -1865,6 +1895,16 @@ func (a *App) UpdateOAuthUserAttrs(userData io.Reader, user *model.User, provide return nil } +func (a *App) RestrictUsersGetByPermissions(userId string, options *model.UserGetOptions) (*model.UserGetOptions, *model.AppError) { + restrictions, err := a.GetViewUsersRestrictions(userId) + if err != nil { + return nil, err + } + + options.ViewRestrictions = restrictions + return options, nil +} + // FilterNonGroupTeamMembers returns the subset of the given user IDs of the users who are not members of groups // associated to the team. func (a *App) FilterNonGroupTeamMembers(userIDs []string, team *model.Team) ([]string, error) { @@ -1930,3 +1970,154 @@ func (a *App) FilterNonGroupChannelMembers(userIDs []string, channel *model.Chan return nonMemberIDs, nil } + +func (a *App) RestrictUsersSearchByPermissions(userId string, options *model.UserSearchOptions) (*model.UserSearchOptions, *model.AppError) { + restrictions, err := a.GetViewUsersRestrictions(userId) + if err != nil { + return nil, err + } + + options.ViewRestrictions = restrictions + return options, nil +} + +func (a *App) UserCanSeeOtherUser(userId string, otherUserId string) (bool, *model.AppError) { + if userId == otherUserId { + return true, nil + } + + restrictions, err := a.GetViewUsersRestrictions(userId) + if err != nil { + return false, err + } + + if restrictions == nil { + return true, nil + } + + if len(restrictions.Teams) > 0 { + result, err := a.userBelongsToTeams(otherUserId, restrictions.Teams) + if err != nil { + return false, err + } + if result { + return true, nil + } + } + + if len(restrictions.Channels) > 0 { + result, err := a.userBelongsToChannels(otherUserId, restrictions.Channels) + if err != nil { + return false, err + } + if result { + return true, nil + } + } + + return false, nil +} + +func (a *App) userBelongsToTeams(userId string, teamIds []string) (bool, *model.AppError) { + result := <-a.Srv.Store.Team().UserBelongsToTeams(userId, teamIds) + if result.Err != nil { + return false, result.Err + } + return result.Data.(bool), nil +} + +func (a *App) userBelongsToChannels(userId string, channelIds []string) (bool, *model.AppError) { + result := <-a.Srv.Store.Channel().UserBelongsToChannels(userId, channelIds) + if result.Err != nil { + return false, result.Err + } + return result.Data.(bool), nil +} + +func (a *App) GetViewUsersRestrictions(userId string) (*model.ViewUsersRestrictions, *model.AppError) { + if a.HasPermissionTo(userId, model.PERMISSION_VIEW_MEMBERS) { + return nil, nil + } + + result := <-a.Srv.Store.Team().GetUserTeamIds(userId, true) + if result.Err != nil { + return nil, result.Err + } + teamIds := result.Data.([]string) + + teamIdsWithPermission := []string{} + teamIdsWithoutPermission := []string{} + for _, teamId := range teamIds { + if a.HasPermissionToTeam(userId, teamId, model.PERMISSION_VIEW_MEMBERS) { + teamIdsWithPermission = append(teamIdsWithPermission, teamId) + } else { + teamIdsWithoutPermission = append(teamIdsWithoutPermission, teamId) + } + } + + if len(teamIdsWithoutPermission) == 0 { + return &model.ViewUsersRestrictions{Teams: teamIdsWithPermission}, nil + } + + userChannelMembers := <-a.Srv.Store.Channel().GetAllChannelMembersForUser(userId, true, true) + if userChannelMembers.Err != nil { + return nil, userChannelMembers.Err + } + + channelIds := []string{} + for channelId := range userChannelMembers.Data.(map[string]string) { + channelIds = append(channelIds, channelId) + } + + return &model.ViewUsersRestrictions{Teams: teamIdsWithPermission, Channels: channelIds}, nil +} + +func (a *App) GetViewUsersRestrictionsForTeam(userId string, teamId string) ([]string, *model.AppError) { + if a.HasPermissionTo(userId, model.PERMISSION_VIEW_MEMBERS) { + return nil, nil + } + + if a.HasPermissionToTeam(userId, teamId, model.PERMISSION_VIEW_MEMBERS) { + return nil, nil + } + + result := <-a.Srv.Store.Channel().GetMembersForUser(teamId, userId) + if result.Err != nil { + return nil, result.Err + } + + channelIds := []string{} + for _, membership := range *result.Data.(*model.ChannelMembers) { + channelIds = append(channelIds, membership.ChannelId) + } + + return channelIds, nil +} + +func (a *App) getListOfAllowedChannelsForTeam(teamId string, viewRestrictions *model.ViewUsersRestrictions) ([]string, *model.AppError) { + var listOfAllowedChannels []string + if viewRestrictions == nil || strings.Contains(strings.Join(viewRestrictions.Teams, "."), teamId) { + result := <-a.Srv.Store.Channel().GetTeamChannels(teamId) + if result.Err != nil { + return nil, result.Err + } + channelIds := []string{} + for _, channel := range *result.Data.(*model.ChannelList) { + channelIds = append(channelIds, channel.Id) + } + + return channelIds, nil + } + + cresult := <-a.Srv.Store.Channel().GetChannelsByIds(viewRestrictions.Channels) + if cresult.Err != nil { + return nil, cresult.Err + } + for _, c := range cresult.Data.([]*model.Channel) { + if c.TeamId == teamId { + listOfAllowedChannels = append(listOfAllowedChannels, c.Id) + } + } + + return listOfAllowedChannels, nil +} diff --git a/app/user_test.go b/app/user_test.go index ea7801dc35..74997d0d11 100644 --- a/app/user_test.go +++ b/app/user_test.go @@ -704,3 +704,206 @@ func TestPasswordRecovery(t *testing.T) { err = th.App.ResetPasswordFromToken(token.Token, "abcdefgh") assert.NotNil(t, err) } + +func TestGetViewUsersRestrictions(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + team1 := th.CreateTeam() + team2 := th.CreateTeam() + th.CreateTeam() // Another team + + user1 := th.CreateUser() + + th.LinkUserToTeam(user1, team1) + th.LinkUserToTeam(user1, team2) + + th.App.UpdateTeamMemberRoles(team1.Id, user1.Id, "team_user team_admin") + + team1channel1 := th.CreateChannel(team1) + team1channel2 := th.CreateChannel(team1) + th.CreateChannel(team1) // Another channel + team1offtopic, err := th.App.GetChannelByName("off-topic", team1.Id, false) + require.Nil(t, err) + team1townsquare, err := th.App.GetChannelByName("town-square", team1.Id, false) + require.Nil(t, err) + + team2channel1 := th.CreateChannel(team2) + th.CreateChannel(team2) // Another channel + team2offtopic, err := th.App.GetChannelByName("off-topic", team2.Id, false) + require.Nil(t, err) + team2townsquare, err := th.App.GetChannelByName("town-square", team2.Id, false) + require.Nil(t, err) + + th.App.AddUserToChannel(user1, team1channel1) + th.App.AddUserToChannel(user1, team1channel2) + th.App.AddUserToChannel(user1, team2channel1) + + addPermission := func(role *model.Role, permission string) *model.AppError { + newPermissions := append(role.Permissions, permission) + _, err := th.App.PatchRole(role, &model.RolePatch{Permissions: &newPermissions}) + return err + } + + removePermission := func(role *model.Role, permission string) *model.AppError { + newPermissions := []string{} + for _, oldPermission := range role.Permissions { + if permission != oldPermission { + newPermissions = append(newPermissions, oldPermission) + } + } + _, err := th.App.PatchRole(role, &model.RolePatch{Permissions: &newPermissions}) + return err + } + + t.Run("VIEW_MEMBERS permission granted at system level", func(t *testing.T) { + restrictions, err := th.App.GetViewUsersRestrictions(user1.Id) + require.Nil(t, err) + + assert.Nil(t, restrictions) + }) + + t.Run("VIEW_MEMBERS permission granted at team level", func(t *testing.T) { + systemUserRole, err := th.App.GetRoleByName(model.SYSTEM_USER_ROLE_ID) + require.Nil(t, err) + teamUserRole, err := th.App.GetRoleByName(model.TEAM_USER_ROLE_ID) + require.Nil(t, err) + + require.Nil(t, removePermission(systemUserRole, model.PERMISSION_VIEW_MEMBERS.Id)) + defer addPermission(systemUserRole, model.PERMISSION_VIEW_MEMBERS.Id) + require.Nil(t, addPermission(teamUserRole, model.PERMISSION_VIEW_MEMBERS.Id)) + defer removePermission(teamUserRole, model.PERMISSION_VIEW_MEMBERS.Id) + + restrictions, err := th.App.GetViewUsersRestrictions(user1.Id) + require.Nil(t, err) + + assert.NotNil(t, restrictions) + assert.NotNil(t, restrictions.Teams) + assert.Len(t, restrictions.Channels, 0) + assert.ElementsMatch(t, []string{team1.Id, team2.Id}, restrictions.Teams) + }) + + t.Run("VIEW_MEMBERS permission not granted at any level", func(t *testing.T) { + systemUserRole, err := th.App.GetRoleByName(model.SYSTEM_USER_ROLE_ID) + require.Nil(t, err) + require.Nil(t, removePermission(systemUserRole, model.PERMISSION_VIEW_MEMBERS.Id)) + defer addPermission(systemUserRole, model.PERMISSION_VIEW_MEMBERS.Id) + + restrictions, err := th.App.GetViewUsersRestrictions(user1.Id) + require.Nil(t, err) + + assert.NotNil(t, restrictions) + assert.Len(t, restrictions.Teams, 0) + assert.NotNil(t, restrictions.Channels) + assert.ElementsMatch(t, []string{team1townsquare.Id, team1offtopic.Id, team1channel1.Id, team1channel2.Id, team2townsquare.Id, team2offtopic.Id, team2channel1.Id}, restrictions.Channels) + }) + + t.Run("VIEW_MEMBERS permission for some teams but not for others", func(t *testing.T) { + systemUserRole, err := th.App.GetRoleByName(model.SYSTEM_USER_ROLE_ID) + require.Nil(t, err) + teamAdminRole, err := th.App.GetRoleByName(model.TEAM_ADMIN_ROLE_ID) + require.Nil(t, err) + + require.Nil(t, removePermission(systemUserRole, model.PERMISSION_VIEW_MEMBERS.Id)) + defer addPermission(systemUserRole, model.PERMISSION_VIEW_MEMBERS.Id) + require.Nil(t, addPermission(teamAdminRole, model.PERMISSION_VIEW_MEMBERS.Id)) + defer removePermission(teamAdminRole, model.PERMISSION_VIEW_MEMBERS.Id) + + restrictions, err := th.App.GetViewUsersRestrictions(user1.Id) + require.Nil(t, err) + + assert.NotNil(t, restrictions) + assert.NotNil(t, restrictions.Teams) + assert.NotNil(t, restrictions.Channels) + assert.ElementsMatch(t, restrictions.Teams, []string{team1.Id}) + assert.ElementsMatch(t, []string{team1townsquare.Id, team1offtopic.Id, team1channel1.Id, team1channel2.Id, team2townsquare.Id, team2offtopic.Id, team2channel1.Id}, restrictions.Channels) + }) +} + +func TestGetViewUsersRestrictionsForTeam(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + team1 := th.CreateTeam() + team2 := th.CreateTeam() + th.CreateTeam() // Another team + + user1 := th.CreateUser() + + th.LinkUserToTeam(user1, team1) + th.LinkUserToTeam(user1, team2) + + th.App.UpdateTeamMemberRoles(team1.Id, user1.Id, "team_user team_admin") + + team1channel1 := th.CreateChannel(team1) + team1channel2 := th.CreateChannel(team1) + th.CreateChannel(team1) // Another channel + team1offtopic, err := th.App.GetChannelByName("off-topic", team1.Id, false) + require.Nil(t, err) + team1townsquare, err := th.App.GetChannelByName("town-square", team1.Id, false) + require.Nil(t, err) + + team2channel1 := th.CreateChannel(team2) + th.CreateChannel(team2) // Another channel + team2offtopic, err := th.App.GetChannelByName("off-topic", team2.Id, false) + require.Nil(t, err) + team2townsquare, err := th.App.GetChannelByName("town-square", team2.Id, false) + require.Nil(t, err) + + th.App.AddUserToChannel(user1, team1channel1) + th.App.AddUserToChannel(user1, team1channel2) + th.App.AddUserToChannel(user1, team2channel1) + + addPermission := func(role *model.Role, permission string) *model.AppError { + newPermissions := append(role.Permissions, permission) + _, err := th.App.PatchRole(role, &model.RolePatch{Permissions: &newPermissions}) + return err + } + + removePermission := func(role *model.Role, permission string) *model.AppError { + newPermissions := []string{} + for _, oldPermission := range role.Permissions { + if permission != oldPermission { + newPermissions = append(newPermissions, oldPermission) + } + } + _, err := th.App.PatchRole(role, &model.RolePatch{Permissions: &newPermissions}) + return err + } + + t.Run("VIEW_MEMBERS permission granted at system level", func(t *testing.T) { + restrictions, err := th.App.GetViewUsersRestrictionsForTeam(user1.Id, team1.Id) + require.Nil(t, err) + + assert.Nil(t, restrictions) + }) + + t.Run("VIEW_MEMBERS permission granted at team level", func(t *testing.T) { + systemUserRole, err := th.App.GetRoleByName(model.SYSTEM_USER_ROLE_ID) + require.Nil(t, err) + teamUserRole, err := th.App.GetRoleByName(model.TEAM_USER_ROLE_ID) + require.Nil(t, err) + + require.Nil(t, removePermission(systemUserRole, model.PERMISSION_VIEW_MEMBERS.Id)) + defer addPermission(systemUserRole, model.PERMISSION_VIEW_MEMBERS.Id) + require.Nil(t, addPermission(teamUserRole, model.PERMISSION_VIEW_MEMBERS.Id)) + defer removePermission(teamUserRole, model.PERMISSION_VIEW_MEMBERS.Id) + + restrictions, err := th.App.GetViewUsersRestrictionsForTeam(user1.Id, team1.Id) + require.Nil(t, err) + assert.Nil(t, restrictions) + }) + + t.Run("VIEW_MEMBERS permission not granted at any level", func(t *testing.T) { + systemUserRole, err := th.App.GetRoleByName(model.SYSTEM_USER_ROLE_ID) + require.Nil(t, err) + require.Nil(t, removePermission(systemUserRole, model.PERMISSION_VIEW_MEMBERS.Id)) + defer addPermission(systemUserRole, model.PERMISSION_VIEW_MEMBERS.Id) + + restrictions, err := th.App.GetViewUsersRestrictionsForTeam(user1.Id, team1.Id) + require.Nil(t, err) + + assert.NotNil(t, restrictions) + assert.ElementsMatch(t, []string{team1townsquare.Id, team1offtopic.Id, team1channel1.Id, team1channel2.Id, team2townsquare.Id, team2offtopic.Id, team2channel1.Id}, restrictions) + }) +} diff --git a/app/user_viewmembers_test.go b/app/user_viewmembers_test.go new file mode 100644 index 0000000000..10fe9d021c --- /dev/null +++ b/app/user_viewmembers_test.go @@ -0,0 +1,1088 @@ +package app + +import ( + "testing" + + "github.com/mattermost/mattermost-server/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestResctrictedViewMembers(t *testing.T) { + th := Setup(t) + defer th.TearDown() + + user1 := th.CreateUser() + user1.Nickname = "test user1" + user1.Username = "test-user-1" + th.App.UpdateUser(user1, false) + user2 := th.CreateUser() + user2.Username = "test-user-2" + user2.Nickname = "test user2" + th.App.UpdateUser(user2, false) + user3 := th.CreateUser() + user3.Username = "test-user-3" + user3.Nickname = "test user3" + th.App.UpdateUser(user3, false) + user4 := th.CreateUser() + user4.Username = "test-user-4" + user4.Nickname = "test user4" + th.App.UpdateUser(user4, false) + user5 := th.CreateUser() + user5.Username = "test-user-5" + user5.Nickname = "test user5" + th.App.UpdateUser(user5, false) + + // user1 is member of all the channels and teams because is the creator + th.BasicUser = user1 + + team1 := th.CreateTeam() + team2 := th.CreateTeam() + + channel1 := th.CreateChannel(team1) + channel2 := th.CreateChannel(team1) + channel3 := th.CreateChannel(team2) + + th.LinkUserToTeam(user1, team1) + th.LinkUserToTeam(user2, team1) + th.LinkUserToTeam(user3, team2) + th.LinkUserToTeam(user4, team1) + th.LinkUserToTeam(user4, team2) + + th.AddUserToChannel(user1, channel1) + th.AddUserToChannel(user2, channel2) + th.AddUserToChannel(user3, channel3) + th.AddUserToChannel(user4, channel1) + th.AddUserToChannel(user4, channel3) + + th.App.SetStatusOnline(user1.Id, true) + th.App.SetStatusOnline(user2.Id, true) + th.App.SetStatusOnline(user3.Id, true) + th.App.SetStatusOnline(user4.Id, true) + th.App.SetStatusOnline(user5.Id, true) + + t.Run("SearchUsers", func(t *testing.T) { + testCases := []struct { + Name string + Restrictions *model.ViewUsersRestrictions + Search model.UserSearch + ExpectedResults []string + }{ + { + "without restrictions team1", + nil, + model.UserSearch{Term: "test", TeamId: team1.Id}, + []string{user1.Id, user2.Id, user4.Id}, + }, + { + "without restrictions team2", + nil, + model.UserSearch{Term: "test", TeamId: team2.Id}, + []string{user3.Id, user4.Id}, + }, + { + "with team restrictions with valid team", + &model.ViewUsersRestrictions{ + Teams: []string{team1.Id}, + }, + model.UserSearch{Term: "test", TeamId: team1.Id}, + []string{user1.Id, user2.Id, user4.Id}, + }, + { + "with team restrictions with invalid team", + &model.ViewUsersRestrictions{ + Teams: []string{team1.Id}, + }, + model.UserSearch{Term: "test", TeamId: team2.Id}, + []string{user4.Id}, + }, + { + "with channel restrictions with valid team", + &model.ViewUsersRestrictions{ + Channels: []string{channel1.Id}, + }, + model.UserSearch{Term: "test", TeamId: team1.Id}, + []string{user1.Id, user4.Id}, + }, + { + "with channel restrictions with invalid team", + &model.ViewUsersRestrictions{ + Channels: []string{channel1.Id}, + }, + model.UserSearch{Term: "test", TeamId: team2.Id}, + []string{user4.Id}, + }, + { + "with restricting everything", + &model.ViewUsersRestrictions{ + Channels: []string{}, + Teams: []string{}, + }, + model.UserSearch{Term: "test", TeamId: team1.Id}, + []string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + options := model.UserSearchOptions{Limit: 100, ViewRestrictions: tc.Restrictions} + results, err := th.App.SearchUsers(&tc.Search, &options) + require.Nil(t, err) + ids := []string{} + for _, result := range results { + ids = append(ids, result.Id) + } + assert.ElementsMatch(t, tc.ExpectedResults, ids) + }) + } + }) + + t.Run("SearchUsersInTeam", func(t *testing.T) { + testCases := []struct { + Name string + Restrictions *model.ViewUsersRestrictions + TeamId string + ExpectedResults []string + }{ + { + "without restrictions team1", + nil, + team1.Id, + []string{user1.Id, user2.Id, user4.Id}, + }, + { + "without restrictions team2", + nil, + team2.Id, + []string{user3.Id, user4.Id}, + }, + { + "with team restrictions with valid team", + &model.ViewUsersRestrictions{ + Teams: []string{team1.Id}, + }, + team1.Id, + []string{user1.Id, user2.Id, user4.Id}, + }, + { + "with team restrictions with invalid team", + &model.ViewUsersRestrictions{ + Teams: []string{team1.Id}, + }, + team2.Id, + []string{user4.Id}, + }, + { + "with channel restrictions with valid team", + &model.ViewUsersRestrictions{ + Channels: []string{channel1.Id}, + }, + team1.Id, + []string{user1.Id, user4.Id}, + }, + { + "with channel restrictions with invalid team", + &model.ViewUsersRestrictions{ + Channels: []string{channel1.Id}, + }, + team2.Id, + []string{user4.Id}, + }, + { + "with restricting everything", + &model.ViewUsersRestrictions{ + Channels: []string{}, + Teams: []string{}, + }, + team1.Id, + []string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + options := model.UserSearchOptions{Limit: 100, ViewRestrictions: tc.Restrictions} + results, err := th.App.SearchUsersInTeam(tc.TeamId, "test", &options) + require.Nil(t, err) + ids := []string{} + for _, result := range results { + ids = append(ids, result.Id) + } + assert.ElementsMatch(t, tc.ExpectedResults, ids) + }) + } + }) + + t.Run("AutocompleteUsersInTeam", func(t *testing.T) { + testCases := []struct { + Name string + Restrictions *model.ViewUsersRestrictions + TeamId string + ExpectedResults []string + }{ + { + "without restrictions team1", + nil, + team1.Id, + []string{user1.Id, user2.Id, user4.Id}, + }, + { + "without restrictions team2", + nil, + team2.Id, + []string{user3.Id, user4.Id}, + }, + { + "with team restrictions with valid team", + &model.ViewUsersRestrictions{ + Teams: []string{team1.Id}, + }, + team1.Id, + []string{user1.Id, user2.Id, user4.Id}, + }, + { + "with team restrictions with invalid team", + &model.ViewUsersRestrictions{ + Teams: []string{team1.Id}, + }, + team2.Id, + []string{user4.Id}, + }, + { + "with channel restrictions with valid team", + &model.ViewUsersRestrictions{ + Channels: []string{channel1.Id}, + }, + team1.Id, + []string{user1.Id, user4.Id}, + }, + { + "with channel restrictions with invalid team", + &model.ViewUsersRestrictions{ + Channels: []string{channel1.Id}, + }, + team2.Id, + []string{user4.Id}, + }, + { + "with restricting everything", + &model.ViewUsersRestrictions{ + Channels: []string{}, + Teams: []string{}, + }, + team1.Id, + []string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + options := model.UserSearchOptions{Limit: 100, ViewRestrictions: tc.Restrictions} + results, err := th.App.AutocompleteUsersInTeam(tc.TeamId, "tes", &options) + require.Nil(t, err) + ids := []string{} + for _, result := range results.InTeam { + ids = append(ids, result.Id) + } + assert.ElementsMatch(t, tc.ExpectedResults, ids) + }) + } + }) + + t.Run("AutocompleteUsersInChannel", func(t *testing.T) { + testCases := []struct { + Name string + Restrictions *model.ViewUsersRestrictions + TeamId string + ChannelId string + ExpectedResults []string + }{ + { + "without restrictions channel1", + nil, + team1.Id, + channel1.Id, + []string{user1.Id, user4.Id}, + }, + { + "without restrictions channel3", + nil, + team2.Id, + channel3.Id, + []string{user1.Id, user3.Id, user4.Id}, + }, + { + "with team restrictions with valid team", + &model.ViewUsersRestrictions{ + Teams: []string{team1.Id}, + }, + team1.Id, + channel1.Id, + []string{user1.Id, user4.Id}, + }, + { + "with team restrictions with invalid team", + &model.ViewUsersRestrictions{ + Teams: []string{team1.Id}, + }, + team2.Id, + channel3.Id, + []string{user1.Id, user4.Id}, + }, + { + "with channel restrictions with valid team", + &model.ViewUsersRestrictions{ + Channels: []string{channel1.Id}, + }, + team1.Id, + channel1.Id, + []string{user1.Id, user4.Id}, + }, + { + "with channel restrictions with invalid team", + &model.ViewUsersRestrictions{ + Channels: []string{channel1.Id}, + }, + team2.Id, + channel3.Id, + []string{user1.Id, user4.Id}, + }, + { + "with restricting everything", + &model.ViewUsersRestrictions{ + Channels: []string{}, + Teams: []string{}, + }, + team1.Id, + channel1.Id, + []string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + options := model.UserSearchOptions{Limit: 100, ViewRestrictions: tc.Restrictions} + results, err := th.App.AutocompleteUsersInChannel(tc.TeamId, tc.ChannelId, "tes", &options) + require.Nil(t, err) + ids := []string{} + for _, result := range results.InChannel { + ids = append(ids, result.Id) + } + assert.ElementsMatch(t, tc.ExpectedResults, ids) + }) + } + }) + + t.Run("GetNewUsersForTeam", func(t *testing.T) { + testCases := []struct { + Name string + Restrictions *model.ViewUsersRestrictions + TeamId string + ExpectedResults []string + }{ + { + "without restrictions team1", + nil, + team1.Id, + []string{user2.Id, user4.Id}, + }, + { + "without restrictions team2", + nil, + team2.Id, + []string{user3.Id, user4.Id}, + }, + { + "with team restrictions with valid team", + &model.ViewUsersRestrictions{ + Teams: []string{team1.Id}, + }, + team1.Id, + []string{user2.Id, user4.Id}, + }, + { + "with team restrictions with invalid team", + &model.ViewUsersRestrictions{ + Teams: []string{team1.Id}, + }, + team2.Id, + []string{user4.Id}, + }, + { + "with channel restrictions with valid team", + &model.ViewUsersRestrictions{ + Channels: []string{channel1.Id}, + }, + team1.Id, + []string{user1.Id, user4.Id}, + }, + { + "with channel restrictions with invalid team", + &model.ViewUsersRestrictions{ + Channels: []string{channel1.Id}, + }, + team2.Id, + []string{user4.Id}, + }, + { + "with restricting everything", + &model.ViewUsersRestrictions{ + Channels: []string{}, + Teams: []string{}, + }, + team1.Id, + []string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + results, err := th.App.GetNewUsersForTeamPage(tc.TeamId, 0, 2, false, tc.Restrictions) + require.Nil(t, err) + ids := []string{} + for _, result := range results { + ids = append(ids, result.Id) + } + assert.ElementsMatch(t, tc.ExpectedResults, ids) + }) + } + }) + + t.Run("GetRecentlyActiveUsersForTeamPage", func(t *testing.T) { + testCases := []struct { + Name string + Restrictions *model.ViewUsersRestrictions + TeamId string + ExpectedResults []string + }{ + { + "without restrictions team1", + nil, + team1.Id, + []string{user2.Id, user4.Id}, + }, + { + "without restrictions team2", + nil, + team2.Id, + []string{user3.Id, user4.Id}, + }, + { + "with team restrictions with valid team", + &model.ViewUsersRestrictions{ + Teams: []string{team1.Id}, + }, + team1.Id, + []string{user2.Id, user4.Id}, + }, + { + "with team restrictions with invalid team", + &model.ViewUsersRestrictions{ + Teams: []string{team1.Id}, + }, + team2.Id, + []string{user4.Id}, + }, + { + "with channel restrictions with valid team", + &model.ViewUsersRestrictions{ + Channels: []string{channel1.Id}, + }, + team1.Id, + []string{user1.Id, user4.Id}, + }, + { + "with channel restrictions with invalid team", + &model.ViewUsersRestrictions{ + Channels: []string{channel1.Id}, + }, + team2.Id, + []string{user4.Id}, + }, + { + "with restricting everything", + &model.ViewUsersRestrictions{ + Channels: []string{}, + Teams: []string{}, + }, + team1.Id, + []string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + results, err := th.App.GetRecentlyActiveUsersForTeamPage(tc.TeamId, 0, 2, false, tc.Restrictions) + require.Nil(t, err) + ids := []string{} + for _, result := range results { + ids = append(ids, result.Id) + } + assert.ElementsMatch(t, tc.ExpectedResults, ids) + }) + } + }) + + t.Run("GetUsers", func(t *testing.T) { + testCases := []struct { + Name string + Restrictions *model.ViewUsersRestrictions + ExpectedResults []string + }{ + { + "without restrictions", + nil, + []string{user1.Id, user2.Id, user3.Id, user4.Id, user5.Id}, + }, + { + "with team restrictions", + &model.ViewUsersRestrictions{ + Teams: []string{team1.Id}, + }, + []string{user1.Id, user2.Id, user4.Id}, + }, + { + "with channel restrictions", + &model.ViewUsersRestrictions{ + Channels: []string{channel1.Id}, + }, + []string{user1.Id, user4.Id}, + }, + { + "with restricting everything", + &model.ViewUsersRestrictions{ + Channels: []string{}, + Teams: []string{}, + }, + []string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + options := model.UserGetOptions{Page: 0, PerPage: 100, ViewRestrictions: tc.Restrictions} + results, err := th.App.GetUsers(&options) + require.Nil(t, err) + ids := []string{} + for _, result := range results { + ids = append(ids, result.Id) + } + assert.ElementsMatch(t, tc.ExpectedResults, ids) + }) + } + }) + + t.Run("GetUsersWithoutTeam", func(t *testing.T) { + testCases := []struct { + Name string + Restrictions *model.ViewUsersRestrictions + ExpectedResults []string + }{ + { + "without restrictions", + nil, + []string{user5.Id}, + }, + { + "with team restrictions", + &model.ViewUsersRestrictions{ + Teams: []string{team1.Id}, + }, + []string{}, + }, + { + "with channel restrictions", + &model.ViewUsersRestrictions{ + Channels: []string{channel1.Id}, + }, + []string{}, + }, + { + "with restricting everything", + &model.ViewUsersRestrictions{ + Channels: []string{}, + Teams: []string{}, + }, + []string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + results, err := th.App.GetUsersWithoutTeam(0, 100, tc.Restrictions) + require.Nil(t, err) + ids := []string{} + for _, result := range results { + ids = append(ids, result.Id) + } + assert.ElementsMatch(t, tc.ExpectedResults, ids) + }) + } + }) + + t.Run("GetUsersNotInTeam", func(t *testing.T) { + testCases := []struct { + Name string + Restrictions *model.ViewUsersRestrictions + TeamId string + ExpectedResults []string + }{ + { + "without restrictions team1", + nil, + team1.Id, + []string{user3.Id, user5.Id}, + }, + { + "without restrictions team2", + nil, + team2.Id, + []string{user1.Id, user2.Id, user5.Id}, + }, + { + "with team restrictions with valid team", + &model.ViewUsersRestrictions{ + Teams: []string{team1.Id}, + }, + team2.Id, + []string{user1.Id, user2.Id}, + }, + { + "with team restrictions with invalid team", + &model.ViewUsersRestrictions{ + Teams: []string{team1.Id}, + }, + team1.Id, + []string{}, + }, + { + "with channel restrictions with valid team", + &model.ViewUsersRestrictions{ + Channels: []string{channel1.Id}, + }, + team2.Id, + []string{user1.Id}, + }, + { + "with channel restrictions with invalid team", + &model.ViewUsersRestrictions{ + Channels: []string{channel1.Id}, + }, + team1.Id, + []string{}, + }, + { + "with restricting everything", + &model.ViewUsersRestrictions{ + Channels: []string{}, + Teams: []string{}, + }, + team2.Id, + []string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + results, err := th.App.GetUsersNotInTeam(tc.TeamId, 0, 100, tc.Restrictions) + require.Nil(t, err) + ids := []string{} + for _, result := range results { + ids = append(ids, result.Id) + } + assert.ElementsMatch(t, tc.ExpectedResults, ids) + }) + } + }) + + t.Run("GetUsersNotInChannel", func(t *testing.T) { + testCases := []struct { + Name string + Restrictions *model.ViewUsersRestrictions + TeamId string + ChannelId string + ExpectedResults []string + }{ + { + "without restrictions channel1", + nil, + team1.Id, + channel1.Id, + []string{user2.Id}, + }, + { + "without restrictions channel2", + nil, + team1.Id, + channel2.Id, + []string{user4.Id}, + }, + { + "with team restrictions with valid team", + &model.ViewUsersRestrictions{ + Teams: []string{team1.Id}, + }, + team1.Id, + channel1.Id, + []string{user2.Id}, + }, + { + "with team restrictions with invalid team", + &model.ViewUsersRestrictions{ + Teams: []string{team2.Id}, + }, + team1.Id, + channel1.Id, + []string{}, + }, + { + "with channel restrictions with valid team", + &model.ViewUsersRestrictions{ + Channels: []string{channel2.Id}, + }, + team1.Id, + channel1.Id, + []string{user2.Id}, + }, + { + "with channel restrictions with invalid team", + &model.ViewUsersRestrictions{ + Channels: []string{channel2.Id}, + }, + team1.Id, + channel2.Id, + []string{}, + }, + { + "with restricting everything", + &model.ViewUsersRestrictions{ + Channels: []string{}, + Teams: []string{}, + }, + team1.Id, + channel1.Id, + []string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + results, err := th.App.GetUsersNotInChannel(tc.TeamId, tc.ChannelId, 0, 100, tc.Restrictions) + require.Nil(t, err) + ids := []string{} + for _, result := range results { + ids = append(ids, result.Id) + } + assert.ElementsMatch(t, tc.ExpectedResults, ids) + }) + } + }) + + t.Run("GetUsersByIds", func(t *testing.T) { + testCases := []struct { + Name string + Restrictions *model.ViewUsersRestrictions + UserIds []string + ExpectedResults []string + }{ + { + "without restrictions", + nil, + []string{user1.Id, user2.Id, user3.Id}, + []string{user1.Id, user2.Id, user3.Id}, + }, + { + "with team restrictions", + &model.ViewUsersRestrictions{ + Teams: []string{team1.Id}, + }, + []string{user1.Id, user2.Id, user3.Id}, + []string{user1.Id, user2.Id}, + }, + { + "with channel restrictions", + &model.ViewUsersRestrictions{ + Channels: []string{channel1.Id}, + }, + []string{user1.Id, user2.Id, user3.Id}, + []string{user1.Id}, + }, + { + "with restricting everything", + &model.ViewUsersRestrictions{ + Channels: []string{}, + Teams: []string{}, + }, + []string{user1.Id, user2.Id, user3.Id}, + []string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + results, err := th.App.GetUsersByIds(tc.UserIds, false, tc.Restrictions) + require.Nil(t, err) + ids := []string{} + for _, result := range results { + ids = append(ids, result.Id) + } + assert.ElementsMatch(t, tc.ExpectedResults, ids) + }) + } + }) + + t.Run("GetUsersByUsernames", func(t *testing.T) { + testCases := []struct { + Name string + Restrictions *model.ViewUsersRestrictions + Usernames []string + ExpectedResults []string + }{ + { + "without restrictions", + nil, + []string{user1.Username, user2.Username, user3.Username}, + []string{user1.Id, user2.Id, user3.Id}, + }, + { + "with team restrictions", + &model.ViewUsersRestrictions{ + Teams: []string{team1.Id}, + }, + []string{user1.Username, user2.Username, user3.Username}, + []string{user1.Id, user2.Id}, + }, + { + "with channel restrictions", + &model.ViewUsersRestrictions{ + Channels: []string{channel1.Id}, + }, + []string{user1.Username, user2.Username, user3.Username}, + []string{user1.Id}, + }, + { + "with restricting everything", + &model.ViewUsersRestrictions{ + Channels: []string{}, + Teams: []string{}, + }, + []string{user1.Username, user2.Username, user3.Username}, + []string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + results, err := th.App.GetUsersByUsernames(tc.Usernames, false, tc.Restrictions) + require.Nil(t, err) + ids := []string{} + for _, result := range results { + ids = append(ids, result.Id) + } + assert.ElementsMatch(t, tc.ExpectedResults, ids) + }) + } + }) + + t.Run("GetTotalUsersStats", func(t *testing.T) { + testCases := []struct { + Name string + Restrictions *model.ViewUsersRestrictions + ExpectedResult int64 + }{ + { + "without restrictions", + nil, + 5, + }, + { + "with team restrictions", + &model.ViewUsersRestrictions{ + Teams: []string{team1.Id}, + }, + 3, + }, + { + "with channel restrictions", + &model.ViewUsersRestrictions{ + Channels: []string{channel1.Id}, + }, + 2, + }, + { + "with restricting everything", + &model.ViewUsersRestrictions{ + Channels: []string{}, + Teams: []string{}, + }, + 0, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + result, err := th.App.GetTotalUsersStats(tc.Restrictions) + require.Nil(t, err) + assert.Equal(t, tc.ExpectedResult, result.TotalUsersCount) + }) + } + }) + + t.Run("GetTeamMembers", func(t *testing.T) { + testCases := []struct { + Name string + Restrictions *model.ViewUsersRestrictions + TeamId string + ExpectedResults []string + }{ + { + "without restrictions team1", + nil, + team1.Id, + []string{user1.Id, user2.Id, user4.Id}, + }, + { + "without restrictions team2", + nil, + team2.Id, + []string{user3.Id, user4.Id}, + }, + { + "with team restrictions with valid team", + &model.ViewUsersRestrictions{ + Teams: []string{team1.Id}, + }, + team1.Id, + []string{user1.Id, user2.Id, user4.Id}, + }, + { + "with team restrictions with invalid team", + &model.ViewUsersRestrictions{ + Teams: []string{team1.Id}, + }, + team2.Id, + []string{user4.Id}, + }, + { + "with channel restrictions with valid team", + &model.ViewUsersRestrictions{ + Channels: []string{channel1.Id}, + }, + team1.Id, + []string{user1.Id, user4.Id}, + }, + { + "with channel restrictions with invalid team", + &model.ViewUsersRestrictions{ + Channels: []string{channel1.Id}, + }, + team2.Id, + []string{user4.Id}, + }, + { + "with restricting everything", + &model.ViewUsersRestrictions{ + Channels: []string{}, + Teams: []string{}, + }, + team1.Id, + []string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + results, err := th.App.GetTeamMembers(tc.TeamId, 0, 100, tc.Restrictions) + require.Nil(t, err) + ids := []string{} + for _, result := range results { + ids = append(ids, result.UserId) + } + assert.ElementsMatch(t, tc.ExpectedResults, ids) + }) + } + }) + + t.Run("GetTeamMembersByIds", func(t *testing.T) { + testCases := []struct { + Name string + Restrictions *model.ViewUsersRestrictions + TeamId string + UserIds []string + ExpectedResults []string + }{ + { + "without restrictions team1", + nil, + team1.Id, + []string{user1.Id, user2.Id, user3.Id}, + []string{user1.Id, user2.Id}, + }, + { + "without restrictions team2", + nil, + team2.Id, + []string{user1.Id, user2.Id, user3.Id}, + []string{user3.Id}, + }, + { + "with team restrictions with valid team", + &model.ViewUsersRestrictions{ + Teams: []string{team1.Id}, + }, + team1.Id, + []string{user1.Id, user2.Id, user3.Id}, + []string{user1.Id, user2.Id}, + }, + { + "with team restrictions with invalid team", + &model.ViewUsersRestrictions{ + Teams: []string{team1.Id}, + }, + team2.Id, + []string{user2.Id, user4.Id}, + []string{user4.Id}, + }, + { + "with channel restrictions with valid team", + &model.ViewUsersRestrictions{ + Channels: []string{channel1.Id}, + }, + team1.Id, + []string{user2.Id, user4.Id}, + []string{user4.Id}, + }, + { + "with channel restrictions with invalid team", + &model.ViewUsersRestrictions{ + Channels: []string{channel1.Id}, + }, + team2.Id, + []string{user2.Id, user4.Id}, + []string{user4.Id}, + }, + { + "with restricting everything", + &model.ViewUsersRestrictions{ + Channels: []string{}, + Teams: []string{}, + }, + team1.Id, + []string{user1.Id, user2.Id, user2.Id, user4.Id}, + []string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + results, err := th.App.GetTeamMembersByIds(tc.TeamId, tc.UserIds, tc.Restrictions) + require.Nil(t, err) + ids := []string{} + for _, result := range results { + ids = append(ids, result.UserId) + } + assert.ElementsMatch(t, tc.ExpectedResults, ids) + }) + } + }) +} diff --git a/app/web_hub.go b/app/web_hub.go index 4a594eaa2c..69db15f7f4 100644 --- a/app/web_hub.go +++ b/app/web_hub.go @@ -303,6 +303,19 @@ func (a *App) InvalidateCacheForUser(userId string) { } } +func (a *App) InvalidateCacheForUserTeams(userId string) { + a.InvalidateCacheForUserTeamsSkipClusterSend(userId) + + if a.Cluster != nil { + msg := &model.ClusterMessage{ + Event: model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_USER_TEAMS, + SendType: model.CLUSTER_SEND_BEST_EFFORT, + Data: userId, + } + a.Cluster.SendClusterMessage(msg) + } +} + func (a *App) InvalidateCacheForUserSkipClusterSend(userId string) { a.Srv.Store.Channel().InvalidateAllChannelMembersForUser(userId) a.Srv.Store.User().InvalidateProfilesInChannelCacheByUser(userId) @@ -314,6 +327,15 @@ func (a *App) InvalidateCacheForUserSkipClusterSend(userId string) { } } +func (a *App) InvalidateCacheForUserTeamsSkipClusterSend(userId string) { + a.Srv.Store.Team().InvalidateAllTeamIdsForUser(userId) + + hub := a.GetHubForUserId(userId) + if hub != nil { + hub.InvalidateUser(userId) + } +} + func (a *App) InvalidateCacheForWebhook(webhookId string) { a.InvalidateCacheForWebhookSkipClusterSend(webhookId) diff --git a/einterfaces/elasticsearch.go b/einterfaces/elasticsearch.go index 1926d4b9b5..1e244f273e 100644 --- a/einterfaces/elasticsearch.go +++ b/einterfaces/elasticsearch.go @@ -19,8 +19,8 @@ type ElasticsearchInterface interface { SearchChannels(teamId, term string) ([]string, *model.AppError) DeleteChannel(channel *model.Channel) *model.AppError IndexUser(user *model.User, teamsIds, channelsIds []string) *model.AppError - SearchUsersInChannel(teamId, channelId, term string, options *model.UserSearchOptions) ([]string, []string, *model.AppError) - SearchUsersInTeam(teamId, term string, options *model.UserSearchOptions) ([]string, *model.AppError) + SearchUsersInChannel(teamId, channelId string, restrictedToChannels []string, term string, options *model.UserSearchOptions) ([]string, []string, *model.AppError) + SearchUsersInTeam(teamId string, restrictedToChannels []string, term string, options *model.UserSearchOptions) ([]string, *model.AppError) DeleteUser(user *model.User) *model.AppError TestConfig(cfg *model.Config) *model.AppError PurgeIndexes() *model.AppError diff --git a/i18n/en.json b/i18n/en.json index 0454047513..4a1342984c 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -2298,10 +2298,6 @@ "id": "api.user.get_authorization_code.unsupported.app_error", "translation": "Unsupported OAuth service provider" }, - { - "id": "api.user.get_profile_image.not_found.app_error", - "translation": "Unable to get profile image, user not found." - }, { "id": "api.user.get_user_by_email.permissions.app_error", "translation": "Unable to get user by email." @@ -5586,6 +5582,10 @@ "id": "store.sql_channel.update_member.app_error", "translation": "We encountered an error updating the channel member" }, + { + "id": "store.sql_channel.user_belongs_to_channels.app_error", + "translation": "Unable to determine if the user belongs to a list of channels" + }, { "id": "store.sql_channel_member_history.get_users_in_channel_during.app_error", "translation": "Failed to get users in channel during specified time period" @@ -6450,6 +6450,10 @@ "id": "store.sql_team.get_unread.app_error", "translation": "Unable to get the teams unread messages" }, + { + "id": "store.sql_team.get_user_team_ids.app_error", + "translation": "Unable to get the list of teams of a user" + }, { "id": "store.sql_team.migrate_team_members.commit_transaction.app_error", "translation": "Failed to commit the database transaction" @@ -6534,6 +6538,10 @@ "id": "store.sql_team.update_last_team_icon_update.app_error", "translation": "Unable to update the date of the last team icon update" }, + { + "id": "store.sql_team.user_belongs_to_teams.app_error", + "translation": "Unable to determine if the user belongs to a list of teams" + }, { "id": "store.sql_terms_of_service.save.app_error", "translation": "Unable to save terms of service." diff --git a/model/cluster_message.go b/model/cluster_message.go index c175bf0fcd..64855218d3 100644 --- a/model/cluster_message.go +++ b/model/cluster_message.go @@ -20,6 +20,7 @@ const ( CLUSTER_EVENT_INVALIDATE_CACHE_FOR_CHANNEL_BY_NAME = "inv_channel_name" CLUSTER_EVENT_INVALIDATE_CACHE_FOR_CHANNEL = "inv_channel" CLUSTER_EVENT_INVALIDATE_CACHE_FOR_USER = "inv_user" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_USER_TEAMS = "inv_user_teams" CLUSTER_EVENT_CLEAR_SESSION_CACHE_FOR_USER = "clear_session_user" CLUSTER_EVENT_INVALIDATE_CACHE_FOR_ROLES = "inv_roles" CLUSTER_EVENT_INVALIDATE_CACHE_FOR_SCHEMES = "inv_schemes" diff --git a/model/permission.go b/model/permission.go index aa1f721faa..ffa9ace12f 100644 --- a/model/permission.go +++ b/model/permission.go @@ -85,6 +85,7 @@ var PERMISSION_READ_BOTS *Permission var PERMISSION_READ_OTHERS_BOTS *Permission var PERMISSION_MANAGE_BOTS *Permission var PERMISSION_MANAGE_OTHERS_BOTS *Permission +var PERMISSION_VIEW_MEMBERS *Permission // General permission that encompasses all system admin functions // in the future this could be broken up to allow access to some @@ -519,6 +520,12 @@ func initializePermissions() { "authentication.permisssions.manage_jobs.description", PERMISSION_SCOPE_SYSTEM, } + PERMISSION_VIEW_MEMBERS = &Permission{ + "view_members", + "authentication.permisssions.view_members.name", + "authentication.permisssions.view_members.description", + PERMISSION_SCOPE_TEAM, + } ALL_PERMISSIONS = []*Permission{ PERMISSION_INVITE_USER, @@ -591,6 +598,7 @@ func initializePermissions() { PERMISSION_MANAGE_BOTS, PERMISSION_MANAGE_OTHERS_BOTS, PERMISSION_MANAGE_SYSTEM, + PERMISSION_VIEW_MEMBERS, } } diff --git a/model/role.go b/model/role.go index 4985946028..eb8ad5c84f 100644 --- a/model/role.go +++ b/model/role.go @@ -268,6 +268,7 @@ func MakeDefaultRoles() map[string]*Role { PERMISSION_JOIN_PUBLIC_TEAMS.Id, PERMISSION_CREATE_DIRECT_CHANNEL.Id, PERMISSION_CREATE_GROUP_CHANNEL.Id, + PERMISSION_VIEW_MEMBERS.Id, }, SchemeManaged: true, BuiltIn: true, @@ -357,6 +358,7 @@ func MakeDefaultRoles() map[string]*Role { PERMISSION_REMOVE_OTHERS_REACTIONS.Id, PERMISSION_LIST_PRIVATE_TEAMS.Id, PERMISSION_JOIN_PRIVATE_TEAMS.Id, + PERMISSION_VIEW_MEMBERS.Id, }, roles[TEAM_USER_ROLE_ID].Permissions..., ), diff --git a/model/user.go b/model/user.go index b3ad63e349..83eef18ee5 100644 --- a/model/user.go +++ b/model/user.go @@ -4,6 +4,7 @@ package model import ( + "crypto/sha256" "encoding/json" "fmt" "io" @@ -118,6 +119,22 @@ type UserForIndexing struct { ChannelsIds []string `json:"channel_id"` } +type ViewUsersRestrictions struct { + Teams []string + Channels []string +} + +func (r *ViewUsersRestrictions) Hash() string { + if r == nil { + return "" + } + ids := append(r.Teams, r.Channels...) + sort.Strings(ids) + hash := sha256.New() + hash.Write([]byte(strings.Join(ids, ""))) + return fmt.Sprintf("%x", hash.Sum(nil)) +} + type UserSlice []*User func (u UserSlice) Usernames() []string { diff --git a/model/user_count.go b/model/user_count.go index cfe4f6e3f5..03dbc95e3f 100644 --- a/model/user_count.go +++ b/model/user_count.go @@ -13,4 +13,6 @@ type UserCountOptions struct { ExcludeRegularUsers bool // Only include users on a specific team. "" for any team. TeamId string + // Restrict to search in a list of teams and channels + ViewRestrictions *ViewUsersRestrictions } diff --git a/model/user_get.go b/model/user_get.go index 7ec604f721..a291140e4f 100644 --- a/model/user_get.go +++ b/model/user_get.go @@ -20,6 +20,8 @@ type UserGetOptions struct { Role string // Sorting option Sort string + // Restrict to search in a list of teams and channels + ViewRestrictions *ViewUsersRestrictions // Page Page int // Page size diff --git a/model/user_search.go b/model/user_search.go index f8da5aeeef..f6ca6c247e 100644 --- a/model/user_search.go +++ b/model/user_search.go @@ -57,4 +57,6 @@ type UserSearchOptions struct { Limit int // Filters for the given role Role string + // Restrict to search in a list of teams and channels + ViewRestrictions *ViewUsersRestrictions } diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index 82a908262c..bf67e96c37 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -2604,3 +2604,27 @@ func (s SqlChannelStore) GetChannelsBatchForIndexing(startTime, endTime int64, l result.Data = channels }) } + +func (s SqlChannelStore) UserBelongsToChannels(userId string, channelIds []string) store.StoreChannel { + return store.Do(func(result *store.StoreResult) { + query := s.getQueryBuilder(). + Select("Count(*)"). + From("ChannelMembers"). + Where(sq.And{ + sq.Eq{"UserId": userId}, + sq.Eq{"ChannelId": channelIds}, + }) + + queryString, args, err := query.ToSql() + if err != nil { + result.Err = model.NewAppError("SqlChannelStore.UserBelongsToChannels", "store.sql_channel.user_belongs_to_channels.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + c, err := s.GetReplica().SelectInt(queryString, args...) + if err != nil { + result.Err = model.NewAppError("SqlChannelStore.UserBelongsToChannels", "store.sql_channel.user_belongs_to_channels.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + result.Data = c > 0 + }) +} diff --git a/store/sqlstore/supplier.go b/store/sqlstore/supplier.go index 50fef8bd01..35b576dd7d 100644 --- a/store/sqlstore/supplier.go +++ b/store/sqlstore/supplier.go @@ -124,7 +124,7 @@ func NewSqlSupplier(settings model.SqlSettings, metrics einterfaces.MetricsInter supplier.initConnection() - supplier.oldStores.team = NewSqlTeamStore(supplier) + supplier.oldStores.team = NewSqlTeamStore(supplier, metrics) supplier.oldStores.channel = NewSqlChannelStore(supplier, metrics) supplier.oldStores.post = NewSqlPostStore(supplier, metrics) supplier.oldStores.user = NewSqlUserStore(supplier, metrics) diff --git a/store/sqlstore/team_store.go b/store/sqlstore/team_store.go index a7a5f96a69..19146e6fe3 100644 --- a/store/sqlstore/team_store.go +++ b/store/sqlstore/team_store.go @@ -5,21 +5,28 @@ package sqlstore import ( "database/sql" + "fmt" "net/http" "strconv" "strings" + sq "github.com/Masterminds/squirrel" "github.com/mattermost/gorp" + "github.com/mattermost/mattermost-server/einterfaces" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/store" + "github.com/mattermost/mattermost-server/utils" ) const ( - TEAM_MEMBER_EXISTS_ERROR = "store.sql_team.save_member.exists.app_error" + TEAM_MEMBER_EXISTS_ERROR = "store.sql_team.save_member.exists.app_error" + ALL_TEAM_IDS_FOR_USER_CACHE_SIZE = model.SESSION_CACHE_SIZE + ALL_TEAM_IDS_FOR_USER_CACHE_SEC = 1800 // 30 mins ) type SqlTeamStore struct { SqlStore + metrics einterfaces.MetricsInterface } type teamMember struct { @@ -132,8 +139,11 @@ func (db teamMemberWithSchemeRolesList) ToModel() []*model.TeamMember { return tms } -func NewSqlTeamStore(sqlStore SqlStore) store.TeamStore { - s := &SqlTeamStore{sqlStore} +func NewSqlTeamStore(sqlStore SqlStore, metrics einterfaces.MetricsInterface) store.TeamStore { + s := &SqlTeamStore{ + sqlStore, + metrics, + } for _, db := range sqlStore.GetAllConns() { table := db.AddTableWithName(model.Team{}, "Teams").SetKeys(false, "Id") @@ -224,6 +234,11 @@ func (s SqlTeamStore) Update(team *model.Team) (*model.Team, *model.AppError) { return nil, model.NewAppError("SqlTeamStore.Update", "store.sql_team.update.app_error", nil, "id="+team.Id, http.StatusInternalServerError) } + if oldTeam.DeleteAt == 0 && team.DeleteAt != 0 { + // Invalidate this cache after any team deletion + allTeamIdsForUserCache.Purge() + } + return team, nil } @@ -461,21 +476,21 @@ func (s SqlTeamStore) AnalyticsTeamCount() store.StoreChannel { }) } -var TEAM_MEMBERS_WITH_SCHEME_SELECT_QUERY = ` - SELECT - TeamMembers.*, - TeamScheme.DefaultTeamUserRole TeamSchemeDefaultUserRole, - TeamScheme.DefaultTeamAdminRole TeamSchemeDefaultAdminRole - FROM - TeamMembers - LEFT JOIN - Teams ON TeamMembers.TeamId = Teams.Id - LEFT JOIN - Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id -` +func (s SqlTeamStore) getTeamMembersWithSchemeSelectQuery() sq.SelectBuilder { + return s.getQueryBuilder(). + Select( + "TeamMembers.*", + "TeamScheme.DefaultTeamUserRole TeamSchemeDefaultUserRole", + "TeamScheme.DefaultTeamAdminRole TeamSchemeDefaultAdminRole", + ). + From("TeamMembers"). + LeftJoin("Teams ON TeamMembers.TeamId = Teams.Id"). + LeftJoin("Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id") +} func (s SqlTeamStore) SaveMember(member *model.TeamMember, maxUsersPerTeam int) store.StoreChannel { return store.Do(func(result *store.StoreResult) { + defer s.InvalidateAllTeamIdsForUser(member.UserId) if result.Err = member.IsValid(); result.Err != nil { return } @@ -517,8 +532,18 @@ func (s SqlTeamStore) SaveMember(member *model.TeamMember, maxUsersPerTeam int) return } + query := s.getTeamMembersWithSchemeSelectQuery(). + Where(sq.Eq{"TeamMembers.TeamId": dbMember.TeamId}). + Where(sq.Eq{"TeamMembers.UserId": dbMember.UserId}) + + queryString, args, err := query.ToSql() + if err != nil { + result.Err = model.NewAppError("SqlTeamStore.SaveMember", "store.sql_team.get_member.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + var retrievedMember teamMemberWithSchemeRoles - if err := s.GetMaster().SelectOne(&retrievedMember, TEAM_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE TeamMembers.TeamId = :TeamId AND TeamMembers.UserId = :UserId", map[string]interface{}{"TeamId": dbMember.TeamId, "UserId": dbMember.UserId}); err != nil { + if err := s.GetMaster().SelectOne(&retrievedMember, queryString, args...); err != nil { if err == sql.ErrNoRows { result.Err = model.NewAppError("SqlTeamStore.SaveMember", "store.sql_team.get_member.missing.app_error", nil, "team_id="+dbMember.TeamId+"user_id="+dbMember.UserId+","+err.Error(), http.StatusNotFound) return @@ -543,8 +568,18 @@ func (s SqlTeamStore) UpdateMember(member *model.TeamMember) store.StoreChannel return } + query := s.getTeamMembersWithSchemeSelectQuery(). + Where(sq.Eq{"TeamMembers.TeamId": member.TeamId}). + Where(sq.Eq{"TeamMembers.UserId": member.UserId}) + + queryString, args, err := query.ToSql() + if err != nil { + result.Err = model.NewAppError("SqlTeamStore.UpdateMember", "store.sql_team.get_member.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + var retrievedMember teamMemberWithSchemeRoles - if err := s.GetMaster().SelectOne(&retrievedMember, TEAM_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE TeamMembers.TeamId = :TeamId AND TeamMembers.UserId = :UserId", map[string]interface{}{"TeamId": member.TeamId, "UserId": member.UserId}); err != nil { + if err := s.GetMaster().SelectOne(&retrievedMember, queryString, args...); err != nil { if err == sql.ErrNoRows { result.Err = model.NewAppError("SqlTeamStore.UpdateMember", "store.sql_team.get_member.missing.app_error", nil, "team_id="+member.TeamId+"user_id="+member.UserId+","+err.Error(), http.StatusNotFound) return @@ -559,8 +594,18 @@ func (s SqlTeamStore) UpdateMember(member *model.TeamMember) store.StoreChannel func (s SqlTeamStore) GetMember(teamId string, userId string) store.StoreChannel { return store.Do(func(result *store.StoreResult) { + query := s.getTeamMembersWithSchemeSelectQuery(). + Where(sq.Eq{"TeamMembers.TeamId": teamId}). + Where(sq.Eq{"TeamMembers.UserId": userId}) + + queryString, args, err := query.ToSql() + if err != nil { + result.Err = model.NewAppError("SqlTeamStore.GetMember", "store.sql_team.get_member.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + var dbMember teamMemberWithSchemeRoles - err := s.GetReplica().SelectOne(&dbMember, TEAM_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE TeamMembers.TeamId = :TeamId AND TeamMembers.UserId = :UserId", map[string]interface{}{"TeamId": teamId, "UserId": userId}) + err = s.GetReplica().SelectOne(&dbMember, queryString, args...) if err != nil { if err == sql.ErrNoRows { result.Err = model.NewAppError("SqlTeamStore.GetMember", "store.sql_team.get_member.missing.app_error", nil, "teamId="+teamId+" userId="+userId+" "+err.Error(), http.StatusNotFound) @@ -573,10 +618,24 @@ func (s SqlTeamStore) GetMember(teamId string, userId string) store.StoreChannel }) } -func (s SqlTeamStore) GetMembers(teamId string, offset int, limit int) store.StoreChannel { +func (s SqlTeamStore) GetMembers(teamId string, offset int, limit int, restrictions *model.ViewUsersRestrictions) store.StoreChannel { return store.Do(func(result *store.StoreResult) { + query := s.getTeamMembersWithSchemeSelectQuery(). + Where(sq.Eq{"TeamMembers.TeamId": teamId}). + Where(sq.Eq{"TeamMembers.DeleteAt": 0}). + Limit(uint64(limit)). + Offset(uint64(offset)) + + query = applyTeamMemberViewRestrictionsFilter(query, teamId, restrictions) + + queryString, args, err := query.ToSql() + if err != nil { + result.Err = model.NewAppError("SqlTeamStore.GetMembers", "store.sql_team.get_members.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + var dbMembers teamMemberWithSchemeRolesList - _, err := s.GetReplica().Select(&dbMembers, TEAM_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE TeamMembers.TeamId = :TeamId AND TeamMembers.DeleteAt = 0 LIMIT :Limit OFFSET :Offset", map[string]interface{}{"TeamId": teamId, "Limit": limit, "Offset": offset}) + _, err = s.GetReplica().Select(&dbMembers, queryString, args...) if err != nil { result.Err = model.NewAppError("SqlTeamStore.GetMembers", "store.sql_team.get_members.app_error", nil, "teamId="+teamId+" "+err.Error(), http.StatusInternalServerError) return @@ -629,24 +688,27 @@ func (s SqlTeamStore) GetActiveMemberCount(teamId string) store.StoreChannel { }) } -func (s SqlTeamStore) GetMembersByIds(teamId string, userIds []string) store.StoreChannel { +func (s SqlTeamStore) GetMembersByIds(teamId string, userIds []string, restrictions *model.ViewUsersRestrictions) store.StoreChannel { return store.Do(func(result *store.StoreResult) { - var dbMembers teamMemberWithSchemeRolesList - props := make(map[string]interface{}) - idQuery := "" - - for index, userId := range userIds { - if len(idQuery) > 0 { - idQuery += ", " - } - - props["userId"+strconv.Itoa(index)] = userId - idQuery += ":userId" + strconv.Itoa(index) + if len(userIds) == 0 { + result.Err = model.NewAppError("SqlTeamStore.GetMembersByIds", "store.sql_team.get_members_by_ids.app_error", nil, "Invalid list of user ids", http.StatusInternalServerError) } - props["TeamId"] = teamId + query := s.getTeamMembersWithSchemeSelectQuery(). + Where(sq.Eq{"TeamMembers.TeamId": teamId}). + Where(sq.Eq{"TeamMembers.UserId": userIds}). + Where(sq.Eq{"TeamMembers.DeleteAt": 0}) - if _, err := s.GetReplica().Select(&dbMembers, TEAM_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE TeamMembers.TeamId = :TeamId AND TeamMembers.UserId IN ("+idQuery+") AND TeamMembers.DeleteAt = 0", props); err != nil { + query = applyTeamMemberViewRestrictionsFilter(query, teamId, restrictions) + + queryString, args, err := query.ToSql() + if err != nil { + result.Err = model.NewAppError("SqlTeamStore.GetMembersByIds", "store.sql_team.get_members_by_ids.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + + var dbMembers teamMemberWithSchemeRolesList + if _, err := s.GetReplica().Select(&dbMembers, queryString, args...); err != nil { result.Err = model.NewAppError("SqlTeamStore.GetMembersByIds", "store.sql_team.get_members_by_ids.app_error", nil, "teamId="+teamId+" "+err.Error(), http.StatusInternalServerError) return } @@ -656,8 +718,17 @@ func (s SqlTeamStore) GetMembersByIds(teamId string, userIds []string) store.Sto func (s SqlTeamStore) GetTeamsForUser(userId string) store.StoreChannel { return store.Do(func(result *store.StoreResult) { + query := s.getTeamMembersWithSchemeSelectQuery(). + Where(sq.Eq{"TeamMembers.UserId": userId}) + + queryString, args, err := query.ToSql() + if err != nil { + result.Err = model.NewAppError("SqlTeamStore.GetMembers", "store.sql_team.get_members.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + var dbMembers teamMemberWithSchemeRolesList - _, err := s.GetReplica().Select(&dbMembers, TEAM_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE TeamMembers.UserId = :UserId", map[string]interface{}{"UserId": userId}) + _, err = s.GetReplica().Select(&dbMembers, queryString, args...) if err != nil { result.Err = model.NewAppError("SqlTeamStore.GetMembers", "store.sql_team.get_members.app_error", nil, "userId="+userId+" "+err.Error(), http.StatusInternalServerError) return @@ -669,9 +740,19 @@ func (s SqlTeamStore) GetTeamsForUser(userId string) store.StoreChannel { func (s SqlTeamStore) GetTeamsForUserWithPagination(userId string, page, perPage int) store.StoreChannel { return store.Do(func(result *store.StoreResult) { + query := s.getTeamMembersWithSchemeSelectQuery(). + Where(sq.Eq{"TeamMembers.UserId": userId}). + Limit(uint64(perPage)). + Offset(uint64(page * perPage)) + + queryString, args, err := query.ToSql() + if err != nil { + result.Err = model.NewAppError("SqlTeamStore.GetTeamsForUserWithPagination", "store.sql_team.get_members.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + var dbMembers teamMemberWithSchemeRolesList - offset := page * perPage - _, err := s.GetReplica().Select(&dbMembers, TEAM_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE TeamMembers.UserId = :UserId Limit :Limit Offset :Offset", map[string]interface{}{"UserId": userId, "Limit": perPage, "Offset": offset}) + _, err = s.GetReplica().Select(&dbMembers, queryString, args...) if err != nil { result.Err = model.NewAppError("SqlTeamStore.GetTeamsForUserWithPagination", "store.sql_team.get_members.app_error", nil, "userId="+userId+" "+err.Error(), http.StatusInternalServerError) return @@ -731,7 +812,7 @@ func (s SqlTeamStore) RemoveMember(teamId string, userId string) store.StoreChan return store.Do(func(result *store.StoreResult) { _, err := s.GetMaster().Exec("DELETE FROM TeamMembers WHERE TeamId = :TeamId AND UserId = :UserId", map[string]interface{}{"TeamId": teamId, "UserId": userId}) if err != nil { - result.Err = model.NewAppError("SqlChannelStore.RemoveMember", "store.sql_team.remove_member.app_error", nil, "team_id="+teamId+", user_id="+userId+", "+err.Error(), http.StatusInternalServerError) + result.Err = model.NewAppError("SqlTeamStore.RemoveMember", "store.sql_team.remove_member.app_error", nil, "team_id="+teamId+", user_id="+userId+", "+err.Error(), http.StatusInternalServerError) } }) } @@ -740,7 +821,7 @@ func (s SqlTeamStore) RemoveAllMembersByTeam(teamId string) store.StoreChannel { return store.Do(func(result *store.StoreResult) { _, err := s.GetMaster().Exec("DELETE FROM TeamMembers WHERE TeamId = :TeamId", map[string]interface{}{"TeamId": teamId}) if err != nil { - result.Err = model.NewAppError("SqlChannelStore.RemoveMember", "store.sql_team.remove_member.app_error", nil, "team_id="+teamId+", "+err.Error(), http.StatusInternalServerError) + result.Err = model.NewAppError("SqlTeamStore.RemoveMember", "store.sql_team.remove_member.app_error", nil, "team_id="+teamId+", "+err.Error(), http.StatusInternalServerError) } }) } @@ -749,7 +830,7 @@ func (s SqlTeamStore) RemoveAllMembersByUser(userId string) store.StoreChannel { return store.Do(func(result *store.StoreResult) { _, err := s.GetMaster().Exec("DELETE FROM TeamMembers WHERE UserId = :UserId", map[string]interface{}{"UserId": userId}) if err != nil { - result.Err = model.NewAppError("SqlChannelStore.RemoveMember", "store.sql_team.remove_member.app_error", nil, "user_id="+userId+", "+err.Error(), http.StatusInternalServerError) + result.Err = model.NewAppError("SqlTeamStore.RemoveMember", "store.sql_team.remove_member.app_error", nil, "user_id="+userId+", "+err.Error(), http.StatusInternalServerError) } }) } @@ -849,6 +930,22 @@ func (s SqlTeamStore) ResetAllTeamSchemes() store.StoreChannel { }) } +var allTeamIdsForUserCache = utils.NewLru(ALL_TEAM_IDS_FOR_USER_CACHE_SIZE) + +func (s SqlTeamStore) ClearCaches() { + allTeamIdsForUserCache.Purge() + if s.metrics != nil { + s.metrics.IncrementMemCacheInvalidationCounter("All Team Ids for User - Purge") + } +} + +func (s SqlTeamStore) InvalidateAllTeamIdsForUser(userId string) { + allTeamIdsForUserCache.Remove(userId) + if s.metrics != nil { + s.metrics.IncrementMemCacheInvalidationCounter("All Team Ids for User - Remove by UserId") + } +} + func (s SqlTeamStore) ClearAllCustomRoleAssignments() store.StoreChannel { return store.Do(func(result *store.StoreResult) { builtInRoles := model.MakeDefaultRoles() @@ -944,6 +1041,48 @@ func (s SqlTeamStore) GetAllForExportAfter(limit int, afterId string) store.Stor }) } +func (s SqlTeamStore) GetUserTeamIds(userId string, allowFromCache bool) store.StoreChannel { + return store.Do(func(result *store.StoreResult) { + if allowFromCache { + if cacheItem, ok := allTeamIdsForUserCache.Get(userId); ok { + if s.metrics != nil { + s.metrics.IncrementMemCacheHitCounter("All Team Ids for User") + } + result.Data = cacheItem.([]string) + return + } + } + + if s.metrics != nil { + s.metrics.IncrementMemCacheMissCounter("All Team Ids for User") + } + + var teamIds []string + _, err := s.GetReplica().Select(&teamIds, ` + SELECT + TeamId + FROM + TeamMembers + INNER JOIN + Teams ON TeamMembers.TeamId = Teams.Id + WHERE + TeamMembers.UserId = :UserId + AND TeamMembers.DeleteAt = 0 + AND Teams.DeleteAt = 0`, + map[string]interface{}{"UserId": userId}) + if err != nil { + result.Err = model.NewAppError("SqlTeamStore.GetUserTeamIds", "store.sql_team.get_user_team_ids.app_error", nil, "userId="+userId+" "+err.Error(), http.StatusInternalServerError) + return + } + + result.Data = teamIds + + if allowFromCache { + allTeamIdsForUserCache.AddWithExpiresInSecs(userId, teamIds, ALL_TEAM_IDS_FOR_USER_CACHE_SEC) + } + }) +} + func (s SqlTeamStore) GetTeamMembersForExport(userId string) store.StoreChannel { return store.Do(func(result *store.StoreResult) { var members []*model.TeamMemberForExport @@ -967,3 +1106,56 @@ func (s SqlTeamStore) GetTeamMembersForExport(userId string) store.StoreChannel result.Data = members }) } + +func (s SqlTeamStore) UserBelongsToTeams(userId string, teamIds []string) store.StoreChannel { + return store.Do(func(result *store.StoreResult) { + props := make(map[string]interface{}) + props["UserId"] = userId + idQuery := "" + + for index, teamId := range teamIds { + if len(idQuery) > 0 { + idQuery += ", " + } + + props["teamId"+strconv.Itoa(index)] = teamId + idQuery += ":teamId" + strconv.Itoa(index) + } + c, err := s.GetReplica().SelectInt("SELECT Count(*) FROM TeamMembers WHERE UserId = :UserId AND TeamId IN ("+idQuery+") AND DeleteAt = 0", props) + if err != nil { + result.Err = model.NewAppError("SqlTeamStore.UserBelongsToTeams", "store.sql_team.user_belongs_to_teams.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + result.Data = c > 0 + }) +} + +func applyTeamMemberViewRestrictionsFilter(query sq.SelectBuilder, teamId string, restrictions *model.ViewUsersRestrictions) sq.SelectBuilder { + if restrictions == nil { + return query + } + + // If you have no access to teams or channels, return and empty result. + if restrictions.Teams != nil && len(restrictions.Teams) == 0 && restrictions.Channels != nil && len(restrictions.Channels) == 0 { + return query.Where("1 = 0") + } + + teams := make([]interface{}, len(restrictions.Teams)) + for i, v := range restrictions.Teams { + teams[i] = v + } + channels := make([]interface{}, len(restrictions.Channels)) + for i, v := range restrictions.Channels { + channels[i] = v + } + + resultQuery := query.Join("Users ru ON (TeamMembers.UserId = ru.Id)") + if restrictions.Teams != nil && len(restrictions.Teams) > 0 { + resultQuery = resultQuery.Join(fmt.Sprintf("TeamMembers rtm ON ( rtm.UserId = ru.Id AND rtm.DeleteAt = 0 AND rtm.TeamId IN (%s))", sq.Placeholders(len(teams))), teams...) + } + if restrictions.Channels != nil && len(restrictions.Channels) > 0 { + resultQuery = resultQuery.Join(fmt.Sprintf("ChannelMembers rcm ON ( rcm.UserId = ru.Id AND rcm.ChannelId IN (%s))", sq.Placeholders(len(channels))), channels...) + } + + return resultQuery.Distinct() +} diff --git a/store/sqlstore/user_store.go b/store/sqlstore/user_store.go index f7aec2045d..c713c6fb3e 100644 --- a/store/sqlstore/user_store.go +++ b/store/sqlstore/user_store.go @@ -407,6 +407,8 @@ func (us SqlUserStore) GetAllProfiles(options *model.UserGetOptions) store.Store OrderBy("u.Username ASC"). Offset(uint64(options.Page * options.PerPage)).Limit(uint64(options.PerPage)) + query = applyViewRestrictionsFilter(query, options.ViewRestrictions, true) + query = applyRoleFilter(query, options.Role, isPostgreSQL) if options.Inactive { @@ -466,6 +468,8 @@ func (us SqlUserStore) GetProfiles(options *model.UserGetOptions) store.StoreCha OrderBy("u.Username ASC"). Offset(uint64(options.Page * options.PerPage)).Limit(uint64(options.PerPage)) + query = applyViewRestrictionsFilter(query, options.ViewRestrictions, true) + query = applyRoleFilter(query, options.Role, isPostgreSQL) if options.Inactive { @@ -632,7 +636,7 @@ func (us SqlUserStore) GetAllProfilesInChannel(channelId string, allowFromCache }) } -func (us SqlUserStore) GetProfilesNotInChannel(teamId string, channelId string, offset int, limit int) store.StoreChannel { +func (us SqlUserStore) GetProfilesNotInChannel(teamId string, channelId string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) store.StoreChannel { return store.Do(func(result *store.StoreResult) { query := us.usersQuery. Join("TeamMembers tm ON ( tm.UserId = u.Id AND tm.DeleteAt = 0 AND tm.TeamId = ? )", teamId). @@ -641,6 +645,8 @@ func (us SqlUserStore) GetProfilesNotInChannel(teamId string, channelId string, OrderBy("u.Username ASC"). Offset(uint64(offset)).Limit(uint64(limit)) + query = applyViewRestrictionsFilter(query, viewRestrictions, true) + queryString, args, err := query.ToSql() if err != nil { result.Err = model.NewAppError("SqlUserStore.GetProfilesNotInChannel", "store.sql_user.app_error", nil, err.Error(), http.StatusInternalServerError) @@ -661,7 +667,7 @@ func (us SqlUserStore) GetProfilesNotInChannel(teamId string, channelId string, }) } -func (us SqlUserStore) GetProfilesWithoutTeam(offset int, limit int) store.StoreChannel { +func (us SqlUserStore) GetProfilesWithoutTeam(offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) store.StoreChannel { return store.Do(func(result *store.StoreResult) { query := us.usersQuery. Where(`( @@ -676,6 +682,8 @@ func (us SqlUserStore) GetProfilesWithoutTeam(offset int, limit int) store.Store OrderBy("u.Username ASC"). Offset(uint64(offset)).Limit(uint64(limit)) + query = applyViewRestrictionsFilter(query, viewRestrictions, true) + queryString, args, err := query.ToSql() if err != nil { result.Err = model.NewAppError("SqlUserStore.GetProfilesWithoutTeam", "store.sql_user.app_error", nil, err.Error(), http.StatusInternalServerError) @@ -696,13 +704,11 @@ func (us SqlUserStore) GetProfilesWithoutTeam(offset int, limit int) store.Store }) } -func (us SqlUserStore) GetProfilesByUsernames(usernames []string, teamId string) store.StoreChannel { +func (us SqlUserStore) GetProfilesByUsernames(usernames []string, viewRestrictions *model.ViewUsersRestrictions) store.StoreChannel { return store.Do(func(result *store.StoreResult) { query := us.usersQuery - if teamId != "" { - query = query.Join("TeamMembers tm ON (tm.UserId = u.Id AND tm.TeamId = ?)", teamId) - } + query = applyViewRestrictionsFilter(query, viewRestrictions, true) query = query. Where(map[string]interface{}{ @@ -731,7 +737,7 @@ type UserWithLastActivityAt struct { LastActivityAt int64 } -func (us SqlUserStore) GetRecentlyActiveUsersForTeam(teamId string, offset, limit int) store.StoreChannel { +func (us SqlUserStore) GetRecentlyActiveUsersForTeam(teamId string, offset, limit int, viewRestrictions *model.ViewUsersRestrictions) store.StoreChannel { return store.Do(func(result *store.StoreResult) { query := us.usersQuery. Column("s.LastActivityAt"). @@ -741,6 +747,8 @@ func (us SqlUserStore) GetRecentlyActiveUsersForTeam(teamId string, offset, limi OrderBy("u.Username ASC"). Offset(uint64(offset)).Limit(uint64(limit)) + query = applyViewRestrictionsFilter(query, viewRestrictions, true) + queryString, args, err := query.ToSql() if err != nil { result.Err = model.NewAppError("SqlUserStore.GetRecentlyActiveUsers", "store.sql_user.app_error", nil, err.Error(), http.StatusInternalServerError) @@ -766,7 +774,7 @@ func (us SqlUserStore) GetRecentlyActiveUsersForTeam(teamId string, offset, limi }) } -func (us SqlUserStore) GetNewUsersForTeam(teamId string, offset, limit int) store.StoreChannel { +func (us SqlUserStore) GetNewUsersForTeam(teamId string, offset, limit int, viewRestrictions *model.ViewUsersRestrictions) store.StoreChannel { return store.Do(func(result *store.StoreResult) { query := us.usersQuery. Join("TeamMembers tm ON (tm.UserId = u.Id AND tm.TeamId = ?)", teamId). @@ -774,6 +782,8 @@ func (us SqlUserStore) GetNewUsersForTeam(teamId string, offset, limit int) stor OrderBy("u.Username ASC"). Offset(uint64(offset)).Limit(uint64(limit)) + query = applyViewRestrictionsFilter(query, viewRestrictions, true) + queryString, args, err := query.ToSql() if err != nil { result.Err = model.NewAppError("SqlUserStore.GetNewUsersForTeam", "store.sql_user.app_error", nil, err.Error(), http.StatusInternalServerError) @@ -794,7 +804,7 @@ func (us SqlUserStore) GetNewUsersForTeam(teamId string, offset, limit int) stor }) } -func (us SqlUserStore) GetProfileByIds(userIds []string, allowFromCache bool) store.StoreChannel { +func (us SqlUserStore) GetProfileByIds(userIds []string, allowFromCache bool, viewRestrictions *model.ViewUsersRestrictions) store.StoreChannel { return store.Do(func(result *store.StoreResult) { users := []*model.User{} remainingUserIds := make([]string, 0) @@ -832,6 +842,8 @@ func (us SqlUserStore) GetProfileByIds(userIds []string, allowFromCache bool) st }). OrderBy("u.Username ASC") + query = applyViewRestrictionsFilter(query, viewRestrictions, true) + queryString, args, err := query.ToSql() if err != nil { result.Err = model.NewAppError("SqlUserStore.GetProfileByIds", "store.sql_user.app_error", nil, err.Error(), http.StatusInternalServerError) @@ -1039,18 +1051,18 @@ func (us SqlUserStore) PermanentDelete(userId string) store.StoreChannel { func (us SqlUserStore) Count(options model.UserCountOptions) store.StoreChannel { return store.Do(func(result *store.StoreResult) { - query := sq.Select("COUNT(Users.Id)").From("Users") + query := sq.Select("COUNT(DISTINCT u.Id)").From("Users AS u") if !options.IncludeDeleted { - query = query.Where("Users.DeleteAt = 0") + query = query.Where("u.DeleteAt = 0") } if options.IncludeBotAccounts { if options.ExcludeRegularUsers { - query = query.Join("Bots ON Users.Id = Bots.UserId") + query = query.Join("Bots ON u.Id = Bots.UserId") } } else { - query = query.LeftJoin("Bots ON Users.Id = Bots.UserId").Where("Bots.UserId IS NULL") + query = query.LeftJoin("Bots ON u.Id = Bots.UserId").Where("Bots.UserId IS NULL") if options.ExcludeRegularUsers { // Currenty this doesn't make sense because it will always return 0 result.Err = model.NewAppError("SqlUserStore.Count", "store.sql_user.count.app_error", nil, "", http.StatusInternalServerError) @@ -1059,8 +1071,9 @@ func (us SqlUserStore) Count(options model.UserCountOptions) store.StoreChannel } if options.TeamId != "" { - query = query.LeftJoin("TeamMembers ON Users.Id = TeamMembers.UserId").Where("TeamMembers.TeamId = ? AND TeamMembers.DeleteAt = 0", options.TeamId) + query = query.LeftJoin("TeamMembers AS tm ON u.Id = tm.UserId").Where("tm.TeamId = ? AND tm.DeleteAt = 0", options.TeamId) } + query = applyViewRestrictionsFilter(query, options.ViewRestrictions, false) if us.DriverName() == model.DATABASE_DRIVER_POSTGRES { query = query.PlaceholderFormat(sq.Dollar) @@ -1285,6 +1298,8 @@ func (us SqlUserStore) performSearch(query sq.SelectBuilder, term string, option query = generateSearchQuery(query, strings.Fields(term), searchType, isPostgreSQL) } + query = applyViewRestrictionsFilter(query, options.ViewRestrictions, true) + queryString, args, err := query.ToSql() if err != nil { result.Err = model.NewAppError("SqlUserStore.Search", "store.sql_user.app_error", nil, err.Error(), http.StatusInternalServerError) @@ -1326,7 +1341,7 @@ func (us SqlUserStore) AnalyticsGetSystemAdminCount() store.StoreChannel { }) } -func (us SqlUserStore) GetProfilesNotInTeam(teamId string, offset int, limit int) store.StoreChannel { +func (us SqlUserStore) GetProfilesNotInTeam(teamId string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) store.StoreChannel { return store.Do(func(result *store.StoreResult) { query := us.usersQuery. LeftJoin("TeamMembers tm ON ( tm.UserId = u.Id AND tm.DeleteAt = 0 AND tm.TeamId = ? )", teamId). @@ -1334,6 +1349,8 @@ func (us SqlUserStore) GetProfilesNotInTeam(teamId string, offset int, limit int OrderBy("u.Username ASC"). Offset(uint64(offset)).Limit(uint64(limit)) + query = applyViewRestrictionsFilter(query, viewRestrictions, true) + queryString, args, err := query.ToSql() if err != nil { result.Err = model.NewAppError("SqlUserStore.GetProfilesNotInTeam", "store.sql_user.app_error", nil, err.Error(), http.StatusInternalServerError) @@ -1611,3 +1628,36 @@ func (us SqlUserStore) GetChannelGroupUsers(channelID string) store.StoreChannel result.Data = users }) } + +func applyViewRestrictionsFilter(query sq.SelectBuilder, restrictions *model.ViewUsersRestrictions, distinct bool) sq.SelectBuilder { + if restrictions == nil { + return query + } + + // If you have no access to teams or channels, return and empty result. + if restrictions.Teams != nil && len(restrictions.Teams) == 0 && restrictions.Channels != nil && len(restrictions.Channels) == 0 { + return query.Where("1 = 0") + } + + teams := make([]interface{}, len(restrictions.Teams)) + for i, v := range restrictions.Teams { + teams[i] = v + } + channels := make([]interface{}, len(restrictions.Channels)) + for i, v := range restrictions.Channels { + channels[i] = v + } + resultQuery := query + if restrictions.Teams != nil && len(restrictions.Teams) > 0 { + resultQuery = resultQuery.Join(fmt.Sprintf("TeamMembers rtm ON ( rtm.UserId = u.Id AND rtm.DeleteAt = 0 AND rtm.TeamId IN (%s))", sq.Placeholders(len(teams))), teams...) + } + if restrictions.Channels != nil && len(restrictions.Channels) > 0 { + resultQuery = resultQuery.Join(fmt.Sprintf("ChannelMembers rcm ON ( rcm.UserId = u.Id AND rcm.ChannelId IN (%s))", sq.Placeholders(len(channels))), channels...) + } + + if distinct { + return resultQuery.Distinct() + } + + return resultQuery +} diff --git a/store/store.go b/store/store.go index 9af90e32e3..e7399ee214 100644 --- a/store/store.go +++ b/store/store.go @@ -103,8 +103,8 @@ type TeamStore interface { SaveMember(member *model.TeamMember, maxUsersPerTeam int) StoreChannel UpdateMember(member *model.TeamMember) StoreChannel GetMember(teamId string, userId string) StoreChannel - GetMembers(teamId string, offset int, limit int) StoreChannel - GetMembersByIds(teamId string, userIds []string) StoreChannel + GetMembers(teamId string, offset int, limit int, restrictions *model.ViewUsersRestrictions) StoreChannel + GetMembersByIds(teamId string, userIds []string, restrictions *model.ViewUsersRestrictions) StoreChannel GetTotalMemberCount(teamId string) StoreChannel GetActiveMemberCount(teamId string) StoreChannel GetTeamsForUser(userId string) StoreChannel @@ -122,6 +122,10 @@ type TeamStore interface { AnalyticsGetTeamCountForScheme(schemeId string) StoreChannel GetAllForExportAfter(limit int, afterId string) StoreChannel GetTeamMembersForExport(userId string) StoreChannel + UserBelongsToTeams(userId string, teamIds []string) StoreChannel + GetUserTeamIds(userId string, allowFromCache bool) StoreChannel + InvalidateAllTeamIdsForUser(userId string) + ClearCaches() } type ChannelStore interface { @@ -195,6 +199,7 @@ type ChannelStore interface { GetChannelMembersForExport(userId string, teamId string) StoreChannel RemoveAllDeactivatedMembers(channelId string) StoreChannel GetChannelsBatchForIndexing(startTime, endTime int64, limit int) StoreChannel + UserBelongsToChannels(userId string, channelIds []string) StoreChannel } type ChannelMemberHistoryStore interface { @@ -256,12 +261,12 @@ type UserStore interface { GetProfilesInChannel(channelId string, offset int, limit int) StoreChannel GetProfilesInChannelByStatus(channelId string, offset int, limit int) StoreChannel GetAllProfilesInChannel(channelId string, allowFromCache bool) StoreChannel - GetProfilesNotInChannel(teamId string, channelId string, offset int, limit int) StoreChannel - GetProfilesWithoutTeam(offset int, limit int) StoreChannel - GetProfilesByUsernames(usernames []string, teamId string) StoreChannel + GetProfilesNotInChannel(teamId string, channelId string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) StoreChannel + GetProfilesWithoutTeam(offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) StoreChannel + GetProfilesByUsernames(usernames []string, viewRestrictions *model.ViewUsersRestrictions) StoreChannel GetAllProfiles(options *model.UserGetOptions) StoreChannel GetProfiles(options *model.UserGetOptions) StoreChannel - GetProfileByIds(userId []string, allowFromCache bool) StoreChannel + GetProfileByIds(userId []string, allowFromCache bool, viewRestrictions *model.ViewUsersRestrictions) StoreChannel InvalidatProfileCacheForUser(userId string) GetByEmail(email string) StoreChannel GetByAuth(authData *string, authService string) StoreChannel @@ -278,8 +283,8 @@ type UserStore interface { GetUnreadCount(userId string) StoreChannel GetUnreadCountForChannel(userId string, channelId string) StoreChannel GetAnyUnreadPostCountForChannel(userId string, channelId string) StoreChannel - GetRecentlyActiveUsersForTeam(teamId string, offset, limit int) StoreChannel - GetNewUsersForTeam(teamId string, offset, limit int) StoreChannel + GetRecentlyActiveUsersForTeam(teamId string, offset, limit int, viewRestrictions *model.ViewUsersRestrictions) StoreChannel + GetNewUsersForTeam(teamId string, offset, limit int, viewRestrictions *model.ViewUsersRestrictions) StoreChannel Search(teamId string, term string, options *model.UserSearchOptions) StoreChannel SearchNotInTeam(notInTeamId string, term string, options *model.UserSearchOptions) StoreChannel SearchInChannel(channelId string, term string, options *model.UserSearchOptions) StoreChannel @@ -287,7 +292,7 @@ type UserStore interface { SearchWithoutTeam(term string, options *model.UserSearchOptions) StoreChannel AnalyticsGetInactiveUsersCount() StoreChannel AnalyticsGetSystemAdminCount() StoreChannel - GetProfilesNotInTeam(teamId string, offset int, limit int) StoreChannel + GetProfilesNotInTeam(teamId string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) StoreChannel GetEtagForProfilesNotInTeam(teamId string) StoreChannel ClearAllCustomRoleAssignments() StoreChannel InferSystemInstallDate() StoreChannel diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go index 539f6c7ef1..375c6e65ab 100644 --- a/store/storetest/mocks/ChannelStore.go +++ b/store/storetest/mocks/ChannelStore.go @@ -1087,3 +1087,19 @@ func (_m *ChannelStore) UpdateMember(member *model.ChannelMember) store.StoreCha return r0 } + +// UserBelongsToChannels provides a mock function with given fields: userId, channelIds +func (_m *ChannelStore) UserBelongsToChannels(userId string, channelIds []string) store.StoreChannel { + ret := _m.Called(userId, channelIds) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func(string, []string) store.StoreChannel); ok { + r0 = rf(userId, channelIds) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} diff --git a/store/storetest/mocks/TeamStore.go b/store/storetest/mocks/TeamStore.go index 66f6066e4b..400311048c 100644 --- a/store/storetest/mocks/TeamStore.go +++ b/store/storetest/mocks/TeamStore.go @@ -61,6 +61,11 @@ func (_m *TeamStore) ClearAllCustomRoleAssignments() store.StoreChannel { return r0 } +// ClearCaches provides a mock function with given fields: +func (_m *TeamStore) ClearCaches() { + _m.Called() +} + // Get provides a mock function with given fields: id func (_m *TeamStore) Get(id string) (*model.Team, *model.AppError) { ret := _m.Called(id) @@ -294,13 +299,13 @@ func (_m *TeamStore) GetMember(teamId string, userId string) store.StoreChannel return r0 } -// GetMembers provides a mock function with given fields: teamId, offset, limit -func (_m *TeamStore) GetMembers(teamId string, offset int, limit int) store.StoreChannel { - ret := _m.Called(teamId, offset, limit) +// GetMembers provides a mock function with given fields: teamId, offset, limit, restrictions +func (_m *TeamStore) GetMembers(teamId string, offset int, limit int, restrictions *model.ViewUsersRestrictions) store.StoreChannel { + ret := _m.Called(teamId, offset, limit, restrictions) var r0 store.StoreChannel - if rf, ok := ret.Get(0).(func(string, int, int) store.StoreChannel); ok { - r0 = rf(teamId, offset, limit) + if rf, ok := ret.Get(0).(func(string, int, int, *model.ViewUsersRestrictions) store.StoreChannel); ok { + r0 = rf(teamId, offset, limit, restrictions) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(store.StoreChannel) @@ -310,13 +315,13 @@ func (_m *TeamStore) GetMembers(teamId string, offset int, limit int) store.Stor return r0 } -// GetMembersByIds provides a mock function with given fields: teamId, userIds -func (_m *TeamStore) GetMembersByIds(teamId string, userIds []string) store.StoreChannel { - ret := _m.Called(teamId, userIds) +// GetMembersByIds provides a mock function with given fields: teamId, userIds, restrictions +func (_m *TeamStore) GetMembersByIds(teamId string, userIds []string, restrictions *model.ViewUsersRestrictions) store.StoreChannel { + ret := _m.Called(teamId, userIds, restrictions) var r0 store.StoreChannel - if rf, ok := ret.Get(0).(func(string, []string) store.StoreChannel); ok { - r0 = rf(teamId, userIds) + if rf, ok := ret.Get(0).(func(string, []string, *model.ViewUsersRestrictions) store.StoreChannel); ok { + r0 = rf(teamId, userIds, restrictions) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(store.StoreChannel) @@ -422,6 +427,27 @@ func (_m *TeamStore) GetTotalMemberCount(teamId string) store.StoreChannel { return r0 } +// GetUserTeamIds provides a mock function with given fields: userId, allowFromCache +func (_m *TeamStore) GetUserTeamIds(userId string, allowFromCache bool) store.StoreChannel { + ret := _m.Called(userId, allowFromCache) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func(string, bool) store.StoreChannel); ok { + r0 = rf(userId, allowFromCache) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} + +// InvalidateAllTeamIdsForUser provides a mock function with given fields: userId +func (_m *TeamStore) InvalidateAllTeamIdsForUser(userId string) { + _m.Called(userId) +} + // MigrateTeamMembers provides a mock function with given fields: fromTeamId, fromUserId func (_m *TeamStore) MigrateTeamMembers(fromTeamId string, fromUserId string) store.StoreChannel { ret := _m.Called(fromTeamId, fromUserId) @@ -686,3 +712,19 @@ func (_m *TeamStore) UpdateMember(member *model.TeamMember) store.StoreChannel { return r0 } + +// UserBelongsToTeams provides a mock function with given fields: userId, teamIds +func (_m *TeamStore) UserBelongsToTeams(userId string, teamIds []string) store.StoreChannel { + ret := _m.Called(userId, teamIds) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func(string, []string) store.StoreChannel); ok { + r0 = rf(userId, teamIds) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} diff --git a/store/storetest/mocks/UserStore.go b/store/storetest/mocks/UserStore.go index 95b8f9323c..e2305ea8c3 100644 --- a/store/storetest/mocks/UserStore.go +++ b/store/storetest/mocks/UserStore.go @@ -347,13 +347,13 @@ func (_m *UserStore) GetForLogin(loginId string, allowSignInWithUsername bool, a return r0 } -// GetNewUsersForTeam provides a mock function with given fields: teamId, offset, limit -func (_m *UserStore) GetNewUsersForTeam(teamId string, offset int, limit int) store.StoreChannel { - ret := _m.Called(teamId, offset, limit) +// GetNewUsersForTeam provides a mock function with given fields: teamId, offset, limit, viewRestrictions +func (_m *UserStore) GetNewUsersForTeam(teamId string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) store.StoreChannel { + ret := _m.Called(teamId, offset, limit, viewRestrictions) var r0 store.StoreChannel - if rf, ok := ret.Get(0).(func(string, int, int) store.StoreChannel); ok { - r0 = rf(teamId, offset, limit) + if rf, ok := ret.Get(0).(func(string, int, int, *model.ViewUsersRestrictions) store.StoreChannel); ok { + r0 = rf(teamId, offset, limit, viewRestrictions) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(store.StoreChannel) @@ -363,13 +363,13 @@ func (_m *UserStore) GetNewUsersForTeam(teamId string, offset int, limit int) st return r0 } -// GetProfileByIds provides a mock function with given fields: userId, allowFromCache -func (_m *UserStore) GetProfileByIds(userId []string, allowFromCache bool) store.StoreChannel { - ret := _m.Called(userId, allowFromCache) +// GetProfileByIds provides a mock function with given fields: userId, allowFromCache, viewRestrictions +func (_m *UserStore) GetProfileByIds(userId []string, allowFromCache bool, viewRestrictions *model.ViewUsersRestrictions) store.StoreChannel { + ret := _m.Called(userId, allowFromCache, viewRestrictions) var r0 store.StoreChannel - if rf, ok := ret.Get(0).(func([]string, bool) store.StoreChannel); ok { - r0 = rf(userId, allowFromCache) + if rf, ok := ret.Get(0).(func([]string, bool, *model.ViewUsersRestrictions) store.StoreChannel); ok { + r0 = rf(userId, allowFromCache, viewRestrictions) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(store.StoreChannel) @@ -395,13 +395,13 @@ func (_m *UserStore) GetProfiles(options *model.UserGetOptions) store.StoreChann return r0 } -// GetProfilesByUsernames provides a mock function with given fields: usernames, teamId -func (_m *UserStore) GetProfilesByUsernames(usernames []string, teamId string) store.StoreChannel { - ret := _m.Called(usernames, teamId) +// GetProfilesByUsernames provides a mock function with given fields: usernames, viewRestrictions +func (_m *UserStore) GetProfilesByUsernames(usernames []string, viewRestrictions *model.ViewUsersRestrictions) store.StoreChannel { + ret := _m.Called(usernames, viewRestrictions) var r0 store.StoreChannel - if rf, ok := ret.Get(0).(func([]string, string) store.StoreChannel); ok { - r0 = rf(usernames, teamId) + if rf, ok := ret.Get(0).(func([]string, *model.ViewUsersRestrictions) store.StoreChannel); ok { + r0 = rf(usernames, viewRestrictions) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(store.StoreChannel) @@ -443,13 +443,13 @@ func (_m *UserStore) GetProfilesInChannelByStatus(channelId string, offset int, return r0 } -// GetProfilesNotInChannel provides a mock function with given fields: teamId, channelId, offset, limit -func (_m *UserStore) GetProfilesNotInChannel(teamId string, channelId string, offset int, limit int) store.StoreChannel { - ret := _m.Called(teamId, channelId, offset, limit) +// GetProfilesNotInChannel provides a mock function with given fields: teamId, channelId, offset, limit, viewRestrictions +func (_m *UserStore) GetProfilesNotInChannel(teamId string, channelId string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) store.StoreChannel { + ret := _m.Called(teamId, channelId, offset, limit, viewRestrictions) var r0 store.StoreChannel - if rf, ok := ret.Get(0).(func(string, string, int, int) store.StoreChannel); ok { - r0 = rf(teamId, channelId, offset, limit) + if rf, ok := ret.Get(0).(func(string, string, int, int, *model.ViewUsersRestrictions) store.StoreChannel); ok { + r0 = rf(teamId, channelId, offset, limit, viewRestrictions) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(store.StoreChannel) @@ -459,13 +459,13 @@ func (_m *UserStore) GetProfilesNotInChannel(teamId string, channelId string, of return r0 } -// GetProfilesNotInTeam provides a mock function with given fields: teamId, offset, limit -func (_m *UserStore) GetProfilesNotInTeam(teamId string, offset int, limit int) store.StoreChannel { - ret := _m.Called(teamId, offset, limit) +// GetProfilesNotInTeam provides a mock function with given fields: teamId, offset, limit, viewRestrictions +func (_m *UserStore) GetProfilesNotInTeam(teamId string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) store.StoreChannel { + ret := _m.Called(teamId, offset, limit, viewRestrictions) var r0 store.StoreChannel - if rf, ok := ret.Get(0).(func(string, int, int) store.StoreChannel); ok { - r0 = rf(teamId, offset, limit) + if rf, ok := ret.Get(0).(func(string, int, int, *model.ViewUsersRestrictions) store.StoreChannel); ok { + r0 = rf(teamId, offset, limit, viewRestrictions) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(store.StoreChannel) @@ -475,13 +475,13 @@ func (_m *UserStore) GetProfilesNotInTeam(teamId string, offset int, limit int) return r0 } -// GetProfilesWithoutTeam provides a mock function with given fields: offset, limit -func (_m *UserStore) GetProfilesWithoutTeam(offset int, limit int) store.StoreChannel { - ret := _m.Called(offset, limit) +// GetProfilesWithoutTeam provides a mock function with given fields: offset, limit, viewRestrictions +func (_m *UserStore) GetProfilesWithoutTeam(offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) store.StoreChannel { + ret := _m.Called(offset, limit, viewRestrictions) var r0 store.StoreChannel - if rf, ok := ret.Get(0).(func(int, int) store.StoreChannel); ok { - r0 = rf(offset, limit) + if rf, ok := ret.Get(0).(func(int, int, *model.ViewUsersRestrictions) store.StoreChannel); ok { + r0 = rf(offset, limit, viewRestrictions) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(store.StoreChannel) @@ -491,13 +491,13 @@ func (_m *UserStore) GetProfilesWithoutTeam(offset int, limit int) store.StoreCh return r0 } -// GetRecentlyActiveUsersForTeam provides a mock function with given fields: teamId, offset, limit -func (_m *UserStore) GetRecentlyActiveUsersForTeam(teamId string, offset int, limit int) store.StoreChannel { - ret := _m.Called(teamId, offset, limit) +// GetRecentlyActiveUsersForTeam provides a mock function with given fields: teamId, offset, limit, viewRestrictions +func (_m *UserStore) GetRecentlyActiveUsersForTeam(teamId string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) store.StoreChannel { + ret := _m.Called(teamId, offset, limit, viewRestrictions) var r0 store.StoreChannel - if rf, ok := ret.Get(0).(func(string, int, int) store.StoreChannel); ok { - r0 = rf(teamId, offset, limit) + if rf, ok := ret.Get(0).(func(string, int, int, *model.ViewUsersRestrictions) store.StoreChannel); ok { + r0 = rf(teamId, offset, limit, viewRestrictions) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(store.StoreChannel) diff --git a/store/storetest/team_store.go b/store/storetest/team_store.go index 2a8914afa2..a9954e7137 100644 --- a/store/storetest/team_store.go +++ b/store/storetest/team_store.go @@ -769,14 +769,14 @@ func testTeamMembers(t *testing.T, ss store.Store) { store.Must(ss.Team().SaveMember(m2, -1)) store.Must(ss.Team().SaveMember(m3, -1)) - if r1 := <-ss.Team().GetMembers(teamId1, 0, 100); r1.Err != nil { + if r1 := <-ss.Team().GetMembers(teamId1, 0, 100, nil); r1.Err != nil { t.Fatal(r1.Err) } else { ms := r1.Data.([]*model.TeamMember) require.Len(t, ms, 2) } - if r1 := <-ss.Team().GetMembers(teamId2, 0, 100); r1.Err != nil { + if r1 := <-ss.Team().GetMembers(teamId2, 0, 100, nil); r1.Err != nil { t.Fatal(r1.Err) } else { ms := r1.Data.([]*model.TeamMember) @@ -798,7 +798,7 @@ func testTeamMembers(t *testing.T, ss store.Store) { t.Fatal(r1.Err) } - if r1 := <-ss.Team().GetMembers(teamId1, 0, 100); r1.Err != nil { + if r1 := <-ss.Team().GetMembers(teamId1, 0, 100, nil); r1.Err != nil { t.Fatal(r1.Err) } else { ms := r1.Data.([]*model.TeamMember) @@ -813,7 +813,7 @@ func testTeamMembers(t *testing.T, ss store.Store) { t.Fatal(r1.Err) } - if r1 := <-ss.Team().GetMembers(teamId1, 0, 100); r1.Err != nil { + if r1 := <-ss.Team().GetMembers(teamId1, 0, 100, nil); r1.Err != nil { t.Fatal(r1.Err) } else { ms := r1.Data.([]*model.TeamMember) @@ -872,7 +872,7 @@ func testTeamMembersWithPagination(t *testing.T, ss store.Store) { r1 = <-ss.Team().RemoveMember(teamId1, m1.UserId) require.Nil(t, r1.Err) - r1 = <-ss.Team().GetMembers(teamId1, 0, 100) + r1 = <-ss.Team().GetMembers(teamId1, 0, 100, nil) require.Nil(t, r1.Err) ms = r1.Data.([]*model.TeamMember) @@ -1077,7 +1077,7 @@ func testGetTeamMembersByIds(t *testing.T, ss store.Store) { m1 := &model.TeamMember{TeamId: teamId1, UserId: model.NewId()} store.Must(ss.Team().SaveMember(m1, -1)) - if r := <-ss.Team().GetMembersByIds(m1.TeamId, []string{m1.UserId}); r.Err != nil { + if r := <-ss.Team().GetMembersByIds(m1.TeamId, []string{m1.UserId}, nil); r.Err != nil { t.Fatal(r.Err) } else { rm1 := r.Data.([]*model.TeamMember)[0] @@ -1094,7 +1094,7 @@ func testGetTeamMembersByIds(t *testing.T, ss store.Store) { m2 := &model.TeamMember{TeamId: teamId1, UserId: model.NewId()} store.Must(ss.Team().SaveMember(m2, -1)) - if r := <-ss.Team().GetMembersByIds(m1.TeamId, []string{m1.UserId, m2.UserId, model.NewId()}); r.Err != nil { + if r := <-ss.Team().GetMembersByIds(m1.TeamId, []string{m1.UserId, m2.UserId, model.NewId()}, nil); r.Err != nil { t.Fatal(r.Err) } else { rm := r.Data.([]*model.TeamMember) @@ -1104,7 +1104,7 @@ func testGetTeamMembersByIds(t *testing.T, ss store.Store) { } } - if r := <-ss.Team().GetMembersByIds(m1.TeamId, []string{}); r.Err == nil { + if r := <-ss.Team().GetMembersByIds(m1.TeamId, []string{}, nil); r.Err == nil { t.Fatal("empty user ids - should have failed") } } diff --git a/store/storetest/user_store.go b/store/storetest/user_store.go index fcc4f3ed56..36244fdd38 100644 --- a/store/storetest/user_store.go +++ b/store/storetest/user_store.go @@ -860,19 +860,19 @@ func testUserStoreGetProfilesWithoutTeam(t *testing.T, ss store.Store) { defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() t.Run("get, offset 0, limit 100", func(t *testing.T) { - result := <-ss.User().GetProfilesWithoutTeam(0, 100) + result := <-ss.User().GetProfilesWithoutTeam(0, 100, nil) require.Nil(t, result.Err) assert.Equal(t, []*model.User{sanitized(u2), sanitized(u3)}, result.Data.([]*model.User)) }) t.Run("get, offset 1, limit 1", func(t *testing.T) { - result := <-ss.User().GetProfilesWithoutTeam(1, 1) + result := <-ss.User().GetProfilesWithoutTeam(1, 1, nil) require.Nil(t, result.Err) assert.Equal(t, []*model.User{sanitized(u3)}, result.Data.([]*model.User)) }) t.Run("get, offset 2, limit 1", func(t *testing.T) { - result := <-ss.User().GetProfilesWithoutTeam(2, 1) + result := <-ss.User().GetProfilesWithoutTeam(2, 1, nil) require.Nil(t, result.Err) assert.Equal(t, []*model.User{}, result.Data.([]*model.User)) }) @@ -1031,7 +1031,7 @@ func testUserStoreGetProfilesNotInChannel(t *testing.T, ss store.Store) { }, -1)).(*model.Channel) t.Run("get team 1, channel 1, offset 0, limit 100", func(t *testing.T) { - result := <-ss.User().GetProfilesNotInChannel(teamId, c1.Id, 0, 100) + result := <-ss.User().GetProfilesNotInChannel(teamId, c1.Id, 0, 100, nil) require.Nil(t, result.Err) assert.Equal(t, []*model.User{ sanitized(u1), @@ -1041,7 +1041,7 @@ func testUserStoreGetProfilesNotInChannel(t *testing.T, ss store.Store) { }) t.Run("get team 1, channel 2, offset 0, limit 100", func(t *testing.T) { - result := <-ss.User().GetProfilesNotInChannel(teamId, c2.Id, 0, 100) + result := <-ss.User().GetProfilesNotInChannel(teamId, c2.Id, 0, 100, nil) require.Nil(t, result.Err) assert.Equal(t, []*model.User{ sanitized(u1), @@ -1075,13 +1075,13 @@ func testUserStoreGetProfilesNotInChannel(t *testing.T, ss store.Store) { })) t.Run("get team 1, channel 1, offset 0, limit 100, after update", func(t *testing.T) { - result := <-ss.User().GetProfilesNotInChannel(teamId, c1.Id, 0, 100) + result := <-ss.User().GetProfilesNotInChannel(teamId, c1.Id, 0, 100, nil) require.Nil(t, result.Err) assert.Equal(t, []*model.User{}, result.Data.([]*model.User)) }) t.Run("get team 1, channel 2, offset 0, limit 100, after update", func(t *testing.T) { - result := <-ss.User().GetProfilesNotInChannel(teamId, c2.Id, 0, 100) + result := <-ss.User().GetProfilesNotInChannel(teamId, c2.Id, 0, 100, nil) require.Nil(t, result.Err) assert.Equal(t, []*model.User{ sanitized(u2), @@ -1122,31 +1122,31 @@ func testUserStoreGetProfilesByIds(t *testing.T, ss store.Store) { defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() t.Run("get u1 by id, no caching", func(t *testing.T) { - result := <-ss.User().GetProfileByIds([]string{u1.Id}, false) + result := <-ss.User().GetProfileByIds([]string{u1.Id}, false, nil) require.Nil(t, result.Err) assert.Equal(t, []*model.User{sanitized(u1)}, result.Data.([]*model.User)) }) t.Run("get u1 by id, caching", func(t *testing.T) { - result := <-ss.User().GetProfileByIds([]string{u1.Id}, true) + result := <-ss.User().GetProfileByIds([]string{u1.Id}, true, nil) require.Nil(t, result.Err) assert.Equal(t, []*model.User{sanitized(u1)}, result.Data.([]*model.User)) }) t.Run("get u1, u2, u3 by id, no caching", func(t *testing.T) { - result := <-ss.User().GetProfileByIds([]string{u1.Id, u2.Id, u3.Id}, false) + result := <-ss.User().GetProfileByIds([]string{u1.Id, u2.Id, u3.Id}, false, nil) require.Nil(t, result.Err) assert.Equal(t, []*model.User{sanitized(u1), sanitized(u2), sanitized(u3)}, result.Data.([]*model.User)) }) t.Run("get u1, u2, u3 by id, caching", func(t *testing.T) { - result := <-ss.User().GetProfileByIds([]string{u1.Id, u2.Id, u3.Id}, true) + result := <-ss.User().GetProfileByIds([]string{u1.Id, u2.Id, u3.Id}, true, nil) require.Nil(t, result.Err) assert.Equal(t, []*model.User{sanitized(u1), sanitized(u2), sanitized(u3)}, result.Data.([]*model.User)) }) t.Run("get unknown id, caching", func(t *testing.T) { - result := <-ss.User().GetProfileByIds([]string{"123"}, true) + result := <-ss.User().GetProfileByIds([]string{"123"}, true, nil) require.Nil(t, result.Err) assert.Equal(t, []*model.User{}, result.Data.([]*model.User)) }) @@ -1185,31 +1185,31 @@ func testUserStoreGetProfilesByUsernames(t *testing.T, ss store.Store) { defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() t.Run("get by u1 and u2 usernames, team id 1", func(t *testing.T) { - result := <-ss.User().GetProfilesByUsernames([]string{u1.Username, u2.Username}, teamId) + result := <-ss.User().GetProfilesByUsernames([]string{u1.Username, u2.Username}, &model.ViewUsersRestrictions{Teams: []string{teamId}}) require.Nil(t, result.Err) assert.Equal(t, []*model.User{u1, u2}, result.Data.([]*model.User)) }) t.Run("get by u1 username, team id 1", func(t *testing.T) { - result := <-ss.User().GetProfilesByUsernames([]string{u1.Username}, teamId) + result := <-ss.User().GetProfilesByUsernames([]string{u1.Username}, &model.ViewUsersRestrictions{Teams: []string{teamId}}) require.Nil(t, result.Err) assert.Equal(t, []*model.User{u1}, result.Data.([]*model.User)) }) t.Run("get by u1 and u3 usernames, no team id", func(t *testing.T) { - result := <-ss.User().GetProfilesByUsernames([]string{u1.Username, u3.Username}, "") + result := <-ss.User().GetProfilesByUsernames([]string{u1.Username, u3.Username}, nil) require.Nil(t, result.Err) assert.Equal(t, []*model.User{u1, u3}, result.Data.([]*model.User)) }) t.Run("get by u1 and u3 usernames, team id 1", func(t *testing.T) { - result := <-ss.User().GetProfilesByUsernames([]string{u1.Username, u3.Username}, teamId) + result := <-ss.User().GetProfilesByUsernames([]string{u1.Username, u3.Username}, &model.ViewUsersRestrictions{Teams: []string{teamId}}) require.Nil(t, result.Err) assert.Equal(t, []*model.User{u1}, result.Data.([]*model.User)) }) t.Run("get by u1 and u3 usernames, team id 2", func(t *testing.T) { - result := <-ss.User().GetProfilesByUsernames([]string{u1.Username, u3.Username}, team2Id) + result := <-ss.User().GetProfilesByUsernames([]string{u1.Username, u3.Username}, &model.ViewUsersRestrictions{Teams: []string{team2Id}}) require.Nil(t, result.Err) assert.Equal(t, []*model.User{u3}, result.Data.([]*model.User)) }) @@ -1778,7 +1778,7 @@ func testUserStoreGetRecentlyActiveUsersForTeam(t *testing.T, ss store.Store) { store.Must(ss.Status().SaveOrUpdate(&model.Status{UserId: u3.Id, Status: model.STATUS_ONLINE, Manual: false, LastActivityAt: u3.LastActivityAt, ActiveChannel: ""})) t.Run("get team 1, offset 0, limit 100", func(t *testing.T) { - result := <-ss.User().GetRecentlyActiveUsersForTeam(teamId, 0, 100) + result := <-ss.User().GetRecentlyActiveUsersForTeam(teamId, 0, 100, nil) require.Nil(t, result.Err) assert.Equal(t, []*model.User{ sanitized(u3), @@ -1788,7 +1788,7 @@ func testUserStoreGetRecentlyActiveUsersForTeam(t *testing.T, ss store.Store) { }) t.Run("get team 1, offset 0, limit 1", func(t *testing.T) { - result := <-ss.User().GetRecentlyActiveUsersForTeam(teamId, 0, 1) + result := <-ss.User().GetRecentlyActiveUsersForTeam(teamId, 0, 1, nil) require.Nil(t, result.Err) assert.Equal(t, []*model.User{ sanitized(u3), @@ -1796,7 +1796,7 @@ func testUserStoreGetRecentlyActiveUsersForTeam(t *testing.T, ss store.Store) { }) t.Run("get team 1, offset 2, limit 1", func(t *testing.T) { - result := <-ss.User().GetRecentlyActiveUsersForTeam(teamId, 2, 1) + result := <-ss.User().GetRecentlyActiveUsersForTeam(teamId, 2, 1, nil) require.Nil(t, result.Err) assert.Equal(t, []*model.User{ sanitized(u2), @@ -1844,7 +1844,7 @@ func testUserStoreGetNewUsersForTeam(t *testing.T, ss store.Store) { store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId2, UserId: u4.Id}, -1)) t.Run("get team 1, offset 0, limit 100", func(t *testing.T) { - result := <-ss.User().GetNewUsersForTeam(teamId, 0, 100) + result := <-ss.User().GetNewUsersForTeam(teamId, 0, 100, nil) require.Nil(t, result.Err) assert.Equal(t, []*model.User{ sanitized(u3), @@ -1854,7 +1854,7 @@ func testUserStoreGetNewUsersForTeam(t *testing.T, ss store.Store) { }) t.Run("get team 1, offset 0, limit 1", func(t *testing.T) { - result := <-ss.User().GetNewUsersForTeam(teamId, 0, 1) + result := <-ss.User().GetNewUsersForTeam(teamId, 0, 1, nil) require.Nil(t, result.Err) assert.Equal(t, []*model.User{ sanitized(u3), @@ -1862,7 +1862,7 @@ func testUserStoreGetNewUsersForTeam(t *testing.T, ss store.Store) { }) t.Run("get team 1, offset 2, limit 1", func(t *testing.T) { - result := <-ss.User().GetNewUsersForTeam(teamId, 2, 1) + result := <-ss.User().GetNewUsersForTeam(teamId, 2, 1, nil) require.Nil(t, result.Err) assert.Equal(t, []*model.User{ sanitized(u1), @@ -1870,7 +1870,7 @@ func testUserStoreGetNewUsersForTeam(t *testing.T, ss store.Store) { }) t.Run("get team 2, offset 0, limit 100", func(t *testing.T) { - result := <-ss.User().GetNewUsersForTeam(teamId2, 0, 100) + result := <-ss.User().GetNewUsersForTeam(teamId2, 0, 100, nil) require.Nil(t, result.Err) assert.Equal(t, []*model.User{ sanitized(u4), @@ -2982,6 +2982,23 @@ func testCount(t *testing.T, ss store.Store) { require.Nil(t, result.Err) require.Equal(t, int64(0), result.Data.(int64)) + result = <-ss.User().Count(model.UserCountOptions{ + IncludeBotAccounts: true, + IncludeDeleted: true, + TeamId: teamId, + ViewRestrictions: &model.ViewUsersRestrictions{Teams: []string{teamId}}, + }) + require.Nil(t, result.Err) + require.Equal(t, int64(1), result.Data.(int64)) + + result = <-ss.User().Count(model.UserCountOptions{ + IncludeBotAccounts: true, + IncludeDeleted: true, + TeamId: teamId, + ViewRestrictions: &model.ViewUsersRestrictions{Teams: []string{model.NewId()}}, + }) + require.Nil(t, result.Err) + require.Equal(t, int64(0), result.Data.(int64)) } func testUserStoreAnalyticsGetInactiveUsersCount(t *testing.T, ss store.Store) { @@ -3097,7 +3114,7 @@ func testUserStoreGetProfilesNotInTeam(t *testing.T, ss store.Store) { }) t.Run("get not in team 1, offset 0, limit 100000", func(t *testing.T) { - result := <-ss.User().GetProfilesNotInTeam(teamId, 0, 100000) + result := <-ss.User().GetProfilesNotInTeam(teamId, 0, 100000, nil) require.Nil(t, result.Err) assert.Equal(t, []*model.User{ sanitized(u2), @@ -3106,7 +3123,7 @@ func testUserStoreGetProfilesNotInTeam(t *testing.T, ss store.Store) { }) t.Run("get not in team 1, offset 1, limit 1", func(t *testing.T) { - result := <-ss.User().GetProfilesNotInTeam(teamId, 1, 1) + result := <-ss.User().GetProfilesNotInTeam(teamId, 1, 1, nil) require.Nil(t, result.Err) assert.Equal(t, []*model.User{ sanitized(u3), @@ -3114,7 +3131,7 @@ func testUserStoreGetProfilesNotInTeam(t *testing.T, ss store.Store) { }) t.Run("get not in team 2, offset 0, limit 100", func(t *testing.T) { - result := <-ss.User().GetProfilesNotInTeam(teamId2, 0, 100) + result := <-ss.User().GetProfilesNotInTeam(teamId2, 0, 100, nil) require.Nil(t, result.Err) assert.Equal(t, []*model.User{ sanitized(u1), @@ -3137,7 +3154,7 @@ func testUserStoreGetProfilesNotInTeam(t *testing.T, ss store.Store) { }) t.Run("get not in team 1, offset 0, limit 100000 after update", func(t *testing.T) { - result := <-ss.User().GetProfilesNotInTeam(teamId, 0, 100000) + result := <-ss.User().GetProfilesNotInTeam(teamId, 0, 100000, nil) require.Nil(t, result.Err) assert.Equal(t, []*model.User{ sanitized(u3), @@ -3161,7 +3178,7 @@ func testUserStoreGetProfilesNotInTeam(t *testing.T, ss store.Store) { }) t.Run("get not in team 1, offset 0, limit 100000 after second update", func(t *testing.T) { - result := <-ss.User().GetProfilesNotInTeam(teamId, 0, 100000) + result := <-ss.User().GetProfilesNotInTeam(teamId, 0, 100000, nil) require.Nil(t, result.Err) assert.Equal(t, []*model.User{ sanitized(u1),