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:
Karl Persson 2022-06-22 10:29:26 +02:00 committed by GitHub
parent 8fa951df04
commit 2698e37291
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 169 additions and 48 deletions

View File

@ -1,26 +1,32 @@
package dtos package dtos
import "time" import (
"time"
"github.com/grafana/grafana/pkg/services/accesscontrol"
)
type Folder struct { type Folder struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Uid string `json:"uid"` Uid string `json:"uid"`
Title string `json:"title"` Title string `json:"title"`
Url string `json:"url"` Url string `json:"url"`
HasAcl bool `json:"hasAcl"` HasAcl bool `json:"hasAcl"`
CanSave bool `json:"canSave"` CanSave bool `json:"canSave"`
CanEdit bool `json:"canEdit"` CanEdit bool `json:"canEdit"`
CanAdmin bool `json:"canAdmin"` CanAdmin bool `json:"canAdmin"`
CanDelete bool `json:"canDelete"` CanDelete bool `json:"canDelete"`
CreatedBy string `json:"createdBy"` CreatedBy string `json:"createdBy"`
Created time.Time `json:"created"` Created time.Time `json:"created"`
UpdatedBy string `json:"updatedBy"` UpdatedBy string `json:"updatedBy"`
Updated time.Time `json:"updated"` Updated time.Time `json:"updated"`
Version int `json:"version"` Version int `json:"version"`
AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"`
} }
type FolderSearchHit struct { type FolderSearchHit struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Uid string `json:"uid"` Uid string `json:"uid"`
Title string `json:"title"` Title string `json:"title"`
AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"`
} }

View File

@ -1,7 +1,6 @@
package api package api
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
@ -11,6 +10,7 @@ import (
"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/models" "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/guardian"
"github.com/grafana/grafana/pkg/services/libraryelements" "github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/services/store" "github.com/grafana/grafana/pkg/services/store"
@ -25,9 +25,10 @@ func (hs *HTTPServer) GetFolders(c *models.ReqContext) response.Response {
return apierrors.ToFolderErrorResponse(err) return apierrors.ToFolderErrorResponse(err)
} }
uids := make(map[string]bool, len(folders))
result := make([]dtos.FolderSearchHit, 0) result := make([]dtos.FolderSearchHit, 0)
for _, f := range folders { for _, f := range folders {
uids[f.Uid] = true
result = append(result, dtos.FolderSearchHit{ result = append(result, dtos.FolderSearchHit{
Id: f.Id, Id: f.Id,
Uid: f.Uid, 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) 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) 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 { 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) 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 { 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) 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 { 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) 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 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() canEdit, _ := g.CanEdit()
canSave, _ := g.CanSave() canSave, _ := g.CanSave()
canAdmin, _ := g.CanAdmin() 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 // Finding creator and last updater of the folder
updater, creator := anonString, anonString updater, creator := anonString, anonString
if folder.CreatedBy > 0 { if folder.CreatedBy > 0 {
creator = hs.getUserLogin(ctx, folder.CreatedBy) creator = hs.getUserLogin(c.Req.Context(), folder.CreatedBy)
} }
if folder.UpdatedBy > 0 { if folder.UpdatedBy > 0 {
updater = hs.getUserLogin(ctx, folder.UpdatedBy) updater = hs.getUserLogin(c.Req.Context(), folder.UpdatedBy)
} }
return dtos.Folder{ return dtos.Folder{
Id: folder.Id, Id: folder.Id,
Uid: folder.Uid, Uid: folder.Uid,
Title: folder.Title, Title: folder.Title,
Url: folder.Url, Url: folder.Url,
HasAcl: folder.HasAcl, HasAcl: folder.HasAcl,
CanSave: canSave, CanSave: canSave,
CanEdit: canEdit, CanEdit: canEdit,
CanAdmin: canAdmin, CanAdmin: canAdmin,
CanDelete: canDelete, CanDelete: canDelete,
CreatedBy: creator, CreatedBy: creator,
Created: folder.Created, Created: folder.Created,
UpdatedBy: updater, UpdatedBy: updater,
Updated: folder.Updated, Updated: folder.Updated,
Version: folder.Version, Version: folder.Version,
AccessControl: hs.getAccessControlMetadata(c, c.OrgId, dashboards.ScopeFoldersPrefix, folder.Uid),
} }
} }

View File

@ -4,15 +4,19 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http"
"testing" "testing"
"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/api/routing" "github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/models" "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/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web/webtest"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require" "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) { func callCreateFolder(sc *scenarioContext) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
} }
func createFolderScenario(t *testing.T, desc string, url string, routePattern string, folderService dashboards.FolderService, func createFolderScenario(t *testing.T, desc string, url string, routePattern string, folderService dashboards.FolderService,
cmd models.CreateFolderCommand, fn scenarioFunc) { cmd models.CreateFolderCommand, fn scenarioFunc) {
setUpRBACGuardian(t)
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
hs := HTTPServer{ hs := HTTPServer{
Cfg: setting.NewCfg(), AccessControl: acmock.New(),
folderService: folderService, folderService: folderService,
Cfg: setting.NewCfg(),
Features: featuremgmt.WithFeatures(), 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, func updateFolderScenario(t *testing.T, desc string, url string, routePattern string, folderService dashboards.FolderService,
cmd models.UpdateFolderCommand, fn scenarioFunc) { cmd models.UpdateFolderCommand, fn scenarioFunc) {
setUpRBACGuardian(t)
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
hs := HTTPServer{ hs := HTTPServer{
Cfg: setting.NewCfg(), Cfg: setting.NewCfg(),
AccessControl: acmock.New(),
folderService: folderService, folderService: folderService,
} }

View File

@ -88,10 +88,18 @@ func (m *Mock) Evaluate(ctx context.Context, user *models.SignedInUser, evaluato
if m.EvaluateFunc != nil { if m.EvaluateFunc != nil {
return m.EvaluateFunc(ctx, user, evaluator) return m.EvaluateFunc(ctx, user, evaluator)
} }
// Otherwise perform an actual evaluation of the permissions
permissions, err := m.GetUserPermissions(ctx, user, accesscontrol.Options{ReloadCache: false}) var permissions map[string][]string
if err != nil { if user.Permissions != nil && user.Permissions[user.OrgId] != nil {
return false, err 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) attributeMutator := m.scopeResolvers.GetScopeAttributeMutator(user.OrgId)
@ -99,7 +107,7 @@ func (m *Mock) Evaluate(ctx context.Context, user *models.SignedInUser, evaluato
if err != nil { if err != nil {
return false, err return false, err
} }
return resolvedEvaluator.Evaluate(accesscontrol.GroupScopesByAction(permissions)), nil return resolvedEvaluator.Evaluate(permissions), nil
} }
// GetUserPermissions returns user permissions. // GetUserPermissions returns user permissions.

View File

@ -1,6 +1,8 @@
import { WithAccessControlMetadata } from '@grafana/data';
import { DashboardAcl } from './acl'; import { DashboardAcl } from './acl';
export interface FolderDTO { export interface FolderDTO extends WithAccessControlMetadata {
id: number; id: number;
uid: string; uid: string;
title: string; title: string;