Preferences: Add pagination to org configuration page (#60896)

* Add auth labels and access control metadata to org users search results

* Fix search result JSON model

* Org users: Use API for pagination

* Fix default page size

* Refactor: UsersListPage to functional component

* Refactor: update UsersTable component code style

* Add pagination to the /orgs/{org_id}/users endpoint

* Use pagination on the AdminEditOrgPage

* Add /orgs/{org_id}/users/search endpoint to prevent breaking API

* Use existing search store method

* Remove unnecessary error

* Remove unused

* Add query param to search endpoint

* Fix endpoint docs

* Minor refactor

* Fix number of pages calculation

* Use SearchOrgUsers for all org users methods

* Refactor: GetOrgUsers as a service method

* Minor refactor: rename orgId => orgID

* Fix integration tests

* Fix tests
This commit is contained in:
Alexander Zobnin
2023-01-09 11:54:33 +03:00
committed by GitHub
parent d44de7f20a
commit f1b5014efd
22 changed files with 454 additions and 410 deletions

View File

@@ -367,6 +367,7 @@ func (hs *HTTPServer) registerRoutes() {
orgsRoute.Put("/address", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgsWrite)), routing.Wrap(hs.UpdateOrgAddress))
orgsRoute.Delete("/", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgsDelete)), routing.Wrap(hs.DeleteOrgByID))
orgsRoute.Get("/users", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersRead)), routing.Wrap(hs.GetOrgUsers))
orgsRoute.Get("/users/search", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersRead)), routing.Wrap(hs.SearchOrgUsers))
orgsRoute.Post("/users", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersAdd, ac.ScopeUsersAll)), routing.Wrap(hs.AddOrgUser))
orgsRoute.Patch("/users/:userId", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersWrite, userIDScope)), routing.Wrap(hs.UpdateOrgUser))
orgsRoute.Delete("/users/:userId", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersRemove, userIDScope)), routing.Wrap(hs.RemoveOrgUser))

View File

