mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
AccessControl: Present user edit actions according to AC metadata (#43602)
* AccessControl: Add user metadata to user detail view * AccessControl: Do not present delete or disable buttons based on ac metadata in admin/users * AccessControl: do not allow password changing or user editing without permission * AccessControl: Fetch global:users scope for admin * AccessControl: optimize org.user metadata fetch * Chore: early return if ac metadata is not available
This commit is contained in:
parent
ba58b34219
commit
056e143664
@ -141,7 +141,7 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
r.Group("/api", func(apiRoute routing.RouteRegister) {
|
r.Group("/api", func(apiRoute routing.RouteRegister) {
|
||||||
// user (signed in)
|
// user (signed in)
|
||||||
apiRoute.Group("/user", func(userRoute routing.RouteRegister) {
|
apiRoute.Group("/user", func(userRoute routing.RouteRegister) {
|
||||||
userRoute.Get("/", routing.Wrap(GetSignedInUser))
|
userRoute.Get("/", routing.Wrap(hs.GetSignedInUser))
|
||||||
userRoute.Put("/", routing.Wrap(UpdateSignedInUser))
|
userRoute.Put("/", routing.Wrap(UpdateSignedInUser))
|
||||||
userRoute.Post("/using/:id", routing.Wrap(UserSetUsingOrg))
|
userRoute.Post("/using/:id", routing.Wrap(UserSetUsingOrg))
|
||||||
userRoute.Get("/orgs", routing.Wrap(GetSignedInUserOrgList))
|
userRoute.Get("/orgs", routing.Wrap(GetSignedInUserOrgList))
|
||||||
@ -167,7 +167,7 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
userIDScope := ac.Scope("global", "users", "id", ac.Parameter(":id"))
|
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("/", 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("/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/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))
|
usersRoute.Get("/:id/orgs", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead, userIDScope)), routing.Wrap(GetUserOrgList))
|
||||||
// query parameters /users/lookup?loginOrEmail=admin@example.com
|
// query parameters /users/lookup?loginOrEmail=admin@example.com
|
||||||
|
@ -105,7 +105,7 @@ func (hs *HTTPServer) GetOrgUsersForCurrentOrgLookup(c *models.ReqContext) respo
|
|||||||
return response.JSON(200, result)
|
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") {
|
if hs.AccessControl == nil || hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@ -115,15 +115,7 @@ func (hs *HTTPServer) getUserAccessControlMetadata(c *models.ReqContext, userID
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
key := fmt.Sprintf("%d", userID)
|
return accesscontrol.GetResourcesMetadata(c.Req.Context(), userPermissions, "users", resourceIDs)
|
||||||
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
|
// 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))
|
filteredUsers := make([]*models.OrgUserDTO, 0, len(query.Result))
|
||||||
|
userIDs := map[string]bool{}
|
||||||
for _, user := range query.Result {
|
for _, user := range query.Result {
|
||||||
if dtos.IsHiddenUser(user.Login, signedInUser, hs.Cfg) {
|
if dtos.IsHiddenUser(user.Login, signedInUser, hs.Cfg) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
user.AvatarUrl = dtos.GetGravatarUrl(user.Email)
|
user.AvatarUrl = dtos.GetGravatarUrl(user.Email)
|
||||||
|
|
||||||
accessControlMetadata, errAC := hs.getUserAccessControlMetadata(c, user.UserId)
|
userIDs[fmt.Sprint(user.UserId)] = true
|
||||||
if errAC != nil {
|
|
||||||
hs.log.Error("Failed to get access control metadata", "error", errAC)
|
|
||||||
}
|
|
||||||
|
|
||||||
user.AccessControl = accessControlMetadata
|
|
||||||
|
|
||||||
filteredUsers = append(filteredUsers, user)
|
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
|
return filteredUsers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,31 +3,33 @@ package api
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/api/response"
|
"github.com/grafana/grafana/pkg/api/response"
|
||||||
"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/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GET /api/user (current authenticated user)
|
// GET /api/user (current authenticated user)
|
||||||
func GetSignedInUser(c *models.ReqContext) response.Response {
|
func (hs *HTTPServer) GetSignedInUser(c *models.ReqContext) response.Response {
|
||||||
return getUserUserProfile(c.Req.Context(), c.UserId)
|
return hs.getUserUserProfile(c, c.UserId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/users/:id
|
// GET /api/users/:id
|
||||||
func GetUserByID(c *models.ReqContext) response.Response {
|
func (hs *HTTPServer) GetUserByID(c *models.ReqContext) response.Response {
|
||||||
return getUserUserProfile(c.Req.Context(), c.ParamsInt64(":id"))
|
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}
|
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) {
|
if errors.Is(err, models.ErrUserNotFound) {
|
||||||
return response.Error(404, models.ErrUserNotFound.Error(), nil)
|
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}
|
getAuthQuery := models.GetAuthInfoQuery{UserId: userID}
|
||||||
query.Result.AuthLabels = []string{}
|
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)
|
authLabel := GetAuthProviderLabel(getAuthQuery.Result.AuthModule)
|
||||||
query.Result.AuthLabels = append(query.Result.AuthLabels, authLabel)
|
query.Result.AuthLabels = append(query.Result.AuthLabels, authLabel)
|
||||||
query.Result.IsExternal = true
|
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)
|
query.Result.AvatarUrl = dtos.GetGravatarUrl(query.Result.Email)
|
||||||
|
|
||||||
return response.JSON(200, query.Result)
|
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
|
// GET /api/users/lookup
|
||||||
func GetUserByLoginOrEmail(c *models.ReqContext) response.Response {
|
func GetUserByLoginOrEmail(c *models.ReqContext) response.Response {
|
||||||
query := models.GetUserByLoginQuery{LoginOrEmail: c.Query("loginOrEmail")}
|
query := models.GetUserByLoginQuery{LoginOrEmail: c.Query("loginOrEmail")}
|
||||||
|
@ -8,6 +8,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/searchusers/filters"
|
"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"
|
"github.com/grafana/grafana/pkg/services/searchusers"
|
||||||
|
|
||||||
@ -20,6 +22,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestUserAPIEndpoint_userLoggedIn(t *testing.T) {
|
func TestUserAPIEndpoint_userLoggedIn(t *testing.T) {
|
||||||
|
settings := setting.NewCfg()
|
||||||
|
hs := &HTTPServer{Cfg: settings}
|
||||||
|
|
||||||
|
sqlStore := sqlstore.InitTestDB(t)
|
||||||
|
hs.SQLStore = sqlStore
|
||||||
|
|
||||||
mockResult := models.SearchUserQueryResult{
|
mockResult := models.SearchUserQueryResult{
|
||||||
Users: []*models.UserSearchHitDTO{
|
Users: []*models.UserSearchHitDTO{
|
||||||
{Name: "user1"},
|
{Name: "user1"},
|
||||||
@ -53,7 +61,7 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
sc.handlerFunc = GetUserByID
|
sc.handlerFunc = hs.GetUserByID
|
||||||
avatarUrl := dtos.GetGravatarUrl("daniel@grafana.com")
|
avatarUrl := dtos.GetGravatarUrl("daniel@grafana.com")
|
||||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
|
|
||||||
|
@ -226,19 +226,20 @@ func (u *SignedInUser) IsRealUser() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UserProfileDTO struct {
|
type UserProfileDTO struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Login string `json:"login"`
|
Login string `json:"login"`
|
||||||
Theme string `json:"theme"`
|
Theme string `json:"theme"`
|
||||||
OrgId int64 `json:"orgId"`
|
OrgId int64 `json:"orgId"`
|
||||||
IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
|
IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
|
||||||
IsDisabled bool `json:"isDisabled"`
|
IsDisabled bool `json:"isDisabled"`
|
||||||
IsExternal bool `json:"isExternal"`
|
IsExternal bool `json:"isExternal"`
|
||||||
AuthLabels []string `json:"authLabels"`
|
AuthLabels []string `json:"authLabels"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
AvatarUrl string `json:"avatarUrl"`
|
AvatarUrl string `json:"avatarUrl"`
|
||||||
|
AccessControl map[string]bool `json:"accessControl,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserSearchHitDTO struct {
|
type UserSearchHitDTO struct {
|
||||||
|
@ -74,11 +74,12 @@ export function UserProfile({
|
|||||||
const lockMessage = authSource ? `Synced via ${authSource}` : '';
|
const lockMessage = authSource ? `Synced via ${authSource}` : '';
|
||||||
const styles = getStyles(config.theme);
|
const styles = getStyles(config.theme);
|
||||||
|
|
||||||
const editLocked = user.isExternal || !contextSrv.hasPermission(AccessControlAction.UsersWrite);
|
const editLocked = user.isExternal || !contextSrv.hasPermissionInMetadata(AccessControlAction.UsersWrite, user);
|
||||||
const passwordChangeLocked = user.isExternal || !contextSrv.hasPermission(AccessControlAction.UsersPasswordUpdate);
|
const passwordChangeLocked =
|
||||||
const canDelete = contextSrv.hasPermission(AccessControlAction.UsersDelete);
|
user.isExternal || !contextSrv.hasPermissionInMetadata(AccessControlAction.UsersPasswordUpdate, user);
|
||||||
const canDisable = contextSrv.hasPermission(AccessControlAction.UsersDisable);
|
const canDelete = contextSrv.hasPermissionInMetadata(AccessControlAction.UsersDelete, user);
|
||||||
const canEnable = contextSrv.hasPermission(AccessControlAction.UsersEnable);
|
const canDisable = contextSrv.hasPermissionInMetadata(AccessControlAction.UsersDisable, user);
|
||||||
|
const canEnable = contextSrv.hasPermissionInMetadata(AccessControlAction.UsersEnable, user);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -25,7 +25,7 @@ import {
|
|||||||
} from './reducers';
|
} from './reducers';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
|
import { addAccessControlQueryParam } from 'app/core/utils/accessControl';
|
||||||
// UserAdminPage
|
// UserAdminPage
|
||||||
|
|
||||||
export function loadAdminUserPage(userId: number): ThunkResult<void> {
|
export function loadAdminUserPage(userId: number): ThunkResult<void> {
|
||||||
@ -54,7 +54,7 @@ export function loadAdminUserPage(userId: number): ThunkResult<void> {
|
|||||||
|
|
||||||
export function loadUserProfile(userId: number): ThunkResult<void> {
|
export function loadUserProfile(userId: number): ThunkResult<void> {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
const user = await getBackendSrv().get(`/api/users/${userId}`);
|
const user = await getBackendSrv().get(addAccessControlQueryParam(`/api/users/${userId}`));
|
||||||
dispatch(userProfileLoadedAction(user));
|
dispatch(userProfileLoadedAction(user));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ export interface User {
|
|||||||
|
|
||||||
export type Unit = { name: string; url: string };
|
export type Unit = { name: string; url: string };
|
||||||
|
|
||||||
export interface UserDTO {
|
export interface UserDTO extends WithAccessControlMetadata {
|
||||||
id: number;
|
id: number;
|
||||||
login: string;
|
login: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
Loading…
Reference in New Issue
Block a user