diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index 9b4fe3dfdfd..d66be5cdd06 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -3,11 +3,13 @@ package api import ( "context" "errors" + "fmt" "net/http" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" ) @@ -65,7 +67,7 @@ func (hs *HTTPServer) addOrgUserHelper(ctx context.Context, cmd models.AddOrgUse // GET /api/org/users func (hs *HTTPServer) GetOrgUsersForCurrentOrg(c *models.ReqContext) response.Response { - result, err := hs.getOrgUsersHelper(c.Req.Context(), &models.GetOrgUsersQuery{ + result, err := hs.getOrgUsersHelper(c, &models.GetOrgUsersQuery{ OrgId: c.OrgId, Query: c.Query("query"), Limit: c.QueryInt("limit"), @@ -80,7 +82,7 @@ func (hs *HTTPServer) GetOrgUsersForCurrentOrg(c *models.ReqContext) response.Re // GET /api/org/users/lookup func (hs *HTTPServer) GetOrgUsersForCurrentOrgLookup(c *models.ReqContext) response.Response { - orgUsers, err := hs.getOrgUsersHelper(c.Req.Context(), &models.GetOrgUsersQuery{ + orgUsers, err := hs.getOrgUsersHelper(c, &models.GetOrgUsersQuery{ OrgId: c.OrgId, Query: c.Query("query"), Limit: c.QueryInt("limit"), @@ -103,9 +105,30 @@ func (hs *HTTPServer) GetOrgUsersForCurrentOrgLookup(c *models.ReqContext) respo return response.JSON(200, result) } +func (hs *HTTPServer) getUserAccessControlMetadata(c *models.ReqContext, userID int64) (accesscontrol.Metadata, error) { + if hs.AccessControl == nil || hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") { + return nil, nil + } + + userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser) + if err != nil || len(userPermissions) == 0 { + return nil, err + } + + key := fmt.Sprintf("%d", userID) + userIDs := map[string]bool{key: true} + + metadata, err := accesscontrol.GetResourcesMetadata(c.Req.Context(), userPermissions, "users", userIDs) + if err != nil { + return nil, err + } + + return metadata[key], err +} + // GET /api/orgs/:orgId/users func (hs *HTTPServer) GetOrgUsers(c *models.ReqContext) response.Response { - result, err := hs.getOrgUsersHelper(c.Req.Context(), &models.GetOrgUsersQuery{ + result, err := hs.getOrgUsersHelper(c, &models.GetOrgUsersQuery{ OrgId: c.ParamsInt64(":orgId"), Query: "", Limit: 0, @@ -118,8 +141,8 @@ func (hs *HTTPServer) GetOrgUsers(c *models.ReqContext) response.Response { return response.JSON(200, result) } -func (hs *HTTPServer) getOrgUsersHelper(ctx context.Context, query *models.GetOrgUsersQuery, signedInUser *models.SignedInUser) ([]*models.OrgUserDTO, error) { - if err := hs.SQLStore.GetOrgUsers(ctx, query); err != nil { +func (hs *HTTPServer) getOrgUsersHelper(c *models.ReqContext, query *models.GetOrgUsersQuery, signedInUser *models.SignedInUser) ([]*models.OrgUserDTO, error) { + if err := hs.SQLStore.GetOrgUsers(c.Req.Context(), query); err != nil { return nil, err } @@ -130,6 +153,13 @@ func (hs *HTTPServer) getOrgUsersHelper(ctx context.Context, query *models.GetOr } user.AvatarUrl = dtos.GetGravatarUrl(user.Email) + accessControlMetadata, errAC := hs.getUserAccessControlMetadata(c, user.UserId) + if errAC != nil { + hs.log.Error("Failed to get access control metadata", "error", errAC) + } + + user.AccessControl = accessControlMetadata + filteredUsers = append(filteredUsers, user) } diff --git a/pkg/api/org_users_test.go b/pkg/api/org_users_test.go index 6f1b017a33a..af135129a78 100644 --- a/pkg/api/org_users_test.go +++ b/pkg/api/org_users_test.go @@ -297,6 +297,63 @@ func setupOrgUsersDBForAccessControlTests(t *testing.T, db sqlstore.SQLStore) { require.NoError(t, err) } +func TestGetOrgUsersAPIEndpoint_AccessControlMetadata(t *testing.T) { + url := "/api/orgs/%v/users?accesscontrol=true" + type testCase struct { + name string + enableAccessControl bool + expectedCode int + expectedMetadata map[string]bool + user models.SignedInUser + targetOrg int64 + } + + tests := []testCase{ + { + name: "access control metadata not requested", + enableAccessControl: false, + expectedCode: http.StatusOK, + expectedMetadata: nil, + user: testServerAdminViewer, + targetOrg: testServerAdminViewer.OrgId, + }, + { + name: "access control metadata requested", + enableAccessControl: true, + expectedCode: http.StatusOK, + expectedMetadata: map[string]bool{ + "org.users.role:update": true, + "org.users:add": true, + "org.users:read": true, + "org.users:remove": true}, + user: testServerAdminViewer, + targetOrg: testServerAdminViewer.OrgId, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + sc := setupHTTPServer(t, false, tc.enableAccessControl) + setupOrgUsersDBForAccessControlTests(t, *sc.db) + setInitCtxSignedInUser(sc.initCtx, tc.user) + + // Perform test + response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(url, tc.targetOrg), nil, t) + require.Equal(t, tc.expectedCode, response.Code) + + var userList []*models.OrgUserDTO + err := json.NewDecoder(response.Body).Decode(&userList) + require.NoError(t, err) + + if tc.expectedMetadata != nil { + assert.Equal(t, tc.expectedMetadata, userList[0].AccessControl) + } else { + assert.Nil(t, userList[0].AccessControl) + } + }) + } +} + func TestGetOrgUsersAPIEndpoint_AccessControl(t *testing.T) { url := "/api/orgs/%v/users/" type testCase struct { diff --git a/pkg/models/org_user.go b/pkg/models/org_user.go index c432746852e..4c6ce11a00a 100644 --- a/pkg/models/org_user.go +++ b/pkg/models/org_user.go @@ -134,13 +134,14 @@ type SearchOrgUsersQueryResult struct { // Projections and DTOs type OrgUserDTO struct { - OrgId int64 `json:"orgId"` - UserId int64 `json:"userId"` - Email string `json:"email"` - Name string `json:"name"` - AvatarUrl string `json:"avatarUrl"` - Login string `json:"login"` - Role string `json:"role"` - LastSeenAt time.Time `json:"lastSeenAt"` - LastSeenAtAge string `json:"lastSeenAtAge"` + OrgId int64 `json:"orgId"` + UserId int64 `json:"userId"` + Email string `json:"email"` + Name string `json:"name"` + AvatarUrl string `json:"avatarUrl"` + Login string `json:"login"` + Role string `json:"role"` + LastSeenAt time.Time `json:"lastSeenAt"` + LastSeenAtAge string `json:"lastSeenAtAge"` + AccessControl map[string]bool `json:"accessControl,omitempty"` }