diff options
-rw-r--r-- | api4/user.go | 13 | ||||
-rw-r--r-- | api4/user_test.go | 46 | ||||
-rw-r--r-- | app/user.go | 25 | ||||
-rw-r--r-- | model/client4.go | 11 | ||||
-rw-r--r-- | store/sql_user_store.go | 73 | ||||
-rw-r--r-- | store/sql_user_store_test.go | 134 | ||||
-rw-r--r-- | store/store.go | 2 |
7 files changed, 304 insertions, 0 deletions
diff --git a/api4/user.go b/api4/user.go index 4eb4479c2..298c5cc8d 100644 --- a/api4/user.go +++ b/api4/user.go @@ -266,6 +266,7 @@ func setProfileImage(c *Context, w http.ResponseWriter, r *http.Request) { func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { inTeamId := r.URL.Query().Get("in_team") + notInTeamId := r.URL.Query().Get("not_in_team") inChannelId := r.URL.Query().Get("in_channel") notInChannelId := r.URL.Query().Get("not_in_channel") @@ -285,6 +286,18 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { } profiles, err = app.GetUsersNotInChannelPage(inTeamId, notInChannelId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin()) + } else if len(notInTeamId) > 0 { + if !app.SessionHasPermissionToTeam(c.Session, notInTeamId, model.PERMISSION_VIEW_TEAM) { + c.SetPermissionError(model.PERMISSION_VIEW_TEAM) + return + } + + etag = app.GetUsersNotInTeamEtag(inTeamId) + if HandleEtag(etag, "Get Users Not in Team", w, r) { + return + } + + profiles, err = app.GetUsersNotInTeamPage(notInTeamId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin()) } else if len(inTeamId) > 0 { if !app.SessionHasPermissionToTeam(c.Session, inTeamId, model.PERMISSION_VIEW_TEAM) { c.SetPermissionError(model.PERMISSION_VIEW_TEAM) diff --git a/api4/user_test.go b/api4/user_test.go index fe53229a5..f6561310b 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -897,6 +897,52 @@ func TestGetUsersInTeam(t *testing.T) { CheckNoError(t, resp) } +func TestGetUsersNotInTeam(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + teamId := th.BasicTeam.Id + + rusers, resp := Client.GetUsersNotInTeam(teamId, 0, 60, "") + CheckNoError(t, resp) + for _, u := range rusers { + CheckUserSanitization(t, u) + } + + rusers, resp = Client.GetUsersNotInTeam(teamId, 0, 60, resp.Etag) + CheckEtag(t, rusers, resp) + + rusers, resp = Client.GetUsersNotInTeam(teamId, 0, 1, "") + CheckNoError(t, resp) + if len(rusers) != 1 { + t.Fatal("should be 1 per page") + } + + rusers, resp = Client.GetUsersNotInTeam(teamId, 1, 1, "") + CheckNoError(t, resp) + if len(rusers) != 1 { + t.Fatal("should be 1 per page") + } + + rusers, resp = Client.GetUsersNotInTeam(teamId, 10000, 100, "") + CheckNoError(t, resp) + if len(rusers) != 0 { + t.Fatal("should be no users") + } + + Client.Logout() + _, resp = Client.GetUsersNotInTeam(teamId, 0, 60, "") + CheckUnauthorizedStatus(t, resp) + + user := th.CreateUser() + Client.Login(user.Email, user.Password) + _, resp = Client.GetUsersNotInTeam(teamId, 0, 60, "") + CheckForbiddenStatus(t, resp) + + _, resp = th.SystemAdminClient.GetUsersNotInTeam(teamId, 0, 60, "") + CheckNoError(t, resp) +} + func TestGetUsersInChannel(t *testing.T) { th := Setup().InitBasic().InitSystemAdmin() defer TearDown() diff --git a/app/user.go b/app/user.go index 40651f56a..1c2aca34f 100644 --- a/app/user.go +++ b/app/user.go @@ -447,6 +447,14 @@ func GetUsersInTeam(teamId string, offset int, limit int) ([]*model.User, *model } } +func GetUsersNotInTeam(teamId string, offset int, limit int) ([]*model.User, *model.AppError) { + if result := <-Srv.Store.User().GetProfilesNotInTeam(teamId, offset, limit); result.Err != nil { + return nil, result.Err + } else { + return result.Data.([]*model.User), nil + } +} + func GetUsersInTeamMap(teamId string, offset int, limit int, asAdmin bool) (map[string]*model.User, *model.AppError) { users, err := GetUsersInTeam(teamId, offset, limit) if err != nil { @@ -476,10 +484,27 @@ func GetUsersInTeamPage(teamId string, page int, perPage int, asAdmin bool) ([]* return users, nil } +func GetUsersNotInTeamPage(teamId string, page int, perPage int, asAdmin bool) ([]*model.User, *model.AppError) { + users, err := GetUsersNotInTeam(teamId, page*perPage, perPage) + if err != nil { + return nil, err + } + + for _, user := range users { + SanitizeProfile(user, asAdmin) + } + + return users, nil +} + func GetUsersInTeamEtag(teamId string) string { return (<-Srv.Store.User().GetEtagForProfiles(teamId)).Data.(string) } +func GetUsersNotInTeamEtag(teamId string) string { + return (<-Srv.Store.User().GetEtagForProfilesNotInTeam(teamId)).Data.(string) +} + func GetUsersInChannel(channelId string, offset int, limit int) ([]*model.User, *model.AppError) { if result := <-Srv.Store.User().GetProfilesInChannel(channelId, offset, limit); result.Err != nil { return nil, result.Err diff --git a/model/client4.go b/model/client4.go index 516a5d23c..c3697e3c9 100644 --- a/model/client4.go +++ b/model/client4.go @@ -467,6 +467,17 @@ func (c *Client4) GetUsersInTeam(teamId string, page int, perPage int, etag stri } } +// GetUsersNotInTeam returns a page of users who are not in a team. Page counting starts at 0. +func (c *Client4) GetUsersNotInTeam(teamId string, page int, perPage int, etag string) ([]*User, *Response) { + query := fmt.Sprintf("?not_in_team=%v&page=%v&per_page=%v", teamId, page, perPage) + if r, err := c.DoApiGet(c.GetUsersRoute()+query, etag); err != nil { + return nil, &Response{StatusCode: r.StatusCode, Error: err} + } else { + defer closeBody(r) + return UserListFromJson(r.Body), BuildResponse(r) + } +} + // GetUsersInChannel returns a page of users on a team. Page counting starts at 0. func (c *Client4) GetUsersInChannel(channelId string, page int, perPage int, etag string) ([]*User, *Response) { query := fmt.Sprintf("?in_channel=%v&page=%v&per_page=%v", channelId, page, perPage) diff --git a/store/sql_user_store.go b/store/sql_user_store.go index c00e37ed6..092f4abc7 100644 --- a/store/sql_user_store.go +++ b/store/sql_user_store.go @@ -1514,3 +1514,76 @@ func (us SqlUserStore) AnalyticsGetSystemAdminCount() StoreChannel { return storeChannel } + +func (us SqlUserStore) GetProfilesNotInTeam(teamId string, offset int, limit int) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var users []*model.User + + if _, err := us.GetReplica().Select(&users, ` + SELECT + u.* + FROM Users u + LEFT JOIN TeamMembers tm + ON tm.UserId = u.Id + AND tm.TeamId = :TeamId + AND tm.DeleteAt = 0 + WHERE tm.UserId IS NULL + ORDER BY u.Username ASC + LIMIT :Limit OFFSET :Offset + `, map[string]interface{}{"TeamId": teamId, "Offset": offset, "Limit": limit}); err != nil { + result.Err = model.NewLocAppError("SqlUserStore.GetProfilesNotInTeam", "store.sql_user.get_profiles.app_error", nil, err.Error()) + } else { + + for _, u := range users { + u.Password = "" + u.AuthData = new(string) + *u.AuthData = "" + } + + result.Data = users + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (us SqlUserStore) GetEtagForProfilesNotInTeam(teamId string) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + updateAt, err := us.GetReplica().SelectInt(` + SELECT + u.UpdateAt + FROM Users u + LEFT JOIN TeamMembers tm + ON tm.UserId = u.Id + AND tm.TeamId = :TeamId + AND tm.DeleteAt = 0 + WHERE tm.UserId IS NULL + ORDER BY u.UpdateAt DESC + LIMIT 1 + `, map[string]interface{}{"TeamId": teamId}) + + if err != nil { + result.Data = fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.GetMillis(), utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress) + } else { + result.Data = fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, updateAt, utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress) + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} diff --git a/store/sql_user_store_test.go b/store/sql_user_store_test.go index 798988246..73d42dbcd 100644 --- a/store/sql_user_store_test.go +++ b/store/sql_user_store_test.go @@ -1632,3 +1632,137 @@ func TestUserStoreAnalyticsGetSystemAdminCount(t *testing.T) { } } } + +func TestUserStoreGetProfilesNotInTeam(t *testing.T) { + Setup() + + teamId := model.NewId() + + u1 := &model.User{} + u1.Email = model.NewId() + Must(store.User().Save(u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id})) + Must(store.User().UpdateUpdateAt(u1.Id)) + + u2 := &model.User{} + u2.Email = model.NewId() + Must(store.User().Save(u2)) + Must(store.User().UpdateUpdateAt(u2.Id)) + + var initialUsersNotInTeam int + var etag1, etag2, etag3 string + + if er1 := <-store.User().GetEtagForProfilesNotInTeam(teamId); er1.Err != nil { + t.Fatal(er1.Err) + } else { + etag1 = er1.Data.(string) + } + + if r1 := <-store.User().GetProfilesNotInTeam(teamId, 0, 100000); r1.Err != nil { + t.Fatal(r1.Err) + } else { + users := r1.Data.([]*model.User) + initialUsersNotInTeam = len(users) + if initialUsersNotInTeam < 1 { + t.Fatalf("Should be at least 1 user not in the team") + } + + found := false + for _, u := range users { + if u.Id == u2.Id { + found = true + } + if u.Id == u1.Id { + t.Fatalf("Should not have found user1") + } + } + + if !found { + t.Fatal("missing user2") + } + } + + time.Sleep(time.Millisecond * 10) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id})) + Must(store.User().UpdateUpdateAt(u2.Id)) + + if er2 := <-store.User().GetEtagForProfilesNotInTeam(teamId); er2.Err != nil { + t.Fatal(er2.Err) + } else { + etag2 = er2.Data.(string) + if etag1 == etag2 { + t.Fatalf("etag should have changed") + } + } + + if r2 := <-store.User().GetProfilesNotInTeam(teamId, 0, 100000); r2.Err != nil { + t.Fatal(r2.Err) + } else { + users := r2.Data.([]*model.User) + + if len(users) != initialUsersNotInTeam-1 { + t.Fatalf("Should be one less user not in team") + } + + for _, u := range users { + if u.Id == u2.Id { + t.Fatalf("Should not have found user2") + } + if u.Id == u1.Id { + t.Fatalf("Should not have found user1") + } + } + } + + time.Sleep(time.Millisecond * 10) + Must(store.Team().RemoveMember(teamId, u1.Id)) + Must(store.Team().RemoveMember(teamId, u2.Id)) + Must(store.User().UpdateUpdateAt(u1.Id)) + Must(store.User().UpdateUpdateAt(u2.Id)) + + if er3 := <-store.User().GetEtagForProfilesNotInTeam(teamId); er3.Err != nil { + t.Fatal(er3.Err) + } else { + etag3 = er3.Data.(string) + t.Log(etag3) + if etag1 == etag3 || etag3 == etag2 { + t.Fatalf("etag should have changed") + } + } + + if r3 := <-store.User().GetProfilesNotInTeam(teamId, 0, 100000); r3.Err != nil { + t.Fatal(r3.Err) + } else { + users := r3.Data.([]*model.User) + found1, found2 := false, false + for _, u := range users { + if u.Id == u2.Id { + found2 = true + } + if u.Id == u1.Id { + found1 = true + } + } + + if !found1 || !found2 { + t.Fatal("missing user1 or user2") + } + } + + time.Sleep(time.Millisecond * 10) + u3 := &model.User{} + u3.Email = model.NewId() + Must(store.User().Save(u3)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id})) + Must(store.User().UpdateUpdateAt(u3.Id)) + + if er4 := <-store.User().GetEtagForProfilesNotInTeam(teamId); er4.Err != nil { + t.Fatal(er4.Err) + } else { + etag4 := er4.Data.(string) + t.Log(etag4) + if etag4 != etag3 { + t.Fatalf("etag should be the same") + } + } +} diff --git a/store/store.go b/store/store.go index 528b26a3f..409280918 100644 --- a/store/store.go +++ b/store/store.go @@ -205,6 +205,8 @@ type UserStore interface { SearchWithoutTeam(term string, options map[string]bool) StoreChannel AnalyticsGetInactiveUsersCount() StoreChannel AnalyticsGetSystemAdminCount() StoreChannel + GetProfilesNotInTeam(teamId string, offset int, limit int) StoreChannel + GetEtagForProfilesNotInTeam(teamId string) StoreChannel } type SessionStore interface { |