RBAC: protect folder creation and moving (#64636)

* protect moving folders to a subfolder and creating folders in a subfolder

* folder update endpoint isn't used for folder parent update

* lint

* move permission check logic to services, fix tests

* linting
This commit is contained in:
Ieva
2023-03-20 11:04:22 +00:00
committed by GitHub
parent 7a17a8f02d
commit 7860ca6c3d
4 changed files with 440 additions and 244 deletions

View File

@@ -186,7 +186,7 @@ func (hs *HTTPServer) setDefaultFolderPermissions(ctx context.Context, orgID int
})
}
if !isNested {
if !isNested || !hs.Features.IsEnabled(featuremgmt.FlagNestedFolders) {
permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_EDIT.String()},
{BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_VIEW.String()},
@@ -209,9 +209,11 @@ func (hs *HTTPServer) MoveFolder(c *contextmodel.ReqContext) response.Response {
}
var theFolder *folder.Folder
var err error
if cmd.NewParentUID != "" {
cmd.OrgID = c.OrgID
cmd.UID = web.Params(c.Req)[":uid"]
cmd.SignedInUser = c.SignedInUser
theFolder, err = hs.folderService.Move(c.Req.Context(), &cmd)
if err != nil {
return response.Error(http.StatusInternalServerError, "update folder uid failed", err)
@@ -228,9 +230,6 @@ func (hs *HTTPServer) MoveFolder(c *contextmodel.ReqContext) response.Response {
//
// Update folder.
//
// If nested folders are enabled then it optionally expects a new parent folder UID that moves the folder and
// includes it into the response.
//
// Responses:
// 200: folderResponse
// 400: badRequestError

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -11,142 +12,270 @@ import (
"github.com/stretchr/testify/require"
"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/infra/db/dbtest"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/folder/foldertest"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/search/model"
"github.com/grafana/grafana/pkg/services/team/teamtest"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web/webtest"
)
func TestFoldersAPIEndpoint(t *testing.T) {
func TestFoldersCreateAPIEndpoint(t *testing.T) {
folderService := &foldertest.FakeService{}
setUpRBACGuardian(t)
t.Run("Given a correct request for creating a folder", func(t *testing.T) {
cmd := folder.CreateFolderCommand{
UID: "uid",
Title: "Folder",
}
folderWithoutParentInput := "{ \"uid\": \"uid\", \"title\": \"Folder\"}"
folderService.ExpectedFolder = &folder.Folder{ID: 1, UID: "uid", Title: "Folder"}
type testCase struct {
description string
expectedCode int
expectedFolder *folder.Folder
expectedFolderSvcError error
permissions []accesscontrol.Permission
withNestedFolders bool
input string
}
tcs := []testCase{
{
description: "folder creation succeeds given the correct request for creating a folder",
input: folderWithoutParentInput,
expectedCode: http.StatusOK,
expectedFolder: &folder.Folder{ID: 1, UID: "uid", Title: "Folder"},
permissions: []accesscontrol.Permission{{Action: dashboards.ActionFoldersCreate}},
},
{
description: "folder creation fails without permissions to create a folder",
input: folderWithoutParentInput,
expectedCode: http.StatusForbidden,
permissions: []accesscontrol.Permission{},
},
{
description: "folder creation fails given folder service error %s",
input: folderWithoutParentInput,
expectedCode: http.StatusConflict,
expectedFolderSvcError: dashboards.ErrFolderWithSameUIDExists,
permissions: []accesscontrol.Permission{{Action: dashboards.ActionFoldersCreate}},
},
{
description: "folder creation fails given folder service error %s",
input: folderWithoutParentInput,
expectedCode: http.StatusBadRequest,
expectedFolderSvcError: dashboards.ErrFolderTitleEmpty,
permissions: []accesscontrol.Permission{{Action: dashboards.ActionFoldersCreate}},
},
{
description: "folder creation fails given folder service error %s",
input: folderWithoutParentInput,
expectedCode: http.StatusBadRequest,
expectedFolderSvcError: dashboards.ErrDashboardInvalidUid,
permissions: []accesscontrol.Permission{{Action: dashboards.ActionFoldersCreate}},
},
{
description: "folder creation fails given folder service error %s",
input: folderWithoutParentInput,
expectedCode: http.StatusBadRequest,
expectedFolderSvcError: dashboards.ErrDashboardUidTooLong,
permissions: []accesscontrol.Permission{{Action: dashboards.ActionFoldersCreate}},
},
{
description: "folder creation fails given folder service error %s",
input: folderWithoutParentInput,
expectedCode: http.StatusConflict,
expectedFolderSvcError: dashboards.ErrFolderSameNameExists,
permissions: []accesscontrol.Permission{{Action: dashboards.ActionFoldersCreate}},
},
{
description: "folder creation fails given folder service error %s",
input: folderWithoutParentInput,
expectedCode: http.StatusForbidden,
expectedFolderSvcError: dashboards.ErrFolderAccessDenied,
permissions: []accesscontrol.Permission{{Action: dashboards.ActionFoldersCreate}},
},
{
description: "folder creation fails given folder service error %s",
input: folderWithoutParentInput,
expectedCode: http.StatusNotFound,
expectedFolderSvcError: dashboards.ErrFolderNotFound,
permissions: []accesscontrol.Permission{{Action: dashboards.ActionFoldersCreate}},
},
{
description: "folder creation fails given folder service error %s",
input: folderWithoutParentInput,
expectedCode: http.StatusPreconditionFailed,
expectedFolderSvcError: dashboards.ErrFolderVersionMismatch,
permissions: []accesscontrol.Permission{{Action: dashboards.ActionFoldersCreate}},
},
}
createFolderScenario(t, "When calling POST on", "/api/folders", "/api/folders", folderService, cmd,
func(sc *scenarioContext) {
callCreateFolder(sc)
for _, tc := range tcs {
folderService.ExpectedFolder = tc.expectedFolder
folderService.ExpectedError = tc.expectedFolderSvcError
folderPermService := acmock.NewMockedPermissionsService()
folderPermService.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil)
folder := dtos.Folder{}
err := json.NewDecoder(sc.resp.Body).Decode(&folder)
require.NoError(t, err)
srv := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = &setting.Cfg{
RBACEnabled: true,
}
if tc.withNestedFolders {
hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
}
hs.folderService = folderService
hs.folderPermissionsService = folderPermService
hs.accesscontrolService = actest.FakeService{}
})
t.Run(testDescription(tc.description, tc.expectedFolderSvcError), func(t *testing.T) {
input := strings.NewReader(tc.input)
req := srv.NewPostRequest("/api/folders", input)
req = webtest.RequestWithSignedInUser(req, userWithPermissions(1, tc.permissions))
resp, err := srv.SendJSON(req)
require.NoError(t, err)
require.Equal(t, tc.expectedCode, resp.StatusCode)
folder := dtos.Folder{}
err = json.NewDecoder(resp.Body).Decode(&folder)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
if tc.expectedCode == http.StatusOK {
assert.Equal(t, int64(1), folder.Id)
assert.Equal(t, "uid", folder.Uid)
assert.Equal(t, "Folder", folder.Title)
})
})
t.Run("Given incorrect requests for creating a folder", func(t *testing.T) {
t.Cleanup(func() {
folderService.ExpectedError = nil
}
})
testCases := []struct {
Error error
ExpectedStatusCode int
}{
{Error: dashboards.ErrFolderWithSameUIDExists, ExpectedStatusCode: 409},
{Error: dashboards.ErrFolderTitleEmpty, ExpectedStatusCode: 400},
{Error: dashboards.ErrFolderSameNameExists, ExpectedStatusCode: 409},
{Error: dashboards.ErrDashboardInvalidUid, ExpectedStatusCode: 400},
{Error: dashboards.ErrDashboardUidTooLong, ExpectedStatusCode: 400},
{Error: dashboards.ErrFolderAccessDenied, ExpectedStatusCode: 403},
{Error: dashboards.ErrFolderNotFound, ExpectedStatusCode: 404},
{Error: dashboards.ErrFolderVersionMismatch, ExpectedStatusCode: 412},
}
}
}
cmd := folder.CreateFolderCommand{
UID: "uid",
Title: "Folder",
}
func TestFoldersUpdateAPIEndpoint(t *testing.T) {
folderService := &foldertest.FakeService{}
setUpRBACGuardian(t)
for _, tc := range testCases {
folderService.ExpectedError = tc.Error
type testCase struct {
description string
expectedCode int
expectedFolder *folder.Folder
expectedFolderSvcError error
permissions []accesscontrol.Permission
}
tcs := []testCase{
{
description: "folder updating succeeds given the correct request and permissions to update a folder",
expectedCode: http.StatusOK,
expectedFolder: &folder.Folder{ID: 1, UID: "uid", Title: "Folder upd"},
permissions: []accesscontrol.Permission{{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersAll}},
},
{
description: "folder updating fails without permissions to update a folder",
expectedCode: http.StatusForbidden,
permissions: []accesscontrol.Permission{},
},
{
description: "folder updating fails given folder service error %s",
expectedCode: http.StatusConflict,
expectedFolderSvcError: dashboards.ErrFolderWithSameUIDExists,
permissions: []accesscontrol.Permission{{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersAll}},
},
{
description: "folder updating fails given folder service error %s",
expectedCode: http.StatusBadRequest,
expectedFolderSvcError: dashboards.ErrFolderTitleEmpty,
permissions: []accesscontrol.Permission{{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersAll}},
},
{
description: "folder updating fails given folder service error %s",
expectedCode: http.StatusBadRequest,
expectedFolderSvcError: dashboards.ErrDashboardInvalidUid,
permissions: []accesscontrol.Permission{{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersAll}},
},
{
description: "folder updating fails given folder service error %s",
expectedCode: http.StatusBadRequest,
expectedFolderSvcError: dashboards.ErrDashboardUidTooLong,
permissions: []accesscontrol.Permission{{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersAll}},
},
{
description: "folder updating fails given folder service error %s",
expectedCode: http.StatusConflict,
expectedFolderSvcError: dashboards.ErrFolderSameNameExists,
permissions: []accesscontrol.Permission{{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersAll}},
},
{
description: "folder updating fails given folder service error %s",
expectedCode: http.StatusForbidden,
expectedFolderSvcError: dashboards.ErrFolderAccessDenied,
permissions: []accesscontrol.Permission{{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersAll}},
},
{
description: "folder updating fails given folder service error %s",
expectedCode: http.StatusNotFound,
expectedFolderSvcError: dashboards.ErrFolderNotFound,
permissions: []accesscontrol.Permission{{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersAll}},
},
{
description: "folder updating fails given folder service error %s",
expectedCode: http.StatusPreconditionFailed,
expectedFolderSvcError: dashboards.ErrFolderVersionMismatch,
permissions: []accesscontrol.Permission{{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersAll}},
},
}
createFolderScenario(t, fmt.Sprintf("Expect '%s' error when calling POST on", tc.Error.Error()),
"/api/folders", "/api/folders", folderService, cmd, func(sc *scenarioContext) {
callCreateFolder(sc)
assert.Equalf(t, tc.ExpectedStatusCode, sc.resp.Code, "Wrong status code for error %s", tc.Error)
})
}
})
for _, tc := range tcs {
folderService.ExpectedFolder = tc.expectedFolder
folderService.ExpectedError = tc.expectedFolderSvcError
t.Run("Given a correct request for updating a folder", func(t *testing.T) {
title := "Folder upd"
cmd := folder.UpdateFolderCommand{
NewTitle: &title,
}
srv := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = &setting.Cfg{
RBACEnabled: true,
}
hs.folderService = folderService
})
folderService.ExpectedFolder = &folder.Folder{ID: 1, UID: "uid", Title: "Folder upd"}
t.Run(testDescription(tc.description, tc.expectedFolderSvcError), func(t *testing.T) {
input := strings.NewReader("{ \"uid\": \"uid\", \"title\": \"Folder upd\" }")
req := srv.NewRequest(http.MethodPut, "/api/folders/uid", input)
req = webtest.RequestWithSignedInUser(req, userWithPermissions(1, tc.permissions))
resp, err := srv.SendJSON(req)
require.NoError(t, err)
require.Equal(t, tc.expectedCode, resp.StatusCode)
updateFolderScenario(t, "When calling PUT on", "/api/folders/uid", "/api/folders/:uid", folderService, cmd,
func(sc *scenarioContext) {
callUpdateFolder(sc)
folder := dtos.Folder{}
err = json.NewDecoder(resp.Body).Decode(&folder)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
folder := dtos.Folder{}
err := json.NewDecoder(sc.resp.Body).Decode(&folder)
require.NoError(t, err)
if tc.expectedCode == http.StatusOK {
assert.Equal(t, int64(1), folder.Id)
assert.Equal(t, "uid", folder.Uid)
assert.Equal(t, "Folder upd", folder.Title)
})
})
}
})
}
}
t.Run("Given incorrect requests for updating a folder", func(t *testing.T) {
testCases := []struct {
Error error
ExpectedStatusCode int
}{
{Error: dashboards.ErrFolderWithSameUIDExists, ExpectedStatusCode: 409},
{Error: dashboards.ErrFolderTitleEmpty, ExpectedStatusCode: 400},
{Error: dashboards.ErrFolderSameNameExists, ExpectedStatusCode: 409},
{Error: dashboards.ErrDashboardInvalidUid, ExpectedStatusCode: 400},
{Error: dashboards.ErrDashboardUidTooLong, ExpectedStatusCode: 400},
{Error: dashboards.ErrFolderAccessDenied, ExpectedStatusCode: 403},
{Error: dashboards.ErrFolderNotFound, ExpectedStatusCode: 404},
{Error: dashboards.ErrFolderVersionMismatch, ExpectedStatusCode: 412},
}
title := "Folder upd"
cmd := folder.UpdateFolderCommand{
NewTitle: &title,
}
for _, tc := range testCases {
folderService.ExpectedError = tc.Error
updateFolderScenario(t, fmt.Sprintf("Expect '%s' error when calling PUT on", tc.Error.Error()),
"/api/folders/uid", "/api/folders/:uid", folderService, cmd, func(sc *scenarioContext) {
callUpdateFolder(sc)
assert.Equalf(t, tc.ExpectedStatusCode, sc.resp.Code, "Wrong status code for %s", tc.Error)
})
}
})
func testDescription(description string, expectedErr error) string {
if expectedErr != nil {
return fmt.Sprintf(description, expectedErr.Error())
} else {
return description
}
}
func TestHTTPServer_FolderMetadata(t *testing.T) {
setUpRBACGuardian(t)
folderService := &foldertest.FakeService{}
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = &setting.Cfg{
RBACEnabled: true,
}
hs.folderService = folderService
hs.AccessControl = acmock.New()
hs.QuotaService = quotatest.New(false, nil)
hs.SearchService = &mockSearchService{
ExpectedResult: model.HitList{},
@@ -229,76 +358,53 @@ func TestHTTPServer_FolderMetadata(t *testing.T) {
})
}
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 folder.Service,
cmd folder.CreateFolderCommand, fn scenarioFunc) {
func TestFolderMoveAPIEndpoint(t *testing.T) {
folderService := &foldertest.FakeService{}
setUpRBACGuardian(t)
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
aclMockResp := []*dashboards.DashboardACLInfoDTO{}
teamSvc := &teamtest.FakeService{}
dashSvc := &dashboards.FakeDashboardService{}
qResult1 := aclMockResp
dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult1, nil)
qResult := &dashboards.Dashboard{}
dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(qResult, nil)
store := dbtest.NewFakeDB()
guardian.InitLegacyGuardian(setting.NewCfg(), store, dashSvc, teamSvc)
folderPermissions := acmock.NewMockedPermissionsService()
folderPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil)
hs := HTTPServer{
AccessControl: acmock.New(),
folderService: folderService,
Cfg: setting.NewCfg(),
Features: featuremgmt.WithFeatures(),
accesscontrolService: actest.FakeService{},
folderPermissionsService: folderPermissions,
}
sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd)
c.Req.Header.Add("Content-Type", "application/json")
sc.context = c
sc.context.SignedInUser = &user.SignedInUser{OrgID: testOrgID, UserID: testUserID}
type testCase struct {
description string
expectedCode int
permissions []accesscontrol.Permission
newParentUid string
}
tcs := []testCase{
{
description: "can move folder to another folder with specific permissions",
newParentUid: "newParentUid",
expectedCode: http.StatusOK,
permissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("uid")},
{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("newParentUid")},
},
},
{
description: "forbidden to move folder to another folder without the write access on the folder being moved",
newParentUid: "newParentUid",
expectedCode: http.StatusForbidden,
permissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("newParentUid")},
},
},
}
return hs.CreateFolder(c)
for _, tc := range tcs {
srv := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = &setting.Cfg{
RBACEnabled: true,
}
hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
hs.folderService = folderService
})
sc.m.Post(routePattern, sc.defaultHandler)
fn(sc)
})
}
func callUpdateFolder(sc *scenarioContext) {
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
}
func updateFolderScenario(t *testing.T, desc string, url string, routePattern string, folderService folder.Service,
cmd folder.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,
}
sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd)
c.Req.Header.Add("Content-Type", "application/json")
sc.context = c
sc.context.SignedInUser = &user.SignedInUser{OrgID: testOrgID, UserID: testUserID}
return hs.UpdateFolder(c)
t.Run(tc.description, func(t *testing.T) {
input := strings.NewReader(fmt.Sprintf("{ \"parentUid\": \"%s\"}", tc.newParentUid))
req := srv.NewRequest(http.MethodPost, "/api/folders/uid/move", input)
req = webtest.RequestWithSignedInUser(req, userWithPermissions(1, tc.permissions))
resp, err := srv.SendJSON(req)
require.NoError(t, err)
require.Equal(t, tc.expectedCode, resp.StatusCode)
require.NoError(t, resp.Body.Close())
})
sc.m.Put(routePattern, sc.defaultHandler)
fn(sc)
})
}
}