@@ -398,6 +398,7 @@ func setupHTTPServerWithCfgDb(
userMock.ExpectedUser = &user.User{ID: 1}
orgMock := orgtest.NewOrgServiceFake()
orgMock.ExpectedOrg = &org.Org{}
orgMock.ExpectedSearchOrgUsersResult = &org.SearchOrgUsersQueryResult{}
// Defining the accesscontrol service has to be done before registering routes
if useFakeAccessControl {

View File

@@ -115,18 +115,18 @@ func (hs *HTTPServer) addOrgUserHelper(c *models.ReqContext, cmd org.AddOrgUserC
// 403: forbiddenError
// 500: internalServerError
func (hs *HTTPServer) GetOrgUsersForCurrentOrg(c *models.ReqContext) response.Response {
result, err := hs.getOrgUsersHelper(c, &org.GetOrgUsersQuery{
result, err := hs.searchOrgUsersHelper(c, &org.SearchOrgUsersQuery{
OrgID: c.OrgID,
Query: c.Query("query"),
Limit: c.QueryInt("limit"),
User: c.SignedInUser,
}, c.SignedInUser)
})
if err != nil {
return response.Error(500, "Failed to get users for current organization", err)
}
return response.JSON(http.StatusOK, result)
return response.JSON(http.StatusOK, result.OrgUsers)
}
// swagger:route GET /org/users/lookup org getOrgUsersForCurrentOrgLookup
@@ -144,13 +144,13 @@ func (hs *HTTPServer) GetOrgUsersForCurrentOrg(c *models.ReqContext) response.Re
// 500: internalServerError
func (hs *HTTPServer) GetOrgUsersForCurrentOrgLookup(c *models.ReqContext) response.Response {
orgUsers, err := hs.getOrgUsersHelper(c, &org.GetOrgUsersQuery{
orgUsersResult, err := hs.searchOrgUsersHelper(c, &org.SearchOrgUsersQuery{
OrgID: c.OrgID,
Query: c.Query("query"),
Limit: c.QueryInt("limit"),
User: c.SignedInUser,
DontEnforceAccessControl: !hs.License.FeatureEnabled("accesscontrol.enforcement"),
}, c.SignedInUser)
})
if err != nil {
return response.Error(500, "Failed to get users for current organization", err)
@@ -158,7 +158,7 @@ func (hs *HTTPServer) GetOrgUsersForCurrentOrgLookup(c *models.ReqContext) respo
result := make([]*dtos.UserLookupDTO, 0)
for _, u := range orgUsers {
for _, u := range orgUsersResult.OrgUsers {
result = append(result, &dtos.UserLookupDTO{
UserID: u.UserID,
Login: u.Login,
@@ -190,12 +190,58 @@ func (hs *HTTPServer) GetOrgUsers(c *models.ReqContext) response.Response {
return response.Error(http.StatusBadRequest, "orgId is invalid", err)
}
result, err := hs.getOrgUsersHelper(c, &org.GetOrgUsersQuery{
result, err := hs.searchOrgUsersHelper(c, &org.SearchOrgUsersQuery{
OrgID: orgId,
Query: "",
Limit: 0,
User: c.SignedInUser,
}, c.SignedInUser)
})
if err != nil {
return response.Error(500, "Failed to get users for organization", err)
}
return response.JSON(http.StatusOK, result.OrgUsers)
}
// swagger:route GET /orgs/{org_id}/users/search orgs searchOrgUsers
//
// Search Users in Organization.
//
// If you are running Grafana Enterprise and have Fine-grained access control enabled
// you need to have a permission with action: `org.users:read` with scope `users:*`.
//
// Security:
// - basic:
//
// Responses:
// 200: searchOrgUsersResponse
// 401: unauthorisedError
// 403: forbiddenError
// 500: internalServerError
func (hs *HTTPServer) SearchOrgUsers(c *models.ReqContext) response.Response {
orgID, err := strconv.ParseInt(web.Params(c.Req)[":orgId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "orgId is invalid", err)
}
perPage := c.QueryInt("perpage")
if perPage <= 0 {
perPage = 1000
}
page := c.QueryInt("page")
if page < 1 {
page = 1
}
result, err := hs.searchOrgUsersHelper(c, &org.SearchOrgUsersQuery{
OrgID: orgID,
Query: c.Query("query"),
Page: page,
Limit: perPage,
User: c.SignedInUser,
})
if err != nil {
return response.Error(500, "Failed to get users for organization", err)
@@ -204,17 +250,46 @@ func (hs *HTTPServer) GetOrgUsers(c *models.ReqContext) response.Response {
return response.JSON(http.StatusOK, result)
}
func (hs *HTTPServer) getOrgUsersHelper(c *models.ReqContext, query *org.GetOrgUsersQuery, signedInUser *user.SignedInUser) ([]*org.OrgUserDTO, error) {
result, err := hs.orgService.GetOrgUsers(c.Req.Context(), query)
// 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 := &org.SearchOrgUsersQuery{
OrgID: c.OrgID,
Query: c.Query("query"),
Page: page,
Limit: perPage,
User: c.SignedInUser,
}
result, err := hs.searchOrgUsersHelper(c, query)
if err != nil {
return response.Error(500, "Failed to get users for current organization", err)
}
return response.JSON(http.StatusOK, result)
}
func (hs *HTTPServer) searchOrgUsersHelper(c *models.ReqContext, query *org.SearchOrgUsersQuery) (*org.SearchOrgUsersQueryResult, error) {
result, err := hs.orgService.SearchOrgUsers(c.Req.Context(), query)
if err != nil {
return nil, err
}
filteredUsers := make([]*org.OrgUserDTO, 0, len(result))
filteredUsers := make([]*org.OrgUserDTO, 0, len(result.OrgUsers))
userIDs := map[string]bool{}
authLabelsUserIDs := make([]int64, 0, len(result))
for _, user := range result {
if dtos.IsHiddenUser(user.Login, signedInUser, hs.Cfg) {
authLabelsUserIDs := make([]int64, 0, len(result.OrgUsers))
for _, user := range result.OrgUsers {
if dtos.IsHiddenUser(user.Login, c.SignedInUser, hs.Cfg) {
continue
}
user.AvatarURL = dtos.GetGravatarUrl(user.Email)
@@ -241,51 +316,10 @@ func (hs *HTTPServer) getOrgUsersHelper(c *models.ReqContext, query *org.GetOrgU
}
}
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 {
ctx := c.Req.Context()
perPage := c.QueryInt("perpage")
if perPage <= 0 {
perPage = 1000
}
page := c.QueryInt("page")
if page < 1 {
page = 1
}
query := &org.SearchOrgUsersQuery{
OrgID: c.OrgID,
Query: c.Query("query"),
Page: page,
Limit: perPage,
User: c.SignedInUser,
}
result, err := hs.orgService.SearchOrgUsers(ctx, query)
if err != nil {
return response.Error(500, "Failed to get users for current organization", err)
}
filteredUsers := make([]*org.OrgUserDTO, 0, len(result.OrgUsers))
for _, user := range result.OrgUsers {
if dtos.IsHiddenUser(user.Login, c.SignedInUser, hs.Cfg) {
continue
}
user.AvatarURL = dtos.GetGravatarUrl(user.Email)
filteredUsers = append(filteredUsers, user)
}
result.OrgUsers = filteredUsers
result.Page = page
result.PerPage = perPage
return response.JSON(http.StatusOK, result)
result.Page = query.Page
result.PerPage = query.Limit
return result, nil
}
// swagger:route PATCH /org/users/{user_id} org updateOrgUserForCurrentOrg

View File

@@ -63,12 +63,15 @@ func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) {
orgService.ExpectedSearchOrgUsersResult = &org.SearchOrgUsersQueryResult{}
hs.orgService = orgService
mock := mockstore.NewSQLStoreMock()
loggedInUserScenario(t, "When calling GET on", "api/org/users", "api/org/users", func(sc *scenarioContext) {
setUpGetOrgUsersDB(t, sqlStore)
orgService.ExpectedOrgUsers = []*org.OrgUserDTO{
{Login: testUserLogin, Email: "testUser@grafana.com"},
{Login: "user1", Email: "user1@grafana.com"},
{Login: "user2", Email: "user2@grafana.com"},
orgService.ExpectedSearchOrgUsersResult = &org.SearchOrgUsersQueryResult{
OrgUsers: []*org.OrgUserDTO{
{Login: testUserLogin, Email: "testUser@grafana.com"},
{Login: "user1", Email: "user1@grafana.com"},
{Login: "user2", Email: "user2@grafana.com"},
},
}
sc.handlerFunc = hs.GetOrgUsersForCurrentOrg
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
@@ -153,6 +156,13 @@ func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) {
loggedInUserScenario(t, "When calling GET on", "api/org/users", "api/org/users", func(sc *scenarioContext) {
setUpGetOrgUsersDB(t, sqlStore)
orgService.ExpectedSearchOrgUsersResult = &org.SearchOrgUsersQueryResult{
OrgUsers: []*org.OrgUserDTO{
{Login: testUserLogin, Email: "testUser@grafana.com"},
{Login: "user1", Email: "user1@grafana.com"},
{Login: "user2", Email: "user2@grafana.com"},
},
}
sc.handlerFunc = hs.GetOrgUsersForCurrentOrg
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()