From 1860d05d623b6fd7670121a7e2391605d1281b27 Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Mon, 13 Mar 2017 08:29:41 -0400 Subject: Implement GET /users/autocomplete endpoint for APIv4 (#5742) --- api/user.go | 32 +++--------- api4/api.go | 2 +- api4/team_test.go | 3 +- api4/user.go | 52 +++++++++++++++++++ api4/user_test.go | 121 +++++++++++++++++++++++++++++++++++++++++++++ app/user.go | 58 +++++++++++++++++----- model/client4.go | 33 +++++++++++++ model/user_autocomplete.go | 25 ++++++++++ 8 files changed, 287 insertions(+), 39 deletions(-) diff --git a/api/user.go b/api/user.go index 183f4e100..0c268b338 100644 --- a/api/user.go +++ b/api/user.go @@ -1538,11 +1538,11 @@ 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) + 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) + profiles, err = app.SearchUsersNotInChannel(props.TeamId, props.NotInChannelId, props.Term, searchOptions, c.IsSystemAdmin()) } else { - profiles, err = app.SearchUsersInTeam(props.TeamId, props.Term, searchOptions) + profiles, err = app.SearchUsersInTeam(props.TeamId, props.Term, searchOptions, c.IsSystemAdmin()) } if err != nil { @@ -1550,10 +1550,6 @@ func searchUsers(c *Context, w http.ResponseWriter, r *http.Request) { return } - for _, p := range profiles { - sanitizeProfile(c, p) - } - w.Write([]byte(model.UserListToJson(profiles))) } @@ -1604,20 +1600,12 @@ func autocompleteUsersInChannel(c *Context, w http.ResponseWriter, r *http.Reque searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY] = true } - autocomplete, err := app.AutocompleteUsersInChannel(teamId, channelId, term, searchOptions) + autocomplete, err := app.AutocompleteUsersInChannel(teamId, channelId, term, searchOptions, c.IsSystemAdmin()) if err != nil { c.Err = err return } - for _, p := range autocomplete.InChannel { - sanitizeProfile(c, p) - } - - for _, p := range autocomplete.OutOfChannel { - sanitizeProfile(c, p) - } - w.Write([]byte(autocomplete.ToJson())) } @@ -1642,16 +1630,12 @@ func autocompleteUsersInTeam(c *Context, w http.ResponseWriter, r *http.Request) searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY] = true } - autocomplete, err := app.AutocompleteUsersInTeam(teamId, term, searchOptions) + autocomplete, err := app.AutocompleteUsersInTeam(teamId, term, searchOptions, c.IsSystemAdmin()) if err != nil { c.Err = err return } - for _, p := range autocomplete.InTeam { - sanitizeProfile(c, p) - } - w.Write([]byte(autocomplete.ToJson())) } @@ -1670,14 +1654,10 @@ func autocompleteUsers(c *Context, w http.ResponseWriter, r *http.Request) { var profiles []*model.User var err *model.AppError - if profiles, err = app.SearchUsersInTeam("", term, searchOptions); err != nil { + if profiles, err = app.SearchUsersInTeam("", term, searchOptions, c.IsSystemAdmin()); err != nil { c.Err = err return } - for _, p := range profiles { - sanitizeProfile(c, p) - } - w.Write([]byte(model.UserListToJson(profiles))) } diff --git a/api4/api.go b/api4/api.go index 71dfbcdf3..223017151 100644 --- a/api4/api.go +++ b/api4/api.go @@ -92,7 +92,7 @@ func InitApi(full bool) { BaseRoutes.ApiRoot = app.Srv.Router.PathPrefix(model.API_URL_SUFFIX).Subrouter() BaseRoutes.Users = BaseRoutes.ApiRoot.PathPrefix("/users").Subrouter() - BaseRoutes.User = BaseRoutes.Users.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter() + BaseRoutes.User = BaseRoutes.ApiRoot.PathPrefix("/users/{user_id:[A-Za-z0-9]+}").Subrouter() BaseRoutes.UserByUsername = BaseRoutes.Users.PathPrefix("/username/{username:[A-Za-z0-9\\_\\-\\.]+}").Subrouter() BaseRoutes.UserByEmail = BaseRoutes.Users.PathPrefix("/email/{email}").Subrouter() diff --git a/api4/team_test.go b/api4/team_test.go index 7a1bbfb69..1ace69685 100644 --- a/api4/team_test.go +++ b/api4/team_test.go @@ -123,10 +123,11 @@ func TestGetAllTeams(t *testing.T) { _, resp := Client.CreateTeam(team) CheckNoError(t, resp) - rrteams, resp := Client.GetAllTeams("", 1, 1) + rrteams, resp := Client.GetAllTeams("", 0, 1) CheckNoError(t, resp) if len(rrteams) != 1 { + t.Log(len(rrteams)) t.Fatal("wrong number of teams - should be 1") } diff --git a/api4/user.go b/api4/user.go index 822cd60c4..b0063c657 100644 --- a/api4/user.go +++ b/api4/user.go @@ -11,6 +11,7 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" ) @@ -20,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("/autocomplete", ApiSessionRequired(autocompleteUsers)).Methods("GET") BaseRoutes.User.Handle("", ApiSessionRequired(getUser)).Methods("GET") BaseRoutes.User.Handle("/image", ApiSessionRequired(getProfileImage)).Methods("GET") @@ -331,6 +333,56 @@ func getUsersByIds(c *Context, w http.ResponseWriter, r *http.Request) { } } +func autocompleteUsers(c *Context, w http.ResponseWriter, r *http.Request) { + channelId := r.URL.Query().Get("in_channel") + teamId := r.URL.Query().Get("in_team") + name := r.URL.Query().Get("name") + + autocomplete := new(model.UserAutocomplete) + var err *model.AppError + + searchOptions := map[string]bool{} + + hideFullName := !utils.Cfg.PrivacySettings.ShowFullName + if hideFullName && !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) { + searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY_NO_FULL_NAME] = true + } else { + searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY] = true + } + + if len(teamId) > 0 { + if len(channelId) > 0 { + if !app.SessionHasPermissionToChannel(c.Session, channelId, model.PERMISSION_READ_CHANNEL) { + c.SetPermissionError(model.PERMISSION_READ_CHANNEL) + return + } + + result, _ := app.AutocompleteUsersInChannel(teamId, channelId, name, searchOptions, c.IsSystemAdmin()) + autocomplete.Users = result.InChannel + autocomplete.OutOfChannel = result.OutOfChannel + } else { + if !app.SessionHasPermissionToTeam(c.Session, teamId, model.PERMISSION_VIEW_TEAM) { + c.SetPermissionError(model.PERMISSION_VIEW_TEAM) + return + } + + result, _ := app.AutocompleteUsersInTeam(teamId, name, searchOptions, c.IsSystemAdmin()) + autocomplete.Users = result.InTeam + } + } else { + // No permission check required + result, _ := app.SearchUsersInTeam("", name, searchOptions, c.IsSystemAdmin()) + autocomplete.Users = result + } + + if err != nil { + c.Err = err + return + } else { + w.Write([]byte((autocomplete.ToJson()))) + } +} + func updateUser(c *Context, w http.ResponseWriter, r *http.Request) { c.RequireUserId() if c.Err != nil { diff --git a/api4/user_test.go b/api4/user_test.go index 4ef1505e7..fd555fe42 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -267,6 +267,127 @@ func TestGetUserByEmail(t *testing.T) { } } +func TestAutocompleteUsers(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + teamId := th.BasicTeam.Id + channelId := th.BasicChannel.Id + username := th.BasicUser.Username + + rusers, resp := Client.AutocompleteUsersInChannel(teamId, channelId, username, "") + CheckNoError(t, resp) + + if len(rusers.Users) != 1 { + t.Fatal("should have returned 1 user") + } + + rusers, resp = Client.AutocompleteUsersInChannel(teamId, channelId, "amazonses", "") + CheckNoError(t, resp) + if len(rusers.Users) != 0 { + t.Fatal("should have returned 0 users") + } + + rusers, resp = Client.AutocompleteUsersInChannel(teamId, channelId, "", "") + CheckNoError(t, resp) + if len(rusers.Users) < 2 { + t.Fatal("should have many users") + } + + rusers, resp = Client.AutocompleteUsersInTeam(teamId, username, "") + CheckNoError(t, resp) + + if len(rusers.Users) != 1 { + t.Fatal("should have returned 1 user") + } + + rusers, resp = Client.AutocompleteUsers(username, "") + CheckNoError(t, resp) + + if len(rusers.Users) != 1 { + t.Fatal("should have returned 1 users") + } + + rusers, resp = Client.AutocompleteUsers("", "") + CheckNoError(t, resp) + + if len(rusers.Users) < 2 { + t.Fatal("should have returned many users") + } + + rusers, resp = Client.AutocompleteUsersInTeam(teamId, "amazonses", "") + CheckNoError(t, resp) + if len(rusers.Users) != 0 { + t.Fatal("should have returned 0 users") + } + + rusers, resp = Client.AutocompleteUsersInTeam(teamId, "", "") + CheckNoError(t, resp) + if len(rusers.Users) < 2 { + t.Fatal("should have many users") + } + + Client.Logout() + _, resp = Client.AutocompleteUsersInChannel(teamId, channelId, username, "") + CheckUnauthorizedStatus(t, resp) + + _, resp = Client.AutocompleteUsersInTeam(teamId, username, "") + CheckUnauthorizedStatus(t, resp) + + _, resp = Client.AutocompleteUsers(username, "") + CheckUnauthorizedStatus(t, resp) + + user := th.CreateUser() + Client.Login(user.Email, user.Password) + _, resp = Client.AutocompleteUsersInChannel(teamId, channelId, username, "") + CheckForbiddenStatus(t, resp) + + _, resp = Client.AutocompleteUsersInTeam(teamId, username, "") + CheckForbiddenStatus(t, resp) + + _, resp = Client.AutocompleteUsers(username, "") + CheckNoError(t, resp) + + _, resp = th.SystemAdminClient.AutocompleteUsersInChannel(teamId, channelId, username, "") + CheckNoError(t, resp) + + _, resp = th.SystemAdminClient.AutocompleteUsersInTeam(teamId, username, "") + CheckNoError(t, resp) + + _, resp = th.SystemAdminClient.AutocompleteUsers(username, "") + CheckNoError(t, resp) + + // Check against privacy config settings + namePrivacy := utils.Cfg.PrivacySettings.ShowFullName + defer func() { + utils.Cfg.PrivacySettings.ShowFullName = namePrivacy + }() + utils.Cfg.PrivacySettings.ShowFullName = false + + th.LoginBasic() + + rusers, resp = Client.AutocompleteUsers(username, "") + CheckNoError(t, resp) + + if rusers.Users[0].FirstName != "" || rusers.Users[0].LastName != "" { + t.Fatal("should not show first/last name") + } + + rusers, resp = Client.AutocompleteUsersInChannel(teamId, channelId, username, "") + CheckNoError(t, resp) + + if rusers.Users[0].FirstName != "" || rusers.Users[0].LastName != "" { + t.Fatal("should not show first/last name") + } + + rusers, resp = Client.AutocompleteUsersInTeam(teamId, username, "") + CheckNoError(t, resp) + + if rusers.Users[0].FirstName != "" || rusers.Users[0].LastName != "" { + t.Fatal("should not show first/last name") + } +} + func TestGetProfileImage(t *testing.T) { th := Setup().InitBasic().InitSystemAdmin() defer TearDown() diff --git a/app/user.go b/app/user.go index d1ddceb23..d01ce7a51 100644 --- a/app/user.go +++ b/app/user.go @@ -1183,31 +1183,49 @@ func VerifyUserEmail(userId string) *model.AppError { return nil } -func SearchUsersInChannel(channelId string, term string, searchOptions map[string]bool) ([]*model.User, *model.AppError) { +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 } else { - return result.Data.([]*model.User), nil + users := result.Data.([]*model.User) + + for _, user := range users { + SanitizeProfile(user, asAdmin) + } + + return users, nil } } -func SearchUsersNotInChannel(teamId string, channelId string, term string, searchOptions map[string]bool) ([]*model.User, *model.AppError) { +func SearchUsersNotInChannel(teamId string, channelId string, term string, searchOptions map[string]bool, asAdmin bool) ([]*model.User, *model.AppError) { if result := <-Srv.Store.User().SearchNotInChannel(teamId, channelId, term, searchOptions); result.Err != nil { return nil, result.Err } else { - return result.Data.([]*model.User), nil + users := result.Data.([]*model.User) + + for _, user := range users { + SanitizeProfile(user, asAdmin) + } + + return users, nil } } -func SearchUsersInTeam(teamId string, term string, searchOptions map[string]bool) ([]*model.User, *model.AppError) { +func SearchUsersInTeam(teamId string, term string, searchOptions map[string]bool, asAdmin bool) ([]*model.User, *model.AppError) { if result := <-Srv.Store.User().Search(teamId, term, searchOptions); result.Err != nil { return nil, result.Err } else { - return result.Data.([]*model.User), nil + 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) (*model.UserAutocompleteInChannel, *model.AppError) { +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) @@ -1216,25 +1234,43 @@ func AutocompleteUsersInChannel(teamId string, channelId string, term string, se if result := <-uchan; result.Err != nil { return nil, result.Err } else { - autocomplete.InChannel = result.Data.([]*model.User) + users := result.Data.([]*model.User) + + for _, user := range users { + SanitizeProfile(user, asAdmin) + } + + autocomplete.InChannel = users } if result := <-nuchan; result.Err != nil { return nil, result.Err } else { - autocomplete.OutOfChannel = result.Data.([]*model.User) + users := result.Data.([]*model.User) + + for _, user := range users { + SanitizeProfile(user, asAdmin) + } + + autocomplete.OutOfChannel = users } return autocomplete, nil } -func AutocompleteUsersInTeam(teamId string, term string, searchOptions map[string]bool) (*model.UserAutocompleteInTeam, *model.AppError) { +func AutocompleteUsersInTeam(teamId string, term string, searchOptions map[string]bool, asAdmin bool) (*model.UserAutocompleteInTeam, *model.AppError) { autocomplete := &model.UserAutocompleteInTeam{} if result := <-Srv.Store.User().Search(teamId, term, searchOptions); result.Err != nil { return nil, result.Err } else { - autocomplete.InTeam = result.Data.([]*model.User) + users := result.Data.([]*model.User) + + for _, user := range users { + SanitizeProfile(user, asAdmin) + } + + autocomplete.InTeam = users } return autocomplete, nil diff --git a/model/client4.go b/model/client4.go index 6441abcc2..9a1d6e1cf 100644 --- a/model/client4.go +++ b/model/client4.go @@ -339,6 +339,39 @@ func (c *Client4) GetUserByEmail(email, etag string) (*User, *Response) { } } +// AutocompleteUsersInTeam returns the users on a team based on search term. +func (c *Client4) AutocompleteUsersInTeam(teamId string, username string, etag string) (*UserAutocomplete, *Response) { + query := fmt.Sprintf("?in_team=%v&name=%v", teamId, username) + if r, err := c.DoApiGet(c.GetUsersRoute()+"/autocomplete"+query, etag); err != nil { + return nil, &Response{StatusCode: r.StatusCode, Error: err} + } else { + defer closeBody(r) + return UserAutocompleteFromJson(r.Body), BuildResponse(r) + } +} + +// AutocompleteUsersInChannel returns the users in a channel based on search term. +func (c *Client4) AutocompleteUsersInChannel(teamId string, channelId string, username string, etag string) (*UserAutocomplete, *Response) { + query := fmt.Sprintf("?in_team=%v&in_channel=%v&name=%v", teamId, channelId, username) + if r, err := c.DoApiGet(c.GetUsersRoute()+"/autocomplete"+query, etag); err != nil { + return nil, &Response{StatusCode: r.StatusCode, Error: err} + } else { + defer closeBody(r) + return UserAutocompleteFromJson(r.Body), BuildResponse(r) + } +} + +// AutocompleteUsers returns the users in the system based on search term. +func (c *Client4) AutocompleteUsers(username string, etag string) (*UserAutocomplete, *Response) { + query := fmt.Sprintf("?name=%v", username) + if r, err := c.DoApiGet(c.GetUsersRoute()+"/autocomplete"+query, etag); err != nil { + return nil, &Response{StatusCode: r.StatusCode, Error: err} + } else { + defer closeBody(r) + return UserAutocompleteFromJson(r.Body), BuildResponse(r) + } +} + // GetProfileImage gets user's profile image. Must be logged in or be a system administrator. func (c *Client4) GetProfileImage(userId, etag string) ([]byte, *Response) { if r, err := c.DoApiGet(c.GetUserRoute(userId)+"/image", etag); err != nil { diff --git a/model/user_autocomplete.go b/model/user_autocomplete.go index b7449a792..b80c6f992 100644 --- a/model/user_autocomplete.go +++ b/model/user_autocomplete.go @@ -17,6 +17,31 @@ type UserAutocompleteInTeam struct { InTeam []*User `json:"in_team"` } +type UserAutocomplete struct { + Users []*User `json:"users"` + OutOfChannel []*User `json:"out_of_channel,omitempty"` +} + +func (o *UserAutocomplete) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func UserAutocompleteFromJson(data io.Reader) *UserAutocomplete { + decoder := json.NewDecoder(data) + autocomplete := new(UserAutocomplete) + err := decoder.Decode(&autocomplete) + if err == nil { + return autocomplete + } else { + return nil + } +} + func (o *UserAutocompleteInChannel) ToJson() string { b, err := json.Marshal(o) if err != nil { -- cgit v1.2.3-1-g7c22