mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
RBAC: Add access control metadata to folder dtos (#51158)
* RBAC: Add access control metadata to Folder dto * Add access control metadata to folder dto response * Add test to verify that access control metadata is attached * Attach access control metadata to multiple folders * Add access control metadata to frontend folder dto
This commit is contained in:
parent
8fa951df04
commit
2698e37291
@ -1,26 +1,32 @@
|
||||
package dtos
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
)
|
||||
|
||||
type Folder struct {
|
||||
Id int64 `json:"id"`
|
||||
Uid string `json:"uid"`
|
||||
Title string `json:"title"`
|
||||
Url string `json:"url"`
|
||||
HasAcl bool `json:"hasAcl"`
|
||||
CanSave bool `json:"canSave"`
|
||||
CanEdit bool `json:"canEdit"`
|
||||
CanAdmin bool `json:"canAdmin"`
|
||||
CanDelete bool `json:"canDelete"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
Created time.Time `json:"created"`
|
||||
UpdatedBy string `json:"updatedBy"`
|
||||
Updated time.Time `json:"updated"`
|
||||
Version int `json:"version"`
|
||||
Id int64 `json:"id"`
|
||||
Uid string `json:"uid"`
|
||||
Title string `json:"title"`
|
||||
Url string `json:"url"`
|
||||
HasAcl bool `json:"hasAcl"`
|
||||
CanSave bool `json:"canSave"`
|
||||
CanEdit bool `json:"canEdit"`
|
||||
CanAdmin bool `json:"canAdmin"`
|
||||
CanDelete bool `json:"canDelete"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
Created time.Time `json:"created"`
|
||||
UpdatedBy string `json:"updatedBy"`
|
||||
Updated time.Time `json:"updated"`
|
||||
Version int `json:"version"`
|
||||
AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"`
|
||||
}
|
||||
|
||||
type FolderSearchHit struct {
|
||||
Id int64 `json:"id"`
|
||||
Uid string `json:"uid"`
|
||||
Title string `json:"title"`
|
||||
Id int64 `json:"id"`
|
||||
Uid string `json:"uid"`
|
||||
Title string `json:"title"`
|
||||
AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"`
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@ -11,6 +10,7 @@ import (
|
||||
"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/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/services/libraryelements"
|
||||
"github.com/grafana/grafana/pkg/services/store"
|
||||
@ -25,9 +25,10 @@ func (hs *HTTPServer) GetFolders(c *models.ReqContext) response.Response {
|
||||
return apierrors.ToFolderErrorResponse(err)
|
||||
}
|
||||
|
||||
uids := make(map[string]bool, len(folders))
|
||||
result := make([]dtos.FolderSearchHit, 0)
|
||||
|
||||
for _, f := range folders {
|
||||
uids[f.Uid] = true
|
||||
result = append(result, dtos.FolderSearchHit{
|
||||
Id: f.Id,
|
||||
Uid: f.Uid,
|
||||
@ -35,6 +36,13 @@ func (hs *HTTPServer) GetFolders(c *models.ReqContext) response.Response {
|
||||
})
|
||||
}
|
||||
|
||||
metadata := hs.getMultiAccessControlMetadata(c, c.OrgId, dashboards.ScopeFoldersPrefix, uids)
|
||||
if len(metadata) > 0 {
|
||||
for i := range result {
|
||||
result[i].AccessControl = metadata[result[i].Uid]
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
@ -45,7 +53,7 @@ func (hs *HTTPServer) GetFolderByUID(c *models.ReqContext) response.Response {
|
||||
}
|
||||
|
||||
g := guardian.New(c.Req.Context(), folder.Id, c.OrgId, c.SignedInUser)
|
||||
return response.JSON(http.StatusOK, hs.toFolderDto(c.Req.Context(), g, folder))
|
||||
return response.JSON(http.StatusOK, hs.toFolderDto(c, g, folder))
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) GetFolderByID(c *models.ReqContext) response.Response {
|
||||
@ -59,7 +67,7 @@ func (hs *HTTPServer) GetFolderByID(c *models.ReqContext) response.Response {
|
||||
}
|
||||
|
||||
g := guardian.New(c.Req.Context(), folder.Id, c.OrgId, c.SignedInUser)
|
||||
return response.JSON(http.StatusOK, hs.toFolderDto(c.Req.Context(), g, folder))
|
||||
return response.JSON(http.StatusOK, hs.toFolderDto(c, g, folder))
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) CreateFolder(c *models.ReqContext) response.Response {
|
||||
@ -81,7 +89,7 @@ func (hs *HTTPServer) CreateFolder(c *models.ReqContext) response.Response {
|
||||
}
|
||||
|
||||
g := guardian.New(c.Req.Context(), folder.Id, c.OrgId, c.SignedInUser)
|
||||
return response.JSON(http.StatusOK, hs.toFolderDto(c.Req.Context(), g, folder))
|
||||
return response.JSON(http.StatusOK, hs.toFolderDto(c, g, folder))
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) UpdateFolder(c *models.ReqContext) response.Response {
|
||||
@ -103,7 +111,7 @@ func (hs *HTTPServer) UpdateFolder(c *models.ReqContext) response.Response {
|
||||
}
|
||||
|
||||
g := guardian.New(c.Req.Context(), cmd.Result.Id, c.OrgId, c.SignedInUser)
|
||||
return response.JSON(http.StatusOK, hs.toFolderDto(c.Req.Context(), g, cmd.Result))
|
||||
return response.JSON(http.StatusOK, hs.toFolderDto(c, g, cmd.Result))
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) DeleteFolder(c *models.ReqContext) response.Response { // temporarily adding this function to HTTPServer, will be removed from HTTPServer when librarypanels featuretoggle is removed
|
||||
@ -136,7 +144,7 @@ func (hs *HTTPServer) DeleteFolder(c *models.ReqContext) response.Response { //
|
||||
})
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) toFolderDto(ctx context.Context, g guardian.DashboardGuardian, folder *models.Folder) dtos.Folder {
|
||||
func (hs *HTTPServer) toFolderDto(c *models.ReqContext, g guardian.DashboardGuardian, folder *models.Folder) dtos.Folder {
|
||||
canEdit, _ := g.CanEdit()
|
||||
canSave, _ := g.CanSave()
|
||||
canAdmin, _ := g.CanAdmin()
|
||||
@ -145,26 +153,27 @@ func (hs *HTTPServer) toFolderDto(ctx context.Context, g guardian.DashboardGuard
|
||||
// Finding creator and last updater of the folder
|
||||
updater, creator := anonString, anonString
|
||||
if folder.CreatedBy > 0 {
|
||||
creator = hs.getUserLogin(ctx, folder.CreatedBy)
|
||||
creator = hs.getUserLogin(c.Req.Context(), folder.CreatedBy)
|
||||
}
|
||||
if folder.UpdatedBy > 0 {
|
||||
updater = hs.getUserLogin(ctx, folder.UpdatedBy)
|
||||
updater = hs.getUserLogin(c.Req.Context(), folder.UpdatedBy)
|
||||
}
|
||||
|
||||
return dtos.Folder{
|
||||
Id: folder.Id,
|
||||
Uid: folder.Uid,
|
||||
Title: folder.Title,
|
||||
Url: folder.Url,
|
||||
HasAcl: folder.HasAcl,
|
||||
CanSave: canSave,
|
||||
CanEdit: canEdit,
|
||||
CanAdmin: canAdmin,
|
||||
CanDelete: canDelete,
|
||||
CreatedBy: creator,
|
||||
Created: folder.Created,
|
||||
UpdatedBy: updater,
|
||||
Updated: folder.Updated,
|
||||
Version: folder.Version,
|
||||
Id: folder.Id,
|
||||
Uid: folder.Uid,
|
||||
Title: folder.Title,
|
||||
Url: folder.Url,
|
||||
HasAcl: folder.HasAcl,
|
||||
CanSave: canSave,
|
||||
CanEdit: canEdit,
|
||||
CanAdmin: canAdmin,
|
||||
CanDelete: canDelete,
|
||||
CreatedBy: creator,
|
||||
Created: folder.Created,
|
||||
UpdatedBy: updater,
|
||||
Updated: folder.Updated,
|
||||
Version: folder.Version,
|
||||
AccessControl: hs.getAccessControlMetadata(c, c.OrgId, dashboards.ScopeFoldersPrefix, folder.Uid),
|
||||
}
|
||||
}
|
||||
|
@ -4,15 +4,19 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web/webtest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -130,16 +134,106 @@ func TestFoldersAPIEndpoint(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestHTTPServer_FolderMetadata(t *testing.T) {
|
||||
setUpRBACGuardian(t)
|
||||
folderService := dashboards.NewFakeFolderService(t)
|
||||
server := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||
hs.folderService = folderService
|
||||
hs.AccessControl = acmock.New()
|
||||
})
|
||||
|
||||
t.Run("Should attach access control metadata to multiple folders", func(t *testing.T) {
|
||||
folderService.On("GetFolders", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]*models.Folder{
|
||||
{Uid: "1"},
|
||||
{Uid: "2"},
|
||||
{Uid: "3"},
|
||||
}, nil)
|
||||
|
||||
req := server.NewGetRequest("/api/folders?accesscontrol=true")
|
||||
webtest.RequestWithSignedInUser(req, &models.SignedInUser{UserId: 1, OrgId: 1, Permissions: map[int64]map[string][]string{
|
||||
1: accesscontrol.GroupScopesByAction([]accesscontrol.Permission{
|
||||
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersAll},
|
||||
{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("2")},
|
||||
}),
|
||||
}})
|
||||
|
||||
res, err := server.Send(req)
|
||||
require.NoError(t, err)
|
||||
defer func() { require.NoError(t, res.Body.Close()) }()
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
body := []dtos.FolderSearchHit{}
|
||||
require.NoError(t, json.NewDecoder(res.Body).Decode(&body))
|
||||
|
||||
for _, f := range body {
|
||||
assert.True(t, f.AccessControl[dashboards.ActionFoldersRead])
|
||||
if f.Uid == "2" {
|
||||
assert.True(t, f.AccessControl[dashboards.ActionFoldersWrite])
|
||||
} else {
|
||||
assert.False(t, f.AccessControl[dashboards.ActionFoldersWrite])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Should attach access control metadata to folder response", func(t *testing.T) {
|
||||
folderService.On("GetFolderByUID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&models.Folder{Uid: "folderUid"}, nil)
|
||||
|
||||
req := server.NewGetRequest("/api/folders/folderUid?accesscontrol=true")
|
||||
webtest.RequestWithSignedInUser(req, &models.SignedInUser{UserId: 1, OrgId: 1, Permissions: map[int64]map[string][]string{
|
||||
1: accesscontrol.GroupScopesByAction([]accesscontrol.Permission{
|
||||
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersAll},
|
||||
{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("folderUid")},
|
||||
}),
|
||||
}})
|
||||
|
||||
res, err := server.Send(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
defer func() { require.NoError(t, res.Body.Close()) }()
|
||||
|
||||
body := dtos.Folder{}
|
||||
require.NoError(t, json.NewDecoder(res.Body).Decode(&body))
|
||||
|
||||
assert.True(t, body.AccessControl[dashboards.ActionFoldersRead])
|
||||
assert.True(t, body.AccessControl[dashboards.ActionFoldersWrite])
|
||||
})
|
||||
|
||||
t.Run("Should attach access control metadata to folder response", func(t *testing.T) {
|
||||
folderService.On("GetFolderByUID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&models.Folder{Uid: "folderUid"}, nil)
|
||||
|
||||
req := server.NewGetRequest("/api/folders/folderUid")
|
||||
webtest.RequestWithSignedInUser(req, &models.SignedInUser{UserId: 1, OrgId: 1, Permissions: map[int64]map[string][]string{
|
||||
1: accesscontrol.GroupScopesByAction([]accesscontrol.Permission{
|
||||
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersAll},
|
||||
{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("folderUid")},
|
||||
}),
|
||||
}})
|
||||
|
||||
res, err := server.Send(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
defer func() { require.NoError(t, res.Body.Close()) }()
|
||||
|
||||
body := dtos.Folder{}
|
||||
require.NoError(t, json.NewDecoder(res.Body).Decode(&body))
|
||||
|
||||
assert.False(t, body.AccessControl[dashboards.ActionFoldersRead])
|
||||
assert.False(t, body.AccessControl[dashboards.ActionFoldersWrite])
|
||||
})
|
||||
}
|
||||
|
||||
func callCreateFolder(sc *scenarioContext) {
|
||||
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
func createFolderScenario(t *testing.T, desc string, url string, routePattern string, folderService dashboards.FolderService,
|
||||
cmd models.CreateFolderCommand, fn scenarioFunc) {
|
||||
setUpRBACGuardian(t)
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
hs := HTTPServer{
|
||||
Cfg: setting.NewCfg(),
|
||||
AccessControl: acmock.New(),
|
||||
folderService: folderService,
|
||||
Cfg: setting.NewCfg(),
|
||||
Features: featuremgmt.WithFeatures(),
|
||||
}
|
||||
|
||||
@ -165,9 +259,11 @@ func callUpdateFolder(sc *scenarioContext) {
|
||||
|
||||
func updateFolderScenario(t *testing.T, desc string, url string, routePattern string, folderService dashboards.FolderService,
|
||||
cmd models.UpdateFolderCommand, fn scenarioFunc) {
|
||||
setUpRBACGuardian(t)
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
hs := HTTPServer{
|
||||
Cfg: setting.NewCfg(),
|
||||
AccessControl: acmock.New(),
|
||||
folderService: folderService,
|
||||
}
|
||||
|
||||
|
@ -88,10 +88,18 @@ func (m *Mock) Evaluate(ctx context.Context, user *models.SignedInUser, evaluato
|
||||
if m.EvaluateFunc != nil {
|
||||
return m.EvaluateFunc(ctx, user, evaluator)
|
||||
}
|
||||
// Otherwise perform an actual evaluation of the permissions
|
||||
permissions, err := m.GetUserPermissions(ctx, user, accesscontrol.Options{ReloadCache: false})
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
||||
var permissions map[string][]string
|
||||
if user.Permissions != nil && user.Permissions[user.OrgId] != nil {
|
||||
permissions = user.Permissions[user.OrgId]
|
||||
}
|
||||
|
||||
if permissions == nil {
|
||||
userPermissions, err := m.GetUserPermissions(ctx, user, accesscontrol.Options{ReloadCache: true})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
permissions = accesscontrol.GroupScopesByAction(userPermissions)
|
||||
}
|
||||
|
||||
attributeMutator := m.scopeResolvers.GetScopeAttributeMutator(user.OrgId)
|
||||
@ -99,7 +107,7 @@ func (m *Mock) Evaluate(ctx context.Context, user *models.SignedInUser, evaluato
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return resolvedEvaluator.Evaluate(accesscontrol.GroupScopesByAction(permissions)), nil
|
||||
return resolvedEvaluator.Evaluate(permissions), nil
|
||||
}
|
||||
|
||||
// GetUserPermissions returns user permissions.
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { WithAccessControlMetadata } from '@grafana/data';
|
||||
|
||||
import { DashboardAcl } from './acl';
|
||||
|
||||
export interface FolderDTO {
|
||||
export interface FolderDTO extends WithAccessControlMetadata {
|
||||
id: number;
|
||||
uid: string;
|
||||
title: string;
|
||||
|
Loading…
Reference in New Issue
Block a user