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 <arve.knudsen@gmail.com>

* Update pkg/api/org_users.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Update pkg/models/org_user.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Update pkg/api/org_users_test.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Update pkg/api/org_users_test.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* 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 <arve.knudsen@gmail.com>

* Update pkg/api/org_users.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Update pkg/models/org_user.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Update pkg/api/org_users_test.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Update pkg/api/org_users_test.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* 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 <arve.knudsen@gmail.com>

* Update pkg/api/org_users_test.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Update pkg/api/org_users_test.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Update pkg/services/sqlstore/org_users.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Update pkg/services/sqlstore/org_users.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Update pkg/services/sqlstore/org_test.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* Update pkg/services/sqlstore/org_test.go

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* 32540: Fix search org users method

* 32540: Fix sqlstore test

* 32540: Fix go-lint

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
This commit is contained in:
idafurjes 2021-05-12 14:10:35 +02:00 committed by GitHub
parent 553d3b9dbc
commit f2fcf721eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 212 additions and 1 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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