Implement POST /users/search endpoint for APIv4 (#5822)

* Implement POST /users/search endpoint for APIv4

* PLT-2713 Added store functions for searching users that don't have a team

* PLT-2713 Added 'without_team' option when searching users

* PLT-2713 Added 'without_team' option when searching users (v4)
This commit is contained in:
Joram Wilander
2017-03-23 06:34:22 -04:00
committed by George Goldberg
parent 7e2e823884
commit 2a753949f1
10 changed files with 392 additions and 13 deletions

View File

@@ -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) {

View File

@@ -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")
}
}
}

View File

@@ -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")

View File

@@ -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()

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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
}