From f2fcf721ebd0256f18a342cce10095e6aba46120 Mon Sep 17 00:00:00 2001 From: idafurjes <36131195+idafurjes@users.noreply.github.com> Date: Wed, 12 May 2021 14:10:35 +0200 Subject: [PATCH] 32540: Add org users with pagination (#33788) * Add model for search org user and add handler for dispatch * 32540_org_users_with_pagination: Add endpoint for search org users * 32540_org_users_with_pagination: Add test for org user search handler * 32540_org_users_with_pagination: fix indentation * 32540_org_users_with_pagination: Remove newline * 32540_org_users_with_pagination: Remove empty line * 32540_org_users_with_pagination: Fix indentation * Update pkg/api/org_users.go Co-authored-by: Arve Knudsen * Update pkg/api/org_users.go Co-authored-by: Arve Knudsen * Update pkg/models/org_user.go Co-authored-by: Arve Knudsen * Update pkg/api/org_users_test.go Co-authored-by: Arve Knudsen * Update pkg/api/org_users_test.go Co-authored-by: Arve Knudsen * 32540_org_users_with_pagination: Use hs.SQLStore.SearchOrgUsers instead of bus * Add model for search org user and add handler for dispatch * 32540_org_users_with_pagination: Add endpoint for search org users * 32540_org_users_with_pagination: Add test for org user search handler * 32540_org_users_with_pagination: fix indentation * 32540_org_users_with_pagination: Remove newline * 32540_org_users_with_pagination: Remove empty line * 32540_org_users_with_pagination: Fix indentation * Update pkg/api/org_users.go Co-authored-by: Arve Knudsen * Update pkg/api/org_users.go Co-authored-by: Arve Knudsen * Update pkg/models/org_user.go Co-authored-by: Arve Knudsen * Update pkg/api/org_users_test.go Co-authored-by: Arve Knudsen * Update pkg/api/org_users_test.go Co-authored-by: Arve Knudsen * 32540_org_users_with_pagination: Use hs.SQLStore.SearchOrgUsers instead of bus * 32540_org_users_with_pagination: Add test for the sqlstore * 32540_org_users_with_pagination: Fix sqlstore test * Update pkg/api/org_users.go Co-authored-by: Arve Knudsen * Update pkg/api/org_users_test.go Co-authored-by: Arve Knudsen * Update pkg/api/org_users_test.go Co-authored-by: Arve Knudsen * Update pkg/services/sqlstore/org_users.go Co-authored-by: Arve Knudsen * Update pkg/services/sqlstore/org_users.go Co-authored-by: Arve Knudsen * Update pkg/services/sqlstore/org_test.go Co-authored-by: Arve Knudsen * Update pkg/services/sqlstore/org_test.go Co-authored-by: Arve Knudsen * 32540: Fix search org users method * 32540: Fix sqlstore test * 32540: Fix go-lint Co-authored-by: Arve Knudsen --- pkg/api/api.go | 1 + pkg/api/org_users.go | 42 ++++++++++++++++++- pkg/api/org_users_test.go | 52 ++++++++++++++++++++++++ pkg/models/org_user.go | 16 ++++++++ pkg/services/sqlstore/org_test.go | 37 +++++++++++++++++ pkg/services/sqlstore/org_users.go | 65 ++++++++++++++++++++++++++++++ 6 files changed, 212 insertions(+), 1 deletion(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 8524fb49cde..1b39d9f0fa7 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -203,6 +203,7 @@ func (hs *HTTPServer) registerRoutes() { orgRoute.Put("/", reqOrgAdmin, bind(dtos.UpdateOrgForm{}), routing.Wrap(UpdateOrgCurrent)) orgRoute.Put("/address", reqOrgAdmin, bind(dtos.UpdateOrgAddressForm{}), routing.Wrap(UpdateOrgAddressCurrent)) orgRoute.Get("/users", authorize(reqOrgAdmin, accesscontrol.ActionOrgUsersRead, accesscontrol.ScopeUsersAll), routing.Wrap(hs.GetOrgUsersForCurrentOrg)) + orgRoute.Get("/users/search", authorize(reqOrgAdmin, accesscontrol.ActionOrgUsersRead, accesscontrol.ScopeUsersAll), routing.Wrap(hs.SearchOrgUsersWithPaging)) orgRoute.Post("/users", authorize(reqOrgAdmin, accesscontrol.ActionOrgUsersAdd, accesscontrol.ScopeUsersAll), quota("user"), bind(models.AddOrgUserCommand{}), routing.Wrap(AddOrgUserToCurrentOrg)) orgRoute.Patch("/users/:userId", authorize(reqOrgAdmin, accesscontrol.ActionOrgUsersRoleUpdate, usersScope), bind(models.UpdateOrgUserCommand{}), routing.Wrap(UpdateOrgUserForCurrentOrg)) orgRoute.Delete("/users/:userId", authorize(reqOrgAdmin, accesscontrol.ActionOrgUsersRemove, usersScope), routing.Wrap(RemoveOrgUserForCurrentOrg)) diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index 7c2dc3e6457..61f8f19c1e9 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -157,6 +157,47 @@ func (hs *HTTPServer) getOrgUsersHelper(query *models.GetOrgUsersQuery, signedIn return filteredUsers, nil } +// SearchOrgUsersWithPaging is an HTTP handler to search for org users with paging. +// GET /api/org/users/search +func (hs *HTTPServer) SearchOrgUsersWithPaging(c *models.ReqContext) response.Response { + perPage := c.QueryInt("perpage") + if perPage <= 0 { + perPage = 1000 + } + page := c.QueryInt("page") + + if page < 1 { + page = 1 + } + + query := &models.SearchOrgUsersQuery{ + OrgID: c.OrgId, + Query: c.Query("query"), + Limit: perPage, + Page: page, + } + + if err := hs.SQLStore.SearchOrgUsers(query); err != nil { + return response.Error(500, "Failed to get users for current organization", err) + } + + filteredUsers := make([]*models.OrgUserDTO, 0, len(query.Result.OrgUsers)) + for _, user := range query.Result.OrgUsers { + if dtos.IsHiddenUser(user.Login, c.SignedInUser, hs.Cfg) { + continue + } + user.AvatarUrl = dtos.GetGravatarUrl(user.Email) + + filteredUsers = append(filteredUsers, user) + } + + query.Result.OrgUsers = filteredUsers + query.Result.Page = page + query.Result.PerPage = perPage + + return response.JSON(200, query.Result) +} + // PATCH /api/org/users/:userId func UpdateOrgUserForCurrentOrg(c *models.ReqContext, cmd models.UpdateOrgUserCommand) response.Response { cmd.OrgId = c.OrgId @@ -175,7 +216,6 @@ func updateOrgUserHelper(cmd models.UpdateOrgUserCommand) response.Response { if !cmd.Role.IsValid() { return response.Error(400, "Invalid role specified", nil) } - if err := bus.Dispatch(&cmd); err != nil { if errors.Is(err, models.ErrLastOrgAdmin) { return response.Error(400, "Cannot change role so that there is no organization admin left", nil) diff --git a/pkg/api/org_users_test.go b/pkg/api/org_users_test.go index 34445e128b9..8b71a024212 100644 --- a/pkg/api/org_users_test.go +++ b/pkg/api/org_users_test.go @@ -1,6 +1,7 @@ package api import ( + "context" "encoding/json" "net/http" "testing" @@ -8,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -24,10 +26,24 @@ func setUpGetOrgUsersHandler() { }) } +func setUpGetOrgUsersDB(t *testing.T, sqlStore *sqlstore.SQLStore) { + setting.AutoAssignOrg = true + setting.AutoAssignOrgId = 1 + + _, err := sqlStore.CreateUser(context.Background(), models.CreateUserCommand{Email: "testUser@grafana.com", Login: "testUserLogin"}) + require.NoError(t, err) + _, err = sqlStore.CreateUser(context.Background(), models.CreateUserCommand{Email: "user1@grafana.com", Login: "user1"}) + require.NoError(t, err) + _, err = sqlStore.CreateUser(context.Background(), models.CreateUserCommand{Email: "user2@grafana.com", Login: "user2"}) + require.NoError(t, err) +} + func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) { settings := setting.NewCfg() hs := &HTTPServer{Cfg: settings} + sqlStore := sqlstore.InitTestDB(t) + loggedInUserScenario(t, "When calling GET on", "api/org/users", func(sc *scenarioContext) { setUpGetOrgUsersHandler() @@ -42,6 +58,42 @@ func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) { assert.Len(t, resp, 3) }) + loggedInUserScenario(t, "When calling GET on", "api/org/users/search", func(sc *scenarioContext) { + setUpGetOrgUsersDB(t, sqlStore) + + sc.handlerFunc = hs.SearchOrgUsersWithPaging + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + + require.Equal(t, http.StatusOK, sc.resp.Code) + + var resp models.SearchOrgUsersQueryResult + err := json.Unmarshal(sc.resp.Body.Bytes(), &resp) + require.NoError(t, err) + + assert.Len(t, resp.OrgUsers, 3) + assert.Equal(t, int64(3), resp.TotalCount) + assert.Equal(t, 1000, resp.PerPage) + assert.Equal(t, 1, resp.Page) + }) + + loggedInUserScenario(t, "When calling GET with page and limit query parameters on", "api/org/users/search", func(sc *scenarioContext) { + setUpGetOrgUsersDB(t, sqlStore) + + sc.handlerFunc = hs.SearchOrgUsersWithPaging + sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "2", "page": "2"}).exec() + + require.Equal(t, http.StatusOK, sc.resp.Code) + + var resp models.SearchOrgUsersQueryResult + err := json.Unmarshal(sc.resp.Body.Bytes(), &resp) + require.NoError(t, err) + + assert.Len(t, resp.OrgUsers, 1) + assert.Equal(t, int64(3), resp.TotalCount) + assert.Equal(t, 2, resp.PerPage) + assert.Equal(t, 2, resp.Page) + }) + loggedInUserScenario(t, "When calling GET as an editor with no team / folder permissions on", "api/org/users/lookup", func(sc *scenarioContext) { setUpGetOrgUsersHandler() diff --git a/pkg/models/org_user.go b/pkg/models/org_user.go index e3823f880a1..c432746852e 100644 --- a/pkg/models/org_user.go +++ b/pkg/models/org_user.go @@ -114,6 +114,22 @@ type GetOrgUsersQuery struct { Result []*OrgUserDTO } +type SearchOrgUsersQuery struct { + OrgID int64 + Query string + Page int + Limit int + + Result SearchOrgUsersQueryResult +} + +type SearchOrgUsersQueryResult struct { + TotalCount int64 `json:"totalCount"` + OrgUsers []*OrgUserDTO `json:"OrgUsers"` + Page int `json:"page"` + PerPage int `json:"perPage"` +} + // ---------------------- // Projections and DTOs diff --git a/pkg/services/sqlstore/org_test.go b/pkg/services/sqlstore/org_test.go index 37a93ed7172..4f089b0fe8d 100644 --- a/pkg/services/sqlstore/org_test.go +++ b/pkg/services/sqlstore/org_test.go @@ -95,6 +95,43 @@ func TestAccountDataAccess(t *testing.T) { }) }) + Convey("Given single org and 2 users inserted", func() { + setting.AutoAssignOrg = true + setting.AutoAssignOrgId = 1 + setting.AutoAssignOrgRole = "Viewer" + + ac1cmd := models.CreateUserCommand{Login: "ac1", Email: "ac1@test.com", Name: "ac1 name"} + ac2cmd := models.CreateUserCommand{Login: "ac2", Email: "ac2@test.com", Name: "ac2 name"} + + ac1, err := sqlStore.CreateUser(context.Background(), ac1cmd) + So(err, ShouldBeNil) + _, err = sqlStore.CreateUser(context.Background(), ac2cmd) + So(err, ShouldBeNil) + + Convey("Can get organization users paginated with query", func() { + query := models.SearchOrgUsersQuery{ + OrgID: ac1.OrgId, + Page: 1, + } + err = sqlStore.SearchOrgUsers(&query) + + So(err, ShouldBeNil) + So(len(query.Result.OrgUsers), ShouldEqual, 2) + }) + + Convey("Can get organization users paginated and limited", func() { + query := models.SearchOrgUsersQuery{ + OrgID: ac1.OrgId, + Limit: 1, + Page: 1, + } + err = sqlStore.SearchOrgUsers(&query) + + So(err, ShouldBeNil) + So(len(query.Result.OrgUsers), ShouldEqual, 1) + }) + }) + Convey("Given two saved users", func() { setting.AutoAssignOrg = false diff --git a/pkg/services/sqlstore/org_users.go b/pkg/services/sqlstore/org_users.go index 320e25a584b..7105634c42d 100644 --- a/pkg/services/sqlstore/org_users.go +++ b/pkg/services/sqlstore/org_users.go @@ -142,6 +142,71 @@ func GetOrgUsers(query *models.GetOrgUsersQuery) error { return nil } +func (ss *SQLStore) SearchOrgUsers(query *models.SearchOrgUsersQuery) error { + query.Result = models.SearchOrgUsersQueryResult{ + OrgUsers: make([]*models.OrgUserDTO, 0), + } + + sess := x.Table("org_user") + sess.Join("INNER", x.Dialect().Quote("user"), fmt.Sprintf("org_user.user_id=%s.id", x.Dialect().Quote("user"))) + + whereConditions := make([]string, 0) + whereParams := make([]interface{}, 0) + + whereConditions = append(whereConditions, "org_user.org_id = ?") + whereParams = append(whereParams, query.OrgID) + + if query.Query != "" { + queryWithWildcards := "%" + query.Query + "%" + whereConditions = append(whereConditions, "(email "+dialect.LikeStr()+" ? OR name "+dialect.LikeStr()+" ? OR login "+dialect.LikeStr()+" ?)") + whereParams = append(whereParams, queryWithWildcards, queryWithWildcards, queryWithWildcards) + } + + if len(whereConditions) > 0 { + sess.Where(strings.Join(whereConditions, " AND "), whereParams...) + } + + if query.Limit > 0 { + offset := query.Limit * (query.Page - 1) + sess.Limit(query.Limit, offset) + } + + sess.Cols( + "org_user.org_id", + "org_user.user_id", + "user.email", + "user.name", + "user.login", + "org_user.role", + "user.last_seen_at", + ) + sess.Asc("user.email", "user.login") + + if err := sess.Find(&query.Result.OrgUsers); err != nil { + return err + } + + // get total count + orgUser := models.OrgUser{} + countSess := x.Table("org_user") + + if len(whereConditions) > 0 { + countSess.Where(strings.Join(whereConditions, " AND "), whereParams...) + } + + count, err := countSess.Count(&orgUser) + if err != nil { + return err + } + query.Result.TotalCount = count + + for _, user := range query.Result.OrgUsers { + user.LastSeenAtAge = util.GetAgeString(user.LastSeenAt) + } + + return nil +} + func RemoveOrgUser(cmd *models.RemoveOrgUserCommand) error { return inTransaction(func(sess *DBSession) error { // check if user exists