diff options
-rw-r--r-- | api/user.go | 16 | ||||
-rw-r--r-- | api/user_test.go | 53 | ||||
-rw-r--r-- | api4/user.go | 57 | ||||
-rw-r--r-- | api4/user_test.go | 150 | ||||
-rw-r--r-- | app/user.go | 26 | ||||
-rw-r--r-- | model/client4.go | 10 | ||||
-rw-r--r-- | model/user_search.go | 1 | ||||
-rw-r--r-- | store/sql_user_store.go | 30 | ||||
-rw-r--r-- | store/sql_user_store_test.go | 61 | ||||
-rw-r--r-- | store/store.go | 1 |
10 files changed, 392 insertions, 13 deletions
diff --git a/api/user.go b/api/user.go index 24a9025e4..795e83a2a 100644 --- a/api/user.go +++ b/api/user.go @@ -1535,22 +1535,12 @@ func searchUsers(c *Context, w http.ResponseWriter, r *http.Request) { } } - var profiles []*model.User - var err *model.AppError - if props.InChannelId != "" { - profiles, err = app.SearchUsersInChannel(props.InChannelId, props.Term, searchOptions, c.IsSystemAdmin()) - } else if props.NotInChannelId != "" { - profiles, err = app.SearchUsersNotInChannel(props.TeamId, props.NotInChannelId, props.Term, searchOptions, c.IsSystemAdmin()) - } else { - profiles, err = app.SearchUsersInTeam(props.TeamId, props.Term, searchOptions, c.IsSystemAdmin()) - } - - if err != nil { + if profiles, err := app.SearchUsers(props, searchOptions, c.IsSystemAdmin()); err != nil { c.Err = err return + } else { + w.Write([]byte(model.UserListToJson(profiles))) } - - w.Write([]byte(model.UserListToJson(profiles))) } func getProfilesByIds(c *Context, w http.ResponseWriter, r *http.Request) { diff --git a/api/user_test.go b/api/user_test.go index 1fdb3a290..cdbccc57e 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -2440,6 +2440,59 @@ func TestSearchUsers(t *testing.T) { } if _, err := Client.SearchUsers(model.UserSearch{Term: th.BasicUser.Username, NotInChannelId: th.BasicChannel.Id}); err == nil { + t.Fatal("should not have access") + } + + userWithoutTeam := th.CreateUser(Client) + if result, err := Client.SearchUsers(model.UserSearch{Term: userWithoutTeam.Username}); err != nil { + t.Fatal(err) + } else { + users := result.Data.([]*model.User) + + found := false + for _, user := range users { + if user.Id == userWithoutTeam.Id { + found = true + } + } + + if !found { + t.Fatal("should have found user without team") + } + } + + if result, err := Client.SearchUsers(model.UserSearch{Term: userWithoutTeam.Username, WithoutTeam: true}); err != nil { + t.Fatal(err) + } else { + users := result.Data.([]*model.User) + + found := false + for _, user := range users { + if user.Id == userWithoutTeam.Id { + found = true + } + } + + if !found { + t.Fatal("should have found user without team") + } + } + + if result, err := Client.SearchUsers(model.UserSearch{Term: th.BasicUser.Username, WithoutTeam: true}); err != nil { + t.Fatal(err) + } else { + users := result.Data.([]*model.User) + + found := false + for _, user := range users { + if user.Id == th.BasicUser.Id { + found = true + } + } + + if found { + t.Fatal("should not have found user with team") + } } } diff --git a/api4/user.go b/api4/user.go index 383bb2f59..b22bc75f6 100644 --- a/api4/user.go +++ b/api4/user.go @@ -21,6 +21,7 @@ func InitUser() { BaseRoutes.Users.Handle("", ApiHandler(createUser)).Methods("POST") BaseRoutes.Users.Handle("", ApiSessionRequired(getUsers)).Methods("GET") BaseRoutes.Users.Handle("/ids", ApiSessionRequired(getUsersByIds)).Methods("POST") + BaseRoutes.Users.Handle("/search", ApiSessionRequired(searchUsers)).Methods("POST") BaseRoutes.Users.Handle("/autocomplete", ApiSessionRequired(autocompleteUsers)).Methods("GET") BaseRoutes.User.Handle("", ApiSessionRequired(getUser)).Methods("GET") @@ -334,6 +335,62 @@ func getUsersByIds(c *Context, w http.ResponseWriter, r *http.Request) { } } +func searchUsers(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.UserSearchFromJson(r.Body) + if props == nil { + c.SetInvalidParam("") + return + } + + if len(props.Term) == 0 { + c.SetInvalidParam("term") + return + } + + if props.TeamId == "" && props.NotInChannelId != "" { + c.SetInvalidParam("team_id") + return + } + + if props.InChannelId != "" && !app.SessionHasPermissionToChannel(c.Session, props.InChannelId, model.PERMISSION_READ_CHANNEL) { + c.SetPermissionError(model.PERMISSION_READ_CHANNEL) + return + } + + if props.NotInChannelId != "" && !app.SessionHasPermissionToChannel(c.Session, props.NotInChannelId, model.PERMISSION_READ_CHANNEL) { + c.SetPermissionError(model.PERMISSION_READ_CHANNEL) + return + } + + if props.TeamId != "" && !app.SessionHasPermissionToTeam(c.Session, props.TeamId, model.PERMISSION_VIEW_TEAM) { + c.SetPermissionError(model.PERMISSION_VIEW_TEAM) + return + } + + searchOptions := map[string]bool{} + searchOptions[store.USER_SEARCH_OPTION_ALLOW_INACTIVE] = props.AllowInactive + + if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) { + hideFullName := !utils.Cfg.PrivacySettings.ShowFullName + hideEmail := !utils.Cfg.PrivacySettings.ShowEmailAddress + + if hideFullName && hideEmail { + searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY_NO_FULL_NAME] = true + } else if hideFullName { + searchOptions[store.USER_SEARCH_OPTION_ALL_NO_FULL_NAME] = true + } else if hideEmail { + searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY] = true + } + } + + if profiles, err := app.SearchUsers(props, searchOptions, c.IsSystemAdmin()); err != nil { + c.Err = err + return + } else { + w.Write([]byte(model.UserListToJson(profiles))) + } +} + func autocompleteUsers(c *Context, w http.ResponseWriter, r *http.Request) { channelId := r.URL.Query().Get("in_channel") teamId := r.URL.Query().Get("in_team") diff --git a/api4/user_test.go b/api4/user_test.go index 923caa761..53dbd53e8 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -284,6 +284,156 @@ func TestGetUserByEmail(t *testing.T) { } } +func TestSearchUsers(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + + search := &model.UserSearch{Term: th.BasicUser.Username} + + users, resp := Client.SearchUsers(search) + CheckNoError(t, resp) + + if !findUserInList(th.BasicUser.Id, users) { + t.Fatal("should have found user") + } + + _, err := app.UpdateActiveNoLdap(th.BasicUser2.Id, false) + if err != nil { + t.Fatal(err) + } + + search.Term = th.BasicUser2.Username + search.AllowInactive = false + + users, resp = Client.SearchUsers(search) + CheckNoError(t, resp) + + if findUserInList(th.BasicUser2.Id, users) { + t.Fatal("should not have found user") + } + + search.AllowInactive = true + + users, resp = Client.SearchUsers(search) + CheckNoError(t, resp) + + if !findUserInList(th.BasicUser2.Id, users) { + t.Fatal("should have found user") + } + + search.Term = th.BasicUser.Username + search.AllowInactive = false + search.TeamId = th.BasicTeam.Id + + users, resp = Client.SearchUsers(search) + CheckNoError(t, resp) + + if !findUserInList(th.BasicUser.Id, users) { + t.Fatal("should have found user") + } + + search.NotInChannelId = th.BasicChannel.Id + + users, resp = Client.SearchUsers(search) + CheckNoError(t, resp) + + if findUserInList(th.BasicUser.Id, users) { + t.Fatal("should not have found user") + } + + search.TeamId = "" + search.NotInChannelId = "" + search.InChannelId = th.BasicChannel.Id + + users, resp = Client.SearchUsers(search) + CheckNoError(t, resp) + + if !findUserInList(th.BasicUser.Id, users) { + t.Fatal("should have found user") + } + + search.InChannelId = "" + search.NotInChannelId = th.BasicChannel.Id + _, resp = Client.SearchUsers(search) + CheckBadRequestStatus(t, resp) + + search.NotInChannelId = model.NewId() + search.TeamId = model.NewId() + _, resp = Client.SearchUsers(search) + CheckForbiddenStatus(t, resp) + + search.NotInChannelId = "" + search.TeamId = model.NewId() + _, resp = Client.SearchUsers(search) + CheckForbiddenStatus(t, resp) + + search.InChannelId = model.NewId() + search.TeamId = "" + _, resp = Client.SearchUsers(search) + CheckForbiddenStatus(t, resp) + + emailPrivacy := utils.Cfg.PrivacySettings.ShowEmailAddress + namePrivacy := utils.Cfg.PrivacySettings.ShowFullName + defer func() { + utils.Cfg.PrivacySettings.ShowEmailAddress = emailPrivacy + utils.Cfg.PrivacySettings.ShowFullName = namePrivacy + }() + utils.Cfg.PrivacySettings.ShowEmailAddress = false + utils.Cfg.PrivacySettings.ShowFullName = false + + _, err = app.UpdateActiveNoLdap(th.BasicUser2.Id, true) + if err != nil { + t.Fatal(err) + } + + search.InChannelId = "" + search.Term = th.BasicUser2.Email + users, resp = Client.SearchUsers(search) + CheckNoError(t, resp) + + if findUserInList(th.BasicUser2.Id, users) { + t.Fatal("should not have found user") + } + + search.Term = th.BasicUser2.FirstName + users, resp = Client.SearchUsers(search) + CheckNoError(t, resp) + + if findUserInList(th.BasicUser2.Id, users) { + t.Fatal("should not have found user") + } + + search.Term = th.BasicUser2.LastName + users, resp = Client.SearchUsers(search) + CheckNoError(t, resp) + + if findUserInList(th.BasicUser2.Id, users) { + t.Fatal("should not have found user") + } + + search.Term = th.BasicUser.FirstName + search.InChannelId = th.BasicChannel.Id + search.NotInChannelId = th.BasicChannel.Id + search.TeamId = th.BasicTeam.Id + users, resp = th.SystemAdminClient.SearchUsers(search) + CheckNoError(t, resp) + + if !findUserInList(th.BasicUser.Id, users) { + t.Fatal("should have found user") + } + +} + +func findUserInList(id string, users []*model.User) bool { + for _, user := range users { + if user.Id == id { + return true + } + } + return false +} + func TestAutocompleteUsers(t *testing.T) { th := Setup().InitBasic().InitSystemAdmin() defer TearDown() diff --git a/app/user.go b/app/user.go index c877640d6..33d052708 100644 --- a/app/user.go +++ b/app/user.go @@ -1211,6 +1211,18 @@ func VerifyUserEmail(userId string) *model.AppError { return nil } +func SearchUsers(props *model.UserSearch, searchOptions map[string]bool, asAdmin bool) ([]*model.User, *model.AppError) { + if props.WithoutTeam { + return SearchUsersWithoutTeam(props.Term, searchOptions, asAdmin) + } else if props.InChannelId != "" { + return SearchUsersInChannel(props.InChannelId, props.Term, searchOptions, asAdmin) + } else if props.NotInChannelId != "" { + return SearchUsersNotInChannel(props.TeamId, props.NotInChannelId, props.Term, searchOptions, asAdmin) + } else { + return SearchUsersInTeam(props.TeamId, props.Term, searchOptions, asAdmin) + } +} + func SearchUsersInChannel(channelId string, term string, searchOptions map[string]bool, asAdmin bool) ([]*model.User, *model.AppError) { if result := <-Srv.Store.User().SearchInChannel(channelId, term, searchOptions); result.Err != nil { return nil, result.Err @@ -1253,6 +1265,20 @@ func SearchUsersInTeam(teamId string, term string, searchOptions map[string]bool } } +func SearchUsersWithoutTeam(term string, searchOptions map[string]bool, asAdmin bool) ([]*model.User, *model.AppError) { + if result := <-Srv.Store.User().SearchWithoutTeam(term, searchOptions); result.Err != nil { + return nil, result.Err + } else { + users := result.Data.([]*model.User) + + for _, user := range users { + SanitizeProfile(user, asAdmin) + } + + return users, nil + } +} + func AutocompleteUsersInChannel(teamId string, channelId string, term string, searchOptions map[string]bool, asAdmin bool) (*model.UserAutocompleteInChannel, *model.AppError) { uchan := Srv.Store.User().SearchInChannel(channelId, term, searchOptions) nuchan := Srv.Store.User().SearchNotInChannel(teamId, channelId, term, searchOptions) diff --git a/model/client4.go b/model/client4.go index f6d665e1c..420b8d510 100644 --- a/model/client4.go +++ b/model/client4.go @@ -487,6 +487,16 @@ func (c *Client4) GetUsersByIds(userIds []string) ([]*User, *Response) { } } +// SearchUsers returns a list of users based on some search criteria. +func (c *Client4) SearchUsers(search *UserSearch) ([]*User, *Response) { + if r, err := c.DoApiPost(c.GetUsersRoute()+"/search", search.ToJson()); err != nil { + return nil, &Response{StatusCode: r.StatusCode, Error: err} + } else { + defer closeBody(r) + return UserListFromJson(r.Body), BuildResponse(r) + } +} + // UpdateUser updates a user in the system based on the provided user struct. func (c *Client4) UpdateUser(user *User) (*User, *Response) { if r, err := c.DoApiPut(c.GetUserRoute(user.Id), user.ToJson()); err != nil { diff --git a/model/user_search.go b/model/user_search.go index 4bbd2bd78..1ef881130 100644 --- a/model/user_search.go +++ b/model/user_search.go @@ -14,6 +14,7 @@ type UserSearch struct { InChannelId string `json:"in_channel_id"` NotInChannelId string `json:"not_in_channel_id"` AllowInactive bool `json:"allow_inactive"` + WithoutTeam bool `json:"without_team"` } // ToJson convert a User to a json string diff --git a/store/sql_user_store.go b/store/sql_user_store.go index 23b0c3696..c00e37ed6 100644 --- a/store/sql_user_store.go +++ b/store/sql_user_store.go @@ -1261,6 +1261,36 @@ func (us SqlUserStore) Search(teamId string, term string, options map[string]boo return storeChannel } +func (us SqlUserStore) SearchWithoutTeam(term string, options map[string]bool) StoreChannel { + storeChannel := make(StoreChannel, 1) + + go func() { + searchQuery := ` + SELECT + * + FROM + Users + WHERE + (SELECT + COUNT(0) + FROM + TeamMembers + WHERE + TeamMembers.UserId = Users.Id + AND TeamMembers.DeleteAt = 0) = 0 + SEARCH_CLAUSE + INACTIVE_CLAUSE + ORDER BY Username ASC + LIMIT 100` + + storeChannel <- us.performSearch(searchQuery, term, options, map[string]interface{}{}) + close(storeChannel) + + }() + + return storeChannel +} + func (us SqlUserStore) SearchNotInChannel(teamId string, channelId string, term string, options map[string]bool) StoreChannel { storeChannel := make(StoreChannel, 1) diff --git a/store/sql_user_store_test.go b/store/sql_user_store_test.go index 104735455..798988246 100644 --- a/store/sql_user_store_test.go +++ b/store/sql_user_store_test.go @@ -1505,6 +1505,67 @@ func TestUserStoreSearch(t *testing.T) { } } +func TestUserStoreSearchWithoutTeam(t *testing.T) { + Setup() + + u1 := &model.User{} + u1.Username = "jimbo" + model.NewId() + u1.FirstName = "Tim" + u1.LastName = "Bill" + u1.Nickname = "Rob" + u1.Email = "harold" + model.NewId() + "@simulator.amazonses.com" + Must(store.User().Save(u1)) + + u2 := &model.User{} + u2.Username = "jim-bobby" + model.NewId() + u2.Email = model.NewId() + Must(store.User().Save(u2)) + + u3 := &model.User{} + u3.Username = "jimbo" + model.NewId() + u3.Email = model.NewId() + u3.DeleteAt = 1 + Must(store.User().Save(u3)) + + tid := model.NewId() + Must(store.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u3.Id})) + + searchOptions := map[string]bool{} + searchOptions[USER_SEARCH_OPTION_NAMES_ONLY] = true + + if r1 := <-store.User().SearchWithoutTeam("", searchOptions); r1.Err != nil { + t.Fatal(r1.Err) + } + + if r1 := <-store.User().SearchWithoutTeam("jim", searchOptions); r1.Err != nil { + t.Fatal(r1.Err) + } else { + profiles := r1.Data.([]*model.User) + + found1 := false + found2 := false + found3 := false + + for _, profile := range profiles { + if profile.Id == u1.Id { + found1 = true + } else if profile.Id == u2.Id { + found2 = true + } else if profile.Id == u3.Id { + found3 = true + } + } + + if !found1 { + t.Fatal("should have found user1") + } else if !found2 { + t.Fatal("should have found user2") + } else if found3 { + t.Fatal("should not have found user3") + } + } +} + func TestUserStoreAnalyticsGetInactiveUsersCount(t *testing.T) { Setup() diff --git a/store/store.go b/store/store.go index 72572b1e0..323727697 100644 --- a/store/store.go +++ b/store/store.go @@ -201,6 +201,7 @@ type UserStore interface { Search(teamId string, term string, options map[string]bool) StoreChannel SearchInChannel(channelId string, term string, options map[string]bool) StoreChannel SearchNotInChannel(teamId string, channelId string, term string, options map[string]bool) StoreChannel + SearchWithoutTeam(term string, options map[string]bool) StoreChannel AnalyticsGetInactiveUsersCount() StoreChannel AnalyticsGetSystemAdminCount() StoreChannel } |