From 6ac87d82e38c83e3b9b3bd12c3122e047f0110b1 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Wed, 29 Mar 2017 21:11:40 -0400 Subject: PLT-2713 Added ability for admins to list users not in any team (#5844) * PLT-2713 Added ability for admins to list users not in any team * Updated style of unit test --- api4/user.go | 11 +++++++++- api4/user_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++++ app/user.go | 21 +++++++++++++++++++ model/authorization.go | 7 +++++++ model/client4.go | 11 ++++++++++ store/sql_user_store.go | 48 ++++++++++++++++++++++++++++++++++++++++++ store/sql_user_store_test.go | 43 +++++++++++++++++++++++++++++++++++++ store/store.go | 1 + 8 files changed, 191 insertions(+), 1 deletion(-) diff --git a/api4/user.go b/api4/user.go index 298c5cc8d..e4595ee54 100644 --- a/api4/user.go +++ b/api4/user.go @@ -269,6 +269,7 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { notInTeamId := r.URL.Query().Get("not_in_team") inChannelId := r.URL.Query().Get("in_channel") notInChannelId := r.URL.Query().Get("not_in_channel") + withoutTeam := r.URL.Query().Get("without_team") if len(notInChannelId) > 0 && len(inTeamId) == 0 { c.SetInvalidParam("team_id") @@ -279,7 +280,15 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { var err *model.AppError etag := "" - if len(notInChannelId) > 0 { + if withoutTeamBool, err := strconv.ParseBool(withoutTeam); err == nil && withoutTeamBool { + // Use a special permission for now + if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_LIST_USERS_WITHOUT_TEAM) { + c.SetPermissionError(model.PERMISSION_LIST_USERS_WITHOUT_TEAM) + return + } + + profiles, err = app.GetUsersWithoutTeamPage(c.Params.Page, c.Params.PerPage, c.IsSystemAdmin()) + } else if len(notInChannelId) > 0 { if !app.SessionHasPermissionToChannel(c.Session, notInChannelId, model.PERMISSION_READ_CHANNEL) { c.SetPermissionError(model.PERMISSION_READ_CHANNEL) return diff --git a/api4/user_test.go b/api4/user_test.go index f6561310b..21b240957 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -851,6 +851,56 @@ func TestGetUsers(t *testing.T) { CheckUnauthorizedStatus(t, resp) } +func TestGetUsersWithoutTeam(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + SystemAdminClient := th.SystemAdminClient + + if _, resp := Client.GetUsersWithoutTeam(0, 100, ""); resp.Error == nil { + t.Fatal("should prevent non-admin user from getting users without a team") + } + + // These usernames need to appear in the first 100 users for this to work + + user, resp := Client.CreateUser(&model.User{ + Username: "a000000000" + model.NewId(), + Email: "success+" + model.NewId() + "@simulator.amazonses.com", + Password: "Password1", + }) + CheckNoError(t, resp) + LinkUserToTeam(user, th.BasicTeam) + defer app.Srv.Store.User().PermanentDelete(user.Id) + + user2, resp := Client.CreateUser(&model.User{ + Username: "a000000001" + model.NewId(), + Email: "success+" + model.NewId() + "@simulator.amazonses.com", + Password: "Password1", + }) + CheckNoError(t, resp) + defer app.Srv.Store.User().PermanentDelete(user2.Id) + + rusers, resp := SystemAdminClient.GetUsersWithoutTeam(0, 100, "") + CheckNoError(t, resp) + + found1 := false + found2 := false + + for _, u := range rusers { + if u.Id == user.Id { + found1 = true + } else if u.Id == user2.Id { + found2 = true + } + } + + if found1 { + t.Fatal("shouldn't have returned user that has a team") + } else if !found2 { + t.Fatal("should've returned user that has no teams") + } +} + func TestGetUsersInTeam(t *testing.T) { th := Setup().InitBasic().InitSystemAdmin() defer TearDown() diff --git a/app/user.go b/app/user.go index 1c2aca34f..8e0615508 100644 --- a/app/user.go +++ b/app/user.go @@ -579,6 +579,27 @@ func GetUsersNotInChannelPage(teamId string, channelId string, page int, perPage return users, nil } +func GetUsersWithoutTeamPage(page int, perPage int, asAdmin bool) ([]*model.User, *model.AppError) { + users, err := GetUsersWithoutTeam(page*perPage, perPage) + if err != nil { + return nil, err + } + + for _, user := range users { + SanitizeProfile(user, asAdmin) + } + + return users, nil +} + +func GetUsersWithoutTeam(offset int, limit int) ([]*model.User, *model.AppError) { + if result := <-Srv.Store.User().GetProfilesWithoutTeam(offset, limit); result.Err != nil { + return nil, result.Err + } else { + return result.Data.([]*model.User), nil + } +} + func GetUsersByIds(userIds []string, asAdmin bool) ([]*model.User, *model.AppError) { if result := <-Srv.Store.User().GetProfileByIds(userIds, true); result.Err != nil { return nil, result.Err diff --git a/model/authorization.go b/model/authorization.go index d49406e47..1f6f34a2a 100644 --- a/model/authorization.go +++ b/model/authorization.go @@ -57,6 +57,7 @@ var PERMISSION_CREATE_TEAM *Permission var PERMISSION_MANAGE_TEAM *Permission var PERMISSION_IMPORT_TEAM *Permission var PERMISSION_VIEW_TEAM *Permission +var PERMISSION_LIST_USERS_WITHOUT_TEAM *Permission // General permission that encompases all system admin functions // in the future this could be broken up to allow access to some @@ -286,6 +287,11 @@ func InitalizePermissions() { "authentication.permissions.view_team.name", "authentication.permissions.view_team.description", } + PERMISSION_LIST_USERS_WITHOUT_TEAM = &Permission{ + "list_users_without_team", + "authentication.permisssions.list_users_without_team.name", + "authentication.permisssions.list_users_without_team.description", + } } func InitalizeRoles() { @@ -400,6 +406,7 @@ func InitalizeRoles() { PERMISSION_DELETE_OTHERS_POSTS.Id, PERMISSION_CREATE_TEAM.Id, PERMISSION_ADD_USER_TO_TEAM.Id, + PERMISSION_LIST_USERS_WITHOUT_TEAM.Id, }, ROLE_TEAM_USER.Permissions..., ), diff --git a/model/client4.go b/model/client4.go index c3697e3c9..27f8328f4 100644 --- a/model/client4.go +++ b/model/client4.go @@ -500,6 +500,17 @@ func (c *Client4) GetUsersNotInChannel(teamId, channelId string, page int, perPa } } +// GetUsersWithoutTeam returns a page of users on the system that aren't on any teams. Page counting starts at 0. +func (c *Client4) GetUsersWithoutTeam(page int, perPage int, etag string) ([]*User, *Response) { + query := fmt.Sprintf("?without_team=1&page=%v&per_page=%v", 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) + } +} + // GetUsersByIds returns a list of users based on the provided user ids. func (c *Client4) GetUsersByIds(userIds []string) ([]*User, *Response) { if r, err := c.DoApiPost(c.GetUsersRoute()+"/ids", ArrayToJson(userIds)); err != nil { diff --git a/store/sql_user_store.go b/store/sql_user_store.go index 092f4abc7..2fb7158ac 100644 --- a/store/sql_user_store.go +++ b/store/sql_user_store.go @@ -726,6 +726,54 @@ func (us SqlUserStore) GetProfilesNotInChannel(teamId string, channelId string, return storeChannel } +func (us SqlUserStore) GetProfilesWithoutTeam(offset int, limit int) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var users []*model.User + + query := ` + SELECT + * + FROM + Users + WHERE + (SELECT + COUNT(0) + FROM + TeamMembers + WHERE + TeamMembers.UserId = Users.Id + AND TeamMembers.DeleteAt = 0) = 0 + ORDER BY + Username ASC + LIMIT + :Limit + OFFSET + :Offset` + + if _, err := us.GetReplica().Select(&users, query, map[string]interface{}{"Offset": offset, "Limit": limit}); err != nil { + result.Err = model.NewLocAppError("SqlUserStore.GetProfilesWithoutTeam", "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) GetProfilesByUsernames(usernames []string, teamId string) StoreChannel { storeChannel := make(StoreChannel) diff --git a/store/sql_user_store_test.go b/store/sql_user_store_test.go index 73d42dbcd..6e9642b60 100644 --- a/store/sql_user_store_test.go +++ b/store/sql_user_store_test.go @@ -373,6 +373,49 @@ func TestUserStoreGetProfilesInChannel(t *testing.T) { } } +func TestUserStoreGetProfilesWithoutTeam(t *testing.T) { + Setup() + + teamId := model.NewId() + + // These usernames need to appear in the first 100 users for this to work + + u1 := &model.User{} + u1.Username = "a000000000" + model.NewId() + u1.Email = model.NewId() + Must(store.User().Save(u1)) + Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id})) + defer store.User().PermanentDelete(u1.Id) + + u2 := &model.User{} + u2.Username = "a000000001" + model.NewId() + u2.Email = model.NewId() + Must(store.User().Save(u2)) + defer store.User().PermanentDelete(u2.Id) + + if r1 := <-store.User().GetProfilesWithoutTeam(0, 100); r1.Err != nil { + t.Fatal(r1.Err) + } else { + users := r1.Data.([]*model.User) + + found1 := false + found2 := false + for _, u := range users { + if u.Id == u1.Id { + found1 = true + } else if u.Id == u2.Id { + found2 = true + } + } + + if found1 { + t.Fatal("shouldn't have returned user on team") + } else if !found2 { + t.Fatal("should've returned user without any teams") + } + } +} + func TestUserStoreGetAllProfilesInChannel(t *testing.T) { Setup() diff --git a/store/store.go b/store/store.go index 409280918..6523df0c8 100644 --- a/store/store.go +++ b/store/store.go @@ -177,6 +177,7 @@ type UserStore interface { GetProfilesInChannel(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 GetAllProfiles(offset int, limit int) StoreChannel GetProfiles(teamId string, offset int, limit int) StoreChannel -- cgit v1.2.3-1-g7c22