summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHarrison Healey <harrisonmhealey@gmail.com>2017-03-29 21:11:40 -0400
committerJoram Wilander <jwawilander@gmail.com>2017-03-29 21:11:40 -0400
commit6ac87d82e38c83e3b9b3bd12c3122e047f0110b1 (patch)
treeaa3f0f8f41df7c987f8bb62756ca3d8259ecbaa1
parenta4764a5c10ec59820eec7338d97be48d41c1a4d6 (diff)
downloadchat-6ac87d82e38c83e3b9b3bd12c3122e047f0110b1.tar.gz
chat-6ac87d82e38c83e3b9b3bd12c3122e047f0110b1.tar.bz2
chat-6ac87d82e38c83e3b9b3bd12c3122e047f0110b1.zip
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
-rw-r--r--api4/user.go11
-rw-r--r--api4/user_test.go50
-rw-r--r--app/user.go21
-rw-r--r--model/authorization.go7
-rw-r--r--model/client4.go11
-rw-r--r--store/sql_user_store.go48
-rw-r--r--store/sql_user_store_test.go43
-rw-r--r--store/store.go1
8 files changed, 191 insertions, 1 deletions
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