The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/pkg/api/folder_test.go

525 lines
20 KiB

package api
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/dashboards"
Access control: Use access control for dashboard and folder (#44702) * Add actions and scopes * add resource service for dashboard and folder * Add dashboard guardian with fgac permission evaluation * Add CanDelete function to guardian interface * Add CanDelete property to folder and dashboard dto and set values * change to correct function name * Add accesscontrol to folder endpoints * add access control to dashboard endpoints * check access for nav links * Add fixed roles for dashboard and folders * use correct package * add hack to override guardian Constructor if accesscontrol is enabled * Add services * Add function to handle api backward compatability * Add permissionServices to HttpServer * Set permission when new dashboard is created * Add default permission when creating new dashboard * Set default permission when creating folder and dashboard * Add access control filter for dashboard search * Add to accept list * Add accesscontrol to dashboardimport * Disable access control in tests * Add check to see if user is allow to create a dashboard * Use SetPermissions * Use function to set several permissions at once * remove permissions for folder and dashboard on delete * update required permission * set permission for provisioning * Add CanCreate to dashboard guardian and set correct permisisons for provisioning * Dont set admin on folder / dashboard creation * Add dashboard and folder permission migrations * Add tests for CanCreate * Add roles and update descriptions * Solve uid to id for dashboard and folder permissions * Add folder and dashboard actions to permission filter * Handle viewer_can_edit flag * set folder and dashboard permissions services * Add dashboard permissions when importing a new dashboard * Set access control permissions on provisioning * Pass feature flags and only set permissions if access control is enabled * only add default permissions for folders and dashboards without folders * Batch create permissions in migrations * Remove `dashboards:edit` action * Remove unused function from interface * Update pkg/services/guardian/accesscontrol_guardian_test.go Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com> Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>
3 years ago
"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/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web/webtest"
)
func TestFoldersCreateAPIEndpoint(t *testing.T) {
folderService := &foldertest.FakeService{}
setUpRBACGuardian(t)
folderWithoutParentInput := "{ \"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"}, // nolint:staticcheck
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}},
},
}
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)
srv := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
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 {
// nolint:staticcheck
assert.Equal(t, int64(1), folder.Id)
assert.Equal(t, "uid", folder.Uid)
assert.Equal(t, "Folder", folder.Title)
}
})
}
}
func TestFoldersUpdateAPIEndpoint(t *testing.T) {
folderService := &foldertest.FakeService{}
setUpRBACGuardian(t)
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"}, // nolint:staticcheck
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}},
},
}
for _, tc := range tcs {
folderService.ExpectedFolder = tc.expectedFolder
folderService.ExpectedError = tc.expectedFolderSvcError
srv := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
hs.folderService = folderService
})
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)
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 {
// nolint:staticcheck
assert.Equal(t, int64(1), folder.Id)
assert.Equal(t, "uid", folder.Uid)
assert.Equal(t, "Folder upd", folder.Title)
}
})
}
}
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{}
features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
hs.folderService = folderService
hs.QuotaService = quotatest.New(false, nil)
hs.SearchService = &mockSearchService{
ExpectedResult: model.HitList{},
}
hs.Features = features
})
t.Run("Should attach access control metadata to folder response", func(t *testing.T) {
folderService.ExpectedFolder = &folder.Folder{UID: "folderUid"}
req := server.NewGetRequest("/api/folders/folderUid?accesscontrol=true")
webtest.RequestWithSignedInUser(req, &user.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 with permissions cascading from nested folders", func(t *testing.T) {
folderService.ExpectedFolder = &folder.Folder{UID: "folderUid"}
folderService.ExpectedFolders = []*folder.Folder{{UID: "parentUid"}}
features = featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
defer func() {
features = featuremgmt.WithFeatures()
folderService.ExpectedFolders = nil
}()
req := server.NewGetRequest("/api/folders/folderUid?accesscontrol=true")
webtest.RequestWithSignedInUser(req, &user.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("parentUid")},
{Action: dashboards.ActionDashboardsCreate, 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])
assert.True(t, body.AccessControl[dashboards.ActionDashboardsCreate])
})
t.Run("Should not attach access control metadata to folder response", func(t *testing.T) {
folderService.ExpectedFolder = &folder.Folder{UID: "folderUid"}
req := server.NewGetRequest("/api/folders/folderUid")
webtest.RequestWithSignedInUser(req, &user.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 TestFolderMoveAPIEndpoint(t *testing.T) {
folderService := &foldertest.FakeService{
ExpectedFolder: &folder.Folder{},
}
setUpRBACGuardian(t)
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: "can move folder to the root folder with specific permissions",
newParentUid: "",
expectedCode: http.StatusOK,
permissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("uid")},
},
},
{
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")},
},
},
}
for _, tc := range tcs {
srv := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
hs.folderService = folderService
})
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())
})
}
}
func TestFolderGetAPIEndpoint(t *testing.T) {
folderService := &foldertest.FakeService{
ExpectedFolder: &folder.Folder{
ID: 1, // nolint:staticcheck
UID: "uid",
Title: "uid title",
},
ExpectedFolders: []*folder.Folder{
{
UID: "parent",
Title: "parent title",
},
{
UID: "subfolder",
Title: "subfolder title",
},
},
}
type testCase struct {
description string
URL string
features *featuremgmt.FeatureManager
expectedCode int
expectedParentUIDs []string
expectedParentTitles []string
permissions []accesscontrol.Permission
g *guardian.FakeDashboardGuardian
}
tcs := []testCase{
{
description: "get folder by UID should return parent folders if nested folder are enabled",
URL: "/api/folders/uid",
expectedCode: http.StatusOK,
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
expectedParentUIDs: []string{"parent", "subfolder"},
expectedParentTitles: []string{"parent title", "subfolder title"},
permissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("uid")},
},
g: &guardian.FakeDashboardGuardian{CanViewValue: true},
},
{
description: "get folder by UID should return parent folders redacted if nested folder are enabled and user does not have read access to parent folders",
URL: "/api/folders/uid",
expectedCode: http.StatusOK,
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
expectedParentUIDs: []string{REDACTED, REDACTED},
expectedParentTitles: []string{REDACTED, REDACTED},
permissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("uid")},
},
g: &guardian.FakeDashboardGuardian{CanViewValue: false},
},
{
description: "get folder by UID should not return parent folders if nested folder are disabled",
URL: "/api/folders/uid",
expectedCode: http.StatusOK,
features: featuremgmt.WithFeatures(),
expectedParentUIDs: []string{},
expectedParentTitles: []string{},
permissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("uid")},
},
g: &guardian.FakeDashboardGuardian{CanViewValue: true},
},
}
for _, tc := range tcs {
srv := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
hs.Features = tc.features
hs.folderService = folderService
})
t.Run(tc.description, func(t *testing.T) {
origNewGuardian := guardian.New
t.Cleanup(func() {
guardian.New = origNewGuardian
})
guardian.MockDashboardGuardian(tc.g)
req := srv.NewGetRequest(tc.URL)
req = webtest.RequestWithSignedInUser(req, userWithPermissions(1, tc.permissions))
resp, err := srv.Send(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.Equal(t, len(folder.Parents), len(tc.expectedParentUIDs))
require.Equal(t, len(folder.Parents), len(tc.expectedParentTitles))
for i := 0; i < len(tc.expectedParentUIDs); i++ {
assert.Equal(t, tc.expectedParentUIDs[i], folder.Parents[i].Uid)
assert.Equal(t, tc.expectedParentTitles[i], folder.Parents[i].Title)
}
require.NoError(t, resp.Body.Close())
})
}
}