mirror of
https://github.com/grafana/grafana.git
synced 2025-09-28 06:54:03 +08:00
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:
@ -203,6 +203,7 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
orgRoute.Put("/", reqOrgAdmin, bind(dtos.UpdateOrgForm{}), routing.Wrap(UpdateOrgCurrent))
|
orgRoute.Put("/", reqOrgAdmin, bind(dtos.UpdateOrgForm{}), routing.Wrap(UpdateOrgCurrent))
|
||||||
orgRoute.Put("/address", reqOrgAdmin, bind(dtos.UpdateOrgAddressForm{}), routing.Wrap(UpdateOrgAddressCurrent))
|
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", 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.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.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))
|
orgRoute.Delete("/users/:userId", authorize(reqOrgAdmin, accesscontrol.ActionOrgUsersRemove, usersScope), routing.Wrap(RemoveOrgUserForCurrentOrg))
|
||||||
|
@ -157,6 +157,47 @@ func (hs *HTTPServer) getOrgUsersHelper(query *models.GetOrgUsersQuery, signedIn
|
|||||||
return filteredUsers, nil
|
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
|
// PATCH /api/org/users/:userId
|
||||||
func UpdateOrgUserForCurrentOrg(c *models.ReqContext, cmd models.UpdateOrgUserCommand) response.Response {
|
func UpdateOrgUserForCurrentOrg(c *models.ReqContext, cmd models.UpdateOrgUserCommand) response.Response {
|
||||||
cmd.OrgId = c.OrgId
|
cmd.OrgId = c.OrgId
|
||||||
@ -175,7 +216,6 @@ func updateOrgUserHelper(cmd models.UpdateOrgUserCommand) response.Response {
|
|||||||
if !cmd.Role.IsValid() {
|
if !cmd.Role.IsValid() {
|
||||||
return response.Error(400, "Invalid role specified", nil)
|
return response.Error(400, "Invalid role specified", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bus.Dispatch(&cmd); err != nil {
|
if err := bus.Dispatch(&cmd); err != nil {
|
||||||
if errors.Is(err, models.ErrLastOrgAdmin) {
|
if errors.Is(err, models.ErrLastOrgAdmin) {
|
||||||
return response.Error(400, "Cannot change role so that there is no organization admin left", nil)
|
return response.Error(400, "Cannot change role so that there is no organization admin left", nil)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
@ -8,6 +9,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"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) {
|
func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) {
|
||||||
settings := setting.NewCfg()
|
settings := setting.NewCfg()
|
||||||
hs := &HTTPServer{Cfg: settings}
|
hs := &HTTPServer{Cfg: settings}
|
||||||
|
|
||||||
|
sqlStore := sqlstore.InitTestDB(t)
|
||||||
|
|
||||||
loggedInUserScenario(t, "When calling GET on", "api/org/users", func(sc *scenarioContext) {
|
loggedInUserScenario(t, "When calling GET on", "api/org/users", func(sc *scenarioContext) {
|
||||||
setUpGetOrgUsersHandler()
|
setUpGetOrgUsersHandler()
|
||||||
|
|
||||||
@ -42,6 +58,42 @@ func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) {
|
|||||||
assert.Len(t, resp, 3)
|
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",
|
loggedInUserScenario(t, "When calling GET as an editor with no team / folder permissions on",
|
||||||
"api/org/users/lookup", func(sc *scenarioContext) {
|
"api/org/users/lookup", func(sc *scenarioContext) {
|
||||||
setUpGetOrgUsersHandler()
|
setUpGetOrgUsersHandler()
|
||||||
|
@ -114,6 +114,22 @@ type GetOrgUsersQuery struct {
|
|||||||
Result []*OrgUserDTO
|
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
|
// Projections and DTOs
|
||||||
|
|
||||||
|
@ -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() {
|
Convey("Given two saved users", func() {
|
||||||
setting.AutoAssignOrg = false
|
setting.AutoAssignOrg = false
|
||||||
|
|
||||||
|
@ -142,6 +142,71 @@ func GetOrgUsers(query *models.GetOrgUsersQuery) error {
|
|||||||
return nil
|
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 {
|
func RemoveOrgUser(cmd *models.RemoveOrgUserCommand) error {
|
||||||
return inTransaction(func(sess *DBSession) error {
|
return inTransaction(func(sess *DBSession) error {
|
||||||
// check if user exists
|
// check if user exists
|
||||||
|
Reference in New Issue
Block a user