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
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"`
}

View File

@ -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),
}
}

View File

@ -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,
}

View File

@ -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.

View File

@ -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;