diff --git a/pkg/api/api.go b/pkg/api/api.go index ebd4a2e5f33..6c7940b5fc3 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -141,7 +141,7 @@ func (hs *HTTPServer) registerRoutes() { r.Group("/api", func(apiRoute routing.RouteRegister) { // user (signed in) apiRoute.Group("/user", func(userRoute routing.RouteRegister) { - userRoute.Get("/", routing.Wrap(GetSignedInUser)) + userRoute.Get("/", routing.Wrap(hs.GetSignedInUser)) userRoute.Put("/", routing.Wrap(UpdateSignedInUser)) userRoute.Post("/using/:id", routing.Wrap(UserSetUsingOrg)) userRoute.Get("/orgs", routing.Wrap(GetSignedInUserOrgList)) @@ -167,7 +167,7 @@ func (hs *HTTPServer) registerRoutes() { userIDScope := ac.Scope("global", "users", "id", ac.Parameter(":id")) usersRoute.Get("/", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead, ac.ScopeGlobalUsersAll)), routing.Wrap(hs.searchUsersService.SearchUsers)) usersRoute.Get("/search", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead, ac.ScopeGlobalUsersAll)), routing.Wrap(hs.searchUsersService.SearchUsersWithPaging)) - usersRoute.Get("/:id", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead, userIDScope)), routing.Wrap(GetUserByID)) + usersRoute.Get("/:id", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead, userIDScope)), routing.Wrap(hs.GetUserByID)) usersRoute.Get("/:id/teams", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersTeamRead, userIDScope)), routing.Wrap(GetUserTeams)) usersRoute.Get("/:id/orgs", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead, userIDScope)), routing.Wrap(GetUserOrgList)) // query parameters /users/lookup?loginOrEmail=admin@example.com diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index d66be5cdd06..c0d896d119c 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -105,7 +105,7 @@ 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) { +func (hs *HTTPServer) getUserAccessControlMetadata(c *models.ReqContext, resourceIDs map[string]bool) (map[string]accesscontrol.Metadata, error) { if hs.AccessControl == nil || hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") { return nil, nil } @@ -115,15 +115,7 @@ func (hs *HTTPServer) getUserAccessControlMetadata(c *models.ReqContext, userID 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 + return accesscontrol.GetResourcesMetadata(c.Req.Context(), userPermissions, "users", resourceIDs) } // GET /api/orgs/:orgId/users @@ -147,22 +139,28 @@ func (hs *HTTPServer) getOrgUsersHelper(c *models.ReqContext, query *models.GetO } filteredUsers := make([]*models.OrgUserDTO, 0, len(query.Result)) + userIDs := map[string]bool{} for _, user := range query.Result { if dtos.IsHiddenUser(user.Login, signedInUser, hs.Cfg) { continue } 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 - + userIDs[fmt.Sprint(user.UserId)] = true filteredUsers = append(filteredUsers, user) } + accessControlMetadata, errAC := hs.getUserAccessControlMetadata(c, userIDs) + if errAC != nil { + hs.log.Error("Failed to get access control metadata", "error", errAC) + + return filteredUsers, nil + } + + for i := range filteredUsers { + filteredUsers[i].AccessControl = accessControlMetadata[fmt.Sprint(filteredUsers[i].UserId)] + } + return filteredUsers, nil } diff --git a/pkg/api/user.go b/pkg/api/user.go index 7ccf2915a9d..decfcf63383 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -3,31 +3,33 @@ 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/bus" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" ) // GET /api/user (current authenticated user) -func GetSignedInUser(c *models.ReqContext) response.Response { - return getUserUserProfile(c.Req.Context(), c.UserId) +func (hs *HTTPServer) GetSignedInUser(c *models.ReqContext) response.Response { + return hs.getUserUserProfile(c, c.UserId) } // GET /api/users/:id -func GetUserByID(c *models.ReqContext) response.Response { - return getUserUserProfile(c.Req.Context(), c.ParamsInt64(":id")) +func (hs *HTTPServer) GetUserByID(c *models.ReqContext) response.Response { + return hs.getUserUserProfile(c, c.ParamsInt64(":id")) } -func getUserUserProfile(ctx context.Context, userID int64) response.Response { +func (hs *HTTPServer) getUserUserProfile(c *models.ReqContext, userID int64) response.Response { query := models.GetUserProfileQuery{UserId: userID} - if err := bus.Dispatch(ctx, &query); err != nil { + if err := bus.Dispatch(c.Req.Context(), &query); err != nil { if errors.Is(err, models.ErrUserNotFound) { return response.Error(404, models.ErrUserNotFound.Error(), nil) } @@ -36,17 +38,44 @@ func getUserUserProfile(ctx context.Context, userID int64) response.Response { getAuthQuery := models.GetAuthInfoQuery{UserId: userID} query.Result.AuthLabels = []string{} - if err := bus.Dispatch(ctx, &getAuthQuery); err == nil { + if err := bus.Dispatch(c.Req.Context(), &getAuthQuery); err == nil { authLabel := GetAuthProviderLabel(getAuthQuery.Result.AuthModule) query.Result.AuthLabels = append(query.Result.AuthLabels, authLabel) query.Result.IsExternal = true } + accessControlMetadata, errAC := hs.getGlobalUserAccessControlMetadata(c, userID) + if errAC != nil { + hs.log.Error("Failed to get access control metadata", "error", errAC) + } + + query.Result.AccessControl = accessControlMetadata query.Result.AvatarUrl = dtos.GetGravatarUrl(query.Result.Email) return response.JSON(200, query.Result) } +func (hs *HTTPServer) getGlobalUserAccessControlMetadata(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, "global:users", userIDs) + if err != nil { + return nil, err + } + + return metadata[key], err +} + // GET /api/users/lookup func GetUserByLoginOrEmail(c *models.ReqContext) response.Response { query := models.GetUserByLoginQuery{LoginOrEmail: c.Query("loginOrEmail")} diff --git a/pkg/api/user_test.go b/pkg/api/user_test.go index 7ca57c76fc0..7d2370c2030 100644 --- a/pkg/api/user_test.go +++ b/pkg/api/user_test.go @@ -8,6 +8,8 @@ import ( "time" "github.com/grafana/grafana/pkg/services/searchusers/filters" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/services/searchusers" @@ -20,6 +22,12 @@ import ( ) func TestUserAPIEndpoint_userLoggedIn(t *testing.T) { + settings := setting.NewCfg() + hs := &HTTPServer{Cfg: settings} + + sqlStore := sqlstore.InitTestDB(t) + hs.SQLStore = sqlStore + mockResult := models.SearchUserQueryResult{ Users: []*models.UserSearchHitDTO{ {Name: "user1"}, @@ -53,7 +61,7 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) { return nil }) - sc.handlerFunc = GetUserByID + sc.handlerFunc = hs.GetUserByID avatarUrl := dtos.GetGravatarUrl("daniel@grafana.com") sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() diff --git a/pkg/models/user.go b/pkg/models/user.go index 00937ceccc8..221b27d6cc2 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -226,19 +226,20 @@ func (u *SignedInUser) IsRealUser() bool { } type UserProfileDTO struct { - Id int64 `json:"id"` - Email string `json:"email"` - Name string `json:"name"` - Login string `json:"login"` - Theme string `json:"theme"` - OrgId int64 `json:"orgId"` - IsGrafanaAdmin bool `json:"isGrafanaAdmin"` - IsDisabled bool `json:"isDisabled"` - IsExternal bool `json:"isExternal"` - AuthLabels []string `json:"authLabels"` - UpdatedAt time.Time `json:"updatedAt"` - CreatedAt time.Time `json:"createdAt"` - AvatarUrl string `json:"avatarUrl"` + Id int64 `json:"id"` + Email string `json:"email"` + Name string `json:"name"` + Login string `json:"login"` + Theme string `json:"theme"` + OrgId int64 `json:"orgId"` + IsGrafanaAdmin bool `json:"isGrafanaAdmin"` + IsDisabled bool `json:"isDisabled"` + IsExternal bool `json:"isExternal"` + AuthLabels []string `json:"authLabels"` + UpdatedAt time.Time `json:"updatedAt"` + CreatedAt time.Time `json:"createdAt"` + AvatarUrl string `json:"avatarUrl"` + AccessControl map[string]bool `json:"accessControl,omitempty"` } type UserSearchHitDTO struct { diff --git a/public/app/features/admin/UserProfile.tsx b/public/app/features/admin/UserProfile.tsx index f036d765356..64f37b0e76b 100644 --- a/public/app/features/admin/UserProfile.tsx +++ b/public/app/features/admin/UserProfile.tsx @@ -74,11 +74,12 @@ export function UserProfile({ const lockMessage = authSource ? `Synced via ${authSource}` : ''; const styles = getStyles(config.theme); - const editLocked = user.isExternal || !contextSrv.hasPermission(AccessControlAction.UsersWrite); - const passwordChangeLocked = user.isExternal || !contextSrv.hasPermission(AccessControlAction.UsersPasswordUpdate); - const canDelete = contextSrv.hasPermission(AccessControlAction.UsersDelete); - const canDisable = contextSrv.hasPermission(AccessControlAction.UsersDisable); - const canEnable = contextSrv.hasPermission(AccessControlAction.UsersEnable); + const editLocked = user.isExternal || !contextSrv.hasPermissionInMetadata(AccessControlAction.UsersWrite, user); + const passwordChangeLocked = + user.isExternal || !contextSrv.hasPermissionInMetadata(AccessControlAction.UsersPasswordUpdate, user); + const canDelete = contextSrv.hasPermissionInMetadata(AccessControlAction.UsersDelete, user); + const canDisable = contextSrv.hasPermissionInMetadata(AccessControlAction.UsersDisable, user); + const canEnable = contextSrv.hasPermissionInMetadata(AccessControlAction.UsersEnable, user); return ( <> diff --git a/public/app/features/admin/state/actions.ts b/public/app/features/admin/state/actions.ts index c96cf61fd9d..ff5aaa23d1e 100644 --- a/public/app/features/admin/state/actions.ts +++ b/public/app/features/admin/state/actions.ts @@ -25,7 +25,7 @@ import { } from './reducers'; import { debounce } from 'lodash'; import { contextSrv } from 'app/core/core'; - +import { addAccessControlQueryParam } from 'app/core/utils/accessControl'; // UserAdminPage export function loadAdminUserPage(userId: number): ThunkResult { @@ -54,7 +54,7 @@ export function loadAdminUserPage(userId: number): ThunkResult { export function loadUserProfile(userId: number): ThunkResult { return async (dispatch) => { - const user = await getBackendSrv().get(`/api/users/${userId}`); + const user = await getBackendSrv().get(addAccessControlQueryParam(`/api/users/${userId}`)); dispatch(userProfileLoadedAction(user)); }; } diff --git a/public/app/types/user.ts b/public/app/types/user.ts index 694fe24acd2..2983c53a781 100644 --- a/public/app/types/user.ts +++ b/public/app/types/user.ts @@ -24,7 +24,7 @@ export interface User { export type Unit = { name: string; url: string }; -export interface UserDTO { +export interface UserDTO extends WithAccessControlMetadata { id: number; login: string; email: string;